override suspend fun run()

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
            }
    }