dino-quant 0.1.0

Experimental quantized DNA sketch prefilters for sequence-search pipelines.
Documentation
use std::error::Error;
use std::ffi::OsStr;
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};

fn temp_dir(name: &str) -> Result<PathBuf, Box<dyn Error>> {
    let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
    let dir =
        std::env::temp_dir().join(format!("dino-quant-{name}-{}-{nanos}", std::process::id()));
    fs::create_dir_all(&dir)?;
    Ok(dir)
}

fn run_dino_quant<I, S>(args: I) -> Result<Output, Box<dyn Error>>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    Ok(Command::new(env!("CARGO_BIN_EXE_dino-quant"))
        .args(args)
        .output()?)
}

fn assert_success(output: &Output) -> Result<(), Box<dyn Error>> {
    if output.status.success() {
        return Ok(());
    }
    Err(format!(
        "command failed\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    )
    .into())
}

#[test]
fn minimizer_cache_rejects_same_path_reference_content_change() -> Result<(), Box<dyn Error>> {
    let dir = temp_dir("cache-fingerprint")?;
    let ref_path = dir.join("ref.fa");
    let reads_path = dir.join("reads.fastq");
    let cache_path = dir.join("ref.dqmi");
    fs::write(&ref_path, b">ref\nACGTACGTGGGGTTTT\n")?;
    fs::write(&reads_path, b"@r1\nACGTACGT\n+\nFFFFFFFF\n")?;

    let args = [
        OsStr::new("emit-candidates"),
        ref_path.as_os_str(),
        reads_path.as_os_str(),
        OsStr::new("--retrieval"),
        OsStr::new("minimizer"),
        OsStr::new("--top-k"),
        OsStr::new("2"),
        OsStr::new("--window"),
        OsStr::new("8"),
        OsStr::new("--stride"),
        OsStr::new("4"),
        OsStr::new("--k"),
        OsStr::new("3"),
        OsStr::new("--dim"),
        OsStr::new("16"),
        OsStr::new("--candidate-limit"),
        OsStr::new("8"),
        OsStr::new("--minimizer-k"),
        OsStr::new("3"),
        OsStr::new("--minimizer-window"),
        OsStr::new("2"),
        OsStr::new("--reference-cache"),
        cache_path.as_os_str(),
    ];

    let first = run_dino_quant(args)?;
    assert_success(&first)?;
    assert!(String::from_utf8_lossy(&first.stderr).contains("cache_hit=false"));

    let second = run_dino_quant(args)?;
    assert_success(&second)?;
    assert!(String::from_utf8_lossy(&second.stderr).contains("cache_hit=true"));

    fs::write(&ref_path, b">ref\nTTTTCCCCAAAAGGGG\n")?;
    let changed = run_dino_quant(args)?;
    assert_success(&changed)?;
    assert!(String::from_utf8_lossy(&changed.stderr).contains("cache_hit=false"));

    fs::remove_dir_all(&dir)?;
    Ok(())
}

#[test]
fn candidate_reference_uses_target_id_for_duplicate_names() -> Result<(), Box<dyn Error>> {
    let dir = temp_dir("target-id")?;
    let ref_path = dir.join("ref.fa");
    let candidates_path = dir.join("candidates.tsv");
    fs::write(
        &ref_path,
        b">dup first copy\nAAAAAAAA\n>dup second copy\nCCCCCCCC\n",
    )?;
    fs::write(
        &candidates_path,
        b"query_name\tquery_len\trank\ttarget_name\ttarget_start\ttarget_end\tlinear_start\tlinear_end\tscore\ttarget_id\nq\t8\t1\tdup\t2\t6\t10\t14\t1.000000\t1\n",
    )?;

    let output = run_dino_quant([
        OsStr::new("emit-candidate-reference"),
        ref_path.as_os_str(),
        candidates_path.as_os_str(),
        OsStr::new("--mask-reference"),
        OsStr::new("--padding"),
        OsStr::new("0"),
    ])?;
    assert_success(&output)?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert_eq!(stdout, ">dup\nNNNNNNNN\n>dup\nNNCCCCNN\n");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("records=2"));
    assert!(stderr.contains("bases=16"));

    fs::remove_dir_all(&dir)?;
    Ok(())
}