test(codec): assert the bases the CODEC consensus tests are named for - #770
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Note Reviews pausedUse the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Included review availability: 0 reviews are currently available. Based on recent review activity, included reviews refill at 1 per hour. WalkthroughThe PR strengthens CODEC consensus tests with exact sequence, orientation, quality, masking, deletion, soft-clip, and rejection assertions. It also updates CLI documentation to describe unconditional quality overrides. ChangesCODEC consensus validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to The change strengthens CODEC consensus assertions and fixture behavior, with the reported formatting, lint, test, and doctest checks passing; no actionable merge-blocking risk remains beyond normal review. Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
Comment |
|
@coderabbitai pause |
✅ Action performedReviews paused. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #770 +/- ##
==========================================
- Coverage 94.30% 94.27% -0.03%
==========================================
Files 186 186
Lines 112416 112508 +92
==========================================
+ Hits 106013 106071 +58
- Misses 6403 6437 +34 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
54f5f10 to
4884585
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
Seven tests in the CODEC caller suite asserted less than their names claim, and all seven passed against a consensus whose bases were wrong. They were written while `create_fr_pair` produced a mostly-`N` consensus, so none of them could look at the bases; with the fixture fixed, they can. Two asserted only non-emptiness, two asserted only a length though their subject is how a CIGAR operation *shifts* bases, one never compared the reverse complement its name describes, and two asserted an `A || B` disjunction either half of which satisfied them. - `test_make_consensus_r1_deletion` / `test_make_consensus_r2_deletion` now assert the reference span with the deleted bases excised, which is what separates a deletion-aware consensus from one that ignores the operation; both are 40bp either way, so a length cannot separate them. A fixture guard asserts the shifted and unshifted spans differ. - `test_make_consensus_soft_clipping` asserts the clipped bases are retained at both ends with the aligned reference span between them. `create_fr_pair`'s fill base is now the named `PLACEHOLDER_BASE`, with static assertions that it differs from both the first reference base and the last aligned one, so the clipped bases stay distinguishable from the aligned ones at either end. - `test_make_consensus_both_soft_clipped_same_end` asserts the shared soft clip ahead of the aligned span, and — for the "fully overlapped" half of its name — that every position carries one uniform quality, i.e. that there is no single-strand tail. The value is pinned as well as its uniformity, since uniformity alone would be satisfied by a fold that collapsed every position to one wrong quality. - `test_emit_consensus_in_r1_orientation` now consenses the same reference span twice, differing only in which mate is on the minus strand, and asserts the R1-reverse consensus is the reverse complement of the R1-forward one, qualities included. The emitted flag cannot carry this: every CODEC consensus is written with a constant `UNMAPPED` flag, so the old `flag & REVERSE == 0` check held either way. - `test_mask_single_stranded_regions` asserts per position which ones were masked: the two single-strand tails at `--single-strand-qual` and the duplex middle at the quality both strands sum to, with the bases untouched throughout. Pinning the duplex value rather than bounding it below keeps a capping or rounding regression in scope. - `test_rejected_reads_tracking_enabled` asserts the rejected records are retained byte-for-byte in input order, matching the contract `test_high_duplex_disagreement_tracks_rejects` pins on the other reject path. The counters are asserted alongside but are not a substitute: they are bumped whether or not tracking is on, which is what made the old disjunction unfalsifiable. `test_reject_records_tracking` gains the tracking-disabled half of the pair. Also corrects two sets of inaccurate docs the strengthened assertions expose. `build_duplex_consensus_from_padded`'s single-strand branch keeps the covering strand's base unless that strand's quality is `MIN_PHRED`, not the "N bases" a test doc comment claimed. And `codec`'s help text described `--single-strand-qual` and `--outer-bases-qual` as capping or reducing quality in four places, where `mask_quality_regions` assigns the value unconditionally — so a position already below it is raised, the opposite of what a reader would predict. `test_mask_single_stranded_regions` now pins that assignment, making the contradiction demonstrable. Each strengthened assertion was verified non-vacuous by perturbing the behavior it now pins and confirming it fails. Six of the seven perturbations were undetected by the suite before this change. Closes #769.
4884585 to
477dbbe
Compare
Closes #769.
Seven tests in the CODEC caller suite asserted less than the behavior they are named for. All seven passed against a consensus whose bases were wrong — #763 is the demonstration: the shared fixture emitted a consensus with 25 of 30 positions no-called and not one of them failed. They were written while
create_fr_pairproduced that mostly-Nconsensus, so none of them could look at the bases. #768 fixes the fixture and addstest_fixture_consensus_reproduces_the_reference_bases, which establishes what a family should produce; this makes the seven assert it.Base branch
Based on #768 (
763/nhomer/fix-codec-fixture-read-orientation), not onmain. The strengthened assertions are only true once the fixture stores the reverse read in reference orientation, so this cannot stand alone.What each test now asserts
For each, the question was what the test's name and setup mean it to prove — not "add a base comparison".
test_make_consensus_r1_deletion5M2D25Mexcises reference 6-7.test_make_consensus_r2_deletion25M5D5Mexcising reference 36-40.test_make_consensus_soft_clippingtest_make_consensus_both_soft_clipped_same_end!is_empty()ignored entirely.test_emit_consensus_in_r1_orientationtest_mask_single_stranded_regions--single-strand-qualmasks the single-strand positions and only those.test_rejected_reads_tracking_enabled--rejectsoutput.test_high_duplex_disagreement_tracks_rejectspins on the other reject path — plus the counters.Two of these needed more than a stronger assertion.
test_emit_consensus_in_r1_orientationcould not state its claim at all with one family. It also assertedflag & REVERSE == 0, which is a tautology:build_output_record_intowrites a constantflags::UNMAPPEDfor every CODEC consensus, so that check holds no matter which strand R1 was on. The orientation lives in the bases. The relationship is structural rather than a coincidence of this reference span —consensus_reads_rawbuilds one single-strand consensus per mate and folds them in a fixed order, so swapping which mate is R1 leaves the folded consensus untouched and changes only the finalif r1_is_negative { reverse_complement_ss(..) }. The flag assertion is kept, reframed as documenting why the bases have to carry it.Neither disjunction had a genuinely reachable second branch.
test_mask_single_stranded_regions'has_low_qual || quals.is_empty()second disjunct is unreachable on a fixture that assertsoutput.count == 1two lines earlier, and the first is satisfied by masking every position — so the interesting failure was inside the branch that "passed".test_rejected_reads_tracking_enabled'!rejected_reads().is_empty() || reads_filtered > 0had both branches true, but only the first is the test's subject:reads_filteredis bumped byreject_records_count, which runs whether or not tracking is enabled, so the disjunction was satisfiable by a caller that never wrote the rejects buffer at all. Both are asserted unconditionally on the correct branch.test_reject_records_trackinggainsassert!(rejected_reads().is_empty())so the two form a real A/B on the tracking flag.The inaccurate doc claim
test_emit_consensus_in_r1_orientationcarriedNote: Positions covered by only one strand get N bases (correct duplex behavior). That is not what the caller does:build_duplex_consensus_from_padded's single-strand branches keep the covering strand's base unless that strand's quality isMIN_PHRED, which none of these fixtures reach. The comment is gone, and the new assertions encode the real behavior — every strengthened expectation spans the single-strand tails, not just the duplex middle. A second copy of the same falsehood insidetest_mask_single_stranded_regions("single-stranded bases (where abConsensus has N/qual=2) get masked") is corrected too:maskCodecConsensusQualsrewrites qualities only.Non-vacuity
Every strengthened assertion was verified by perturbing the behavior it now pins and confirming the test fails. Each perturbation was also run against the pre-change tree to establish what the suite used to catch.
r1_deletion,r2_deletionsoft_clipping,both_soft_clipped_same_endboth_soft_clipped_same_endif r1_is_negative { reverse_complement_ss(..) }re-orientationemit_consensus_in_r1_orientationmask_single_stranded_regions+ 3 unit testsrejected_reads_tracking_enabledN— i.e. the behavior the deleted comment describedSix of the seven perturbations were entirely undetected before this change. The last row is the doc correction stated as a test: the false claim now costs seven failures instead of two.
Fixture change
create_fr_pair's fill byte for query-only CIGAR operations is now the namedPLACEHOLDER_BASE(same value,b'A'), with a compile-time assertion that it differs from the first reference base. Without that, a leading soft clip would be indistinguishable from the reference span it precedes andtest_make_consensus_soft_clippingwould silently lose its subject. This is the only non-test-body change and it is not output-changing.Verification
cargo ci-fmt,cargo ci-lint,cargo ci-test(7495 passed),cargo ci-doctest— all green.Audit: other assertions in this module that cannot fail
Swept while in these tests, as #769 asks. Not changed here — the diff stays on the seven tests the issue names. The first two are worth a follow-up issue; the rest are listed so they are on the record.
Verified by perturbation:
test_not_emit_consensus_chimeric_pairpasses with the chimeric mutation deleted. Its fixture iscreate_fr_pair("read1", 100, 135, 30, …)— R1 spans 100-129 and R2 spans 135-164, so the two do not overlap at all and the family is rejected asInsufficientOverlapbeforeis_primary_fr_pair_raw's cross-chromosome check is ever consulted. I removed bothset_ref_id/set_mate_ref_idcalls and the test still passed. It needs an overlapping fixture and an assertion on the reason, the way its siblingtest_not_emit_consensus_for_rf_pairalready does.test_check_overlap_phase_indel_mismatchdiscards its result:let _result = caller.check_overlap_phase(&r1, &r2, 100, 129);, under a comment saying the outcome is indeterminate. It is not — the fixture yields29 != 24and returnsfalsedeterministically, which the siblingtest_check_overlap_phase_deletion_mismatchalready asserts.Asserted by the test harness rather than by production:
codec_downsampling_retains_lowest_hashing_pairs' "R2 must keep the same templates as R1" is guaranteed by the#[cfg(test)]downsample_pairswrapper, which derives one index vector from R1 and maps it over R2. Production caps each strand on its own ranks (codec_caps_each_strand_from_its_own_filtered_setdocuments exactly that), so the assertion states the opposite of the real contract and still cannot fail.test_downsample_pairs_no_limitreturns at the wrapper'slet ... elsebefore any production code runs.test_clip_overlap_failed_counted_and_labeled's negative guard — that the reject did not leak intoIndelErrorBetweenStrands— cannot fail, because the test's only interaction with the caller is a directreject_records_count(2, ClipOverlapFailed); nothing in it can insert the other reason.Post-condition already held / no state exercised:
test_clear_rejected_readsclears an already-empty buffer, so aclear_rejected_readsbody of{}passes.test_codec_statistics_trackingfeeds no records and asserts threeDefault-derived zeroes.Weaker than the contract the name claims:
test_mask_end_qualitiesassertsq <= 5wheremask_consensus_quals_query_basedassigns exactly 5, and never asserts the interior is untouched — so masking every base to 0 passes both loops, which is the one thing a test named for masking ends must exclude.test_build_clipped_info_clip_from_start_reverseassertsadjusted_pos > 100where the value is exactly 103; every sibling assertion in the same test is anassert_eq!.test_reverse_complement_ss_depths_errors_reversedusesACGT, whose reverse complement is itself, so the base handling passes for a no-op — and unliketo_source_read,reverse_complement_sshas no non-palindromic sibling test anywhere.test_pad_consensus_shorter_targetasserts only a length where the claim is "returned unchanged".test_build_clipped_info_zero_clip_preserves_allasserts the CIGAR has one op, not which op.test_downsample_pairsasserts only that two reads survived, not which two.test_codec_caller_creationandtest_vanilla_to_single_strand_negative_strandecho their own constructor arguments.test_consensus_reads_typed_disagreement_count'sassert!(err.is_duplex_disagreement())is implied by thematches!on the line above it.Risk: command output changes none;
unsafechanges none and CLAUDE.md allowlist changes none; memory bounds, queue capacity, and thread/backpressure policy changes none.Fix: Strengthen seven CODEC consensus tests to assert exact bases, orientations, masking, qualities, and rejected-read records.