From 1acc0548ad33b0aeeccd4e95078490997abf2d54 Mon Sep 17 00:00:00 2001 From: Sergey Shanshin Date: Thu, 3 Oct 2024 13:33:27 +0300 Subject: [PATCH] Improved Kover Settings plugin - fixed small bug with non-existing binary report files - added ability to skip projects - added ability to limit instrumented class globally and locally in a project - added ability to exclude test task from instrumentation in a project config - implement feature to check verification rule on every project PR #689 Co-authored-by: Leonid Startsev --- .../api/kover-gradle-plugin.api | 26 +++ .../functional/cases/SettingsPluginTests.kt | 32 +++- .../build.gradle.kts | 12 ++ .../settings.gradle.kts | 27 +++ .../src/main/kotlin/RootClass.kt | 19 ++ .../src/test/kotlin/RootTest.kt | 15 ++ .../subproject/build.gradle.kts | 11 ++ .../src/main/kotlin/SubprojectClass.kt | 17 ++ .../subproject/src/test/kotlin/ATest.kt | 15 ++ .../subproject2/build.gradle.kts | 17 ++ .../src/main/kotlin/Subproject2Class.kt | 17 ++ .../subproject2/src/test/kotlin/ATest.kt | 15 ++ .../build.gradle.kts | 15 ++ .../ignored/build.gradle.kts | 11 ++ .../src/main/kotlin/SubprojectClass.kt | 11 ++ .../ignored/src/test/kotlin/ATest.kt | 14 ++ .../settings.gradle.kts | 46 +++++ .../src/main/kotlin/RootClass.kt | 28 +++ .../src/test/kotlin/RootTest.kt | 17 ++ .../subproject/build.gradle.kts | 12 ++ .../src/main/kotlin/SubprojectClass.kt | 17 ++ .../subproject/src/test/kotlin/ATest.kt | 16 ++ .../subproject2/build.gradle.kts | 11 ++ .../src/main/kotlin/SubprojectClass.kt | 23 +++ .../subproject2/src/test/kotlin/ATest.kt | 14 ++ .../commons/artifacts/ArtifactSerializer.kt | 4 +- .../project/KoverProjectGradlePlugin.kt | 55 ++---- .../JvmTestTaskInstrumentation.kt | 48 +++-- .../settings/KoverSettingsGradlePlugin.kt | 100 +++++++++- .../settings/dsl/KoverSettingsExtension.kt | 180 ++++++++++++++++++ .../dsl/intern/KoverProjectExtensionImpl.kt | 23 +++ .../dsl/intern/KoverSettingsExtensionImpl.kt | 8 +- .../dsl/intern/ReportsSettingsImpl.kt | 4 +- .../intern/VerificationRuleSettingsImpl.kt | 9 + .../settings/dsl/intern/VerifySettingsImpl.kt | 34 ++-- .../aggregation/settings/tasks/CommonTypes.kt | 18 +- .../settings/tasks/KoverReportVerifyTask.kt | 78 ++++++++ 37 files changed, 922 insertions(+), 97 deletions(-) create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/build.gradle.kts create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/settings.gradle.kts create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/src/main/kotlin/RootClass.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/src/test/kotlin/RootTest.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/build.gradle.kts create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/src/main/kotlin/SubprojectClass.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/src/test/kotlin/ATest.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/build.gradle.kts create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/src/main/kotlin/Subproject2Class.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/src/test/kotlin/ATest.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/build.gradle.kts create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/build.gradle.kts create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/src/main/kotlin/SubprojectClass.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/src/test/kotlin/ATest.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/settings.gradle.kts create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/src/main/kotlin/RootClass.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/src/test/kotlin/RootTest.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/build.gradle.kts create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/src/main/kotlin/SubprojectClass.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/src/test/kotlin/ATest.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/build.gradle.kts create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/src/main/kotlin/SubprojectClass.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/src/test/kotlin/ATest.kt create mode 100644 kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/KoverProjectExtensionImpl.kt create mode 100644 kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/KoverReportVerifyTask.kt diff --git a/kover-gradle-plugin/api/kover-gradle-plugin.api b/kover-gradle-plugin/api/kover-gradle-plugin.api index f81f863c..16200fae 100644 --- a/kover-gradle-plugin/api/kover-gradle-plugin.api +++ b/kover-gradle-plugin/api/kover-gradle-plugin.api @@ -11,10 +11,24 @@ public abstract interface class kotlinx/kover/gradle/aggregation/settings/dsl/Bo public abstract fun getMinValue ()Lorg/gradle/api/provider/Property; } +public abstract interface class kotlinx/kover/gradle/aggregation/settings/dsl/InstrumentationSettings { + public abstract fun getExcludedClasses ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getIncludedClasses ()Lorg/gradle/api/provider/SetProperty; +} + +public abstract interface class kotlinx/kover/gradle/aggregation/settings/dsl/KoverProjectExtension { + public abstract fun getInstrumentation ()Lkotlinx/kover/gradle/aggregation/settings/dsl/ProjectInstrumentationSettings; + public abstract fun instrumentation (Lorg/gradle/api/Action;)V +} + public abstract interface class kotlinx/kover/gradle/aggregation/settings/dsl/KoverSettingsExtension { public abstract fun enableCoverage ()V + public abstract fun getInstrumentation ()Lkotlinx/kover/gradle/aggregation/settings/dsl/InstrumentationSettings; public abstract fun getReports ()Lkotlinx/kover/gradle/aggregation/settings/dsl/ReportsSettings; + public abstract fun getSkipProjects ()Lorg/gradle/api/provider/SetProperty; + public abstract fun instrumentation (Lorg/gradle/api/Action;)V public abstract fun reports (Lorg/gradle/api/Action;)V + public fun skipProjects ([Ljava/lang/String;)V } public final class kotlinx/kover/gradle/aggregation/settings/dsl/KoverSettingsExtensionKt { @@ -28,6 +42,17 @@ public final class kotlinx/kover/gradle/aggregation/settings/dsl/KoverSettingsEx public static synthetic fun minBound$default (Lkotlinx/kover/gradle/aggregation/settings/dsl/VerificationRuleSettings;ILkotlinx/kover/gradle/plugin/dsl/CoverageUnit;Lkotlinx/kover/gradle/plugin/dsl/AggregationType;ILjava/lang/Object;)V } +public abstract interface class kotlinx/kover/gradle/aggregation/settings/dsl/ProjectInstrumentationSettings { + public abstract fun getDisabledForTestTasks ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getExcludedClasses ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getIncludedClasses ()Lorg/gradle/api/provider/SetProperty; +} + +public abstract interface class kotlinx/kover/gradle/aggregation/settings/dsl/ProjectVerificationRuleSettings : kotlinx/kover/gradle/aggregation/settings/dsl/VerificationRuleSettings { + public abstract fun getProjectName ()Ljava/lang/String; + public abstract fun getProjectPath ()Ljava/lang/String; +} + public abstract interface class kotlinx/kover/gradle/aggregation/settings/dsl/ReportFiltersSettings { public fun clearFilters ()V public abstract fun getExcludedClasses ()Lorg/gradle/api/provider/SetProperty; @@ -56,6 +81,7 @@ public abstract interface class kotlinx/kover/gradle/aggregation/settings/dsl/Ve } public abstract interface class kotlinx/kover/gradle/aggregation/settings/dsl/VerifySettings { + public abstract fun eachProjectRule (Lorg/gradle/api/Action;)V public abstract fun getRules ()Lorg/gradle/api/provider/ListProperty; public abstract fun getWarningInsteadOfFailure ()Lorg/gradle/api/provider/Property; public abstract fun rule (Ljava/lang/String;Lorg/gradle/api/Action;)V diff --git a/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/SettingsPluginTests.kt b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/SettingsPluginTests.kt index 87e8b976..7b38ba13 100644 --- a/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/SettingsPluginTests.kt +++ b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/SettingsPluginTests.kt @@ -6,6 +6,7 @@ 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.assertContains import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -132,8 +133,35 @@ Rule violated: lines covered percentage is 50.000000, but expected maximum is 10 fun CheckerContext.testVerifyMin() { taskOutput("koverVerify") { assertTrue(contains("Rule 'CLI parameters' violated:\n" + - " lines covered percentage is 7.407400, but expected minimum is 100\n" + - " lines covered percentage is 7.407400, but expected maximum is 5")) + " lines covered percentage is 7.407400, but expected maximum is 5\n" + + " lines covered percentage is 7.407400, but expected minimum is 100")) + } + } + + @TemplateTest("settings-plugin-verify-each", ["check", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"]) + fun CheckerContext.testVerifyEach() { + taskOutput("koverProjectVerify") { + assertContains(this, "Kover Verification Error\n" + + "Rule 'Coverage for project 'subproject'' violated: lines covered percentage is 50.000000, but expected minimum is 100\n" + + "\n" + + "Rule 'Coverage for project 'subproject2'' violated: lines covered percentage is 66.666700, but expected minimum is 100") + } + } + + @TemplateTest("settings-plugin-instrumentation", ["check", "koverXmlReport", "-Dorg.gradle.unsafe.isolated-projects=true", "--configuration-cache", "--build-cache"]) + fun CheckerContext.testInstrumentationConfiguring() { + xmlReport { + // all classes matches to pattern '*Class' should be excluded from instrumentation + classCounter("tests.settings.root.RootClass").assertFullyMissed() + classCounter("tests.settings.subproject.SubprojectClass").assertFullyMissed() + + // all classes from subproject2 should be uncovered as 'test' task isn't instrumented + classCounter("tests.settings.subproject2.Subproject2Class").assertFullyMissed() + classCounter("tests.settings.subproject2.Tested").assertFullyMissed() + + // other classes should be covered + classCounter("tests.settings.root.Tested").assertFullyCovered() + classCounter("tests.settings.subproject.Tested").assertFullyCovered() } } } \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/build.gradle.kts b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/build.gradle.kts new file mode 100644 index 00000000..2a0971b5 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/build.gradle.kts @@ -0,0 +1,12 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + kotlin("jvm") version ("2.0.0") +} + +dependencies { + testImplementation(kotlin("test")) +} + diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/settings.gradle.kts b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/settings.gradle.kts new file mode 100644 index 00000000..a7607d15 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/settings.gradle.kts @@ -0,0 +1,27 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +plugins { + id("org.jetbrains.kotlinx.kover.aggregation") version "SNAPSHOT" +} + +extensions.configure { + enableCoverage() + + instrumentation.excludedClasses.add("*Class") +} + +buildCache { + local { + directory = "$settingsDir/build-cache" + } +} + +rootProject.name = "settings-plugin-verify" + +include(":subproject") +include(":subproject2") diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/src/main/kotlin/RootClass.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/src/main/kotlin/RootClass.kt new file mode 100644 index 00000000..d4f6dbeb --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/src/main/kotlin/RootClass.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 tests.settings.root + +import java.lang.AutoCloseable + +class RootClass { + fun action() { + println("It's root class") + } +} + +class Tested { + fun action() { + println("It's tested root class") + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/src/test/kotlin/RootTest.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/src/test/kotlin/RootTest.kt new file mode 100644 index 00000000..b1c95400 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/src/test/kotlin/RootTest.kt @@ -0,0 +1,15 @@ +/* + * 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() + Tested().action() + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/build.gradle.kts b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/build.gradle.kts new file mode 100644 index 00000000..22b86f76 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/build.gradle.kts @@ -0,0 +1,11 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + kotlin("jvm") +} + +dependencies { + testImplementation(kotlin("test")) +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/src/main/kotlin/SubprojectClass.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/src/main/kotlin/SubprojectClass.kt new file mode 100644 index 00000000..828341eb --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/src/main/kotlin/SubprojectClass.kt @@ -0,0 +1,17 @@ +/* + * 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") + } +} + +class Tested { + fun action() { + println("It's tested subproject class") + } +} diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/src/test/kotlin/ATest.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/src/test/kotlin/ATest.kt new file mode 100644 index 00000000..b5d05c8b --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject/src/test/kotlin/ATest.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package tests.settings.subproject + +import kotlin.test.Test + +class SubprojectTest { + @Test + fun test() { + SubprojectClass().action() + Tested().action() + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/build.gradle.kts b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/build.gradle.kts new file mode 100644 index 00000000..991b6037 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.kover.gradle.aggregation.settings.dsl.* + +plugins { + kotlin("jvm") +} + +dependencies { + testImplementation(kotlin("test")) +} + +extensions.configure { + instrumentation.disabledForTestTasks.add("test") +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/src/main/kotlin/Subproject2Class.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/src/main/kotlin/Subproject2Class.kt new file mode 100644 index 00000000..d1a484b9 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/src/main/kotlin/Subproject2Class.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package tests.settings.subproject2 + +class Subproject2Class { + fun action() { + println("It's class from the subproject2") + } +} + +class Tested { + fun action() { + println("It's tested subproject2 class") + } +} diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/src/test/kotlin/ATest.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/src/test/kotlin/ATest.kt new file mode 100644 index 00000000..693ef692 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-instrumentation/subproject2/src/test/kotlin/ATest.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package tests.settings.subproject2 + +import kotlin.test.Test + +class Subproject2Test { + @Test + fun test() { + Subproject2Class().action() + Tested().action() + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/build.gradle.kts b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/build.gradle.kts new file mode 100644 index 00000000..30ef4776 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/build.gradle.kts @@ -0,0 +1,15 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + kotlin("jvm") version ("2.0.0") +} + +dependencies { + implementation(project(":subproject")) + implementation(project(":ignored")) + + testImplementation(kotlin("test")) +} + diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/build.gradle.kts b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/build.gradle.kts new file mode 100644 index 00000000..22b86f76 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/build.gradle.kts @@ -0,0 +1,11 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + kotlin("jvm") +} + +dependencies { + testImplementation(kotlin("test")) +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/src/main/kotlin/SubprojectClass.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/src/main/kotlin/SubprojectClass.kt new file mode 100644 index 00000000..bda8a922 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/src/main/kotlin/SubprojectClass.kt @@ -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.ignored + +class SubprojectClass { + fun action() { + println("It's class from the subproject") + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/src/test/kotlin/ATest.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/src/test/kotlin/ATest.kt new file mode 100644 index 00000000..d422b5b1 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/ignored/src/test/kotlin/ATest.kt @@ -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.ignored + +import kotlin.test.Test + +class SubprojectTest { + @Test + fun test() { + SubprojectClass().action() + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/settings.gradle.kts b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/settings.gradle.kts new file mode 100644 index 00000000..dd91a35f --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/settings.gradle.kts @@ -0,0 +1,46 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +import kotlinx.kover.gradle.aggregation.settings.dsl.* + +plugins { + id("org.jetbrains.kotlinx.kover.aggregation") version "SNAPSHOT" +} + +extensions.configure { + enableCoverage() + + skipProjects( + // skip root project and everything works ok + // skip by path + ":", + // skip by project name + "ignored" + ) + + reports { + verify { + warningInsteadOfFailure = true + + eachProjectRule { + minBound(100) + } + } + } +} + +buildCache { + local { + directory = "$settingsDir/build-cache" + } +} + +rootProject.name = "settings-plugin-verify" + +include(":subproject") +include(":subproject2") +include(":ignored") diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/src/main/kotlin/RootClass.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/src/main/kotlin/RootClass.kt new file mode 100644 index 00000000..b078013a --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/src/main/kotlin/RootClass.kt @@ -0,0 +1,28 @@ +/* + * 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 java.lang.AutoCloseable + +class RootClass { + fun action() { + println("It's root class") + } +} + +class InheritedClass: AutoCloseable { + override fun close() { + println("close") + } +} + +annotation class Generated + +@Generated +class AnnotatedClass { + fun function() { + println("function") + } +} diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/src/test/kotlin/RootTest.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/src/test/kotlin/RootTest.kt new file mode 100644 index 00000000..8bcda457 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/src/test/kotlin/RootTest.kt @@ -0,0 +1,17 @@ +/* + * 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 +import tests.settings.subproject.UsedInRootClass + +class RootTest { + @Test + fun test() { + RootClass().action() + + UsedInRootClass().action() + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/build.gradle.kts b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/build.gradle.kts new file mode 100644 index 00000000..5d7b768f --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/build.gradle.kts @@ -0,0 +1,12 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + kotlin("jvm") +} + +dependencies { + implementation(project(":subproject2")) + testImplementation(kotlin("test")) +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/src/main/kotlin/SubprojectClass.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/src/main/kotlin/SubprojectClass.kt new file mode 100644 index 00000000..b615fee5 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/src/main/kotlin/SubprojectClass.kt @@ -0,0 +1,17 @@ +/* + * 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") + } +} + +class UsedInRootClass { + fun action() { + println("It's class from the subproject") + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/src/test/kotlin/ATest.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/src/test/kotlin/ATest.kt new file mode 100644 index 00000000..a341e01e --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject/src/test/kotlin/ATest.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package tests.settings.subproject + +import kotlin.test.Test +import tests.settings.subproject2.ToRegularSubprojectClass + +class SubprojectTest { + @Test + fun test() { + SubprojectClass().action() + ToRegularSubprojectClass().action() + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/build.gradle.kts b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/build.gradle.kts new file mode 100644 index 00000000..22b86f76 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/build.gradle.kts @@ -0,0 +1,11 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + kotlin("jvm") +} + +dependencies { + testImplementation(kotlin("test")) +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/src/main/kotlin/SubprojectClass.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/src/main/kotlin/SubprojectClass.kt new file mode 100644 index 00000000..f9ee4618 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/src/main/kotlin/SubprojectClass.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package tests.settings.subproject2 + +class Subproject2Class { + fun action() { + println("It's class from the subproject2") + } +} + +class ToRegularSubprojectClass { + fun action() { + println("It's class for the subproject") + } +} + +class IgnoredClass { + fun action() { + println("It's uncovered class") + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/src/test/kotlin/ATest.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/src/test/kotlin/ATest.kt new file mode 100644 index 00000000..b4b87e6b --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/settings-plugin-verify-each/subproject2/src/test/kotlin/ATest.kt @@ -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.subproject2 + +import kotlin.test.Test + +class Subproject2Test { + @Test + fun test() { + Subproject2Class().action() + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/artifacts/ArtifactSerializer.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/artifacts/ArtifactSerializer.kt index 279ed57d..7285cfcc 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/artifacts/ArtifactSerializer.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/commons/artifacts/ArtifactSerializer.kt @@ -73,8 +73,8 @@ internal object ArtifactSerializer { } } - val map = all.associate { it.name!! to CompilationInfo(it.sourceDirs.toList(), it.outputDirs.toSet()) } - return ProjectArtifactInfo(projectPath!!, reports, map) + val map = all.associate { it.name!! to CompilationInfo(it.sourceDirs.filter { f -> f.exists() }, it.outputDirs.filter { f -> f.exists() }) } + return ProjectArtifactInfo(projectPath!!, reports.filter { f -> f.exists() }, map) } } 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 index 0d854906..a52644b2 100644 --- 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 @@ -4,12 +4,7 @@ package kotlinx.kover.gradle.aggregation.project -import kotlinx.kover.features.jvm.KoverFeatures import kotlinx.kover.gradle.aggregation.commons.artifacts.* -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 @@ -17,10 +12,9 @@ import kotlinx.kover.gradle.aggregation.commons.names.PluginId.KOTLIN_MULTIPLATF 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.aggregation.settings.dsl.intern.KoverProjectExtensionImpl import kotlinx.kover.gradle.plugin.commons.KOTLIN_ANDROID_PLUGIN_ID import kotlinx.kover.gradle.plugin.commons.KoverCriticalException import org.gradle.api.Plugin @@ -32,22 +26,25 @@ 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.named -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() + val projectExtension = target.configureExtension() + target.configureInstrumentation(projectExtension) target.configureArtifactGeneration() } - private fun Project.configureInstrumentation() { + private fun Project.configureExtension(): KoverProjectExtensionImpl { + val projectExtension = extensions.create("kover") + projectExtension.instrumentation.excludedClasses.convention(emptySet()) + projectExtension.instrumentation.includedClasses.convention(emptySet()) + return projectExtension + } + + private fun Project.configureInstrumentation(projectExtension: KoverProjectExtensionImpl) { val koverJarDependency = configurations.getByName(SettingsNames.DEPENDENCY_AGENT) val jarConfig = configurations.create("agentJarSource") { asConsumer() @@ -57,7 +54,7 @@ internal class KoverProjectGradlePlugin : Plugin { } extendsFrom(koverJarDependency) } - JvmOnFlyInstrumenter.instrument(tasks.withType(), jarConfig, InstrumentationFilter(setOf(), setOf())) + JvmOnFlyInstrumenter.instrument(tasks.withType(), jarConfig, projectExtension.instrumentation) } private fun Project.configureArtifactGeneration() { @@ -153,34 +150,6 @@ internal class KoverProjectGradlePlugin : Plugin { } } - - 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(Usage.USAGE_ATTRIBUTE, objects.named(KoverUsageAttr.VALUE)) - 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 index 4abe3767..5953199d 100644 --- 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 @@ -5,6 +5,7 @@ package kotlinx.kover.gradle.aggregation.project.instrumentation import kotlinx.kover.gradle.aggregation.commons.names.KoverPaths +import kotlinx.kover.gradle.aggregation.settings.dsl.ProjectInstrumentationSettings import org.gradle.api.Named import org.gradle.api.artifacts.Configuration import org.gradle.api.file.ConfigurableFileCollection @@ -24,9 +25,14 @@ internal object JvmOnFlyInstrumenter { fun instrument( tasks: TaskCollection, jarConfiguration: Configuration, - filter: InstrumentationFilter + instrumentation: ProjectInstrumentationSettings ) { tasks.configureEach { + val taskName = name + val enabledProvider = instrumentation.disabledForTestTasks.map { taskName !in it } + val included = instrumentation.includedClasses + val excluded = instrumentation.excludedClasses + val binReportProvider = project.layout.buildDirectory.map { dir -> dir.file(KoverPaths.binReportPath(name)) @@ -46,26 +52,21 @@ internal object JvmOnFlyInstrumenter { "jdk.internal.*" ) - val excludedClassesWithAndroid = filter.copy(excludes = filter.excludes + androidClasses) + val excludedWithAndroid = excluded.map { it + androidClasses } dependsOn(jarConfiguration) jvmArgumentProviders += JvmTestTaskArgumentProvider( temporaryDir, project.objects.fileCollection().from(jarConfiguration), - excludedClassesWithAndroid, + enabledProvider, + included, + excludedWithAndroid, 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. */ @@ -77,8 +78,12 @@ private class JvmTestTaskArgumentProvider( @get:PathSensitive(PathSensitivity.RELATIVE) val jarFiles: ConfigurableFileCollection, - @get:Nested - val filter: InstrumentationFilter, + @get:Input + val enabled: Provider, + @get:Input + val includedClasses: Provider>, + @get:Input + val excludedClasses: Provider>, @get:OutputFile val reportProvider: Provider @@ -91,13 +96,18 @@ private class JvmTestTaskArgumentProvider( override fun asArguments(): MutableIterable { val files = jarFiles.files - if (files.size != 1) { + if (!enabled.get() || files.size != 1) { return mutableSetOf() } val jarFile = files.single() - return buildKoverJvmAgentArgs(jarFile, tempDir, reportProvider.get().asFile, filter.excludes) - .toMutableList() + return buildKoverJvmAgentArgs( + jarFile, + tempDir, + reportProvider.get().asFile, + includedClasses.orNull ?: emptySet(), + excludedClasses.orNull ?: emptySet() + ).toMutableList() } } @@ -106,15 +116,16 @@ private fun buildKoverJvmAgentArgs( jarFile: File, tempDir: File, binReportFile: File, + includedClasses: Set, excludedClasses: Set ): List { val argsFile = tempDir.resolve("kover-agent.args") - argsFile.writeAgentArgs(binReportFile, excludedClasses) + argsFile.writeAgentArgs(binReportFile, includedClasses, excludedClasses) return mutableListOf("-javaagent:${jarFile.canonicalPath}=file:${argsFile.canonicalPath}") } -private fun File.writeAgentArgs(binReportFile: File, excludedClasses: Set) { +private fun File.writeAgentArgs(binReportFile: File, includedClasses: Set, excludedClasses: Set) { binReportFile.parentFile.mkdirs() val binReportPath = binReportFile.canonicalPath @@ -123,5 +134,8 @@ private fun File.writeAgentArgs(binReportFile: File, excludedClasses: Set pw.append("exclude=").appendLine(e) } + includedClasses.forEach { i -> + pw.append("include=").appendLine(i) + } } } \ 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 index 3629a036..0dc34b52 100644 --- 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 @@ -4,6 +4,8 @@ package kotlinx.kover.gradle.aggregation.settings +import kotlinx.kover.features.jvm.KoverFeatures +import kotlinx.kover.gradle.aggregation.commons.artifacts.* import kotlinx.kover.gradle.aggregation.commons.artifacts.KoverContentAttr import kotlinx.kover.gradle.aggregation.commons.artifacts.KoverUsageAttr import kotlinx.kover.gradle.aggregation.commons.artifacts.asConsumer @@ -13,20 +15,22 @@ 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.project.tasks.KoverAgentSearchTask +import kotlinx.kover.gradle.aggregation.settings.dsl.intern.KoverProjectExtensionImpl +import kotlinx.kover.gradle.aggregation.settings.dsl.intern.ProjectVerificationRuleSettingsImpl +import kotlinx.kover.gradle.aggregation.settings.dsl.intern.inheritFrom import kotlinx.kover.gradle.aggregation.settings.tasks.* import kotlinx.kover.gradle.aggregation.settings.tasks.KoverHtmlReportTask import kotlinx.kover.gradle.aggregation.settings.tasks.KoverVerifyTask import kotlinx.kover.gradle.aggregation.settings.tasks.KoverXmlReportTask +import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.attributes.Usage 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.named -import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.* import org.gradle.kotlin.dsl.support.serviceOf import org.gradle.language.base.plugins.LifecycleBasePlugin @@ -56,9 +60,16 @@ public class KoverSettingsGradlePlugin: Plugin { if (path == Project.PATH_SEPARATOR) { configureRootProject(target, settingsExtension) + configureAgentSearch() } - apply() + if (settingsExtension.skipProjects.get().none { excluded -> excluded.match(name, path) }) { + apply() + + val projectExtension = extensions.getByType() + projectExtension.instrumentation.includedClasses.convention(settingsExtension.instrumentation.includedClasses) + projectExtension.instrumentation.excludedClasses.convention(settingsExtension.instrumentation.excludedClasses) + } } } @@ -71,9 +82,33 @@ public class KoverSettingsGradlePlugin: Plugin { attribute(Usage.USAGE_ATTRIBUTE, objects.named(KoverUsageAttr.VALUE)) } } + + val eachProjectRules = mutableMapOf>() + val rootDependencies = dependencies settings.rootProject.walkSubprojects { descriptor -> - rootDependencies.add(KOVER_DEPENDENCY_NAME, project(descriptor.path)) + if (settingsExtension.skipProjects.get().none { excluded -> excluded.match(descriptor.name, descriptor.path) }) { + rootDependencies.add(KOVER_DEPENDENCY_NAME, project(descriptor.path)) + + val rules = settingsExtension.reports.verify.eachProjectRule.get().map { action -> + val eachProjectRule = objects.newInstance( + descriptor.name, + descriptor.path + ) + eachProjectRule.name = "Coverage for project '${descriptor.name}'" + eachProjectRule.filters.inheritFrom(settingsExtension.reports) + eachProjectRule.disabled.convention(false) + eachProjectRule.groupBy.convention(GroupingEntityType.APPLICATION) + action.execute(eachProjectRule) + eachProjectRule + }.filter { rule -> + rule.bounds.get().isNotEmpty() + } + + if (rules.isNotEmpty()) { + eachProjectRules[descriptor.path] = rules + } + } } val artifacts = configurations.create("koverArtifactsCollector") { @@ -123,11 +158,64 @@ public class KoverSettingsGradlePlugin: Plugin { settingsExtension.reports.verify.rules.map { it.map { rule -> rule.asInput() } } ) } + + val projectVerifyTask = tasks.register("koverProjectVerify") + projectVerifyTask.configure { + dependsOn(artifacts) + this.artifacts.from(artifacts) + group = "verification" + warningInsteadOfFailure.convention(settingsExtension.reports.verify.warningInsteadOfFailure) + rulesByProjectPath.convention( + eachProjectRules.mapValues { entry -> entry.value.map { rule -> rule.asInput() } } + ) + } + // dependency on check tasks.configureEach { if (name == LifecycleBasePlugin.CHECK_TASK_NAME) { dependsOn(verifyTask) + dependsOn(projectVerifyTask) + } + } + } + + 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(Usage.USAGE_ATTRIBUTE, objects.named(KoverUsageAttr.VALUE)) + 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) + } + } + } + + private fun String.match(projectName: String, projectPath: String): Boolean { + return if (this.contains(':')) { + val correctedPath = if (this.startsWith(':')) { + this + } else { + ":$this" } + correctedPath == projectPath + } else { + this == projectName } } 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 index 2fa07637..e07b5164 100644 --- 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 @@ -18,10 +18,128 @@ import org.gradle.api.provider.SetProperty public interface KoverSettingsExtension { fun enableCoverage() + /** + * Set of projects, classes and tests from which will not be included in the reports. + * + * See details in [skipProjects]. + */ + val skipProjects: SetProperty + + /** + * Specify projects, classes and tests from which will not be included in the reports. + * + * This means that all classes declared in these projects will be excluded from the report, + * as well as all test tasks will not be instrumented - accordingly, coverage of them will not be taken into account. + * + * Several project name syntaxes are supported: + * - project name + * - full path (starts with the symbol `:`) + * - abbreviated path (does not start with `:`, but contains the path separator `:`) + * + * ``` + * skipProjects(":project1", "project2", "nested:subproject") + * ``` + */ + fun skipProjects(vararg projects: String) { + skipProjects.addAll(projects.toList()) + } + + /** + * Instance for configuring instrumentation in all non-skipped Gradle projects. + * + * See details in [instrumentation]. + */ + val instrumentation: InstrumentationSettings + + /** + * Instrumentation settings for the all non-skipped Gradle projects. + * + * Instrumentation is the modification of classes when they are loaded into the JVM, which helps to determine which code was called and which was not. + * Instrumentation changes the bytecode of the class, so it may disable some JVM optimizations, slow down performance and concurrency tests, and may also be incompatible with other instrumentation libraries. + * + * For this reason, it may be necessary to fine-tune the instrumentation, for example, disabling instrumentation for problematic classes. Note that such classes would be marked as uncovered because of that. + * + * Example: + * ``` + * instrumentation { + * // disable instrumentation of specified classes in test tasks + * excludedClasses.addAll("foo.bar.*Biz", "*\$Generated") + * + * // enable instrumentation only for specified classes. Classes in excludedClasses have priority over classes from includedClasses. + * includedClasses.addAll("foo.bar.*") + * } + * ``` + */ + fun instrumentation(action: Action) + + /** + * Instance for configuring merged reports. + * + * See details in [reports]. + */ val reports: ReportsSettings + + /** + * Configure Kover merged reports + * ``` + * reports { + * verify.warningInsteadOfFailure = false + * verify.rule("First rule") { minBound(50) } + * + * verify { + * warningInsteadOfFailure = false + * rule("Second rule") { + * minBound(75) + * } + * } + * } + * ``` + */ fun reports(action: Action) } +@KoverGradlePluginDsl +public interface InstrumentationSettings { + /** + * Disable instrumentation in test tasks of specified classes in all Gradle projects. + * + * Classes in [excludedClasses] have priority over classes from [includedClasses]. + */ + public val excludedClasses: SetProperty + + /** + * Enable instrumentation in test tasks only of specified classes in all Gradle projects. + * All other classes will not be instrumented. + * + * Classes in [excludedClasses] have priority over classes from [includedClasses]. + */ + public val includedClasses: SetProperty +} + +@KoverGradlePluginDsl +public interface ProjectInstrumentationSettings { + /** + * Specifies not to use test task with passed names to measure coverage. + * These tasks will also not be called when generating Kover reports and these tasks will not be instrumented even if you explicitly run them. + */ + public val disabledForTestTasks: SetProperty + + /** + * Disable instrumentation in test tasks of specified classes + * + * Classes in [excludedClasses] have priority over classes from [includedClasses]. + */ + public val excludedClasses: SetProperty + + /** + * Enable instrumentation in test tasks only of specified classes. + * All other classes will not be instrumented. + * + * Classes in [excludedClasses] have priority over classes from [includedClasses]. + */ + public val includedClasses: SetProperty +} + @KoverGradlePluginDsl public interface ReportsSettings: ReportFiltersSettings { val verify: VerifySettings @@ -71,6 +189,30 @@ public interface VerifySettings { */ public fun rule(name: String, action: Action) + /** + * Add new coverage verification rule to check in each non-skipped project. + * + * When checking the rule, only the classes declared in the corresponding project will be analyzed. + * + * The `projectName` and `projectPath` properties can be used to identify the project. + * + * The specified action will be called for each non-skipped Gradle project, + * that is, it must be taken into account that it is performed several times. + * + * ``` + * eachProjectRule { + * if (projectPath != ":badly-covered-project") { + * // all other projects should be covered with 80% + * minBound(80) + * } else { + * // :badly-covered-project should be covered with 50% + * minBound(50) + * } + * } + * ``` + */ + public fun eachProjectRule(action: Action) + /** * In case of a verification error, print a message to the log with the warn level instead of the Gradle task execution error. * @@ -81,6 +223,20 @@ public interface VerifySettings { public val warningInsteadOfFailure: Property } + +@KoverGradlePluginDsl +public interface ProjectVerificationRuleSettings: VerificationRuleSettings { + /** + * Get the name of the project for which classes coverage is being checked. + */ + val projectName: String + + /** + * Get the path of the project for which classes coverage is being checked + */ + val projectPath: String +} + @KoverGradlePluginDsl public interface VerificationRuleSettings { /** @@ -313,3 +469,27 @@ public fun VerificationRuleSettings.maxBound( this.aggregationForGroup.set(aggregationForGroup) } } + + +@KoverGradlePluginDsl +public interface KoverProjectExtension { + /** + * Instance for configuring instrumentation. + * + * See details in [instrumentation]. + */ + val instrumentation: ProjectInstrumentationSettings + + /** + * Configure instrumentation for current project. + * + * ``` + * instrumentation { + * disabledForTestTasks.add("test") + * excludedClasses.add("*.excluded.*") + * includedClasses.add("my.project.*") + * } + * ``` + */ + fun instrumentation(action: Action) +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/KoverProjectExtensionImpl.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/KoverProjectExtensionImpl.kt new file mode 100644 index 00000000..355c66d0 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/KoverProjectExtensionImpl.kt @@ -0,0 +1,23 @@ +/* + * 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.KoverProjectExtension +import kotlinx.kover.gradle.aggregation.settings.dsl.ProjectInstrumentationSettings +import org.gradle.api.Action +import org.gradle.api.model.ObjectFactory +import org.gradle.kotlin.dsl.newInstance +import javax.inject.Inject + +internal abstract class KoverProjectExtensionImpl: KoverProjectExtension { + @get:Inject + abstract val objects: ObjectFactory + + override val instrumentation: ProjectInstrumentationSettings = objects.newInstance() + + override fun instrumentation(action: Action) { + action.execute(instrumentation) + } +} \ No newline at end of file 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 index 8266ae34..558d6286 100644 --- 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 @@ -4,6 +4,7 @@ package kotlinx.kover.gradle.aggregation.settings.dsl.intern +import kotlinx.kover.gradle.aggregation.settings.dsl.InstrumentationSettings import kotlinx.kover.gradle.aggregation.settings.dsl.KoverSettingsExtension import kotlinx.kover.gradle.aggregation.settings.dsl.ReportsSettings import org.gradle.api.Action @@ -18,7 +19,8 @@ internal abstract class KoverSettingsExtensionImpl @Inject constructor( ) : KoverSettingsExtension { abstract val coverageIsEnabled: Property - override val reports: ReportsSettings = objects.newInstance() + override val reports: ReportsSettingsImpl = objects.newInstance() + override val instrumentation: InstrumentationSettings = objects.newInstance() init { coverageIsEnabled.convention(false) @@ -28,6 +30,10 @@ internal abstract class KoverSettingsExtensionImpl @Inject constructor( coverageIsEnabled.set(true) } + override fun instrumentation(action: Action) { + action.execute(instrumentation) + } + override fun reports(action: Action) { action.execute(reports) } diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/ReportsSettingsImpl.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/ReportsSettingsImpl.kt index 2deba662..e45cefd0 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/ReportsSettingsImpl.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/ReportsSettingsImpl.kt @@ -5,9 +5,7 @@ package kotlinx.kover.gradle.aggregation.settings.dsl.intern import kotlinx.kover.gradle.aggregation.settings.dsl.ReportsSettings -import kotlinx.kover.gradle.aggregation.settings.dsl.VerifySettings import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.SetProperty import org.gradle.kotlin.dsl.newInstance import javax.inject.Inject @@ -16,5 +14,5 @@ internal abstract class ReportsSettingsImpl: ReportsSettings { abstract val objects: ObjectFactory @Suppress("LeakingThis") - override val verify: VerifySettings = objects.newInstance(this) + override val verify: VerifySettingsImpl = objects.newInstance(this) } \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/VerificationRuleSettingsImpl.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/VerificationRuleSettingsImpl.kt index 8d114261..e075d17a 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/VerificationRuleSettingsImpl.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/VerificationRuleSettingsImpl.kt @@ -5,6 +5,7 @@ package kotlinx.kover.gradle.aggregation.settings.dsl.intern import kotlinx.kover.gradle.aggregation.settings.dsl.BoundSettings +import kotlinx.kover.gradle.aggregation.settings.dsl.ProjectVerificationRuleSettings import kotlinx.kover.gradle.aggregation.settings.dsl.ReportFiltersSettings import kotlinx.kover.gradle.aggregation.settings.dsl.VerificationRuleSettings import kotlinx.kover.gradle.plugin.dsl.AggregationType @@ -30,4 +31,12 @@ internal abstract class VerificationRuleSettingsImpl: VerificationRuleSettings { action.execute(bound) bounds.add(bound) } +} + +internal abstract class ProjectVerificationRuleSettingsImpl @Inject constructor( + override val projectName: String, + override val projectPath: String +): VerificationRuleSettingsImpl(), ProjectVerificationRuleSettings { + + } \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/VerifySettingsImpl.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/VerifySettingsImpl.kt index 28fd6acd..a4ac2b57 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/VerifySettingsImpl.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/dsl/intern/VerifySettingsImpl.kt @@ -4,18 +4,18 @@ package kotlinx.kover.gradle.aggregation.settings.dsl.intern -import kotlinx.kover.gradle.aggregation.settings.dsl.ReportFiltersSettings -import kotlinx.kover.gradle.aggregation.settings.dsl.VerificationRuleSettings -import kotlinx.kover.gradle.aggregation.settings.dsl.VerifySettings +import kotlinx.kover.gradle.aggregation.settings.dsl.* import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType import org.gradle.api.Action import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty import org.gradle.kotlin.dsl.newInstance import javax.inject.Inject internal abstract class VerifySettingsImpl @Inject constructor( private val commonFilters: ReportFiltersSettings, ) : VerifySettings { + abstract val eachProjectRule: ListProperty> @get:Inject abstract val objects: ObjectFactory @@ -38,6 +38,10 @@ internal abstract class VerifySettingsImpl @Inject constructor( rules.add(rule) } + override fun eachProjectRule(action: Action) { + eachProjectRule.add(action) + } + private fun newRule(): VerificationRuleSettings { val rule: VerificationRuleSettings = objects.newInstance() rule.filters.inheritFrom(commonFilters) @@ -45,15 +49,15 @@ internal abstract class VerifySettingsImpl @Inject constructor( rule.groupBy.convention(GroupingEntityType.APPLICATION) return rule } - - private fun ReportFiltersSettings.inheritFrom(other: ReportFiltersSettings) { - includedProjects.convention(other.includedProjects) - excludedProjects.convention(other.excludedProjects) - includedClasses.convention(other.includedClasses) - excludedClasses.convention(other.excludedClasses) - includesAnnotatedBy.convention(other.includesAnnotatedBy) - excludesAnnotatedBy.convention(other.excludesAnnotatedBy) - includesInheritedFrom.convention(other.includesInheritedFrom) - excludesInheritedFrom.convention(other.excludesInheritedFrom) - } -} \ No newline at end of file +} + +internal fun ReportFiltersSettings.inheritFrom(other: ReportFiltersSettings) { + includedProjects.convention(other.includedProjects) + excludedProjects.convention(other.excludedProjects) + includedClasses.convention(other.includedClasses) + excludedClasses.convention(other.excludedClasses) + includesAnnotatedBy.convention(other.includesAnnotatedBy) + excludesAnnotatedBy.convention(other.excludesAnnotatedBy) + includesInheritedFrom.convention(other.includesInheritedFrom) + excludesInheritedFrom.convention(other.excludesInheritedFrom) +} diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/CommonTypes.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/CommonTypes.kt index 619cfaa4..0a1e1af4 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/CommonTypes.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/CommonTypes.kt @@ -14,6 +14,8 @@ import kotlinx.kover.gradle.aggregation.settings.dsl.VerificationRuleSettings import kotlinx.kover.gradle.plugin.dsl.AggregationType import kotlinx.kover.gradle.plugin.dsl.CoverageUnit import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional @@ -21,21 +23,21 @@ import java.io.Serializable internal data class FiltersInput( @get:Input - val includedProjects: Set, + val includedProjects: Set = emptySet(), @get:Input - val excludedProjects: Set, + val excludedProjects: Set = emptySet(), @get:Input - val includedClasses: Set, + val includedClasses: Set = emptySet(), @get:Input - val excludedClasses: Set, + val excludedClasses: Set = emptySet(), @get:Input - val includesAnnotatedBy: Set, + val includesAnnotatedBy: Set = emptySet(), @get:Input - val excludesAnnotatedBy: Set, + val excludesAnnotatedBy: Set = emptySet(), @get:Input - val includesInheritedFrom: Set, + val includesInheritedFrom: Set = emptySet(), @get:Input - val excludesInheritedFrom: Set + val excludesInheritedFrom: Set = emptySet() ): Serializable diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/KoverReportVerifyTask.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/KoverReportVerifyTask.kt new file mode 100644 index 00000000..912dd172 --- /dev/null +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/aggregation/settings/tasks/KoverReportVerifyTask.kt @@ -0,0 +1,78 @@ +/* + * 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.KoverLegacyFeatures +import kotlinx.kover.gradle.plugin.commons.KoverVerificationException +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.TaskAction + +@CacheableTask +internal abstract class KoverReportVerifyTask : AbstractKoverTask() { + @get:Input + abstract val warningInsteadOfFailure: Property + + @get:Nested + abstract val rulesByProjectPath: MapProperty> + + init { + // disable caching if we emit only warnings + outputs.upToDateWhen { !warningInsteadOfFailure.get() } + + @Suppress("LeakingThis") + onlyIf { rulesByProjectPath.get().isNotEmpty() } + } + + @TaskAction + fun generate() { + val commonArtifacts = artifacts.elements.get().data() + val binaryReports = commonArtifacts.values.flatMap { artifact -> artifact.reports } + + val violations = rulesByProjectPath.get().mapValues { (path, rules) -> + rules + .filterNot { rule -> rule.disabled } + .flatMap { rule -> + val outputs = commonArtifacts.values + .map { artifact -> + artifact.filterProjectSources(FiltersInput(includedProjects = setOf(path))) + } + .flatMap { artifact -> + artifact.compilations.flatMap { compilation -> compilation.value.outputDirs } + } + + KoverLegacyFeatures.verify( + listOf(rule.toExternal()), + temporaryDir, + rule.filters.toExternalFilters(), + binaryReports, + outputs + ) + } + }.filter { + ruleViolations -> ruleViolations.value.isNotEmpty() + } + + if (violations.isEmpty()) { + // no errors + return + } + + val stringBuilder = StringBuilder() + violations.forEach { (_, projectViolations) -> + stringBuilder.appendLine(KoverLegacyFeatures.violationMessage(projectViolations)) + } + + val errorMessage = stringBuilder.toString() + if (warningInsteadOfFailure.get()) { + logger.warn("Kover Verification Error\n$errorMessage") + } else { + throw KoverVerificationException(errorMessage) + } + } +} \ No newline at end of file