elixir-lang/elixir-lang.github.com

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
end

KV/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
end

Welcome! 👋

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!