Skip to content

Commit 7a55fab

Browse files
committed
behavior
1 parent f500d84 commit 7a55fab

File tree

14 files changed

+437
-171
lines changed

14 files changed

+437
-171
lines changed

.credo.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
{Credo.Check.Readability.RedundantBlankLines, []},
110110
{Credo.Check.Readability.Semicolons, []},
111111
{Credo.Check.Readability.SpaceAfterCommas, []},
112-
# {Credo.Check.Readability.Specs, []},
112+
{Credo.Check.Readability.Specs, []},
113113
{Credo.Check.Readability.StringSigils, []},
114114
{Credo.Check.Readability.TrailingBlankLine, []},
115115
{Credo.Check.Readability.TrailingWhiteSpace, []},

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ if the code it depends on, or the example itself, have not been changed.
4646

4747
When the code changes, the example is executed again.
4848

49-
## Tests
49+
## Tests
5050

51-
The examples are created to work with the code base, but they can also serve as a unit test.
51+
The examples are created to work with the code base, but they can also serve as a unit test.
5252

5353
To let ExUnit use the examples in your codebase as tests, add a test file in the `test/` folder, and
54-
import the `ExExample.Test` module.
54+
import the `ExExample.Test` module.
5555

5656
To run the examples from above, add a file `ny_examples_test.exs` to your `test/` folder and include the following.
5757

lib/ex_example.ex

