Skip to main content

binseq/bq/
reader.rs

1//! Binary sequence reader module
2//!
3//! This module provides functionality for reading binary sequence files using either:
4//! 1. Memory mapping for efficient access to entire files
5//! 2. Streaming for processing data as it arrives
6//!
7//! It supports both sequential and parallel processing of records,
8//! with configurable record layouts for different sequence types.
9
10use 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/// A reference to a binary sequence record in a memory-mapped file
27///
28/// This struct provides a view into a single record within a binary sequence file,
29/// allowing access to the record's components (sequence data, flags, etc.) without
30/// copying the data from the memory-mapped file.
31///
32/// The record's data is stored in a compact binary format where:
33/// - The first u64 contains flags
34/// - Subsequent u64s contain the primary sequence data
35/// - If present, final u64s contain the extended sequence data
36#[derive(Clone, Copy)]
37pub struct RefRecord<'a> {
38    /// The position (index) of this record in the file (0-based record index, not byte offset)
39    id: u64,
40    /// The underlying u64 buffer representing the record's binary data
41    buffer: &'a [u64],
42    /// Reusable default quality buffer
43    qbuf: &'a [u8],
44    /// The configuration that defines the layout and size of record components
45    config: RecordConfig,
46    /// Cached index string for the sequence header
47    header_buf: [u8; 20],
48    /// Length of the header in bytes
49    header_len: usize,
50}
51impl<'a> RefRecord<'a> {
52    /// Creates a new record reference
53    ///
54    /// # Arguments
55    ///
56    /// * `id` - The record's position in the file (0-based record index, not byte offset)
57    /// * `buffer` - The u64 slice containing the record's binary data
58    /// * `config` - Configuration defining the record's layout
59    ///
60    /// # Panics
61    ///
62    /// Panics if the buffer length doesn't match the expected size from the config
63    #[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    /// Returns the record's configuration
76    ///
77    /// The configuration defines the layout and size of the record's components.
78    #[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    /// Clear the buffer and fill it with the sequence header
97    fn sheader(&self) -> &[u8] {
98        &self.header_buf[..self.header_len]
99    }
100
101    /// Clear the buffer and fill it with the extended header
102    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
141/// A reference to a record in the map with a precomputed decoded buffer slice
142pub struct BatchRecord<'a> {
143    /// Unprocessed buffer slice (with flags)
144    buffer: &'a [u64],
145    /// Decoded buffer slice
146    dbuf: &'a [u8],
147    /// Record ID
148    id: u64,
149    /// The configuration that defines the layout and size of record components
150    config: RecordConfig,
151    /// A reusable pre-initialized quality score buffer
152    qbuf: &'a [u8],
153    /// Cached index string for the sequence header
154    header_buf: [u8; 20],
155    /// Length of the header in bytes
156    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    /// Clear the buffer and fill it with the sequence header
166    fn sheader(&self) -> &[u8] {
167        &self.header_buf[..self.header_len]
168    }
169
170    /// Clear the buffer and fill it with the extended header
171    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    /// Override this method since we can make use of block information
211    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    /// Override this method since we can make use of block information
222    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/// Configuration for binary sequence record layout
241///
242/// This struct defines the size and layout of binary sequence records,
243/// including both primary sequence data and optional extended data.
244/// It handles the translation between sequence lengths in base pairs
245/// and the number of u64 chunks needed to store the compressed data.
246#[derive(Clone, Copy)]
247pub struct RecordConfig {
248    /// The primary sequence length in base pairs
249    slen: u64,
250    /// The extended sequence length in base pairs
251    xlen: u64,
252    /// The number of u64 chunks needed to store the primary sequence
253    /// (each u64 stores 32 nucleotides)
254    schunk: u64,
255    /// The number of u64 chunks needed to store the extended sequence
256    /// (each u64 stores 32 values)
257    xchunk: u64,
258    /// The bitsize of the record
259    bitsize: BitSize,
260    /// Whether flags are present
261    flags: bool,
262}
263impl RecordConfig {
264    /// Creates a new record configuration
265    ///
266    /// This constructor initializes a configuration for a binary sequence record
267    /// with specified primary and extended sequence lengths.
268    ///
269    /// # Arguments
270    ///
271    /// * `slen` - The length of primary sequences in the file
272    /// * `xlen` - The length of secondary/extended sequences in the file
273    /// * `bitsize` - The bitsize of the record
274    /// * `flags` - Whether flags are present
275    ///
276    /// # Returns
277    ///
278    /// A new `RecordConfig` instance with the specified sequence lengths
279    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    /// Creates a new record configuration from a header
295    ///
296    /// This constructor initializes a configuration based on a header that contains
297    /// the sequence lengths for primary and extended sequences.
298    ///
299    /// # Arguments
300    ///
301    /// * `header` - A reference to a `FileHeader` containing sequence lengths
302    ///
303    /// # Returns
304    ///
305    /// A new `RecordConfig` instance with the sequence lengths from the header
306    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    /// Returns whether this record contains extended sequence data
316    ///
317    /// A record is considered paired if it has a non-zero extended sequence length.
318    pub fn paired(&self) -> bool {
319        self.xlen > 0
320    }
321
322    /// Returns the primary sequence length in base pairs
323    ///
324    /// This method returns the length of the primary sequence in base pairs.
325    pub fn slen(&self) -> usize {
326        self.slen as usize
327    }
328
329    /// Returns the extended sequence length in base pairs
330    ///
331    /// This method returns the length of the extended sequence in base pairs.
332    pub fn xlen(&self) -> usize {
333        self.xlen as usize
334    }
335
336    /// Returns the number of u64 chunks needed to store the primary sequence
337    ///
338    /// This method returns the number of u64 chunks required to store the primary
339    /// sequence, where each u64 stores 32 nucleotides.
340    pub fn schunk(&self) -> usize {
341        self.schunk as usize
342    }
343
344    /// Returns the number of u64 chunks needed to store the extended sequence
345    ///
346    /// This method returns the number of u64 chunks required to store the extended
347    /// sequence, where each u64 stores 32 values.
348    pub fn xchunk(&self) -> usize {
349        self.xchunk as usize
350    }
351
352    /// Returns the full record size in bytes (u8):
353    /// 8 * (schunk + xchunk + 1 (flag))
354    pub fn record_size_bytes(&self) -> usize {
355        8 * self.record_size_u64()
356    }
357
358    /// Returns the full record size in u64
359    /// schunk + xchunk + 1 (flag)
360    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    /// The number of nucleotides per word
369    pub fn scalar(&self) -> usize {
370        match self.bitsize {
371            BitSize::Two => 32,
372            BitSize::Four => 16,
373        }
374    }
375}
376
377/// A memory-mapped reader for binary sequence files
378///
379/// This reader provides efficient access to binary sequence files by memory-mapping
380/// them instead of performing traditional I/O operations. It supports both
381/// sequential access to individual records and parallel processing of records
382/// across multiple threads.
383///
384/// The reader ensures thread-safety through the use of `Arc` for sharing the
385/// memory-mapped data between threads.
386///
387/// Records are returned as [`RefRecord`] which implement the [`BinseqRecord`] trait.
388///
389/// # Examples
390///
391/// ```
392/// use binseq::bq::MmapReader;
393/// use binseq::Result;
394///
395/// fn main() -> Result<()> {
396///     let path = "./data/subset.bq";
397///     let reader = MmapReader::new(path)?;
398///
399///     // Calculate the number of records in the file
400///     let num_records = reader.num_records();
401///     println!("Number of records: {}", num_records);
402///
403///     // Get the record at index 20 (0-indexed)
404///     let record = reader.get(20)?;
405///
406///     Ok(())
407/// }
408/// ```
409pub struct MmapReader {
410    /// Memory mapped file contents, wrapped in Arc for thread-safe sharing
411    mmap: Arc<Mmap>,
412
413    /// Binary sequence file header containing format information
414    header: FileHeader,
415
416    /// Configuration defining the layout of records in the file
417    config: RecordConfig,
418
419    /// Reusable buffer for quality scores
420    qbuf: Vec<u8>,
421
422    /// Default quality score for records without quality scores
423    default_quality_score: u8,
424}
425
426impl MmapReader {
427    /// Creates a new memory-mapped reader for a binary sequence file
428    ///
429    /// This method opens the file, memory-maps its contents, and validates
430    /// the file structure to ensure it contains valid binary sequence data.
431    ///
432    /// # Arguments
433    ///
434    /// * `path` - Path to the binary sequence file
435    ///
436    /// # Returns
437    ///
438    /// * `Ok(MmapReader)` - A new reader if the file is valid
439    /// * `Err(Error)` - If the file is invalid or cannot be opened
440    ///
441    /// # Errors
442    ///
443    /// Returns an error if:
444    /// * The file cannot be opened
445    /// * The file is not a regular file
446    /// * The file header is invalid
447    /// * The file size doesn't match the expected size based on the header
448    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
449        // Verify input file is a file before attempting to map
450        let file = File::open(path)?;
451        if !file.metadata()?.is_file() {
452            return Err(ReadError::IncompatibleFile.into());
453        }
454
455        // Safety: the file is open and won't be modified while mapped
456        let mmap = unsafe { Mmap::map(&file)? };
457
458        // Read header from mapped memory
459        let header = FileHeader::from_buffer(&mmap)?;
460
461        // Record configuraration
462        let config = RecordConfig::from_header(&header);
463
464        // Immediately validate the size of the file against the expected byte size of records
465        if !(mmap.len() - SIZE_HEADER).is_multiple_of(config.record_size_bytes()) {
466            return Err(ReadError::FileTruncation(mmap.len()).into());
467        }
468
469        // preinitialize quality buffer
470        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    /// Returns the total number of records in the file
482    ///
483    /// This is calculated by subtracting the header size from the total file size
484    /// and dividing by the size of each record.
485    #[must_use]
486    pub fn num_records(&self) -> usize {
487        (self.mmap.len() - SIZE_HEADER) / self.config.record_size_bytes()
488    }
489
490    /// Returns a copy of the binary sequence file header
491    ///
492    /// The header contains format information and sequence length specifications.
493    #[must_use]
494    pub fn header(&self) -> FileHeader {
495        self.header
496    }
497
498    /// Checks if the file has paired-records
499    #[must_use]
500    pub fn is_paired(&self) -> bool {
501        self.header.is_paired()
502    }
503
504    /// Sets the default quality score for records without quality information
505    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    /// Creates a new quality score buffer
511    #[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    /// Returns a reference to a specific record
517    ///
518    /// # Arguments
519    ///
520    /// * `idx` - The index of the record to retrieve (0-based)
521    ///
522    /// # Returns
523    ///
524    /// * `Ok(RefRecord)` - A reference to the requested record
525    /// * `Err(Error)` - If the index is out of bounds
526    ///
527    /// # Errors
528    ///
529    /// Returns an error if the requested index is beyond the number of records in the file
530    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    /// Returns a slice of the buffer containing the underlying u64 for that range
547    /// of records.
548    ///
549    /// Note: range 10..40 will return all u64s in the mmap between the record index 10 and 40
550    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
568/// A reader for streaming binary sequence data from any source that implements Read
569///
570/// Unlike `MmapReader` which requires the entire file to be accessible at once,
571/// `StreamReader` processes data as it becomes available, making it suitable for:
572/// - Processing data as it arrives over a network
573/// - Handling very large files that exceed available memory
574/// - Pipeline processing where data is flowing continuously
575///
576/// The reader maintains an internal buffer and can handle partial record reconstruction
577/// across chunk boundaries.
578pub struct StreamReader<R: Read> {
579    /// The source reader for binary sequence data
580    reader: R,
581
582    /// Binary sequence file header containing format information
583    header: Option<FileHeader>,
584
585    /// Configuration defining the layout of records in the file
586    config: Option<RecordConfig>,
587
588    /// Buffer for storing incoming data
589    buffer: Vec<u8>,
590
591    /// Buffer for reusable quality scores
592    qbuf: Vec<u8>,
593
594    /// Default quality score for records without quality information
595    default_quality_score: u8,
596
597    /// Current position in the buffer
598    buffer_pos: usize,
599
600    /// Length of valid data in the buffer
601    buffer_len: usize,
602
603    /// Number of records returned so far, used to assign each record's id
604    ///
605    /// This is tracked independently of `buffer_pos` because `fill_buffer`
606    /// shifts remaining bytes to the start of the buffer and resets
607    /// `buffer_pos` whenever a mid-stream refill is needed, so `buffer_pos`
608    /// no longer reflects the absolute stream offset once that happens.
609    records_read: u64,
610}
611
612impl<R: Read> StreamReader<R> {
613    /// Creates a new `StreamReader` with the default buffer size
614    ///
615    /// This constructor initializes a `StreamReader` that will read from the provided
616    /// source, using an 8K default buffer size.
617    ///
618    /// # Arguments
619    ///
620    /// * `reader` - The source to read binary sequence data from
621    ///
622    /// # Returns
623    ///
624    /// A new `StreamReader` instance
625    pub fn new(reader: R) -> Self {
626        Self::with_capacity(reader, 8192)
627    }
628
629    /// Creates a new `StreamReader` with a specified buffer capacity
630    ///
631    /// This constructor initializes a `StreamReader` with a custom buffer size,
632    /// which can be tuned based on the expected usage pattern.
633    ///
634    /// # Arguments
635    ///
636    /// * `reader` - The source to read binary sequence data from
637    /// * `capacity` - The size of the internal buffer in bytes
638    ///
639    /// # Returns
640    ///
641    /// A new `StreamReader` instance with the specified buffer capacity
642    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    /// Sets the default quality score for records without quality information
657    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    /// Reads and validates the header from the underlying reader
665    ///
666    /// This method reads the binary sequence file header and validates it.
667    /// It caches the header internally for future use.
668    ///
669    /// # Returns
670    ///
671    /// * `Ok(&FileHeader)` - A reference to the validated header
672    /// * `Err(Error)` - If reading or validating the header fails
673    ///
674    /// # Panics
675    ///
676    /// Panics if the header is missing when expected in the stream.
677    ///
678    /// # Errors
679    ///
680    /// Returns an error if:
681    /// * There is an I/O error when reading from the source
682    /// * The header data is invalid
683    /// * End of stream is reached before the full header can be read
684    pub fn read_header(&mut self) -> Result<&FileHeader> {
685        if self.header.is_none() {
686            // Ensure we have enough data for the header
687            while self.buffer_len - self.buffer_pos < SIZE_HEADER {
688                self.fill_buffer()?;
689            }
690
691            // Parse header
692            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    /// Fills the internal buffer with more data from the reader
707    ///
708    /// This method reads more data from the underlying reader, handling
709    /// the case where some unprocessed data remains in the buffer.
710    ///
711    /// # Returns
712    ///
713    /// * `Ok(())` - If the buffer was successfully filled with new data
714    /// * `Err(Error)` - If reading from the source fails
715    ///
716    /// # Errors
717    ///
718    /// Returns an error if:
719    /// * There is an I/O error when reading from the source
720    /// * End of stream is reached (no more data available)
721    fn fill_buffer(&mut self) -> Result<()> {
722        // Move remaining data to beginning of buffer if needed
723        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        // Read more data
733        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    /// Retrieves the next record from the stream
743    ///
744    /// This method reads and processes the next complete record from the stream.
745    /// It handles the case where a record spans multiple buffer fills.
746    ///
747    /// # Returns
748    ///
749    /// * `Ok(Some(RefRecord))` - The next record was successfully read
750    /// * `Ok(None)` - End of stream was reached (no more records)
751    /// * `Err(Error)` - If an error occurred during reading
752    ///
753    /// # Panics
754    ///
755    /// Panics if the configuration is missing when expected in the stream.
756    ///
757    /// # Errors
758    ///
759    /// Returns an error if:
760    /// * There is an I/O error when reading from the source
761    /// * The header has not been read yet
762    /// * The data format is invalid
763    pub fn next_record(&mut self) -> Option<Result<RefRecord<'_>>> {
764        // Ensure header is read
765        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        // Ensure we have enough data for a complete record
777        while self.buffer_len - self.buffer_pos < record_size {
778            match self.fill_buffer() {
779                Ok(()) => {}
780                Err(Error::ReadError(ReadError::EndOfStream)) => {
781                    // End of stream reached - if we have any partial data, it's an error
782                    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        // Process record
795        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        // update quality score buffer if necessary
802        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        // Create record with an incremental ID, tracked independently of
808        // buffer position since `fill_buffer` may have shifted the buffer
809        let id = self.records_read;
810        self.records_read += 1;
811        Some(Ok(RefRecord::new(id, record_u64s, &self.qbuf, config)))
812    }
813
814    /// Consumes the stream reader and returns the inner reader
815    ///
816    /// This method is useful when you need access to the underlying reader
817    /// after processing is complete.
818    ///
819    /// # Returns
820    ///
821    /// The inner reader that was used by this `StreamReader`
822    pub fn into_inner(self) -> R {
823        self.reader
824    }
825}
826
827/// Default batch size for parallel processing
828///
829/// This constant defines how many records each thread processes at a time
830/// during parallel processing operations.
831pub const BATCH_SIZE: usize = 1024;
832
833/// Parallel processing implementation for memory-mapped readers
834impl ParallelReader for MmapReader {
835    /// Processes all records in parallel using multiple threads
836    ///
837    /// This method distributes the records across the specified number of threads
838    /// and processes them using the provided processor. Each thread receives its
839    /// own clone of the processor and processes a contiguous chunk of records.
840    ///
841    /// # Arguments
842    ///
843    /// * `processor` - The processor to use for handling records
844    /// * `num_threads` - The number of threads to use for processing
845    ///
846    /// # Type Parameters
847    ///
848    /// * `P` - A type that implements `ParallelProcessor` and can be cloned
849    ///
850    /// # Returns
851    ///
852    /// * `Ok(())` - If all records were processed successfully
853    /// * `Err(Error)` - If an error occurred during processing
854    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    /// Process records in parallel within a specified range
864    ///
865    /// This method allows parallel processing of a subset of records within the file,
866    /// defined by a start and end index. The range is distributed across the specified
867    /// number of threads.
868    ///
869    /// # Arguments
870    ///
871    /// * `processor` - The processor to use for each record
872    /// * `num_threads` - The number of threads to spawn
873    /// * `range` - The range of record indices to process
874    ///
875    /// # Type Parameters
876    ///
877    /// * `P` - A type that implements `ParallelProcessor` and can be cloned
878    ///
879    /// # Returns
880    ///
881    /// * `Ok(())` - If all records were processed successfully
882    /// * `Err(Error)` - If an error occurred during processing
883    fn process_parallel_range<P: ParallelProcessor + Clone + 'static>(
884        self,
885        processor: P,
886        num_threads: usize,
887        range: Range<usize>,
888    ) -> Result<()> {
889        // Calculate the number of threads to use
890        let num_threads = if num_threads == 0 {
891            num_cpus::get()
892        } else {
893            num_threads.min(num_cpus::get())
894        };
895
896        // Validate range
897        let num_records = self.num_records();
898        self.validate_range(num_records, &range)?;
899
900        // Calculate number of records for each thread within the range
901        let range_size = range.end - range.start;
902        let records_per_thread = range_size.div_ceil(num_threads);
903
904        // Arc self
905        let reader = Arc::new(self);
906
907        // Build thread handles
908        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(()); // No records for this thread
920                }
921
922                // create a reusable buffer for translating record IDs
923                let mut translater = itoa::Buffer::new();
924
925                // initialize a decoding buffer
926                let mut dbuf = Vec::new();
927
928                // initialize a quality score buffer
929                let qbuf = reader.build_qbuf();
930
931                // calculate the size of a record in the cast u64 slice
932                let rsize_u64 = reader.config.record_size_bytes() / 8;
933
934                // determine the required scalar size
935                let scalar = reader.config.scalar();
936
937                // calculate the size of a record in the batch decoded buffer
938                let mut dbuf_rsize = { (reader.config.schunk() + reader.config.xchunk()) * scalar };
939                if reader.config.flags {
940                    dbuf_rsize += scalar;
941                }
942
943                // iterate over the range of indices
944                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                    // clear the decoded buffer
948                    dbuf.clear();
949
950                    // get the encoded buffer slice
951                    let ebuf = reader.get_buffer_slice(range_start..range_end)?;
952
953                    // decode the entire buffer at once (with flags and extra bases)
954                    reader
955                        .config
956                        .bitsize
957                        .decode(ebuf, ebuf.len() * scalar, &mut dbuf)?;
958
959                    // iterate over each index in the range
960                    for (inner_idx, idx) in (range_start..range_end).enumerate() {
961                        // translate the index
962                        let id_str = translater.format(idx);
963
964                        // create the index buffer
965                        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                        // find the buffer starts
970                        let ebuf_start = inner_idx * rsize_u64;
971                        let dbuf_start = inner_idx * dbuf_rsize;
972
973                        // initialize the record
974                        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                        // process the record
985                        processor.process_record(record)?;
986                    }
987
988                    // process the batch
989                    processor.on_batch_complete()?;
990                }
991
992                // process the thread
993                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    // ==================== MmapReader Basic Tests ====================
1021
1022    #[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        // The fixture file contains paired records
1039        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    // ==================== Record Access Tests ====================
1061
1062    #[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    // ==================== Default Quality Score Tests ====================
1114
1115    #[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            // All quality scores should be the custom score
1125            assert!(
1126                squal.iter().all(|&q| q == custom_score),
1127                "All quality scores should be {custom_score}"
1128            );
1129        }
1130    }
1131
1132    // ==================== Parallel Processing Tests ====================
1133
1134    #[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    // ==================== RecordConfig Tests ====================
1191
1192    #[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    // ==================== RefRecord Tests ====================
1225
1226    #[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            // Flag should be Some if header has flags enabled
1246            assert!(flag.is_some() || flag.is_none()); // Tests method works
1247        }
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    // ==================== Error Handling Tests ====================
1270
1271    #[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        // Try to open a non-BQ file as BQ (use Cargo.toml for example)
1280        let result = MmapReader::new("./Cargo.toml");
1281        // This should either fail to open or fail validation
1282        if let Ok(reader) = result {
1283            // If it opens, try to access records (should fail or have issues)
1284            let num_records = reader.num_records();
1285            // The number might be nonsensical for invalid data
1286            let _ = num_records; // Just verify it doesn't panic
1287        }
1288    }
1289
1290    // ==================== Multiple Records Tests ====================
1291
1292    #[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    // ==================== get_buffer_slice Tests ====================
1325
1326    #[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    // ==================== MmapReader Error Path Tests ====================
1347
1348    #[test]
1349    fn test_mmap_reader_directory_is_incompatible() {
1350        // Directories cannot be memory-mapped as regular files
1351        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            // Write a partial record (not a full multiple of the record size)
1368            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    // ==================== StreamReader Tests ====================
1378
1379    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        // Second call should hit the cached path
1433        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        // Use a tiny buffer capacity so `fill_buffer` must shift remaining
1476        // bytes to the start of the internal buffer mid-stream (reader.rs
1477        // 716-719). Record ids are tracked via a dedicated `records_read`
1478        // counter (not derived from `buffer_pos`), so they must still come
1479        // back sequential across that shift boundary.
1480        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        // Truncate the data in the middle of the last record
1495        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}