From de2ceb267e1b361ffb2c0745fad4109f104fc556 Mon Sep 17 00:00:00 2001 From: Christophe De Troyer Date: Mon, 9 Dec 2024 20:49:32 +0100 Subject: [PATCH] draft version --- README.md | 49 +++++- lib/ex_example.ex | 101 ++++++++++- lib/ex_example/analyzer/analyze.ex | 171 +++++++++++++++++++ lib/ex_example/application.ex | 13 ++ lib/ex_example/behaviour.ex | 12 -- lib/ex_example/cache.ex | 0 lib/ex_example/cache/cache.ex | 53 ++++++ lib/ex_example/cache/key.ex | 18 ++ lib/ex_example/cache/result.ex | 19 +++ lib/ex_example/cache_result.ex | 19 --- lib/ex_example/examples/e_cache_result.ex | 16 -- lib/ex_example/executor.ex | 113 ++++++++++++ lib/ex_example/supervisor.ex | 24 --- lib/examples/stack.ex | 46 +++++ lib/examples/stack_examples.ex | 32 ++++ mix.exs | 14 +- mix.lock | 14 +- test/{ex_example_test.exs => stack_test.exs} | 4 +- 18 files changed, 621 insertions(+), 97 deletions(-) create mode 100644 lib/ex_example/analyzer/analyze.ex create mode 100644 lib/ex_example/application.ex delete mode 100644 lib/ex_example/behaviour.ex create mode 100644 lib/ex_example/cache.ex create mode 100644 lib/ex_example/cache/cache.ex create mode 100644 lib/ex_example/cache/key.ex create mode 100644 lib/ex_example/cache/result.ex delete mode 100644 lib/ex_example/cache_result.ex delete mode 100644 lib/ex_example/examples/e_cache_result.ex create mode 100644 lib/ex_example/executor.ex delete mode 100644 lib/ex_example/supervisor.ex create mode 100644 lib/examples/stack.ex create mode 100644 lib/examples/stack_examples.ex rename test/{ex_example_test.exs => stack_test.exs} (56%) diff --git a/README.md b/README.md index b83b7a3..aa6cacb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # ExExample -**TODO: Add description** +`ExExample` aims to provide an example-driven test framework for Elixir applications. + +As opposed to regular unit tests, examples are supposed to be executed from within the REPL. + +Examples serve both as a unit test, but also as a tool to discover, learn, and interact with a live +system such as Elixir applications. ## Installation @@ -15,7 +20,43 @@ def deps do end ``` -Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) -and published on [HexDocs](https://hexdocs.pm). Once published, the docs can -be found at . +## Your First Example + +To get started, create a new module in the `lib/` folder of your Elixir application and add an example. + +```elixir +defmodule MyExamples do + use ExExample + defexample read_data() do + 1..1000 |> Enum.shuffle() |> Enum.take(10) + end +end +``` + +In a running REPL with your application loaded, you can execute this example using `MyExamples.read_data()`. +The example will be executed once, and the cached result will be returned the next time around. + +## Caching +In a REPL session it's not uncommon to recompile your code (e.g., using `recompile()`). This changes +the semantics of your examples. + +To avoid working with stale outputs, `ExExample` only returns the cached version of your example +if the code it depends on, or the example itself, have not been changed. + +When the code changes, the example is executed again. + +## Tests + +The examples are created to work with the code base, but they can also serve as a unit test. + +To let ExUnit use the examples in your codebase as tests, add a test file in the `test/` folder, and +import the `ExExample.Test` module. + +To run the examples from above, add a file `ny_examples_test.exs` to your `test/` folder and include the following. + +```elixir +defmodule MyExamplesTest do + use ExExample.Test, for: MyExamples +end +``` \ No newline at end of file diff --git a/lib/ex_example.ex b/lib/ex_example.ex index ee90b41..35af520 100644 --- a/lib/ex_example.ex +++ b/lib/ex_example.ex @@ -1,17 +1,100 @@ defmodule ExExample do @moduledoc """ - I am the ExExample Application Module + Documentation for `ExExample`. + """ + alias ExExample.Analyze + alias ExExample.Executor - I startup the ExExample system as an OTP application. Moreover Ι - provide all the API necessary for the user of the system. I contain - all public functionality + defmacro __using__(_options) do + quote do + import unquote(__MODULE__) - ### Public API - """ + # module attribute that holds all the examples + Module.register_attribute(__MODULE__, :example_dependencies, accumulate: true) + Module.register_attribute(__MODULE__, :examples, accumulate: true) + + @before_compile unquote(__MODULE__) + end + end + + defmacro __before_compile__(_env) do + quote do + @doc """ + I return a list of all the dependencies for a given example, + or the list of all dependencies if no argument is given. + """ + def __example_dependencies__, do: @example_dependencies + + def __example_dependencies__(dependee) do + @example_dependencies + |> Enum.find({nil, []}, fn {name, _} -> name == dependee end) + |> elem(1) + end + + @doc """ + I reutrn all the examples in this module. + """ + def __examples__ do + @examples + end + + @doc """ + I run all the examples in this module. + """ + def __run_examples__ do + __sorted__() + |> Enum.each(fn {module, name} -> + apply(module, name, []) + end) + end + + @doc """ + I return a topologically sorted list of examples. + This list is the order in which the examples should be run. + """ + @spec __sorted__() :: list({atom(), atom()}) + def __sorted__ do + __example_dependencies__() + |> Enum.reduce(Graph.new(), fn + {example, []}, g -> + Graph.add_vertex(g, {__MODULE__, example}) + + {example, dependencies}, g -> + dependencies + # filter out all non-example dependencies + |> Enum.filter(&Executor.example?/1) + |> Enum.reduce(g, fn {{module, func}, _arity}, g -> + Graph.add_edge(g, {module, func}, {__MODULE__, example}) + end) + end) + |> Graph.topsort() + end + end + end + + defmacro example({example_name, context, args} = name, do: body) do + called_functions = Analyze.extract_function_calls(body, __CALLER__) + + # example_name is the name of the function that is being tested + # e.g., `example_name` + + # hidden_func_name is the name of the hidden function that is being tested + # this will contain the actual body of the example + # __example_name__ + hidden_example_name = String.to_atom("__#{example_name}__") + + quote do + def unquote({hidden_example_name, context, args}) do + unquote(body) + end - use Application + @example_dependencies {unquote(example_name), unquote(called_functions)} + @examples unquote(example_name) + def unquote(name) do + example_dependencies = __example_dependencies__(unquote(example_name)) - def start(_type, args \\ []) do - ExExample.Supervisor.start_link(args) + Executor.maybe_run_example(__MODULE__, unquote(example_name), example_dependencies) + end + end end end diff --git a/lib/ex_example/analyzer/analyze.ex b/lib/ex_example/analyzer/analyze.ex new file mode 100644 index 0000000..ce8f391 --- /dev/null +++ b/lib/ex_example/analyzer/analyze.ex @@ -0,0 +1,171 @@ +defmodule ExExample.Analyze do + @moduledoc """ + I contain functionality to analyze ASTs. + + I have functionality to extract modules on which an AST depends, + function calls it makes, and definitions from a module AST. + """ + require Logger + + defmodule State do + @moduledoc """ + I implement the state for analyzing an AST. + """ + defstruct called_functions: [], env: nil, functions: [] + + def put_call(state, mod, arg) do + %{state | called_functions: [{mod, arg} | state.called_functions]} + end + + def put_def(state, func, arity) do + %{state | functions: [{func, arity} | state.functions]} + end + end + + # ---------------------------------------------------------------------------- + # Compute hash of all modules that the example depends on + + def compile_dependency_hash(dependencies) do + dependencies + |> Enum.map(fn {{module, _func}, _arity} -> + module.__info__(:attributes)[:vsn] + end) + |> :erlang.phash2() + end + + # ---------------------------------------------------------------------------- + # Exctract function calls from ast + + def extract_function_calls(ast, env) do + state = %State{env: env} + # IO.inspect(env) + {_, state} = Macro.prewalk(ast, state, &extract_function_calls_logged/2) + state.called_functions + end + + defp extract_function_calls_logged(ast, state) do + # IO.puts("------------------------------------------- ") + # IO.inspect(ast) + + extract_function_call(ast, state) + end + + # qualified function call + # e.g., Foo.bar() + defp extract_function_call( + {{:., _, [{:__aliases__, _, aliases}, func_name]}, _, args} = ast, + state + ) do + case Macro.Env.expand_alias(state.env, [], aliases) do + :error -> + arg_count = Enum.count(args) + module = Module.concat(aliases) + state = State.put_call(state, {module, func_name}, arg_count) + {ast, state} + + {:alias, resolved} -> + arg_count = Enum.count(args) + state = State.put_call(state, {resolved, func_name}, arg_count) + {ast, state} + end + end + + # variable in binding + # e.g. `x` in `x = 1` + defp extract_function_call({_func, _, nil} = ast, state) do + {ast, state} + end + + @special_forms Kernel.SpecialForms.__info__(:macros) + defp extract_function_call({func, _, args} = ast, state) do + arg_count = Enum.count(args) + + state = + case Macro.Env.lookup_import(state.env, {func, arg_count}) do + # imported call + [{:function, module}] -> + State.put_call(state, {module, func}, arg_count) + + [{:macro, _module}] -> + state + + # local def + [] -> + if {func, arg_count} in @special_forms or func in [:__block__, :&, :__aliases__] do + state + else + State.put_call(state, {state.env.module, func}, arg_count) + end + end + + {ast, state} + end + + defp extract_function_call(ast, state) do + {ast, state} + end + + # ---------------------------------------------------------------------------- + # Exctract function definitions from module + + @doc """ + Given the path of a source file, I extract the definitions of the functions. + """ + @spec extract_defs(String.t(), Macro.Env.t()) :: [{atom(), non_neg_integer()}] + def extract_defs(file, env) do + source = File.read!(file) + {:ok, ast} = Code.string_to_quoted(source) + extract_defs_from_source(ast, env) + end + + defp extract_defs_from_source(ast, env) do + # create the initial state + state = %State{env: env} + + # walk the ast and extract the function definitions + {_, state} = Macro.prewalk(ast, state, &extract_def_logged/2) + + state.functions + end + + defp extract_def_logged(ast, state) do + # IO.puts("------------------------------------------- ") + # IO.inspect(ast) + + extract_def(ast, state) + end + + defp extract_def({:example, _, [{fun, _, args}, _body]} = ast, state) do + state = + args + |> count_args() + |> Enum.reduce(state, fn i, state -> + State.put_def(state, fun, i) + end) + + {ast, state} + end + + defp extract_def(ast, state) do + {ast, state} + end + + # @doc """ + # I count the arguments in an argument list. + # I return the number of required arguments followed by the number of optional arguments. + # """ + @spec count_args([any()]) :: any() + defp count_args(args) do + {req, opt} = + args + |> Enum.reduce({0, 0}, fn + {:\\, _, [{_arg, _, _}, _]}, {req, opt} -> + {req, opt + 1} + + _, {req, opt} -> + {req + 1, opt} + end) + + req..(req + opt) + end +end diff --git a/lib/ex_example/application.ex b/lib/ex_example/application.ex new file mode 100644 index 0000000..31a253f --- /dev/null +++ b/lib/ex_example/application.ex @@ -0,0 +1,13 @@ +defmodule ExExample.Application do + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [{Cachex, [ExExample.Cache]}] + + opts = [strategy: :one_for_one, name: ExExample.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/lib/ex_example/behaviour.ex b/lib/ex_example/behaviour.ex deleted file mode 100644 index 4e1cbcb..0000000 --- a/lib/ex_example/behaviour.ex +++ /dev/null @@ -1,12 +0,0 @@ -defmodule ExExample.Behaviour do - @moduledoc """ - I help determine when Examples ought to be run again or be copied - - - I do this by defining out a behaviour that is to be used with the - use macro for ExExample - """ - - @callback rerun?(ExExample.CacheResult.t()) :: boolean() - @callback copy(ExExample.CacheResult.t()) :: any() -end diff --git a/lib/ex_example/cache.ex b/lib/ex_example/cache.ex new file mode 100644 index 0000000..e69de29 diff --git a/lib/ex_example/cache/cache.ex b/lib/ex_example/cache/cache.ex new file mode 100644 index 0000000..7c4a3e5 --- /dev/null +++ b/lib/ex_example/cache/cache.ex @@ -0,0 +1,53 @@ +defmodule ExExample.Cache do + @moduledoc """ + I define logic to store and retrieve results from the cache. + """ + + alias ExExample.Cache.Key + alias ExExample.Cache.Result + + require Logger + + @cache_name __MODULE__ + + @doc """ + I store a result in cache for a given key. + """ + @spec put_result(Result.t(), Key.t()) :: {atom(), boolean()} + def put_result(%Result{} = result, %Key{} = key) do + Cachex.put(@cache_name, key, result) + end + + @doc """ + I fetch a previous Result from the cache if it exists. + If it does not exist, I return `{:error, :not_found}`. + """ + @spec get_result(Key.t()) :: {:ok, any()} | {:error, :no_result} + def get_result(%Key{} = key) do + case Cachex.get(@cache_name, key) do + {:ok, nil} -> + {:error, :no_result} + + {:ok, result} -> + {:ok, result} + end + end + + @doc """ + I return the state of the last execution of an example. + """ + @spec state(Key.t() | {atom(), atom()}) :: :succeeded | :failed | :skipped + def state({module, function}) do + state(%Key{module: module, function: function}) + end + + def state(%Key{} = key) do + case Cachex.get(@cache_name, key) do + {:ok, nil} -> + nil + + {:ok, result} -> + result.success + end + end +end diff --git a/lib/ex_example/cache/key.ex b/lib/ex_example/cache/key.ex new file mode 100644 index 0000000..bb6d390 --- /dev/null +++ b/lib/ex_example/cache/key.ex @@ -0,0 +1,18 @@ +defmodule ExExample.Cache.Key do + @moduledoc """ + I represent the key for an example invocation. + + I identify an invocation by means of its module, name, arity, and list of arguments. + """ + use TypedStruct + + typedstruct enforce: true do + @typedoc """ + I represent the key for an example invocation. + """ + field(:module, atom()) + field(:function, atom()) + field(:arguments, [term()], default: []) + field(:deps_hash, any(), default: nil) + end +end diff --git a/lib/ex_example/cache/result.ex b/lib/ex_example/cache/result.ex new file mode 100644 index 0000000..558b085 --- /dev/null +++ b/lib/ex_example/cache/result.ex @@ -0,0 +1,19 @@ +defmodule ExExample.Cache.Result do + @moduledoc """ + I represent the result of an example execution. + + I contain the key for the example I am the result of, the status of the execution, and the result of the execution. + """ + use TypedStruct + + alias ExExample.Cache.Key + + typedstruct enforce: false do + @typedoc """ + I represent the result of a completed Example Computation + """ + field(:key, Key.t()) + field(:success, :failed | :success | :skipped) + field(:result, term()) + end +end diff --git a/lib/ex_example/cache_result.ex b/lib/ex_example/cache_result.ex deleted file mode 100644 index 3793f47..0000000 --- a/lib/ex_example/cache_result.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule ExExample.CacheResult do - @moduledoc """ - I represent the cached result of a ran Example - """ - - use TypedStruct - - typedstruct enforce: true do - @typedoc """ - I represent the result of a completed Example Computation - """ - - field(:arguments, Macro.input() | nil, default: nil) - field(:source, Macro.input()) - field(:source_name, {module(), atom(), non_neg_integer()}) - field(:result, term()) - field(:pure, boolean()) - end -end diff --git a/lib/ex_example/examples/e_cache_result.ex b/lib/ex_example/examples/e_cache_result.ex deleted file mode 100644 index c962705..0000000 --- a/lib/ex_example/examples/e_cache_result.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule ExExample.Examples.ECacheResult do - alias ExExample.CacheResult - - def trivial_definition() do - 5 - end - - def trivial_cached_result do - %CacheResult{ - source: [do: 5], - pure: true, - result: 5, - source_name: {__MODULE__, :trivial_definition, 0} - } - end -end diff --git a/lib/ex_example/executor.ex b/lib/ex_example/executor.ex new file mode 100644 index 0000000..9be0b58 --- /dev/null +++ b/lib/ex_example/executor.ex @@ -0,0 +1,113 @@ +defmodule ExExample.Executor do + @moduledoc """ + I contain functionality to execute examples. + + I contain logic to determine if a cachd result should be used, computation should be done again, + or if an example should be skipped. + """ + alias ExExample.Cache + + require Logger + @type dependency :: {{atom(), atom()}, non_neg_integer()} + + @doc """ + I determine if a module/function pair is an example or not. + + A function is an example if it is defined in a module that has the `__examples__/0` function + implemented, and when the `__examples__()` output lists that function name as being an example. + """ + @spec example?(dependency) :: boolean() + def example?({{module, func}, _arity}) do + {:__examples__, 0} in module.__info__(:functions) and func in module.__examples__() + end + + @doc """ + Given an example, I return a hash of all its dependencies. + This hash can be used to determine of an example was run with + an older version of a dependency. + """ + def deps_hash(dependencies) do + dependencies + |> Enum.map(fn {{module, _func}, _arity} -> + module.__info__(:attributes)[:vsn] + end) + |> :erlang.phash2() + end + + @doc """ + I run an example, iff all its dependencies have succeeded. + + If all the dependencies of this example executed succesfully, + I will execute the example. + + If any of the example its dependencies either failed or were skipped, + I will skip the example. + """ + @spec maybe_run_example(atom(), atom(), list(dependency)) :: any() + def maybe_run_example(module, func, dependencies) do + dependency_results = + dependencies + |> Enum.map(fn {{module, func}, _arity} -> + Cache.state({module, func}) + end) + |> Enum.group_by(& &1) + |> Map.put_new(:success, []) + |> Map.put_new(:skipped, []) + |> Map.put_new(:failed, []) + + deps_hash = deps_hash(dependencies) + + case dependency_results do + %{success: _, failed: [], skipped: []} -> + # check for a cached result + case Cache.get_result(%Cache.Key{module: module, function: func, deps_hash: deps_hash}) do + # cached result, no recompile + {:ok, result} -> + Logger.debug("found cached result for #{inspect(module)}.#{func}") + result.result + + {:error, :no_result} -> + Logger.debug("running #{inspect(module)}.#{func} for the first time") + hidden_example_name = String.to_atom("__#{func}__") + + run_example(module, hidden_example_name, [], func, deps_hash) + |> Map.get(:result) + end + + map -> + Logger.warning( + "skipping #{inspect(module)}.#{func} due to failed or skipped dependencies" + ) + + {:error, :skipped_or_failed, map} + end + end + + # @doc """ + # I run an example in a module and wrap its output in + # something that can be cached. + # """ + @spec run_example(atom(), atom(), list(term()), atom(), any()) :: Cache.Result.t() + defp run_example(module, func, arguments, example_name, deps_hash) do + key = %Cache.Key{ + module: module, + function: example_name, + arguments: arguments, + deps_hash: deps_hash + } + + result = + try do + %Cache.Result{key: key, success: :success, result: apply(module, func, [])} + rescue + e -> + Logger.error(inspect(e)) + %Cache.Result{key: key, success: :failed, result: e} + end + + # put the result of this invocation in the cache. + Cache.put_result(result, key) + + result + end +end diff --git a/lib/ex_example/supervisor.ex b/lib/ex_example/supervisor.ex deleted file mode 100644 index 9f3100d..0000000 --- a/lib/ex_example/supervisor.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule ExExample.Supervisor do - @moduledoc """ - I am the ExUnit Supervisor for Caching. - """ - - use Supervisor - - @type startup_options :: {:name, atom()} - - @spec start_link(list(startup_options())) :: GenServer.on_start() - def start_link(args \\ []) do - {:ok, keys} = - args - |> Keyword.validate(name: __MODULE__) - - Supervisor.start_link(__MODULE__, keys, name: keys[:name]) - end - - @impl true - def init(_args) do - children = [{Cachex, [:ex_examples]}] - Supervisor.init(children, strategy: :one_for_one) - end -end diff --git a/lib/examples/stack.ex b/lib/examples/stack.ex new file mode 100644 index 0000000..6861f7a --- /dev/null +++ b/lib/examples/stack.ex @@ -0,0 +1,46 @@ +defmodule Stack do + @moduledoc """ + I am an example implementation of a Stack. I am used to show example.. examples. + """ + use TypedStruct + + typedstruct enforce: true do + @typedoc """ + I represent the key for an example invocation. + """ + field(:elements, [any()], default: []) + end + + @spec create() :: {:ok, t()} + def create do + {:ok, %Stack{}} + end + + # yesyesyes + @spec empty?(t()) :: boolean + def empty?(%Stack{elements: []}), do: true + def empty?(%Stack{elements: _}), do: false + + @spec push(t(), any()) :: {:ok, t()} + def push(%Stack{elements: xs}, x) do + {:ok, %Stack{elements: [x | xs]}} + end + + @spec pop(t()) :: {:ok, t(), any()} | {:error, :empty} + def pop(%Stack{elements: []}) do + {:error, :empty} + end + + def pop(%Stack{elements: [x | xs]}) do + {:ok, %Stack{elements: xs}, x} + end + + @spec peek(t()) :: {:ok, t(), any()} | {:error, :empty} + def peek(%Stack{elements: []}) do + {:error, :empty} + end + + def peek(%Stack{elements: [x | xs]}) do + {:ok, %Stack{elements: [x | xs]}, x} + end +end diff --git a/lib/examples/stack_examples.ex b/lib/examples/stack_examples.ex new file mode 100644 index 0000000..c9a361d --- /dev/null +++ b/lib/examples/stack_examples.ex @@ -0,0 +1,32 @@ +defmodule Examples.Stack do + @moduledoc """ + I contain examples that test the `Stack` implementation. + """ + use ExExample + + import ExUnit.Assertions + + example new_stack do + {:ok, stack} = Stack.create() + assert stack == %Stack{} + stack + end + + example empty_stack_should_be_empty do + stack = new_stack() + + assert Stack.empty?(stack) + end + + example push_stack do + stack = new_stack() + {:ok, stack} = Stack.push(stack, 1) + stack + end + + example pop_stack do + stack = push_stack() + {:ok, stack, 1} = Stack.pop(stack) + stack + end +end diff --git a/mix.exs b/mix.exs index 76ad48d..4037108 100644 --- a/mix.exs +++ b/mix.exs @@ -7,15 +7,20 @@ defmodule ExExample.MixProject do version: "0.1.0", elixir: "~> 1.17", start_permanent: Mix.env() == :prod, - deps: deps() + deps: deps(), + dialyzer: [ + plt_add_deps: :apps_direct, + plt_add_apps: [:wx, :ex_unit], + plt_ignore_apps: [:mnesia] + ] ] end # Run "mix help compile.app" to learn about applications. def application do [ - mod: {ExExample, []}, - extra_applications: [:logger, :observer, :wx] + extra_applications: [:logger, :observer, :wx], + mod: {ExExample.Application, []} ] end @@ -29,7 +34,8 @@ defmodule ExExample.MixProject do # non-runtime dependencies below {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.3", only: [:dev], runtime: false}, - {:ex_doc, "~> 0.31", only: [:dev], runtime: false} + {:ex_doc, "~> 0.31", only: [:dev], runtime: false}, + {:libgraph, "~> 0.16.0"} ] end end diff --git a/mix.lock b/mix.lock index cbb4abf..6ff4b68 100644 --- a/mix.lock +++ b/mix.lock @@ -1,19 +1,19 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "cachex": {:hex, :cachex, "4.0.2", "120f9c27b0a453c7cb3319d9dc6c61c050a480e5299fc1f8bded1e2e334992ab", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "4f4890122bddd979f6c217d5e300d0c0d3eb858a976cbe1f65a94e6322bc5825"}, - "credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"}, - "dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"}, + "cachex": {:hex, :cachex, "4.0.3", "95e88c3ef4d37990948eaecccefe40b4ce4a778e0d7ade29081e6b7a89309ee2", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d5d632da7f162f8a190f1c39b712c0ebc9cf0007c4e2029d44eddc8041b52d55"}, + "credo": {:hex, :credo, "1.7.10", "6e64fe59be8da5e30a1b96273b247b5cf1cc9e336b5fd66302a64b25749ad44d", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "71fbc9a6b8be21d993deca85bf151df023a3097b01e09a2809d460348561d8cd"}, + "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, - "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, - "excache": {:hex, :excache, "0.1.0", "cb47ccc8372a4490d7738d2353b3b743d0975e820b5251b6881821b02e062770", [:mix], [], "hexpm", "b5bbefc2b8a82bc92b848fd1da5b32cfb856fe4ff4ffd615aa77e16de772500f"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, - "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.0", "74bb8348c9b3a51d5c589bf5aebb0466a84b33274150e3b6ece1da45584afc82", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49159b7d7d999e836bedaf09dcf35ca18b312230cf901b725a64f3f42e407983"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, diff --git a/test/ex_example_test.exs b/test/stack_test.exs similarity index 56% rename from test/ex_example_test.exs rename to test/stack_test.exs index 0cf82cc..73a60c9 100644 --- a/test/ex_example_test.exs +++ b/test/stack_test.exs @@ -1,8 +1,8 @@ -defmodule ExExampleTest do +defmodule StackTest do use ExUnit.Case doctest ExExample test "greets the world" do - assert 1 == 1 + Examples.Stack.__run_examples__() end end