Skip to content
19 changes: 11 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: CI

on: [push, pull_request]
on:
push:
branches: [main]
pull_request:

env:
CARGO_TERM_COLOR: always
Expand All @@ -9,7 +12,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
- name: Build
run: cargo build --verbose
- name: Run tests
Expand All @@ -20,19 +23,19 @@ jobs:
fmt_lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
- name: Formatting
run: cargo fmt --check
- name: Linting
run: cargo clippy --verbose
run: cargo clippy --all-features --all-targets -- -D warnings

example_grep:
runs-on: ubuntu-latest
strategy:
matrix:
ext: [bq, vbq, cbq]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
- name: run example ${{ matrix.ext }}
run: cargo run --release --example grep -- ./data/subset.${{ matrix.ext }} "ACGTACGT"

Expand All @@ -42,7 +45,7 @@ jobs:
matrix:
ext: [bq, vbq, cbq]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
- name: run example ${{ matrix.ext }}
run: cargo run --release --example parallel_range -- ./data/subset.${{ matrix.ext }} 4 30 200

Expand All @@ -52,7 +55,7 @@ jobs:
matrix:
ext: [bq, vbq, cbq]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
- name: run example (single) ${{ matrix.ext }}
run: cargo run --release --example write -- ./data/subset_R1.fastq.gz -o ./output.${{ matrix.ext }}
- name: run example (paired) ${{ matrix.ext }}
Expand All @@ -64,6 +67,6 @@ jobs:
matrix:
ext: [bq, vbq, cbq]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
- name: run example ${{ matrix.ext }}
run: cargo run --release --example read -- ./data/subset.${{ matrix.ext }}
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ paraseq = ["dep:paraseq", "dep:parking_lot"]
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
cast_possible_truncation = "allow"
cast_precision_loss = "allow"
missing_errors_doc = "allow"
struct_excessive_bools = "allow"
fn_params_excessive_bools = "allow"
20 changes: 9 additions & 11 deletions examples/auto-write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,15 @@ impl Args {
fn format(&self) -> Format {
if let Some(format) = self.format {
format
} else {
if let Some(output) = &self.output {
match output.split(".").last() {
Some("bq") => Format::Bq,
Some("vbq") => Format::Vbq,
Some("cbq") => Format::Cbq,
_ => Format::default(),
}
} else {
Format::default()
} else if let Some(output) = &self.output {
match output.split('.').next_back() {
Some("bq") => Format::Bq,
Some("vbq") => Format::Vbq,
Some("cbq") => Format::Cbq,
_ => Format::default(),
}
} else {
Format::default()
}
}
fn bitsize(&self) -> BitSize {
Expand All @@ -87,7 +85,7 @@ impl Args {
/// Creates an output file handle
fn ohandle(&self) -> Result<BoxedWriter> {
let path = if let Some(output) = &self.output {
output.to_string()
output.clone()
} else {
format!("{}{}", &self.prefix, self.format().extension())
};
Expand Down
2 changes: 1 addition & 1 deletion examples/grep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl GrepCounter {
}
impl ParallelProcessor for GrepCounter {
fn process_record<R: binseq::BinseqRecord>(&mut self, record: R) -> binseq::Result<()> {
if self.match_sequence(&record.sseq()) || self.match_sequence(&record.xseq()) {
if self.match_sequence(record.sseq()) || self.match_sequence(record.xseq()) {
self.local_count += 1;
}

Expand Down
24 changes: 12 additions & 12 deletions examples/parallel_range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@ impl ParallelProcessor for RangeProcessor {
let count = self.counter.fetch_add(1, Ordering::Relaxed);

// Print progress every 10,000 records
if count % 10_000 == 0 {
if let Some(tid) = self.tid {
println!(
"Thread {}: Processed {} records (Range: {}-{}, Index: {}, Len: {})",
tid,
count + 1,
self.range_start,
self.range_end,
record.index(),
record.sseq().len(),
);
}
if count.is_multiple_of(10_000)
&& let Some(tid) = self.tid
{
println!(
"Thread {}: Processed {} records (Range: {}-{}, Index: {}, Len: {})",
tid,
count + 1,
self.range_start,
self.range_end,
record.index(),
record.sseq().len(),
);
}

Ok(())
Expand Down
22 changes: 10 additions & 12 deletions examples/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,15 @@ impl Args {
fn format(&self) -> Format {
if let Some(format) = self.format {
format
} else {
if let Some(output) = &self.output {
match output.split(".").last() {
Some("bq") => Format::Bq,
Some("vbq") => Format::Vbq,
Some("cbq") => Format::Cbq,
_ => Format::default(),
}
} else {
Format::default()
} else if let Some(output) = &self.output {
match output.split('.').next_back() {
Some("bq") => Format::Bq,
Some("vbq") => Format::Vbq,
Some("cbq") => Format::Cbq,
_ => Format::default(),
}
} else {
Format::default()
}
}
fn bitsize(&self) -> BitSize {
Expand All @@ -98,7 +96,7 @@ impl Args {
/// Creates an output file handle
fn ohandle(&self) -> Result<BoxedWriter> {
let path = if let Some(output) = &self.output {
output.to_string()
output.clone()
} else {
format!("{}{}", &self.prefix, self.format().extension())
};
Expand Down Expand Up @@ -225,7 +223,7 @@ impl<Rf: paraseq::Record> PairedParallelProcessor<Rf> for Encoder {

fn encode_paired(args: &Args) -> Result<()> {
let mut r1 = fastx::Reader::from_path(&args.input)?;
let mut r2 = fastx::Reader::from_path(&args.input2.as_ref().expect("Missing input2"))?;
let mut r2 = fastx::Reader::from_path(args.input2.as_ref().expect("Missing input2"))?;
let ohandle = args.ohandle()?;

// prepare writer
Expand Down
136 changes: 136 additions & 0 deletions src/bq/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,139 @@ impl FileHeader {
Self::from_bytes(&buffer)
}
}

#[cfg(test)]
mod tests {
use super::*;

// ==================== FileHeaderBuilder Tests ====================

#[test]
fn test_builder_default() {
let builder = FileHeaderBuilder::default();
// Missing slen should fail to build
assert!(builder.build().is_err());
}

#[test]
fn test_builder_bitsize() {
let header = FileHeaderBuilder::new()
.slen(64)
.bitsize(BitSize::Four)
.build()
.unwrap();
assert_eq!(header.bits, BitSize::Four);
}

#[test]
fn test_builder_missing_slen() {
let result = FileHeaderBuilder::new().xlen(10).build();
assert!(result.is_err());
}

// ==================== FileHeader Constructor Tests ====================

#[test]
fn test_header_new() {
let header = FileHeader::new(BitSize::Two, 100, true);
assert_eq!(header.slen, 100);
assert_eq!(header.xlen, 0);
assert!(header.flags);
assert!(!header.is_paired());
}

#[test]
fn test_header_new_extended() {
let header = FileHeader::new_extended(BitSize::Four, 100, 50, false);
assert_eq!(header.slen, 100);
assert_eq!(header.xlen, 50);
assert_eq!(header.bits, BitSize::Four);
assert!(header.is_paired());
}

#[test]
fn test_set_bitsize() {
let mut header = FileHeader::new(BitSize::Two, 100, false);
header.set_bitsize(BitSize::Four);
assert_eq!(header.bits, BitSize::Four);
}

// ==================== from_bytes Tests ====================

#[test]
fn test_from_bytes_invalid_format_version() {
let header = FileHeader::new(BitSize::Two, 32, false);
let mut buffer = [0u8; SIZE_HEADER];
let mut cursor = std::io::Cursor::new(&mut buffer[..]);
header.write_bytes(&mut cursor).unwrap();
buffer[4] = 99; // corrupt format version
let result = FileHeader::from_bytes(&buffer);
assert!(result.is_err());
}

#[test]
fn test_from_bytes_four_bit_size() {
let header = FileHeader::new(BitSize::Four, 32, false);
let mut buffer = [0u8; SIZE_HEADER];
let mut cursor = std::io::Cursor::new(&mut buffer[..]);
header.write_bytes(&mut cursor).unwrap();
let parsed = FileHeader::from_bytes(&buffer).unwrap();
assert_eq!(parsed.bits, BitSize::Four);
}

#[test]
fn test_from_bytes_invalid_bitsize() {
let header = FileHeader::new(BitSize::Two, 32, false);
let mut buffer = [0u8; SIZE_HEADER];
let mut cursor = std::io::Cursor::new(&mut buffer[..]);
header.write_bytes(&mut cursor).unwrap();
buffer[13] = 99; // corrupt bitsize byte
let result = FileHeader::from_bytes(&buffer);
assert!(result.is_err());
}

#[test]
fn test_from_bytes_invalid_magic() {
let buffer = [0u8; SIZE_HEADER];
let result = FileHeader::from_bytes(&buffer);
assert!(result.is_err());
}

// ==================== from_buffer Tests ====================

#[test]
fn test_from_buffer_too_small() {
let buffer = [0u8; 10];
let result = FileHeader::from_buffer(&buffer);
assert!(result.is_err());
}

#[test]
fn test_from_buffer_valid() {
let header = FileHeader::new(BitSize::Two, 32, false);
let mut buffer = Vec::new();
header.write_bytes(&mut buffer).unwrap();
buffer.extend_from_slice(&[0u8; 16]); // trailing data beyond header
let parsed = FileHeader::from_buffer(&buffer).unwrap();
assert_eq!(parsed.slen, 32);
}

// ==================== from_reader Tests ====================

#[test]
fn test_from_reader_valid() {
let header = FileHeader::new_extended(BitSize::Two, 32, 16, true);
let mut buffer = Vec::new();
header.write_bytes(&mut buffer).unwrap();
let mut cursor = std::io::Cursor::new(buffer);
let parsed = FileHeader::from_reader(&mut cursor).unwrap();
assert_eq!(parsed, header);
}

#[test]
fn test_from_reader_truncated() {
let mut cursor = std::io::Cursor::new(vec![0u8; 5]);
let result = FileHeader::from_reader(&mut cursor);
assert!(result.is_err());
}
}
Loading