use tempfile::TempDir;
use super::support::*;
use crate::languages::runner::*;
use crate::languages::spec::{DiagnosticsStream, OutputFormat, ToolSpec};
#[test]
fn repo_local_executable_is_preferred_over_path() {
let dir = TempDir::new().unwrap();
let bin = dir.path().join("mytool");
write_executable(&bin, "#!/bin/sh\n");
let spec = ToolSpec {
local_paths: &["mytool"],
command: &["mytool"],
..ToolSpec::default()
};
assert_eq!(resolve_tool(&spec, dir.path()), Some(bin));
}
#[test]
fn non_executable_repo_local_path_falls_through_to_path() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("mytool"), "not executable").unwrap();
let path_dir = TempDir::new().unwrap();
let on_path = path_dir.path().join("mytool");
write_executable(&on_path, "#!/bin/sh\nexit 0\n");
let spec = ToolSpec {
local_paths: &["mytool"],
command: &["mytool"],
..ToolSpec::default()
};
let path = std::env::join_paths([path_dir.path()]).expect("joins");
assert_eq!(
resolve_tool_in(&spec, dir.path(), Some(path.as_os_str())),
Some(on_path),
"the non-executable local hit must be skipped and PATH consulted"
);
}
#[test]
fn resolve_tool_returns_none_when_neither_local_nor_path_has_it() {
let dir = TempDir::new().unwrap();
let spec = ToolSpec {
local_paths: &["definitely-not-installed-x9z"],
command: &["definitely-not-installed-x9z"],
..ToolSpec::default()
};
assert_eq!(resolve_tool(&spec, dir.path()), None);
}
#[test]
fn is_configured_true_when_any_config_file_exists() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("pyproject.toml"), "").unwrap();
assert!(is_configured(&ruff_like_spec(), dir.path()));
}
#[test]
fn tool_status_skipped_when_not_configured_and_detail_lists_config() {
let dir = TempDir::new().unwrap();
let outcome = tool_status(&ruff_like_spec(), dir.path());
assert_eq!(outcome.status, ToolStatus::Skipped);
assert!(
outcome.detail.contains("pyproject.toml"),
"detail was {:?}",
outcome.detail
);
}
#[test]
fn tool_status_unavailable_when_configured_but_binary_missing() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("pyproject.toml"), "").unwrap();
let spec = ToolSpec {
name: "ruff",
local_paths: &["no/such/ruff"],
command: &["definitely-not-installed-ruff-abc"],
config_files: &["pyproject.toml"],
output_format: OutputFormat::Json,
diagnostics_stream: DiagnosticsStream::Stdout,
..ToolSpec::default()
};
let outcome = tool_status(&spec, dir.path());
assert_eq!(outcome.status, ToolStatus::Unavailable);
}
#[test]
fn missing_nested_tool_detail_names_the_workspace_search_boundary() {
let dir = TempDir::new().unwrap();
let workspace = dir.path().join("apps/web");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::write(workspace.join("project.config"), "").unwrap();
let spec = ToolSpec {
name: "nested-tool",
local_paths: &["node_modules/.bin/nested-tool"],
command: &["definitely-not-installed-nested-tool"],
config_files: &["project.config"],
..ToolSpec::default()
};
let outcome = tool_status_at(&spec, dir.path(), &workspace);
assert_eq!(outcome.status, ToolStatus::Unavailable);
assert!(
outcome.detail.contains(&workspace.display().to_string()),
"the diagnostic must name where its ancestor search started: {}",
outcome.detail
);
assert!(
outcome.detail.contains(&dir.path().display().to_string()),
"the diagnostic must name the repository boundary: {}",
outcome.detail
);
}
#[test]
fn a_glob_config_marker_matches_by_extension() {
let dir = TempDir::new().unwrap();
let root = dir.path();
let spec = ToolSpec {
name: "globbed",
config_files: &["*.csproj"],
..ToolSpec::default()
};
assert!(
!is_configured(&spec, root),
"an empty directory has no marker"
);
std::fs::write(root.join("notes.txt"), "").unwrap();
assert!(
!is_configured(&spec, root),
"another extension is not the marker"
);
std::fs::write(root.join("Widget.csproj"), "").unwrap();
assert!(
is_configured(&spec, root),
"any name with the extension counts"
);
}
#[test]
fn a_glob_config_marker_does_not_look_in_subdirectories() {
let dir = TempDir::new().unwrap();
let root = dir.path();
std::fs::create_dir(root.join("src")).unwrap();
std::fs::write(root.join("src").join("Widget.csproj"), "").unwrap();
let spec = ToolSpec {
name: "globbed",
config_files: &["*.csproj"],
..ToolSpec::default()
};
assert!(
!is_configured(&spec, root),
"the project lives in src/, so src/ is the workspace and not the root"
);
assert!(is_configured(&spec, &root.join("src")));
}
#[test]
fn only_a_leading_star_dot_is_treated_as_a_glob() {
let dir = TempDir::new().unwrap();
let root = dir.path();
let spec = ToolSpec {
name: "literal",
config_files: &["eslint.config.js"],
..ToolSpec::default()
};
std::fs::write(root.join("other.js"), "").unwrap();
assert!(
!is_configured(&spec, root),
"a literal entry must not match by extension"
);
std::fs::write(root.join("eslint.config.js"), "").unwrap();
assert!(is_configured(&spec, root));
}
#[test]
fn a_glob_config_marker_matches_files_not_directories() {
let dir = TempDir::new().unwrap();
let root = dir.path();
std::fs::create_dir(root.join("Widget.csproj")).unwrap();
let spec = ToolSpec {
name: "globbed",
config_files: &["*.csproj"],
..ToolSpec::default()
};
assert!(
!is_configured(&spec, root),
"a directory carrying the extension is not a project file"
);
}