test(lesavka): keep coverage helpers out of source gates

This commit is contained in:
Brad Stein 2026-05-19 11:28:49 -03:00
parent 84f84b4891
commit 3c7ccfdbcc
12 changed files with 567 additions and 567 deletions

View File

@ -179,206 +179,5 @@ impl Drop for OpusPacketEncoder {
}
#[cfg(test)]
mod tests {
use super::*;
use lesavka_common::lesavka::AudioEncoding;
#[test]
fn requested_audio_codec_defaults_to_pcm_and_parses_opus() {
temp_env::with_var(AUDIO_CODEC_ENV, None::<&str>, || {
temp_env::with_var(AUDIO_CODEC_LEGACY_ENV, None::<&str>, || {
assert_eq!(
requested_upstream_audio_codec_from_env(),
UpstreamAudioCodec::PcmS16le
);
});
});
temp_env::with_var(AUDIO_CODEC_ENV, Some("pcm"), || {
assert_eq!(
requested_upstream_audio_codec_from_env(),
UpstreamAudioCodec::PcmS16le
);
});
temp_env::with_var(AUDIO_CODEC_ENV, Some("opus"), || {
temp_env::with_var(AUDIO_CODEC_LEGACY_ENV, Some("pcm"), || {
assert_eq!(
requested_upstream_audio_codec_from_env(),
UpstreamAudioCodec::Opus
);
});
});
temp_env::with_var(AUDIO_CODEC_ENV, None::<&str>, || {
temp_env::with_var(AUDIO_CODEC_LEGACY_ENV, Some("compressed"), || {
assert_eq!(
requested_upstream_audio_codec_from_env(),
UpstreamAudioCodec::Opus
);
});
});
temp_env::with_var(AUDIO_CODEC_ENV, Some("aac"), || {
temp_env::with_var(AUDIO_CODEC_LEGACY_ENV, Some("opus"), || {
assert_eq!(
requested_upstream_audio_codec_from_env(),
UpstreamAudioCodec::PcmS16le
);
});
});
}
#[test]
fn opus_encoder_preserves_packet_timing_when_plugin_is_available() {
let _ = gst::init();
if gst::ElementFactory::find("opusenc").is_none() {
return;
}
let mut packet = AudioPacket {
pts: 123_000,
data: vec![0; AudioTransportProfile::pcm_s16le().expected_payload_bytes() as usize],
frame_duration_us: 20_000,
..AudioPacket::default()
};
let raw_len = packet.data.len();
audio_transport::mark_packet_pcm_s16le(&mut packet);
let mut encoder = OpusPacketEncoder::new().expect("opus encoder");
let encoded = encoder
.encode_packet(packet.clone())
.expect("encode")
.unwrap_or_else(|| packet.clone());
assert_eq!(encoded.pts, 123_000);
assert_eq!(encoded.sample_rate, 48_000);
assert_eq!(encoded.channels, 2);
if encoded.encoding == AudioEncoding::Opus as i32 {
assert!(
encoded.data.len() < raw_len,
"Opus packet should be smaller than raw PCM"
);
}
}
#[test]
fn opus_encoder_decodes_non_silent_voice_like_pcm_when_plugin_is_available() {
let _ = gst::init();
if gst::ElementFactory::find("opusenc").is_none()
|| gst::ElementFactory::find("opusdec").is_none()
{
return;
}
let mut packet = AudioPacket {
pts: 456_000,
data: sine_pcm_packet(
440.0,
AudioTransportProfile::pcm_s16le().expected_payload_bytes() as usize,
),
frame_duration_us: 20_000,
..AudioPacket::default()
};
audio_transport::mark_packet_pcm_s16le(&mut packet);
let mut encoder = OpusPacketEncoder::new().expect("opus encoder");
let mut encoded = None;
for _ in 0..4 {
encoded = encoder.encode_packet(packet.clone()).expect("encode");
if encoded.is_some() {
break;
}
}
let Some(encoded) = encoded else {
return;
};
let decoded = decode_opus_payload(&encoded.data).expect("decode opus payload");
let peak = decoded
.chunks_exact(2)
.map(|sample| i16::from_le_bytes([sample[0], sample[1]]).unsigned_abs())
.max()
.unwrap_or(0);
assert!(
peak > 1_000,
"decoded Opus should contain a real waveform, not silence/garbage"
);
}
#[test]
fn packet_metadata_keeps_capture_timing_without_pcm_payload() {
let packet = AudioPacket {
pts: 987_000,
encoding: AudioEncoding::PcmS16le as i32,
sample_rate: 48_000,
channels: 2,
frame_duration_us: 20_000,
data: vec![0x11; 3_840],
..AudioPacket::default()
};
let metadata = packet_metadata(&packet);
assert_eq!(metadata.pts, 987_000);
assert_eq!(metadata.encoding, AudioEncoding::PcmS16le as i32);
assert_eq!(metadata.sample_rate, 48_000);
assert_eq!(metadata.channels, 2);
assert_eq!(metadata.frame_duration_us, 20_000);
assert!(metadata.data.is_empty());
}
#[test]
fn pending_packet_metadata_drops_oldest_when_encoder_lags() {
let mut pending = VecDeque::new();
for pts in 100..120 {
push_pending_packet(
&mut pending,
AudioPacket {
pts,
..AudioPacket::default()
},
);
}
assert_eq!(pending.len(), MAX_PENDING_OPUS_METADATA);
assert_eq!(pending.front().expect("oldest retained").pts, 104);
assert_eq!(pending.back().expect("newest retained").pts, 119);
}
fn sine_pcm_packet(freq_hz: f32, len: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(len);
let frames = len / 4;
for frame in 0..frames {
let phase = (frame as f32 * freq_hz * std::f32::consts::TAU) / 48_000.0;
let sample = (phase.sin() * 12_000.0) as i16;
out.extend_from_slice(&sample.to_le_bytes());
out.extend_from_slice(&sample.to_le_bytes());
}
out
}
fn decode_opus_payload(payload: &[u8]) -> Option<Vec<u8>> {
let desc = "\
appsrc name=src is-live=true block=false format=time \
caps=audio/x-opus,channel-mapping-family=0 ! \
opusdec plc=false use-inband-fec=false min-latency=0 tolerance=0 ! \
audioconvert ! audioresample ! \
audio/x-raw,format=S16LE,layout=interleaved,channels=2,rate=48000 ! \
appsink name=sink emit-signals=false sync=false max-buffers=8 drop=true";
let pipeline: gst::Pipeline = gst::parse::launch(desc).ok()?.downcast().ok()?;
let appsrc = pipeline
.by_name("src")?
.downcast::<gst_app::AppSrc>()
.ok()?;
let appsink = pipeline
.by_name("sink")?
.downcast::<gst_app::AppSink>()
.ok()?;
pipeline.set_state(gst::State::Playing).ok()?;
let mut buffer = gst::Buffer::from_slice(payload.to_vec());
if let Some(meta) = buffer.get_mut() {
meta.set_pts(Some(gst::ClockTime::from_useconds(456_000)));
meta.set_duration(Some(gst::ClockTime::from_useconds(20_000)));
}
appsrc.push_buffer(buffer).ok()?;
let sample = appsink.try_pull_sample(gst::ClockTime::from_mseconds(100))?;
let decoded = sample.buffer()?.map_readable().ok()?.to_vec();
let _ = pipeline.set_state(gst::State::Null);
Some(decoded)
}
}
#[path = "audio_codec/tests/mod.rs"]
mod tests;

