1use crate::resources::RawImageFormat;
19
20#[repr(C)]
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub struct CaptureStreamId {
25 pub id: u64,
26}
27
28#[repr(C)]
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub enum CameraFacing {
32 Front,
34 Back,
36 External,
38}
39
40#[repr(C)]
42#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub enum StreamState {
44 Starting,
46 Running,
48 Paused,
50 Stopped,
52 Error,
54}
55
56#[repr(C)]
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
60pub enum CaptureOrientation {
61 Up,
63 Down,
65 Left,
67 Right,
69 Mirror,
71}
72
73#[repr(C)]
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
76pub enum CaptureErrorCode {
77 PermissionDenied,
79 DeviceUnavailable,
81 DeviceLost,
83 Unsupported,
85 Internal,
87}
88
89#[repr(C)]
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct CameraConfig {
94 pub facing: CameraFacing,
96 pub width: u32,
98 pub height: u32,
100 pub fps: u32,
102 pub output_format: RawImageFormat,
106}
107
108impl Default for CameraConfig {
109 fn default() -> Self {
110 Self {
111 facing: CameraFacing::Back,
112 width: 0,
113 height: 0,
114 fps: 0,
115 output_format: RawImageFormat::BGRA8,
116 }
117 }
118}
119
120impl CameraConfig {
121 #[must_use] pub fn new(facing: CameraFacing) -> Self {
124 Self {
125 facing,
126 ..Self::default()
127 }
128 }
129}
130
131#[repr(C)]
133#[derive(Debug, Clone, Copy, PartialEq)]
134pub struct CaptureStats {
135 pub measured_fps: f32,
137 pub frames_delivered: u64,
139 pub frames_dropped: u64,
141}
142
143impl Default for CaptureStats {
144 fn default() -> Self {
145 Self {
146 measured_fps: 0.0,
147 frames_delivered: 0,
148 frames_dropped: 0,
149 }
150 }
151}
152
153#[cfg(test)]
154mod autotest_generated {
155 use super::*;
156
157 const ALL_FACINGS: [CameraFacing; 3] =
158 [CameraFacing::Front, CameraFacing::Back, CameraFacing::External];
159
160 #[test]
163 fn new_does_not_panic_for_any_facing() {
164 for &facing in &ALL_FACINGS {
166 let _ = CameraConfig::new(facing);
167 }
168 }
169
170 #[test]
171 fn new_sets_facing_and_leaves_everything_else_at_default() {
172 let def = CameraConfig::default();
176 for &facing in &ALL_FACINGS {
177 let cfg = CameraConfig::new(facing);
178 assert_eq!(cfg.facing, facing, "facing must equal the argument");
179 assert_eq!(cfg.width, def.width);
180 assert_eq!(cfg.height, def.height);
181 assert_eq!(cfg.fps, def.fps);
182 assert_eq!(cfg.output_format, def.output_format);
183 assert_eq!(cfg.width, 0, "0 => backend default width");
185 assert_eq!(cfg.height, 0, "0 => backend default height");
186 assert_eq!(cfg.fps, 0, "0 => backend default fps");
187 assert_eq!(cfg.output_format, RawImageFormat::BGRA8);
188 }
189 }
190
191 #[test]
192 fn new_equals_struct_update_syntax() {
193 for &facing in &ALL_FACINGS {
195 let via_new = CameraConfig::new(facing);
196 let via_update = CameraConfig {
197 facing,
198 ..CameraConfig::default()
199 };
200 assert_eq!(via_new, via_update);
201 }
202 }
203
204 #[test]
205 fn new_back_equals_full_default() {
206 assert_eq!(
208 CameraConfig::new(CameraFacing::Back),
209 CameraConfig::default()
210 );
211 }
212
213 #[test]
214 fn new_result_is_copy_and_independent() {
215 let a = CameraConfig::new(CameraFacing::Front);
217 let mut b = a;
218 b.width = 1920;
219 b.facing = CameraFacing::Back;
220 assert_eq!(a.width, 0, "original must be untouched by copy mutation");
221 assert_eq!(a.facing, CameraFacing::Front);
222 assert_eq!(b.width, 1920);
223 assert_eq!(b.facing, CameraFacing::Back);
224 }
225
226 #[test]
227 fn new_differs_only_by_facing_between_variants() {
228 let front = CameraConfig::new(CameraFacing::Front);
231 let mut back = CameraConfig::new(CameraFacing::Back);
232 assert_ne!(front, back);
233 back.facing = CameraFacing::Front;
234 assert_eq!(front, back);
235 }
236
237 #[test]
240 fn camera_config_stores_extreme_dimensions_verbatim() {
241 let cfg = CameraConfig {
243 width: u32::MAX,
244 height: u32::MAX,
245 fps: u32::MAX,
246 ..CameraConfig::new(CameraFacing::External)
247 };
248 assert_eq!(cfg.width, u32::MAX);
249 assert_eq!(cfg.height, u32::MAX);
250 assert_eq!(cfg.fps, u32::MAX);
251 assert_eq!(cfg.facing, CameraFacing::External);
252 }
253
254 #[test]
255 fn camera_config_output_format_can_hold_non_default() {
256 let cfg = CameraConfig {
258 output_format: RawImageFormat::RGBA8,
259 ..CameraConfig::new(CameraFacing::Front)
260 };
261 assert_eq!(cfg.output_format, RawImageFormat::RGBA8);
262 assert_ne!(cfg.output_format, RawImageFormat::BGRA8);
263 }
264
265 #[test]
268 fn camera_config_default_is_back_bgra8_zeroed() {
269 let d = CameraConfig::default();
270 assert_eq!(d.facing, CameraFacing::Back);
271 assert_eq!(d.width, 0);
272 assert_eq!(d.height, 0);
273 assert_eq!(d.fps, 0);
274 assert_eq!(d.output_format, RawImageFormat::BGRA8);
275 }
276
277 #[test]
278 fn camera_config_default_is_idempotent() {
279 assert_eq!(CameraConfig::default(), CameraConfig::default());
280 }
281
282 #[test]
283 fn capture_stats_default_is_fully_zeroed() {
284 let s = CaptureStats::default();
285 assert_eq!(s.measured_fps, 0.0);
286 assert_eq!(s.frames_delivered, 0);
287 assert_eq!(s.frames_dropped, 0);
288 }
289
290 #[test]
293 fn capture_stats_nan_fps_breaks_eq_reflexivity() {
294 let s = CaptureStats {
298 measured_fps: f32::NAN,
299 ..CaptureStats::default()
300 };
301 assert_ne!(s, s, "NaN measured_fps must break PartialEq reflexivity");
302 assert!(s.measured_fps.is_nan());
303 }
304
305 #[test]
306 fn capture_stats_holds_infinities_and_saturated_counters() {
307 let s = CaptureStats {
308 measured_fps: f32::INFINITY,
309 frames_delivered: u64::MAX,
310 frames_dropped: u64::MAX,
311 };
312 assert!(s.measured_fps.is_infinite() && s.measured_fps > 0.0);
313 assert_eq!(s.frames_delivered, u64::MAX);
314 assert_eq!(s.frames_dropped, u64::MAX);
315
316 let neg = CaptureStats {
317 measured_fps: f32::NEG_INFINITY,
318 ..CaptureStats::default()
319 };
320 assert!(neg.measured_fps.is_infinite() && neg.measured_fps < 0.0);
321 }
322
323 #[test]
324 fn capture_stats_negative_zero_fps_equals_zero() {
325 let s = CaptureStats {
327 measured_fps: -0.0,
328 ..CaptureStats::default()
329 };
330 assert_eq!(s, CaptureStats::default());
331 }
332
333 #[test]
336 fn capture_stream_id_roundtrips_boundary_ids() {
337 for id in [0u64, 1, 2, u64::MAX / 2, u64::MAX - 1, u64::MAX] {
338 let s = CaptureStreamId { id };
339 assert_eq!(s.id, id);
340 assert_eq!(s, CaptureStreamId { id }, "Eq must hold for equal ids");
341 }
342 }
343
344 #[test]
345 fn capture_stream_id_distinct_ids_are_unequal_and_ordered() {
346 let lo = CaptureStreamId { id: 0 };
347 let hi = CaptureStreamId { id: u64::MAX };
348 assert_ne!(lo, hi);
349 assert!(lo < hi, "Ord must follow the inner u64");
350 }
351
352 #[test]
355 fn enum_ord_matches_declaration_order() {
356 assert!(CameraFacing::Front < CameraFacing::Back);
358 assert!(CameraFacing::Back < CameraFacing::External);
359 assert!(StreamState::Starting < StreamState::Running);
361 assert!(StreamState::Running < StreamState::Paused);
362 assert!(StreamState::Paused < StreamState::Stopped);
363 assert!(StreamState::Stopped < StreamState::Error);
364 assert!(CaptureOrientation::Up < CaptureOrientation::Down);
366 assert!(CaptureOrientation::Down < CaptureOrientation::Left);
367 assert!(CaptureOrientation::Left < CaptureOrientation::Right);
368 assert!(CaptureOrientation::Right < CaptureOrientation::Mirror);
369 assert!(CaptureErrorCode::PermissionDenied < CaptureErrorCode::DeviceUnavailable);
372 assert!(CaptureErrorCode::DeviceUnavailable < CaptureErrorCode::DeviceLost);
373 assert!(CaptureErrorCode::DeviceLost < CaptureErrorCode::Unsupported);
374 assert!(CaptureErrorCode::Unsupported < CaptureErrorCode::Internal);
375 }
376
377 #[test]
378 fn camera_facing_ord_is_total_and_antisymmetric() {
379 use core::cmp::Ordering;
380 for &a in &ALL_FACINGS {
381 assert_eq!(a.cmp(&a), Ordering::Equal, "reflexive equality");
382 for &b in &ALL_FACINGS {
383 let lt = (a < b) as u8;
385 let eq = (a == b) as u8;
386 let gt = (a > b) as u8;
387 assert_eq!(lt + eq + gt, 1, "trichotomy must hold for ({a:?},{b:?})");
388 if a < b {
390 assert!(!(b < a), "antisymmetry violated for ({a:?},{b:?})");
391 }
392 }
393 }
394 }
395
396 #[test]
397 fn enum_hash_eq_consistency() {
398 use std::collections::HashSet;
399 let mut set = HashSet::new();
400 assert!(set.insert(CameraFacing::Front));
401 assert!(set.insert(CameraFacing::Back));
402 assert!(set.insert(CameraFacing::External));
403 assert!(!set.insert(CameraFacing::Front));
405 assert_eq!(set.len(), 3);
406 assert!(set.contains(&CameraFacing::Back));
407 }
408
409 const ALL_STATES: [StreamState; 5] = [
418 StreamState::Starting,
419 StreamState::Running,
420 StreamState::Paused,
421 StreamState::Stopped,
422 StreamState::Error,
423 ];
424
425 const ALL_ORIENTATIONS: [CaptureOrientation; 5] = [
426 CaptureOrientation::Up,
427 CaptureOrientation::Down,
428 CaptureOrientation::Left,
429 CaptureOrientation::Right,
430 CaptureOrientation::Mirror,
431 ];
432
433 const ALL_ERRORS: [CaptureErrorCode; 5] = [
434 CaptureErrorCode::PermissionDenied,
435 CaptureErrorCode::DeviceUnavailable,
436 CaptureErrorCode::DeviceLost,
437 CaptureErrorCode::Unsupported,
438 CaptureErrorCode::Internal,
439 ];
440
441 const ALL_FORMATS: [RawImageFormat; 12] = [
444 RawImageFormat::R8,
445 RawImageFormat::RG8,
446 RawImageFormat::RGB8,
447 RawImageFormat::RGBA8,
448 RawImageFormat::R16,
449 RawImageFormat::RG16,
450 RawImageFormat::RGB16,
451 RawImageFormat::RGBA16,
452 RawImageFormat::BGR8,
453 RawImageFormat::BGRA8,
454 RawImageFormat::RGBF32,
455 RawImageFormat::RGBAF32,
456 ];
457
458 fn facing_from_discriminant(i: i32) -> Option<CameraFacing> {
467 match i {
468 0 => Some(CameraFacing::Front),
469 1 => Some(CameraFacing::Back),
470 2 => Some(CameraFacing::External),
471 _ => None,
472 }
473 }
474
475 fn state_from_discriminant(i: i32) -> Option<StreamState> {
476 match i {
477 0 => Some(StreamState::Starting),
478 1 => Some(StreamState::Running),
479 2 => Some(StreamState::Paused),
480 3 => Some(StreamState::Stopped),
481 4 => Some(StreamState::Error),
482 _ => None,
483 }
484 }
485
486 fn orientation_from_discriminant(i: i32) -> Option<CaptureOrientation> {
487 match i {
488 0 => Some(CaptureOrientation::Up),
489 1 => Some(CaptureOrientation::Down),
490 2 => Some(CaptureOrientation::Left),
491 3 => Some(CaptureOrientation::Right),
492 4 => Some(CaptureOrientation::Mirror),
493 _ => None,
494 }
495 }
496
497 fn error_from_discriminant(i: i32) -> Option<CaptureErrorCode> {
498 match i {
499 0 => Some(CaptureErrorCode::PermissionDenied),
500 1 => Some(CaptureErrorCode::DeviceUnavailable),
501 2 => Some(CaptureErrorCode::DeviceLost),
502 3 => Some(CaptureErrorCode::Unsupported),
503 4 => Some(CaptureErrorCode::Internal),
504 _ => None,
505 }
506 }
507
508 #[test]
509 fn enum_discriminants_are_the_pinned_ffi_wire_values() {
510 assert_eq!(CameraFacing::Front as i32, 0);
513 assert_eq!(CameraFacing::Back as i32, 1);
514 assert_eq!(CameraFacing::External as i32, 2);
515
516 assert_eq!(StreamState::Starting as i32, 0);
517 assert_eq!(StreamState::Running as i32, 1);
518 assert_eq!(StreamState::Paused as i32, 2);
519 assert_eq!(StreamState::Stopped as i32, 3);
520 assert_eq!(StreamState::Error as i32, 4);
521
522 assert_eq!(CaptureOrientation::Up as i32, 0);
523 assert_eq!(CaptureOrientation::Down as i32, 1);
524 assert_eq!(CaptureOrientation::Left as i32, 2);
525 assert_eq!(CaptureOrientation::Right as i32, 3);
526 assert_eq!(CaptureOrientation::Mirror as i32, 4);
527
528 assert_eq!(CaptureErrorCode::PermissionDenied as i32, 0);
529 assert_eq!(CaptureErrorCode::DeviceUnavailable as i32, 1);
530 assert_eq!(CaptureErrorCode::DeviceLost as i32, 2);
531 assert_eq!(CaptureErrorCode::Unsupported as i32, 3);
532 assert_eq!(CaptureErrorCode::Internal as i32, 4);
533 }
534
535 #[test]
536 fn enum_discriminant_roundtrip_is_identity() {
537 for &f in &ALL_FACINGS {
538 assert_eq!(facing_from_discriminant(f as i32), Some(f));
539 }
540 for &s in &ALL_STATES {
541 assert_eq!(state_from_discriminant(s as i32), Some(s));
542 }
543 for &o in &ALL_ORIENTATIONS {
544 assert_eq!(orientation_from_discriminant(o as i32), Some(o));
545 }
546 for &e in &ALL_ERRORS {
547 assert_eq!(error_from_discriminant(e as i32), Some(e));
548 }
549 }
550
551 #[test]
552 fn enum_decode_rejects_out_of_range_discriminants() {
553 for bad in [-1i32, 3, 5, 99, i32::MIN, i32::MAX] {
556 assert_eq!(facing_from_discriminant(bad), None, "facing {bad}");
557 }
558 for bad in [-1i32, 5, 6, 255, i32::MIN, i32::MAX] {
559 assert_eq!(state_from_discriminant(bad), None, "state {bad}");
560 assert_eq!(orientation_from_discriminant(bad), None, "orient {bad}");
561 assert_eq!(error_from_discriminant(bad), None, "error {bad}");
562 }
563 assert!(facing_from_discriminant(2).is_some());
565 assert!(facing_from_discriminant(3).is_none());
566 assert!(state_from_discriminant(4).is_some());
567 assert!(state_from_discriminant(5).is_none());
568 }
569
570 #[test]
571 fn enum_discriminant_order_agrees_with_ord() {
572 for &a in &ALL_STATES {
575 for &b in &ALL_STATES {
576 assert_eq!(
577 a < b,
578 (a as i32) < (b as i32),
579 "Ord and discriminant disagree for ({a:?}, {b:?})"
580 );
581 }
582 }
583 }
584
585 #[test]
588 fn camera_config_field_roundtrip_is_identity_over_full_cross_product() {
589 for &facing in &ALL_FACINGS {
592 for &output_format in &ALL_FORMATS {
593 for &(width, height, fps) in &[
594 (0u32, 0u32, 0u32),
595 (1, 1, 1),
596 (1920, 1080, 60),
597 (u32::MAX, u32::MAX, u32::MAX),
598 (u32::MAX, 0, 1),
599 ] {
600 let cfg = CameraConfig {
601 facing,
602 width,
603 height,
604 fps,
605 output_format,
606 };
607 let rebuilt = CameraConfig {
608 facing: cfg.facing,
609 width: cfg.width,
610 height: cfg.height,
611 fps: cfg.fps,
612 output_format: cfg.output_format,
613 };
614 assert_eq!(cfg, rebuilt);
615 assert_eq!(rebuilt.width, width);
617 assert_eq!(rebuilt.height, height);
618 assert_eq!(rebuilt.fps, fps);
619 assert_eq!(rebuilt.facing, facing);
620 assert_eq!(rebuilt.output_format, output_format);
621 }
622 }
623 }
624 }
625
626 #[test]
627 fn camera_config_does_not_confuse_width_and_height() {
628 let a = CameraConfig {
631 width: 1920,
632 height: 1080,
633 ..CameraConfig::default()
634 };
635 let transposed = CameraConfig {
636 width: 1080,
637 height: 1920,
638 ..CameraConfig::default()
639 };
640 assert_ne!(a, transposed, "width/height must be distinguishable");
641 let square = CameraConfig {
643 width: 512,
644 height: 512,
645 ..CameraConfig::default()
646 };
647 let square2 = CameraConfig {
648 width: 512,
649 height: 512,
650 ..CameraConfig::default()
651 };
652 assert_eq!(square, square2);
653 }
654
655 #[test]
656 fn camera_config_eq_is_sensitive_to_every_field() {
657 let base = CameraConfig::default();
660 assert_ne!(
661 base,
662 CameraConfig {
663 facing: CameraFacing::Front,
664 ..base
665 }
666 );
667 assert_ne!(base, CameraConfig { width: 1, ..base });
668 assert_ne!(base, CameraConfig { height: 1, ..base });
669 assert_ne!(base, CameraConfig { fps: 1, ..base });
670 assert_ne!(
671 base,
672 CameraConfig {
673 output_format: RawImageFormat::RGBA8,
674 ..base
675 }
676 );
677 }
678
679 #[test]
680 fn capture_stats_eq_is_sensitive_to_every_field() {
681 let base = CaptureStats::default();
682 assert_ne!(
683 base,
684 CaptureStats {
685 measured_fps: 1.0,
686 ..base
687 }
688 );
689 assert_ne!(
690 base,
691 CaptureStats {
692 frames_delivered: 1,
693 ..base
694 }
695 );
696 assert_ne!(
697 base,
698 CaptureStats {
699 frames_dropped: 1,
700 ..base
701 }
702 );
703 }
704
705 #[test]
706 #[allow(clippy::clone_on_copy)]
709 fn clone_is_identity_for_all_pod_types_at_extremes() {
710 let cfg = CameraConfig {
711 facing: CameraFacing::External,
712 width: u32::MAX,
713 height: u32::MAX,
714 fps: u32::MAX,
715 output_format: RawImageFormat::RGBAF32,
716 };
717 assert_eq!(cfg.clone(), cfg);
718
719 let id = CaptureStreamId { id: u64::MAX };
720 assert_eq!(id.clone(), id);
721
722 let nan_stats = CaptureStats {
726 measured_fps: f32::NAN,
727 frames_delivered: u64::MAX,
728 frames_dropped: u64::MAX,
729 };
730 let cloned = nan_stats.clone();
731 assert_eq!(
732 cloned.measured_fps.to_bits(),
733 nan_stats.measured_fps.to_bits(),
734 "clone must preserve the exact NaN bit pattern"
735 );
736 assert_eq!(cloned.frames_delivered, u64::MAX);
737 assert_eq!(cloned.frames_dropped, u64::MAX);
738 }
739
740 #[test]
743 fn capture_stats_nan_fps_makes_partial_cmp_none() {
744 let nan = f32::NAN;
747 assert!(nan.partial_cmp(&0.0).is_none());
748 assert!(nan.partial_cmp(&nan).is_none());
749
750 let s = CaptureStats {
751 measured_fps: nan,
752 ..CaptureStats::default()
753 };
754 let haystack = vec![s, CaptureStats::default()];
757 assert!(!haystack.contains(&s), "NaN stats is unfindable by Eq");
758 assert!(haystack.contains(&CaptureStats::default()));
759 }
760
761 #[test]
762 fn measured_fps_to_integer_cast_saturates_instead_of_wrapping() {
763 let cases: [(f32, u32); 6] = [
766 (f32::NAN, 0),
767 (f32::INFINITY, u32::MAX),
768 (f32::NEG_INFINITY, 0),
769 (-1.0, 0),
770 (-0.0, 0),
771 (f32::MAX, u32::MAX),
772 ];
773 for (fps, expected) in cases {
774 let s = CaptureStats {
775 measured_fps: fps,
776 ..CaptureStats::default()
777 };
778 assert_eq!(
779 s.measured_fps as u32, expected,
780 "cast of {fps} must saturate to {expected}"
781 );
782 }
783 let ok = CaptureStats {
785 measured_fps: 59.94,
786 ..CaptureStats::default()
787 };
788 assert_eq!(ok.measured_fps as u32, 59);
789 }
790
791 #[test]
792 fn frame_counters_overflow_only_via_checked_arithmetic() {
793 let s = CaptureStats {
796 measured_fps: 30.0,
797 frames_delivered: u64::MAX,
798 frames_dropped: 1,
799 };
800 assert_eq!(
801 s.frames_delivered.checked_add(s.frames_dropped),
802 None,
803 "total-frames overflows u64; consumers must saturate/check"
804 );
805 assert_eq!(
806 s.frames_delivered.saturating_add(s.frames_dropped),
807 u64::MAX,
808 "saturating_add is the safe path"
809 );
810 let ok = CaptureStats {
812 frames_delivered: u64::MAX - 1,
813 frames_dropped: 1,
814 ..CaptureStats::default()
815 };
816 assert_eq!(
817 ok.frames_delivered.checked_add(ok.frames_dropped),
818 Some(u64::MAX)
819 );
820 }
821
822 #[test]
823 fn drop_rate_over_zeroed_stats_must_be_guarded() {
824 let s = CaptureStats::default();
829 let total = s.frames_delivered + s.frames_dropped;
830 assert_eq!(total, 0);
831 assert_eq!(
832 s.frames_dropped.checked_div(total),
833 None,
834 "integer drop-rate on zeroed stats must be checked, not raw `/`"
835 );
836 let rate = s.frames_dropped as f64 / total as f64;
837 assert!(rate.is_nan(), "float drop-rate on zeroed stats is NaN");
838
839 let live = CaptureStats {
841 measured_fps: 30.0,
842 frames_delivered: 75,
843 frames_dropped: 25,
844 };
845 let total = live.frames_delivered + live.frames_dropped;
846 assert_eq!(live.frames_dropped.checked_div(total), Some(0));
847 assert!(((live.frames_dropped as f64 / total as f64) - 0.25).abs() < 1e-12);
848 }
849
850 #[test]
851 fn config_pixel_count_overflows_u32_at_extreme_dimensions() {
852 let cfg = CameraConfig {
855 width: u32::MAX,
856 height: u32::MAX,
857 ..CameraConfig::default()
858 };
859 assert_eq!(
860 cfg.width.checked_mul(cfg.height),
861 None,
862 "u32 pixel count overflows; widen to u64 before multiplying"
863 );
864 assert_eq!(
865 u64::from(cfg.width) * u64::from(cfg.height),
866 (u64::from(u32::MAX)) * (u64::from(u32::MAX)),
867 "u64 widening is the safe path"
868 );
869 let hd = CameraConfig {
871 width: 1920,
872 height: 1080,
873 ..CameraConfig::default()
874 };
875 assert_eq!(hd.width.checked_mul(hd.height), Some(2_073_600));
876 }
877
878 #[test]
879 fn fps_u32_to_f32_roundtrip_is_lossy_above_2_pow_24() {
880 let exact: u32 = 1 << 24; assert_eq!(exact as f32 as u32, exact, "2^24 round-trips exactly");
884
885 let lossy: u32 = (1 << 24) + 1; assert_ne!(
887 lossy as f32 as u32,
888 lossy,
889 "2^24+1 must NOT round-trip through f32 - precision is lost"
890 );
891 assert_eq!(lossy as f32 as u32, exact, "it rounds down to 2^24");
892
893 for fps in [0u32, 1, 24, 30, 60, 120, 240] {
895 let cfg = CameraConfig {
896 fps,
897 ..CameraConfig::default()
898 };
899 assert_eq!(cfg.fps as f32 as u32, fps, "fps {fps} must be exact");
900 }
901 }
902
903 #[test]
904 fn frames_delivered_u64_to_f32_roundtrip_is_lossy_at_the_boundary() {
905 let s = CaptureStats {
908 frames_delivered: (1 << 24) + 1,
909 ..CaptureStats::default()
910 };
911 assert_ne!(s.frames_delivered as f32 as u64, s.frames_delivered);
912 let sat = CaptureStats {
914 frames_delivered: u64::MAX,
915 ..CaptureStats::default()
916 };
917 assert_eq!(
918 sat.frames_delivered as f32 as u64,
919 u64::MAX,
920 "saturating cast clamps rather than wrapping to 0"
921 );
922 }
923
924 #[test]
927 fn all_enum_variants_are_pairwise_distinct_and_hash_consistently() {
928 use std::collections::HashSet;
929 assert_eq!(
930 ALL_STATES.iter().collect::<HashSet<_>>().len(),
931 ALL_STATES.len(),
932 "StreamState variants must be pairwise distinct"
933 );
934 assert_eq!(
935 ALL_ORIENTATIONS.iter().collect::<HashSet<_>>().len(),
936 ALL_ORIENTATIONS.len(),
937 "CaptureOrientation variants must be pairwise distinct"
938 );
939 assert_eq!(
940 ALL_ERRORS.iter().collect::<HashSet<_>>().len(),
941 ALL_ERRORS.len(),
942 "CaptureErrorCode variants must be pairwise distinct"
943 );
944 assert_eq!(
945 ALL_FORMATS.iter().collect::<HashSet<_>>().len(),
946 ALL_FORMATS.len(),
947 "RawImageFormat variants must be pairwise distinct"
948 );
949 }
950
951 #[test]
952 fn ord_is_a_total_order_for_every_camera_enum() {
953 use core::cmp::Ordering;
954 macro_rules! assert_total_order {
957 ($set:expr) => {{
958 for &a in $set {
959 assert_eq!(a.cmp(&a), Ordering::Equal);
960 for &b in $set {
961 let lt = (a < b) as u8;
962 let eq = (a == b) as u8;
963 let gt = (a > b) as u8;
964 assert_eq!(lt + eq + gt, 1, "trichotomy ({a:?}, {b:?})");
965 if a < b {
966 assert!(!(b < a), "antisymmetry ({a:?}, {b:?})");
967 }
968 for &c in $set {
969 if a < b && b < c {
970 assert!(a < c, "transitivity ({a:?}, {b:?}, {c:?})");
971 }
972 }
973 }
974 }
975 }};
976 }
977 assert_total_order!(&ALL_STATES);
978 assert_total_order!(&ALL_ORIENTATIONS);
979 assert_total_order!(&ALL_ERRORS);
980 }
981
982 #[test]
983 fn capture_stream_id_hash_eq_and_copy_independence() {
984 use std::collections::HashSet;
985 let mut set = HashSet::new();
986 for id in [0u64, 1, u64::MAX / 2, u64::MAX] {
987 assert!(set.insert(CaptureStreamId { id }), "id {id} is new");
988 }
989 assert_eq!(set.len(), 4);
990 assert!(!set.insert(CaptureStreamId { id: u64::MAX }));
992 assert!(set.contains(&CaptureStreamId { id: 0 }));
993
994 let a = CaptureStreamId { id: 7 };
996 let mut b = a;
997 b.id = 9;
998 assert_eq!(a.id, 7);
999 assert_eq!(b.id, 9);
1000 }
1001
1002 #[test]
1003 fn capture_stats_copy_is_independent() {
1004 let a = CaptureStats {
1005 measured_fps: 30.0,
1006 frames_delivered: 100,
1007 frames_dropped: 2,
1008 };
1009 let mut b = a;
1010 b.measured_fps = 0.0;
1011 b.frames_delivered = 0;
1012 b.frames_dropped = 0;
1013 assert_eq!(a.measured_fps, 30.0, "original must survive copy mutation");
1014 assert_eq!(a.frames_delivered, 100);
1015 assert_eq!(a.frames_dropped, 2);
1016 assert_eq!(b, CaptureStats::default());
1017 }
1018
1019 #[test]
1020 fn debug_formatting_never_panics_on_extreme_or_nan_values() {
1021 let stats = CaptureStats {
1024 measured_fps: f32::NAN,
1025 frames_delivered: u64::MAX,
1026 frames_dropped: u64::MAX,
1027 };
1028 assert!(!format!("{stats:?}").is_empty());
1029 assert!(!format!("{:?}", CaptureStats {
1030 measured_fps: f32::NEG_INFINITY,
1031 ..CaptureStats::default()
1032 })
1033 .is_empty());
1034
1035 let cfg = CameraConfig {
1036 facing: CameraFacing::External,
1037 width: u32::MAX,
1038 height: u32::MAX,
1039 fps: u32::MAX,
1040 output_format: RawImageFormat::RGBAF32,
1041 };
1042 let s = format!("{cfg:?}");
1043 assert!(s.contains("External"), "Debug must name the facing: {s}");
1044 assert!(s.contains("4294967295"), "Debug must show raw extents: {s}");
1045
1046 assert!(!format!("{:?}", CaptureStreamId { id: u64::MAX }).is_empty());
1047 for &st in &ALL_STATES {
1048 assert!(!format!("{st:?}").is_empty());
1049 }
1050 for &o in &ALL_ORIENTATIONS {
1051 assert!(!format!("{o:?}").is_empty());
1052 }
1053 for &e in &ALL_ERRORS {
1054 assert!(!format!("{e:?}").is_empty());
1055 }
1056 }
1057
1058 #[test]
1059 fn repr_c_layout_invariants_hold_for_the_ffi_pod_types() {
1060 use core::mem::{align_of, size_of};
1061 assert_eq!(size_of::<CaptureStreamId>(), size_of::<u64>());
1064 assert_eq!(align_of::<CaptureStreamId>(), align_of::<u64>());
1065
1066 assert!(size_of::<CaptureStats>() >= size_of::<f32>() + 2 * size_of::<u64>());
1071 assert_eq!(size_of::<CaptureStats>() % align_of::<CaptureStats>(), 0);
1072
1073 assert!(size_of::<CameraConfig>() >= 3 * size_of::<u32>());
1074 assert_eq!(size_of::<CameraConfig>() % align_of::<CameraConfig>(), 0);
1075
1076 assert_eq!(size_of::<CameraFacing>(), size_of::<StreamState>());
1078 assert_eq!(size_of::<StreamState>(), size_of::<CaptureOrientation>());
1079 assert_eq!(size_of::<CaptureOrientation>(), size_of::<CaptureErrorCode>());
1080 assert!(size_of::<CameraFacing>() > 0);
1081 }
1082}