use tempfile::TempDir;
use super::support::*;
use crate::languages::runner::*;
#[tokio::test]
async fn run_tool_returns_skipped_when_no_config_file_present() {
let spec = ToolSpec {
name: "ruff",
local_paths: &["nope"],
command: &["definitely-not-installed-ruff-xyz"],
config_files: &["pyproject.toml"],
output_format: "json",
diagnostics_stream: "stdout",
..ToolSpec::default()
};
let dir = TempDir::new().unwrap();
let outcome = run_tool(&spec, dir.path(), &[]).await;
assert_eq!(outcome.status, ToolStatus::Skipped);
}
#[tokio::test]
async fn run_tool_returns_unavailable_when_binary_cannot_be_found() {
let spec = ToolSpec {
name: "no-such-tool",
local_paths: &[],
command: &["definitely-not-installed-tool-zzz"],
config_files: &["any"],
output_format: "json",
diagnostics_stream: "stdout",
..ToolSpec::default()
};
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("any"), "").unwrap();
let outcome = run_tool(&spec, dir.path(), &[]).await;
assert_eq!(outcome.status, ToolStatus::Unavailable);
}
#[tokio::test]
async fn compilation_ground_truth_requires_a_successful_compiler() {
for (establishes_compilation, expected) in [(true, true), (false, false)] {
let dir = TempDir::new().unwrap();
let bin = dir.path().join("checker");
write_executable(&bin, "#!/bin/sh\nexit 0\n");
std::fs::write(dir.path().join("project.config"), "").unwrap();
let spec = ToolSpec {
name: "checker",
command: &["checker"],
local_paths: &["checker"],
config_files: &["project.config"],
output_format: "lines",
establishes_compilation,
..ToolSpec::default()
};
let outcome = run_tool(&spec, dir.path(), &["src/lib.rs".to_owned()]).await;
assert_eq!(outcome.status, ToolStatus::Ok);
assert_eq!(
outcome.compilation_succeeded, expected,
"a successful linter cannot disprove an LLM compile-error claim"
);
}
}
#[tokio::test]
async fn run_tool_reads_stderr_when_diagnostics_stream_is_stderr() {
let dir = TempDir::new().unwrap();
let bin = dir.path().join("diag");
write_executable(
&bin,
"#!/bin/sh\nprintf '%s' '[{\"code\":\"E1\",\"filename\":\"x\",\"location\":{\"row\":1,\"column\":1},\"message\":\"m\"}]' 1>&2\n",
);
let spec = ToolSpec {
name: "diag",
command: &["diag"],
local_paths: &["diag"],
config_files: &["marker"],
output_format: "json",
diagnostics_stream: "stderr",
..ToolSpec::default()
};
std::fs::write(dir.path().join("marker"), "").unwrap();
let outcome = run_tool(&spec, dir.path(), &[]).await;
assert_eq!(outcome.status, ToolStatus::Ok);
assert_eq!(outcome.findings.len(), 1);
assert_eq!(outcome.findings[0].kind, "E1");
let spec_stdout = ToolSpec {
diagnostics_stream: "stdout",
..spec
};
let outcome = run_tool(&spec_stdout, dir.path(), &[]).await;
assert_eq!(outcome.status, ToolStatus::Ok);
assert!(
outcome.findings.is_empty(),
"stdout-streamed run should not pick up stderr diagnostics"
);
}
#[tokio::test]
async fn run_tool_reports_unavailable_for_unparseable_output_not_empty_ok() {
let dir = TempDir::new().unwrap();
let bin = dir.path().join("noisy");
write_executable(&bin, "#!/bin/sh\necho 'this is not json'\n");
let spec = ToolSpec {
name: "noisy",
command: &["noisy"],
local_paths: &["noisy"],
config_files: &["marker"],
output_format: "json",
diagnostics_stream: "stdout",
..ToolSpec::default()
};
std::fs::write(dir.path().join("marker"), "").unwrap();
let outcome = run_tool(&spec, dir.path(), &[]).await;
assert_eq!(outcome.status, ToolStatus::Unavailable);
assert!(
outcome.findings.is_empty(),
"unparseable must not be reported as zero findings"
);
}
#[tokio::test]
async fn a_silent_non_zero_exit_is_unavailable_not_a_clean_pass() {
let dir = TempDir::new().unwrap();
let bin = dir.path().join("failtool");
write_executable(&bin, "#!/bin/sh\necho 'fatal: bad config' >&2\nexit 2\n");
std::fs::write(dir.path().join("pyproject.toml"), "").unwrap();
let spec = ToolSpec {
name: "failtool",
command: &["failtool"],
local_paths: &["failtool"],
config_files: &["pyproject.toml"],
output_format: "lines",
diagnostics_stream: "stdout",
..ToolSpec::default()
};
let outcome = run_tool(&spec, dir.path(), &["a.py".to_owned()]).await;
assert_eq!(
outcome.status,
ToolStatus::Unavailable,
"a silent non-zero exit must not read as a clean pass, got {outcome:?}"
);
assert!(
outcome.detail.contains("fatal: bad config"),
"the other stream carries the real error and must reach the detail: {}",
outcome.detail
);
}
#[tokio::test]
async fn a_relative_root_still_finds_the_repo_local_tool() {
let dir = TempDir::new().unwrap();
let cwd = std::env::current_dir().expect("cwd");
let relative = pathdiff_relative(&cwd, dir.path());
let bin = dir.path().join("mytool");
write_executable(&bin, "#!/bin/sh\nexit 0\n");
std::fs::write(dir.path().join("pyproject.toml"), "").unwrap();
let spec = ToolSpec {
name: "mytool",
command: &["mytool"],
local_paths: &["mytool"],
config_files: &["pyproject.toml"],
output_format: "lines",
diagnostics_stream: "stdout",
..ToolSpec::default()
};
let outcome = run_tool(&spec, &relative, &[]).await;
assert_ne!(
outcome.status,
ToolStatus::Unavailable,
"a relative root must still spawn the repo-local tool, got {outcome:?}"
);
}
fn pathdiff_relative(from: &std::path::Path, to: &std::path::Path) -> std::path::PathBuf {
to.strip_prefix(from)
.map(std::path::Path::to_path_buf)
.unwrap_or_else(|_| to.to_path_buf())
}
#[tokio::test]
async fn a_whole_project_tool_is_invoked_without_file_arguments() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(root.join("marker"), "").expect("config file");
let log = root.join("argv.log");
let stub = format!(
"#!/bin/sh\nfor a in \"$@\"; do printf '%s\\n' \"$a\" >> {}; done\nprintf '[]\\n'\n",
log.to_string_lossy()
);
let tool = root.join("wholeproject");
write_executable(&tool, stub);
let spec = ToolSpec {
name: "wholeproject",
command: &["wholeproject", "--flag"],
local_paths: &["wholeproject"],
config_files: &["marker"],
output_format: "lines",
accepts_files: false,
..ToolSpec::default()
};
let outcome = run_tool(&spec, root, &["a.rs".to_owned(), "b.rs".to_owned()]).await;
assert_eq!(outcome.status, ToolStatus::Ok, "detail: {}", outcome.detail);
let recorded: Vec<String> = std::fs::read_to_string(&log)
.unwrap_or_default()
.lines()
.map(str::to_owned)
.collect();
assert_eq!(
recorded,
vec!["--flag"],
"the declared flags are passed and the files are not"
);
}
#[tokio::test]
async fn a_whole_project_tools_findings_are_narrowed_to_the_requested_files() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(root.join("marker"), "").expect("config file");
let stub = "#!/bin/sh\nprintf 'wanted.rs\\nuntouched.rs\\nalso_wanted.rs\\n'\n";
let tool = root.join("wholeproject");
write_executable(&tool, stub);
let spec = ToolSpec {
name: "wholeproject",
command: &["wholeproject"],
local_paths: &["wholeproject"],
config_files: &["marker"],
output_format: "lines",
accepts_files: false,
..ToolSpec::default()
};
let outcome = run_tool(
&spec,
root,
&["wanted.rs".to_owned(), "./also_wanted.rs".to_owned()],
)
.await;
assert_eq!(outcome.status, ToolStatus::Ok, "detail: {}", outcome.detail);
let mut reported: Vec<&str> = outcome
.findings
.iter()
.map(|f| f.file_path.as_str())
.collect();
reported.sort_unstable();
assert_eq!(
reported,
vec!["also_wanted.rs", "wanted.rs"],
"untouched.rs was not asked about, so its finding must be dropped"
);
}
#[tokio::test]
async fn a_file_taking_tools_findings_are_not_filtered() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(root.join("marker"), "").expect("config file");
let stub = "#!/bin/sh\nprintf 'somewhere/else.rs\\n'\n";
let tool = root.join("perfile");
write_executable(&tool, stub);
let spec = ToolSpec {
name: "perfile",
command: &["perfile"],
local_paths: &["perfile"],
config_files: &["marker"],
output_format: "lines",
..ToolSpec::default()
};
let outcome = run_tool(&spec, root, &["asked.rs".to_owned()]).await;
assert_eq!(outcome.status, ToolStatus::Ok, "detail: {}", outcome.detail);
assert_eq!(
outcome.findings.len(),
1,
"a per-file tool only reports on what it was given, so nothing is dropped"
);
}
#[tokio::test]
async fn a_filename_that_looks_like_a_flag_is_passed_as_a_path() {
let dir = tempfile::tempdir().expect("tempdir");
let bin = dir.path().join("bin");
std::fs::create_dir_all(&bin).expect("mkdir bin");
let tool = bin.join("argvdump");
write_executable(
&tool,
format!(
"#!/bin/sh\nfor a in \"$@\"; do echo \"$a\"; done > {}/argv.txt\nexit 0\n",
dir.path().display()
),
);
std::fs::write(dir.path().join("pyproject.toml"), "").expect("config");
let spec = ToolSpec {
name: "argvdump",
command: &["argvdump"],
local_paths: &["bin/argvdump"],
config_files: &["pyproject.toml"],
output_format: "lines",
diagnostics_stream: "stdout",
..ToolSpec::default()
};
let _ = run_tool(&spec, dir.path(), &["--fix".to_owned()]).await;
let argv = std::fs::read_to_string(dir.path().join("argv.txt")).unwrap_or_default();
assert!(
argv.lines().any(|a| a == "./--fix"),
"a dash-leading filename must reach the tool as `./--fix`, got: {argv:?}"
);
assert!(
!argv.lines().any(|a| a == "--fix"),
"it must not reach the tool as a bare option: {argv:?}"
);
}
fn argv_after_running(spec: &ToolSpec, config_files: &[&str]) -> (TempDir, Vec<String>) {
let dir = tempfile::tempdir().expect("tempdir");
let bin = dir.path().join("bin");
std::fs::create_dir_all(&bin).expect("mkdir bin");
write_executable(
&bin.join("argvdump"),
format!(
"#!/bin/sh\nfor a in \"$@\"; do echo \"$a\"; done > {}/argv.txt\nexit 0\n",
dir.path().display()
),
);
for config in config_files {
let path = dir.path().join(config);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("mkdir config parent");
}
std::fs::write(path, "").expect("config");
}
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
runtime.block_on(run_tool(spec, dir.path(), &["Sample.java".to_owned()]));
let argv = std::fs::read_to_string(dir.path().join("argv.txt")).unwrap_or_default();
let lines = argv.lines().map(str::to_owned).collect();
(dir, lines)
}
#[test]
fn config_flag_passes_the_discovered_config_before_the_files() {
let spec = ToolSpec {
name: "argvdump",
command: &["argvdump", "-f", "sarif"],
local_paths: &["bin/argvdump"],
config_files: &["config/checkstyle/checkstyle.xml"],
config_flag: Some("-c"),
output_format: "sarif",
diagnostics_stream: "stdout",
..ToolSpec::default()
};
let (_dir, argv) = argv_after_running(&spec, &["config/checkstyle/checkstyle.xml"]);
assert_eq!(
argv,
vec![
"-f",
"sarif",
"-c",
"config/checkstyle/checkstyle.xml",
"Sample.java"
],
"the config must land between the static command and the files"
);
}
#[test]
fn config_flag_uses_the_first_config_file_that_exists() {
let spec = ToolSpec {
name: "argvdump",
command: &["argvdump"],
local_paths: &["bin/argvdump"],
config_files: &["checkstyle.xml", "config/checkstyle/checkstyle.xml"],
config_flag: Some("-c"),
output_format: "sarif",
diagnostics_stream: "stdout",
..ToolSpec::default()
};
let (_dir, argv) = argv_after_running(&spec, &["config/checkstyle/checkstyle.xml"]);
assert_eq!(
argv,
vec!["-c", "config/checkstyle/checkstyle.xml", "Sample.java"]
);
}
#[test]
fn no_config_flag_leaves_the_command_alone() {
let spec = ToolSpec {
name: "argvdump",
command: &["argvdump", "check"],
local_paths: &["bin/argvdump"],
config_files: &["pyproject.toml"],
config_flag: None,
output_format: "lines",
diagnostics_stream: "stdout",
..ToolSpec::default()
};
let (_dir, argv) = argv_after_running(&spec, &["pyproject.toml"]);
assert_eq!(argv, vec!["check", "Sample.java"]);
}