98 lines
3.8 KiB
Rust
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"));
|
|
}
|