Getting Started - GenServer: Modifying server callbacks to monitor processes breaks unit tests
Closed this issue · 2 comments
The GenServer chapter in the Getting Started guide for Elixir closes by introducing process monitoring. Before moving on to the next chapter, I ran mix test. The tests for KV.Registry do not pass. Here is the output of mix test:
13:05:52.321 [error] GenServer #PID<0.167.0> terminating
** (FunctionClauseError) no function clause matching in KV.Registry.handle_cast/2
(kv 0.1.0) lib/kv/registry.ex:45: KV.Registry.handle_cast({:create, "shopping"}, %{})
(stdlib 3.12) gen_server.erl:637: :gen_server.try_dispatch/4
(stdlib 3.12) gen_server.erl:711: :gen_server.handle_msg/6
(stdlib 3.12) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message: {:"$gen_cast", {:create, "shopping"}}
State: %{}
13:05:52.336 [error] GenServer #PID<0.171.0> terminating
** (MatchError) no match of right hand side value: %{"shopping" => #PID<0.172.0>}
(kv 0.1.0) lib/kv/registry.ex:40: KV.Registry.handle_call/3
(stdlib 3.12) gen_server.erl:661: :gen_server.try_handle_call/4
(stdlib 3.12) gen_server.erl:690: :gen_server.handle_msg/6
(stdlib 3.12) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.169.0>): {:lookup, "shopping"}
State: %{"shopping" => #PID<0.172.0>}
Client #PID<0.169.0> is alive
(stdlib 3.12) gen.erl:167: :gen.do_call/4
(elixir 1.12.1) lib/gen_server.ex:1021: GenServer.call/3
test/kv/registry_test.exs:23: KV.RegistryTest."test removes buckets on exit"/1
(ex_unit 1.12.1) lib/ex_unit/runner.ex:502: ExUnit.Runner.exec_test/1
(stdlib 3.12) timer.erl:166: :timer.tc/1
(ex_unit 1.12.1) lib/ex_unit/runner.ex:453: anonymous fn/4 in ExUnit.Runner.spawn_test_monitor/4
1) test spawns buckets (KV.RegistryTest)
test/kv/registry_test.exs:9
** (exit) exited in: GenServer.call(#PID<0.167.0>, {:lookup, "shopping"}, 5000)
** (EXIT) an exception was raised:
** (FunctionClauseError) no function clause matching in KV.Registry.handle_cast/2
The following arguments were given to KV.Registry.handle_cast/2:
# 1
{:create, "shopping"}
# 2
%{}
Attempted function clauses (showing 1 out of 1):
def handle_cast({:create, name}, -{names, refs}-)
stacktrace:
(kv 0.1.0) lib/kv/registry.ex:45: KV.Registry.handle_cast/2
(stdlib 3.12) gen_server.erl:637: :gen_server.try_dispatch/4
(stdlib 3.12) gen_server.erl:711: :gen_server.handle_msg/6
(elixir 1.12.1) lib/gen_server.ex:1024: GenServer.call/3
test/kv/registry_test.exs:13: (test)
2) test removes buckets on exit (KV.RegistryTest)
test/kv/registry_test.exs:19
** (exit) exited in: GenServer.call(#PID<0.171.0>, {:lookup, "shopping"}, 5000)
** (EXIT) an exception was raised:
** (MatchError) no match of right hand side value: %{"shopping" => #PID<0.172.0>}
(kv 0.1.0) lib/kv/registry.ex:40: KV.Registry.handle_call/3
(stdlib 3.12) gen_server.erl:661: :gen_server.try_handle_call/4
(stdlib 3.12) gen_server.erl:690: :gen_server.handle_msg/6
(stdlib 3.12) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
code: assert KV.Registry.lookup(registry, "shopping") == :error
stacktrace:
(elixir 1.12.1) lib/gen_server.ex:1024: GenServer.call/3
test/kv/registry_test.exs:23: (test)I am learning Elixir for the first time, so I am not sure how to proceed with fixing the tests to make them pass. However, my assumption is that we are testing the client API of our KV.Registry module, so modifying the server callbacks should not have affected the tests. Here is the code for the KV.Registry module, and the corresponding tests:
KV/lib/kv/registry.ex
defmodule KV.Registry do
use GenServer
## Client API
@doc """
Starts the registry.
"""
def start_link(opts) do
GenServer.start_link(__MODULE__, :ok, opts)
end
@doc """
Looks up the bucket pid for `name` stored in `server`.
Returns `{:ok, pid}` if the bucket exists, `:error` otherwise.
"""
def lookup(server, name) do
GenServer.call(server, {:lookup, name})
end
@doc """
Ensures there is a bucket associated with the given `name` in `server`.
"""
def create(server, name) do
GenServer.cast(server, {:create, name})
end
## Server callbacks
@impl true
def init(:ok) do
names = %{}
refs = %{}
{:ok, {names, refs}}
end
@impl true
def handle_call({:lookup, name}, _from, state) do
{names, _} = state
{:reply, Map.fetch(names, name), names}
end
@impl true
def handle_cast({:create, name}, {names, refs}) do
if Map.has_key?(names, name) do
{:noreply, {names, refs}}
else
{:ok, bucket} = KV.Bucket.start_link([])
ref = Process.monitor(bucket)
refs = Map.put(refs, ref, name)
names = Map.put(names, name, bucket)
{:noreply, {names, refs}}
end
end
@impl true
def handle_info({:DOWN, ref, :process, _pid, _reason}, {names, refs}) do
{name, refs} = Map.pop(refs, ref)
names = Map.delete(names, name)
{:noreply, {names, refs}}
end
@impl true
def handle_info(_msg, state) do
{:noreply, state}
end
endKV/test/kv/registry_test.exs
defmodule KV.RegistryTest do
use ExUnit.Case, async: true
setup do
registry = start_supervised!(KV.Registry)
%{registry: registry}
end
test "spawns buckets", %{registry: registry} do
assert KV.Registry.lookup(registry, "shopping") == :error
KV.Registry.create(registry, "shopping")
assert {:ok, bucket} = KV.Registry.lookup(registry, "shopping")
KV.Bucket.put(bucket, "milk", 1)
assert KV.Bucket.get(bucket, "milk") == 1
end
test "removes buckets on exit", %{registry: registry} do
KV.Registry.create(registry, "shopping")
{:ok, bucket} = KV.Registry.lookup(registry, "shopping")
Agent.stop(bucket)
assert KV.Registry.lookup(registry, "shopping") == :error
end
endWelcome! 👋
Your handle_call is wrong. You are returning only the “names” as the state, instead of the {names, refs} tuple, causing further invocations to fail. :)
Wow, that was so simple. I should definitely be more careful in reading over the example code in the tutorial. Thank you!