defmodule GitHub.Testing.Mock do @moduledoc """ Internal representation of a mocked API call """ @typedoc """ Specification of arguments Each set of arguments can be given as a list of elements to match or a total arity. When given as a list, each element can be any Erlang term or the special value `:_` to match any value. Specifications given as a list will have higher precedence depending on the number of arguments that match exactly. """ @type args :: [any] | non_neg_integer @typedoc """ Limit to the number of times a mock can be used The special value `:infinity` can be used to place no limit. """ @type limit :: pos_integer | :infinity @typedoc """ Return value from a mocked API call Return values are tagged tuples with optional additional information. For example: {:ok, %GitHub.Repository{}} {:ok, %GitHub.Repository{}, code: 200} {:error, %GitHub.Error{}} {:error, %GitHub.Error{}, code: 404} For more on the possible values, see `GitHub.Testing`. """ @type return :: {:ok, any} | {:ok, any, keyword} | {:error, any} | {:error, any, keyword} @typedoc """ Return value or generator of a mocked API call This may be a constant (value) or a zero-arity function returning a constant (generator). """ @type return_fun :: return | (-> return) | (... -> return) @typedoc """ Mocked API call """ @type t :: %__MODULE__{ args: args, cache: boolean, function: atom, implicit: boolean, limit: limit, module: module, return: return_fun } defstruct [:args, :cache, :function, :implicit, :limit, :module, :return] # # Matching # @doc false @spec choose([t], [any]) :: t | nil def choose(mocks, args) def choose(nil, _args), do: nil def choose(mocks, args) do mocks |> filter_by_arity(args) |> filter_by_argument(args) |> Enum.sort_by(ranking_fn(args)) |> List.first() end @spec filter_by_arity([t], [any]) :: [t] defp filter_by_arity(mocks, args) do Enum.filter(mocks, fn %{args: arity} when is_integer(arity) -> arity == length(args) %{args: mock_args} when is_list(mock_args) -> length(mock_args) == length(args) end) end @spec filter_by_argument([t], [any]) :: [t] defp filter_by_argument(mocks, args) do Enum.filter(mocks, fn %{args: arity} when is_integer(arity) -> true %{args: mock_args} -> Enum.zip(mock_args, args) |> Enum.all?(fn {:_, _} -> true {same_value, same_value} -> true _else -> false end) end) end @spec ranking_fn([any]) :: ([t] -> integer) defp ranking_fn(args) do args_length = length(args) fn %{args: ^args, implicit: false} -> 1 %{args: ^args, implicit: true} -> 2 %{args: ^args_length, implicit: false} -> 100 %{args: ^args_length, implicit: true} -> 200 %{args: mock_args, implicit: false} -> 100 - count_matching_args(mock_args, args) %{args: mock_args, implicit: true} -> 200 - count_matching_args(mock_args, args) _else -> 999 end end @spec count_matching_args([any], [any], non_neg_integer) :: non_neg_integer defp count_matching_args(mock_args, actual_args, counter \\ 0) defp count_matching_args([same | rest_mock], [same | rest_real], counter) do count_matching_args(rest_mock, rest_real, counter + 1) end defp count_matching_args([_a | rest_mock], [_b | rest_real], counter) do count_matching_args(rest_mock, rest_real, counter) end defp count_matching_args([], [], counter), do: counter end