Lockstep.GenServer (Lockstep v0.1.0)

Copy Markdown View Source

GenServer-shaped wrapper that runs under Lockstep's controller.

Exposes start_link/2, call/2, cast/2, and stop/1 — the subset of the GenServer API that round-trips through Lockstep's controlled send / selective-receive primitives. Use this when you want to test a module that follows the GenServer callback contract without rewriting it to use bare send/recv.

Limitations (v0.5)

  • Calls do not honour :timeouts; everything blocks indefinitely under the controller. The runner's per-iteration timeout still applies.
  • No supervision tree; start_link/2 returns a raw managed pid.
  • No process registration. Pass the pid around explicitly.
  • Callback module must implement init/1, plus any of handle_call/3, handle_cast/2, handle_info/2 it needs.
  • terminate/2 and code_change/3 are not invoked.

Example

defmodule Counter do
  def init(_), do: {:ok, 0}
  def handle_call(:get, _from, n), do: {:reply, n, n}
  def handle_cast({:add, x}, n), do: {:noreply, n + x}
end

ctest "counter increments" do
  {:ok, srv} = Lockstep.GenServer.start_link(Counter, [])
  Lockstep.GenServer.cast(srv, {:add, 5})
  assert Lockstep.GenServer.call(srv, :get) == 5
end

Summary

Functions

Synchronous request. Blocks (in the controller) until the server replies. Selective-receive-matched on a unique reference, so other messages in the caller's mailbox are not disturbed.

Three-arg call form for OTP source compatibility. The timeout is ignored -- Lockstep blocks via the controller's per-iteration timeout instead.

Fire-and-forget request.

OTP-shape reply for handle_call clauses that returned :noreply and need to reply asynchronously. Same contract as GenServer.reply/2: given a {caller_pid, request_ref} tuple and a value, sends {ref, value} to the caller.

Unlinked variant -- same as start_link/3 for Lockstep purposes.

Three-arg unlinked variant.

Spawn a managed GenServer process. Returns {:ok, pid} so that vanilla OTP-style call sites ({:ok, srv} = GenServer.start_link(...)) remain pattern-compatible after Lockstep.Rewriter rewrites them.

Three-arg form for OTP source compatibility.

Erlang gen_server-style 4-arg start_link. The first argument is a registration shape

Send a stop signal. The server runs terminate/2 is not invoked.

Three-arg stop form for OTP source compatibility. Timeout ignored.

Types

server()

@type server() :: pid()

state()

@type state() :: any()

Functions

call(server, request)

@spec call(server() | {:via, module(), term()}, any()) :: any()

Synchronous request. Blocks (in the controller) until the server replies. Selective-receive-matched on a unique reference, so other messages in the caller's mailbox are not disturbed.

Mirrors OTP semantics for a dead target: monitors the server before sending; if a :DOWN arrives before a reply, raises an :exit matching GenServer.call/2's contract. This is what lets call sites use try/catch :exit, _ -- same as vanilla OTP -- to handle "the server died while we were waiting" cleanly.

call(server, request, timeout)

@spec call(server(), any(), timeout()) :: any()

Three-arg call form for OTP source compatibility. The timeout is ignored -- Lockstep blocks via the controller's per-iteration timeout instead.

cast(server, request)

@spec cast(server() | {:via, module(), term()}, any()) :: :ok

Fire-and-forget request.

reply(arg, value)

@spec reply(
  {pid(), reference()},
  any()
) :: :ok

OTP-shape reply for handle_call clauses that returned :noreply and need to reply asynchronously. Same contract as GenServer.reply/2: given a {caller_pid, request_ref} tuple and a value, sends {ref, value} to the caller.

start(module, init_arg)

@spec start(module(), any()) :: {:ok, pid()}

Unlinked variant -- same as start_link/3 for Lockstep purposes.

start(module, init_arg, opts)

@spec start(module(), any(), keyword()) ::
  {:ok, pid()} | {:error, {:already_started, pid()}}

Three-arg unlinked variant.

start_link(module, init_arg)

@spec start_link(module(), any()) :: {:ok, pid()} | {:stop, term()} | :ignore

Spawn a managed GenServer process. Returns {:ok, pid} so that vanilla OTP-style call sites ({:ok, srv} = GenServer.start_link(...)) remain pattern-compatible after Lockstep.Rewriter rewrites them.

Failures inside init/1 raise on the spawned process and propagate as a normal child crash to Lockstep's controller.

start_link(module, init_arg, opts)

@spec start_link(module(), any(), keyword()) ::
  {:ok, pid()} | {:error, {:already_started, pid()}}

Three-arg form for OTP source compatibility.

The opts keyword may include:

  • :name -- only {:via, Lockstep.Registry, {reg, key}} form is modeled. Atom names are accepted but ignored (Lockstep doesn't have a global name table). Other :via modules are accepted and forwarded.

If a :via registration fails (key already taken), returns {:error, {:already_started, pid}} matching OTP's contract.

start_link(arg, module, init_arg, opts)

@spec start_link(
  {:local, atom()} | {:via, module(), term()},
  module(),
  any(),
  keyword()
) ::
  {:ok, pid()} | {:error, {:already_started, pid()}}

Erlang gen_server-style 4-arg start_link. The first argument is a registration shape:

  • {:local, atom} -- register locally as that atom (BEAM Process.register/2).
  • {:via, mod, term} -- delegate to a registry module.
  • {:global, _} -- not supported (no global name table modeled).

After registration, behaves like start_link/3. Mirrors OTP's gen_server:start_link/4.

stop(server, reason \\ :normal)

@spec stop(server() | {:via, module(), term()}, any()) :: :ok

Send a stop signal. The server runs terminate/2 is not invoked.

stop(server, reason, timeout)

@spec stop(server() | {:via, module(), term()}, any(), timeout()) :: :ok

Three-arg stop form for OTP source compatibility. Timeout ignored.