Skip to content

Commit

Permalink
Add tests for game manager server.
Browse files Browse the repository at this point in the history
  • Loading branch information
alexpearce committed Dec 8, 2023
1 parent a134f38 commit 9a0c620
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 20 deletions.
41 changes: 21 additions & 20 deletions lib/twenty_forty_eight/manager_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ defmodule TwentyFortyEight.Game.ManagerServer do
@registry TwentyFortyEight.Game.Registry
@supervisor TwentyFortyEight.Game.Supervisor
# Shutdown the server after 10 minutes to avoid dangling processes.
@timeout 10 * 60 * 1_000
@default_timeout 10 * 60 * 1_000

@doc """
Ensure a server is running for the game named `name`.
"""
def start(%Manager{name: name} = manager) do
def start(%Manager{name: name} = manager, opts \\ []) do
case Registry.lookup(@registry, name) do
[{pid, _value}] -> {:ok, pid}
[] -> DynamicSupervisor.start_child(@supervisor, {__MODULE__, manager})
[] -> DynamicSupervisor.start_child(@supervisor, {__MODULE__, {manager, opts}})
end
end

def start_link(%Manager{name: name} = manager) do
GenServer.start_link(__MODULE__, manager, name: via_tuple(name))
def start_link({%Manager{name: name} = manager, opts}) do
GenServer.start_link(__MODULE__, {manager, opts}, name: via_tuple(name))
end

@doc """
Expand All @@ -44,36 +44,37 @@ defmodule TwentyFortyEight.Game.ManagerServer do
end

@impl true
def init(manager) do
def init({manager, opts}) do
Process.flag(:trap_exit, true)
{:ok, manager, @timeout}
timeout = Keyword.get(opts, :timeout, @default_timeout)
{:ok, %{manager: manager, timeout: timeout}, timeout}
end

@impl true
def handle_call({:tick, move}, _from, manager) do
manager = Manager.tick(manager, move)
{:reply, :ok, manager, @timeout}
def handle_call({:tick, move}, _from, state) do
manager = Manager.tick(state.manager, move)
state = %{state | manager: manager}
{:reply, :ok, state, state.timeout}
end

@impl true
def handle_call(:manager, _from, manager) do
{:reply, manager, manager, @timeout}
def handle_call(:manager, _from, state) do
{:reply, state.manager, state, state.timeout}
end

@impl true
def handle_info(:timeout, manager) do
handle_exit(manager)
{:stop, :shutdown, manager}
def handle_info(:timeout, state) do
{:stop, :shutdown, state}
end

@impl true
def handle_info({:EXIT, _from, reason}, manager) do
handle_exit(manager)
{:stop, reason, manager, @timeout}
def handle_info({:EXIT, _from, reason}, state) do
{:stop, reason, state}
end

defp handle_exit(manager) do
Manager.save(manager)
@impl true
def terminate(_reason, state) do
Manager.save(state.manager)
end

defp via_tuple(name) do
Expand Down
132 changes: 132 additions & 0 deletions test/twenty_forty_eight/game/manager_server_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
defmodule TwentyFortyEight.Game.ManagerServerTest do
# async: false as we want exclusive access to the manager server registry.
use TwentyFortyEight.DataCase, async: false

alias TwentyFortyEight.Game.{Game, Manager, ManagerServer}

describe "start/2" do
test "returns an OK tuple containing the GenServer PID" do
manager = create_manager()
assert {:ok, pid} = ManagerServer.start(manager, timeout: 50)
assert Process.alive?(pid)
end

test "stops the process after the timeout" do
manager = create_manager()
assert {:ok, pid} = ManagerServer.start(manager, timeout: 10)
:timer.sleep(50)
refute Process.alive?(pid)

assert {:ok, pid} = ManagerServer.start(manager, timeout: 100)
:timer.sleep(50)
assert Process.alive?(pid)
:timer.sleep(100)
refute Process.alive?(pid)
end

test "starts a new process when one does not already exist" do
manager = create_manager()
assert {:ok, pid} = ManagerServer.start(manager, timeout: 10)
:timer.sleep(20)
assert {:ok, other_pid} = ManagerServer.start(manager, timeout: 10)
assert pid != other_pid
end

test "returns an existing process when one already exists" do
manager = create_manager()
assert {:ok, pid} = ManagerServer.start(manager, timeout: 50)
assert {:ok, ^pid} = ManagerServer.start(manager, timeout: 50)
end
end

describe "manager/1" do
test "returns the manager struct" do
%Manager{name: name} = manager = create_manager()
{:ok, _pid} = ManagerServer.start(manager, timeout: 50)
assert ^manager = ManagerServer.manager(name)
end

test "returns an updated manager struct after a tick" do
%Manager{name: name} = manager = create_manager()
{:ok, _pid} = ManagerServer.start(manager, timeout: 50)
cycle_moves(name)
assert manager != ManagerServer.manager(name)
end
end

describe "tick/1" do
test "propagates the tick to the manager" do
%Manager{name: name, engine: %{turns: 0}} = manager = create_manager()
{:ok, _pid} = ManagerServer.start(manager, timeout: 50)
cycle_moves(name)
%Manager{engine: %{turns: turns}} = ManagerServer.manager(name)
assert turns > 0
end
end

describe "state persistence" do
test "upon timeout" do
%Manager{name: name, engine: %{turns: 0}} = manager = create_manager()
{:ok, pid} = ManagerServer.start(manager, timeout: 50)
cycle_moves(name)
:timer.sleep(100)
refute Process.alive?(pid)

%{rows: [[turns, "running", board]]} =
TwentyFortyEight.Repo.query!("SELECT turns, state, board FROM game")

assert turns > 0
assert %{"cells" => _cells} = board
end

test "when the server is stopped" do
%Manager{name: name, engine: %{turns: 0}} = manager = create_manager()

{:ok, pid} = ManagerServer.start(manager, timeout: 1000)
cycle_moves(name)
Process.exit(pid, :normal)
:timer.sleep(100)
refute Process.alive?(pid)

%{rows: [[turns, "running", board]]} =
TwentyFortyEight.Repo.query!("SELECT turns, state, board FROM game")

assert turns > 0
assert %{"cells" => _cells} = board
end

test "when a linked process is stopped" do
%Manager{name: name, engine: %{turns: 0}} = manager = create_manager()

spawn_pid =
spawn(fn ->
{:ok, _pid} = ManagerServer.start_link({manager, [timeout: 1000]})
cycle_moves(name)
end)

:timer.sleep(100)
refute Process.alive?(spawn_pid)

%{rows: [[turns, "running", board]]} =
TwentyFortyEight.Repo.query!("SELECT turns, state, board FROM game")

assert turns > 0
assert %{"cells" => _cells} = board
end
end

defp create_manager do
{:ok, game} = Game.create_changeset(%{}) |> Game.insert()

{:ok, manager} = Manager.get(game.slug)

manager
end

defp cycle_moves(name) do
:ok = ManagerServer.tick(name, :up)
:ok = ManagerServer.tick(name, :down)
:ok = ManagerServer.tick(name, :left)
:ok = ManagerServer.tick(name, :right)
end
end

0 comments on commit 9a0c620

Please sign in to comment.