gwseq-io 0.2.0

Rust library for processing bigWig, bigBed, BAM and HiC files
Documentation
//! The source stack against the file system, from outside the crate.
//!
//! The unit tests in `source::cache` check the cache against an in-memory
//! source; this checks the whole stack — `source::open`, so `LocalSource`
//! under `CachedSource` — against what `std::fs::read` says is on disk. Every
//! block size crossed with every interesting offset, because the bugs this
//! class of code has are all at block boundaries.

use std::io::Write;

use gwseq_io::source::{self, ByteSource};

/// A file that removes itself, seeded deterministically so a failure is
/// reproducible from the test name alone.
struct Fixture {
    path: std::path::PathBuf,
    data: Vec<u8>,
}

impl Fixture {
    fn new(tag: &str, len: usize) -> Self {
        // xorshift, so the content is incompressible-ish and every byte differs
        // from its neighbours — a mis-slice shows up rather than matching by
        // luck.
        let mut state: u32 = 0x9E37_79B9;
        let data: Vec<u8> = (0..len)
            .map(|_| {
                state ^= state << 13;
                state ^= state >> 17;
                state ^= state << 5;
                (state & 0xFF) as u8
            })
            .collect();
        let mut path = std::env::temp_dir();
        path.push(format!("gwseq-stack-{tag}-{}.bin", std::process::id()));
        let mut file = std::fs::File::create(&path).unwrap();
        file.write_all(&data).unwrap();
        file.sync_all().unwrap();
        Self { path, data }
    }

    fn as_str(&self) -> &str {
        self.path.to_str().unwrap()
    }
}

impl Drop for Fixture {
    fn drop(&mut self) {
        let _ = std::fs::remove_file(&self.path);
    }
}

#[test]
fn every_range_matches_the_file_at_every_block_size() {
    let fixture = Fixture::new("ranges", 300_000);
    let on_disk = std::fs::read(&fixture.path).unwrap();
    assert_eq!(on_disk, fixture.data);

    for block_size in [1u64, 3, 512, 4096, 32_768, 1 << 20] {
        let source = source::open(fixture.as_str(), Some(block_size), Some(8)).unwrap();
        assert_eq!(source.len().unwrap(), on_disk.len() as u64);

        // Offsets straddling block boundaries, the start, and the end.
        let offsets = [
            0u64,
            1,
            block_size.saturating_sub(1),
            block_size,
            block_size + 1,
            block_size * 3 - 1,
            150_000,
            299_999,
            300_000,
            400_000,
        ];
        for offset in offsets {
            for len in [0usize, 1, 2, 511, 4097, 100_000] {
                let got = source.read_at(offset, len).unwrap();
                let start = (offset as usize).min(on_disk.len());
                let end = (start + len).min(on_disk.len());
                assert_eq!(
                    &got[..],
                    &on_disk[start..end],
                    "block_size={block_size} offset={offset} len={len}"
                );
            }
        }
    }
}

#[test]
fn reading_the_whole_file_in_one_call_matches() {
    let fixture = Fixture::new("whole", 250_000);
    let on_disk = std::fs::read(&fixture.path).unwrap();
    for block_size in [1024u64, 32_768] {
        // A cache far too small to hold the file: the read must still be
        // complete, just with every block evicted behind it.
        let source = source::open(fixture.as_str(), Some(block_size), Some(2)).unwrap();
        assert_eq!(&source.read_to_end(0).unwrap()[..], &on_disk[..]);
    }
}

#[test]
fn a_default_open_uses_the_recommended_settings() {
    let fixture = Fixture::new("defaults", 100_000);
    let on_disk = std::fs::read(&fixture.path).unwrap();
    let source = source::open(fixture.as_str(), None, None).unwrap();
    assert_eq!(
        &source.read_at(1234, 5678).unwrap()[..],
        &on_disk[1234..6912]
    );
}

#[test]
fn threads_reading_overlapping_ranges_all_get_the_right_bytes() {
    let fixture = Fixture::new("threads", 1 << 18);
    let on_disk = std::sync::Arc::new(std::fs::read(&fixture.path).unwrap());
    let source = source::open(fixture.as_str(), Some(4096), Some(16)).unwrap();

    let threads: Vec<_> = (0..8u64)
        .map(|t| {
            let source = source.clone();
            let on_disk = on_disk.clone();
            std::thread::spawn(move || {
                for i in 0..400u64 {
                    let offset = (t * 4093 + i * 271) % 250_000;
                    let len = 1 + ((t + i) % 9000) as usize;
                    let got = source.read_at(offset, len).unwrap();
                    let end = (offset as usize + len).min(on_disk.len());
                    assert_eq!(&got[..], &on_disk[offset as usize..end]);
                }
            })
        })
        .collect();
    for t in threads {
        t.join().unwrap();
    }
}

// URL coverage is not here: it needs a local HTTP server to be worth
// anything. The case that matters is a server that ignores `Range` and answers
// 200 with the whole file, since a reader that trusts the status code then
// reads the wrong bytes at every offset. Writing those servers is the missing
// work, not the assertions.