use super::{ adaptive_gray_roi_mask, adaptive_rgb_roi_mask, dark_roi_factor, extract_audio_samples, extract_video_brightness, extract_video_colors, extract_video_timestamps, palette_match_score, retain_largest_connected_roi, run_command, summarize_frame_brightness, summarize_frame_color, }; use crate::sync_probe::analyze::test_support::{ audio_samples_to_bytes, frame_json, thumbnail_rgb_video_bytes, thumbnail_video_bytes, with_fake_media_tools, }; use std::process::Command; #[test] /// Keeps `extract_video_timestamps_reads_fake_ffprobe_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew. /// Inputs are the typed parameters; output is the return value or side effect. fn extract_video_timestamps_reads_fake_ffprobe_output() { let timestamps = vec![0.0, 0.5, 1.0]; with_fake_media_tools( &frame_json(×tamps), &[1, 2, 3], &[1, 0], |capture_path| { let parsed = extract_video_timestamps(capture_path).expect("video timestamps"); assert_eq!(parsed, timestamps); }, ); } #[test] /// Keeps `extract_video_timestamps_rejects_empty_and_invalid_outputs` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew. /// Inputs are the typed parameters; output is the return value or side effect. fn extract_video_timestamps_rejects_empty_and_invalid_outputs() { with_fake_media_tools(br#"{"frames":[]}"#, &[1], &[1, 0], |capture_path| { let error = extract_video_timestamps(capture_path).expect_err("empty frames fail"); assert!( error .to_string() .contains("did not return any video frame timestamps") ); }); with_fake_media_tools( br#"{"frames":[{"best_effort_timestamp_time":"bad"}]}"#, &[1], &[1, 0], |capture_path| { let error = extract_video_timestamps(capture_path).expect_err("invalid timestamp fails"); assert!(error.to_string().contains("parsing frame timestamp")); }, ); } #[test] /// Keeps `extract_video_brightness_reads_fake_ffmpeg_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew. /// Inputs are the typed parameters; output is the return value or side effect. fn extract_video_brightness_reads_fake_ffmpeg_output() { let brightness = vec![5u8, 100, 250]; with_fake_media_tools( br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#, &thumbnail_video_bytes(&brightness), &[1, 0], |capture_path| { let parsed = extract_video_brightness(capture_path).expect("video brightness"); assert_eq!(parsed, brightness); }, ); } #[test] /// Keeps `extract_video_brightness_rejects_empty_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew. /// Inputs are the typed parameters; output is the return value or side effect. fn extract_video_brightness_rejects_empty_output() { with_fake_media_tools( br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#, &[], &[1, 0], |capture_path| { let error = extract_video_brightness(capture_path).expect_err("empty brightness"); assert!( error .to_string() .contains("did not emit any video brightness data") ); }, ); } #[test] /// Keeps `extract_video_brightness_uses_full_frame_thumbnail_average` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew. /// Inputs are the typed parameters; output is the return value or side effect. fn extract_video_brightness_uses_full_frame_thumbnail_average() { let brightness = vec![20u8, 45, 20]; with_fake_media_tools( &frame_json(&[0.0, 0.1, 0.2]), &thumbnail_video_bytes(&brightness), &[1, 0], |capture_path| { let parsed = extract_video_brightness(capture_path).expect("video brightness"); assert_eq!(parsed, brightness); }, ); } #[test] fn extract_video_brightness_rejects_truncated_frame_data() { with_fake_media_tools(&frame_json(&[0.0]), &[1, 2, 3], &[1, 0], |capture_path| { let error = extract_video_brightness(capture_path).expect_err("truncated frame bytes"); assert!(error.to_string().contains("not divisible")); }); } #[test] /// Keeps `extract_video_colors_reads_fake_ffmpeg_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew. /// Inputs are the typed parameters; output is the return value or side effect. fn extract_video_colors_reads_fake_ffmpeg_output() { let colors = vec![(255, 45, 45), (0, 230, 118), (41, 121, 255)]; with_fake_media_tools( &frame_json(&[0.0, 0.1, 0.2]), &thumbnail_rgb_video_bytes(&colors), &[1, 0], |capture_path| { let parsed = extract_video_colors(capture_path).expect("video colors"); assert_eq!(parsed[0].r, 255); assert_eq!(parsed[1].g, 230); assert_eq!(parsed[2].b, 255); }, ); } #[test] fn extract_video_colors_rejects_empty_and_truncated_frame_data() { with_fake_media_tools( br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#, &[], &[1, 0], |capture_path| { let error = extract_video_colors(capture_path).expect_err("empty colors"); assert!( error .to_string() .contains("did not emit any video color data") ); }, ); with_fake_media_tools(&frame_json(&[0.0]), &[1, 2, 3], &[1, 0], |capture_path| { let error = extract_video_colors(capture_path).expect_err("truncated color bytes"); assert!(error.to_string().contains("not divisible")); }); } #[test] /// Keeps `extract_video_colors_tracks_small_flashing_screen_region` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew. /// Inputs are the typed parameters; output is the return value or side effect. fn extract_video_colors_tracks_small_flashing_screen_region() { const SIDE: usize = 64; let mut bytes = Vec::new(); for color in [(24, 28, 32), (255, 45, 45), (24, 28, 32), (0, 230, 118)] { let mut frame = vec![34u8; SIDE * SIDE * 3]; for y in 6..18 { for x in 40..54 { let offset = (y * SIDE + x) * 3; frame[offset] = color.0; frame[offset + 1] = color.1; frame[offset + 2] = color.2; } } bytes.extend_from_slice(&frame); } with_fake_media_tools( &frame_json(&[0.0, 0.1, 0.2, 0.3]), &bytes, &[1, 0], |capture_path| { let parsed = extract_video_colors(capture_path).expect("video colors"); assert!( parsed[1].r > 220 && parsed[1].g < 80, "red pulse should dominate selected ROI: {:?}", parsed[1] ); assert!( parsed[3].g > 190 && parsed[3].r < 60, "green pulse should dominate selected ROI: {:?}", parsed[3] ); }, ); } #[test] /// Keeps `extract_audio_samples_reads_fake_ffmpeg_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew. /// Inputs are the typed parameters; output is the return value or side effect. fn extract_audio_samples_reads_fake_ffmpeg_output() { let samples = vec![1i16, -2, 32_000]; with_fake_media_tools( br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#, &[1], &audio_samples_to_bytes(&samples), |capture_path| { let parsed = extract_audio_samples(capture_path).expect("audio samples"); assert_eq!(parsed, samples); }, ); } #[test] /// Keeps `extract_audio_samples_rejects_too_short_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew. /// Inputs are the typed parameters; output is the return value or side effect. fn extract_audio_samples_rejects_too_short_output() { with_fake_media_tools( br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#, &[1], &[7], |capture_path| { let error = extract_audio_samples(capture_path).expect_err("short audio"); assert!( error .to_string() .contains("did not emit enough audio data to analyze") ); }, ); } #[test] /// Keeps `run_command_reports_success_and_failure` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew. /// Inputs are the typed parameters; output is the return value or side effect. fn run_command_reports_success_and_failure() { let output = run_command( Command::new("sh").arg("-c").arg("printf 'ok'"), "success command", ) .expect("success output"); assert_eq!(output, b"ok"); let error = run_command( Command::new("sh") .arg("-c") .arg("printf 'boom' >&2; exit 7"), "failing command", ) .expect_err("failing command should error"); assert!(error.to_string().contains("failing command failed: boom")); } #[test] /// Verifies adaptive ROI helpers have explicit fallback behavior. /// /// Inputs: tiny masks and frames that cannot produce a stable ROI plus one /// connected flashing region. Outputs: helper-level assertions. Why: analyzer /// robustness depends on falling back to whole-frame summaries when the RCT /// capture has too little color/brightness evidence for a reliable mask. fn adaptive_roi_helpers_cover_fallbacks_and_connected_region_retention() { assert!(adaptive_gray_roi_mask(&[], 4).is_none()); assert!(adaptive_rgb_roi_mask(&[], 4).is_none()); assert!(adaptive_gray_roi_mask(&[&[1, 2, 3, 4]], 4).is_none()); assert!(adaptive_rgb_roi_mask(&[&[1, 2, 3, 4, 5, 6]], 2).is_none()); assert_eq!( summarize_frame_brightness(&[10, 30], Some(&[false, false])), 20 ); let color = summarize_frame_color(&[10, 20, 30, 40, 50, 60], Some(&[false, false])); assert_eq!((color.r, color.g, color.b), (25, 35, 45)); assert_eq!(dark_roi_factor(130), 0.25); assert_eq!(dark_roi_factor(200), 0.10); assert_eq!(palette_match_score(10, 10, 10), 0.0); assert!(palette_match_score(255, 45, 45) > 0.95); let non_square = vec![true, false, true]; assert_eq!(retain_largest_connected_roi(non_square.clone()), non_square); let mut mask = vec![false; 36]; for selected in mask.iter_mut().take(20) { *selected = true; } mask[35] = true; let retained = retain_largest_connected_roi(mask); assert_eq!(retained.iter().filter(|selected| **selected).count(), 20); assert!(!retained[35]); }