test: cover mjpeg normalizer memory regression

This commit is contained in:
Brad Stein 2026-05-16 12:14:49 -03:00
parent 0c8d4732ae
commit 3337ceac61
4 changed files with 143 additions and 0 deletions

View File

@ -561,6 +561,10 @@ path = "tests/regression/server/gadget/server_gadget_recovery_contract.rs"
name = "server_main_usb_recovery_contract"
path = "tests/regression/server/main/server_main_usb_recovery_contract.rs"
[[test]]
name = "server_mjpeg_normalizer_memory_regression"
path = "tests/regression/server/video_sinks/server_mjpeg_normalizer_memory_regression.rs"
[[test]]
name = "client_log_noise_contract"
path = "tests/reliability/client/diagnostics/client_log_noise_contract.rs"

View File

@ -128,6 +128,10 @@ fn corrupt_video_and_codec_modes_have_guard_compatibility_and_chaos_evidence() {
category: "chaos",
path: "tests/chaos/server/video_sinks/hevc_mjpeg_guard_chaos_contract.rs",
},
EvidencePath {
category: "regression",
path: "tests/regression/server/video_sinks/server_mjpeg_normalizer_memory_regression.rs",
},
EvidencePath {
category: "performance",
path: "tests/performance/server/video_sinks/hevc_mjpeg_handoff_performance_contract.rs",

View File

@ -0,0 +1,131 @@
// Regression coverage for the direct-MJPEG normalizer RSS leak.
//
// Scope: lock down the safe production defaults and lifecycle cleanup for the
// server UVC MJPEG path.
// Targets: server/src/video_sinks/hevc_mjpeg_guard.rs,
// server/src/video_sinks/webcam_sink.rs, server/src/camera_runtime.rs, and
// the camera/upstream media RPC lifecycles.
// Why: the native GStreamer jpegdec/jpegenc normalizer can retain process RSS
// during long calls; future changes must keep it opt-in, guarded, and cleaned
// up when the client stream closes.
mod video_support {
pub fn env_u32(name: &str, default: u32) -> u32 {
std::env::var(name)
.ok()
.and_then(|value| value.trim().parse().ok())
.unwrap_or(default)
}
}
#[allow(clippy::items_after_test_module)]
mod guard {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/server/src/video_sinks/hevc_mjpeg_guard.rs"
));
pub fn normalizer_enabled() -> bool {
direct_mjpeg_normalize_enabled()
}
pub fn normalizer_rss_limit_kb() -> Option<u64> {
direct_mjpeg_normalize_rss_limit_kb()
}
}
const SERVER_INSTALL: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/scripts/install/server.sh"
));
const WEBCAM_SINK: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/server/src/video_sinks/webcam_sink.rs"
));
const CAMERA_RUNTIME: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/server/src/camera_runtime.rs"
));
const CAMERA_RPC: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/server/src/main/relay_service/camera_stream_rpc.rs"
));
const UPSTREAM_MEDIA_RPC: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/server/src/main/relay_service/upstream_media_rpc.rs"
));
fn assert_ordered(haystack: &str, earlier: &str, later: &str) {
let earlier_pos = haystack
.find(earlier)
.unwrap_or_else(|| panic!("missing earlier marker {earlier:?}"));
let later_pos = haystack
.find(later)
.unwrap_or_else(|| panic!("missing later marker {later:?}"));
assert!(
earlier_pos < later_pos,
"expected marker {earlier:?} before {later:?}"
);
}
#[test]
fn direct_mjpeg_normalizer_is_opt_in_after_native_rss_leak() {
temp_env::with_vars(
[
("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE", None::<&str>),
(
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB",
None::<&str>,
),
],
|| {
assert!(
!guard::normalizer_enabled(),
"direct MJPEG normalization must stay opt-in after the native RSS leak"
);
assert_eq!(guard::normalizer_rss_limit_kb(), Some(768 * 1024));
},
);
temp_env::with_vars(
[
("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE", Some("on")),
("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB", Some("0")),
],
|| {
assert!(guard::normalizer_enabled());
assert_eq!(guard::normalizer_rss_limit_kb(), None);
},
);
}
#[test]
fn installer_keeps_the_leaking_native_normalizer_disabled_by_default() {
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-0}"));
assert!(!SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-1}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB:-768}"));
}
#[test]
fn opt_in_normalizer_has_rss_fuse_before_per_frame_gstreamer_allocation() {
assert!(WEBCAM_SINK.contains("current_process_rss_kb"));
assert!(WEBCAM_SINK.contains("direct_mjpeg_normalize_rss_limit_kb"));
assert!(WEBCAM_SINK.contains(
"direct MJPEG normalization disabled because server RSS exceeded its safety limit"
));
assert_ordered(
WEBCAM_SINK,
"direct_mjpeg_normalize_rss_limit_kb()",
"gst::Buffer::from_slice(pkt.data.clone())",
);
}
#[test]
fn closed_camera_streams_release_native_uvc_pipeline_state() {
assert!(CAMERA_RUNTIME.contains("pub async fn release_if_active"));
assert!(CAMERA_RUNTIME.contains("slot.take()"));
assert!(WEBCAM_SINK.contains("impl Drop for WebcamSink"));
assert!(WEBCAM_SINK.contains("set_state(gst::State::Null)"));
assert!(CAMERA_RPC.contains("camera_rt.release_if_active(camera_session_id).await"));
assert!(UPSTREAM_MEDIA_RPC.contains("camera_rt.release_if_active(camera_session_id).await"));
}

View File

@ -267,6 +267,10 @@
"category": "regression",
"new": "tests/regression/server/main/server_main_usb_recovery_contract.rs"
},
{
"category": "regression",
"new": "tests/regression/server/video_sinks/server_mjpeg_normalizer_memory_regression.rs"
},
{
"category": "contract",
"new": "tests/contract/server/runtime_support/server_runtime_contract.rs"