GenDurable.Testing (gen_durable v0.2.10)

Copy Markdown View Source

Inline execution and assertions for tests.

No engine, no background processes: drain/1 synchronously runs everything runnable in the calling process, through the production pick/executor path — so a test inserts, drains, delivers a signal, drains again, and asserts. Because every query runs in the test process, it composes with Ecto.Adapters.SQL.Sandbox ownership out of the box.

defmodule MyApp.CheckoutTest do
  use ExUnit.Case, async: true
  use GenDurable.Testing, repo: MyApp.Repo

  test "checkout waits for payment, then ships" do
    {:ok, id} = GenDurable.insert(Checkout, state: %{order: 42}, repo: MyApp.Repo)

    assert %{await: 1} = drain()
    assert_awaiting(id, "payment_confirmed")

    :ok = GenDurable.signal(id, "payment_confirmed", %{amount: 100}, repo: MyApp.Repo)

    assert %{done: 1} = drain()
    assert_done(id, %{"shipped" => true})
  end
end

use GenDurable.Testing, repo: MyRepo injects repo-bound wrappers for every helper below; without use, call them with the repo as the first argument. FSMs with a custom :name (or pinned old versions) need registering, exactly as with the engine's :fsms option: use GenDurable.Testing, repo: MyRepo, fsms: [My.CustomNamed] (or per call, drain(fsms: [...])).

Inline semantics vs the engine:

  • steps run one at a time, in pick order — concurrency_key serialization is trivially satisfied;
  • scheduled/backoff delays are collapsed by default (with_scheduled: true) so retries and schedule_in run immediately — an FSM that retries forever hits the max_steps cap and raises instead of hanging the test;
  • a step that raises routes to handle/2 exactly as in production; a bare exit crashes the test process (there is no reaper to catch it);
  • await timeouts do not fire by themselves (that is the reaper's tick) — fire_timeouts/1 force-fires every armed deadline.

Summary

Functions

Assert the instance is parked on a signal await — optionally on the given name(s) (a subset check against its awaits). Returns the row map.

Assert the instance is :done — and, unless :any, that its result equals result. Returns the result.

Assert the instance is :failed — and, unless :any, that last_error matches (== for strings, =~ for regexes). Returns last_error.

Assert the instance exists and has expected status. Returns the row map.

Synchronously execute runnable instances until quiescence (nothing left that can run) and return a tally of the outcomes committed

Fetch an instance for arbitrary asserts: a map with :id, :fsm, :step, :status (atom), :state, :result, :last_error, :attempt, :awaits, :queue — or nil. target is the internal id, or a correlation key (the occupied instance; falls back to the latest row carrying the key, so a finished instance is still assertable by key).

Force-fire every armed await deadline: parked instances with a timeout: go straight back to runnable (as if the deadline had passed and the reaper swept), regardless of how much time is actually left. Follow with drain/1.

Functions

assert_awaiting(repo, target, names \\ nil)

Assert the instance is parked on a signal await — optionally on the given name(s) (a subset check against its awaits). Returns the row map.

assert_done(repo, target, result \\ :any)

Assert the instance is :done — and, unless :any, that its result equals result. Returns the result.

assert_failed(repo, target, error \\ :any)

Assert the instance is :failed — and, unless :any, that last_error matches (== for strings, =~ for regexes). Returns last_error.

assert_status(repo, target, expected)

Assert the instance exists and has expected status. Returns the row map.

drain(opts)

Synchronously execute runnable instances until quiescence (nothing left that can run) and return a tally of the outcomes committed:

%{steps: 5, next: 3, retry: 0, await: 1, schedule_childs: 0, done: 1, stop: 0}

Options:

  • :repo — required (injected by use);
  • :queue — drain one queue only (default: all queues);
  • :with_scheduled — collapse future eligible_at (scheduled work, retry backoffs) so it runs immediately (default true);
  • :fsms — modules to register explicitly, as the engine's :fsms option (only for custom :names/versions — module-named FSMs resolve dynamically);
  • :max_steps — safety cap, raises when exceeded (default 1_000): an FSM that retries or transitions forever fails the test instead of hanging it.

durable(repo, target)

Fetch an instance for arbitrary asserts: a map with :id, :fsm, :step, :status (atom), :state, :result, :last_error, :attempt, :awaits, :queue — or nil. target is the internal id, or a correlation key (the occupied instance; falls back to the latest row carrying the key, so a finished instance is still assertable by key).

Test-only introspection: the public API deliberately does not expose state/result (they are GC-bounded); in a test you own the lifecycle.

fire_timeouts(repo)

Force-fire every armed await deadline: parked instances with a timeout: go straight back to runnable (as if the deadline had passed and the reaper swept), regardless of how much time is actually left. Follow with drain/1.