-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
621 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file was deleted.
Oops, something went wrong.
Empty file.
Oops, something went wrong.