use std::path::PathBuf;
use super::deterministic::work_for;
use super::deterministic_staged::ruff_fixture;
use crate::analysis::result::FailureReason;
use crate::cli::check::deterministic;
use crate::test_support::write_executable;
fn hadolint_fixture(root: &std::path::Path, name: &str) -> PathBuf {
std::fs::write(root.join(".hadolint.yaml"), "").expect("hadolint config");
let file = root.join(name);
std::fs::create_dir_all(file.parent().unwrap()).expect("dockerfile dir");
std::fs::write(&file, "FROM scratch\n").expect("dockerfile");
file
}
async fn hadolint_refusal(
root: &std::path::Path,
file: &std::path::Path,
uncommitted: &[&str],
) -> Option<Vec<PathBuf>> {
let mut work = work_for(&[file.to_path_buf()]);
work.uncommitted = uncommitted.iter().map(PathBuf::from).collect();
let (_findings, failures, _compiled) = deterministic::run(&work, root).await;
match failures.get(file) {
Some(FailureReason::UncommittedChanges { tool, paths }) => {
assert_eq!(tool, "hadolint");
Some(paths.clone())
}
_ => None,
}
}
#[tokio::test]
async fn a_tool_reading_only_its_files_refuses_its_own_file_and_config() {
let dir = tempfile::tempdir().expect("tempdir");
let file = hadolint_fixture(dir.path(), "Dockerfile");
assert_eq!(
hadolint_refusal(dir.path(), &file, &["Dockerfile"]).await,
Some(vec![PathBuf::from("Dockerfile")])
);
assert_eq!(
hadolint_refusal(dir.path(), &file, &[".hadolint.yaml"]).await,
Some(vec![PathBuf::from(".hadolint.yaml")])
);
}
#[tokio::test]
async fn a_symlinked_file_or_marker_refuses_the_tool_when_its_target_differs() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
hadolint_fixture(root, "real/Dockerfile");
std::fs::create_dir_all(root.join("svc")).expect("svc dir");
let link = root.join("svc/Dockerfile");
std::os::unix::fs::symlink("../real/Dockerfile", &link).expect("dockerfile link");
assert_eq!(
hadolint_refusal(root, &link, &["real/Dockerfile"]).await,
Some(vec![PathBuf::from("real/Dockerfile")])
);
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
hadolint_fixture(root, "real/Dockerfile");
std::fs::write(root.join("real/.hadolint.yaml"), "").expect("nearer config");
std::os::unix::fs::symlink("real", root.join("svc")).expect("directory link");
let file = root.join("svc/Dockerfile");
assert_eq!(
hadolint_refusal(root, &file, &["real/.hadolint.yaml"]).await,
Some(vec![PathBuf::from("real/.hadolint.yaml")])
);
}
#[tokio::test]
async fn a_marker_link_to_a_deleted_target_refuses_the_tool() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let file = hadolint_fixture(root, "Dockerfile");
std::fs::remove_file(root.join(".hadolint.yaml")).expect("remove the marker");
std::os::unix::fs::symlink("shared/hadolint.yaml", root.join(".hadolint.yaml"))
.expect("dangling marker link");
assert_eq!(
hadolint_refusal(root, &file, &["shared/hadolint.yaml"]).await,
Some(vec![PathBuf::from("shared/hadolint.yaml")])
);
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let file = hadolint_fixture(root, "Dockerfile");
std::fs::remove_file(root.join(".hadolint.yaml")).expect("remove the marker");
std::os::unix::fs::symlink("chain.yaml", root.join(".hadolint.yaml")).expect("first link");
std::os::unix::fs::symlink("shared/hadolint.yaml", root.join("chain.yaml"))
.expect("second, dangling link");
assert_eq!(
hadolint_refusal(root, &file, &["shared/hadolint.yaml"]).await,
Some(vec![PathBuf::from("shared/hadolint.yaml")])
);
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let file = hadolint_fixture(root, "pkg/Dockerfile");
std::os::unix::fs::symlink("../shared/pkg.yaml", root.join("pkg/.hadolint.yaml"))
.expect("dangling nearer marker link");
assert_eq!(
hadolint_refusal(root, &file, &["shared/pkg.yaml"]).await,
Some(vec![PathBuf::from("shared/pkg.yaml")])
);
}
async fn index_only_refusal(
root: &std::path::Path,
file: &std::path::Path,
index_only: &[&str],
) -> Option<Vec<PathBuf>> {
let mut work = work_for(&[file.to_path_buf()]);
work.index_only = index_only.iter().map(PathBuf::from).collect();
let (_findings, failures, _compiled) = deterministic::run(&work, root).await;
match failures.get(file) {
Some(FailureReason::UncommittedChanges { paths, .. }) => Some(paths.clone()),
_ => None,
}
}
#[tokio::test]
async fn a_marker_a_sparse_checkout_left_out_refuses_the_tool() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let file = hadolint_fixture(root, "pkg/Dockerfile");
assert_eq!(
index_only_refusal(root, &file, &["pkg/.hadolint.yaml"]).await,
Some(vec![PathBuf::from("pkg/.hadolint.yaml")])
);
std::fs::remove_file(root.join(".hadolint.yaml")).expect("remove the root marker");
assert_eq!(
index_only_refusal(root, &file, &[".hadolint.yaml"]).await,
Some(vec![PathBuf::from(".hadolint.yaml")])
);
let dir = tempfile::tempdir().expect("tempdir");
let argv = ruff_fixture(dir.path());
let a = dir.path().join("a.py");
std::fs::write(&a, "a = 1\n").expect("a.py");
let mut work = work_for(std::slice::from_ref(&a));
work.index_only = [PathBuf::from("lib.py")].into_iter().collect();
let (_findings, failures, _compiled) = deterministic::run(&work, dir.path()).await;
assert!(
failures.is_empty(),
"no tool reads an index-only file, got {failures:?}"
);
assert!(argv.exists(), "ruff runs beside it");
}
#[tokio::test]
async fn a_batch_file_a_sparse_checkout_left_out_refuses_its_tool() {
let dir = tempfile::tempdir().expect("tempdir");
let file = hadolint_fixture(dir.path(), "Dockerfile");
assert_eq!(
index_only_refusal(dir.path(), &file, &["Dockerfile"]).await,
Some(vec![PathBuf::from("Dockerfile")])
);
let dir = tempfile::tempdir().expect("tempdir");
let _argv = ruff_fixture(dir.path());
let a = dir.path().join("a.py");
std::fs::write(&a, "a = 1\n").expect("a.py");
assert_eq!(
index_only_refusal(dir.path(), &a, &["a.py"]).await,
Some(vec![PathBuf::from("a.py")])
);
}
#[tokio::test]
async fn a_nearer_marker_missing_from_the_working_tree_refuses_the_tool() {
let dir = tempfile::tempdir().expect("tempdir");
let file = hadolint_fixture(dir.path(), "pkg/sub/Dockerfile");
assert_eq!(
hadolint_refusal(dir.path(), &file, &["pkg/.hadolint.yaml"]).await,
Some(vec![PathBuf::from("pkg/.hadolint.yaml")])
);
}
#[tokio::test]
async fn a_tool_reading_only_its_files_runs_beside_an_uncommitted_file_of_its_language() {
let dir = tempfile::tempdir().expect("tempdir");
let file = hadolint_fixture(dir.path(), "Dockerfile");
assert_eq!(
hadolint_refusal(dir.path(), &file, &["other/Dockerfile"]).await,
None
);
}
#[tokio::test]
async fn a_config_marker_missing_from_the_working_tree_fails_the_file() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let bin = root.join("venv/bin/ruff");
std::fs::create_dir_all(bin.parent().unwrap()).expect("bin dir");
let argv = root.join("argv.txt");
write_executable(
&bin,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> {}\nprintf '%s' '[]'\n",
argv.display()
),
);
let a = root.join("a.py");
std::fs::write(&a, "a = 1\n").expect("a.py");
let mut work = work_for(std::slice::from_ref(&a));
work.uncommitted = [PathBuf::from("pyproject.toml")].into_iter().collect();
let (_findings, failures, _compiled) = deterministic::run(&work, root).await;
assert!(
!argv.exists(),
"ruff never ran, so this is not a run refusal"
);
match failures.get(&a) {
Some(FailureReason::UncommittedChanges { tool, paths }) => {
assert_eq!(tool, "ruff");
assert_eq!(paths, &[PathBuf::from("pyproject.toml")]);
}
other => panic!("expected UncommittedChanges for {a:?}, got {other:?}"),
}
}
#[tokio::test]
async fn a_nested_config_marker_missing_from_the_working_tree_fails_the_file() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let a = root.join("A.java");
std::fs::write(&a, "class A {}\n").expect("A.java");
let mut work = work_for(std::slice::from_ref(&a));
work.uncommitted = [PathBuf::from("config/checkstyle/checkstyle.xml")]
.into_iter()
.collect();
let (_findings, failures, _compiled) = deterministic::run(&work, root).await;
match failures.get(&a) {
Some(FailureReason::UncommittedChanges { tool, paths }) => {
assert_eq!(tool, "checkstyle");
assert_eq!(paths, &[PathBuf::from("config/checkstyle/checkstyle.xml")]);
}
other => panic!("expected UncommittedChanges for {a:?}, got {other:?}"),
}
}
#[tokio::test]
async fn an_uncommitted_marker_beside_the_files_directory_does_not_fail_it() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::create_dir_all(root.join("pkg")).expect("pkg dir");
let a = root.join("pkg/a.py");
std::fs::write(&a, "a = 1\n").expect("a.py");
let mut work = work_for(std::slice::from_ref(&a));
work.uncommitted = [PathBuf::from("other/pyproject.toml")]
.into_iter()
.collect();
let (_findings, failures, _compiled) = deterministic::run(&work, root).await;
assert!(
failures.is_empty(),
"a marker outside the file's ancestry must not fail it, got {failures:?}"
);
}