PropertyDamage.Persistence (PropertyDamage v0.2.0)
View SourceSave and load failure reports for later analysis and regression testing.
Failures are saved in Erlang term format (.pd files) which preserves all struct information losslessly. This enables:
- Debugging failures later without re-running tests
- Building regression test suites from discovered bugs
- Sharing failures across team members
- Tracking which bugs have been fixed
Usage
# Save a failure
{:error, failure} = PropertyDamage.run(model: M, adapter: A)
{:ok, path} = PropertyDamage.save_failure(failure, "failures/")
# Load and replay later
{:ok, failure} = PropertyDamage.load_failure(path)
PropertyDamage.replay(failure)
# List all saved failures
failures = PropertyDamage.list_failures("failures/")File Format
Files use the .pd extension and contain:
- Version header for forward compatibility
- Erlang term-encoded FailureReport struct
- Checksum for integrity verification
Naming Convention
Auto-generated filenames follow the pattern:
{timestamp}-{failure_type}-{check_name}-seed{seed}.pd
Example: 2025-12-26T14-30-00-check_failed-NonNegativeBalance-seed512902757.pd
Sensitive data
A .pd file losslessly preserves the failing run: the command structs, the
full event log, and projection state at the point of failure. If those carry
personal or otherwise sensitive data (account numbers, emails, tokens), so
does the saved file. Treat .pd files as you would the data they capture:
- Do not commit them to a public repository or attach them to a public issue.
- Scrub or synthesize sensitive fields in your commands/events before saving if the file will be shared, or keep saved failures in a controlled location.
PropertyDamage does not redact automatically; what the run touched is what the file holds.
Summary
Functions
Capture dependency versions from a failure report.
Delete a saved failure.
Export a failure to JSON format for external tools.
List all saved failures in a directory.
Load a failure report from disk.
Load a failure report, raising on version warnings.
Save a failure report to disk.
Check if a failure file is valid and loadable.
Types
Functions
@spec capture_dependency_versions(PropertyDamage.FailureReport.t()) :: %{ required(atom()) => String.t() }
Capture dependency versions from a failure report.
Extracts struct modules from commands and events, then looks up their application versions. This enables version tracking for saved test files.
Delete a saved failure.
Returns
:ok- File deleted{:error, reason}- File not found, permission denied, etc.
@spec export_json(PropertyDamage.FailureReport.t()) :: String.t()
Export a failure to JSON format for external tools.
Note: JSON export is lossy - some Elixir-specific data may be simplified.
Use save/3 for lossless storage.
List all saved failures in a directory.
Returns a list of maps with failure metadata (without loading full reports).
Options
:sort- Sort order::newest,:oldest,:seed(default::newest):filter- Filter function(metadata -> boolean)
Examples
# List all failures
failures = Persistence.list("failures/")
# => [%{path: "...", seed: 123, failure_type: :check_failed, ...}, ...]
# List only check failures
failures = Persistence.list("failures/", filter: &(&1.failure_type == :check_failed))
@spec load(Path.t()) :: {:ok, PropertyDamage.FailureReport.t()} | {:ok, PropertyDamage.FailureReport.t(), [warning()]} | {:error, term()}
Load a failure report from disk.
Returns
{:ok, report}- Successfully loaded FailureReport with no version warnings{:ok, report, warnings}- Loaded with version compatibility warnings{:error, reason}- File not found, corrupted, incompatible version, etc.
Version warnings indicate that the saved test may not reproduce correctly due to changes in PropertyDamage or dependency versions. Warnings include:
{:property_damage_version_mismatch, saved_version, current_version}{:dependency_version_mismatch, app, saved_version, current_version}{:dependency_missing, app, saved_version}
Examples
{:ok, failure} = Persistence.load("failures/currency-bug.pd")
PropertyDamage.replay(failure)
# With version warnings
{:ok, failure, warnings} = Persistence.load("failures/old-test.pd")
IO.warn("Version mismatch: #{inspect(warnings)}")
@spec load!(Path.t()) :: PropertyDamage.FailureReport.t()
Load a failure report, raising on version warnings.
Use this when you want strict version compatibility. Raises ArgumentError
if there are any version mismatches between the saved file and current
environment.
Examples
report = Persistence.load!("failures/currency-bug.pd")
@spec save(PropertyDamage.FailureReport.t(), Path.t(), save_opts()) :: {:ok, Path.t()} | {:error, term()}
Save a failure report to disk.
Options
:filename- Custom filename (default: auto-generated from failure metadata):overwrite- Whether to overwrite existing files (default: false)
Returns
{:ok, path}- Full path to saved file{:error, reason}- File already exists, directory doesn't exist, etc.
Examples
# Save with auto-generated name
{:ok, path} = Persistence.save(failure, "failures/")
# => {:ok, "failures/2025-12-26T14-30-00-check_failed-NonNegativeBalance-seed512902757.pd"}
# Save with custom name
{:ok, path} = Persistence.save(failure, "failures/", filename: "currency-bug.pd")
Check if a failure file is valid and loadable.
Performs integrity check without fully loading the report.