Skip to content

Commit 2bbb2d3

Browse files
committed
Implemented prototype of Kover Aggregated Plugin
Relates #608
1 parent 06f4de4 commit 2bbb2d3

36 files changed

+1360
-7
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ in this case report will be generated for current project joined with `:another:
113113

114114
**More examples of Gradle plugin applying can be found in [example folder](kover-gradle-plugin/examples)**
115115

116+
## Kover Aggregated Plugin
117+
Kover Aggregated Plugin as a prototype of Gradle Settings plugin, created to simplify the setup of multi-project builds.
118+
It is in its infancy, it is recommended to use it only for test or pet projects.
119+
120+
Refer to the [documentation](https://kotlin.github.io/kotlinx-kover/gradle-plugin/aggregated.html) for details.
121+
116122
## Kover CLI
117123
Standalone JVM application used for offline instrumentation and generation of human-readable reports.
118124

build-logic/src/main/kotlin/kotlinx/kover/conventions/kover-publishing-conventions.gradle.kts

+5
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,8 @@ val Project.sourceSets: SourceSetContainer
210210

211211
val SourceSetContainer.main: NamedDomainObjectProvider<SourceSet>
212212
get() = named<SourceSet>("main")
213+
214+
signing {
215+
// disable signing if private key isn't passed
216+
isRequired = findProperty("libs.sign.key.private") != null
217+
}

kover-gradle-plugin/build.gradle.kts

+37-6
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ val functionalTestImplementation = "functionalTestImplementation"
4242

4343
dependencies {
4444
implementation(project(":kover-features-jvm"))
45+
implementation(project(":kover-jvm-agent"))
4546
// exclude transitive dependency on stdlib, the Gradle version should be used
4647
compileOnly(kotlin("stdlib"))
4748
compileOnly(libs.gradlePlugin.kotlin)
@@ -52,6 +53,13 @@ dependencies {
5253

5354
snapshotRelease(project(":kover-features-jvm"))
5455
snapshotRelease(project(":kover-jvm-agent"))
56+
57+
functionalTestImplementation(gradleTestKit())
58+
// dependencies only for plugin's classpath to work with Kotlin Multi-Platform and Android plugins
59+
functionalTestImplementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$embeddedKotlinVersion")
60+
functionalTestImplementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:$embeddedKotlinVersion")
61+
functionalTestImplementation("org.jetbrains.kotlin:kotlin-compiler-runner:$embeddedKotlinVersion")
62+
5563
}
5664

5765
kotlin {
@@ -70,7 +78,20 @@ val functionalTest by tasks.registering(Test::class) {
7078
useJUnitPlatform()
7179

7280
dependsOn(tasks.collectRepository)
81+
82+
// While gradle testkit supports injection of the plugin classpath it doesn't allow using dependency notation
83+
// to determine the actual runtime classpath for the plugin. It uses isolation, so plugins applied by the build
84+
// script are not visible in the plugin classloader. This means optional dependencies (dependent on applied plugins -
85+
// for example kotlin multiplatform) are not visible even if they are in regular gradle use. This hack will allow
86+
// extending the classpath. It is based upon: https://docs.gradle.org/6.0/userguide/test_kit.html#sub:test-kit-classpath-injection
87+
// Create a configuration to register the dependencies against
7388
doFirst {
89+
val file = File(temporaryDir, "plugin-classpath.txt")
90+
file.writeText(sourceSets["functionalTest"].compileClasspath
91+
.filter { it.name.startsWith("stdlib") }
92+
.joinToString("\n"))
93+
systemProperties["plugin-classpath"] = file.absolutePath
94+
7495
// basic build properties
7596
setSystemPropertyFromProject("kover.test.kotlin.version")
7697

@@ -87,7 +108,7 @@ val functionalTest by tasks.registering(Test::class) {
87108
systemProperties["junit.jupiter.execution.parallel.config.fixed.parallelism"] =
88109
junitParallelism?.toIntOrNull()?.toString() ?: "2"
89110
// this is necessary if tests are run for debugging, in this case it is more difficult to stop at the test you need when they are executed in parallel and you are not sure on which test the execution will pause
90-
systemProperties["junit.jupiter.execution.parallel.enabled"] = if (junitParallelism == "no") "false" else "true"
111+
systemProperties["junit.jupiter.execution.parallel.enabled"] = if (junitParallelism == "no") "false" else "false"
91112

92113

93114
// customizing functional tests
@@ -180,11 +201,6 @@ extensions.configure<Kover_publishing_conventions_gradle.KoverPublicationExtensi
180201
addPublication.set(false)
181202
}
182203

183-
signing {
184-
// disable signing if private key isn't passed
185-
isRequired = findProperty("libs.sign.key.private") != null
186-
}
187-
188204
gradlePlugin {
189205
website.set("https://github.com/Kotlin/kotlinx-kover")
190206
vcsUrl.set("https://github.com/Kotlin/kotlinx-kover.git")
@@ -199,3 +215,18 @@ gradlePlugin {
199215
}
200216
}
201217
}
218+
219+
gradlePlugin {
220+
website.set("https://github.com/Kotlin/kotlinx-kover")
221+
vcsUrl.set("https://github.com/Kotlin/kotlinx-kover.git")
222+
223+
plugins {
224+
create("KoverSettingsPlugin") {
225+
id = "org.jetbrains.kotlinx.kover.settings"
226+
implementationClass = "kotlinx.kover.gradle.aggregation.settings.KoverSettingsGradlePlugin"
227+
displayName = "Gradle Plugin for Kotlin Code Coverage Tools"
228+
description = "Evaluate code coverage for projects written in Kotlin"
229+
tags.addAll("kover", "kotlin", "coverage", "settings plugin")
230+
}
231+
}
232+
}
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Kover Aggregated Plugin
2+
3+
## Quickstart
4+
This plugin is a prototype for testing ideas related to an alternative configuration method.
5+
6+
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.
7+
8+
To use the plugin, just add into a `settings.gradle.kts` file
9+
```kotlin
10+
plugins {
11+
id("org.jetbrains.kotlinx.kover.settings") version "0.8.1"
12+
}
13+
```
14+
**There is no need to apply Kover plugin in other places, the `org.jetbrains.kotlinx.kover` plug-in should not be applied anywhere.**
15+
16+
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`.
17+
Example, if you want to measure the coverage of the `test` task:
18+
```shell
19+
./gradlew test -Pkover koverHtmlReport
20+
```
21+
22+
Only those classes that were compiled as part of the current Gradle build are included in the report.
23+
If no compilation tasks were called in the build, the report will be empty.
24+
25+
The report covers only those tests that were run as part of the current build.
26+
If no tests were called in the assembly, then the coverage for all classes will be 0.
27+
28+
## Configuring
29+
At the moment, Kover Settings Plugin allows to configure reports minimally.
30+
31+
There are two ways to configure, using a build script in `settings.gradle.kts` and CLI
32+
33+
Acceptable settings in `settings.gradle.kts` and their equivalent in CLI
34+
```kotlin
35+
kover {
36+
// -Pkover
37+
enableCoverage()
38+
39+
reports {
40+
// -Pkover.classes.from=:a
41+
includedProjects.add(":a")
42+
43+
// -Pkover.classes.from.excludes=:b
44+
excludedProjects.add(":b")
45+
46+
// -Pkover.classes.includes=classes.to.exclude.*
47+
includedClasses.add("classes.to.include.*")
48+
49+
// -Pkover.classes.excludes=classes.to.include.*
50+
excludedClasses.add("classes.to.exclude.*")
51+
}
52+
}
53+
```
54+
55+
For example, the following setting is in `settings.gradle.kts`
56+
```kotlin
57+
kover {
58+
enableCoverage()
59+
60+
reports {
61+
excludedClasses.add("org.test.MyClass*")
62+
}
63+
}
64+
```
65+
fully equivalent to Gradle CLI arguments `-Pkover -Pkover.classes.excludes=org.test.MyClass*`
66+
67+
**Any of the specified settings or DSL is preliminary and can be deleted or changed without maintaining backward compatibility**

kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/ReportsCachingTests.kt

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
*/
44
package kotlinx.kover.gradle.plugin.test.functional.cases
55

6-
import kotlinx.kover.gradle.plugin.commons.CoverageToolVendor
76
import kotlinx.kover.gradle.plugin.test.functional.framework.configurator.*
87
import kotlinx.kover.gradle.plugin.test.functional.framework.starter.*
98

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.kover.gradle.plugin.test.functional.cases
6+
7+
import kotlinx.kover.gradle.plugin.test.functional.framework.checker.CheckerContext
8+
import kotlinx.kover.gradle.plugin.test.functional.framework.starter.TemplateTest
9+
import kotlin.test.assertFalse
10+
import kotlin.test.assertTrue
11+
12+
internal class SettingsPluginTests {
13+
@TemplateTest("settings-plugin", [":tasks"])
14+
fun CheckerContext.testNoReportTasks() {
15+
taskOutput(":tasks") {
16+
assertFalse("koverXmlReport" in this)
17+
assertFalse("koverHtmlReport" in this)
18+
}
19+
}
20+
21+
@TemplateTest("settings-plugin", ["test"])
22+
fun CheckerContext.testNoInstrumentation() {
23+
checkDefaultBinReport(false)
24+
subproject("subproject") {
25+
checkDefaultBinReport(false)
26+
}
27+
}
28+
29+
@TemplateTest("settings-plugin", ["-Pkover", ":tasks", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
30+
fun CheckerContext.testHasReportTasks() {
31+
taskOutput(":tasks") {
32+
assertTrue("koverXmlReport" in this)
33+
assertTrue("koverHtmlReport" in this)
34+
}
35+
}
36+
37+
@TemplateTest("settings-plugin", ["-Pkover", "koverXmlReport", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
38+
fun CheckerContext.testNoCompilations() {
39+
xmlReport {
40+
classCounter("tests.settings.root.RootClass").assertAbsent()
41+
classCounter("tests.settings.subproject.SubprojectClass").assertAbsent()
42+
}
43+
}
44+
45+
@TemplateTest("settings-plugin", ["-Pkover", ":compileKotlin", "koverXmlReport", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
46+
fun CheckerContext.testCompilationOnlyForRoot() {
47+
xmlReport {
48+
classCounter("tests.settings.root.RootClass").assertFullyMissed()
49+
classCounter("tests.settings.subproject.SubprojectClass").assertAbsent()
50+
}
51+
}
52+
53+
@TemplateTest("settings-plugin", ["-Pkover", ":subproject:compileKotlin", ":test", "koverXmlReport", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
54+
fun CheckerContext.testRootAndOnlyCompileSubproject() {
55+
xmlReport {
56+
classCounter("tests.settings.root.RootClass").assertFullyCovered()
57+
classCounter("tests.settings.subproject.SubprojectClass").assertFullyMissed()
58+
}
59+
}
60+
61+
62+
@TemplateTest("settings-plugin", ["-Pkover", "test", "koverXmlReport", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
63+
fun CheckerContext.testAll() {
64+
xmlReport {
65+
classCounter("tests.settings.root.RootClass").assertFullyCovered()
66+
classCounter("tests.settings.subproject.SubprojectClass").assertFullyCovered()
67+
}
68+
}
69+
70+
@TemplateTest("settings-plugin", ["-Pkover", "test", "koverXmlReport", "-Pkover.classes.from.excludes=:subproject", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
71+
fun CheckerContext.testExcludeSubproject() {
72+
xmlReport {
73+
classCounter("tests.settings.root.RootClass").assertFullyCovered()
74+
classCounter("tests.settings.subproject.SubprojectClass").assertAbsent()
75+
}
76+
}
77+
78+
@TemplateTest("settings-plugin", ["-Pkover", "test", "koverXmlReport", "-Pkover.classes.excludes=tests.settings.subproject.*", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"])
79+
fun CheckerContext.testExcludeClasses() {
80+
xmlReport {
81+
classCounter("tests.settings.root.RootClass").assertFullyCovered()
82+
classCounter("tests.settings.subproject.SubprojectClass").assertAbsent()
83+
}
84+
}
85+
}

kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/framework/writer/SettingsWriter.kt

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ internal fun FormattedWriter.writePluginManagement(language: ScriptLanguage,
1515
call("resolutionStrategy") {
1616
call("eachPlugin") {
1717
line("if (requested.id.id == \"org.jetbrains.kotlinx.kover\") { useVersion(\"$koverVersion\") }")
18+
line("if (requested.id.id == \"org.jetbrains.kotlinx.kover.settings\") { useVersion(\"$koverVersion\") }")
1819
if (overrideKotlinVersion != null) {
1920
line("if (requested.id.id == \"org.jetbrains.kotlin.jvm\") useVersion(\"$overrideKotlinVersion\")")
2021
line("if (requested.id.id == \"org.jetbrains.kotlin.multiplatform\") useVersion(\"$overrideKotlinVersion\")")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
plugins {
2+
kotlin("jvm") version ("2.0.0")
3+
}
4+
5+
dependencies {
6+
testImplementation(kotlin("test"))
7+
}
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
pluginManagement {
2+
repositories {
3+
gradlePluginPortal()
4+
mavenCentral()
5+
}
6+
}
7+
8+
plugins {
9+
id("org.jetbrains.kotlinx.kover.settings") version "SNAPSHOT"
10+
}
11+
12+
buildCache {
13+
local {
14+
directory = "$settingsDir/build-cache"
15+
}
16+
}
17+
18+
rootProject.name = "settings-plugin"
19+
20+
include(":subproject")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package tests.settings.root
6+
7+
class RootClass {
8+
fun action() {
9+
println("It's root class")
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package tests.settings.root
6+
7+
import kotlin.test.Test
8+
9+
class RootTest {
10+
@Test
11+
fun test() {
12+
RootClass().action()
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
plugins {
2+
kotlin("jvm")
3+
}
4+
5+
dependencies {
6+
testImplementation(kotlin("test"))
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package tests.settings.subproject
6+
7+
class SubprojectClass {
8+
fun action() {
9+
println("It's class from the subproject")
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package tests.settings.subproject
2+
3+
import kotlin.test.Test
4+
5+
class SubprojectTest {
6+
@Test
7+
fun test() {
8+
SubprojectClass().action()
9+
}
10+
}

0 commit comments

Comments
 (0)