PropertyDamage.Persistence (PropertyDamage v0.1.0)

View Source

Save 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

Summary

Functions

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.

Save a failure report to disk.

Check if a failure file is valid and loadable.

Types

save_opts()

@type save_opts() :: [filename: String.t(), overwrite: boolean()]

Functions

delete(path)

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

Delete a saved failure.

Returns

  • :ok - File deleted
  • {:error, reason} - File not found, permission denied, etc.

export_json(report)

@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(directory, opts \\ [])

@spec list(
  Path.t(),
  keyword()
) :: [map()]

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))

load(path)

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

Load a failure report from disk.

Returns

  • {:ok, report} - Successfully loaded FailureReport
  • {:error, reason} - File not found, corrupted, incompatible version, etc.

Examples

{:ok, failure} = Persistence.load("failures/currency-bug.pd")
PropertyDamage.replay(failure)

save(report, directory, opts \\ [])

@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")

valid?(path)

@spec valid?(Path.t()) :: boolean()

Check if a failure file is valid and loadable.

Performs integrity check without fully loading the report.