use super::*;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
include!(concat!(env!("OUT_DIR"), "/fixer_tests.rs"));
fn issues_match_with_wildcards(expected: &[LintianIssue], actual: &[LintianIssue]) -> bool {
if expected.len() != actual.len() {
return false;
}
for (exp, act) in expected.iter().zip(actual.iter()) {
if exp.package != act.package || exp.package_type != act.package_type || exp.tag != act.tag
{
return false;
}
match (&exp.info, &act.info) {
(Some(exp_info), Some(act_info)) => {
if !crate::lintian_overrides::info_matches(exp_info, act_info) {
return false;
}
}
(None, None) => {}
_ => return false,
}
}
true
}
#[cfg(all(feature = "pgp", feature = "bts", feature = "upstream"))]
#[test]
fn test_all_test_dirs_have_matching_fixers() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let tests_dir = Path::new(manifest_dir).join("tests");
let all_fixers = all_lintian_fixers();
let fixer_names: std::collections::HashSet<String> =
all_fixers.map(|f| f.name().to_string()).collect();
let test_dirs = std::fs::read_dir(&tests_dir).expect("Failed to read tests directory");
let mut tests_without_fixers = Vec::new();
for entry in test_dirs {
let entry = entry.expect("Failed to read directory entry");
let path = entry.path();
if path.is_dir() {
let test_name = entry.file_name().to_string_lossy().to_string();
if test_name == "slow" || test_name.starts_with('.') {
continue;
}
if !fixer_names.contains(&test_name) {
tests_without_fixers.push(test_name);
}
}
}
if !tests_without_fixers.is_empty() {
panic!(
"The following test directories have no matching fixers in all_lintian_fixers():\n{}",
tests_without_fixers.join("\n")
);
}
}
#[test]
fn test_all_fixers_handle_missing_source_stanza() {
let fixers = all_lintian_fixers();
let mut failures = Vec::new();
for fixer in fixers {
let td = tempfile::tempdir().unwrap();
let testdir = td.path().join("testdir");
std::fs::create_dir_all(testdir.join("debian")).unwrap();
std::fs::write(
testdir.join("debian/control"),
"Package: test-binary\nArchitecture: any\nDescription: A test package\n",
)
.unwrap();
std::fs::write(
testdir.join("debian/changelog"),
"test-package (1.0-1) unstable; urgency=low\n\n * Initial release.\n\n -- Test <test@test.com> Mon, 01 Jan 2024 00:00:00 +0000\n",
)
.unwrap();
let preferences = FixerPreferences {
compat_release: Some("sid".to_string()),
minimum_certainty: None,
net_access: Some(false),
trust_package: Some(false),
opinionated: Some(false),
..Default::default()
};
let current_version: debversion::Version = "1.0-1".parse().unwrap();
let ws = debian_workspace::fs_workspace::FsWorkspace::new(
&testdir,
Some("test-package".into()),
Some(current_version.clone()),
);
let result = crate::detector::detect_and_fix(fixer.as_ref(), &ws, &preferences);
match result {
Ok(_) => {} Err(FixerError::NoChanges) => {} Err(FixerError::NoChangesAfterOverrides(_)) => {}
Err(FixerError::NotDebianPackage(_)) => {}
Err(FixerError::NotCertainEnough(..)) => {}
Err(FixerError::FormattingUnpreservable(_)) => {}
Err(FixerError::GeneratedFile(_)) => {}
Err(FixerError::Other(_)) => {} Err(FixerError::Io(_)) => {} Err(FixerError::ScriptNotFound(_)) => {} Err(e) => {
failures.push(format!("Fixer '{}' crashed: {:?}", fixer.name(), e));
}
}
}
if !failures.is_empty() {
panic!(
"The following fixers crashed on a control file without a Source stanza:\n{}",
failures.join("\n")
);
}
}
fn compare_trees(expected: &Path, actual: &Path) -> Vec<String> {
let mut differences = Vec::new();
compare_dir(expected, actual, Path::new(""), &mut differences);
differences
}
fn tree_entries(dir: &Path) -> Vec<std::ffi::OsString> {
let mut names: Vec<std::ffi::OsString> = match std::fs::read_dir(dir) {
Ok(rd) => rd
.map(|e| e.expect("Failed to read directory entry").file_name())
.filter(|name| !name.to_string_lossy().ends_with('~'))
.collect(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
Err(e) => panic!("Error reading {}: {}", dir.display(), e),
};
names.sort();
names
}
fn compare_dir(expected: &Path, actual: &Path, rel: &Path, differences: &mut Vec<String>) {
let expected_names = tree_entries(&expected.join(rel));
let actual_names = tree_entries(&actual.join(rel));
let mut all: Vec<&std::ffi::OsString> =
expected_names.iter().chain(actual_names.iter()).collect();
all.sort();
all.dedup();
for name in all {
let child = rel.join(name);
let in_expected = expected_names.contains(name);
let in_actual = actual_names.contains(name);
match (in_expected, in_actual) {
(true, false) => differences.push(format!("Only in expected: {}", child.display())),
(false, true) => differences.push(format!("Only in actual: {}", child.display())),
(true, true) => compare_entry(expected, actual, &child, differences),
(false, false) => unreachable!(),
}
}
}
fn compare_entry(expected: &Path, actual: &Path, rel: &Path, differences: &mut Vec<String>) {
let expected_path = expected.join(rel);
let actual_path = actual.join(rel);
let expected_meta = std::fs::symlink_metadata(&expected_path)
.unwrap_or_else(|e| panic!("Error stat-ing {}: {}", expected_path.display(), e));
let actual_meta = std::fs::symlink_metadata(&actual_path)
.unwrap_or_else(|e| panic!("Error stat-ing {}: {}", actual_path.display(), e));
let expected_type = expected_meta.file_type();
let actual_type = actual_meta.file_type();
if expected_type.is_symlink() || actual_type.is_symlink() {
if !expected_type.is_symlink() || !actual_type.is_symlink() {
differences.push(format!(
"{}: symlink vs non-symlink mismatch",
rel.display()
));
return;
}
let expected_target = std::fs::read_link(&expected_path).unwrap();
let actual_target = std::fs::read_link(&actual_path).unwrap();
if expected_target != actual_target {
differences.push(format!(
"{}: symlink target differs ({} vs {})",
rel.display(),
expected_target.display(),
actual_target.display()
));
}
return;
}
if expected_type.is_dir() != actual_type.is_dir() {
differences.push(format!("{}: directory vs file mismatch", rel.display()));
return;
}
if expected_type.is_dir() {
compare_dir(expected, actual, rel, differences);
return;
}
let expected_bytes = std::fs::read(&expected_path).unwrap();
let actual_bytes = std::fs::read(&actual_path).unwrap();
if expected_bytes != actual_bytes {
differences.push(format!(
"{}: content differs\n--- expected\n{}\n--- actual\n{}",
rel.display(),
String::from_utf8_lossy(&expected_bytes),
String::from_utf8_lossy(&actual_bytes),
));
}
}
fn run_fixer_testcase(fixer_name: &str, test_name: &str, path: &Path) {
let td = tempfile::tempdir().unwrap();
let indir = path.join("in");
let outdir = path.join("out");
let testdir = td.path().join("testdir");
std::fs::create_dir(&testdir).unwrap();
let mut options = fs_extra::dir::CopyOptions::new();
options.copy_inside = true;
options.content_only = true;
fs_extra::dir::copy(indir, &testdir, &options).unwrap();
let xfail_path = path.join("xfail");
match std::fs::read_to_string(&xfail_path) {
Ok(s) => {
eprintln!(
"Skipping test {} because it is expected to fail: {}",
test_name, s
);
return;
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => panic!("Error reading {}: {}", xfail_path.display(), e),
}
let mut preferences = FixerPreferences {
compat_release: Some("sid".to_string()),
minimum_certainty: None, net_access: Some(false), trust_package: Some(false), opinionated: Some(false), ..Default::default()
};
let mut current_version_override = None;
let mut extra_env = std::collections::HashMap::new();
let env_path = path.join("env");
match std::fs::File::open(&env_path) {
Ok(f) => {
use std::io::BufRead;
let br = std::io::BufReader::new(f);
for line in br.lines() {
let line = line.unwrap();
if let Some((name, value)) = line.split_once('=') {
match name {
"MINIMUM_CERTAINTY" => {
preferences.minimum_certainty = Some(match value {
"certain" => Certainty::Certain,
"confident" => Certainty::Confident,
"likely" => Certainty::Likely,
"possible" => Certainty::Possible,
_ => panic!("Unknown certainty value: {}", value),
});
}
"COMPAT_RELEASE" => {
preferences.compat_release = Some(value.to_string());
}
"UPGRADE_RELEASE" => {
preferences.upgrade_release = Some(value.to_string());
}
"OPINIONATED" => {
preferences.opinionated = Some(value == "yes");
}
"NET_ACCESS" => {
preferences.net_access = Some(match value {
"allow" => true,
"disallow" => false,
_ => panic!(
"Unknown NET_ACCESS value: {} (must be 'allow' or 'disallow')",
value
),
});
}
"TRUST_PACKAGE" => {
preferences.trust_package = Some(match value {
"true" => true,
"false" => false,
_ => panic!(
"Unknown TRUST_PACKAGE value: {} (must be 'true' or 'false')",
value
),
});
}
"CURRENT_VERSION" => {
current_version_override = Some(value.parse().unwrap());
}
_ => {
extra_env.insert(name.to_string(), value.to_string());
}
}
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => panic!("Error reading {}: {}", env_path.display(), e),
}
if !extra_env.is_empty() {
preferences.extra_env = Some(extra_env);
}
let cl_path = testdir.join("debian/changelog");
let current_version = if let Some(version) = current_version_override {
version
} else {
match std::fs::File::open(&cl_path) {
Ok(f) => {
match ChangeLog::read_relaxed(f) {
Ok(cl) => {
let first_entry = cl.iter().next().unwrap();
let version = first_entry.version().unwrap();
if first_entry.distributions().as_deref().unwrap() == vec!["UNRELEASED"] {
version
} else {
let mut version = version;
version.increment_debian();
version
}
}
Err(_) => {
"1.0-1".parse().unwrap()
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => "1.0-1".parse().unwrap(),
Err(e) => panic!("Error reading {}: {}", cl_path.display(), e),
}
};
let fixers = all_lintian_fixers();
let Some(fixer) = fixers.into_iter().find(|f| f.name() == fixer_name) else {
eprintln!(
"Skipping test {}: fixer '{}' is not built",
test_name, fixer_name
);
return;
};
let ws = debian_workspace::fs_workspace::FsWorkspace::new(
&testdir,
Some("test-package".into()),
Some(current_version.clone()),
);
let (actual_result, exit_code) =
match crate::detector::detect_and_fix(fixer.as_ref(), &ws, &preferences) {
Ok(result) => (Some(result), 0),
Err(FixerError::NoChanges) => {
eprintln!("Fixer returned NoChanges for test {}", test_name);
(None, 1) }
Err(FixerError::NoChangesAfterOverrides(_)) => {
eprintln!(
"Fixer returned NoChangesAfterOverrides for test {}",
test_name
);
(None, 1) }
Err(e) => {
if let FixerError::ScriptFailed {
path,
exit_code,
stderr,
} = &e
{
eprintln!(
"Script failed: {} (exit code: {})",
path.display(),
exit_code
);
if !stderr.is_empty() {
eprintln!("Stderr:\n{}", stderr);
}
}
panic!("Fixer error: {:?}", e);
}
};
if exit_code != 0 && exit_code != 1 {
panic!("Test {} failed with exit code {}", test_name, exit_code);
}
if exit_code == 0 {
let expected_dir = if outdir.is_symlink() {
path.join(std::fs::read_link(&outdir).unwrap())
} else {
outdir.clone()
};
let differences = compare_trees(&expected_dir, &testdir);
if !differences.is_empty() {
eprintln!("Differences:\n{}", differences.join("\n"));
panic!("Test {} failed", test_name);
}
}
let check_message = !outdir.is_symlink() || outdir.read_link().unwrap() != PathBuf::from("in");
let message_path = path.join("message");
match std::fs::read_to_string(&message_path) {
Ok(expected_message) => {
let expected_result = match parse_script_fixer_output(&expected_message) {
Ok(result) => result,
Err(e) => {
eprintln!("Failed to parse expected message as FixerResult: {:?}", e);
eprintln!("Expected message:\n{}", expected_message);
panic!(
"Test {} failed - invalid expected message format",
test_name
);
}
};
let actual_result = actual_result
.as_ref()
.expect("Expected a FixerResult but fixer returned NoChanges");
if expected_result.description != actual_result.description {
eprintln!("Expected description: {:?}", expected_result.description);
eprintln!("Got description: {:?}", actual_result.description);
panic!("Test {} failed - description mismatch", test_name);
}
let expected_tags: HashSet<&str> =
expected_result.fixed_lintian_tags().into_iter().collect();
let actual_tags: HashSet<&str> =
actual_result.fixed_lintian_tags().into_iter().collect();
if expected_tags != actual_tags {
eprintln!("Expected tags: {:?}", expected_tags);
eprintln!("Got tags: {:?}", actual_tags);
panic!("Test {} failed - tags mismatch", test_name);
}
if !issues_match_with_wildcards(
&expected_result.fixed_lintian_issues,
&actual_result.fixed_lintian_issues,
) {
eprintln!(
"Expected issues: {:?}",
expected_result.fixed_lintian_issues
);
eprintln!("Got issues: {:?}", actual_result.fixed_lintian_issues);
eprintln!("\nExpected message format:");
eprintln!("Fixed-Lintian-Issues:");
for issue in &actual_result.fixed_lintian_issues {
eprintln!(" {}", issue);
}
panic!("Test {} failed - issue details mismatch", test_name);
}
if expected_result.certainty != actual_result.certainty {
eprintln!("Expected certainty: {:?}", expected_result.certainty);
eprintln!("Got certainty: {:?}", actual_result.certainty);
panic!("Test {} failed - certainty mismatch", test_name);
}
if expected_result.patch_name != actual_result.patch_name {
eprintln!("Expected patch_name: {:?}", expected_result.patch_name);
eprintln!("Got patch_name: {:?}", actual_result.patch_name);
panic!("Test {} failed - patch_name mismatch", test_name);
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
if check_message {
panic!("No message file found for test {}", test_name);
}
}
Err(e) => panic!("Error reading {}: {}", message_path.display(), e),
}
}