A facial recognition login service for Linux.
2

Configure Feed

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

core: Saturate IR/calibration logic with property and stress tests

A targeted testing pass over the pure logic added across the IR-capture
and calibration work, plus one latent bug it surfaced.

# Bug fix: strobe period truncation

`classify_ir_mode` computed the strobe period as `(w[1] - w[0]) as u8`,
which *truncates* (wraps mod 256) when bright frames are more than 255
indices apart — a long, sparse burst could classify as a bogus tiny
period. Now uses a saturating `u8::try_from(...).unwrap_or(u8::MAX)`. A
regression test pins it (gap of 300 → 255, not 44), and was confirmed to
fail against the old code.

# Property tests

- `select_brightest`: result is an in-bounds true argmax; `Some` iff
the input is non-empty.
- `classify_ir_mode`: total (never panics) over arbitrary `&[f32]`
including NaN/inf/empty; a synthesized period-`p` strobe round-trips
to `Strobed { period: p }`; a sub-contrast stream is always
`Constant`; the result's `min_burst()` is always >= 1 and any strobed
period >= 2.
- Calibration store: `save_all`→`load` round-trips an arbitrary record
set (mirrors the enrollment store's headline property); `upsert`
leaves exactly one record per identity carrying the new mode.
- `resolve_calibration`: total; `Exact` implies an identical full
identity; any non-`None` match implies a same-card+kind record exists;
deterministic.
- `resolve_ir_camera` / `resolve_rgb_camera`: explicit path always wins;
total; an adopted camera is always of the matching kind; RGB adoption
happens exactly when there's a single path-bearing RGB camera.

# Stress test

Calibration store: 16 threads racing `upsert` on one file. The
read-modify-write isn't serialized (last-writer-wins on the record set,
as documented), so the test asserts only *integrity* — atomic
temp+rename means every post-race `load` succeeds on a structurally
valid, non-torn file and two consecutive loads agree.

Isaac Corbrey (May 29, 2026, 7:34 AM EDT) 8fb19a63 e8119d13

