diff --git a/README.md b/README.md index b9435338..a08f7928 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,12 @@ in this case report will be generated for current project joined with `:another: **More examples of Gradle plugin applying can be found in [example folder](kover-gradle-plugin/examples)** +## Kover Aggregated Plugin +Kover Aggregated Plugin as a prototype of Gradle Settings plugin, created to simplify the setup of multi-project builds. +It is in its infancy, it is recommended to use it only for test or pet projects. + +Refer to the [documentation](https://kotlin.github.io/kotlinx-kover/gradle-plugin/aggregated.html) for details. + ## Kover CLI Standalone JVM application used for offline instrumentation and generation of human-readable reports. diff --git a/build-logic/src/main/kotlin/kotlinx/kover/conventions/kover-publishing-conventions.gradle.kts b/build-logic/src/main/kotlin/kotlinx/kover/conventions/kover-publishing-conventions.gradle.kts index 5477dd8d..daf086da 100644 --- a/build-logic/src/main/kotlin/kotlinx/kover/conventions/kover-publishing-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/kotlinx/kover/conventions/kover-publishing-conventions.gradle.kts @@ -210,3 +210,8 @@ val Project.sourceSets: SourceSetContainer val SourceSetContainer.main: NamedDomainObjectProvider get() = named("main") + +signing { + // disable signing if private key isn't passed + isRequired = findProperty("libs.sign.key.private") != null +} diff --git a/kover-gradle-plugin/api/kover-gradle-plugin.api b/kover-gradle-plugin/api/kover-gradle-plugin.api index df2eaa65..fd5c0ad3 100644 --- a/kover-gradle-plugin/api/kover-gradle-plugin.api +++ b/kover-gradle-plugin/api/kover-gradle-plugin.api @@ -1,3 +1,18 @@ +public abstract interface class kotlinx/kover/gradle/aggregation/settings/dsl/KoverSettingsExtension { + public abstract fun enableCoverage ()V + public abstract fun getReports ()Lkotlinx/kover/gradle/aggregation/settings/dsl/ReportsSettings; + public abstract fun reports (Lorg/gradle/api/Action;)V +} + +public abstract interface class kotlinx/kover/gradle/aggregation/settings/dsl/ReportsSettings { + public abstract fun getExcludedClasses ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getExcludedProjects ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getExcludesAnnotatedBy ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getIncludedClasses ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getIncludedProjects ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getIncludesAnnotatedBy ()Lorg/gradle/api/provider/SetProperty; +} + public final class kotlinx/kover/gradle/plugin/KoverGradlePlugin : org/gradle/api/Plugin { public fun ()V public synthetic fun apply (Ljava/lang/Object;)V diff --git a/kover-gradle-plugin/build.gradle.kts b/kover-gradle-plugin/build.gradle.kts index 7ead46a1..68d9433a 100644 --- a/kover-gradle-plugin/build.gradle.kts +++ b/kover-gradle-plugin/build.gradle.kts @@ -42,6 +42,7 @@ val functionalTestImplementation = "functionalTestImplementation" dependencies { implementation(project(":kover-features-jvm")) + implementation(project(":kover-jvm-agent")) // exclude transitive dependency on stdlib, the Gradle version should be used compileOnly(kotlin("stdlib")) compileOnly(libs.gradlePlugin.kotlin) @@ -52,6 +53,13 @@ dependencies { snapshotRelease(project(":kover-features-jvm")) snapshotRelease(project(":kover-jvm-agent")) + + functionalTestImplementation(gradleTestKit()) + // dependencies only for plugin's classpath to work with Kotlin Multi-Platform and Android plugins + functionalTestImplementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$embeddedKotlinVersion") + functionalTestImplementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:$embeddedKotlinVersion") + functionalTestImplementation("org.jetbrains.kotlin:kotlin-compiler-runner:$embeddedKotlinVersion") + } kotlin { @@ -70,7 +78,20 @@ val functionalTest by tasks.registering(Test::class) { useJUnitPlatform() dependsOn(tasks.collectRepository) + + // While gradle testkit supports injection of the plugin classpath it doesn't allow using dependency notation + // to determine the actual runtime classpath for the plugin. It uses isolation, so plugins applied by the build + // script are not visible in the plugin classloader. This means optional dependencies (dependent on applied plugins - + // for example kotlin multiplatform) are not visible even if they are in regular gradle use. This hack will allow + // extending the classpath. It is based upon: https://docs.gradle.org/6.0/userguide/test_kit.html#sub:test-kit-classpath-injection + // Create a configuration to register the dependencies against doFirst { + val file = File(temporaryDir, "plugin-classpath.txt") + file.writeText(sourceSets["functionalTest"].compileClasspath + .filter { it.name.startsWith("stdlib") } + .joinToString("\n")) + systemProperties["plugin-classpath"] = file.absolutePath + // basic build properties setSystemPropertyFromProject("kover.test.kotlin.version") @@ -180,11 +201,6 @@ extensions.configure + appender.appendLine( "REPORT=${report.toRelativeString(rootDir)}") + } + info.compilations.forEach { (name, info) -> + appender.appendLine("[COMPILATION]") + appender.appendLine("NAME=$name") + info.sourceDirs.forEach { sourceDir -> + appender.appendLine("SOURCE=${sourceDir.toRelativeString(rootDir)}") + } + info.outputDirs.forEach { outputDir -> + appender.appendLine("OUTPUT=${outputDir.toRelativeString(rootDir)}") + } + appender.appendLine("[END]") + } + } + + fun deserialize(reader: Reader, rootDir: File): ProjectArtifactInfo { + class Comp( + var name: String? = null, + val sourceDirs: MutableSet = mutableSetOf(), + val outputDirs: MutableSet = mutableSetOf() + ) + + var projectPath: String? = null + val reports: MutableSet = mutableSetOf() + val all: MutableList = mutableListOf() + + var current: Comp? = null + + reader.forEachLine { line -> + when { + line.startsWith("PROJECT=") -> { + projectPath = line.substringAfter("PROJECT=") + } + + line.startsWith("REPORT=") -> { + reports.add(rootDir.resolve(line.substringAfter("REPORT="))) + } + + line.startsWith("[COMPILATION]") -> { current = Comp() } + line.startsWith("NAME=") -> { + current?.name = line.substringAfter("NAME=") + } + + line.startsWith("SOURCE=") -> { + current?.sourceDirs?.add(rootDir.resolve(line.substringAfter("SOURCE="))) + } + line.startsWith("OUTPUT=") -> { + current?.outputDirs?.add(rootDir.resolve(line.substringAfter("OUTPUT="))) + } + line.startsWith("[END]") -> { + all += current!! + current = null + } + } + } + + val map = all.associate { it.name!! to CompilationInfo(it.sourceDirs.toList(), it.outputDirs.toSet()) } + return ProjectArtifactInfo(projectPath!!, reports, map) + } +} + +internal class ProjectArtifactInfo( + @get:Input + val path: String, + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + val reports: Collection, + + @get:Nested + val compilations: Map +) + +internal class CompilationInfo( + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + val sourceDirs: Collection, + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + val outputDirs: Collection +) \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/artifacts/Configurations.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/artifacts/Configurations.kt new file mode 100644 index 00000000..9bf76b2c --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/artifacts/Configurations.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.commons.artifacts + +import org.gradle.api.artifacts.Configuration + +/** + * Mark this [Configuration] as a transitive. + * + * Dependencies must be added to this configuration, as a result of its resolution, artifacts from these dependencies are returned. + * + * See: https://docs.gradle.org/7.5.1/userguide/declaring_dependencies.html + */ +internal fun Configuration.asTransitiveDependencies() { + isVisible = false + isCanBeConsumed = false + isTransitive = true + isCanBeResolved = true +} + +/** + * Mark this [Configuration] as a bucket for declaring dependencies. + * + * Bucket combines artifacts from the specified dependencies, + * and allows you to resolve in consumer configuration. + * + * See: https://docs.gradle.org/7.5.1/userguide/declaring_dependencies.html#sec:resolvable-consumable-configs + */ +internal fun Configuration.asDependency() { + isVisible = true + isCanBeResolved = false + isCanBeConsumed = false +} + +/** + * Mark this [Configuration] as a 'producer' that exposes artifacts and their dependencies for consumption by other + * projects + * + * See: https://docs.gradle.org/7.5.1/userguide/declaring_dependencies.html#sec:resolvable-consumable-configs + */ +internal fun Configuration.asProducer() { + // disable generation of Kover artifacts on `assemble`, fix of https://github.com/Kotlin/kotlinx-kover/issues/353 + isVisible = false + isCanBeResolved = false + // this configuration produces modules that can be consumed by other projects + isCanBeConsumed = true +} + + +/** + * Mark this [Configuration] as consumable, which means it’s an "exchange" meant for consumers. + * + * See: https://docs.gradle.org/7.5.1/userguide/declaring_dependencies.html#sec:resolvable-consumable-configs + */ +internal fun Configuration.asConsumer() { + isVisible = false + isCanBeResolved = true + // this config consumes modules from OTHER projects, and cannot be consumed by other projects + isCanBeConsumed = false +} diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/artifacts/KoverContentAttr.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/artifacts/KoverContentAttr.kt new file mode 100644 index 00000000..2ca76485 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/artifacts/KoverContentAttr.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.commons.artifacts + +import org.gradle.api.attributes.* + +internal interface KoverContentAttr { + companion object { + val ATTRIBUTE = Attribute.of( + "kotlinx.kover.content.type", + String::class.java + ) + + const val AGENT_JAR = "AgentJar" + const val LOCAL_ARTIFACT = "localArtifact" + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/names/KoverPaths.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/names/KoverPaths.kt new file mode 100644 index 00000000..4772e50d --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/names/KoverPaths.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.commons.names + +import java.io.File + +internal object KoverPaths { + internal fun htmlReportPath(): String { + return "reports${separator}kover${separator}html" + } + + internal fun xmlReportPath(): String { + return "reports${separator}kover${separator}report.xml" + } + + internal fun binReportPath(taskName: String): String { + return "${binReportsRootPath()}$separator${binReportName(taskName)}" + } + + internal fun binReportName(taskName: String) = "$taskName.bin" + + internal fun binReportsRootPath() = "kover${separator}bin-reports" + + private val separator = File.separatorChar +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/names/PluginId.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/names/PluginId.kt new file mode 100644 index 00000000..3256b314 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/names/PluginId.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.commons.names + +internal object PluginId { + const val KOTLIN_JVM_PLUGIN_ID = "org.jetbrains.kotlin.jvm" + const val KOTLIN_MULTIPLATFORM_PLUGIN_ID = "org.jetbrains.kotlin.multiplatform" +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/names/SettingsNames.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/names/SettingsNames.kt new file mode 100644 index 00000000..e5908130 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/names/SettingsNames.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.commons.names + +internal object SettingsNames { + const val DEPENDENCY_AGENT = "koverJar" +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/utils/Dynamic.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/utils/Dynamic.kt new file mode 100644 index 00000000..446d0765 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/utils/Dynamic.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.commons.utils + +import org.gradle.internal.metaobject.* + +internal fun Any?.bean(): DynamicBean = DynamicBean(this) + +internal class DynamicBean(private val origin: Any?) { + private val wrappedOrigin = origin?.let { BeanDynamicObject(origin) } + + operator fun get(name: String): DynamicBean = getNotNull("get property '$name'").getProperty(name).bean() + + operator fun contains(name: String): Boolean = getNotNull("check for a property '$name'").hasProperty(name) + + inline fun value(): T { + val notNull = origin ?: throw IllegalStateException("Value is null, failed to get value") + return notNull as? T ?: throw IllegalStateException("Invalid property value type, expected ${T::class.qualifiedName}, found ${notNull::class.qualifiedName}") + } + + fun sequence(): Sequence { + return value>().asSequence().map { it.bean() } + } + + private fun getNotNull(extra: String): BeanDynamicObject { + return wrappedOrigin ?: throw IllegalArgumentException("Wrapped value is null, failed to $extra") + } +} + +internal fun Any.hasSuper(className: String): Boolean { + val kClass: Class<*> = this::class.java + return extendedOf(className, kClass) +} + +private fun extendedOf(className: String, currentClass: Class<*>): Boolean { + if (currentClass.simpleName == className) return true + + if (currentClass.superclass != null) { + if (extendedOf(className, currentClass.superclass)) return true + } + + for (iface in currentClass.interfaces) { + if (extendedOf(className, iface)) return true + } + + return false +} diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/KoverProjectGradlePlugin.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/KoverProjectGradlePlugin.kt new file mode 100644 index 00000000..27cefac2 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/KoverProjectGradlePlugin.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.project + +import kotlinx.kover.features.jvm.KoverFeatures +import kotlinx.kover.gradle.aggregation.commons.artifacts.CompilationInfo +import kotlinx.kover.gradle.aggregation.commons.artifacts.KoverContentAttr +import kotlinx.kover.gradle.aggregation.commons.artifacts.asConsumer +import kotlinx.kover.gradle.aggregation.commons.artifacts.asProducer +import kotlinx.kover.gradle.aggregation.commons.names.KoverPaths.binReportName +import kotlinx.kover.gradle.aggregation.commons.names.KoverPaths.binReportsRootPath +import kotlinx.kover.gradle.aggregation.commons.names.PluginId.KOTLIN_JVM_PLUGIN_ID +import kotlinx.kover.gradle.aggregation.commons.names.PluginId.KOTLIN_MULTIPLATFORM_PLUGIN_ID +import kotlinx.kover.gradle.aggregation.commons.names.SettingsNames +import kotlinx.kover.gradle.aggregation.commons.utils.bean +import kotlinx.kover.gradle.aggregation.commons.utils.hasSuper +import kotlinx.kover.gradle.aggregation.project.instrumentation.InstrumentationFilter +import kotlinx.kover.gradle.aggregation.project.instrumentation.JvmOnFlyInstrumenter +import kotlinx.kover.gradle.aggregation.project.tasks.ArtifactGenerationTask +import kotlinx.kover.gradle.aggregation.project.tasks.KoverAgentSearchTask +import kotlinx.kover.gradle.plugin.commons.KoverCriticalException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.withType +import java.io.File + +internal class KoverProjectGradlePlugin : Plugin { + + override fun apply(target: Project) { + if (target.path == Project.PATH_SEPARATOR) { + target.configureAgentSearch() + } + + target.configureInstrumentation() + target.configureArtifactGeneration() + } + + private fun Project.configureInstrumentation() { + val koverJarDependency = configurations.getByName(SettingsNames.DEPENDENCY_AGENT) + val jarConfig = configurations.create("agentJarSource") { + asConsumer() + attributes { + attribute(KoverContentAttr.ATTRIBUTE, KoverContentAttr.AGENT_JAR) + } + extendsFrom(koverJarDependency) + } + JvmOnFlyInstrumenter.instrument(tasks.withType(), jarConfig, InstrumentationFilter(setOf(), setOf())) + } + + private fun Project.configureArtifactGeneration() { + val taskGraph = gradle.taskGraph + + val artifactFile = layout.buildDirectory.file("kover/kover.artifact") + + // we create task immediately because of mustRunAfter + val generateArtifactTask = tasks.create("koverGenerateArtifact") + generateArtifactTask.outputFile.set(artifactFile) + + // add tests + val testTasks = tasks.withType().matching { task -> + taskGraph.hasTask(task.path) + } + + val binReportFiles = project.layout.buildDirectory.dir(binReportsRootPath()) + .map { dir -> testTasks.map { dir.file(binReportName(it.name)) } } + + val exts = extensions + val pluginManager = pluginManager + val projectPath = path + + val compilations = project.layout.buildDirectory.map { + val compilations = when { + pluginManager.hasPlugin(KOTLIN_JVM_PLUGIN_ID) -> { + val kotlin = exts.findByName("kotlin")?.bean() + ?: throw KoverCriticalException("Kotlin JVM extension not found") + kotlin["target"]["compilations"].sequence() + } + + pluginManager.hasPlugin(KOTLIN_MULTIPLATFORM_PLUGIN_ID) -> { + val kotlin = exts.findByName("kotlin")?.bean() + ?: throw KoverCriticalException("Kotlin JVM multiplatform not found") + kotlin["targets"].sequence() + .filter { + val platformType = it["platformType"]["name"].value() + platformType == "jvm" || platformType == "androidJvm" + }.flatMap { + it["compilations"].sequence() + } + } + else -> emptySequence() + } + + compilations.filter { compilation -> + val compilationName = compilation["name"].value() + if (compilationName == "test") return@filter false + + val taskPath = projectPath + (if (projectPath == Project.PATH_SEPARATOR) "" else Project.PATH_SEPARATOR) + compilation["compileTaskProvider"]["name"].value() + taskGraph.hasTask(taskPath) + } + } + + val compilationMap = compilations.map { allCompilations -> + allCompilations.associate { compilation -> + val sourceDirs = compilation["allKotlinSourceSets"].sequence() + .flatMap { sourceSet -> sourceSet["kotlin"]["srcDirs"].sequence().map { it.value() } } + .toSet() + val outputDirs = compilation["output"]["classesDirs"].value().files + + compilation["name"].value() to CompilationInfo(sourceDirs, outputDirs) + } + } + + // TODO describe the trick + tasks.withType().configureEach { + generateArtifactTask.mustRunAfter(this) + } + tasks.withType().configureEach { + if (this.hasSuper("KotlinCompilationTask")) { + generateArtifactTask.mustRunAfter(this) + } + } + tasks.withType().configureEach { + generateArtifactTask.mustRunAfter(this) + } + + generateArtifactTask.compilations.putAll(compilationMap) + generateArtifactTask.reportFiles.from(binReportFiles) + + configurations.register("KoverArtifactProducer") { + asProducer() + attributes { + attribute(KoverContentAttr.ATTRIBUTE, KoverContentAttr.LOCAL_ARTIFACT) + } + + outgoing.artifact(artifactFile) { + // Before resolving this configuration, it is necessary to execute the task of generating an artifact + builtBy(generateArtifactTask) + } + } + } + + + private fun Project.configureAgentSearch() { + val agentConfiguration = configurations.create("AgentConfiguration") + dependencies.add(agentConfiguration.name, "org.jetbrains.kotlinx:kover-jvm-agent:${KoverFeatures.version}") + + val agentJar = layout.buildDirectory.file("kover/kover-jvm-agent-${KoverFeatures.version}.jar") + + val findAgentTask = tasks.register("koverAgentSearch") + findAgentTask.configure { + this@configure.agentJar.set(agentJar) + dependsOn(agentConfiguration) + agentClasspath.from(agentConfiguration) + } + + configurations.register("AgentJar") { + asProducer() + attributes { + attribute(KoverContentAttr.ATTRIBUTE, KoverContentAttr.AGENT_JAR) + } + + outgoing.artifact(agentJar) { + // Before resolving this configuration, it is necessary to execute the task of generating an artifact + builtBy(findAgentTask) + } + } + } + +} + + diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/instrumentation/JvmTestTaskInstrumentation.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/instrumentation/JvmTestTaskInstrumentation.kt new file mode 100644 index 00000000..4abe3767 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/instrumentation/JvmTestTaskInstrumentation.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.project.instrumentation + +import kotlinx.kover.gradle.aggregation.commons.names.KoverPaths +import org.gradle.api.Named +import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.gradle.api.tasks.testing.Test +import org.gradle.process.CommandLineArgumentProvider +import java.io.File + + +internal object JvmOnFlyInstrumenter { + + /** + * Add online instrumentation to all JVM test tasks. + */ + fun instrument( + tasks: TaskCollection, + jarConfiguration: Configuration, + filter: InstrumentationFilter + ) { + tasks.configureEach { + val binReportProvider = + project.layout.buildDirectory.map { dir -> + dir.file(KoverPaths.binReportPath(name)) + } + + doFirst { + // delete report so that when the data is re-measured, it is not appended to an already existing file + // see https://github.com/Kotlin/kotlinx-kover/issues/489 + binReportProvider.get().asFile.delete() + } + + // Always excludes android classes, see https://github.com/Kotlin/kotlinx-kover/issues/89 + val androidClasses = setOf( + // Always excludes android classes, see https://github.com/Kotlin/kotlinx-kover/issues/89 + "android.*", "com.android.*", + // excludes JVM internal classes, in some cases, errors occur when trying to instrument these classes, for example, when using JaCoCo + Robolectric. There is also no point in instrumenting them in Kover. + "jdk.internal.*" + ) + + val excludedClassesWithAndroid = filter.copy(excludes = filter.excludes + androidClasses) + + dependsOn(jarConfiguration) + jvmArgumentProviders += JvmTestTaskArgumentProvider( + temporaryDir, + project.objects.fileCollection().from(jarConfiguration), + excludedClassesWithAndroid, + binReportProvider + ) + } + } +} + +internal data class InstrumentationFilter( + @get:Input + val includes: Set, + @get:Input + val excludes: Set +) + +/** + * Provider of additional JVM string arguments for running a test task. + */ +private class JvmTestTaskArgumentProvider( + private val tempDir: File, + + // relative sensitivity for file is a comparison by file name and its contents + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + val jarFiles: ConfigurableFileCollection, + + @get:Nested + val filter: InstrumentationFilter, + + @get:OutputFile + val reportProvider: Provider +) : CommandLineArgumentProvider, Named { + + @Internal + override fun getName(): String { + return "koverArgumentsProvider" + } + + override fun asArguments(): MutableIterable { + val files = jarFiles.files + if (files.size != 1) { + return mutableSetOf() + } + val jarFile = files.single() + + return buildKoverJvmAgentArgs(jarFile, tempDir, reportProvider.get().asFile, filter.excludes) + .toMutableList() + } +} + + +private fun buildKoverJvmAgentArgs( + jarFile: File, + tempDir: File, + binReportFile: File, + excludedClasses: Set +): List { + val argsFile = tempDir.resolve("kover-agent.args") + argsFile.writeAgentArgs(binReportFile, excludedClasses) + + return mutableListOf("-javaagent:${jarFile.canonicalPath}=file:${argsFile.canonicalPath}") +} + +private fun File.writeAgentArgs(binReportFile: File, excludedClasses: Set) { + binReportFile.parentFile.mkdirs() + val binReportPath = binReportFile.canonicalPath + + printWriter().use { pw -> + pw.append("report.file=").appendLine(binReportPath) + excludedClasses.forEach { e -> + pw.append("exclude=").appendLine(e) + } + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/tasks/ArtifactGenerationTask.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/tasks/ArtifactGenerationTask.kt new file mode 100644 index 00000000..a484e867 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/tasks/ArtifactGenerationTask.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.project.tasks + +import kotlinx.kover.gradle.aggregation.commons.artifacts.ArtifactSerializer +import kotlinx.kover.gradle.aggregation.commons.artifacts.CompilationInfo +import kotlinx.kover.gradle.aggregation.commons.artifacts.ProjectArtifactInfo +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.tasks.* +import org.gradle.work.DisableCachingByDefault + +@DisableCachingByDefault(because = "There are no heavy computations") +internal abstract class ArtifactGenerationTask: DefaultTask() { + + @get:OutputFile + internal abstract val outputFile: RegularFileProperty + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val reportFiles: ConfigurableFileCollection + + @get:Nested + abstract val compilations: MapProperty + + @get:Input + internal val projectPath = project.path + + private val rootDir = project.rootDir + + @TaskAction + internal fun generate() { + val file = outputFile.get().asFile + file.parentFile.mkdirs() + + val projectInfo = ProjectArtifactInfo(projectPath, reportFiles.files, compilations.get()) + + file.bufferedWriter().use { writer -> + ArtifactSerializer.serialize(writer, rootDir, projectInfo) + } + } +} diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/tasks/KoverAgentSearchTask.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/tasks/KoverAgentSearchTask.kt new file mode 100644 index 00000000..3356245d --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/project/tasks/KoverAgentSearchTask.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.project.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.* +import org.gradle.work.DisableCachingByDefault + +/** + * Task to get on-the-wly JVM instrumentation agent's jar file for Kover coverage tool. + */ +@DisableCachingByDefault(because = "This task only copies one file") +internal abstract class KoverAgentSearchTask : DefaultTask() { + // relative sensitivity for file collections which are not FileTree is a comparison by file name and its contents + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val agentClasspath: ConfigurableFileCollection + + @get:OutputFile + abstract val agentJar: RegularFileProperty + + @TaskAction + fun find() { + val srcJar = agentClasspath.filter { it.name.startsWith("kover-jvm-agent") }.files.firstOrNull() + ?: throw GradleException("JVM instrumentation agent not found for Kover Coverage Tool") + + srcJar.copyTo(agentJar.get().asFile, true) + } +} diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/KoverParametersProcessor.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/KoverParametersProcessor.kt new file mode 100644 index 00000000..23516a5d --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/KoverParametersProcessor.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.settings + +import kotlinx.kover.gradle.aggregation.settings.dsl.intern.KoverSettingsExtensionImpl +import org.gradle.api.provider.HasMultipleValues +import org.gradle.api.provider.ProviderFactory + + +internal object KoverParametersProcessor { + fun process(settingsExtension: KoverSettingsExtensionImpl, providers: ProviderFactory) { + val koverProperty = providers.gradleProperty("kover") + if (koverProperty.isPresent) { + val disabled = koverProperty.get().equals("false", ignoreCase = true) + settingsExtension.coverageIsEnabled.set(!disabled) + } + + settingsExtension.reports.includedProjects.readAppendableArgument(providers, "kover.projects.includes") + settingsExtension.reports.excludedProjects.readAppendableArgument(providers, "kover.projects.excludes") + settingsExtension.reports.excludedClasses.readAppendableArgument(providers, "kover.classes.excludes") + settingsExtension.reports.includedClasses.readAppendableArgument(providers, "kover.classes.includes") + settingsExtension.reports.excludesAnnotatedBy.readAppendableArgument(providers, "kover.classes.excludesAnnotated") + settingsExtension.reports.includesAnnotatedBy.readAppendableArgument(providers, "kover.classes.includesAnnotated") + } + + private fun HasMultipleValues.readAppendableArgument(providers: ProviderFactory, propertyName: String) { + val propertyProvider = providers.gradleProperty(propertyName) + if (propertyProvider.isPresent) { + val arg = propertyProvider.get().parseCollection() + if (!arg.append) { + empty() + } + addAll(arg.values) + } + } + + private fun String.parseCollection(): ArgCollection { + val append = startsWith('+') + val str = if (append) substring(1) else this + val values = str.split(',') + return ArgCollection(append, values) + } + + private data class ArgCollection(val append: Boolean, val values: List) +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/KoverSettingsGradlePlugin.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/KoverSettingsGradlePlugin.kt new file mode 100644 index 00000000..b22e3466 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/KoverSettingsGradlePlugin.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.settings + +import kotlinx.kover.gradle.aggregation.commons.artifacts.KoverContentAttr +import kotlinx.kover.gradle.aggregation.commons.artifacts.asConsumer +import kotlinx.kover.gradle.aggregation.commons.artifacts.asDependency +import kotlinx.kover.gradle.aggregation.commons.names.KoverPaths +import kotlinx.kover.gradle.aggregation.settings.dsl.KoverNames +import kotlinx.kover.gradle.aggregation.settings.dsl.intern.KoverSettingsExtensionImpl +import kotlinx.kover.gradle.aggregation.commons.names.SettingsNames +import kotlinx.kover.gradle.aggregation.project.KoverProjectGradlePlugin +import kotlinx.kover.gradle.aggregation.settings.tasks.KoverHtmlReportTask +import kotlinx.kover.gradle.aggregation.settings.tasks.KoverXmlReportTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.initialization.ProjectDescriptor +import org.gradle.api.initialization.Settings +import org.gradle.api.model.ObjectFactory +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.support.serviceOf + +internal class KoverSettingsGradlePlugin: Plugin { + + override fun apply(target: Settings) { + val objects = target.serviceOf() + + val settingsExtension = target.extensions.create(KoverNames.settingsExtensionName, objects) + + target.gradle.settingsEvaluated { + KoverParametersProcessor.process(settingsExtension, providers) + } + + target.gradle.beforeProject { + if (!settingsExtension.coverageIsEnabled.get()) { + return@beforeProject + } + + val agentDependency = configurations.create(SettingsNames.DEPENDENCY_AGENT) { + asDependency() + } + dependencies.add(agentDependency.name, rootProject) + + if (path == Project.PATH_SEPARATOR) { + configureRootProject(target, settingsExtension) + } + + apply() + } + } + + private fun Project.configureRootProject(settings: Settings, settingsExtension: KoverSettingsExtensionImpl) { + val projectPath = path + + val dependencyConfig = configurations.create(KOVER_DEPENDENCY_NAME) { + asDependency() + } + val rootDependencies = dependencies + settings.rootProject.walkSubprojects { descriptor -> + rootDependencies.add(KOVER_DEPENDENCY_NAME, project(descriptor.path)) + } + + val artifacts = configurations.create("koverArtifactsCollector") { + asConsumer() + attributes { + attribute(KoverContentAttr.ATTRIBUTE, KoverContentAttr.LOCAL_ARTIFACT) + } + extendsFrom(dependencyConfig) + } + + val htmlTask = tasks.register("koverHtmlReport") + htmlTask.configure { + dependsOn(artifacts) + this.artifacts.from(artifacts) + group = "verification" + includedProjects.convention(settingsExtension.reports.includedProjects) + excludedProjects.convention(settingsExtension.reports.excludedProjects) + excludedClasses.convention(settingsExtension.reports.excludedClasses) + includedClasses.convention(settingsExtension.reports.includedClasses) + excludesAnnotatedBy.convention(settingsExtension.reports.excludesAnnotatedBy) + includesAnnotatedBy.convention(settingsExtension.reports.includesAnnotatedBy) + title.convention(projectPath) + + htmlDir.convention(layout.buildDirectory.dir(KoverPaths.htmlReportPath())) + + this.onlyIf { + // `onlyIf` is used to ensure that the path is always printed, even when the task is not running and has the FROM-CACHE outcome + printPath() + true + } + } + + val xmlTask = tasks.register("koverXmlReport") + xmlTask.configure { + dependsOn(artifacts) + this.artifacts.from(artifacts) + group = "verification" + includedProjects.convention(settingsExtension.reports.includedProjects) + excludedProjects.convention(settingsExtension.reports.excludedProjects) + excludedClasses.convention(settingsExtension.reports.excludedClasses) + includedClasses.convention(settingsExtension.reports.includedClasses) + excludesAnnotatedBy.convention(settingsExtension.reports.excludesAnnotatedBy) + includesAnnotatedBy.convention(settingsExtension.reports.includesAnnotatedBy) + title.convention(projectPath) + + reportFile.convention(layout.buildDirectory.file(KoverPaths.xmlReportPath())) + } + } + + private fun ProjectDescriptor.walkSubprojects(block: (ProjectDescriptor) -> Unit) { + block(this) + children.forEach { child -> + child.walkSubprojects(block) + } + } + + private val KOVER_DEPENDENCY_NAME = "kover" +} + diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/KoverNames.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/KoverNames.kt new file mode 100644 index 00000000..3e0ce17d --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/KoverNames.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.settings.dsl + +internal object KoverNames { + val settingsExtensionName = "kover" +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/KoverSettingsExtension.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/KoverSettingsExtension.kt new file mode 100644 index 00000000..e896a4ab --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/KoverSettingsExtension.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.settings.dsl + +import kotlinx.kover.gradle.plugin.dsl.KoverGradlePluginDsl +import org.gradle.api.Action +import org.gradle.api.provider.SetProperty + +@KoverGradlePluginDsl +public interface KoverSettingsExtension { + fun enableCoverage() + + val reports: ReportsSettings + fun reports(action: Action) +} + +@KoverGradlePluginDsl +public interface ReportsSettings { + val includedProjects: SetProperty + val excludedProjects: SetProperty + val excludedClasses: SetProperty + val includedClasses: SetProperty + val excludesAnnotatedBy: SetProperty + val includesAnnotatedBy: SetProperty +} diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/KoverSettingsExtensionImpl.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/KoverSettingsExtensionImpl.kt new file mode 100644 index 00000000..6629623a --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/KoverSettingsExtensionImpl.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.settings.dsl.intern + +import kotlinx.kover.gradle.aggregation.settings.dsl.KoverSettingsExtension +import kotlinx.kover.gradle.aggregation.settings.dsl.ReportsSettings +import org.gradle.api.Action +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.kotlin.dsl.newInstance +import javax.inject.Inject + +@Suppress("LeakingThis") +internal abstract class KoverSettingsExtensionImpl @Inject constructor( + objects: ObjectFactory +) : KoverSettingsExtension { + abstract val coverageIsEnabled: Property + + override val reports: ReportsSettings = objects.newInstance() + + init { + coverageIsEnabled.convention(false) + } + + override fun enableCoverage() { + coverageIsEnabled.set(true) + } + + override fun reports(action: Action) { + action.execute(reports) + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/AbstractKoverTask.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/AbstractKoverTask.kt new file mode 100644 index 00000000..9518fc1f --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/AbstractKoverTask.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.settings.tasks + +import kotlinx.kover.features.jvm.KoverFeatures +import kotlinx.kover.gradle.aggregation.commons.artifacts.ArtifactSerializer +import kotlinx.kover.gradle.aggregation.commons.artifacts.ProjectArtifactInfo +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.Provider +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.* + +internal abstract class AbstractKoverTask : DefaultTask() { + // relative sensitivity for file collections which are not FileTree is a comparison by file name and its contents + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val artifacts: ConfigurableFileCollection + + @get:Input + abstract val includedProjects: SetProperty + @get:Input + abstract val excludedProjects: SetProperty + @get:Input + abstract val includedClasses: SetProperty + @get:Input + abstract val excludedClasses: SetProperty + @get:Input + abstract val includesAnnotatedBy: SetProperty + @get:Input + abstract val excludesAnnotatedBy: SetProperty + + @get:Nested + val data: Provider> = artifacts.elements.map { elements -> + elements.map { location -> location.asFile } + .map { file -> ArtifactSerializer.deserialize(file.bufferedReader(), rootDir) } + .map(::filterProjectSources) + .associateBy { it.path } + } + + @get:Internal + protected val rootDir = project.rootDir + + @get:Internal + protected val reports get() = data.get().values.flatMap { artifact -> artifact.reports } + + @get:Internal + protected val sources get() = + data.get().values.flatMap { artifact -> artifact.compilations.flatMap { compilation -> compilation.value.sourceDirs } } + + @get:Internal + protected val outputs get() = + data.get().values.flatMap { artifact -> artifact.compilations.flatMap { compilation -> compilation.value.outputDirs } } + + private fun filterProjectSources(info: ProjectArtifactInfo): ProjectArtifactInfo { + val included = includedProjects.get() + val excluded = excludedProjects.get() + + if (included.isNotEmpty()) { + val notIncluded = included.none { filter -> + KoverFeatures.koverWildcardToRegex(filter).toRegex().matches(info.path) + } + if (notIncluded) { + return ProjectArtifactInfo(info.path, info.reports, emptyMap()) + } + } + + if (excluded.isNotEmpty()) { + val excl = excluded.any { filter -> + KoverFeatures.koverWildcardToRegex(filter).toRegex().matches(info.path) + } + if (excl) { + return ProjectArtifactInfo(info.path, info.reports, emptyMap()) + } + } + return info + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/KoverHtmlReportTask.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/KoverHtmlReportTask.kt new file mode 100644 index 00000000..d6cb7bc5 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/KoverHtmlReportTask.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.settings.tasks + +import kotlinx.kover.features.jvm.ClassFilters +import kotlinx.kover.features.jvm.KoverLegacyFeatures +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import java.io.File +import java.net.URI + +@CacheableTask +internal abstract class KoverHtmlReportTask : AbstractKoverTask() { + @get:OutputDirectory + abstract val htmlDir: DirectoryProperty + + @get:Input + abstract val title: Property + + @get:Input + @get:Optional + abstract val charset: Property + + private val projectPath = project.path + + @TaskAction + fun generate() { + KoverLegacyFeatures.generateHtmlReport( + htmlDir.asFile.get(), + charset.orNull, + reports, + outputs, + sources, + title.get(), + ClassFilters(includedClasses.get(), excludedClasses.get(), emptySet(), emptySet(), emptySet(), emptySet()) + ) + } + + fun printPath() { + val clickablePath = URI( + "file", + "", + File(htmlDir.get().asFile.canonicalPath, "index.html").toURI().path, + null, + null, + ).toASCIIString() + logger.lifecycle("Kover: HTML report for '$projectPath' $clickablePath") + } + +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/KoverXmlReportTask.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/KoverXmlReportTask.kt new file mode 100644 index 00000000..6b923a60 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/KoverXmlReportTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.aggregation.settings.tasks + +import kotlinx.kover.features.jvm.ClassFilters +import kotlinx.kover.features.jvm.KoverLegacyFeatures +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +@CacheableTask +internal abstract class KoverXmlReportTask : AbstractKoverTask() { + @get:OutputFile + internal abstract val reportFile: RegularFileProperty + + @get:Input + abstract val title: Property + + @TaskAction + fun generate() { + val xmlFile = reportFile.get().asFile + xmlFile.parentFile.mkdirs() + + KoverLegacyFeatures.generateXmlReport( + xmlFile, + reports, + outputs, + sources, + title.get(), + ClassFilters(includedClasses.get(), excludedClasses.get(), emptySet(), emptySet(), emptySet(), emptySet()) + ) + } + +} \ No newline at end of file