vike
DeveloperTesting

Adding to the parity suite

The procedure for a new golden fixture, a new second-oracle gate or a new property test — where each lives, what it may assert, and the never-widen rule as a review checklist.

The parity suite is the set of tests that pin arithmetic exactly rather than approximately. This page is how to add to it. What the suite is — frozen bytes, the retired oracle, why bit-exactness is still the criterion — is The parity gate.

Where a new test goes

crates/vike-backtest/tests/parity.rs is one binary over five member files included as plain modules; the tests, their names and their fixtures are byte-unchanged from when each was its own binary, so only the --test slot moved. Filtering still works by name:

cargo test -p vike-backtest --test parity -- broker_sim_bit_parity

The eligibility rule for joining a grouped binary, stated in that header: the file must be plain — no #![cfg] feature gate, no #[ignore]d live or heavy test, no proptest regression sidecar, no process-global mutation (env::set_var, set_current_dir). A plain test run executes one binary's tests as threads in a single process, so a file mutating process state would turn independent binaries into a race. Everything else — store-backed, feature-gated, ignored, and every property suite — stays its own binary on purpose.

A new golden fixture

The frozen rounds cannot be regenerated

fixtures/r0 through r6 were exported from the Python app while it was still the source of truth, and every exporter was deleted from this tree. The committed bytes are the oracle: a changed fixture byte is a changed contract, not a refresh. r1_parity's loader asserts only that the manifest records the exporting oracle's SHA as provenance. A missing file is a deleted or moved fixture, never a stale one — and the dead exporter path stays named in the panic message on purpose, because it is the evidence for where the bytes came from. A citation whose target was genuinely deleted goes in the citation gate's exception table; it is never simply removed.

New golden expectations are still authored — just not by re-running an oracle. fixtures/recon/ is the sanctioned shape: hand-written scenarios with no manifest, auto-discovered at run time by crates/vike-exec/tests/recon/recon_fixtures.rs's recon_golden_fixtures, where adding a directory is authoring a new expectation rather than regenerating an oracle.

Whatever you add must be replayed. crates/vike-ops/tests/fixture_consumption_gate.rs fails CI on any committed file under fixtures/ that no test under crates/ consumes, unless it carries a written exception row saying why. The check is text-only and recognises three shapes: the full relative path spelled verbatim in one file; the directory and the bare file name in the same file; or a fixture under a DISCOVERY_ROOTS entry whose consumer enumerates directories and names no file. Two files each holding one half do not count. The gate exists because a retired engine took its parity suite with it and left four fixtures behind, reading as coverage and pinning nothing.

Floats go in as IEEE-754 hex bit patterns — vike_model::f64_from_hex_bits decodes them, and its doc gives the reason: fixtures carry f64::to_bits hex so a last-ulp divergence is unambiguous.

What a new assertion may claim

Two tiers, and neither is ever widened. Pure + − × ÷ sqrt paths assert exact equality, because IEEE 754 requires those operations to be correctly rounded. Outputs derived from erf, exp or log gate at ≤ 1e-12 relative — crates/vike-options/tests/oracle_parity.rs's assert_rel is the shape, and that file's header states the tier and the never-widen rule together.

Portability is the trap underneath. crates/vike-backtest/tests/libm_platform_probe.rs spells it out: IEEE 754 requires nothing of ln/exp/pow/cos, so an f64 method call reaches whichever libm the platform ships, and two platforms disagree in the last bit. Transcendentals therefore come from the libm crate, pinned per crate by a probe file. The frozen r1/r3/r4 rounds are portable by construction only because every module they drive reaches no transcendental and no powi at any arity — the parity.rs header lists those modules and tells you to grep them before trusting the paragraph.

One porting rule outlives the oracle: vike_model::py_sum is the CPython builtin sum() twin, Neumaier-compensated since CPython 3.12 rather than a naive left fold. A ported site the original wrote as builtin sum(...) over floats must use it; an explicit accumulate loop stays a naive fold.

A second oracle instead of a fixture

Where no frozen bytes exist, reconcile two implementations. engine_kernel_parity runs the event engine and the vectorized kernel over identical synthetic bars and requires the same trade count and a bit-identical final equity — its header says plainly: never widen this to a tolerance. riskgate_simbroker_parity is the risk-model twin, and its discipline is the one to copy: for each row the two sides must agree, or the row must pin the difference with a named reason. Row::divergence is None exactly when they agree, so an unpinned divergence fails the test and a newly pinned one is a loud, reviewable diff rather than a silent behaviour change.

A new property test

Property suites randomize the pure cores where golden fixtures only pin examples; the roster is on Property tests, and the decimal wire sites on Wire quantizers. Three authoring rules travel with them:

  • Bound the domain to the real one, then assert exactly. scalar_props keeps values and steps inside the price/tick range so value/step stays under f64's exact-integer ceiling, then asserts idempotence with no tolerance at all; format_props draws only terminating-decimal steps, so the internal decimal division is exact and every assertion is an exact comparison. A bounded domain buys the exactness — it is not a fudge factor.
  • Commit the regression seed. A counterexample lands as a proptest-regressions/ sidecar, and the workspace convention is to commit it so a CI-found case reproduces. That sidecar is also what disqualifies the file from a grouped binary, above.
  • A counterexample is a finding, not a spec bug. order_fsm_props's failure policy is the model: stop and report with the printed seed, and never adjust the subject to satisfy a property without an explicit decision. It also ships a generator-coverage witness, because a property over an arm the generator never reaches is vacuously green — worth copying whenever a generator enumerates a variant set.

Relaxing an existing assertion

Retiring the Python oracle licensed nothing here, and the record that retired it is what a relaxation is argued against. Four guards, all four required: the comparison stays exact — bits against bits of a stated function of the reference's own values, never a tolerance band; the reference's raw value is still bounded as the pre-image of that function; a control fails if the transformation is a no-op on the fixture; and the fixture bytes are untouched. A widened tolerance, a deleted assertion, or an "expected" value quietly recomputed from the port is not covered by that record and never was. How to read one is on Decision records.

On this page