From f58d8773fae661890d102b24eb76f0ecfca9ebcf Mon Sep 17 00:00:00 2001 From: "Dmitriy.Panov" Date: Mon, 30 Jun 2025 15:02:35 +0200 Subject: [PATCH] build scripts: an option to present existing coverage data files as a single report IJI-2636 IJI-2672 GitOrigin-RevId: f84b55650360951b0f046065ac84a9c18909082b --- build/src/CommunityRunTestsBuildTarget.kt | 5 +- .../jetbrains/intellij/build/TestingTasks.kt | 22 +++++-- .../intellij/build/impl/TestingTasksImpl.kt | 57 +++++++++++-------- .../intellij/build/impl/coverage/Coverage.kt | 22 ++++++- .../build/impl/coverage/CoverageTest.kt | 15 ++++- 5 files changed, 88 insertions(+), 33 deletions(-) diff --git a/build/src/CommunityRunTestsBuildTarget.kt b/build/src/CommunityRunTestsBuildTarget.kt index b6b5348c1604..1a379ebadd30 100644 --- a/build/src/CommunityRunTestsBuildTarget.kt +++ b/build/src/CommunityRunTestsBuildTarget.kt @@ -2,6 +2,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import org.jetbrains.intellij.build.BuildPaths.Companion.COMMUNITY_ROOT +import org.jetbrains.intellij.build.TestingOptions import org.jetbrains.intellij.build.TestingTasks import org.jetbrains.intellij.build.impl.createCompilationContext @@ -22,7 +23,9 @@ object CommunityRunTestsBuildTarget { projectHome = COMMUNITY_ROOT.communityRoot, defaultOutputRoot = COMMUNITY_ROOT.communityRoot.resolve("out/tests") ) - TestingTasks.create(context).runTests(defaultMainModule = "intellij.idea.community.main") + val options = TestingOptions() + options.mainModule = options.mainModule ?: "intellij.idea.community.main" + TestingTasks.create(context, options).runTests() } } } \ No newline at end of file diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/TestingTasks.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/TestingTasks.kt index ffd799318455..da85bf890ab6 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/TestingTasks.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/TestingTasks.kt @@ -1,6 +1,4 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -@file:Suppress("ReplaceJavaStaticMethodWithKotlinAnalog") - package org.jetbrains.intellij.build import org.jetbrains.annotations.ApiStatus @@ -29,10 +27,22 @@ interface TestingTasks { * @param defaultMainModule the main module to be used if [TestingOptions.mainModule] is not specified * @param rootExcludeCondition if not `null`, tests from modules which sources are fit this predicate will be skipped */ - suspend fun runTests(additionalJvmOptions: List = java.util.List.of(), - additionalSystemProperties: Map = java.util.Map.of(), - defaultMainModule: String? = null, - rootExcludeCondition: ((Path) -> Boolean)? = null) + @Deprecated(message = "the `defaultMainModule` should be passed via `TestingOptions#mainModule`") + suspend fun runTests( + additionalJvmOptions: List = emptyList(), + additionalSystemProperties: Map = emptyMap(), + defaultMainModule: String? = null, + rootExcludeCondition: ((Path) -> Boolean)? = null, + ) + + /** + * @param rootExcludeCondition if not `null`, tests from modules which sources are fit this predicate will be skipped + */ + suspend fun runTests( + additionalJvmOptions: List = emptyList(), + additionalSystemProperties: Map = emptyMap(), + rootExcludeCondition: ((Path) -> Boolean)? = null, + ) /** * Run all tests annotated with [com.intellij.testFramework.SkipInHeadlessEnvironment] 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 be539adc4315..7d545196861e 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 @@ -34,6 +34,7 @@ import org.jetbrains.intellij.build.TestingOptions import org.jetbrains.intellij.build.TestingTasks 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 @@ -72,7 +73,19 @@ private const val NO_TESTS_ERROR = 42 internal class TestingTasksImpl(context: CompilationContext, private val options: TestingOptions) : TestingTasks { private val context: CompilationContext = if (options.useArchivedCompiledClasses) context.asArchived else context - override lateinit var coverage: CoverageImpl + + override val coverage: Coverage by lazy { + CoverageImpl( + context = this.context, + coveredModuleNames = runConfigurations + .map { it.moduleName } + .takeIf { it.any() } + ?: listOfNotNull(options.mainModule), + coveredClasses = requireNotNull(options.coveredClassesPatterns) { + "Test coverage is enabled but the classes pattern is not specified" + }.splitToSequence(';').map(::Regex).toList(), + ) + } private fun loadRunConfigurations(name: String): List { return try { @@ -99,31 +112,38 @@ internal class TestingTasksImpl(context: CompilationContext, private val options } } - private fun loadTestRunConfigurations(): List? { - val testConfigurationsOption = options.testConfigurations ?: return null - return testConfigurationsOption - .splitToSequence(';') - .filter(String::isNotEmpty) - .flatMap(::loadRunConfigurations) - .toList() + private val runConfigurations: List by lazy { + options.testConfigurations + ?.splitToSequence(';') + ?.filter(String::isNotEmpty) + ?.flatMap(::loadRunConfigurations) + ?.toList() ?: emptyList() } + @Deprecated("the `defaultMainModule` should be passed via `TestingOptions#mainModule`") override suspend fun runTests( additionalJvmOptions: List, additionalSystemProperties: Map, defaultMainModule: String?, rootExcludeCondition: ((Path) -> Boolean)?, ) { + require(defaultMainModule == null) { + "The `defaultMainModule` parameter is deprecated, please use `TestingOptions#mainModule` instead." + } + runTests(additionalJvmOptions, additionalSystemProperties, rootExcludeCondition) + } + + override suspend fun runTests(additionalJvmOptions: List, additionalSystemProperties: Map, rootExcludeCondition: ((Path) -> Boolean)?) { if (options.redirectStdOutToFile && !TeamCityHelper.isUnderTeamCity) { context.messages.warning("'${TestingOptions.REDIRECT_STDOUT_TO_FILE}' can be set only for a TeamCity build, ignored.") } if (TeamCityHelper.isUnderTeamCity && options.redirectStdOutToFile) { redirectStdOutToFile { - runTestsImpl(additionalJvmOptions, additionalSystemProperties, defaultMainModule, rootExcludeCondition) + runTestsImpl(additionalJvmOptions, additionalSystemProperties, rootExcludeCondition) } } else { - runTestsImpl(additionalJvmOptions, additionalSystemProperties, defaultMainModule, rootExcludeCondition) + runTestsImpl(additionalJvmOptions, additionalSystemProperties, rootExcludeCondition) } } @@ -148,7 +168,6 @@ internal class TestingTasksImpl(context: CompilationContext, private val options private suspend fun runTestsImpl(additionalJvmOptions: List, additionalSystemProperties: Map, - defaultMainModule: String?, rootExcludeCondition: ((Path) -> Boolean)?) { if (options.enableCoverage && options.isPerformanceTestsOnly) { context.messages.buildStatus("Skipping performance testing with Coverage, {build.status.text}") @@ -159,13 +178,12 @@ internal class TestingTasksImpl(context: CompilationContext, private val options return } - val mainModule = options.mainModule ?: defaultMainModule + val mainModule = options.mainModule checkOptions(mainModule) - val runConfigurations = loadTestRunConfigurations() if (options.validateMainModule) { checkNotNull(mainModule) - val withModuleMismatch = runConfigurations?.filter { it.moduleName != mainModule } ?: emptyList() + val withModuleMismatch = runConfigurations.filter { it.moduleName != mainModule } if (withModuleMismatch.isNotEmpty()) { val errorMessage = withModuleMismatch.joinToString( prefix = "Run configuration module mismatch, expected '$mainModule' (set in option 'intellij.build.test.main.module'), actual:\n", @@ -181,7 +199,7 @@ internal class TestingTasksImpl(context: CompilationContext, private val options compilationTasks.buildProjectArtifacts(it) } - if (runConfigurations != null) { + if (runConfigurations.any()) { compilationTasks.compileModules(listOf("intellij.tools.testsBootstrap"), listOf("intellij.platform.buildScripts") + runConfigurations.map { it.moduleName }) compilationTasks.buildProjectArtifacts(runConfigurations.flatMapTo(LinkedHashSet()) { it.requiredArtifacts }) @@ -211,16 +229,9 @@ internal class TestingTasksImpl(context: CompilationContext, private val options loadTestDiscovery(effectiveAdditionalJvmOptions, systemProperties) } if (options.enableCoverage) { - coverage = CoverageImpl( - context, - coveredModuleNames = runConfigurations?.map { it.moduleName } ?: listOfNotNull(mainModule), - coveredClasses = requireNotNull(options.coveredClassesPatterns) { - "Test coverage is enabled but the classes pattern is not specified" - }.splitToSequence(';').map(::Regex).toList(), - ) coverage.enable(jvmOptions = effectiveAdditionalJvmOptions, systemProperties = systemProperties) } - if (runConfigurations == null) { + if (runConfigurations.none()) { runTestsFromGroupsAndPatterns(effectiveAdditionalJvmOptions, checkNotNull(mainModule) { "Main module is not specified" }, rootExcludeCondition, systemProperties) diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/coverage/Coverage.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/coverage/Coverage.kt index c0da4b5869a2..5c51d168245a 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/coverage/Coverage.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/coverage/Coverage.kt @@ -1,6 +1,7 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.intellij.build.impl.coverage +import com.intellij.rt.coverage.aggregate.api.AggregatorApi import com.intellij.rt.coverage.instrumentation.CoverageArgs import com.intellij.rt.coverage.report.api.Filters import com.intellij.rt.coverage.report.api.ReportApi @@ -8,6 +9,7 @@ import com.intellij.rt.coverage.util.ErrorReporter import com.intellij.util.io.Compressor import org.jetbrains.annotations.ApiStatus import org.jetbrains.intellij.build.CompilationContext +import org.jetbrains.intellij.build.dependencies.TeamCityHelper import org.jetbrains.intellij.build.telemetry.block import org.jetbrains.jps.model.java.JavaSourceRootType import org.jetbrains.jps.model.java.JpsJavaClasspathKind @@ -34,6 +36,8 @@ interface Coverage { fun enable(jvmOptions: MutableList, systemProperties: MutableMap) suspend fun generateReport() + + suspend fun aggregateAndReport(dataToMerge: List) } @OptIn(ExperimentalPathApi::class) @@ -42,7 +46,9 @@ internal class CoverageImpl( coveredModuleNames: List, private val coveredClasses: List, ) : Coverage { - private val data: Path = context.paths.tempDir.resolve("coverage.ic") + private val buildId: String = TeamCityHelper.allProperties["teamcity.build.id"] ?: "" + + private val data: Path = context.paths.tempDir.resolve("coverage$buildId.ic") init { require(coveredModuleNames.any()) { @@ -201,4 +207,18 @@ internal class CoverageImpl( publishReport() } } + + override suspend fun aggregateAndReport(dataToMerge: List) { + require(dataToMerge.any()) { + "No coverage data files to merge" + } + block("Merging coverage data files") { + @Suppress("IO_FILE_USAGE") + AggregatorApi.merge( + dataToMerge.map { it.toFile() }, + data.toFile(), + ) + } + generateReport() + } } \ No newline at end of file diff --git a/platform/build-scripts/tests/testSrc/org/jetbrains/intellij/build/impl/coverage/CoverageTest.kt b/platform/build-scripts/tests/testSrc/org/jetbrains/intellij/build/impl/coverage/CoverageTest.kt index 6b244f15df47..e6937c08aec6 100644 --- a/platform/build-scripts/tests/testSrc/org/jetbrains/intellij/build/impl/coverage/CoverageTest.kt +++ b/platform/build-scripts/tests/testSrc/org/jetbrains/intellij/build/impl/coverage/CoverageTest.kt @@ -13,6 +13,7 @@ import org.jsoup.Jsoup import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path +import kotlin.io.path.createFile import kotlin.io.path.extension import kotlin.io.path.name import kotlin.io.path.readText @@ -44,8 +45,7 @@ class CoverageTest { @Test fun test() { runBlocking(Dispatchers.Default) { - val context = context() - val tests = TestingTasks.create(context, testingOptions) + val tests = TestingTasks.create(context(), testingOptions) if (isAlreadyUnderCoverage) return@runBlocking tests.runTests(additionalSystemProperties = mapOf(coverageFlag to "true")) assertCoverage(coveredClassName = coveredClass.simpleName, reportDir = tests.coverage.reportDir) @@ -73,4 +73,15 @@ class CoverageTest { "$coveredClassName is expected to be called by this test class but 0% coverage is detected" } } + + @Test + fun `test aggregating reports`() { + if (isAlreadyUnderCoverage) return + runBlocking(Dispatchers.Default) { + val tests = TestingTasks.create(context(), testingOptions) + tests.coverage.aggregateAndReport((1..2).map { + tempDir.resolve("coverage-report-$it.ic").createFile() + }) + } + } }