use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use serde_json::Value;
use crate::helpers::{fixture, put, scratch};
fn sva(dir: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_sva-cli"))
.current_dir(dir)
.args(args)
.arg("--cache")
.arg("none")
.output()
.expect("sva-cli runs")
}
fn json(out: &Output) -> Value {
serde_json::from_slice(&out.stdout).expect("one JSON envelope on stdout")
}
fn at<'v>(v: &'v Value, path: &[&str]) -> &'v Value {
path.iter().fold(v, |v, key| &v[*key])
}
fn items(v: &Value) -> &[Value] {
v.as_array().map_or(&[], Vec::as_slice)
}
fn composition(name: &str) -> (PathBuf, String) {
let root = scratch(name);
let dir = root.join("song");
std::fs::create_dir_all(dir.join("variables")).expect("a copy");
for rel in [
"tone",
"variables/gain",
"variables/idle",
"variables/tonic",
] {
std::fs::copy(fixture("solve").join(rel), dir.join(rel)).expect("a fixture file");
}
let reference = root.join("ref.wav");
let samples = format!("samples={}", reference.display());
let made = sva(
&dir,
&[
"render",
"@tone",
"--set",
"gain=0.8",
"--bits",
"32",
"--representation",
&samples,
],
);
assert!(
made.status.success(),
"{}",
String::from_utf8_lossy(&made.stdout)
);
(dir, reference.display().to_string())
}
fn envelope_cost(reference: &str) -> String {
format!("norm(repr.envelope(@tone) - repr.envelope(sample(\"{reference}\")))")
}
#[test]
fn a_solve_answers_each_exit_and_each_mistake_its_code() {
let (dir, reference) = composition("solve-refusals");
put(
&dir,
"reads-repr",
"; Models: a node reading a representation | Neglects: all | IO: (t) -> amplitude | Tags: fixture\nrepr.envelope(@tone)\n",
);
let cost = envelope_cost(&reference);
let missing = envelope_cost("/nowhere/at/all.wav");
let cases: [(Vec<&str>, &str, i32); 10] = [
(vec!["solve", &cost, "--vary", "gain=0..1/0.25"], "", 0),
(
vec!["solve", &cost, "--vary", "gain=1..0"],
"solve.bad_vary",
3,
),
(
vec!["solve", &cost, "--vary", "gain:0..1"],
"solve.bad_vary",
3,
),
(vec!["solve", &cost], "solve.no_vary", 3),
(
vec!["solve", &cost, "--vary", "tonic=0ms..10ms"],
"solve.reaches_no_number",
3,
),
(
vec!["solve", &cost, "--vary", "idle=0..2"],
"solve.reaches_nothing",
3,
),
(
vec![
"solve",
"repr.envelope(@tone) - repr.envelope(@tone)",
"--vary",
"gain=0..1",
],
"solve.vector_cost",
3,
),
(
vec!["solve", &missing, "--vary", "gain=0..1"],
"io.no_such_path",
24,
),
(
vec!["render", "@reads-repr", "--representation", "loudness"],
"repr-outside-solve",
3,
),
(
vec![
"solve",
&cost,
"--vary",
"gain=0..1/0.25",
"--max-renders",
"2",
"--write",
],
"",
4,
),
];
for (argv, code, exit) in cases {
let out = sva(&dir, &argv);
let answer = json(&out);
assert_eq!(out.status.code(), Some(exit), "{argv:?}: {answer:?}");
if !code.is_empty() {
let codes = items(at(&answer, &["error", "details", "codes", "items"]));
assert!(
codes.iter().any(|c| c == code),
"{argv:?} refuses as `{code}`: {answer:?}"
);
}
}
}
#[test]
fn a_solve_answers_values_cost_terms_renders_stopped_by_and_trace() {
let (dir, reference) = composition("solve-envelope");
let cost = format!(
"{} + 0.5*norm(repr.loudness(@tone) - repr.loudness(sample(\"{reference}\")))",
envelope_cost(&reference)
);
let out = sva(
&dir,
&[
"solve",
&cost,
"--vary",
"gain=0..1/0.25",
"--max-renders",
"4",
"--threads",
"1",
],
);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stdout)
);
let data = json(&out)["data"].clone();
let gain = at(&data, &["values", "gain"]);
assert!(gain["value"].is_f64() || gain["value"].is_u64(), "{data:?}");
assert!(
gain["text"].as_str().is_some_and(|t| !t.is_empty()),
"{data:?}"
);
let terms = items(at(&data, &["cost", "terms", "items"]));
assert_eq!(terms.len(), 2, "one per top-level term: {data:?}");
assert!(at(&data, &["cost", "total"]).is_number(), "{data:?}");
let renders = data["renders"].as_u64().expect("a count") as usize;
assert!(renders >= 1, "{data:?}");
assert_eq!(
items(at(&data, &["trace", "items"])).len(),
renders,
"{data:?}"
);
let stopped = data["stopped_by"].as_str().unwrap_or_default();
assert!(["converged", "budget"].contains(&stopped), "{data:?}");
let lines = String::from_utf8_lossy(&out.stderr);
assert_eq!(
lines.lines().count(),
renders,
"one progress line per render: {lines}"
);
}
#[cfg(unix)]
#[test]
fn a_signal_stops_a_solve_with_the_best_so_far() {
use std::io::{BufRead, BufReader};
let (dir, reference) = composition("solve-signal");
let cost = envelope_cost(&reference);
let mut child = Command::new(env!("CARGO_BIN_EXE_sva-cli"))
.current_dir(&dir)
.args([
"solve",
&cost,
"--vary",
"gain=0..1/0.001",
"--cache",
"none",
])
.args(["--max-renders", "100000", "--step-tol", "0.0000001"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("sva-cli runs");
let mut lines = BufReader::new(child.stderr.take().expect("stderr"));
let mut first = String::new();
lines.read_line(&mut first).expect("a first progress line");
Command::new("kill")
.args(["-INT", &child.id().to_string()])
.status()
.expect("kill runs");
let mut rest = String::new();
std::io::Read::read_to_string(&mut lines, &mut rest).expect("the progress after it");
let out = child.wait_with_output().expect("it exits");
let answer = json(&out);
assert_eq!(out.status.code(), Some(130), "{answer:?}");
assert_eq!(answer["error"]["code"], "cancelled", "{answer:?}");
assert_eq!(answer["data"]["stopped_by"], "cancelled", "{answer:?}");
assert!(at(&answer, &["data", "values", "gain", "value"]).is_number());
}
#[test]
fn sample_of_a_path_is_the_recording_at_the_render_rate() {
let (dir, reference) = composition("solve-sample");
let back = dir.parent().expect("a parent").join("back.wav");
let samples = format!("samples={}", back.display());
let read = format!("sample(\"{reference}\")");
let out = sva(
&dir,
&[
"render",
&read,
"--bits",
"32",
"--representation",
&samples,
],
);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stdout)
);
let planes = |path: &str| sva_cli::read_channels(Path::new(path)).expect("a wav").0;
assert_eq!(planes(&reference), planes(&back.display().to_string()));
let out = sva(
&dir,
&[
"render",
&read,
"--rate",
"48000",
"--representation",
"loudness",
],
);
let answer = json(&out);
let one = items(at(&answer, &["data", "label", "resampled", "items"]))
.first()
.expect("a resampling stated");
assert_eq!(one["from_hz"], 44_100);
assert_eq!(one["to_hz"], 48_000);
for key in ["passband_hz", "stopband_hz", "ripple_db", "stopband_db"] {
assert!(one[key].as_f64().is_some_and(|v| v > 0.0), "{key}: {one:?}");
}
let out = sva(
&dir,
&["render", "0.5*\"x.wav\"", "--representation", "loudness"],
);
assert_eq!(out.status.code(), Some(3));
assert!(
String::from_utf8_lossy(&out.stdout).contains("sample("),
"names what works"
);
}