View File

@ -0,0 +1,201 @@
use super::*;
use lesavka_common::lesavka::AudioEncoding;
#[test]
fn requested_audio_codec_defaults_to_pcm_and_parses_opus() {
temp_env::with_var(AUDIO_CODEC_ENV, None::<&str>, || {
temp_env::with_var(AUDIO_CODEC_LEGACY_ENV, None::<&str>, || {
assert_eq!(
requested_upstream_audio_codec_from_env(),
UpstreamAudioCodec::PcmS16le
);
});
});
temp_env::with_var(AUDIO_CODEC_ENV, Some("pcm"), || {
assert_eq!(
requested_upstream_audio_codec_from_env(),
UpstreamAudioCodec::PcmS16le
);
});
temp_env::with_var(AUDIO_CODEC_ENV, Some("opus"), || {
temp_env::with_var(AUDIO_CODEC_LEGACY_ENV, Some("pcm"), || {
assert_eq!(
requested_upstream_audio_codec_from_env(),
UpstreamAudioCodec::Opus
);
});
});
temp_env::with_var(AUDIO_CODEC_ENV, None::<&str>, || {
temp_env::with_var(AUDIO_CODEC_LEGACY_ENV, Some("compressed"), || {
assert_eq!(
requested_upstream_audio_codec_from_env(),
UpstreamAudioCodec::Opus
);
});
});
temp_env::with_var(AUDIO_CODEC_ENV, Some("aac"), || {
temp_env::with_var(AUDIO_CODEC_LEGACY_ENV, Some("opus"), || {
assert_eq!(
requested_upstream_audio_codec_from_env(),
UpstreamAudioCodec::PcmS16le
);
});
});
}
#[test]
fn opus_encoder_preserves_packet_timing_when_plugin_is_available() {
let _ = gst::init();
if gst::ElementFactory::find("opusenc").is_none() {
return;
}
let mut packet = AudioPacket {
pts: 123_000,
data: vec![0; AudioTransportProfile::pcm_s16le().expected_payload_bytes() as usize],
frame_duration_us: 20_000,
..AudioPacket::default()
};
let raw_len = packet.data.len();
audio_transport::mark_packet_pcm_s16le(&mut packet);
let mut encoder = OpusPacketEncoder::new().expect("opus encoder");
let encoded = encoder
.encode_packet(packet.clone())
.expect("encode")
.unwrap_or_else(|| packet.clone());
assert_eq!(encoded.pts, 123_000);
assert_eq!(encoded.sample_rate, 48_000);
assert_eq!(encoded.channels, 2);
if encoded.encoding == AudioEncoding::Opus as i32 {
assert!(
encoded.data.len() < raw_len,
"Opus packet should be smaller than raw PCM"
);
}
}
#[test]
fn opus_encoder_decodes_non_silent_voice_like_pcm_when_plugin_is_available() {
let _ = gst::init();
if gst::ElementFactory::find("opusenc").is_none()
|| gst::ElementFactory::find("opusdec").is_none()
{
return;
}
let mut packet = AudioPacket {
pts: 456_000,
data: sine_pcm_packet(
440.0,
AudioTransportProfile::pcm_s16le().expected_payload_bytes() as usize,
),
frame_duration_us: 20_000,
..AudioPacket::default()
};
audio_transport::mark_packet_pcm_s16le(&mut packet);
let mut encoder = OpusPacketEncoder::new().expect("opus encoder");
let mut encoded = None;
for _ in 0..4 {
encoded = encoder.encode_packet(packet.clone()).expect("encode");
if encoded.is_some() {
break;
}
}
let Some(encoded) = encoded else {
return;
};
let decoded = decode_opus_payload(&encoded.data).expect("decode opus payload");
let peak = decoded
.chunks_exact(2)
.map(|sample| i16::from_le_bytes([sample[0], sample[1]]).unsigned_abs())
.max()
.unwrap_or(0);
assert!(
peak > 1_000,
"decoded Opus should contain a real waveform, not silence/garbage"
);
}
#[test]
fn packet_metadata_keeps_capture_timing_without_pcm_payload() {
let packet = AudioPacket {
pts: 987_000,
encoding: AudioEncoding::PcmS16le as i32,
sample_rate: 48_000,
channels: 2,
frame_duration_us: 20_000,
data: vec![0x11; 3_840],
..AudioPacket::default()
};
let metadata = packet_metadata(&packet);
assert_eq!(metadata.pts, 987_000);
assert_eq!(metadata.encoding, AudioEncoding::PcmS16le as i32);
assert_eq!(metadata.sample_rate, 48_000);
assert_eq!(metadata.channels, 2);
assert_eq!(metadata.frame_duration_us, 20_000);
assert!(metadata.data.is_empty());
}
#[test]
fn pending_packet_metadata_drops_oldest_when_encoder_lags() {
let mut pending = VecDeque::new();
for pts in 100..120 {
push_pending_packet(
&mut pending,
AudioPacket {
pts,
..AudioPacket::default()
},
);
}
assert_eq!(pending.len(), MAX_PENDING_OPUS_METADATA);
assert_eq!(pending.front().expect("oldest retained").pts, 104);
assert_eq!(pending.back().expect("newest retained").pts, 119);
}
fn sine_pcm_packet(freq_hz: f32, len: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(len);
let frames = len / 4;
for frame in 0..frames {
let phase = (frame as f32 * freq_hz * std::f32::consts::TAU) / 48_000.0;
let sample = (phase.sin() * 12_000.0) as i16;
out.extend_from_slice(&sample.to_le_bytes());
out.extend_from_slice(&sample.to_le_bytes());
}
out
}
fn decode_opus_payload(payload: &[u8]) -> Option<Vec<u8>> {
let desc = "\
appsrc name=src is-live=true block=false format=time \
caps=audio/x-opus,channel-mapping-family=0 ! \
opusdec plc=false use-inband-fec=false min-latency=0 tolerance=0 ! \
audioconvert ! audioresample ! \
audio/x-raw,format=S16LE,layout=interleaved,channels=2,rate=48000 ! \
appsink name=sink emit-signals=false sync=false max-buffers=8 drop=true";
let pipeline: gst::Pipeline = gst::parse::launch(desc).ok()?.downcast().ok()?;
let appsrc = pipeline
.by_name("src")?
.downcast::<gst_app::AppSrc>()
.ok()?;
let appsink = pipeline
.by_name("sink")?
.downcast::<gst_app::AppSink>()
.ok()?;
pipeline.set_state(gst::State::Playing).ok()?;
let mut buffer = gst::Buffer::from_slice(payload.to_vec());
if let Some(meta) = buffer.get_mut() {
meta.set_pts(Some(gst::ClockTime::from_useconds(456_000)));
meta.set_duration(Some(gst::ClockTime::from_useconds(20_000)));
}
appsrc.push_buffer(buffer).ok()?;
let sample = appsink.try_pull_sample(gst::ClockTime::from_mseconds(100))?;
let decoded = sample.buffer()?.map_readable().ok()?.to_vec();
let _ = pipeline.set_state(gst::State::Null);
Some(decoded)
}

