Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better error formatting #132

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
107 changes: 72 additions & 35 deletions lib/hammox/type_match_error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,32 +59,32 @@ 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
"Expected function to have arity #{expected} but got #{actual}."
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
Expand All @@ -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
Expand All @@ -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
96 changes: 96 additions & 0 deletions test/hammox/type_match_error_test.exs
Original file line number Diff line number Diff line change
@@ -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