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