msz / hammox Goto Github PK
View Code? Open in Web Editor NEW๐ automated contract testing via type checking for Elixir functions and mocks
License: Apache License 2.0
๐ automated contract testing via type checking for Elixir functions and mocks
License: Apache License 2.0
As I was reading through the code, I noticed that while expect
wraps the replacement implementation with protected
, stub
(and stub_with
) do not.
Is there a reason for this decision? Would you consider a PR that wraps stub
's code with protected
?
I'm thinking of an implementation like this:
def stub(mock, name, code) do
arity = :erlang.fun_info(code)[:arity]
hammox_code =
case fetch_typespecs_for_mock(mock, name, arity) do
# This is really an error case where we're trying to mock a function
# that does not exist in the behaviour. Mox will flag it better though
# so just let it pass through.
[] -> code
typespecs -> protected(code, typespecs, arity)
end
Mox.stub(mock, name, hammox_code)
end
I'm not sure what a protected implementation for stub_with
would look like, though, so ideas are welcome there.
Thanks for this awesome library!
Ran into the following bug when a "complex" (see below) type was being rendered in Hammox.
The "complex" type in question is a parameter defined type:
@type car(wheel_type) :: %{
# ...
:wheels => [
wheel_type,
...
],
# ...
}
This resulted in the following error in Hammox:
** (CaseClauseError) no case clause matching: ["foo() ::\n Garage.car(\n Wheels.chrome_rims()\n )"
Which occurred with the following stack trace:
(hammox 0.5.0) lib/hammox/type_match_error.ex:157: Hammox.TypeMatchError.type_to_string/1
(hammox 0.5.0) lib/hammox/type_match_error.ex:20: Hammox.TypeMatchError.human_reason/1
(hammox 0.5.0) lib/hammox/type_match_error.ex:123: anonymous fn/1 in Hammox.TypeMatchError.message_string/1
(elixir 1.13.1) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
(hammox 0.5.0) lib/hammox/type_match_error.ex:121: Hammox.TypeMatchError.message_string/1
(hammox 0.5.0) lib/hammox/type_match_error.ex:13: Hammox.TypeMatchError.exception/1
(hammox 0.5.0) lib/hammox.ex:355: Hammox.check_call/3
(hammox 0.5.0) lib/hammox.ex:338: Hammox.protected_code/3
The library is very useful. But I think that there is a lot of boilerplate to write when setting a new module/area. You need the behaviour, implementation, mock and changes to config - it would be cool to have an official generator for that does the boilerplate for you.
It would be great if hammox reports the behaviour and callback which was used to verify values.
Modules are set in runtime, so it takes time to find the behaviour. This is not critical if you write the code which fails, so you know the context, but the situation is the different when you add a library to the existing project and have to fix a lot of errors.
Some Elixir libraries have references to erlang types, like Erlang records.
For example, the OpenTelemetry library has start_opts
type:
https://hexdocs.pm/opentelemetry_api/OpenTelemetry.Tracer.html#t:start_opts/0
If you try to reference it and use Hammox protect, it raises an exception like this:
The following arguments were given to Hammox.TypeEngine.match_type/2:
# 1
{:some_attribute, "somevallue"}
# 2
{:type, 116, :record, [{:atom, 116, :link}]}
Attempted function clauses (showing 10 out of 92):
def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types)
def match_type(_value, {:type, _, :any, []})
def match_type(value, {:type, _, :none, []} = type)
def match_type(value, {:type, _, :atom, []}) when is_atom(value)
def match_type(value, {:type, _, :atom, []} = type)
def match_type(value, {:type, _, :map, :any}) when is_map(value)
def match_type(value, {:type, _, :pid, []}) when is_pid(value)
def match_type(value, {:type, _, :pid, []} = type)
def match_type(value, {:type, _, :port, []}) when is_port(value)
def match_type(value, {:type, _, :port, []} = type)
It would be very cool if Hammox could verify it.
The following code:
defmodule Result do
@type ok(any) :: {:ok, any}
end
defmodule HammoxPlayground do
@type t :: String.t()
@callback perform(Decimal.t()) :: Result.ok(t())
end
Results in the following TypeMatchError
for the correct types:
** (Hammox.TypeMatchError)
Returned value {:ok, "hello"} does not match type Result.ok(t()).
2nd tuple element "hello" does not match 2nd element type Result.t().
Could not find type t/0 in Result.
code: HammoxPlayground.Mock.perform(Decimal.new("0.992"))
stacktrace:
(hammox) lib/hammox.ex:273: Hammox.check_call/3
(hammox) lib/hammox.ex:256: Hammox.protected_code/3
test/hammox_playground_test.exs:13: (test)
As t()
is not qualified in Result.ok(t())
Hammox interprets the type as Result.t()
, but it is HammoxPlayground .t()
. Change to Result.ok(__MODULE__.t())
works, but it seems that it should work unqualified.
Right now Hammox is treating @opaque
just like a public type, which means if matching fails it will show a stacktrace going into the internals of the type. We should modify this so that the stacktrace stops at the opaque type.
In some setups behaviours and default implementations are put together in one module. In such cases, the call to protect/3
is
Hammox.protect(SomeModule, SomeModule, foo: 1)
We can add a shorthand version
Hammox.protect(SomeModule, foo: 1)
which will expand to that.
A behaviour with a spec containing the type Ecto.Changeset.t()
will raise a FunctionClauseError
:
** (FunctionClauseError) no function clause matching in Hammox.TypeEngine.match_type/2
The following arguments were given to Hammox.TypeEngine.match_type/2:
# 1
nil
# 2
{:var, 294, :data_type}
Attempted function clauses (showing 10 out of 91):
def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types)
def match_type(_value, {:type, _, :any, []})
def match_type(value, {:type, _, :none, []} = type)
def match_type(value, {:type, _, :atom, []}) when is_atom(value)
def match_type(value, {:type, _, :atom, []} = type)
def match_type(value, {:type, _, :map, :any}) when is_map(value)
def match_type(value, {:type, _, :pid, []}) when is_pid(value)
def match_type(value, {:type, _, :pid, []} = type)
def match_type(value, {:type, _, :port, []}) when is_port(value)
def match_type(value, {:type, _, :port, []} = type)
code: HammoxPlayground.Mock.perform(Decimal.new("0.992"))
stacktrace:
(hammox) lib/hammox/type_engine.ex:6: Hammox.TypeEngine.match_type/2
(hammox) lib/hammox/type_engine.ex:372: anonymous fn/3 in Hammox.TypeEngine.match_type/2
(elixir) lib/enum.ex:1340: anonymous fn/3 in Enum.map/2
(stdlib) maps.erl:232: :maps.fold_1/3
(elixir) lib/enum.ex:1964: Enum.map/2
(hammox) lib/hammox/type_engine.ex:371: anonymous fn/2 in Hammox.TypeEngine.match_type/2
(elixir) lib/enum.ex:3358: Enumerable.Map.reduce_list/3
(elixir) lib/enum.ex:1998: Enum.reduce_while/3
(hammox) lib/hammox/type_engine.ex:369: Hammox.TypeEngine.match_type/2
(hammox) lib/hammox.ex:312: Hammox.match_return_value/2
(hammox) lib/hammox.ex:280: Hammox.match_call/3
(hammox) lib/hammox.ex:264: anonymous fn/4 in Hammox.check_call/3
(elixir) lib/enum.ex:3325: Enumerable.List.reduce/3
(elixir) lib/enum.ex:1998: Enum.reduce_while/3
(hammox) lib/hammox.ex:263: Hammox.check_call/3
(hammox) lib/hammox.ex:256: Hammox.protected_code/3
test/hammox_playground_test.exs:13: (test)
Suppose I have a GenServer that calls a callback module implementing some behaviour.
I want to test that my GenServer does the right thing when the module does not return expected values. But hammox will not let me badly implement a behaviour.
Is there a way to opt-out type checking for a mock ?
** (MatchError) no match of right hand side value: :error
stacktrace:
(hammox) lib/hammox.ex:336: Hammox.fetch_typespecs/3
(hammox) lib/hammox.ex:322: Hammox.fetch_typespecs!/3
(hammox) lib/hammox.ex:121: Hammox.protect/2
(hammox) lib/hammox.ex:178: anonymous fn/4 in Hammox.protect/3
(elixir) lib/enum.ex:1336: Enum."-map/2-lists^map/1-0-"/2
(elixir) lib/enum.ex:2994: Enum.flat_map_list/2
(hammox) lib/hammox.ex:168: Hammox.protect/3
Should show a good explanation what happened instead.
This is to make it actually backwards compatible with Mox which requires Elixir ~1.5
.
Areas to look at:
@doc
keyword tagsCode.Typespec
directly into the projectI'm using the latest commit on the master
branch.
I have a function in a behaviour that returns an Ecto Changeset. If I attempt to wrap this function with Hammox.protect
while testing it, I get the following error message and stack trace. I've reproduced this error in a separate repo that I can publish somewhere if it would be helpful; important extracts are below.
I'm happy to try to help track this down, but I'm not sure of the best way to do that. Tips welcome!
** (CaseClauseError) no case clause matching: [{:type, 441, :map_field_exact, [{:atom, 0, :__struct__}, {:type, 441, :atom, []}]}]
stacktrace:
(hammox 0.2.5) lib/hammox/type_engine.ex:346: Hammox.TypeEngine.match_type/2
(hammox 0.2.5) lib/hammox/type_engine.ex:12: anonymous fn/3 in Hammox.TypeEngine.match_type/2
(elixir 1.10.0) lib/enum.ex:3686: Enumerable.List.reduce/3
(elixir 1.10.0) lib/enum.ex:2161: Enum.reduce_while/3
(hammox 0.2.5) lib/hammox/type_engine.ex:11: Hammox.TypeEngine.match_type/2
(hammox 0.2.5) lib/hammox/type_engine.ex:12: anonymous fn/3 in Hammox.TypeEngine.match_type/2
(elixir 1.10.0) lib/enum.ex:3686: Enumerable.List.reduce/3
(elixir 1.10.0) lib/enum.ex:2161: Enum.reduce_while/3
(hammox 0.2.5) lib/hammox/type_engine.ex:11: Hammox.TypeEngine.match_type/2
(hammox 0.2.5) lib/hammox/type_engine.ex:375: anonymous fn/3 in Hammox.TypeEngine.match_type/2
(elixir 1.10.0) lib/enum.ex:1400: anonymous fn/3 in Enum.map/2
(stdlib 3.11.1) maps.erl:232: :maps.fold_1/3
(elixir 1.10.0) lib/enum.ex:2127: Enum.map/2
(hammox 0.2.5) lib/hammox/type_engine.ex:374: anonymous fn/2 in Hammox.TypeEngine.match_type/2
(elixir 1.10.0) lib/enum.ex:3686: Enumerable.List.reduce/3
(elixir 1.10.0) lib/enum.ex:2161: Enum.reduce_while/3
(hammox 0.2.5) lib/hammox/type_engine.ex:372: Hammox.TypeEngine.match_type/2
(hammox 0.2.5) lib/hammox.ex:359: Hammox.match_return_value/2
(hammox 0.2.5) lib/hammox.ex:327: Hammox.match_call/3
(hammox 0.2.5) lib/hammox.ex:311: anonymous fn/4 in Hammox.check_call/3
Here are the behavior and implementation:
defmodule HammoxTest.Friends.Impl do
alias HammoxTest.Friends.Person
@callback change_person(Person.t(), map()) :: Ecto.Changeset.t()
end
defmodule HammoxTest.Friends do
alias HammoxTest.Friends.{Impl, Person}
@behaviour Impl
@impl Impl
def change_person(%Person{} = person, attrs \\ %{}) do
person
|> Person.changeset(attrs)
end
end
defmodule HammoxTest.Friends.Person do
use Ecto.Schema
@type t :: %__MODULE__{
first_name: String.t() | nil,
last_name: String.t() | nil,
age: integer() | nil
}
schema "people" do
field(:first_name, :string)
field(:last_name, :string)
field(:age, :integer)
end
def changeset(person, attrs \\ %{}) do
person
|> Ecto.Changeset.cast(attrs, [:first_name, :last_name, :age])
|> Ecto.Changeset.validate_required([:first_name, :last_name])
end
end
And here is the test:
defmodule HammoxTest.FriendsTest do
use ExUnit.Case
import Hammox
alias HammoxTest.Friends
alias HammoxTest.Friends.{Impl, Person}
test "can I type-check a changeset?" do
change_person_2 = protect({Friends, :change_person, 2}, Impl)
changeset = change_person_2.(%Person{}, %{age: 42})
assert changeset.valid?
end
end
From Ecto.Changeset
, there is a t()
defined such as:
@type t(data_type) :: %Changeset{valid?: boolean(),
...
data: data_type,
...
types: nil | %{atom => Ecto.Type.t}}
In our behaviour, we have something like:
@type user_changeset :: Ecto.Changeset.t(User.t())
@callback password_operation(user_changeset) :: user_changeset
@callback password_operation(user_changeset, attrs :: map) :: user_changeset
In our implementation:
@type user_changeset :: Ecto.Changeset.t(User.t())
@impl CredentialsHelper
@spec password_operation(user_changeset) :: user_changeset
@spec password_operation(user_changeset, map) :: user_changeset
def password_operation(cs, attrs \\ %{}),
do: ...
When trying to stub
or expect
, Hammox raises:
** (FunctionClauseError) no function clause matching in Hammox.TypeEngine.match_type/2
The following arguments were given to Hammox.TypeEngine.match_type/2:
# 1
nil
# 2
{:var, 282, :data_type}
Note: we tried every possible combination of typing here - directly Ecto.Changeset.t
, Ecto.Changeset.t(any)
etc. Only replacing the changeset
type by any
on the behaviour seems to fix the problem.
this doesn't work
@type event :: {event_name :: atom(), topics :: [String.t()]}
but this works
@type event_name :: atom()
@type topics :: [String.t()]
@type event :: {event_name(), topics()}
This code in type_engine.ex accepts only @type
:
defp get_type(type_list, type_name, arity) do
case Enum.find(type_list, fn {:type, {name, _type, params}} ->
name == type_name and length(params) == arity
end) do
nil -> {:error, {:type_not_found, {type_name, arity}}}
type -> {:ok, type}
end
end
However, Typespecs accepts @type
, @typep
and @opaque
(source). This causes hammox to break:
** (FunctionClauseError) no function clause matching in anonymous fn/1 in Hammox.TypeEngine.get_type/3
The following arguments were given to anonymous fn/1 in Hammox.TypeEngine.get_type/3:
# 1
{:typep, {:names, {:remote_type, 133, [{:atom, 0, MapSet}, {:atom, 0, :t}, []]}, []}}
stacktrace:
(hammox 0.2.4) lib/hammox/type_engine.ex:695: anonymous fn/1 in Hammox.TypeEngine.get_type/3
(elixir 1.10.2) lib/enum.ex:3305: Enum.find_list/3
(hammox 0.2.4) lib/hammox/type_engine.ex:695: Hammox.TypeEngine.get_type/3
(hammox 0.2.4) lib/hammox/type_engine.ex:656: Hammox.TypeEngine.resolve_remote_type/1
(hammox 0.2.4) lib/hammox/type_engine.ex:621: Hammox.TypeEngine.match_type/2
(hammox 0.2.4) lib/hammox.ex:312: Hammox.match_return_value/2
(hammox 0.2.4) lib/hammox.ex:280: Hammox.match_call/3
(hammox 0.2.4) lib/hammox.ex:264: anonymous fn/4 in Hammox.check_call/3
(elixir 1.10.2) lib/enum.ex:3686: Enumerable.List.reduce/3
(elixir 1.10.2) lib/enum.ex:2161: Enum.reduce_while/3
(hammox 0.2.4) lib/hammox.ex:263: Hammox.check_call/3
(hammox 0.2.4) lib/hammox.ex:256: Hammox.protected_code/3
In my case, I'm using Ecto.Multi.t()
as the return value of a function, and this structure defines @typep
: https://github.com/elixir-ecto/ecto/blob/v3.4.4/lib/ecto/multi.ex#L133
I have been using hammox for a little while, and I recently tried to mock GenServer handle_call/3
callback.
I ve trimmed my problem down to this script, where I attempt to mock GenServer.handle_call/3
to provide various implementation to a genserver while testing, as a mock.
mocktest.exs
:
ExUnit.start()
Hammox.defmock(MyMock, for: GenServer)
defmodule GSTest do
use GenServer
def start_link(opts) do
GenServer.start_link(
__MODULE__,
{42},
opts
)
end
def call(srv, req) do
GenServer.call(srv, req)
end
@impl true
def init(args) do
{:ok, {"some", "state", args}}
end
@impl true
def handle_call(request, from, state) do
# transparent forward
MyMock.handle_call(request, from, state)
end
end
defmodule GSTest.Test do
use ExUnit.Case
import Hammox
setup do
proc = start_supervised!({GSTest, name: GSTest.Process})
%{proc: proc}
end
test "lets mock the genserver callback", %{proc: proc} do
MyMock
|> allow(self(), proc)
# replying with the current state
|> expect(:handle_call, fn (request, from, state) -> {:reply, state, state} end)
# call and get a response
{:ok, response} = GenServer.call(proc, {:myrequest, "my_param"})
# the response is from the mock, returning the actual process state
assert response == {"some", "state"}
end
end
when running this (with mix run and the proper dependencies installed), I get the following error
$ mix run mocktest.exs
warning: variable "from" is unused (if the variable is not meant to be used, prefix it with an underscore)
mocktest.exs:46: GSTest.Test."test lets mock the genserver callback"/1
warning: variable "request" is unused (if the variable is not meant to be used, prefix it with an underscore)
mocktest.exs:46: GSTest.Test."test lets mock the genserver callback"/1
[error] GenServer GSTest.Process terminating
** (MatchError) no match of right hand side value: {:type, 518, :bounded_fun, [{:type, 518, :fun, [{:type, 518, :product, [{:ann_type, 518, [{:var, 518, :request}, {:type, 518, :term, []}]}, {:remote_type, 0, [{:atom, 0, GenServer}, {:atom, 0, :from}, []]}, {:ann_type, 518, [{:var, 518, :state}, {:type, 518, :term, []}]}]}, {:type, 520, :union, [{:type, 519, :tuple, [{:atom, 0, :reply}, {:var, 519, :reply}, {:var, 519, :new_state}]}, {:type, 520, :tuple, [{:atom, 0, :reply}, {:var, 520, :reply}, {:var, 520, :new_state}, {:type, 520, :union, [{:type, 520, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 520, :term, []}]}]}]}, {:type, 0, :tuple, [{:atom, 0, :noreply}, {:var, 521, :new_state}]}, {:type, 522, :tuple, [{:atom, 0, :noreply}, {:var, 522, :new_state}, {:type, 522, :union, [{:type, 522, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 522, :term, []}]}]}]}, {:type, 523, :tuple, [{:atom, 0, :stop}, {:var, 523, :reason}, {:var, 523, :reply}, {:var, 523, :new_state}]}, {:type, 524, :tuple, [{:atom, 0, :stop}, {:var, 524, :reason}, {:var, 524, :new_state}]}]}]}, [{:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reply}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :new_state}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reason}, {:type, 525, :term, []}]]}]]}
(hammox 0.4.0) lib/hammox.ex:466: Hammox.arg_typespec/2
(hammox 0.4.0) lib/hammox.ex:375: anonymous fn/2 in Hammox.match_args/2
(elixir 1.11.2) lib/enum.ex:1399: Enum."-map/2-lists^map/1-0-"/2
(hammox 0.4.0) lib/hammox.ex:374: Hammox.match_args/2
(hammox 0.4.0) lib/hammox.ex:361: Hammox.match_call/3
(hammox 0.4.0) lib/hammox.ex:346: anonymous fn/4 in Hammox.check_call/3
(elixir 1.11.2) lib/enum.ex:3764: Enumerable.List.reduce/3
(elixir 1.11.2) lib/enum.ex:2231: Enum.reduce_while/3
(hammox 0.4.0) lib/hammox.ex:345: Hammox.check_call/3
(hammox 0.4.0) lib/hammox.ex:338: Hammox.protected_code/3
(stdlib 3.13.2) gen_server.erl:706: :gen_server.try_handle_call/4
(stdlib 3.13.2) gen_server.erl:735: :gen_server.handle_msg/6
(stdlib 3.13.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.513.0>): {:myrequest, "my_param"}
State: {"some", "state", {42}}
Client #PID<0.513.0> is alive
(stdlib 3.13.2) gen.erl:208: :gen.do_call/4
(elixir 1.11.2) lib/gen_server.ex:1024: GenServer.call/3
mocktest.exs:49: GSTest.Test."test lets mock the genserver callback"/1
(ex_unit 1.11.2) lib/ex_unit/runner.ex:391: ExUnit.Runner.exec_test/1
(stdlib 3.13.2) timer.erl:166: :timer.tc/1
(ex_unit 1.11.2) lib/ex_unit/runner.ex:342: anonymous fn/4 in ExUnit.Runner.spawn_test_monitor/4
1) test lets mock the genserver callback (GSTest.Test)
mocktest.exs:42
** (exit) exited in: GenServer.call(#PID<0.515.0>, {:myrequest, "my_param"}, 5000)
** (EXIT) an exception was raised:
** (MatchError) no match of right hand side value: {:type, 518, :bounded_fun, [{:type, 518, :fun, [{:type, 518, :product, [{:ann_type, 518, [{:var, 518, :request}, {:type, 518, :term, []}]}, {:remote_type, 0, [{:atom, 0, GenServer}, {:atom, 0, :from}, []]}, {:ann_type, 518, [{:var, 518, :state}, {:type, 518, :term, []}]}]}, {:type, 520, :union, [{:type, 519, :tuple, [{:atom, 0, :reply}, {:var, 519, :reply}, {:var, 519, :new_state}]}, {:type, 520, :tuple, [{:atom, 0, :reply}, {:var, 520, :reply}, {:var, 520, :new_state}, {:type, 520, :union, [{:type, 520, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 520, :term, []}]}]}]}, {:type, 0, :tuple, [{:atom, 0, :noreply}, {:var, 521, :new_state}]}, {:type, 522, :tuple, [{:atom, 0, :noreply}, {:var, 522, :new_state}, {:type, 522, :union, [{:type, 522, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 522, :term, []}]}]}]}, {:type, 523, :tuple, [{:atom, 0, :stop}, {:var, 523, :reason}, {:var, 523, :reply}, {:var, 523, :new_state}]}, {:type, 524, :tuple, [{:atom, 0, :stop}, {:var, 524, :reason}, {:var, 524, :new_state}]}]}]}, [{:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reply}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :new_state}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reason}, {:type, 525, :term, []}]]}]]}
(hammox 0.4.0) lib/hammox.ex:466: Hammox.arg_typespec/2
(hammox 0.4.0) lib/hammox.ex:375: anonymous fn/2 in Hammox.match_args/2
(elixir 1.11.2) lib/enum.ex:1399: Enum."-map/2-lists^map/1-0-"/2
(hammox 0.4.0) lib/hammox.ex:374: Hammox.match_args/2
(hammox 0.4.0) lib/hammox.ex:361: Hammox.match_call/3
(hammox 0.4.0) lib/hammox.ex:346: anonymous fn/4 in Hammox.check_call/3
(elixir 1.11.2) lib/enum.ex:3764: Enumerable.List.reduce/3
(elixir 1.11.2) lib/enum.ex:2231: Enum.reduce_while/3
(hammox 0.4.0) lib/hammox.ex:345: Hammox.check_call/3
(hammox 0.4.0) lib/hammox.ex:338: Hammox.protected_code/3
(stdlib 3.13.2) gen_server.erl:706: :gen_server.try_handle_call/4
(stdlib 3.13.2) gen_server.erl:735: :gen_server.handle_msg/6
(stdlib 3.13.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
code: {:ok, response} = GenServer.call(proc, {:myrequest, "my_param"})
stacktrace:
(elixir 1.11.2) lib/gen_server.ex:1027: GenServer.call/3
mocktest.exs:49: (test)
Finished in 0.09 seconds (0.08s on load, 0.01s on tests)
1 test, 1 failure
Randomized with seed 249050
[os_mon] memory supervisor port (memsup): Erlang has closed
[os_mon] cpu supervisor port (cpu_sup): Erlang has closed
This is with
$ mix -v
Erlang/OTP 23 [erts-11.1.3] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [hipe]
Mix 1.11.2 (compiled with Erlang/OTP 23)
So it seems to me that somehow hammox cannot handle GenServer.handle_call typespec ?
I am aware of a simple workaround, which is to have a separate behaviour describing what the handle_call will do, and mock that instead. Which is what I usually do, to keep my GenServers light.
Unfortunately in my actual usecase (adding a more generic behavior for many servers in various apps), I need to mock the same signature as GenServer.handle_call/3
, so that workaround wouldn't apply.
So, is there a way to mock GenServer.handle_call/3
? Am I missing something ?
And Thanks for Hammox!
When using Hammox.expect
returning a struct from stripity_stripe, like the Stripe.Customer
struct, it becomes too slow (more or less 5 seconds for each expect
call).
Seems to be an issue with the typespec checking (after returning the value), because if I use Hammox.stub
or even use Mox.expect
, it runs normally.
An example of the call I'am doing:
Hammox.expect(StripeMock, :get_customer, fn _ ->
%Stripe,Customer{
id: "",
object: "customer",
address: nil,
balance: 0,
created: 1_599_741_039,
currency: "usd",
default_source: "",
deleted: nil,
delinquent: false,
description: nil,
discount: nil,
email: nil,
invoice_prefix: "",
invoice_settings: nil,
livemode: false,
metadata: %{},
name: "",
next_invoice_sequence: 1,
payment_method: nil,
phone: "",
preferred_locales: [],
shipping: nil,
sources: %List{data: [], has_more: false, object: "", total_count: 0, url: ""},
subscriptions: %List{data: [], has_more: false, object: "", total_count: 0, url: ""},
tax_exempt: nil,
tax_ids: %List{data: [], has_more: false, object: "", total_count: 0, url: ""},
}
end)
Using
Add an enumerable type which can be parametrized by element type. This type will be checked for implementing the Enumerable protocol and then each element of the enumerable will be checked with element type.
Add a warning that this will consume the enumerable at type check time if the enumerable is lazy or has side effects.
With #8, there are now many different ways to call protect
. We should refactor the docs to explain them in a structured way.
It is possible to define guards in function typespecs. However, trying to use Hammox with such typespecs will cause a MatchError involving a :bounded_fun
type tuple similar to this one:
** (MatchError) no match of right hand side value: {:type, 518, :bounded_fun, [{:type, 518, :fun, [{:type, 518, :product, [{:ann_type, 518, [{:var, 518, :request}, {:type, 518, :term, []}]}, {:remote_type, 0, [{:atom, 0, GenServer}, {:atom, 0, :from}, []]}, {:ann_type, 518, [{:var, 518, :state}, {:type, 518, :term, []}]}]}, {:type, 520, :union, [{:type, 519, :tuple, [{:atom, 0, :reply}, {:var, 519, :reply}, {:var, 519, :new_state}]}, {:type, 520, :tuple, [{:atom, 0, :reply}, {:var, 520, :reply}, {:var, 520, :new_state}, {:type, 520, :union, [{:type, 520, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 520, :term, []}]}]}]}, {:type, 0, :tuple, [{:atom, 0, :noreply}, {:var, 521, :new_state}]}, {:type, 522, :tuple, [{:atom, 0, :noreply}, {:var, 522, :new_state}, {:type, 522, :union, [{:type, 522, :timeout, []}, {:atom, 0, :hibernate}, {:type, 0, :tuple, [{:atom, 0, :continue}, {:type, 522, :term, []}]}]}]}, {:type, 523, :tuple, [{:atom, 0, :stop}, {:var, 523, :reason}, {:var, 523, :reply}, {:var, 523, :new_state}]}, {:type, 524, :tuple, [{:atom, 0, :stop}, {:var, 524, :reason}, {:var, 524, :new_state}]}]}]}, [{:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reply}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :new_state}, {:type, 525, :term, []}]]}, {:type, 518, :constraint, [{:atom, 518, :is_subtype}, [{:var, 518, :reason}, {:type, 525, :term, []}]]}]]}
(hammox 0.4.0) lib/hammox.ex:466: Hammox.arg_typespec/2
(hammox 0.4.0) lib/hammox.ex:375: anonymous fn/2 in Hammox.match_args/2
(elixir 1.11.2) lib/enum.ex:1399: Enum."-map/2-lists^map/1-0-"/2
(hammox 0.4.0) lib/hammox.ex:374: Hammox.match_args/2
(hammox 0.4.0) lib/hammox.ex:361: Hammox.match_call/3
(hammox 0.4.0) lib/hammox.ex:346: anonymous fn/4 in Hammox.check_call/3
(elixir 1.11.2) lib/enum.ex:3764: Enumerable.List.reduce/3
(elixir 1.11.2) lib/enum.ex:2231: Enum.reduce_while/3
(hammox 0.4.0) lib/hammox.ex:345: Hammox.check_call/3
(hammox 0.4.0) lib/hammox.ex:338: Hammox.protected_code/3
(stdlib 3.13.2) gen_server.erl:706: :gen_server.try_handle_call/4
(stdlib 3.13.2) gen_server.erl:735: :gen_server.handle_msg/6
(stdlib 3.13.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
This is a public feature of Elixir typespecs and needs to be supported.
Should Hammox support parameterized union type consisting of custom types?
defmodule Result do
@type ok(any) :: {:ok, any}
@type error(any) :: {:error, any}
@typedoc "the two-track type"
@type t(a, b) :: ok(a) | error(b)
end
I get the following error:
** (FunctionClauseError) no function clause matching in Hammox.TypeEngine.match_type/2
The following arguments were given to Hammox.TypeEngine.match_type/2:
# 1
:foo
# 2
{:var, 7, :a}
Attempted function clauses (showing 10 out of 91):
def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types)
def match_type(_value, {:type, _, :any, []})
def match_type(value, {:type, _, :none, []} = type)
def match_type(value, {:type, _, :atom, []}) when is_atom(value)
def match_type(value, {:type, _, :atom, []} = type)
def match_type(value, {:type, _, :map, :any}) when is_map(value)
def match_type(value, {:type, _, :pid, []}) when is_pid(value)
def match_type(value, {:type, _, :pid, []} = type)
def match_type(value, {:type, _, :port, []}) when is_port(value)
def match_type(value, {:type, _, :port, []} = type)
code: HammoxPlayground.Mock.perform(Decimal.new("0.992"))
stacktrace:
(hammox) lib/hammox/type_engine.ex:6: Hammox.TypeEngine.match_type/2
(hammox) lib/hammox/type_engine.ex:100: anonymous fn/1 in Hammox.TypeEngine.match_type/2
(elixir) lib/enum.ex:2984: Enum.find_value_list/3
(hammox) lib/hammox/type_engine.ex:99: Hammox.TypeEngine.match_type/2
(hammox) lib/hammox/type_engine.ex:9: anonymous fn/3 in Hammox.TypeEngine.match_type/2
(elixir) lib/enum.ex:3325: Enumerable.List.reduce/3
(elixir) lib/enum.ex:1998: Enum.reduce_while/3
(hammox) lib/hammox/type_engine.ex:8: Hammox.TypeEngine.match_type/2
(hammox) lib/hammox.ex:312: Hammox.match_return_value/2
(hammox) lib/hammox.ex:280: Hammox.match_call/3
(hammox) lib/hammox.ex:264: anonymous fn/4 in Hammox.check_call/3
(elixir) lib/enum.ex:3325: Enumerable.List.reduce/3
(elixir) lib/enum.ex:1998: Enum.reduce_while/3
(hammox) lib/hammox.ex:263: Hammox.check_call/3
(hammox) lib/hammox.ex:256: Hammox.protected_code/3
test/hammox_playground_test.exs:13: (test)
When using use Hammox.Protect
(and protect/2
) with older versions of Elixir (< 1.12), the following error is raised because Code.ensure_compiled!
isn't available in the check_module_exists
util function.
** (UndefinedFunctionError) function Code.ensure_compiled!/1 is undefined or private. Did you mean one of:
* ensure_compiled/1
* ensure_loaded/1
(elixir 1.11.4) Code.ensure_compiled!(Hui.ResponseParsers.Parser)
lib/hammox/protect.ex:153: Hammox.Protect.get_funs!/1
A fix should be created in similar vein to Mox:
https://github.com/dashbitco/mox/blob/a59a1840676ceb0ef6c3e0f4959242ccb5d593ab/lib/mox.ex#L794-L809
Happy to submit a PR since Hammox is still elixir: "~> 1.7"
.
For example, the following type definition works:
@type greeting :: String.t()
@callback perform(Decimal.t()) :: greeting
but the following raises a FunctionCaluseError
exception:
@callback perform(Decimal.t()) :: greeting :: String.t()
Stacktrace:
** (FunctionClauseError) no function clause matching in Hammox.TypeEngine.match_type/2
The following arguments were given to Hammox.TypeEngine.match_type/2:
# 1
"hello"
# 2
{:ann_type, 7, [{:var, 7, :greeting}, {:remote_type, 7, [{:atom, 0, String}, {:atom, 0, :t}, []]}]}
Attempted function clauses (showing 10 out of 91):
def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types)
def match_type(_value, {:type, _, :any, []})
def match_type(value, {:type, _, :none, []} = type)
def match_type(value, {:type, _, :atom, []}) when is_atom(value)
def match_type(value, {:type, _, :atom, []} = type)
def match_type(value, {:type, _, :map, :any}) when is_map(value)
def match_type(value, {:type, _, :pid, []}) when is_pid(value)
def match_type(value, {:type, _, :pid, []} = type)
def match_type(value, {:type, _, :port, []}) when is_port(value)
def match_type(value, {:type, _, :port, []} = type)
code: HammoxPlayground.Mock.perform(Decimal.new("0.992"))
stacktrace:
(hammox) lib/hammox/type_engine.ex:6: Hammox.TypeEngine.match_type/2
(hammox) lib/hammox.ex:312: Hammox.match_return_value/2
(hammox) lib/hammox.ex:280: Hammox.match_call/3
(hammox) lib/hammox.ex:264: anonymous fn/4 in Hammox.check_call/3
(elixir) lib/enum.ex:3325: Enumerable.List.reduce/3
(elixir) lib/enum.ex:1998: Enum.reduce_while/3
(hammox) lib/hammox.ex:263: Hammox.check_call/3
(hammox) lib/hammox.ex:256: Hammox.protected_code/3
test/hammox_playground_test.exs:13: (test)
Could you please tell me if hammox
has support for custom types? For example:
defmodule MyType do
defstruct [:value]
@type t :: %__MODULE__{
value: number()
}
end
Looking through the source code and the following error, I am assuming it doesn't currently:
The following arguments were given to Hammox.TypeEngine.match_type/2:
# 1
1000
# 2
{:op, 93, :-, {:integer, 93, 1}}
Attempted function clauses (showing 10 out of 89):
def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types)
def match_type(_value, {:type, _, :any, []})
def match_type(value, {:type, _, :none, []} = type)
def match_type(value, {:type, _, :atom, []}) when is_atom(value)
def match_type(value, {:type, _, :atom, []} = type)
def match_type(value, {:type, _, :map, :any}) when is_map(value)
def match_type(value, {:type, _, :pid, []}) when is_pid(value)
def match_type(value, {:type, _, :pid, []} = type)
def match_type(value, {:type, _, :port, []}) when is_port(value)
def match_type(value, {:type, _, :port, []} = type)
stacktrace:
(hammox) lib/hammox/type_engine.ex:6: Hammox.TypeEngine.match_type/2
(hammox) lib/hammox/type_engine.ex:9: anonymous fn/3 in Hammox.TypeEngine.match_type/2
(elixir) lib/enum.ex:3325: Enumerable.List.reduce/3
(elixir) lib/enum.ex:1998: Enum.reduce_while/3
(hammox) lib/hammox/type_engine.ex:8: Hammox.TypeEngine.match_type/2
(hammox) lib/hammox/type_engine.ex:364: anonymous fn/3 in Hammox.TypeEngine.match_type/2
(elixir) lib/enum.ex:1340: anonymous fn/3 in Enum.map/2
(stdlib) maps.erl:232: :maps.fold_1/3
(elixir) lib/enum.ex:1964: Enum.map/2
(hammox) lib/hammox/type_engine.ex:363: anonymous fn/2 in Hammox.TypeEngine.match_type/2
(elixir) lib/enum.ex:3358: Enumerable.Map.reduce_list/3
(elixir) lib/enum.ex:1998: Enum.reduce_while/3
(hammox) lib/hammox/type_engine.ex:361: Hammox.TypeEngine.match_type/2
(hammox) lib/hammox/type_engine.ex:364: anonymous fn/3 in Hammox.TypeEngine.match_type/2
(elixir) lib/enum.ex:1340: anonymous fn/3 in Enum.map/2
(stdlib) maps.erl:232: :maps.fold_1/3
(elixir) lib/enum.ex:1964: Enum.map/2
(hammox) lib/hammox/type_engine.ex:363: anonymous fn/2 in Hammox.TypeEngine.match_type/2
(elixir) lib/enum.ex:3358: Enumerable.Map.reduce_list/3
(elixir) lib/enum.ex:1998: Enum.reduce_while/3
Thank you for your help!
In a complex type, when there is a module that cannot be loaded, and the types are similar (the type match stacks are the same height) the error is instead that it couldn't match the last element of the union. Possible working example:
defmodule Module1 do
@type t :: :module1
end
defmodule Module3 do
@type t :: :module3
end
defmodule Foo do
@callback foo() :: {:ok, Module1.t()} | {:ok, Module2.t()} | {:ok, Module3.t()}
def foo, do: {:ok, :nothing}
end
Expected kind of error from calling protected Foo.foo/0
: "Could not load Module2"
Actual kind of error from calling protected Foo.foo/0
: "Could not match Module3"
Hey. I just changed Mox to Hammox in one file and I have lots of these errors:
I'm not sure if there should be a head for that. Just leaving to check if it's a bug
The following arguments were given to Hammox.TypeEngine.match_type/2:
# 1
true
# 2
{:type, 25, :bool, []}
There is controversy about how improper list should be treated (see josefs/Gradualizer#110). Explain the stance Hammox takes on this issue.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.