PropertyDamage.Differential (PropertyDamage v0.2.0)

View Source

Differential testing for comparing multiple implementations.

Differential testing runs the same command sequences against multiple targets (adapters) and compares results. This enables:

  • Oracle testing: Compare SUT against a reference implementation
  • Performance comparison: Compare latency/throughput across implementations
  • Regression testing: Compare old vs new versions
  • Migration validation: Compare legacy vs new systems

Basic Usage

# Oracle testing (correctness comparison)
PropertyDamage.Differential.run(
  model: MyModel,
  targets: [
    {OracleAdapter, role: :reference},
    {SUTAdapter, name: "new-impl"}
  ],
  compare: :correctness
)

# Performance comparison
PropertyDamage.Differential.run(
  model: MyModel,
  targets: [
    {ImplA, name: "redis-backend"},
    {ImplB, name: "postgres-backend"}
  ],
  compare: :performance
)

Same Adapter, Different Configurations

A key use case is comparing the same adapter with different configurations:

PropertyDamage.Differential.run(
  model: MyModel,
  targets: [
    {HTTPAdapter, role: :reference, opts: [base_url: "https://prod.example.com"]},
    {HTTPAdapter, name: "staging", opts: [base_url: "https://staging.example.com"]}
  ],
  compare: :correctness
)

Time-Separated Execution

Run against one system now, save results, compare later:

# Save baseline
PropertyDamage.Differential.run(
  model: MyModel,
  targets: [{ProdAdapter, name: "v2.3"}],
  compare: :performance,
  export_to: "baselines/v2.3.json"
)

# Later, compare against baseline
PropertyDamage.Differential.run(
  model: MyModel,
  targets: [{ProdAdapter, name: "v2.4"}],
  compare: :performance,
  baseline: "baselines/v2.3.json"
)

Execution Modes

  • :interleaved - Execute commands round-robin across targets (default for correctness)
  • :sequential - Execute full sequence on each target (default for performance)

When using baseline:, execution is implicitly sequential.

Equivalence Strategies

For correctness comparison:

  • :exact - Results must be identical (default)
  • :structural - Ignore common non-deterministic fields (id, timestamps)
  • Custom function - fn ref_result, target_result -> boolean

Summary

Functions

Run differential testing against multiple targets.

Types

compare_mode()

@type compare_mode() :: :correctness | :performance | :both

equivalence_strategy()

@type equivalence_strategy() :: :exact | :structural | (term(), term() -> boolean())

target_spec()

@type target_spec() :: {module()} | {module(), keyword()}

Functions

run(opts)

@spec run(keyword()) ::
  {:ok, PropertyDamage.Differential.Result.t()} | {:error, term()}

Run differential testing against multiple targets.

Required Options

  • :model - Model module implementing PropertyDamage.Model
  • :targets - List of target specifications (see Target Specification below)
  • :compare - Comparison mode: :correctness, :performance, or :both

Target Specification

Each target is a tuple of {AdapterModule} or {AdapterModule, opts}:

  • name: - Display name for reporting (default: derived from module)
  • role: - Set to :reference for oracle testing
  • opts: - Options passed to adapter's setup/1

Examples:

{MyAdapter}
{MyAdapter, name: "staging"}
{MyAdapter, role: :reference, opts: [url: "http://prod"]}

Optional Options

  • :max_commands - Maximum commands per sequence (default: 50)
  • :max_runs - Number of test sequences to run (default: 100)
  • :seed - Random seed for reproducibility
  • :execution - :interleaved or :sequential
  • :equivalence - Equivalence strategy (default: :exact)
  • :baseline - Path to baseline file for comparison
  • :export_to - Path to export results for future baseline
  • :metrics - Performance metrics to collect (default: [:latency, :throughput])
  • :percentiles - Latency percentiles (default: [50, 95, 99])
  • :warmup_runs - Runs to discard before measuring (default: 0)
  • :verbose - Print progress (default: false)
  • :on_progress - Progress consumer (DR-022). A 1-arity function called with a %PropertyDamage.Progress{} per run/target (data: %DifferentialUpdate{}) and once at the end with the terminal result (data: %DifferentialResult{}).

Returns

  • {:ok, %Result{}} - Differential testing completed
  • {:error, reason} - Setup or validation failed