PropertyDamage.SeedLibrary (PropertyDamage v0.2.0)

View Source

An ephemeral, self-pruning working set of recently-failing seeds that PropertyDamage.run/1 replays before random exploration (DR-023).

Its sole job is to address the probabilistic nature of property-based testing: a path that already produced a failure is replayed deterministically at the start of a run, so while you fix the bug you do not have to wait for random generation to rediscover it.

This is not a durable regression corpus. A seed reproduces its command sequence only while the model's generators are byte-stable: changing a generator, weight, when: predicate, or the command set makes a stored seed replay a different sequence. A seed is therefore a fragile, version-local pointer, not a durable test of a behavior.

  • Durable regressions belong to the Export subsystem. Exporting a failure to an ExUnit test (PropertyDamage.Export) freezes the concrete shrunk sequence, which survives generator changes. Use that for anything you want to keep.
  • The library self-cleans. Each entry tracks a consecutive_passes streak; an entry is pruned once it reaches the configured threshold (default 3). Genuinely-fixed seeds pass repeatedly and age out; flaky seeds keep failing intermittently, reset their streak, and self-retain; seeds that no longer reproduce anything (generator drift) also simply age out.

Usage

The library is wired entirely through PropertyDamage.run/1; you rarely touch this module directly.

# Enable the working set (default file) — failing seeds are replayed first
# on the next run, and any new failure's seed is appended.
PropertyDamage.run(model: M, adapter: A, seed_library: true)

# Or an explicit file
PropertyDamage.run(model: M, adapter: A, seed_library: "seeds.json")

See DR-023 for the full design.

Seed Entry Structure

Each entry contains:

  • seed - The random seed value
  • model - Model module name (descriptive)
  • failure_type - What kind of failure it last produced (descriptive)
  • check_name - Which check last failed, if applicable (descriptive)
  • tags - User-provided categorization tags
  • description - Human-readable description
  • discovered_at - When the seed was added
  • last_run - When the seed was last replayed
  • consecutive_passes - Replays in a row without a failure; reset to 0 on any failure, and the entry is pruned once it reaches the prune threshold
  • dependency_versions - Dependency versions captured at discovery (descriptive)

failure_type, check_name, and dependency_versions are inert descriptive metadata: they appear in the console banner and in stats/1/format/1 but participate in no verdict logic.

Summary

Functions

Add a seed from a failure report to the library.

Add a seed directly (without a failure report).

The default seed library filename.

The default prune threshold (K): an entry is removed after this many consecutive passing replays.

Format library for display.

Load a seed library from a JSON file.

Create a new empty seed library.

Remove entries whose consecutive_passes streak has reached k (DR-023).

Record the result of replaying a seed, updating its consecutive_passes streak (DR-023). The verdict is binary

Remove a seed from the library.

Save a seed library to a JSON file.

Get statistics about the library.

Types

seed_entry()

@type seed_entry() :: %{
  seed: integer(),
  model: String.t(),
  failure_type: atom(),
  check_name: atom() | nil,
  tags: [atom()],
  description: String.t() | nil,
  discovered_at: String.t(),
  last_run: String.t() | nil,
  consecutive_passes: non_neg_integer(),
  dependency_versions: %{required(atom()) => String.t()}
}

t()

@type t() :: %{version: integer(), entries: [seed_entry()]}

Functions

add(library, failure, opts \\ [])

@spec add(t(), PropertyDamage.FailureReport.t(), keyword()) ::
  {:ok, t()} | {:error, term()}

Add a seed from a failure report to the library.

Entries are prepended, so the library is ordered most-recently-discovered first (the order in which run/1 replays them).

Options

  • :tags - List of categorization tags (e.g., [:currency, :race_condition])
  • :description - Human-readable description of what this seed tests

Duplicate seeds are rejected with {:error, {:duplicate_seed, seed}}.

add_seed(library, seed, opts \\ [])

@spec add_seed(t(), integer(), keyword()) :: {:ok, t()} | {:error, term()}

Add a seed directly (without a failure report).

Useful for manual entry. Duplicate seeds are rejected.

Example

{:ok, library} = SeedLibrary.add_seed(library, 512902757,
  model: "ToyBankTest.Model",
  tags: [:currency_mismatch],
  description: "Captures with mismatched currencies"
)

default_file()

@spec default_file() :: Path.t()

The default seed library filename.

default_prune_threshold()

@spec default_prune_threshold() :: pos_integer()

The default prune threshold (K): an entry is removed after this many consecutive passing replays.

format(library)

@spec format(t()) :: String.t()

Format library for display.

load(path \\ "property_damage_seeds.json")

@spec load(Path.t()) :: {:ok, t()} | {:error, term()}

Load a seed library from a JSON file.

Tolerates libraries written by older versions: missing fields (including the pre-DR-023 status/run_count/fail_count tri-state) are dropped and a fresh consecutive_passes streak of 0 is assumed.

new()

@spec new() :: t()

Create a new empty seed library.

prune(library, k \\ 3)

@spec prune(t(), pos_integer()) :: {t(), non_neg_integer()}

Remove entries whose consecutive_passes streak has reached k (DR-023).

Returns {pruned_library, removed_count}.

record_run(library, seed, opts \\ [])

@spec record_run(t(), integer(), keyword()) :: t()

Record the result of replaying a seed, updating its consecutive_passes streak (DR-023). The verdict is binary:

  • a passing replay increments the streak;
  • a failing replay resets the streak to 0 and refreshes the entry's descriptive failure_type/check_name from the new report (via the :failure_type/:check_name options) so the description never goes stale.

Pruning of streaks that have reached the threshold is a separate step (prune/2), applied after a full replay pass.

Options

  • :failed - Whether the replay failed (default false)
  • :failure_type - Refreshed failure type (used only when failed: true)
  • :check_name - Refreshed check name (used only when failed: true)

remove(library, seed)

@spec remove(t(), integer()) :: {:ok, t()} | {:error, :not_found}

Remove a seed from the library.

save(library, path \\ "property_damage_seeds.json")

@spec save(t(), Path.t()) :: :ok | {:error, term()}

Save a seed library to a JSON file.

The write is atomic: the JSON is written to a temporary file in the same directory and then renamed over the destination, so a concurrent reader never observes a partially-written file.

stats(library)

@spec stats(t()) :: map()

Get statistics about the library.