mod commands;
use super::extras::Language;
use super::output::{StringOrVec, TestConfig};
use super::tools::{LangContext, require_ruby_bundler, require_tool, ruby_bundle_exec, wrap_command as wrap};
pub(crate) fn default_test_config(lang: Language, output_dir: &str, ctx: &LangContext) -> TestConfig {
let output_dir = super::shell::quote_word(output_dir);
match lang {
Language::Python => commands::python_config(&output_dir, ctx),
Language::Node | Language::Wasm => commands::node_wasm_config(&output_dir, ctx),
Language::Go => commands::go_config(&output_dir, ctx),
Language::Ruby => commands::ruby_config(&output_dir, ctx),
Language::Php => commands::php_config(&output_dir, ctx),
Language::Java => commands::java_config(&output_dir, ctx),
Language::Csharp => commands::csharp_config(&output_dir, ctx),
Language::Elixir => commands::elixir_config(&output_dir, ctx),
Language::R => commands::r_config(&output_dir, ctx),
Language::Rust => commands::rust_config(),
Language::Ffi => commands::ffi_config(),
Language::C => commands::c_config(),
Language::Kotlin | Language::KotlinAndroid => commands::kotlin_kotlin_android_config(&output_dir, ctx),
Language::Swift => commands::swift_config(&output_dir, ctx),
Language::Dart => commands::dart_config(&output_dir, ctx),
Language::Zig => commands::zig_config(&output_dir, ctx),
Language::Gleam => commands::gleam_config(&output_dir, ctx),
Language::Jni => commands::jni_config(),
}
}
#[cfg(test)]
mod tests {
use super::super::tools::ToolsConfig;
use super::*;
fn all_languages() -> Vec<Language> {
vec![
Language::Python,
Language::Node,
Language::Wasm,
Language::Ruby,
Language::Php,
Language::Go,
Language::Java,
Language::Csharp,
Language::Elixir,
Language::R,
Language::Ffi,
Language::Rust,
Language::Kotlin,
Language::Swift,
Language::Dart,
Language::Gleam,
Language::Zig,
]
}
fn cfg(lang: Language, dir: &str) -> TestConfig {
let tools = ToolsConfig::default();
let ctx = LangContext::default(&tools);
default_test_config(lang, dir, &ctx)
}
fn quoted(dir: &str) -> String {
super::super::shell::quote_word(dir)
}
#[test]
fn generated_test_quotes_configured_output_directory() {
let malicious = "packages/python; touch /tmp/alef-test; #";
let commands = cfg(Language::Python, malicious)
.command
.expect("python test command")
.commands()
.join(" ");
assert!(commands.contains(&format!("cd {}", super::super::shell::quote_word(malicious))));
}
#[test]
fn ffi_has_no_test_commands() {
let c = cfg(Language::Ffi, "packages/ffi");
assert!(c.command.is_none());
assert!(c.e2e.is_none());
assert!(c.coverage.is_none());
}
#[test]
fn non_ffi_languages_have_command_and_coverage() {
for lang in all_languages() {
if matches!(lang, Language::Ffi) {
continue;
}
let c = cfg(lang, "packages/test");
assert!(c.command.is_some(), "{lang} should have a default test command");
assert!(c.coverage.is_some(), "{lang} should have a default coverage command");
}
}
#[test]
fn non_ffi_languages_have_default_precondition() {
for lang in all_languages() {
if matches!(lang, Language::Ffi) {
continue;
}
let c = cfg(lang, "packages/test");
let pre = c
.precondition
.unwrap_or_else(|| panic!("{lang} should have a precondition"));
assert!(pre.starts_with("command -v "));
}
}
#[test]
fn e2e_is_none_for_non_dart_languages() {
for lang in all_languages() {
if matches!(lang, Language::Dart) {
continue;
}
let c = cfg(lang, "packages/test");
assert!(c.e2e.is_none(), "{lang} e2e should always be None (user-configured)");
}
}
#[test]
fn dart_has_default_e2e_command_using_dart_sdk() {
let c = cfg(Language::Dart, "packages/dart");
let e2e = c.e2e.expect("dart should have a default e2e command");
let cmd = e2e.commands().join(" ");
assert!(
cmd.contains("e2e/dart"),
"dart e2e should target e2e/dart directory, got: {cmd}"
);
assert!(
cmd.contains("dart pub get"),
"dart e2e should use dart pub get (not flutter pub get), got: {cmd}"
);
assert!(
cmd.contains("dart test"),
"dart e2e should use dart test (not flutter test), got: {cmd}"
);
assert!(
cmd.contains("--concurrency=1"),
"dart e2e should run with --concurrency=1, got: {cmd}"
);
assert!(
!cmd.contains("flutter"),
"dart e2e must not use flutter (impellerc fails in headless env), got: {cmd}"
);
}
#[test]
fn python_uses_pytest_via_uv_by_default() {
let c = cfg(Language::Python, "packages/python");
let cmd = c.command.unwrap().commands().join(" ");
assert!(cmd.contains("uv run --no-sync pytest"));
}
#[test]
fn python_test_dispatches_on_package_manager() {
for (pm, expected) in [("pip", "&& pytest"), ("poetry", "poetry run pytest")] {
let tools = ToolsConfig {
python_package_manager: Some(pm.to_string()),
..Default::default()
};
let ctx = LangContext::default(&tools);
let c = default_test_config(Language::Python, "packages/python", &ctx);
assert!(
c.command.unwrap().commands().join(" ").contains(expected),
"{pm}: expected {expected}"
);
}
}
#[test]
fn node_uses_pnpm_by_default() {
let c = cfg(Language::Node, "packages/node");
let cmd = c.command.unwrap().commands().join(" ");
assert!(cmd.contains("pnpm test"));
}
#[test]
fn node_test_dispatches_on_package_manager() {
for (pm, expected) in [("npm", "npm test"), ("yarn", "yarn test")] {
let tools = ToolsConfig {
node_package_manager: Some(pm.to_string()),
..Default::default()
};
let ctx = LangContext::default(&tools);
let c = default_test_config(Language::Node, "packages/node", &ctx);
assert!(
c.command.unwrap().commands().join(" ").contains(expected),
"{pm}: expected {expected}"
);
}
}
#[test]
fn go_uses_go_test() {
let c = cfg(Language::Go, "packages/go");
let cmd = c.command.unwrap().commands().join(" ");
assert!(cmd.contains("go test ./..."));
}
#[test]
fn ruby_uses_rspec() {
let c = cfg(Language::Ruby, "packages/ruby");
assert_eq!(
c.precondition.as_deref(),
Some(
"command -v ruby >/dev/null 2>&1 && BUNDLE_PATH=vendor/bundle ruby -S bundle --version >/dev/null 2>&1"
)
);
let cmd = c.command.unwrap().commands().join(" ");
let coverage = c.coverage.unwrap().commands().join(" ");
assert!(
cmd.contains("BUNDLE_PATH=vendor/bundle ruby -S bundle exec ruby -S rspec"),
"got: {cmd}"
);
assert!(
coverage.contains("BUNDLE_PATH=vendor/bundle ruby -S bundle exec ruby -S rspec --format documentation"),
"got: {coverage}"
);
}
#[test]
fn java_uses_maven() {
let c = cfg(Language::Java, "packages/java");
let cmd = c.command.unwrap().commands().join(" ");
let cov = c.coverage.unwrap().commands().join(" ");
assert!(cmd.contains("mvn"));
assert!(cov.contains("jacoco:report"));
}
#[test]
fn java_project_file_is_quoted() {
let ctx = LangContext {
tools: &ToolsConfig::default(),
run_wrapper: None,
extra_lint_paths: &[],
project_file: Some("pom.xml"),
};
let c = default_test_config(Language::Java, "packages/java", &ctx);
let cmd = c.command.unwrap().commands().join(" ");
let cov = c.coverage.unwrap().commands().join(" ");
let proj = quoted("pom.xml");
assert!(
cmd.contains(&format!("-f {proj}")),
"test command should use project_file: {cmd}"
);
assert!(!cmd.contains("packages/java/pom.xml"), "should not use output_dir path");
assert!(
cov.contains(&format!("-f {proj}")),
"coverage command should use project_file: {cov}"
);
}
#[test]
fn csharp_project_file_is_quoted() {
let ctx = LangContext {
tools: &ToolsConfig::default(),
run_wrapper: None,
extra_lint_paths: &[],
project_file: Some("my project.csproj"),
};
let c = default_test_config(Language::Csharp, "packages/csharp", &ctx);
let cmd = c.command.unwrap().commands().join(" ");
let cov = c.coverage.unwrap().commands().join(" ");
let proj = quoted("my project.csproj");
assert!(
cmd.contains(&format!("dotnet test {proj}")),
"test command should use quoted project_file: {cmd}"
);
assert!(!cmd.contains("packages/csharp"), "should not use output_dir path");
assert!(
cov.contains(&format!("dotnet test {proj}")),
"coverage command should use quoted project_file: {cov}"
);
}
#[cfg(unix)]
#[test]
fn java_project_file_keeps_a_hostile_value_out_of_shell_text() {
let root = tempfile::tempdir().expect("tempdir");
let hostile = "pom.xml$(touch witness)#";
let witness = root.path().join("witness");
let ctx = LangContext {
tools: &ToolsConfig::default(),
run_wrapper: None,
extra_lint_paths: &[],
project_file: Some(hostile),
};
let c = default_test_config(Language::Java, "packages/java", &ctx);
let cmd = c.command.unwrap().commands().join(" ");
std::process::Command::new("sh")
.args(["-c", &cmd])
.current_dir(root.path())
.output()
.expect("shell should start");
assert!(
!witness.exists(),
"command substitution in project_file must not run: {cmd}"
);
}
#[test]
fn rust_uses_cargo_and_llvm_cov() {
let c = cfg(Language::Rust, "packages/rust");
let cmd = c.command.unwrap().commands().join(" ");
let cov = c.coverage.unwrap().commands().join(" ");
assert!(cmd.contains("cargo test --workspace"));
assert!(cov.contains("cargo llvm-cov"));
}
#[test]
fn output_dir_substituted_in_commands() {
let c = cfg(Language::Python, "my/custom/dir");
let cmd = c.command.unwrap().commands().join(" ");
assert!(cmd.contains("my/custom/dir"));
}
#[test]
fn kotlin_uses_gradle_test() {
let c = cfg(Language::Kotlin, "packages/kotlin");
let cmd = c.command.unwrap().commands().join(" ");
assert!(
cmd.contains("gradle test"),
"Kotlin test should use gradle test, got: {cmd}"
);
assert_eq!(c.precondition.as_deref(), Some("command -v gradle >/dev/null 2>&1"));
}
#[test]
fn swift_uses_swift_test_with_package_path() {
let c = cfg(Language::Swift, "packages/swift");
let cmd = c.command.unwrap().commands().join(" ");
assert!(
cmd.contains("swift test"),
"Swift test should use swift test, got: {cmd}"
);
assert!(
cmd.contains(&format!("--package-path {}", quoted("packages/swift"))),
"Swift test should include package path, got: {cmd}"
);
}
#[test]
fn dart_uses_dart_test() {
let c = cfg(Language::Dart, "packages/dart");
let cmd = c.command.unwrap().commands().join(" ");
assert!(cmd.contains("dart test"), "Dart test should use dart test, got: {cmd}");
assert_eq!(c.precondition.as_deref(), Some("command -v dart >/dev/null 2>&1"));
}
#[test]
fn gleam_uses_gleam_test() {
let c = cfg(Language::Gleam, "packages/gleam");
let cmd = c.command.unwrap().commands().join(" ");
assert!(
cmd.contains("gleam test"),
"Gleam test should use gleam test, got: {cmd}"
);
assert_eq!(c.precondition.as_deref(), Some("command -v gleam >/dev/null 2>&1"));
}
#[test]
fn zig_uses_zig_build_test() {
let c = cfg(Language::Zig, "packages/zig");
let cmd = c.command.unwrap().commands().join(" ");
assert!(
cmd.contains("zig build test"),
"Zig test should use zig build test, got: {cmd}"
);
assert_eq!(c.precondition.as_deref(), Some("command -v zig >/dev/null 2>&1"));
}
}