A facial recognition login service for Linux.
2

Configure Feed

Select the types of activity you want to include in your feed.

core,just: Skip hardware-gated tests cleanly with visible warnings

The hardware-gated tests were panicking when their prerequisite (a
plugged-in camera, or a PAREIDOLIA_TEST_* env var pointing at a model
file) was missing. That made sense as a 'you ran this wrong' assertion
when the only invocation path was explicit `just test-hardware` from
inside `nix develop` with hardware present — but it falls over the
moment any of those conditions slip.

Concrete case: a USB unplug or driver hiccup leaves /dev/video* gone,
and `just test-hardware` fails noisily even though the other hardware
tests (model-loading, inference) would have run fine. The panic also
wrongly suggests something's broken when in fact the test simply lacks
its physical prerequisite.

Replace each panic with an `eprintln!` describing what's missing and
how to set it up, then early-return. Nextest treats the early return
as a pass — conventional behaviour for hardware tests in the Rust
ecosystem — and the suite stays green when hardware is temporarily
absent.

The eprintln output is captured by nextest's default profile, so the
skip messages would be invisible unless tests failed. `test-hardware`
now passes `--no-capture` so each test's stderr streams through in
real time, and each skip message is prefixed `warning:` so it stands
out.

While I was in there: the per-camera capture test was tolerantly
`continue`-ing on individual open errors but then *succeeding* even
when every camera failed, defeating the point of the test (saw this
when the BRIO firmware wedged into an EIO state after a previous run).
It now asserts at least one capture succeeded among the enumerated
devices, and inserts a 500ms cooldown between iterations because UVC
cameras commonly EIO on rapid open-close-open cycles.

Isaac Corbrey (May 27, 2026, 7:15 PM EDT) fd91f71e 3cb4bbce

