build scripts: an option to present existing coverage data files as a single report

IJI-2636 IJI-2672

GitOrigin-RevId: f84b55650360951b0f046065ac84a9c18909082b
This commit is contained in:
Dmitriy.Panov
2025-07-04 18:47:26 +00:00
committed by intellij-monorepo-bot
parent 11cf5bf265
commit f58d8773fa
5 changed files with 88 additions and 33 deletions
+4 -1
View File
@@ -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()
}
}
}
@@ -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<String> = java.util.List.of(),
additionalSystemProperties: Map<String, String> = 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<String> = emptyList(),
additionalSystemProperties: Map<String, String> = 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<String> = emptyList(),
additionalSystemProperties: Map<String, String> = emptyMap(),
rootExcludeCondition: ((Path) -> Boolean)? = null,
)
/**
* Run all tests annotated with [com.intellij.testFramework.SkipInHeadlessEnvironment]
@@ -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<JUnitRunConfigurationProperties> {
return try {
@@ -99,31 +112,38 @@ internal class TestingTasksImpl(context: CompilationContext, private val options
}
}
private fun loadTestRunConfigurations(): List<JUnitRunConfigurationProperties>? {
val testConfigurationsOption = options.testConfigurations ?: return null
return testConfigurationsOption
.splitToSequence(';')
.filter(String::isNotEmpty)
.flatMap(::loadRunConfigurations)
.toList()
private val runConfigurations: List<JUnitRunConfigurationProperties> 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<String>,
additionalSystemProperties: Map<String, String>,
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<String>, additionalSystemProperties: Map<String, String>, 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<String>,
additionalSystemProperties: Map<String, String>,
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)
@@ -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<String>, systemProperties: MutableMap<String, String>)
suspend fun generateReport()
suspend fun aggregateAndReport(dataToMerge: List<Path>)
}
@OptIn(ExperimentalPathApi::class)
@@ -42,7 +46,9 @@ internal class CoverageImpl(
coveredModuleNames: List<String>,
private val coveredClasses: List<Regex>,
) : 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<Path>) {
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()
}
}
@@ -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()
})
}
}
}