in sources/amper-cli/src/org/jetbrains/amper/tasks/jvm/JvmTestTask.kt [79:211]
override suspend fun run(dependenciesResult: List<TaskResult>, executionContext: TaskGraphExecutionContext): TaskResult {
val jvmTestSettings = module.leafFragments.single { it.platform == platform && it.isTest }.settings.jvm.test
// https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.10.1/junit-platform-console-standalone-1.10.1.jar
val junitConsoleUrl = Downloader.getUriForMavenArtifact(
mavenRepository = "https://repo1.maven.org/maven2",
groupId = "org.junit.platform",
artifactId = "junit-platform-console-standalone",
packaging = "jar",
version = jvmTestSettings.junitPlatformVersion,
).toString()
// test task depends on compile test task
val compileTask = dependenciesResult.filterIsInstance<JvmCompileTask.Result>().singleOrNull()
?: error("${JvmCompileTask::class.simpleName} result is not found in dependencies")
if (compileTask.classesOutputRoots.all { it.listDirectoryEntries().isEmpty() }) {
logger.warn("No test classes, skipping test execution for module '${module.userReadableName}'")
return EmptyTaskResult
}
// test task depends on test jvm classpath task
val jvmRuntimeClasspathTask = dependenciesResult.filterIsInstance<JvmRuntimeClasspathTask.Result>().singleOrNull()
?: error("${JvmRuntimeClasspathTask::class.simpleName} result is not found in dependencies")
val userTestRuntimeClasspath = jvmRuntimeClasspathTask.jvmRuntimeClasspath
val junitConsole = Downloader.downloadFileToCacheLocation(junitConsoleUrl, userCacheRoot)
// TODO use maven instead of packing this in the distribution?
val amperJUnitListenersJars = extractJUnitListenersClasspath()
val jdk = jdkProvider.getJdkOrUserError(module.jdkSettings)
cleanDirectory(taskOutputRoot.path)
val reportsDir = buildOutputRoot.path / "reports" / module.userReadableName / platform.schemaValue
cleanDirectory(reportsDir)
val junitArgs = buildList {
add("--disable-banner")
// TODO I certainly want to have it here by default (too many real life errors when tests are skipped for a some reason)
// but probably there should be an option to disable it
add("--fail-if-no-tests")
add("--reports-dir=${reportsDir}")
val filterArguments = runSettings.testFilters.map { it.toJUnitArgument() }
addAll(filterArguments)
if (filterArguments.isEmpty() ||
filterArguments.any { it.startsWith("--include") || it.startsWith("--exclude") }) {
for (classesOutputRoot in compileTask.classesOutputRoots) {
add("--scan-class-path=${classesOutputRoot}")
}
}
add("--details=summary") // disable default console tree output, just print the summary
}
val jvmArgs = buildList {
add("-ea")
if (runSettings.testResultsFormat == TestResultsFormat.Pretty) {
add("-Dorg.jetbrains.amper.junit.listener.console.enabled=true")
// We don't use inherited IO when starting the test launcher process, so the Mordant Terminal library
// inside the test launcher cannot detect the supported features of the current console.
// This is why we currently just "transfer" the detected features via CLI arguments.
// Using ProcessBuilder.inheritIO() would make any auto-detection in the test launcher work, but then the
// test launcher output doesn't mix well with our task progress renderer.
add("-Dorg.jetbrains.amper.junit.listener.console.ansiLevel=${terminal.terminalInfo.ansiLevel}")
add("-Dorg.jetbrains.amper.junit.listener.console.ansiHyperlinks=${terminal.terminalInfo.ansiHyperLinks}")
}
if (runSettings.testResultsFormat == TestResultsFormat.TeamCity) {
add("-Dorg.jetbrains.amper.junit.listener.teamcity.enabled=true")
}
// The JUnit Vintage engine (used to run JUnit 4 tests) is deprecated since JUnit Platform 6.0.0.
// The built-in warning about this inside the JUnit Platform is not user-friendly (it talks about the JUnit
// Vintage engine, which users shouldn't have to know about). Instead, if we want to warn users about this,
// we should do so directly in our frontend when they choose 'junit-4'. See AMPER-4826
if (ComparableVersion(jvmTestSettings.junitPlatformVersion) >= FirstJUnitPlatformVersionWithDeprecatedVintageEngine) {
add("-Djunit.vintage.discovery.issue.reporting.enabled=false")
}
addAll(jvmTestSettings.systemProperties.map { (k, v) -> "-D${k.value}=${v.value}" })
addAll(jvmTestSettings.freeJvmArgs)
addAll(runSettings.userJvmArgs)
}
// We pass both the user classpath and the "infra" classpath (JUnit itself and our listeners) together
// instead of using the separate --class-path option of the JUnit Console Launcher itself.
// This is intentional, to work around this JUnit issue: https://github.com/junit-team/junit5/issues/4469.
// In short, the separate --class-path option of the launcher was mostly meant as a convenience, but in
// some cases it is harmful. It is loaded in a separate class loader that is closed at the end of the tests,
// but before the test JVM shuts down. If the user code uses shutdown hooks (e.g., in testcontainers), they
// would not be able to load user classes anymore because of this.
val testJvmClasspath = listOf(junitConsole) + amperJUnitListenersJars + userTestRuntimeClasspath
// TODO should be customizable?
// There is no way of knowing what the working dir should be for generated/unresolved test modules,
// the project root is a somewhat safe choice.
val workingDirectory = module.source.moduleDir
return spanBuilder("junit-platform-console-standalone")
.setAttribute("junit-platform-console-standalone", junitConsole.pathString)
.setAttribute("working-dir", workingDirectory.pathString)
.setListAttribute("tests-classpath", userTestRuntimeClasspath.map { it.pathString })
.setListAttribute("jvm-args", jvmArgs)
.setListAttribute("junit-args", junitArgs)
.use {
logger.info("Testing module '${module.userReadableName}' for platform '${platform.pretty}'...")
DeadLockMonitor.disable()
val result = processRunner.runJava(
jdk = jdk,
workingDir = workingDirectory,
mainClass = "org.junit.platform.console.ConsoleLauncher",
classpath = testJvmClasspath,
programArgs = listOf("execute") + junitArgs,
argsMode = ArgsMode.ArgFile(tempRoot = tempRoot),
jvmArgs = jvmArgs,
outputListener = PrintToTerminalProcessOutputListener(terminal),
)
// TODO exit code from junit launcher should be carefully become some kind of exit code for entire Amper run
// + one more interesting case: if we reported some failed tests to TeamCity, exit code of Amper should be 0,
// since the build will be failed anyway and it'll just have one more useless build failure about exit code
if (result.exitCode != 0) {
val meaning = if (result.exitCode == 2) " (no tests were discovered)" else ""
userReadableError("JVM tests failed for module '${module.userReadableName}' with exit code ${result.exitCode}$meaning (see errors above)")
}
EmptyTaskResult
}
}