defmodule AssertValue do # Assertions with right argument like "assert_value actual == expected" defmacro assert_value({:==, _, [left, right]} = assertion) do {expected_type, expected_file} = case right do {{:., _, [{:__aliases__, _, [:File]}, :read!]}, _, [filename]} -> {:file, filename} str when is_binary(str) -> {:string, nil} _ -> {:other, nil} end quote do assertion_ast = unquote(Macro.escape(assertion)) actual_ast = unquote(Macro.escape(left)) actual_value = unquote(left) expected_type = unquote(expected_type) expected_file = unquote(expected_file) expected_ast = unquote(Macro.escape(right)) expected_value = case expected_type do :string -> unquote(right) |> String.replace(~r/\n\Z/, "", global: false) # TODO: should deal with a no-bang File.read instead, may # want to deal with different errors differently :file -> File.exists?(expected_file) && File.read!(expected_file) || "" :other -> unquote(right) end check_serializable(actual_value) check_string_and_file_read(actual_value, expected_type) # We need to check for reformat_expected? first to disable # "this check/guard will always yield the same result" warnings if AssertValue.Server.reformat_expected? || (actual_value != expected_value) do decision = AssertValue.Server.ask_user_about_diff( caller: [ file: unquote(__CALLER__.file), line: unquote(__CALLER__.line), function: unquote(__CALLER__.function), ], assertion_ast: assertion_ast, actual_ast: actual_ast, actual_value: actual_value, expected_type: expected_type, expected_ast: expected_ast, expected_value: expected_value, expected_file: expected_file) case decision do {:ok, value} -> true {:error, :ex_unit_assertion_error, error_attrs} -> raise ExUnit.AssertionError, error_attrs {:error, :parse_error} -> # raise ParseError in test instead of genserver # to show readable error message and stacktrace raise AssertValue.Parser.ParseError end else true end end end # Assertions without right argument like (assert_value "foo") defmacro assert_value(assertion) do quote do assertion_ast = unquote(Macro.escape(assertion)) actual_value = unquote(assertion) check_serializable(actual_value) decision = AssertValue.Server.ask_user_about_diff( caller: [ file: unquote(__CALLER__.file), line: unquote(__CALLER__.line), function: unquote(__CALLER__.function), ], assertion_ast: assertion_ast, # :_not_present_ is to show the difference between # nil and actually not present actual/expected actual_ast: :_not_present_, actual_value: actual_value, expected_type: :source, expected_ast: :_not_present_) case decision do {:ok, value} -> true {:error, :ex_unit_assertion_error, error_attrs} -> raise ExUnit.AssertionError, error_attrs {:error, :parse_error} -> # raise ParseError in test instead of genserver # to show readable error message and stacktrace raise AssertValue.Parser.ParseError end end end defmodule ArgumentError do defexception [:message] end def check_serializable(arg) when is_pid(arg) when is_port(arg) when is_reference(arg) when is_function(arg) do raise AssertValue.ArgumentError, message: """ Unable to serialize #{get_value_type(arg)} You might want to use inspect/1 to use it in assert_value """ end def check_serializable(_), do: :ok def check_string_and_file_read(actual_value, _expected_type = :file) when is_binary(actual_value), do: :ok def check_string_and_file_read(actual_value, _expected_type = :file) do raise AssertValue.ArgumentError, message: """ Unable to compare #{get_value_type(actual_value)} with File.read! File.read! always return binary result and requires left argument in assert_value to be binary. You might want to use to_string/1 or inspect/1 to compare other types with File.read! assert_value to_string(:foo) == File.read!("foo.log") assert_value inspect(:foo) == File.read!("foo.log") """ end def check_string_and_file_read(_, _), do: :ok defp get_value_type(arg) do [h | _t] = IEx.Info.info(arg) case h do # Elixir < 1.6 {:"Data type", type} -> type # Elixir 1.6 {"Data type", type} -> type # else CaseClauseError end end end