orphos-cli 0.3.0

Command-line interface for Orphos, a tool for finding protein-coding genes in microbial genomes.
mod common;

use std::collections::HashSet;
use std::fs;

use insta::assert_snapshot;
use tempfile::NamedTempFile;

use crate::common::{parse_gff_cds_records, run_orphos};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct CdsCoord {
    start: u32,
    end: u32,
    strand: char,
}

#[derive(Debug, Clone, Copy)]
struct ReferenceFixture {
    label: &'static str,
    accession: &'static str,
    input_path: &'static str,
    reference_path: &'static str,
}

const SELECTED_FIXTURES: &[ReferenceFixture] = &[
    ReferenceFixture {
        label: "salmonella",
        accession: "NC_003198.1",
        input_path: "tests/data/salmonella.fasta",
        reference_path: "tests/data/refseq/NC_003198.1.cds.tsv",
    },
    ReferenceFixture {
        label: "ecoli",
        accession: "NC_000913.3",
        input_path: "tests/data/ecoli.fasta",
        reference_path: "tests/data/refseq/NC_000913.3.cds.tsv",
    },
    ReferenceFixture {
        label: "helicobacter",
        accession: "NC_000915.1",
        input_path: "tests/data/single/NC_000915.1.fasta",
        reference_path: "tests/data/refseq/NC_000915.1.cds.tsv",
    },
    ReferenceFixture {
        label: "bacillus",
        accession: "NC_000964.3",
        input_path: "tests/data/single/NC_000964.3.fasta",
        reference_path: "tests/data/refseq/NC_000964.3.cds.tsv",
    },
];

#[test]
fn selected_fixtures_full_refseq_cds_summary() {
    unsafe {
        std::env::set_var("RAYON_NUM_THREADS", "1");
    }

    let mut summary = vec![
        "fixture\taccession\trefseq_cds\torphos_cds\torphos_hits(%)\torphos_only_vs_ref\tref_missing_orphos".to_string(),
    ];

    for fixture in SELECTED_FIXTURES {
        let refseq_coords = load_refseq_cds(fixture.reference_path);
        assert!(
            !refseq_coords.is_empty(),
            "No RefSeq CDS coordinates loaded for {}",
            fixture.accession
        );

        let rust_out = NamedTempFile::new().unwrap();

        run_orphos(
            fixture.input_path,
            rust_out.path().to_str().unwrap(),
            "gff",
            "single",
        )
        .unwrap();

        let orphos_coords = cds_coord_set(&fs::read_to_string(rust_out.path()).unwrap());
        let orphos_hits = intersection_size(&refseq_coords, &orphos_coords);

        summary.push(format!(
            "{}\t{}\t{}\t{}\t{:.3}\t{}\t{}",
            fixture.label,
            fixture.accession,
            refseq_coords.len(),
            orphos_coords.len(),
            percentage(orphos_hits, refseq_coords.len()),
            orphos_coords.difference(&refseq_coords).count(),
            refseq_coords.difference(&orphos_coords).count(),
        ));
    }

    assert_snapshot!("selected_refseq_full_cds_summary", summary.join("\n"));
}

fn cds_coord_set(gff: &str) -> HashSet<CdsCoord> {
    parse_gff_cds_records(gff)
        .into_iter()
        .map(|record| CdsCoord {
            start: record.start,
            end: record.end,
            strand: record.strand,
        })
        .collect()
}

fn load_refseq_cds(path: &str) -> HashSet<CdsCoord> {
    fs::read_to_string(path)
        .unwrap()
        .lines()
        .skip(1)
        .filter_map(|line| {
            let mut cols = line.split('\t');
            let start = cols.next()?.parse().ok()?;
            let end = cols.next()?.parse().ok()?;
            let strand = cols.next()?.chars().next()?;
            Some(CdsCoord { start, end, strand })
        })
        .collect()
}

fn intersection_size(left: &HashSet<CdsCoord>, right: &HashSet<CdsCoord>) -> usize {
    left.intersection(right).count()
}

fn percentage(matches: usize, total: usize) -> f32 {
    (matches as f32) * 100.0 / (total as f32)
}