Lines changed: 109 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,120 @@ defmodule ExExample do
33
Documentation for `ExExample`.
44
"""
55
alias ExExample.Analyze
6+
alias ExExample.Cache
67
alias ExExample.Executor
78

9+
############################################################
10+
# Types #
11+
############################################################
12+
13+
@typedoc """
14+
A dependency is a function that will be called by an example.
15+
The format of a dependency is `{{module, function}, arity}`
16+
"""
17+
@type dependency :: {{atom(), atom()}, non_neg_integer()}
18+
19+
@typedoc """
20+
"""
21+
@type example :: {atom(), list(dependency)}
22+
23+
############################################################
24+
# Helpers #
25+
############################################################
26+
27+
@doc """
28+
I return the hidden name of an example.
29+
The hidden name is the example body without modification.
30+
"""
31+
@spec hidden_name({atom(), atom()}) :: {atom(), atom()}
32+
def hidden_name({module, func}) do
33+
{module, String.to_atom("__#{func}__")}
34+
end
35+
36+
@doc """
37+
I determine if a module/function pair is an example or not.
38+
39+
A function is an example if it is defined in a module that has the `__examples__/0` function
40+
implemented, and when the `__examples__()` output lists that function name as being an example.
41+
"""
42+
@spec example?(dependency()) :: boolean()
43+
def example?({{module, func}, _arity}) do
44+
example_module?(module) and Keyword.has_key?(module.__examples__(), func)
45+
end
46+
47+
@doc """
48+
I return true if the given module contains examples.
49+
"""
50+
@spec example_module?(atom()) :: boolean
51+
def example_module?(module) do
52+
{:__examples__, 0} in module.__info__(:functions)
53+
end
54+
55+
@doc """
56+
I return a list of all dependencies for this example.
57+
Note: this does includes other called modules too (e.g., Enum).
58+
"""
59+
@spec all_dependencies({atom(), atom()}) :: [dependency()]
60+
def all_dependencies({module, func}) do
61+
module.__examples__()
62+
|> Keyword.get(func, [])
63+
end
64+
65+
@doc """
66+
I return a list of example dependencies for this example.
67+
Note: this does not include other called modules.
68+
"""
69+
@spec example_dependencies({atom(), atom()}) :: [dependency()]
70+
def example_dependencies({module, func}) do
71+
all_dependencies({module, func})
72+
|> Enum.filter(&example?/1)
73+
end
74+
75+
@doc """
76+
I return a list of examples in the order they should be
77+
executed in.
78+
79+
I do this by topologically sorting their execution order.
80+
"""
81+
@spec execution_order(atom()) :: [{atom(), atom()}]
82+
def execution_order(module) do
83+
module.__examples__()
84+
|> Enum.reduce(Graph.new(), fn
85+
{function, []}, g ->
86+
Graph.add_vertex(g, {__MODULE__, function})
87+
88+
{function, dependencies}, g ->
89+
dependencies
90+
# filter out all non-example dependencies
91+
|> Enum.filter(&example?/1)
92+
|> Enum.reduce(g, fn {{module, func}, _arity}, g ->
93+
Graph.add_edge(g, {module, func}, {module, function})
94+
end)
95+
end)
96+
|> Graph.topsort()
97+
end
98+
99+
############################################################
100+
# Macros #
101+
############################################################
102+
8103
defmacro __using__(_options) do
9104
quote do
10105
import unquote(__MODULE__)
11106

107+
@behaviour ExExample.Behaviour
108+
12109
# module attribute that holds all the examples
13-
Module.register_attribute(__MODULE__, :example_dependencies, accumulate: true)
14110
Module.register_attribute(__MODULE__, :examples, accumulate: true)
15-
Module.register_attribute(__MODULE__, :copies, accumulate: true)
16-
Module.register_attribute(__MODULE__, :copy, accumulate: false)
17111

18112
@before_compile unquote(__MODULE__)
19113
end
20114
end
21115

22116
defmacro __before_compile__(_env) do
23117
quote do
24-
@doc """
25-
I return a list of all the dependencies for a given example,
26-
or the list of all dependencies if no argument is given.
27-
"""
28-
def __example_dependencies__, do: @example_dependencies
29-
30-
def __example_dependencies__(dependee) do
31-
@example_dependencies
32-
|> Enum.find({nil, []}, fn {name, _} -> name == dependee end)
33-
|> elem(1)
34-
end
35-
36-
@doc """
37-
I reutrn all the examples in this module.
38-
"""
39-
def __examples__ do
40-
@examples
41-
end
42-
43-
@doc """
44-
I run all the examples in this module.
45-
"""
46-
def __run_examples__ do
47-
__sorted__()
48-
|> Enum.each(fn {module, name} ->
49-
apply(module, name, [])
50-
end)
51-
end
52-
53-
@doc """
54-
I return a topologically sorted list of examples.
55-
This list is the order in which the examples should be run.
56-
"""
57-
@spec __sorted__() :: list({atom(), atom()})
58-
def __sorted__ do
59-
__example_dependencies__()
60-
|> Enum.reduce(Graph.new(), fn
61-
{example, []}, g ->
62-
Graph.add_vertex(g, {__MODULE__, example})
63-
64-
{example, dependencies}, g ->
65-
dependencies
66-
# filter out all non-example dependencies
67-
|> Enum.filter(&Executor.example?/1)
68-
|> Enum.reduce(g, fn {{module, func}, _arity}, g ->
69-
Graph.add_edge(g, {module, func}, {__MODULE__, example})
70-
end)
71-
end)
72-
|> Graph.topsort()
73-
end
74-
75-
def __example_copy__(example_name) do
76-
@copies
77-
|> Keyword.get(example_name, nil)
78-
end
118+
@spec __examples__ :: [ExExample.example()]
119+
def __examples__, do: @examples
79120
end
80121
end
81122

@@ -91,24 +132,22 @@ defmodule ExExample do
91132
hidden_example_name = String.to_atom("__#{example_name}__")
92133

93134
quote do
94-
# fetch the attribute value, and then clear it for the next examples.
95-
example_copy_tag = Module.get_attribute(unquote(__CALLER__.module), :copy)
96-
Module.delete_attribute(unquote(__CALLER__.module), :copy)
97-
98135
def unquote({hidden_example_name, context, args}) do
99136
unquote(body)
100137
end
101138

102-
@copies {unquote(example_name), {unquote(__CALLER__.module), example_copy_tag}}
103-
@example_dependencies {unquote(example_name), unquote(called_functions)}
104-
@examples unquote(example_name)
139+
@examples {unquote(example_name), unquote(called_functions)}
105140
def unquote(name) do
106-
example_dependencies = __example_dependencies__(unquote(example_name))
107-
example_copy = __example_copy__(unquote(example_name))
141+
case Executor.attempt_example({__MODULE__, unquote(example_name)}, []) do
142+
%{result: %Cache.Result{success: :success} = result} ->
143+
result.result
144+
145+
%{result: %Cache.Result{success: :failed} = result} ->
146+
raise result.result
108147

109-
Executor.maybe_run_example(__MODULE__, unquote(example_name), example_dependencies,
110-
copy: example_copy
111-
)
148+
%{result: %Cache.Result{success: :skipped} = result} ->
149+
:skipped
150+
end
112151
end
113152
end
114153
end

lib/ex_example/analyzer/analyze.ex

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,21 @@ defmodule ExExample.Analyze do
1313
"""
1414
defstruct called_functions: [], env: nil, functions: []
1515

