use std::collections::BTreeMap;
use std::path::PathBuf;
use crate::analysis::result::FailureReason;
use crate::cli::check::deterministic;
use crate::cli::check::input::Work;
use crate::diff::hunks::Hunk;
use crate::test_support::write_executable;
fn work_for(paths: &[PathBuf]) -> Work {
Work {
lint_only: Vec::new(),
by_file: paths
.iter()
.map(|p| vec![Hunk::whole_file(p.clone(), "x = 1\n")])
.collect(),
read_failures: BTreeMap::new(),
}
}
#[tokio::test]
async fn two_python_files_invoke_their_tool_once_not_twice() {
let dir = tempfile::tempdir().expect("tempdir");
let bin = dir.path().join("venv/bin/ruff");
std::fs::create_dir_all(bin.parent().unwrap()).expect("bin dir");
let counter = dir.path().join("counter.txt");
let counter_str = counter.to_string_lossy().into_owned();
write_executable(
&bin,
format!("#!/bin/sh\nprintf '%s\\n' called >> {counter_str}\nprintf '%s' '[]'\n"),
);
std::fs::write(dir.path().join("pyproject.toml"), "").expect("pyproject");
let a = dir.path().join("a.py");
let b = dir.path().join("b.py");
std::fs::write(&a, "a = 1\n").expect("a.py");
std::fs::write(&b, "b = 2\n").expect("b.py");
let work = work_for(&[a.clone(), b.clone()]);
let (_findings, _failures) = deterministic::run(&work, dir.path()).await;
let lines = std::fs::read_to_string(&counter)
.map(|s| s.lines().count())
.unwrap_or(0);
assert_eq!(
lines, 1,
"two Python files must produce exactly one tool invocation; counter has {lines} line(s)"
);
}
#[tokio::test]
async fn unavailable_tool_marks_every_file_in_the_batch_as_failed() {
let dir = tempfile::tempdir().expect("tempdir");
let bin = dir.path().join("venv/bin/ruff");
std::fs::create_dir_all(bin.parent().unwrap()).expect("bin dir");
write_executable(&bin, "#!/bin/sh\nprintf '%s' 'this is not json, sorry'\n");
std::fs::write(dir.path().join("pyproject.toml"), "").expect("pyproject");
let a = dir.path().join("a.py");
let b = dir.path().join("b.py");
std::fs::write(&a, "a = 1\n").expect("a.py");
std::fs::write(&b, "b = 2\n").expect("b.py");
let work = work_for(&[a.clone(), b.clone()]);
let (findings, failures) = deterministic::run(&work, dir.path()).await;
assert!(
findings.is_empty(),
"an Unavailable tool must contribute no findings, got {findings:?}"
);
for path in [&a, &b] {
let reason = failures
.get(path)
.unwrap_or_else(|| panic!("missing failure for {path:?}, got {failures:?}"));
assert!(
matches!(reason, FailureReason::ToolUnavailable { .. }),
"expected ToolUnavailable for {path:?}, got {reason:?}"
);
}
}
#[tokio::test]
async fn unconfigured_tool_is_skipped_and_adds_no_failure() {
let dir = tempfile::tempdir().expect("tempdir");
let a = dir.path().join("a.py");
let b = dir.path().join("b.py");
std::fs::write(&a, "a = 1\n").expect("a.py");
std::fs::write(&b, "b = 2\n").expect("b.py");
let work = work_for(&[a.clone(), b.clone()]);
let (findings, failures) = deterministic::run(&work, dir.path()).await;
assert!(
findings.is_empty(),
"Skipped must contribute no findings, got {findings:?}"
);
assert!(
failures.is_empty(),
"Skipped must contribute no failures, got {failures:?}"
);
}