//! Include-based coverage for camera capture configuration branches. //! //! Scope: include `client/src/input/camera.rs` and exercise encoder/source //! selection helpers plus non-device fallbacks. //! Targets: `client/src/input/camera.rs`. //! Why: camera startup should remain robust across codec/env permutations. #[allow(warnings)] mod camera_include_contract { include!(env!("LESAVKA_CLIENT_CAMERA_SRC")); use serial_test::serial; use std::os::unix::fs::symlink; use temp_env::with_var; use tempfile::tempdir; fn init_gst() { gst::init().ok(); } #[test] fn env_u32_parses_values_and_falls_back() { with_var("LESAVKA_TEST_CAM_U32", Some("77"), || { assert_eq!(env_u32("LESAVKA_TEST_CAM_U32", 11), 77); }); with_var("LESAVKA_TEST_CAM_U32", Some("not-a-number"), || { assert_eq!(env_u32("LESAVKA_TEST_CAM_U32", 11), 11); }); } #[test] fn encoder_helpers_return_supported_defaults() { init_gst(); let (enc, _caps) = CameraCapture::pick_encoder(); assert!( matches!( enc, "nvh264enc" | "vaapih264enc" | "v4l2h264enc" | "x264enc" ), "unexpected encoder: {enc}" ); let (enc, key_prop) = CameraCapture::choose_encoder(); assert!( matches!( enc, "nvh264enc" | "vaapih264enc" | "v4l2h264enc" | "x264enc" ), "unexpected encoder: {enc}" ); if let Some(key_prop) = key_prop { assert!(!key_prop.is_empty()); } } #[test] fn find_device_and_capture_detection_handle_missing_nodes() { assert!(CameraCapture::find_device("never-matches-this-fragment").is_none()); assert!(!CameraCapture::is_capture( "/dev/definitely-missing-camera0" )); } #[test] #[serial] fn find_device_honors_override_roots_and_handles_non_capture_targets() { let dir = tempdir().expect("tempdir"); let by_id = dir.path().join("by-id"); let dev_root = dir.path().join("dev-root"); std::fs::create_dir_all(&by_id).expect("create by-id"); std::fs::create_dir_all(&dev_root).expect("create dev root"); std::fs::write(dev_root.join("video42"), "").expect("create fake node"); symlink("../dev-root/video42", by_id.join("usb-Cam_42")).expect("create camera symlink"); with_var( "LESAVKA_CAM_BY_ID_DIR", Some(by_id.to_string_lossy().to_string()), || { with_var( "LESAVKA_CAM_DEV_ROOT", Some(dev_root.to_string_lossy().to_string()), || { let found = CameraCapture::find_device("Cam_42"); assert!( found.is_none(), "fake file should not pass V4L capture capability checks" ); }, ); }, ); } #[test] #[cfg(coverage)] #[serial] fn find_device_returns_dev_path_when_fake_target_matches_capture_shape() { let dir = tempdir().expect("tempdir"); let by_id = dir.path().join("by-id"); std::fs::create_dir_all(&by_id).expect("create by-id"); symlink("../video42", by_id.join("usb-Cam_42")).expect("create camera symlink"); with_var( "LESAVKA_CAM_BY_ID_DIR", Some(by_id.to_string_lossy().to_string()), || { with_var("LESAVKA_CAM_DEV_ROOT", Some("/dev".to_string()), || { let found = CameraCapture::find_device("Cam_42"); assert_eq!(found.as_deref(), Some("/dev/video42")); }); }, ); } #[test] #[serial] fn new_covers_test_pattern_and_mjpg_source_branches() { init_gst(); let _ = CameraCapture::new(Some("test"), None); with_var("LESAVKA_CAM_CODEC", Some("mjpeg"), || { let _ = CameraCapture::new(Some("test"), None); }); let mjpeg_cfg = CameraConfig { codec: CameraCodec::Mjpeg, width: 640, height: 480, fps: 30, }; let _ = CameraCapture::new(Some("test"), Some(mjpeg_cfg)); with_var("LESAVKA_CAM_MJPG", Some("1"), || { let _ = CameraCapture::new(Some("/dev/video0"), None); }); } #[test] fn new_stub_and_pull_are_stable_without_frames() { init_gst(); let stub = CameraCapture::new_stub(); assert!(stub.pull().is_none()); } #[test] #[serial] fn new_covers_device_path_fragment_and_default_source_branches() { init_gst(); let by_path = CameraCapture::new(Some("/dev/video42"), None); assert!(by_path.is_ok() || by_path.is_err()); let by_fragment = CameraCapture::new(Some("definitely-missing-fragment"), None); assert!(by_fragment.is_ok() || by_fragment.is_err()); let default_source = CameraCapture::new(None, None); assert!(default_source.is_ok() || default_source.is_err()); } #[test] #[serial] fn new_covers_output_codec_and_mjpg_source_switches() { init_gst(); let mjpeg_cfg = CameraConfig { codec: CameraCodec::Mjpeg, width: 320, height: 240, fps: 15, }; let mjpeg_out = CameraCapture::new(Some("/dev/video42"), Some(mjpeg_cfg)); assert!(mjpeg_out.is_ok() || mjpeg_out.is_err()); with_var("LESAVKA_CAM_MJPG", Some("1"), || { let h264_cfg = CameraConfig { codec: CameraCodec::H264, width: 640, height: 480, fps: 25, }; let mjpg_source = CameraCapture::new(Some("/dev/video42"), Some(h264_cfg)); assert!(mjpg_source.is_ok() || mjpg_source.is_err()); }); } #[test] #[cfg(coverage)] #[serial] fn new_covers_non_x264_encoder_option_branch_in_coverage_harness() { init_gst(); let cfg = CameraConfig { codec: CameraCodec::H264, width: 640, height: 480, fps: 30, }; with_var("LESAVKA_CAM_TEST_ENCODER", Some("v4l2h264enc"), || { let result = CameraCapture::new(Some("test"), Some(cfg)); assert!(result.is_ok() || result.is_err()); }); } #[test] #[serial] fn pull_returns_packet_from_test_pattern_pipeline_when_available() { init_gst(); let cfg = CameraConfig { codec: CameraCodec::H264, width: 320, height: 240, fps: 15, }; match CameraCapture::new(Some("test"), Some(cfg)) { Ok(cap) => { for _ in 0..20 { if let Some(pkt) = cap.pull() { assert_eq!(pkt.id, 2); assert!( !pkt.data.is_empty(), "test pattern should emit payload bytes" ); return; } std::thread::sleep(std::time::Duration::from_millis(30)); } } Err(err) => { assert!(!err.to_string().trim().is_empty()); } } } }