#[cfg(any(not(coverage), test))] use anyhow::{Context, Result, bail}; #[cfg(not(coverage))] use serde::Serialize; #[cfg(not(coverage))] use lesavka_client::sync_probe::analyze::{ SyncAnalysisOptions, SyncAnalysisReport, SyncCalibrationRecommendation, analyze_capture, }; #[cfg(not(coverage))] #[derive(Serialize)] struct SyncAnalyzeOutput<'a> { #[serde(flatten)] report: &'a SyncAnalysisReport, calibration: SyncCalibrationRecommendation, } #[cfg(not(coverage))] fn main() -> Result<()> { let (capture_path, emit_json) = parse_args(std::env::args().skip(1))?; let report = analyze_capture(&capture_path, &SyncAnalysisOptions::default()) .with_context(|| format!("analyzing sync capture {}", capture_path.display()))?; let calibration = report.calibration_recommendation(); if emit_json { let output = SyncAnalyzeOutput { report: &report, calibration, }; println!( "{}", serde_json::to_string_pretty(&output).context("serializing JSON report")? ); } else { println!("A/V sync report for {}", capture_path.display()); println!("- video onsets: {}", report.video_event_count); println!("- audio onsets: {}", report.audio_event_count); println!("- paired pulses: {}", report.paired_event_count); println!( "- first skew: {:+.1} ms (audio after video is positive)", report.first_skew_ms ); println!("- last skew: {:+.1} ms", report.last_skew_ms); println!("- mean skew: {:+.1} ms", report.mean_skew_ms); println!("- median skew: {:+.1} ms", report.median_skew_ms); println!("- max abs skew: {:.1} ms", report.max_abs_skew_ms); println!("- drift: {:+.1} ms", report.drift_ms); println!("- calibration ready: {}", calibration.ready); println!( "- recommended audio offset adjust: {:+} us", calibration.recommended_audio_offset_adjust_us ); println!( "- alternative video offset adjust: {:+} us", calibration.recommended_video_offset_adjust_us ); println!("- calibration note: {}", calibration.note); } Ok(()) } #[cfg(any(not(coverage), test))] fn parse_args(args: I) -> Result<(std::path::PathBuf, bool)> where I: IntoIterator, S: Into, { let args = args.into_iter().map(Into::into).collect::>(); if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") { println!("Usage: lesavka-sync-analyze [--json]"); std::process::exit(0); } let mut emit_json = false; let mut capture_path = None::; for arg in args { if arg == "--json" { emit_json = true; continue; } if capture_path.is_some() { bail!("unexpected extra argument `{arg}`"); } capture_path = Some(std::path::PathBuf::from(arg)); } let capture_path = capture_path.context("capture path is required")?; Ok((capture_path, emit_json)) } #[cfg(coverage)] fn main() {} #[cfg(test)] mod tests { use super::parse_args; #[test] fn parse_args_accepts_capture_path_and_json_flag() { let (path, json) = parse_args(["capture.mkv", "--json"]).expect("args"); assert_eq!(path, std::path::PathBuf::from("capture.mkv")); assert!(json); } #[test] fn parse_args_rejects_extra_positional_arguments() { assert!(parse_args(["one.mkv", "two.mkv"]).is_err()); } #[test] fn parse_args_requires_a_capture_path() { let error = parse_args(["--json"]).expect_err("missing capture path should fail"); assert!( error.to_string().contains("capture path is required"), "unexpected error: {error:#}" ); } #[test] fn coverage_main_stub_is_non_panicking() { let _ = super::main(); } }