Add scan depth, exit codes, and fix validation test
This commit is contained in:
@@ -3,7 +3,6 @@ use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use fs_err as fs;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::fix::{FixKind, FixOutcome, FixPlan};
|
||||
@@ -102,11 +101,19 @@ struct OutputPaths {
|
||||
}
|
||||
|
||||
fn prepare_output_path(path: &Path, config: &Config) -> Result<OutputPaths> {
|
||||
let suffix = path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.map(|ext| format!(".{}", ext))
|
||||
.unwrap_or_else(|| ".tmp".to_string());
|
||||
|
||||
if config.repair.output_dir.is_empty() {
|
||||
let parent = path
|
||||
.parent()
|
||||
.context("Input file has no parent directory")?;
|
||||
let temp = NamedTempFile::new_in(parent)
|
||||
let temp = tempfile::Builder::new()
|
||||
.suffix(&suffix)
|
||||
.tempfile_in(parent)
|
||||
.with_context(|| format!("Failed to create temp file in {}", parent.display()))?;
|
||||
let temp_path = temp.path().to_path_buf();
|
||||
temp.keep()?;
|
||||
@@ -126,9 +133,10 @@ fn prepare_output_path(path: &Path, config: &Config) -> Result<OutputPaths> {
|
||||
.to_os_string();
|
||||
|
||||
let final_path = output_dir.join(file_name);
|
||||
let temp = NamedTempFile::new_in(&output_dir).with_context(|| {
|
||||
format!("Failed to create temp file in {}", output_dir.display())
|
||||
})?;
|
||||
let temp = tempfile::Builder::new()
|
||||
.suffix(&suffix)
|
||||
.tempfile_in(&output_dir)
|
||||
.with_context(|| format!("Failed to create temp file in {}", output_dir.display()))?;
|
||||
let temp_path = temp.path().to_path_buf();
|
||||
temp.keep()?;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::config::ScanDepth;
|
||||
use crate::rules::RuleSet;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -13,12 +14,18 @@ pub struct DecodeOutput {
|
||||
pub early_stop: bool,
|
||||
}
|
||||
|
||||
pub fn run_decode(path: &Path, ffmpeg_path: &str, ruleset: &RuleSet) -> Result<DecodeOutput> {
|
||||
pub fn run_decode(
|
||||
path: &Path,
|
||||
ffmpeg_path: &str,
|
||||
ruleset: &RuleSet,
|
||||
depth: ScanDepth,
|
||||
) -> Result<DecodeOutput> {
|
||||
let mut child = Command::new(ffmpeg_path)
|
||||
.arg("-v")
|
||||
.arg("error")
|
||||
.arg("-i")
|
||||
.arg(path)
|
||||
.args(depth_args(depth))
|
||||
.arg("-f")
|
||||
.arg("null")
|
||||
.arg("-")
|
||||
@@ -58,6 +65,14 @@ pub fn run_decode(path: &Path, ffmpeg_path: &str, ruleset: &RuleSet) -> Result<D
|
||||
})
|
||||
}
|
||||
|
||||
fn depth_args(depth: ScanDepth) -> Vec<&'static str> {
|
||||
match depth {
|
||||
ScanDepth::Quick => vec!["-t", "5"],
|
||||
ScanDepth::Standard => vec!["-t", "30"],
|
||||
ScanDepth::Deep => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn should_stop(line: &str, ruleset: &RuleSet) -> bool {
|
||||
for rule in &ruleset.rules {
|
||||
if !rule.rule.stop_scan {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config::{Config, ScanDepth};
|
||||
use crate::rules::{build_context, RuleMatch, RuleSet};
|
||||
|
||||
mod decode;
|
||||
@@ -14,10 +14,19 @@ pub use types::{Issue, ProbeData, ScanOutcome, ScanRequest};
|
||||
pub fn scan_file(path: &Path, config: &Config, ruleset: &RuleSet) -> Result<ScanOutcome> {
|
||||
let probe = ffprobe::run_ffprobe(path, &config.ffprobe_path)?;
|
||||
|
||||
let decode = decode::run_decode(path, &config.ffmpeg_path, ruleset)?;
|
||||
let mut decode = decode::run_decode(path, &config.ffmpeg_path, ruleset, config.scan.depth)?;
|
||||
|
||||
let context = build_context(&probe);
|
||||
let matches = ruleset.match_lines(&decode.lines, &context);
|
||||
let mut matches = ruleset.match_lines(&decode.lines, &context);
|
||||
|
||||
if config.scan.auto_escalate
|
||||
&& config.scan.depth != ScanDepth::Deep
|
||||
&& !decode.early_stop
|
||||
&& !matches.is_empty()
|
||||
{
|
||||
decode = decode::run_decode(path, &config.ffmpeg_path, ruleset, ScanDepth::Deep)?;
|
||||
matches = ruleset.match_lines(&decode.lines, &context);
|
||||
}
|
||||
|
||||
let issues = matches
|
||||
.iter()
|
||||
|
||||
80
vid-repair-core/tests/fix.rs
Normal file
80
vid-repair-core/tests/fix.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use tempfile::tempdir;
|
||||
|
||||
use vid_repair_core::config::Config;
|
||||
use vid_repair_core::fix::{FixAction, FixKind, FixPlan};
|
||||
use vid_repair_core::rules::RuleSet;
|
||||
use vid_repair_core::scan::scan_file;
|
||||
use vid_repair_core::{fix, ConfigOverrides};
|
||||
|
||||
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 remux_fix_succeeds_on_clean_fixture() {
|
||||
if !command_available("ffmpeg") || !command_available("ffprobe") {
|
||||
eprintln!("ffmpeg/ffprobe not available; skipping fix test");
|
||||
return;
|
||||
}
|
||||
|
||||
let fixture = fixture_dir().join("clean.mp4");
|
||||
if !fixture.exists() {
|
||||
eprintln!("fixture not found: {}; skipping", fixture.display());
|
||||
return;
|
||||
}
|
||||
|
||||
let temp = tempdir().expect("tempdir");
|
||||
|
||||
let mut config = Config::default();
|
||||
let mut overrides = ConfigOverrides::default();
|
||||
overrides.output_dir = Some(temp.path().to_string_lossy().to_string());
|
||||
config.apply_overrides(&overrides);
|
||||
|
||||
let ruleset_dir = fixture_dir()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("rulesets");
|
||||
let ruleset = RuleSet::load_from_dir(&ruleset_dir).expect("ruleset load");
|
||||
|
||||
let plan = FixPlan {
|
||||
policy: config.repair.policy,
|
||||
recommended: Some(FixKind::Remux),
|
||||
actions: vec![FixAction {
|
||||
kind: FixKind::Remux,
|
||||
command: Vec::new(),
|
||||
destructive: true,
|
||||
}],
|
||||
blocked_reason: None,
|
||||
};
|
||||
|
||||
let outcome = fix::executor::apply_fix(&fixture, &plan, &config, &ruleset)
|
||||
.expect("apply fix");
|
||||
|
||||
assert!(outcome.success, "Expected remux to succeed");
|
||||
let output_path = outcome.output_path.expect("output path");
|
||||
|
||||
let scan = scan_file(PathBuf::from(output_path).as_path(), &config, &ruleset)
|
||||
.expect("scan output");
|
||||
assert!(scan.issues.is_empty(), "Output should be clean");
|
||||
}
|
||||
Reference in New Issue
Block a user