@@ -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