+62 -17
+25 -5
crates/core/src/camera/v4l2.rs
··· 397 397 /// capture one frame. Hardware-only; CI skips this via #[ignore]. 398 398 /// Run with: `cargo nextest run -p pareidolia-core -- --ignored` or 399 399 /// `just test-hardware`. 400 + /// 401 + /// Skips (passes) when no `/dev/video*` is present, so an unplugged 402 + /// camera doesn't take down the rest of the hardware suite. Failing 403 + /// to capture from *any* enumerated device is a hard failure, though 404 + /// — silently passing because every camera errored would defeat the 405 + /// point of the test. 400 406 #[test] 401 407 #[ignore = "requires a real V4L2 camera"] 402 408 fn capture_one_frame_from_each_enumerated_camera() { 403 409 let cams = enumerate(); 404 410 if cams.is_empty() { 405 - panic!( 406 - "no /dev/video* found; this test should be skipped via #[ignore] \ 407 - when no camera is present" 411 + eprintln!( 412 + "warning: no /dev/video* found; skipping \ 413 + (plug a camera in to exercise this test)" 408 414 ); 415 + return; 409 416 } 410 - for cam in cams { 417 + let mut captured_any = false; 418 + for cam in &cams { 411 419 let path = cam.path.clone().unwrap(); 412 420 let cfg = V4l2Config::new(&path, cam.name.clone(), cam.kind); 413 421 let mut src = match V4l2FrameSource::open(cfg) { 414 422 Ok(s) => s, 415 423 Err(e) => { 416 - eprintln!("skipping {path:?}: {e}"); 424 + eprintln!("warning: skipping {path:?}: {e}"); 417 425 continue; 418 426 } 419 427 }; ··· 421 429 assert!(frame.width > 0); 422 430 assert!(frame.height > 0); 423 431 assert!(!frame.data.is_empty()); 432 + captured_any = true; 433 + // Brief cooldown between device opens. UVC cameras commonly 434 + // return EIO if the next open happens before the firmware has 435 + // fully released the previous session; on the BRIO this 436 + // manifests as both video0 and video2 erroring out when the 437 + // test iterates them back-to-back. 438 + std::thread::sleep(std::time::Duration::from_millis(500)); 424 439 } 440 + assert!( 441 + captured_any, 442 + "every enumerated camera failed to open; saw {} candidate(s) but captured no frames", 443 + cams.len(), 444 + ); 425 445 } 426 446 }
+23 -5
crates/core/src/pipeline/arcface.rs
··· 264 264 Some(ArcfaceEmbedder::open(path, ArcfaceConfig::default()).expect("load arcface")) 265 265 } 266 266 267 + /// Helper: skip the test body cleanly (print + early return → nextest 268 + /// treats it as passing) when the model env var isn't set. Macro 269 + /// rather than function so the early return targets the caller. 270 + macro_rules! arcface_or_skip { 271 + () => { 272 + match open_for_validation() { 273 + Some(e) => e, 274 + None => { 275 + eprintln!( 276 + "warning: PAREIDOLIA_TEST_ARCFACE not set; \ 277 + skipping (use `nix develop`)" 278 + ); 279 + return; 280 + } 281 + } 282 + }; 283 + } 284 + 267 285 #[test] 268 286 #[ignore = "requires PAREIDOLIA_TEST_ARCFACE env var (set by nix develop)"] 269 287 fn embed_rejects_non_rgb8_input() { 270 - let mut embedder = open_for_validation().expect("env var"); 288 + let mut embedder = arcface_or_skip!(); 271 289 let bad = Frame { 272 290 width: ARCFACE_CHIP_SIZE, 273 291 height: ARCFACE_CHIP_SIZE, ··· 283 301 #[test] 284 302 #[ignore = "requires PAREIDOLIA_TEST_ARCFACE env var (set by nix develop)"] 285 303 fn embed_rejects_wrong_dimensions() { 286 - let mut embedder = open_for_validation().expect("env var"); 304 + let mut embedder = arcface_or_skip!(); 287 305 let bad = solid_chip(64, [128, 128, 128]); // not 112 288 306 let err = embedder.embed(&bad).expect_err("wrong size rejected"); 289 307 assert!(matches!(err, PipelineError::InvalidInputShape { .. })); ··· 292 310 #[test] 293 311 #[ignore = "requires PAREIDOLIA_TEST_ARCFACE env var (set by nix develop)"] 294 312 fn arcface_embeds_solid_gray_chip_to_unit_magnitude_vector() { 295 - let mut embedder = open_for_validation().expect("env var"); 313 + let mut embedder = arcface_or_skip!(); 296 314 let chip = solid_chip(ARCFACE_CHIP_SIZE, [128, 128, 128]); 297 315 let embedding = embedder.embed(&chip).expect("embed"); 298 316 ··· 307 325 #[test] 308 326 #[ignore = "requires PAREIDOLIA_TEST_ARCFACE env var (set by nix develop)"] 309 327 fn arcface_is_deterministic_on_repeated_inputs() { 310 - let mut embedder = open_for_validation().expect("env var"); 328 + let mut embedder = arcface_or_skip!(); 311 329 let chip = solid_chip(ARCFACE_CHIP_SIZE, [200, 100, 50]); 312 330 let a = embedder.embed(&chip).expect("embed a"); 313 331 let b = embedder.embed(&chip).expect("embed b"); ··· 325 343 #[test] 326 344 #[ignore = "requires PAREIDOLIA_TEST_ARCFACE env var (set by nix develop)"] 327 345 fn arcface_different_inputs_produce_different_embeddings() { 328 - let mut embedder = open_for_validation().expect("env var"); 346 + let mut embedder = arcface_or_skip!(); 329 347 let a = embedder 330 348 .embed(&solid_chip(ARCFACE_CHIP_SIZE, [200, 100, 50])) 331 349 .unwrap();
+5 -4
crates/core/src/pipeline/onnx.rs
··· 107 107 /// pointing at an .onnx file. Skipped by default; opt in via 108 108 /// `just test-hardware` or 109 109 /// `PAREIDOLIA_TEST_MODEL=/path/to/model.onnx cargo test ... -- --ignored`. 110 + /// 111 + /// Skips (passes) when the env var isn't set so a `just test-hardware` 112 + /// run outside the Nix devShell doesn't fail spuriously. 110 113 #[test] 111 114 #[ignore = "requires a real ONNX model file via PAREIDOLIA_TEST_MODEL"] 112 115 fn loads_real_model_and_exposes_io_names() { 113 116 let Ok(path) = std::env::var("PAREIDOLIA_TEST_MODEL") else { 114 - panic!( 115 - "PAREIDOLIA_TEST_MODEL not set; should be skipped via #[ignore] \ 116 - unless an env-provided model path is available" 117 - ); 117 + eprintln!("warning: PAREIDOLIA_TEST_MODEL not set; skipping (use `nix develop`)"); 118 + return; 118 119 }; 119 120 let session = OnnxSession::from_file(&path).expect("load real model"); 120 121 assert!(
+4 -2
crates/core/src/pipeline/scrfd.rs
··· 683 683 /// Loads the pinned SCRFD model and runs it on a synthetic black frame. 684 684 /// The detector should not panic and should find no faces (a black 685 685 /// image has none). Requires PAREIDOLIA_TEST_SCRFD (set automatically 686 - /// by `nix develop`). 686 + /// by `nix develop`). Skips when unset so the suite doesn't fail 687 + /// outside Nix. 687 688 #[test] 688 689 #[ignore = "requires PAREIDOLIA_TEST_SCRFD env var (set by nix develop)"] 689 690 fn scrfd_finds_no_faces_in_synthetic_black_frame() { 690 691 let Ok(path) = std::env::var("PAREIDOLIA_TEST_SCRFD") else { 691 - panic!("PAREIDOLIA_TEST_SCRFD not set; should be skipped via #[ignore]"); 692 + eprintln!("warning: PAREIDOLIA_TEST_SCRFD not set; skipping (use `nix develop`)"); 693 + return; 692 694 }; 693 695 694 696 let mut detector =
+5 -1
justfile
··· 40 40 # host: a V4L2 camera at /dev/video*, and for the inference tests a model 41 41 # file path in PAREIDOLIA_TEST_MODEL. 42 42 # 43 + # `--no-capture` makes each test's stderr visible in real time — important 44 + # because individual tests skip with a `warning:` line when their 45 + # prerequisite is missing, and you want to see that. 46 + # 43 47 # CI never runs this; it's the developer's responsibility to run locally 44 48 # before merging anything that touches a hardware-facing surface. 45 49 test-hardware: 46 - cargo nextest run -p pareidolia-core --features v4l2,inference --run-ignored only 50 + cargo nextest run -p pareidolia-core --features v4l2,inference --run-ignored only --no-capture 47 51 48 52 # Unit tests with all default-on features enabled. Runs the always-on 49 53 # portions of the v4l2 and inference modules without touching hardware.