qbix 0.0.8

Random access to BAM records by read name using a compact .qbi index
mod common;

use std::path::{Path, PathBuf};

use common::{write_unmapped_bam, TempDir};

#[test]
fn public_error_implements_std_error() {
    fn assert_std_error<T: std::error::Error>() {}

    assert_std_error::<qbix::Error>();
}

#[test]
fn public_api_builds_opens_and_queries_an_index() {
    let temp = TempDir::new("api");
    let bam = temp.path().join("reads.bam");
    let bam_str = bam.to_str().unwrap();
    write_unmapped_bam(bam_str, &["read_b", "read_a", "read_a"]);

    let index_path = qbix::build_index(&bam, qbix::BuildOptions::default()).unwrap();
    assert_eq!(index_path, PathBuf::from(format!("{bam_str}.qbi")));

    qbix::check_index(&bam, qbix::CheckOptions::default()).unwrap();
    let mut check_options = qbix::CheckOptions::default();
    check_options.mode = qbix::CheckMode::Full;
    qbix::check_index(&bam, check_options).unwrap();

    let records = qbix::read_index_records(&index_path).unwrap();
    assert_eq!(records.len(), 3);
    assert!(records
        .iter()
        .all(|record| record.virtual_offset.as_i64() >= 0));

    let mut indexed = qbix::IndexedBam::open(&bam, qbix::LookupOptions::default()).unwrap();
    assert_eq!(indexed.bam_path(), bam.as_path());
    assert_eq!(indexed.index_path(), index_path.as_path());
    assert_eq!(indexed.record_count(), 3);

    let unverified_offsets = indexed.lookup_offsets_unverified("read_a").unwrap();
    assert_eq!(unverified_offsets.len(), 2);

    let offsets = indexed.lookup_offsets("read_a").unwrap();
    assert_eq!(offsets.len(), 2);
    assert!(offsets.iter().all(|offset| offset.as_i64() >= 0));

    let hits = indexed.lookup("read_a").unwrap();
    assert_eq!(hits.len(), 2);
    assert!(hits.iter().all(|hit| hit.read_name == "read_a"));
    assert!(indexed.lookup("missing").unwrap().is_empty());
}

#[test]
fn public_api_writes_sam_records_to_a_path() {
    let temp = TempDir::new("api-write");
    let bam = temp.path().join("reads.bam");
    let query_sam = temp.path().join("query.sam");
    let bam_sam = temp.path().join("bam.sam");
    let bam_str = bam.to_str().unwrap();
    write_unmapped_bam(bam_str, &["read_b", "read_a", "read_a"]);

    qbix::build_index(&bam, qbix::BuildOptions::default()).unwrap();
    let mut indexed = qbix::IndexedBam::open(&bam, qbix::LookupOptions::default()).unwrap();
    let bam_before = std::fs::read(&bam).unwrap();
    let index_before = std::fs::read(indexed.index_path()).unwrap();
    assert!(indexed
        .write_sam_records_to_path(&bam, &["read_a"], qbix::OutputOrder::Query)
        .unwrap_err()
        .message()
        .contains("input BAM"));
    let index_path = indexed.index_path().to_path_buf();
    assert!(indexed
        .write_sam_records_to_path(&index_path, &["read_a"], qbix::OutputOrder::Query)
        .unwrap_err()
        .message()
        .contains("input index"));
    assert_eq!(std::fs::read(&bam).unwrap(), bam_before);
    assert_eq!(std::fs::read(&index_path).unwrap(), index_before);

    let query_written = indexed
        .write_sam_records_to_path(&query_sam, &["read_a", "read_b"], qbix::OutputOrder::Query)
        .unwrap();
    let bam_written = indexed
        .write_sam_records_to_path(&bam_sam, &["read_a", "read_b"], qbix::OutputOrder::Bam)
        .unwrap();

    assert_eq!(query_written, 3);
    assert_eq!(bam_written, 3);
    assert_eq!(sam_read_names(&query_sam), ["read_a", "read_a", "read_b"]);
    assert_eq!(sam_read_names(&bam_sam), ["read_b", "read_a", "read_a"]);
}

fn sam_read_names(path: &Path) -> Vec<String> {
    std::fs::read_to_string(path)
        .unwrap()
        .lines()
        .filter(|line| !line.starts_with('@'))
        .map(|line| line.split('\t').next().unwrap().to_string())
        .collect()
}