diff --git a/README.md b/README.md index aa2cd07..b381705 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,15 @@ Parameter types and return types are not checked. Hammox now includes telemetry events! See [Telemetry Guide](https://hexdocs.pm/hammox/Telemetry.html) for more information. +## Configuration + +Hammox includes experimental pretty printing of error messages. +To enable it add to `config/test.exs` + +```elixir +config :hammox, pretty: true +``` + ## License Copyright 2019 MichaƂ Szewczak diff --git a/lib/hammox/type_match_error.ex b/lib/hammox/type_match_error.ex index ba72f96..6fc2fbc 100644 --- a/lib/hammox/type_match_error.ex +++ b/lib/hammox/type_match_error.ex @@ -14,20 +14,36 @@ defmodule Hammox.TypeMatchError do } end + defp message_string(reasons) when is_list(reasons) do + reasons + |> Enum.with_index() + |> Enum.map_join("\n\n", fn {reason, index} -> + padding = put_padding(index) + + reason + |> human_reason() + |> String.replace_prefix("", padding) + end) + end + + defp message_string(reason) when is_tuple(reason) do + message_string([reason]) + end + defp human_reason({:arg_type_mismatch, index, value, type}) do - "#{Ordinal.ordinalize(index + 1)} argument value #{inspect(value)} does not match #{Ordinal.ordinalize(index + 1)} parameter's type #{type_to_string(type)}." + "#{Ordinal.ordinalize(index + 1)} argument value #{custom_inspect(value)} does not match #{Ordinal.ordinalize(index + 1)} parameter's type #{type_to_string(type)}." end defp human_reason({:return_type_mismatch, value, type}) do - "Returned value #{inspect(value)} does not match type #{type_to_string(type)}." + "Returned value #{custom_inspect(value)} does not match type #{type_to_string(type)}." end defp human_reason({:tuple_elem_type_mismatch, index, elem, elem_type}) do - "#{Ordinal.ordinalize(index + 1)} tuple element #{inspect(elem)} does not match #{Ordinal.ordinalize(index + 1)} element type #{type_to_string(elem_type)}." + "#{Ordinal.ordinalize(index + 1)} tuple element #{custom_inspect(elem)} does not match #{Ordinal.ordinalize(index + 1)} element type #{type_to_string(elem_type)}." end defp human_reason({:elem_type_mismatch, index, elem, elem_type}) do - "Element #{inspect(elem)} at index #{index} does not match element type #{type_to_string(elem_type)}." + "Element #{custom_inspect(elem)} at index #{index} does not match element type #{type_to_string(elem_type)}." end defp human_reason({:empty_list_type_mismatch, type}) do @@ -43,7 +59,7 @@ defmodule Hammox.TypeMatchError do end defp human_reason({:improper_list_terminator_type_mismatch, terminator, terminator_type}) do - "Improper list terminator #{inspect(terminator)} does not match terminator type #{type_to_string(terminator_type)}." + "Improper list terminator #{custom_inspect(terminator)} does not match terminator type #{type_to_string(terminator_type)}." end defp human_reason({:function_arity_type_mismatch, expected, actual}) do @@ -51,24 +67,24 @@ defmodule Hammox.TypeMatchError do end defp human_reason({:type_mismatch, value, type}) do - "Value #{inspect(value)} does not match type #{type_to_string(type)}." + "Value #{custom_inspect(value)} does not match type #{type_to_string(type)}." end defp human_reason({:map_key_type_mismatch, key, key_types}) when is_list(key_types) do - "Map key #{inspect(key)} does not match any of the allowed map key types #{key_types |> Enum.map_join(", ", &type_to_string/1)}." + "Map key #{custom_inspect(key)} does not match any of the allowed map key types #{key_types |> Enum.map_join(", ", &type_to_string/1)}." end defp human_reason({:map_key_type_mismatch, key, key_type}) do - "Map key #{inspect(key)} does not match map key type #{type_to_string(key_type)}." + "Map key #{custom_inspect(key)} does not match map key type #{type_to_string(key_type)}." end defp human_reason({:map_value_type_mismatch, key, value, value_types}) when is_list(value_types) do - "Map value #{inspect(value)} for key #{inspect(key)} does not match any of the allowed map value types #{value_types |> Enum.map_join(", ", &type_to_string/1)}." + "Map value #{custom_inspect(value)} for key #{inspect(key)} does not match any of the allowed map value types #{value_types |> Enum.map_join(", ", &type_to_string/1)}." end defp human_reason({:map_value_type_mismatch, key, value, value_type}) do - "Map value #{inspect(value)} for key #{inspect(key)} does not match map value type #{type_to_string(value_type)}." + "Map value #{custom_inspect(value)} for key #{inspect(key)} does not match map value type #{type_to_string(value_type)}." end defp human_reason({:required_field_unfulfilled_map_type_mismatch, entry_type}) do @@ -92,30 +108,7 @@ defmodule Hammox.TypeMatchError do end defp human_reason({:protocol_type_mismatch, value, protocol_name}) do - "Value #{inspect(value)} does not implement the #{protocol_name} protocol." - end - - defp message_string(reasons) when is_list(reasons) do - reasons - |> Enum.zip(0..length(reasons)) - |> Enum.map_join("\n", fn {reason, index} -> - reason - |> human_reason() - |> leftpad(index) - end) - end - - defp message_string(reason) when is_tuple(reason) do - message_string([reason]) - end - - defp leftpad(string, level) do - padding = - for(_ <- 0..level, do: " ") - |> Enum.drop(1) - |> Enum.join() - - padding <> string + "Value #{custom_inspect(value)} does not implement the #{protocol_name} protocol." end defp type_to_string({:type, _, :map_field_exact, [type1, type2]}) do @@ -136,8 +129,52 @@ defmodule Hammox.TypeMatchError do |> Enum.map_join(&String.replace(&1, ~r/ +/, " ")) |> String.split(" :: ") |> case do - [_, type_string] -> type_string + [_, type_string] -> format_multiple(type_string) [_, type_name, type_string] -> "#{type_string} (\"#{type_name}\")" end end + + defp format_multiple(type_string) do + if pretty_print() do + padding = get_padding() + + type_string + |> String.replace(" | ", "\n" <> padding <> " | ") + |> String.replace_prefix("", "\n" <> padding) + else + type_string + end + end + + defp custom_inspect(value) do + if pretty_print() do + padding = get_padding() + + value + |> inspect(limit: :infinity, printable_limit: 500, pretty: true) + |> String.replace("\n", "\n" <> padding) + else + inspect(value) + end + end + + defp put_padding(level) when is_integer(level) do + for(_ <- 0..level, do: " ") + |> Enum.drop(1) + |> Enum.join() + |> put_padding + end + + defp put_padding(padding) do + Process.put(:padding, padding) + padding + end + + defp get_padding() do + Process.get(:padding) + end + + defp pretty_print do + Application.get_env(:hammox, :pretty) + end end diff --git a/test/hammox/type_match_error_test.exs b/test/hammox/type_match_error_test.exs new file mode 100644 index 0000000..0c386dd --- /dev/null +++ b/test/hammox/type_match_error_test.exs @@ -0,0 +1,96 @@ +defmodule Hammox.TypeMatchErrorTest do + use ExUnit.Case, async: true + + alias Hammox.TypeMatchError + + describe "standard error" do + test "reason" do + error = TypeMatchError.exception({:error, reason()}) + + assert error.message == + """ + + Returned value {:ok, %{__struct__: Post, body: "post body", post_body: "nil"}} does not match type {:ok, Post.t()} | {:error, any()}. + + Value {:ok, %{__struct__: Post, body: "post body", post_body: "nil"}} does not match type {:ok, Post.t()} | {:error, any()}. + + 1st tuple element :ok does not match 1st element type :error. + + Value :ok does not match type :error. + """ + |> String.replace_trailing("\n", "") + end + end + + describe "pretty error" do + setup do + Application.put_env(:hammox, :pretty, true) + + on_exit(fn -> + Application.delete_env(:hammox, :pretty) + end) + end + + test "reason" do + error = TypeMatchError.exception({:error, reason()}) + + assert error.message == + """ + + Returned value {:ok, %{__struct__: Post, body: \"post body\", post_body: \"nil\"}} does not match type + {:ok, Post.t()} + | {:error, any()}. + + Value {:ok, %{__struct__: Post, body: \"post body\", post_body: \"nil\"}} does not match type + {:ok, Post.t()} + | {:error, any()}. + + 1st tuple element :ok does not match 1st element type + :error. + + Value :ok does not match type + :error. + """ + |> String.replace_trailing("\n", "") + end + end + + defp reason do + [ + {:return_type_mismatch, + {:ok, + %{ + __struct__: Post, + body: "post body", + post_body: "nil" + }}, + {:type, 49, :union, + [ + {:type, 0, :tuple, + [ + {:atom, 0, :ok}, + {:remote_type, 49, [{:atom, 0, Post}, {:atom, 0, :t}, []]} + ]}, + {:type, 0, :tuple, [{:atom, 0, :error}, {:type, 49, :any, []}]} + ]}}, + {:type_mismatch, + {:ok, + %{ + __struct__: Post, + body: "post body", + post_body: "nil" + }}, + {:type, 49, :union, + [ + {:type, 0, :tuple, + [ + {:atom, 0, :ok}, + {:remote_type, 49, [{:atom, 0, Post}, {:atom, 0, :t}, []]} + ]}, + {:type, 0, :tuple, [{:atom, 0, :error}, {:type, 49, :any, []}]} + ]}}, + {:tuple_elem_type_mismatch, 0, :ok, {:atom, 0, :error}}, + {:type_mismatch, :ok, {:atom, 0, :error}} + ] + end +end