use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use libmagic_rs::MagicDatabase;
use libmagic_rs::parser::{MagicFileFormat, detect_format};
#[derive(Debug, Clone)]
struct TestResult {
test_file: PathBuf,
status: TestStatus,
#[allow(dead_code)]
expected_output: String,
#[allow(dead_code)]
actual_output: String,
error: Option<String>,
errors: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
enum TestStatus {
Pass,
Fail,
Error,
}
struct CompatibilityTestRunner {
test_dir: PathBuf,
rmagic_binary: PathBuf,
}
impl CompatibilityTestRunner {
fn new() -> Result<Self, Box<dyn std::error::Error>> {
let test_dir = PathBuf::from("third_party/tests");
let rmagic_binary = find_rmagic_binary()?;
if !test_dir.exists() {
return Err(
"Compatibility test files not found. Ensure third_party/tests directory exists."
.into(),
);
}
Ok(Self {
test_dir,
rmagic_binary,
})
}
fn find_test_files(&self) -> Vec<(PathBuf, PathBuf)> {
let mut test_files = Vec::new();
if let Ok(entries) = fs::read_dir(&self.test_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("testfile") {
let result_file = path.with_extension("result");
if result_file.exists() {
test_files.push((path, result_file));
}
}
}
}
test_files.sort_unstable_by_key(|(input_path, _)| input_path.clone());
test_files
}
fn run_rmagic(&self, test_file: &Path) -> Result<String, Box<dyn std::error::Error>> {
let output = Command::new(&self.rmagic_binary)
.arg("--use-builtin")
.arg(test_file)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("rmagic failed: {}", stderr).into());
}
let full_output = String::from_utf8_lossy(&output.stdout).trim().to_string();
if let Some((_filename, description)) = full_output.split_once(':') {
Ok(description.trim().to_string())
} else {
Ok(full_output)
}
}
fn normalize_output(&self, output: &str) -> String {
output
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n")
}
fn run_single_test(&self, test_file: PathBuf, result_file: PathBuf) -> TestResult {
let expected_output = match fs::read_to_string(&result_file) {
Ok(content) => content.trim().to_string(),
Err(e) => {
return TestResult {
test_file: test_file.clone(),
status: TestStatus::Error,
expected_output: String::new(),
actual_output: String::new(),
error: Some(format!("Failed to read result file: {}", e)),
errors: vec![],
};
}
};
let actual_output = match self.run_rmagic(&test_file) {
Ok(output) => output,
Err(e) => {
return TestResult {
test_file: test_file.clone(),
status: TestStatus::Error,
expected_output,
actual_output: String::new(),
error: Some(format!("rmagic failed: {}", e)),
errors: vec![],
};
}
};
let normalized_expected = self.normalize_output(&expected_output);
let normalized_actual = self.normalize_output(&actual_output);
let (status, errors) = if normalized_expected == normalized_actual {
(TestStatus::Pass, vec![])
} else {
let error_message = format!(
"Test failed for {}:\nExpected: {}\nActual: {}",
test_file.display(),
expected_output,
actual_output
);
(TestStatus::Fail, vec![error_message])
};
TestResult {
test_file,
status,
expected_output,
actual_output,
error: None,
errors,
}
}
fn run_all_tests(&self) -> Vec<TestResult> {
let test_files = self.find_test_files();
let mut results = Vec::new();
println!("Found {} test files", test_files.len());
for (test_file, result_file) in test_files {
let result = self.run_single_test(test_file, result_file);
results.push(result);
}
results
}
fn generate_report(&self, results: &[TestResult]) -> HashMap<String, usize> {
let mut summary = HashMap::new();
summary.insert("total".to_string(), results.len());
summary.insert("passed".to_string(), 0);
summary.insert("failed".to_string(), 0);
summary.insert("errors".to_string(), 0);
for result in results {
match result.status {
TestStatus::Pass => {
*summary.get_mut("passed").unwrap() += 1;
}
TestStatus::Fail => {
*summary.get_mut("failed").unwrap() += 1;
}
TestStatus::Error => {
*summary.get_mut("errors").unwrap() += 1;
}
}
}
summary
}
}
fn find_rmagic_binary() -> Result<PathBuf, Box<dyn std::error::Error>> {
let cargo_bin_path = assert_cmd::cargo::cargo_bin!("rmagic");
if cargo_bin_path.exists() {
return Ok(cargo_bin_path.to_path_buf());
}
let candidates = [
"target/release/rmagic",
"target/release/rmagic.exe",
"target/debug/rmagic",
"target/debug/rmagic.exe",
];
candidates
.iter()
.find(|c| Path::new(c).exists())
.map(PathBuf::from)
.ok_or_else(|| "rmagic binary not found. Please build the project first.".into())
}
#[test]
#[ignore] fn test_compatibility_with_original_libmagic() {
let runner = match CompatibilityTestRunner::new() {
Ok(runner) => runner,
Err(e) => {
println!("Skipping compatibility tests: {}", e);
return;
}
};
let results = runner.run_all_tests();
let summary = runner.generate_report(&results);
println!("\n=== COMPATIBILITY TEST SUMMARY ===");
println!("Total tests: {}", summary["total"]);
println!("Passed: {}", summary["passed"]);
println!("Failed: {}", summary["failed"]);
println!("Errors: {}", summary["errors"]);
let failed_tests: Vec<_> = results
.iter()
.filter(|r| r.status == TestStatus::Fail)
.collect();
if !failed_tests.is_empty() {
println!("\n=== FAILED TESTS ===");
for result in failed_tests {
println!("FAIL {}", result.test_file.display());
for error in &result.errors {
println!(" {}", error);
}
println!();
}
}
let error_tests: Vec<_> = results
.iter()
.filter(|r| r.status == TestStatus::Error)
.collect();
if !error_tests.is_empty() {
println!("\n=== ERROR TESTS ===");
for result in error_tests {
println!("ERROR {}", result.test_file.display());
if let Some(error) = &result.error {
println!(" Error: {}", error);
}
println!();
}
}
assert!(summary["total"] > 0, "No compatibility tests found");
if summary["errors"] > 0 {
panic!("{} tests had errors", summary["errors"]);
}
println!("\nCompatibility tests completed successfully!");
}
#[test]
fn test_magic_database_loading() {
let magic_file = Path::new("third_party/magic.mgc");
if !magic_file.exists() {
println!("Skipping magic database test: third_party/magic.mgc not found");
return;
}
match detect_format(magic_file) {
Ok(MagicFileFormat::Binary) => {
println!("Skipping magic database test: binary .mgc not supported");
return;
}
Ok(MagicFileFormat::Text | MagicFileFormat::Directory) => {}
Err(e) => {
println!("Skipping magic database test: failed to detect format: {e}");
return;
}
}
let db = MagicDatabase::load_from_file(magic_file);
assert!(db.is_ok(), "Failed to load magic database");
}
#[test]
fn test_rmagic_binary() {
let binary = find_rmagic_binary();
assert!(binary.is_ok(), "rmagic binary not found");
let binary_path = binary.unwrap();
assert!(binary_path.exists(), "rmagic binary does not exist");
let output = Command::new(&binary_path)
.output()
.expect("Failed to run rmagic binary");
assert!(
!output.status.success(),
"rmagic should fail with missing arguments"
);
}
#[test]
fn test_compatibility_files_available() {
let test_dir = Path::new("third_party/tests");
if !test_dir.exists() {
println!("Skipping compatibility files test: third_party/tests not found");
return;
}
let runner = CompatibilityTestRunner::new().expect("Failed to create test runner");
let test_files = runner.find_test_files();
assert!(!test_files.is_empty(), "No compatibility test files found");
println!("Found {} compatibility test files", test_files.len());
}