@@ -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
0 commit comments