Current section
Files
Jump to
Current section
Files
lib/old/server.ex
defmodule Old.Ersatz.Server do
@moduledoc false
use GenServer
@timeout 30000
# API
def start_link(_options) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def add_expectation(owner_pid, key, value) do
GenServer.call(__MODULE__, {:add_expectation, owner_pid, key, value}, @timeout)
end
def fetch_fun_to_dispatch(caller_pids, key) do
GenServer.call(__MODULE__, {:fetch_fun_to_dispatch, caller_pids, key}, @timeout)
end
def verify(owner_pid, for) do
GenServer.call(__MODULE__, {:verify, owner_pid, for}, @timeout)
end
def verify_on_exit(pid) do
GenServer.call(__MODULE__, {:verify_on_exit, pid}, @timeout)
end
def allow(mock, owner_pid, pid) do
GenServer.call(__MODULE__, {:allow, mock, owner_pid, pid}, @timeout)
end
def exit(pid) do
GenServer.cast(__MODULE__, {:exit, pid})
end
def set_mode(owner_pid, mode) do
GenServer.call(__MODULE__, {:set_mode, owner_pid, mode})
end
# Callbacks
def init(:ok) do
{:ok, %{expectations: %{}, allowances: %{}, deps: %{}, mode: :private, global_owner_pid: nil}}
end
def handle_call(
{:add_expectation, owner_pid, {mock, _, _} = key, expectation},
_from,
%{mode: :private} = state
) do
if allowance = state.allowances[owner_pid][mock] do
{:reply, {:error, {:currently_allowed, allowance}}, state}
else
state = maybe_add_and_monitor_pid(state, owner_pid)
state =
update_in(state, [:expectations, pid_map(owner_pid)], fn owned_expectations ->
Map.update(owned_expectations, key, expectation, &merge_expectation(&1, expectation))
end)
{:reply, :ok, state}
end
end
def handle_call(
{:add_expectation, owner_pid, {_mock, _, _} = key, expectation},
_from,
%{mode: :global, global_owner_pid: global_owner_pid} = state
) do
if owner_pid != global_owner_pid do
{:reply, {:error, {:not_global_owner, global_owner_pid}}, state}
else
state =
update_in(state, [:expectations, pid_map(owner_pid)], fn owned_expectations ->
Map.update(owned_expectations, key, expectation, &merge_expectation(&1, expectation))
end)
{:reply, :ok, state}
end
end
def handle_call(
{:fetch_fun_to_dispatch, caller_pids, {mock, _, _} = key},
_from,
%{mode: :private} = state
) do
owner_pid =
Enum.find_value(caller_pids, List.first(caller_pids), fn caller_pid ->
cond do
state.allowances[caller_pid][mock] -> state.allowances[caller_pid][mock]
state.expectations[caller_pid][key] -> caller_pid
true -> false
end
end)
case state.expectations[owner_pid][key] do
nil ->
{:reply, :no_expectation, state}
{total, [], nil} ->
{:reply, {:out_of_expectations, total}, state}
{_, [], stub} ->
{:reply, {:ok, stub}, state}
{total, [call | calls], stub} ->
new_state = put_in(state.expectations[owner_pid][key], {total, calls, stub})
{:reply, {:ok, call}, new_state}
end
end
def handle_call(
{:fetch_fun_to_dispatch, _caller_pids, {_mock, _, _} = key},
_from,
%{mode: :global} = state
) do
case state.expectations[state.global_owner_pid][key] do
nil ->
{:reply, :no_expectation, state}
{total, [], nil} ->
{:reply, {:out_of_expectations, total}, state}
{_, [], stub} ->
{:reply, {:ok, stub}, state}
{total, [call | calls], stub} ->
new_state = put_in(state.expectations[state.global_owner_pid][key], {total, calls, stub})
{:reply, {:ok, call}, new_state}
end
end
def handle_call({:verify, owner_pid, mock}, _from, state) do
expectations = state.expectations[owner_pid] || %{}
pending =
for {{module, _, _} = key, {count, [_ | _] = calls, _stub}} <- expectations,
module == mock or mock == :all do
{key, count, length(calls)}
end
{:reply, pending, state}
end
def handle_call({:verify_on_exit, pid}, _from, state) do
state = maybe_add_and_monitor_pid(state, pid, :EXIT, fn {_, deps} -> {:EXIT, deps} end)
{:reply, :ok, state}
end
def handle_call({:allow, _, _, _}, _from, %{mode: :global} = state) do
{:reply, {:error, :in_global_mode}, state}
end
def handle_call({:allow, mock, owner_pid, pid}, _from, %{mode: :private} = state) do
%{allowances: allowances, expectations: expectations} = state
owner_pid = state.allowances[owner_pid][mock] || owner_pid
allowance = allowances[pid][mock]
cond do
Map.has_key?(expectations, pid) ->
{:reply, {:error, :expectations_defined}, state}
allowance && allowance != owner_pid ->
{:reply, {:error, {:already_allowed, allowance}}, state}
true ->
state =
maybe_add_and_monitor_pid(state, owner_pid, :DOWN, fn {on, deps} ->
{on, [{pid, mock} | deps]}
end)
state = put_in(state, [:allowances, pid_map(pid), mock], owner_pid)
{:reply, :ok, state}
end
end
def handle_call({:set_mode, owner_pid, :global}, _from, state) do
state = maybe_add_and_monitor_pid(state, owner_pid)
{:reply, :ok, %{state | mode: :global, global_owner_pid: owner_pid}}
end
def handle_call({:set_mode, _owner_pid, :private}, _from, state) do
{:reply, :ok, %{state | mode: :private, global_owner_pid: nil}}
end
def handle_cast({:exit, pid}, state) do
{:noreply, down(state, pid)}
end
def handle_info({:DOWN, _, _, pid, _}, state) do
state =
case state.global_owner_pid do
^pid -> %{state | mode: :private, global_owner_pid: nil}
_ -> state
end
state =
case state.deps do
%{^pid => {:DOWN, _}} -> down(state, pid)
%{} -> state
end
{:noreply, state}
end
# Helper functions
defp down(state, pid) do
{{_, deps}, state} = pop_in(state.deps[pid])
{_, state} = pop_in(state.expectations[pid])
{_, state} = pop_in(state.allowances[pid])
Enum.reduce(deps, state, fn {pid, mock}, acc ->
acc.allowances[pid][mock] |> pop_in() |> elem(1)
end)
end
defp pid_map(pid) do
Access.key(pid, %{})
end
defp maybe_add_and_monitor_pid(state, pid) do
maybe_add_and_monitor_pid(state, pid, :DOWN, nil)
end
defp maybe_add_and_monitor_pid(state, pid, on, fun) do
case state.deps do
%{^pid => entry} ->
if fun do
put_in(state.deps[pid], fun.(entry))
else
state
end
_ ->
Process.monitor(pid)
state = put_in(state.deps[pid], {on, []})
state
end
end
defp merge_expectation({current_n, current_calls, current_stub}, {n, calls, stub}) do
{current_n + n, current_calls ++ calls, stub || current_stub}
end
end