Skip to main content

bv_conformance/
assertions.rs

1use std::path::Path;
2
3use anyhow::Context;
4use bv_core::manifest::IoSpec;
5
6/// Verify that an output file or directory exists and passes a minimal
7/// format check for its declared type.
8pub 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}