mod support;
use std::process::{Command, Output};
use spoars::graph::Graph;
use support::generators::upstream_fastq_with_names;
use support::oracle::{run_oracle, AlignType, OracleCase, OracleResult};
const DATA: &str = "third_party/spoa/test/data/sample.fastq.gz";
fn run_bin_with_env(args: &[&str], envs: &[(&str, &str)]) -> Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_spoars"));
command.args(args).current_dir(env!("CARGO_MANIFEST_DIR"));
for (key, value) in envs {
command.env(key, value);
}
command
.output()
.expect("failed to execute the spoars binary")
}
fn run_bin(args: &[&str]) -> Output {
run_bin_with_env(args, &[])
}
fn run_bin_ok(args: &[&str]) -> String {
let output = run_bin(args);
assert!(
output.status.success(),
"spoars {args:?} exited with {}: stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).expect("spoars stdout was not valid UTF-8")
}
fn run_bin_ok_forced_sisd(args: &[&str]) -> String {
let output = run_bin_with_env(args, &[("SPOARS_FORCE_SISD", "1")]);
assert!(
output.status.success(),
"spoars {args:?} (SPOARS_FORCE_SISD=1) exited with {}: stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).expect("spoars stdout was not valid UTF-8")
}
fn base_case(ty: AlignType) -> (OracleCase, Vec<String>) {
let (seqs, quals, names) = upstream_fastq_with_names();
let seq_refs: Vec<&str> = seqs.iter().map(|s| s.as_str()).collect();
let qual_refs: Vec<&str> = quals.iter().map(|q| q.as_str()).collect();
let mut case = OracleCase::with_quals(ty, &seq_refs, &qual_refs);
case.names = Some(names.clone());
(case, names)
}
fn build_graph_from_oracle_alignments(case: &OracleCase, exp: &OracleResult) -> Graph {
let mut g = Graph::new();
for i in 0..case.seqs.len() {
let seq = case.seqs[i].as_bytes();
let alignment = &exp.alignments[i];
if let Some(quals) = &case.quals {
g.add_alignment_quality(alignment, seq, quals[i].as_bytes())
.unwrap_or_else(|e| panic!("add_alignment_quality failed for seq {i}: {e}"));
} else {
g.add_alignment_weight(alignment, seq, 1)
.unwrap_or_else(|e| panic!("add_alignment_weight failed for seq {i}: {e}"));
}
}
g
}
#[test]
fn cli_r0_consensus_matches_oracle_defaults() {
let (case, _names) = base_case(AlignType::Sw);
let exp = &run_oracle(std::slice::from_ref(&case))[0];
let stdout = run_bin_ok(&["-r", "0", DATA]);
let expected = format!(
">Consensus LN:i:{}\n{}\n",
exp.consensus.len(),
exp.consensus
);
assert_eq!(stdout, expected);
}
#[test]
fn cli_r1_msa_matches_oracle() {
let (case, names) = base_case(AlignType::Sw);
let exp = &run_oracle(std::slice::from_ref(&case))[0];
let stdout = run_bin_ok(&["-r", "1", DATA]);
let mut expected = String::new();
for (i, row) in exp.msa.iter().enumerate() {
expected.push_str(&format!(">{}\n{row}\n", names[i]));
}
assert_eq!(stdout, expected);
}
#[test]
fn cli_r2_msa_and_consensus_matches_library_replay() {
let (case, names) = base_case(AlignType::Sw);
let exp = &run_oracle(std::slice::from_ref(&case))[0];
let mut g = build_graph_from_oracle_alignments(&case, exp);
let msa_with_consensus = g.generate_msa(true);
let stdout = run_bin_ok(&["-r", "2", DATA]);
let mut expected = String::new();
for (i, row) in msa_with_consensus.iter().enumerate() {
let name = if i < names.len() {
names[i].as_str()
} else {
"Consensus"
};
expected.push_str(&format!(">{name}\n{row}\n"));
}
assert_eq!(stdout, expected);
}
#[test]
fn cli_r3_gfa_matches_oracle() {
let (case, _names) = base_case(AlignType::Sw);
let exp = &run_oracle(std::slice::from_ref(&case))[0];
let stdout = run_bin_ok(&["-r", "3", DATA]);
assert_eq!(stdout, exp.gfa);
}
#[test]
fn cli_r0_consensus_matches_oracle_with_non_default_scores() {
let (mut case, _names) = base_case(AlignType::Sw);
case.m = 3;
case.n = -2;
let exp = &run_oracle(std::slice::from_ref(&case))[0];
let stdout = run_bin_ok(&["-m", "3", "-n", "-2", "-r", "0", DATA]);
let expected = format!(
">Consensus LN:i:{}\n{}\n",
exp.consensus.len(),
exp.consensus
);
assert_eq!(stdout, expected);
}
#[test]
fn cli_l1_nw_consensus_matches_oracle() {
let (case, _names) = base_case(AlignType::Nw);
let exp = &run_oracle(std::slice::from_ref(&case))[0];
let stdout = run_bin_ok(&["-l", "1", "-r", "0", DATA]);
let expected = format!(
">Consensus LN:i:{}\n{}\n",
exp.consensus.len(),
exp.consensus
);
assert_eq!(stdout, expected);
}
#[test]
fn cli_version_prints_crate_version() {
let stdout = run_bin_ok(&["--version"]);
assert_eq!(stdout, format!("{}\n", env!("CARGO_PKG_VERSION")));
}
#[test]
fn cli_invalid_algorithm_exits_non_zero() {
let output = run_bin(&["-l", "5", DATA]);
assert!(
!output.status.success(),
"spoars -l 5 should fail (algorithm >= 3 is invalid)"
);
}
#[test]
fn cli_score_i8_wrap_does_not_error() {
let output = run_bin(&["-m", "200", "-r", "0", DATA]);
assert!(
output.status.success(),
"spoars -m 200 should succeed via i32->i8 wraparound (200 -> -56), not fail to parse; \
stderr={}",
String::from_utf8_lossy(&output.stderr)
);
}
fn assert_simd_matches_sisd_and_expected(args: &[&str], expected: &str) {
let stdout_simd = run_bin_ok(args);
let stdout_sisd = run_bin_ok_forced_sisd(args);
assert_eq!(
stdout_simd, expected,
"spoars {args:?} (default SimdEngine/NEON) must match the oracle"
);
assert_eq!(
stdout_sisd, expected,
"spoars {args:?} (SPOARS_FORCE_SISD=1) must match the oracle"
);
assert_eq!(
stdout_simd, stdout_sisd,
"spoars {args:?}: SimdEngine (NEON) and forced-SISD output must be byte-identical"
);
}
#[test]
fn cli_r0_consensus_simd_matches_sisd_and_oracle() {
let (case, _names) = base_case(AlignType::Sw);
let exp = &run_oracle(std::slice::from_ref(&case))[0];
let expected = format!(
">Consensus LN:i:{}\n{}\n",
exp.consensus.len(),
exp.consensus
);
assert_simd_matches_sisd_and_expected(&["-r", "0", DATA], &expected);
}
#[test]
fn cli_r1_msa_simd_matches_sisd_and_oracle() {
let (case, names) = base_case(AlignType::Sw);
let exp = &run_oracle(std::slice::from_ref(&case))[0];
let mut expected = String::new();
for (i, row) in exp.msa.iter().enumerate() {
expected.push_str(&format!(">{}\n{row}\n", names[i]));
}
assert_simd_matches_sisd_and_expected(&["-r", "1", DATA], &expected);
}
#[test]
fn cli_r3_gfa_simd_matches_sisd_and_oracle() {
let (case, _names) = base_case(AlignType::Sw);
let exp = &run_oracle(std::slice::from_ref(&case))[0];
assert_simd_matches_sisd_and_expected(&["-r", "3", DATA], &exp.gfa);
}
#[test]
fn cli_l1_nw_consensus_simd_matches_sisd_and_oracle() {
let (case, _names) = base_case(AlignType::Nw);
let exp = &run_oracle(std::slice::from_ref(&case))[0];
let expected = format!(
">Consensus LN:i:{}\n{}\n",
exp.consensus.len(),
exp.consensus
);
assert_simd_matches_sisd_and_expected(&["-l", "1", "-r", "0", DATA], &expected);
}
#[test]
fn cli_r0_consensus_simd_matches_sisd_and_oracle_with_non_default_scores() {
let (mut case, _names) = base_case(AlignType::Sw);
case.m = 3;
case.n = -2;
let exp = &run_oracle(std::slice::from_ref(&case))[0];
let expected = format!(
">Consensus LN:i:{}\n{}\n",
exp.consensus.len(),
exp.consensus
);
assert_simd_matches_sisd_and_expected(&["-m", "3", "-n", "-2", "-r", "0", DATA], &expected);
}