From a5664207d5e47db9e1e7a51f6e5cb27d03be140d Mon Sep 17 00:00:00 2001 From: Sergey Rostov Date: Wed, 22 May 2019 14:00:06 +0300 Subject: [PATCH] Gradle, js, tests: always create a jsTest task for compatibility This requires runnable aggregate test task even if there are not tests in it. That, in turn, requires support aggregate test tasks hierarchy. Also TestReport integrated in aggregate for all gradle versions (was for 5 only). #KT-31527 Fixed --- .../gradle/targets/js/KotlinJsTarget.kt | 8 +- .../targets/js/KotlinJsTargetConfigurator.kt | 7 +- .../testing/internal/AggregateTestReport.kt | 97 ---------- .../internal/AggregateTestReportGradle5.kt | 117 ----------- .../testing/internal/KotlinTestReport.kt | 181 ++++++++++++++++++ .../gradle/testing/internal/testsRegistry.kt | 54 +++--- 6 files changed, 222 insertions(+), 242 deletions(-) delete mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/AggregateTestReport.kt delete mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/AggregateTestReportGradle5.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/KotlinTestReport.kt diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTarget.kt index 73f3070c037..cd4a2fb8ea8 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTarget.kt @@ -17,7 +17,8 @@ import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsNodeDsl import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl import org.jetbrains.kotlin.gradle.targets.js.subtargets.KotlinBrowserJs import org.jetbrains.kotlin.gradle.targets.js.subtargets.KotlinNodeJs -import org.jetbrains.kotlin.gradle.testing.internal.getAggregatedTestTask +import org.jetbrains.kotlin.gradle.testing.internal.allTestsTask +import org.jetbrains.kotlin.gradle.testing.internal.getOrCreateAggregatedTestTask import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName class KotlinJsTarget(project: Project, platformType: KotlinPlatformType) : @@ -25,10 +26,11 @@ class KotlinJsTarget(project: Project, platformType: KotlinPlatformType) : val testTaskName get() = lowerCamelCaseName(disambiguationClassifier, testTaskNameSuffix) val testTask - get() = project.getAggregatedTestTask( + get() = project.getOrCreateAggregatedTestTask( name = testTaskName, description = "Run JS tests for all platforms", - reportName = "jsAll" + reportName = "jsAll", + parent = project.allTestsTask.doGetTask() ) val runTaskName get() = lowerCamelCaseName(disambiguationClassifier, runTaskNameSuffix) diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt index a7183812341..1ac56ce7991 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt @@ -5,6 +5,7 @@ package org.jetbrains.kotlin.gradle.targets.js +import org.gradle.language.base.plugins.LifecycleBasePlugin import org.jetbrains.kotlin.gradle.plugin.Kotlin2JsSourceSetProcessor import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetProcessor import org.jetbrains.kotlin.gradle.plugin.KotlinTargetConfigurator @@ -27,7 +28,11 @@ open class KotlinJsTargetConfigurator(kotlinPluginVersion: String) : } override fun configureTest(target: KotlinOnlyTarget) { - // tests configured in KotlinJsSubTarget.configure + target as KotlinJsTarget + + // always create jsTest task for compatibility (KT-31527) + // actual tests tasks for browser and nodejs will be configured in KotlinJsSubTarget.configure + target.testTask } override fun buildCompilationProcessor(compilation: KotlinJsCompilation): KotlinSourceSetProcessor<*> { diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/AggregateTestReport.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/AggregateTestReport.kt deleted file mode 100644 index 78ea58aa6a2..00000000000 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/AggregateTestReport.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2010-2019 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.GradleException -import org.gradle.api.internal.AbstractTask -import org.gradle.api.tasks.* -import org.gradle.api.tasks.testing.AbstractTestTask -import org.gradle.api.tasks.testing.TestDescriptor -import org.gradle.api.tasks.testing.TestListener -import org.gradle.api.tasks.testing.TestResult -import java.io.File - -open class AggregateTestReport : AbstractTask() { - @Internal - val testTasks = mutableListOf() - - /** - * Returns the set of binary test results to include in the report. - */ - @Suppress("MemberVisibilityCanBePrivate") - val testResultDirs: Collection - @PathSensitive(PathSensitivity.NONE) - @InputFiles - @SkipWhenEmpty - get() { - val dirs = mutableListOf() - testTasks.forEach { - @Suppress("UnstableApiUsage") - dirs.add(it.binResultsDir) - } - return dirs - } - - @Input - var ignoreFailures: Boolean = false - - private var hasFailedTests = false - - private val failedTestsListener = object : TestListener { - override fun beforeTest(testDescriptor: TestDescriptor) { - } - - override fun afterSuite(suite: TestDescriptor, result: TestResult) { - } - - override fun beforeSuite(suite: TestDescriptor) { - } - - override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) { - if (result.failedTestCount > 0) { - hasFailedTests = true - } - } - } - - @Input - var checkFailedTests: Boolean = false - - fun registerTestTask(task: AbstractTestTask) { - testTasks.add(task) - task.addTestListener(failedTestsListener) - } - - open fun configureReportsConvention(name: String) { - - } - - @Internal - protected open val htmlReportUrl: String? = null - - protected fun checkFailedTests() { - if (checkFailedTests && hasFailedTests) { - val message = StringBuilder("There were failing tests.") - - val reportUrl = htmlReportUrl - if (reportUrl != null) { - message.append(" See the report at: $reportUrl") - } - - if (ignoreFailures) { - logger.warn(message.toString()) - } else { - throw GradleException(message.toString()) - } - } - } - - open fun overrideReporting(task: AbstractTestTask) { - task.ignoreFailures = true - checkFailedTests = true - ignoreFailures = false - } -} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/AggregateTestReportGradle5.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/AggregateTestReportGradle5.kt deleted file mode 100644 index 237ef441091..00000000000 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/AggregateTestReportGradle5.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2010-2019 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.internal.CollectionCallbackActionDecorator -import org.gradle.api.internal.tasks.testing.DefaultTestTaskReports -import org.gradle.api.internal.tasks.testing.junit.result.* -import org.gradle.api.internal.tasks.testing.report.DefaultTestReport -import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.Nested -import org.gradle.api.tasks.TaskAction -import org.gradle.api.tasks.testing.AbstractTestTask -import org.gradle.internal.concurrent.CompositeStoppable.stoppable -import org.gradle.internal.logging.ConsoleRenderer -import org.gradle.internal.operations.BuildOperationExecutor -import org.gradle.internal.reflect.Instantiator -import org.gradle.internal.remote.internal.inet.InetAddressFactory -import org.jetbrains.kotlin.gradle.utils.injected -import java.util.* -import javax.inject.Inject - -@Suppress("UnstableApiUsage") -open class AggregateTestReportGradle5 : AggregateTestReport() { - @Internal - val reports: DefaultTestTaskReports - - protected open val instantiator: Instantiator - @Inject get() = injected - - protected open val collectionCallbackActionDecorator: CollectionCallbackActionDecorator - @Inject get() = injected - - protected open val buildOperationExecutor: BuildOperationExecutor - @Inject get() = injected - - protected open val inetAddressFactory: InetAddressFactory - @Inject get() = injected - - init { - @Suppress("LeakingThis") - reports = instantiator.newInstance(DefaultTestTaskReports::class.java, this, collectionCallbackActionDecorator) - reports.junitXml.isEnabled = true - reports.html.isEnabled = true - } - - override fun configureReportsConvention(name: String) { - reports.configureConventions(project, name) - } - - override val htmlReportUrl: String? - get() = - if (reports.html.isEnabled) ConsoleRenderer().asClickableFileUrl(reports.html.destination.resolve("index.html")) - else null - - @TaskAction - fun generateReport() { - val resultsProvider = createAggregateProvider() - try { - if (resultsProvider.isHasResults) { - val junitXml = reports.junitXml - if (junitXml.isEnabled) { - Binary2JUnitXmlReportGenerator( - junitXml.destination, - resultsProvider, - when { - junitXml.isOutputPerTestCase -> TestOutputAssociation.WITH_TESTCASE - else -> TestOutputAssociation.WITH_SUITE - }, - buildOperationExecutor, - inetAddressFactory.hostname - ).generate() - } - - val html = reports.html - if (html.isEnabled) { - val testReport = DefaultTestReport(buildOperationExecutor) - testReport.generateReport(resultsProvider, html.destination) - } - } else { - logger.info("{} - no binary test results found in dirs: {}.", path, testResultDirs) - didWork = false - } - } finally { - stoppable(resultsProvider).stop() - } - - checkFailedTests() - } - - private fun createAggregateProvider(): TestResultsProvider { - val resultsProviders = LinkedList() - try { - val resultDirs = testResultDirs - return if (resultDirs.size == 1) BinaryResultBackedTestResultsProvider(resultDirs.single()) - else AggregateTestResultsProvider(resultDirs.mapTo(resultsProviders) { BinaryResultBackedTestResultsProvider(it) }) - } catch (e: RuntimeException) { - stoppable(resultsProviders).stop() - throw e - } - } - - override fun overrideReporting(task: AbstractTestTask) { - task.ignoreFailures = true - - @Suppress("UnstableApiUsage") - task.reports.html.isEnabled = false - - @Suppress("UnstableApiUsage") - task.reports.junitXml.isEnabled = false - - checkFailedTests = true - ignoreFailures = false - } -} \ No newline at end of file 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 new file mode 100644 index 00000000000..09958c9d5a9 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/KotlinTestReport.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2010-2019 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.GradleException +import org.gradle.api.execution.TaskExecutionGraph +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.testing.* +import java.io.File +import java.net.URI + +/** + * Aggregates tests reports for kotlin test tasks added by [registerTestTask]. + * + * Individual test tasks will not fail build if this task will be executed, + * also individual html and xml reports will replaced by one consolidated html report. + * This behavior can be disabled by setting [overrideReporting] to `false`. + * + * Aggregate test reports may form hierarchy, for example: + * - allTest // aggregates all tests + * - jvmTest + * - nativeTest // aggregates all native tests + * - macos64Test + * - linux64Test + * - mingw64 + * - jsTests // aggregates all js tests + * - browserTest + * - nodejsTest + * + * In this case, only topmost aggregate test task will override reporting, + * event if child tasks will be executed. + */ +open class KotlinTestReport : TestReport() { + @Internal + val testTasks = mutableListOf() + + @Internal + private var parent: KotlinTestReport? = null + + @Internal + val children = mutableListOf() + + @Input + var overrideReporting: Boolean = true + + @Input + var checkFailedTests: Boolean = false + + @Input + var ignoreFailures: Boolean = false + + private var hasOwnFailedTests = false + private val hasFailedTests: Boolean + get() = hasOwnFailedTests || children.any { it.hasFailedTests } + + private val failedTestsListener = object : TestListener { + override fun beforeTest(testDescriptor: TestDescriptor) { + } + + override fun afterSuite(suite: TestDescriptor, result: TestResult) { + } + + override fun beforeSuite(suite: TestDescriptor) { + } + + override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) { + if (result.failedTestCount > 0) { + hasOwnFailedTests = true + } + } + } + + fun addChild(child: KotlinTestReport) { + check(child.parent == null) { "$child already registers as child of ${child.parent}" } + child.parent = this + + children.add(child) + reportOnChildTasks(child) + } + + private fun reportOnChildTasks(child: KotlinTestReport) { + child.testTasks.forEach { + reportOn(it) + } + child.children.forEach { + reportOnChildTasks(it) + } + } + + fun registerTestTask(task: AbstractTestTask) { + testTasks.add(task) + task.addTestListener(failedTestsListener) + reportOn(task) + + addToParents(task) + } + + private fun addToParents(task: AbstractTestTask) { + val parent = parent + if (parent != null) { + parent.reportOn(task) + parent.addToParents(task) + } + } + + private fun reportOn(task: AbstractTestTask) { + @Suppress("UnstableApiUsage") + reportOn(task.binResultsDir) + } + + open val htmlReportUrl: String? + @Internal get() = destinationDir?.let { asClickableFileUrl(it.resolve("index.html")) } + + private fun asClickableFileUrl(path: File): String { + return URI("file", "", path.toURI().path, null, null).toString() + } + + @TaskAction + fun checkFailedTests() { + if (checkFailedTests && hasFailedTests) { + val message = StringBuilder("There were failing tests.") + + val reportUrl = htmlReportUrl + if (reportUrl != null) { + message.append(" See the report at: $reportUrl") + } + + if (ignoreFailures) { + logger.warn(message.toString()) + } else { + throw GradleException(message.toString()) + } + } + } + + fun maybeOverrideReporting(graph: TaskExecutionGraph) { + if (!graph.hasTask(this)) return + + // only topmost aggregate should override reporting + var parent = parent + while (parent != null) { + if (parent.overrideReporting && graph.hasTask(parent)) return + parent = parent.parent + } + + overrideReporting() + } + + private fun overrideReporting() { + ignoreFailures = false + checkFailedTests = true + + disableIndividualTestTaskReportingAndFailing() + } + + private fun disableIndividualTestTaskReportingAndFailing() { + testTasks.forEach { + disableTestReporting(it) + } + + children.forEach { + it.checkFailedTests = false + it.disableIndividualTestTaskReportingAndFailing() + } + } + + private fun disableTestReporting(task: AbstractTestTask) { + task.ignoreFailures = true + + @Suppress("UnstableApiUsage") + task.reports.html.isEnabled = false + + @Suppress("UnstableApiUsage") + task.reports.junitXml.isEnabled = false + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/testsRegistry.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/testsRegistry.kt index 2141c0004c8..148c16e11f4 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/testsRegistry.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/testing/internal/testsRegistry.kt @@ -11,33 +11,51 @@ import org.gradle.api.plugins.JavaBasePlugin import org.gradle.api.tasks.testing.AbstractTestTask import org.gradle.language.base.plugins.LifecycleBasePlugin import org.jetbrains.kotlin.gradle.plugin.TaskHolder -import org.jetbrains.kotlin.gradle.tasks.locateOrRegisterTask +import org.jetbrains.kotlin.gradle.tasks.createOrRegisterTask +import org.jetbrains.kotlin.gradle.tasks.locateTask -private val Project.allTestsTask: TaskHolder - get() = getAggregatedTestTask( +internal val Project.allTestsTask: TaskHolder + get() = getOrCreateAggregatedTestTask( name = "allTests", description = "Runs the tests for all targets and create aggregated report", reportName = "all" - ).also { + ) { tasks.maybeCreate(LifecycleBasePlugin.CHECK_TASK_NAME) .dependsOn(it.getTaskOrProvider()) } -internal fun Project.getAggregatedTestTask(name: String, description: String, reportName: String): TaskHolder { - return locateOrRegisterTask(name) { aggregate -> +internal fun Project.getOrCreateAggregatedTestTask( + name: String, + description: String, + reportName: String, + parent: KotlinTestReport? = null, + configure: (TaskHolder) -> Unit = {} +): TaskHolder { + val existed = locateTask(name) + if (existed != null) return existed + + val aggregate: TaskHolder = createOrRegisterTask(name) { aggregate -> aggregate.description = description aggregate.group = JavaBasePlugin.VERIFICATION_GROUP - aggregate.configureReportsConvention(reportName) - - aggregate.onlyIf { - aggregate.testTasks.size > 1 - } + aggregate.destinationDir = testReportsDir.resolve(reportName) if (System.getProperty("idea.active") != null) { aggregate.extensions.extraProperties.set("idea.internal.test", true) } + + project.gradle.taskGraph.whenReady { graph -> + aggregate.maybeOverrideReporting(graph) + } + + parent?.addChild(aggregate) } + + parent?.dependsOn(aggregate.getTaskOrProvider()) + + configure(aggregate) + + return aggregate } private fun cleanTaskName(taskName: String): String { @@ -61,7 +79,7 @@ internal fun registerTestTask(taskHolder: TaskHolder) { internal fun registerTestTaskInAggregate( taskHolder: TaskHolder, - allTests: AggregateTestReport + allTests: KotlinTestReport ) { val project = taskHolder.project @@ -71,18 +89,6 @@ internal fun registerTestTaskInAggregate( taskHolder.configure { task -> allTests.registerTestTask(task) - - project.gradle.taskGraph.whenReady { - if (it.hasTask(allTests)) { - // when [allTestsTask] task enabled, test failure should be reported only on [allTestsTask], - // not at individual target's test tasks. To do that, we need: - // - disable all reporting in test tasks - // - enable [checkFailedTests] on [allTestsTask] - - allTests.overrideReporting(task) - } - } - ijListenTestTask(task) } } \ No newline at end of file