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(),
reviewed_directories: std::collections::BTreeSet::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, _compiled) = 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, _compiled) = 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, _compiled) = 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:?}"
);
}
#[tokio::test]
async fn nested_workspace_config_runs_with_workspace_relative_files() {
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' \"$PWD\" \"$@\" > {}\nprintf '%s' '[]'\n",
argv.display()
),
);
let member = root.join("apps/web");
std::fs::create_dir_all(member.join("src")).expect("member dirs");
std::fs::write(member.join("pyproject.toml"), "").expect("member config");
let file = member.join("src/app.py");
std::fs::write(&file, "x = 1\n").expect("source");
let work = work_for(std::slice::from_ref(&file));
let (_findings, failures, _compiled) = deterministic::run(&work, root).await;
assert!(failures.is_empty(), "workspace tool failed: {failures:?}");
let recorded = std::fs::read_to_string(argv).expect("recorded argv");
let lines: Vec<&str> = recorded.lines().collect();
assert_eq!(
std::path::Path::new(lines[0])
.canonicalize()
.expect("actual cwd"),
member.canonicalize().expect("member cwd")
);
assert_eq!(lines.last().copied(), Some("src/app.py"));
}
#[tokio::test]
async fn separate_workspace_configs_produce_separate_invocations() {
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 counter = root.join("counter.txt");
write_executable(
&bin,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$PWD\" >> {}\nprintf '%s' '[]'\n",
counter.display()
),
);
let files: Vec<PathBuf> = ["apps/web", "packages/core"]
.into_iter()
.map(|member| {
let member = root.join(member);
std::fs::create_dir_all(&member).expect("member dir");
std::fs::write(member.join("pyproject.toml"), "").expect("member config");
let file = member.join("mod.py");
std::fs::write(&file, "x = 1\n").expect("source");
file
})
.collect();
let work = work_for(&files);
let (_findings, failures, _compiled) = deterministic::run(&work, root).await;
assert!(failures.is_empty(), "workspace tools failed: {failures:?}");
assert_eq!(
std::fs::read_to_string(counter)
.expect("counter")
.lines()
.count(),
2
);
}