M0.37: resolve relocated test binaries and expose GPU failures

This commit is contained in:
skylar taylor-barrick
2026-10-26 16:22:51 +01:00
parent 69b9ce2bd1
commit af354e49e8
7 changed files with 75 additions and 16 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ use std::process::{Command, Stdio};
use serde_json::{Value, json};
fn bin() -> Command {
Command::new(env!("CARGO_BIN_EXE_effectcraft-cli"))
Command::new(crate::binary())
}
/// Run with `--json`; returns (exit code, parsed stdout).
+5
View File
@@ -2,3 +2,8 @@
mod cli;
mod qa_template;
/// Nextest archives relocate binaries; plain Cargo keeps the compile-time path valid.
fn binary() -> std::ffi::OsString {
std::env::var_os("NEXTEST_BIN_EXE_effectcraft_cli").unwrap_or_else(|| env!("CARGO_BIN_EXE_effectcraft-cli").into())
}
+6 -9
View File
@@ -9,7 +9,7 @@ use std::process::Command;
use serde_json::Value;
fn cli(dir: &PathBuf, args: &[&str]) -> (i32, Value) {
let out = Command::new(env!("CARGO_BIN_EXE_effectcraft-cli")).current_dir(dir).args(args).arg("--json").output().unwrap();
let out = Command::new(crate::binary()).current_dir(dir).args(args).arg("--json").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let v = serde_json::from_str(stdout.trim()).unwrap_or_else(|e| panic!("{args:?}: {e}: {stdout} {}", String::from_utf8_lossy(&out.stderr)));
(out.status.code().unwrap_or(-1), v)
@@ -191,7 +191,7 @@ fn essential_graphics_template_via_script_and_commands() {
vec!["render", "--template", "lt.ectemplate", "--queue"],
vec!["render-frame", "--template", "lt.ectemplate", "--bridge", "1"],
] {
let output = Command::new(env!("CARGO_BIN_EXE_effectcraft-cli")).current_dir(&dir).args(&args).output().unwrap();
let output = Command::new(crate::binary()).current_dir(&dir).args(&args).output().unwrap();
assert_eq!(output.status.code(), Some(2), "{args:?}: {}", String::from_utf8_lossy(&output.stderr));
}
@@ -251,7 +251,7 @@ fn assert_template_output_preserves_inputs(dir: &PathBuf, out: &str) -> Value {
let template_before = std::fs::read(dir.join("lt.ectemplate")).unwrap();
let values_before = std::fs::read(dir.join("values.json")).unwrap();
// Read the inputs before interpreting the subprocess status or error text.
let result = Command::new(env!("CARGO_BIN_EXE_effectcraft-cli"))
let result = Command::new(crate::binary())
.current_dir(dir)
.args(["render-frame", "--template", "lt.ectemplate", "--values", "values.json", "--time", "1", "--out", out, "--json"])
.output()
@@ -527,7 +527,7 @@ fn template_unknown_control_does_not_replace_extracted_media() {
let invalid_values_before = std::fs::read(dir.join("bad-values.json")).unwrap();
// Capture every file before interpreting status/JSON, so a failed command cannot hide a write.
let result = Command::new(env!("CARGO_BIN_EXE_effectcraft-cli"))
let result = Command::new(crate::binary())
.current_dir(&dir)
.args(["render-frame", "--template", "lt.ectemplate", "--values", "bad-values.json", "--time", "1", "--out", "failed.png", "--json"])
.output()
@@ -623,11 +623,8 @@ fn template_missing_output_does_not_replace_extracted_media() {
assert_ne!(sentinel_before, source_before);
// Observe writes before interpreting the actual CLI usage exit.
let result = Command::new(env!("CARGO_BIN_EXE_effectcraft-cli"))
.current_dir(&dir)
.args(["render", "--template", "lt.ectemplate", "--values", "values.json", "--json"])
.output()
.unwrap();
let result =
Command::new(crate::binary()).current_dir(&dir).args(["render", "--template", "lt.ectemplate", "--values", "values.json", "--json"]).output().unwrap();
let template_after = std::fs::read(dir.join("lt.ectemplate")).unwrap();
let values_after = std::fs::read(dir.join("values.json")).unwrap();
let source_after = std::fs::read(dir.join("source.png")).unwrap();
+7 -2
View File
@@ -6,6 +6,11 @@ use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
/// Nextest archives relocate binaries; plain Cargo keeps the compile-time path valid.
fn binary() -> std::ffi::OsString {
std::env::var_os("NEXTEST_BIN_EXE_effectcraft").unwrap_or_else(|| env!("CARGO_BIN_EXE_effectcraft").into())
}
/// A settings folder of its own, so a launch that got past the arguments would show up as
/// preferences (Recent Footage) written there instead of in the user's settings.
fn config_dir(name: &str) -> PathBuf {
@@ -19,7 +24,7 @@ fn config_dir(name: &str) -> PathBuf {
/// regression fails instead of leaving a window open: the process is killed after 5 s.
fn rejects(name: &str, args: &[&str], env: &[(&str, &str)], message: &str) {
let config = config_dir(name);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_effectcraft"));
let mut cmd = Command::new(binary());
cmd.args(args).env_remove("EFFECTCRAFT_CONTROL_PORT").env_remove("RUST_LOG").env("EFFECTCRAFT_CONFIG_DIR", &config);
for (k, v) in env {
cmd.env(k, v);
@@ -67,7 +72,7 @@ fn a_bad_control_port_stops_the_launch() {
#[test]
fn version_still_prints_with_a_bad_port_variable() {
let out = Command::new(env!("CARGO_BIN_EXE_effectcraft")).arg("--version").env("EFFECTCRAFT_CONTROL_PORT", "abc").output().unwrap();
let out = Command::new(binary()).arg("--version").env("EFFECTCRAFT_CONTROL_PORT", "abc").output().unwrap();
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8(out.stdout).unwrap(), format!("effectcraft {}\n", env!("CARGO_PKG_VERSION")));
}
+30 -1
View File
@@ -52,6 +52,26 @@ pub(crate) fn gpu() -> Option<&'static Gpu> {
static G: OnceLock<Result<Gpu, String>> = OnceLock::new();
hold_gpu_lock();
let g = G.get_or_init(|| {
// Readback mapping/allocation errors become None without necessarily retiring the
// context. Preserve their original messages alongside the failure's health snapshot.
struct GpuTestLog;
impl log::Log for GpuTestLog {
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
metadata.level() <= log::Level::Warn && metadata.target().starts_with("effectcraft_gpu")
}
fn log(&self, record: &log::Record<'_>) {
if self.enabled(record.metadata()) {
eprintln!("{} {}: {}", record.level(), record.target(), record.args());
}
}
fn flush(&self) {}
}
static LOGGER: GpuTestLog = GpuTestLog;
if log::set_logger(&LOGGER).is_ok() {
log::set_max_level(log::LevelFilter::Warn);
}
let g = Gpu::try_headless();
match &g {
Ok(g) => eprintln!("effectcraft-gpu tests: adapter {}", g.ctx.name),
@@ -481,7 +501,16 @@ pub(crate) fn effect_direct(id: &str, vals: &[(&str, Value)], adjustment: bool)
let buf = effectcraft_effects::Buf { img, offset: [0.0, 0.0], scale };
let ctx = || effectcraft_effects::EffectCtx { params: &params, time: 0.25, layer_size: size, seed: 11, adjustment, env: Default::default() };
let cpu = (spec.render)(&ctx(), buf.clone());
let out = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
let started = std::time::Instant::now();
let out = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).unwrap_or_else(|| {
panic!(
"the GPU runs {id} {vals:?} adj {adjustment} scale {scale}; elapsed={:?}, health={:?}, unavailable={:?}, transfers={:?}",
started.elapsed(),
g.ctx.check_health(),
g.ctx.unavailable_kernels(),
g.ctx.transfer_stats()
)
});
if expect_empty {
assert!(out.img.data.iter().all(|p| p[3] == 0.0), "{id} {vals:?} adj {adjustment} scale {scale}: Cut Away 100 produced non-zero alpha");
}
+20 -2
View File
@@ -237,8 +237,17 @@ fn median_levels_match_cpu_division() {
// A monotone row's median is its centre, including repeated edges. Exercise both
// the bisection and sliding-histogram kernels at every quantised level.
let mut enc = crate::context::Enc::new(&g.ctx);
let started = std::time::Instant::now();
let median = crate::fx_noise::median_image(&mut enc, &input, radius, false);
let actual = enc.download(&median).unwrap();
let actual = enc.download(&median).unwrap_or_else(|| {
panic!(
"median download radius {radius}; elapsed={:?}, health={:?}, unavailable={:?}, transfers={:?}",
started.elapsed(),
g.ctx.check_health(),
g.ctx.unavailable_kernels(),
g.ctx.transfer_stats()
)
});
for (level, (want, got)) in image.data.iter().zip(&actual.data).enumerate() {
assert_eq!(got, want, "radius {radius}, level {level}");
}
@@ -273,7 +282,16 @@ fn dust_strict_threshold_boundaries() {
let buf = effectcraft_effects::Buf { img, offset: [0.0; 2], scale: 1.0 };
let cpu = (spec.render)(&ctx(), buf.clone());
assert_eq!(cpu.img.data[24] != original, replaced, "CPU radius {radius}, threshold {threshold}");
let gpu = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).unwrap();
let started = std::time::Instant::now();
let gpu = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).unwrap_or_else(|| {
panic!(
"dust radius {radius}, threshold {threshold}; elapsed={:?}, health={:?}, unavailable={:?}, transfers={:?}",
started.elapsed(),
g.ctx.check_health(),
g.ctx.unavailable_kernels(),
g.ctx.transfer_stats()
)
});
assert_eq!(gpu.img.data[24], cpu.img.data[24], "radius {radius}, threshold {threshold}");
}
}
+6 -1
View File
@@ -2,6 +2,11 @@
use serde_json::{Value, json};
use std::process::{Command, Output};
/// Nextest archives relocate binaries; plain Cargo keeps the compile-time path valid.
fn binary() -> std::ffi::OsString {
std::env::var_os("NEXTEST_BIN_EXE_xtask").unwrap_or_else(|| env!("CARGO_BIN_EXE_xtask").into())
}
struct ResultsDir(tempfile::TempDir);
impl ResultsDir {
fn new() -> Self {
@@ -12,7 +17,7 @@ impl ResultsDir {
let candidate = self.0.path().join("candidate.jsonl");
std::fs::write(&baseline, old).unwrap();
std::fs::write(&candidate, new).unwrap();
let mut command = Command::new(env!("CARGO_BIN_EXE_xtask"));
let mut command = Command::new(binary());
command.args(["bench", "--compare"]).arg(baseline).arg("--with").arg(candidate);
if gate {
command.args(["--fail-over", "5"]);