Giter Club home page Giter Club logo

hammox's People

Contributors

adamnbowen avatar adzz avatar camilleryr avatar codepr avatar dependabot-preview[bot] avatar dependabot[bot] avatar dkuku avatar florian3k avatar kamilkowalski avatar kianmeng avatar msz avatar saneery avatar sophisticasean avatar vitortrin avatar wojtekmach avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

hammox's Issues

Should stubs be `protected` too?

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!

`CaseClauseError` when Displaying Complex Types

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

[idea] add a mix task to setup an area

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.

Display behaviour and callback in errors

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.

Support erlang records

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.

Parameterized type from another module conflicts with local custom type

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.

respect opaqueness of `@opaque` types

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.

FunctionClauseError with Ecto.Changeset.t()

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)

How to test bad behaviour

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 ?

obscure error when module to `protect` does not exist

** (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.

support all Elixir versions since 1.5

This is to make it actually backwards compatible with Mox which requires Elixir ~1.5.

Areas to look at:

  • removing @doc keyword tags
  • looking into copying code from Code.Typespec directly into the project

CaseClauseError with return type of Ecto.Changeset.t()

I'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

Errors on Parametrized Types

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.

Add support for annotated types in tuples

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()}

Hammox breaks when there is a @typep

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

GenServer.handle_call/3 mock ?

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!

Hammox slow with stripity_stripe lib

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

  • Erlang/OTP 23
  • Elixir 1.10.4
  • Hammox 0.2.5
  • stripity_stripe: 2.9

add a builtin Hammox.enumerable/1 type

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.

Type guards in specs not supported

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.

FunctionClauseError for a parameterized type referencing another parameterized type

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)

ensure_compiled! issue when using Hammox.Protect

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".

FunctionClauseError raised on return type annotation

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)

Exception thrown with Decimal.t

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!

"Cannot load module" error gets swallowed

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"

match_type missing head for bool?

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, []}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.