[Gradle] Rework KotlinTestReport to be compatible with configuration cache

Minimal supported version is effectively changed to 6.1 due to usage of Gradle build services
Relates to #KT-43605
This commit is contained in:
Alexander Likhachev
2021-02-19 05:58:29 +03:00
committed by TeamCityServer
parent 3e09bb3d3f
commit ef890464d8
8 changed files with 100 additions and 57 deletions
@@ -22,7 +22,7 @@ open class KotlinAndroid36GradleIT : KotlinAndroid33GradleIT() {
get() = AGPVersion.v3_6_0 get() = AGPVersion.v3_6_0
override val defaultGradleVersion: GradleVersionRequired override val defaultGradleVersion: GradleVersionRequired
get() = GradleVersionRequired.AtLeast("6.0") get() = GradleVersionRequired.AtLeast("6.1")
@Ignore @Ignore
@Test @Test
@@ -24,7 +24,7 @@ sealed class GradleVersionRequired(val minVersion: String, val maxVersion: Strin
companion object { companion object {
const val OLDEST_SUPPORTED = "5.3" const val OLDEST_SUPPORTED = "5.3"
val FOR_MPP_SUPPORT = AtLeast("6.0") val FOR_MPP_SUPPORT = AtLeast("6.1")
} }
class Exact(version: String) : GradleVersionRequired(version, version) class Exact(version: String) : GradleVersionRequired(version, version)
@@ -15,7 +15,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
class KlibBasedMppIT : BaseGradleIT() { class KlibBasedMppIT : BaseGradleIT() {
override val defaultGradleVersion = GradleVersionRequired.AtLeast("6.0") override val defaultGradleVersion = GradleVersionRequired.AtLeast("6.1")
companion object { companion object {
private const val MODULE_GROUP = "com.example" private const val MODULE_GROUP = "com.example"
@@ -88,13 +88,11 @@ class Kotlin2JsIrGradlePluginIT : AbstractKotlin2JsGradlePluginIT(true) {
val appProject = transformProjectWithPluginsDsl( val appProject = transformProjectWithPluginsDsl(
projectName = "app", projectName = "app",
directoryPrefix = rootProjectName, directoryPrefix = rootProjectName,
wrapperVersion = GradleVersionRequired.AtLeast("5.4")
) )
val libProject = transformProjectWithPluginsDsl( val libProject = transformProjectWithPluginsDsl(
projectName = "lib", projectName = "lib",
directoryPrefix = rootProjectName, directoryPrefix = rootProjectName,
wrapperVersion = GradleVersionRequired.AtLeast("5.4")
) )
libProject.gradleProperties().appendText(jsCompilerType(KotlinJsCompilerType.IR)) libProject.gradleProperties().appendText(jsCompilerType(KotlinJsCompilerType.IR))
@@ -300,6 +298,8 @@ class Kotlin2JsGradlePluginIT : AbstractKotlin2JsGradlePluginIT(false) {
} }
abstract class AbstractKotlin2JsGradlePluginIT(val irBackend: Boolean) : BaseGradleIT() { abstract class AbstractKotlin2JsGradlePluginIT(val irBackend: Boolean) : BaseGradleIT() {
override val defaultGradleVersion = GradleVersionRequired.AtLeast("6.1")
override fun defaultBuildOptions(): BuildOptions = override fun defaultBuildOptions(): BuildOptions =
super.defaultBuildOptions().copy( super.defaultBuildOptions().copy(
jsIrBackend = irBackend, jsIrBackend = irBackend,
@@ -10,6 +10,8 @@ import org.jetbrains.kotlin.gradle.util.modify
import org.junit.Test import org.junit.Test
class KotlinJsIrLibraryGradlePluginIT : BaseGradleIT() { class KotlinJsIrLibraryGradlePluginIT : BaseGradleIT() {
override val defaultGradleVersion = GradleVersionRequired.AtLeast("6.1")
override fun defaultBuildOptions(): BuildOptions = override fun defaultBuildOptions(): BuildOptions =
super.defaultBuildOptions().copy( super.defaultBuildOptions().copy(
jsIrBackend = true, jsIrBackend = true,
@@ -14,7 +14,7 @@ import org.junit.Test
class CleanDataTaskIT : BaseGradleIT() { class CleanDataTaskIT : BaseGradleIT() {
override val defaultGradleVersion: GradleVersionRequired override val defaultGradleVersion: GradleVersionRequired
get() = GradleVersionRequired.AtLeast("5.5.1") get() = GradleVersionRequired.AtLeast("6.1")
@Test @Test
fun testDownloadedFolderDeletion() { fun testDownloadedFolderDeletion() {
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.gradle.testing.internal
import org.gradle.api.GradleException import org.gradle.api.GradleException
import org.gradle.api.execution.TaskExecutionGraph import org.gradle.api.execution.TaskExecutionGraph
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
@@ -47,9 +48,12 @@ open class KotlinTestReport : TestReport() {
@Transient @Transient
private var parent: KotlinTestReport? = null private var parent: KotlinTestReport? = null
private val parentPaths = project.provider {
computeAllParentTasksPaths()
}
// TODO: this field is used in task action when the task is aggregate report, which makes it incompatible with configuration cache
@Internal @Internal
@Transient
val children = mutableListOf<TaskProvider<KotlinTestReport>>() val children = mutableListOf<TaskProvider<KotlinTestReport>>()
@Transient @Transient
@@ -66,14 +70,24 @@ open class KotlinTestReport : TestReport() {
@Input @Input
var ignoreFailures: Boolean = false var ignoreFailures: Boolean = false
private val hasOwnFailedTests private val testReportServiceProvider = TestReportService.registerIfAbsent(project.gradle)
get() = failedTestsListener.hasOwnFailedTests ?: false private val testReportService
get() = testReportServiceProvider.get()
private val hasFailedTests: Boolean private val hasFailedTests: Boolean
get() = hasOwnFailedTests || children.any { it.get().hasFailedTests } get() = testReportService.hasFailedTests(path)
private val suppressedRunningFailureListeners: MutableSet<SuppressedTestRunningFailureListener> = mutableSetOf() private val failedTestsListener = FailedTestListener(parentPaths, testReportServiceProvider)
private val failedTestsListener = FailedTestListener() private fun computeAllParentTasksPaths(): List<String> {
val allParents = mutableListOf<String>()
var cur: KotlinTestReport? = this
while (cur != null) {
allParents.add(cur.path)
cur = cur.parent
}
return allParents
}
fun addChild(childProvider: TaskProvider<KotlinTestReport>) { fun addChild(childProvider: TaskProvider<KotlinTestReport>) {
val child = childProvider.get() val child = childProvider.get()
@@ -85,7 +99,7 @@ open class KotlinTestReport : TestReport() {
reportOnChildTasks(childProvider) reportOnChildTasks(childProvider)
} }
private fun reportOnChildTasks(childProvider: TaskProvider<KotlinTestReport>): Unit { private fun reportOnChildTasks(childProvider: TaskProvider<KotlinTestReport>) {
val child = childProvider.get() val child = childProvider.get()
child.testTasks.forEach { child.testTasks.forEach {
@@ -101,9 +115,8 @@ open class KotlinTestReport : TestReport() {
task.addTestListener(failedTestsListener) task.addTestListener(failedTestsListener)
if (task is KotlinTest) { if (task is KotlinTest) {
val listener = SuppressedTestRunningFailureListener(task.path) val listener = SuppressedTestRunningFailureListener(parentPaths, task.path, testReportServiceProvider)
task.addRunListener(listener) task.addRunListener(listener)
suppressedRunningFailureListeners.add(listener)
} }
reportOn(task) reportOn(task)
@@ -119,7 +132,7 @@ open class KotlinTestReport : TestReport() {
} }
private fun reportOn(task: AbstractTestTask) { private fun reportOn(task: AbstractTestTask) {
reportOn(task.binResultsDir) reportOn(task.binaryResultsDirectory)
} }
open val htmlReportUrl: String? open val htmlReportUrl: String?
@@ -133,7 +146,6 @@ open class KotlinTestReport : TestReport() {
fun checkFailedTests() { fun checkFailedTests() {
if (checkFailedTests) { if (checkFailedTests) {
checkSuppressedRunningFailures() checkSuppressedRunningFailures()
if (hasFailedTests) { if (hasFailedTests) {
if (ignoreFailures) { if (ignoreFailures) {
logger.warn(getFailingTestsMessage()) logger.warn(getFailingTestsMessage())
@@ -155,27 +167,12 @@ open class KotlinTestReport : TestReport() {
} }
private fun checkSuppressedRunningFailures() { private fun checkSuppressedRunningFailures() {
val allSuppressedRunningFailures = mutableListOf<Pair<String, Error>>() val taskFailures = testReportService.getAggregatedTaskFailures(path)
if (taskFailures.isNotEmpty()) {
fun visitSuppressedRunningFailures(report: KotlinTestReport) {
report.suppressedRunningFailureListeners.forEach { listener ->
listener.failures.forEach { failure ->
allSuppressedRunningFailures.add(listener.taskPath to failure)
}
}
report.children.forEach {
visitSuppressedRunningFailures(it.get())
}
}
visitSuppressedRunningFailures(this)
if (allSuppressedRunningFailures.isNotEmpty()) {
val allErrors = mutableListOf<Error>() val allErrors = mutableListOf<Error>()
val msg = buildString { val msg = buildString {
appendln("Failed to execute all tests:") appendln("Failed to execute all tests:")
allSuppressedRunningFailures.groupBy { it.first }.forEach { path, errors -> taskFailures.groupBy { it.first }.forEach { (path, errors) ->
append(path) append(path)
append(": ") append(": ")
var first = true var first = true
@@ -246,40 +243,39 @@ open class KotlinTestReport : TestReport() {
task.reports.junitXml.isEnabled = false task.reports.junitXml.isEnabled = false
} }
private class SuppressedTestRunningFailureListener(val taskPath: String) : KotlinTestRunnerListener { private class SuppressedTestRunningFailureListener(
@Transient private val allListenedTaskParentsPaths: Provider<List<String>>,
private var failuresMutable: MutableList<Error>? = mutableListOf() private val failedTaskPath: String,
private val testReportServiceProvider: Provider<TestReportService>
val failures: List<Error> ) : KotlinTestRunnerListener {
get() = failuresMutable ?: emptyList()
override fun runningFailure(failure: Error) { override fun runningFailure(failure: Error) {
var failures = failuresMutable allListenedTaskParentsPaths.get().forEach {
if (failures == null) { testReportServiceProvider.get().reportFailure(failedTaskPath, it, failure)
// it is possible after deserialization
failures = mutableListOf()
failuresMutable = failures
} }
failures.add(failure)
} }
} }
private class FailedTestListener : TestListener { private class FailedTestListener(
@Transient private val allListenedTaskParentsPaths: Provider<List<String>>,
var hasOwnFailedTests: Boolean? = false private val testReportServiceProvider: Provider<TestReportService>
) : TestListener {
override fun beforeTest(testDescriptor: TestDescriptor) { override fun beforeTest(testDescriptor: TestDescriptor) {}
}
override fun afterSuite(suite: TestDescriptor, result: TestResult) { override fun afterSuite(suite: TestDescriptor, result: TestResult) {
reportFailure(result)
} }
override fun beforeSuite(suite: TestDescriptor) { override fun beforeSuite(suite: TestDescriptor) {}
}
override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) { override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {
reportFailure(result)
}
private fun reportFailure(result: TestResult) {
if (result.failedTestCount > 0) { if (result.failedTestCount > 0) {
hasOwnFailedTests = true allListenedTaskParentsPaths.get().forEach {
testReportServiceProvider.get().testFailed(it)
}
} }
} }
} }
@@ -0,0 +1,45 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.testing.internal
import org.gradle.api.invocation.Gradle
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
typealias TaskError = Pair<String, Error>
abstract class TestReportService : BuildService<BuildServiceParameters.None> {
private val taskHasFailedTests = mutableMapOf<String, Boolean>()
private val taskSuppressedFailures = mutableMapOf<String, MutableList<TaskError>>()
fun testFailed(taskPath: String) {
taskHasFailedTests[taskPath] = true
}
fun hasFailedTests(path: String): Boolean {
return taskHasFailedTests[path] ?: false
}
fun reportFailure(failedTaskPath: String, parentTaskPath: String, failure: Error) {
taskSuppressedFailures.computeIfAbsent(parentTaskPath) { mutableListOf() }.add(failedTaskPath to failure)
}
fun getAggregatedTaskFailures(taskPath: String): List<TaskError> {
return taskSuppressedFailures[taskPath] ?: emptyList()
}
companion object {
fun registerIfAbsent(gradle: Gradle): Provider<TestReportService> {
// Use class loader hashcode in case there are multiple class loaders in the same build
return gradle.sharedServices
.registerIfAbsent(
"${TestReportService::class.java.canonicalName}_${TestReportService::class.java.classLoader.hashCode()}",
TestReportService::class.java
) {}
}
}
}