ExUnit case template for controlled concurrency tests.
defmodule MyConcurrencyTest do
use Lockstep.Test, iterations: 200, strategy: :fair_pct
ctest "two workers race on shared state" do
state = Lockstep.spawn(fn -> stateful_loop(0) end)
parent = self()
for _ <- 1..2 do
Lockstep.spawn(fn ->
Lockstep.send(state, {:get, self()})
val = Lockstep.recv()
Lockstep.send(state, {:put, val + 1})
Lockstep.send(parent, :done)
end)
end
Lockstep.recv()
Lockstep.recv()
Lockstep.send(state, {:get, self()})
assert Lockstep.recv() == 2
end
endOptions
:iterations(default100) — how many randomized schedules to try:strategy(default:pct) —:random | :pct | :fair_pct:seed(optional) — top-level RNG seed; per-iterationseeds are derived deterministically:max_steps(default1000) — hard limit on scheduling stepsper iteration
Summary
Functions
Define a controlled-concurrency test. The body is run repeatedly with different schedules until either all iterations pass or one finds a bug.
Run a controlled test body N times, with the body's AST automatically
rewritten to use Lockstep.* calls everywhere GenServer.call,
Task.async, send, spawn, etc. appear. Useful inside an
ordinary test block when you want to keep the body looking like
vanilla OTP.
Functions
Define a controlled-concurrency test. The body is run repeatedly with different schedules until either all iterations pass or one finds a bug.
At compile time, the body is scanned for bare send/receive/spawn
and OTP-GenServer/Task/Process calls that should be routed
through Lockstep instead. Each occurrence prints a compile-time
warning. Suppress a specific warning by qualifying the call (e.g.
Kernel.send(p, m) to mean "I really do want raw send here").
Run a controlled test body N times, with the body's AST automatically
rewritten to use Lockstep.* calls everywhere GenServer.call,
Task.async, send, spawn, etc. appear. Useful inside an
ordinary test block when you want to keep the body looking like
vanilla OTP.
test "lost update finds the race" do
assert_raise Lockstep.BugFound, fn ->
Lockstep.Test.vanilla_run iterations: 100, strategy: :pct, seed: 1 do
{:ok, c} = GenServer.start_link(MyMod.Counter, 0)
tasks = for _ <- 1..2 do
Task.async(fn ->
v = GenServer.call(c, :get)
GenServer.call(c, {:set, v + 1})
end)
end
Task.await_many(tasks)
assert GenServer.call(c, :get) == 2
end
end
end