use crate::compile::{
flat_package_src_dirs, flat_package_test_dirs, javac_release_arg, KOTLIN_COMPILER_COORD,
KOTLIN_STDLIB_COORD,
};
use crate::incremental::{
self, javac_version, needs_recompile, walk_files, write_javac_version_stamp, CompileStatus,
Inputs, Stamp,
};
use crate::jar::classpath_string;
use crate::{build, descriptor};
use crate::build::central_repos;
use anyhow::{bail, Context, Result};
use curie_deps::resolver::{resolve, DepEntry, ResolveOptions};
use std::path::{Path, PathBuf};
use std::process::Command;
const JUNIT_STANDALONE_COORD: &str =
"org.junit.platform:junit-platform-console-standalone";
const TEST_SOURCE_SET_STAMP: &str = ".test-sources";
#[allow(clippy::too_many_arguments)]
pub fn run_tests(
project_root: &Path,
desc: &descriptor::Descriptor,
classes_dir: &Path,
dep_jars: &[PathBuf],
kotlin_stdlib_jars: &[PathBuf],
groovy_jars: &[PathBuf],
resources_dir: Option<&Path>,
test_resources_dir: Option<&Path>,
filter: Option<&str>,
offline: bool,
coverage: bool,
extra_cp: &[PathBuf],
) -> Result<()> {
let (java_test_sources, kotlin_test_sources) = discover_test_sources(project_root);
let groovy_test_sources = discover_groovy_test_sources(project_root);
let all_test_sources: Vec<PathBuf> = {
let mut v = java_test_sources.clone();
v.extend(kotlin_test_sources.iter().cloned());
v.extend(groovy_test_sources.iter().cloned());
v.sort();
v.dedup();
v
};
if all_test_sources.is_empty() {
crate::parallel::emit(&crate::style::neutral("Tests", "no test sources found"));
return Ok(());
}
let has_kotlin_tests = !kotlin_test_sources.is_empty();
let has_java_tests = !java_test_sources.is_empty();
let has_groovy_tests = !groovy_test_sources.is_empty();
let extra_repos = build::extra_repos(desc);
let junit_version = if desc.spock.enabled() && !desc.test.junit_platform_version_is_user_set() {
"1.14.4"
} else {
desc.test.junit_platform_version()
};
let standalone_jar = resolve_standalone(&extra_repos, offline, junit_version)
.context("failed to resolve JUnit Platform Console Standalone")?;
let test_bom_gavs = desc.test_bom_gavs()?;
let test_dep_jars = if desc.test_dependencies.is_empty() {
vec![]
} else {
let pairs: Vec<DepEntry> = desc
.test_dependencies
.iter()
.map(|(k, v)| DepEntry { key: k, version: v.version(), repo_id: v.repository(), exclusions: v.exclusions(), classifier: None, allow_version_conflict: v.allow_version_conflict() })
.collect();
resolve(
&pairs,
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos.clone(),
progress: crate::parallel::try_get_sink().is_none(),
bom_imports: test_bom_gavs.clone(),
offline,
skip_version_ranges: false,
error_on_version_conflict: true,
},
)
.context("test dependency resolution failed")?
};
let spock_jars: Vec<PathBuf> = if desc.spock.enabled() {
let spock_version = desc.spock.version();
let spock_bom = curie_deps::Gav::from_key_version(
"org.spockframework:spock-bom",
spock_version,
)?;
let mut spock_bom_imports = test_bom_gavs.clone();
spock_bom_imports.push(spock_bom);
let jars = resolve(
&[DepEntry {
key: "org.spockframework:spock-core",
version: spock_version,
repo_id: None,
exclusions: vec![],
classifier: None,
allow_version_conflict: false,
}],
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos.clone(),
progress: crate::parallel::try_get_sink().is_none(),
bom_imports: spock_bom_imports,
offline,
skip_version_ranges: false, error_on_version_conflict: false,
},
)
.context("Spock resolution failed")?;
crate::parallel::emit(&crate::style::resolve("Resolve Spock", &format!("{} JAR(s)", jars.len())));
jars
} else {
vec![]
};
let test_kotlin_stdlib_jars: Vec<PathBuf>;
let test_kotlin_compiler_jars: Vec<PathBuf>;
if has_kotlin_tests && kotlin_stdlib_jars.is_empty() {
let kver = desc.kotlin.version();
let kotlin_jars = resolve(
&[
DepEntry { key: KOTLIN_COMPILER_COORD, version: kver, repo_id: None, exclusions: vec![], classifier: None, allow_version_conflict: false },
DepEntry { key: KOTLIN_STDLIB_COORD, version: kver, repo_id: None, exclusions: vec![], classifier: None, allow_version_conflict: false },
],
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos.clone(),
progress: crate::parallel::try_get_sink().is_none(),
bom_imports: test_bom_gavs.clone(),
offline,
skip_version_ranges: false, error_on_version_conflict: false,
},
)
.context("Kotlin compiler/stdlib resolution failed (test phase)")?;
let stdlib: Vec<PathBuf> = kotlin_jars
.iter()
.filter(|p| {
p.file_name()
.map(|f| !f.to_string_lossy().starts_with("kotlin-compiler-embeddable"))
.unwrap_or(true)
})
.cloned()
.collect();
test_kotlin_compiler_jars = kotlin_jars;
test_kotlin_stdlib_jars = stdlib;
} else if has_kotlin_tests {
let kver = desc.kotlin.version();
let kotlin_jars = resolve(
&[
DepEntry { key: KOTLIN_COMPILER_COORD, version: kver, repo_id: None, exclusions: vec![], classifier: None, allow_version_conflict: false },
DepEntry { key: KOTLIN_STDLIB_COORD, version: kver, repo_id: None, exclusions: vec![], classifier: None, allow_version_conflict: false },
],
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos.clone(),
progress: false,
bom_imports: test_bom_gavs.clone(),
offline,
skip_version_ranges: false, error_on_version_conflict: false,
},
)
.context("Kotlin compiler resolution failed (test phase)")?;
test_kotlin_compiler_jars = kotlin_jars;
test_kotlin_stdlib_jars = kotlin_stdlib_jars.to_vec();
} else {
test_kotlin_compiler_jars = Vec::new();
test_kotlin_stdlib_jars = kotlin_stdlib_jars.to_vec();
}
let mut test_ap_coords: Vec<(&str, &str)> = desc.ap_pairs();
test_ap_coords.extend(desc.test_ap_pairs());
let (test_ap_jars, test_ap_on_cp_jars) = if test_ap_coords.is_empty() {
(Vec::new(), Vec::new())
} else {
let ap_entries: Vec<DepEntry> = test_ap_coords
.iter()
.map(|(k, v)| DepEntry { key: k, version: v, repo_id: None, exclusions: vec![], classifier: None, allow_version_conflict: false })
.collect();
let jars = resolve(
&ap_entries,
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos.clone(),
progress: crate::parallel::try_get_sink().is_none(),
bom_imports: test_bom_gavs.clone(),
offline,
skip_version_ranges: false, error_on_version_conflict: false,
},
)
.context("test annotation-processor resolution failed")?;
let on_cp_coords = desc.test_ap_on_compile_classpath_coords();
let mut on_cp_jars: Vec<PathBuf> = Vec::new();
for coord in on_cp_coords {
let version = test_ap_coords
.iter()
.find(|(k, _)| *k == coord)
.map(|(_, v)| *v)
.expect("on-cp coord must be in test_ap_coords");
let single = resolve(
&[DepEntry { key: coord, version, repo_id: None, exclusions: vec![], classifier: None, allow_version_conflict: false }],
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos.clone(),
progress: false,
bom_imports: test_bom_gavs.clone(),
offline,
skip_version_ranges: false, error_on_version_conflict: false,
},
)
.with_context(|| {
format!("test annotation-processor classpath resolution failed for {}", coord)
})?;
on_cp_jars.extend(single);
}
(jars, on_cp_jars)
};
let test_classes_dir = project_root.join("target").join("test-classes");
std::fs::create_dir_all(&test_classes_dir)
.context("failed to create target/test-classes")?;
let toml_path = project_root.join("Curie.toml");
let test_manifest_path = project_root.join("target").join(".test-classes.toml");
let old_test_manifest = crate::class_manifest::load(&test_manifest_path)?;
let current_test_sources_set: std::collections::HashSet<String> = all_test_sources
.iter()
.filter_map(|p| p.canonicalize().ok())
.map(|p| p.to_string_lossy().into_owned())
.collect();
let canonical_test_target = project_root
.join("target")
.canonicalize()
.ok()
.and_then(|p| p.to_str().map(String::from));
let pre_pruned_tests: usize = match &old_test_manifest {
Some(old) => {
let stale = crate::class_manifest::stale_classes(
old,
None,
¤t_test_sources_set,
canonical_test_target.as_deref(),
);
crate::class_manifest::delete_classes(&test_classes_dir, &stale)?
}
None => 0,
};
let test_source_set = incremental::canonical_source_set(&all_test_sources);
let test_source_set_prev = incremental::load_source_set(
&incremental::source_set_stamp_path(&project_root.join("target"), TEST_SOURCE_SET_STAMP),
);
let test_source_set_changed =
incremental::source_set_changed(test_source_set_prev.as_ref(), &test_source_set);
let test_compile_status = if pre_pruned_tests > 0 {
CompileStatus::StaleClasses
} else if test_source_set_changed {
CompileStatus::SourceSetChanged
} else if old_test_manifest
.as_ref()
.is_some_and(|m| crate::class_manifest::has_missing_classes(m, &test_classes_dir))
{
CompileStatus::MissingClasses
} else {
needs_recompile(
&all_test_sources,
&test_classes_dir,
&toml_path,
&project_root.join("target"),
&[classes_dir],
)
};
let needs_recompile_tests = test_compile_status.needs_recompile();
if needs_recompile_tests {
let reason = if pre_pruned_tests > 0 {
" [stale classes removed]".to_string()
} else if test_source_set_changed {
" [source set changed]".to_string()
} else {
format!(" [{}]", test_compile_status.reason())
};
crate::parallel::emit(&crate::style::active(
"Compile tests",
&format!("{} source file(s){}", all_test_sources.len(), reason),
));
let mut shared_cp: Vec<PathBuf> = Vec::new();
shared_cp.push(classes_dir.to_path_buf());
if let Some(rd) = resources_dir {
shared_cp.push(rd.to_path_buf());
}
shared_cp.extend_from_slice(dep_jars);
shared_cp.extend_from_slice(&test_dep_jars);
shared_cp.extend_from_slice(extra_cp);
shared_cp.extend_from_slice(&test_ap_on_cp_jars);
shared_cp.extend_from_slice(&test_kotlin_stdlib_jars);
shared_cp.extend_from_slice(groovy_jars);
shared_cp.extend_from_slice(&spock_jars);
shared_cp.push(standalone_jar.clone());
if has_kotlin_tests {
let mut kotlinc = Command::new("java");
kotlinc.arg("--enable-native-access=ALL-UNNAMED");
kotlinc.arg("-cp").arg(classpath_string(&test_kotlin_compiler_jars));
kotlinc.arg("org.jetbrains.kotlin.cli.jvm.K2JVMCompiler");
kotlinc.arg("-no-stdlib").arg("-no-reflect");
kotlinc.arg("-module-name").arg(kotlin_test_module_name(desc));
kotlinc.arg("-d").arg(&test_classes_dir);
if !shared_cp.is_empty() {
kotlinc.arg("-cp").arg(classpath_string(&shared_cp));
}
for src in &kotlin_test_sources {
kotlinc.arg(src);
}
for src in &java_test_sources {
kotlinc.arg(src);
}
let status = crate::proc::spawn_cmd(&mut kotlinc)
.context("failed to invoke kotlinc for test compilation")?;
if !status.success() {
bail!("Kotlin test compilation failed");
}
}
if has_java_tests {
let wrapper_jar = crate::wrapper::ensure()?;
let mut javac = Command::new("java");
javac.arg("-jar").arg(&wrapper_jar);
javac.arg("--curie-manifest-out").arg(&test_manifest_path);
if let Some(release) = javac_release_arg(desc)? {
javac.arg("--release").arg(release);
}
if desc.java.preview_enabled() {
javac.arg("--enable-preview");
}
javac
.arg("-g")
.arg("-d")
.arg(&test_classes_dir);
let mut compile_cp: Vec<PathBuf> = Vec::new();
if has_kotlin_tests {
compile_cp.push(test_classes_dir.clone());
}
compile_cp.extend_from_slice(&shared_cp);
javac.arg("-cp").arg(classpath_string(&compile_cp));
if !test_ap_jars.is_empty() {
let gen_dir = project_root
.join("target")
.join("generated-test-sources")
.join("annotations");
std::fs::create_dir_all(&gen_dir).with_context(|| {
format!("failed to create {}", gen_dir.display())
})?;
javac.arg("-processorpath").arg(classpath_string(&test_ap_jars));
javac.arg("-s").arg(&gen_dir);
}
for (key, value) in desc.flat_test_ap_options() {
javac.arg(format!("-A{}={}", key, value));
}
for src in &java_test_sources {
javac.arg(src);
}
let status = crate::proc::spawn_cmd(&mut javac)
.context("failed to invoke java — is a JRE installed?")?;
if !status.success() {
bail!("test compilation failed");
}
if let Some(old) = &old_test_manifest {
if let Some(new) = crate::class_manifest::load(&test_manifest_path)? {
let stale = crate::class_manifest::stale_classes(
old, Some(&new), ¤t_test_sources_set, None,
);
let n = crate::class_manifest::delete_classes(&test_classes_dir, &stale)?;
if n > 0 {
crate::parallel::emit(&crate::style::stale(
"Stale tests",
&format!("removed {} orphaned class file{}", n, if n == 1 { "" } else { "s" }),
));
}
}
}
}
if has_groovy_tests {
let groovy_cp_jars: Vec<PathBuf> = {
if groovy_jars.is_empty() {
use crate::compile::GROOVY_COORD;
resolve(
&[DepEntry { key: GROOVY_COORD, version: desc.groovy.version(), repo_id: None, exclusions: vec![], classifier: None, allow_version_conflict: false }],
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos.clone(),
progress: crate::parallel::try_get_sink().is_none(),
bom_imports: test_bom_gavs.clone(),
offline,
skip_version_ranges: false, error_on_version_conflict: false,
},
)
.context("Groovy test compiler resolution failed")?
} else {
groovy_jars.to_vec()
}
};
let groovyc_process_cp: Vec<PathBuf> = {
let mut cp = groovy_cp_jars.clone();
cp.extend(shared_cp.iter().filter(|p| {
!p.file_name()
.map(|f| f.to_string_lossy().starts_with("junit-platform-console-standalone"))
.unwrap_or(false)
}).cloned());
cp
};
let mut groovyc = Command::new("java");
if let Some(arg) = crate::compile::groovy_target_bytecode_arg(desc) {
groovyc.arg(arg);
}
groovyc.arg("-cp").arg(classpath_string(&groovyc_process_cp));
groovyc.arg("org.codehaus.groovy.tools.FileSystemCompiler");
groovyc.arg("-d").arg(&test_classes_dir);
let gcp = crate::compile::groovyc_compiler_classpath(
&shared_cp,
&groovy_cp_jars,
if has_java_tests { Some(&test_classes_dir) } else { None },
);
if !gcp.is_empty() {
groovyc.arg("--classpath").arg(classpath_string(&gcp));
}
for src in &groovy_test_sources { groovyc.arg(src); }
let status = crate::proc::spawn_cmd(&mut groovyc)
.context("failed to invoke groovyc for test compilation")?;
if !status.success() {
bail!("Groovy test compilation failed");
}
}
if let Ok(version) = javac_version() {
write_javac_version_stamp(&project_root.join("target"), &version)?;
}
incremental::write_source_set(
&incremental::source_set_stamp_path(&project_root.join("target"), TEST_SOURCE_SET_STAMP),
&test_source_set,
)?;
} else {
crate::parallel::emit(&crate::style::up_to_date("Compile tests"));
}
let stamp_path = project_root.join("target").join(".test-stamp");
if filter.is_none() && !needs_test_run(&all_test_sources, classes_dir, &toml_path, &stamp_path, resources_dir, test_resources_dir) {
crate::parallel::emit(&crate::style::up_to_date("Tests"));
return Ok(());
}
let mut run_cp: Vec<PathBuf> = Vec::new();
run_cp.push(test_classes_dir.clone());
run_cp.push(classes_dir.to_path_buf());
if let Some(rd) = resources_dir {
run_cp.push(rd.to_path_buf());
}
if let Some(trd) = test_resources_dir {
run_cp.push(trd.to_path_buf());
}
run_cp.extend_from_slice(dep_jars);
run_cp.extend_from_slice(&test_dep_jars);
run_cp.extend_from_slice(extra_cp);
run_cp.extend_from_slice(&test_kotlin_stdlib_jars);
run_cp.extend_from_slice(groovy_jars);
run_cp.extend_from_slice(&spock_jars);
crate::parallel::emit("");
let sidecar_path = project_root.join("target").join("build.tests.json");
let output_dir = project_root.join("target").join("test-output");
if output_dir.exists() {
std::fs::remove_dir_all(&output_dir)
.context("failed to clear target/test-output")?;
}
std::fs::create_dir_all(&output_dir)
.context("failed to create target/test-output")?;
let effective_filter = if filter.is_some() {
filter
} else if !spock_jars.is_empty() {
Some(".*Tests?$|^Test.*|.*TestCase$|.*Spec$")
} else {
None
};
let runner_jar = crate::test_runner::ensure_runner_jar(&standalone_jar)
.context("failed to prepare Curie test runner")?;
let agent_coords = desc.test_dep_java_agent_coords();
let agent_jars = crate::java_agent::find_agent_jars(&agent_coords, &run_cp);
let coverage_dir = project_root.join("target").join("coverage");
let coverage_exec = coverage_dir.join("jacoco.exec");
let jacoco_agent_jar: Option<PathBuf> = if coverage {
std::fs::create_dir_all(&coverage_dir)
.context("failed to create target/coverage")?;
let default_repos = build::central_repos();
let extra_repos = build::extra_repos(desc);
let jar = crate::coverage::resolve_agent_jar(&default_repos, &extra_repos, offline)
.context("failed to resolve JaCoCo agent for coverage")?;
crate::parallel::emit(&crate::style::resolve("Coverage", "JaCoCo agent"));
Some(jar)
} else {
None
};
let mut extra_jvm_args: Vec<String> = Vec::new();
if let Some(ref agent_jar) = jacoco_agent_jar {
extra_jvm_args.push(crate::coverage::agent_jvm_arg(agent_jar, &coverage_exec));
}
let mut java = crate::test_runner::build_runner_command(
&runner_jar,
&standalone_jar,
&classpath_string(&run_cp),
&sidecar_path,
&output_dir,
effective_filter,
desc.java.preview_enabled(),
&agent_jars,
&extra_jvm_args,
);
let status = crate::proc::spawn_cmd(&mut java)
.context("failed to invoke java — is a JRE installed?")?;
crate::parallel::emit("");
if !status.success() {
bail!("tests failed");
}
if coverage && coverage_exec.exists() {
let default_repos = build::central_repos();
let extra_repos = build::extra_repos(desc);
let cli_jar = crate::coverage::resolve_cli_jar(&default_repos, &extra_repos, offline)
.context("failed to resolve JaCoCo CLI for report generation")?;
let source_dirs = discover_source_roots(project_root);
let summary = crate::coverage::generate_report(
&cli_jar,
&coverage_exec,
classes_dir,
&source_dirs,
&coverage_dir,
)?;
let html_index = coverage_dir.join("html").join("index.html");
let rel_html = html_index
.strip_prefix(project_root)
.unwrap_or(&html_index);
crate::parallel::emit(&crate::style::active("Coverage", &summary.summary_line()));
crate::parallel::emit(&crate::style::info(
"Report",
&rel_html.display().to_string(),
));
}
std::fs::write(&stamp_path, b"")
.with_context(|| format!("failed to write test stamp {}", stamp_path.display()))?;
Ok(())
}
fn discover_source_roots(project_root: &Path) -> Vec<PathBuf> {
use crate::compile::flat_package_src_dirs;
let mut roots: Vec<PathBuf> = Vec::new();
let maven_java = project_root.join("src").join("main").join("java");
if maven_java.exists() {
roots.push(maven_java);
}
let maven_kotlin = project_root.join("src").join("main").join("kotlin");
if maven_kotlin.exists() {
roots.push(maven_kotlin);
}
let maven_groovy = project_root.join("src").join("main").join("groovy");
if maven_groovy.exists() {
roots.push(maven_groovy);
}
roots.extend(flat_package_src_dirs(project_root));
roots
}
fn discover_test_sources(project_root: &Path) -> (Vec<PathBuf>, Vec<PathBuf>) {
let mut java_sources: Vec<PathBuf> = Vec::new();
let mut kotlin_sources: Vec<PathBuf> = Vec::new();
let test_java_src = project_root.join("src").join("test").join("java");
if test_java_src.exists() {
let separate: Vec<PathBuf> = walk_files(&test_java_src)
.filter(|e| e.file_name().to_string_lossy().ends_with(".java"))
.map(|e| e.into_path())
.collect();
java_sources.extend(separate);
}
let test_kotlin_src = project_root.join("src").join("test").join("kotlin");
if test_kotlin_src.exists() {
let separate: Vec<PathBuf> = walk_files(&test_kotlin_src)
.filter(|e| e.file_name().to_string_lossy().ends_with(".kt"))
.map(|e| e.into_path())
.collect();
kotlin_sources.extend(separate);
}
for pkg_dir in flat_package_src_dirs(project_root) {
let colocated_java: Vec<PathBuf> = walk_files(&pkg_dir)
.filter(|e| {
let name = e.file_name().to_string_lossy();
name.ends_with("Test.java")
|| name.ends_with("Tests.java")
|| name.ends_with("Spec.java")
})
.map(|e| e.into_path())
.collect();
java_sources.extend(colocated_java);
let colocated_kotlin: Vec<PathBuf> = walk_files(&pkg_dir)
.filter(|e| {
let name = e.file_name().to_string_lossy();
name.ends_with("Test.kt")
|| name.ends_with("Tests.kt")
|| name.ends_with("Spec.kt")
})
.map(|e| e.into_path())
.collect();
kotlin_sources.extend(colocated_kotlin);
}
for pkg_dir in flat_package_test_dirs(project_root) {
let java_int: Vec<PathBuf> = walk_files(&pkg_dir)
.filter(|e| e.file_name().to_string_lossy().ends_with(".java"))
.map(|e| e.into_path())
.collect();
java_sources.extend(java_int);
let kotlin_int: Vec<PathBuf> = walk_files(&pkg_dir)
.filter(|e| e.file_name().to_string_lossy().ends_with(".kt"))
.map(|e| e.into_path())
.collect();
kotlin_sources.extend(kotlin_int);
}
java_sources.sort();
java_sources.dedup();
kotlin_sources.sort();
kotlin_sources.dedup();
(java_sources, kotlin_sources)
}
fn discover_groovy_test_sources(project_root: &Path) -> Vec<PathBuf> {
let mut sources: Vec<PathBuf> = Vec::new();
let test_groovy = project_root.join("src").join("test").join("groovy");
if test_groovy.exists() {
let all: Vec<PathBuf> = walk_files(&test_groovy)
.filter(|e| e.file_name().to_string_lossy().ends_with(".groovy"))
.map(|e| e.into_path())
.collect();
sources.extend(all);
}
for pkg_dir in flat_package_src_dirs(project_root) {
let colocated: Vec<PathBuf> = walk_files(&pkg_dir)
.filter(|e| {
let name = e.file_name().to_string_lossy();
name.ends_with("Test.groovy")
|| name.ends_with("Tests.groovy")
|| name.ends_with("Spec.groovy")
})
.map(|e| e.into_path())
.collect();
sources.extend(colocated);
}
for pkg_dir in flat_package_test_dirs(project_root) {
let all: Vec<PathBuf> = walk_files(&pkg_dir)
.filter(|e| e.file_name().to_string_lossy().ends_with(".groovy"))
.map(|e| e.into_path())
.collect();
sources.extend(all);
}
sources.sort();
sources.dedup();
sources
}
fn resolve_standalone(
extra_repos: &[curie_deps::repo::Repository],
offline: bool,
junit_version: &str,
) -> Result<PathBuf> {
let coord = format!("{}:{}", JUNIT_STANDALONE_COORD, junit_version);
let jars = resolve(
&[DepEntry { key: JUNIT_STANDALONE_COORD, version: junit_version, repo_id: None, exclusions: vec![], classifier: None, allow_version_conflict: false }],
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos.to_vec(),
progress: false,
bom_imports: vec![],
offline,
skip_version_ranges: false, error_on_version_conflict: false,
},
)
.with_context(|| format!("failed to resolve {}", coord))?;
jars.into_iter()
.find(|p| {
p.file_name()
.map(|f| {
let s = f.to_string_lossy();
s.starts_with("junit-platform-console-standalone")
})
.unwrap_or(false)
})
.with_context(|| {
format!(
"junit-platform-console-standalone-{}.jar not found after resolution",
junit_version
)
})
}
fn needs_test_run(
test_sources: &[PathBuf],
classes_dir: &Path,
toml_path: &Path,
stamp_path: &Path,
resources_dir: Option<&Path>,
test_resources_dir: Option<&Path>,
) -> bool {
let mut inputs = Inputs::new();
inputs
.add_paths(test_sources)
.add_file(toml_path)
.add_dir(classes_dir)
.add_dir_opt(resources_dir)
.add_dir_opt(test_resources_dir);
!Stamp::of(stamp_path).covers(&inputs)
}
fn kotlin_test_module_name(desc: &descriptor::Descriptor) -> String {
format!("{}-test", desc.buildable_name())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn kotlin_test_module_name_appends_test_suffix() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("Curie.toml"),
"[application]\nname = \"hello-kotlin\"\nversion = \"0.1.0\"\nmainClass = \"Main\"\n\
[java]\nreleaseVersion = \"21\"\n",
)
.unwrap();
let desc = descriptor::load(dir.path()).unwrap();
assert_eq!(kotlin_test_module_name(&desc), "hello-kotlin-test");
}
#[test]
fn maven_layout_test_named_files_are_not_test_sources() {
let dir = tempfile::tempdir().unwrap();
let main_java = dir.path().join("src").join("main").join("java").join("com").join("example");
fs::create_dir_all(&main_java).unwrap();
fs::write(main_java.join("LoadTest.java"), b"public class LoadTest {}").unwrap();
fs::write(main_java.join("SmokeTests.java"), b"public class SmokeTests {}").unwrap();
fs::write(main_java.join("OpenApiSpec.java"), b"public class OpenApiSpec {}").unwrap();
let (java, _kotlin) = discover_test_sources(dir.path());
assert!(
java.is_empty(),
"test-named files in src/main/java must not be discovered as test sources; got: {java:?}"
);
}
#[test]
fn separate_test_tree_java_is_discovered() {
let dir = tempfile::tempdir().unwrap();
let test_java = dir.path().join("src").join("test").join("java").join("com").join("example");
fs::create_dir_all(&test_java).unwrap();
fs::write(test_java.join("GreeterTest.java"), b"public class GreeterTest {}").unwrap();
fs::write(test_java.join("HelperUtil.java"), b"public class HelperUtil {}").unwrap();
let (java, _kotlin) = discover_test_sources(dir.path());
assert_eq!(java.len(), 2, "all files in src/test/java are test sources; got: {java:?}");
}
#[test]
fn flat_package_colocated_tests_are_discovered() {
let dir = tempfile::tempdir().unwrap();
let pkg = dir.path().join("src").join("com.example");
fs::create_dir_all(&pkg).unwrap();
fs::write(pkg.join("Greeter.java"), b"package com.example; class Greeter {}").unwrap();
fs::write(pkg.join("GreeterTest.java"), b"package com.example; class GreeterTest {}").unwrap();
fs::write(pkg.join("GreeterSpec.java"), b"package com.example; class GreeterSpec {}").unwrap();
let (java, _kotlin) = discover_test_sources(dir.path());
assert_eq!(java.len(), 2, "only *Test.java and *Spec.java from flat-package are test sources; got: {java:?}");
assert!(java.iter().any(|p| p.ends_with("GreeterTest.java")));
assert!(java.iter().any(|p| p.ends_with("GreeterSpec.java")));
}
#[test]
fn groovy_colocated_tests_not_from_maven_layout() {
let dir = tempfile::tempdir().unwrap();
let main_groovy = dir.path().join("src").join("main").join("groovy").join("com").join("example");
fs::create_dir_all(&main_groovy).unwrap();
fs::write(main_groovy.join("GreeterSpec.groovy"), b"package com.example; class GreeterSpec {}").unwrap();
let sources = discover_groovy_test_sources(dir.path());
assert!(
sources.is_empty(),
"test-named files in src/main/groovy must not be test sources; got: {sources:?}"
);
}
#[test]
fn groovy_separate_test_tree_is_discovered() {
let dir = tempfile::tempdir().unwrap();
let test_groovy = dir.path().join("src").join("test").join("groovy").join("com").join("example");
fs::create_dir_all(&test_groovy).unwrap();
fs::write(test_groovy.join("GreeterSpec.groovy"), b"package com.example; class GreeterSpec {}").unwrap();
let sources = discover_groovy_test_sources(dir.path());
assert_eq!(sources.len(), 1, "src/test/groovy files must be test sources; got: {sources:?}");
}
}