Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 48 additions & 116 deletions src/commands/clip.rs

Large diffs are not rendered by default.

76 changes: 72 additions & 4 deletions src/commands/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ pub struct ConsensusCallingOptions {
pub min_input_base_quality: u8,

/// Produce per-base tags (cd, ce) in addition to per-read tags
#[arg(short = 'B', long = "output-per-base-tags", default_value = "true")]
#[arg(short = 'B', long = "output-per-base-tags", default_value = "true", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub output_per_base_tags: bool,

/// Quality-trim reads before consensus calling (removes low-quality bases from ends)
#[arg(long = "trim", default_value = "false")]
#[arg(long = "trim", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub trim: bool,

/// Minimum consensus base quality (output consensus bases below this are masked to N)
Expand Down Expand Up @@ -214,7 +214,7 @@ impl Default for ReadGroupOptions {
#[derive(Debug, Clone, Args)]
pub struct OverlappingConsensusOptions {
/// Consensus call overlapping bases in read pairs before UMI consensus calling
#[arg(long = "consensus-call-overlapping-bases", default_value = "true")]
#[arg(long = "consensus-call-overlapping-bases", default_value = "true", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub consensus_call_overlapping_bases: bool,
}

Expand Down Expand Up @@ -451,7 +451,7 @@ pub struct QueueMemoryOptions {
/// When true, total memory = queue-memory * threads. For example,
/// --queue-memory 768 with --threads 16 allocates 12 GB total.
/// Set to false for a fixed total memory budget regardless of thread count.
#[arg(long = "queue-memory-per-thread", default_value = "true")]
#[arg(long = "queue-memory-per-thread", default_value = "true", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub queue_memory_per_thread: bool,

/// DEPRECATED: Use --queue-memory instead. Memory limit for pipeline queues in megabytes.
Expand Down Expand Up @@ -1102,4 +1102,72 @@ mod tests {
assert!(available > 0);
assert!(available <= total);
}

use clap::Parser;

/// Test-only wrapper to exercise clap parsing of flattened Args structs.
#[derive(Debug, Parser)]
#[command(name = "test")]
struct TestBoolFlags {
#[command(flatten)]
consensus: ConsensusCallingOptions,
#[command(flatten)]
overlapping: OverlappingConsensusOptions,
#[command(flatten)]
queue_memory: QueueMemoryOptions,
}

use rstest::rstest;

#[rstest]
// --output-per-base-tags (default true)
#[case(&["test"], true)]
#[case(&["test", "--output-per-base-tags"], true)]
#[case(&["test", "--output-per-base-tags", "true"], true)]
#[case(&["test", "--output-per-base-tags", "false"], false)]
#[case(&["test", "--output-per-base-tags=true"], true)]
#[case(&["test", "--output-per-base-tags=false"], false)]
fn test_output_per_base_tags_parsing(#[case] args: &[&str], #[case] expected: bool) {
let cmd = TestBoolFlags::try_parse_from(args).unwrap();
assert_eq!(cmd.consensus.output_per_base_tags, expected);
}

#[rstest]
// --trim (default false)
#[case(&["test"], false)]
#[case(&["test", "--trim"], true)]
#[case(&["test", "--trim", "true"], true)]
#[case(&["test", "--trim", "false"], false)]
#[case(&["test", "--trim=true"], true)]
#[case(&["test", "--trim=false"], false)]
fn test_trim_parsing(#[case] args: &[&str], #[case] expected: bool) {
let cmd = TestBoolFlags::try_parse_from(args).unwrap();
assert_eq!(cmd.consensus.trim, expected);
}

#[rstest]
// --consensus-call-overlapping-bases (default true)
#[case(&["test"], true)]
#[case(&["test", "--consensus-call-overlapping-bases"], true)]
#[case(&["test", "--consensus-call-overlapping-bases", "true"], true)]
#[case(&["test", "--consensus-call-overlapping-bases", "false"], false)]
#[case(&["test", "--consensus-call-overlapping-bases=true"], true)]
#[case(&["test", "--consensus-call-overlapping-bases=false"], false)]
fn test_overlapping_bases_parsing(#[case] args: &[&str], #[case] expected: bool) {
let cmd = TestBoolFlags::try_parse_from(args).unwrap();
assert_eq!(cmd.overlapping.consensus_call_overlapping_bases, expected);
}

#[rstest]
// --queue-memory-per-thread (default true)
#[case(&["test"], true)]
#[case(&["test", "--queue-memory-per-thread"], true)]
#[case(&["test", "--queue-memory-per-thread", "true"], true)]
#[case(&["test", "--queue-memory-per-thread", "false"], false)]
#[case(&["test", "--queue-memory-per-thread=true"], true)]
#[case(&["test", "--queue-memory-per-thread=false"], false)]
fn test_queue_memory_per_thread_parsing(#[case] args: &[&str], #[case] expected: bool) {
let cmd = TestBoolFlags::try_parse_from(args).unwrap();
assert_eq!(cmd.queue_memory.queue_memory_per_thread, expected);
}
}
2 changes: 1 addition & 1 deletion src/commands/compare/bams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ pub struct CompareBams {
/// Required for comparing output from consensus commands (simplex/duplex/codec)
/// when run with --threads, as parallel processing causes non-deterministic ordering.
/// Only valid with --mode grouping.
#[arg(long = "ignore-order", default_value = "false")]
#[arg(long = "ignore-order", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub ignore_order: bool,

/// Initial buffer size for --ignore-order mode (number of records)
Expand Down
4 changes: 2 additions & 2 deletions src/commands/dedup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,7 +1073,7 @@ pub struct MarkDuplicates {
pub family_size_histogram: Option<PathBuf>,

/// Remove duplicates instead of just marking them
#[arg(short = 'r', long = "remove-duplicates", default_value = "false")]
#[arg(short = 'r', long = "remove-duplicates", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub remove_duplicates: bool,

/// The tag containing the raw UMI sequence
Expand All @@ -1097,7 +1097,7 @@ pub struct MarkDuplicates {
pub min_map_q: Option<u8>,

/// Include reads flagged as not passing QC
#[arg(short = 'n', long = "include-non-pf-reads", default_value = "false")]
#[arg(short = 'n', long = "include-non-pf-reads", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub include_non_pf_reads: bool,

/// UMI grouping strategy
Expand Down
2 changes: 1 addition & 1 deletion src/commands/downsample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub struct Downsample {
pub seed: Option<u64>,

/// Validate that MI tags appear in consecutive groups (error if seen non-consecutively)
#[arg(long = "validate-mi-order", default_value = "false")]
#[arg(long = "validate-mi-order", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub validate_mi_order: bool,

/// Output file for kept family size histogram
Expand Down
2 changes: 1 addition & 1 deletion src/commands/duplex_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ pub struct DuplexMetrics {
pub min_ba_reads: usize,

/// Collect duplex UMI counts (memory intensive)
#[arg(long = "duplex-umi-counts", default_value = "false")]
#[arg(long = "duplex-umi-counts", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub duplex_umi_counts: bool,

/// Optional intervals file to restrict analysis (BED or Picard interval list format)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/fastq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub struct Fastq {
pub input: PathBuf,

/// Don't append /1 and /2 to read names.
#[arg(short = 'n', long = "no-read-suffix", default_value = "false")]
#[arg(short = 'n', long = "no-read-suffix", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub no_suffix: bool,

/// Exclude reads with any of these flags present [0x900 = secondary|supplementary].
Expand Down
45 changes: 42 additions & 3 deletions src/commands/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,15 @@ pub struct Filter {
pub max_no_call_fraction: f64,

/// Reverse per-base tags for negative strand reads
#[arg(short = 'R', long = "reverse-per-base-tags", default_value = "false")]
#[arg(short = 'R', long = "reverse-per-base-tags", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub reverse_per_base_tags: bool,

/// Threading options for parallel processing
#[command(flatten)]
pub threading: ThreadingOptions,

/// Filter templates together (all primary reads must pass)
#[arg(long = "filter-by-template", default_value = "true")]
#[arg(long = "filter-by-template", default_value = "true", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub filter_by_template: bool,

/// Optional output BAM file for rejected reads
Expand All @@ -167,7 +167,7 @@ pub struct Filter {
pub stats: Option<PathBuf>,

/// Require single-strand agreement for duplex consensus (mask bases where AB and BA disagree)
#[arg(short = 's', long = "require-single-strand-agreement", default_value = "false")]
#[arg(short = 's', long = "require-single-strand-agreement", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub require_single_strand_agreement: bool,

/// Compression options for output BAM.
Expand Down Expand Up @@ -3948,4 +3948,43 @@ mod tests {

Ok(())
}

#[rstest]
// --reverse-per-base-tags (default false)
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1"], false)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--reverse-per-base-tags"], true)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--reverse-per-base-tags", "true"], true)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--reverse-per-base-tags", "false"], false)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--reverse-per-base-tags=true"], true)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--reverse-per-base-tags=false"], false)]
fn test_reverse_per_base_tags_parsing(#[case] args: &[&str], #[case] expected: bool) {
let cmd = Filter::try_parse_from(args).unwrap();
assert_eq!(cmd.reverse_per_base_tags, expected);
}

#[rstest]
// --filter-by-template (default true)
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1"], true)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--filter-by-template"], true)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--filter-by-template", "true"], true)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--filter-by-template", "false"], false)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--filter-by-template=true"], true)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--filter-by-template=false"], false)]
fn test_filter_by_template_parsing(#[case] args: &[&str], #[case] expected: bool) {
let cmd = Filter::try_parse_from(args).unwrap();
assert_eq!(cmd.filter_by_template, expected);
}

