lesavka/testing/tests/uvc_frame_meta_log_contract.rs

98 lines
3.8 KiB
Rust

//! Contract tests for UVC frame metadata artifact summarization.
//!
//! Scope: optional `LESAVKA_UVC_FRAME_META_LOG_PATH` artifacts.
//! Targets: `scripts/manual/summarize_uvc_frame_meta_log.py`.
//! Why: HEVC client-to-RCT work needs a safe local way to decide whether
//! event-coded frames reached the UVC spool before we add riskier server-side
//! introspection.
use std::{fs, path::PathBuf, process::Command};
use serde_json::Value;
const SUMMARIZER: &str = include_str!("../../scripts/manual/summarize_uvc_frame_meta_log.py");
fn repo_script_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("repo root")
.join("scripts/manual/summarize_uvc_frame_meta_log.py")
}
#[test]
fn uvc_frame_meta_summarizer_documents_boundary_diagnostics() {
for expected in [
"lesavka.uvc-mjpeg-spool-meta.v1",
"lesavka.uvc-mjpeg-spool-summary.v1",
"event_coverage",
"source_cadence_hiccup_count",
"decoded_pts_delta_p95_ms",
"sequence_gap_count",
"HEVC client-to-RCT",
] {
assert!(
SUMMARIZER.contains(expected),
"UVC frame metadata summarizer should preserve marker {expected}"
);
}
}
#[test]
fn uvc_frame_meta_summarizer_reports_cadence_and_event_coverage() {
let dir = tempfile::tempdir().expect("tempdir");
let log_path = dir.path().join("uvc-frame-meta.jsonl");
let timeline_path = dir.path().join("client-timeline.json");
let json_out = dir.path().join("summary.json");
let txt_out = dir.path().join("summary.txt");
fs::write(
&log_path,
r#"not-json
{"schema":"lesavka.uvc-mjpeg-spool-meta.v1","sequence":10,"profile":"hevc-decoded-mjpeg","bytes":1000,"source_pts_us":0,"decoded_pts_us":2000,"spool_unix_ns":1000000000}
{"schema":"lesavka.uvc-mjpeg-spool-meta.v1","sequence":11,"profile":"hevc-decoded-mjpeg","bytes":1100,"source_pts_us":33333,"decoded_pts_us":35333,"spool_unix_ns":1033333000}
{"schema":"lesavka.uvc-mjpeg-spool-meta.v1","sequence":12,"profile":"hevc-decoded-mjpeg","bytes":1200,"source_pts_us":66666,"decoded_pts_us":68666,"spool_unix_ns":1066666000}
{"schema":"lesavka.uvc-mjpeg-spool-meta.v1","sequence":14,"profile":"hevc-decoded-mjpeg","bytes":1300,"source_pts_us":133333,"decoded_pts_us":135333,"spool_unix_ns":1133333000}
"#,
)
.expect("write log");
fs::write(
&timeline_path,
r#"{"events":[{"event_id":0,"code":1,"planned_start_us":0,"planned_end_us":120000},{"event_id":1,"code":2,"planned_start_us":200000,"planned_end_us":320000}]}"#,
)
.expect("write timeline");
let output = Command::new("python3")
.arg(repo_script_path())
.arg(&log_path)
.arg(&json_out)
.arg(&txt_out)
.arg("--fps")
.arg("30")
.arg("--timeline")
.arg(&timeline_path)
.output()
.expect("run summarizer");
assert!(
output.status.success(),
"summarizer should succeed: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let summary: Value =
serde_json::from_str(&fs::read_to_string(&json_out).expect("summary json"))
.expect("parse summary json");
assert_eq!(summary["record_count"], 4);
assert_eq!(summary["ignored_line_count"], 1);
assert_eq!(summary["profiles"]["hevc-decoded-mjpeg"], 4);
assert_eq!(summary["sequence_gap_count"], 1);
assert_eq!(summary["source_cadence_hiccup_count"], 1);
assert_eq!(summary["event_coverage"]["covered_events"], 1);
assert_eq!(summary["event_coverage"]["missing_codes"][0], 2);
assert_eq!(summary["decoded_pts_delta_median_ms"], 2.0);
let text = fs::read_to_string(&txt_out).expect("summary text");
assert!(text.contains("event coverage: 1/2 missing_codes=[2]"));
assert!(text.contains("sequence: 10..14 gaps=1"));
}