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/2returns a raw managed pid. - No process registration. Pass the pid around explicitly.
- Callback module must implement
init/1, plus any ofhandle_call/3,handle_cast/2,handle_info/2it needs. terminate/2andcode_change/3are 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
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.
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.
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.
Failures inside init/1 raise on the spawned process and propagate
as a normal child crash to Lockstep's controller.
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:viamodules are accepted and forwarded.
If a :via registration fails (key already taken), returns
{:error, {:already_started, pid}} matching OTP's contract.
@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 (BEAMProcess.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.
Send a stop signal. The server runs terminate/2 is not invoked.
Three-arg stop form for OTP source compatibility. Timeout ignored.