Skip to content

refactor(metrics): improve code reuse, quality, and efficiency in fgumi-metrics - #130

Merged
nh13 merged 1 commit into
mainfrom
refactor/nh/metrics-crate-improvements
Feb 28, 2026
Merged

nh13 merged 1 commit into
mainfrom
refactor/nh/metrics-crate-improvements

Conversation

@nh13

@nh13 nh13 commented Feb 28, 2026

Copy link
Copy Markdown
Member

Summary

  • Add frac()/frac_u64() safe division utilities to centralize the repeated zero-denominator-check pattern (9+ call sites)
  • Extract UmiCountTracker struct to replace 6 parallel HashMap fields in DuplexMetricsCollector, eliminating redundant .keys() + .get() lookups
  • Introduce ClipCounts struct to bundle 5 clipping parameters into a single argument
  • Add tsv_key()/kv_description() methods to RejectionReason with constant arrays and rejection_count() helper, simplifying total_rejections(), rejection_summary(), push_optional_rejections(), and to_kv_metrics()
  • Add generic read_metrics()/read_metrics_auto() to writer module, delegate from UmiCorrectionMetrics
  • Derive Default for UmiGroupingMetrics, implement AddAssign for ClippingMetrics, return [&ClippingMetrics; 5] from all_metrics(), optimize format_count() with direct string building

Test plan

  • All 1834 existing tests pass unchanged (cargo ci-test)
  • Formatting passes (cargo ci-fmt)
  • Linting passes (cargo ci-lint)
  • New unit tests for frac()/frac_u64() edge cases
  • New unit tests for UmiCountTracker record/iter/empty
  • New unit tests for ClipCounts default
  • New unit test for AddAssign on ClippingMetrics
  • New unit tests for RejectionReason::tsv_key() and kv_description()
  • New unit tests for read_metrics()/read_metrics_auto() roundtrip and error handling

@nh13
nh13 temporarily deployed to github-actions February 28, 2026 19:18 — with GitHub Actions Inactive
@codecov

codecov Bot commented Feb 28, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.61%. Comparing base (fa96485) to head (d8fc0f8).
⚠️ Report is 7 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #130      +/-   ##
==========================================
+ Coverage   83.50%   83.61%   +0.10%     
==========================================
  Files         126      126              
  Lines       51375    51516     +141     
==========================================
+ Hits        42901    43074     +173     
+ Misses       8474     8442      -32     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai

coderabbitai Bot commented Feb 28, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@nh13 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 26 minutes and 26 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between bf2db79 and d8fc0f8.

📒 Files selected for processing (10)
  • crates/fgumi-metrics/src/clip.rs
  • crates/fgumi-metrics/src/consensus.rs
  • crates/fgumi-metrics/src/correct.rs
  • crates/fgumi-metrics/src/duplex.rs
  • crates/fgumi-metrics/src/group.rs
  • crates/fgumi-metrics/src/lib.rs
  • crates/fgumi-metrics/src/rejection.rs
  • crates/fgumi-metrics/src/writer.rs
  • src/commands/clip.rs
  • src/lib/metrics/mod.rs
📝 Walkthrough

Walkthrough

This change refactors the metrics crate to centralize data aggregation and improve API consistency. ClipCounts bundles per-operation clipping counts into a single struct, replacing discrete parameters in the clipping metrics update method. UmiCountTracker consolidates per-UMI tracking across multiple HashMaps. Rejection handling now uses constant arrays to organize core, optional, and all rejection reasons. Helper functions frac() and frac_u64() provide standardized division-by-zero-safe fraction computation. New RejectionReason methods tsv_key() and kv_description() support metric key generation. Writer module gains read_metrics and read_metrics_auto functions for symmetric read/write operations. UmiGroupingMetrics now derives Default. Updates propagate these changes through command handlers and public re-exports.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main refactoring effort: improving code reuse, quality, and efficiency in fgumi-metrics through structural improvements and optimizations.
Description check ✅ Passed The description comprehensively details the specific refactoring changes (frac utilities, UmiCountTracker, ClipCounts, RejectionReason methods, writer functions, etc.) and includes a complete test plan with verification checkboxes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/nh/metrics-crate-improvements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
crates/fgumi-metrics/src/clip.rs (2)

557-573: Add one proptest for += invariants.

The example test is solid; a property test would protect additive behavior across many inputs.

