1use std::fs::File;
11use std::io::Read;
12use std::ops::Range;
13use std::path::Path;
14use std::sync::Arc;
15
16use bitnuc::BitSize;
17use bytemuck::cast_slice;
18use memmap2::Mmap;
19
20use super::header::{FileHeader, SIZE_HEADER};
21use crate::{
22 BinseqRecord, DEFAULT_QUALITY_SCORE, Error, ParallelProcessor, ParallelReader,
23 error::{ReadError, Result},
24};
25
26#[derive(Clone, Copy)]
37pub struct RefRecord<'a> {
38 id: u64,
40 buffer: &'a [u64],
42 qbuf: &'a [u8],
44 config: RecordConfig,
46 header_buf: [u8; 20],
48 header_len: usize,
50}
51impl<'a> RefRecord<'a> {
52 #[must_use]
64 pub fn new(id: u64, buffer: &'a [u64], qbuf: &'a [u8], config: RecordConfig) -> Self {
65 assert_eq!(buffer.len(), config.record_size_u64());
66 Self {
67 id,
68 buffer,
69 qbuf,
70 config,
71 header_buf: [0; 20],
72 header_len: 0,
73 }
74 }
75 #[must_use]
79 pub fn config(&self) -> RecordConfig {
80 self.config
81 }
82
83 pub fn set_id(&mut self, id: &[u8]) {
84 self.header_len = id.len();
85 self.header_buf[..self.header_len].copy_from_slice(id);
86 }
87}
88
89impl BinseqRecord for RefRecord<'_> {
90 fn bitsize(&self) -> BitSize {
91 self.config.bitsize
92 }
93 fn index(&self) -> u64 {
94 self.id
95 }
96 fn sheader(&self) -> &[u8] {
98 &self.header_buf[..self.header_len]
99 }
100
101 fn xheader(&self) -> &[u8] {
103 self.sheader()
104 }
105
106 fn flag(&self) -> Option<u64> {
107 if self.config.flags {
108 Some(self.buffer[0])
109 } else {
110 None
111 }
112 }
113 fn slen(&self) -> u64 {
114 self.config.slen
115 }
116 fn xlen(&self) -> u64 {
117 self.config.xlen
118 }
119 fn sbuf(&self) -> &[u64] {
120 if self.config.flags {
121 &self.buffer[1..=(self.config.schunk as usize)]
122 } else {
123 &self.buffer[..(self.config.schunk as usize)]
124 }
125 }
126 fn xbuf(&self) -> &[u64] {
127 if self.config.flags {
128 &self.buffer[1 + self.config.schunk as usize..]
129 } else {
130 &self.buffer[self.config.schunk as usize..]
131 }
132 }
133 fn squal(&self) -> &[u8] {
134 &self.qbuf[..self.config.slen as usize]
135 }
136 fn xqual(&self) -> &[u8] {
137 &self.qbuf[..self.config.xlen as usize]
138 }
139}
140
141pub struct BatchRecord<'a> {
143 buffer: &'a [u64],
145 dbuf: &'a [u8],
147 id: u64,
149 config: RecordConfig,
151 qbuf: &'a [u8],
153 header_buf: [u8; 20],
155 header_len: usize,
157}
158impl BinseqRecord for BatchRecord<'_> {
159 fn bitsize(&self) -> BitSize {
160 self.config.bitsize
161 }
162 fn index(&self) -> u64 {
163 self.id
164 }
165 fn sheader(&self) -> &[u8] {
167 &self.header_buf[..self.header_len]
168 }
169
170 fn xheader(&self) -> &[u8] {
172 self.sheader()
173 }
174
175 fn flag(&self) -> Option<u64> {
176 if self.config.flags {
177 Some(self.buffer[0])
178 } else {
179 None
180 }
181 }
182 fn slen(&self) -> u64 {
183 self.config.slen
184 }
185 fn xlen(&self) -> u64 {
186 self.config.xlen
187 }
188 fn sbuf(&self) -> &[u64] {
189 if self.config.flags {
190 &self.buffer[1..=(self.config.schunk as usize)]
191 } else {
192 &self.buffer[..(self.config.schunk as usize)]
193 }
194 }
195 fn xbuf(&self) -> &[u64] {
196 if self.config.flags {
197 &self.buffer[1 + self.config.schunk as usize..]
198 } else {
199 &self.buffer[self.config.schunk as usize..]
200 }
201 }
202 fn decode_s(&self, dbuf: &mut Vec<u8>) -> Result<()> {
203 dbuf.extend_from_slice(self.sseq());
204 Ok(())
205 }
206 fn decode_x(&self, dbuf: &mut Vec<u8>) -> Result<()> {
207 dbuf.extend_from_slice(self.xseq());
208 Ok(())
209 }
210 fn sseq(&self) -> &[u8] {
212 let scalar = self.config.scalar();
213 let mut lbound = 0;
214 let mut rbound = self.config.slen();
215 if self.config.flags {
216 lbound += scalar;
217 rbound += scalar;
218 }
219 &self.dbuf[lbound..rbound]
220 }
221 fn xseq(&self) -> &[u8] {
223 let scalar = self.config.scalar();
224 let mut lbound = scalar * self.config.schunk();
225 let mut rbound = lbound + self.config.xlen();
226 if self.config.flags {
227 lbound += scalar;
228 rbound += scalar;
229 }
230 &self.dbuf[lbound..rbound]
231 }
232 fn squal(&self) -> &[u8] {
233 &self.qbuf[..self.config.slen()]
234 }
235 fn xqual(&self) -> &[u8] {
236 &self.qbuf[..self.config.xlen()]
237 }
238}
239
240#[derive(Clone, Copy)]
247pub struct RecordConfig {
248 slen: u64,
250 xlen: u64,
252 schunk: u64,
255 xchunk: u64,
258 bitsize: BitSize,
260 flags: bool,
262}
263impl RecordConfig {
264 pub fn new(slen: usize, xlen: usize, bitsize: BitSize, flags: bool) -> Self {
280 let (schunk, xchunk) = match bitsize {
281 BitSize::Two => (slen.div_ceil(32), xlen.div_ceil(32)),
282 BitSize::Four => (slen.div_ceil(16), xlen.div_ceil(16)),
283 };
284 Self {
285 slen: slen as u64,
286 xlen: xlen as u64,
287 schunk: schunk as u64,
288 xchunk: xchunk as u64,
289 bitsize,
290 flags,
291 }
292 }
293
294 pub fn from_header(header: &FileHeader) -> Self {
307 Self::new(
308 header.slen as usize,
309 header.xlen as usize,
310 header.bits,
311 header.flags,
312 )
313 }
314
315 pub fn paired(&self) -> bool {
319 self.xlen > 0
320 }
321
322 pub fn slen(&self) -> usize {
326 self.slen as usize
327 }
328
329 pub fn xlen(&self) -> usize {
333 self.xlen as usize
334 }
335
336 pub fn schunk(&self) -> usize {
341 self.schunk as usize
342 }
343
344 pub fn xchunk(&self) -> usize {
349 self.xchunk as usize
350 }
351
352 pub fn record_size_bytes(&self) -> usize {
355 8 * self.record_size_u64()
356 }
357
358 pub fn record_size_u64(&self) -> usize {
361 if self.flags {
362 (self.schunk + self.xchunk + 1) as usize
363 } else {
364 (self.schunk + self.xchunk) as usize
365 }
366 }
367
368 pub fn scalar(&self) -> usize {
370 match self.bitsize {
371 BitSize::Two => 32,
372 BitSize::Four => 16,
373 }
374 }
375}
376
377pub struct MmapReader {
410 mmap: Arc<Mmap>,
412
413 header: FileHeader,
415
416 config: RecordConfig,
418
419 qbuf: Vec<u8>,
421
422 default_quality_score: u8,
424}
425
426impl MmapReader {
427 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
449 let file = File::open(path)?;
451 if !file.metadata()?.is_file() {
452 return Err(ReadError::IncompatibleFile.into());
453 }
454
455 let mmap = unsafe { Mmap::map(&file)? };
457
458 let header = FileHeader::from_buffer(&mmap)?;
460
461 let config = RecordConfig::from_header(&header);
463
464 if !(mmap.len() - SIZE_HEADER).is_multiple_of(config.record_size_bytes()) {
466 return Err(ReadError::FileTruncation(mmap.len()).into());
467 }
468
469 let qbuf = vec![DEFAULT_QUALITY_SCORE; header.slen.max(header.xlen) as usize];
471
472 Ok(Self {
473 mmap: Arc::new(mmap),
474 header,
475 config,
476 qbuf,
477 default_quality_score: DEFAULT_QUALITY_SCORE,
478 })
479 }
480
481 #[must_use]
486 pub fn num_records(&self) -> usize {
487 (self.mmap.len() - SIZE_HEADER) / self.config.record_size_bytes()
488 }
489
490 #[must_use]
494 pub fn header(&self) -> FileHeader {
495 self.header
496 }
497
498 #[must_use]
500 pub fn is_paired(&self) -> bool {
501 self.header.is_paired()
502 }
503
504 pub fn set_default_quality_score(&mut self, score: u8) {
506 self.default_quality_score = score;
507 self.qbuf = self.build_qbuf();
508 }
509
510 #[must_use]
512 pub fn build_qbuf(&self) -> Vec<u8> {
513 vec![self.default_quality_score; self.header.slen.max(self.header.xlen) as usize]
514 }
515
516 pub fn get(&self, idx: usize) -> Result<RefRecord<'_>> {
531 if idx > self.num_records() {
532 return Err(ReadError::OutOfRange {
533 requested_index: idx,
534 max_index: self.num_records(),
535 }
536 .into());
537 }
538 let rsize = self.config.record_size_bytes();
539 let lbound = SIZE_HEADER + (idx * rsize);
540 let rbound = lbound + rsize;
541 let bytes = &self.mmap[lbound..rbound];
542 let buffer = cast_slice(bytes);
543 Ok(RefRecord::new(idx as u64, buffer, &self.qbuf, self.config))
544 }
545
546 pub fn get_buffer_slice(&self, range: Range<usize>) -> Result<&[u64]> {
551 if range.end > self.num_records() {
552 return Err(ReadError::OutOfRange {
553 requested_index: range.end,
554 max_index: self.num_records(),
555 }
556 .into());
557 }
558 let rsize = self.config.record_size_bytes();
559 let total_records = range.end - range.start;
560 let lbound = SIZE_HEADER + (range.start * rsize);
561 let rbound = lbound + (total_records * rsize);
562 let bytes = &self.mmap[lbound..rbound];
563 let buffer = cast_slice(bytes);
564 Ok(buffer)
565 }
566}
567
568pub struct StreamReader<R: Read> {
579 reader: R,
581
582 header: Option<FileHeader>,
584
585 config: Option<RecordConfig>,
587
588 buffer: Vec<u8>,
590
591 qbuf: Vec<u8>,
593
594 default_quality_score: u8,
596
597 buffer_pos: usize,
599
600 buffer_len: usize,
602
603 records_read: u64,
610}
611
612impl<R: Read> StreamReader<R> {
613 pub fn new(reader: R) -> Self {
626 Self::with_capacity(reader, 8192)
627 }
628
629 pub fn with_capacity(reader: R, capacity: usize) -> Self {
643 Self {
644 reader,
645 header: None,
646 config: None,
647 buffer: vec![0; capacity],
648 qbuf: vec![0; capacity],
649 buffer_pos: 0,
650 buffer_len: 0,
651 default_quality_score: DEFAULT_QUALITY_SCORE,
652 records_read: 0,
653 }
654 }
655
656 pub fn set_default_quality_score(&mut self, score: u8) {
658 if score != self.default_quality_score {
659 self.qbuf.clear();
660 }
661 self.default_quality_score = score;
662 }
663
664 pub fn read_header(&mut self) -> Result<&FileHeader> {
685 if self.header.is_none() {
686 while self.buffer_len - self.buffer_pos < SIZE_HEADER {
688 self.fill_buffer()?;
689 }
690
691 let header_slice = &self.buffer[self.buffer_pos..self.buffer_pos + SIZE_HEADER];
693 let header = FileHeader::from_buffer(header_slice)?;
694
695 self.header = Some(header);
696 self.config = Some(RecordConfig::from_header(&header));
697 self.buffer_pos += SIZE_HEADER;
698 }
699
700 Ok(self
701 .header
702 .as_ref()
703 .expect("header was just populated above"))
704 }
705
706 fn fill_buffer(&mut self) -> Result<()> {
722 if self.buffer_pos > 0 && self.buffer_pos < self.buffer_len {
724 self.buffer.copy_within(self.buffer_pos..self.buffer_len, 0);
725 self.buffer_len -= self.buffer_pos;
726 self.buffer_pos = 0;
727 } else if self.buffer_pos == self.buffer_len {
728 self.buffer_len = 0;
729 self.buffer_pos = 0;
730 }
731
732 let bytes_read = self.reader.read(&mut self.buffer[self.buffer_len..])?;
734 if bytes_read == 0 {
735 return Err(ReadError::EndOfStream.into());
736 }
737
738 self.buffer_len += bytes_read;
739 Ok(())
740 }
741
742 pub fn next_record(&mut self) -> Option<Result<RefRecord<'_>>> {
764 if self.header.is_none()
766 && let Some(e) = self.read_header().err()
767 {
768 return Some(Err(e));
769 }
770
771 let config = self
772 .config
773 .expect("Missing configuration when expected in stream");
774 let record_size = config.record_size_bytes();
775
776 while self.buffer_len - self.buffer_pos < record_size {
778 match self.fill_buffer() {
779 Ok(()) => {}
780 Err(Error::ReadError(ReadError::EndOfStream)) => {
781 if self.buffer_len - self.buffer_pos > 0 {
783 return Some(Err(ReadError::PartialRecord(
784 self.buffer_len - self.buffer_pos,
785 )
786 .into()));
787 }
788 return None;
789 }
790 Err(e) => return Some(Err(e)),
791 }
792 }
793
794 let record_start = self.buffer_pos;
796 self.buffer_pos += record_size;
797
798 let record_bytes = &self.buffer[record_start..record_start + record_size];
799 let record_u64s = cast_slice(record_bytes);
800
801 if self.qbuf.is_empty() {
803 let max_size = config.slen.max(config.xlen) as usize;
804 self.qbuf.resize(max_size, self.default_quality_score);
805 }
806
807 let id = self.records_read;
810 self.records_read += 1;
811 Some(Ok(RefRecord::new(id, record_u64s, &self.qbuf, config)))
812 }
813
814 pub fn into_inner(self) -> R {
823 self.reader
824 }
825}
826
827pub const BATCH_SIZE: usize = 1024;
832
833impl ParallelReader for MmapReader {
835 fn process_parallel<P: ParallelProcessor + Clone + 'static>(
855 self,
856 processor: P,
857 num_threads: usize,
858 ) -> Result<()> {
859 let num_records = self.num_records();
860 self.process_parallel_range(processor, num_threads, 0..num_records)
861 }
862
863 fn process_parallel_range<P: ParallelProcessor + Clone + 'static>(
884 self,
885 processor: P,
886 num_threads: usize,
887 range: Range<usize>,
888 ) -> Result<()> {
889 let num_threads = if num_threads == 0 {
891 num_cpus::get()
892 } else {
893 num_threads.min(num_cpus::get())
894 };
895
896 let num_records = self.num_records();
898 self.validate_range(num_records, &range)?;
899
900 let range_size = range.end - range.start;
902 let records_per_thread = range_size.div_ceil(num_threads);
903
904 let reader = Arc::new(self);
906
907 let mut handles = Vec::new();
909 for tid in 0..num_threads {
910 let mut processor = processor.clone();
911 let reader = reader.clone();
912 processor.set_tid(tid);
913
914 let handle = std::thread::spawn(move || -> Result<()> {
915 let start_idx = range.start + tid * records_per_thread;
916 let end_idx = (start_idx + records_per_thread).min(range.end);
917
918 if start_idx >= end_idx {
919 return Ok(()); }
921
922 let mut translater = itoa::Buffer::new();
924
925 let mut dbuf = Vec::new();
927
928 let qbuf = reader.build_qbuf();
930
931 let rsize_u64 = reader.config.record_size_bytes() / 8;
933
934 let scalar = reader.config.scalar();
936
937 let mut dbuf_rsize = { (reader.config.schunk() + reader.config.xchunk()) * scalar };
939 if reader.config.flags {
940 dbuf_rsize += scalar;
941 }
942
943 for range_start in (start_idx..end_idx).step_by(BATCH_SIZE) {
945 let range_end = (range_start + BATCH_SIZE).min(end_idx);
946
947 dbuf.clear();
949
950 let ebuf = reader.get_buffer_slice(range_start..range_end)?;
952
953 reader
955 .config
956 .bitsize
957 .decode(ebuf, ebuf.len() * scalar, &mut dbuf)?;
958
959 for (inner_idx, idx) in (range_start..range_end).enumerate() {
961 let id_str = translater.format(idx);
963
964 let mut header_buf = [0; 20];
966 let header_len = id_str.len();
967 header_buf[..header_len].copy_from_slice(id_str.as_bytes());
968
969 let ebuf_start = inner_idx * rsize_u64;
971 let dbuf_start = inner_idx * dbuf_rsize;
972
973 let record = BatchRecord {
975 buffer: &ebuf[ebuf_start..(ebuf_start + rsize_u64)],
976 dbuf: &dbuf[dbuf_start..(dbuf_start + dbuf_rsize)],
977 qbuf: &qbuf,
978 id: idx as u64,
979 config: reader.config,
980 header_buf,
981 header_len,
982 };
983
984 processor.process_record(record)?;
986 }
987
988 processor.on_batch_complete()?;
990 }
991
992 processor.on_thread_complete()?;
994
995 Ok(())
996 });
997
998 handles.push(handle);
999 }
1000
1001 for handle in handles {
1002 handle
1003 .join()
1004 .expect("Error joining handle (1)")
1005 .expect("Error joining handle (2)");
1006 }
1007
1008 Ok(())
1009 }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014 use super::*;
1015 use crate::BinseqRecord;
1016 use bitnuc::BitSize;
1017
1018 const TEST_BQ_FILE: &str = "./data/subset.bq";
1019
1020 #[test]
1023 fn test_mmap_reader_new() {
1024 let reader = MmapReader::new(TEST_BQ_FILE);
1025 assert!(reader.is_ok(), "Failed to create reader");
1026 }
1027
1028 #[test]
1029 fn test_mmap_reader_num_records() {
1030 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1031 let num_records = reader.num_records();
1032 assert!(num_records > 0, "Expected non-zero records");
1033 }
1034
1035 #[test]
1036 fn test_mmap_reader_is_paired() {
1037 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1038 assert!(reader.is_paired());
1040 }
1041
1042 #[test]
1043 fn test_mmap_reader_header_access() {
1044 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1045 let header = reader.header();
1046 assert!(header.slen > 0, "Expected non-zero sequence length");
1047 }
1048
1049 #[test]
1050 fn test_mmap_reader_config_access() {
1051 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1052 let header = reader.header();
1053 let config = RecordConfig::from_header(&header);
1054 assert!(
1055 config.slen > 0,
1056 "Expected non-zero sequence length in config"
1057 );
1058 }
1059
1060 #[test]
1063 fn test_get_record() {
1064 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1065 let num_records = reader.num_records();
1066
1067 if num_records > 0 {
1068 let record = reader.get(0);
1069 assert!(record.is_ok(), "Expected to get first record");
1070
1071 let record = record.unwrap();
1072 assert_eq!(record.index(), 0, "Expected record index to be 0");
1073 }
1074 }
1075
1076 #[test]
1077 fn test_get_record_out_of_bounds() {
1078 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1079 let num_records = reader.num_records();
1080
1081 let record = reader.get(num_records + 100);
1082 assert!(record.is_err(), "Expected error for out of bounds index");
1083 }
1084
1085 #[test]
1086 fn test_record_sequence_data() {
1087 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1088
1089 if let Ok(record) = reader.get(0) {
1090 let sbuf = record.sbuf();
1091 assert!(!sbuf.is_empty(), "Expected non-empty sequence buffer");
1092
1093 let slen = record.slen();
1094 assert!(slen > 0, "Expected non-zero sequence length");
1095 }
1096 }
1097
1098 #[test]
1099 fn test_record_quality_data() {
1100 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1101
1102 if let Ok(record) = reader.get(0) {
1103 let squal = record.squal();
1104 let slen = record.slen() as usize;
1105 assert_eq!(
1106 squal.len(),
1107 slen,
1108 "Quality length should match sequence length"
1109 );
1110 }
1111 }
1112
1113 #[test]
1116 fn test_set_default_quality_score() {
1117 let mut reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1118 let custom_score = 42u8;
1119
1120 reader.set_default_quality_score(custom_score);
1121
1122 if let Ok(record) = reader.get(0) {
1123 let squal = record.squal();
1124 assert!(
1126 squal.iter().all(|&q| q == custom_score),
1127 "All quality scores should be {custom_score}"
1128 );
1129 }
1130 }
1131
1132 #[derive(Clone)]
1135 struct CountingProcessor {
1136 count: Arc<std::sync::Mutex<usize>>,
1137 }
1138
1139 impl ParallelProcessor for CountingProcessor {
1140 fn process_record<R: BinseqRecord>(&mut self, _record: R) -> Result<()> {
1141 let mut count = self.count.lock().unwrap();
1142 *count += 1;
1143 Ok(())
1144 }
1145 }
1146
1147 #[test]
1148 fn test_parallel_processing() {
1149 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1150 let num_records = reader.num_records();
1151
1152 let count = Arc::new(std::sync::Mutex::new(0));
1153 let processor = CountingProcessor {
1154 count: count.clone(),
1155 };
1156
1157 reader.process_parallel(processor, 2).unwrap();
1158
1159 let final_count = *count.lock().unwrap();
1160 assert_eq!(final_count, num_records, "All records should be processed");
1161 }
1162
1163 #[test]
1164 fn test_parallel_processing_range() {
1165 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1166 let num_records = reader.num_records();
1167
1168 if num_records >= 100 {
1169 let start = 10;
1170 let end = 50;
1171 let expected_count = end - start;
1172
1173 let count = Arc::new(std::sync::Mutex::new(0));
1174 let processor = CountingProcessor {
1175 count: count.clone(),
1176 };
1177
1178 reader
1179 .process_parallel_range(processor, 2, start..end)
1180 .unwrap();
1181
1182 let final_count = *count.lock().unwrap();
1183 assert_eq!(
1184 final_count, expected_count,
1185 "Should process exactly {expected_count} records"
1186 );
1187 }
1188 }
1189
1190 #[test]
1193 fn test_record_config_from_header() {
1194 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1195 let header = reader.header();
1196 let config = RecordConfig::from_header(&header);
1197
1198 assert_eq!(
1199 config.slen,
1200 u64::from(header.slen),
1201 "Sequence length mismatch"
1202 );
1203 assert_eq!(
1204 config.xlen,
1205 u64::from(header.xlen),
1206 "Extended length mismatch"
1207 );
1208 assert_eq!(config.bitsize, header.bits, "Bit size mismatch");
1209 }
1210
1211 #[test]
1212 fn test_record_config_record_size() {
1213 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1214 let header = reader.header();
1215 let config = RecordConfig::from_header(&header);
1216
1217 let size_u64 = config.record_size_u64();
1218 assert!(size_u64 > 0, "Record size should be non-zero");
1219
1220 let size_bytes = config.record_size_bytes();
1221 assert_eq!(size_bytes, size_u64 * 8, "Byte size should be 8x u64 size");
1222 }
1223
1224 #[test]
1227 fn test_ref_record_bitsize() {
1228 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1229
1230 if let Ok(record) = reader.get(0) {
1231 let bitsize = record.bitsize();
1232 assert!(
1233 matches!(bitsize, BitSize::Two | BitSize::Four),
1234 "Bitsize should be Two or Four"
1235 );
1236 }
1237 }
1238
1239 #[test]
1240 fn test_ref_record_flag() {
1241 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1242
1243 if let Ok(record) = reader.get(0) {
1244 let flag = record.flag();
1245 assert!(flag.is_some() || flag.is_none()); }
1248 }
1249
1250 #[test]
1251 fn test_ref_record_paired_data() {
1252 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1253
1254 if reader.is_paired()
1255 && let Ok(record) = reader.get(0)
1256 {
1257 let xbuf = record.xbuf();
1258 let xlen = record.xlen();
1259
1260 if xlen > 0 {
1261 assert!(
1262 !xbuf.is_empty(),
1263 "Extended buffer should not be empty for paired"
1264 );
1265 }
1266 }
1267 }
1268
1269 #[test]
1272 fn test_nonexistent_file() {
1273 let result = MmapReader::new("./data/nonexistent.bq");
1274 assert!(result.is_err(), "Should fail on nonexistent file");
1275 }
1276
1277 #[test]
1278 fn test_invalid_file_format() {
1279 let result = MmapReader::new("./Cargo.toml");
1281 if let Ok(reader) = result {
1283 let num_records = reader.num_records();
1285 let _ = num_records; }
1288 }
1289
1290 #[test]
1293 fn test_sequential_record_access() {
1294 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1295 let num_records = reader.num_records().min(10);
1296
1297 for i in 0..num_records {
1298 let record = reader.get(i);
1299 assert!(record.is_ok(), "Should get record at index {i}");
1300 assert_eq!(
1301 record.unwrap().index() as usize,
1302 i,
1303 "Record index mismatch at {i}"
1304 );
1305 }
1306 }
1307
1308 #[test]
1309 fn test_random_record_access() {
1310 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1311 let num_records = reader.num_records();
1312
1313 if num_records > 10 {
1314 let indices = [0, 5, num_records / 2, num_records - 1];
1315
1316 for &idx in &indices {
1317 let record = reader.get(idx);
1318 assert!(record.is_ok(), "Should get record at index {idx}");
1319 assert_eq!(record.unwrap().index() as usize, idx);
1320 }
1321 }
1322 }
1323
1324 #[test]
1327 fn test_get_buffer_slice_valid() {
1328 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1329 let num_records = reader.num_records().min(10);
1330 let slice = reader.get_buffer_slice(0..num_records);
1331 assert!(slice.is_ok());
1332 assert_eq!(
1333 slice.unwrap().len(),
1334 num_records * reader.config.record_size_u64()
1335 );
1336 }
1337
1338 #[test]
1339 fn test_get_buffer_slice_out_of_range() {
1340 let reader = MmapReader::new(TEST_BQ_FILE).unwrap();
1341 let num_records = reader.num_records();
1342 let slice = reader.get_buffer_slice(0..(num_records + 100));
1343 assert!(slice.is_err());
1344 }
1345
1346 #[test]
1349 fn test_mmap_reader_directory_is_incompatible() {
1350 let result = MmapReader::new("./data");
1352 assert!(result.is_err(), "Should fail when given a directory");
1353 }
1354
1355 #[test]
1356 fn test_mmap_reader_truncated_file() {
1357 use std::io::Write as _;
1358
1359 let path = "test_truncated_reader.bq";
1360 {
1361 let header = crate::bq::FileHeaderBuilder::new()
1362 .slen(64)
1363 .build()
1364 .unwrap();
1365 let mut file = std::fs::File::create(path).unwrap();
1366 header.write_bytes(&mut file).unwrap();
1367 file.write_all(&[0u8; 4]).unwrap();
1369 }
1370
1371 let result = MmapReader::new(path);
1372 assert!(result.is_err(), "Should fail on truncated record data");
1373
1374 std::fs::remove_file(path).unwrap();
1375 }
1376
1377 fn build_stream_bytes(paired: bool) -> Vec<u8> {
1380 use crate::SequencingRecordBuilder;
1381 use crate::bq::{FileHeaderBuilder, WriterBuilder};
1382
1383 let header = if paired {
1384 FileHeaderBuilder::new().slen(64).xlen(32).build().unwrap()
1385 } else {
1386 FileHeaderBuilder::new().slen(64).build().unwrap()
1387 };
1388
1389 let mut writer = WriterBuilder::default()
1390 .header(header)
1391 .build(Vec::new())
1392 .unwrap();
1393
1394 for i in 0..5 {
1395 let s_seq = vec![b"ACGT"[i % 4]; 64];
1396 let x_seq = vec![b"TGCA"[i % 4]; 32];
1397 let record = if paired {
1398 SequencingRecordBuilder::default()
1399 .s_seq(&s_seq)
1400 .x_seq(&x_seq)
1401 .build()
1402 .unwrap()
1403 } else {
1404 SequencingRecordBuilder::default()
1405 .s_seq(&s_seq)
1406 .build()
1407 .unwrap()
1408 };
1409 writer.push(record).unwrap();
1410 }
1411 writer.flush().unwrap();
1412 writer.into_inner()
1413 }
1414
1415 #[test]
1416 fn test_stream_reader_new_and_with_capacity() {
1417 let data = build_stream_bytes(false);
1418 let cursor = std::io::Cursor::new(data.clone());
1419 let _reader = StreamReader::new(cursor);
1420
1421 let cursor = std::io::Cursor::new(data);
1422 let _reader = StreamReader::with_capacity(cursor, 64);
1423 }
1424
1425 #[test]
1426 fn test_stream_reader_read_header() {
1427 let data = build_stream_bytes(false);
1428 let mut reader = StreamReader::new(std::io::Cursor::new(data));
1429 let header = reader.read_header().unwrap();
1430 assert_eq!(header.slen, 64);
1431
1432 let header_again = reader.read_header().unwrap();
1434 assert_eq!(header_again.slen, 64);
1435 }
1436
1437 #[test]
1438 fn test_stream_reader_next_record_unpaired() {
1439 let data = build_stream_bytes(false);
1440 let mmap_reader = {
1441 let path = "test_stream_reader_compare.bq";
1442 std::fs::write(path, &data).unwrap();
1443 let reader = MmapReader::new(path).unwrap();
1444 std::fs::remove_file(path).unwrap();
1445 reader
1446 };
1447
1448 let mut reader = StreamReader::new(std::io::Cursor::new(data));
1449 let mut count = 0;
1450 while let Some(record) = reader.next_record() {
1451 let record = record.unwrap();
1452 let expected = mmap_reader.get(count).unwrap();
1453 assert_eq!(record.index(), expected.index());
1454 assert_eq!(record.sbuf(), expected.sbuf());
1455 count += 1;
1456 }
1457 assert_eq!(count, mmap_reader.num_records());
1458 }
1459
1460 #[test]
1461 fn test_stream_reader_next_record_paired() {
1462 let data = build_stream_bytes(true);
1463 let mut reader = StreamReader::new(std::io::Cursor::new(data));
1464 let mut count = 0;
1465 while let Some(record) = reader.next_record() {
1466 let record = record.unwrap();
1467 assert!(record.is_paired());
1468 count += 1;
1469 }
1470 assert_eq!(count, 5);
1471 }
1472
1473 #[test]
1474 fn test_stream_reader_small_buffer_forces_multiple_fills() {
1475 let data = build_stream_bytes(false);
1481 let mut reader = StreamReader::with_capacity(std::io::Cursor::new(data), 40);
1482 let mut expected_id = 0u64;
1483 while let Some(record) = reader.next_record() {
1484 let record = record.unwrap();
1485 assert_eq!(record.index(), expected_id);
1486 expected_id += 1;
1487 }
1488 assert_eq!(expected_id, 5);
1489 }
1490
1491 #[test]
1492 fn test_stream_reader_partial_record_error() {
1493 let mut data = build_stream_bytes(false);
1494 data.truncate(data.len() - 4);
1496 let mut reader = StreamReader::new(std::io::Cursor::new(data));
1497
1498 let mut saw_error = false;
1499 while let Some(record) = reader.next_record() {
1500 if record.is_err() {
1501 saw_error = true;
1502 break;
1503 }
1504 }
1505 assert!(saw_error, "Expected a partial record error");
1506 }
1507
1508 #[test]
1509 fn test_stream_reader_set_default_quality_score() {
1510 let data = build_stream_bytes(false);
1511 let mut reader = StreamReader::new(std::io::Cursor::new(data));
1512 reader.set_default_quality_score(42);
1513 if let Some(Ok(record)) = reader.next_record() {
1514 assert!(record.squal().iter().all(|&q| q == 42));
1515 }
1516 }
1517
1518 #[test]
1519 fn test_stream_reader_into_inner() {
1520 let data = build_stream_bytes(false);
1521 let reader = StreamReader::new(std::io::Cursor::new(data.clone()));
1522 let cursor = reader.into_inner();
1523 assert_eq!(cursor.into_inner(), data);
1524 }
1525}