test(engine): unit tests + tarpaulin gate (#21) - #74
Merged
Merged
Conversation
Extracts the row → AssetChains folding from `fetch_chains` into a pure helper `assemble_chains(rows, now)`. The I/O surface (the ClickHouse `argMax(...) GROUP BY ...` query) keeps its boundary but the row-folding logic — unknown asset/kind drop, finite-strike filter, call+put leg merge, per-expiry year-fraction — is now reachable from unit tests without a database. Nine new tests cover: - unknown asset dropped (warn + skip) - unknown kind dropped (warn + skip) - non-finite or non-positive strike dropped (NaN, ∞, 0, negative) - call + put for same strike fold into one ChainLeg - BTC and ETH chains isolated under the outer map - multiple expiries within an asset isolated under the inner map - non-finite mid + iv get filtered to `None` - `now` parameter drives the year-fraction computation - empty input yields empty map Engine coverage on `chain.rs` jumps from 19 % (13/67) to 62 % (43/69); the remaining 26 lines are the async query path which the e2e smoke exercises end-to-end.
Strip builder — four new tests covering the regimes called out in #21: - irregular strike spacing (asymmetric, non-uniform) still produces a monotonic 801-point dense grid - very wide strike range (K spans an order of magnitude) covers the full listed range end-to-end - single-side wings (call-only above F, put-only below F) build via the IV fallback path - forward outside listed strike range rejects (the §4.3 step 3 guard against extrapolation) Sinks envelopes — three new tests pin the JSON wire shape for the `/v1/options/strip` endpoint that the Go API and methodology page consume: - `leg_envelope` field names and the `[K, Q, iv]` triple ordering - `strip_envelope` top-level `index_id` + `ts` + `near` + `next` - per-leg quote counts preserved (no truncation / aliasing between near and next) Bug found while writing the sinks tests: `strip_envelope` emits `ts` via `OffsetDateTime`'s default Serialize (numeric array) instead of the RFC 3339 form used everywhere else IndexValue is serialized. Filed as #73; the test pins "ts present in some form" so the fix-PR has a flip target.
`cargo tarpaulin` (no args) now reads `tarpaulin.toml` and reports engine-only numerics coverage. The async I/O wrappers (`chain::fetch_chains`, `sinks::IndexSinks::publish`) and the binary entry (`engine/src/main.rs`) are excluded — they are exercised by the e2e smoke (`scripts/e2e-smoke.sh`, gated by CI on every PR), not by unit tests. Counting them against the unit-test target would push us toward brittle mocked-client tests that don't catch real-world regressions. Current numerics coverage: 86.5 % (351/406 lines), passing #21's 80 % gate.
HIGH-1: `cargo fmt` run, two formatting drifts (chain.rs row() args, sinks.rs assert! lines) now match rustfmt. MED-1: tarpaulin.toml comment corrected — `chain::fetch_chains` is NOT excluded from the denominator (only main.rs + sinks.rs are). Reviewer caught the stale claim; the actual exclude list now matches the prose. MED-2: `single_side_wings_still_build_via_iv_fallback` renamed to `lower_wing_put_fallback_upper_wing_call_primary_still_build` to match what it actually exercises (only the lower wing tests the put-side fallback). Added the complementary test `upper_wing_put_fallback_still_build` so both directions of the fallback are now covered. MED-4: `rejects_when_forward_is_outside_listed_strike_range` now asserts the K_max=50 invariant up-front so a future maintainer adding a leg above K=50 fails this test loudly instead of exiting via a different `BuildError` variant. LOW-1: sinks.rs `ts` assertion tightened from `!is_null()` to `is_array() || is_string()`. Documents the current broken numeric-array shape (#73) and the post-#73 RFC 3339 string form explicitly, so the bug-fix PR flips the assertion in one line. LOW-2 rejected — `packages = ["volx-engine"]` does NOT filter the coverage scan; tarpaulin walks the engine dep graph and counts normalizer/shared-types lines unless excluded. Re-added those exclude-files entries with a comment. MED-3 not actionable — `assert_eq!(chain.legs.len(), 1)` is already present at chain.rs:323 in the assemble_drops_unknown_kind test; reviewer missed it. MED-5 + LOW-3 + LOW-4 skipped per the project's existing test-style convention (bare `unwrap()` in tests, hand-rolled fixture math).
Owner
Author
Review r1 — addressed in fixup
|
| Severity | Finding | Status |
|---|---|---|
| HIGH-1 | cargo fmt --check failures in chain.rs + sinks.rs |
Fixed — cargo fmt applied |
| MED-1 | tarpaulin.toml claimed chain::fetch_chains excluded; it wasn't |
Fixed — comment corrected, exclude list is authoritative |
| MED-2 | single_side_wings test only exercised lower-wing put-fallback |
Fixed — renamed for accuracy, added complementary upper_wing_put_fallback_still_build |
| MED-3 | assemble_drops_unknown_kind tautological without legs.len() check |
N/A — assert_eq!(chain.legs.len(), 1) already at chain.rs:323 |
| MED-4 | forward_outside test had no K_max anchor |
Fixed — added explicit K_max=50 fixture assertion |
| MED-5 | unwrap() vs expect("...") in test code |
Skipped — matches existing project style |
| LOW-1 | ts assertion too loose |
Fixed — tightened to is_array() || is_string() with explicit broken-vs-fixed comments |
| LOW-2 | normalizer/* + shared-types/* excludes redundant given packages filter |
Rejected — packages does NOT filter coverage scan, only test-run set. Tarpaulin walks the engine dep graph and counts those lines unless excluded. Re-added with explanatory comment. Verified empirically: removing them drops coverage from 86.45% to 72.23%. |
| LOW-3 | fixture_strip math obscure |
Skipped — editorial |
| LOW-4 | assemble_chains missing #[must_use] |
N/A — private fn, single caller consumes |
Coverage unchanged at 86.45 % (351/406 engine numerics lines). Engine test count up one (82 → 83) from the added case.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Drives engine-numerics coverage to 86.5 % (351/406 lines) — above #21's 80 % gate — by:
chain.rs::fetch_chainsto extract a pureassemble_chains(rows, now)helper, then exhaustively testing the folding logic without a ClickHouse connection.sinks::strip_envelope/leg_envelopeto pin the/v1/options/stripcontract.tarpaulin.tomlprofile that runscargo tarpaulin(no args) against engine numerics and excludes the async I/O wrappers + binary entry — those are covered by the e2e smoke from M1 end-to-end smoke: Deribit → ingestion → normalizer → engine → API → browser tick verification #66/feat(scripts): M1 end-to-end smoke (Deribit → ingestion → engine → API → WS) #69, not by unit tests.Closes #21.
Coverage delta
bs.rschain.rsinterpolate.rssnapshot.rsspline.rsstrip.rsvariance.rsExcluded from the denominator per
tarpaulin.toml:crates/engine/src/main.rs— binary entry, exercised byscripts/e2e-smoke.shcrates/engine/src/sinks.rs— ClickHouse + Redis publish path, exercised byscripts/e2e-smoke.shTest count delta
All 82 pass:
RUSTFLAGS=\"-D warnings\" cargo test -p volx-engineclean.The chain.rs refactor
fetch_chainswas a single async function doing both the ClickHouse query and the row-→-tree fold. Splitting them turns the latter into a pure synchronous function reachable from unit tests:The async I/O surface (~30 lines) shrinks to "build query, hand off to the helper" — straightforward enough that the e2e smoke is the right place to assert it, not a mocked-client unit test.
Strip edge-case coverage
Issue #21 listed four regimes to cover. All addressed:
rejects_chain_without_two_sided_strike,rejects_chain_with_too_few_iv(pre-existing)irregular_strike_spacing_still_buildsvery_wide_strike_range_builds_with_dense_grid_spanning_full_rangesingle_side_wings_still_build_via_iv_fallbackAlso added:
rejects_when_forward_is_outside_listed_strike_rangeto nail the §4.3 step 3 "no extrapolation" guard.Sinks envelope tests
Three tests pin the wire shape of
/v1/options/strip:leg_envelope_pins_field_names_and_quotes_triple_shape—forward,k_zero,time_to_expiry_y,quotes: [[K, Q, iv], ...]strip_envelope_wraps_two_legs_with_top_level_id_and_ts—index_idis the ticker string,tsis present,near+nextare distinct envelopesstrip_envelope_preserves_quote_count_per_leg— no truncation / cross-leg aliasingBug found while writing those tests:
strip_envelopeemitstsviaOffsetDateTime's default Serialize (a numeric array) instead of the RFC 3339 string used everywhere elseIndexValueis serialized. Filed as #73 with the fix hint inline. Test relaxed (!ts.is_null()) so the #73 PR can flip it to a proper string assertion.tarpaulin.toml
Run with
cargo tarpaulin— no flags needed.Out of scope
engine: unit tests 80% coverage (vs Python ref) #21 body's "vs Python reference notebook outputs" line). Skipped because the M0 reference notebook (research/02-bvol-replication.ipynbfrom Offline BVOL backtest, validate ±5% vs external IV benchmark #5/feat(research): BVOL backtest on real Deribit chains #36) emits no fixture file in a format the Rust tests can consume. Filing a follow-up to add a notebook → JSON dump → Rust regression test would expand the scope materially; the BS pricer + variance integral already pass analytic baselines (flat_iv_recovers_variance_within_one_percent,variance_scales_quadratically_with_iv,put_call_parity_*) which approximate the same property.tsfix in this PR (out of scope per atomic-commit rule; tracked as bug(engine): strip_envelope serializestsas integer array, not RFC 3339 string #73).Verification
RUSTFLAGS=\"-D warnings\" cargo test --workspace→ 82 engine tests pass, all other crates greenRUSTFLAGS=\"-D warnings\" cargo clippy --workspace --all-targetscleancargo tarpaulin(no args) → 86.45 % engine numerics coverageTest plan
cargo test -p volx-enginegreen (82 tests)cargo clippy --workspace --all-targets -- -D warningscleancargo tarpaulinreports ≥ 80 % on engine numerics