diff --git a/README.md b/README.md index c53c9c04..e33c20c2 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,16 @@ Minimal supported `Gradle` version: `6.6`. ## Table of contents - [Features](#features) - [Quickstart](#quickstart) - - [Apply plugin to a single-project build](#apply-plugin-to-a-single-project-build) + - [Apply plugin to a project](#apply-plugin) - [Applying plugins with the plugins DSL](#applying-plugins-with-the-plugins-dsl) - [Legacy Plugin Application: applying plugins with the buildscript block](#legacy-plugin-application-applying-plugins-with-the-buildscript-block) - - [Apply plugin to a multi-project build](#apply-plugin-to-a-multi-project-build) + - [Merged reports](#merged-reports) - [Configuration](#configuration) - - [Configuring JVM test task](#configuring-jvm-test-task) + - [Configuring project](#configuring-project) - [Configuring merged reports](#configuring-merged-reports) - - [Configuring project reports](#configuring-project-reports) - - [Configuring entire plugin](#configuring-entire-plugin) -- [Verification](#verification) -- [Tasks](#tasks) + - [Configuring JVM test task](#configuring-jvm-test-task) + - [Specifying Coverage Engine](#specifying-coverage-engine) +- [Tasks](#kover-default-tasks) - [Implicit plugin dependencies](#implicit-plugin-dependencies) ## Features @@ -34,7 +33,7 @@ Minimal supported `Gradle` version: `6.6`. * Customizable filters for instrumented classes ## Quickstart -### Apply plugin to a single-project build +### Apply plugin #### Applying plugins with the plugins DSL In top-level build file: @@ -43,7 +42,7 @@ In top-level build file: ```kotlin plugins { - id("org.jetbrains.kotlinx.kover") version "0.5.0" + id("org.jetbrains.kotlinx.kover") version "0.6.0-RC" } ``` @@ -53,7 +52,7 @@ plugins { ```groovy plugins { - id 'org.jetbrains.kotlinx.kover' version '0.5.0' + id 'org.jetbrains.kotlinx.kover' version '0.6.0-RC' } ``` @@ -71,7 +70,7 @@ buildscript { } dependencies { - classpath("org.jetbrains.kotlinx:kover:0.5.0") + classpath("org.jetbrains.kotlinx:kover:0.6.0-RC") } } @@ -88,7 +87,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.jetbrains.kotlinx:kover:0.5.0' + classpath 'org.jetbrains.kotlinx:kover:0.6.0-RC' } } @@ -96,9 +95,14 @@ apply plugin: 'kover' ``` -### Apply plugin to a multi-project build -To apply the plugin to all Gradle projects, you only need to apply the plugin to the top-level build file as shown [above](#apply-plugin-to-a-single-project-build). -Applying the plugin to subprojects if you have already applied it to the root project will cause configuration errors. +### Merged reports +Merged reports are reports that combine statistics of code coverage by test tasks from several projects. + +At the same time, for each project, its configuration of instrumentation and special filters (non-class filters) is applied. + +In all projects used for merged reports, a Kover plugin must be applied, as well as all Coverage Engines must match. + +See how to enable merge reports in [this section](#configuring-merged-reports). ## Configuration @@ -108,7 +112,9 @@ However, in some cases, custom settings are needed - this can be done by configu ### Configuring JVM test task -If you need to disable or filter instrumentation for a test task, you may configure the Kover extension for it. +In rare cases, you may need to disable instrumentation for certain classes if it causes execution errors. +It may also be convenient to ignore the test task when calculating coverage. +You may configure the Kover extension for it. For example, to configure a standard test task for Kotlin/JVM named `test`, you need to add the following code to the build script of the project where this task is declared: @@ -118,10 +124,10 @@ For example, to configure a standard test task for Kotlin/JVM named `test`, you ```kotlin tasks.test { extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) { - isDisabled = false - binaryReportFile.set(file("$buildDir/custom/result.bin")) - includes = listOf("com.example.*") - excludes = listOf("com.example.subpackage.*") + isDisabled.set(false) // true to disable instrumentation tests of this task, Kover reports will not depend on the results of their execution + binaryReportFile.set(file("$buildDir/custom/result.bin")) // set file name of binary report + includes = listOf("com.example.*") // see "Instrumentation inclusion rules" below + excludes = listOf("com.example.subpackage.*") // see "Instrumentation exclusion rules" below } } ``` @@ -133,16 +139,16 @@ tasks.test { ```groovy tasks.test { kover { - disabled = false - binaryReportFile.set(file("$buildDir/custom/result.bin")) - includes = ['com.example.*'] - excludes = ['com.example.subpackage.*'] + disabled = false // true to disable instrumentation tests of this task, Kover reports will not depend on the results of their execution + binaryReportFile.set(file("$buildDir/custom/result.bin")) // set file name of binary report + includes = ['com.example.*'] // see "Instrumentation inclusion rules" below + excludes = ['com.example.subpackage.*'] // see "Instrumentation exclusion rules" below } } ``` -**For other platforms (Android, Kotlin-Multiplatform) the names may differ and you may also have several test tasks, so you first need to determine the name of the required task.** +**For other platforms (Android, Kotlin-Multiplatform) the names may differ, and you may also have several test tasks, so you first need to determine the name of the required task.** Example of configuring test task for build type `debug` in Android:
@@ -156,10 +162,10 @@ android { unitTests.all { if (it.name == "testDebugUnitTest") { it.extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) { - isDisabled = false - binaryReportFile.set(file("$buildDir/custom/debug-report.bin")) - includes = listOf("com.example.*") - excludes = listOf("com.example.subpackage.*") + isDisabled = false // true to disable instrumentation tests of this task, Kover reports will not depend on the results of their execution + binaryReportFile.set(file("$buildDir/custom/debug-report.bin")) // set file name of binary report + includes = listOf("com.example.*") // see "Instrumentation inclusion rules" below + excludes = listOf("com.example.subpackage.*") // see "Instrumentation exclusion rules" below } } } @@ -180,10 +186,10 @@ android { unitTests.all { if (name == "testDebugUnitTest") { kover { - disabled = false - binaryReportFile.set(file("$buildDir/custom/debug-report.bin")) - includes = ['com.example.*'] - excludes = ['com.example.subpackage.*'] + disabled = false // true to disable instrumentation tests of this task, Kover reports will not depend on the results of their execution + binaryReportFile.set(file("$buildDir/custom/debug-report.bin")) // set file name of binary report + includes = ['com.example.*'] // see "Instrumentation inclusion rules" below + excludes = ['com.example.subpackage.*'] // see "Instrumentation exclusion rules" below } } } @@ -193,79 +199,85 @@ android {
-### Configuring merged reports -Merged reports combine classpath and coverage stats from the project in which the plugin is applied and all of its subprojects. - -If you need to change the name of the XML report file or HTML directory, you may configure the corresponding tasks in -the project in which the plugin is applied (usually this is the root project): +**Instrumentation inclusion rules** -
-Kotlin +Only the specified classes will be instrumented, the remaining (non-included) classes will still be present in the report, but their coverage will be zero. -```kotlin -tasks.koverMergedHtmlReport { - isEnabled = true // false to disable report generation - htmlReportDir.set(layout.buildDirectory.dir("my-merged-report/html-result")) +**Instrumentation exclusion rules** - includes = listOf("com.example.*") // inclusion rules for classes - excludes = listOf("com.example.subpackage.*") // exclusion rules for classes -} +The specified classes will not be instrumented and their coverage will be zero. -tasks.koverMergedXmlReport { - isEnabled = true // false to disable report generation - xmlReportFile.set(layout.buildDirectory.file("my-merged-report/result.xml")) +Instrumentation inclusion/exclusion rules are represented as a fully-qualified name of the class (several classes if wildcards are used). +File or directory names are not allowed. +It is possible to use `*` (zero or several of any chars) and `?` (one any char) wildcards. Wildcard `**` is similar to the `*`. - includes = listOf("com.example.*") // inclusion rules for classes - excludes = listOf("com.example.subpackage.*") // exclusion rules for classes -} -``` -
+Examples `my.package.ClassName` or `my.*.*Name` are allowed, while `my/package/ClassName.kt` or `src/my.**.ClassName` are not. -
-Groovy - -```groovy -tasks.koverMergedHtmlReport { - enabled = true // false to disable report generation - htmlReportDir.set(layout.buildDirectory.dir("my-merged-report/html-result")) - - includes = ['com.example.*'] // inclusion rules for classes - excludes = ['com.example.subpackage.*'] // exclusion rules for classes -} - -tasks.koverMergedXmlReport { - enabled = true // false to disable report generation - xmlReportFile.set(layout.buildDirectory.file("my-merged-report/result.xml")) - - includes = ['com.example.*'] // inclusion rules for classes - excludes = ['com.example.subpackage.*'] // exclusion rules for classes -} -``` -
- -### Configuring project reports -If you need to change the name of the XML report file or HTML directory for a specific project, you may configure -the corresponding tasks in this project: +Exclusion rules have priority over inclusion ones. +### Configuring project +In the project in which the plugin is applied, you can configure instrumentation and default Kover tasks:
Kotlin ```kotlin -tasks.koverHtmlReport { - isEnabled = true // false to disable report generation - htmlReportDir.set(layout.buildDirectory.dir("my-project-report/html-result")) +kover { + isDisabled.set(false) // true to disable instrumentation and all Kover tasks in this project + engine.set(DefaultIntellijEngine) // change Coverage Engine + filters { // common filters for all default Kover tasks + classes { // common class filter for all default Kover tasks + includes += "com.example.*" // class inclusion rules + excludes += listOf("com.example.subpackage.*") // class exclusion rules + } + } - includes = listOf("com.example.*") // inclusion rules for classes - excludes = listOf("com.example.subpackage.*") // exclusion rules for classes -} + instrumentation { + excludeTasks += "dummy-tests" // set of test tasks names to exclude from instrumentation. The results of their execution will not be presented in the report + } + + xmlReport { + onCheck.set(false) // true to run koverXmlReport task during the execution of the check task + reportFile.set(layout.buildDirectory.file("my-project-report/result.xml")) // change report file name + overrideFilters { + classes { // override common class filter + includes += "com.example2.*" // override class inclusion rules + excludes += listOf("com.example2.subpackage.*") // override class exclusion rules + } + } + } -tasks.koverXmlReport { - isEnabled = true // false to disable report generation - xmlReportFile.set(layout.buildDirectory.file("my-project-report/result.xml")) + htmlReport { + onCheck.set(false) // true to run koverHtmlReport task during the execution of the check task + reportDir.set(layout.buildDirectory.dir("my-project-report/html-result")) // change report directory + overrideFilters { + classes { // override common class filter + includes += "com.example2.*" // class inclusion rules + excludes += listOf("com.example2.subpackage.*") // override class exclusion rules + } + } + } + + verify { + onCheck.set(true) // true to run koverVerify task during the execution of the check task + rule { // add verification rule + isEnabled = true // false to disable rule checking + name = null // custom name for the rule + target = kotlinx.kover.api.VerificationTarget.ALL // specify by which entity the code for separate coverage evaluation will be grouped + + overrideClassFilter { // override common class filter + includes += "com.example.verify.*" // override class inclusion rules + excludes += listOf("com.example.verify.subpackage.*") // override class exclusion rules + } - includes = listOf("com.example.*") // inclusion rules for classes - excludes = listOf("com.example.subpackage.*") // exclusion rules for classes + bound { // add rule bound + minValue = 10 + maxValue = 20 + counter = kotlinx.kover.api.CounterType.LINE // change coverage metric to evaluate (LINE, INSTRUCTION, BRANCH) + valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE // change counter value (COVERED_COUNT, MISSED_COUNT, COVERED_PERCENTAGE, MISSED_PERCENTAGE) + } + } + } } ```
@@ -274,69 +286,139 @@ tasks.koverXmlReport { Groovy ```groovy -tasks.koverHtmlReport { - enabled = true // false to disable report generation - htmlReportDir.set(layout.buildDirectory.dir("my-project-report/html-result")) +kover { + isDisabled.set(false) // true to disable instrumentation and all Kover tasks in this project + engine = kotlinx.kover.api.DefaultIntellijEngine.INSTANCE // change Coverage Engine + filters { // common filters for all default Kover tasks + classes { // common class filter for all default Kover tasks + includes.add("com.example.*") // class inclusion rules + excludes.addAll("com.example.subpackage.*") // class exclusion rules + } + } - includes = ['com.example.*'] // inclusion rules for classes - excludes = ['com.example.subpackage.*'] // exclusion rules for classes -} + instrumentation { + excludeTasks.add("dummy-tests") // set of test tasks names to exclude from instrumentation. The results of their execution will not be presented in the report + } -tasks.koverXmlReport { - enabled = true // false to disable report generation - xmlReportFile.set(layout.buildDirectory.file("my-project-report/result.xml")) - includes = ['com.example.*'] // inclusion rules for classes - excludes = ['com.example.subpackage.*'] // exclusion rules for classes + xmlReport { + onCheck.set(false) // true to run koverXmlReport task during the execution of the check task + reportFile.set(layout.buildDirectory.file("my-project-report/result.xml")) // change report file name + overrideFilters { + classes { // override common class filter + includes.add("com.example2.*") // override class inclusion rules + excludes.addAll("com.example2.subpackage.*") // override class exclusion rules + } + } + } + + htmlReport { + onCheck.set(false) // true to run koverHtmlReport task during the execution of the check task + reportDir.set(layout.buildDirectory.dir("my-project-report/html-result")) // change report directory + overrideFilters { + classes { // override common class filter + includes.add("com.example2.*") // class inclusion rules + excludes.addAll("com.example2.subpackage.*") // override class exclusion rules + } + } + } + + verify { + onCheck.set(true) // true to run koverVerify task during the execution of the check task + rule { // add verification rule + enabled = true // false to disable rule checking + name = null // custom name for the rule + target = 'ALL' // specify by which entity the code for separate coverage evaluation will be grouped + + overrideClassFilter { // override common class filter + includes.add("com.example.verify.*") // override class inclusion rules + excludes.addAll("com.example.verify.subpackage.*") // override class exclusion rules + } + + bound { // add rule bound + minValue = 10 + maxValue = 20 + counter = 'LINE' // change coverage metric to evaluate (LINE, INSTRUCTION, BRANCH) + valueType = 'COVERED_PERCENTAGE' // change counter value (COVERED_COUNT, MISSED_COUNT, COVERED_PERCENTAGE, MISSED_PERCENTAGE) + } + } + } } ``` -By default, for tasks `koverHtmlReport` and `koverXmlReport` coverage is calculated only for the tests of the one project. -If classes or functions are called from tests of another module, then you need to set a flag `runAllTestsForProjectTask` for `KoverExtension` to `true` ([see](#configuring-entire-plugin)). +Engine version is specified separately, see [specifying coverage engine](#specifying-coverage-engine) section. -**In this case, then running tasks `koverHtmlReport` or `koverXmlReport` will trigger the execution of all active tests from all projects!** +### Configuring merged reports + +In order to create a merged report, it has to be enabled explicitly in a containing project with + `koverMerged.enable()` or +``` +koverMerged { + enable() +} +``` +By default, merged reports include containing project along with all its subprojects. -You may collect all project reports into one directory using the `koverCollectReports` task. -Also, you may specify a custom directory to collect project reports in the build directory of the project in which the plugin -is applied (usually this is the root project): +Merged reports can also be configured in a similar manner. To do this, you need to configure the extension in the containing project (where `koverMerged.enable()` is called)
Kotlin ```kotlin -tasks.koverCollectReports { - outputDir.set(layout.buildDirectory.dir("all-projects-reports") ) -} -``` -
+koverMerged { + enable() // create Kover merged reports + + filters { // common filters for all default Kover merged tasks + classes { // common class filter for all default Kover merged tasks + includes += "com.example.*" // class inclusion rules + excludes += listOf("com.example.subpackage.*") // class exclusion rules + } -
-Groovy + projects { // common projects filter for all default Kover merged tasks + excludes += listOf("project1", ":child:project") // Specifies the projects excluded from the merged tasks + } + } -```groovy -tasks.koverCollectReports { - outputDir.set(layout.buildDirectory.dir("all-projects-reports") ) -} -``` -
-### Configuring entire plugin -In the project in which the plugin is applied, you can configure the following properties: + xmlReport { + onCheck.set(false) // true to run koverMergedXmlReport task during the execution of the check task + reportFile.set(layout.buildDirectory.file("my-merged-report/result.xml")) // change report file name + overrideClassFilter { // override common class filter + includes += "com.example2.*" // override class inclusion rules + excludes += listOf("com.example2.subpackage.*") // override class exclusion rules + } + } -
-Kotlin + htmlReport { + onCheck.set(false) // true to run koverMergedHtmlReport task during the execution of the check task + reportDir.set(layout.buildDirectory.dir("my-merged-report/html-result")) // change report directory + overrideClassFilter { // override common class filter + includes += "com.example2.*" // override class inclusion rules + excludes += listOf("com.example2.subpackage.*") // override class exclusion rules + } + } -```kotlin -kover { - isDisabled = false // true to disable instrumentation of all test tasks in all projects - coverageEngine.set(kotlinx.kover.api.CoverageEngine.INTELLIJ) // change instrumentation agent and reporter - intellijEngineVersion.set("1.0.656") // change version of IntelliJ agent and reporter - jacocoEngineVersion.set("0.8.7") // change version of JaCoCo agent and reporter - generateReportOnCheck = true // false to do not execute `koverMergedReport` task before `check` task - disabledProjects = setOf() // setOf("project-name") or setOf(":project-name") to disable coverage for project with path `:project-name` (`:` for the root project) - instrumentAndroidPackage = false // true to instrument packages `android.*` and `com.android.*` - runAllTestsForProjectTask = false // true to run all tests in all projects if `koverHtmlReport`, `koverXmlReport`, `koverReport`, `koverVerify` or `check` tasks executed on some project + verify { + onCheck.set(true) // true to run koverMergedVerify task during the execution of the check task + rule { // add verification rule + isEnabled = true // false to disable rule checking + name = null // custom name for the rule + target = kotlinx.kover.api.VerificationTarget.ALL // specify by which entity the code for separate coverage evaluation will be grouped + + overrideClassFilter { // override common class filter + includes += "com.example.verify.*" // override class inclusion rules + excludes += listOf("com.example.verify.subpackage.*") // override class exclusion rules + } + + bound { // add rule bound + minValue = 10 + maxValue = 20 + counter = kotlinx.kover.api.CounterType.LINE // change coverage metric to evaluate (LINE, INSTRUCTION, BRANCH) + valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE // change counter value (COVERED_COUNT, MISSED_COUNT, COVERED_PERCENTAGE, MISSED_PERCENTAGE) + } + } + } } ```
@@ -345,61 +427,71 @@ kover { Groovy ```groovy -kover { - disabled = false // true to disable instrumentation of all test tasks in all projects - coverageEngine.set(kotlinx.kover.api.CoverageEngine.INTELLIJ) // change instrumentation agent and reporter - intellijEngineVersion.set('1.0.656') // change version of IntelliJ agent and reporter - jacocoEngineVersion.set('0.8.7') // change version of JaCoCo agent and reporter - generateReportOnCheck = true // false to do not execute `koverMergedReport` task before `check` task - disabledProjects = [] // ["project-name"] or [":project-name"] to disable coverage for project with path `:project-name` (`:` for the root project) - instrumentAndroidPackage = false // true to instrument packages `android.*` and `com.android.*` - runAllTestsForProjectTask = false // true to run all tests in all projects if `koverHtmlReport`, `koverXmlReport`, `koverReport`, `koverVerify` or `check` tasks executed on some project +koverMerged { + enable() // create Kover merged reports + + filters { // common filters for all default Kover merged tasks + classes { // common class filter for all default Kover merged tasks + includes.add("com.example.*") // class inclusion rules + excludes.addAll("com.example.subpackage.*") // class exclusion rules + } + + projects { // common projects filter for all default Kover merged tasks + excludes.addAll("project1", ":child:project") // Specifies the projects excluded in the merged tasks + } + } + + + xmlReport { + onCheck.set(false) // true to run koverMergedXmlReport task during the execution of the check task + reportFile.set(layout.buildDirectory.file("my-merged-report/result.xml")) // change report file name + overrideClassFilter { // override common class filter + includes.add("com.example2.*") // override class inclusion rules + excludes.addAll("com.example2.subpackage.*") // override class exclusion rules + } + } + + htmlReport { + onCheck.set(false) // true to run koverMergedHtmlReport task during the execution of the check task + reportDir.set(layout.buildDirectory.dir("my-merged-report/html-result")) // change report directory + overrideClassFilter { // override common class filter + includes.add("com.example2.*") // override class inclusion rules + excludes.addAll("com.example2.subpackage.*") // override class exclusion rules + } + } + + verify { + onCheck.set(true) // true to run koverMergedVerify task during the execution of the check task + rule { // add verification rule + isEnabled = true // false to disable rule checking + name = null // custom name for the rule + target = 'ALL' // specify by which entity the code for separate coverage evaluation will be grouped + + overrideClassFilter { // override common class filter + includes.add("com.example.verify.*") // override class inclusion rules + excludes.addAll("com.example.verify.subpackage.*") // override class exclusion rules + } + + bound { // add rule bound + minValue = 10 + maxValue = 20 + counter = 'LINE' // change coverage metric to evaluate (LINE, INSTRUCTION, BRANCH) + valueType = 'COVERED_PERCENTAGE' // change counter value (COVERED_COUNT, MISSED_COUNT, COVERED_PERCENTAGE, MISSED_PERCENTAGE) + } + } + } } ``` -## Verification -You may specify one or more rules that check the values of the code coverage counters. - -Validation rules work for both types of agents. - -*The plugin currently only supports line counter values.* - - -To add a rule to check coverage of the code of all projects, you need to add configuration to the project in which the plugin -is applied (usually this is the root project): +### Specifying Coverage Engine +#### IntelliJ Coverage Engine with default version
Kotlin ```kotlin -tasks.koverMergedVerify { - includes = listOf("com.example.*") // inclusion rules for classes - excludes = listOf("com.example.subpackage.*") // exclusion rules for classes - - rule { - name = "Minimum number of lines covered" - bound { - minValue = 100000 - valueType = kotlinx.kover.api.VerificationValueType.COVERED_LINES_COUNT - } - } - rule { - // rule without a custom name - bound { - minValue = 1 - maxValue = 1000 - valueType = kotlinx.kover.api.VerificationValueType.MISSED_LINES_COUNT - } - } - rule { - name = "Minimal line coverage rate in percent" - bound { - minValue = 50 - // valueType is kotlinx.kover.api.VerificationValueType.COVERED_LINES_PERCENTAGE by default - } - } -} +kotlinx.kover.api.DefaultIntellijEngine ```
@@ -407,53 +499,21 @@ tasks.koverMergedVerify { Groovy ```groovy -tasks.koverMergedVerify { - includes = ['com.example.*'] // inclusion rules for classes - excludes = ['com.example.subpackage.*'] // exclusion rules for classes - - rule { - name = "Minimum number of lines covered" - bound { - minValue = 100000 - valueType = 'COVERED_LINES_COUNT' - } - } - rule { - // rule without a custom name - bound { - minValue = 1 - maxValue = 1000 - valueType = 'MISSED_LINES_COUNT' - } - } - rule { - name = "Minimal line coverage rate in percent" - bound { - minValue = 50 - // valueType is 'COVERED_LINES_PERCENTAGE' by default - } - } -} +kotlinx.kover.api.DefaultIntellijEngine.INSTANCE ``` -To add rules for code coverage checks for the code of one specific project, you need to add a configuration to this project: +#### IntelliJ Coverage Engine with custom version +``` +kotlinx.kover.api.IntellijEngine("1.0.668") +``` +#### JaCoCo Coverage Engine with default version
Kotlin ```kotlin -tasks.koverVerify { - includes = listOf("com.example.*") // inclusion rules for classes - excludes = listOf("com.example.subpackage.*") // exclusion rules for classes - - rule { - name = "Minimal line coverage rate in percent" - bound { - minValue = 75 - } - } -} +kotlinx.kover.api.DefaultJacocoEngine ```
@@ -461,44 +521,33 @@ tasks.koverVerify { Groovy ```groovy -tasks.koverVerify { - includes = ['com.example.*'] // inclusion rules for classes - excludes = ['com.example.subpackage.*'] // exclusion rules for classes - - rule { - name = "Minimal line coverage rate in percent" - bound { - minValue = 75 - } - } -} +kotlinx.kover.api.DefaultJacocoEngine.INSTANCE ``` -By default, for the task `koverVerify` coverage is calculated only for the tests of the one project. -If classes or functions are called from tests of another module, then you need to set a flag `runAllTestsForProjectTask` for `KoverExtension` to `true` ([see](#configuring-entire-plugin)). +#### JaCoCo Coverage Engine with custom version +``` +kotlinx.kover.api.JacocoEngine("0.8.8") +``` -**In this case, if verification rules are added, then running tasks `koverVerify` or `check` will trigger the execution of all active tests from all projects!** +## Kover default tasks +Tasks that are created for a project where the Kover plugin is applied: +- `koverHtmlReport` - Generates code coverage HTML report for all enabled test tasks in the project. +- `koverXmlReport` - Generates code coverage XML report for all enabled test tasks in the project. +- `koverReport` - Executes both `koverXmlReport` and `koverHtmlReport` tasks. +- `koverVerify` - Verifies code coverage metrics of the project based on configured rules. Always is executed before `check` task. -## Tasks -The plugin, when applied, automatically creates tasks for the project in which it is applied (usually this is the root project): +Tasks that are created for project where the Kover plugin is applied and merged reports are enabled: - `koverMergedHtmlReport` - Generates code coverage HTML report for all enabled test tasks in all projects. -- `koverMergedXmlReport` - Generates code coverage XML report for all enabled test tasks in all projects. -- `koverMergedReport` - Executes both `koverMergedXmlReport` and `koverMergedHtmlReport` tasks. Executes before `check` task if property `generateReportOnCheck` for `KoverExtension` is `true` ([see](#configuring-entire-plugin)). +- `koverMergedXmlReport` - Generates code coverage XML report for all enabled test tasks in all projects. +- `koverMergedReport` - Executes both `koverMergedXmlReport` and `koverMergedHtmlReport` tasks. - `koverMergedVerify` - Verifies code coverage metrics of all projects based on specified rules. Always executes before `check` task. -- `koverCollectReports` - Collects all projects reports into one directory. Default directory is `$buildDir/reports/kover/projects`, names for XML reports and dirs for HTML are projects names. Executing this task does not run `koverMergedXmlReport` or `koverMergedHtmlReport`, it only copies previously created reports if they exist to the output directory. - -Tasks that are created for all projects: -- `koverHtmlReport` - Generates code coverage HTML report for all enabled test tasks in one project. -- `koverXmlReport` - Generates code coverage XML report for all enabled test tasks in one project. -- `koverReport` - Executes both `koverXmlReport` and `koverHtmlReport` tasks. -- `koverVerify` - Verifies code coverage metrics of one project based on specified rules. Always executes before `check` task. ## Implicit plugin dependencies While the plugin is being applied, the artifacts of the JaCoCo or IntelliJ toolkit are dynamically loaded. They are downloaded from the `mavenCentral` repository. -For the plugin to work correctly, you need to make sure that the `mavenCentral` (or its mirror) is added to the repository list of the project in which the plugin is applied, if it doesn't already exist (usually this is the root project): +For the plugin to work correctly, you need to make sure that the `mavenCentral` (or any of its mirrors) is added to the repository list of the project in which the plugin is applied, if it doesn't already exist (usually this is the root project):
Kotlin diff --git a/build.gradle.kts b/build.gradle.kts index 7d40d330..ac9a2de4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,7 @@ +import org.jetbrains.kotlin.gradle.plugin.* + plugins { - kotlin("jvm") version "1.6.10" + kotlin("jvm") version "1.7.10" `java-gradle-plugin` `maven-publish` @@ -19,21 +21,26 @@ sourceSets { } } +// adding the ability to use internal classes inside functional tests +kotlin.target.compilations.run { + getByName("functionalTest").associateWith(getByName(KotlinCompilation.MAIN_COMPILATION_NAME)) +} + dependencies { implementation(gradleApi()) // exclude transitive dependency on stdlib, the Gradle version should be used compileOnly(kotlin("stdlib")) - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:${property("kotlin.version")}") compileOnly("com.android.tools.build:gradle:4.2.2") testImplementation(kotlin("test")) "functionalTestImplementation"(gradleTestKit()) // dependencies only for plugin's classpath to work with Kotlin Multi-Platform and Android plugins - "functionalTestCompileOnly"("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") - "functionalTestCompileOnly"("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.6.10") - "functionalTestCompileOnly"("org.jetbrains.kotlin:kotlin-compiler-runner:1.6.10") + "functionalTestCompileOnly"("org.jetbrains.kotlin:kotlin-gradle-plugin:${property("kotlin.version")}") + "functionalTestCompileOnly"("org.jetbrains.kotlin:kotlin-compiler-embeddable:${property("kotlin.version")}") + "functionalTestCompileOnly"("org.jetbrains.kotlin:kotlin-compiler-runner:${property("kotlin.version")}") } java { @@ -73,7 +80,7 @@ tasks.withType().configureEach // Kover works with the stdlib of at least version `1.4.x` languageVersion = "1.4" apiVersion = "1.4" - // Kotlin compiler 1.6 issues a warning if `languageVersion` or `apiVersion` 1.4 is used - suppress it + // Kotlin compiler 1.7 issues a warning if `languageVersion` or `apiVersion` 1.4 is used - suppress it freeCompilerArgs = freeCompilerArgs + "-Xsuppress-version-warnings" } } diff --git a/buildSrc/src/main/kotlin/PublicationMavenCentral.kt b/buildSrc/src/main/kotlin/PublicationMavenCentral.kt index ce2ee67c..14d0f56e 100644 --- a/buildSrc/src/main/kotlin/PublicationMavenCentral.kt +++ b/buildSrc/src/main/kotlin/PublicationMavenCentral.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ import org.gradle.api.* diff --git a/docs/migration-to-0.6.0.md b/docs/migration-to-0.6.0.md new file mode 100644 index 00000000..1c8da9ca --- /dev/null +++ b/docs/migration-to-0.6.0.md @@ -0,0 +1,552 @@ +The new API allows you to configure Kover in a more flexible manner, while being more concise than the previous API. +From now on, there is no need to configure Kover or test tasks separately. + +# Main differences +- applying the plugin to the root project no longer causes it to be recursively applied to all subprojects - you must explicitly apply it to all projects that will be covered +- merged tasks are not created by default. You must explicitly enable it if necessary (for details [see](#merged-report-changes)) +- the extension `kover {}` is used to configure tasks `koverXmlReport`, `koverHtmlReport`, `koverVerify`, test tasks, instead of configuring these tasks directly +- the extension `koverMerged {}` is used to configure tasks `koverMergedXmlReport`, `koverMergedHtmlReport`, `koverMergedVerify`, instead of configuring these tasks directly +- task `koverCollectReports` was removed + +# Merged report changes + +In the new API, merged tasks are not created by default. To create them, you need to use the plugin in the `koverMerged` extension, it is necessary to call the function `enable()`. +e.g. +``` +koverMerged.enable() +``` +or +``` +koverMerged { + enable() +} +``` +or +``` +extensions.configure { + enable() +} +``` + +Now any merged tasks settings occur only in the `koverMerged` extension. +By default, tasks use the results of measuring the coverage of the project in which they were created and all subprojects. +At the same time, it is important that the Kover plugin is used in all these projects, as well as that they use the same variant (vendor and version) of the coverage engine. + +For example, it can be done this way +``` +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlinx:kover:0.6.0-BETA") + } +} + +apply(plugin = "kover") + +extensions.configure { + enable() + // configure merged tasks +} + +allprojects { + apply(plugin = "kover") + + extensions.configure { + // `true` - to disable the collection of coverage metrics for tests from this project + isDisabled.set(false) + + // configure engine variant + engine.set(kotlinx.kover.api.IntellijEngine("1.0.657")) + + // configure project's tasks if needed + } +} +``` + +In order not to use measurements from some projects now instead of `disabledProjects` property for `kover` extension you need to use `koverMerged` extension: +``` +extensions.configure { + enable() + filters { + projects { + excludes.addAll("project-to-exclude", ":path:to:exclude") + } + } +} +``` + +Instead of configuring the merged XML report task +``` +koverMergedXmlReport { + // config task +} +``` +or +``` +tasks.withType { + // config task +} +``` +you need to configure `koverMerged` extension: +``` +extensions.configure { + enable() + xmlReport { + // config task + } +} +``` + +Instead of configuring the merged HTML report task +``` +koverMergedHtmlReport { + // config task +} +``` +or +``` +tasks.withType { + // config task +} +``` +you need to configure `koverMerged` extension: +``` +extensions.configure { + enable() + htmlReport { + // config task + } +} +``` + + +Instead of configuring the merged verification report task +``` +koverMergedVerify { + // config task +} +``` +or +``` +tasks.withType { + // config task +} +``` +you need to configure `koverMerged` extension: +``` +extensions.configure { + enable() + verify { + // config task + } +} +``` + +# Migration Issues + +## Root kover extension + +### type of `isDisabled` property changed from `Boolean` to `Property`. + +_Error message:_ + +``` +Val cannot be reassigned +``` + +_Solution for Kotlin script:_ change `isDisabled = true` to `isDisabled.set(true)` + +### Properties `coverageEngine`, `intellijEngineVersion` and `jacocoEngineVersion` were removed. + +_Error messages:_ + +``` +Using 'coverageEngine: Property' is an error +``` +``` +Using 'intellijEngineVersion: Property' is an error +``` +``` +Using 'jacocoEngineVersion: Property' is an error +``` + +_Solution:_ + +Use property `engine` - it combines version and coverage engine vendor. + +To use IntelliJ Coverage Engine with default version write `engine.set(kotlinx.kover.api.DefaultIntellijEngine)` +(Kotlin) or `engine = kotlinx.kover.api.DefaultIntellijEngine.INSTANCE` (Groovy). + +To use IntelliJ Coverage Engine with custom version write `engine.set(kotlinx.kover.api.IntellijEngine("version"))` +(Kotlin) or `engine = kotlinx.kover.api.IntellijEngine("version")` (Groovy). + +To use JaCoCo Coverage Engine with default version write `engine.set(kotlinx.kover.api.DefaultJacocoEngine)` +(Kotlin) or `engine = kotlinx.kover.api.DefaultJacocoEngine.INSTANCE` (Groovy). + +To use JaCoCo Coverage Engine with custom version write `engine.set(kotlinx.kover.api.JacocoEngine("version"))` +(Kotlin) or `engine = kotlinx.kover.api.JacocoEngine("version")` (Groovy). + +### Property "generateReportOnCheck" was removed + +Use the properties individually for each report + +``` +kover { + xmlReport { + onCheck.set(true) + } + + htmlReport { + onCheck.set(true) + } + + verify { + onCheck.set(true) + } +} +``` + +### property `disabledProjects` was removed + +_Error message:_ + +```Using 'disabledProjects: Set' is an error.``` + +_Solution_ + +- read about [merged reports changes](#merged-report-changes) +- use exclusion list in project filters of merged configuration extension + +``` +koverMerged { + enable() + filters { + projects { + excludes.add(":path or unique project name") + } + } +} +``` + +If `includes` are empty, all subprojects and current project are used in merged reports. + +### Property `instrumentAndroidPackage` was removed + +There is no replacement. At the moment, all classes from the packages "android." and "com.android.*" excluded from +instrumentation. + +### property `runAllTestsForProjectTask` was removed + +In the new API for single-project reports, it is impossible to call test tasks of another project. To account for coverage from tests of another module, use merged reports. + +## Kover extension for test task + +### type of `isDisabled` property changed from `Boolean` to `Property`. +_Error message_ + +``` +Val cannot be reassigned +``` + +_Solution for Kotlin script:_ change `isDisabled = true` to `isDisabled.set(true)` + +### `binaryReportFile` was renamed to `reportFile` + +Solution: change `binaryReportFile` to `reportFile` + +### Type of `includes` property changed from `List` to `ListProperty` + +Solution: + +```includes.addAll("com.example.*", "foo.bar.*")``` + +### type of `excludes` property changed from `List` to `ListProperty` + +Solution for Kotlin: change `excludes = listOf("com.example.*", "foo.bar.*")` +to `includes.addAll("com.example.*", "foo.bar.*")` + +Solution for Groovy: change `excludes = ["com.example.*", "foo.bar.*"]` +to `includes.addAll("com.example.*", "foo.bar.*")` + +## `koverXmlReport` and `koverMergedXmlReport` tasks configuration + +### Property `xmlReportFile` was removed +Solution: use property in Kover extension at the root of the project + +``` +kover { + xmlReport { + reportFile.set(yourFile) + } +} +``` +* for `xmlReportFile` task use `koverMerged { ... }` extension of the project. + +### Property `includes` was removed + +Solution for Kotlin: use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + includes += listOf("foo.bar.*", "foo.biz.*") + } + } +} +``` + +Solution for Groovy: use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + includes.addAll("foo.bar.*", "foo.biz.*") + } + } +} +``` +* for `xmlReportFile` task use `koverMerged { ... }` extension of the project. + +### Property `excludes` was removed + +Solution for Kotlin: use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + excludes += listOf("foo.bar.*", "foo.biz.*") + } + } +} +``` + +Solution for Groovy: use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + excludes.addAll("foo.bar.*", "foo.biz.*") + } + } +} +``` +* for `xmlReportFile` task use `koverMerged { ... }` extension of the project. + +## `KoverTaskExtension` configuration + +### Type of `excludes` and `includes` property changed from `List` to `ListProperty` +_Error message:_ +``` +Val cannot be reassigned +``` + +_Solution:_ + +``` +includes.addAll("com.example.*", "foo.bar.*") +``` +and +``` +excludes.addAll("com.example.*", "foo.bar.*") +``` + + +## `koverHtmlReport` and `koverMergedHtmlReport` tasks configuration + +### Class `KoverHtmlReportTask` was removed + +_Error message:_ +``` +Using 'KoverHtmlReportTask' is an error +``` + +_Solution:_ + +Configure report by Kover project extension + +``` +kover { + htmlReport { + // HTML report settings + } +} +``` + +### Property `htmlReportDir` was removed + +_Error message:_ +``` +Using 'htmlReportDir: DirectoryProperty' is an error +``` + +Solution: use property `reportDir` in Kover extension at the root of the project + +``` +kover { + htmlReport { + reportDir.set(yourDir) + } +} +``` +* for `koverMergedHtmlReport` task use `koverMerged { ... }` extension of the project. + +### Property `includes` was removed + +_Error message:_ +``` +Using 'includes: List' is an error +``` + +_Solution for Kotlin:_ use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + includes += listOf("foo.bar.*", "foo.biz.*") + } + } +} +``` + +_Solution for Groovy:_ use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + includes.addAll("foo.bar.*", "foo.biz.*") + } + } +} +``` +* for `koverMergedHtmlReport` task use `koverMerged { ... }` extension of the project. + +### Property `excludes` was removed + +Error message: +``` +Using 'excludes: List' is an error +``` + +_Solution for Kotlin:_ use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + excludes += listOf("foo.bar.*", "foo.biz.*") + } + } +} +``` + +Solution for Groovy: use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + excludes.addAll("foo.bar.*", "foo.biz.*") + } + } +} +``` +* for `koverMergedHtmlReport` task use `koverMerged { ... }` extension of the project. + +## `koverVerify` and `koverMergedVerify` tasks configuration + +### Function `rule` was removed for single-project verification + +Error message: +``` +Using 'rule(Action): Unit' is an error +``` + +_Solution:_ + +use function `rule` in Kover project extension + +``` +kover { + verify { + rule { + // your verification rule + } + } +} +``` + +* For `koverMergedVerify` task use `koverMerged { ... }` extension of the project. + +### Property `includes` was removed + +_Error message:_ +``` +Using 'includes: List' is an error +``` + +Solution for Kotlin: use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + includes += listOf("foo.bar.*", "foo.biz.*") + } + } +} +``` + +Solution for Groovy: use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + includes.addAll("foo.bar.*", "foo.biz.*") + } + } +} +``` +* for `koverMergedVerify` task use `koverMerged { ... }` extension of the project. + +### Property `excludes` was removed + +_Error message:_ +``` +Using 'excludes: List' is an error +``` + +_Solution for Kotlin:_ use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + excludes += listOf("foo.bar.*", "foo.biz.*") + } + } +} +``` + +_Solution for Groovy:_ use filter in Kover extension at the root of the project + +``` +kover { + filters { + classes { + excludes.addAll("foo.bar.*", "foo.biz.*") + } + } +} +``` +* For `koverMergedVerify` task use `koverMerged { ... }` extension of the project. + diff --git a/gradle.properties b/gradle.properties index 92f2dfd3..11800157 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ -version=0.5.1 +version=0.6.0-SNAPSHOT group=org.jetbrains.kotlinx +kotlin.version=1.7.10 kotlin.code.style=official diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt index 4b392e11..57440f2b 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt @@ -14,7 +14,7 @@ internal class AdaptersTests : BaseGradleScriptTest() { Classes from plugins applied in subproject not accessible for Kover in root project. Therefore, Kover is forced to use reflection to work with extensions of the kotlin multiplatform plugin. */ - internalSample("different-plugins") + sampleBuild("different-plugins") .run("koverMergedXmlReport") { xml(defaultMergedXmlReport()) { classCounter("org.jetbrains.CommonClass").assertFullyCovered() @@ -22,7 +22,7 @@ internal class AdaptersTests : BaseGradleScriptTest() { } } - internalSample("different-plugins") + sampleBuild("different-plugins") .run("koverXmlReport") { subproject("subproject-multiplatform") { xml(defaultXmlReport()) { diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ConfigurationCacheTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ConfigurationCacheTests.kt index 5c2d89bb..125a3be0 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ConfigurationCacheTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ConfigurationCacheTests.kt @@ -3,12 +3,25 @@ package kotlinx.kover.test.functional.cases import kotlinx.kover.test.functional.core.* import kotlin.test.* -internal class ConfigurationCacheTests: BaseGradleScriptTest() { +internal class ConfigurationCacheTests : BaseGradleScriptTest() { @Test fun testConfigCache() { - builder("Testing configuration cache support") - .sources("simple") - .build() - .run("build", "koverMergedReport", "koverMergedVerify", "koverReport", "koverVerify", "--configuration-cache") + val build = diverseBuild() + build.addKoverRootProject { + sourcesFrom("simple") + koverMerged { + enable() + } + } + + val runner = build.prepare() + runner.run( + "build", + "koverMergedReport", + "koverMergedVerify", + "koverReport", + "koverVerify", + "--configuration-cache" + ) } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt index a95f1a07..75486281 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt @@ -7,15 +7,20 @@ import kotlin.test.* internal class DefaultConfigTests : BaseGradleScriptTest() { @Test fun testImplicitConfigs() { - builder("Test implicit default settings") - .languages(GradleScriptLanguage.GROOVY, GradleScriptLanguage.KOTLIN) - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .sources("simple") - .build() - .run("build") { - checkDefaultBinaryReport() - checkDefaultMergedReports() - } + val build = diverseBuild( + languages = ALL_LANGUAGES, + types = ALL_TYPES + ) + build.addKoverRootProject { + sourcesFrom("simple") + } + val runner = build.prepare() + + runner.run("koverReport") { + checkDefaultBinaryReport() + checkDefaultReports() + } + } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt index b318bbed..ea7d857b 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt @@ -1,6 +1,5 @@ package kotlinx.kover.test.functional.cases -import kotlinx.kover.api.* import kotlinx.kover.test.functional.cases.utils.* import kotlinx.kover.test.functional.core.* import kotlinx.kover.test.functional.core.BaseGradleScriptTest @@ -10,47 +9,59 @@ internal class InstrumentationFilteringTests : BaseGradleScriptTest() { @Test fun testExclude() { - builder("Test exclusion of classes from instrumentation") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("simple") - .configTest( - """excludes = listOf("org.jetbrains.*Exa?ple*")""", - """excludes = ['org.jetbrains.*Exa?ple*']""" - ) - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.ExampleClass").assertFullyMissed() - classCounter("org.jetbrains.SecondClass").assertCovered() - } + val build = diverseBuild(ALL_LANGUAGES, ALL_ENGINES, ALL_TYPES) + build.addKoverRootProject { + sourcesFrom("simple") + testTasks { + excludes("org.jetbrains.*Exa?ple*") + } + } + val runner = build.prepare() + runner.run("build", "koverXmlReport") { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.ExampleClass").assertFullyMissed() + classCounter("org.jetbrains.SecondClass").assertCovered() } + } + } @Test fun testExcludeInclude() { - builder("Test inclusion and exclusion of classes in instrumentation") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("simple") - .configTest( - """includes = listOf("org.jetbrains.*Cla?s")""", - """includes = ['org.jetbrains.*Cla?s']""" - ) - .configTest( - """excludes = listOf("org.jetbrains.*Exa?ple*")""", - """excludes = ['org.jetbrains.*Exa?ple*']""" - ) - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.ExampleClass").assertFullyMissed() - classCounter("org.jetbrains.Unused").assertFullyMissed() - classCounter("org.jetbrains.SecondClass").assertCovered() + val build = diverseBuild(ALL_LANGUAGES, ALL_ENGINES, ALL_TYPES) + build.addKoverRootProject { + sourcesFrom("simple") + testTasks { + includes("org.jetbrains.*Cla?s") + excludes("org.jetbrains.*Exa?ple*") + } + } + val runner = build.prepare() + runner.run("build", "koverXmlReport") { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.ExampleClass").assertFullyMissed() + classCounter("org.jetbrains.Unused").assertFullyMissed() + classCounter("org.jetbrains.SecondClass").assertCovered() + } + } + } + + @Test + fun testDisableInstrumentationOfTask() { + val build = diverseBuild(ALL_LANGUAGES, ALL_ENGINES, listOf(ProjectType.KOTLIN_JVM)) + build.addKoverRootProject { + sourcesFrom("simple") + kover { + instrumentation { + excludeTasks += "test" } } + } + val runner = build.prepare() + runner.run("build", "koverXmlReport") { + // if task `test` is excluded from instrumentation then the binary report is not created for it + checkDefaultBinaryReport(false) + } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiProjectTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiProjectTests.kt index 48b7d475..413b60b8 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiProjectTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiProjectTests.kt @@ -1,175 +1,223 @@ package kotlinx.kover.test.functional.cases -import kotlinx.kover.api.* import kotlinx.kover.test.functional.cases.utils.* import kotlinx.kover.test.functional.cases.utils.defaultMergedXmlReport +import kotlinx.kover.test.functional.core.* import kotlinx.kover.test.functional.core.BaseGradleScriptTest -import kotlinx.kover.test.functional.core.ProjectType import org.gradle.testkit.runner.* import kotlin.test.* internal class MultiProjectTests : BaseGradleScriptTest() { private val subprojectName = "common" + private val rootName = "kover-functional-test" + @Test fun testMergedReports() { - builder("Testing the generation of merged reports") - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") - } - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.CommonClass").assertFullyCovered() - classCounter("org.jetbrains.CommonInternalClass").assertFullyCovered() - classCounter("org.jetbrains.UserClass").assertFullyCovered() - } - } - } + val build = diverseBuild(engines = ALL_ENGINES, types = ALL_TYPES) + val subPath = build.addKoverSubproject(subprojectName) { + sourcesFrom("multiproject-common") + } - @Test - fun testIsolatedProjectsReports() { - builder("Testing the generation of project reports with running tests only for this project") - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") + build.addKoverRootProject { + sourcesFrom("multiproject-user") + subproject(subPath) + + koverMerged { + enable() } - .build() - .run("koverReport") { - xml(defaultXmlReport()) { - classCounter("org.jetbrains.CommonClass").assertAbsent() - classCounter("org.jetbrains.CommonInternalClass").assertAbsent() - classCounter("org.jetbrains.UserClass").assertFullyCovered() - } + } - subproject(subprojectName) { - xml(defaultXmlReport()) { - classCounter("org.jetbrains.UserClass").assertAbsent() - // common class covered partially because calls from the root project are not counted - classCounter("org.jetbrains.CommonClass").assertCovered() - classCounter("org.jetbrains.CommonInternalClass").assertCovered() - } - } + val runner = build.prepare() + runner.run(":koverMergedXmlReport") { + xml(defaultMergedXmlReport()) { + classCounter("org.jetbrains.CommonClass").assertFullyCovered() + classCounter("org.jetbrains.CommonInternalClass").assertFullyCovered() + classCounter("org.jetbrains.UserClass").assertFullyCovered() } + } } @Test - fun testLinkedProjectsReports() { - builder("Testing the generation of project reports with running all tests for all projects") - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .configKover { runAllTestsForProjectTask = true } - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") + fun testIsolatedProjectsReports() { + val build = diverseBuild(engines = ALL_ENGINES, types = ALL_TYPES) + val subPath = build.addKoverSubproject(subprojectName) { + sourcesFrom("multiproject-common") + } + + build.addKoverRootProject { + sourcesFrom("multiproject-user") + subproject(subPath) + } + + + val runner = build.prepare() + runner.run("koverXmlReport") { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.CommonClass").assertAbsent() + classCounter("org.jetbrains.CommonInternalClass").assertAbsent() + classCounter("org.jetbrains.UserClass").assertFullyCovered() } - .build() - .run("koverReport") { - xml(defaultXmlReport()) { - classCounter("org.jetbrains.CommonClass").assertAbsent() - classCounter("org.jetbrains.CommonInternalClass").assertAbsent() - classCounter("org.jetbrains.UserClass").assertFullyCovered() - } - subproject(subprojectName) { - xml(defaultXmlReport()) { - classCounter("org.jetbrains.UserClass").assertAbsent() + subproject(subprojectName) { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.UserClass").assertAbsent() - // common class fully covered because calls from the root project are counted too - classCounter("org.jetbrains.CommonClass").assertFullyCovered() - classCounter("org.jetbrains.CommonInternalClass").assertFullyCovered() - } + // common class covered partially because calls from the root project are not counted + classCounter("org.jetbrains.CommonClass").assertCovered() + classCounter("org.jetbrains.CommonInternalClass").assertCovered() } } + } } +// +// @Test +// fun testLinkedProjectsReports() { +// builder("Testing the generation of project reports with running all tests for all projects") +// .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) +// .engines(CoverageEngineVendor.INTELLIJ, CoverageEngineVendor.JACOCO) +// .configKover { runAllTestsForProjectTask = true } +// .sources("multiproject-user") +// .subproject(subprojectName) { +// sources("multiproject-common") +// } +// .build() +// .run("koverReport") { +// xml(defaultXmlReport()) { +// classCounter("org.jetbrains.CommonClass").assertAbsent() +// classCounter("org.jetbrains.CommonInternalClass").assertAbsent() +// classCounter("org.jetbrains.UserClass").assertFullyCovered() +// } +// +// subproject(subprojectName) { +// xml(defaultXmlReport()) { +// classCounter("org.jetbrains.UserClass").assertAbsent() +// +// // common class fully covered because calls from the root project are counted too +// classCounter("org.jetbrains.CommonClass").assertFullyCovered() +// classCounter("org.jetbrains.CommonInternalClass").assertFullyCovered() +// } +// } +// } +// } @Test fun testDisabledKover() { - builder("Testing disabling whole Kover") - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") + val build = diverseBuild(engines = ALL_ENGINES, types = ALL_TYPES) + val subPath = build.addKoverSubproject(subprojectName) { + sourcesFrom("multiproject-common") + kover { + isDisabled = true } - .configKover { disabled = true } - .build() - .run("build", "koverHtmlReport") { - checkDefaultBinaryReport(false) - checkOutcome("koverMergedHtmlReport", TaskOutcome.SKIPPED) - checkOutcome("koverMergedVerify", TaskOutcome.SKIPPED) + } + build.addKoverRootProject { + sourcesFrom("multiproject-user") + subproject(subPath) + kover { + isDisabled = true + } + } + + val runner = build.prepare() + runner.run("koverReport", "koverVerify") { + checkDefaultBinaryReport(false) + + checkOutcome("koverHtmlReport", TaskOutcome.SKIPPED) + checkOutcome("koverXmlReport", TaskOutcome.SKIPPED) + checkOutcome("koverVerify", TaskOutcome.SKIPPED) + + subproject(subprojectName) { + checkDefaultBinaryReport(false) checkOutcome("koverHtmlReport", TaskOutcome.SKIPPED) + checkOutcome("koverXmlReport", TaskOutcome.SKIPPED) checkOutcome("koverVerify", TaskOutcome.SKIPPED) - checkOutcome("koverMergedHtmlReport", TaskOutcome.SKIPPED) - - subproject(subprojectName) { - checkDefaultBinaryReport(false) - checkOutcome("koverHtmlReport", TaskOutcome.SKIPPED) - checkOutcome("koverVerify", TaskOutcome.SKIPPED) - } } + } } @Test - fun testDisableSubproject() { - builder("Testing disabling one of subproject") - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") - } - .configKover { disabledProjects += subprojectName } - .build() - .run("build", "koverReport") { - checkDefaultBinaryReport() - checkDefaultMergedReports() - checkDefaultReports() - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.UserClass").assertFullyCovered() - - // classes from disabled project should not be included in the merged report - classCounter("org.jetbrains.CommonClass").assertAbsent() - classCounter("org.jetbrains.CommonInternalClass").assertAbsent() + fun testExcludeProject() { + val build = diverseBuild(engines = ALL_ENGINES, types = ALL_TYPES) + val subPath = build.addKoverSubproject(subprojectName) { + sourcesFrom("multiproject-common") + } + + build.addKoverRootProject { + sourcesFrom("multiproject-user") + subproject(subPath) + + koverMerged { + enable() + filters { + projects { + excludes += subprojectName + } } + } + } + + val runner = build.prepare() + runner.run("koverMergedReport") { + checkDefaultBinaryReport() + checkDefaultReports(false) + checkDefaultMergedReports() + xml(defaultMergedXmlReport()) { + classCounter("org.jetbrains.UserClass").assertFullyCovered() + + // classes from disabled project should not be included in the merged report + classCounter("org.jetbrains.CommonClass").assertAbsent() + classCounter("org.jetbrains.CommonInternalClass").assertAbsent() + } - subproject(subprojectName) { - checkDefaultBinaryReport(false) - checkDefaultMergedReports(false) - checkDefaultReports(false) - } + subproject(subprojectName) { + checkDefaultBinaryReport(false) + checkDefaultMergedReports(false) + checkDefaultReports(false) } + } } @Test - fun testDisableSubprojectByPath() { - builder("Testing disabling one of subproject by path") - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") - } - .configKover { disabledProjects += ":$subprojectName" } - .build() - .run("build", "koverReport") { - checkDefaultBinaryReport() - checkDefaultMergedReports() - checkDefaultReports() - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.UserClass").assertFullyCovered() - - // classes from disabled project should not be included in the merged report - classCounter("org.jetbrains.CommonClass").assertAbsent() - classCounter("org.jetbrains.CommonInternalClass").assertAbsent() + fun testExcludeProjectByPath() { + val build = diverseBuild(engines = ALL_ENGINES, types = ALL_TYPES) + val subPath = build.addKoverSubproject(subprojectName) { + sourcesFrom("multiproject-common") + } + + build.addKoverRootProject { + sourcesFrom("multiproject-user") + subproject(subPath) + + koverMerged { + enable() + filters { + projects { + excludes += subPath + } } + } + } + + val runner = build.prepare() + runner.run("koverMergedReport") { + checkDefaultBinaryReport() + checkDefaultReports(false) + checkDefaultMergedReports() + xml(defaultMergedXmlReport()) { + classCounter("org.jetbrains.UserClass").assertFullyCovered() + + // classes from disabled project should not be included in the merged report + classCounter("org.jetbrains.CommonClass").assertAbsent() + classCounter("org.jetbrains.CommonInternalClass").assertAbsent() + } - subproject(subprojectName) { - checkDefaultBinaryReport(false) - checkDefaultMergedReports(false) - checkDefaultReports(false) - } + subproject(subprojectName) { + checkDefaultBinaryReport(false) + checkDefaultMergedReports(false) + checkDefaultReports(false) } + } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt index f1491a82..f2ea6cd8 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt @@ -1,7 +1,8 @@ package kotlinx.kover.test.functional.cases -import kotlinx.kover.api.* import kotlinx.kover.test.functional.cases.utils.* +import kotlinx.kover.test.functional.core.* +import kotlinx.kover.test.functional.core.ALL_ENGINES import kotlinx.kover.test.functional.core.BaseGradleScriptTest import org.gradle.testkit.runner.* import kotlin.test.* @@ -9,56 +10,56 @@ import kotlin.test.* internal class ReportsCachingTests : BaseGradleScriptTest() { @Test fun testCaching() { - builder("Test caching of merged reports") - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("simple") - .withLocalCache() - .build() - .run("build", "--build-cache") { - checkDefaultBinaryReport() - checkDefaultMergedReports() - checkOutcome("test", TaskOutcome.SUCCESS) - checkOutcome("koverMergedXmlReport", TaskOutcome.SUCCESS) - checkOutcome("koverMergedHtmlReport", TaskOutcome.SUCCESS) - } - .run("clean", "--build-cache") { - checkDefaultBinaryReport(false) - checkDefaultMergedReports(false) - } - .run("build", "--build-cache") { - checkDefaultBinaryReport() - checkDefaultMergedReports() - checkOutcome("test", TaskOutcome.FROM_CACHE) - checkOutcome("koverMergedXmlReport", TaskOutcome.FROM_CACHE) - checkOutcome("koverMergedHtmlReport", TaskOutcome.FROM_CACHE) - } + val build = diverseBuild(engines = ALL_ENGINES, withCache = true) + build.addKoverRootProject { + sourcesFrom("simple") + } + val runner = build.prepare() + runner.run("koverReport") { + checkDefaultBinaryReport() + checkDefaultReports() + checkOutcome("test", TaskOutcome.SUCCESS) + checkOutcome("koverXmlReport", TaskOutcome.SUCCESS) + checkOutcome("koverHtmlReport", TaskOutcome.SUCCESS) + } + runner.run("clean") { + checkDefaultBinaryReport(false) + checkDefaultReports(false) + } + runner.run("koverReport") { + checkDefaultBinaryReport() + checkDefaultReports() + checkOutcome("test", TaskOutcome.FROM_CACHE) + checkOutcome("koverXmlReport", TaskOutcome.FROM_CACHE) + checkOutcome("koverHtmlReport", TaskOutcome.FROM_CACHE) + } } @Test fun testProjectReportCaching() { - builder("Test caching projects reports") - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("simple") - .withLocalCache() - .build() - .run("koverReport", "--build-cache") { - checkDefaultBinaryReport() - checkDefaultReports() - checkOutcome("test", TaskOutcome.SUCCESS) - checkOutcome("koverXmlReport", TaskOutcome.SUCCESS) - checkOutcome("koverHtmlReport", TaskOutcome.SUCCESS) - } - .run("clean", "--build-cache") { - checkDefaultBinaryReport(false) - checkDefaultReports(false) - } - .run("koverReport", "--build-cache") { - checkDefaultBinaryReport() - checkDefaultReports() - checkOutcome("test", TaskOutcome.FROM_CACHE) - checkOutcome("koverXmlReport", TaskOutcome.FROM_CACHE) - checkOutcome("koverHtmlReport", TaskOutcome.FROM_CACHE) - } + val build = diverseBuild(engines = ALL_ENGINES, withCache = true) + build.addKoverRootProject { + sourcesFrom("simple") + } + val runner = build.prepare() + runner.run("koverReport") { + checkDefaultBinaryReport() + checkDefaultReports() + checkOutcome("test", TaskOutcome.SUCCESS) + checkOutcome("koverXmlReport", TaskOutcome.SUCCESS) + checkOutcome("koverHtmlReport", TaskOutcome.SUCCESS) + } + runner.run("clean") { + checkDefaultBinaryReport(false) + checkDefaultReports(false) + } + runner.run("koverReport") { + checkDefaultBinaryReport() + checkDefaultReports() + checkOutcome("test", TaskOutcome.FROM_CACHE) + checkOutcome("koverXmlReport", TaskOutcome.FROM_CACHE) + checkOutcome("koverHtmlReport", TaskOutcome.FROM_CACHE) + } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsFilteringTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsFilteringTests.kt index c310e411..8d5e4ce2 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsFilteringTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsFilteringTests.kt @@ -9,54 +9,50 @@ internal class ReportsFilteringTests : BaseGradleScriptTest() { @Test fun testExclude() { - builder("Test exclusion of classes from XML report") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .sources("simple") - .config( - """ - tasks.koverMergedXmlReport { - excludes = listOf("org.jetbrains.*Exa?ple*") - }""".trimIndent(), - """ - tasks.koverMergedXmlReport { - excludes = ['org.jetbrains.*Exa?ple*'] - }""".trimIndent() - ) - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.ExampleClass").assertAbsent() - classCounter("org.jetbrains.SecondClass").assertCovered() + val build = diverseBuild(languages = ALL_LANGUAGES) + build.addKoverRootProject { + sourcesFrom("simple") + + kover { + filters { + classes { + excludes += "org.jetbrains.*Exa?ple*" + } } } + } + val runner = build.prepare() + runner.run("koverXmlReport") { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.ExampleClass").assertAbsent() + classCounter("org.jetbrains.SecondClass").assertCovered() + } + } } @Test fun testExcludeInclude() { - builder("Test inclusion and exclusion of classes in XML report") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .sources("simple") - .config( - """ - tasks.koverMergedXmlReport { - includes = listOf("org.jetbrains.*Cla?s") - excludes = listOf("org.jetbrains.*Exa?ple*") - }""".trimIndent(), + val build = diverseBuild(languages = ALL_LANGUAGES) + build.addKoverRootProject { + sourcesFrom("simple") - """ - tasks.koverMergedXmlReport { - includes = ['org.jetbrains.*Cla?s'] - excludes = ['org.jetbrains.*Exa?ple*'] - }""".trimIndent() - ) - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.ExampleClass").assertAbsent() - classCounter("org.jetbrains.Unused").assertAbsent() - classCounter("org.jetbrains.SecondClass").assertFullyCovered() + kover { + filters { + classes { + excludes += "org.jetbrains.*Exa?ple*" + includes += "org.jetbrains.*Cla?s" + } } } + } + val runner = build.prepare() + runner.run("koverXmlReport") { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.ExampleClass").assertAbsent() + classCounter("org.jetbrains.Unused").assertAbsent() + classCounter("org.jetbrains.SecondClass").assertFullyCovered() + } + } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt index b423b3b1..678599c5 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt @@ -12,84 +12,102 @@ import kotlin.test.* internal class VerificationTests : BaseGradleScriptTest() { @Test fun testVerified() { - builder("Test verification passed") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("simple") - .rule("test rule") { - bound { - minValue = 50 - maxValue = 60 - } - bound { - valueType = VerificationValueType.COVERED_LINES_COUNT - minValue = 2 - maxValue = 10 + val build = diverseBuild(languages = ALL_LANGUAGES, engines = ALL_ENGINES) + build.addKoverRootProject { + sourcesFrom("simple") + + kover { + verify { + rule { + name = "test rule" + bound { + minValue = 50 + maxValue = 60 + } + bound { + valueType = VerificationValueType.COVERED_COUNT + minValue = 2 + maxValue = 10 + } + } } } - .build() - .run("koverVerify") + } + + build.prepare().run("koverVerify", "--stacktrace") } @Test fun testNotVerifiedIntelliJ() { - builder("Test verification failed for IntelliJ Engine") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .engines(CoverageEngine.INTELLIJ) - .sources("simple") - .rule("test rule") { - bound { - minValue = 58 - maxValue = 60 - } - bound { - valueType = VerificationValueType.COVERED_LINES_COUNT - minValue = 2 - maxValue = 3 + val build = diverseBuild(languages = ALL_LANGUAGES, engines = listOf(CoverageEngineVendor.INTELLIJ)) + build.addKoverRootProject { + sourcesFrom("simple") + + kover { + verify { + rule { + name = "test rule" + bound { + minValue = 58 + maxValue = 60 + } + bound { + valueType = VerificationValueType.COVERED_COUNT + minValue = 2 + maxValue = 3 + } + } } } - .build() - .runWithError("koverVerify") { - output { - assertTrue { - this.contains( - "> Rule 'test rule' violated:\n" + - " covered lines percentage is 57.142900, but expected minimum is 58\n" + - " covered lines count is 4, but expected maximum is 3" - ) - } + } + + build.prepare().runWithError("koverVerify") { + output { + assertTrue { + this.contains( + "> Rule 'test rule' violated:\n" + + " covered lines percentage is 57.142900, but expected minimum is 58\n" + + " covered lines count is 4, but expected maximum is 3" + ) } } + } } @Test fun testNotVerifiedJaCoCo() { - builder("Test verification failed for JaCoCo Engine") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .engines(CoverageEngine.JACOCO) - .sources("simple") - .rule("test rule") { - bound { - minValue = 58 - maxValue = 60 - } - bound { - valueType = VerificationValueType.COVERED_LINES_COUNT - minValue = 2 - maxValue = 3 + val build = diverseBuild(languages = ALL_LANGUAGES, engines = listOf(CoverageEngineVendor.JACOCO)) + build.addKoverRootProject { + sourcesFrom("simple") + + kover { + verify { + rule { + name = "test rule" + bound { + minValue = 58 + maxValue = 60 + } + bound { + valueType = VerificationValueType.COVERED_COUNT + minValue = 2 + maxValue = 3 + } + } } } - .build() - .runWithError("koverVerify") { - output { - assertTrue { - this.contains( - "[ant:jacocoReport] Rule violated for bundle :: lines covered ratio is 0.50, but expected minimum is 0.58\n" + - "[ant:jacocoReport] Rule violated for bundle :: lines covered count is 4, but expected maximum is 3" - ) - } + } + + build.prepare().runWithError("koverVerify") { + output { + assertTrue { + this.contains( + "[ant:jacocoReport] Rule violated for bundle :: lines covered ratio is 0.50, but expected minimum is 0.58\n" + + "[ant:jacocoReport] Rule violated for bundle :: lines covered count is 4, but expected maximum is 3" + ) } } + } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/counters/CountersValueTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/counters/CountersValueTests.kt index 8a9e3bb1..040a7d25 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/counters/CountersValueTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/counters/CountersValueTests.kt @@ -1,45 +1,51 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ package kotlinx.kover.test.functional.cases.counters import kotlinx.kover.api.* import kotlinx.kover.test.functional.cases.utils.* +import kotlinx.kover.test.functional.core.* import kotlinx.kover.test.functional.core.BaseGradleScriptTest import kotlin.test.* internal class CountersValueTests : BaseGradleScriptTest() { @Test fun testBasicCounterCases() { - builder("Testing of basic counting capabilities by an IntelliJ Agent") - .engines(CoverageEngine.INTELLIJ) - .sources("counters") - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - // test on branch counter - methodCounter("org.jetbrains.MyBranchedClass", "foo", type = "BRANCH").assertCovered(1, 3) - - // test on constructor of used sealed classes - methodCounter("org.jetbrains.Sealed", "").assertFullyCovered() - methodCounter("org.jetbrains.SealedWithInit", "").assertFullyCovered() - methodCounter("org.jetbrains.SealedWithConstructor", "").assertFullyCovered() - - // test on empty objects - classCounter("org.jetbrains.UnusedObject").assertFullyMissed() - classCounter("org.jetbrains.UsedObject").assertFullyCovered() - - // deprecated functions (ERROR and HIDDEN) should not be included in the report. - methodCounter("org.jetbrains.Different", "deprecatedError").assertAbsent() - methodCounter("org.jetbrains.Different", "deprecatedHidden").assertAbsent() - // WARNING deprecated functions should be included in the report. - methodCounter("org.jetbrains.Different", "deprecatedWarn").assertFullyMissed() - - // empty functions must be included in the report with line counter - methodCounter("org.jetbrains.Different", "emptyFun", type = "LINE").assertFullyMissed() - - // Instruction counters - // helloWorld contains 4 instructions. `return` ignored by reporter and the first instruction is not covered because of a bug in the IR compiler - // FIXME after https://youtrack.jetbrains.com/issue/KT-51080 is fixed, the number should become 3 - methodCounter("org.jetbrains.Different", "helloWorld").assertTotal(2) - } + val build = diverseBuild( + engines = listOf(CoverageEngineVendor.INTELLIJ) + ) + build.addKoverRootProject { + sourcesFrom("counters") + } + + val runner = build.prepare() + runner.run("koverXmlReport") { + xml(defaultXmlReport()) { + // test on branch counter + methodCounter("org.jetbrains.MyBranchedClass", "foo", type = "BRANCH").assertCovered(1, 3) + + // test on constructor of used sealed classes + methodCounter("org.jetbrains.Sealed", "").assertFullyCovered() + methodCounter("org.jetbrains.SealedWithInit", "").assertFullyCovered() + methodCounter("org.jetbrains.SealedWithConstructor", "").assertFullyCovered() + + // test on empty objects + classCounter("org.jetbrains.UnusedObject").assertFullyMissed() + classCounter("org.jetbrains.UsedObject").assertFullyCovered() + + // deprecated functions (ERROR and HIDDEN) should not be included in the report. + methodCounter("org.jetbrains.Different", "deprecatedError").assertAbsent() + methodCounter("org.jetbrains.Different", "deprecatedHidden").assertAbsent() + // WARNING deprecated functions should be included in the report. + methodCounter("org.jetbrains.Different", "deprecatedWarn").assertFullyMissed() + + // empty functions must be included in the report with line counter + methodCounter("org.jetbrains.Different", "emptyFun", type = "LINE").assertFullyMissed() + + // Instruction counters - value may depend on Kotlin compiler version + methodCounter("org.jetbrains.Different", "helloWorld").assertTotal(4) } + } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Commons.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Commons.kt index 48383530..2d6b2b4a 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Commons.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Commons.kt @@ -60,7 +60,7 @@ internal fun RunResult.checkReports(xmlPath: String, htmlPath: String, mustExist } internal fun RunResult.checkIntellijErrors(errorExpected: Boolean = false) { - if (engine != CoverageEngine.INTELLIJ) return + if (engine != CoverageEngineVendor.INTELLIJ) return file(errorsDirectory()) { if (this.exists() && !errorExpected) { diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Defaults.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Defaults.kt index 947f69a3..09e836af 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Defaults.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Defaults.kt @@ -1,22 +1,30 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + package kotlinx.kover.test.functional.cases.utils import kotlinx.kover.api.* import kotlinx.kover.test.functional.core.ProjectType -internal fun defaultBinaryReport(engine: CoverageEngine, projectType: ProjectType): String { - val extension = if (engine == CoverageEngine.INTELLIJ) "ic" else "exec" +internal fun defaultTestTask(engine: CoverageEngineVendor, projectType: ProjectType): String { + val extension = if (engine == CoverageEngineVendor.INTELLIJ) "ic" else "exec" return when (projectType) { - ProjectType.KOTLIN_JVM -> "kover/test.$extension" - ProjectType.KOTLIN_MULTIPLATFORM -> "kover/jvmTest.$extension" - ProjectType.ANDROID -> "kover/jvmTest.$extension" + ProjectType.KOTLIN_JVM -> "test.$extension" + ProjectType.KOTLIN_MULTIPLATFORM -> "jvmTest.$extension" + ProjectType.ANDROID -> "jvmTest.$extension" } } -internal fun defaultMergedXmlReport() = "reports/kover/report.xml" -internal fun defaultMergedHtmlReport() = "reports/kover/html" +internal fun defaultBinaryReport(engine: CoverageEngineVendor, projectType: ProjectType): String { + return "kover/" + defaultTestTask(engine, projectType) +} + +internal fun defaultMergedXmlReport() = "reports/kover/merged/xml/report.xml" +internal fun defaultMergedHtmlReport() = "reports/kover/merged/html" -internal fun defaultXmlReport() = "reports/kover/project-xml/report.xml" -internal fun defaultHtmlReport() = "reports/kover/project-html" +internal fun defaultXmlReport() = "reports/kover/xml/report.xml" +internal fun defaultHtmlReport() = "reports/kover/html" internal fun errorsDirectory() = "kover/errors" diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt index f5e2a3ba..1895ad58 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt @@ -1,19 +1,83 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + package kotlinx.kover.test.functional.core +import kotlinx.kover.api.* import org.junit.* import org.junit.rules.* +/** + * Kotlin by default. + */ +internal val DEFAULT_LANGUAGE = listOf(GradleScriptLanguage.KOTLIN) + +/** + * Used engine from configuration if specified. Otherwise, used [DefaultIntellijEngine] engine. + */ +internal val DEFAULT_ENGINE = listOf() + +/** + * Kotlin JVM project by default. + */ +internal val DEFAULT_TYPE = listOf(ProjectType.KOTLIN_JVM) + +internal val ALL_LANGUAGES = listOf(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) +internal val ALL_ENGINES = listOf(CoverageEngineVendor.INTELLIJ, CoverageEngineVendor.JACOCO) +internal val ALL_TYPES = listOf(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) + internal open class BaseGradleScriptTest { @Rule @JvmField internal val rootFolder: TemporaryFolder = TemporaryFolder() - fun builder(description: String): TestCaseBuilder { - return createBuilder(rootFolder.root, description) + + internal fun diverseBuild( + languages: List = DEFAULT_LANGUAGE, + engines: List = DEFAULT_ENGINE, + types: List = DEFAULT_TYPE, + withCache: Boolean = false + ): DiverseBuild { + return DiverseBuildState(rootFolder.root, languages, engines, types, withCache) } - fun internalSample(name: String): GradleRunner { - return createInternalSample(name, rootFolder.root) + internal fun sampleBuild(templateName: String): GradleRunner { + return createInternalSample(templateName, rootFolder.root) } } + + +internal fun DiverseBuild.addKoverRootProject(builder: ProjectBuilder.() -> Unit) { + addProject("root", ":") { + plugins { + kotlin("1.7.10") + kover("DEV") + } + + repositories { + repository("mavenCentral()") + } + + builder() + } + +} + +internal fun DiverseBuild.addKoverSubproject(name: String, builder: ProjectBuilder.() -> Unit): String { + addProject(name, ":$name") { + plugins { + kotlin() + kover() + } + + repositories { + repository("mavenCentral()") + } + + builder() + } + + return ":$name" +} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt index b638381c..6d33425e 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt @@ -5,163 +5,247 @@ package kotlinx.kover.test.functional.core import kotlinx.kover.api.* +import kotlinx.kover.test.functional.core.writer.initSlice import org.gradle.api.* import java.io.* -internal fun createBuilder(rootDir: File, description: String): TestCaseBuilder { - return TestCaseBuilderImpl(rootDir, description) -} +internal class DiverseBuildState( + private val rootDir: File, + private val languages: List, + private val engines: List, + private val types: List, + private val withCache: Boolean +) : DiverseBuild { + private val projects: MutableMap = mutableMapOf() -internal class CommonBuilderState(val description: String) { - var pluginVersion: String? = null - val languages: MutableSet = mutableSetOf() - val types: MutableSet = mutableSetOf() - val engines: MutableSet = mutableSetOf() - val koverConfig: KoverRootConfig = KoverRootConfig() - val rootProject: ProjectBuilderState = ProjectBuilderState() - val subprojects: MutableMap = mutableMapOf() - var localCache: Boolean = false -} + override fun addProject(name: String, path: String, builder: ProjectBuilder.() -> Unit) { + projects[path] = ProjectBuilderState(name).also(builder) + } + + override fun prepare(): GradleRunner { + val initSlices: MutableMap = mutableMapOf() + + val targetEngines = engines.ifEmpty { listOf(null) } + + + languages.forEach { language -> + types.forEach { type -> + targetEngines.forEach { engine -> + val slice = ProjectSlice(language, type, engine) + initSlices[slice] = initSlice(rootDir, slice, projects, withCache) + } + } + } -internal class ProjectBuilderState { - val sourceTemplates: MutableList = mutableListOf() - val scripts: MutableList = mutableListOf() - val testScripts: MutableList = mutableListOf() - val dependencies: MutableList = mutableListOf() - val rules: MutableList = mutableListOf() - val mainSources: MutableMap = mutableMapOf() - val testSources: MutableMap = mutableMapOf() + val extraArgs = if (withCache) { + listOf("--build-cache") + } else { + listOf() + } + + return DiverseGradleRunner(initSlices, extraArgs) + } } -internal data class GradleScript(val kotlin: String, val groovy: String) -private class TestCaseBuilderImpl( - val rootDir: File, - description: String, - private val state: CommonBuilderState = CommonBuilderState(description) -) : ProjectBuilderImpl(state.rootProject), TestCaseBuilder { +internal class ProjectBuilderState(val name: String) : ProjectBuilder { + val sourceTemplates: MutableSet = mutableSetOf() + val plugins: PluginsState = PluginsState() + val repositories: RepositoriesState = RepositoriesState() + var kover: TestKoverProjectConfigState? = null + var merged: TestKoverMergedConfigState? = null + val testTasks: TestTaskConfigState = TestTaskConfigState() + val subprojects: MutableList = mutableListOf() - override fun languages(vararg languages: GradleScriptLanguage) = also { - state.languages += languages + override fun plugins(block: Plugins.() -> Unit) { + plugins.also(block) } - override fun engines(vararg engines: CoverageEngine) = also { - state.engines += engines + override fun repositories(block: Repositories.() -> Unit) { + repositories.also(block) } - override fun types(vararg types: ProjectType) = also { - state.types += types + override fun kover(config: TestKoverProjectConfig.() -> Unit) { + kover = TestKoverProjectConfigState().also(config) } - override fun withLocalCache(): TestCaseBuilder = also { - state.localCache = true + override fun koverMerged(config: TestKoverMergedConfig.() -> Unit) { + merged = TestKoverMergedConfigState().also(config) } - override fun configKover(config: KoverRootConfig.() -> Unit) = also { - state.koverConfig.config() + override fun subproject(path: String) { + subprojects += path } - override fun subproject(name: String, builder: ProjectBuilder<*>.() -> Unit) = also { - val projectState = state.subprojects.computeIfAbsent(name) { ProjectBuilderState() } - @Suppress("UPPER_BOUND_VIOLATED_WARNING") - ProjectBuilderImpl>(projectState).builder() + override fun testTasks(block: TestTaskConfig.() -> Unit) { + testTasks.also(block) } - override fun build(): GradleRunner { - if (state.languages.isEmpty()) { - state.languages += GradleScriptLanguage.KOTLIN - } - if (state.types.isEmpty()) { - state.types += ProjectType.KOTLIN_JVM - } - if (state.engines.isEmpty()) { - state.engines += null - } - if (state.pluginVersion == null) { - state.pluginVersion = "0.5.0" - } + override fun sourcesFrom(template: String) { + sourceTemplates += template + } +} - val projects: MutableMap = mutableMapOf() +internal class TestKoverProjectConfigState : TestKoverProjectConfig { + override var isDisabled: Boolean? = null + override var engine: CoverageEngineVariant? = null + val filters: TestKoverProjectFiltersState = TestKoverProjectFiltersState() + val instrumentation: KoverProjectInstrumentation = KoverProjectInstrumentation() + val xml: TestKoverProjectXmlConfigState = TestKoverProjectXmlConfigState() + val html: TestKoverProjectHtmlConfigState = TestKoverProjectHtmlConfigState() + val verify: TestKoverVerifyConfigState = TestKoverVerifyConfigState() + override fun filters(config: TestKoverProjectFilters.() -> Unit) { + filters.also(config) + } - state.languages.forEach { language -> - state.types.forEach { type -> - state.engines.forEach { engine -> - val slice = ProjectSlice(language, type, engine ?: CoverageEngine.INTELLIJ) - projects[slice] = state.createProject(rootDir, slice) - } - } - } + override fun instrumentation(config: KoverProjectInstrumentation.() -> Unit) { + instrumentation.also(config) + } - return GradleRunnerImpl(projects) + override fun xmlReport(config: TestKoverProjectXmlConfig.() -> Unit) { + xml.also(config) } + override fun htmlReport(config: TestKoverProjectHtmlConfig.() -> Unit) { + html.also(config) + } + + override fun verify(config: TestKoverVerifyConfig.() -> Unit) { + verify.also(config) + } } +internal class TestKoverProjectFiltersState : TestKoverProjectFilters { + var classes: KoverClassFilter? = null + var sourceSets: KoverSourceSetFilter? = null -@Suppress("UNCHECKED_CAST") -private open class ProjectBuilderImpl>(val projectState: ProjectBuilderState) : ProjectBuilder { + override fun classes(config: KoverClassFilter.() -> Unit) { + classes = KoverClassFilter().also(config) + } - override fun rule(name: String?, builder: RuleBuilder.() -> Unit): B { - projectState.rules += TestVerificationRule(projectState.rules.size, name).apply(builder) - return this as B + override fun sourceSets(config: KoverSourceSetFilter.() -> Unit) { + sourceSets = KoverSourceSetFilter().also(config) } +} + +internal class TestKoverProjectXmlConfigState : TestKoverProjectXmlConfig { + override var onCheck: Boolean? = null + override var reportFile: File? = null + var overrideFilters: TestKoverProjectFiltersState? = null - override fun configTest(script: String): B { - projectState.testScripts += GradleScript(script, script) - return this as B + override fun overrideFilters(config: TestKoverProjectFilters.() -> Unit) { + if (overrideFilters == null) { + overrideFilters = TestKoverProjectFiltersState() + } + overrideFilters!!.also(config) } +} - override fun configTest(kotlin: String, groovy: String): B { - projectState.testScripts += GradleScript(kotlin, groovy) - return this as B +internal class TestKoverProjectHtmlConfigState : TestKoverProjectHtmlConfig { + override var onCheck: Boolean? = null + override var reportDir: File? = null + var overrideFilters: TestKoverProjectFiltersState? = null + + override fun overrideFilters(config: TestKoverProjectFilters.() -> Unit) { + if (overrideFilters == null) { + overrideFilters = TestKoverProjectFiltersState() + } + overrideFilters!!.also(config) } +} - override fun config(script: String): B { - projectState.scripts += GradleScript(script, script) - return this as B +internal class TestKoverVerifyConfigState : TestKoverVerifyConfig { + override val onCheck: Boolean? = null + val rules: MutableList = mutableListOf() + + override fun rule(config: TestVerificationRule.() -> Unit) { + rules += TestVerificationRule().also(config) } +} + +internal class TestVerificationRule { + var isEnabled: Boolean? = null + var name: String? = null + var target: VerificationTarget? = null + + var overrideClassFilter: KoverClassFilter? = null + val bounds: MutableList = mutableListOf() - override fun config(kotlin: String, groovy: String): B { - projectState.scripts += GradleScript(kotlin, groovy) - return this as B + fun overrideClassFilter(config: Action) { + overrideClassFilter = KoverClassFilter().also { config.execute(it) } } - override fun dependency(script: String): B { - projectState.dependencies += GradleScript(script, script) - return this as B + fun bound(configureBound: VerificationBoundState.() -> Unit) { + bounds += VerificationBoundState().also(configureBound) } +} + +internal class VerificationBoundState { + var minValue: Int? = null + var maxValue: Int? = null + var counter: CounterType? = null + var valueType: VerificationValueType? = null +} - override fun dependency(kotlin: String, groovy: String): B { - projectState.dependencies += GradleScript(kotlin, groovy) - return this as B +internal class TestTaskConfigState : TestTaskConfig { + var excludes: List? = null + var includes: List? = null + override fun excludes(vararg classes: String) { + excludes = classes.toList() } - override fun sources(template: String): B { - projectState.sourceTemplates += template - return this as B + override fun includes(vararg classes: String) { + includes = classes.toList() } + } -private data class TestVerificationRule( - override val id: Int, - override var name: String? -) : VerificationRule, RuleBuilder { - override val bounds: MutableList = mutableListOf() - override fun bound(configureBound: Action) { - bounds += TestVerificationBound(bounds.size).also { configureBound.execute(it) } +internal class RepositoriesState : Repositories { + val repositories: MutableList = mutableListOf() + override fun repository(name: String) { + repositories += name } - override fun bound(builder: VerificationBound.() -> Unit) { - bounds += TestVerificationBound(bounds.size).apply(builder) +} + +internal class TestKoverMergedConfigState : TestKoverMergedConfig { + var enabled: Boolean = false + val filters: TestKoverMergedFiltersState = TestKoverMergedFiltersState() + override fun enable() { + enabled = true + } + + override fun filters(config: TestKoverMergedFilters.() -> Unit) { + filters.also(config) } } -private data class TestVerificationBound( - override val id: Int, - override var minValue: Int? = null, - override var maxValue: Int? = null, - override var valueType: VerificationValueType = VerificationValueType.COVERED_LINES_PERCENTAGE -) : VerificationBound +internal class TestKoverMergedFiltersState : TestKoverMergedFilters { + var classes: KoverClassFilter? = null + var projects: KoverProjectsFilter? = null + override fun classes(config: KoverClassFilter.() -> Unit) { + classes = KoverClassFilter().also(config) + } + override fun projects(config: KoverProjectsFilter.() -> Unit) { + projects = KoverProjectsFilter().also(config) + } +} + +internal class PluginsState : Plugins { + var useKotlin: Boolean = false + var useKover: Boolean = false + var kotlinVersion: String? = null + var koverVersion: String? = null + + override fun kotlin(version: String?) { + useKotlin = true + kotlinVersion = version + } + override fun kover(version: String?) { + useKover = true + koverVersion = version + } +} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt index 9966d57a..f90583f9 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt @@ -12,11 +12,11 @@ import java.io.* import javax.xml.parsers.* -internal class GradleRunnerImpl(private val projects: Map) : +internal class DiverseGradleRunner(private val projects: Map, private val extraArgs: List) : GradleRunner { - override fun run(vararg args: String, checker: RunResult.() -> Unit): GradleRunnerImpl { - val argsList = listOf(*args) + override fun run(vararg args: String, checker: RunResult.() -> Unit): DiverseGradleRunner { + val argsList = listOf(*args) + extraArgs projects.forEach { (slice, project) -> try { val buildResult = project.runGradle(argsList) @@ -27,8 +27,8 @@ internal class GradleRunnerImpl(private val projects: Map) : } return this } - override fun runWithError(vararg args: String, errorChecker: RunResult.() -> Unit): GradleRunnerImpl { - val argsList = listOf(*args) + override fun runWithError(vararg args: String, errorChecker: RunResult.() -> Unit): DiverseGradleRunner { + val argsList = listOf(*args) + extraArgs projects.forEach { (slice, project) -> try { project.runGradleWithError(argsList) @@ -98,11 +98,11 @@ private class RunResultImpl( private val buildScriptFile: File = buildFile() private val buildScript: String by lazy { buildScriptFile.readText() } - override val engine: CoverageEngine by lazy { + override val engine: CoverageEngineVendor by lazy { if (buildScript.contains("set(kotlinx.kover.api.CoverageEngine.JACOCO)")) { - CoverageEngine.JACOCO + CoverageEngineVendor.JACOCO } else { - CoverageEngine.INTELLIJ + CoverageEngineVendor.INTELLIJ } } @@ -116,9 +116,9 @@ private class RunResultImpl( throw IllegalArgumentException("Impossible to determine the type of project") } } else { - if (buildScript.contains("""id 'org.jetbrains.kotlin.jvm'""")) { + if (buildScript.contains("""id "org.jetbrains.kotlin.jvm"""")) { ProjectType.KOTLIN_JVM - } else if (buildScript.contains("""id 'org.jetbrains.kotlin.multiplatform'""")) { + } else if (buildScript.contains("""id "org.jetbrains.kotlin.multiplatform"""")) { ProjectType.KOTLIN_MULTIPLATFORM } else { throw IllegalArgumentException("Impossible to determine the type of project") diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt index 4ee1689a..602ed310 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt @@ -12,55 +12,109 @@ internal enum class GradleScriptLanguage { KOTLIN, GROOVY } internal enum class ProjectType { KOTLIN_JVM, KOTLIN_MULTIPLATFORM, ANDROID } -internal interface ProjectBuilder> { - fun sources(template: String): B +internal interface DiverseBuild { + fun addProject(name: String, path: String, builder: ProjectBuilder.() -> Unit) + fun prepare(): GradleRunner +} + +internal interface ProjectBuilder { + fun plugins(block: Plugins.() -> Unit) + + fun repositories(block: Repositories.() -> Unit) - fun rule(name: String? = null, builder: RuleBuilder.() -> Unit): B + fun kover(config: TestKoverProjectConfig.() -> Unit) - fun configTest(script: String): B - fun configTest(kotlin: String, groovy: String): B + fun koverMerged(config: TestKoverMergedConfig.() -> Unit) - fun config(script: String): B - fun config(kotlin: String, groovy: String): B + fun testTasks(block: TestTaskConfig.() -> Unit) - fun dependency(script: String): B - fun dependency(kotlin: String, groovy: String): B + fun subproject(path: String) + + fun sourcesFrom(template: String) } -internal interface RuleBuilder { - fun bound(builder: VerificationBound.() -> Unit) + +interface Plugins { + fun kotlin(version: String? = null) + fun kover(version: String? = null) } -internal interface TestCaseBuilder : ProjectBuilder { - fun languages(vararg languages: GradleScriptLanguage): TestCaseBuilder - fun engines(vararg engines: CoverageEngine): TestCaseBuilder - fun types(vararg types: ProjectType): TestCaseBuilder +interface Repositories { + fun repository(name: String) +} + +interface TestTaskConfig { + fun excludes(vararg classes: String) + fun includes(vararg classes: String) +} - fun withLocalCache(): TestCaseBuilder +/** + * Same as [KoverProjectConfig] + */ +internal interface TestKoverProjectConfig { + var isDisabled: Boolean? - fun configKover(config: KoverRootConfig.() -> Unit): TestCaseBuilder + var engine: CoverageEngineVariant? - fun subproject(name: String, builder: ProjectBuilder<*>.() -> Unit): TestCaseBuilder + fun filters(config: TestKoverProjectFilters.() -> Unit) - fun build(): GradleRunner + fun instrumentation(config: KoverProjectInstrumentation.() -> Unit) + + fun xmlReport(config: TestKoverProjectXmlConfig.() -> Unit) + + fun htmlReport(config: TestKoverProjectHtmlConfig.() -> Unit) + + fun verify(config: TestKoverVerifyConfig.() -> Unit) } -internal data class ProjectSlice(val language: GradleScriptLanguage, val type: ProjectType, val engine: CoverageEngine?) { - fun encodedString(): String { - return "${language.ordinal}_${type.ordinal}_${engine?.ordinal?:"default"}" - } + +internal interface TestKoverVerifyConfig { + val onCheck: Boolean? + + fun rule(config: TestVerificationRule.() -> Unit) +} + +internal interface TestKoverProjectXmlConfig { + var onCheck: Boolean? + var reportFile: File? + + fun overrideFilters(config: TestKoverProjectFilters.() -> Unit) +} + +internal interface TestKoverProjectHtmlConfig { + var onCheck: Boolean? + var reportDir: File? + + fun overrideFilters(config: TestKoverProjectFilters.() -> Unit) } -internal data class KoverRootConfig( - var disabled: Boolean? = null, - var intellijVersion: String? = null, - var jacocoVersion: String? = null, - var generateReportOnCheck: Boolean? = null, - var runAllTestsForProjectTask: Boolean? = null, - val disabledProjects: MutableSet = mutableSetOf() +internal interface TestKoverProjectFilters { + fun classes(config: KoverClassFilter.() -> Unit) + + fun sourceSets(config: KoverSourceSetFilter.() -> Unit) +} + +internal interface TestKoverMergedConfig { + public fun enable() + + public fun filters(config: TestKoverMergedFilters.() -> Unit) +} + +public interface TestKoverMergedFilters { + public fun classes(config: KoverClassFilter.() -> Unit) + + public fun projects(config: KoverProjectsFilter.() -> Unit) +} + + +internal data class ProjectSlice( + val language: GradleScriptLanguage, + val type: ProjectType, + val engine: CoverageEngineVendor? ) { - val isDefault = - disabled == null && intellijVersion == null && jacocoVersion == null && generateReportOnCheck == null + fun encodedString(): String { + return "${language.ordinal}_${type.ordinal}_${engine?.ordinal ?: "default"}" + } } internal interface GradleRunner { @@ -69,7 +123,7 @@ internal interface GradleRunner { } internal interface RunResult { - val engine: CoverageEngine + val engine: CoverageEngineVendor val projectType: ProjectType fun subproject(name: String, checker: RunResult.() -> Unit) diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt deleted file mode 100644 index 4e068906..00000000 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.test.functional.core - -import kotlinx.kover.api.* -import kotlinx.kover.test.functional.core.GradleScriptLanguage.KOTLIN -import java.io.* - -private const val TEMPLATES_PATH = "src/functionalTest/templates" -private const val BUILD_SCRIPTS_PATH = "$TEMPLATES_PATH/scripts/buildscripts" -private const val SETTINGS_PATH = "$TEMPLATES_PATH/scripts/settings" -private const val SOURCES_PATH = "$TEMPLATES_PATH/sources" - -internal fun CommonBuilderState.createProject(rootDir: File, slice: ProjectSlice): File { - val projectDir = File(rootDir, slice.encodedString()).also { it.mkdirs() } - - val extension = slice.scriptExtension - - val buildScript = loadScriptTemplate(true, slice) - .processRootBuildScript(this, slice) - .processProjectBuildScript(rootProject, slice, subprojects.keys) - - File(projectDir, "build.$extension").writeText(buildScript) - File(projectDir, "settings.$extension").writeText(buildSettings(slice)) - - rootProject.writeSources(projectDir, slice) - - subprojects.forEach { (name, state) -> state.writeSubproject(File(projectDir, name), slice) } - - return projectDir -} - -private fun ProjectBuilderState.writeSubproject(directory: File, slice: ProjectSlice) { - directory.mkdirs() - - val extension = slice.scriptExtension - - val buildScript = loadScriptTemplate(false, slice).processProjectBuildScript(this, slice) - - File(directory, "build.$extension").writeText(buildScript) - - writeSources(directory, slice) -} - - -private val ProjectSlice.scriptExtension get() = if (language == KOTLIN) "gradle.kts" else "gradle" - -private val ProjectSlice.srcPath: String - get() { - return when (type) { - ProjectType.KOTLIN_JVM -> "src/main" - ProjectType.KOTLIN_MULTIPLATFORM -> "src/jvmMain" - ProjectType.ANDROID -> "src/jvmMain" - } - } - -private val ProjectSlice.testPath: String - get() { - return when (type) { - ProjectType.KOTLIN_JVM -> "src/test" - ProjectType.KOTLIN_MULTIPLATFORM -> "src/jvmTest" - ProjectType.ANDROID -> "src/jvmTest" - } - } - - -private fun String.processRootBuildScript(state: CommonBuilderState, slice: ProjectSlice): String { - return replace("//PLUGIN_VERSION", state.pluginVersion!!) - .replace("//KOVER", state.buildRootExtension(slice)) -} - -private fun String.processProjectBuildScript( - state: ProjectBuilderState, - slice: ProjectSlice, - subprojects: Set = emptySet() -): String { - return replace("//REPOSITORIES", "") - .replace("//DEPENDENCIES", state.buildDependencies(slice, subprojects)) - .replace("//SCRIPTS", state.buildScripts(slice)) - .replace("//TEST_TASK", state.buildTestTask(slice)) - .replace("//VERIFICATIONS", state.buildVerifications(slice)) -} - - -private fun ProjectBuilderState.writeSources(projectDir: File, slice: ProjectSlice) { - fun File.processDir(result: MutableMap, targetRootPath: String, relativePath: String = "") { - listFiles()?.forEach { file -> - val filePath = "$relativePath/${file.name}" - if (file.isDirectory) { - file.processDir(result, targetRootPath, filePath) - } else if (file.exists() && file.length() > 0) { - val targetFile = File(projectDir, "$targetRootPath/$filePath") - targetFile.parentFile.mkdirs() - file.copyTo(targetFile) - } - } - } - - val srcPath = slice.srcPath - val testPath = slice.testPath - - sourceTemplates.forEach { template -> - File(SOURCES_PATH, "$template/main").processDir(mainSources, srcPath) - File(SOURCES_PATH, "$template/test").processDir(testSources, testPath) - } -} - -private fun ProjectSlice.scriptPath(): String { - val languageString = if (language == KOTLIN) "kotlin" else "groovy" - val typeString = when (type) { - ProjectType.KOTLIN_JVM -> "kjvm" - ProjectType.KOTLIN_MULTIPLATFORM -> "kmp" - ProjectType.ANDROID -> "android" - } - return "$BUILD_SCRIPTS_PATH/$languageString/$typeString" -} - -private fun buildSubprojectsIncludes(subprojects: Set): String { - if (subprojects.isEmpty()) return "" - - return subprojects.joinToString("\n", "\n", "\n") { - """include("$it")""" - } -} - -private fun CommonBuilderState.buildExtraSettings(): String { - return if (localCache) { - """ -buildCache { - local { - directory = "${"$"}settingsDir/build-cache" - } -} -""" - } else { - "" - } -} - -private fun CommonBuilderState.buildRootExtension(slice: ProjectSlice): String { - if (slice.engine == null && koverConfig.isDefault) { - return "" - } - - val builder = StringBuilder() - builder.appendLine() - builder.appendLine("kover {") - - if (koverConfig.disabled != null) { - val property = if (slice.language == KOTLIN) "isDisabled" else "disabled" - builder.appendLine("$property = ${koverConfig.disabled}") - } - - if (slice.engine == CoverageEngine.INTELLIJ) { - builder.appendLine(" coverageEngine.set(kotlinx.kover.api.CoverageEngine.INTELLIJ)") - if (koverConfig.intellijVersion != null) { - builder.appendLine(""" intellijEngineVersion.set("${koverConfig.intellijVersion}")""") - } - } - if (slice.engine == CoverageEngine.JACOCO) { - builder.appendLine(" coverageEngine.set(kotlinx.kover.api.CoverageEngine.JACOCO)") - if (koverConfig.jacocoVersion != null) { - builder.appendLine(""" jacocoEngineVersion.set("${koverConfig.jacocoVersion}")""") - } - } - - if (koverConfig.disabledProjects.isNotEmpty()) { - val prefix = if (slice.language == KOTLIN) "setOf(" else "[" - val postfix = if (slice.language == KOTLIN) ")" else "]" - val value = koverConfig.disabledProjects.joinToString(prefix = prefix, postfix = postfix) { "\"$it\"" } - builder.appendLine(" disabledProjects = $value") - } - - if (koverConfig.runAllTestsForProjectTask != null) { - builder.appendLine(" runAllTestsForProjectTask = ${koverConfig.runAllTestsForProjectTask}") - } - - builder.appendLine("}") - - return builder.toString() -} - -private fun ProjectBuilderState.buildTestTask(slice: ProjectSlice): String { - if (testScripts.isEmpty()) { - return "" - } - - val configs = testScripts.map { if (slice.language == KOTLIN) it.kotlin else it.groovy } - - return loadTestTaskTemplate(slice).replace("//KOVER_TEST_CONFIG", configs.joinToString("\n")) -} - -@Suppress("UNUSED_PARAMETER") -private fun ProjectBuilderState.buildVerifications(slice: ProjectSlice): String { - if (rules.isEmpty()) { - return "" - } - - val builder = StringBuilder() - builder.appendLine() - builder.appendLine("tasks.koverVerify {") - - for (rule in rules) { - builder.appendLine(" rule {") - rule.name?.also { builder.appendLine(""" name = "$it"""") } - for (bound in rule.bounds) { - builder.appendLine(" bound {") - bound.minValue?.let { builder.appendLine(" minValue = $it") } - bound.maxValue?.let { builder.appendLine(" maxValue = $it") } - if (bound.valueType != VerificationValueType.COVERED_LINES_PERCENTAGE) { - builder.appendLine(" valueType = kotlinx.kover.api.VerificationValueType.${bound.valueType}") - } - builder.appendLine(" }") - } - builder.appendLine(" }") - } - builder.appendLine("}") - - return builder.toString() -} - -private fun CommonBuilderState.buildSettings(slice: ProjectSlice): String { - return loadSettingsTemplate(slice) - .replace("//SUBPROJECTS", buildSubprojectsIncludes(subprojects.keys)) - .replace("//EXTRA_SETTINGS", buildExtraSettings()) -} - -private fun ProjectBuilderState.buildScripts(slice: ProjectSlice): String { - if (scripts.isEmpty()) { - return "" - } - val configs = scripts.map { if (slice.language == KOTLIN) it.kotlin else it.groovy } - return configs.joinToString("\n", "\n", "\n") -} - -private fun ProjectBuilderState.buildDependencies(slice: ProjectSlice, subprojects: Set): String { - if (dependencies.isEmpty() && subprojects.isEmpty()) { - return "" - } - - val configs: MutableList = mutableListOf(); - - configs += subprojects.map { - if (slice.language == KOTLIN) "implementation(project(\":$it\"))" else "implementation project(':$it')" - } - configs += dependencies.map { if (slice.language == KOTLIN) it.kotlin else it.groovy } - return configs.joinToString("\n", "\n", "\n") -} - - -private fun loadSettingsTemplate(slice: ProjectSlice): String { - return File("$SETTINGS_PATH/settings.${slice.scriptExtension}").readText() -} - -private fun loadScriptTemplate(root: Boolean, slice: ProjectSlice): String { - val filename = if (root) "root" else "subproject" - return File("${slice.scriptPath()}/$filename").readText() -} - -private fun loadTestTaskTemplate(slice: ProjectSlice): String { - return File("${slice.scriptPath()}/testTask").readText() -} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/CommonWriter.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/CommonWriter.kt new file mode 100644 index 00000000..0a946715 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/CommonWriter.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.api.* +import kotlinx.kover.test.functional.core.* +import java.io.PrintWriter + +internal fun PrintWriter.printClassFilter(classFilter: KoverClassFilter, slice: ProjectSlice, indents: Int) { + if (classFilter.excludes.isNotEmpty()) { + indented(indents, "excludes".addAllList(classFilter.excludes, slice.language)) + } + if (classFilter.includes.isNotEmpty()) { + indented(indents, "includes".addAllList(classFilter.includes, slice.language)) + } +} + +internal fun PrintWriter.printVerify(state: TestKoverVerifyConfigState, slice: ProjectSlice, indents: Int) { + val onCheck = state.onCheck + val rules = state.rules + if (onCheck == null && rules.isEmpty()) return + + indented(indents, "verify {") + if (onCheck != null) { + indented(indents + 1, "onCheck".setProperty(onCheck.toString(), slice.language)) + } + rules.forEach { rule -> + indented(indents + 1, "rule {") + if (rule.isEnabled != null) { + indented(indents + 2, "isEnabled = ${rule.isEnabled}") + } + if (rule.name != null) { + indented(indents + 2, "name = ${rule.name?.text(slice.language)}") + } + if (rule.target != null) { + indented(indents + 2, "target = ${rule.target?.enum(slice.language)}") + } + if (rule.overrideClassFilter != null) { + indented(indents + 2, "overrideClassFilter {") + printClassFilter(rule.overrideClassFilter!!, slice, indents + 3) + indented(indents + 2, "}") + } + rule.bounds.forEach { bound -> + indented(indents + 2, "bound {") + if (bound.minValue != null) { + indented(indents + 3, "minValue = ${bound.minValue}") + } + if (bound.maxValue != null) { + indented(indents + 3, "maxValue = ${bound.maxValue}") + } + if (bound.counter != null) { + indented(indents + 3, "counter = ${bound.counter?.enum(slice.language)}") + } + if (bound.valueType != null) { + indented(indents + 3, "valueType = ${bound.valueType?.enum(slice.language)}") + } + indented(indents + 2, "}") + } + indented(indents + 1, "}") + } + indented(indents, "}") +} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/KoverExtensionWriter.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/KoverExtensionWriter.kt new file mode 100644 index 00000000..8325dc99 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/KoverExtensionWriter.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.api.* +import kotlinx.kover.test.functional.core.* +import kotlinx.kover.test.functional.core.ProjectSlice +import kotlinx.kover.test.functional.core.TestKoverProjectConfigState +import java.io.* + +internal fun PrintWriter.printKover(kover: TestKoverProjectConfigState?, slice: ProjectSlice, indents: Int) { + if (kover == null) return + + indented(indents, "kover {") + printDisabled(kover.isDisabled, slice, indents + 1) + printEngine(kover.engine, slice, indents + 1) + printInstrumentation(kover.instrumentation, slice, indents + 1) + printFilters(kover.filters, slice, indents + 1) + printVerify(kover.verify, slice, indents + 1) + indented(indents, "}") +} + + +private fun PrintWriter.printEngine(scriptEngine: CoverageEngineVariant?, slice: ProjectSlice, indents: Int) { + if (scriptEngine == null && slice.engine == null) return + + val value = if (slice.engine != null) { + val clazz = + if (slice.engine == CoverageEngineVendor.INTELLIJ) DefaultIntellijEngine::class else DefaultJacocoEngine::class + clazz.obj(slice.language) + } else { + val clazz = + if (scriptEngine!!.vendor == CoverageEngineVendor.INTELLIJ) IntellijEngine::class else JacocoEngine::class + val new = if (slice.language == GradleScriptLanguage.KOTLIN) { + clazz.qualifiedName + } else { + clazz.qualifiedName + } + "$new(\"${scriptEngine.version}\")" + } + + indented(indents, "engine".setProperty(value, slice.language)) +} + +private fun PrintWriter.printDisabled(isDisabled: Boolean?, slice: ProjectSlice, indents: Int) { + if (isDisabled == null) return + + if (slice.language == GradleScriptLanguage.KOTLIN) { + indented(indents, "isDisabled.set($isDisabled)") + } else { + indented(indents, "disabled = $isDisabled") + } +} + +private fun PrintWriter.printFilters(state: TestKoverProjectFiltersState, slice: ProjectSlice, indents: Int) { + val classes = state.classes + val sourceSets = state.sourceSets + if (sourceSets == null && classes == null) return + + indented(indents, "filters {") + if (classes != null && (classes.excludes.isNotEmpty() || classes.includes.isNotEmpty())) { + indented(indents + 1, "classes {") + printClassFilter(classes, slice, indents + 2) + indented(indents + 1, "}") + } + + if (sourceSets != null) { + indented(indents + 1, "sourceSets {") + if (sourceSets.excludes.isNotEmpty()) { + indented(indents + 2, "excludes".addAllList(sourceSets.excludes, slice.language)) + } + indented(indents + 2, "excludeTests = " + sourceSets.excludeTests) + indented(indents + 1, "}") + } + + indented(indents, "}") +} + +private fun PrintWriter.printInstrumentation(state: KoverProjectInstrumentation, slice: ProjectSlice, indents: Int) { + if (state.excludeTasks.isEmpty()) return + + indented(indents, "instrumentation {") + indented(indents + 1, "excludeTasks".addAllList(state.excludeTasks, slice.language)) + indented(indents, "}") +} + diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Languages.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Languages.kt new file mode 100644 index 00000000..46efb89c --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Languages.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.test.functional.core.GradleScriptLanguage +import kotlinx.kover.test.functional.core.ProjectSlice +import java.io.PrintWriter +import kotlin.reflect.* + +internal val ProjectSlice.scriptExtension get() = if (language == GradleScriptLanguage.KOTLIN) "gradle.kts" else "gradle" + +internal fun Iterable.formatList(language: GradleScriptLanguage): String { + val prefix = if (language == GradleScriptLanguage.KOTLIN) "listOf(" else "[" + val postfix = if (language == GradleScriptLanguage.KOTLIN) ")" else "]" + + return prefix + this.joinToString(separator = ",") { "\"$it\"" } + postfix +} + +internal fun String.addAllList(list: Iterable, language: GradleScriptLanguage): String { + val listString = list.formatList(language) + return this + if (language == GradleScriptLanguage.KOTLIN) " += $listString" else ".addAll($listString)" +} + +internal fun KClass<*>.obj(language: GradleScriptLanguage): String { + return if (language == GradleScriptLanguage.KOTLIN) { + qualifiedName!! + } else { + "$qualifiedName.INSTANCE" + } +} + +internal fun String.setProperty(value: String, language: GradleScriptLanguage): String { + return this + if (language == GradleScriptLanguage.KOTLIN) ".set($value)" else " = $value" +} + +private fun indent(count: Int): String { + return when (count) { + 0 -> "" + 1 -> " " + 2 -> " " + 3 -> " " + 4 -> " " + 5 -> " " + else -> " ".repeat(count) + } +} + +@Suppress("UNUSED_PARAMETER") +internal fun String.text(language: GradleScriptLanguage): String { + return "\"$this\"" +} + +internal fun Enum<*>.enum(language: GradleScriptLanguage): String { + return if (language == GradleScriptLanguage.KOTLIN) { + this::class.qualifiedName + '.' + this.name + } else { + "\"$name\"" + } +} + +internal fun PrintWriter.indented(indents: Int, content: String) { + println(indent(indents) + content) +} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/MergedExtensionWriter.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/MergedExtensionWriter.kt new file mode 100644 index 00000000..f42a5a7f --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/MergedExtensionWriter.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.test.functional.core.* +import kotlinx.kover.test.functional.core.ProjectSlice +import java.io.* + +internal fun PrintWriter.printKoverMerged(merged: TestKoverMergedConfigState?, slice: ProjectSlice, indents: Int) { + if (merged == null) return + + indented(indents, "koverMerged {") + printEnabled(merged.enabled, indents + 1) + printFilters(merged.filters, slice, indents + 1) + indented(indents, "}") +} + +private fun PrintWriter.printEnabled(isEnabled: Boolean, indents: Int) { + if (isEnabled) { + indented(indents, "enable()") + } +} + +private fun PrintWriter.printFilters(state: TestKoverMergedFiltersState, slice: ProjectSlice, indents: Int) { + val classes = state.classes + val projects = state.projects + if (projects == null && classes == null) return + + indented(indents, "filters {") + if (classes != null && (classes.excludes.isNotEmpty() || classes.includes.isNotEmpty())) { + indented(indents + 1, "classes {") + printClassFilter(classes, slice, indents + 2) + indented(indents + 1, "}") + } + + if (projects != null) { + indented(indents + 1, "projects {") + if (projects.excludes.isNotEmpty()) { + indented(indents + 2, "excludes".addAllList(projects.excludes, slice.language)) + } + indented(indents + 1, "}") + } + + indented(indents, "}") +} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/SettingsFileWriter.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/SettingsFileWriter.kt new file mode 100644 index 00000000..5af72278 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/SettingsFileWriter.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.test.functional.core.* +import java.io.* + +internal fun generateSettingsFile( + sliceDir: File, + slice: ProjectSlice, + projects: Map, + localCache: Boolean +) { + File(sliceDir, "settings.${slice.scriptExtension}").printWriter().use { + it.println("""rootProject.name = "kover-functional-test"""") + it.println() + projects.keys.forEach { path -> + if (path != ":") { + it.println("""include("$path")""") + } + } + + if (localCache) { + it.println(LOCAL_CACHE) + } + } +} + +private const val LOCAL_CACHE = """ +buildCache { + local { + directory = "${"$"}settingsDir/build-cache" + } +} +""" diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Writer.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Writer.kt new file mode 100644 index 00000000..3d83cb30 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Writer.kt @@ -0,0 +1,290 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.test.functional.core.GradleScriptLanguage.GROOVY +import kotlinx.kover.test.functional.core.GradleScriptLanguage.KOTLIN +import kotlinx.kover.test.functional.core.PluginsState +import kotlinx.kover.test.functional.core.ProjectBuilderState +import kotlinx.kover.test.functional.core.ProjectSlice +import kotlinx.kover.test.functional.core.ProjectType +import kotlinx.kover.test.functional.core.RepositoriesState +import kotlinx.kover.test.functional.core.TestTaskConfigState +import java.io.* + +private const val SAMPLES_PATH = "src/functionalTest/templates" +private const val SAMPLES_SOURCES_PATH = "$SAMPLES_PATH/sources" + +internal fun initSlice( + rootDir: File, + slice: ProjectSlice, + projects: Map, + localCache: Boolean +): File { + val sliceDir = File(rootDir, slice.encodedString()).also { it.mkdirs() } + generateSettingsFile(sliceDir, slice, projects, localCache) + projects.forEach { (path, state) -> state.generateProject(sliceDir, path, slice) } + return sliceDir +} + +private fun ProjectBuilderState.generateProject(sliceDir: File, path: String, slice: ProjectSlice) { + val subpath = path.replace(':', '/') + val projectDir = File(sliceDir, subpath).also { it.mkdirs() } + copySources(projectDir, slice) + File(projectDir, "build.${slice.scriptExtension}").printWriter().use { + it.printPlugins(plugins, slice) + it.printRepositories(repositories) + it.printDependencies(subprojects, slice) + it.printKover(kover, slice, 0) + it.printKoverMerged(merged, slice, 0) + it.printTestTasks(testTasks, slice) + } +} + + +private fun PrintWriter.printPlugins(plugins: PluginsState, slice: ProjectSlice) { + if (!plugins.useKotlin && !plugins.useKover) return + + println("plugins {") + if (plugins.useKotlin) { + when { + slice.language == GROOVY && slice.type == ProjectType.KOTLIN_JVM -> print(""" id "org.jetbrains.kotlin.jvm"""") + slice.language == GROOVY && slice.type == ProjectType.KOTLIN_MULTIPLATFORM -> print(""" id "org.jetbrains.kotlin.multiplatform"""") + slice.language == KOTLIN && slice.type == ProjectType.KOTLIN_JVM -> print(""" kotlin("jvm")""") + slice.language == KOTLIN && slice.type == ProjectType.KOTLIN_MULTIPLATFORM -> print(""" kotlin("multiplatform")""") + else -> throw Exception("Unsupported test combination: language ${slice.language} and project type ${slice.type}") + } + plugins.kotlinVersion?.let { print(""" version "$it"""") } + println() + } + + if (plugins.useKover) { + if (slice.language == KOTLIN) { + print(""" id("org.jetbrains.kotlinx.kover")""") + } else { + print(""" id "org.jetbrains.kotlinx.kover"""") + } + plugins.koverVersion?.let { print(""" version "$it"""") } + println() + } + println("}") +} + +private fun PrintWriter.printRepositories(repositories: RepositoriesState) { + if (repositories.repositories.isEmpty()) return + + println("repositories {") + repositories.repositories.forEach { + print(" ") + println(it) + } + println("}") +} + +private fun PrintWriter.printDependencies(subprojects: List, slice: ProjectSlice) { + val subprojectsPart = subprojects.joinToString(separator = "\n") { + if (slice.language == KOTLIN) "implementation(project(\"$it\"))" else "implementation project('$it')" + } + + val template = when { + slice.language == GROOVY && slice.type == ProjectType.KOTLIN_JVM -> GROOVY_JVM_DEPS + slice.language == GROOVY && slice.type == ProjectType.KOTLIN_MULTIPLATFORM -> GROOVY_KMP_DEPS + slice.language == KOTLIN && slice.type == ProjectType.KOTLIN_JVM -> KOTLIN_JVM_DEPS + slice.language == KOTLIN && slice.type == ProjectType.KOTLIN_MULTIPLATFORM -> KOTLIN_KMP_DEPS + else -> throw Exception("Unsupported test combination: language ${slice.language} and project type ${slice.type}") + } + println(template.replace(DEPS_PLACEHOLDER, subprojectsPart)) +} + +private fun PrintWriter.printTestTasks(state: TestTaskConfigState, slice: ProjectSlice) { + if (state.excludes == null && state.includes == null) return + + val testTaskName = when { + slice.type == ProjectType.KOTLIN_JVM -> "test" + slice.type == ProjectType.KOTLIN_MULTIPLATFORM && slice.language == KOTLIN -> "named(\"jvmTest\").configure" + slice.type == ProjectType.KOTLIN_MULTIPLATFORM -> "jvmTest" + else -> throw Exception("Project with type ${slice.type} and language ${slice.language} not supported for test task configuring") + } + val extension = + if (slice.language == KOTLIN) "extensions.configure(kotlinx.kover.api.KoverTaskExtension::class)" else "kover" + + println("tasks.$testTaskName {") + println(" $extension {") + if (state.excludes != null) { + val excludesString = state.excludes!!.joinToString(separator = ",") { "\"$it\"" } + println(" excludes.addAll($excludesString)") + } + if (state.includes != null) { + val includesString = state.includes!!.joinToString(separator = ",") { "\"$it\"" } + println(" includes.addAll($includesString)") + } + println(" }") + println("}") +} + +private const val DEPS_PLACEHOLDER = "/*DEPS*/" + +private const val KOTLIN_JVM_DEPS = """ +dependencies { + $DEPS_PLACEHOLDER + testImplementation(kotlin("test")) +} +""" + +private const val KOTLIN_KMP_DEPS = """ +kotlin { + jvm() { + withJava() + } + dependencies { + commonTestImplementation(kotlin("test")) + } + sourceSets { + val jvmMain by getting { + dependencies { + $DEPS_PLACEHOLDER + } + } + } +} +""" + +private const val GROOVY_JVM_DEPS = """ +dependencies { + $DEPS_PLACEHOLDER + testImplementation 'org.jetbrains.kotlin:kotlin-test' +} +""" + +private const val GROOVY_KMP_DEPS = """ +kotlin { + jvm() { + withJava() + } + dependencies { + commonTestImplementation 'org.jetbrains.kotlin:kotlin-test' + } + sourceSets { + jvmMain { + dependencies { + $DEPS_PLACEHOLDER + } + } + } +} +""" + +private fun ProjectBuilderState.copySources(projectDir: File, slice: ProjectSlice) { + fun File.copyInto(targetFile: File) { + listFiles()?.forEach { src -> + val subTarget = File(targetFile, src.name) + if (src.isDirectory) { + subTarget.mkdirs() + src.copyInto(subTarget) + } else if (src.exists() && src.length() > 0) { + src.copyTo(subTarget) + } + } + } + + sourceTemplates.forEach { template -> + File(SAMPLES_SOURCES_PATH, "$template/main").copyInto(File(projectDir, slice.mainPath)) + File(SAMPLES_SOURCES_PATH, "$template/test").copyInto(File(projectDir, slice.testPath)) + } +} + +private val ProjectSlice.mainPath: String + get() { + return when (type) { + ProjectType.KOTLIN_JVM -> "src/main" + ProjectType.KOTLIN_MULTIPLATFORM -> "src/jvmMain" + ProjectType.ANDROID -> "src/jvmMain" + } + } + +private val ProjectSlice.testPath: String + get() { + return when (type) { + ProjectType.KOTLIN_JVM -> "src/test" + ProjectType.KOTLIN_MULTIPLATFORM -> "src/jvmTest" + ProjectType.ANDROID -> "src/jvmTest" + } + } + + +// +//private fun CommonBuilderState.buildRootExtension(slice: ProjectSlice): String { +// if (slice.engine == null && koverConfig.isDefault) { +// return "" +// } +// +// val builder = StringBuilder() +// builder.appendLine() +// builder.appendLine("kover {") +// +// if (koverConfig.disabled != null) { +// val property = if (slice.language == KOTLIN) "isDisabled" else "disabled" +// builder.appendLine("$property = ${koverConfig.disabled}") +// } +// +// if (slice.engine == CoverageEngineVendor.INTELLIJ) { +// builder.appendLine(" coverageEngine.set(kotlinx.kover.api.CoverageEngine.INTELLIJ)") +// if (koverConfig.intellijVersion != null) { +// builder.appendLine(""" intellijEngineVersion.set("${koverConfig.intellijVersion}")""") +// } +// } +// if (slice.engine == CoverageEngineVendor.JACOCO) { +// builder.appendLine(" coverageEngine.set(kotlinx.kover.api.CoverageEngine.JACOCO)") +// if (koverConfig.jacocoVersion != null) { +// builder.appendLine(""" jacocoEngineVersion.set("${koverConfig.jacocoVersion}")""") +// } +// } +// +// if (koverConfig.disabledProjects.isNotEmpty()) { +// val prefix = if (slice.language == KOTLIN) "setOf(" else "[" +// val postfix = if (slice.language == KOTLIN) ")" else "]" +// val value = koverConfig.disabledProjects.joinToString(prefix = prefix, postfix = postfix) { "\"$it\"" } +// builder.appendLine(" disabledProjects = $value") +// } +// +// if (koverConfig.runAllTestsForProjectTask != null) { +// builder.appendLine(" runAllTestsForProjectTask = ${koverConfig.runAllTestsForProjectTask}") +// } +// +// builder.appendLine("}") +// +// return builder.toString() +//} + +// +//@Suppress("UNUSED_PARAMETER") +//private fun ProjectBuilderState.buildVerifications(slice: ProjectSlice): String { +// if (rules.isEmpty()) { +// return "" +// } +// +// val builder = StringBuilder() +// builder.appendLine() +// builder.appendLine("tasks.koverVerify {") +// +// for (rule in rules) { +// builder.appendLine(" rule {") +// rule.name?.also { builder.appendLine(""" name = "$it"""") } +// for (bound in rule.bounds) { +// builder.appendLine(" bound {") +// bound.minValue?.let { builder.appendLine(" minValue = $it") } +// bound.maxValue?.let { builder.appendLine(" maxValue = $it") } +// if (bound.valueType != VerificationValueType.COVERED_PERCENTAGE) { +// builder.appendLine(" valueType = kotlinx.kover.api.VerificationValueType.${bound.valueType}") +// } +// builder.appendLine(" }") +// } +// builder.appendLine(" }") +// } +// builder.appendLine("}") +// +// return builder.toString() +//} + diff --git a/src/functionalTest/templates/samples/different-plugins/build.gradle.kts b/src/functionalTest/templates/samples/different-plugins/build.gradle.kts index 095535e8..4207547b 100644 --- a/src/functionalTest/templates/samples/different-plugins/build.gradle.kts +++ b/src/functionalTest/templates/samples/different-plugins/build.gradle.kts @@ -5,3 +5,5 @@ plugins { repositories { mavenCentral() } + +koverMerged.enable() diff --git a/src/functionalTest/templates/samples/different-plugins/subproject-multiplatform/build.gradle.kts b/src/functionalTest/templates/samples/different-plugins/subproject-multiplatform/build.gradle.kts index 15fecee9..2dc779a1 100644 --- a/src/functionalTest/templates/samples/different-plugins/subproject-multiplatform/build.gradle.kts +++ b/src/functionalTest/templates/samples/different-plugins/subproject-multiplatform/build.gradle.kts @@ -1,5 +1,6 @@ plugins { kotlin("multiplatform") + id("org.jetbrains.kotlinx.kover") } repositories { diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/root b/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/root deleted file mode 100644 index 8a66aba6..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/root +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.jvm' version '1.6.10' - id 'org.jetbrains.kotlinx.kover' version '//PLUGIN_VERSION' -} - -repositories { - mavenCentral()//REPOSITORIES -} - -dependencies {//DEPENDENCIES - testImplementation 'org.jetbrains.kotlin:kotlin-test' -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/subproject b/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/subproject deleted file mode 100644 index 9cc59b26..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/subproject +++ /dev/null @@ -1,12 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.jvm' -} - -repositories { - mavenCentral()//REPOSITORIES -} - -dependencies {//DEPENDENCIES - testImplementation 'org.jetbrains.kotlin:kotlin-test' -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/testTask b/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/testTask deleted file mode 100644 index a9d184d8..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/testTask +++ /dev/null @@ -1,5 +0,0 @@ -tasks.test { - kover { - //KOVER_TEST_CONFIG - } -} diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/root b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/root deleted file mode 100644 index 7c1727f6..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/root +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.multiplatform' version '1.6.10' - id 'org.jetbrains.kotlinx.kover' version '//PLUGIN_VERSION' -} - -repositories { - mavenCentral()//REPOSITORIES -} - -kotlin { - jvm() { - withJava() - } - - dependencies { - commonTestImplementation 'org.jetbrains.kotlin:kotlin-test' - } - - sourceSets { - jvmMain { - dependencies {//DEPENDENCIES - } - } - } -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/subproject b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/subproject deleted file mode 100644 index 3f710e4d..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/subproject +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.multiplatform' -} - -repositories { - mavenCentral()//REPOSITORIES -} - -kotlin { - jvm() { - withJava() - } - - dependencies { - commonTestImplementation 'org.jetbrains.kotlin:kotlin-test' - } - - sourceSets { - jvmMain { - dependencies {//DEPENDENCIES - } - } - } -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/testTask b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/testTask deleted file mode 100644 index 2ccf87c9..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/testTask +++ /dev/null @@ -1,5 +0,0 @@ -tasks.jvmTest { - kover { - //KOVER_TEST_CONFIG - } -} diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/root b/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/root deleted file mode 100644 index 2217fed6..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/root +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - kotlin("jvm") version "1.6.10" - id("org.jetbrains.kotlinx.kover") version "//PLUGIN_VERSION" -} - -repositories { - mavenCentral()//REPOSITORIES -} - -dependencies {//DEPENDENCIES - testImplementation(kotlin("test")) -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/subproject b/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/subproject deleted file mode 100644 index 3fa1382e..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/subproject +++ /dev/null @@ -1,12 +0,0 @@ -plugins { - kotlin("jvm") -} - -repositories { - mavenCentral()//REPOSITORIES -} - -dependencies {//DEPENDENCIES - testImplementation(kotlin("test")) -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/testTask b/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/testTask deleted file mode 100644 index 3a225fde..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/testTask +++ /dev/null @@ -1,5 +0,0 @@ -tasks.test { - extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) { - //KOVER_TEST_CONFIG - } -} diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/root b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/root deleted file mode 100644 index 410231c5..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/root +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - kotlin("multiplatform") version "1.6.10" - id("org.jetbrains.kotlinx.kover") version "//PLUGIN_VERSION" -} - -repositories { - mavenCentral()//REPOSITORIES -} - -kotlin { - jvm() { - withJava() - } - - dependencies { - commonTestImplementation(kotlin("test")) - } - - sourceSets { - val jvmMain by getting { - dependencies {//DEPENDENCIES - } - } - } -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/subproject b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/subproject deleted file mode 100644 index d826c33d..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/subproject +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - kotlin("multiplatform") -} - -repositories { - mavenCentral()//REPOSITORIES -} - -kotlin { - jvm() { - withJava() - } - - dependencies { - commonTestImplementation(kotlin("test")) - } - - sourceSets { - val jvmMain by getting { - dependencies {//DEPENDENCIES - } - } - } -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/testTask b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/testTask deleted file mode 100644 index 0c1f5175..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/testTask +++ /dev/null @@ -1,5 +0,0 @@ -tasks.named("jvmTest").configure { - extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) { - //KOVER_TEST_CONFIG - } -} diff --git a/src/functionalTest/templates/scripts/settings/settings.gradle b/src/functionalTest/templates/scripts/settings/settings.gradle deleted file mode 100644 index 368b66a0..00000000 --- a/src/functionalTest/templates/scripts/settings/settings.gradle +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } - mavenCentral() - maven { url 'https://plugins.gradle.org/m2/' } - } -} -rootProject.name = 'plugin-test-project-groovy' -//SUBPROJECTS//EXTRA_SETTINGS diff --git a/src/functionalTest/templates/scripts/settings/settings.gradle.kts b/src/functionalTest/templates/scripts/settings/settings.gradle.kts deleted file mode 100644 index 60f592ab..00000000 --- a/src/functionalTest/templates/scripts/settings/settings.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { - maven { url=uri("https://oss.sonatype.org/content/repositories/snapshots") } - mavenCentral() - maven { url=uri("https://plugins.gradle.org/m2/") } - } -} -rootProject.name = "plugin-test-project-kotlin" -//SUBPROJECTS//EXTRA_SETTINGS diff --git a/src/main/kotlin/kotlinx/kover/KoverPlugin.kt b/src/main/kotlin/kotlinx/kover/KoverPlugin.kt index 013bf80d..9aadc639 100644 --- a/src/main/kotlin/kotlinx/kover/KoverPlugin.kt +++ b/src/main/kotlin/kotlinx/kover/KoverPlugin.kt @@ -1,398 +1,15 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.kover -import kotlinx.kover.adapters.* -import kotlinx.kover.api.* -import kotlinx.kover.api.KoverPaths.MERGED_HTML_REPORT_DEFAULT_PATH -import kotlinx.kover.api.KoverNames.CHECK_TASK_NAME -import kotlinx.kover.api.KoverNames.COLLECT_REPORTS_TASK_NAME -import kotlinx.kover.api.KoverNames.MERGED_HTML_REPORT_TASK_NAME -import kotlinx.kover.api.KoverNames.HTML_REPORT_TASK_NAME -import kotlinx.kover.api.KoverNames.REPORT_TASK_NAME -import kotlinx.kover.api.KoverNames.VERIFY_TASK_NAME -import kotlinx.kover.api.KoverNames.MERGED_REPORT_TASK_NAME -import kotlinx.kover.api.KoverNames.XML_REPORT_TASK_NAME -import kotlinx.kover.api.KoverNames.ROOT_EXTENSION_NAME -import kotlinx.kover.api.KoverNames.TASK_EXTENSION_NAME -import kotlinx.kover.api.KoverNames.VERIFICATION_GROUP -import kotlinx.kover.api.KoverNames.MERGED_VERIFY_TASK_NAME -import kotlinx.kover.api.KoverNames.MERGED_XML_REPORT_TASK_NAME -import kotlinx.kover.api.KoverPaths.ALL_PROJECTS_REPORTS_DEFAULT_PATH -import kotlinx.kover.api.KoverPaths.PROJECT_HTML_REPORT_DEFAULT_PATH -import kotlinx.kover.api.KoverPaths.MERGED_XML_REPORT_DEFAULT_PATH -import kotlinx.kover.api.KoverPaths.PROJECT_XML_REPORT_DEFAULT_PATH -import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.CoverageAgent -import kotlinx.kover.engines.intellij.* -import kotlinx.kover.tasks.* +import kotlinx.kover.appliers.* import org.gradle.api.* -import org.gradle.api.provider.* -import org.gradle.api.tasks.* -import org.gradle.api.tasks.testing.* -import org.gradle.process.* -import java.io.File -import kotlin.reflect.* class KoverPlugin : Plugin { - private val defaultJacocoVersion = "0.8.7" - override fun apply(target: Project) { - val koverExtension = target.createKoverExtension() - val agents = AgentsFactory.createAgents(target, koverExtension) - - val providers = target.createProviders(agents) - - target.allprojects { - it.applyToProject(providers, agents) - } - target.createCollectingTask() - - target.createMergedTasks(providers) - } - - private fun Project.applyToProject(providers: BuildProviders, agents: Map) { - val projectProviders = - providers.projects[path] - ?: throw GradleException("Kover: Providers for project '$name' ('$path') was not found") - - val xmlReportTask = createKoverProjectTask( - XML_REPORT_TASK_NAME, - KoverXmlReportTask::class, - providers, - projectProviders - ) { - it.xmlReportFile.set(layout.buildDirectory.file(PROJECT_XML_REPORT_DEFAULT_PATH)) - it.description = "Generates code coverage XML report for all enabled test tasks in one project." - } - - val htmlReportTask = createKoverProjectTask( - HTML_REPORT_TASK_NAME, - KoverHtmlReportTask::class, - providers, - projectProviders - ) { - it.htmlReportDir.set(it.project.layout.buildDirectory.dir(PROJECT_HTML_REPORT_DEFAULT_PATH)) - it.description = "Generates code coverage HTML report for all enabled test tasks in one project." - } - - val verifyTask = createKoverProjectTask( - VERIFY_TASK_NAME, - KoverVerificationTask::class, - providers, - projectProviders - ) { - it.onlyIf { t -> (t as KoverVerificationTask).rules.isNotEmpty() } - it.description = "Verifies code coverage metrics of one project based on specified rules." - } - - tasks.create(REPORT_TASK_NAME) { - it.group = VERIFICATION_GROUP - it.dependsOn(xmlReportTask) - it.dependsOn(htmlReportTask) - it.description = "Generates code coverage HTML and XML reports for all enabled test tasks in one project." - } - - tasks.configureEach { - if (it.name == CHECK_TASK_NAME) { - it.dependsOn(verifyTask) - } - } - - tasks.withType(Test::class.java).configureEach { t -> - t.configTestTask(providers, agents) - } - } - - private fun Project.createMergedTasks(providers: BuildProviders) { - val xmlReportTask = createKoverMergedTask( - MERGED_XML_REPORT_TASK_NAME, - KoverMergedXmlReportTask::class, - providers - ) { - it.xmlReportFile.set(layout.buildDirectory.file(MERGED_XML_REPORT_DEFAULT_PATH)) - it.description = "Generates code coverage XML report for all enabled test tasks in all projects." - } - - val htmlReportTask = createKoverMergedTask( - MERGED_HTML_REPORT_TASK_NAME, - KoverMergedHtmlReportTask::class, - providers - ) { - it.htmlReportDir.set(layout.buildDirectory.dir(MERGED_HTML_REPORT_DEFAULT_PATH)) - it.description = "Generates code coverage HTML report for all enabled test tasks in all projects." - } - - val reportTask = tasks.create(MERGED_REPORT_TASK_NAME) { - it.group = VERIFICATION_GROUP - it.dependsOn(xmlReportTask) - it.dependsOn(htmlReportTask) - it.description = "Generates code coverage HTML and XML reports for all enabled test tasks in all projects." - } - - val verifyTask = createKoverMergedTask( - MERGED_VERIFY_TASK_NAME, - KoverMergedVerificationTask::class, - providers - ) { - it.onlyIf { t -> (t as KoverMergedVerificationTask).rules.isNotEmpty() } - it.description = "Verifies code coverage metrics of all projects based on specified rules." - } - - tasks.configureEach { - if (it.name == CHECK_TASK_NAME) { - it.dependsOn(provider { - val koverExtension = extensions.getByType(KoverExtension::class.java) - if (koverExtension.generateReportOnCheck) { - listOf(reportTask, verifyTask) - } else { - listOf(verifyTask) - } - }) - } - } - } - - - private fun Project.createKoverMergedTask( - taskName: String, - type: KClass, - providers: BuildProviders, - block: (T) -> Unit - ): T { - val task = tasks.create(taskName, type.java) - - task.group = VERIFICATION_GROUP - - providers.projects.forEach { (projectPath, m) -> - task.binaryReportFiles.put(projectPath, NestedFiles(task.project.objects, m.reports)) - task.srcDirs.put(projectPath, NestedFiles(task.project.objects, m.sources)) - task.outputDirs.put(projectPath, NestedFiles(task.project.objects, m.output)) - } - - task.coverageEngine.set(providers.engine) - task.classpath.set(providers.classpath) - task.dependsOn(providers.merged.tests) - - val disabledProvider = providers.merged.disabled - task.onlyIf { !disabledProvider.get() } - - block(task) - return task - } - - private fun Project.createCollectingTask() { - tasks.create(COLLECT_REPORTS_TASK_NAME, KoverCollectingTask::class.java) { task -> - task.group = VERIFICATION_GROUP - task.description = "Collects all projects reports into one directory." - task.outputDir.set(project.layout.buildDirectory.dir(ALL_PROJECTS_REPORTS_DEFAULT_PATH)) - // disable UP-TO-DATE check for task: it will be executed every time - task.outputs.upToDateWhen { false } - - allprojects { proj -> - val xmlReportTask = - proj.tasks.withType(KoverXmlReportTask::class.java).getByName(XML_REPORT_TASK_NAME) - val htmlReportTask = - proj.tasks.withType(KoverHtmlReportTask::class.java).getByName(HTML_REPORT_TASK_NAME) - - task.mustRunAfter(xmlReportTask) - task.mustRunAfter(htmlReportTask) - - task.xmlFiles[proj.path] = xmlReportTask.xmlReportFile - task.htmlDirs[proj.path] = htmlReportTask.htmlReportDir - } - } - } - - - private fun Project.createKoverProjectTask( - taskName: String, - type: KClass, - providers: BuildProviders, - projectProviders: ProjectProviders, - block: (T) -> Unit - ): T { - tasks.findByName(taskName)?.let { - throw GradleException("Kover task '$taskName' already exist. Plugin should not be applied in child project if it has already been applied in one of the parent projects.") - } - - val task = tasks.create(taskName, type.java) - task.group = VERIFICATION_GROUP - - task.coverageEngine.set(providers.engine) - task.classpath.set(providers.classpath) - task.srcDirs.set(projectProviders.sources) - task.outputDirs.set(projectProviders.output) - - // it is necessary to read all binary reports because project's classes can be invoked in another project - task.binaryReportFiles.set(projectProviders.reports) - task.dependsOn(projectProviders.tests) - - val disabledProvider = projectProviders.disabled - task.onlyIf { !disabledProvider.get() } - task.onlyIf { !(it as KoverProjectTask).binaryReportFiles.get().isEmpty } - - block(task) - - return task - } - - private fun Project.createKoverExtension(): KoverExtension { - val extension = extensions.create(ROOT_EXTENSION_NAME, KoverExtension::class.java, objects) - extension.isDisabled = false - extension.coverageEngine.set(CoverageEngine.INTELLIJ) - extension.intellijEngineVersion.set(defaultIntellijVersion.toString()) - extension.jacocoEngineVersion.set(defaultJacocoVersion) - - afterEvaluate(CollectDisabledProjectsPathsAction(extension)) - - return extension - } - - private fun Test.configTestTask( - providers: BuildProviders, - agents: Map - ) { - val taskExtension = extensions.create(TASK_EXTENSION_NAME, KoverTaskExtension::class.java, project.objects) - - taskExtension.isDisabled = false - taskExtension.binaryReportFile.set(project.provider { - val koverExtension = providers.koverExtension.get() - val suffix = if (koverExtension.coverageEngine.get() == CoverageEngine.INTELLIJ) ".ic" else ".exec" - project.layout.buildDirectory.get().file("kover/$name$suffix").asFile - }) - - val pluginContainer = project.plugins - val excludeAndroidPackages = - project.provider { pluginContainer.androidPluginIsApplied && !providers.koverExtension.get().instrumentAndroidPackage } - - jvmArgumentProviders.add( - CoverageArgumentProvider( - this, - agents, - taskExtension, - providers.koverExtension, - excludeAndroidPackages - ) - ) - - val sourceErrorProvider = project.provider { - File(taskExtension.binaryReportFile.get().parentFile, "coverage-error.log") - } - val targetErrorProvider = project.layout.buildDirectory.file("kover/errors/$name.log").map { it.asFile } - - doFirst(BinaryReportCleanupAction(project.path, providers.koverExtension, taskExtension)) - doLast(MoveIntellijErrorLogAction(sourceErrorProvider, targetErrorProvider)) - } -} - -/* - To support parallel tests, both Coverage Engines work in append to data file mode. - For this reason, before starting the tests, it is necessary to clear the file from the results of previous runs. -*/ -private class BinaryReportCleanupAction( - private val projectPath: String, - private val koverExtensionProvider: Provider, - private val taskExtension: KoverTaskExtension -) : Action { - override fun execute(task: Task) { - val koverExtension = koverExtensionProvider.get() - val file = taskExtension.binaryReportFile.get() - - // always delete previous data file - file.delete() - - if (!taskExtension.isDisabled - && !koverExtension.isDisabled - && !koverExtension.disabledProjectsPaths.contains(projectPath) - && koverExtension.coverageEngine.get() == CoverageEngine.INTELLIJ - ) { - // IntelliJ engine expected empty file for parallel test execution. - // Since it is impossible to know in advance whether the tests will be run in parallel, we always create an empty file. - file.createNewFile() - } - } -} - -private class CollectDisabledProjectsPathsAction( - private val koverExtension: KoverExtension, -) : Action { - override fun execute(project: Project) { - val allProjects = project.allprojects - val paths = allProjects.associate { it.name to mutableListOf() } - allProjects.forEach { paths.getValue(it.name) += it.path } - - val result: MutableSet = mutableSetOf() - - koverExtension.disabledProjects.map { - if (it.startsWith(':')) { - result += it - } else { - val projectPaths = paths[it] ?: return@map - if (projectPaths.size > 1) { - throw GradleException("Kover configuring error: ambiguous name of the excluded project '$it': suitable projects with paths $projectPaths. Consider using fully-qualified name starting with ':'") - } - result += projectPaths - } - } - - koverExtension.disabledProjectsPaths = result - } -} - -private class MoveIntellijErrorLogAction( - private val sourceFile: Provider, - private val targetFile: Provider -) : Action { - override fun execute(task: Task) { - val origin = sourceFile.get() - if (origin.exists() && origin.isFile) { - origin.copyTo(targetFile.get(), true) - origin.delete() - } - } -} - -private class CoverageArgumentProvider( - private val task: Task, - private val agents: Map, - @get:Nested val taskExtension: KoverTaskExtension, - @get:Nested val koverExtension: Provider, - @get:Input val excludeAndroidPackage: Provider -) : CommandLineArgumentProvider, Named { - - private val projectPath: String = task.project.path - - @Internal - override fun getName(): String { - return "koverArgumentsProvider" - } - - override fun asArguments(): MutableIterable { - val koverExtensionValue = koverExtension.get() - - if (taskExtension.isDisabled - || koverExtensionValue.isDisabled - || koverExtensionValue.disabledProjectsPaths.contains(projectPath) - ) { - return mutableListOf() - } - - if (excludeAndroidPackage.get()) { - /* - The instrumentation of android classes often causes errors when using third-party - frameworks (see https://github.com/Kotlin/kotlinx-kover/issues/89). - - Because android classes are not part of the project, in any case they do not get into the report, - and they can be excluded from instrumentation. - - FIXME Remove this code if the IntelliJ Agent stops changing project classes during instrumentation - */ - taskExtension.excludes = taskExtension.excludes + "android.*" + "com.android.*" - } - - return agents.getFor(koverExtensionValue.coverageEngine.get()).buildCommandLineArgs(task, taskExtension) + target.applyToProject() + target.applyMerged() } } diff --git a/src/main/kotlin/kotlinx/kover/Providers.kt b/src/main/kotlin/kotlinx/kover/Providers.kt deleted file mode 100644 index 6b33a3b7..00000000 --- a/src/main/kotlin/kotlinx/kover/Providers.kt +++ /dev/null @@ -1,135 +0,0 @@ -package kotlinx.kover - -import kotlinx.kover.adapters.* -import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.CoverageAgent -import org.gradle.api.* -import org.gradle.api.file.* -import org.gradle.api.provider.* -import org.gradle.api.tasks.testing.* -import java.io.* - - -internal fun Project.createProviders(agents: Map): BuildProviders { - val projects: MutableMap = mutableMapOf() - - allprojects { - projects[it.path] = ProjectProviders( - it.provider { it.files(if (runAllTests()) allBinaryReports() else it.binaryReports(this)) }, - it.provider { if (runAllTests()) allTestTasks() else it.testTasks(this) }, - it.provider { it.collectDirs(this).first }, - it.provider { it.collectDirs(this).second }, - it.provider { it.isDisabled(this) } - ) - } - - val engineProvider = provider { extensions.getByType(KoverExtension::class.java).coverageEngine.get() } - - val classpathProvider: Provider = provider { - val koverExtension = extensions.getByType(KoverExtension::class.java) - agents.getFor(koverExtension.coverageEngine.get()).classpath - } - - val extensionProvider = provider { extensions.getByType(KoverExtension::class.java) } - - - val allReportsProvider: Provider = provider { files(allBinaryReports()) } - val allTestsProvider = provider { allTestTasks() } - val koverDisabledProvider = provider { extensions.getByType(KoverExtension::class.java).isDisabled } - - - // all sources and all outputs providers are unused, so NOW it can return empty file collection - val emptyProvider: Provider = provider { files() } - val mergedProviders = - ProjectProviders( - allReportsProvider, - allTestsProvider, - emptyProvider, - emptyProvider, - koverDisabledProvider) - - return BuildProviders(projects, mergedProviders, engineProvider, classpathProvider, extensionProvider) -} - - -internal fun Project.allTestTasks(): List { - return allprojects.flatMap { it.testTasks(this) } -} - -internal fun Project.allBinaryReports(): List { - return allprojects.flatMap { it.binaryReports(this) } -} - - -internal fun Project.testTasks(rootProject: Project): List { - if (isDisabled(rootProject)) { - return emptyList() - } - - return tasks.withType(Test::class.java) - .filterNot { t -> t.extensions.getByType(KoverTaskExtension::class.java).isDisabled } -} - -internal fun Project.binaryReports(root: Project): List { - if (isDisabled(root)) { - return emptyList() - } - - return tasks.withType(Test::class.java).asSequence() - .map { t -> t.extensions.getByType(KoverTaskExtension::class.java) } - // process binary report only from tasks with enabled cover - .filterNot { e -> e.isDisabled } - .map { e -> e.binaryReportFile.get() } - // process binary report only from tasks with sources - .filter { f -> f.exists() } - .toList() -} - -private fun Project.collectDirs(root: Project): Pair { - if (isDisabled(root)) { - return files() to files() - } - - val srcDirs = HashMap() - val outDirs = HashMap() - - createAdapters().forEach { - val dirs = it.findDirs(this) - srcDirs += dirs.sources.asSequence().map { f -> f.canonicalPath to f } - outDirs += dirs.output.asSequence().map { f -> f.canonicalPath to f } - } - - val src = srcDirs.asSequence().map { it.value }.filter { it.exists() && it.isDirectory }.toList() - val out = outDirs.asSequence().map { it.value }.filter { it.exists() && it.isDirectory }.toList() - - return files(src) to files(out) -} - -private fun Project.isDisabled(root: Project): Boolean { - val koverExtension = root.extensions.getByType(KoverExtension::class.java) - return koverExtension.isDisabled || koverExtension.disabledProjectsPaths.contains(path) -} - -private fun Project.runAllTests(): Boolean { - return extensions.getByType(KoverExtension::class.java).runAllTestsForProjectTask -} - - -internal class BuildProviders( - val projects: Map, - val merged: ProjectProviders, - - val engine: Provider, - val classpath: Provider, - val koverExtension: Provider -) - -internal class ProjectProviders( - val reports: Provider, - val tests: Provider>, - val sources: Provider, - val output: Provider, - val disabled: Provider -) - diff --git a/src/main/kotlin/kotlinx/kover/adapters/AndroidPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/adapters/AndroidPluginAdapter.kt deleted file mode 100644 index d9228841..00000000 --- a/src/main/kotlin/kotlinx/kover/adapters/AndroidPluginAdapter.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.adapters - -import com.android.build.gradle.* -import kotlinx.kover.adapters.api.* -import org.gradle.api.* - -class AndroidPluginAdapter : CompilationPluginAdapter { - - override fun findDirs(project: Project): PluginDirs { - return safe(project) { - this.plugins.findPlugin("android") ?: return@safe PluginDirs(emptyList(), emptyList()) - - val extension = project.extensions.findByType(BaseExtension::class.java) ?: return@safe PluginDirs( - emptyList(), - emptyList() - ) - - val sourceDirs = extension.sourceSets.asSequence() - .filter { !it.name.startsWith("test") && !it.name.startsWith("androidTest") } - .map { it.java }.toList() - .flatMap { it.srcDirs } - - - - return@safe PluginDirs(sourceDirs, emptyList()) - } - } - -} diff --git a/src/main/kotlin/kotlinx/kover/adapters/KotlinAndroidPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/adapters/KotlinAndroidPluginAdapter.kt deleted file mode 100644 index 56aa6648..00000000 --- a/src/main/kotlin/kotlinx/kover/adapters/KotlinAndroidPluginAdapter.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.adapters - -import kotlinx.kover.adapters.api.* -import org.gradle.api.* -import org.jetbrains.kotlin.gradle.dsl.* - -class KotlinAndroidPluginAdapter : CompilationPluginAdapter { - - override fun findDirs(project: Project): PluginDirs { - return safe(project) { - this.plugins.findPlugin("kotlin-android") ?: return@safe PluginDirs(emptyList(), emptyList()) - - val extension = project.extensions.findByType(KotlinAndroidProjectExtension::class.java) ?: return@safe PluginDirs( - emptyList(), - emptyList() - ) - - - val sourceDirs = extension.target.compilations - .filter { !it.name.endsWith("Test") } - .flatMap { it.allKotlinSourceSets } - .map { it.kotlin } - .flatMap { it.srcDirs } - - val outputDirs = extension.target.compilations - .filter { !it.name.endsWith("Test") } - .flatMap { it.output.classesDirs } - - return@safe PluginDirs(sourceDirs, outputDirs) - } - } - -} diff --git a/src/main/kotlin/kotlinx/kover/adapters/OldJavaPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/adapters/OldJavaPluginAdapter.kt deleted file mode 100644 index 9640083c..00000000 --- a/src/main/kotlin/kotlinx/kover/adapters/OldJavaPluginAdapter.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.adapters - -import kotlinx.kover.adapters.api.* -import org.gradle.api.* -import org.gradle.api.tasks.* - -class OldJavaPluginAdapter : CompilationPluginAdapter { - - override fun findDirs(project: Project): PluginDirs { - return safe(project) { - this.plugins.findPlugin("java") ?: return@safe PluginDirs(emptyList(), emptyList()) - - val sourceSetContainer = project.extensions.findByType( - SourceSetContainer::class.java - ) ?: return@safe PluginDirs(emptyList(), emptyList()) - - val sourceSets = sourceSetContainer.filter { it.name != SourceSet.TEST_SOURCE_SET_NAME } - - val sourceDirs = sourceSets.flatMap { it.allSource.srcDirs } - val outputDirs = sourceSets.flatMap { it.output.classesDirs } - - return@safe PluginDirs(sourceDirs, outputDirs) - } - } - -} diff --git a/src/main/kotlin/kotlinx/kover/adapters/PluginAdaptersFactory.kt b/src/main/kotlin/kotlinx/kover/adapters/PluginAdaptersFactory.kt deleted file mode 100644 index ec4d2ca6..00000000 --- a/src/main/kotlin/kotlinx/kover/adapters/PluginAdaptersFactory.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.adapters - -import kotlinx.kover.adapters.api.* -import org.gradle.api.plugins.PluginContainer - -internal fun createAdapters(): List { - return listOf( - OldJavaPluginAdapter(), - KotlinMultiplatformPluginAdapter(), - AndroidPluginAdapter(), - KotlinAndroidPluginAdapter() - ) -} - -val PluginContainer.androidPluginIsApplied: Boolean - get() { - return findPlugin("android") != null || findPlugin("kotlin-android") != null - } diff --git a/src/main/kotlin/kotlinx/kover/adapters/api/CompilationPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/adapters/api/CompilationPluginAdapter.kt deleted file mode 100644 index 9555412e..00000000 --- a/src/main/kotlin/kotlinx/kover/adapters/api/CompilationPluginAdapter.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.adapters.api - -import org.gradle.api.* -import java.io.* - -interface CompilationPluginAdapter { - fun findDirs(project: Project): PluginDirs -} - - -data class PluginDirs(val sources: List, val output: List) - -internal inline fun safe(project: Project, block: Project.() -> PluginDirs): PluginDirs { - return try { - project.block() - } catch (e: Throwable) { - when (e) { - is NoSuchMethodError, is NoSuchFieldError, is ClassNotFoundException, is NoClassDefFoundError -> { - project.logger.info("Problem occurred in Kover source set adapter", e) - PluginDirs(emptyList(), emptyList()) - } - else -> throw e - } - } -} diff --git a/src/main/kotlin/kotlinx/kover/api/CoverageEngines.kt b/src/main/kotlin/kotlinx/kover/api/CoverageEngines.kt new file mode 100644 index 00000000..7058dd52 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/api/CoverageEngines.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.api + +import kotlinx.kover.api.KoverVersions.DEFAULT_INTELLIJ_VERSION +import kotlinx.kover.api.KoverVersions.DEFAULT_JACOCO_VERSION +import org.gradle.api.tasks.* + +public sealed class CoverageEngineVariant( + @get:Input + internal val vendor: CoverageEngineVendor, + @get:Input + internal val version: String +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CoverageEngineVariant + + if (vendor != other.vendor) return false + if (version != other.version) return false + + return true + } + + override fun hashCode(): Int { + var result = vendor.hashCode() + result = 31 * result + version.hashCode() + return result + } + + override fun toString(): String { + return "$vendor Coverage Engine $version" + } +} + +internal enum class CoverageEngineVendor { + INTELLIJ, + JACOCO +} + +// TODO make internal in 0.7 version +@Deprecated( + message = "Class was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.WARNING +) +public enum class CoverageEngine { + INTELLIJ, + JACOCO +} + +/** + * Coverage Engine by IntelliJ. + */ +public class IntellijEngine(version: String): CoverageEngineVariant(CoverageEngineVendor.INTELLIJ, version) + +/** + * IntelliJ Coverage Engine with default version [DEFAULT_INTELLIJ_VERSION]. + */ +public object DefaultIntellijEngine: CoverageEngineVariant(CoverageEngineVendor.INTELLIJ, DEFAULT_INTELLIJ_VERSION) + +/** + * Coverage Engine by [JaCoCo](https://www.jacoco.org/jacoco/). + */ +public class JacocoEngine(version: String): CoverageEngineVariant(CoverageEngineVendor.JACOCO, version) + +/** + * JaCoCo Coverage Engine with default version [DEFAULT_JACOCO_VERSION]. + */ +public object DefaultJacocoEngine: CoverageEngineVariant(CoverageEngineVendor.JACOCO, DEFAULT_JACOCO_VERSION) diff --git a/src/main/kotlin/kotlinx/kover/api/KoverConfig.kt b/src/main/kotlin/kotlinx/kover/api/KoverConfig.kt new file mode 100644 index 00000000..ba6351b5 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/api/KoverConfig.kt @@ -0,0 +1,550 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.api + +import org.gradle.api.* +import org.gradle.api.file.* +import org.gradle.api.model.* +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import javax.annotation.* +import javax.inject.* + +public open class KoverProjectConfig @Inject constructor(objects: ObjectFactory) { + internal val filters: KoverProjectFilters = objects.newInstance(KoverProjectFilters::class.java, objects) + + internal val instrumentation: KoverProjectInstrumentation = + objects.newInstance(KoverProjectInstrumentation::class.java) + + internal val xmlReport: KoverProjectXmlConfig = objects.newInstance(KoverProjectXmlConfig::class.java, objects) + + internal val htmlReport: KoverProjectHtmlConfig = objects.newInstance(KoverProjectHtmlConfig::class.java, objects) + + internal val verify: KoverVerifyConfig = objects.newInstance(KoverVerifyConfig::class.java, objects) + + /** + * Specifies whether instrumentation is disabled for all test tasks of current project. + * + * `false` by default. + */ + public val isDisabled: Property = objects.property(Boolean::class.java) + + /** + * Specifies the coverage engine variant to be used to collect execution data. + */ + public val engine: Property = objects.property(CoverageEngineVariant::class.java) + + /** + * Configures filtering for all Kover's tasks of current project by class names and source sets. + */ + public fun filters(config: Action) { + config.execute(filters) + } + + /** + * Configures a list of tasks, the execution of tests from which is registered in the coverage counters. + * + */ + public fun instrumentation(config: Action) { + config.execute(instrumentation) + } + + /** + * Configures the task of generating an XML report, including XML report location and whether it should be + * generated during the 'check' task. + * + * By default, [KoverPaths.PROJECT_XML_REPORT_DEFAULT_PATH] location in build directory is used. + */ + public fun xmlReport(config: Action) { + config.execute(xmlReport) + } + + /** + * Configures the task of generating an HTML report, including HTML report location and whether it should be + * generated during the 'check' task. + * + * By default, [KoverPaths.PROJECT_HTML_REPORT_DEFAULT_PATH] location in build directory is used. + */ + public fun htmlReport(config: Action) { + config.execute(htmlReport) + } + + /** + * Configures the verification task, including adding verification rules and whether it should be + * verified during the 'check' task. + */ + public fun verify(config: Action) { + config.execute(verify) + } + + + // DEPRECATIONS + // TODO delete in 0.7 version + @Suppress("DEPRECATION") + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2, use `engine` property instead. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + replaceWith = ReplaceWith("engine"), + level = DeprecationLevel.ERROR + ) + public val coverageEngine: Property = objects.property(CoverageEngine::class.java) + + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2, use `engine.set(kotlinx.kover.api.IntellijEngine(\"version\"))` instead. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + replaceWith = ReplaceWith("engine"), + level = DeprecationLevel.ERROR + ) + public val intellijEngineVersion: Property = objects.property(String::class.java) + + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2, use `engine.set(kotlinx.kover.api.JacocoEngine(\"version\"))` instead. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + replaceWith = ReplaceWith("engine"), + level = DeprecationLevel.ERROR + ) + public val jacocoEngineVersion: Property = objects.property(String::class.java) + + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + public var generateReportOnCheck: Boolean = true + + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + public var disabledProjects: Set = emptySet() + + @Deprecated( + message = "Property was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + @get:Internal + public var instrumentAndroidPackage: Boolean = false + + @Deprecated( + message = "Property was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + @get:Internal + public var runAllTestsForProjectTask: Boolean = false +} + +// DEPRECATIONS +// TODO delete in 0.7 version +@Deprecated( + message = "Class KoverExtension was renamed to KoverProjectConfig in Kover API version 2", + replaceWith = ReplaceWith("KoverProjectConfig"), + level = DeprecationLevel.ERROR +) +public open class KoverExtension + +public open class KoverProjectFilters @Inject constructor(private val objects: ObjectFactory) { + internal val classes: Property = objects.property(KoverClassFilter::class.java) + + internal val sourceSets: Property = objects.property(KoverSourceSetFilter::class.java) + + /** + * Configures class filter in order to include and exclude specific classes. + * + * Example: + * ``` + * classes { + * excludes += "com.example.FooBar" + * includes += "com.example.*Bar" + * } + * ``` + * Excludes have priority over includes. + */ + public fun classes(config: Action) { + val classFilter = objects.newInstance(KoverClassFilter::class.java) + config.execute(classFilter) + classes.set(classFilter) + } + + /** + * Configures source set filter. + */ + public fun sourceSets(config: Action) { + val sourceSetFilters = objects.newInstance(KoverSourceSetFilter::class.java) + config.execute(sourceSetFilters) + sourceSets.set(sourceSetFilters) + } +} + +public open class KoverProjectInstrumentation { + /** + * Specifies the names of test tasks for which instrumentation will be disabled. + */ + public val excludeTasks: MutableSet = mutableSetOf() +} + +public open class KoverProjectXmlConfig @Inject constructor(objects: ObjectFactory) { + internal val filters: KoverProjectFilters = objects.newInstance(KoverProjectFilters::class.java, objects) + + /** + * Specifies whether the XML report generation task should be executed before the `check` task. + * + * `false` by default. + */ + public val onCheck: Property = objects.property(Boolean::class.java) + + /** + * Specifies file path of generated XML report file with coverage data. + * + * By default, is a value of [KoverPaths.PROJECT_XML_REPORT_DEFAULT_PATH] in the build directory. + */ + public val reportFile: RegularFileProperty = objects.fileProperty() + + /** + * Override filters for the XML report generation task. + * Only the explicitly specified filters will be overridden, the rest will be inherited from the common filters (see [KoverProjectConfig.filters]). + */ + public fun overrideFilters(config: Action) { + config.execute(filters) + } +} + +public open class KoverProjectHtmlConfig @Inject constructor(private val objects: ObjectFactory) { + internal val taskFilters: KoverProjectFilters = objects.newInstance(KoverProjectFilters::class.java, objects) + + /** + * Specifies whether the HTML report generation task should be executed before the `check` task. + * + * `false` by default. + */ + public val onCheck: Property = objects.property(Boolean::class.java) + + /** + * Specifies directory path of generated HTML report. + * + * By default, is a value of [KoverPaths.PROJECT_HTML_REPORT_DEFAULT_PATH] in the build directory. + */ + public val reportDir: DirectoryProperty = objects.directoryProperty() + + /** + * Override filters for the HTML report generation task. + * Only the explicitly specified filters will be overrided, the rest will be inherited from the common filters (see [KoverProjectConfig.filters]). + */ + public fun overrideFilters(config: Action) { + config.execute(taskFilters) + } +} + + +public open class KoverMergedConfig @Inject constructor(objects: ObjectFactory) { + internal var isEnabled: Property = objects.property(Boolean::class.java) + internal val filters: KoverMergedFilters = objects.newInstance(KoverMergedFilters::class.java, objects) + internal val xmlReport: KoverMergedXmlConfig = objects.newInstance(KoverMergedXmlConfig::class.java, objects) + internal val htmlReport: KoverMergedHtmlConfig = objects.newInstance(KoverMergedHtmlConfig::class.java, objects) + internal val verify: KoverVerifyConfig = objects.newInstance(KoverVerifyConfig::class.java, objects) + + /** + * Create Kover tasks for generating merged reports. + */ + public fun enable() { + isEnabled.set(true) + } + + /** + * Configures filters for all Kover merged tasks in current project. + */ + public fun filters(config: Action) { + config.execute(filters) + } + + /** + * Configures the task of generating a merged XML report. + */ + public fun xmlReport(config: Action) { + config.execute(xmlReport) + } + + /** + * Configures the task of generating a merged HTML report. + */ + public fun htmlReport(config: Action) { + config.execute(htmlReport) + } + + /** + * Configures the merged verification task. + */ + public fun verify(config: Action) { + config.execute(verify) + } +} + +public open class KoverMergedFilters @Inject constructor(private val objects: ObjectFactory) { + internal val classes: Property = objects.property(KoverClassFilter::class.java) + + internal val projects: Property = objects.property(KoverProjectsFilter::class.java) + + /** + * Configures class filter. + */ + public fun classes(config: Action) { + val classFilter = objects.newInstance(KoverClassFilter::class.java) + config.execute(classFilter) + classes.set(classFilter) + } + + /** + * Configures projects filter. + */ + public fun projects(config: Action) { + val projectsFilters = objects.newInstance(KoverProjectsFilter::class.java) + config.execute(projectsFilters) + projects.set(projectsFilters) + } +} + + +public open class KoverMergedXmlConfig @Inject constructor(private val objects: ObjectFactory) { + internal val classFilter: Property = objects.property(KoverClassFilter::class.java) + + /** + * Specifies whether the merged XML report generation task should be executed before the `check` task. + */ + public val onCheck: Property = objects.property(Boolean::class.java) + + /** + * Specifies file path of generated XML report file with coverage data. + */ + public val reportFile: RegularFileProperty = objects.fileProperty() + + /** + * Override class filter for the merged XML report generation task. + */ + public fun overrideClassFilter(config: Action) { + val newClassFilter = objects.newInstance(KoverClassFilter::class.java) + config.execute(newClassFilter) + classFilter.set(newClassFilter) + } +} + +public open class KoverMergedHtmlConfig @Inject constructor(private val objects: ObjectFactory) { + internal val classFilter: Property = objects.property(KoverClassFilter::class.java) + + /** + * Specifies whether the merged HTML report generation task should be executed before the `check` task. + */ + public val onCheck: Property = objects.property(Boolean::class.java) + + /** + * Specifies directory path of generated HTML report. + */ + public val reportDir: DirectoryProperty = objects.directoryProperty() + + /** + * Override class filter for the merged HTML report generation task. + */ + public fun overrideClassFilter(config: Action) { + val newClassFilter = objects.newInstance(KoverClassFilter::class.java) + config.execute(newClassFilter) + classFilter.set(newClassFilter) + } +} + +public open class KoverProjectsFilter { + /** + * Specifies the projects excluded from subprojects (included current project) using in the merged tasks. Both the project name (if it is unique) and the project path can be used. + * + * If empty, the current project and all subprojects are used. + */ + @get:Input + public val excludes: MutableList = mutableListOf() +} + + +public open class KoverVerifyConfig @Inject constructor(private val objects: ObjectFactory) { + internal val rules: ListProperty = objects.listProperty(VerificationRule::class.java) + + /** + * Specifies whether the verification task should be executed before the `check` task. + * + * By default, `true` for project reports and `false` for merged. + */ + public val onCheck: Property = objects.property(Boolean::class.java).value(true) + + /** + * Add new coverage verification rule to check after test task execution. + */ + public fun rule(configureRule: Action) { + rules.add(objects.newInstance(VerificationRule::class.java, objects).also { configureRule.execute(it) }) + } +} + +public open class KoverClassFilter { + /** + * Specifies class inclusion rules into report. + * Only the specified classes may be present in the report. + * Exclusion rules have priority over inclusion ones. + * + * Inclusion rules are represented as a set of fully-qualified names of the classes being instrumented. + * It's possible to use `*` and `?` wildcards. + */ + @get:Input + public val includes: MutableList = mutableListOf() + + /** + * Specifies class exclusion rules into report. + * The specified classes will definitely be missing from report. + * Exclusion rules have priority over inclusion ones. + * + * Exclusion rules are represented as a set of fully-qualified names of the classes being instrumented. + * It's possible to use `*` and `?` wildcards. + */ + @get:Input + public val excludes: MutableList = mutableListOf() +} + +public open class KoverSourceSetFilter { + /** + * Not implemented in beta version. + */ + @get:Input + public val excludes: MutableSet = mutableSetOf() + + /** + * Not implemented in beta version. + */ + @get:Input + public var excludeTests: Boolean = true +} + + +public open class VerificationRule @Inject constructor(private val objects: ObjectFactory) { + @get:Nested + @get:Optional + internal val classFilter: Property = objects.property(KoverClassFilter::class.java) + + @get:Nested + internal val bounds: ListProperty = objects.listProperty(VerificationBound::class.java) + + /** + * Specifies that the rule will be checked during verification. + */ + @get:Input + public var isEnabled: Boolean = true + + /** + * Specifies custom name of the rule. + */ + @get:Input + @get:Nullable + @get:Optional + public var name: String? = null + + /** + * Specifies by which entity the code for separate coverage evaluation will be grouped. + */ + @get:Input + public var target: VerificationTarget = VerificationTarget.ALL + + /** + * Override class filter for the rule. + */ + public fun overrideClassFilter(config: Action) { + if (!classFilter.isPresent) { + classFilter.set(objects.newInstance(KoverClassFilter::class.java)) + } + config.execute(classFilter.get()) + } + + /** + * Add a constraint on the value of the code coverage metric. + */ + public fun bound(configureBound: Action) { + bounds.add(objects.newInstance(VerificationBound::class.java).also { configureBound.execute(it) }) + } +} + +public open class VerificationBound { + /** + * Specifies minimal value to compare with counter value. + */ + @get:Input + @get:Nullable + @get:Optional + public var minValue: Int? = null + + /** + * Specifies maximal value to compare with counter value. + */ + @get:Input + @get:Nullable + @get:Optional + public var maxValue: Int? = null + + /** + * Specifies which metric will be evaluation code coverage. + */ + @get:Input + public var counter: CounterType = CounterType.LINE + + /** + * Specifies type of lines counter value to compare with minimal and maximal values if them defined. + * Default is [VerificationValueType.COVERED_PERCENTAGE] + */ + @get:Input + public var valueType: VerificationValueType = VerificationValueType.COVERED_PERCENTAGE +} + +/** + * Entity type for grouping code to coverage evaluation. + */ +public enum class VerificationTarget { + /** + * Counts the coverage for all code. + */ + ALL, + + /** + * Counts the coverage for each class separately. + */ + CLASS, + + /** + * Counts the coverage for each package that has classes separately. + */ + PACKAGE +} + +/** + * Type of the metric to evaluate code coverage. + */ +public enum class CounterType { + /** + * Evaluates coverage for lines. + */ + LINE, + + /** + * Evaluates coverage for JVM bytecode instructions. + */ + INSTRUCTION, + + /** + * Evaluates coverage for code branches excluded dead-branches. + */ + BRANCH +} + + +/** + * Type of counter value to compare with minimal and maximal values if them defined. + */ +public enum class VerificationValueType { + COVERED_COUNT, + MISSED_COUNT, + COVERED_PERCENTAGE, + MISSED_PERCENTAGE +} diff --git a/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt b/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt index 8353963d..b83bed7c 100644 --- a/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt +++ b/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt @@ -1,10 +1,15 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + package kotlinx.kover.api public object KoverNames { public const val CHECK_TASK_NAME = "check" public const val VERIFICATION_GROUP = "verification" - public const val ROOT_EXTENSION_NAME = "kover" + public const val PROJECT_EXTENSION_NAME = "kover" + public const val MERGED_EXTENSION_NAME = "koverMerged" public const val TASK_EXTENSION_NAME = "kover" public const val MERGED_XML_REPORT_TASK_NAME = "koverMergedXmlReport" @@ -15,17 +20,28 @@ public object KoverNames { public const val XML_REPORT_TASK_NAME = "koverXmlReport" public const val HTML_REPORT_TASK_NAME = "koverHtmlReport" public const val REPORT_TASK_NAME = "koverReport" - public const val COLLECT_REPORTS_TASK_NAME = "koverCollectReports" public const val VERIFY_TASK_NAME = "koverVerify" + + public const val CONFIGURATION_NAME = "KoverEngineConfig" } public object KoverPaths { - public const val MERGED_HTML_REPORT_DEFAULT_PATH = "reports/kover/html" - public const val MERGED_XML_REPORT_DEFAULT_PATH = "reports/kover/report.xml" + public const val MERGED_HTML_REPORT_DEFAULT_PATH = "reports/kover/merged/html" + public const val MERGED_XML_REPORT_DEFAULT_PATH = "reports/kover/merged/xml/report.xml" + public const val MERGED_VERIFICATION_REPORT_DEFAULT_PATH = "reports/kover/merged/verification/errors.txt" - public const val PROJECT_HTML_REPORT_DEFAULT_PATH = "reports/kover/project-html" - public const val PROJECT_XML_REPORT_DEFAULT_PATH = "reports/kover/project-xml/report.xml" + public const val PROJECT_HTML_REPORT_DEFAULT_PATH = "reports/kover/html" + public const val PROJECT_XML_REPORT_DEFAULT_PATH = "reports/kover/xml/report.xml" + public const val PROJECT_VERIFICATION_REPORT_DEFAULT_PATH = "reports/kover/verification/errors.txt" +} - public const val ALL_PROJECTS_REPORTS_DEFAULT_PATH = "reports/kover/projects" +public object KoverVersions { + internal const val MINIMAL_INTELLIJ_VERSION = "1.0.675" + internal const val DEFAULT_INTELLIJ_VERSION = "1.0.675" + + internal const val DEFAULT_JACOCO_VERSION = "0.8.8" +} +public object KoverMigrations { + public const val MIGRATION_0_5_TO_0_6 = "https://github.com/Kotlin/kotlinx-kover/blob/v0.6.0-BETA/docs/migration-to-0.6.0.md" } diff --git a/src/main/kotlin/kotlinx/kover/api/KoverExtension.kt b/src/main/kotlin/kotlinx/kover/api/KoverExtension.kt deleted file mode 100644 index d7fabfcb..00000000 --- a/src/main/kotlin/kotlinx/kover/api/KoverExtension.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.api - -import org.gradle.api.model.* -import org.gradle.api.provider.* -import org.gradle.api.tasks.* - -open class KoverExtension(objects: ObjectFactory) { - - /** - * Specifies whether instrumentation is disabled for all test tasks of all projects. - */ - @get:Input - public var isDisabled: Boolean = false - - /** - * Specifies the coverage engine to be used to collect execution data. - */ - @get:Input - public val coverageEngine: Property = objects.property(CoverageEngine::class.java) - - /** - * Specifies the version of Intellij-coverage dependency. - */ - @get:Input - public val intellijEngineVersion: Property = objects.property(String::class.java) - - /** - * Specifies the version of JaCoCo dependency. - */ - @get:Input - public val jacocoEngineVersion: Property = objects.property(String::class.java) - - /** - * Specifies whether the reports will be generated within 'check' task execution. - */ - @get:Input - public var generateReportOnCheck: Boolean = true - - /** - * Specifies the projects to be disabled from instrumentation and reportings. - */ - @get:Input - public var disabledProjects: Set = emptySet() - - /** - * Specifies whether the classes from 'android' and 'com.android' packages should be included if Android plugin is applied. - */ - @get:Input - public var instrumentAndroidPackage: Boolean = false - - /** - * Specifies whether to perform all test tasks from all projects for Kover single-project tasks. - * If the value is `false`, then executed only test tasks of the project for which its Kover task is called. - */ - @get:Input - public var runAllTestsForProjectTask: Boolean = false - - /** - * Fully-qualified path of disabled projects. - * It is filled in only automatically based on custom values in `disabledProjects'. - */ - @get:Internal - internal var disabledProjectsPaths: Set = emptySet() -} - -public enum class CoverageEngine { - INTELLIJ, - JACOCO -} diff --git a/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt b/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt index cf3caa4b..fa29c968 100644 --- a/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt +++ b/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt @@ -1,13 +1,13 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("RedundantVisibilityModifier") - package kotlinx.kover.api +import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.* -import org.gradle.api.provider.* +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property import org.gradle.api.tasks.* import java.io.* @@ -15,18 +15,19 @@ import java.io.* * Extension for Kover plugin that additionally configures test tasks and * runs them with coverage agent to generate coverage execution data. */ -open class KoverTaskExtension(objects: ObjectFactory) { +public open class KoverTaskExtension(objects: ObjectFactory) { /** * Specifies whether instrumentation is disabled for an extended test task. */ @get:Input - public var isDisabled: Boolean = false + @get:JvmName("getIsDisabled") + public val isDisabled: Property = objects.property(Boolean::class.java) /** * Specifies file path of generated binary file with coverage data. */ @get:OutputFile - public val binaryReportFile: Property = objects.property(File::class.java) + public val reportFile: RegularFileProperty = objects.fileProperty() /** * Specifies class instrumentation inclusion rules. @@ -37,7 +38,7 @@ open class KoverTaskExtension(objects: ObjectFactory) { * It's possible to use `*` and `?` wildcards. */ @get:Input - public var includes: List = emptyList() + public val includes: ListProperty = objects.listProperty(String::class.java) /** * Specifies class instrumentation exclusion rules. @@ -48,5 +49,16 @@ open class KoverTaskExtension(objects: ObjectFactory) { * It's possible to use `*` and `?` wildcards. */ @get:Input - public var excludes: List = emptyList() + public val excludes: ListProperty = objects.listProperty(String::class.java) + + + // DEPRECATIONS + // TODO delete in 0.7 version + @get:Internal + @Deprecated( + message = "Property was renamed in Kover API version 2", + replaceWith = ReplaceWith("reportFile"), + level = DeprecationLevel.ERROR + ) + public val binaryReportFile: Property = objects.property(File::class.java) } diff --git a/src/main/kotlin/kotlinx/kover/api/Rules.kt b/src/main/kotlin/kotlinx/kover/api/Rules.kt deleted file mode 100644 index 18ff50da..00000000 --- a/src/main/kotlin/kotlinx/kover/api/Rules.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.api - -import org.gradle.api.* -import org.gradle.api.tasks.* -import javax.annotation.* - -/** - * Simple verification rule for code coverage. - * Works only with lines counter. - */ -public interface VerificationRule { - /** - * Unique ID of the rule. - */ - @get:Internal - public val id: Int - - /** - * Custom name of the rule. - */ - @get:Input - @get:Nullable - @get:Optional - public var name: String? - - /** - * Added constraints on the values of code coverage metrics. - */ - @get:Input - public val bounds: List - - /** - * Add a constraint on the value of the code coverage metric. - */ - public fun bound(configureBound: Action) -} - -public interface VerificationBound { - /** - * ID of the bound unique in the rule. - */ - @get:Internal - public val id: Int - - /** - * Minimal value to compare with counter value. - */ - @get:Input - @get:Nullable - @get:Optional - public var minValue: Int? - - /** - * Maximal value to compare with counter value. - */ - @get:Input - @get:Nullable - @get:Optional - public var maxValue: Int? - - /** - * Type of lines counter value to compare with minimal and maximal values if them defined. - * Default is [VerificationValueType.COVERED_LINES_PERCENTAGE] - */ - @get:Input - public var valueType: VerificationValueType -} - -/** - * Type of lines counter value to compare with minimal and maximal values if them defined. - */ -public enum class VerificationValueType { - COVERED_LINES_COUNT, - MISSED_LINES_COUNT, - COVERED_LINES_PERCENTAGE -} diff --git a/src/main/kotlin/kotlinx/kover/appliers/KoverMergedApplier.kt b/src/main/kotlin/kotlinx/kover/appliers/KoverMergedApplier.kt new file mode 100644 index 00000000..0424a9e2 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/appliers/KoverMergedApplier.kt @@ -0,0 +1,235 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.appliers + +import kotlinx.kover.api.* +import kotlinx.kover.api.KoverNames.CHECK_TASK_NAME +import kotlinx.kover.api.KoverNames.CONFIGURATION_NAME +import kotlinx.kover.api.KoverNames.MERGED_HTML_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.MERGED_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.MERGED_VERIFY_TASK_NAME +import kotlinx.kover.api.KoverNames.MERGED_XML_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.VERIFICATION_GROUP +import kotlinx.kover.api.KoverPaths.MERGED_HTML_REPORT_DEFAULT_PATH +import kotlinx.kover.api.KoverPaths.MERGED_VERIFICATION_REPORT_DEFAULT_PATH +import kotlinx.kover.api.KoverPaths.MERGED_XML_REPORT_DEFAULT_PATH +import kotlinx.kover.tasks.* +import org.gradle.api.* +import org.gradle.api.file.* +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.testing.* +import org.gradle.configurationcache.extensions.* + +internal fun Project.applyMerged() { + val extension = createMergedExtension() + afterEvaluate(ProcessMergeExtensionAction(extension)) +} + +private fun Project.createMergedExtension(): KoverMergedConfig { + val extension = extensions.create(KoverNames.MERGED_EXTENSION_NAME, KoverMergedConfig::class.java, objects) + extension.isEnabled.set(false) + extension.filters.classes.set(KoverClassFilter()) + extension.filters.projects.set(KoverProjectsFilter()) + extension.xmlReport.onCheck.set(false) + extension.xmlReport.reportFile.set(layout.buildDirectory.file(MERGED_XML_REPORT_DEFAULT_PATH)) + extension.xmlReport.classFilter.set(extension.filters.classes) + extension.htmlReport.onCheck.set(false) + extension.htmlReport.reportDir.set(layout.buildDirectory.dir(MERGED_HTML_REPORT_DEFAULT_PATH)) + extension.htmlReport.classFilter.set(extension.filters.classes) + extension.verify.onCheck.set(false) + return extension +} + +private class ProcessMergeExtensionAction(private val extension: KoverMergedConfig) : Action { + override fun execute(container: Project) { + // don't create tasks if merge wasn't enabled + if (!extension.isEnabled.get()) { + return + } + + val extensionByProject = container.projectsExtensionsProvider(extension, container.allprojects) + val engineProvider = container.engineProvider(extensionByProject) + val testsProvider = container.instrumentedTasksProvider(extensionByProject) + + val xmlTask = container.createMergedTask( + MERGED_XML_REPORT_TASK_NAME, + extension.xmlReport.classFilter, + extensionByProject, + engineProvider, + testsProvider, + { e -> e.xmlReport.filters.sourceSets.get() } + ) { + it.reportFile.set(extension.xmlReport.reportFile) + it.description = "Generates code coverage XML report for all enabled test tasks in specified projects." + } + + val htmlTask = container.createMergedTask( + MERGED_HTML_REPORT_TASK_NAME, + extension.htmlReport.classFilter, + extensionByProject, + engineProvider, + testsProvider, + { e -> e.htmlReport.taskFilters.sourceSets.get() } + ) { + it.reportDir.set(extension.htmlReport.reportDir) + it.description = "Generates code coverage HTML report for all enabled test tasks in specified projects." + } + + val verifyTask = container.createMergedTask( + MERGED_VERIFY_TASK_NAME, + extension.filters.classes, + extensionByProject, + engineProvider, + testsProvider, + { e -> e.filters.sourceSets.get() } + ) { + it.rules.set(extension.verify.rules) + it.resultFile.set(container.layout.buildDirectory.file(MERGED_VERIFICATION_REPORT_DEFAULT_PATH)) + it.description = "Verifies code coverage metrics of specified projects based on specified rules." + } + // TODO `onlyIf` block moved out from config lambda because of bug in Kotlin compiler - it implicitly adds closure on `Project` inside onlyIf's lambda + verifyTask.onlyIf { extension.verify.hasActiveRules() } + + + container.tasks.create(MERGED_REPORT_TASK_NAME) { + it.group = VERIFICATION_GROUP + it.dependsOn(xmlTask) + it.dependsOn(htmlTask) + it.description = "Generates code coverage HTML and XML reports for all enabled test tasks in one project." + } + + container.tasks.configureEach { + if (it.name == CHECK_TASK_NAME) { + it.dependsOn(container.provider { + val tasks = mutableListOf() + if (extension.xmlReport.onCheck.get()) { + tasks += xmlTask + } + if (extension.htmlReport.onCheck.get()) { + tasks += htmlTask + } + if (extension.verify.onCheck.get() && extension.verify.hasActiveRules()) { + // don't add dependency if there is no active verification rules https://github.com/Kotlin/kotlinx-kover/issues/168 + tasks += verifyTask + } + tasks + }) + } + } + } +} + +private inline fun Project.createMergedTask( + taskName: String, + classFilter: Provider, + extensionByProject: Provider>, + engineProvider: Provider, + testsProvider: Provider>, + crossinline filterExtractor: (KoverProjectConfig) -> KoverSourceSetFilter, + crossinline block: (T) -> Unit +): T { + val task = tasks.create(taskName, T::class.java) { + it.files.set(mergedFilesProvider(extensionByProject, filterExtractor)) + it.engine.set(engineProvider) + it.dependsOn(testsProvider) + + it.classFilter.set(classFilter) + it.group = VERIFICATION_GROUP + block(it) + } + // TODO `onlyIf` block moved out from config lambda because of bug in Kotlin compiler - it implicitly adds closure on `Project` inside onlyIf's lambda + // execute task only if there is at least one binary report + task.onlyIf { t -> (t as KoverReportTask).files.get().any { f -> !f.value.binaryReportFiles.isEmpty } } + + return task +} + +private fun Project.projectsExtensionsProvider( + extension: KoverMergedConfig, + allProjects: Iterable +): Provider> { + return provider { + val projects = filterProjects(extension.filters.projects.get(), allProjects) + + val notAppliedProjects = + projects.filter { it.extensions.findByType(KoverProjectConfig::class.java) == null }.map { it.path } + if (notAppliedProjects.isNotEmpty()) { + throw GradleException("Can't create Kover merge tasks: Kover plugin not applied in projects $notAppliedProjects") + } + projects.associateWith { it.extensions.getByType(KoverProjectConfig::class.java) } + } +} + + +private inline fun Project.mergedFilesProvider( + extensionByProject: Provider>, + crossinline filterExtractor: (KoverProjectConfig) -> KoverSourceSetFilter +): Provider> { + return provider { + extensionByProject.get() + .map { (project, extension) -> project.path to project.projectFiles(filterExtractor(extension), extension) } + .associate { it } + } +} + + +private fun filterProjects(filters: KoverProjectsFilter, allProjects: Iterable): List { + if (filters.excludes.isEmpty()) { + return allProjects.toList() + } + + val projectsByPath = allProjects.associateBy { p -> p.path }.toMutableMap() + val pathsByName = allProjects.associate { it.name to mutableListOf() } + allProjects.forEach { pathsByName.getValue(it.name) += it.path } + + val excludedPaths = filters.excludes.map { + if (it.startsWith(':')) { + projectsByPath[it] + ?: throw GradleException("Kover configuring error: not found project '$it' for merged tasks") + it + } else { + val paths = pathsByName[it] + ?: throw GradleException("Kover configuring error: not found project '$it' for merged tasks") + if (paths.size > 1) { + throw GradleException("Kover configuring error: ambiguous name of the project '$it' for merged tasks: suitable projects with paths $paths. Consider using fully-qualified name starting with ':'") + } + paths[0] + } + }.toSet() + + return projectsByPath.filterNot { it.key in excludedPaths }.map { it.value } +} + + +private fun Project.engineProvider(extensionByProject: Provider>): Provider { + val archiveOperations: ArchiveOperations = this.serviceOf() + // configuration already created in all projects at this moment because merge tasks creates in afterEvaluate step + val config = configurations.getByName(CONFIGURATION_NAME) + // the plugin is always applied to the containing project + val containerEngine = extensions.getByType(KoverProjectConfig::class.java).engine + + val containerPath = path + return provider { + val map: MutableMap> = mutableMapOf() + extensionByProject.get().forEach { (p, v) -> + map.computeIfAbsent(v.engine.get()) { mutableListOf() } += p.path + } + + // check same engine variants are used + if (map.size > 1) { + throw GradleException("Can't create Kover merge tasks: different coverage engines are used in projects.\n\tProjects by engines: $map") + } + val variant = map.keys.first() + if (variant != containerEngine.get()) { + throw GradleException("Can't create Kover merge tasks: child projects engines '$variant' are different from the engine '${containerEngine.get()}' from the containing project '$containerPath'") + } + engineByVariant(variant, config, archiveOperations) + } +} + + +private fun Project.instrumentedTasksProvider(extensionByProject: Provider>): Provider> { + return provider { extensionByProject.get().flatMap { it.key.instrumentedTasks(it.value) } } +} diff --git a/src/main/kotlin/kotlinx/kover/appliers/KoverProjectApplier.kt b/src/main/kotlin/kotlinx/kover/appliers/KoverProjectApplier.kt new file mode 100644 index 00000000..48a7a34b --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/appliers/KoverProjectApplier.kt @@ -0,0 +1,233 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.appliers + +import kotlinx.kover.api.* +import kotlinx.kover.api.KoverNames.CONFIGURATION_NAME +import kotlinx.kover.api.KoverNames.HTML_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.VERIFY_TASK_NAME +import kotlinx.kover.api.KoverNames.XML_REPORT_TASK_NAME +import kotlinx.kover.engines.commons.* +import kotlinx.kover.lookup.* +import kotlinx.kover.tasks.* +import org.gradle.api.* +import org.gradle.api.artifacts.* +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.provider.* +import org.gradle.api.tasks.testing.* +import org.gradle.configurationcache.extensions.* +import java.io.* + +internal fun Project.applyToProject() { + val extension = createProjectExtension() + val config = createEngineConfig(extension.engine) + + val engineProvider = engineProvider(config, extension.engine) + + tasks.withType(Test::class.java).configureEach { t -> + t.applyToTestTask(extension, engineProvider) + } + + val testsProvider = instrumentedTasksProvider(extension) + + val xmlTask = createTask( + XML_REPORT_TASK_NAME, + extension.xmlReport.filters, + extension, + engineProvider, + testsProvider, + ) { + it.reportFile.set(extension.xmlReport.reportFile) + it.description = "Generates code coverage XML report for all enabled test tasks in one project." + } + + val htmlTask = createTask( + HTML_REPORT_TASK_NAME, + extension.htmlReport.taskFilters, + extension, + engineProvider, + testsProvider, + ) { + it.reportDir.set(extension.htmlReport.reportDir) + it.description = "Generates code coverage HTML report for all enabled test tasks in one project." + } + + val verifyTask = createTask( + VERIFY_TASK_NAME, + extension.filters, + extension, + engineProvider, + testsProvider, + ) { + it.rules.set(extension.verify.rules) + it.resultFile.set(layout.buildDirectory.file(KoverPaths.PROJECT_VERIFICATION_REPORT_DEFAULT_PATH)) + it.description = "Verifies code coverage metrics of one project based on specified rules." + } + // TODO `onlyIf` block moved out from config lambda because of bug in Kotlin compiler - it implicitly adds closure on `Project` inside onlyIf's lambda + verifyTask.onlyIf { extension.verify.hasActiveRules() } + + + tasks.create(KoverNames.REPORT_TASK_NAME) { + it.group = KoverNames.VERIFICATION_GROUP + it.dependsOn(xmlTask) + it.dependsOn(htmlTask) + it.description = "Generates code coverage HTML and XML reports for all enabled test tasks in one project." + } + + + tasks.configureEach { + if (it.name == KoverNames.CHECK_TASK_NAME) { + it.dependsOn(provider { + // don't add dependency if Kover is disabled + if (extension.isDisabled.get()) { + return@provider emptyList() + } + + val tasks = mutableListOf() + if (extension.xmlReport.onCheck.get()) { + tasks += xmlTask + } + if (extension.htmlReport.onCheck.get()) { + tasks += htmlTask + } + if (extension.verify.onCheck.get() && extension.verify.hasActiveRules()) { + // don't add dependency if there is no active verification rules https://github.com/Kotlin/kotlinx-kover/issues/168 + tasks += verifyTask + } + tasks + }) + } + } +} + +private inline fun Project.createTask( + taskName: String, + filters: KoverProjectFilters, + extension: KoverProjectConfig, + engineProvider: Provider, + testsProvider: Provider>, + crossinline block: (T) -> Unit +): T { + val task = tasks.create(taskName, T::class.java) { + it.files.put(path, projectFilesProvider(extension, filters.sourceSets)) + it.engine.set(engineProvider) + it.dependsOn(testsProvider) + it.classFilter.set(filters.classes) + it.group = KoverNames.VERIFICATION_GROUP + block(it) + } + + // TODO `onlyIf` block moved out from config lambda because of bug in Kotlin compiler - it implicitly adds closure on `Project` inside onlyIf's lambda + // execute task only if Kover not disabled for project + task.onlyIf { !extension.isDisabled.get() } + // execute task only if there is at least one binary report + task.onlyIf { t -> (t as KoverReportTask).files.get().any { f -> !f.value.binaryReportFiles.isEmpty } } + + return task +} + +private fun Project.createProjectExtension(): KoverProjectConfig { + val extension = extensions.create(KoverNames.PROJECT_EXTENSION_NAME, KoverProjectConfig::class.java, objects) + // default values + + extension.isDisabled.set(false) + extension.engine.set(DefaultIntellijEngine) + extension.filters.classes.set(KoverClassFilter()) + extension.filters.sourceSets.set(KoverSourceSetFilter()) + extension.xmlReport.reportFile.set(layout.buildDirectory.file(KoverPaths.PROJECT_XML_REPORT_DEFAULT_PATH)) + extension.xmlReport.onCheck.set(false) + extension.xmlReport.filters.classes.set(extension.filters.classes) + extension.xmlReport.filters.sourceSets.set(extension.filters.sourceSets) + extension.htmlReport.reportDir.set(layout.buildDirectory.dir(KoverPaths.PROJECT_HTML_REPORT_DEFAULT_PATH)) + extension.htmlReport.onCheck.set(false) + extension.htmlReport.taskFilters.classes.set(extension.filters.classes) + extension.htmlReport.taskFilters.sourceSets.set(extension.filters.sourceSets) + extension.verify.onCheck.set(true) + + return extension +} + +private fun Project.createEngineConfig(engineVariantProvider: Provider): Configuration { + val config = project.configurations.create(CONFIGURATION_NAME) + config.isVisible = false + config.isTransitive = true + config.description = "Kotlin Kover Plugin configuration for Coverage Engine" + + config.defaultDependencies { default -> + val variant = engineVariantProvider.get() + EngineManager.dependencies(variant).forEach { + default.add(dependencies.create(it)) + } + } + return config +} + +private fun Project.engineProvider( + config: Configuration, + engineVariantProvider: Provider +): Provider { + val archiveOperations: ArchiveOperations = project.serviceOf() + return project.provider { engineByVariant(engineVariantProvider.get(), config, archiveOperations) } +} + +internal fun engineByVariant( + variant: CoverageEngineVariant, + config: Configuration, + archiveOperations: ArchiveOperations +): EngineDetails { + val jarFile = EngineManager.findJarFile(variant, config, archiveOperations) + return EngineDetails(variant, jarFile, config) +} + +internal fun Project.projectFilesProvider( + extension: KoverProjectConfig, + sourceSetFiltersProvider: Provider +): Provider { + return provider { projectFiles(sourceSetFiltersProvider.get(), extension) } +} + +internal fun Project.projectFiles( + filters: KoverSourceSetFilter, + extension: KoverProjectConfig +): ProjectFiles { + val directories = DirsLookup.lookup(project, filters) + val reportFiles = files(binaryReports(extension)) + return ProjectFiles(reportFiles, directories.sources, directories.outputs) +} + +internal fun Project.instrumentedTasks(extension: KoverProjectConfig): List { + return tasks.withType(Test::class.java).asSequence() + // task can be disabled in the project extension + .filterNot { extension.instrumentation.excludeTasks.contains(it.name) } + .filterNot { t -> t.extensions.getByType(KoverTaskExtension::class.java).isDisabled.get() } + .toList() +} + +private fun Project.instrumentedTasksProvider(extension: KoverProjectConfig): Provider> { + return provider { instrumentedTasks(extension) } +} + + +private fun Project.binaryReports(extension: KoverProjectConfig): List { + if (extension.isDisabled.get()) { + return emptyList() + } + + return tasks.withType(Test::class.java).asSequence() + // task can be disabled in the project extension + .filterNot { extension.instrumentation.excludeTasks.contains(it.name) } + .map { t -> t.extensions.getByType(KoverTaskExtension::class.java) } + // process binary report only from tasks with enabled cover + .filterNot { e -> e.isDisabled.get() } + .map { e -> e.reportFile.get().asFile } + // process binary report only from tasks with sources + .filter { f -> f.exists() } + .toList() +} + + +internal fun KoverVerifyConfig.hasActiveRules(): Boolean { + return rules.get().any { rule -> rule.isEnabled } +} diff --git a/src/main/kotlin/kotlinx/kover/appliers/KoverTaskApplier.kt b/src/main/kotlin/kotlinx/kover/appliers/KoverTaskApplier.kt new file mode 100644 index 00000000..0e1853e1 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/appliers/KoverTaskApplier.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.appliers + +import kotlinx.kover.api.* +import kotlinx.kover.engines.commons.* +import kotlinx.kover.tasks.* +import org.gradle.api.* +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.* +import org.gradle.api.tasks.* +import org.gradle.api.tasks.testing.* +import org.gradle.process.* +import java.io.* + + +internal fun Test.applyToTestTask( + projectExtension: KoverProjectConfig, + engineProvider: Provider +) { + val extension = createTaskExtension(projectExtension) + + val disabledProvider = project.provider { + (projectExtension.isDisabled.get() || extension.isDisabled.get() || + projectExtension.instrumentation.excludeTasks.contains(name)) + } + + jvmArgumentProviders.add(CoverageArgumentProvider(this, engineProvider, disabledProvider, extension)) + + val sourceErrorProvider = project.provider { + File(extension.reportFile.get().asFile.parentFile, "coverage-error.log") + } + val targetErrorProvider = project.layout.buildDirectory.file("kover/errors/$name.log").map { it.asFile } + + doFirst(BinaryReportCleanupAction(disabledProvider, extension.reportFile, engineProvider.map { e -> e.variant })) + doLast(MoveIntellijErrorLogAction(sourceErrorProvider, targetErrorProvider)) +} + +private fun Task.createTaskExtension(projectExtension: KoverProjectConfig): KoverTaskExtension { + val taskExtension = + extensions.create(KoverNames.TASK_EXTENSION_NAME, KoverTaskExtension::class.java, project.objects) + + taskExtension.isDisabled.set(false) + taskExtension.reportFile.set(project.provider { + val engine = projectExtension.engine.get() + val suffix = if (engine.vendor == CoverageEngineVendor.INTELLIJ) ".ic" else ".exec" + project.layout.buildDirectory.get().file("kover/$name$suffix") + }) + + return taskExtension +} + + +private class CoverageArgumentProvider( + private val task: Task, + @get:Nested val engineProvider: Provider, + @get:Input val disabledProvider: Provider, + @get:Nested val taskExtension: KoverTaskExtension +) : CommandLineArgumentProvider, Named { + @Internal + override fun getName(): String { + return "koverArgumentsProvider" + } + + override fun asArguments(): MutableIterable { + if (disabledProvider.get()) { + return mutableListOf() + } + + val reportFile = taskExtension.reportFile.get().asFile + val classFilter = KoverClassFilter() + + classFilter.includes += taskExtension.includes.get() + classFilter.excludes += taskExtension.excludes.get() + /* + The instrumentation of android classes often causes errors when using third-party + frameworks (see https://github.com/Kotlin/kotlinx-kover/issues/89). + + Because android classes are not part of the project, in any case they do not get into the report, + and they can be excluded from instrumentation. + + FIXME Remove this code if the IntelliJ Agent stops changing project classes during instrumentation, see https://github.com/Kotlin/kotlinx-kover/issues/196 + */ + classFilter.excludes += "android.*" + classFilter.excludes += "com.android.*" + + return EngineManager.buildAgentArgs(engineProvider.get(), task, reportFile, classFilter) + } +} + + +/* + To support parallel tests, both Coverage Engines work in append to data file mode. + For this reason, before starting the tests, it is necessary to clear the file from the results of previous runs. +*/ +private class BinaryReportCleanupAction( + private val disabledProvider: Provider, + private val reportFileProvider: Provider, + private val engineVariantProvider: Provider, +) : Action { + override fun execute(task: Task) { + if (disabledProvider.get()) { + return + } + val file = reportFileProvider.get().asFile + // always delete previous data file + file.delete() + if (engineVariantProvider.get().vendor == CoverageEngineVendor.INTELLIJ) { + // IntelliJ engine expected empty file for parallel test execution. + // Since it is impossible to know in advance whether the tests will be run in parallel, we always create an empty file. + file.createNewFile() + } + } +} + +private class MoveIntellijErrorLogAction( + private val sourceFile: Provider, + private val targetFile: Provider +) : Action { + override fun execute(task: Task) { + val origin = sourceFile.get() + if (origin.exists() && origin.isFile) { + origin.copyTo(targetFile.get(), true) + origin.delete() + } + } +} diff --git a/src/main/kotlin/kotlinx/kover/engines/commons/AgentsFactory.kt b/src/main/kotlin/kotlinx/kover/engines/commons/AgentsFactory.kt deleted file mode 100644 index 2bf4ccac..00000000 --- a/src/main/kotlin/kotlinx/kover/engines/commons/AgentsFactory.kt +++ /dev/null @@ -1,19 +0,0 @@ -package kotlinx.kover.engines.commons - -import kotlinx.kover.api.* -import kotlinx.kover.engines.intellij.* -import kotlinx.kover.engines.jacoco.* -import org.gradle.api.* - -internal object AgentsFactory { - fun createAgents(project: Project, koverExtension: KoverExtension): Map { - return mapOf( - CoverageEngine.INTELLIJ to project.createIntellijAgent(koverExtension), - CoverageEngine.JACOCO to project.createJacocoAgent(koverExtension), - ) - } -} - -internal fun Map.getFor(engine: CoverageEngine): CoverageAgent { - return this[engine] ?: throw GradleException("Coverage agent for Coverage Engine '$engine' not found") -} diff --git a/src/main/kotlin/kotlinx/kover/engines/commons/CoverageAgent.kt b/src/main/kotlin/kotlinx/kover/engines/commons/CoverageAgent.kt deleted file mode 100644 index 77197f25..00000000 --- a/src/main/kotlin/kotlinx/kover/engines/commons/CoverageAgent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package kotlinx.kover.engines.commons - -import kotlinx.kover.api.* -import org.gradle.api.* -import org.gradle.api.file.* - -internal interface CoverageAgent { - val engine: CoverageEngine - val classpath: FileCollection - fun buildCommandLineArgs(task: Task, extension: KoverTaskExtension): MutableList -} diff --git a/src/main/kotlin/kotlinx/kover/engines/commons/EngineManager.kt b/src/main/kotlin/kotlinx/kover/engines/commons/EngineManager.kt new file mode 100644 index 00000000..6dea8b3f --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/engines/commons/EngineManager.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.engines.commons + +import kotlinx.kover.api.* +import kotlinx.kover.engines.intellij.* +import kotlinx.kover.engines.jacoco.* +import kotlinx.kover.tasks.* +import org.gradle.api.* +import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.* +import org.gradle.process.* +import java.io.* + + +@Suppress("UNUSED_PARAMETER") +internal object EngineManager { + fun buildAgentArgs( + details: EngineDetails, + task: Task, + reportFile: File, + classFilter: KoverClassFilter + ): MutableList { + return if (details.variant.vendor == CoverageEngineVendor.INTELLIJ) { + task.buildIntellijAgentJvmArgs(details.jarFile, reportFile, classFilter) + } else { + reportFile.parentFile.mkdirs() + task.buildJacocoAgentJvmArgs(details.jarFile, reportFile, classFilter) + } + } + + fun report( + details: EngineDetails, + task: Task, + exec: ExecOperations, + projectFiles: Map, + classFilter: KoverClassFilter, + xmlFile: File?, + htmlDir: File? + ) { + if (details.variant.vendor == CoverageEngineVendor.INTELLIJ) { + task.intellijReport(exec, projectFiles, classFilter, xmlFile, htmlDir, details.classpath) + } else { + task.jacocoReport(projectFiles, xmlFile, htmlDir, details.classpath) + } + } + + fun verify( + details: EngineDetails, + task: Task, + exec: ExecOperations, + projectFiles: Map, + classFilter: KoverClassFilter, + rules: List, + ): String? { + return if (details.variant.vendor == CoverageEngineVendor.INTELLIJ) { + task.intellijVerification(exec, projectFiles, classFilter, rules, details.classpath) + } else { + task.jacocoVerification(projectFiles, rules, details.classpath) + } + } + + + fun findJarFile(variant: CoverageEngineVariant, config: Configuration, archiveOperations: ArchiveOperations): File { + return if (variant.vendor == CoverageEngineVendor.INTELLIJ) { + config.fileCollection { it.name == "intellij-coverage-agent" }.singleFile + } else { + val fatJar = config.fileCollection { it.name == "org.jacoco.agent" }.singleFile + archiveOperations.zipTree(fatJar).filter { it.name == "jacocoagent.jar" }.singleFile + } + } + + fun dependencies(engineVariant: CoverageEngineVariant): List { + return if (engineVariant.vendor == CoverageEngineVendor.INTELLIJ) { + getIntellijDependencies(engineVariant.version) + } else { + getJacocoDependencies(engineVariant.version) + } + } +} diff --git a/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt b/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt index ee7d7749..e9c7238a 100644 --- a/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt +++ b/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt @@ -1,14 +1,28 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + package kotlinx.kover.engines.commons -import java.io.* +import kotlinx.kover.api.* +import java.math.BigDecimal + + +internal class ReportVerificationRule( + val id: Int, + val name: String?, + val target: VerificationTarget, + val filters: KoverClassFilter?, + val bounds: List +) -internal class Report( - val files: List, - val projects: List, - val includes: List = emptyList(), - val excludes: List = emptyList() +internal class ReportVerificationBound( + val id: Int, + val minValue: BigDecimal?, + val maxValue: BigDecimal?, + val counter: CounterType, + val valueType: VerificationValueType ) -internal class ProjectInfo(val sources: Iterable, val outputs: Iterable) private val regexMetacharactersSet = "<([{\\^-=$!|]})+.>".toSet() diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt index 5ade3b98..93716c33 100644 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt @@ -1,91 +1,52 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.kover.engines.intellij import kotlinx.kover.api.* import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.CoverageAgent -import org.gradle.api.* -import org.gradle.api.artifacts.* -import org.gradle.api.file.* -import org.gradle.api.provider.Provider +import org.gradle.api.Task import java.io.* - -internal fun Project.createIntellijAgent(koverExtension: KoverExtension): CoverageAgent { - val intellijConfig = createIntellijConfig(koverExtension) - val jarProvider = provider { intellijConfig.fileCollection { it.name == "intellij-coverage-agent" }.singleFile } - return IntellijAgent(intellijConfig, jarProvider) +private const val trackingPerTest = false // a flag to enable tracking per test coverage +private const val calculateForUnloadedClasses = false // a flag to calculate coverage for unloaded classes +private const val appendToDataFile = true // a flag to use data file as initial coverage +private const val samplingMode = false //a flag to run coverage in sampling mode or in tracing mode otherwise + +internal fun Task.buildIntellijAgentJvmArgs(jarFile: File, reportFile: File, classFilter: KoverClassFilter): MutableList { + val argsFile = File(temporaryDir, "intellijagent.args") + argsFile.writeAgentArgs(reportFile, classFilter) + + return mutableListOf( + "-javaagent:${jarFile.canonicalPath}=${argsFile.canonicalPath}", + "-Didea.new.sampling.coverage=true", + "-Didea.new.tracing.coverage=true", + "-Didea.coverage.log.level=error", + "-Dcoverage.ignore.private.constructor.util.class=true" + ) } -private class IntellijAgent(override val classpath: FileCollection, private val jarProvider: Provider): CoverageAgent { - private val trackingPerTest = false // a flag to enable tracking per test coverage - private val calculateForUnloadedClasses = false // a flag to calculate coverage for unloaded classes - private val appendToDataFile = true // a flag to use data file as initial coverage - private val samplingMode = false //a flag to run coverage in sampling mode or in tracing mode otherwise - - override val engine: CoverageEngine = CoverageEngine.INTELLIJ - - override fun buildCommandLineArgs(task: Task, extension: KoverTaskExtension): MutableList { - val argsFile = File(task.temporaryDir, "intellijagent.args") - argsFile.writeArgsToFile(extension) - - return mutableListOf( - "-javaagent:${jarProvider.get().canonicalPath}=${argsFile.canonicalPath}", - "-Didea.new.sampling.coverage=true", - "-Didea.new.tracing.coverage=true", - "-Didea.coverage.log.level=error", - "-Dcoverage.ignore.private.constructor.util.class=true" - ) - } - - private fun File.writeArgsToFile(extension: KoverTaskExtension) { - val binary = extension.binaryReportFile.get() - binary.parentFile.mkdirs() - val binaryPath = binary.canonicalPath - - printWriter().use { pw -> - pw.appendLine(binaryPath) - pw.appendLine(trackingPerTest.toString()) - pw.appendLine(calculateForUnloadedClasses.toString()) - pw.appendLine(appendToDataFile.toString()) - pw.appendLine(samplingMode.toString()) - extension.includes.forEach { i -> - pw.appendLine(i.wildcardsToRegex()) - } - - if (extension.excludes.isNotEmpty()) { - pw.appendLine("-exclude") - } - - extension.excludes.forEach { e -> - pw.appendLine(e.wildcardsToRegex()) - } +private fun File.writeAgentArgs(reportFile: File, classFilter: KoverClassFilter) { + reportFile.parentFile.mkdirs() + val binaryPath = reportFile.canonicalPath + + printWriter().use { pw -> + pw.appendLine(binaryPath) + pw.appendLine(trackingPerTest.toString()) + pw.appendLine(calculateForUnloadedClasses.toString()) + pw.appendLine(appendToDataFile.toString()) + pw.appendLine(samplingMode.toString()) + classFilter.includes.forEach { i -> + pw.appendLine(i.wildcardsToRegex()) } - } -} -private fun Project.createIntellijConfig(koverExtension: KoverExtension): Configuration { - val config = project.configurations.create("IntellijKoverConfig") - config.isVisible = false - config.isTransitive = true - config.description = "Kotlin Kover Plugin configuration for IntelliJ agent and reporter" - - config.defaultDependencies { dependencies -> - val agentVersion = koverExtension.intellijEngineVersion.get() - IntellijEngineVersion.parseOrNull(agentVersion)?.let { - if (it < minimalIntellijVersion) throw GradleException("IntelliJ engine version $it is too low, minimal version is $minimalIntellijVersion") + if (classFilter.excludes.isNotEmpty()) { + pw.appendLine("-exclude") } - dependencies.add( - this.dependencies.create("org.jetbrains.intellij.deps:intellij-coverage-agent:$agentVersion") - ) - - dependencies.add( - this.dependencies.create("org.jetbrains.intellij.deps:intellij-coverage-reporter:$agentVersion") - ) + classFilter.excludes.forEach { e -> + pw.appendLine(e.wildcardsToRegex()) + } } - return config } diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijEngine.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijEngine.kt new file mode 100644 index 00000000..e36ac608 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijEngine.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.kover.engines.intellij + +import kotlinx.kover.api.KoverVersions.MINIMAL_INTELLIJ_VERSION +import org.gradle.api.* + +internal fun getIntellijDependencies(engineVersion: String): List { + if (engineVersion.isLowVersion(MINIMAL_INTELLIJ_VERSION)) { + throw GradleException("IntelliJ engine version $engineVersion is too low, minimal version is $MINIMAL_INTELLIJ_VERSION") + } + return listOf( + "org.jetbrains.intellij.deps:intellij-coverage-agent:$engineVersion", + "org.jetbrains.intellij.deps:intellij-coverage-reporter:$engineVersion" + ) +} + + +private fun String.isLowVersion(minimalVersion: String): Boolean { + val customParts = this.split(".") + if (customParts.size != 3) { + throw GradleException("Invalid custom IntelliJ Coverage Engine version '$this'. Expected 'x.x.xxx'") + } + + val minimalParts = minimalVersion.split(".") + if (minimalParts.size != 3) { + throw GradleException("Invalid minimal IntelliJ Coverage Engine version '$this'") + } + + for (i in 0..2) { + if (customParts[i] > minimalParts[i]) { + return false + } else if (customParts[i] < minimalParts[i]) { + return true + } + } + + return false +} diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt index d5978aee..d4c330ff 100644 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt @@ -4,9 +4,10 @@ package kotlinx.kover.engines.intellij +import kotlinx.kover.api.* import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.Report import kotlinx.kover.json.* +import kotlinx.kover.tasks.* import org.gradle.api.* import org.gradle.api.file.* import org.gradle.process.ExecOperations @@ -14,7 +15,8 @@ import java.io.* internal fun Task.intellijReport( exec: ExecOperations, - report: Report, + projectFiles: Map, + filters: KoverClassFilter, xmlFile: File?, htmlDir: File?, classpath: FileCollection @@ -28,7 +30,7 @@ internal fun Task.intellijReport( } val argsFile = File(temporaryDir, "intellijreport.json") - argsFile.writeReportsJson(report, xmlFile, htmlDir) + argsFile.writeReportsJson(projectFiles, filters, xmlFile, htmlDir) exec.javaexec { e -> e.mainClass.set("com.intellij.rt.coverage.report.Main") @@ -77,19 +79,20 @@ JSON example: ``` */ private fun File.writeReportsJson( - report: Report, + projectFiles: Map, + classFilter: KoverClassFilter, xmlFile: File?, htmlDir: File? ) { writeJsonObject(mutableMapOf( - "reports" to report.files.map { mapOf("ic" to it) }, - "modules" to report.projects.map { mapOf("sources" to it.sources, "output" to it.outputs) }, + "reports" to projectFiles.flatMap { it.value.binaryReportFiles }.map { mapOf("ic" to it) }, + "modules" to projectFiles.map { mapOf("sources" to it.value.sources, "output" to it.value.outputs) }, ).also { - if (report.includes.isNotEmpty()) { - it["include"] = mapOf("classes" to report.includes.map { c -> c.wildcardsToRegex() }) + if (classFilter.includes.isNotEmpty()) { + it["include"] = mapOf("classes" to classFilter.includes.map { c -> c.wildcardsToRegex() }) } - if (report.excludes.isNotEmpty()) { - it["exclude"] = mapOf("classes" to report.excludes.map { c -> c.wildcardsToRegex() }) + if (classFilter.excludes.isNotEmpty()) { + it["exclude"] = mapOf("classes" to classFilter.excludes.map { c -> c.wildcardsToRegex() }) } xmlFile?.also { f -> it["xml"] = f diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt index 5b90ddc6..63e58aa0 100644 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt @@ -5,10 +5,10 @@ package kotlinx.kover.engines.intellij import kotlinx.kover.api.* -import kotlinx.kover.api.VerificationValueType.COVERED_LINES_PERCENTAGE +import kotlinx.kover.api.VerificationValueType.* import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.Report import kotlinx.kover.json.* +import kotlinx.kover.tasks.* import org.gradle.api.* import org.gradle.api.file.* import org.gradle.process.ExecOperations @@ -19,13 +19,14 @@ import java.math.RoundingMode @Suppress("UNUSED_PARAMETER") internal fun Task.intellijVerification( exec: ExecOperations, - report: Report, - rules: Iterable, + projectFiles: Map, + classFilter: KoverClassFilter, + rules: List, classpath: FileCollection -) { +): String? { val aggRequest = File(temporaryDir, "agg-request.json") - val aggFile = File(temporaryDir, "aggregated.ic") - aggRequest.writeAggVerifyJson(report, aggFile) + val groupedRules = groupRules(classFilter, rules) + aggRequest.writeAggJson(projectFiles, groupedRules) exec.javaexec { e -> e.mainClass.set("com.intellij.rt.coverage.aggregate.Main") e.classpath = classpath @@ -33,19 +34,46 @@ internal fun Task.intellijVerification( } val verifyRequest = File(temporaryDir, "verify-request.json") - val resultFile = File(temporaryDir, "verify-result.json") - verifyRequest.writeVerifyJson(aggFile, resultFile, rules) + val verifyResponseFile = File(temporaryDir, "verify-result.json") + verifyRequest.writeVerifyJson(groupedRules, verifyResponseFile) exec.javaexec { e -> e.mainClass.set("com.intellij.rt.coverage.verify.Main") e.classpath = classpath e.args = mutableListOf(verifyRequest.canonicalPath) } - val violations = resultFile.readJsonObject() - if (violations.isNotEmpty()) { + val violations = verifyResponseFile.readJsonObject() + return if (violations.isNotEmpty()) { val result = processViolationsModel(violations) raiseViolations(result, rules) + } else { + null + } +} + +private data class RulesGroup( + val aggFile: File, + val filters: KoverClassFilter, + val rules: List +) + +private fun Task.groupRules( + commonClassFilter: KoverClassFilter, + allRules: List +): List { + val result = mutableListOf() + val commonAggFile = File(temporaryDir, "aggregated-common.ic") + val commonRules = mutableListOf() + result += RulesGroup(commonAggFile, commonClassFilter, commonRules) + + allRules.forEach { + if (it.filters == null) { + commonRules += it + } else { + result += RulesGroup(File(temporaryDir, "aggregated-${result.size}.ic"), it.filters, listOf(it)) + } } + return result } /* @@ -68,24 +96,28 @@ internal fun Task.intellijVerification( } } */ -private fun File.writeAggVerifyJson( - report: Report, - aggReport: File +private fun File.writeAggJson( + projectFiles: Map, + groups: List ) { writeJsonObject(mapOf( - "reports" to report.files.map { mapOf("ic" to it) }, - "modules" to report.projects.map { mapOf("sources" to it.sources, "output" to it.outputs) }, - "result" to listOf(mapOf( - "aggregatedReportFile" to aggReport, - "filters" to mutableMapOf().also { - if (report.includes.isNotEmpty()) { - it["include"] = mapOf("classes" to report.includes.map { c -> c.wildcardsToRegex() }) - } - if (report.excludes.isNotEmpty()) { - it["exclude"] = mapOf("classes" to report.excludes.map { c -> c.wildcardsToRegex() }) + "reports" to projectFiles.flatMap { it.value.binaryReportFiles }.map { mapOf("ic" to it) }, + "modules" to projectFiles.map { mapOf("sources" to it.value.sources, "output" to it.value.outputs) }, + "result" to groups.map { group -> + mapOf( + "aggregatedReportFile" to group.aggFile, + "filters" to mutableMapOf().also { + if (group.filters.includes.isNotEmpty()) { + it["include"] = + mapOf("classes" to group.filters.includes.map { c -> c.wildcardsToRegex() }) + } + if (group.filters.excludes.isNotEmpty()) { + it["exclude"] = + mapOf("classes" to group.filters.excludes.map { c -> c.wildcardsToRegex() }) + } } - } - )) + ) + } )) } @@ -110,51 +142,70 @@ private fun File.writeAggVerifyJson( } */ private fun File.writeVerifyJson( - aggReport: File, + groups: List, result: File, - rules: Iterable ) { - writeJsonObject(mapOf( - "resultFile" to result, - "rules" to rules.map { rule -> - mapOf( + val rulesArray = mutableListOf>() + + groups.forEach { group -> + group.rules.forEach { rule -> + rulesArray += mapOf( "id" to rule.id, - "aggregatedReportFile" to aggReport, - "targetType" to "ALL", + "aggregatedReportFile" to group.aggFile, + "targetType" to rule.targetToReporter(), "bounds" to rule.bounds.map { b -> mutableMapOf( "id" to b.id, - "counter" to "LINE", - "valueType" to b.valueTypeConverted(), + "counter" to b.counterToReporter(), + "valueType" to b.valueTypeToReporter(), ).also { val minValue = b.minValue val maxValue = b.maxValue if (minValue != null) { - it["min"] = b.valueAligned(minValue) + it["min"] = b.valueToReporter(minValue) } if (maxValue != null) { - it["max"] = b.valueAligned(maxValue) + it["max"] = b.valueToReporter(maxValue) } } } ) } - )) + } + + writeJsonObject(mapOf("resultFile" to result, "rules" to rulesArray)) +} + +private fun ReportVerificationRule.targetToReporter(): String { + return when (target) { + VerificationTarget.ALL -> "ALL" + VerificationTarget.CLASS -> "CLASS" + VerificationTarget.PACKAGE -> "PACKAGE" + } +} + +private fun ReportVerificationBound.counterToReporter(): String { + return when (counter) { + CounterType.LINE -> "LINE" + CounterType.INSTRUCTION -> "INSTRUCTION" + CounterType.BRANCH -> "BRANCH" + } } -private fun VerificationBound.valueTypeConverted(): String { +private fun ReportVerificationBound.valueTypeToReporter(): String { return when (valueType) { - VerificationValueType.COVERED_LINES_COUNT -> "COVERED" - VerificationValueType.MISSED_LINES_COUNT -> "MISSED" - COVERED_LINES_PERCENTAGE -> "COVERED_RATE" + COVERED_COUNT -> "COVERED" + MISSED_COUNT -> "MISSED" + COVERED_PERCENTAGE -> "COVERED_RATE" + MISSED_PERCENTAGE -> "MISSED_RATE" } } -private fun VerificationBound.valueAligned(value: Int): BigDecimal { - return if (valueType == COVERED_LINES_PERCENTAGE) { - value.toBigDecimal().divide(ONE_HUNDRED, 6, RoundingMode.HALF_UP) +private fun ReportVerificationBound.valueToReporter(value: BigDecimal): BigDecimal { + return if (valueType == COVERED_PERCENTAGE || valueType == MISSED_PERCENTAGE) { + value.divide(ONE_HUNDRED, 6, RoundingMode.HALF_UP) } else { - value.toBigDecimal() + value } } @@ -180,31 +231,33 @@ private fun processViolationsModel(violations: Map): ViolationsResu rules[ruleId.toInt()] = RuleViolation(bounds) } } catch (e: Throwable) { - throw GradleException("Error occurred while parsing verifier result", e) + throw GradleException("Error occurred while parsing verifier result", e) } return ViolationsResult(rules) } -private fun raiseViolations(result: ViolationsResult, rules: Iterable) { +private fun raiseViolations(result: ViolationsResult, rules: Iterable): String { val messageBuilder = StringBuilder() val rulesMap = rules.associateBy { r -> r.id } result.ruleViolations.forEach { (ruleId, rv) -> - val rule = rulesMap[ruleId] ?: throw Exception("") + val rule = rulesMap[ruleId] + ?: throw Exception("Error occurred while parsing verification error: unmapped rule with ID $ruleId") val ruleName = if (rule.name != null) " '${rule.name}' " else " " val boundsMap = rule.bounds.associateBy { b -> b.id } val boundMessages = rv.boundViolations.mapNotNull { (boundId, v) -> - val bound = boundsMap[boundId] ?: throw Exception("") + val bound = boundsMap[boundId] + ?: throw Exception("Error occurred while parsing verification error: unmapped bound with ID $boundId") val minViolation = v.min["all"] val maxViolation = v.max["all"] if (minViolation != null) { - "${bound.readableValueType} is ${minViolation.toRateIfNeeded(bound)}, but expected minimum is ${bound.minValue}" + "${bound.readableValueType} is ${minViolation.fromRateIfNeeded(bound)}, but expected minimum is ${bound.minValue}" } else if (maxViolation != null) { - "${bound.readableValueType} is ${maxViolation.toRateIfNeeded(bound)}, but expected maximum is ${bound.maxValue}" + "${bound.readableValueType} is ${maxViolation.fromRateIfNeeded(bound)}, but expected maximum is ${bound.maxValue}" } else { null } @@ -219,22 +272,23 @@ private fun raiseViolations(result: ViolationsResult, rules: Iterable "covered lines count" - VerificationValueType.MISSED_LINES_COUNT -> "missed lines count" - COVERED_LINES_PERCENTAGE -> "covered lines percentage" + COVERED_COUNT -> "covered lines count" + MISSED_COUNT -> "missed lines count" + COVERED_PERCENTAGE -> "covered lines percentage" + MISSED_PERCENTAGE -> "missed lines percentage" } diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/Versions.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/Versions.kt deleted file mode 100644 index 320ee50d..00000000 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/Versions.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.engines.intellij - -val minimalIntellijVersion = IntellijEngineVersion(1, 0, 668) -val defaultIntellijVersion = minimalIntellijVersion - -data class IntellijEngineVersion(val major: Int, val minor: Int, val build: Int): Comparable { - override fun compareTo(other: IntellijEngineVersion): Int { - var compared = major.compareTo(other.major) - if (compared != 0) { - return compared - } - compared = minor.compareTo(other.minor) - if (compared != 0) { - return compared - } - return build.compareTo(other.build) - } - - override fun toString(): String { - return "$major.$minor.$build" - } - - companion object { - fun parseOrNull(string: String): IntellijEngineVersion? { - val parts = string.split('.') - if (parts.size != 3) return null - val major = parts[0].toIntOrNull() ?: return null - val minor = parts[1].toIntOrNull() ?: return null - val build = parts[2].toIntOrNull() ?: return null - - return IntellijEngineVersion(major, minor, build) - } - } -} - - diff --git a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt index c6d18b74..1bc31b02 100644 --- a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt +++ b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt @@ -1,73 +1,24 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.kover.engines.jacoco import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.CoverageAgent -import org.gradle.api.* -import org.gradle.api.artifacts.* -import org.gradle.api.file.* -import org.gradle.api.provider.* -import org.gradle.configurationcache.extensions.* +import org.gradle.api.Task import java.io.* -internal fun Project.createJacocoAgent(koverExtension: KoverExtension): CoverageAgent { - val jacocoConfig = createJacocoConfig(koverExtension) - val archiveOperations: ArchiveOperations = serviceOf() - val jarProvider = provider { - val fatJar = jacocoConfig.fileCollection { it.name == "org.jacoco.agent" }.singleFile - archiveOperations.zipTree(fatJar).filter { it.name == "jacocoagent.jar" }.singleFile - } - return JacocoAgent(jacocoConfig, jarProvider) -} - -private class JacocoAgent( - override val classpath: FileCollection, - private val jarProvider: Provider -) : CoverageAgent { - override val engine: CoverageEngine = CoverageEngine.JACOCO - - override fun buildCommandLineArgs(task: Task, extension: KoverTaskExtension): MutableList { - return mutableListOf("-javaagent:${jarProvider.get().canonicalPath}=${agentArgs(extension)}") - } - - private fun agentArgs(extension: KoverTaskExtension): String { - val binary = extension.binaryReportFile.get() - binary.parentFile.mkdirs() +internal fun Task.buildJacocoAgentJvmArgs(jarFile: File, reportFile: File, classFilter: KoverClassFilter): MutableList { + val agentArgs = listOfNotNull( + "destfile=${reportFile.canonicalPath},append=true,inclnolocationclasses=false,dumponexit=true,output=file,jmx=false", + classFilter.includes.joinToFilterString("includes"), + classFilter.excludes.joinToFilterString("excludes") + ).joinToString(",") - return listOfNotNull( - "destfile=${binary.canonicalPath}", - "append=true", - "inclnolocationclasses=false", - "dumponexit=true", - "output=file", - "jmx=false", - extension.includes.joinToFilterString("includes"), - extension.excludes.joinToFilterString("excludes") - ).joinToString(",") - } + return mutableListOf("-javaagent:${jarFile.canonicalPath}=$agentArgs") } private fun List.joinToFilterString(name: String): String? { if (isEmpty()) return null return name + "=" + joinToString(":") } - -private fun Project.createJacocoConfig(koverExtension: KoverExtension): Configuration { - val config = project.configurations.create("JacocoKoverConfig") - config.isVisible = false - config.isTransitive = true - config.description = "Kotlin Kover Plugin configuration for JaCoCo agent and reporter" - - config.defaultDependencies { dependencies -> - dependencies.add( - this.dependencies.create("org.jacoco:org.jacoco.agent:${koverExtension.jacocoEngineVersion.get()}") - ) - dependencies.add( - this.dependencies.create("org.jacoco:org.jacoco.ant:${koverExtension.jacocoEngineVersion.get()}") - ) - } - return config -} diff --git a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoEngine.kt b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoEngine.kt new file mode 100644 index 00000000..0942c846 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoEngine.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.kover.engines.jacoco + +internal fun getJacocoDependencies(engineVersion: String): List { + return listOf("org.jacoco:org.jacoco.agent:$engineVersion", "org.jacoco:org.jacoco.ant:$engineVersion") +} + + diff --git a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt index 9cdb07ee..655f339a 100644 --- a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt +++ b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt @@ -1,14 +1,14 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.kover.engines.jacoco import groovy.lang.* import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.* +import kotlinx.kover.engines.commons.ReportVerificationRule import kotlinx.kover.engines.commons.ONE_HUNDRED -import kotlinx.kover.engines.commons.Report +import kotlinx.kover.tasks.* import org.gradle.api.* import org.gradle.api.file.* import org.gradle.internal.reflect.* @@ -16,51 +16,13 @@ import java.io.* import java.math.* import java.util.* -private fun Task.callJacocoAntReportTask( - report: Report, - classpath: FileCollection, - block: GroovyObject.() -> Unit -) { - val builder = ant - builder.invokeMethod( - "taskdef", - mapOf( - "name" to "jacocoReport", - "classname" to "org.jacoco.ant.ReportTask", - "classpath" to classpath.asPath - ) - ) - - val sources: MutableList = mutableListOf() - val outputs: MutableList = mutableListOf() - report.projects.forEach { projectInfo -> - sources.addAll(projectInfo.sources) - outputs.addAll(projectInfo.outputs) - } - - builder.invokeWithBody("jacocoReport") { - invokeWithBody("executiondata") { - project.files(report.files).addToAntBuilder(this, "resources") - } - invokeWithBody("structure", mapOf("name" to project.path)) { - invokeWithBody("sourcefiles") { - project.files(sources).addToAntBuilder(this, "resources") - } - invokeWithBody("classfiles") { - project.files(outputs).addToAntBuilder(this, "resources") - } - } - block() - } -} - internal fun Task.jacocoReport( - report: Report, + projectFiles: Map, xmlFile: File?, htmlDir: File?, classpath: FileCollection ) { - callJacocoAntReportTask(report, classpath) { + callJacocoAntReportTask(projectFiles, classpath) { if (xmlFile != null) { xmlFile.parentFile.mkdirs() invokeMethod("xml", mapOf("destfile" to xmlFile)) @@ -74,30 +36,35 @@ internal fun Task.jacocoReport( internal fun Task.jacocoVerification( - report: Report, - rules: Iterable, + projectFiles: Map, + rules: List, classpath: FileCollection -) { - callJacocoAntReportTask(report, classpath) { +): String? { + callJacocoAntReportTask(projectFiles, classpath) { invokeWithBody("check", mapOf("failonviolation" to "false", "violationsproperty" to "jacocoErrors")) { rules.forEach { invokeWithBody("rule", mapOf("element" to "BUNDLE")) { it.bounds.forEach { b -> val limitArgs = mutableMapOf("counter" to "LINE") - var min: BigDecimal? = b.minValue?.toBigDecimal() - var max: BigDecimal? = b.maxValue?.toBigDecimal() + var min: BigDecimal? = b.minValue + var max: BigDecimal? = b.maxValue when (b.valueType) { - VerificationValueType.COVERED_LINES_COUNT -> { + VerificationValueType.COVERED_COUNT -> { limitArgs["value"] = "COVEREDCOUNT" } - VerificationValueType.MISSED_LINES_COUNT -> { + VerificationValueType.MISSED_COUNT -> { limitArgs["value"] = "MISSEDCOUNT" } - VerificationValueType.COVERED_LINES_PERCENTAGE -> { + VerificationValueType.COVERED_PERCENTAGE -> { limitArgs["value"] = "COVEREDRATIO" min = min?.divide(ONE_HUNDRED) max = max?.divide(ONE_HUNDRED) } + VerificationValueType.MISSED_PERCENTAGE -> { + limitArgs["value"] = "MISSEDRATIO" + min = min?.divide(ONE_HUNDRED) + max = max?.divide(ONE_HUNDRED) + } } if (min != null) { @@ -114,7 +81,47 @@ internal fun Task.jacocoVerification( } } - ant.violations?.let { throw GradleException(it) } + return ant.violations +} + +private fun Task.callJacocoAntReportTask( + projectFiles: Map, + classpath: FileCollection, + block: GroovyObject.() -> Unit +) { + val builder = ant + builder.invokeMethod( + "taskdef", + mapOf( + "name" to "jacocoReport", + "classname" to "org.jacoco.ant.ReportTask", + "classpath" to classpath.asPath + ) + ) + + val sources: MutableList = mutableListOf() + val outputs: MutableList = mutableListOf() + val binaries: MutableList = mutableListOf() + projectFiles.forEach { pf -> + binaries += pf.value.binaryReportFiles + sources += pf.value.sources + outputs += pf.value.outputs + } + + builder.invokeWithBody("jacocoReport") { + invokeWithBody("executiondata") { + project.files(binaries).addToAntBuilder(this, "resources") + } + invokeWithBody("structure", mapOf("name" to project.path)) { + invokeWithBody("sourcefiles") { + project.files(sources).addToAntBuilder(this, "resources") + } + invokeWithBody("classfiles") { + project.files(outputs).addToAntBuilder(this, "resources") + } + } + block() + } } private val GroovyObject.violations: String? diff --git a/src/main/kotlin/kotlinx/kover/lookup/DirsLookup.kt b/src/main/kotlin/kotlinx/kover/lookup/DirsLookup.kt new file mode 100644 index 00000000..f3b44f00 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/lookup/DirsLookup.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.kover.lookup + +import kotlinx.kover.api.* +import kotlinx.kover.lookup.adapters.* +import org.gradle.api.Project +import org.gradle.api.file.* +import java.io.File + + +open class ProjectDirectories(val sources: FileCollection, val outputs: FileCollection) + +internal object DirsLookup { + private val adapters: List = listOf( + JavaPluginAdapter(), + AndroidPluginAdapter(), + KotlinMultiplatformPluginAdapter(), + KotlinAndroidPluginAdapter() + ) + + @Suppress("UNUSED_PARAMETER") + fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilter): ProjectDirectories { + val srcDirs = HashMap() + val outDirs = HashMap() + + adapters.forEach { + val (src, out) = it.lookupSafe(project, sourceSetFilters) + srcDirs += src.map { file -> file.canonicalPath to file } + outDirs += out.map { file -> file.canonicalPath to file } + } + + val src = srcDirs.values.filter { it.exists() && it.isDirectory } + val out = outDirs.values.filter { it.exists() && it.isDirectory } + + return ProjectDirectories(project.files(src), project.files(out)) + } + + +} + +/** + * Allows to get a list of directories with source code and generated class-files that used by various Gradle plugins. + */ +internal abstract class LookupAdapter { + /** + * Find the sources and output directories used by the plugin whose support is implemented in the child class. + */ + protected abstract fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilter): Dirs + + fun lookupSafe(project: Project, sourceSetFilters: KoverSourceSetFilter): Dirs { + return try { + lookup(project, sourceSetFilters) + } catch (e: Throwable) { + when (e) { + is NoSuchMethodError, is NoSuchFieldError, is ClassNotFoundException, is NoClassDefFoundError -> { + project.logger.info("Problem occurred in Kover source set adapter", e) + Dirs() + } + else -> throw e + } + } + } + + protected fun filterSourceSet(name: String, sourceSetFilters: KoverSourceSetFilter): Boolean { + if (sourceSetFilters.excludes.contains(name)) { + return false + } + if (sourceSetFilters.excludeTests && (name == "test" || name.endsWith("Test") || name.startsWith("test") || name.startsWith("androidTest"))) { + return false + } + return true + } + + data class Dirs(val sources: List = emptyList(), val outputs: List = emptyList()) +} diff --git a/src/main/kotlin/kotlinx/kover/lookup/adapters/AndroidPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/lookup/adapters/AndroidPluginAdapter.kt new file mode 100644 index 00000000..a3965572 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/lookup/adapters/AndroidPluginAdapter.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.lookup.adapters + +import com.android.build.gradle.* +import kotlinx.kover.api.* +import kotlinx.kover.lookup.* +import org.gradle.api.* + +/** + * Adapter to get sources and outputs of Android Gradle plugin. + * Required to support java sources for android. + */ +internal class AndroidPluginAdapter : LookupAdapter() { + override fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilter): Dirs { + project.plugins.findPlugin("android") ?: return Dirs() + + val extension = project.extensions.findByType(BaseExtension::class.java) ?: return Dirs() + + val sourceDirs = extension.sourceSets.asSequence() + .filter { filterSourceSet(it.name, sourceSetFilters) } + .map { it.java }.toList() + .flatMap { it.srcDirs } + + return Dirs(sourceDirs, emptyList()) + } +} diff --git a/src/main/kotlin/kotlinx/kover/lookup/adapters/JavaPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/lookup/adapters/JavaPluginAdapter.kt new file mode 100644 index 00000000..d7989a41 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/lookup/adapters/JavaPluginAdapter.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.lookup.adapters + +import kotlinx.kover.api.* +import kotlinx.kover.lookup.LookupAdapter +import org.gradle.api.* +import org.gradle.api.tasks.* + +/** + * Adapter to get sources and outputs of java plugin. + * Required to support java sources. + */ +internal class JavaPluginAdapter : LookupAdapter() { + + override fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilter): Dirs { + project.plugins.findPlugin("java") ?: return Dirs() + + val sourceSetContainer = project.extensions.findByType( + SourceSetContainer::class.java + ) ?: return Dirs() + + val sourceSets = sourceSetContainer.filter { filterSourceSet(it.name, sourceSetFilters) } + + val sourceDirs = sourceSets.flatMap { it.allSource.srcDirs } + val outputDirs = sourceSets.flatMap { it.output.classesDirs } + + return Dirs(sourceDirs, outputDirs) + } + +} diff --git a/src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinAndroidPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinAndroidPluginAdapter.kt new file mode 100644 index 00000000..5ac17979 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinAndroidPluginAdapter.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.lookup.adapters + +import kotlinx.kover.api.* +import kotlinx.kover.lookup.LookupAdapter +import org.gradle.api.* +import org.jetbrains.kotlin.gradle.dsl.* + +/** + * Adapter to get sources and outputs of Kotlin Android Gradle plugin. + * Required to support kotlin sources for android. + */ +internal class KotlinAndroidPluginAdapter : LookupAdapter() { + + override fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilter): Dirs { + project.plugins.findPlugin("kotlin-android") ?: return Dirs() + + val extension = project.extensions.findByType(KotlinAndroidProjectExtension::class.java) ?: return Dirs() + + val sourceDirs = extension.target.compilations + .filter { !it.name.endsWith("Test") } + .flatMap { it.allKotlinSourceSets } + .map { it.kotlin } + .flatMap { it.srcDirs } + + val outputDirs = extension.target.compilations + .filter { !it.name.endsWith("Test") } + .flatMap { it.output.classesDirs } + + return Dirs(sourceDirs, outputDirs) + + } + +} diff --git a/src/main/kotlin/kotlinx/kover/adapters/KotlinMultiplatformPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinMultiplatformPluginAdapter.kt similarity index 50% rename from src/main/kotlin/kotlinx/kover/adapters/KotlinMultiplatformPluginAdapter.kt rename to src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinMultiplatformPluginAdapter.kt index 4830b8b2..a9d0c2db 100644 --- a/src/main/kotlin/kotlinx/kover/adapters/KotlinMultiplatformPluginAdapter.kt +++ b/src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinMultiplatformPluginAdapter.kt @@ -1,46 +1,47 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.kover.adapters +package kotlinx.kover.lookup.adapters import groovy.lang.* -import kotlinx.kover.adapters.api.* +import kotlinx.kover.api.* +import kotlinx.kover.lookup.LookupAdapter import org.gradle.api.* import org.gradle.api.file.* import org.gradle.internal.metaobject.* import org.jetbrains.kotlin.gradle.dsl.* import org.jetbrains.kotlin.gradle.plugin.* -class KotlinMultiplatformPluginAdapter : CompilationPluginAdapter { +/** + * Adapter to get sources and outputs of Kotlin Multi-Platform Gradle plugin. + * Required to support kotlin sources for multiplatform projects. + */ +internal class KotlinMultiplatformPluginAdapter : LookupAdapter() { - override fun findDirs(project: Project): PluginDirs { - return safe(project) { - this.plugins.findPlugin("kotlin-multiplatform") ?: return@safe PluginDirs(emptyList(), emptyList()) + override fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilter): Dirs { + project.plugins.findPlugin("kotlin-multiplatform") ?: return Dirs() - val extension = try { - project.extensions.findByType( - KotlinMultiplatformExtension::class.java - ) ?: return@safe PluginDirs(emptyList(), emptyList()) - } catch (e: ClassNotFoundException) { - return findByReflection(project) - } catch (e: NoClassDefFoundError) { - return findByReflection(project) - } + val extension = try { + project.extensions.findByType(KotlinMultiplatformExtension::class.java) ?: return Dirs() + } catch (e: ClassNotFoundException) { + return findByReflection(project) + } catch (e: NoClassDefFoundError) { + return findByReflection(project) + } - val targets = - extension.targets.filter { it.platformType == KotlinPlatformType.jvm || it.platformType == KotlinPlatformType.androidJvm } + val targets = + extension.targets.filter { it.platformType == KotlinPlatformType.jvm || it.platformType == KotlinPlatformType.androidJvm } - val compilations = targets.flatMap { it.compilations.filter { c -> c.name != "test" } } - val sourceDirs = - compilations.asSequence().flatMap { it.allKotlinSourceSets }.map { it.kotlin }.flatMap { it.srcDirs } - .toList() + val compilations = targets.flatMap { it.compilations.filter { c -> c.name != "test" } } + val sourceDirs = + compilations.asSequence().flatMap { it.allKotlinSourceSets }.map { it.kotlin }.flatMap { it.srcDirs } + .toList() - val outputDirs = - compilations.asSequence().flatMap { it.output.classesDirs }.toList() + val outputDirs = + compilations.asSequence().flatMap { it.output.classesDirs }.toList() - return@safe PluginDirs(sourceDirs, outputDirs) - } + return Dirs(sourceDirs, outputDirs) } /* @@ -49,12 +50,9 @@ class KotlinMultiplatformPluginAdapter : CompilationPluginAdapter { * Therefore, the only way to work with such an object is to use reflection. */ @Suppress("UNCHECKED_CAST") - fun findByReflection(project: Project): PluginDirs { + fun findByReflection(project: Project): Dirs { val extension = - project.extensions.findByName("kotlin")?.let { BeanDynamicObject(it) } ?: return PluginDirs( - emptyList(), - emptyList() - ) + project.extensions.findByName("kotlin")?.let { BeanDynamicObject(it) } ?: return Dirs() val targets = (extension.getProperty("targets") as NamedDomainObjectCollection).filter { val platformTypeName = (it.getProperty("platformType") as Named).name @@ -75,7 +73,7 @@ class KotlinMultiplatformPluginAdapter : CompilationPluginAdapter { compilations.asSequence().map { BeanDynamicObject(it.getProperty("output")) } .flatMap { it.getProperty("classesDirs") as FileCollection }.toList() - return PluginDirs(sourceDirs, outputDirs) + return Dirs(sourceDirs, outputDirs) } } diff --git a/src/main/kotlin/kotlinx/kover/tasks/Deprecations.kt b/src/main/kotlin/kotlinx/kover/tasks/Deprecations.kt new file mode 100644 index 00000000..b3f1d79c --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/Deprecations.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.tasks + +import kotlinx.kover.api.* +import org.gradle.api.* +import org.gradle.api.file.* +import org.gradle.api.tasks.Internal + +@Deprecated( + message = "Class was removed in Kover API version 2, use Kover project extension instead. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR +) +open class KoverHtmlReportTask : DefaultTask() { + @Deprecated( + message = "Property was removed in Kover API version 2, use property 'htmlReport { reportDir }' in Kover project extension instead. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + @get:Internal + val htmlReportDir: DirectoryProperty = project.objects.directoryProperty() +} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverCollectingTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverCollectingTask.kt deleted file mode 100644 index b8cd2fab..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverCollectingTask.kt +++ /dev/null @@ -1,49 +0,0 @@ -package kotlinx.kover.tasks - -import org.gradle.api.* -import org.gradle.api.file.* -import org.gradle.api.tasks.* - -open class KoverCollectingTask : DefaultTask() { - /** - * Specifies directory path for collecting of all XML and HTML reports from all projects. - */ - @get:OutputDirectory - val outputDir: DirectoryProperty = project.objects.directoryProperty() - - @get:Internal - internal var xmlFiles: MutableMap = mutableMapOf() - - @get:Internal - internal var htmlDirs: MutableMap = mutableMapOf() - - - @TaskAction - fun collect() { - project.copy { - it.into(outputDir) - xmlFiles.forEach { (p, f) -> - it.from(f) { c -> - c.rename { "${p.pathAsFilename()}.xml" } - } - } - } - - - htmlDirs.forEach { (p, d) -> - val name = p.pathAsFilename() - - // delete directory for HTML reports so that the old reports do not overlap with the new ones - project.delete(outputDir.dir("html/$name")) - - project.copy { - it.from(d) - it.into(outputDir.dir("html/$name")) - } - } - } - - private fun String.pathAsFilename(): String { - return if (this == ":") "_root_" else replace(':', '_') - } -} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReport.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReport.kt deleted file mode 100644 index f1055831..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReport.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.tasks - -import kotlinx.kover.api.* -import kotlinx.kover.engines.intellij.* -import kotlinx.kover.engines.jacoco.* -import org.gradle.api.file.* -import org.gradle.api.tasks.* - -@CacheableTask -open class KoverMergedHtmlReportTask : KoverMergedTask() { - /** - * Specifies directory path of generated HTML report. - */ - @get:OutputDirectory - val htmlReportDir: DirectoryProperty = project.objects.directoryProperty() - - @TaskAction - fun generate() { - val htmlDirFile = htmlReportDir.get().asFile - - if (coverageEngine.get() == CoverageEngine.INTELLIJ) { - intellijReport( - exec, - report(), - null, - htmlDirFile, - classpath.get() - ) - } else { - jacocoReport( - report(), - null, - htmlDirFile, - classpath.get(), - ) - } - logger.lifecycle("Kover: merged HTML report file://${htmlDirFile.canonicalPath}/index.html") - } -} - -@CacheableTask -open class KoverHtmlReportTask : KoverProjectTask() { - private val projectPath = project.path - - /** - * Specifies directory path of generated HTML report. - */ - @get:OutputDirectory - val htmlReportDir: DirectoryProperty = project.objects.directoryProperty() - - @TaskAction - fun generate() { - val htmlDirFile = htmlReportDir.get().asFile - - if (coverageEngine.get() == CoverageEngine.INTELLIJ) { - intellijReport( - exec, - report(), - null, - htmlDirFile, - classpath.get() - ) - } else { - jacocoReport( - report(), - null, - htmlDirFile, - classpath.get(), - ) - } - logger.lifecycle("Kover: HTML report for '$projectPath' file://${htmlDirFile.canonicalPath}/index.html") - } -} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlTask.kt new file mode 100644 index 00000000..ff7f324b --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlTask.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.tasks + +import kotlinx.kover.api.* +import kotlinx.kover.engines.commons.* +import org.gradle.api.file.* +import org.gradle.api.tasks.* + +// TODO make internal in 0.7 version - for now it public to save access to deprecated fields to print deprecation message +@CacheableTask +public open class KoverHtmlTask : KoverReportTask() { + @get:OutputDirectory + internal val reportDir: DirectoryProperty = project.objects.directoryProperty() + + @TaskAction + fun generate() { + val reportDirFile = reportDir.get().asFile + + val projectFiles = files.get() + EngineManager.report( + engine.get(), + this, + exec, + projectFiles, + classFilter.get(), + null, + reportDirFile + ) + + if (projectFiles.keys.size > 1) { + logger.lifecycle("Kover: HTML merged report for '$projectPath' file://${reportDirFile.canonicalPath}/index.html \n merged projects ${projectFiles.keys}") + } else { + logger.lifecycle("Kover: HTML report for '$projectPath' file://${reportDirFile.canonicalPath}/index.html") + } + } + + + // DEPRECATIONS + // TODO delete in 0.7 version + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + val htmlReportDir: DirectoryProperty = project.objects.directoryProperty() + + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2, use property 'filters { classes { includes } }' in Kover project extension instead. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + public var includes: List = emptyList() + + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2, use property 'filters { classes { excludes } }' in Kover project extension instead. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + public var excludes: List = emptyList() +} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverMergedTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverMergedTask.kt deleted file mode 100644 index 29753a0a..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverMergedTask.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.tasks - -import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.Report -import org.gradle.api.* -import org.gradle.api.file.* -import org.gradle.api.model.* -import org.gradle.api.provider.* -import org.gradle.api.tasks.* -import org.gradle.configurationcache.extensions.* -import org.gradle.process.* -import java.io.* - -@CacheableTask -open class KoverMergedTask : DefaultTask() { - @get:Nested - val binaryReportFiles: MapProperty = - project.objects.mapProperty(String::class.java, NestedFiles::class.java) - - @get:Nested - val srcDirs: MapProperty = - project.objects.mapProperty(String::class.java, NestedFiles::class.java) - - @get:Nested - val outputDirs: MapProperty = - project.objects.mapProperty(String::class.java, NestedFiles::class.java) - - @get:Input - internal val coverageEngine: Property = project.objects.property(CoverageEngine::class.java) - - @get:Classpath - internal val classpath: Property = project.objects.property(FileCollection::class.java) - - /** - * Specifies class inclusion rules into report. - * Only the specified classes may be present in the report. - * Exclusion rules have priority over inclusion ones. - * - * Inclusion rules are represented as a set of fully-qualified names of the classes being instrumented. - * It's possible to use `*` and `?` wildcards. - * - * **Works only with IntelliJ Coverage Engine.** - */ - @get:Input - public var includes: List = emptyList() - - /** - * Specifies class exclusion rules into report. - * The specified classes will definitely be missing from report. - * Exclusion rules have priority over inclusion ones. - * - * Exclusion rules are represented as a set of fully-qualified names of the classes being instrumented. - * It's possible to use `*` and `?` wildcards. - * - * **Works only with IntelliJ Coverage Engine.** - */ - @get:Input - public var excludes: List = emptyList() - - // exec operations to launch Java applications - @get:Internal - protected val exec: ExecOperations = project.serviceOf() - - internal fun report(): Report { - val binariesMap = binaryReportFiles.get() - val sourcesMap = srcDirs.get() - val outputsMap = outputDirs.get() - - val projectsPaths = sourcesMap.keys - - val reportFiles: MutableList = mutableListOf() - val projects: MutableList = mutableListOf() - - projectsPaths.map { projectPath -> - reportFiles += binariesMap.getValue(projectPath).files.get() - projects += ProjectInfo( - sources = sourcesMap.getValue(projectPath).files.get(), - outputs = outputsMap.getValue(projectPath).files.get() - ) - } - - return Report(reportFiles, projects, includes, excludes) - } -} - - -class NestedFiles(objects: ObjectFactory, files: Provider) { - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - val files: Property = objects.property(FileCollection::class.java).also { it.set(files) } -} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverProjectTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverProjectTask.kt deleted file mode 100644 index a9b39d65..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverProjectTask.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.tasks - -import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.ProjectInfo -import kotlinx.kover.engines.commons.Report -import org.gradle.api.* -import org.gradle.api.file.* -import org.gradle.api.provider.* -import org.gradle.api.tasks.* -import org.gradle.configurationcache.extensions.* -import org.gradle.process.* - -abstract class KoverProjectTask : DefaultTask() { - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - val binaryReportFiles: Property = project.objects.property(FileCollection::class.java) - - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - val srcDirs: Property = project.objects.property(FileCollection::class.java) - - @get:Classpath - val outputDirs: Property = project.objects.property(FileCollection::class.java) - - @get:Input - internal val coverageEngine: Property = project.objects.property(CoverageEngine::class.java) - - @get:Classpath - internal val classpath: Property = project.objects.property(FileCollection::class.java) - - /** - * Specifies class inclusion rules into report. - * Only the specified classes may be present in the report. - * Exclusion rules have priority over inclusion ones. - * - * Inclusion rules are represented as a set of fully-qualified names of the classes being instrumented. - * It's possible to use `*` and `?` wildcards. - * - * **Works only with IntelliJ Coverage Engine.** - */ - @get:Input - public var includes: List = emptyList() - - /** - * Specifies class exclusion rules into report. - * The specified classes will definitely be missing from report. - * Exclusion rules have priority over inclusion ones. - * - * Exclusion rules are represented as a set of fully-qualified names of the classes being instrumented. - * It's possible to use `*` and `?` wildcards. - * - * **Works only with IntelliJ Coverage Engine.** - */ - @get:Input - public var excludes: List = emptyList() - - // exec operations to launch Java applications - @get:Internal - protected val exec: ExecOperations = project.serviceOf() - - internal fun report(): Report { - return Report( - binaryReportFiles.get().toList(), - listOf(ProjectInfo(srcDirs.get(), outputDirs.get())), - includes, - excludes - ) - } -} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverReportTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverReportTask.kt new file mode 100644 index 00000000..5e5af09b --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverReportTask.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.tasks + +import kotlinx.kover.api.* +import org.gradle.api.* +import org.gradle.api.file.* +import org.gradle.api.provider.* +import org.gradle.api.tasks.* +import org.gradle.configurationcache.extensions.* +import org.gradle.process.* +import java.io.* + +// TODO make internal in 0.7 version - for now it public to save access to deprecated fields to print deprecation message +public abstract class KoverReportTask : DefaultTask() { + @get:Nested + internal val files: MapProperty = + project.objects.mapProperty(String::class.java, ProjectFiles::class.java) + + @get:Nested + internal val classFilter: Property = project.objects.property(KoverClassFilter::class.java) + + @get:Nested + internal val engine: Property = project.objects.property(EngineDetails::class.java) + + // exec operations to launch Java applications + @get:Internal + protected val exec: ExecOperations = project.serviceOf() + + @get:Internal + protected val projectPath: String = project.path + +} + +open class ProjectFiles( + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + val binaryReportFiles: FileCollection, + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + val sources: FileCollection, + + @get:Classpath + val outputs: FileCollection +) + +internal class EngineDetails( + @get:Nested val variant: CoverageEngineVariant, + @get:Internal val jarFile: File, + @get:Internal val classpath: FileCollection +) + + + + + + + + + + + diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverVerification.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverVerification.kt deleted file mode 100644 index 60681a87..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverVerification.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.tasks - -import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.intellij.* -import kotlinx.kover.engines.jacoco.* -import org.gradle.api.* -import org.gradle.api.file.* -import org.gradle.api.model.* -import org.gradle.api.tasks.* -import org.gradle.process.* -import javax.inject.* - -open class KoverMergedVerificationTask : KoverMergedTask() { - private val rulesInternal: MutableList = mutableListOf() - - /** - * Added verification rules for test task. - */ - @get:Nested - public val rules: List - get() = rulesInternal - - /** - * Add new coverage verification rule to check after test task execution. - */ - public fun rule(configureRule: Action) { - rulesInternal += project.objects.newInstance( - VerificationRuleImpl::class.java, - rulesInternal.size, - project.objects - ) - .also { configureRule.execute(it) } - } - - @TaskAction - fun verify() { - verify(exec, report(), coverageEngine.get(), rulesInternal, classpath.get()) - } - -} - -open class KoverVerificationTask : KoverProjectTask() { - private val rulesInternal: MutableList = mutableListOf() - - /** - * Added verification rules for test task. - */ - @get:Nested - public val rules: List - get() = rulesInternal - - /** - * Add new coverage verification rule to check after test task execution. - */ - public fun rule(configureRule: Action) { - rulesInternal += project.objects.newInstance( - VerificationRuleImpl::class.java, - rulesInternal.size, - project.objects - ) - .also { configureRule.execute(it) } - } - - @TaskAction - fun verify() { - verify(exec, report(), coverageEngine.get(), rulesInternal, classpath.get()) - } - -} - -private fun Task.verify( - exec: ExecOperations, - report: Report, - engine: CoverageEngine, - rules: List, - classpath: FileCollection -) { - if (engine == CoverageEngine.INTELLIJ) { - intellijVerification(exec, report, rules, classpath) - } else { - jacocoVerification(report, rules, classpath) - } -} - -private open class VerificationRuleImpl @Inject constructor(override val id: Int, private val objects: ObjectFactory) : - VerificationRule { - override var name: String? = null - override val bounds: MutableList = mutableListOf() - override fun bound(configureBound: Action) { - bounds += objects.newInstance(VerificationBoundImpl::class.java, bounds.size) - .also { configureBound.execute(it) } - } -} - -private open class VerificationBoundImpl @Inject constructor(override val id: Int) : VerificationBound { - override var minValue: Int? = null - override var maxValue: Int? = null - override var valueType: VerificationValueType = VerificationValueType.COVERED_LINES_PERCENTAGE -} - - - diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverVerificationTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverVerificationTask.kt new file mode 100644 index 00000000..69421c4b --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverVerificationTask.kt @@ -0,0 +1,84 @@ +package kotlinx.kover.tasks + +import kotlinx.kover.api.* +import kotlinx.kover.engines.commons.* +import org.gradle.api.* +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.* + +// TODO make internal in 0.7 version - for now it public to save access to deprecated fields to print deprecation message +@CacheableTask +public open class KoverVerificationTask : KoverReportTask() { + @get:Nested + internal val rules: ListProperty = project.objects.listProperty(VerificationRule::class.java) + + @get:OutputFile + internal val resultFile: RegularFileProperty = project.objects.fileProperty() + + @TaskAction + fun verify() { + val reportRules = convertRules() + val errors = EngineManager.verify( + engine.get(), + this, + exec, + files.get(), + classFilter.get(), + reportRules, + ) + resultFile.get().asFile.writeText(errors ?: "") + + if (errors != null) { + throw GradleException(errors) + } + } + + private fun convertRules(): List { + val result = mutableListOf() + + rules.get().forEach { rule -> + if (!rule.isEnabled) { + return@forEach + } + + val ruleBounds = mutableListOf() + rule.bounds.get().forEach { bound -> + val min = bound.minValue?.toBigDecimal() + val max = bound.maxValue?.toBigDecimal() + ruleBounds += ReportVerificationBound(ruleBounds.size, min, max, bound.counter, bound.valueType) + } + + result += ReportVerificationRule(result.size, rule.name, rule.target, rule.classFilter.orNull, ruleBounds) + } + + return result + } + + + // DEPRECATIONS + // TODO delete in 0.7 version + @Suppress("UNUSED_PARAMETER") + @Deprecated( + message = "Function was removed in Kover API version 2, move it in 'verify { }' in Kover project extension instead. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + public fun rule(configureRule: Action) { + throw Exception("Function was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}") + } + + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + public var includes: List = emptyList() + + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + public var excludes: List = emptyList() +} + diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverXmlReport.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverXmlReport.kt deleted file mode 100644 index 5e582a9e..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverXmlReport.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.tasks - -import kotlinx.kover.api.* -import kotlinx.kover.engines.intellij.* -import kotlinx.kover.engines.jacoco.* -import org.gradle.api.file.* -import org.gradle.api.tasks.* - -@CacheableTask -open class KoverMergedXmlReportTask : KoverMergedTask() { - - /** - * Specifies file path of generated XML report file with coverage data. - */ - @get:OutputFile - val xmlReportFile: RegularFileProperty = project.objects.fileProperty() - - @TaskAction - fun generate() { - if (coverageEngine.get() == CoverageEngine.INTELLIJ) { - intellijReport( - exec, - report(), - xmlReportFile.get().asFile, - null, - classpath.get() - ) - } else { - jacocoReport( - report(), - xmlReportFile.get().asFile, - null, - classpath.get() - ) - } - } -} - -@CacheableTask -open class KoverXmlReportTask : KoverProjectTask() { - - /** - * Specifies file path of generated XML report file with coverage data. - */ - @get:OutputFile - val xmlReportFile: RegularFileProperty = project.objects.fileProperty() - - @TaskAction - fun generate() { - if (coverageEngine.get() == CoverageEngine.INTELLIJ) { - intellijReport( - exec, - report(), - xmlReportFile.get().asFile, - null, - classpath.get() - ) - } else { - jacocoReport( - report(), - xmlReportFile.get().asFile, - null, - classpath.get() - ) - } - } -} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverXmlTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverXmlTask.kt new file mode 100644 index 00000000..7cfe34a9 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverXmlTask.kt @@ -0,0 +1,52 @@ +package kotlinx.kover.tasks + +import kotlinx.kover.api.* +import kotlinx.kover.engines.commons.* +import org.gradle.api.file.* +import org.gradle.api.tasks.* + +// TODO make internal in 0.7 version - for now it public to save access to deprecated fields to print deprecation message +@CacheableTask +public open class KoverXmlTask : KoverReportTask() { + @get:OutputFile + internal val reportFile: RegularFileProperty = project.objects.fileProperty() + + @TaskAction + fun generate() { + val projectFiles = files.get() + + EngineManager.report( + engine.get(), + this, + exec, + projectFiles, + classFilter.get(), + reportFile.get().asFile, + null + ) + } + + + // DEPRECATIONS + // TODO delete in 0.7 version + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + public val xmlReportFile: RegularFileProperty = project.objects.fileProperty() + + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + public var includes: List = emptyList() + + @get:Internal + @Deprecated( + message = "Property was removed in Kover API version 2. Please refer to migration guide in order to migrate: ${KoverMigrations.MIGRATION_0_5_TO_0_6}", + level = DeprecationLevel.ERROR + ) + public var excludes: List = emptyList() +}