Current section
Files
Jump to
Current section
Files
lib/mecks_unit.ex
defmodule MecksUnit do
@moduledoc false
require :elixir_module
def mock do
functions =
test_file_patterns()
|> Enum.map(fn pattern ->
pattern
|> Path.wildcard()
|> Enum.map(&extract_mock_functions/1)
end)
|> List.flatten()
|> Enum.uniq()
MecksUnit.Unloader.register()
MecksUnit.Server.register_mocked(functions)
mock_functions(functions)
end
defp test_file_patterns do
System.argv()
|> Enum.slice(1..-1)
|> Enum.filter(fn string ->
String.contains?(string, ".exs") or
Path.wildcard(string <> "/**/*.exs") != []
end)
|> case do
[] -> ["test/**/*.exs"]
patterns -> patterns
end
|> Enum.map(fn pattern ->
pattern = Regex.replace(~r/\.exs:\d*/, pattern, ".exs")
if String.contains?(pattern, ".exs") do
pattern
else
pattern <> "**/*.exs"
end
end)
end
defp extract_mock_functions(file) do
extract_functions = fn node, name, block, acc ->
mocked_functions =
name
|> Module.concat()
|> extract_function_heads(block)
{node, acc ++ mocked_functions}
end
file
|> File.read!()
|> Code.string_to_quoted!()
|> Macro.traverse([], fn node, acc -> {node, acc} end, fn node, acc ->
case node do
{:defmock, _, [{:__aliases__, _meta, name}, [do: block]]} ->
extract_functions.(node, name, block, acc)
{:defmock, _,
[{:__aliases__, _meta, name}, [preserve: true], [do: block]]} ->
extract_functions.(node, name, block, acc)
node ->
{node, acc}
end
end)
|> elem(1)
end
defp extract_function_heads(module, {:__block__, [], ast}) do
extract_function_heads(module, ast)
end
defp extract_function_heads(module, ast) do
ast
|> List.wrap()
|> Enum.map(fn {:def, _, [{func, _meta, args} | _tail]} ->
{module, func, args |> List.wrap() |> length()}
end)
end
defp mock_functions(functions) do
Enum.each(functions, fn {module, func, arity} ->
mock_function = to_mock_function(module, func, arity)
:meck.expect(module, func, mock_function)
end)
end
defp to_mock_function(module, func, arity) do
arguments = Macro.generate_arguments(arity, :"Elixir")
module =
case module do
[_head | _tail] = module -> module
module -> [module]
end
quote do
fn unquote_splicing(arguments) ->
mock_env = MecksUnit.Server.running(self())
mock_module = Module.concat([mock_env, unquote_splicing(module)])
arguments = [unquote_splicing(arguments)]
passthrough = fn arguments ->
:meck.passthrough(arguments)
end
if mock_env &&
function_exported?(mock_module, unquote(func), unquote(arity)) do
try do
case apply(mock_module, unquote(func), arguments) do
{:passthrough, arguments} -> passthrough.(arguments)
:passthrough -> passthrough.(arguments)
returned -> returned
end
rescue
FunctionClauseError ->
passthrough.(arguments)
end
else
passthrough.(arguments)
end
end
end
|> Code.eval_quoted()
|> elem(0)
end
def define_mocks(mocks, test_module, mock_index, caller) do
prefix = [Atom.to_string(test_module), mock_index, "."] |> Enum.join()
mocks
|> Enum.map(fn {mock_module, block} ->
mock_module =
mock_module
|> Atom.to_string()
|> String.replace("__INDEX__", Integer.to_string(mock_index))
|> String.to_atom()
{mock_module, block}
end)
|> Keyword.new()
|> Enum.each(fn {mock_module, block} ->
original_module =
mock_module
|> Atom.to_string()
|> String.replace(prefix, "Elixir.")
|> String.to_atom()
if function_exported?(mock_module, :__info__, 1) do
IO.warn("Already defined mock module for #{original_module}")
else
block = interpolate_modules_attributes(block, caller)
Code.eval_quoted(
{:defmodule, [import: Kernel], [mock_module, [do: block]]}
)
end
end)
end
defp interpolate_modules_attributes(ast, caller) do
table =
if function_exported?(:elixir_module, :data_table, 1) do
apply(:elixir_module, :data_table, [caller])
else
{set, _bag} = apply(:elixir_module, :data_tables, [caller])
set
end
Macro.postwalk(ast, fn
{:@, _, [{key, _, nil}]} ->
[^key, val] =
table
|> :ets.lookup(key)
|> hd()
|> Tuple.to_list()
|> Enum.slice(0, 2)
quote do
unquote(val)
end
quoted ->
quoted
end)
end
def called(module, func, arguments) do
module
|> history(func)
|> Enum.any?(fn {_pid, {_module, _func, args}, _returned} ->
if length(arguments) == length(args) do
arguments
|> Enum.with_index()
|> Enum.all?(fn {arg, index} ->
arg == :_ || Enum.at(args, index) == arg
end)
end
end)
end
def history(module, func) do
pid = self()
module
|> :meck.history()
|> Enum.reduce([], fn
{^pid, {^module, ^func, _args}, _returned} = call, calls ->
calls ++ [call]
_, calls ->
calls
end)
end
end