fibertools-rs 0.12.0

Fiber-seq toolkit in rust
Documentation
use std::path::{Path, PathBuf};
use std::process::Command;

pub fn ft() -> PathBuf {
    env!("CARGO_BIN_EXE_ft").into()
}

pub fn fixture(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/data")
        .join(name)
}

/// Select named columns from TSV output by header name, in the order given.
/// Snapshots only the specified columns so that adding new columns to a
/// command's output doesn't count as a regression.
pub fn select_tsv_cols(tsv: &str, cols: &[&str]) -> String {
    let mut lines = tsv.lines();
    let headers: Vec<&str> = lines.next().unwrap_or("").split('\t').collect();
    let indices: Vec<usize> = cols
        .iter()
        .map(|c| {
            headers
                .iter()
                .position(|h| h == c)
                .unwrap_or_else(|| panic!("column {c:?} not found; headers: {headers:?}"))
        })
        .collect();
    let mut out = cols.join("\t") + "\n";
    for line in lines {
        let fields: Vec<&str> = line.split('\t').collect();
        out += &indices
            .iter()
            .map(|&i| fields.get(i).copied().unwrap_or(""))
            .collect::<Vec<_>>()
            .join("\t");
        out += "\n";
    }
    out
}

/// Select bed12 fields by name from headerless bed12 output. Field names follow
/// the bed12 spec. Emits a synthetic header so snapshots stay self-describing,
/// and lets tests drop redundant/constant columns (score, thick_*, item_rgb)
/// without binding to numeric indices.
pub fn select_bed12_cols(tsv: &str, cols: &[&str]) -> String {
    const BED12: &[&str] = &[
        "chrom",
        "start",
        "end",
        "name",
        "score",
        "strand",
        "thick_start",
        "thick_end",
        "item_rgb",
        "block_count",
        "block_sizes",
        "block_starts",
    ];
    let indices: Vec<usize> = cols
        .iter()
        .map(|c| {
            BED12
                .iter()
                .position(|f| f == c)
                .unwrap_or_else(|| panic!("bed12 field {c:?} not in spec; valid: {BED12:?}"))
        })
        .collect();
    let mut out = cols.join("\t") + "\n";
    for line in tsv.lines() {
        let fields: Vec<&str> = line.split('\t').collect();
        out += &indices
            .iter()
            .map(|&i| fields.get(i).copied().unwrap_or(""))
            .collect::<Vec<_>>()
            .join("\t");
        out += "\n";
    }
    out
}

/// Run ft with the given args; return stdout as a String. Panics on non-zero exit.
pub fn run(args: &[&str]) -> String {
    let out = Command::new(ft())
        .args(args)
        .output()
        .expect("failed to spawn ft");
    assert!(
        out.status.success(),
        "ft exited {}\nstderr: {}",
        out.status,
        String::from_utf8_lossy(&out.stderr)
    );
    String::from_utf8(out.stdout).expect("non-UTF8 stdout")
}