use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use serde_json::Value;
use crate::{ExecutorId, RuntimeError};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExecutorTarget {
pub executor: ExecutorId,
pub cwd: PathBuf,
}
pub fn discover_executor_targets(repo: &Path) -> Result<Vec<ExecutorTarget>, RuntimeError> {
if !repo.is_dir() {
return Err(RuntimeError::InvalidArg(format!(
"repository root is not a directory: {}",
repo.display()
)));
}
let mut targets = BTreeSet::new();
let root_cargo = repo.join("Cargo.toml").is_file();
if root_cargo {
insert(&mut targets, "cargo-test", repo)?;
}
discover_dir(repo, repo, root_cargo, &mut targets, 0)?;
Ok(targets.into_iter().collect())
}
fn discover_dir(
repo: &Path,
dir: &Path,
root_cargo: bool,
targets: &mut BTreeSet<ExecutorTarget>,
depth: usize,
) -> Result<(), RuntimeError> {
const MAX_DEPTH: usize = 12;
if depth > MAX_DEPTH {
return Ok(());
}
if dir != repo && ignored_dir(dir) {
return Ok(());
}
if dir != repo && !root_cargo && dir.join("Cargo.toml").is_file() {
insert(targets, "cargo-test", dir)?;
}
if dir.join("go.mod").is_file() {
insert(targets, "go-test", dir)?;
}
let package = dir.join("package.json");
if package.is_file() {
discover_package(&package, targets)?;
}
let entries = fs::read_dir(dir)
.map_err(|err| RuntimeError::InvalidArg(format!("cannot read {}: {err}", dir.display())))?;
for entry in entries {
let entry = entry.map_err(|err| {
RuntimeError::InvalidArg(format!("cannot read {}: {err}", dir.display()))
})?;
if entry
.file_type()
.map_err(|err| RuntimeError::InvalidArg(err.to_string()))?
.is_dir()
{
discover_dir(repo, &entry.path(), root_cargo, targets, depth + 1)?;
}
}
Ok(())
}
fn discover_package(
package: &Path,
targets: &mut BTreeSet<ExecutorTarget>,
) -> Result<(), RuntimeError> {
let raw = fs::read_to_string(package).map_err(|err| {
RuntimeError::InvalidArg(format!("cannot read {}: {err}", package.display()))
})?;
let json: Value = serde_json::from_str(&raw).map_err(|err| RuntimeError::Malformed {
kind: "package.json".into(),
message: format!("{}: {err}", package.display()),
})?;
let cwd = package.parent().ok_or_else(|| {
RuntimeError::InvalidArg(format!(
"package manifest has no parent: {}",
package.display()
))
})?;
let scripts = json.get("scripts").and_then(Value::as_object);
if has_dependency(&json, "vitest")
&& (has_dependency(&json, "@storybook/addon-vitest")
|| has_dependency(&json, "@storybook/experimental-addon-test"))
&& cwd.join(".storybook").is_dir()
{
insert(
targets,
if has_dependency(&json, "@vitest/coverage-v8") {
"storybook-vitest-v8"
} else {
"storybook-vitest"
},
cwd,
)?;
}
if scripts
.and_then(|value| value.get("test"))
.and_then(Value::as_str)
.is_some_and(|script| !is_placeholder_test_script(script))
{
let script = scripts
.and_then(|value| value.get("test"))
.and_then(Value::as_str)
.unwrap_or_default();
if cwd.join("bun.lock").is_file() || cwd.join("bun.lockb").is_file() {
insert(targets, "bun-test", cwd)?;
} else if direct_test_executor(script, &json).is_some_and(|id| id == "vitest") {
insert(targets, "vitest", cwd)?;
} else {
insert(targets, "npm-test", cwd)?;
}
return Ok(());
}
for (dependency, executor) in [
("vitest", "vitest"),
("jest", "jest"),
("@playwright/test", "playwright"),
] {
if has_dependency(&json, dependency) {
insert(targets, executor, cwd)?;
}
}
Ok(())
}
fn direct_test_executor<'a>(script: &str, package: &'a Value) -> Option<&'a str> {
let normalized = script.split_whitespace().collect::<Vec<_>>().join(" ");
(matches!(normalized.as_str(), "vitest" | "vitest run") && has_dependency(package, "vitest"))
.then_some("vitest")
}
fn has_dependency(json: &Value, name: &str) -> bool {
["dependencies", "devDependencies"]
.iter()
.filter_map(|key| json.get(*key).and_then(Value::as_object))
.any(|items| items.contains_key(name))
}
fn is_placeholder_test_script(script: &str) -> bool {
let normalized = script.to_ascii_lowercase();
normalized.contains("error: no test specified") || normalized.trim().is_empty()
}
fn insert(
targets: &mut BTreeSet<ExecutorTarget>,
executor: &str,
cwd: &Path,
) -> Result<(), RuntimeError> {
targets.insert(ExecutorTarget {
executor: ExecutorId::new(executor)?,
cwd: cwd.to_path_buf(),
});
Ok(())
}
fn ignored_dir(dir: &Path) -> bool {
dir.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
matches!(
name,
".git"
| ".weavatrix-quality"
| "node_modules"
| "target"
| "dist"
| "build"
| "vendor"
| "fixtures"
| "benchmark"
| "benchmarks"
)
})
}