Skip to content

Commit 4bb2803

Browse files
committed
Merge branch 'main' into pr/pinin4fjords/41
2 parents 94f7e78 + 199290d commit 4bb2803

6 files changed

Lines changed: 195 additions & 37 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,20 @@ jobs:
120120
- uses: rustsec/audit-check@v2
121121
with:
122122
token: ${{ secrets.GITHUB_TOKEN }}
123+
124+
# Check that all tests defined above pass. This makes it easy to set a single "required" test in branch
125+
# protection instead of having to update it frequently. See https://github.com/re-actors/alls-green#why.
126+
check:
127+
name: Checks pass
128+
if: always()
129+
needs:
130+
- test
131+
- fmt
132+
- clippy
133+
- msrv
134+
- audit
135+
runs-on: ubuntu-latest
136+
steps:
137+
- uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # ratchet:re-actors/alls-green@v1.2.2
138+
with:
139+
jobs: ${{ toJSON(needs) }}

src/chimeric/output.rs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ impl ChimericJunctionWriter {
2222
///
2323
/// Creates file: {prefix}Chimeric.out.junction
2424
pub fn new(prefix: &str) -> Result<Self, Error> {
25-
let mut path = PathBuf::from(prefix);
26-
path.push("Chimeric.out.junction");
25+
let path = PathBuf::from(format!("{prefix}Chimeric.out.junction"));
2726

2827
let file = File::create(&path).map_err(|e| Error::io(e, &path))?;
2928

@@ -229,23 +228,39 @@ mod tests {
229228
#[test]
230229
fn test_chimeric_junction_writer_creation() {
231230
let dir = tempdir().unwrap();
232-
let prefix = dir.path().to_str().unwrap();
231+
let prefix = format!("{}/", dir.path().display());
233232

234-
let writer = ChimericJunctionWriter::new(prefix);
233+
let writer = ChimericJunctionWriter::new(&prefix);
235234
assert!(writer.is_ok());
236235

237-
let mut path = PathBuf::from(prefix);
238-
path.push("Chimeric.out.junction");
236+
let path = PathBuf::from(format!("{prefix}Chimeric.out.junction"));
239237
assert!(path.exists());
240238
}
241239

240+
#[test]
241+
fn test_chimeric_junction_writer_bare_dot_prefix() {
242+
let dir = tempdir().unwrap();
243+
let prefix = format!("{}/SAMPLE.", dir.path().display());
244+
245+
let writer = ChimericJunctionWriter::new(&prefix);
246+
assert!(writer.is_ok());
247+
248+
let path = PathBuf::from(format!("{prefix}Chimeric.out.junction"));
249+
assert!(path.exists(), "expected {} to exist", path.display());
250+
assert!(
251+
path.file_name().unwrap().to_str().unwrap() == "SAMPLE.Chimeric.out.junction",
252+
"expected literal concatenation, got {}",
253+
path.display()
254+
);
255+
}
256+
242257
#[test]
243258
fn test_write_inter_chromosomal() {
244259
use cigar::op::{Kind, Op};
245260
let dir = tempdir().unwrap();
246-
let prefix = dir.path().to_str().unwrap();
261+
let prefix = format!("{}/", dir.path().display());
247262

248-
let mut writer = ChimericJunctionWriter::new(prefix).unwrap();
263+
let mut writer = ChimericJunctionWriter::new(&prefix).unwrap();
249264

250265
// Create mock chimeric alignment (chr9 -> chr22, BCR-ABL fusion)
251266
let donor = ChimericSegment {
@@ -290,8 +305,7 @@ mod tests {
290305
writer.flush().unwrap();
291306

292307
// Read file and verify
293-
let mut path = PathBuf::from(prefix);
294-
path.push("Chimeric.out.junction");
308+
let path = PathBuf::from(format!("{prefix}Chimeric.out.junction"));
295309

296310
let mut content = String::new();
297311
File::open(&path)
@@ -323,9 +337,9 @@ mod tests {
323337
fn test_write_strand_break() {
324338
use cigar::op::{Kind, Op};
325339
let dir = tempdir().unwrap();
326-
let prefix = dir.path().to_str().unwrap();
340+
let prefix = format!("{}/", dir.path().display());
327341

328-
let mut writer = ChimericJunctionWriter::new(prefix).unwrap();
342+
let mut writer = ChimericJunctionWriter::new(&prefix).unwrap();
329343

330344
// Create mock chimeric alignment (same chr, opposite strands)
331345
let donor = ChimericSegment {
@@ -370,8 +384,7 @@ mod tests {
370384
writer.flush().unwrap();
371385

372386
// Read file and verify
373-
let mut path = PathBuf::from(prefix);
374-
path.push("Chimeric.out.junction");
387+
let path = PathBuf::from(format!("{prefix}Chimeric.out.junction"));
375388

376389
let mut content = String::new();
377390
File::open(&path)

src/io/sam.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,11 +1162,11 @@ pub(crate) fn apply_pe_transcriptome_mate_fields(
11621162
let pos2 = (t2.genome_start + 1) as usize;
11631163
*rec1.mate_alignment_start_mut() = Some(
11641164
pos2.try_into()
1165-
.map_err(|e| Error::Alignment(format!("invalid mate position {}: {}", pos2, e)))?,
1165+
.map_err(|e| Error::Alignment(format!("invalid mate position {pos2}: {e}")))?,
11661166
);
11671167
*rec2.mate_alignment_start_mut() = Some(
11681168
pos1.try_into()
1169-
.map_err(|e| Error::Alignment(format!("invalid mate position {}: {}", pos1, e)))?,
1169+
.map_err(|e| Error::Alignment(format!("invalid mate position {pos1}: {e}")))?,
11701170
);
11711171

11721172
let tlen = calculate_insert_size(t1, t2);

src/lib.rs

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ fn align_reads(params: &Parameters) -> anyhow::Result<()> {
273273
let time_finish = chrono::Local::now();
274274

275275
// Write Log.final.out
276-
let log_path = params.out_file_name_prefix.join("Log.final.out");
276+
let log_path = params.output_path("Log.final.out");
277277
if let Some(parent) = log_path.parent() {
278278
std::fs::create_dir_all(parent)?;
279279
}
@@ -282,7 +282,7 @@ fn align_reads(params: &Parameters) -> anyhow::Result<()> {
282282

283283
// Write ReadsPerGene.out.tab if quantMode GeneCounts was requested.
284284
if let Some(ref ctx) = quant_ctx {
285-
let quant_path = params.out_file_name_prefix.join("ReadsPerGene.out.tab");
285+
let quant_path = params.output_path("ReadsPerGene.out.tab");
286286
ctx.counts.write_output(&quant_path, &ctx.gene_ann)?;
287287
info!("Wrote {}", quant_path.display());
288288
}
@@ -313,9 +313,7 @@ fn run_single_pass(
313313

314314
// Open transcriptome BAM writer if requested.
315315
let mut tr_writer: Option<BamWriter> = if let Some(tidx) = tr.as_ref() {
316-
let path = params
317-
.out_file_name_prefix
318-
.join("Aligned.toTranscriptome.out.bam");
316+
let path = params.output_path("Aligned.toTranscriptome.out.bam");
319317
info!("Writing transcriptome BAM to {}", path.display());
320318
if let Some(parent) = path.parent() {
321319
std::fs::create_dir_all(parent)?;
@@ -332,15 +330,15 @@ fn run_single_pass(
332330
let is_paired = params.read_files_in.len() == 2;
333331
let mut unmapped_w1: Option<UnmappedFastqWriter> =
334332
if params.out_reads_unmapped == OutReadsUnmapped::Fastx {
335-
let path = params.out_file_name_prefix.join("Unmapped.out.mate1");
333+
let path = params.output_path("Unmapped.out.mate1");
336334
info!("Writing unmapped reads to {}", path.display());
337335
Some(UnmappedFastqWriter::create(&path)?)
338336
} else {
339337
None
340338
};
341339
let mut unmapped_w2: Option<UnmappedFastqWriter> =
342340
if params.out_reads_unmapped == OutReadsUnmapped::Fastx && is_paired {
343-
let path = params.out_file_name_prefix.join("Unmapped.out.mate2");
341+
let path = params.output_path("Unmapped.out.mate2");
344342
info!("Writing unmapped mate2 reads to {}", path.display());
345343
Some(UnmappedFastqWriter::create(&path)?)
346344
} else {
@@ -379,7 +377,7 @@ fn run_single_pass(
379377
}
380378
OutStd::None => match out_type.format {
381379
OutSamFormat::Sam => {
382-
let output_path = params.out_file_name_prefix.join("Aligned.out.sam");
380+
let output_path = params.output_path("Aligned.out.sam");
383381
info!("Writing SAM to {}", output_path.display());
384382
if let Some(parent) = output_path.parent() {
385383
std::fs::create_dir_all(parent)?;
@@ -389,11 +387,9 @@ fn run_single_pass(
389387
OutSamFormat::Bam => {
390388
let sorted = out_type.sort_order == Some(OutSamSortOrder::SortedByCoordinate);
391389
let output_path = if sorted {
392-
params
393-
.out_file_name_prefix
394-
.join("Aligned.sortedByCoord.out.bam")
390+
params.output_path("Aligned.sortedByCoord.out.bam")
395391
} else {
396-
params.out_file_name_prefix.join("Aligned.out.bam")
392+
params.output_path("Aligned.out.bam")
397393
};
398394
info!("Writing BAM to {}", output_path.display());
399395
if let Some(parent) = output_path.parent() {
@@ -451,7 +447,7 @@ fn run_single_pass(
451447
}
452448

453449
// 5. Write SJ.out.tab file
454-
let sj_output_path = params.out_file_name_prefix.join("SJ.out.tab");
450+
let sj_output_path = params.output_path("SJ.out.tab");
455451
if !sj_stats.is_empty() {
456452
info!(
457453
"Writing splice junction statistics to {}",
@@ -480,7 +476,7 @@ fn run_two_pass(
480476
let (sj_stats_pass1, novel_junctions) = run_pass1(index, params)?;
481477

482478
// Write SJ.pass1.out.tab
483-
let pass1_path = params.out_file_name_prefix.join("SJ.pass1.out.tab");
479+
let pass1_path = params.output_path("SJ.pass1.out.tab");
484480

485481
// Create output directory if it doesn't exist
486482
if let Some(parent) = pass1_path.parent() {
@@ -887,12 +883,11 @@ fn align_reads_single_end<W: AlignmentWriter + ?Sized>(
887883
// Create chimeric output writer if enabled
888884
let mut chimeric_writer = if params.chim_segment_min > 0 && params.chim_out_junctions() {
889885
use crate::chimeric::ChimericJunctionWriter;
890-
let prefix = params.out_file_name_prefix.to_str().unwrap_or(".");
891886
info!(
892887
"Chimeric detection enabled (chimSegmentMin={})",
893888
params.chim_segment_min
894889
);
895-
Some(ChimericJunctionWriter::new(prefix)?)
890+
Some(ChimericJunctionWriter::new(&params.out_file_name_prefix)?)
896891
} else {
897892
None
898893
};
@@ -1318,12 +1313,11 @@ fn align_reads_paired_end<W: AlignmentWriter + ?Sized>(
13181313
// Create chimeric output writer if enabled
13191314
let mut chimeric_writer = if params.chim_segment_min > 0 && params.chim_out_junctions() {
13201315
use crate::chimeric::ChimericJunctionWriter;
1321-
let prefix = params.out_file_name_prefix.to_str().unwrap_or(".");
13221316
info!(
13231317
"Chimeric detection enabled (chimSegmentMin={})",
13241318
params.chim_segment_min
13251319
);
1326-
Some(ChimericJunctionWriter::new(prefix)?)
1320+
Some(ChimericJunctionWriter::new(&params.out_file_name_prefix)?)
13271321
} else {
13281322
None
13291323
};

src/params.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ pub struct Parameters {
344344
// ── Output ──────────────────────────────────────────────────────────
345345
/// Output file name prefix (including path)
346346
#[arg(long = "outFileNamePrefix", default_value = "./")]
347-
pub out_file_name_prefix: PathBuf,
347+
pub out_file_name_prefix: String,
348348

349349
/// Output type: SAM, BAM Unsorted, BAM SortedByCoordinate, None.
350350
/// Provide as space-separated tokens, e.g. "BAM SortedByCoordinate".
@@ -704,6 +704,11 @@ pub struct Parameters {
704704
}
705705

706706
impl Parameters {
707+
/// Build an output path by concatenating `suffix` onto `out_file_name_prefix`.
708+
pub fn output_path(&self, suffix: &str) -> PathBuf {
709+
PathBuf::from(format!("{}{suffix}", self.out_file_name_prefix))
710+
}
711+
707712
/// Parse the raw `--outSAMtype` tokens into a structured `OutSamType`.
708713
pub fn out_sam_type(&self) -> Result<OutSamType, String> {
709714
match self
@@ -1008,7 +1013,7 @@ mod tests {
10081013
assert_eq!(p.read_map_number, -1);
10091014
assert_eq!(p.clip5p_nbases, 0);
10101015
assert_eq!(p.clip3p_nbases, 0);
1011-
assert_eq!(p.out_file_name_prefix, PathBuf::from("./"));
1016+
assert_eq!(p.out_file_name_prefix, "./");
10121017
assert_eq!(p.out_sam_type_raw, vec!["SAM".to_string()]);
10131018
assert_eq!(p.out_sam_strand_field, "None");
10141019
assert_eq!(p.out_sam_attributes, vec!["Standard".to_string()]);
@@ -1145,7 +1150,7 @@ mod tests {
11451150
sam_type.sort_order,
11461151
Some(OutSamSortOrder::SortedByCoordinate)
11471152
);
1148-
assert_eq!(p.out_file_name_prefix, PathBuf::from("/out/sample1_"));
1153+
assert_eq!(p.out_file_name_prefix, "/out/sample1_");
11491154
assert_eq!(p.out_filter_multimap_nmax, 20);
11501155
assert_eq!(p.align_intron_max, 1_000_000);
11511156
assert_eq!(p.sjdb_gtf_file, Some(PathBuf::from("gencode.gtf")));
@@ -1487,4 +1492,50 @@ mod tests {
14871492
]);
14881493
assert_eq!(p.align_sj_stitch_mismatch_nmax, vec![1, -1, 2, 3]);
14891494
}
1495+
1496+
#[test]
1497+
fn output_path_bare_dot_prefix() {
1498+
let p = parse(&["--readFilesIn", "r.fq", "--outFileNamePrefix", "SAMPLE."]);
1499+
assert_eq!(p.out_file_name_prefix, "SAMPLE.");
1500+
assert_eq!(
1501+
p.output_path("Aligned.out.bam"),
1502+
PathBuf::from("SAMPLE.Aligned.out.bam")
1503+
);
1504+
assert_eq!(
1505+
p.output_path("Log.final.out"),
1506+
PathBuf::from("SAMPLE.Log.final.out")
1507+
);
1508+
}
1509+
1510+
#[test]
1511+
fn output_path_trailing_slash_prefix() {
1512+
let p = parse(&["--readFilesIn", "r.fq", "--outFileNamePrefix", "out/"]);
1513+
assert_eq!(
1514+
p.output_path("Aligned.out.bam"),
1515+
PathBuf::from("out/Aligned.out.bam")
1516+
);
1517+
}
1518+
1519+
#[test]
1520+
fn output_path_default_prefix() {
1521+
let p = parse(&["--readFilesIn", "r.fq"]);
1522+
assert_eq!(
1523+
p.output_path("Aligned.out.bam"),
1524+
PathBuf::from("./Aligned.out.bam")
1525+
);
1526+
}
1527+
1528+
#[test]
1529+
fn output_path_path_with_underscore() {
1530+
let p = parse(&[
1531+
"--readFilesIn",
1532+
"r.fq",
1533+
"--outFileNamePrefix",
1534+
"/out/sample1_",
1535+
]);
1536+
assert_eq!(
1537+
p.output_path("Aligned.out.bam"),
1538+
PathBuf::from("/out/sample1_Aligned.out.bam")
1539+
);
1540+
}
14901541
}

0 commit comments

Comments
 (0)