Repository navigation
test: convert integration tests to in-process Command::execute() calls #352
Description
Problem
The integration test suite under tests/integration/ invokes the fgumi CLI as a subprocess (std::process::Command::new(...)) in ~120 places across 22 files, covering every user-facing command. The coverage CI job uses cargo llvm-cov nextest, which only instruments the test binary — spawned subprocesses are not instrumented. As a result, every Command::execute() body is effectively unmeasured even though it is well exercised by integration tests, which depresses reported coverage and produces misleading patch-coverage failures on PRs that touch execute() (see #351 for a recent example: 4 of 4 patch-coverage misses fell inside MarkDuplicates::execute).
Proposal
Convert the integration test suite to construct the clap-parsed command struct directly and call Command::execute() in-process. The Command trait (src/lib/commands/command.rs) is already shaped for this — &self, all I/O parameterized via PathBuf fields, returns Result<()> — and one test already does it.
Existing template
tests/integration/test_dedup_command.rs::test_dedup_no_umi_large_position_group (around line 201) is the model the rest of the suite should follow:
use clap::Parser;
use fgumi_lib::commands::command::Command as FgumiCommand;
use fgumi_lib::commands::dedup::MarkDuplicates;
let cmd = MarkDuplicates::try_parse_from([
"dedup", "--input", in_bam, "--output", out_bam, "--no-umi", "--compression-level", "1",
])?;
cmd.execute("test")?;Subprocess footprint by command
| Command | Files | Calls |
|---|---|---|
| extract | 1 | 18 |
| group | 2 | 3 |
| dedup | 1 | 3 (+1 already in-process) |
| correct | 1 | 4 |
| zipper | 2 | 9 |
| fastq | 1 | 7 |
| filter | 1 | 6 |
| clip | 1 | 2 |
| sort | 3 | 8 |
| downsample | 1 | 7 |
| simplex | 2 | 13 |
| duplex | 2 | 11 |
| codec | 1 | 6 |
| review | 1 | 3 |
| e2e / utilities | 5 | 24 |
Prerequisite refactors
Two commands need small refactors before their tests can move in-process:
zipperreadsstd::io::stdin()directly when--input=-(affects 4 tests intest_streaming_input.rs). Options: parameterize the reader, or have the tests use a temp file instead of the stdin path.comparecallsstd::process::exit(1)on a mismatch insrc/lib/commands/compare/{bams,metrics}.rs. Change toResult<bool>(or a dedicated error variant) so in-process callers can observe the result. Affectstest_compare_bams.rsand uses intest_e2e_regression.rs.
No other global-state hazards were found: no static Once/Mutex, no global env_logger init dependency, rayon pools are scoped per-command.
Phasing
Land as stacked PRs so the test-mechanics review stays separable from any logic changes:
- Phase 1 — extract, group, dedup, correct, fastq, filter, clip (~48 tests). Pure
--input/--outputPathBuf commands; straight 1:1 conversion. - Phase 2 — simplex, duplex, codec,
*-metrics, review (~28 tests). Same pattern, larger argument surfaces. - Phase 3 —
zipperrefactor + its 9 tests;comparerefactor + its tests. - Phase 4 — sort, downsample, remaining e2e files (~15 tests).
Why this beats relaxing codecov.yml
Adding patch/project thresholds would hide the symptom for #351 but leave the underlying gap: every future execute() change would land uncovered by metrics. Capturing subprocess coverage via LLVM_PROFILE_FILE=%p-%m.profraw propagation is also possible, but in-process invocation is simpler, gives faster tests (no fork/exec per case), and produces clearer failure traces (real Rust backtraces instead of "process exited with status 1").
Acceptance criteria
- All integration tests that exercise a single
fgumisubcommand callCommand::execute()in-process. cargo llvm-cov nextestreports coverage onexecute()bodies.compareandzipperrefactors land with their own focused commits.- Remaining subprocess invocations are limited to genuine pipeline tests (e.g.,
fgumi extract | samtools view, multi-binary end-to-end flows).
Activity
test: in-process Command::execute for Phase 1 commands (#352) (#353)
test: in-process Command::execute for Phase 2 commands (#352) (#354)
refactor(compare,fastq) + test: in-process Command::execute for Phase…
test: in-process Command::execute for Phase 4 commands (#352) (#356)
test: in-process follow-ups from Opus/Sonnet review of #352 (#357)
nh13 commentedon May 22, 2026
Completed across phases 1–4 plus follow-ups:
compare+fastqto returnResult<bool>)Command::newinvocations intests/integration/dropped from ~137 to 37. The remaining 37 are intentional and not in scope:test_async_reader.rs,test_bgzf_eof.rs,test_streaming_input.rs,test_zipper_command.rs::test_zipper_bam_stdin_input)AHashMapseed per process (test_group_determinism.rs,test_extract_command.rs::test_parallel_parse_determinism,test_e2e_regression.rs)fgumiitself (test_simulate_sort.rs,test_sort_correctness.rs,test_sort_write_index.rscallsamtoolsandcargo)test_compare_bams.rsexit-code assertionCoverage instrumentation for
Command::executebodies is restored.