PropertyDamage.Differential (PropertyDamage v0.2.0)
View SourceDifferential 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
Types
Functions
@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:referencefor oracle testingopts:- Options passed to adapter'ssetup/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-:interleavedor: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