diff --git a/Cargo.lock b/Cargo.lock index 9f2990a..3d2d514 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -733,6 +733,7 @@ dependencies = [ "rayon", "serde", "serde_json", + "tempfile", "toml", "vid-repair-core", ] diff --git a/rulesets/decode.toml b/rulesets/decode.toml index 748f4ec..fccae82 100644 --- a/rulesets/decode.toml +++ b/rulesets/decode.toml @@ -17,3 +17,13 @@ fix_tier = "reencode" stop_scan = false patterns = ["(?i)File ended prematurely"] notes = "File appears truncated." + +[[rule]] +id = "ERROR_WHILE_DECODING" +domain = "decode" +severity = "high" +confidence = 0.5 +fix_tier = "reencode" +stop_scan = false +patterns = ["(?i)Error while decoding"] +notes = "Decoder reported an error while decoding stream." diff --git a/vid-repair-core/tests/fixtures.rs b/vid-repair-core/tests/fixtures.rs index 943a83b..ef6d5c1 100644 --- a/vid-repair-core/tests/fixtures.rs +++ b/vid-repair-core/tests/fixtures.rs @@ -23,9 +23,23 @@ fn ruleset_dir() -> PathBuf { .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 !command_available("ffmpeg") || !command_available("ffprobe") { + if should_skip() { eprintln!("ffmpeg/ffprobe not available; skipping fixture test"); return; } @@ -74,3 +88,76 @@ fn scan_clean_fixture_has_no_issues() { 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::>() + ); +} diff --git a/vid-repair/Cargo.toml b/vid-repair/Cargo.toml index 6dd826a..f74f4bd 100644 --- a/vid-repair/Cargo.toml +++ b/vid-repair/Cargo.toml @@ -11,3 +11,6 @@ serde_json = { workspace = true } toml = { workspace = true } rayon = { workspace = true } vid-repair-core = { path = "../vid-repair-core" } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/vid-repair/tests/cli.rs b/vid-repair/tests/cli.rs new file mode 100644 index 0000000..f412973 --- /dev/null +++ b/vid-repair/tests/cli.rs @@ -0,0 +1,52 @@ +use std::path::PathBuf; +use std::process::Command; + +use tempfile::tempdir; + +fn command_available(cmd: &str) -> bool { + Command::new(cmd) + .arg("-version") + .output() + .map(|out| out.status.success()) + .unwrap_or(false) +} + +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") +} + +#[test] +fn cli_scan_summarizes_fixtures() { + if !command_available("ffmpeg") || !command_available("ffprobe") { + eprintln!("ffmpeg/ffprobe not available; skipping cli test"); + return; + } + + let fixture_dir = fixture_dir(); + if !fixture_dir.exists() { + eprintln!("fixture dir missing; skipping cli test"); + return; + } + + let temp = tempdir().expect("tempdir"); + let bin = env!("CARGO_BIN_EXE_vid-repair"); + + let output = Command::new(bin) + .current_dir(fixture_dir.parent().unwrap().parent().unwrap().parent().unwrap()) + .env("XDG_CONFIG_HOME", temp.path()) + .arg("scan") + .arg(fixture_dir) + .output() + .expect("run vid-repair"); + + assert!(output.status.success(), "cli scan failed"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Summary:"), "missing summary in output"); +}