Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,32 @@ cargo fmt
cargo clippy --workspace
```

## Regression tests

End-to-end regression tests live in `tests/regression/` and compare subcommand
output against committed snapshots using [insta](https://insta.rs/). Each test
runs the `ft` binary against a fixture in `tests/data/`, projects a stable
subset of columns (so trailing/added columns aren't false positives), and
asserts the result matches the snapshot in `tests/regression/snapshots/`.

Run just the regression suite with:

```bash
cargo test --test regression
```

When you intentionally change output, regenerate snapshots with
[`cargo-insta`](https://insta.rs/docs/cli/):

```bash
cargo install cargo-insta # one-time
cargo insta test --review # walk through diffs interactively
# or, if the new output is correct as-is:
cargo insta accept
```

Inspect the resulting `.snap` diff before committing.

## Cutting a release

The changelog is managed by git-cliff which will run with the release action.
Expand Down
63 changes: 54 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ vergen-git2 = { version = "1.0.2", features = [

[dev-dependencies]
assert_cmd = "2.0.11"
insta = "1.47.2"
predicates = "3.0.3"
tempfile = "3.3.0"

[features]
cli = ["dep:clap", "dep:clap_complete", "dep:clap_mangen", "dep:console"]
Expand Down
3 changes: 3 additions & 0 deletions tests/data/center.small.bed
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
chr1 41417257 41417292 CTCF_XL 3.160000e-11 - CAGCAGTTTCTGCCGCATGCCACCAGAGGGCACCA
chr11 44286466 44286501 CTCF_XL 9.450000e-11 + CTGCAGTTGTGCATGCTGGCCACCAGGAGGCACTG
chr15 74054174 74054209 CTCF_XL 3.870000e-11 + AAGCAGTTCCCCCATGTAACCTGCAGGTGGCACCA
12 changes: 12 additions & 0 deletions tests/regression.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#[path = "regression/center.rs"]
mod center;
#[path = "regression/common.rs"]
mod common;
#[path = "regression/extract.rs"]
mod extract;
#[path = "regression/fire.rs"]
mod fire;
#[path = "regression/pileup.rs"]
mod pileup;
#[path = "regression/qc.rs"]
mod qc;
28 changes: 28 additions & 0 deletions tests/regression/center.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use super::common::{fixture, run, select_tsv_cols};

// Subset to 3 motifs and cap features to ±200 bp of center to keep the
// snapshot small. The center math is uniform across distance, so this
// still exercises liftover + strand flipping for m6a/nuc/msp types.
#[test]
fn center_default() {
let out = run(&[
"center",
fixture("center.bam").to_str().unwrap(),
"--bed",
fixture("center.small.bed").to_str().unwrap(),
"--dist",
"200",
]);
insta::assert_snapshot!(select_tsv_cols(
&out,
&[
"chrom",
"centering_position",
"strand",
"query_name",
"centered_position_type",
"centered_start",
"centered_end",
]
));
}
96 changes: 96 additions & 0 deletions tests/regression/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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")
}
Loading
Loading