From af354e49e81df2a020915d71306e2dc530b6737c Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Mon, 26 Oct 2026 16:22:51 +0100 Subject: [PATCH] M0.37: resolve relocated test binaries and expose GPU failures --- apps/effectcraft-cli/tests/it/cli.rs | 2 +- apps/effectcraft-cli/tests/it/main.rs | 5 ++++ apps/effectcraft-cli/tests/it/qa_template.rs | 15 ++++------ apps/effectcraft/tests/args.rs | 9 ++++-- crates/gpu/src/tests.rs | 31 +++++++++++++++++++- crates/gpu/src/tests_fx_noise.rs | 22 ++++++++++++-- xtask/tests/bench.rs | 7 ++++- 7 files changed, 75 insertions(+), 16 deletions(-) diff --git a/apps/effectcraft-cli/tests/it/cli.rs b/apps/effectcraft-cli/tests/it/cli.rs index 538887d9b..c7a905e58 100644 --- a/apps/effectcraft-cli/tests/it/cli.rs +++ b/apps/effectcraft-cli/tests/it/cli.rs @@ -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). diff --git a/apps/effectcraft-cli/tests/it/main.rs b/apps/effectcraft-cli/tests/it/main.rs index 17149f545..bca36ec78 100644 --- a/apps/effectcraft-cli/tests/it/main.rs +++ b/apps/effectcraft-cli/tests/it/main.rs @@ -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()) +} diff --git a/apps/effectcraft-cli/tests/it/qa_template.rs b/apps/effectcraft-cli/tests/it/qa_template.rs index 0618c0d63..8cb533fda 100644 --- a/apps/effectcraft-cli/tests/it/qa_template.rs +++ b/apps/effectcraft-cli/tests/it/qa_template.rs @@ -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(); diff --git a/apps/effectcraft/tests/args.rs b/apps/effectcraft/tests/args.rs index 8367d2a99..41281eb23 100644 --- a/apps/effectcraft/tests/args.rs +++ b/apps/effectcraft/tests/args.rs @@ -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"))); } diff --git a/crates/gpu/src/tests.rs b/crates/gpu/src/tests.rs index b2e1113e2..f4cc2edec 100644 --- a/crates/gpu/src/tests.rs +++ b/crates/gpu/src/tests.rs @@ -52,6 +52,26 @@ pub(crate) fn gpu() -> Option<&'static Gpu> { static G: OnceLock> = 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: ¶ms, 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"); } diff --git a/crates/gpu/src/tests_fx_noise.rs b/crates/gpu/src/tests_fx_noise.rs index 476f08642..d0a28231c 100644 --- a/crates/gpu/src/tests_fx_noise.rs +++ b/crates/gpu/src/tests_fx_noise.rs @@ -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}"); } } diff --git a/xtask/tests/bench.rs b/xtask/tests/bench.rs index 641b0d56b..3b2ad5722 100644 --- a/xtask/tests/bench.rs +++ b/xtask/tests/bench.rs @@ -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"]);