PropertyDamage.Replay (PropertyDamage v0.2.0)
View SourceStep-by-step replay of failure sequences for debugging.
Replay mode lets you execute a failing sequence one command at a time, inspecting the state after each step. This is invaluable for understanding exactly how the system reached the failure state.
Replay is a thin stepping shell over the Executor: every command runs
through the exact same engine path as a real run (ref/placeholder resolution,
settle for probe/async commands, nemesis injection, injector and mock events,
projection updates, @trigger assertions, and stutter). This is what makes a
recorded failure replay to the identical step sequence and state.
Usage Modes
Functional Mode (Recommended for scripts)
# Get all steps at once
{:ok, steps} = PropertyDamage.replay(failure)
for step <- steps do
IO.puts("Command #{step.index}: #{step.command_name}")
IO.inspect(step.projections, label: "State")
endInteractive Mode (For LiveBook/IEx)
{:ok, session} = Replay.start(failure)
{:ok, session, step} = Replay.step(session) # Execute first command
IO.inspect(step.projections) # Inspect state
{:ok, session, step} = Replay.step(session) # Next command
# ...continue stepping...
:ok = Replay.stop(session) # Release the adapter/queueJump to Failure Point
{:ok, session} = Replay.start(failure)
{:ok, session, steps} = Replay.step_to(session, failure.failed_at_index)
# Now at the exact point where the failure occurredStep Information
Each step returns:
index- Command index in sequencecommand- The command structcommand_name- Short name for displayevents- Events produced by this command (in chronological order)projections- Projection states after this commandprojections_before- Projection states before this commandrefs- Ref resolution mapresult-:ok,{:check_failed, name, exception}, or{:error, reason}
Limitations
- Branching sequences are not steppable. Interactive stepping is linear by
nature; the fork/merge semantics of a parallel sequence cannot be reproduced
one command at a time.
start/2andrun/2return{:error, :branching_replay_unsupported}for a branchingshrunk_sequence. Inspect a branching failure via theFailureReportfields or re-run it throughPropertyDamage.Executor.run/4. - Stutter config is not stored in the report. The
FailureReportrecords the model, adapter, and sequence, but not thestutter:config a run was given. If the original run used stutter and you want it re-applied during replay, pass it viaopts(:stutter_config). It is deliberately not persisted yet: stutter is not seed-deterministic (decisions consume the process:randstream at execution time), so persisting the config alone would not make replay reproduce which commands stuttered, and a{:custom, fn}comparison cannot survive the[:safe]term decode used byPropertyDamage.Persistence. Persisting it belongs with the determinism hardening that introduces a generation-time stutter plan; see that item in the project's fix checklist. (external_markersis not a gap: real runs never set it, and external paths are derived from the event struct definitions, which are reachable from the persisted model.)
Summary
Functions
Get the current state of projections.
Format all steps for display.
Format a step for display.
Get all executed steps so far.
Get the command at a specific index (without executing).
Replay an entire failure sequence, returning all steps.
Start an interactive replay session.
Execute the next command in the sequence.
Execute commands up to (and including) the specified index.
Clean up session resources.
Types
@type step() :: %{ index: non_neg_integer(), command: struct(), command_name: String.t(), events: [struct()], projections: map(), projections_before: map(), result: :ok | {:check_failed, atom(), Exception.t()} | {:error, term()} }
@type t() :: %PropertyDamage.Replay{ adapter: module(), adapter_config: map(), adapter_context: map() | nil, commands: [struct()], current_index: integer(), event_queue: pid() | nil, exec_state: map() | nil, failure: PropertyDamage.FailureReport.t(), model: module(), status: :ready | :in_progress | :completed | :failed, steps: [step()] }
Functions
Get the current state of projections.
Format all steps for display.
Format a step for display.
Get all executed steps so far.
@spec peek(t(), non_neg_integer()) :: {:ok, struct()} | {:error, :out_of_bounds}
Get the command at a specific index (without executing).
@spec run( PropertyDamage.FailureReport.t(), keyword() ) :: {:ok, [step()]} | {:error, term()}
Replay an entire failure sequence, returning all steps.
This is the simplest way to replay - it executes all commands and returns structured information about each step.
Options
:adapter_config- Override adapter configuration:stop_on_failure- Stop at first failure (default: true):stutter_config- Stutter config to apply during replay (not stored in the report):external_markers- External markers to apply during replay (not stored in the report)
Returns
{:ok, [step]} where each step contains command, events, and state info.
Example
{:ok, steps} = PropertyDamage.replay(failure)
# Find where things went wrong
Enum.each(steps, fn step ->
IO.puts("[#{step.index}] #{step.command_name}")
case step.result do
:ok -> IO.puts(" -> OK")
{:check_failed, check, _} -> IO.puts(" -> FAILED: #{check}")
{:error, reason} -> IO.puts(" -> ERROR: #{inspect(reason)}")
end
end)
@spec start( PropertyDamage.FailureReport.t(), keyword() ) :: {:ok, t()} | {:error, term()}
Start an interactive replay session.
Sets up the adapter and an event queue, then leaves the session positioned
before the first command. Use step/1 to advance, and stop/1 when done to
tear the adapter and queue down.
Options
See run/2 for the supported options.
Example
{:ok, session} = Replay.start(failure)
{:ok, session, step1} = Replay.step(session)
{:ok, session, step2} = Replay.step(session)
:ok = Replay.stop(session)
Execute the next command in the sequence.
Returns
{:ok, session, step}- Command executed;step.resultholds the outcome, including{:check_failed, ...}/{:error, ...}when the command failed{:done, session}- No more commands to execute
@spec step_to(t(), non_neg_integer()) :: {:ok, t(), [step()]}
Execute commands up to (and including) the specified index.
Returns
{:ok, session, [step]}- Commands executed; failed commands appear as steps whoseresultis{:check_failed, ...}or{:error, ...}
@spec stop(t()) :: :ok
Clean up session resources.
Stops any pollers spawned during stepping, the event queue, tears the adapter
down, and runs teardown_each/1 if the model defines it. Safe to call more
than once.