Skip to content

fix(cli): reject colliding output paths across every command output - #845

Merged
nh13 merged 1 commit into
mainfrom
715/nhomer/guard-stats-metrics-output-collision
Aug 21, 2026
Merged

nh13 merged 1 commit into
mainfrom
715/nhomer/guard-stats-metrics-output-collision

Conversation

@nh13

@nh13 nh13 commented Aug 20, 2026

Copy link
Copy Markdown
Member

Problem

PR #689 added reject_colliding_outputs, but it only compared --output against --rejects, and only in the consensus commands. The same collision — two of a command's outputs resolving to one destination — was unguarded for --stats, --metrics, histograms, and the --metrics PREFIX expansions. It is arguably worse there, because the secondary write happens after the BAM is complete:

fgumi simplex -o out.bam --stats out.bam   # writes a correct BAM, then truncates it to a TSV

The inventory across the CLI found ~11 commands with 2–5 output paths and no guard across the other pairs (e.g. group's -M prefix files landing on --output, dedup's three metrics files, downsample's two histograms).

Addresses #715 (items 2 and 3).

What changed

Generalized the guard (item 2). reject_colliding_outputs(output, secondary, flag) becomes a set-based reject_output_collisions(&[(path, flag)]): given every output a command will open as (path, flag-label) pairs, it rejects any two that resolve to the same destination. stdout may be named by at most one target; the null device stays exempt; the identity comparison and its error messages reuse the existing pre-open (canonical parent, file name) logic, with wording neutralized for non-BAM outputs.

Wired every multi-output command to hand the guard its full output list before any writer opens:

command outputs guarded
simplex / duplex / codec / filter --output, --rejects, --stats
correct --output, --rejects, --metrics
downsample --output, --rejects, --histogram-kept, --histogram-rejected
group --output, -f, -g, and the three --metrics PREFIX files
dedup --output, --metrics, --family-size-histogram, --duplication-ladder
clip --output, --metrics

Fixed open_pipeline_output (item 3). It re-implemented the stdout dispatch and returned a LineWriter-backed std::io::stdout(), which tears every BGZF flush at each 0x0a. It now delegates to open_output_writer — the single place the - convention is honoured for BAM output, which returns a block-buffered stdout handle.

Tests

  • Unit (common.rs): the existing output-vs-rejects cases, migrated to the set signature, plus new cases for a collision between any two outputs (--stats on --output, --metrics on --rejects), an all-distinct set, an empty set, and more-than-one-stdout.
  • End-to-end (test_streaming_output.rs): a new case per command (simplex/duplex/codec/filter --stats; correct/dedup/clip --metrics; group --family-size-histogram) runs -o out.bam <flag> out.bam and asserts it is rejected, the error names the flag and the path, and the output BAM is never created (proving the guard fires before File::create truncates).

Verified: full suite (cargo ci-test) 3532 passed / 0 failed; cargo ci-lint (-D warnings) and cargo ci-fmt clean.

Deliberately out of scope

PR #689 guarded only `--output` vs `--rejects`, and only in the consensus
commands. The same collision — two of a command's outputs resolving to one
destination — was unguarded for `--stats`, `--metrics`, histograms, and the
`--metrics PREFIX` expansions, and is worse there: the write happens after the
BAM is complete, so `fgumi simplex -o out.bam --stats out.bam` writes a correct
BAM and then truncates it to a TSV.

Generalize the pairwise guard into a set-based `reject_output_collisions` that
takes every output path a command will open as `(path, flag)` pairs and rejects
any two that resolve to one destination (stdout may be named at most once; the
null device stays exempt). Wire it into simplex, duplex, codec, filter, correct,
downsample, group (including the three `--metrics PREFIX` files, `-f`, and `-g`),
dedup, and clip, each handing the guard its full output list before any writer
opens.

Also fix `open_pipeline_output` to delegate to `open_output_writer` rather than
returning a `LineWriter`-backed `std::io::stdout()`, which tears every BGZF flush
at each 0x0a; `open_output_writer` is the single place the `-` convention is
honoured for BAM output.

Addresses #715 (items 2 and 3). Item 1 — moving identity into the writer layer
with a `dev`+`ino` check to catch symlink/hardlink/case-insensitive collisions a
pre-open path comparison cannot — remains open as a follow-up.
@nh13
nh13 deployed to github-actions August 20, 2026 08:08 — with GitHub Actions Active
@nh13

nh13 commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2a9c8fcc-d359-469b-ac39-76eb950a87c6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@nh13

nh13 commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai pause

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews paused.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.74436% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.46%. Comparing base (e51dc4b) to head (387aa1c).

Files with missing lines Patch % Lines
src/lib/commands/common.rs 95.34% 2 Missing ⚠️
src/lib/commands/dedup.rs 91.66% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main     #845    +/-   ##
========================================
  Coverage   94.46%   94.46%            
========================================
  Files         187      187            
  Lines      115301   115405   +104     
========================================
+ Hits       108921   109021   +100     
- Misses       6380     6384     +4     

☔ View full report in Codecov by Harness.
📢 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.

@nh13
nh13 added this pull request to the merge queue Aug 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 21, 2026
@nh13
nh13 added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit e28880e Aug 21, 2026
16 checks passed
@nh13
nh13 deleted the 715/nhomer/guard-stats-metrics-output-collision branch August 21, 2026 23:47

This branch was successfully deployed

1 active deployment
github-actions — 387aa1ce Deployed Aug 20, 2026 by nh13 via coverage #3755
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