use std::process::Command;
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
fn engines() -> &'static [&'static str] {
&["--run-tree", "--run-vm"]
}
fn run_ok(engine: &str, src: &str, fn_name: &str) -> String {
let out = ilo()
.args([src, engine, fn_name])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"ilo {engine} failed for `{src}`: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn run_err(engine: &str, src: &str, fn_name: &str) -> String {
let out = ilo()
.args([src, engine, fn_name])
.output()
.expect("failed to run ilo");
assert!(
!out.status.success(),
"ilo {engine} unexpectedly succeeded for `{src}`: stdout={}",
String::from_utf8_lossy(&out.stdout)
);
String::from_utf8_lossy(&out.stderr).to_string()
}
fn parse_pairs(s: &str) -> Vec<f64> {
let cleaned: String = s
.chars()
.map(|c| match c {
'[' | ']' | ',' => ' ',
_ => c,
})
.collect();
cleaned
.split_whitespace()
.map(|tok| tok.parse::<f64>().expect("non-numeric token in output"))
.collect()
}
fn parse_reals(s: &str) -> Vec<f64> {
let cleaned: String = s
.chars()
.map(|c| match c {
'[' | ']' | ',' => ' ',
_ => c,
})
.collect();
cleaned
.split_whitespace()
.map(|tok| tok.parse::<f64>().expect("non-numeric token in output"))
.collect()
}
#[test]
fn fft_dc_unit_impulse() {
let src = "f>L (L n);fft [1, 0, 0, 0]";
for engine in engines() {
let out = run_ok(engine, src, "f");
let flat = parse_pairs(&out);
assert_eq!(flat.len(), 8, "engine={engine}: got {out}");
for chunk in flat.chunks_exact(2) {
assert!(
(chunk[0] - 1.0).abs() < 1e-10,
"engine={engine}: re={}",
chunk[0]
);
assert!(chunk[1].abs() < 1e-10, "engine={engine}: im={}", chunk[1]);
}
}
}
#[test]
fn fft_pure_cosine_has_single_nonzero_bin() {
let samples: Vec<f64> = (0..8)
.map(|k| (2.0 * std::f64::consts::PI * (k as f64) / 8.0).cos())
.collect();
let lit = samples
.iter()
.map(|x| format!("{x}"))
.collect::<Vec<_>>()
.join(", ");
let src = format!("f>L (L n);fft [{lit}]");
for engine in engines() {
let out = run_ok(engine, &src, "f");
let flat = parse_pairs(&out);
assert_eq!(flat.len(), 16, "engine={engine}: got {out}");
for (i, chunk) in flat.chunks_exact(2).enumerate() {
let mag = (chunk[0] * chunk[0] + chunk[1] * chunk[1]).sqrt();
if i == 1 || i == 7 {
assert!(
(mag - 4.0).abs() < 1e-10,
"engine={engine} bin {i}: mag={mag}"
);
} else {
assert!(mag < 1e-10, "engine={engine} bin {i}: mag={mag}");
}
}
}
}
#[test]
fn ifft_round_trip_recovers_input() {
let src = "f>L n;ifft (fft [1, 2, 3, 4])";
for engine in engines() {
let out = run_ok(engine, src, "f");
let reals = parse_reals(&out);
assert_eq!(reals.len(), 4, "engine={engine}: got {out}");
let expected = [1.0, 2.0, 3.0, 4.0];
for (i, (got, want)) in reals.iter().zip(expected.iter()).enumerate() {
assert!(
(got - want).abs() < 1e-10,
"engine={engine} idx {i}: got {got}, want {want}"
);
}
}
}
#[test]
fn ifft_round_trip_with_zero_padding() {
let src = "f>L n;ifft (fft [1, 2, 3])";
for engine in engines() {
let out = run_ok(engine, src, "f");
let reals = parse_reals(&out);
assert_eq!(reals.len(), 4, "engine={engine}: got {out}");
let expected = [1.0, 2.0, 3.0, 0.0];
for (i, (got, want)) in reals.iter().zip(expected.iter()).enumerate() {
assert!(
(got - want).abs() < 1e-10,
"engine={engine} idx {i}: got {got}, want {want}"
);
}
}
}
#[test]
fn fft_empty_input_errors() {
let src = "f>L (L n);fft []";
for engine in engines() {
let err = run_err(engine, src, "f");
assert!(
err.contains("fft") || err.contains("empty"),
"engine={engine}: stderr={err}"
);
}
}
#[test]
fn fft_single_element_returns_single_pair() {
let src = "f>L (L n);fft [7]";
for engine in engines() {
let out = run_ok(engine, src, "f");
let flat = parse_pairs(&out);
assert_eq!(flat.len(), 2, "engine={engine}: got {out}");
assert!((flat[0] - 7.0).abs() < 1e-10, "engine={engine}");
assert!(flat[1].abs() < 1e-10, "engine={engine}");
}
}