Skip to content

Commit fc5889d

Browse files
authored
Merge pull request #96 from ArcInstitute/fix/issue-94
Fix/issue 94
2 parents cf74abd + bb749ba commit fc5889d

3 files changed

Lines changed: 56 additions & 2 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "binseq"
3-
version = "0.9.2"
3+
version = "0.9.3"
44
edition = "2024"
55
description = "A high efficiency binary format for sequencing data"
66
license = "MIT"

src/cbq/core/block.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ impl ColumnarBlock {
112112
self.num_records = 0;
113113
self.current_size = 0;
114114
self.num_npos = 0;
115+
self.len_nef = 0;
115116
}
116117

117118
// clear spans
@@ -432,6 +433,11 @@ impl ColumnarBlock {
432433
}
433434

434435
/// Decompress all columns back to native representation
436+
///
437+
/// Note: `resize` can be only be used with `copy_decode` if passing
438+
/// as `&mut [T]`. Passing a resized `&mut Vec<T>` will lead to an
439+
/// append operation, not an overwrite. If passing `&mut Vec<T>`, the
440+
/// `Vec` will be resized automatically by `copy_decode`.
435441
pub fn decompress_columns(&mut self) -> Result<()> {
436442
// decompress sequence lengths
437443
{
@@ -450,7 +456,7 @@ impl ColumnarBlock {
450456

451457
// decompress npos
452458
if !self.z_npos.is_empty() {
453-
self.ef_bytes.resize(self.len_nef, 0);
459+
self.ef_bytes.reserve(self.len_nef);
454460
copy_decode(self.z_npos.as_slice(), &mut self.ef_bytes)?;
455461

456462
let ef = EliasFano::deserialize_from(self.ef_bytes.as_slice())?;

src/cbq/write.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,54 @@ mod tests {
427427
Ok(())
428428
}
429429

430+
/// Sequences containing `N`s, which drive the Elias-Fano `npos` column.
431+
/// Blocks with no `N`s at all never populate `z_npos`, so a suite built
432+
/// only from `sample_sequences` (all ACGT) never exercises this path.
433+
fn sample_sequences_with_n(n_seq: usize, seq_len: usize) -> Vec<Vec<u8>> {
434+
const BASES: [u8; 4] = [b'A', b'C', b'G', b'T'];
435+
(0..n_seq)
436+
.map(|i| {
437+
(0..seq_len)
438+
.map(|j| {
439+
if i % 5 == 0 {
440+
b'N'
441+
} else {
442+
BASES[(i as usize + j) % 4]
443+
}
444+
})
445+
.collect::<Vec<u8>>()
446+
})
447+
.collect()
448+
}
449+
450+
/// Round-trips sequences containing `N`s through a `ColumnarBlockWriter`
451+
/// and back through a `Reader`, exercising the Elias-Fano `npos`
452+
/// decompression path in [`ColumnarBlock::decompress_columns`].
453+
#[test]
454+
fn test_roundtrip_sequences_with_n() -> Result<()> {
455+
// Small block size so N-bearing sequences span multiple blocks.
456+
let block_size = 256;
457+
let mut writer = ColumnarBlockWriter::new(Vec::new(), header(block_size))?;
458+
459+
let seqs = sample_sequences_with_n(1024, 100);
460+
assert!(
461+
seqs.iter().any(|s| s.contains(&b'N')),
462+
"test fixture must actually contain N's to exercise npos"
463+
);
464+
for seq in &seqs {
465+
writer.push(record(seq))?;
466+
}
467+
writer.finish()?;
468+
469+
let read_back = read_all_sequences(writer.inner);
470+
assert_eq!(
471+
read_back, seqs,
472+
"round-trip mismatch for N-bearing sequences"
473+
);
474+
475+
Ok(())
476+
}
477+
430478
/// `ingest_completed` on a source with no completed blocks is a no-op for
431479
/// the global writer and preserves the source's incomplete block.
432480
#[test]

0 commit comments

Comments
 (0)