PropertyDamage.SeedLibrary (PropertyDamage v0.2.0)
View SourceAn 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_passesstreak; 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 valuemodel- 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 tagsdescription- Human-readable descriptiondiscovered_at- When the seed was addedlast_run- When the seed was last replayedconsecutive_passes- Replays in a row without a failure; reset to 0 on any failure, and the entry is pruned once it reaches the prune thresholddependency_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
@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()} }
@type t() :: %{version: integer(), entries: [seed_entry()]}
Functions
@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 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"
)
@spec default_file() :: Path.t()
The default seed library filename.
@spec default_prune_threshold() :: pos_integer()
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.
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.
@spec new() :: t()
Create a new empty seed library.
@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 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_namefrom the new report (via the:failure_type/:check_nameoptions) 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 (defaultfalse):failure_type- Refreshed failure type (used only whenfailed: true):check_name- Refreshed check name (used only whenfailed: true)
Remove a seed from the library.
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.
Get statistics about the library.