From ef890464d801e5b201c211a76797b0694cdcf100 Mon Sep 17 00:00:00 2001 From: Alexander Likhachev Date: Fri, 19 Feb 2021 05:58:29 +0300 Subject: [PATCH] [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 --- .../AbstractKotlinAndroidGradleTests.kt | 2 +- .../kotlin/gradle/GradleVersionRequired.kt | 2 +- .../jetbrains/kotlin/gradle/KlibBasedMppIT.kt | 2 +- .../kotlin/gradle/Kotlin2JsGradlePluginIT.kt | 4 +- .../gradle/KotlinJsLibraryGradlePluginIT.kt | 2 + .../kotlin/gradle/tasks/CleanDataTaskIT.kt | 2 +- .../testing/internal/KotlinTestReport.kt | 98 +++++++++---------- .../testing/internal/TestReportService.kt | 45 +++++++++ 8 files changed, 100 insertions(+), 57 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/TestReportService.kt diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt index 674d1f61ef3..08153f052fb 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt @@ -22,7 +22,7 @@ open class KotlinAndroid36GradleIT : KotlinAndroid33GradleIT() { get() = AGPVersion.v3_6_0 override val defaultGradleVersion: GradleVersionRequired - get() = GradleVersionRequired.AtLeast("6.0") + get() = GradleVersionRequired.AtLeast("6.1") @Ignore @Test diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/GradleVersionRequired.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/GradleVersionRequired.kt index 6e01ad20f71..79d8a7a67c8 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/GradleVersionRequired.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/GradleVersionRequired.kt @@ -24,7 +24,7 @@ sealed class GradleVersionRequired(val minVersion: String, val maxVersion: Strin companion object { 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) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KlibBasedMppIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KlibBasedMppIT.kt index 2dc736dc950..089345bab9d 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KlibBasedMppIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KlibBasedMppIT.kt @@ -15,7 +15,7 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class KlibBasedMppIT : BaseGradleIT() { - override val defaultGradleVersion = GradleVersionRequired.AtLeast("6.0") + override val defaultGradleVersion = GradleVersionRequired.AtLeast("6.1") companion object { private const val MODULE_GROUP = "com.example" diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt index 9b22611c856..2cf8c6f3e66 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt @@ -88,13 +88,11 @@ class Kotlin2JsIrGradlePluginIT : AbstractKotlin2JsGradlePluginIT(true) { val appProject = transformProjectWithPluginsDsl( projectName = "app", directoryPrefix = rootProjectName, - wrapperVersion = GradleVersionRequired.AtLeast("5.4") ) val libProject = transformProjectWithPluginsDsl( projectName = "lib", directoryPrefix = rootProjectName, - wrapperVersion = GradleVersionRequired.AtLeast("5.4") ) libProject.gradleProperties().appendText(jsCompilerType(KotlinJsCompilerType.IR)) @@ -300,6 +298,8 @@ class Kotlin2JsGradlePluginIT : AbstractKotlin2JsGradlePluginIT(false) { } abstract class AbstractKotlin2JsGradlePluginIT(val irBackend: Boolean) : BaseGradleIT() { + override val defaultGradleVersion = GradleVersionRequired.AtLeast("6.1") + override fun defaultBuildOptions(): BuildOptions = super.defaultBuildOptions().copy( jsIrBackend = irBackend, diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinJsLibraryGradlePluginIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinJsLibraryGradlePluginIT.kt index 1c79a8937ea..2d76295da36 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinJsLibraryGradlePluginIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinJsLibraryGradlePluginIT.kt @@ -10,6 +10,8 @@ import org.jetbrains.kotlin.gradle.util.modify import org.junit.Test class KotlinJsIrLibraryGradlePluginIT : BaseGradleIT() { + override val defaultGradleVersion = GradleVersionRequired.AtLeast("6.1") + override fun defaultBuildOptions(): BuildOptions = super.defaultBuildOptions().copy( jsIrBackend = true, diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/tasks/CleanDataTaskIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/tasks/CleanDataTaskIT.kt index cf6b7805d3b..52743e8d225 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/tasks/CleanDataTaskIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/tasks/CleanDataTaskIT.kt @@ -14,7 +14,7 @@ import org.junit.Test class CleanDataTaskIT : BaseGradleIT() { override val defaultGradleVersion: GradleVersionRequired - get() = GradleVersionRequired.AtLeast("5.5.1") + get() = GradleVersionRequired.AtLeast("6.1") @Test fun testDownloadedFolderDeletion() { diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/KotlinTestReport.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/KotlinTestReport.kt index 23e592e4837..feb5d5e71bf 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/KotlinTestReport.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/KotlinTestReport.kt @@ -7,6 +7,7 @@ package org.jetbrains.kotlin.gradle.testing.internal import org.gradle.api.GradleException import org.gradle.api.execution.TaskExecutionGraph +import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction @@ -47,9 +48,12 @@ open class KotlinTestReport : TestReport() { @Transient 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 + @Transient val children = mutableListOf>() @Transient @@ -66,14 +70,24 @@ open class KotlinTestReport : TestReport() { @Input var ignoreFailures: Boolean = false - private val hasOwnFailedTests - get() = failedTestsListener.hasOwnFailedTests ?: false + private val testReportServiceProvider = TestReportService.registerIfAbsent(project.gradle) + private val testReportService + get() = testReportServiceProvider.get() + private val hasFailedTests: Boolean - get() = hasOwnFailedTests || children.any { it.get().hasFailedTests } + get() = testReportService.hasFailedTests(path) - private val suppressedRunningFailureListeners: MutableSet = mutableSetOf() + private val failedTestsListener = FailedTestListener(parentPaths, testReportServiceProvider) - private val failedTestsListener = FailedTestListener() + private fun computeAllParentTasksPaths(): List { + val allParents = mutableListOf() + var cur: KotlinTestReport? = this + while (cur != null) { + allParents.add(cur.path) + cur = cur.parent + } + return allParents + } fun addChild(childProvider: TaskProvider) { val child = childProvider.get() @@ -85,7 +99,7 @@ open class KotlinTestReport : TestReport() { reportOnChildTasks(childProvider) } - private fun reportOnChildTasks(childProvider: TaskProvider): Unit { + private fun reportOnChildTasks(childProvider: TaskProvider) { val child = childProvider.get() child.testTasks.forEach { @@ -101,9 +115,8 @@ open class KotlinTestReport : TestReport() { task.addTestListener(failedTestsListener) if (task is KotlinTest) { - val listener = SuppressedTestRunningFailureListener(task.path) + val listener = SuppressedTestRunningFailureListener(parentPaths, task.path, testReportServiceProvider) task.addRunListener(listener) - suppressedRunningFailureListeners.add(listener) } reportOn(task) @@ -119,7 +132,7 @@ open class KotlinTestReport : TestReport() { } private fun reportOn(task: AbstractTestTask) { - reportOn(task.binResultsDir) + reportOn(task.binaryResultsDirectory) } open val htmlReportUrl: String? @@ -133,7 +146,6 @@ open class KotlinTestReport : TestReport() { fun checkFailedTests() { if (checkFailedTests) { checkSuppressedRunningFailures() - if (hasFailedTests) { if (ignoreFailures) { logger.warn(getFailingTestsMessage()) @@ -155,27 +167,12 @@ open class KotlinTestReport : TestReport() { } private fun checkSuppressedRunningFailures() { - val allSuppressedRunningFailures = mutableListOf>() - - 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 taskFailures = testReportService.getAggregatedTaskFailures(path) + if (taskFailures.isNotEmpty()) { val allErrors = mutableListOf() val msg = buildString { appendln("Failed to execute all tests:") - allSuppressedRunningFailures.groupBy { it.first }.forEach { path, errors -> + taskFailures.groupBy { it.first }.forEach { (path, errors) -> append(path) append(": ") var first = true @@ -246,40 +243,39 @@ open class KotlinTestReport : TestReport() { task.reports.junitXml.isEnabled = false } - private class SuppressedTestRunningFailureListener(val taskPath: String) : KotlinTestRunnerListener { - @Transient - private var failuresMutable: MutableList? = mutableListOf() - - val failures: List - get() = failuresMutable ?: emptyList() - + private class SuppressedTestRunningFailureListener( + private val allListenedTaskParentsPaths: Provider>, + private val failedTaskPath: String, + private val testReportServiceProvider: Provider + ) : KotlinTestRunnerListener { override fun runningFailure(failure: Error) { - var failures = failuresMutable - if (failures == null) { - // it is possible after deserialization - failures = mutableListOf() - failuresMutable = failures + allListenedTaskParentsPaths.get().forEach { + testReportServiceProvider.get().reportFailure(failedTaskPath, it, failure) } - failures.add(failure) } } - private class FailedTestListener : TestListener { - @Transient - var hasOwnFailedTests: Boolean? = false - - override fun beforeTest(testDescriptor: TestDescriptor) { - } + private class FailedTestListener( + private val allListenedTaskParentsPaths: Provider>, + private val testReportServiceProvider: Provider + ) : TestListener { + override fun beforeTest(testDescriptor: TestDescriptor) {} 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) { + reportFailure(result) + } + + private fun reportFailure(result: TestResult) { if (result.failedTestCount > 0) { - hasOwnFailedTests = true + allListenedTaskParentsPaths.get().forEach { + testReportServiceProvider.get().testFailed(it) + } } } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/TestReportService.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/TestReportService.kt new file mode 100644 index 00000000000..5755704187a --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/TestReportService.kt @@ -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 + +abstract class TestReportService : BuildService { + private val taskHasFailedTests = mutableMapOf() + private val taskSuppressedFailures = mutableMapOf>() + + 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 { + return taskSuppressedFailures[taskPath] ?: emptyList() + } + + companion object { + fun registerIfAbsent(gradle: Gradle): Provider { + // 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 + ) {} + } + } +} \ No newline at end of file