use std::path::PathBuf; use std::process::Command; use tempfile::tempdir; use vid_repair_core::config::Config; use vid_repair_core::rules::RuleSet; use vid_repair_core::scan::scan_file; fn command_available(cmd: &str) -> bool { Command::new(cmd) .arg("-version") .output() .map(|out| out.status.success()) .unwrap_or(false) } fn ruleset_dir() -> PathBuf { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); manifest_dir .parent() .expect("workspace root") .join("rulesets") } fn fixture_dir() -> PathBuf { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); manifest_dir .parent() .expect("workspace root") .join("tests") .join("fixtures") .join("generated") } fn should_skip() -> bool { !command_available("ffmpeg") || !command_available("ffprobe") } #[test] fn scan_clean_fixture_has_no_issues() { if should_skip() { eprintln!("ffmpeg/ffprobe not available; skipping fixture test"); return; } let dir = tempdir().expect("tempdir"); let output = dir.path().join("clean.mp4"); let status = Command::new("ffmpeg") .args([ "-y", "-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", "testsrc=size=128x72:rate=30", "-f", "lavfi", "-i", "sine=frequency=1000:sample_rate=44100", "-t", "1", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-movflags", "+faststart", output.to_str().unwrap(), ]) .status() .expect("ffmpeg run"); if !status.success() { eprintln!("ffmpeg failed to create fixture"); return; } let config = Config::default(); let ruleset = RuleSet::load_from_dir(&ruleset_dir()).expect("ruleset load"); let scan = scan_file(&output, &config, &ruleset).expect("scan file"); assert!(scan.issues.is_empty(), "Expected no issues, got {}", scan.issues.len()); } #[test] fn scan_truncated_fixture_has_errors() { if should_skip() { eprintln!("ffmpeg/ffprobe not available; skipping fixture test"); return; } let path = fixture_dir().join("truncated.mp4"); if !path.exists() { eprintln!("fixture not found: {}; skipping", path.display()); return; } let config = Config::default(); let ruleset = RuleSet::load_from_dir(&ruleset_dir()).expect("ruleset load"); let scan = scan_file(&path, &config, &ruleset).expect("scan file"); let allowed = [ "FILE_ENDED_PREMATURELY", "INVALID_DATA_FOUND", "ERROR_WHILE_DECODING", ]; let matched = scan .issues .iter() .any(|issue| allowed.contains(&issue.code.as_str())); assert!( matched, "Expected truncated fixture to match one of {:?}, got {:?}", allowed, scan.issues.iter().map(|i| i.code.clone()).collect::>() ); } #[test] fn scan_corrupt_fixture_has_errors() { if should_skip() { eprintln!("ffmpeg/ffprobe not available; skipping fixture test"); return; } let path = fixture_dir().join("corrupt_mid.mp4"); if !path.exists() { eprintln!("fixture not found: {}; skipping", path.display()); return; } let config = Config::default(); let ruleset = RuleSet::load_from_dir(&ruleset_dir()).expect("ruleset load"); let scan = scan_file(&path, &config, &ruleset).expect("scan file"); let allowed = [ "INVALID_NAL_UNIT_SIZE", "MISSING_PICTURE_ACCESS_UNIT", "INVALID_DATA_FOUND", "ERROR_WHILE_DECODING", ]; let matched = scan .issues .iter() .any(|issue| allowed.contains(&issue.code.as_str())); assert!( matched, "Expected corrupt fixture to match one of {:?}, got {:?}", allowed, scan.issues.iter().map(|i| i.code.clone()).collect::>() ); }