bv_conformance/
assertions.rs1use std::path::Path;
2
3use anyhow::Context;
4use bv_core::manifest::IoSpec;
5
6pub fn check_output(spec: &IoSpec, path: &Path) -> anyhow::Result<()> {
9 if !path.exists() {
10 anyhow::bail!(
11 "output '{}' does not exist at {}",
12 spec.name,
13 path.display()
14 );
15 }
16
17 let type_id = spec.r#type.base_id();
18 check_format(type_id, path).with_context(|| {
19 format!(
20 "output '{}' failed format check for type '{}'",
21 spec.name, type_id
22 )
23 })
24}
25
26fn check_format(type_id: &str, path: &Path) -> anyhow::Result<()> {
27 match type_id {
28 "dir" | "blast_db" | "mmseqs_db" => {
29 if !path.is_dir() {
30 anyhow::bail!("expected a directory");
31 }
32 }
33 "fasta" => check_fasta(path)?,
34 "fastq" => check_fastq(path)?,
35 "bam" => check_bam(path)?,
36 "hmm_profile" => check_hmm(path)?,
37 "tabular" | "blast_tab" | "sam" | "vcf" | "mmseqs_output" | "hmmer_output" => {
38 check_tabular(path)?
39 }
40 _ => {
41 if !path.exists() {
42 anyhow::bail!("file does not exist");
43 }
44 }
45 }
46 Ok(())
47}
48
49fn check_fasta(path: &Path) -> anyhow::Result<()> {
50 let first = first_byte(path)?;
51 if first != b'>' {
52 anyhow::bail!("FASTA files must start with '>'");
53 }
54 Ok(())
55}
56
57fn check_fastq(path: &Path) -> anyhow::Result<()> {
58 let first = first_byte(path)?;
59 if first != b'@' {
60 anyhow::bail!("FASTQ files must start with '@'");
61 }
62 Ok(())
63}
64
65fn check_bam(path: &Path) -> anyhow::Result<()> {
66 let bytes = std::fs::read(path).context("could not read BAM file")?;
67 if bytes.len() < 4 || &bytes[..4] != b"BAM\x01" {
68 anyhow::bail!("BAM magic bytes not found");
69 }
70 Ok(())
71}
72
73fn check_hmm(path: &Path) -> anyhow::Result<()> {
74 let content = std::fs::read_to_string(path).context("could not read HMM profile")?;
75 if !content.contains("HMMER3") {
76 anyhow::bail!("HMMER3 header not found in HMM profile");
77 }
78 Ok(())
79}
80
81fn check_tabular(path: &Path) -> anyhow::Result<()> {
82 let content = std::fs::read_to_string(path).context("could not read tabular file")?;
83 if content.is_empty() {
84 anyhow::bail!("tabular output is empty");
85 }
86 let data_lines: Vec<_> = content.lines().filter(|l| !l.starts_with('#')).collect();
87 if data_lines.is_empty() {
88 return Ok(());
89 }
90 let col_count = data_lines[0].split('\t').count();
91 if col_count < 2 {
92 anyhow::bail!("tabular output has fewer than 2 columns");
93 }
94 Ok(())
95}
96
97fn first_byte(path: &Path) -> anyhow::Result<u8> {
98 use std::io::Read;
99 let mut f = std::fs::File::open(path).context("could not open file")?;
100 let mut buf = [0u8; 1];
101 f.read_exact(&mut buf).context("file is empty")?;
102 Ok(buf[0])
103}