diff --git a/crates/core/src/camera/v4l2.rs b/crates/core/src/camera/v4l2.rs index d6c9038..b61280b 100644 --- a/crates/core/src/camera/v4l2.rs +++ b/crates/core/src/camera/v4l2.rs @@ -397,23 +397,31 @@ mod tests { /// capture one frame. Hardware-only; CI skips this via #[ignore]. /// Run with: `cargo nextest run -p pareidolia-core -- --ignored` or /// `just test-hardware`. + /// + /// Skips (passes) when no `/dev/video*` is present, so an unplugged + /// camera doesn't take down the rest of the hardware suite. Failing + /// to capture from *any* enumerated device is a hard failure, though + /// — silently passing because every camera errored would defeat the + /// point of the test. #[test] #[ignore = "requires a real V4L2 camera"] fn capture_one_frame_from_each_enumerated_camera() { let cams = enumerate(); if cams.is_empty() { - panic!( - "no /dev/video* found; this test should be skipped via #[ignore] \ - when no camera is present" + eprintln!( + "warning: no /dev/video* found; skipping \ + (plug a camera in to exercise this test)" ); + return; } - for cam in cams { + let mut captured_any = false; + for cam in &cams { let path = cam.path.clone().unwrap(); let cfg = V4l2Config::new(&path, cam.name.clone(), cam.kind); let mut src = match V4l2FrameSource::open(cfg) { Ok(s) => s, Err(e) => { - eprintln!("skipping {path:?}: {e}"); + eprintln!("warning: skipping {path:?}: {e}"); continue; } }; @@ -421,6 +429,18 @@ mod tests { assert!(frame.width > 0); assert!(frame.height > 0); assert!(!frame.data.is_empty()); + captured_any = true; + // Brief cooldown between device opens. UVC cameras commonly + // return EIO if the next open happens before the firmware has + // fully released the previous session; on the BRIO this + // manifests as both video0 and video2 erroring out when the + // test iterates them back-to-back. + std::thread::sleep(std::time::Duration::from_millis(500)); } + assert!( + captured_any, + "every enumerated camera failed to open; saw {} candidate(s) but captured no frames", + cams.len(), + ); } } diff --git a/crates/core/src/pipeline/arcface.rs b/crates/core/src/pipeline/arcface.rs index a010cee..2d83d41 100644 --- a/crates/core/src/pipeline/arcface.rs +++ b/crates/core/src/pipeline/arcface.rs @@ -264,10 +264,28 @@ mod tests { Some(ArcfaceEmbedder::open(path, ArcfaceConfig::default()).expect("load arcface")) } + /// Helper: skip the test body cleanly (print + early return → nextest + /// treats it as passing) when the model env var isn't set. Macro + /// rather than function so the early return targets the caller. + macro_rules! arcface_or_skip { + () => { + match open_for_validation() { + Some(e) => e, + None => { + eprintln!( + "warning: PAREIDOLIA_TEST_ARCFACE not set; \ + skipping (use `nix develop`)" + ); + return; + } + } + }; + } + #[test] #[ignore = "requires PAREIDOLIA_TEST_ARCFACE env var (set by nix develop)"] fn embed_rejects_non_rgb8_input() { - let mut embedder = open_for_validation().expect("env var"); + let mut embedder = arcface_or_skip!(); let bad = Frame { width: ARCFACE_CHIP_SIZE, height: ARCFACE_CHIP_SIZE, @@ -283,7 +301,7 @@ mod tests { #[test] #[ignore = "requires PAREIDOLIA_TEST_ARCFACE env var (set by nix develop)"] fn embed_rejects_wrong_dimensions() { - let mut embedder = open_for_validation().expect("env var"); + let mut embedder = arcface_or_skip!(); let bad = solid_chip(64, [128, 128, 128]); // not 112 let err = embedder.embed(&bad).expect_err("wrong size rejected"); assert!(matches!(err, PipelineError::InvalidInputShape { .. })); @@ -292,7 +310,7 @@ mod tests { #[test] #[ignore = "requires PAREIDOLIA_TEST_ARCFACE env var (set by nix develop)"] fn arcface_embeds_solid_gray_chip_to_unit_magnitude_vector() { - let mut embedder = open_for_validation().expect("env var"); + let mut embedder = arcface_or_skip!(); let chip = solid_chip(ARCFACE_CHIP_SIZE, [128, 128, 128]); let embedding = embedder.embed(&chip).expect("embed"); @@ -307,7 +325,7 @@ mod tests { #[test] #[ignore = "requires PAREIDOLIA_TEST_ARCFACE env var (set by nix develop)"] fn arcface_is_deterministic_on_repeated_inputs() { - let mut embedder = open_for_validation().expect("env var"); + let mut embedder = arcface_or_skip!(); let chip = solid_chip(ARCFACE_CHIP_SIZE, [200, 100, 50]); let a = embedder.embed(&chip).expect("embed a"); let b = embedder.embed(&chip).expect("embed b"); @@ -325,7 +343,7 @@ mod tests { #[test] #[ignore = "requires PAREIDOLIA_TEST_ARCFACE env var (set by nix develop)"] fn arcface_different_inputs_produce_different_embeddings() { - let mut embedder = open_for_validation().expect("env var"); + let mut embedder = arcface_or_skip!(); let a = embedder .embed(&solid_chip(ARCFACE_CHIP_SIZE, [200, 100, 50])) .unwrap(); diff --git a/crates/core/src/pipeline/onnx.rs b/crates/core/src/pipeline/onnx.rs index ecc1ced..45aef43 100644 --- a/crates/core/src/pipeline/onnx.rs +++ b/crates/core/src/pipeline/onnx.rs @@ -107,14 +107,15 @@ mod tests { /// pointing at an .onnx file. Skipped by default; opt in via /// `just test-hardware` or /// `PAREIDOLIA_TEST_MODEL=/path/to/model.onnx cargo test ... -- --ignored`. + /// + /// Skips (passes) when the env var isn't set so a `just test-hardware` + /// run outside the Nix devShell doesn't fail spuriously. #[test] #[ignore = "requires a real ONNX model file via PAREIDOLIA_TEST_MODEL"] fn loads_real_model_and_exposes_io_names() { let Ok(path) = std::env::var("PAREIDOLIA_TEST_MODEL") else { - panic!( - "PAREIDOLIA_TEST_MODEL not set; should be skipped via #[ignore] \ - unless an env-provided model path is available" - ); + eprintln!("warning: PAREIDOLIA_TEST_MODEL not set; skipping (use `nix develop`)"); + return; }; let session = OnnxSession::from_file(&path).expect("load real model"); assert!( diff --git a/crates/core/src/pipeline/scrfd.rs b/crates/core/src/pipeline/scrfd.rs index 7c91ff0..6d3938f 100644 --- a/crates/core/src/pipeline/scrfd.rs +++ b/crates/core/src/pipeline/scrfd.rs @@ -683,12 +683,14 @@ mod tests { /// Loads the pinned SCRFD model and runs it on a synthetic black frame. /// The detector should not panic and should find no faces (a black /// image has none). Requires PAREIDOLIA_TEST_SCRFD (set automatically - /// by `nix develop`). + /// by `nix develop`). Skips when unset so the suite doesn't fail + /// outside Nix. #[test] #[ignore = "requires PAREIDOLIA_TEST_SCRFD env var (set by nix develop)"] fn scrfd_finds_no_faces_in_synthetic_black_frame() { let Ok(path) = std::env::var("PAREIDOLIA_TEST_SCRFD") else { - panic!("PAREIDOLIA_TEST_SCRFD not set; should be skipped via #[ignore]"); + eprintln!("warning: PAREIDOLIA_TEST_SCRFD not set; skipping (use `nix develop`)"); + return; }; let mut detector = diff --git a/justfile b/justfile index 0ccf882..37e774b 100644 --- a/justfile +++ b/justfile @@ -40,10 +40,14 @@ test-unit: # host: a V4L2 camera at /dev/video*, and for the inference tests a model # file path in PAREIDOLIA_TEST_MODEL. # +# `--no-capture` makes each test's stderr visible in real time — important +# because individual tests skip with a `warning:` line when their +# prerequisite is missing, and you want to see that. +# # CI never runs this; it's the developer's responsibility to run locally # before merging anything that touches a hardware-facing surface. test-hardware: - cargo nextest run -p pareidolia-core --features v4l2,inference --run-ignored only + cargo nextest run -p pareidolia-core --features v4l2,inference --run-ignored only --no-capture # Unit tests with all default-on features enabled. Runs the always-on # portions of the v4l2 and inference modules without touching hardware.