//! 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] #[serial] fn camera_source_profile_defaults_to_auto_decode_for_v4l2_sources() { with_var("LESAVKA_CAM_MJPG", None::<&str>, || { with_var("LESAVKA_CAM_FORMAT", None::<&str>, || { assert_eq!(camera_source_profile(true), CameraSourceProfile::AutoDecode); assert_eq!(camera_source_profile(false), CameraSourceProfile::Raw); }); }); with_var("LESAVKA_CAM_FORMAT", Some("raw"), || { assert_eq!(camera_source_profile(true), CameraSourceProfile::Raw); }); with_var("LESAVKA_CAM_FORMAT", Some("mjpeg"), || { assert_eq!(camera_source_profile(true), CameraSourceProfile::Mjpeg); }); with_var("LESAVKA_CAM_MJPG", Some("1"), || { assert_eq!(camera_source_profile(true), CameraSourceProfile::Mjpeg); }); } #[test] fn camera_auto_decode_caps_accept_raw_and_mjpeg_at_requested_profile() { let caps = camera_auto_decode_caps(1280, 720, 30); assert!(caps.contains("video/x-raw,width=(int)1280,height=(int)720")); assert!(caps.contains("image/jpeg,width=(int)1280,height=(int)720")); assert!(caps.contains("framerate=(fraction)30/1")); let chain = camera_raw_source_chain( "v4l2src device=/dev/video0 do-timestamp=true", "video/x-raw,width=1280,height=720,framerate=30/1", 1280, 720, 30, CameraSourceProfile::AutoDecode, ); assert!(chain.contains("decodebin ! videoconvert ! videoscale ! videorate")); assert!(chain.contains("capsfilter caps=\"")); } #[test] fn camera_preview_tap_uses_the_actual_uplink_dimensions() { let branch = camera_preview_tap_branch(1920, 1080, 30); assert!(branch.contains("width=1920,height=1080")); assert!(branch.contains("framerate=30/1")); assert!(!branch.contains("width=128,height=72")); let branch = camera_preview_tap_branch(1280, 720, 120); assert!(branch.contains("width=1280,height=720")); assert!( branch.contains("framerate=60/1"), "preview tap should cap frame production while preserving resolution" ); } #[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] #[cfg(coverage)] fn camera_bus_logger_coverage_stub_is_non_blocking() { init_gst(); let pipeline = gst::Pipeline::new(); spawn_camera_bus_logger(&pipeline, "test-camera".to_string()); } #[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] #[serial] fn active_camera_capture_can_publish_local_preview_tap() { init_gst(); let dir = tempdir().expect("tempdir"); let path = dir.path().join("uplink-camera-preview.rgba"); let cfg = CameraConfig { codec: CameraCodec::H264, width: 160, height: 90, fps: 10, }; with_var( "LESAVKA_UPLINK_CAMERA_PREVIEW", Some(path.to_string_lossy().to_string()), || { let Ok(cap) = CameraCapture::new(Some("test"), Some(cfg)) else { return; }; for _ in 0..30 { let _ = cap.pull(); if let Ok(bytes) = std::fs::read(&path) { assert!( bytes.starts_with(b"LESAVKA_RGBA "), "preview tap should publish an RGBA frame header" ); let header_end = bytes .iter() .position(|byte| *byte == b'\n') .expect("preview header newline"); let header = std::str::from_utf8(&bytes[..header_end]).expect("utf8 header"); assert!( header.starts_with("LESAVKA_RGBA 160 90 "), "preview tap should prove the selected uplink size, got {header:?}" ); return; } std::thread::sleep(std::time::Duration::from_millis(50)); } panic!("camera preview tap did not publish a frame"); }, ); } #[test] fn spawned_camera_preview_tap_tolerates_publish_errors() { init_gst(); let dir = tempdir().expect("tempdir"); let path = dir.path().join("missing-parent").join("preview.rgba"); let pipeline: gst::Pipeline = gst::parse::launch( "appsrc name=src is-live=true format=time caps=video/x-raw,format=RGBA,width=2,height=2,framerate=1/1 ! \ appsink name=sink emit-signals=false sync=false max-buffers=4 drop=true", ) .expect("pipeline") .downcast() .expect("pipeline cast"); let src: gst_app::AppSrc = pipeline .by_name("src") .expect("appsrc") .downcast() .expect("appsrc cast"); let sink: gst_app::AppSink = pipeline .by_name("sink") .expect("appsink") .downcast() .expect("appsink cast"); pipeline.set_state(gst::State::Playing).expect("playing"); let running = spawn_camera_preview_tap(sink, path); src.push_buffer(gst::Buffer::from_slice(vec![255_u8; 16])) .expect("push buffer"); std::thread::sleep(std::time::Duration::from_millis(150)); running.store(false, Ordering::Release); let _ = pipeline.set_state(gst::State::Null); } #[test] #[serial] fn new_covers_preview_tap_output_format_combinations() { init_gst(); let dir = tempdir().expect("tempdir"); let path = dir.path().join("preview.rgba"); let mjpeg_cfg = CameraConfig { codec: CameraCodec::Mjpeg, width: 320, height: 240, fps: 15, }; let h264_cfg = CameraConfig { codec: CameraCodec::H264, width: 320, height: 240, fps: 15, }; with_var( "LESAVKA_UPLINK_CAMERA_PREVIEW", Some(path.to_string_lossy().to_string()), || { 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 mjpeg_passthrough = CameraCapture::new(Some("/dev/video42"), Some(mjpeg_cfg)); assert!(mjpeg_passthrough.is_ok() || mjpeg_passthrough.is_err()); let mjpeg_to_h264 = CameraCapture::new(Some("/dev/video42"), Some(h264_cfg)); assert!(mjpeg_to_h264.is_ok() || mjpeg_to_h264.is_err()); }); }, ); } #[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 mjpeg_passthrough = CameraCapture::new(Some("/dev/video42"), Some(mjpeg_cfg)); assert!(mjpeg_passthrough.is_ok() || mjpeg_passthrough.is_err()); 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()); }); assert_eq!( CameraCapture::encoder_options("nvh264enc", None, 30), "nvh264enc" ); } #[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()); } } } }