From 3ab71444f544b684b479a03cf2aea513a2d49e58 Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Thu, 7 Mar 2024 09:29:22 -0500 Subject: [PATCH] top-secret --- elixir/top-secret/.exercism/config.json | 19 ++ elixir/top-secret/.exercism/metadata.json | 1 + elixir/top-secret/.formatter.exs | 4 + elixir/top-secret/.gitignore | 24 ++ elixir/top-secret/HELP.md | 75 +++++ elixir/top-secret/HINTS.md | 51 ++++ elixir/top-secret/README.md | 137 ++++++++++ elixir/top-secret/lib/top_secret.ex | 35 +++ elixir/top-secret/mix.exs | 28 ++ elixir/top-secret/test/test_helper.exs | 2 + elixir/top-secret/test/top_secret_test.exs | 301 +++++++++++++++++++++ 11 files changed, 677 insertions(+) create mode 100644 elixir/top-secret/.exercism/config.json create mode 100644 elixir/top-secret/.exercism/metadata.json create mode 100644 elixir/top-secret/.formatter.exs create mode 100644 elixir/top-secret/.gitignore create mode 100644 elixir/top-secret/HELP.md create mode 100644 elixir/top-secret/HINTS.md create mode 100644 elixir/top-secret/README.md create mode 100644 elixir/top-secret/lib/top_secret.ex create mode 100644 elixir/top-secret/mix.exs create mode 100644 elixir/top-secret/test/test_helper.exs create mode 100644 elixir/top-secret/test/top_secret_test.exs diff --git a/elixir/top-secret/.exercism/config.json b/elixir/top-secret/.exercism/config.json new file mode 100644 index 0000000..fbefcdb --- /dev/null +++ b/elixir/top-secret/.exercism/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "jiegillet", + "angelikatyborska" + ], + "files": { + "solution": [ + "lib/top_secret.ex" + ], + "test": [ + "test/top_secret_test.exs" + ], + "exemplar": [ + ".meta/exemplar.ex" + ] + }, + "language_versions": ">=1.10", + "blurb": "Learn about the Abstract Syntax Tree (AST) by helping decode secret messages from Agent Ex." +} diff --git a/elixir/top-secret/.exercism/metadata.json b/elixir/top-secret/.exercism/metadata.json new file mode 100644 index 0000000..5c38833 --- /dev/null +++ b/elixir/top-secret/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"elixir","exercise":"top-secret","id":"6ab6565f409840a8977c99288b6a10b4","url":"https://exercism.org/tracks/elixir/exercises/top-secret","handle":"negrienko","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/elixir/top-secret/.formatter.exs b/elixir/top-secret/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/top-secret/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/top-secret/.gitignore b/elixir/top-secret/.gitignore new file mode 100644 index 0000000..a0f694e --- /dev/null +++ b/elixir/top-secret/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +maps-*.tar + diff --git a/elixir/top-secret/HELP.md b/elixir/top-secret/HELP.md new file mode 100644 index 0000000..2ecc46d --- /dev/null +++ b/elixir/top-secret/HELP.md @@ -0,0 +1,75 @@ +# Help + +## Running the tests + +From the terminal, change to the base directory of the exercise then execute the tests with: + +```bash +$ mix test +``` + +This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs` + +Documentation: + +* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html) +* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html) + +## Pending tests + +In test suites of practice exercises, all but the first test have been tagged to be skipped. + +Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol. + +For example: + +```elixir +# @tag :pending +test "shouting" do + assert Bob.hey("WATCH OUT!") == "Whoa, chill out!" +end +``` + +If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command: + +```bash +$ mix test --include pending +``` + +Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`. + +```elixir +# ExUnit.configure(exclude: :pending, trace: true) +``` + +## Useful `mix test` options + +* `test/.exs:LINENUM` - runs only a single test, the test from `.exs` whose definition is on line `LINENUM` +* `--failed` - runs only tests that failed the last time they ran +* `--max-failures` - the suite stops evaluating tests when this number of test failures +is reached +* `--seed 0` - disables randomization so the tests in a single file will always be ran +in the same order they were defined in + +## Submitting your solution + +You can submit your solution using the `exercism submit lib/top_secret.ex` command. +This command will upload your solution to the Exercism website and print the solution page's URL. + +It's possible to submit an incomplete solution which allows you to: + +- See how others have completed the exercise +- Request help from a mentor + +## Need to get help? + +If you'd like help solving the exercise, check the following pages: + +- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir) +- The [Elixir track's programming category on the forum](https://forum.exercism.org/c/programming/elixir) +- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5) +- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs) + +Should those resources not suffice, you could submit your (incomplete) solution to request mentoring. + +If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found. \ No newline at end of file diff --git a/elixir/top-secret/HINTS.md b/elixir/top-secret/HINTS.md new file mode 100644 index 0000000..9920832 --- /dev/null +++ b/elixir/top-secret/HINTS.md @@ -0,0 +1,51 @@ +# Hints + +## General + +- Read about quoting in the [official Getting Started guide][getting-started-quote]. +- Read the [introduction to Elixir AST by Lucas San Román][ast-intro-lucas]. +- Read [the official documentation for `quote`][doc-quote]. +- Inspect the output of [`quote`][doc-quote] to familiarize yourself with how ASTs look like for specific code snippets. + +## 1. Turn code into data + +- There is a [built-in function][doc-code-string-to-quoted] that turns a string with code into an AST. + +## 2. Parse a single AST node + +- Inspect the output of [`quote`][doc-quote] to familiarize yourself with how ASTs look like for specific code snippets. +- The operations that define a function are `:def` and `:defp`. +- The operation is the first element in a three-element AST node tuple. +- You can ignore the second element in the tuple in this exercise completely. +- The third element in the tuple is the argument list of the operation that defines the function. +- The first element on that list is a tuple with the function's name and arguments, and the second element is the function's body. + +## 3. Decode the secret message part from function definition + +- Inspect the output of [`quote`][doc-quote] to familiarize yourself with how ASTs look like for specific code snippets. +- The AST node that contains the function's name also contains the function's argument list as the third element. +- The arity of a function is the length of its argument list. +- There is a [built-in function in the `String` module][string-slice] that can get the first `n` characters from a string. +- A function without arguments written without parentheses will not have a list as argument but an atom. + +## 4. Fix the decoding for functions with guards + +- Inspect the output of [`quote`][doc-quote] to familiarize yourself with how ASTs look like for specific code snippets. +- When a function has a guard, the third element in the tuple for the `:def/:defp` operation is a bit different. +- That third element is a list with two elements, the first one is the tuple for the `:when` operation, and the second one is the function's body. +- The `:when` operation's arguments are a two-element list, where the first argument is the function's name, and the second is the guard expression. + +## 5. Decode the full secret message + +- Use the function `to_ast/1` that you implemented in the first task to create the AST. +- There is a [built-in function][macro-prewalk] that can visit each node in an AST with an accumulator. +- Use the function `decode_secret_message_part/2` that you implemented in previous tasks to prewalk the AST. +- To reverse the accumulator at the end and turn it into a string, refresh your knowledge of the [`Enum` module][enum]. + +[getting-started-quote]: https://hexdocs.pm/elixir/quote-and-unquote.html +[doc-quote]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#quote/2 +[ast-intro-lucas]: https://dorgan.ar/posts/2021/04/the_elixir_ast/ +[doc-code-string-to-quoted]: https://hexdocs.pm/elixir/Code.html#string_to_quoted/2 +[string-slice]: https://hexdocs.pm/elixir/String.html#slice/2 +[macro-prewalk]: https://hexdocs.pm/elixir/Macro.html#prewalk/3 +[enum]: https://hexdocs.pm/elixir/Enum.html \ No newline at end of file diff --git a/elixir/top-secret/README.md b/elixir/top-secret/README.md new file mode 100644 index 0000000..8e50bce --- /dev/null +++ b/elixir/top-secret/README.md @@ -0,0 +1,137 @@ +# Top Secret + +Welcome to Top Secret on Exercism's Elixir Track. +If you need help running the tests or submitting your code, check out `HELP.md`. +If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :) + +## Introduction + +## AST + +The Abstract Syntax Tree (AST), also called a _quoted expression_, is a way to represent code as data. + +Each node in the AST is a three-element tuple. + +```elixir +# AST representation of: +# 2 + 3 +{:+, [], [2, 3]} +``` + +The first element, an atom, is the operation. The second element, a keyword list, is the metadata. The third element is a list of arguments, which contains other nodes. Literal values such as integers, atoms, and strings are represented in the AST as themselves instead of three-element tuples. + +### Turning code into ASTs + +Changing Elixir code to ASTs and ASTs back to code is part of the standard library. You can find functions for working with ASTs in the modules `Code` (e.g. to change a string with code to an AST) and `Macro` (e.g. to traverse the AST or change it to a string). + +Note that all of the functions in the standard library use the name "quoted" to mean the AST (short for _quoted expression_). + +The special form for turning code into an AST is called `quote`. It accepts a code block and returns its AST. + +```elixir +quote do + 2 + 3 - 1 +end + +# => {:-, [], [ +# {:+, [], [2, 3]}, +# 1 +# ]} +``` + +### Use cases + +The ability to represent code as an AST is at the heart of metaprogramming in Elixir. _Macros_, which is a way to write Elixir code that produces Elixir code, work by returning ASTs as output. + +Another use case for ASTs is static code analysis, like Exercism's own tool, the Analyzer, which you might already know as the little bot that leaves comments on your solutions. + +## Instructions + +You're part of a task force fighting against corporate espionage. You have a secret informer at Shady Company X, which you suspect of stealing secrets from its competitors. + +Your informer, Agent Ex, is an Elixir developer. She is encoding secret messages in her code. + +To decode her secret messages: + +- Take all functions (public and private) in the order they're defined in. +- For each function, take the first `n` characters from its name, where `n` is the function's arity. + +## 1. Turn code into data + +Implement the `TopSecret.to_ast/1` function. It should take a string with Elixir code and return its AST. + +```elixir +TopSecret.to_ast("div(4, 3)") +# => {:div, [line: 1], [4, 3]} +``` + +## 2. Parse a single AST node + +Implement the `TopSecret.decode_secret_message_part/2` function. It should take an AST node and an accumulator for the secret message (a list). It should return a tuple with the AST node unchanged as the first element, and the accumulator as the second element. + +If the operation of the AST node is defining a function (`def` or `defp`), prepend the function name (changed to a string) to the accumulator. If the operation is something else, return the accumulator unchanged. + +```elixir +ast_node = TopSecret.to_ast("defp cat(a, b, c), do: nil") +TopSecret.decode_secret_message_part(ast_node, ["day"]) +# => {ast_node, ["cat", "day"]} + +ast_node = TopSecret.to_ast("10 + 3") +TopSecret.decode_secret_message_part(ast_node, ["day"]) +# => {ast_node, ["day"]} +``` + +This function doesn't need to do any recursive calls to check the whole AST, only the given node. We will traverse the whole AST with built-in tools in the last step. + +## 3. Decode the secret message part from function definition + +Extend the `TopSecret.decode_secret_message_part/2` function. If the operation in the AST node is defining a function, don't return the whole function name. Instead, check the function's arity. Then, return only first `n` character from the name, where `n` is the arity. + +```elixir +ast_node = TopSecret.to_ast("defp cat(a, b), do: nil") +TopSecret.decode_secret_message_part(ast_node, ["day"]) +# => {ast_node, ["ca", "day"]} + +ast_node = TopSecret.to_ast("defp cat(), do: nil") +TopSecret.decode_secret_message_part(ast_node, ["day"]) +# => {ast_node, ["", "day"]} +``` + +## 4. Fix the decoding for functions with guards + +Extend the `TopSecret.decode_secret_message_part/2` function. Make sure the function's name and arity is correctly detected for function definitions that use guards. + +```elixir +ast_node = TopSecret.to_ast("defp cat(a, b) when is_nil(a), do: nil") +TopSecret.decode_secret_message_part(ast_node, ["day"]) +# => {ast_node, ["ca", "day"]} +``` + +## 5. Decode the full secret message + +Implement the `TopSecret.decode_secret_message/1` function. It should take a string with Elixir code and return the secret message as a string decoded from all function definitions found in the code. Make sure to reuse functions defined in previous steps. + +```elixir +code = """ +defmodule MyCalendar do + def busy?(date, time) do + Date.day_of_week(date) != 7 and + time.hour in 10..16 + end + + def yesterday?(date) do + Date.diff(Date.utc_today, date) + end +end +""" + +TopSecret.decode_secret_message(code) +# => "buy" +``` + +## Source + +### Created by + +- @jiegillet +- @angelikatyborska \ No newline at end of file diff --git a/elixir/top-secret/lib/top_secret.ex b/elixir/top-secret/lib/top_secret.ex new file mode 100644 index 0000000..809b75b --- /dev/null +++ b/elixir/top-secret/lib/top_secret.ex @@ -0,0 +1,35 @@ +defmodule TopSecret do + def to_ast(string), do: Code.string_to_quoted!(string) + + def decode_secret_message_part({is_function?, _, arguments} = ast, acc) + when is_function? in [:defp, :def] do + {ast, [get_letters(arguments) | acc]} + end + + def decode_secret_message_part(ast, acc), do: {ast, acc} + + defp get_letters([{:when, _, arguments} | _]), do: get_letters(arguments) + + defp get_letters([{name, _, arguments} | _]) when is_list(arguments), + do: letters_from_name_and_arguments(name, arguments) + + defp get_letters([{name, _, arguments} | _]) when is_atom(arguments), + do: letters_from_name_and_arguments(name, []) + + defp letters_from_name_and_arguments(name, arguments) do + name + |> Atom.to_string() + |> String.slice(0, length(arguments)) + end + + def decode_secret_message(string) do + {_ast, result} = + string + |> to_ast() + |> Macro.prewalk([], &decode_secret_message_part/2) + + result + |> Enum.reverse() + |> Enum.join() + end +end diff --git a/elixir/top-secret/mix.exs b/elixir/top-secret/mix.exs new file mode 100644 index 0000000..e670bb8 --- /dev/null +++ b/elixir/top-secret/mix.exs @@ -0,0 +1,28 @@ +defmodule TopSecret.MixProject do + use Mix.Project + + def project do + [ + app: :top_secret, + version: "0.1.0", + # elixir: "~> 1.10", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/elixir/top-secret/test/test_helper.exs b/elixir/top-secret/test/test_helper.exs new file mode 100644 index 0000000..e8677a3 --- /dev/null +++ b/elixir/top-secret/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true, seed: 0) diff --git a/elixir/top-secret/test/top_secret_test.exs b/elixir/top-secret/test/top_secret_test.exs new file mode 100644 index 0000000..a102489 --- /dev/null +++ b/elixir/top-secret/test/top_secret_test.exs @@ -0,0 +1,301 @@ +defmodule TopSecretTest do + use ExUnit.Case + + describe "to_ast/1" do + @tag task_id: 1 + test "handles an empty string" do + string = "" + ast = {:__block__, [], []} + + assert TopSecret.to_ast(string) == ast + end + + @tag task_id: 1 + test "handles a small code snippet" do + string = """ + x = 7 + y = x - 2 + """ + + ast = + {:__block__, [], + [ + {:=, [line: 1], [{:x, [line: 1], nil}, 7]}, + {:=, [line: 2], [{:y, [line: 2], nil}, {:-, [line: 2], [{:x, [line: 2], nil}, 2]}]} + ]} + + assert TopSecret.to_ast(string) == ast + end + + @tag task_id: 1 + test "handles a bigger code snippet" do + string = """ + defmodule List do + @spec delete([], any) :: [] + @spec delete([...], any) :: list + def delete(list, element) + end + """ + + ast = { + :defmodule, + [line: 1], + [ + {:__aliases__, [line: 1], [:List]}, + [ + do: { + :__block__, + [], + [ + {:@, [line: 2], + [ + {:spec, [line: 2], + [{:"::", [line: 2], [{:delete, [line: 2], [[], {:any, [line: 2], nil}]}, []]}]} + ]}, + {:@, [line: 3], + [ + {:spec, [line: 3], + [ + {:"::", [line: 3], + [ + {:delete, [line: 3], [[{:..., [line: 3], nil}], {:any, [line: 3], nil}]}, + {:list, [line: 3], nil} + ]} + ]} + ]}, + {:def, [line: 4], + [{:delete, [line: 4], [{:list, [line: 4], nil}, {:element, [line: 4], nil}]}]} + ] + } + ] + ] + } + + assert TopSecret.to_ast(string) == ast + end + end + + describe "decode_secret_message_part/2" do + @tag task_id: 2 + test "returns the AST and accumulator unchanged (function call)" do + string = "2 + 3" + ast = TopSecret.to_ast(string) + acc = ["le", "mo"] + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc) + assert actual_ast == ast + assert actual_acc == acc + end + + @tag task_id: 2 + test "returns the AST and accumulator unchanged (literal values)" do + acc = ["abc"] + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(12, acc) + assert actual_ast == 12 + assert actual_acc == acc + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(true, acc) + assert actual_ast == true + assert actual_acc == acc + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(:ok, acc) + assert actual_ast == :ok + assert actual_acc == acc + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part("meh", acc) + assert actual_ast == "meh" + assert actual_acc == acc + end + + @tag task_id: 2 + test "appends a public function name to the accumulator" do + string = "def fit(a, b, c), do: :scale" + ast = TopSecret.to_ast(string) + acc = ["at"] + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc) + assert actual_ast == ast + assert actual_acc == ["fit", "at"] + end + + @tag task_id: 2 + test "appends a private function name to the accumulator" do + string = "defp op(a, b), do: 2" + ast = TopSecret.to_ast(string) + acc = ["e", "ced"] + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc) + assert actual_ast == ast + assert actual_acc == ["op", "e", "ced"] + end + + @tag task_id: 2 + test "ignores not top-level function definition" do + string = """ + defmodule Math do + def sin(x), do: do_sin(x) + defp do_sin(x), do: nil + end + """ + + ast = TopSecret.to_ast(string) + acc = [] + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc) + assert actual_ast == ast + assert actual_acc == acc + end + + @tag task_id: 3 + test "function arity affects message part length" do + string = "def adjust(a, b), do: :scale" + ast = TopSecret.to_ast(string) + acc = ["re"] + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc) + assert actual_ast == ast + assert actual_acc == ["ad", "re"] + end + + @tag task_id: 3 + test "function arity 0 results in empty string" do + string = "def adjust(), do: :scale" + ast = TopSecret.to_ast(string) + acc = ["re"] + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc) + assert actual_ast == ast + assert actual_acc == ["", "re"] + end + + @tag task_id: 3 + test "function arity 0 and no parentheses results in empty string" do + string = "def adjust, do: :scale" + ast = TopSecret.to_ast(string) + acc = ["re"] + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc) + assert actual_ast == ast + assert actual_acc == ["", "re"] + end + + @tag task_id: 4 + test "works for public functions with a guard" do + string = "def sign(a) when a >= 0, do: :+" + ast = TopSecret.to_ast(string) + acc = ["e"] + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc) + assert actual_ast == ast + assert actual_acc == ["s", "e"] + end + + @tag task_id: 4 + test "works for private functions with a guard" do + string = "defp do_sign(a) when a < 0, do: :-" + ast = TopSecret.to_ast(string) + acc = ["e"] + + {actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc) + assert actual_ast == ast + assert actual_acc == ["d", "e"] + end + end + + describe "decode_secret_message/1" do + @tag task_id: 5 + test "decodes a secret message from a single function definition" do + code = """ + defmodule Notebook do + def note(notebook, text) do + add_to_notebook(notebook, text, append: true) + end + end + """ + + secret_message = "no" + + assert TopSecret.decode_secret_message(code) == secret_message + end + + @tag task_id: 5 + test "decodes a secret message from a two function definitions" do + code = """ + defmodule MyCalendar do + def busy?(date, time) do + Date.day_of_week(date) != 7 and + time.hour in 10..16 + end + + def yesterday?(date) do + Date.diff(Date.utc_today, date) + end + end + """ + + secret_message = "buy" + + assert TopSecret.decode_secret_message(code) == secret_message + end + + @tag task_id: 5 + test "decodes a secret message from many function definitions" do + code = """ + defmodule TotallyNotTopSecret do + def force(mass, acceleration), do: mass * acceleration + def uniform(from, to), do: rand.uniform(to - from) + from + def data(%{metadata: metadata}, _opts), do: model(metadata) + defp model(metadata, _opts), do: metadata |> less_data |> Enum.reverse() |> Enum.take(3) + defp less_data(data, _opts), do: Enum.reject(data, &is_nil/1) + end + """ + + secret_message = "foundamole" + + assert TopSecret.decode_secret_message(code) == secret_message + end + + @tag task_id: 5 + test "decodes a secret message without a module definition" do + code = """ + def force(mass, acceleration), do: mass * acceleration + def uniform(from, to), do: rand.uniform(to - from) + from + def data(%{metadata: metadata}, _opts), do: model(metadata) + defp model(metadata, _opts), do: metadata |> less_data |> Enum.reverse() |> Enum.take(3) + defp less_data(data, _opts), do: Enum.reject(data, &is_nil/1) + """ + + secret_message = "foundamole" + + assert TopSecret.decode_secret_message(code) == secret_message + end + + @tag task_id: 5 + test "decodes another secret message from multiple modules" do + code = """ + defmodule IOHelpers do + def inspect(x, opts), do: IO.inspect(x, opts) + def vi_or_vim(_env, _preference), do: :vim + def signal(pid, string), do: send(pid, {:signal, string}) + def black(text, label), do: IO.ANSI.black <> label <> text <> IO.ANSI.reset() + end + + defmodule TimeHelpers do + defp est_to_cet(time), do: Time.add(time, 6 * 60 * 60) + end + + defmodule ASTHelpers do + def submodule?(m, _f, _args), do: String.contains?(m, ".") + def module({m, _f, _args}), do: m + def arity(_m, _f, args), do: length(args) + defp nested?(x, y) when is_list(y), do: x in y + end + """ + + secret_message = "invisiblesubmarine" + + assert TopSecret.decode_secret_message(code) == secret_message + end + end +end