Lockstep.Test (Lockstep v0.1.0)

Copy Markdown View Source

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
end

Options

  • :iterations (default 100) — how many randomized schedules to try
  • :strategy (default :pct) — :random | :pct | :fair_pct

  • :seed (optional) — top-level RNG seed; per-iteration
                                  seeds are derived deterministically
  • :max_steps (default 1000) — hard limit on scheduling steps
                                  per 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

ctest(message, var \\ quote do _ end, list)

(macro)

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").

vanilla_run(opts, list)

(macro)

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