use super::*;
use crate::cram::crai::{CramIndex, IndexEntry};
use crate::source::ByteSource;
fn test_file(name: &str) -> Option<String> {
let path = format!(
"{}/../../local/test_data/{name}",
env!("CARGO_MANIFEST_DIR")
);
std::path::Path::new(&path).exists().then_some(path)
}
#[test]
fn every_block_of_a_real_cram_decodes() {
let Some(path) = test_file("AtTPax7_H3K4me.10M.cram") else {
return;
};
let source = crate::source::open(&path, None, None).expect("opens");
let head = source.read_at(0, 64).expect("head");
let definition = container::FileDefinition::parse(&head, &path).expect("file definition");
assert_eq!((definition.major, definition.minor), (3, 1));
let mut offset = container::FILE_DEFINITION_SIZE as u64;
let mut containers = 0usize;
let mut blocks = 0usize;
let mut methods = std::collections::BTreeMap::new();
let mut names: Vec<String> = Vec::new();
let file_len = source.len().expect("length");
while offset < file_len {
let header = container::ContainerHeader::read(source.as_ref(), offset).expect("container");
if header.is_eof() {
break;
}
let body = source
.read_exact_at(header.blocks_offset(), header.length as usize)
.expect("container body");
let mut at = 0usize;
for _ in 0..header.n_blocks {
let Some(rest) = body.get(at..) else { break };
if rest.is_empty() {
break;
}
let block = container::Block::parse(rest, header.blocks_offset() + at as u64, &path)
.unwrap_or_else(|e| panic!("container at {offset}, block at {at}: {e}"));
at += block.total_size;
blocks += 1;
*methods.entry(block.method.name()).or_insert(0usize) += 1;
if block.method == container::CompressionMethod::NameTok {
let decoded: Vec<&[u8]> = block
.data
.split(|b| *b == 0)
.filter(|s| !s.is_empty())
.collect();
assert!(
decoded.len() > 9000,
"only {} names in a slice of ten thousand",
decoded.len()
);
for name in &decoded {
assert!(
name.iter().all(|b| b.is_ascii_graphic()),
"a read name with something unprintable in it: {:?}",
String::from_utf8_lossy(name)
);
}
if names.is_empty() {
names = decoded
.iter()
.take(2)
.map(|n| String::from_utf8_lossy(n).into_owned())
.collect();
}
}
}
containers += 1;
offset = header.end_offset();
if containers >= 20 {
break;
}
}
assert!(containers >= 20, "only {containers} containers walked");
assert!(blocks > 400, "only {blocks} blocks decoded");
assert!(!names.is_empty(), "no read names were decoded");
println!("{containers} containers, {blocks} blocks: {methods:?}");
println!("read names: {names:?}");
}
#[test]
fn a_real_cram_reads_its_entries() {
let Some(path) = test_file("AtTPax7_H3K4me.10M.cram") else {
return;
};
let reference = test_file("mm10.fa.gz");
let reader = CramReader::open(&path, None, reference.as_deref(), 4, None, None).expect("opens");
assert_eq!(reader.version(), (3, 1));
assert!(reader.is_indexed(), "{}", reader.index_error());
let request = crate::bam::EntriesRequest::new(
crate::genomic::Locs::spans(&["chr1".to_string()], &[3_100_000], &[3_101_000])
.expect("locs"),
);
let entries = reader.read_entries(&request).expect("reads");
assert_eq!(entries.len(), 1);
let alignments = &entries[0];
assert!(!alignments.is_empty(), "no alignments in the locus");
for entry in alignments {
assert_eq!(entry.chr(), "chr1");
assert!(entry.end() > 3_100_000 && entry.start() < 3_101_000);
assert_eq!(entry.sequence().len() as i64, entry.query_length());
assert!(entry.sequence().bytes().all(|b| b"ACGTN=".contains(&b)));
assert_eq!(entry.qualities().len(), entry.sequence().len());
}
}
#[test]
fn an_index_built_from_containers_answers_a_locus_like_the_crai() {
let Some(path) = test_file("AtTPax7_H3K4me.10M.cram") else {
return;
};
let Some(crai) = test_file("AtTPax7_H3K4me.10M.cram.crai") else {
return;
};
let source = crate::source::open(&path, None, None).expect("opens");
let built = CramIndex::build(source.as_ref()).expect("builds");
let listed = CramIndex::parse(
&crate::source::open(&crai, None, None)
.expect("opens")
.read_to_end(0)
.expect("reads"),
&crai,
)
.expect("parses");
assert_eq!(
built.multi_len(),
0,
"a built index should place every slice on a reference"
);
for (ref_id, start, end) in [
(0, 3_100_000, 3_101_000),
(1, 5_000_000, 5_001_000),
(2, 0, 100_000),
] {
let from_build = built.slices(ref_id, start, end);
let from_crai = listed.slices(ref_id, start, end);
assert_eq!(
from_build.len(),
from_crai.len(),
"reference {ref_id}:{start}-{end}: {} built against {} listed",
from_build.len(),
from_crai.len()
);
let key = |e: &IndexEntry| (e.container_offset, e.landmark);
assert_eq!(
from_build.iter().map(key).collect::<Vec<_>>(),
from_crai.iter().map(key).collect::<Vec<_>>(),
);
}
}
#[test]
fn a_real_cram_without_a_reference_reads_everything_but_its_sequences() {
let Some(path) = test_file("AtTPax7_H3K4me.10M.cram") else {
return;
};
let locs = || {
crate::genomic::Locs::spans(&["chr1".to_string()], &[3_100_000], &[3_200_000])
.expect("locs")
};
let request = || crate::bam::EntriesRequest::new(locs()).filter(false);
let without = CramReader::open(&path, None, Some("/dev/null"), 2, None, None).expect("opens");
assert!(
!without.reference_error().is_empty(),
"a reference that cannot be read should say so"
);
let bare = without
.read_entries(&request())
.expect("reads without a reference");
let reference = test_file("mm10.fa.gz");
let with = CramReader::open(&path, None, reference.as_deref(), 2, None, None).expect("opens");
let full = with
.read_entries(&request())
.expect("reads with a reference");
assert_eq!(bare[0].len(), full[0].len());
for (bare, full) in bare[0].iter().zip(&full[0]) {
assert_eq!(bare.read_name(), full.read_name());
assert_eq!(bare.start(), full.start());
assert_eq!(bare.end(), full.end());
assert_eq!(bare.flag(), full.flag());
assert_eq!(bare.cigar(), full.cigar());
assert_eq!(bare.qualities(), full.qualities());
assert_eq!(bare.next_start(), full.next_start());
assert_eq!(bare.template_length(), full.template_length());
assert_eq!(bare.sequence().len(), full.sequence().len());
assert!(
bare.sequence().bytes().all(|b| b == b'N'),
"{}: {} without a reference",
bare.read_name(),
bare.sequence()
);
}
let substituted = full[0]
.iter()
.filter(|e| {
e.tags().is_ok_and(|tags| {
tags.iter()
.any(|(tag, value)| tag == "NM" && format!("{value:?}") != "Int(0)")
})
})
.count();
assert!(
substituted > 0,
"this window holds no mismatching read, so it cannot test substitution"
);
}