View File

@ -68,224 +68,5 @@ include!("camera/preview_tap.rs");
include!("camera/bus_and_encoder.rs");
#[cfg(test)]
mod tests {
use super::{
CameraCapture, CameraCodec, CameraConfig, hevc_keyframe_interval, resolved_capture_profile,
resolved_output_profile,
};
use serial_test::serial;
#[test]
#[serial]
/// Keeps the selected local webcam mode independent from the UVC gadget mode.
fn local_capture_profile_keeps_launcher_quality_env_by_default() {
let cfg = CameraConfig {
codec: CameraCodec::Mjpeg,
width: 640,
height: 480,
fps: 20,
};
temp_env::with_vars(
[
("LESAVKA_CAM_WIDTH", Some("1280")),
("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None),
("LESAVKA_CAM_EMIT_UI_PROFILE", None),
],
|| assert_eq!(resolved_capture_profile(Some(cfg)), (1280, 720, 30)),
);
}
#[test]
#[serial]
/// UVC output must match the gadget profile that browsers negotiate.
fn negotiated_output_profile_matches_server_uvc_contract_by_default() {
let cfg = CameraConfig {
codec: CameraCodec::Mjpeg,
width: 640,
height: 480,
fps: 20,
};
temp_env::with_vars(
[
("LESAVKA_CAM_WIDTH", Some("1280")),
("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None),
("LESAVKA_CAM_EMIT_UI_PROFILE", None),
],
|| {
let capture_profile = resolved_capture_profile(Some(cfg));
assert_eq!(capture_profile, (1280, 720, 30));
assert_eq!(
resolved_output_profile(Some(cfg), capture_profile),
(640, 480, 20)
);
},
);
}
#[test]
#[serial]
/// Keeps UI-profile emission explicit until the server can reconfigure UVC.
fn explicit_ui_profile_emission_keeps_lab_mode_available() {
let cfg = CameraConfig {
codec: CameraCodec::Mjpeg,
width: 640,
height: 480,
fps: 20,
};
temp_env::with_vars(
[
("LESAVKA_CAM_WIDTH", Some("1280")),
("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None),
("LESAVKA_CAM_EMIT_UI_PROFILE", Some("1")),
],
|| {
let capture_profile = resolved_capture_profile(Some(cfg));
assert_eq!(capture_profile, (1280, 720, 30));
assert_eq!(
resolved_output_profile(Some(cfg), capture_profile),
(1280, 720, 30)
);
},
);
}
#[test]
#[serial]
/// The safety lock wins if both experimental flags are set.
fn explicit_server_profile_lock_wins_over_ui_emission() {
let cfg = CameraConfig {
codec: CameraCodec::Mjpeg,
width: 640,
height: 480,
fps: 20,
};
temp_env::with_vars(
[
("LESAVKA_CAM_WIDTH", Some("1280")),
("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", Some("1")),
("LESAVKA_CAM_EMIT_UI_PROFILE", Some("1")),
],
|| {
let capture_profile = resolved_capture_profile(Some(cfg));
assert_eq!(capture_profile, (1280, 720, 30));
assert_eq!(
resolved_output_profile(Some(cfg), capture_profile),
(640, 480, 20)
);
},
);
}
#[test]
#[serial]
/// HEVC lab fallback options must stay shaped for live transport.
fn hevc_lab_fallback_options_keep_low_latency_and_keyframes() {
temp_env::with_var("LESAVKA_CAM_HEVC_KBIT", Some("2400"), || {
let options = CameraCapture::encoder_options("x265enc", Some("key-int-max"), 30);
assert!(options.starts_with("x265enc "));
assert!(options.contains("tune=zerolatency"));
assert!(options.contains("speed-preset=ultrafast"));
assert!(options.contains("bitrate=2400"));
assert!(options.contains("key-int-max=30"));
});
}
#[test]
#[serial]
/// Vulkan H.264 hardware encode should stay live-call shaped when available.
fn vulkan_h264_encoder_options_keep_cbr_and_keyframes() {
temp_env::with_var("LESAVKA_CAM_H264_KBIT", Some("6000"), || {
let options = CameraCapture::encoder_options("vulkanh264enc", Some("idr-period"), 30);
assert_eq!(
options,
"vulkanh264enc bitrate=6000 rate-control=cbr idr-period=30"
);
});
}
#[test]
#[serial]
/// HEVC should recover quickly after freshness drops without changing H.264 knobs.
fn hevc_keyframe_interval_defaults_short_and_honors_overrides() {
temp_env::with_vars(
[
("LESAVKA_CAM_KEYFRAME_INTERVAL", None::<&str>),
("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", None::<&str>),
],
|| {
assert_eq!(hevc_keyframe_interval(30), 1);
assert_eq!(hevc_keyframe_interval(2), 1);
},
);
temp_env::with_vars(
[
("LESAVKA_CAM_KEYFRAME_INTERVAL", Some("5")),
("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", Some("2")),
],
|| {
assert_eq!(hevc_keyframe_interval(30), 2);
},
);
}
#[cfg(coverage)]
#[test]
/// Coverage builds use a deterministic HEVC encoder choice.
fn coverage_hevc_encoder_choice_is_stable() {
assert_eq!(
CameraCapture::choose_hevc_encoder().unwrap(),
("x265enc", Some("key-int-max"))
);
}
#[cfg(coverage)]
#[test]
#[serial]
/// Coverage builds can exercise H.264 encoder branches without host GPU assumptions.
fn coverage_h264_encoder_choice_honors_stable_test_overrides() {
let cases = [
("nvh264enc", ("nvh264enc", None)),
("vulkanh264enc", ("vulkanh264enc", Some("idr-period"))),
("vaapih264enc", ("vaapih264enc", Some("keyframe-period"))),
("v4l2h264enc", ("v4l2h264enc", Some("idrcount"))),
("unknown", ("x264enc", Some("key-int-max"))),
];
for (override_value, expected) in cases {
temp_env::with_var("LESAVKA_CAM_TEST_ENCODER", Some(override_value), || {
assert_eq!(CameraCapture::choose_encoder().unwrap(), expected);
});
}
}
#[cfg(coverage)]
#[test]
/// Coverage mode keeps software video fallback enabled for deterministic tests.
fn coverage_software_video_fallback_is_enabled() {
assert!(CameraCapture::software_video_fallback_allowed());
}
#[cfg(coverage)]
#[test]
/// Coverage mode replaces the FFmpeg preview reader with an inert cancellation flag.
fn coverage_ffmpeg_preview_tap_stub_starts_stopped() {
let running = super::spawn_ffmpeg_raw_preview_tap(
std::io::empty(),
std::path::PathBuf::from("/tmp/lesavka-preview-tap.coverage"),
1,
1,
);
assert!(!running.load(std::sync::atomic::Ordering::Acquire));
}
}
#[path = "camera/tests/mod.rs"]
mod tests;

View File

@ -0,0 +1,219 @@
use super::{
CameraCapture, CameraCodec, CameraConfig, hevc_keyframe_interval, resolved_capture_profile,
resolved_output_profile,
};
use serial_test::serial;
#[test]
#[serial]
/// Keeps the selected local webcam mode independent from the UVC gadget mode.
fn local_capture_profile_keeps_launcher_quality_env_by_default() {
let cfg = CameraConfig {
codec: CameraCodec::Mjpeg,
width: 640,
height: 480,
fps: 20,
};
temp_env::with_vars(
[
("LESAVKA_CAM_WIDTH", Some("1280")),
("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None),
("LESAVKA_CAM_EMIT_UI_PROFILE", None),
],
|| assert_eq!(resolved_capture_profile(Some(cfg)), (1280, 720, 30)),
);
}
#[test]
#[serial]
/// UVC output must match the gadget profile that browsers negotiate.
fn negotiated_output_profile_matches_server_uvc_contract_by_default() {
let cfg = CameraConfig {
codec: CameraCodec::Mjpeg,
width: 640,
height: 480,
fps: 20,
};
temp_env::with_vars(
[
("LESAVKA_CAM_WIDTH", Some("1280")),
("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None),
("LESAVKA_CAM_EMIT_UI_PROFILE", None),
],
|| {
let capture_profile = resolved_capture_profile(Some(cfg));
assert_eq!(capture_profile, (1280, 720, 30));
assert_eq!(
resolved_output_profile(Some(cfg), capture_profile),
(640, 480, 20)
);
},
);
}
#[test]
#[serial]
/// Keeps UI-profile emission explicit until the server can reconfigure UVC.
fn explicit_ui_profile_emission_keeps_lab_mode_available() {
let cfg = CameraConfig {
codec: CameraCodec::Mjpeg,
width: 640,
height: 480,
fps: 20,
};
temp_env::with_vars(
[
("LESAVKA_CAM_WIDTH", Some("1280")),
("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None),
("LESAVKA_CAM_EMIT_UI_PROFILE", Some("1")),
],
|| {
let capture_profile = resolved_capture_profile(Some(cfg));
assert_eq!(capture_profile, (1280, 720, 30));
assert_eq!(
resolved_output_profile(Some(cfg), capture_profile),
(1280, 720, 30)
);
},
);
}
#[test]
#[serial]
/// The safety lock wins if both experimental flags are set.
fn explicit_server_profile_lock_wins_over_ui_emission() {
let cfg = CameraConfig {
codec: CameraCodec::Mjpeg,
width: 640,
height: 480,
fps: 20,
};
temp_env::with_vars(
[
("LESAVKA_CAM_WIDTH", Some("1280")),
("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", Some("1")),
("LESAVKA_CAM_EMIT_UI_PROFILE", Some("1")),
],
|| {
let capture_profile = resolved_capture_profile(Some(cfg));
assert_eq!(capture_profile, (1280, 720, 30));
assert_eq!(
resolved_output_profile(Some(cfg), capture_profile),
(640, 480, 20)
);
},
);
}
#[test]
#[serial]
/// HEVC lab fallback options must stay shaped for live transport.
fn hevc_lab_fallback_options_keep_low_latency_and_keyframes() {
temp_env::with_var("LESAVKA_CAM_HEVC_KBIT", Some("2400"), || {
let options = CameraCapture::encoder_options("x265enc", Some("key-int-max"), 30);
assert!(options.starts_with("x265enc "));
assert!(options.contains("tune=zerolatency"));
assert!(options.contains("speed-preset=ultrafast"));
assert!(options.contains("bitrate=2400"));
assert!(options.contains("key-int-max=30"));
});
}
#[test]
#[serial]
/// Vulkan H.264 hardware encode should stay live-call shaped when available.
fn vulkan_h264_encoder_options_keep_cbr_and_keyframes() {
temp_env::with_var("LESAVKA_CAM_H264_KBIT", Some("6000"), || {
let options = CameraCapture::encoder_options("vulkanh264enc", Some("idr-period"), 30);
assert_eq!(
options,
"vulkanh264enc bitrate=6000 rate-control=cbr idr-period=30"
);
});
}
#[test]
#[serial]
/// HEVC should recover quickly after freshness drops without changing H.264 knobs.
fn hevc_keyframe_interval_defaults_short_and_honors_overrides() {
temp_env::with_vars(
[
("LESAVKA_CAM_KEYFRAME_INTERVAL", None::<&str>),
("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", None::<&str>),
],
|| {
assert_eq!(hevc_keyframe_interval(30), 1);
assert_eq!(hevc_keyframe_interval(2), 1);
},
);
temp_env::with_vars(
[
("LESAVKA_CAM_KEYFRAME_INTERVAL", Some("5")),
("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", Some("2")),
],
|| {
assert_eq!(hevc_keyframe_interval(30), 2);
},
);
}
#[cfg(coverage)]
#[test]
/// Coverage builds use a deterministic HEVC encoder choice.
fn coverage_hevc_encoder_choice_is_stable() {
assert_eq!(
CameraCapture::choose_hevc_encoder().unwrap(),
("x265enc", Some("key-int-max"))
);
}
#[cfg(coverage)]
#[test]
#[serial]
/// Coverage builds can exercise H.264 encoder branches without host GPU assumptions.
fn coverage_h264_encoder_choice_honors_stable_test_overrides() {
let cases = [
("nvh264enc", ("nvh264enc", None)),
("vulkanh264enc", ("vulkanh264enc", Some("idr-period"))),
("vaapih264enc", ("vaapih264enc", Some("keyframe-period"))),
("v4l2h264enc", ("v4l2h264enc", Some("idrcount"))),
("unknown", ("x264enc", Some("key-int-max"))),
];
for (override_value, expected) in cases {
temp_env::with_var("LESAVKA_CAM_TEST_ENCODER", Some(override_value), || {
assert_eq!(CameraCapture::choose_encoder().unwrap(), expected);
});
}
}
#[cfg(coverage)]
#[test]
/// Coverage mode keeps software video fallback enabled for deterministic tests.
fn coverage_software_video_fallback_is_enabled() {
assert!(CameraCapture::software_video_fallback_allowed());
}
#[cfg(coverage)]
#[test]
/// Coverage mode replaces the FFmpeg preview reader with an inert cancellation flag.
fn coverage_ffmpeg_preview_tap_stub_starts_stopped() {
let running = super::spawn_ffmpeg_raw_preview_tap(
std::io::empty(),
std::path::PathBuf::from("/tmp/lesavka-preview-tap.coverage"),
1,
1,
);
assert!(!running.load(std::sync::atomic::Ordering::Acquire));
}

View File

@ -442,5 +442,5 @@ fn control_request_nonce() -> u128 {
}
#[cfg(test)]
#[path = "live_media_control/tests.rs"]
#[path = "live_media_control/tests/mod.rs"]
mod tests;

View File

@ -147,143 +147,5 @@ impl Drop for OpusPacketDecoder {
}
#[cfg(test)]
mod tests {
use super::*;
use lesavka_common::lesavka::AudioEncoding;
#[test]
fn opus_decoder_roundtrips_to_pcm_when_plugins_are_available() {
let _ = gst::init();
if gst::ElementFactory::find("opusenc").is_none()
|| gst::ElementFactory::find("opusdec").is_none()
{
return;
}
let Some(opus_payload) = encode_silent_opus_payload() else {
return;
};
let packet = AudioPacket {
pts: 42_000,
encoding: AudioEncoding::Opus as i32,
sample_rate: 48_000,
channels: 2,
frame_duration_us: 20_000,
data: opus_payload,
..AudioPacket::default()
};
let mut decoder = OpusPacketDecoder::new().expect("opus decoder");
let decoded = decoder
.decode_packet(&packet)
.expect("decode")
.expect("decoded pcm");
assert_eq!(decoded.pts, 42_000);
assert_eq!(decoded.encoding, AudioEncoding::PcmS16le as i32);
assert_eq!(decoded.sample_rate, 48_000);
assert_eq!(decoded.channels, 2);
assert!(
decoded.data.len() >= 1_000,
"decoded PCM should be far larger than one compressed Opus frame"
);
assert!(
decoded.data.chunks_exact(2).any(|sample| {
i16::from_le_bytes([sample[0], sample[1]]).unsigned_abs() > 250
}),
"decoded Opus payload should preserve non-silent waveform energy"
);
}
#[test]
fn packet_metadata_keeps_timing_and_clears_payload() {
let packet = AudioPacket {
pts: 123_000,
encoding: AudioEncoding::Opus as i32,
sample_rate: 48_000,
channels: 2,
frame_duration_us: 20_000,
data: vec![1, 2, 3, 4],
..AudioPacket::default()
};
let metadata = packet_metadata(&packet);
assert_eq!(metadata.pts, 123_000);
assert_eq!(metadata.encoding, AudioEncoding::Opus as i32);
assert_eq!(metadata.sample_rate, 48_000);
assert_eq!(metadata.channels, 2);
assert_eq!(metadata.frame_duration_us, 20_000);
assert!(metadata.data.is_empty());
}
#[test]
fn pending_packet_metadata_is_bounded_to_recent_frames() {
let mut pending = VecDeque::new();
for pts in 0..20 {
push_pending_packet(
&mut pending,
AudioPacket {
pts,
..AudioPacket::default()
},
);
}
assert_eq!(pending.len(), MAX_PENDING_OPUS_METADATA);
assert_eq!(pending.front().expect("oldest retained").pts, 4);
assert_eq!(pending.back().expect("newest retained").pts, 19);
}
fn encode_silent_opus_payload() -> Option<Vec<u8>> {
let desc = "\
appsrc name=src is-live=true block=false format=time \
caps=audio/x-raw,format=S16LE,layout=interleaved,channels=2,rate=48000 ! \
opusenc audio-type=restricted-lowdelay bitrate=96000 bitrate-type=cbr complexity=7 frame-size=20 perfect-timestamp=true hard-resync=true ! \
appsink name=sink emit-signals=false sync=false max-buffers=8 drop=true";
let pipeline: gst::Pipeline = gst::parse::launch(desc).ok()?.downcast().ok()?;
let appsrc = pipeline
.by_name("src")?
.downcast::<gst_app::AppSrc>()
.ok()?;
let appsink = pipeline
.by_name("sink")?
.downcast::<gst_app::AppSink>()
.ok()?;
pipeline.set_state(gst::State::Playing).ok()?;
for index in 0..4u64 {
let mut buffer = gst::Buffer::from_slice(sine_pcm_packet(index, 3_840));
if let Some(meta) = buffer.get_mut() {
let pts = gst::ClockTime::from_useconds(index * 20_000);
meta.set_pts(Some(pts));
meta.set_dts(Some(pts));
meta.set_duration(Some(gst::ClockTime::from_useconds(20_000)));
}
appsrc.push_buffer(buffer).ok()?;
if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(50)) {
let payload = sample.buffer()?.map_readable().ok()?.to_vec();
let _ = pipeline.set_state(gst::State::Null);
if !payload.is_empty() {
return Some(payload);
}
}
}
let _ = pipeline.set_state(gst::State::Null);
None
}
fn sine_pcm_packet(packet_index: u64, len: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(len);
let frames = len / 4;
for frame in 0..frames {
let absolute = packet_index as usize * frames + frame;
let phase = (absolute as f32 * 440.0 * std::f32::consts::TAU) / 48_000.0;
let sample = (phase.sin() * 12_000.0) as i16;
out.extend_from_slice(&sample.to_le_bytes());
out.extend_from_slice(&sample.to_le_bytes());
}
out
}
}
#[path = "opus_decode/tests/mod.rs"]
mod tests;

View File

@ -0,0 +1,138 @@
use super::*;
use lesavka_common::lesavka::AudioEncoding;
#[test]
fn opus_decoder_roundtrips_to_pcm_when_plugins_are_available() {
let _ = gst::init();
if gst::ElementFactory::find("opusenc").is_none()
|| gst::ElementFactory::find("opusdec").is_none()
{
return;
}
let Some(opus_payload) = encode_silent_opus_payload() else {
return;
};
let packet = AudioPacket {
pts: 42_000,
encoding: AudioEncoding::Opus as i32,
sample_rate: 48_000,
channels: 2,
frame_duration_us: 20_000,
data: opus_payload,
..AudioPacket::default()
};
let mut decoder = OpusPacketDecoder::new().expect("opus decoder");
let decoded = decoder
.decode_packet(&packet)
.expect("decode")
.expect("decoded pcm");
assert_eq!(decoded.pts, 42_000);
assert_eq!(decoded.encoding, AudioEncoding::PcmS16le as i32);
assert_eq!(decoded.sample_rate, 48_000);
assert_eq!(decoded.channels, 2);
assert!(
decoded.data.len() >= 1_000,
"decoded PCM should be far larger than one compressed Opus frame"
);
assert!(
decoded.data.chunks_exact(2).any(|sample| {
i16::from_le_bytes([sample[0], sample[1]]).unsigned_abs() > 250
}),
"decoded Opus payload should preserve non-silent waveform energy"
);
}
#[test]
fn packet_metadata_keeps_timing_and_clears_payload() {
let packet = AudioPacket {
pts: 123_000,
encoding: AudioEncoding::Opus as i32,
sample_rate: 48_000,
channels: 2,
frame_duration_us: 20_000,
data: vec![1, 2, 3, 4],
..AudioPacket::default()
};
let metadata = packet_metadata(&packet);
assert_eq!(metadata.pts, 123_000);
assert_eq!(metadata.encoding, AudioEncoding::Opus as i32);
assert_eq!(metadata.sample_rate, 48_000);
assert_eq!(metadata.channels, 2);
assert_eq!(metadata.frame_duration_us, 20_000);
assert!(metadata.data.is_empty());
}
#[test]
fn pending_packet_metadata_is_bounded_to_recent_frames() {
let mut pending = VecDeque::new();
for pts in 0..20 {
push_pending_packet(
&mut pending,
AudioPacket {
pts,
..AudioPacket::default()
},
);
}
assert_eq!(pending.len(), MAX_PENDING_OPUS_METADATA);
assert_eq!(pending.front().expect("oldest retained").pts, 4);
assert_eq!(pending.back().expect("newest retained").pts, 19);
}
fn encode_silent_opus_payload() -> Option<Vec<u8>> {
let desc = "\
appsrc name=src is-live=true block=false format=time \
caps=audio/x-raw,format=S16LE,layout=interleaved,channels=2,rate=48000 ! \
opusenc audio-type=restricted-lowdelay bitrate=96000 bitrate-type=cbr complexity=7 frame-size=20 perfect-timestamp=true hard-resync=true ! \
appsink name=sink emit-signals=false sync=false max-buffers=8 drop=true";
let pipeline: gst::Pipeline = gst::parse::launch(desc).ok()?.downcast().ok()?;
let appsrc = pipeline
.by_name("src")?
.downcast::<gst_app::AppSrc>()
.ok()?;
let appsink = pipeline
.by_name("sink")?
.downcast::<gst_app::AppSink>()
.ok()?;
pipeline.set_state(gst::State::Playing).ok()?;
for index in 0..4u64 {
let mut buffer = gst::Buffer::from_slice(sine_pcm_packet(index, 3_840));
if let Some(meta) = buffer.get_mut() {
let pts = gst::ClockTime::from_useconds(index * 20_000);
meta.set_pts(Some(pts));
meta.set_dts(Some(pts));
meta.set_duration(Some(gst::ClockTime::from_useconds(20_000)));
}
appsrc.push_buffer(buffer).ok()?;
if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(50)) {
let payload = sample.buffer()?.map_readable().ok()?.to_vec();
let _ = pipeline.set_state(gst::State::Null);
if !payload.is_empty() {
return Some(payload);
}
}
}
let _ = pipeline.set_state(gst::State::Null);
None
}
fn sine_pcm_packet(packet_index: u64, len: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(len);
let frames = len / 4;
for frame in 0..frames {
let absolute = packet_index as usize * frames + frame;
let phase = (absolute as f32 * 440.0 * std::f32::consts::TAU) / 48_000.0;
let sample = (phase.sin() * 12_000.0) as i16;
out.extend_from_slice(&sample.to_le_bytes());
out.extend_from_slice(&sample.to_le_bytes());
}
out
}

View File

@ -444,5 +444,5 @@ async fn main() -> Result<()> {
include!("lesavka_synthetic_uplink/support.rs");
#[cfg(test)]
#[path = "lesavka_synthetic_uplink/tests.rs"]
#[path = "lesavka_synthetic_uplink/tests/mod.rs"]
mod tests;

View File

@ -338,5 +338,5 @@ pub fn reserve_local_pts(counter: &AtomicU64, preferred_pts_us: u64, frame_step_
}
#[cfg(test)]
#[path = "video_support/tests.rs"]
#[path = "video_support/tests/mod.rs"]
mod tests;