Proposed property test
 #[cfg(test)]
 mod tests {
     use super::*;
     use fgumi_sam::builder::RecordBuilder;
+    use proptest::prelude::*;
@@
+    proptest! {
+        #[test]
+        fn prop_add_assign_sums_reads_and_bases(
+            r1 in 0usize..10_000,
+            r2 in 0usize..10_000,
+            b1 in 0usize..100_000,
+            b2 in 0usize..100_000,
+        ) {
+            let mut left = ClippingMetrics::new(ReadType::All);
+            left.reads = r1;
+            left.bases = b1;
+
+            let mut right = ClippingMetrics::new(ReadType::All);
+            right.reads = r2;
+            right.bases = b2;
+
+            left += &right;
+            prop_assert_eq!(left.reads, r1 + r2);
+            prop_assert_eq!(left.bases, b1 + b2);
+        }
+    }
 }

As per coding guidelines, "Use proptest for property-based testing".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fgumi-metrics/src/clip.rs` around lines 557 - 573, Add a proptest that
verifies the additive invariants of ClippingMetrics when using the += operator:
generate random ReadType, two random records (or lengths) and random ClipCounts,
build metrics_a and metrics_b via ClippingMetrics::new and update(...) calls,
then assert that metrics_a.clone() += &metrics_b yields the same result as
metrics_b.clone() += &metrics_a (commutativity) and that metrics_a += &metrics_b
equals the metrics produced by creating an empty ClippingMetrics and performing
both updates sequentially (consistency with sequential updates); place this
proptest alongside the existing test_clipping_metrics_add_assign and reference
ClippingMetrics, update, ClipCounts, ReadType, and the += operator in the test.

331-380: Refactor repetitive clip-variant tests into rstest cases.

These tests are the same shape and are easier to extend as table-driven cases.

Proposed refactor
 #[cfg(test)]
 mod tests {
     use super::*;
     use fgumi_sam::builder::RecordBuilder;
+    use rstest::rstest;
@@
-    #[test]
-    fn test_clipping_metrics_update_five_prime() {
-        let mut metrics = ClippingMetrics::new(ReadType::ReadOne);
-        let record = create_test_record("95M", false);
-        metrics.update(&record, ClipCounts { five_prime: 5, ..ClipCounts::default() });
-        assert_eq!(metrics.reads_clipped_five_prime, 1);
-        assert_eq!(metrics.bases_clipped_five_prime, 5);
-        assert_eq!(metrics.reads_clipped_post, 1);
-        assert_eq!(metrics.bases_clipped_post, 5);
-    }
-
-    #[test]
-    fn test_clipping_metrics_update_three_prime() { /* ... */ }
-    #[test]
-    fn test_clipping_metrics_update_overlapping() { /* ... */ }
-    #[test]
-    fn test_clipping_metrics_update_extending() { /* ... */ }
+    #[rstest]
+    #[case(ClipCounts { prior: 0, five_prime: 5, three_prime: 0, overlapping: 0, extending: 0 }, 1, 0, 0, 0, 5)]
+    #[case(ClipCounts { prior: 0, five_prime: 0, three_prime: 3, overlapping: 0, extending: 0 }, 0, 1, 0, 0, 3)]
+    #[case(ClipCounts { prior: 0, five_prime: 0, three_prime: 0, overlapping: 20, extending: 0 }, 0, 0, 1, 0, 20)]
+    #[case(ClipCounts { prior: 0, five_prime: 0, three_prime: 0, overlapping: 0, extending: 15 }, 0, 0, 0, 1, 15)]
+    fn test_clipping_metrics_update_single_clip_type(
+        #[case] counts: ClipCounts,
+        #[case] exp_five: usize,
+        #[case] exp_three: usize,
+        #[case] exp_overlap: usize,
+        #[case] exp_extend: usize,
+        #[case] exp_post_bases: usize,
+    ) {
+        let mut metrics = ClippingMetrics::new(ReadType::ReadOne);
+        let record = create_test_record("100M", false);
+        metrics.update(&record, counts);
+        assert_eq!(metrics.reads_clipped_five_prime, exp_five);
+        assert_eq!(metrics.reads_clipped_three_prime, exp_three);
+        assert_eq!(metrics.reads_clipped_overlapping, exp_overlap);
+        assert_eq!(metrics.reads_clipped_extending, exp_extend);
+        assert_eq!(metrics.bases_clipped_post, exp_post_bases);
+    }
 }

As per coding guidelines, "Use rstest for parameterized tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fgumi-metrics/src/clip.rs` around lines 331 - 380, Refactor the four
repetitive tests (test_clipping_metrics_update_five_prime,
test_clipping_metrics_update_three_prime,
test_clipping_metrics_update_overlapping,
test_clipping_metrics_update_extending) into a single parameterized rstest that
drives ClippingMetrics::new(ReadType::ReadOne), create_test_record, and
metrics.update(&record, ClipCounts { ... }) with a table of variants
(five_prime, three_prime, overlapping, extending) and expected counters; for
each case assert the corresponding reads_clipped_* and bases_clipped_* fields
plus reads_clipped_post and bases_clipped_post match the expected values;
reference the ClipCounts fields (five_prime, three_prime, overlapping,
extending), ClippingMetrics::update, and the existing create_test_record helper
when building the rstest cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/fgumi-metrics/src/clip.rs`:
- Around line 557-573: Add a proptest that verifies the additive invariants of
ClippingMetrics when using the += operator: generate random ReadType, two random
records (or lengths) and random ClipCounts, build metrics_a and metrics_b via
ClippingMetrics::new and update(...) calls, then assert that metrics_a.clone()
+= &metrics_b yields the same result as metrics_b.clone() += &metrics_a
(commutativity) and that metrics_a += &metrics_b equals the metrics produced by
creating an empty ClippingMetrics and performing both updates sequentially
(consistency with sequential updates); place this proptest alongside the
existing test_clipping_metrics_add_assign and reference ClippingMetrics, update,
ClipCounts, ReadType, and the += operator in the test.
- Around line 331-380: Refactor the four repetitive tests
(test_clipping_metrics_update_five_prime,
test_clipping_metrics_update_three_prime,
test_clipping_metrics_update_overlapping,
test_clipping_metrics_update_extending) into a single parameterized rstest that
drives ClippingMetrics::new(ReadType::ReadOne), create_test_record, and
metrics.update(&record, ClipCounts { ... }) with a table of variants
(five_prime, three_prime, overlapping, extending) and expected counters; for
each case assert the corresponding reads_clipped_* and bases_clipped_* fields
plus reads_clipped_post and bases_clipped_post match the expected values;
reference the ClipCounts fields (five_prime, three_prime, overlapping,
extending), ClippingMetrics::update, and the existing create_test_record helper
when building the rstest cases.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa96485 and bf2db79.

📒 Files selected for processing (10)
  • crates/fgumi-metrics/src/clip.rs
  • crates/fgumi-metrics/src/consensus.rs
  • crates/fgumi-metrics/src/correct.rs
  • crates/fgumi-metrics/src/duplex.rs
  • crates/fgumi-metrics/src/group.rs
  • crates/fgumi-metrics/src/lib.rs
  • crates/fgumi-metrics/src/rejection.rs
  • crates/fgumi-metrics/src/writer.rs
  • src/commands/clip.rs
  • src/lib/metrics/mod.rs

…mi-metrics

Add frac()/frac_u64() safe division utilities to eliminate 9+ copies of
the inline division-with-zero-check pattern. Extract UmiCountTracker to
replace 6 parallel HashMaps in DuplexMetricsCollector. Introduce
ClipCounts struct to bundle 5 clipping parameters. Add tsv_key() and
kv_description() to RejectionReason with constant arrays and
rejection_count() helper to simplify consensus metrics methods. Add
generic read_metrics()/read_metrics_auto() to writer module and delegate
from UmiCorrectionMetrics. Derive Default for UmiGroupingMetrics,
implement AddAssign for ClippingMetrics, return fixed-size array from
all_metrics(), and optimize format_count() with direct string building.
@nh13
nh13 force-pushed the refactor/nh/metrics-crate-improvements branch from bf2db79 to d8fc0f8 Compare February 28, 2026 22:59
@nh13
nh13 temporarily deployed to github-actions February 28, 2026 22:59 — with GitHub Actions Inactive
@nh13
nh13 merged commit 1bdeaa6 into main Feb 28, 2026
7 checks passed
@nh13
nh13 deleted the refactor/nh/metrics-crate-improvements branch February 28, 2026 23:52
This was referenced Feb 28, 2026

This branch was previously deployed

1 inactive deployment
github-actions — d8fc0f81 Deployed Feb 28, 2026 by nh13 via coverage #482
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant