Skip to content

Commit

Permalink
Implemented protype of Kover Aggregation Plugin
Browse files Browse the repository at this point in the history
Relates #608

PR #644
  • Loading branch information
shanshin authored Jun 26, 2024
1 parent 325bd50 commit e03bc8c
Show file tree
Hide file tree
Showing 34 changed files with 1,377 additions and 6 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,8 @@ val Project.sourceSets: SourceSetContainer

val SourceSetContainer.main: NamedDomainObjectProvider<SourceSet>
get() = named<SourceSet>("main")

signing {
// disable signing if private key isn't passed
isRequired = findProperty("libs.sign.key.private") != null
}
15 changes: 15 additions & 0 deletions kover-gradle-plugin/api/kover-gradle-plugin.api
Original file line number Diff line number Diff line change
@@ -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 <init> ()V
public synthetic fun apply (Ljava/lang/Object;)V
Expand Down
41 changes: 36 additions & 5 deletions kover-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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")

Expand Down Expand Up @@ -180,11 +201,6 @@ extensions.configure<Kover_publishing_conventions_gradle.KoverPublicationExtensi
addPublication.set(false)
}

signing {
// disable signing if private key isn't passed
isRequired = findProperty("libs.sign.key.private") != null
}

gradlePlugin {
website.set("https://github.com/Kotlin/kotlinx-kover")
vcsUrl.set("https://github.com/Kotlin/kotlinx-kover.git")
Expand All @@ -199,3 +215,18 @@ gradlePlugin {
}
}
}

gradlePlugin {
website.set("https://github.com/Kotlin/kotlinx-kover")
vcsUrl.set("https://github.com/Kotlin/kotlinx-kover.git")

plugins {
create("KoverSettingsPlugin") {
id = "org.jetbrains.kotlinx.kover.aggregation"
implementationClass = "kotlinx.kover.gradle.aggregation.settings.KoverSettingsGradlePlugin"
displayName = "Gradle Settings Plugin for Kotlin Code Coverage Tools"
description = "Evaluate code coverage for projects written in Kotlin, applied only inside settings.gradle[.kts] files"
tags.addAll("kover", "kotlin", "coverage", "settings plugin")
}
}
}
73 changes: 73 additions & 0 deletions kover-gradle-plugin/docs/aggregated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Kover Aggregated Plugin

## Quickstart
This plugin is a prototype for testing ideas related to an alternative configuration method.

The main difference from the existing Kover Gradle Plugin is the reactive content of the report: only those classes and tests that were compiled and executed within the same build get into the report.

To use the plugin, just add into a `settings.gradle.kts` file
```kotlin
plugins {
id("org.jetbrains.kotlinx.kover.aggregation") version "0.8.1"
}
```
**There is no need to apply Kover plugin in other places, the `org.jetbrains.kotlinx.kover` plug-in should not be applied anywhere.**

To measure coverage you should pass special `-Pkover` argument to Gradle CLI command and call the command to generate the corresponding report `koverHtmlReport` or `koverXmlReport`.
Example, if you want to measure the coverage of the `test` task:
```shell
./gradlew test -Pkover koverHtmlReport
```

Only those classes that were compiled as part of the current Gradle build are included in the report.
If no compilation tasks were called in the build, the report will be empty.

The report covers only those tests that were run as part of the current build.
If no tests were called in the assembly, then the coverage for all classes will be 0.

## Configuring
At the moment, Kover Settings Plugin allows to configure reports minimally.

There are two ways to configure, using a build script in `settings.gradle.kts` and CLI

Acceptable settings in `settings.gradle.kts` and their equivalent in CLI
```kotlin
kover {
// -Pkover
enableCoverage()

reports {
// -Pkover.projects.includes=:a
includedProjects.add(":a")

// -Pkover.projects.excludes=:b
excludedProjects.add(":b")

// -Pkover.classes.includes=classes.to.exclude.*
includedClasses.add("classes.to.include.*")

// -Pkover.classes.excludes=classes.to.include.*
excludedClasses.add("classes.to.exclude.*")

// -Pkover.classes.excludesAnnotated=*.Generated*
excludesAnnotatedBy.add("*.Generated*")

// -Pkover.classes.includesAnnotated=*Included*
includesAnnotatedBy.add("*Included*")
}
}
```

For example, the following setting is in `settings.gradle.kts`
```kotlin
kover {
enableCoverage()

reports {
excludedClasses.add("org.test.MyClass*")
}
}
```
fully equivalent to Gradle CLI arguments `-Pkover -Pkover.classes.excludes=org.test.MyClass*`

**Any of the specified settings or DSL is preliminary and can be deleted or changed without maintaining backward compatibility**
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
*/
package kotlinx.kover.gradle.plugin.test.functional.cases

import kotlinx.kover.gradle.plugin.commons.CoverageToolVendor
import kotlinx.kover.gradle.plugin.test.functional.framework.configurator.*
import kotlinx.kover.gradle.plugin.test.functional.framework.starter.*

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.kover.gradle.plugin.test.functional.cases

import kotlinx.kover.gradle.plugin.test.functional.framework.checker.CheckerContext
import kotlinx.kover.gradle.plugin.test.functional.framework.starter.TemplateTest
import kotlin.test.assertFalse
import kotlin.test.assertTrue

internal class SettingsPluginTests {
@TemplateTest("settings-plugin", [":tasks"])
fun CheckerContext.testNoReportTasks() {
taskOutput(":tasks") {
assertFalse("koverXmlReport" in this)
assertFalse("koverHtmlReport" in this)
}
}

@TemplateTest("settings-plugin", ["test"])
fun CheckerContext.testNoInstrumentation() {
checkDefaultBinReport(false)
subproject("subproject") {
checkDefaultBinReport(false)
}
}

@TemplateTest("settings-plugin", ["-Pkover", ":tasks", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
fun CheckerContext.testHasReportTasks() {
taskOutput(":tasks") {
assertTrue("koverXmlReport" in this)
assertTrue("koverHtmlReport" in this)
}
}

@TemplateTest("settings-plugin", ["-Pkover", "koverXmlReport", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
fun CheckerContext.testNoCompilations() {
xmlReport {
classCounter("tests.settings.root.RootClass").assertAbsent()
classCounter("tests.settings.subproject.SubprojectClass").assertAbsent()
}
}

@TemplateTest("settings-plugin", ["-Pkover", ":compileKotlin", "koverXmlReport", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
fun CheckerContext.testCompilationOnlyForRoot() {
xmlReport {
classCounter("tests.settings.root.RootClass").assertFullyMissed()
classCounter("tests.settings.subproject.SubprojectClass").assertAbsent()
}
}

@TemplateTest("settings-plugin", ["-Pkover", ":subproject:compileKotlin", ":test", "koverXmlReport", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
fun CheckerContext.testRootAndOnlyCompileSubproject() {
xmlReport {
classCounter("tests.settings.root.RootClass").assertFullyCovered()
classCounter("tests.settings.subproject.SubprojectClass").assertFullyMissed()
}
}


@TemplateTest("settings-plugin", ["-Pkover", "test", "koverXmlReport", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
fun CheckerContext.testAll() {
xmlReport {
classCounter("tests.settings.root.RootClass").assertFullyCovered()
classCounter("tests.settings.subproject.SubprojectClass").assertFullyCovered()
}
}

@TemplateTest("settings-plugin", ["-Pkover", "test", "koverXmlReport", "-Pkover.projects.excludes=:subproject", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
fun CheckerContext.testExcludeSubproject() {
xmlReport {
classCounter("tests.settings.root.RootClass").assertFullyCovered()
classCounter("tests.settings.subproject.SubprojectClass").assertAbsent()
}
}

@TemplateTest("settings-plugin", ["-Pkover", "test", "koverXmlReport", "-Pkover.classes.excludes=tests.settings.subproject.*", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
fun CheckerContext.testExcludeClasses() {
xmlReport {
classCounter("tests.settings.root.RootClass").assertFullyCovered()
classCounter("tests.settings.subproject.SubprojectClass").assertAbsent()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ internal fun FormattedWriter.writePluginManagement(language: ScriptLanguage,
call("resolutionStrategy") {
call("eachPlugin") {
line("if (requested.id.id == \"org.jetbrains.kotlinx.kover\") { useVersion(\"$koverVersion\") }")
line("if (requested.id.id == \"org.jetbrains.kotlinx.kover.aggregation\") { useVersion(\"$koverVersion\") }")
if (overrideKotlinVersion != null) {
line("if (requested.id.id == \"org.jetbrains.kotlin.jvm\") useVersion(\"$overrideKotlinVersion\")")
line("if (requested.id.id == \"org.jetbrains.kotlin.multiplatform\") useVersion(\"$overrideKotlinVersion\")")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
kotlin("jvm") version ("2.0.0")
}

dependencies {
testImplementation(kotlin("test"))
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}

plugins {
id("org.jetbrains.kotlinx.kover.aggregation") version "SNAPSHOT"
}

buildCache {
local {
directory = "$settingsDir/build-cache"
}
}

rootProject.name = "settings-plugin"

include(":subproject")
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package tests.settings.root

class RootClass {
fun action() {
println("It's root class")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package tests.settings.root

import kotlin.test.Test

class RootTest {
@Test
fun test() {
RootClass().action()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
kotlin("jvm")
}

dependencies {
testImplementation(kotlin("test"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package tests.settings.subproject

class SubprojectClass {
fun action() {
println("It's class from the subproject")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package tests.settings.subproject

import kotlin.test.Test

class SubprojectTest {
@Test
fun test() {
SubprojectClass().action()
}
}
Loading

0 comments on commit e03bc8c

Please sign in to comment.