diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/TestingOptions.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/TestingOptions.kt index 5d24c99b3fd4..ddb466aed64a 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/TestingOptions.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/TestingOptions.kt @@ -153,14 +153,6 @@ open class TestingOptions { */ var isEnableCausalProfiling: Boolean = getBooleanProperty("intellij.build.test.enable.causal.profiling", false) - /** - * Pattern to match tests in [mainModule] or default main module tests compilation outputs. - * Tests from each matched class will be executed in a forked Runtime. - * - * E.g. "com/intellij/util/ui/standalone/ **Test.class" - */ - var batchTestIncludes: String? = System.getProperty("intellij.build.test.batchTest.includes") - /** * Run only whole classes/packages in forked Runtime * Allowed values: diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/TestingTasksImpl.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/TestingTasksImpl.kt index c38bf5fa2339..1f22fc0f73a9 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/TestingTasksImpl.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/TestingTasksImpl.kt @@ -10,7 +10,6 @@ import com.intellij.idea.IJIgnore import com.intellij.openapi.application.PathManager import com.intellij.openapi.util.Pair import com.intellij.openapi.util.SystemInfoRt -import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtilRt import com.intellij.openapi.util.io.NioFiles import com.intellij.openapi.util.text.StringUtilRt @@ -19,7 +18,6 @@ import com.intellij.platform.ijent.community.buildConstants.MULTI_ROUTING_FILE_S import com.intellij.testFramework.SkipInHeadlessEnvironment import com.intellij.util.io.awaitExit import com.intellij.util.lang.UrlClassLoader -import io.opentelemetry.api.common.AttributeKey import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -39,8 +37,6 @@ import org.jetbrains.intellij.build.causal.CausalProfilingOptions import org.jetbrains.intellij.build.dependencies.TeamCityHelper import org.jetbrains.intellij.build.impl.coverage.Coverage import org.jetbrains.intellij.build.impl.coverage.CoverageImpl -import org.jetbrains.intellij.build.io.ZipEntryProcessorResult -import org.jetbrains.intellij.build.io.readZipFile import org.jetbrains.intellij.build.io.runProcess import org.jetbrains.intellij.build.telemetry.TraceManager.spanBuilder import org.jetbrains.intellij.build.telemetry.block @@ -59,13 +55,11 @@ import java.nio.charset.Charset import java.nio.file.AccessDeniedException import java.nio.file.Files import java.nio.file.Path -import java.util.regex.Pattern import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.absolutePathString import kotlin.io.path.copyTo import kotlin.io.path.deleteRecursively import kotlin.io.path.exists -import kotlin.io.path.extension import kotlin.io.path.isRegularFile import kotlin.io.path.outputStream import kotlin.io.path.pathString @@ -261,11 +255,6 @@ internal class TestingTasksImpl(context: CompilationContext, private val options else if (options.testPatterns != null && options.testGroups != TestingOptions.ALL_EXCLUDE_DEFINED_GROUP) { warnOptionIgnored("intellij.build.test.patterns", "intellij.build.test.groups") } - if (options.batchTestIncludes != null && !isRunningInBatchMode) { - context.messages.warning( - "'intellij.build.test.batchTest.includes' option will be ignored as other tests matching options are specified." - ) - } if (options.validateMainModule && mainModule.isNullOrEmpty()) { context.messages.logErrorAndThrow("'intellij.build.test.main.module.validate' option requires 'intellij.build.test.main.module' to be set") @@ -459,7 +448,7 @@ internal class TestingTasksImpl(context: CompilationContext, private val options } } - if (isBootstrapSuiteDefault && !isRunningInBatchMode) { + if (isBootstrapSuiteDefault) { //module with "com.intellij.TestAll" which output should be found in `testClasspath + modulePath` val testFrameworkCoreModule = outputProvider.findRequiredModule("intellij.platform.testFramework.core") val testFrameworkCoreModuleOutputRoots = outputProvider.getModuleOutputRoots(testFrameworkCoreModule) @@ -508,10 +497,7 @@ internal class TestingTasksImpl(context: CompilationContext, private val options val allJvmArgs = ArrayList(jvmArgs) prepareEnvForTestRun(jvmArgs = allJvmArgs, systemProperties = systemProperties, classPath = bootstrapClasspath, remoteDebugging = remoteDebugging) val messages = context.messages - if (isRunningInBatchMode) { - messages.info("Running tests from $mainModule matched by '${options.batchTestIncludes}' pattern.") - } - else if (!testPatterns.isNullOrEmpty()) { + if (!testPatterns.isNullOrEmpty()) { messages.info("Starting tests from patterns '${testPatterns}' from classpath of module '${mainModule}'") } else { @@ -790,140 +776,6 @@ internal class TestingTasksImpl(context: CompilationContext, private val options }.flatMap { it.getCompleted() } } - private fun getTestClassesForModule(mainModule: String, filteringPattern: Pattern = Pattern.compile(".*\\.class")): List { - val testClasses: List = context.outputProvider.getModuleOutputRoots(context.outputProvider.findRequiredModule(mainModule), forTests = true).flatMap { root -> - if (root.isRegularFile() && root.extension == "jar") { - val classes = ArrayList() - val regex = filteringPattern.toRegex() - readZipFile(root) { name, _ -> - if (FileUtilRt.toSystemIndependentName(name).matches(regex)) { - classes.add(name) - } - ZipEntryProcessorResult.CONTINUE - } - classes - } - else { - Files.walk(root).use { stream -> - stream.map { FileUtilRt.toSystemIndependentName(root.relativize(it).toString()) }.filter { - filteringPattern.matcher(it).matches() - }.toList() - } ?: listOf() - } - } - - if (testClasses.isEmpty()) { - throw RuntimeException("No tests were found in module '$mainModule' with $filteringPattern") - } - - return testClasses - } - - private suspend fun runInBatchMode( - mainModule: String, - systemProperties: Map, - jvmArgs: List, - envVariables: Map, - bootstrapClasspath: List, - testClasspath: List, - devBuildServerSettings: DevBuildServerSettings?, - ) { - val pattern = Pattern.compile(FileUtil.convertAntToRegexp(options.batchTestIncludes!!)) - val testClasses = getTestClassesForModule(mainModule = mainModule, filteringPattern = pattern) - - val files = testClasspath.map { Path.of(it) } - val loader = UrlClassLoader.build().files(files).get() - - @Suppress("UNCHECKED_CAST") - val testAnnotation4 = loader.loadClass("org.junit.Test") as Class - - @Suppress("UNCHECKED_CAST") - val testAnnotation5 = loader.loadClass("org.junit.jupiter.api.Test") as Class - - @Suppress("UNCHECKED_CAST") - val testFactoryAnnotation5 = loader.loadClass("org.junit.jupiter.api.TestFactory") as Class - - var noTestsInAllClasses = true - for (testClass in testClasses) { - val qName = FileUtilRt.getNameWithoutExtension(testClass).replace('/', '.') - try { - var noTests = true - val aClass = loader.loadClass(qName) - - val jUnit4And5TestMethods = getAnnotatedTestMethods(aClass, testAnnotation4, testAnnotation5, testFactoryAnnotation5) - - // Run JUnit 4 and 5 whole test classes separately - if (options.isDedicatedTestRuntime != "false" && jUnit4And5TestMethods.isNotEmpty()) { - val exitCode = runJUnit5Engine( - mainModule = mainModule, - systemProperties = systemProperties, - jvmArgs = jvmArgs, - envVariables = envVariables, - bootstrapClasspath = bootstrapClasspath, - modulePath = null, - testClasspath = testClasspath, - suiteName = qName, - methodName = null, - devBuildSettings = devBuildServerSettings, - ) - noTests = exitCode == NO_TESTS_ERROR - } - // Run JUnit 4 and 5 test methods separately if any - else if (jUnit4And5TestMethods.isNotEmpty()) { - for (method in jUnit4And5TestMethods) { - val exitCode = runJUnit5Engine( - mainModule = mainModule, - systemProperties = systemProperties, - jvmArgs = jvmArgs, - envVariables = envVariables, - bootstrapClasspath = bootstrapClasspath, - modulePath = null, - testClasspath = testClasspath, - suiteName = qName, - methodName = method, - devBuildSettings = devBuildServerSettings, - ) - noTests = noTests && exitCode == NO_TESTS_ERROR - } - } - - // Fallback to running whole class (JUnit 3+4) - if (noTests) { - val exitCode = runJUnit5Engine( - mainModule = mainModule, - systemProperties = systemProperties, - jvmArgs = jvmArgs, - envVariables = envVariables, - bootstrapClasspath = bootstrapClasspath, - modulePath = null, - testClasspath = testClasspath, - suiteName = qName, - methodName = null, - devBuildSettings = devBuildServerSettings, - ) - noTests = exitCode == NO_TESTS_ERROR - } - noTestsInAllClasses = noTestsInAllClasses && noTests - } - catch (e: Throwable) { - throw RuntimeException("Failed to process $qName", e) - } - } - - if (noTestsInAllClasses) { - throw RuntimeException("No tests were found in $mainModule with $pattern") - } - } - - private fun getAnnotatedTestMethods(aClass: Class<*>, vararg annotations: Class): List { - return aClass.methods - .asSequence() - .filter { m -> Modifier.isPublic(m.modifiers) } - .filter { m -> annotations.any { a -> m.isAnnotationPresent(a) } } - .map { m -> m.name } - .toList() - } - private suspend fun runJUnit5Engine( mainModule: String, systemProperties: Map, @@ -934,291 +786,274 @@ internal class TestingTasksImpl(context: CompilationContext, private val options testClasspath: List, devBuildServerSettings: DevBuildServerSettings?, ) { - if (isRunningInBatchMode) { - spanBuilder("run tests in batch mode") - .setAttribute(AttributeKey.stringKey("pattern"), options.batchTestIncludes ?: "") - .use { - runInBatchMode( - mainModule = mainModule, - systemProperties = systemProperties, - jvmArgs = jvmArgs, - envVariables = envVariables, - bootstrapClasspath = bootstrapClasspath, - testClasspath = testClasspath, - devBuildServerSettings = devBuildServerSettings, - ) + val messages = context.messages + if (options.isDedicatedTestRuntime != "false") { + if (options.isDedicatedTestRuntime != "class" && options.isDedicatedTestRuntime != "package") { + messages.logErrorAndThrow("Unsupported 'intellij.build.test.dedicated.runtime' value: ${options.isDedicatedTestRuntime}. Expected 'class', 'package' or 'false'") + } + messages.info("Will run tests in dedicated runtimes ('${options.isDedicatedTestRuntime}')") + // First, collect all tests for both JUnit5 and JUnit3+4 + val testClassesJUnit5 = spanBuilder("collect junit 5 tests").use { + if (options.shouldSkipJUnit5Tests) { + messages.warning("JUnit 5 tests collections is skipped") + return@use emptyList() } + + val testClassesListFile = Files.createTempFile("tests-to-run-", ".list").apply { Files.delete(this) } + runJUnit5Engine( + mainModule = mainModule, + systemProperties = systemProperties + ("intellij.build.test.list.classes" to testClassesListFile.absolutePathString()), + jvmArgs = jvmArgs, + envVariables = envVariables, + bootstrapClasspath = bootstrapClasspath, + modulePath = modulePath, + testClasspath = testClasspath, + suiteName = null, + methodName = null, + devBuildSettings = null, + ) + testClassesListFile.let { if (Files.exists(it)) it.readLines() else emptyList() } + } + + val testClassesJUnit34 = block("collect junit 3+4 tests") { + if (options.shouldSkipJUnit34Tests) { + messages.warning("JUnit 3+4 tests collections is skipped") + return@block emptyList() + } + + val testClassesListFile = Files.createTempFile("tests-to-run-", ".list").apply { Files.delete(this) } + runJUnit5Engine( + mainModule = mainModule, + systemProperties = systemProperties + ("intellij.build.test.list.classes" to testClassesListFile.absolutePathString()), + jvmArgs = jvmArgs, + envVariables = envVariables, + bootstrapClasspath = bootstrapClasspath, + modulePath = modulePath, + testClasspath = testClasspath, + suiteName = options.bootstrapSuite, + methodName = null, + devBuildSettings = null, + ) + return@block testClassesListFile.let { if (Files.exists(it)) it.readLines() else emptyList() } + } + + if (testClassesJUnit5.isEmpty() && testClassesJUnit34.isEmpty() && + // a bucket might be empty for run configurations with too few tests due to imperfect tests balancing + options.bucketsCount < 2) { + throw NoTestsFound() + } + + if (options.isDedicatedTestRuntime == "class") { + var hasFailures = false + + suspend fun runOneClass(testClassName: String) { + val exitCode = block("running test class '$testClassName'") { + runJUnit5Engine( + mainModule = mainModule, + systemProperties = systemProperties + ("idea.performance.tests.discovery.filter" to "true"), + jvmArgs = jvmArgs, + envVariables = envVariables, + bootstrapClasspath = bootstrapClasspath, + modulePath = modulePath, + testClasspath = testClasspath, + suiteName = testClassName, + methodName = null, + devBuildSettings = devBuildServerSettings, + ) + } + if (exitCode == 1) hasFailures = true // reported as test failure or assertNoUnhandledExceptions if exception + else if (exitCode == NO_TESTS_ERROR) throw NoTestsFound() + else if (exitCode != 0) throw RuntimeException("Unexpected exit code $exitCode when running tests in dedicated runtime (class mode)") + } + + if (testClassesJUnit5.isNotEmpty()) { + messages.info("Will run JUnit 5 tests:\n${testClassesJUnit5.joinToString("\n")}") + for (s in testClassesJUnit5) { + runOneClass(s) + } + } + if (testClassesJUnit34.isNotEmpty()) { + messages.info("Will run JUnit 3+4 tests:\n${testClassesJUnit34.joinToString("\n")}") + for (s in testClassesJUnit34) { + runOneClass(s) + } + } + + // On TeamCity test failures themselves control the build status, no need to report them as additional errors + if (hasFailures && !TeamCityHelper.isUnderTeamCity) { + throw RuntimeException("Tests failed in dedicated runtime (class mode)") + } + } + else if (options.isDedicatedTestRuntime == "package") { + var hasFailures = false + + fun groupByPackages(tests: List): Map> { + return tests.groupBy { + val i = it.lastIndexOf('.') + if (i != -1) it.substring(0, i) else "" + } + } + + suspend fun runOnePackage(entry: Map.Entry>) { + val packageName = entry.key + val classes = entry.value + + val exitCode = block("running tests in package '$packageName'") { + runJUnit5Engine( + mainModule = mainModule, + systemProperties = systemProperties + ("idea.performance.tests.discovery.filter" to "true"), + jvmArgs = jvmArgs, + envVariables = envVariables, + bootstrapClasspath = bootstrapClasspath, + modulePath = modulePath, + testClasspath = testClasspath, + suiteName = "__classes__", + methodName = classes.joinToString(";"), + devBuildSettings = devBuildServerSettings, + ) + } + if (exitCode == 1) hasFailures = true // reported as test failure or assertNoUnhandledExceptions if exception + else if (exitCode == NO_TESTS_ERROR) throw NoTestsFound() + else if (exitCode != 0) throw RuntimeException("Unexpected exit code $exitCode when running tests in dedicated runtime (package mode)") + } + + if (testClassesJUnit5.isNotEmpty()) { + val packages = groupByPackages(testClassesJUnit5) + messages.info(packages.entries.joinToString(prefix = "Will run JUnit 5 packages:\n", separator = "\n") { e -> + e.value.joinToString(prefix = "${e.key}\n ", separator = "\n ") + }) + for (entry in packages) { + runOnePackage(entry) + } + } + if (testClassesJUnit34.isNotEmpty()) { + val packages = groupByPackages(testClassesJUnit34) + messages.info(packages.entries.joinToString(prefix = "Will run JUnit 3+4 packages:\n", separator = "\n") { e -> + e.value.joinToString(prefix = "${e.key}\n ", separator = "\n ") + }) + for (entry in packages) { + runOnePackage(entry) + } + } + + // On TeamCity test failures themselves control the build status, no need to report them as additional errors + if (hasFailures && !TeamCityHelper.isUnderTeamCity) { + throw RuntimeException("Tests failed in dedicated runtime (package mode)") + } + } } else { - val messages = context.messages - if (options.isDedicatedTestRuntime != "false") { - if (options.isDedicatedTestRuntime != "class" && options.isDedicatedTestRuntime != "package") { - messages.logErrorAndThrow("Unsupported 'intellij.build.test.dedicated.runtime' value: ${options.isDedicatedTestRuntime}. Expected 'class', 'package' or 'false'") + val failedClassesJUnit5List = Files.createTempFile("failed-classes-junit5-", ".list").apply { Files.delete(this) } + val failedClassesJUnit34List = Files.createTempFile("failed-classes-junit34-", ".list").apply { Files.delete(this) } + val additionalPropertiesJUnit5: Map = failedClassesJUnit5List.let { + if (options.attemptCount > 1) mapOf("intellij.build.test.retries.failedClasses.file" to "$it", "intellij.build.test.list.file" to "$it") + else emptyMap() + } + val additionalPropertiesJUnit34: Map = failedClassesJUnit34List.let { + if (options.attemptCount > 1) mapOf("intellij.build.test.retries.failedClasses.file" to "$it", "intellij.build.test.list.file" to "$it") + else emptyMap() + } + var runJUnit5 = !options.shouldSkipJUnit5Tests + var runJUnit34 = !options.shouldSkipJUnit34Tests + var lastExitCode5 = 0 + var lastExitCode34 = 0 + for (attempt in 1..options.attemptCount) { + if (!runJUnit5 && !runJUnit34) break + val spanNameSuffix = if (options.attemptCount > 1) " (attempt $attempt)" else "" + val systemPath = systemProperties[PathManager.PROPERTY_SYSTEM_PATH] + + val additionalProperties = mutableMapOf(PathManager.PROPERTY_LOG_PATH to "$systemPath${File.separator}log${File.separator}junit5") + if (attempt > 1) { + additionalProperties["intellij.build.test.ignoreFirstAndLastTests"] = "true" } - messages.info("Will run tests in dedicated runtimes ('${options.isDedicatedTestRuntime}')") - // First, collect all tests for both JUnit5 and JUnit3+4 - val testClassesJUnit5 = spanBuilder("collect junit 5 tests").use { - if (options.shouldSkipJUnit5Tests) { - messages.warning("JUnit 5 tests collections is skipped") - return@use emptyList() + + val exitCode5: Int = if (runJUnit5) { + block("run junit 5 tests${spanNameSuffix}") { + runJUnit5Engine( + mainModule = mainModule, + systemProperties = systemProperties + additionalProperties + additionalPropertiesJUnit5, + jvmArgs = jvmArgs, + envVariables = envVariables, + bootstrapClasspath = bootstrapClasspath, + modulePath = modulePath, + testClasspath = testClasspath, + suiteName = null, + methodName = null, + devBuildSettings = devBuildServerSettings, + ) } - - val testClassesListFile = Files.createTempFile("tests-to-run-", ".list").apply { Files.delete(this) } - runJUnit5Engine( - mainModule = mainModule, - systemProperties = systemProperties + ("intellij.build.test.list.classes" to testClassesListFile.absolutePathString()), - jvmArgs = jvmArgs, - envVariables = envVariables, - bootstrapClasspath = bootstrapClasspath, - modulePath = modulePath, - testClasspath = testClasspath, - suiteName = null, - methodName = null, - devBuildSettings = null, - ) - testClassesListFile.let { if (Files.exists(it)) it.readLines() else emptyList() } } + else { + 0 + } + if (runJUnit5) lastExitCode5 = exitCode5 - val testClassesJUnit34 = block("collect junit 3+4 tests") { - if (options.shouldSkipJUnit34Tests) { - messages.warning("JUnit 3+4 tests collections is skipped") - return@block emptyList() + additionalProperties[PathManager.PROPERTY_LOG_PATH] = "$systemPath${File.separator}log${File.separator}junit3and4" + val exitCode34: Int = if (runJUnit34) { + block("run junit 3+4 tests${spanNameSuffix}") { + runJUnit5Engine( + mainModule = mainModule, + systemProperties = systemProperties + additionalProperties + additionalPropertiesJUnit34, + jvmArgs = jvmArgs, + envVariables = envVariables, + bootstrapClasspath = bootstrapClasspath, + modulePath = modulePath, + testClasspath = testClasspath, + suiteName = options.bootstrapSuite, + methodName = null, + devBuildSettings = devBuildServerSettings, + ) } - - val testClassesListFile = Files.createTempFile("tests-to-run-", ".list").apply { Files.delete(this) } - runJUnit5Engine( - mainModule = mainModule, - systemProperties = systemProperties + ("intellij.build.test.list.classes" to testClassesListFile.absolutePathString()), - jvmArgs = jvmArgs, - envVariables = envVariables, - bootstrapClasspath = bootstrapClasspath, - modulePath = modulePath, - testClasspath = testClasspath, - suiteName = options.bootstrapSuite, - methodName = null, - devBuildSettings = null, - ) - return@block testClassesListFile.let { if (Files.exists(it)) it.readLines() else emptyList() } } + else { + 0 + } + if (runJUnit34) lastExitCode34 = exitCode34 - if (testClassesJUnit5.isEmpty() && testClassesJUnit34.isEmpty() && + if (exitCode5 == NO_TESTS_ERROR && exitCode34 == NO_TESTS_ERROR && + // only check on the first (full) attempt + attempt == 1 && // a bucket might be empty for run configurations with too few tests due to imperfect tests balancing options.bucketsCount < 2) { throw NoTestsFound() } + if (exitCode5 != 0 && exitCode5 != 1 && exitCode5 != NO_TESTS_ERROR) { + throw RuntimeException("Unexpected exit code $exitCode5 when running JUnit 5 tests") + } + if (exitCode34 != 0 && exitCode34 != 1 && exitCode34 != NO_TESTS_ERROR) { + throw RuntimeException("Unexpected exit code $exitCode34 when running JUnit 3+4 tests") + } - if (options.isDedicatedTestRuntime == "class") { - var hasFailures = false - - suspend fun runOneClass(testClassName: String) { - val exitCode = block("running test class '$testClassName'") { - runJUnit5Engine( - mainModule = mainModule, - systemProperties = systemProperties + ("idea.performance.tests.discovery.filter" to "true"), - jvmArgs = jvmArgs, - envVariables = envVariables, - bootstrapClasspath = bootstrapClasspath, - modulePath = modulePath, - testClasspath = testClasspath, - suiteName = testClassName, - methodName = null, - devBuildSettings = devBuildServerSettings, - ) - } - if (exitCode == 1) hasFailures = true // reported as test failure or assertNoUnhandledExceptions if exception - else if (exitCode == NO_TESTS_ERROR) throw NoTestsFound() - else if (exitCode != 0) throw RuntimeException("Unexpected exit code $exitCode when running tests in dedicated runtime (class mode)") + if (runJUnit5) { + val failedClassesJUnit5 = failedClassesJUnit5List.let { if (Files.exists(it)) it.readLines() else emptyList() } + if (failedClassesJUnit5.isNotEmpty()) { + messages.warning("Will rerun JUnit 5 tests: $failedClassesJUnit5") } - - if (testClassesJUnit5.isNotEmpty()) { - messages.info("Will run JUnit 5 tests:\n${testClassesJUnit5.joinToString("\n")}") - for (s in testClassesJUnit5) { - runOneClass(s) - } - } - if (testClassesJUnit34.isNotEmpty()) { - messages.info("Will run JUnit 3+4 tests:\n${testClassesJUnit34.joinToString("\n")}") - for (s in testClassesJUnit34) { - runOneClass(s) - } - } - - // On TeamCity test failures themselves control the build status, no need to report them as additional errors - if (hasFailures && !TeamCityHelper.isUnderTeamCity) { - throw RuntimeException("Tests failed in dedicated runtime (class mode)") + else { + runJUnit5 = false } } - else if (options.isDedicatedTestRuntime == "package") { - var hasFailures = false - fun groupByPackages(tests: List): Map> { - return tests.groupBy { - val i = it.lastIndexOf('.') - if (i != -1) it.substring(0, i) else "" - } + if (runJUnit34) { + val failedClassesJUnit34 = failedClassesJUnit34List.let { if (Files.exists(it)) it.readLines() else emptyList() } + if (failedClassesJUnit34.isNotEmpty()) { + messages.warning("Will rerun JUnit 3+4 tests: $failedClassesJUnit34") } - - suspend fun runOnePackage(entry: Map.Entry>) { - val packageName = entry.key - val classes = entry.value - - val exitCode = block("running tests in package '$packageName'") { - runJUnit5Engine( - mainModule = mainModule, - systemProperties = systemProperties + ("idea.performance.tests.discovery.filter" to "true"), - jvmArgs = jvmArgs, - envVariables = envVariables, - bootstrapClasspath = bootstrapClasspath, - modulePath = modulePath, - testClasspath = testClasspath, - suiteName = "__classes__", - methodName = classes.joinToString(";"), - devBuildSettings = devBuildServerSettings, - ) - } - if (exitCode == 1) hasFailures = true // reported as test failure or assertNoUnhandledExceptions if exception - else if (exitCode == NO_TESTS_ERROR) throw NoTestsFound() - else if (exitCode != 0) throw RuntimeException("Unexpected exit code $exitCode when running tests in dedicated runtime (package mode)") - } - - if (testClassesJUnit5.isNotEmpty()) { - val packages = groupByPackages(testClassesJUnit5) - messages.info(packages.entries.joinToString(prefix = "Will run JUnit 5 packages:\n", separator = "\n") { e -> - e.value.joinToString(prefix = "${e.key}\n ", separator = "\n ") - }) - for (entry in packages) { - runOnePackage(entry) - } - } - if (testClassesJUnit34.isNotEmpty()) { - val packages = groupByPackages(testClassesJUnit34) - messages.info(packages.entries.joinToString(prefix = "Will run JUnit 3+4 packages:\n", separator = "\n") { e -> - e.value.joinToString(prefix = "${e.key}\n ", separator = "\n ") - }) - for (entry in packages) { - runOnePackage(entry) - } - } - - // On TeamCity test failures themselves control the build status, no need to report them as additional errors - if (hasFailures && !TeamCityHelper.isUnderTeamCity) { - throw RuntimeException("Tests failed in dedicated runtime (package mode)") + else { + runJUnit34 = false } } } - else { - val failedClassesJUnit5List = Files.createTempFile("failed-classes-junit5-", ".list").apply { Files.delete(this) } - val failedClassesJUnit34List = Files.createTempFile("failed-classes-junit34-", ".list").apply { Files.delete(this) } - val additionalPropertiesJUnit5: Map = failedClassesJUnit5List.let { - if (options.attemptCount > 1) mapOf("intellij.build.test.retries.failedClasses.file" to "$it", "intellij.build.test.list.file" to "$it") - else emptyMap() + + // Check if tests failed after all retry attempts are exhausted + val hadTestFailures = lastExitCode5 == 1 || lastExitCode34 == 1 + // On TeamCity test failures themselves control the build status, no need to report them as additional errors + if (!TeamCityHelper.isUnderTeamCity) { + if (hadTestFailures) { + throw RuntimeException("Tests failed (JUnit5 exit code: $lastExitCode5, JUnit3+4 exit code: $lastExitCode34, $NO_TESTS_ERROR means no test for this test framework)") } - val additionalPropertiesJUnit34: Map = failedClassesJUnit34List.let { - if (options.attemptCount > 1) mapOf("intellij.build.test.retries.failedClasses.file" to "$it", "intellij.build.test.list.file" to "$it") - else emptyMap() - } - var runJUnit5 = !options.shouldSkipJUnit5Tests - var runJUnit34 = !options.shouldSkipJUnit34Tests - var lastExitCode5 = 0 - var lastExitCode34 = 0 - for (attempt in 1..options.attemptCount) { - if (!runJUnit5 && !runJUnit34) break - val spanNameSuffix = if (options.attemptCount > 1) " (attempt $attempt)" else "" - val systemPath = systemProperties[PathManager.PROPERTY_SYSTEM_PATH] - - val additionalProperties = mutableMapOf(PathManager.PROPERTY_LOG_PATH to "$systemPath${File.separator}log${File.separator}junit5") - if (attempt > 1) { - additionalProperties["intellij.build.test.ignoreFirstAndLastTests"] = "true" - } - - val exitCode5: Int = if (runJUnit5) { - block("run junit 5 tests${spanNameSuffix}") { - runJUnit5Engine( - mainModule = mainModule, - systemProperties = systemProperties + additionalProperties + additionalPropertiesJUnit5, - jvmArgs = jvmArgs, - envVariables = envVariables, - bootstrapClasspath = bootstrapClasspath, - modulePath = modulePath, - testClasspath = testClasspath, - suiteName = null, - methodName = null, - devBuildSettings = devBuildServerSettings, - ) - } - } - else { - 0 - } - if (runJUnit5) lastExitCode5 = exitCode5 - - additionalProperties[PathManager.PROPERTY_LOG_PATH] = "$systemPath${File.separator}log${File.separator}junit3and4" - val exitCode34: Int = if (runJUnit34) { - block("run junit 3+4 tests${spanNameSuffix}") { - runJUnit5Engine( - mainModule = mainModule, - systemProperties = systemProperties + additionalProperties + additionalPropertiesJUnit34, - jvmArgs = jvmArgs, - envVariables = envVariables, - bootstrapClasspath = bootstrapClasspath, - modulePath = modulePath, - testClasspath = testClasspath, - suiteName = options.bootstrapSuite, - methodName = null, - devBuildSettings = devBuildServerSettings, - ) - } - } - else { - 0 - } - if (runJUnit34) lastExitCode34 = exitCode34 - - if (exitCode5 == NO_TESTS_ERROR && exitCode34 == NO_TESTS_ERROR && - // only check on the first (full) attempt - attempt == 1 && - // a bucket might be empty for run configurations with too few tests due to imperfect tests balancing - options.bucketsCount < 2) { - throw NoTestsFound() - } - if (exitCode5 != 0 && exitCode5 != 1 && exitCode5 != NO_TESTS_ERROR) { - throw RuntimeException("Unexpected exit code $exitCode5 when running JUnit 5 tests") - } - if (exitCode34 != 0 && exitCode34 != 1 && exitCode34 != NO_TESTS_ERROR) { - throw RuntimeException("Unexpected exit code $exitCode34 when running JUnit 3+4 tests") - } - - if (runJUnit5) { - val failedClassesJUnit5 = failedClassesJUnit5List.let { if (Files.exists(it)) it.readLines() else emptyList() } - if (failedClassesJUnit5.isNotEmpty()) { - messages.warning("Will rerun JUnit 5 tests: $failedClassesJUnit5") - } - else { - runJUnit5 = false - } - } - - if (runJUnit34) { - val failedClassesJUnit34 = failedClassesJUnit34List.let { if (Files.exists(it)) it.readLines() else emptyList() } - if (failedClassesJUnit34.isNotEmpty()) { - messages.warning("Will rerun JUnit 3+4 tests: $failedClassesJUnit34") - } - else { - runJUnit34 = false - } - } - } - - // Check if tests failed after all retry attempts are exhausted - val hadTestFailures = lastExitCode5 == 1 || lastExitCode34 == 1 - // On TeamCity test failures themselves control the build status, no need to report them as additional errors - if (!TeamCityHelper.isUnderTeamCity) { - if (hadTestFailures) { - throw RuntimeException("Tests failed (JUnit5 exit code: $lastExitCode5, JUnit3+4 exit code: $lastExitCode34, $NO_TESTS_ERROR means no test for this test framework)") - } - else { - println("*** All tests passed ***") - } + else { + println("*** All tests passed ***") } } } @@ -1303,7 +1138,7 @@ internal class TestingTasksImpl(context: CompilationContext, private val options appendJUnitStarter(classpath, context) } - if (!isBootstrapSuiteDefault || isRunningInBatchMode || options.isDedicatedTestRuntime != "false" || suiteName == null) { + if (!isBootstrapSuiteDefault || options.isDedicatedTestRuntime != "false" || suiteName == null) { classpath.addAll(testClasspath) } @@ -1401,14 +1236,6 @@ internal class TestingTasksImpl(context: CompilationContext, private val options private val isBootstrapSuiteDefault: Boolean get() = options.bootstrapSuite == TestingOptions.BOOTSTRAP_SUITE_DEFAULT - - private val isRunningInBatchMode: Boolean - get() { - return options.batchTestIncludes != null && - options.testPatterns == null && - options.testConfigurations == null && - options.testGroups == TestingOptions.ALL_EXCLUDE_DEFINED_GROUP - } } private fun appendJUnitStarter(classPath: MutableList, context: CompilationContext) {