#[rstest]
// --require-single-strand-agreement (default false)
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1"], false)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--require-single-strand-agreement"], true)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--require-single-strand-agreement", "true"], true)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--require-single-strand-agreement", "false"], false)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--require-single-strand-agreement=true"], true)]
#[case(&["filter", "-i", "in.bam", "-o", "out.bam", "-M", "1", "--require-single-strand-agreement=false"], false)]
fn test_require_single_strand_agreement_parsing(#[case] args: &[&str], #[case] expected: bool) {
let cmd = Filter::try_parse_from(args).unwrap();
assert_eq!(cmd.require_single_strand_agreement, expected);
}
}
2 changes: 1 addition & 1 deletion src/commands/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pub struct Review {
pub sample: Option<String>,

/// Ignore N bases in consensus reads
#[arg(short = 'N', long = "ignore-ns", default_value = "false")]
#[arg(short = 'N', long = "ignore-ns", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub ignore_ns: bool,

/// Only output detailed information for variants at or below this MAF
Expand Down
4 changes: 2 additions & 2 deletions src/commands/sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ pub struct Sort {
///
/// When enabled (default), --max-memory specifies memory per thread.
/// Total memory = `max_memory` × threads. Disable for fixed total memory.
#[arg(long = "memory-per-thread", default_value = "true", action = clap::ArgAction::Set)]
#[arg(long = "memory-per-thread", default_value = "true", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub memory_per_thread: bool,

/// Temporary directory for intermediate files.
Expand Down Expand Up @@ -174,7 +174,7 @@ pub struct Sort {
/// Only valid for coordinate sort. The index file will be written to
/// `<output>.bai`. Uses single-threaded compression for accurate virtual
/// position tracking.
#[arg(long = "write-index", default_value = "false")]
#[arg(long = "write-index", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub write_index: bool,

/// Cell barcode tag for template-coordinate sort.
Expand Down
31 changes: 29 additions & 2 deletions src/commands/zipper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,14 @@ pub struct Zipper {

/// Exclude reads from the unmapped BAM that are not present in the aligned BAM.
/// Useful when reads were intentionally removed (e.g., by adapter trimming) prior to alignment.
#[arg(long = "exclude-missing-reads", default_value = "false")]
#[arg(long = "exclude-missing-reads", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub exclude_missing_reads: bool,

/// Skip adding `pa` (primary alignment) tags to secondary/supplementary reads.
/// By default, zipper adds a `pa` tag containing the primary alignment's template
/// sort key coordinates, which enables correct template-coordinate sorting and
/// deduplication of these reads. Use this flag if you don't need this functionality.
#[arg(long = "skip-pa-tags", default_value = "false")]
#[arg(long = "skip-pa-tags", default_value = "false", num_args = 0..=1, default_missing_value = "true", action = clap::ArgAction::Set)]
pub skip_pa_tags: bool,
}

Expand Down Expand Up @@ -946,6 +946,7 @@ mod tests {
use noodles::sam::alignment::record::data::field::Tag;
use noodles::sam::alignment::record_buf::RecordBuf;
use noodles::sam::alignment::record_buf::data::field::Value as BufValue;
use rstest::rstest;
use std::collections::HashMap;
use tempfile::TempDir;

Expand Down Expand Up @@ -2549,4 +2550,30 @@ mod tests {

Ok(())
}

#[rstest]
// --exclude-missing-reads (default false)
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam"], false)]
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam", "--exclude-missing-reads"], true)]
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam", "--exclude-missing-reads", "true"], true)]
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam", "--exclude-missing-reads", "false"], false)]
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam", "--exclude-missing-reads=true"], true)]
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam", "--exclude-missing-reads=false"], false)]
fn test_exclude_missing_reads_parsing(#[case] args: &[&str], #[case] expected: bool) {
let cmd = Zipper::try_parse_from(args).unwrap();
assert_eq!(cmd.exclude_missing_reads, expected);
}

#[rstest]
// --skip-pa-tags (default false)
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam"], false)]
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam", "--skip-pa-tags"], true)]
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam", "--skip-pa-tags", "true"], true)]
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam", "--skip-pa-tags", "false"], false)]
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam", "--skip-pa-tags=true"], true)]
#[case(&["zipper", "-u", "u.bam", "-r", "ref.fa", "-o", "out.bam", "--skip-pa-tags=false"], false)]
fn test_skip_pa_tags_parsing(#[case] args: &[&str], #[case] expected: bool) {
let cmd = Zipper::try_parse_from(args).unwrap();
assert_eq!(cmd.skip_pa_tags, expected);
}
}
Loading