defmodule ExUnitAssertions do @doc """ Matches pattern in list: > assert_match_in?(1, [1, 2, 3]) true > assert_match_in?(%{a: 1}, [%{a: 1, b: 2}, %{a: 2, b: 3}]) true > assert_match_in?(%{a: 1}, [%{a: 2, b: 2}, %{a: 2, b: 3}]) false > a = 1 > assert_match_in?(%{a: ^a}, [%{a: 1, b: 2}, %{a: 2, b: 3}]) true > assert_match_in?(%{a: 1, b: b}, [%{a: 1, b: 2}, %{a: 2, b: 3}]) true > b 2 """ @spec assert_match_in?(term, list(term)) :: true | false defmacro assert_match_in?(pattern, expr) do left = Macro.expand(pattern, __CALLER__) vars = collect_vars_from_pattern(left) pins = collect_pins_from_pattern(left) quote do left = unquote(Macro.escape(left)) expr = unquote(expr) matching = Enum.reduce(expr, nil, fn (element, nil) -> case element do unquote(pattern) -> _ = unquote(vars) element _ -> nil end (element, acc) -> acc end) if is_nil(matching) do raise ExUnit.AssertionError, right: expr, expr: left, message: "match_in? failed" <> ExUnit.Assertions.__pins__(unquote(pins)) end assert unquote(pattern) = matching matching end end defp collect_vars_from_pattern({:when, _, [left, right]}) do pattern = collect_vars_from_pattern(left) for {name, _, context} = var <- collect_vars_from_pattern(right), Enum.any?(pattern, &match?({^name, _, ^context}, &1)), into: pattern, do: var end defp collect_vars_from_pattern(expr) do Macro.prewalk(expr, [], fn {:::, _, [left, _]}, acc -> {[left], acc} {skip, _, [_]}, acc when skip in [:^, :@] -> {:ok, acc} {:_, _, context}, acc when is_atom(context) -> {:ok, acc} {name, meta, context}, acc when is_atom(name) and is_atom(context) -> {:ok, [{name, [generated: true] ++ meta, context} | acc]} node, acc -> {node, acc} end) |> elem(1) end defp collect_pins_from_pattern(expr) do {_, pins} = Macro.prewalk(expr, [], fn {:^, _, [{name, _, _} = var]}, acc -> {:ok, [{name, var} | acc]} form, acc -> {form, acc} end) Enum.uniq_by(pins, &elem(&1, 0)) end end