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
This commit is contained in:
Sergey Rostov
2019-05-22 14:00:06 +03:00
parent 78fe2fde33
commit a5664207d5
6 changed files with 222 additions and 242 deletions
@@ -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)
@@ -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<KotlinJsCompilation>) {
// 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<*> {
@@ -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<AbstractTestTask>()
/**
* Returns the set of binary test results to include in the report.
*/
@Suppress("MemberVisibilityCanBePrivate")
val testResultDirs: Collection<File>
@PathSensitive(PathSensitivity.NONE)
@InputFiles
@SkipWhenEmpty
get() {
val dirs = mutableListOf<File>()
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
}
}
@@ -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<TestResultsProvider>()
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
}
}
@@ -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<AbstractTestTask>()
@Internal
private var parent: KotlinTestReport? = null
@Internal
val children = mutableListOf<KotlinTestReport>()
@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
}
}
@@ -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<AggregateTestReport>
get() = getAggregatedTestTask(
internal val Project.allTestsTask: TaskHolder<KotlinTestReport>
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<AggregateTestReport> {
return locateOrRegisterTask(name) { aggregate ->
internal fun Project.getOrCreateAggregatedTestTask(
name: String,
description: String,
reportName: String,
parent: KotlinTestReport? = null,
configure: (TaskHolder<KotlinTestReport>) -> Unit = {}
): TaskHolder<KotlinTestReport> {
val existed = locateTask<KotlinTestReport>(name)
if (existed != null) return existed
val aggregate: TaskHolder<KotlinTestReport> = 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<AbstractTestTask>) {
internal fun registerTestTaskInAggregate(
taskHolder: TaskHolder<AbstractTestTask>,
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)
}
}