lesavka/testing/tests/client_camera_include_contract.rs

274 lines
8.7 KiB
Rust

//! 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]
#[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"
);
return;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
panic!("camera preview tap did not publish a frame");
},
);
}
#[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());
});
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());
}
}
}
}