+466 -4
+103
crates/core/src/camera/calibration/mod.rs
··· 323 323 CalibrationMatch::MovedPort { .. }, 324 324 )); 325 325 } 326 + 327 + // ----- proptests ---------------------------------------------------------- 328 + 329 + use proptest::prelude::*; 330 + 331 + fn arb_kind() -> impl Strategy<Value = CameraKind> { 332 + prop_oneof![Just(CameraKind::Ir), Just(CameraKind::Rgb)] 333 + } 334 + 335 + fn arb_camera() -> impl Strategy<Value = CameraInfo> { 336 + // Small alphabets for card/bus so collisions (the interesting 337 + // matching cases) actually occur during generation. 338 + ( 339 + prop_oneof![Just("A"), Just("B"), Just("C")], 340 + proptest::option::of(prop_oneof![Just("u1"), Just("u2"), Just("u3")]), 341 + arb_kind(), 342 + ) 343 + .prop_map(|(card, bus, kind)| CameraInfo { 344 + name: card.to_string(), 345 + path: Some(std::path::PathBuf::from("/dev/videoX")), 346 + kind, 347 + bus: bus.map(str::to_string), 348 + }) 349 + } 350 + 351 + fn arb_calibration() -> impl Strategy<Value = IrCalibration> { 352 + ( 353 + prop_oneof![Just("A"), Just("B"), Just("C")], 354 + proptest::option::of(prop_oneof![Just("u1"), Just("u2"), Just("u3")]), 355 + arb_kind(), 356 + ) 357 + .prop_map(|(card, bus, kind)| IrCalibration { 358 + identity: CameraIdentity { 359 + card: card.to_string(), 360 + bus: bus.map(str::to_string), 361 + kind, 362 + }, 363 + mode: IrCaptureMode::Strobed { period: 2 }, 364 + calibrated_at_unix: 0, 365 + }) 366 + } 367 + 368 + proptest! { 369 + /// `resolve_calibration` is total: it never panics on any 370 + /// combination of target, record set, and connected list. 371 + #[test] 372 + fn resolve_calibration_never_panics( 373 + target in arb_camera(), 374 + records in proptest::collection::vec(arb_calibration(), 0..=8), 375 + connected in proptest::collection::vec(arb_camera(), 0..=8), 376 + ) { 377 + let _ = resolve_calibration(&target, &records, &connected); 378 + } 379 + 380 + /// An `Exact` result always corresponds to a record whose full 381 + /// identity (card + bus + kind) equals the target's. This is the 382 + /// invariant that guarantees we never apply another camera's 383 + /// calibration as an exact hit. 384 + #[test] 385 + fn exact_match_implies_identical_identity( 386 + target in arb_camera(), 387 + records in proptest::collection::vec(arb_calibration(), 0..=8), 388 + connected in proptest::collection::vec(arb_camera(), 0..=8), 389 + ) { 390 + let target_id = CameraIdentity::from_info(&target); 391 + if let CalibrationMatch::Exact(rec) = 392 + resolve_calibration(&target, &records, &connected) 393 + { 394 + prop_assert_eq!(rec.identity, target_id); 395 + } 396 + } 397 + 398 + /// Any non-`None` match implies at least one record shares the 399 + /// target's card AND kind — the resolver never adopts or flags a 400 + /// record for a different modality or device family. 401 + #[test] 402 + fn any_match_implies_same_card_and_kind_record_exists( 403 + target in arb_camera(), 404 + records in proptest::collection::vec(arb_calibration(), 0..=8), 405 + connected in proptest::collection::vec(arb_camera(), 0..=8), 406 + ) { 407 + let id = CameraIdentity::from_info(&target); 408 + let outcome = resolve_calibration(&target, &records, &connected); 409 + if !matches!(outcome, CalibrationMatch::None) { 410 + let has = records 411 + .iter() 412 + .any(|r| r.identity.card == id.card && r.identity.kind == id.kind); 413 + prop_assert!(has, "non-None match without a same-card+kind record"); 414 + } 415 + } 416 + 417 + /// Determinism: identical inputs always yield the same outcome. 418 + #[test] 419 + fn resolve_calibration_is_deterministic( 420 + target in arb_camera(), 421 + records in proptest::collection::vec(arb_calibration(), 0..=8), 422 + connected in proptest::collection::vec(arb_camera(), 0..=8), 423 + ) { 424 + let a = resolve_calibration(&target, &records, &connected); 425 + let b = resolve_calibration(&target, &records, &connected); 426 + prop_assert_eq!(a, b); 427 + } 428 + } 326 429 }
+128 -3
crates/core/src/camera/calibration/store.rs
··· 182 182 use super::*; 183 183 use crate::camera::calibration::{CameraIdentity, IrCalibration}; 184 184 use crate::camera::v4l2::IrCaptureMode; 185 + use proptest::prelude::*; 185 186 186 187 fn temp_dir() -> PathBuf { 187 - let id: u64 = std::time::SystemTime::now() 188 + use std::sync::atomic::{AtomicU64, Ordering}; 189 + // Monotonic counter so rapid successive calls (e.g. within a 190 + // single proptest run) never collide on the same nanosecond. 191 + static COUNTER: AtomicU64 = AtomicU64::new(0); 192 + let nanos = std::time::SystemTime::now() 188 193 .duration_since(std::time::UNIX_EPOCH) 189 194 .unwrap() 190 - .as_nanos() as u64 191 - ^ std::process::id() as u64; 195 + .as_nanos() as u64; 196 + let id = nanos ^ (std::process::id() as u64) ^ COUNTER.fetch_add(1, Ordering::Relaxed); 192 197 let p = std::env::temp_dir().join(format!("pareidolia-calib-test-{id}")); 193 198 fs::create_dir_all(&p).unwrap(); 194 199 p ··· 288 293 .collect(); 289 294 assert!(leaked.is_empty(), "temp files leaked: {leaked:?}"); 290 295 fs::remove_dir_all(&dir).ok(); 296 + } 297 + 298 + // ----- concurrent-writer integrity --------------------------------------- 299 + 300 + #[test] 301 + fn concurrent_writers_never_leave_a_corrupt_file() { 302 + use std::sync::Arc; 303 + use std::thread; 304 + 305 + // N threads race to upsert *different* records into the same 306 + // store. The read-modify-write is not serialized, so the final 307 + // record SET is last-writer-wins and we deliberately don't assert 308 + // on its contents (see the calibration store docs). What must 309 + // ALWAYS hold is file integrity: atomic temp+rename means the 310 + // file on disk is never torn — every load() after the race 311 + // succeeds and yields a structurally valid record set, never a 312 + // decode error or partial write. 313 + let dir = temp_dir(); 314 + let store = Arc::new(CalibrationStore::in_dir(&dir)); 315 + 316 + const N: usize = 16; 317 + let mut handles = Vec::new(); 318 + for i in 0..N { 319 + let store = Arc::clone(&store); 320 + handles.push(thread::spawn(move || { 321 + store 322 + .upsert(rec( 323 + &format!("Cam{i}"), 324 + Some("usb-1"), 325 + IrCaptureMode::Strobed { period: (i as u8 % 6) + 2 }, 326 + )) 327 + .expect("upsert under contention should not error"); 328 + })); 329 + } 330 + for h in handles { 331 + h.join().unwrap(); 332 + } 333 + 334 + // The file must decode cleanly after the race. We make no claim 335 + // about how many of the N records survived — only that what's 336 + // there is valid and that re-reading is stable. 337 + let first = store.load().expect("post-race load must succeed (no torn file)"); 338 + let second = store.load().expect("second load must also succeed"); 339 + assert_eq!(first, second, "two consecutive loads must agree"); 340 + for r in &first { 341 + // Every surviving record is well-formed. 342 + assert!(r.mode.min_burst() >= 1); 343 + } 344 + fs::remove_dir_all(&dir).ok(); 345 + } 346 + 347 + // ----- proptests ---------------------------------------------------------- 348 + 349 + fn arb_mode() -> impl Strategy<Value = IrCaptureMode> { 350 + prop_oneof![ 351 + Just(IrCaptureMode::Constant), 352 + (2u8..=64).prop_map(|period| IrCaptureMode::Strobed { period }), 353 + ] 354 + } 355 + 356 + fn arb_record() -> impl Strategy<Value = IrCalibration> { 357 + ( 358 + "[A-Za-z0-9 _:-]{1,32}", 359 + proptest::option::of("[A-Za-z0-9.:-]{1,24}"), 360 + any::<bool>(), 361 + arb_mode(), 362 + any::<u64>(), 363 + ) 364 + .prop_map(|(card, bus, is_ir, mode, ts)| IrCalibration { 365 + identity: CameraIdentity { 366 + card, 367 + bus, 368 + kind: if is_ir { 369 + crate::camera::CameraKind::Ir 370 + } else { 371 + crate::camera::CameraKind::Rgb 372 + }, 373 + }, 374 + mode, 375 + calibrated_at_unix: ts, 376 + }) 377 + } 378 + 379 + proptest! { 380 + /// save_all → load round-trips an arbitrary record set: the file 381 + /// format loses no information. Mirrors the enrollment store's 382 + /// headline round-trip property. 383 + #[test] 384 + fn save_load_roundtrips_arbitrary_records( 385 + records in proptest::collection::vec(arb_record(), 0..=12), 386 + ) { 387 + let dir = temp_dir(); 388 + let store = CalibrationStore::in_dir(&dir); 389 + store.save_all(&records).unwrap(); 390 + let back = store.load().unwrap(); 391 + prop_assert_eq!(back, records); 392 + fs::remove_dir_all(&dir).ok(); 393 + } 394 + 395 + /// After `upsert`, the store contains exactly one record for that 396 + /// identity and it carries the upserted mode — regardless of what 397 + /// was there before. 398 + #[test] 399 + fn upsert_leaves_exactly_one_record_per_identity( 400 + seed in proptest::collection::vec(arb_record(), 0..=8), 401 + new in arb_record(), 402 + ) { 403 + let dir = temp_dir(); 404 + let store = CalibrationStore::in_dir(&dir); 405 + store.save_all(&seed).unwrap(); 406 + store.upsert(new.clone()).unwrap(); 407 + let loaded = store.load().unwrap(); 408 + let matching: Vec<_> = loaded 409 + .iter() 410 + .filter(|r| r.identity == new.identity) 411 + .collect(); 412 + prop_assert_eq!(matching.len(), 1); 413 + prop_assert_eq!(matching[0].mode, new.mode); 414 + fs::remove_dir_all(&dir).ok(); 415 + } 291 416 } 292 417 }
+103
crates/core/src/camera/select.rs
··· 453 453 RgbCameraResolution::Ambiguous(_), 454 454 )); 455 455 } 456 + 457 + // ----- proptests --------------------------------------------------------- 458 + 459 + use proptest::prelude::*; 460 + 461 + fn arb_camera() -> impl Strategy<Value = CameraInfo> { 462 + ( 463 + "[a-z]{1,8}", 464 + proptest::option::of("[a-z0-9/]{1,12}"), 465 + prop_oneof![Just(CameraKind::Ir), Just(CameraKind::Rgb)], 466 + ) 467 + .prop_map(|(name, path, kind)| CameraInfo { 468 + name, 469 + path: path.map(PathBuf::from), 470 + kind, 471 + bus: None, 472 + }) 473 + } 474 + 475 + proptest! { 476 + // ----- resolve_ir_camera ----- 477 + 478 + /// An explicit IR path always wins, whatever the camera list or 479 + /// opt-out flag. 480 + #[test] 481 + fn ir_explicit_always_wins( 482 + cams in proptest::collection::vec(arb_camera(), 0..=8), 483 + opt_out in any::<bool>(), 484 + ) { 485 + let r = resolve_ir_camera(Some(Path::new("/dev/explicit")), opt_out, &cams); 486 + prop_assert_eq!(r, IrCameraResolution::Explicit(PathBuf::from("/dev/explicit"))); 487 + } 488 + 489 + /// Never panics on any camera list. 490 + #[test] 491 + fn ir_resolver_never_panics( 492 + explicit in proptest::option::of("[a-z/]{1,12}"), 493 + opt_out in any::<bool>(), 494 + cams in proptest::collection::vec(arb_camera(), 0..=8), 495 + ) { 496 + let _ = resolve_ir_camera(explicit.as_deref().map(Path::new), opt_out, &cams); 497 + } 498 + 499 + /// When auto-detection adopts a camera, the adopted info is 500 + /// always IR-classified and has the returned path. 501 + #[test] 502 + fn ir_autodetected_camera_is_ir_kind( 503 + cams in proptest::collection::vec(arb_camera(), 0..=8), 504 + ) { 505 + if let IrCameraResolution::AutoDetected { path, info } = 506 + resolve_ir_camera(None, false, &cams) 507 + { 508 + prop_assert_eq!(info.kind, CameraKind::Ir); 509 + prop_assert_eq!(info.path.as_deref(), Some(path.as_path())); 510 + } 511 + } 512 + 513 + // ----- resolve_rgb_camera ----- 514 + 515 + /// An explicit RGB path always wins. 516 + #[test] 517 + fn rgb_explicit_always_wins( 518 + cams in proptest::collection::vec(arb_camera(), 0..=8), 519 + ) { 520 + let r = resolve_rgb_camera(Some(Path::new("/dev/explicit")), &cams); 521 + prop_assert_eq!(r, RgbCameraResolution::Explicit(PathBuf::from("/dev/explicit"))); 522 + } 523 + 524 + /// Never panics on any camera list. 525 + #[test] 526 + fn rgb_resolver_never_panics( 527 + explicit in proptest::option::of("[a-z/]{1,12}"), 528 + cams in proptest::collection::vec(arb_camera(), 0..=8), 529 + ) { 530 + let _ = resolve_rgb_camera(explicit.as_deref().map(Path::new), &cams); 531 + } 532 + 533 + /// An auto-detected RGB camera is always RGB-classified, and the 534 + /// result is `AutoDetected` exactly when there is precisely one 535 + /// path-bearing RGB camera. 536 + #[test] 537 + fn rgb_autodetect_matches_rgb_count( 538 + cams in proptest::collection::vec(arb_camera(), 0..=8), 539 + ) { 540 + let rgb_with_path = cams 541 + .iter() 542 + .filter(|c| c.kind == CameraKind::Rgb && c.path.is_some()) 543 + .count(); 544 + let rgb_total = cams.iter().filter(|c| c.kind == CameraKind::Rgb).count(); 545 + match resolve_rgb_camera(None, &cams) { 546 + RgbCameraResolution::AutoDetected { info, .. } => { 547 + prop_assert_eq!(info.kind, CameraKind::Rgb); 548 + // Adoption only when exactly one RGB camera, and it 549 + // has a path. 550 + prop_assert_eq!(rgb_total, 1); 551 + prop_assert_eq!(rgb_with_path, 1); 552 + } 553 + RgbCameraResolution::NoHardware => prop_assert_eq!(rgb_total, 0), 554 + RgbCameraResolution::Ambiguous(_) => prop_assert!(rgb_total >= 1), 555 + RgbCameraResolution::Explicit(_) => prop_assert!(false, "no explicit path given"), 556 + } 557 + } 558 + } 456 559 }
+132 -1
crates/core/src/camera/v4l2.rs
··· 189 189 // "bright" frames (above the midpoint between min and max). The first 190 190 // two bright frames are enough to establish the cadence; falling back 191 191 // to 2 (strict alternation) is the safe, common default. 192 + // 193 + // The gap is a `usize` frame-index difference; clamp it into `u8` 194 + // with a saturating conversion rather than `as u8`, which would 195 + // *truncate* (wrap mod 256) and could turn a genuinely large, sparse 196 + // period into a bogus small one. Saturating to u8::MAX is the right 197 + // failure mode: an implausibly long period just reads as "very 198 + // sparse", and burst sizing clamps it sanely anyway. 192 199 let midpoint = (max + min) / 2.0; 193 200 let bright_indices: Vec<usize> = brightnesses 194 201 .iter() ··· 198 205 .collect(); 199 206 let period = bright_indices 200 207 .windows(2) 201 - .map(|w| (w[1] - w[0]) as u8) 208 + .map(|w| u8::try_from(w[1] - w[0]).unwrap_or(u8::MAX)) 202 209 .min() 203 210 .unwrap_or(2) 204 211 .max(2); ··· 513 520 #[cfg(test)] 514 521 mod tests { 515 522 use super::*; 523 + use proptest::prelude::*; 516 524 517 525 // ----- frame brightness (no hardware) ------------------------------------ 518 526 ··· 609 617 assert_eq!(IrCaptureMode::Constant.min_burst(), 1); 610 618 assert_eq!(IrCaptureMode::Strobed { period: 2 }.min_burst(), 2); 611 619 assert_eq!(IrCaptureMode::Strobed { period: 3 }.min_burst(), 3); 620 + } 621 + 622 + #[test] 623 + fn classify_sparse_strobe_does_not_truncate_period() { 624 + // Regression: a period larger than 255 frames must not wrap via 625 + // `as u8` into a tiny bogus period. Bright frames at indices 0 626 + // and 300 (gap 300) should saturate to u8::MAX (255), never 44 627 + // (300 mod 256). We build 301 frames: bright at the ends, dark 628 + // between, contrast well above the floor. 629 + let mut b = vec![10.0_f32; 301]; 630 + b[0] = 90.0; 631 + b[300] = 90.0; 632 + match classify_ir_mode(&b, 20.0) { 633 + IrCaptureMode::Strobed { period } => assert_eq!( 634 + period, 255, 635 + "gap of 300 must saturate to 255, not truncate", 636 + ), 637 + other => panic!("expected Strobed, got {other:?}"), 638 + } 639 + } 640 + 641 + // ----- proptests: brightest-frame selection ------------------------------ 642 + 643 + proptest! { 644 + /// `select_brightest` returns `Some` iff the input is non-empty, 645 + /// and the returned index is always in bounds. 646 + #[test] 647 + fn select_brightest_index_in_bounds_or_none( 648 + frames in proptest::collection::vec( 649 + proptest::collection::vec(any::<u8>(), 0..=32), 650 + 0..=16, 651 + ), 652 + ) { 653 + let slices: Vec<&[u8]> = frames.iter().map(|f| f.as_slice()).collect(); 654 + match select_brightest(slices.iter().copied()) { 655 + Some(i) => prop_assert!(i < frames.len()), 656 + None => prop_assert!(frames.is_empty()), 657 + } 658 + } 659 + 660 + /// The chosen frame is a true argmax: no other frame is brighter. 661 + #[test] 662 + fn select_brightest_picks_a_maximum( 663 + frames in proptest::collection::vec( 664 + proptest::collection::vec(any::<u8>(), 1..=32), 665 + 1..=16, 666 + ), 667 + ) { 668 + let slices: Vec<&[u8]> = frames.iter().map(|f| f.as_slice()).collect(); 669 + let idx = select_brightest(slices.iter().copied()).unwrap(); 670 + let chosen = frame_brightness(&frames[idx]); 671 + for f in &frames { 672 + prop_assert!( 673 + frame_brightness(f) <= chosen, 674 + "found a frame brighter than the chosen one", 675 + ); 676 + } 677 + } 678 + } 679 + 680 + // ----- proptests: IR mode classification --------------------------------- 681 + 682 + proptest! { 683 + /// `classify_ir_mode` never panics, whatever the input — including 684 + /// NaN, infinities, empty, and huge slices. The function is `pub`, 685 + /// so it must be total even though production only feeds it finite 686 + /// 0..=255 means. 687 + #[test] 688 + fn classify_never_panics( 689 + brightnesses in proptest::collection::vec( 690 + prop_oneof![ 691 + any::<f32>(), 692 + Just(f32::NAN), 693 + Just(f32::INFINITY), 694 + Just(f32::NEG_INFINITY), 695 + 0.0f32..=255.0, 696 + ], 697 + 0..=64, 698 + ), 699 + min_contrast in any::<f32>(), 700 + ) { 701 + let mode = classify_ir_mode(&brightnesses, min_contrast); 702 + // Whatever it returns, the burst length it implies is sane. 703 + prop_assert!(mode.min_burst() >= 1); 704 + if let IrCaptureMode::Strobed { period } = mode { 705 + prop_assert!(period >= 2, "strobed period is always >= 2"); 706 + } 707 + } 708 + 709 + /// Round-trip: a synthesized strict period-`p` strobe (bright 710 + /// every `p` frames, dark otherwise, contrast above the floor) 711 + /// classifies back as `Strobed { period: p }`, for the small 712 + /// realistic periods we'd actually see. 713 + #[test] 714 + fn classify_round_trips_synthesized_strobe( 715 + period in 2u8..=8, 716 + cycles in 2usize..=10, 717 + ) { 718 + let p = period as usize; 719 + let n = p * cycles; 720 + let brightnesses: Vec<f32> = (0..n) 721 + .map(|i| if i % p == 0 { 90.0 } else { 10.0 }) 722 + .collect(); 723 + prop_assert_eq!( 724 + classify_ir_mode(&brightnesses, 20.0), 725 + IrCaptureMode::Strobed { period }, 726 + ); 727 + } 728 + 729 + /// A stream whose entire spread is below `min_contrast` is always 730 + /// `Constant` — any wobble within the floor is treated as noise. 731 + #[test] 732 + fn classify_low_spread_is_always_constant( 733 + base in 0.0f32..=200.0, 734 + wobble in proptest::collection::vec(0.0f32..=10.0, 2..=32), 735 + ) { 736 + let brightnesses: Vec<f32> = wobble.iter().map(|w| base + w).collect(); 737 + // Spread <= 10 here; use a min_contrast comfortably above it. 738 + prop_assert_eq!( 739 + classify_ir_mode(&brightnesses, 20.0), 740 + IrCaptureMode::Constant, 741 + ); 742 + } 612 743 } 613 744 614 745 // ----- pure fourcc mapping (no hardware) ---------------------------------