16+
@spec put_call(map(), {atom(), atom()}, non_neg_integer()) :: map()
1617
def put_call(state, mod, arg) do
1718
%{state | called_functions: [{mod, arg} | state.called_functions]}
1819
end
1920

21+
@spec put_def(map(), atom(), non_neg_integer()) :: map()
2022
def put_def(state, func, arity) do
2123
%{state | functions: [{func, arity} | state.functions]}
2224
end
2325
end
2426

25-
# ----------------------------------------------------------------------------
26-
# Compute hash of all modules that the example depends on
27-
28-
def compile_dependency_hash(dependencies) do
29-
dependencies
30-
|> Enum.map(fn {{module, _func}, _arity} ->
31-
module.__info__(:attributes)[:vsn]
32-
end)
33-
|> :erlang.phash2()
34-
end
35-
3627
# ----------------------------------------------------------------------------
3728
# Exctract function calls from ast
3829

30+
@spec extract_function_calls(tuple(), Macro.Env.t()) :: [{{atom(), atom()}, non_neg_integer()}]
3931
def extract_function_calls(ast, env) do
4032
state = %State{env: env}
4133
# IO.inspect(env)
@@ -52,6 +44,7 @@ defmodule ExExample.Analyze do
5244

5345
# qualified function call
5446
# e.g., Foo.bar()
47+
5548
defp extract_function_call(
5649
{{:., _, [{:__aliases__, _, aliases}, func_name]}, _, args} = ast,
5750
state
@@ -70,6 +63,10 @@ defmodule ExExample.Analyze do
7063
end
7164
end
7265

66+
defp extract_function_call({{:., _, _args}, _, _} = ast, state) do
67+
{ast, state}
68+
end
69+
7370
# variable in binding
7471
# e.g. `x` in `x = 1`
7572
defp extract_function_call({_func, _, nil} = ast, state) do

lib/ex_example/behaviour.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
defmodule ExExample.Behaviour do
2+
@moduledoc """
3+
I help determine when Examples ought to be run again or be copied
4+
5+
6+
I do this by defining out a behaviour that is to be used with the
7+
use macro for ExExample
8+
"""
9+
10+
@callback rerun?(any()) :: boolean()
11+
@callback copy(any()) :: any()
12+
end

lib/ex_example/cache.ex

Whitespace-only changes.

lib/ex_example/cache/cache.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ defmodule ExExample.Cache do
1010

1111
@cache_name __MODULE__
1212

13+
@doc """
14+
I clear the entire cache.
15+
"""
16+
@spec clear() :: :ok
17+
def clear do
18+
Cachex.clear!(@cache_name)
19+
:ok
20+
end
21+
1322
@doc """
1423
I store a result in cache for a given key.
1524
"""
@@ -22,7 +31,7 @@ defmodule ExExample.Cache do
2231
I fetch a previous Result from the cache if it exists.
2332
If it does not exist, I return `{:error, :not_found}`.
2433
"""
25-
@spec get_result(Key.t()) :: {:ok, any()} | {:error, :no_result}
34+
@spec get_result(Key.t()) :: {:ok, Result.t()} | {:error, :no_result}
2635
def get_result(%Key{} = key) do
2736
case Cachex.get(@cache_name, key) do
2837
{:ok, nil} ->

lib/ex_example/cache/result.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ defmodule ExExample.Cache.Result do
1515
field(:key, Key.t())
1616
field(:success, :failed | :success | :skipped)
1717
field(:result, term())
18+
field(:cached, boolean(), default: true)
1819
end
1920
end

0 commit comments

Comments
 (0)