From c9be72a84d37bbdac3d4cff60ef8177c69328ca2 Mon Sep 17 00:00:00 2001 From: Sergey Shanshin Date: Wed, 22 Dec 2021 19:15:40 +0300 Subject: [PATCH] Implemented aggregate reports Resolves #20, #43 Co-authored-by: Leonid Startsev --- README.md | 151 +++++++-- .../functional/cases/DefaultConfigTests.kt | 25 +- .../cases/InstrumentationFilteringTests.kt | 14 +- .../test/functional/cases/MultiModulesTest.kt | 66 ++++ .../functional/cases/ReportsCachingTests.kt | 55 ++-- .../test/functional/cases/utils/Commons.kt | 63 ++-- .../test/functional/cases/utils/Defaults.kt | 26 +- .../functional/core/BaseGradleScriptTest.kt | 4 +- .../kover/test/functional/core/Builder.kt | 45 +-- .../kover/test/functional/core/Runner.kt | 22 +- .../kover/test/functional/core/Types.kt | 11 +- .../kover/test/functional/core/Writer.kt | 45 ++- .../buildscripts/groovy/kjvm/submodule | 12 + .../scripts/buildscripts/groovy/kmp/root | 7 + .../scripts/buildscripts/groovy/kmp/submodule | 25 ++ .../buildscripts/kotlin/kjvm/submodule | 12 + .../scripts/buildscripts/kotlin/kmp/root | 7 + .../scripts/buildscripts/kotlin/kmp/submodule | 25 ++ .../multimodule-common/main/kotlin/Sources.kt | 24 ++ .../test/kotlin/CommonTestClass.kt | 17 + .../multimodule-user/main/kotlin/Sources.kt | 14 + .../test/kotlin/UserTestClass.kt | 17 + src/main/kotlin/kotlinx/kover/KoverPlugin.kt | 309 ++++++++++-------- src/main/kotlin/kotlinx/kover/Providers.kt | 111 +++++++ .../kotlinx/kover/api/KoverConstants.kt | 31 ++ .../kotlin/kotlinx/kover/api/KoverNames.kt | 15 - .../kover/engines/commons/AgentsFactory.kt | 19 ++ .../kover/engines/commons/CoverageAgent.kt | 11 + .../kotlinx/kover/engines/commons/Reports.kt | 7 + .../kover/engines/intellij/IntellijAgent.kt | 11 +- ...IntellijCoverage.kt => IntellijReports.kt} | 40 +-- .../kover/engines/jacoco/JacocoAgent.kt | 13 +- .../{JacocoCoverage.kt => JacocoReports.kt} | 41 +-- .../kotlinx/kover/tasks/KoverAggregateTask.kt | 84 +++++ ...gTask.kt => KoverCollectingModulesTask.kt} | 2 +- ...erHtmlReportTask.kt => KoverHtmlReport.kt} | 42 ++- ...{KoverCommonTask.kt => KoverModuleTask.kt} | 22 +- .../kotlinx/kover/tasks/KoverVerification.kt | 137 ++++++++ .../kover/tasks/KoverVerificationTask.kt | 91 ------ .../kotlinx/kover/tasks/KoverXmlReport.kt | 69 ++++ .../kotlinx/kover/tasks/KoverXmlReportTask.kt | 45 --- 41 files changed, 1273 insertions(+), 514 deletions(-) create mode 100644 src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiModulesTest.kt create mode 100644 src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/submodule create mode 100644 src/functionalTest/templates/scripts/buildscripts/groovy/kmp/submodule create mode 100644 src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/submodule create mode 100644 src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/submodule create mode 100644 src/functionalTest/templates/sources/multimodule-common/main/kotlin/Sources.kt create mode 100644 src/functionalTest/templates/sources/multimodule-common/test/kotlin/CommonTestClass.kt create mode 100644 src/functionalTest/templates/sources/multimodule-user/main/kotlin/Sources.kt create mode 100644 src/functionalTest/templates/sources/multimodule-user/test/kotlin/UserTestClass.kt create mode 100644 src/main/kotlin/kotlinx/kover/Providers.kt create mode 100644 src/main/kotlin/kotlinx/kover/api/KoverConstants.kt delete mode 100644 src/main/kotlin/kotlinx/kover/api/KoverNames.kt create mode 100644 src/main/kotlin/kotlinx/kover/engines/commons/AgentsFactory.kt create mode 100644 src/main/kotlin/kotlinx/kover/engines/commons/CoverageAgent.kt create mode 100644 src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt rename src/main/kotlin/kotlinx/kover/engines/intellij/{IntellijCoverage.kt => IntellijReports.kt} (82%) rename src/main/kotlin/kotlinx/kover/engines/jacoco/{JacocoCoverage.kt => JacocoReports.kt} (83%) create mode 100644 src/main/kotlin/kotlinx/kover/tasks/KoverAggregateTask.kt rename src/main/kotlin/kotlinx/kover/tasks/{KoverCollectingTask.kt => KoverCollectingModulesTask.kt} (95%) rename src/main/kotlin/kotlinx/kover/tasks/{KoverHtmlReportTask.kt => KoverHtmlReport.kt} (52%) rename src/main/kotlin/kotlinx/kover/tasks/{KoverCommonTask.kt => KoverModuleTask.kt} (61%) create mode 100644 src/main/kotlin/kotlinx/kover/tasks/KoverVerification.kt delete mode 100644 src/main/kotlin/kotlinx/kover/tasks/KoverVerificationTask.kt create mode 100644 src/main/kotlin/kotlinx/kover/tasks/KoverXmlReport.kt delete mode 100644 src/main/kotlin/kotlinx/kover/tasks/KoverXmlReportTask.kt diff --git a/README.md b/README.md index dad1ee23..dbab0168 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ Minimal supported `Gradle` version: `6.4`. ## Table of content - [Features](#features) - [Quickstart](#quickstart) - - [Apply plugin to single-module project](#apply-plugin-to-single-module-project) + - [Apply plugin to a single-module build](#apply-plugin-to-a-single-module-build) - [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 multi-module project](#apply-plugin-to-multi-module-project) + - [Apply plugin to a multi-module build](#apply-plugin-to-a-multi-module-build) - [Configuration](#configuration) - [Configuring JVM test task](#configuring-jvm-test-task) - - [Configuring reports](#configuring-reports) - - [Configuring reports collecting](#configuring-reports-collecting) + - [Configuring aggregated reports](#configuring-aggregated-reports) + - [Configuring module reports](#configuring-module-reports) - [Configuring entire plugin](#configuring-entire-plugin) - [Verification](#verification) - [Tasks](#tasks) @@ -32,7 +32,7 @@ Minimal supported `Gradle` version: `6.4`. * Customizable filters for instrumented classes ## Quickstart -### Apply plugin to single-module project +### Apply plugin to a single-module build #### Applying plugins with the plugins DSL In top level build file @@ -94,12 +94,9 @@ apply plugin: 'kover' ``` -### Apply plugin to multi-module project -To apply the plugin to all modules in the project, you need to apply the plugin only to the root module, as shown [above](#apply-plugin-to-single-module-project). - -**There are no dependencies between tasks from different modules, they are executed independently.** - -**Cross-module tests are not supported in reports and validation yet. For each test, only the classpath belonging to the current module is taken.** +### Apply plugin to a multi-module build +To apply the plugin to all Gradle modules, you just need to apply the plugin only to the root module, as shown [above](#apply-plugin-to-a-single-module-build). +Applying the plugin to submodules if you have already applied it to the root module will cause configuration error. ## Configuration @@ -194,8 +191,11 @@ android { -### Configuring reports -If you need to change the name of the XML report file or HTML directory, you may configure the corresponding tasks +### Configuring aggregated reports +Aggregated report provides report using combined classpath and coverage stats from the module in which plugin is applied and all its submodules. + +If you need to change the name of the XML report file or HTML directory, you may configure the corresponding tasks in +the module in which the plugin is applied (usually this is the root module).
Kotlin @@ -203,12 +203,12 @@ If you need to change the name of the XML report file or HTML directory, you may ```kotlin tasks.koverHtmlReport { isEnabled = true // false to disable report generation - htmlReportDir.set(layout.buildDirectory.dir("my-reports/html-result")) + htmlReportDir.set(layout.buildDirectory.dir("my-agg-report/html-result")) } tasks.koverXmlReport { isEnabled = true // false to disable report generation - xmlReportFile.set(layout.buildDirectory.file("my-reports/result.xml")) + xmlReportFile.set(layout.buildDirectory.file("my-agg-report/result.xml")) } ```
@@ -229,15 +229,23 @@ tasks.koverXmlReport { ``` -### Configuring reports collecting -You may specify custom directory to collect reports from all modules in the build file of the root module: +### Configuring module reports +If you need to change the name of the XML report file or HTML directory for a specific module, you may configure +the corresponding tasks in this module. +
Kotlin ```kotlin -tasks.koverCollectReports { - outputDir.set(layout.buildDirectory.dir("my-reports-dir") ) +tasks.koverHtmlModuleReport { + isEnabled = true // false to disable report generation + htmlReportDir.set(layout.buildDirectory.dir("my-module-report/html-result")) +} + +tasks.koverXmlModuleReport { + isEnabled = true // false to disable report generation + xmlReportFile.set(layout.buildDirectory.file("my-module-report/result.xml")) } ```
@@ -246,8 +254,38 @@ tasks.koverCollectReports { Groovy ```groovy -tasks.koverCollectReports { - outputDir.set(layout.buildDirectory.dir("my-reports-dir") ) +tasks.koverHtmlModuleReport { + enabled = true // false to disable report generation + htmlReportDir.set(layout.buildDirectory.dir("my-module-report/html-result")) +} + +tasks.koverXmlModuleReport { + enabled = true // false to disable report generation + xmlReportFile.set(layout.buildDirectory.file("my-module-report/result.xml")) +} +``` + + +You may collect all modules reports into one directory using `koverCollectModuleReports` task. +Also, you may specify custom directory to collect modules reports in the build file of the module in which the plugin +is applied (usually this is the root module): + +
+Kotlin + +```kotlin +tasks.koverCollectModuleReports { + outputDir.set(layout.buildDirectory.dir("all-modules-reports") ) +} +``` +
+ +
+Groovy + +```groovy +tasks.koverCollectModuleReports { + outputDir.set(layout.buildDirectory.dir("all-modules-reports") ) } ```
@@ -284,13 +322,15 @@ kover { ## Verification -For all test task of module, you can specify one or more rules that check the values of the code coverage counters. +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.* -In the build file of the verified module: + +To add a rule check to cover the code of all modules, you need to add configuration to the module in which the plugin +is applied (usually this is the root module):
Kotlin @@ -298,9 +338,9 @@ In the build file of the verified module: ```kotlin tasks.koverVerify { rule { - name = "The project has upper limit on lines covered" + name = "Minimum number of lines covered" bound { - maxValue = 100000 + minValue = 100000 valueType = kotlinx.kover.api.VerificationValueType.COVERED_LINES_COUNT } } @@ -313,7 +353,7 @@ tasks.koverVerify { } } rule { - name = "Minimal line coverage rate in percents" + name = "Minimal line coverage rate in percent" bound { minValue = 50 // valueType is kotlinx.kover.api.VerificationValueType.COVERED_LINES_PERCENTAGE by default @@ -329,9 +369,9 @@ tasks.koverVerify { ```groovy tasks.koverVerify { rule { - name = "The project doesn't has upper limit on lines covered" + name = "Minimum number of lines covered" bound { - maxValue = 100000 + minValue = 100000 valueType = 'COVERED_LINES_COUNT' } } @@ -344,7 +384,7 @@ tasks.koverVerify { } } rule { - name = "Minimal line coverage rate in percents" + name = "Minimal line coverage rate in percent" bound { minValue = 50 // valueType is 'COVERED_LINES_PERCENTAGE' by default @@ -354,19 +394,58 @@ tasks.koverVerify { ```
+To add rules for code coverage checks for one specific module, you need to add a configuration to this module: + +
+Kotlin + +```kotlin +tasks.koverModuleVerify { + rule { + name = "Minimal line coverage rate in percent" + bound { + minValue = 75 + } + } +} +``` +
+ +
+Groovy + +```groovy +tasks.koverModuleVerify { + rule { + name = "Minimal line coverage rate in percent" + bound { + minValue = 75 + } + } +} +``` +
+ + ## Tasks -The plugin, when applied, automatically creates tasks for the module (all modules, if the project is multi-module, and it applied in root build script): -- `koverXmlReport` - Generates code coverage XML report for all module's test tasks. -- `koverHtmlReport` - Generates code coverage HTML report for all module's test tasks. -- `koverReport` - Executes both `koverXmlReport` and `koverHtmlReport` tasks. -- `koverCollectReports` - Collects reports from all submodules in one directory. Default directory is `$buildDir/reports/kover/all`, names for XML reports and dirs for HTML are projects names. Executing this task does not run `koverXmlReport` or `koverHtmlReport`, it only copies previously created reports if they exist to the output directory. -- `koverVerify` - Verifies code coverage metrics based on specified rules. Always executes before `check` task. +The plugin, when applied, automatically creates tasks for the module in which it is applied (usually this is the root module): +- `koverHtmlReport` - Generates code coverage HTML report for all enabled test tasks in all modules. +- `koverXmlReport` - Generates code coverage XML report for all enabled test tasks in all modules. +- `koverReport` - Executes both `koverXmlReport` and `koverHtmlReport` tasks. Executes before `check` task if property `generateReportOnCheck` for `KoverExtension` is `true` ([see](#configuring-entire-plugin)). +- `koverVerify` - Verifies code coverage metrics of all modules based on specified rules. Always executes before `check` task. +- `koverCollectModuleReports` - Collects all modules reports into one directory. Default directory is `$buildDir/reports/kover/modules`, names for XML reports and dirs for HTML are modules names. Executing this task does not run `koverXmlReport` or `koverHtmlReport`, it only copies previously created reports if they exist to the output directory. + +Tasks that are created for all modules: +- `koverHtmlModuleReport` - Generates code coverage HTML report for all enabled test tasks in one module. +- `koverXmlModuleReport` - Generates code coverage XML report for all enabled test tasks in one module. +- `koverModuleReport` - Executes both `koverXmlModuleReport` and `koverHtmlModuleReport` tasks. +- `koverModuleVerify` - Verifies code coverage metrics of one module based on specified rules. Always executes before `check` task. ## Implicit plugin dependencies During the applying of the plugin, 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 list by the repository of the module in which the plugin is applied (usually this is the root module of the project) and add it if necessary. +For the plugin to work correctly, you need to make sure that the `mavenCentral` or its mirror is added to the list by the repository of the module in which the plugin is applied (usually this is the root module) and add it if necessary.
Kotlin 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 7f912da8..d74b6b18 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt @@ -6,30 +6,15 @@ import kotlin.test.* internal class DefaultConfigTests : BaseGradleScriptTest() { @Test - fun testImplicitConfigsJvm() { - builder() - .case("Test default setting for Kotlin/JVM") + fun testImplicitConfigs() { + builder("Test implicit default settings") .languages(GradleScriptLanguage.GROOVY, GradleScriptLanguage.KOTLIN) - .types(ProjectType.KOTLIN_JVM) + .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) .sources("simple") .build() .run("build") { - checkIntellijBinaryReport(DEFAULT_INTELLIJ_KJVM_BINARY, DEFAULT_INTELLIJ_KJVM_SMAP) - checkReports(DEFAULT_XML, DEFAULT_HTML) - } - } - - @Test - fun testImplicitConfigsKmp() { - builder() - .case("Test default setting for Kotlin Multi-Platform") - .languages(GradleScriptLanguage.GROOVY, GradleScriptLanguage.KOTLIN) - .types(ProjectType.KOTLIN_MULTIPLATFORM) - .sources("simple") - .build() - .run("build") { - checkIntellijBinaryReport(DEFAULT_INTELLIJ_KMP_BINARY, DEFAULT_INTELLIJ_KMP_SMAP) - checkReports(DEFAULT_XML, DEFAULT_HTML) + 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 cb5f2a25..18dd08e3 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt @@ -10,8 +10,7 @@ internal class InstrumentationFilteringTests : BaseGradleScriptTest() { @Test fun testExclude() { - builder() - .case("Test exclusion of classes from instrumentation") + builder("Test exclusion of classes from instrumentation") .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) @@ -22,17 +21,16 @@ internal class InstrumentationFilteringTests : BaseGradleScriptTest() { ) .build() .run("build") { - xml(DEFAULT_XML) { + xml(defaultXmlReport()) { assertCounterExcluded(classCounter("org.jetbrains.ExampleClass"), this@run.engine) - assertCounterCoveredAndIncluded(classCounter("org.jetbrains.SecondClass")) + assertCounterCovered(classCounter("org.jetbrains.SecondClass")) } } } @Test fun testExcludeInclude() { - builder() - .case("Test inclusion and exclusion of classes in instrumentation") + 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) @@ -47,10 +45,10 @@ internal class InstrumentationFilteringTests : BaseGradleScriptTest() { ) .build() .run("build") { - xml(DEFAULT_XML) { + xml(defaultXmlReport()) { assertCounterExcluded(classCounter("org.jetbrains.ExampleClass"), this@run.engine) assertCounterExcluded(classCounter("org.jetbrains.Unused"), this@run.engine) - assertCounterCoveredAndIncluded(classCounter("org.jetbrains.SecondClass")) + assertCounterCovered(classCounter("org.jetbrains.SecondClass")) } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiModulesTest.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiModulesTest.kt new file mode 100644 index 00000000..8725bc81 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiModulesTest.kt @@ -0,0 +1,66 @@ +package kotlinx.kover.test.functional.cases + +import kotlinx.kover.api.* +import kotlinx.kover.test.functional.cases.utils.* +import kotlinx.kover.test.functional.cases.utils.defaultXmlReport +import kotlinx.kover.test.functional.core.BaseGradleScriptTest +import kotlinx.kover.test.functional.core.ProjectType +import kotlin.test.* + +private const val SUBMODULE_NAME = "common" + +internal class MultiModulesTest : BaseGradleScriptTest() { + @Test + fun testAggregateReports() { + builder("Testing the generation of aggregating reports") + .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) + .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) + .sources("multimodule-user") + .submodule(SUBMODULE_NAME) { + sources("multimodule-common") + } + .dependency( + "implementation(project(\":$SUBMODULE_NAME\"))", + "implementation project(':$SUBMODULE_NAME')" + ) + .build() + .run("build") { + xml(defaultXmlReport()) { + assertCounterFullyCovered(classCounter("org.jetbrains.CommonClass")) + assertCounterFullyCovered(classCounter("org.jetbrains.CommonInternalClass")) + assertCounterFullyCovered(classCounter("org.jetbrains.UserClass")) + } + } + } + + @Test + fun testModuleReports() { + builder("Testing the generation of module reports") + .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) + .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) + .sources("multimodule-user") + .submodule(SUBMODULE_NAME) { + sources("multimodule-common") + } + .dependency( + "implementation(project(\":$SUBMODULE_NAME\"))", + "implementation project(':$SUBMODULE_NAME')" + ) + .build() + .run("koverModuleReport") { + xml(defaultXmlModuleReport()) { + assertCounterAbsent(classCounter("org.jetbrains.CommonClass")) + assertCounterAbsent(classCounter("org.jetbrains.CommonInternalClass")) + assertCounterFullyCovered(classCounter("org.jetbrains.UserClass")) + } + + submodule(SUBMODULE_NAME) { + xml(defaultXmlModuleReport()) { + assertCounterFullyCovered(classCounter("org.jetbrains.CommonClass")) + assertCounterFullyCovered(classCounter("org.jetbrains.CommonInternalClass")) + assertCounterAbsent(classCounter("org.jetbrains.UserClass")) + } + } + } + } +} 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 2bd8edac..9f347d3e 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt @@ -8,51 +8,48 @@ import kotlin.test.* internal class ReportsCachingTests : BaseGradleScriptTest() { @Test - fun testCachingForIntellij() { - builder() - .case("Test caching reports for IntelliJ Coverage Engine") - .engines(CoverageEngine.INTELLIJ) + fun testCaching() { + builder("Test caching aggregate reports") + .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) .sources("simple") .build() .run("build", "--build-cache") { - checkIntellijBinaryReport(DEFAULT_INTELLIJ_KJVM_BINARY, DEFAULT_INTELLIJ_KJVM_SMAP) - checkReports(DEFAULT_XML, DEFAULT_HTML) + checkDefaultBinaryReport() + checkDefaultReports() } .run("clean", "--build-cache") { - checkIntellijBinaryReport(DEFAULT_INTELLIJ_KJVM_BINARY, DEFAULT_INTELLIJ_KJVM_SMAP, false) - checkReports(DEFAULT_XML, DEFAULT_HTML, false) + checkDefaultBinaryReport(false) + checkDefaultReports(false) } .run("build", "--build-cache") { - checkIntellijBinaryReport(DEFAULT_INTELLIJ_KJVM_BINARY, DEFAULT_INTELLIJ_KJVM_SMAP) - checkReports(DEFAULT_XML, DEFAULT_HTML) - outcome(":test") { assertEquals(TaskOutcome.FROM_CACHE, this)} - outcome(":koverXmlReport") { assertEquals(TaskOutcome.FROM_CACHE, this)} - outcome(":koverHtmlReport") { assertEquals(TaskOutcome.FROM_CACHE, this)} + checkDefaultBinaryReport() + checkDefaultReports() + outcome(":test") { assertEquals(TaskOutcome.FROM_CACHE, this) } + outcome(":koverXmlReport") { assertEquals(TaskOutcome.FROM_CACHE, this) } + outcome(":koverHtmlReport") { assertEquals(TaskOutcome.FROM_CACHE, this) } } } @Test - fun testCachingForJacoco() { - builder() - .case("Test caching reports for JaCoCo Coverage Engine") - .engines(CoverageEngine.JACOCO) + fun testModuleCaching() { + builder("Test caching module reports") + .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) .sources("simple") .build() - .run("build", "--build-cache") { - checkJacocoBinaryReport(DEFAULT_JACOCO_KJVM_BINARY) - checkReports(DEFAULT_XML, DEFAULT_HTML) + .run("koverModuleReport", "--build-cache") { + checkDefaultBinaryReport() + checkDefaultModuleReports() } .run("clean", "--build-cache") { - checkJacocoBinaryReport(DEFAULT_JACOCO_KJVM_BINARY, false) - checkReports(DEFAULT_XML, DEFAULT_HTML, false) + checkDefaultBinaryReport(false) + checkDefaultModuleReports(false) } - .run("build", "--build-cache") { - checkJacocoBinaryReport(DEFAULT_JACOCO_KJVM_BINARY) - checkReports(DEFAULT_XML, DEFAULT_HTML) - - outcome(":test") { assertEquals(TaskOutcome.FROM_CACHE, this)} - outcome(":koverXmlReport") { assertEquals(TaskOutcome.FROM_CACHE, this)} - outcome(":koverHtmlReport") { assertEquals(TaskOutcome.FROM_CACHE, this)} + .run("koverModuleReport", "--build-cache") { + checkDefaultBinaryReport() + checkDefaultModuleReports() + outcome(":test") { assertEquals(TaskOutcome.FROM_CACHE, this) } + outcome(":koverXmlModuleReport") { assertEquals(TaskOutcome.FROM_CACHE, this) } + outcome(":koverHtmlModuleReport") { assertEquals(TaskOutcome.FROM_CACHE, this) } } } 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 b3261098..d190c3c0 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 @@ -5,43 +5,48 @@ import kotlinx.kover.test.functional.core.* import kotlinx.kover.test.functional.core.RunResult import kotlin.test.* -internal fun RunResult.checkIntellijBinaryReport(binary: String, smap: String, mustExists: Boolean = true) { - if (mustExists) { +internal fun RunResult.checkDefaultBinaryReport(mustExist: Boolean = true) { + val binary: String = defaultBinaryReport(engine, projectType) + + if (mustExist) { file(binary) { assertTrue { exists() } assertTrue { length() > 0 } } - file(smap) { - assertTrue { exists() } - assertTrue { length() > 0 } - } } else { file(binary) { assertFalse { exists() } } - file(smap) { - assertFalse { exists() } - } } -} -internal fun RunResult.checkJacocoBinaryReport(binary: String, mustExists: Boolean = true) { - if (mustExists) { - file(binary) { - assertTrue { exists() } - assertTrue { length() > 0 } - } - } else { - file(binary) { - assertFalse { exists() } + if (engine == CoverageEngine.INTELLIJ) { + val smap = defaultSmapFile(projectType) + + if (mustExist) { + file(smap) { + assertTrue { exists() } + assertTrue { length() > 0 } + } + } else { + file(smap) { + assertFalse { exists() } + } } } } -internal fun RunResult.checkReports(xmlPath: String, htmlPath: String, mustExists: Boolean = true) { - if (mustExists) { +internal fun RunResult.checkDefaultReports(mustExist: Boolean = true) { + checkReports(defaultXmlReport(), defaultHtmlReport(), mustExist) +} + +internal fun RunResult.checkDefaultModuleReports(mustExist: Boolean = true) { + checkReports(defaultXmlModuleReport(), defaultHtmlModuleReport(), mustExist) +} + +internal fun RunResult.checkReports(xmlPath: String, htmlPath: String, mustExist: Boolean) { + if (mustExist) { file(xmlPath) { - assertTrue { exists() } + assertTrue("XML file must exist '$xmlPath'") { exists() } assertTrue { length() > 0 } } file(htmlPath) { @@ -58,6 +63,10 @@ internal fun RunResult.checkReports(xmlPath: String, htmlPath: String, mustExist } } +internal fun assertCounterAbsent(counter: Counter?) { + assertNull(counter) +} + internal fun assertCounterExcluded(counter: Counter?, engine: CoverageEngine) { if (engine == CoverageEngine.INTELLIJ) { assertNull(counter) @@ -67,7 +76,15 @@ internal fun assertCounterExcluded(counter: Counter?, engine: CoverageEngine) { } } -internal fun assertCounterCoveredAndIncluded(counter: Counter?) { +internal fun assertCounterCovered(counter: Counter?) { assertNotNull(counter) assertTrue { counter.covered > 0 } } + +internal fun assertCounterFullyCovered(counter: Counter?) { + assertNotNull(counter) + assertTrue { counter.covered > 0 } + assertEquals(0, counter.missed) +} + + 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 e0d14a3e..ecd4c69f 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,14 +1,24 @@ package kotlinx.kover.test.functional.cases.utils +import kotlinx.kover.api.* +import kotlinx.kover.test.functional.core.ProjectType -internal const val DEFAULT_INTELLIJ_KJVM_BINARY = "kover/test.ic" -internal const val DEFAULT_INTELLIJ_KJVM_SMAP = "kover/test.ic.smap" -internal const val DEFAULT_INTELLIJ_KMP_BINARY = "kover/jvmTest.ic" -internal const val DEFAULT_INTELLIJ_KMP_SMAP = "kover/jvmTest.ic.smap" +internal fun defaultBinaryReport(engine: CoverageEngine, projectType: ProjectType): String { + val extension = if (engine == CoverageEngine.INTELLIJ) "ic" else "exec" + return when (projectType) { + ProjectType.KOTLIN_JVM -> "kover/test.$extension" + ProjectType.KOTLIN_MULTIPLATFORM -> "kover/jvmTest.$extension" + ProjectType.ANDROID -> "kover/jvmTest.$extension" + } +} -internal const val DEFAULT_JACOCO_KJVM_BINARY = "kover/test.exec" -internal const val DEFAULT_JACOCO_KMP_BINARY = "kover/jvmTest.exec" +internal fun defaultSmapFile(projectType: ProjectType): String { + return defaultBinaryReport(CoverageEngine.INTELLIJ, projectType) + ".smap" +} -internal const val DEFAULT_XML = "reports/kover/report.xml" -internal const val DEFAULT_HTML = "reports/kover/html" +internal fun defaultXmlReport() = "reports/kover/report.xml" +internal fun defaultHtmlReport() = "reports/kover/html" + +internal fun defaultXmlModuleReport() = "reports/kover/module-xml/report.xml" +internal fun defaultHtmlModuleReport() = "reports/kover/module-html" 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 25567121..6f305822 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt @@ -9,7 +9,7 @@ internal open class BaseGradleScriptTest { @JvmField internal val rootFolder: TemporaryFolder = TemporaryFolder() - fun builder(): ProjectBuilder { - return createBuilder(rootFolder.root) + fun builder(description: String): ProjectBuilder { + return createBuilder(rootFolder.root, description) } } 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 6cb36f1e..49bc1420 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt @@ -3,12 +3,11 @@ package kotlinx.kover.test.functional.core import kotlinx.kover.api.* import java.io.* -internal fun createBuilder(rootDir: File): ProjectBuilder { - return ProjectBuilderImpl(rootDir) +internal fun createBuilder(rootDir: File, description: String): ProjectBuilder { + return ProjectBuilderImpl(rootDir, description) } -internal class ProjectBuilderState { - var description: String? = null +internal class ProjectBuilderState(val description: String) { var pluginVersion: String? = null val languages: MutableSet = mutableSetOf() val types: MutableSet = mutableSetOf() @@ -20,24 +19,22 @@ internal class ProjectBuilderState { internal class ModuleBuilderState { val sourceTemplates: MutableList = mutableListOf() - val kotlinScripts: MutableList = mutableListOf() - val groovyScripts: MutableList = mutableListOf() - val testKotlinScripts: MutableList = mutableListOf() - val testGroovyScripts: 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() } +internal data class GradleScript(val kotlin: String, val groovy: String) + private class ProjectBuilderImpl( val rootDir: File, - private val state: ProjectBuilderState = ProjectBuilderState() + description: String, + private val state: ProjectBuilderState = ProjectBuilderState(description) ) : ModuleBuilderImpl(state.rootModule), ProjectBuilder { - override fun case(description: String) = also { - state.description = description - } - override fun languages(vararg languages: GradleScriptLanguage) = also { state.languages += languages } @@ -100,26 +97,32 @@ private open class ModuleBuilderImpl>(val moduleState: Modu } override fun configTest(script: String): B { - moduleState.testKotlinScripts += script - moduleState.testGroovyScripts += script + moduleState.testScripts += GradleScript(script, script) return this as B } override fun configTest(kotlin: String, groovy: String): B { - moduleState.testKotlinScripts += kotlin - moduleState.testGroovyScripts += groovy + moduleState.testScripts += GradleScript(kotlin, groovy) return this as B } override fun config(script: String): B { - moduleState.kotlinScripts += script - moduleState.groovyScripts += script + moduleState.scripts += GradleScript(script, script) return this as B } override fun config(kotlin: String, groovy: String): B { - moduleState.kotlinScripts += kotlin - moduleState.groovyScripts += groovy + moduleState.testScripts += GradleScript(kotlin, groovy) + return this as B + } + + override fun dependency(script: String): B { + moduleState.dependencies += GradleScript(script, script) + return this as B + } + + override fun dependency(kotlin: String, groovy: String): B { + moduleState.dependencies += GradleScript(kotlin, groovy) return this as B } 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 dfb9c79f..6908cd10 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt @@ -18,14 +18,14 @@ internal class ProjectRunnerImpl(private val projects: Map) } private fun File.runGradle(args: List, slice: ProjectSlice, checker: RunResult.() -> Unit) { - val buildResult = GradleRunner.create() - .withProjectDir(this) - .withPluginClasspath() - .addPluginTestRuntimeClasspath() - .withArguments(args) - .build() - try { + val buildResult = GradleRunner.create() + .withProjectDir(this) + .withPluginClasspath() + .addPluginTestRuntimeClasspath() + .withArguments(args) + .build() + RunResultImpl(buildResult, slice, this).apply(checker) } catch (e: Throwable) { throw AssertionError("Assertion error occurred in test for project $slice", e) @@ -44,10 +44,16 @@ private fun GradleRunner.addPluginTestRuntimeClasspath() = apply { } -private class RunResultImpl(private val result: BuildResult, private val slice: ProjectSlice, dir: File) : RunResult { +private class RunResultImpl(private val result: BuildResult, private val slice: ProjectSlice, private val dir: File) : + RunResult { val buildDir: File = File(dir, "build") override val engine: CoverageEngine = slice.engine ?: CoverageEngine.INTELLIJ + override val projectType: ProjectType = slice.type + + override fun submodule(name: String, checker: RunResult.() -> Unit) { + RunResultImpl(result, slice, File(dir, name)).also(checker) + } override fun output(checker: String.() -> Unit) { result.output.checker() 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 77830cb9..570e2659 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt @@ -17,17 +17,19 @@ internal interface ModuleBuilder> { fun config(script: String): B fun config(kotlin: String, groovy: String): B + + fun dependency(script: String): B + fun dependency(kotlin: String, groovy: String): B } internal interface ProjectBuilder : ModuleBuilder { - fun case(description: String): ProjectBuilder fun languages(vararg languages: GradleScriptLanguage): ProjectBuilder fun engines(vararg engines: CoverageEngine): ProjectBuilder fun types(vararg types: ProjectType): ProjectBuilder fun configKover(config: KoverRootConfig.() -> Unit): ProjectBuilder - fun submodule(name: String, builder: ModuleBuilder<*>.() -> Unit): ModuleBuilder<*> + fun submodule(name: String, builder: ModuleBuilder<*>.() -> Unit): ProjectBuilder fun build(): ProjectRunner } @@ -49,11 +51,14 @@ internal data class KoverRootConfig( } internal interface ProjectRunner { - fun run(vararg args: String, checker: RunResult.() -> Unit): ProjectRunner + fun run(vararg args: String, checker: RunResult.() -> Unit = {}): ProjectRunner } internal interface RunResult { val engine: CoverageEngine + val projectType: ProjectType + + fun submodule(name: String, checker: RunResult.() -> Unit) fun output(checker: String.() -> 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 index ac70259f..75e9aa6e 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt @@ -17,16 +17,28 @@ internal fun ProjectBuilderState.createProject(rootDir: File, slice: ProjectSlic .processRootBuildScript(this, slice) .processModuleBuildScript(rootModule, slice) - val settings = buildSettings(slice) - File(projectDir, "build.$extension").writeText(buildScript) - File(projectDir, "settings.$extension").writeText(settings) + File(projectDir, "settings.$extension").writeText(buildSettings(slice)) rootModule.writeSources(projectDir, slice) + submodules.forEach { (name, state) -> state.writeSubmodule(File(projectDir, name), slice) } + return projectDir } +private fun ModuleBuilderState.writeSubmodule(directory: File, slice: ProjectSlice) { + directory.mkdirs() + + val extension = slice.scriptExtension + + val buildScript = loadScriptTemplate(false, slice).processModuleBuildScript(this, slice) + + File(directory, "build.$extension").writeText(buildScript) + + writeSources(directory, slice) +} + private val ProjectSlice.scriptExtension get() = if (language == GradleScriptLanguage.KOTLIN) "gradle.kts" else "gradle" @@ -56,7 +68,7 @@ private fun String.processRootBuildScript(state: ProjectBuilderState, slice: Pro private fun String.processModuleBuildScript(state: ModuleBuilderState, slice: ProjectSlice): String { return replace("//REPOSITORIES", "") - .replace("//DEPENDENCIES", "") + .replace("//DEPENDENCIES", state.buildDependencies(slice)) .replace("//SCRIPTS", state.buildScripts(slice)) .replace("//TEST_TASK", state.buildTestTask(slice)) .replace("//VERIFICATIONS", state.buildVerifications(slice)) @@ -136,14 +148,13 @@ private fun ProjectBuilderState.buildRootExtension(slice: ProjectSlice): String return builder.toString() } -@Suppress("UNUSED_PARAMETER") private fun ModuleBuilderState.buildTestTask(slice: ProjectSlice): String { - val configs = if (slice.language == GradleScriptLanguage.KOTLIN) testKotlinScripts else testGroovyScripts - - if (configs.isEmpty()) { + if (testScripts.isEmpty()) { return "" } + val configs = testScripts.map { if (slice.language == GradleScriptLanguage.KOTLIN) it.kotlin else it.groovy } + return loadTestTaskTemplate(slice).replace("//KOVER_TEST_CONFIG", configs.joinToString("\n")) } @@ -158,13 +169,19 @@ private fun ProjectBuilderState.buildSettings(slice: ProjectSlice): String { } private fun ModuleBuilderState.buildScripts(slice: ProjectSlice): String { - val scripts = if (slice.language == GradleScriptLanguage.KOTLIN) kotlinScripts else groovyScripts + if (scripts.isEmpty()) { + return "" + } + val configs = scripts.map { if (slice.language == GradleScriptLanguage.KOTLIN) it.kotlin else it.groovy } + return configs.joinToString("\n", "\n", "\n") +} - return if (scripts.isNotEmpty()) { - scripts.joinToString("\n", "\n", "\n") - } else { - "" +private fun ModuleBuilderState.buildDependencies(slice: ProjectSlice): String { + if (dependencies.isEmpty()) { + return "" } + val configs = dependencies.map { if (slice.language == GradleScriptLanguage.KOTLIN) it.kotlin else it.groovy } + return configs.joinToString("\n", "\n", "\n") } @@ -173,7 +190,7 @@ private fun loadSettingsTemplate(slice: ProjectSlice): String { } private fun loadScriptTemplate(root: Boolean, slice: ProjectSlice): String { - val filename = if (root) "root" else "child" + val filename = if (root) "root" else "submodule" return File("${slice.scriptPath()}/$filename").readText() } diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/submodule b/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/submodule new file mode 100644 index 00000000..9cc59b26 --- /dev/null +++ b/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/submodule @@ -0,0 +1,12 @@ +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/kmp/root b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/root index 0a8bcb40..3d4fbc4d 100644 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/root +++ b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/root @@ -15,5 +15,12 @@ kotlin { 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/submodule b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/submodule new file mode 100644 index 00000000..3f710e4d --- /dev/null +++ b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/submodule @@ -0,0 +1,25 @@ +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/kotlin/kjvm/submodule b/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/submodule new file mode 100644 index 00000000..3fa1382e --- /dev/null +++ b/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/submodule @@ -0,0 +1,12 @@ +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/kmp/root b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/root index 5f8c9e09..7abd2345 100644 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/root +++ b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/root @@ -15,5 +15,12 @@ kotlin { 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/submodule b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/submodule new file mode 100644 index 00000000..d826c33d --- /dev/null +++ b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/submodule @@ -0,0 +1,25 @@ +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/sources/multimodule-common/main/kotlin/Sources.kt b/src/functionalTest/templates/sources/multimodule-common/main/kotlin/Sources.kt new file mode 100644 index 00000000..31dbebc3 --- /dev/null +++ b/src/functionalTest/templates/sources/multimodule-common/main/kotlin/Sources.kt @@ -0,0 +1,24 @@ +package org.jetbrains + +class CommonClass { + fun callFromThisModule() { + println("Call from this module") + } + + fun callFromAnotherModule() { + println("Call from another module") + } +} + +internal class CommonInternalClass { + fun function() { + println("ModuleClass#function call") + } +} + +class AUnused { + fun functionInUsedClass() { + println("unused") + } +} + diff --git a/src/functionalTest/templates/sources/multimodule-common/test/kotlin/CommonTestClass.kt b/src/functionalTest/templates/sources/multimodule-common/test/kotlin/CommonTestClass.kt new file mode 100644 index 00000000..4b133a11 --- /dev/null +++ b/src/functionalTest/templates/sources/multimodule-common/test/kotlin/CommonTestClass.kt @@ -0,0 +1,17 @@ +package org.jetbrains.serialuser + +import org.jetbrains.CommonClass +import org.jetbrains.CommonInternalClass +import kotlin.test.Test + +class TestClass { + @Test + fun callCommonTest() { + CommonClass().callFromThisModule() + } + + @Test + fun callInternalTest() { + CommonInternalClass().function() + } +} diff --git a/src/functionalTest/templates/sources/multimodule-user/main/kotlin/Sources.kt b/src/functionalTest/templates/sources/multimodule-user/main/kotlin/Sources.kt new file mode 100644 index 00000000..4c518162 --- /dev/null +++ b/src/functionalTest/templates/sources/multimodule-user/main/kotlin/Sources.kt @@ -0,0 +1,14 @@ +package org.jetbrains + +internal class UserClass { + fun function() { + println("UserClass#function call") + } +} + +class BUnused { + fun functionInUsedClass() { + println("unused") + } +} + diff --git a/src/functionalTest/templates/sources/multimodule-user/test/kotlin/UserTestClass.kt b/src/functionalTest/templates/sources/multimodule-user/test/kotlin/UserTestClass.kt new file mode 100644 index 00000000..34231a74 --- /dev/null +++ b/src/functionalTest/templates/sources/multimodule-user/test/kotlin/UserTestClass.kt @@ -0,0 +1,17 @@ +package org.jetbrains.serialuser + +import org.jetbrains.CommonClass +import org.jetbrains.UserClass +import kotlin.test.Test + +class TestClass { + @Test + fun callCommonTest() { + CommonClass().callFromAnotherModule() + } + + @Test + fun callUserTest() { + UserClass().function() + } +} diff --git a/src/main/kotlin/kotlinx/kover/KoverPlugin.kt b/src/main/kotlin/kotlinx/kover/KoverPlugin.kt index 5a3a070e..b5bd06c3 100644 --- a/src/main/kotlin/kotlinx/kover/KoverPlugin.kt +++ b/src/main/kotlin/kotlinx/kover/KoverPlugin.kt @@ -4,21 +4,31 @@ package kotlinx.kover -import kotlinx.kover.adapters.* import kotlinx.kover.api.* +import kotlinx.kover.api.KoverPaths.HTML_AGG_REPORT_DEFAULT_PATH import kotlinx.kover.api.KoverNames.CHECK_TASK_NAME -import kotlinx.kover.api.KoverNames.COLLECT_TASK_NAME +import kotlinx.kover.api.KoverNames.COLLECT_MODULE_REPORTS_TASK_NAME import kotlinx.kover.api.KoverNames.HTML_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.HTML_MODULE_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.MODULE_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.MODULE_VERIFY_TASK_NAME import kotlinx.kover.api.KoverNames.REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.XML_MODULE_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.VERIFY_TASK_NAME import kotlinx.kover.api.KoverNames.XML_REPORT_TASK_NAME +import kotlinx.kover.api.KoverPaths.ALL_MODULES_REPORTS_DEFAULT_PATH +import kotlinx.kover.api.KoverPaths.HTML_MODULE_REPORT_DEFAULT_PATH +import kotlinx.kover.api.KoverPaths.XML_AGG_REPORT_DEFAULT_PATH +import kotlinx.kover.api.KoverPaths.XML_MODULE_REPORT_DEFAULT_PATH +import kotlinx.kover.engines.commons.* +import kotlinx.kover.engines.commons.CoverageAgent import kotlinx.kover.engines.intellij.* -import kotlinx.kover.engines.jacoco.* import kotlinx.kover.tasks.* import org.gradle.api.* +import org.gradle.api.provider.* import org.gradle.api.tasks.* import org.gradle.api.tasks.testing.* import org.gradle.process.* @@ -29,174 +39,194 @@ class KoverPlugin : Plugin { private val defaultJacocoVersion = "0.8.7" override fun apply(target: Project) { + target.checkAlreadyApplied() + val koverExtension = target.createKoverExtension() - val intellijAgent = target.createIntellijAgent(koverExtension) - val jacocoAgent = target.createJacocoAgent(koverExtension) + val agents = AgentsFactory.createAgents(target, koverExtension) + + val providers = target.createProviders(agents) target.allprojects { - it.applyToProject(koverExtension, intellijAgent, jacocoAgent) + it.applyToModule(providers, agents) } target.createCollectingTask() + + target.createAggregateTasks(providers) } - private fun Project.createCollectingTask() { - tasks.create(COLLECT_TASK_NAME, KoverCollectingTask::class.java) { - it.group = VERIFICATION_GROUP - it.description = "Collects reports from all submodules in one directory." - it.outputDir.set(project.layout.buildDirectory.dir("reports/kover/all")) - // disable UP-TO-DATE check for task: it will be executed every time - it.outputs.upToDateWhen { false } + private fun Project.applyToModule(providers: ProjectProviders, agents: Map) { + val moduleProviders = + providers.modules[name] ?: throw GradleException("Kover: Providers for module '$name' was not found") - 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) + val xmlReportTask = createKoverModuleTask( + XML_MODULE_REPORT_TASK_NAME, + KoverXmlModuleReportTask::class, + providers, + moduleProviders + ) { + it.xmlReportFile.set(layout.buildDirectory.file(XML_MODULE_REPORT_DEFAULT_PATH)) + it.description = "Generates code coverage XML report for all enabled test tasks in one module." + } + + val htmlReportTask = createKoverModuleTask( + HTML_MODULE_REPORT_TASK_NAME, + KoverHtmlModuleReportTask::class, + providers, + moduleProviders + ) { + it.htmlReportDir.set(it.project.layout.buildDirectory.dir(HTML_MODULE_REPORT_DEFAULT_PATH)) + it.description = "Generates code coverage HTML report for all enabled test tasks in one module." + } + + val verifyTask = createKoverModuleTask( + MODULE_VERIFY_TASK_NAME, + KoverModuleVerificationTask::class, + providers, + moduleProviders + ) { + it.onlyIf { t -> (t as KoverModuleVerificationTask).rules.isNotEmpty() } + // kover takes counter values from XML file. Remove after reporter upgrade + it.mustRunAfter(xmlReportTask) + it.description = "Verifies code coverage metrics of one module based on specified rules." + } - it.mustRunAfter(xmlReportTask) - it.mustRunAfter(htmlReportTask) + tasks.create(MODULE_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 module." + } - it.xmlFiles[proj.name] = xmlReportTask.xmlReportFile - it.htmlDirs[proj.name] = htmlReportTask.htmlReportDir + tasks.configureEach { + if (it.name == CHECK_TASK_NAME) { + it.dependsOn(verifyTask) } } - } + tasks.withType(Test::class.java).configureEach { t -> + t.configTest(providers, agents) + } + } - private fun Project.applyToProject( - koverExtension: KoverExtension, - intellijAgent: IntellijAgent, - jacocoAgent: JacocoAgent - ) { - val xmlReportTask = createKoverCommonTask( + private fun Project.createAggregateTasks(providers: ProjectProviders) { + val xmlReportTask = createKoverAggregateTask( XML_REPORT_TASK_NAME, KoverXmlReportTask::class, - koverExtension, - intellijAgent, - jacocoAgent + providers ) { - it.xmlReportFile.set(provider { - layout.buildDirectory.get().file("reports/kover/report.xml") - }) + it.xmlReportFile.set(layout.buildDirectory.file(XML_AGG_REPORT_DEFAULT_PATH)) + it.description = "Generates code coverage XML report for all enabled test tasks in all modules." } - val htmlReportTask = createKoverCommonTask( + val htmlReportTask = createKoverAggregateTask( HTML_REPORT_TASK_NAME, KoverHtmlReportTask::class, - koverExtension, - intellijAgent, - jacocoAgent + providers ) { - it.htmlReportDir.set(it.project.provider { - it.project.layout.buildDirectory.get().dir("reports/kover/html") - }) + it.htmlReportDir.set(layout.buildDirectory.dir(HTML_AGG_REPORT_DEFAULT_PATH)) + it.description = "Generates code coverage HTML report for all enabled test tasks in all modules." } - val verificationTask = createKoverCommonTask( + val reportTask = 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 all modules." + } + + val verifyTask = createKoverAggregateTask( VERIFY_TASK_NAME, KoverVerificationTask::class, - koverExtension, - intellijAgent, - jacocoAgent + providers ) { it.onlyIf { t -> (t as KoverVerificationTask).rules.isNotEmpty() } // kover takes counter values from XML file. Remove after reporter upgrade it.mustRunAfter(xmlReportTask) - } - - val koverReportTask = tasks.create(REPORT_TASK_NAME) { - it.group = VERIFICATION_GROUP - it.description = "Generates code coverage HTML and XML reports for all module's test tasks." - it.dependsOn(xmlReportTask) - it.dependsOn(htmlReportTask) + it.description = "Verifies code coverage metrics of all modules based on specified rules." } tasks.configureEach { if (it.name == CHECK_TASK_NAME) { - it.dependsOn(verificationTask) it.dependsOn(provider { + val koverExtension = extensions.getByType(KoverExtension::class.java) if (koverExtension.generateReportOnCheck.get()) { - koverReportTask + listOf(reportTask, verifyTask) } else { - verificationTask + listOf(verifyTask) } }) } } + } - val srcProvider = provider { collectDirs().first } - xmlReportTask.srcDirs.set(srcProvider) - htmlReportTask.srcDirs.set(srcProvider) - verificationTask.srcDirs.set(srcProvider) - val outputProvider = provider { collectDirs().second } - xmlReportTask.outputDirs.set(outputProvider) - htmlReportTask.outputDirs.set(outputProvider) - verificationTask.outputDirs.set(outputProvider) + private fun Project.createKoverAggregateTask( + taskName: String, + type: KClass, + providers: ProjectProviders, + block: (T) -> Unit + ): T { + return tasks.create(taskName, type.java) { + it.group = VERIFICATION_GROUP - tasks.withType(Test::class.java).configureEach { t -> - t.applyToTask(koverExtension, intellijAgent, jacocoAgent) - } + providers.modules.forEach { (moduleName, m) -> + it.binaryReportFiles.put(moduleName, NestedFiles(it.project.objects, m.reports)) + it.smapFiles.put(moduleName, NestedFiles(it.project.objects, m.smap)) + it.srcDirs.put(moduleName, NestedFiles(it.project.objects, m.sources)) + it.outputDirs.put(moduleName, NestedFiles(it.project.objects, m.output)) + } - val binariesProvider = provider { - // process binary report only from tasks with enabled cover - val files = tasks.withType(Test::class.java) - .map { t -> t.extensions.getByType(KoverTaskExtension::class.java) } - .filter { e -> e.isEnabled } - .map { e -> e.binaryReportFile.get() } - .filter { f -> f.exists() } - files(files) - } - xmlReportTask.binaryReportFiles.set(binariesProvider) - htmlReportTask.binaryReportFiles.set(binariesProvider) - verificationTask.binaryReportFiles.set(binariesProvider) - - val smapProvider = provider { - val files = tasks.withType(Test::class.java) - .map { t -> t.extensions.getByType(KoverTaskExtension::class.java) } - .filter { e -> e.isEnabled } - .map { e -> e.smapFile.orNull } - /* - Binary reports and SMAP files have same ordering for IntelliJ engine: - * SMAP file is null if coverage engine is a JaCoCo by default - in this case property is unused - * SMAP file not creates by JaCoCo - property is unused - * test task have no sources - in this case binary report and SMAP file not exists - */ - .filter { f -> f?.exists() ?: true } - files(files) - } - xmlReportTask.smapFiles.set(smapProvider) - htmlReportTask.smapFiles.set(smapProvider) - verificationTask.smapFiles.set(smapProvider) + it.coverageEngine.set(providers.engine) + it.classpath.set(providers.classpath) + it.dependsOn(providers.allModules.tests) - val enabledTestsProvider = provider { - tasks.withType(Test::class.java) - .filter { t -> t.extensions.getByType(KoverTaskExtension::class.java).isEnabled } + block(it) } - xmlReportTask.dependsOn(enabledTestsProvider) - htmlReportTask.dependsOn(enabledTestsProvider) - verificationTask.dependsOn(enabledTestsProvider) + } - xmlReportTask.description = "Generates code coverage XML report for all module's test tasks." - htmlReportTask.description = "Generates code coverage HTML report for all module's test tasks." - verificationTask.description = "Verifies code coverage metrics based on specified rules." + private fun Project.createCollectingTask() { + tasks.create(COLLECT_MODULE_REPORTS_TASK_NAME, KoverCollectingModulesTask::class.java) { task -> + task.group = VERIFICATION_GROUP + task.description = "Collects all modules reports into one directory." + task.outputDir.set(project.layout.buildDirectory.dir(ALL_MODULES_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(KoverXmlModuleReportTask::class.java).getByName(XML_MODULE_REPORT_TASK_NAME) + val htmlReportTask = + proj.tasks.withType(KoverHtmlModuleReportTask::class.java).getByName(HTML_MODULE_REPORT_TASK_NAME) + + task.mustRunAfter(xmlReportTask) + task.mustRunAfter(htmlReportTask) + + task.xmlFiles[proj.name] = xmlReportTask.xmlReportFile + task.htmlDirs[proj.name] = htmlReportTask.htmlReportDir + } + } } - private fun Project.createKoverCommonTask( + private fun Project.createKoverModuleTask( taskName: String, type: KClass, - koverExtension: KoverExtension, - intellijAgent: IntellijAgent, - jacocoAgent: JacocoAgent, + providers: ProjectProviders, + moduleProviders: ModuleProviders, block: (T) -> Unit ): T { return tasks.create(taskName, type.java) { it.group = VERIFICATION_GROUP - it.coverageEngine.set(koverExtension.coverageEngine) - it.classpath.set(provider { - if (koverExtension.coverageEngine.get() == CoverageEngine.INTELLIJ) intellijAgent.config else jacocoAgent.config - }) + it.coverageEngine.set(providers.engine) + it.classpath.set(providers.classpath) + it.srcDirs.set(moduleProviders.sources) + it.outputDirs.set(moduleProviders.output) + + // it is necessary to read all binary reports because module's classes can be invoked in another module + it.binaryReportFiles.set(providers.allModules.reports) + it.smapFiles.set(providers.allModules.smap) + it.dependsOn(providers.allModules.tests) block(it) } @@ -212,60 +242,67 @@ class KoverPlugin : Plugin { return extension } - private fun Test.applyToTask( - koverExtension: KoverExtension, - intellijAgent: IntellijAgent, - jacocoAgent: JacocoAgent + private fun Test.configTest( + providers: ProjectProviders, + agents: Map ): KoverTaskExtension { val taskExtension = extensions.create(TASK_EXTENSION_NAME, KoverTaskExtension::class.java, project.objects) taskExtension.isEnabled = true taskExtension.binaryReportFile.set(this.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 }) taskExtension.smapFile.set(this.project.provider { + val koverExtension = providers.koverExtension.get() if (koverExtension.coverageEngine.get() == CoverageEngine.INTELLIJ) File(taskExtension.binaryReportFile.get().canonicalPath + ".smap") else null }) - jvmArgumentProviders.add( - CoverageArgumentProvider( - jacocoAgent, - intellijAgent, - this, - koverExtension, - taskExtension - ) - ) + jvmArgumentProviders.add(CoverageArgumentProvider(this, agents, providers.koverExtension)) return taskExtension } + + private fun Project.checkAlreadyApplied() { + var parent = parent + + while (parent != null) { + if (parent.plugins.hasPlugin(KoverPlugin::class.java)) { + throw GradleException("Kover plugin is applied in both parent module '${parent.name}' and child module '${this.name}'. Kover plugin should be applied only in parent module.") + } + parent = this.parent + } + } } private class CoverageArgumentProvider( - private val jacocoAgent: JacocoAgent, - private val intellijAgent: IntellijAgent, private val task: Task, - @get:Nested val koverExtension: KoverExtension, - @get:Nested val taskExtension: KoverTaskExtension + private val agents: Map, + @get:Nested + val koverExtension: Provider ) : CommandLineArgumentProvider, Named { + @get:Nested + val taskExtension: Provider = task.project.provider { + task.extensions.getByType(KoverTaskExtension::class.java) + } + @Internal override fun getName(): String { return "koverArgumentsProvider" } override fun asArguments(): MutableIterable { - if (!taskExtension.isEnabled || !koverExtension.isEnabled) { + val koverExtensionValue = koverExtension.get() + val taskExtensionValue = taskExtension.get() + + if (!taskExtensionValue.isEnabled || !koverExtensionValue.isEnabled) { return mutableListOf() } - return if (koverExtension.coverageEngine.get() == CoverageEngine.INTELLIJ) { - intellijAgent.buildCommandLineArgs(taskExtension, task) - } else { - jacocoAgent.buildCommandLineArgs(taskExtension) - } + return agents.getFor(koverExtensionValue.coverageEngine.get()).buildCommandLineArgs(task, taskExtensionValue) } } diff --git a/src/main/kotlin/kotlinx/kover/Providers.kt b/src/main/kotlin/kotlinx/kover/Providers.kt new file mode 100644 index 00000000..da190d3c --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/Providers.kt @@ -0,0 +1,111 @@ +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): ProjectProviders { + val modules: MutableMap = mutableMapOf() + + allprojects { + modules[it.name] = ModuleProviders( + it.provider { it.files(it.binaryReports()) }, + it.provider { it.files(it.smapFiles()) }, + it.provider { it.testTasks() }, + it.provider { it.collectDirs().first }, + it.provider { it.collectDirs().second } + ) + } + + 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 allSmapProvider: Provider = provider { files(allSmapFiles()) } + val allTestsProvider = provider { allTestTasks() } + + // all sources and all outputs providers are unused, so NOW it can return empty file collection + val emptyProvider: Provider = provider { files() } + val allModulesProviders = + ModuleProviders(allReportsProvider, allSmapProvider, allTestsProvider, emptyProvider, emptyProvider) + + return ProjectProviders(modules, allModulesProviders, engineProvider, classpathProvider, extensionProvider) +} + + +internal fun Project.allTestTasks(): List { + return allprojects.flatMap { it.testTasks() } +} + +internal fun Project.allBinaryReports(): List { + return allprojects.flatMap { it.binaryReports() } +} + +internal fun Project.allSmapFiles(): List { + return allprojects.flatMap { it.smapFiles() } +} + + +internal fun Project.testTasks(): List { + return tasks.withType(Test::class.java) + .filter { t -> t.extensions.getByType(KoverTaskExtension::class.java).isEnabled } +} + +internal fun Project.binaryReports(): List { + return tasks.withType(Test::class.java).asSequence() + .map { t -> t.extensions.getByType(KoverTaskExtension::class.java) } + // process binary report only from tasks with enabled cover + .filter { e -> e.isEnabled } + .map { e -> e.binaryReportFile.get() } + // process binary report only from tasks with sources + .filter { f -> f.exists() } + .toList() +} + +internal fun Project.smapFiles(): List { + return tasks.withType(Test::class.java).asSequence() + .map { t -> t.extensions.getByType(KoverTaskExtension::class.java) } + .filter { e -> e.isEnabled } + .mapNotNull { e -> e.smapFile.orNull } + /* + Binary reports and SMAP files have same ordering for IntelliJ engine: + * SMAP file is null if coverage engine is a JaCoCo by default - in this case property is unused + * SMAP file not creates by JaCoCo - property is unused + * test task have no sources - in this case binary report and SMAP file not exists + */ + .filter { f -> f.exists() } + .toList() +} + + +internal class ProjectProviders( + val modules: Map, + val allModules: ModuleProviders, + + val engine: Provider, + val classpath: Provider, + val koverExtension: Provider +) + +internal class ModuleProviders( + val reports: Provider, + val smap: Provider, + val tests: Provider>, + val sources: Provider, + val output: Provider +) + diff --git a/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt b/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt new file mode 100644 index 00000000..128dfda3 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt @@ -0,0 +1,31 @@ +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 TASK_EXTENSION_NAME = "kover" + + 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 VERIFY_TASK_NAME = "koverVerify" + + public const val XML_MODULE_REPORT_TASK_NAME = "koverXmlModuleReport" + public const val HTML_MODULE_REPORT_TASK_NAME = "koverHtmlModuleReport" + public const val MODULE_REPORT_TASK_NAME = "koverModuleReport" + public const val COLLECT_MODULE_REPORTS_TASK_NAME = "koverCollectModuleReports" + public const val MODULE_VERIFY_TASK_NAME = "koverModuleVerify" +} + +public object KoverPaths { + public const val HTML_AGG_REPORT_DEFAULT_PATH = "reports/kover/html" + public const val XML_AGG_REPORT_DEFAULT_PATH = "reports/kover/report.xml" + + public const val HTML_MODULE_REPORT_DEFAULT_PATH = "reports/kover/module-html" + public const val XML_MODULE_REPORT_DEFAULT_PATH = "reports/kover/module-xml/report.xml" + + public const val ALL_MODULES_REPORTS_DEFAULT_PATH = "reports/kover/modules" + +} diff --git a/src/main/kotlin/kotlinx/kover/api/KoverNames.kt b/src/main/kotlin/kotlinx/kover/api/KoverNames.kt deleted file mode 100644 index 6b962c83..00000000 --- a/src/main/kotlin/kotlinx/kover/api/KoverNames.kt +++ /dev/null @@ -1,15 +0,0 @@ -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 TASK_EXTENSION_NAME = "kover" - - public const val REPORT_TASK_NAME = "koverReport" - public const val COLLECT_TASK_NAME = "koverCollectReports" - public const val XML_REPORT_TASK_NAME = "koverXmlReport" - public const val HTML_REPORT_TASK_NAME = "koverHtmlReport" - public const val VERIFY_TASK_NAME = "koverVerify" -} diff --git a/src/main/kotlin/kotlinx/kover/engines/commons/AgentsFactory.kt b/src/main/kotlin/kotlinx/kover/engines/commons/AgentsFactory.kt new file mode 100644 index 00000000..2bf4ccac --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/engines/commons/AgentsFactory.kt @@ -0,0 +1,19 @@ +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 new file mode 100644 index 00000000..77197f25 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/engines/commons/CoverageAgent.kt @@ -0,0 +1,11 @@ +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/Reports.kt b/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt new file mode 100644 index 00000000..66768c79 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt @@ -0,0 +1,7 @@ +package kotlinx.kover.engines.commons + +import java.io.* + +internal class Report(val files: List, val modules: List) +internal class ReportFiles(val binary: File, val smap: File? = null) +internal class ModuleInfo(val sources: Iterable, val outputs: Iterable) diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt index c664c6ef..25856ee2 100644 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt @@ -5,24 +5,29 @@ package kotlinx.kover.engines.intellij 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 java.io.* -internal fun Project.createIntellijAgent(koverExtension: KoverExtension): IntellijAgent { +internal fun Project.createIntellijAgent(koverExtension: KoverExtension): CoverageAgent { val intellijConfig = createIntellijConfig(koverExtension) return IntellijAgent(intellijConfig) } -internal class IntellijAgent(val config: Configuration) { +private class IntellijAgent(private val config: Configuration): CoverageAgent { private val trackingPerTest = false // a flag to enable tracking per test coverage private val calculateForUnloadedClasses = true // a flag to calculate coverage for unloaded classes private val appendToDataFile = false // 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 private val generateSmapFile = true - fun buildCommandLineArgs(extension: KoverTaskExtension, task: Task): MutableList { + override val engine: CoverageEngine = CoverageEngine.INTELLIJ + override val classpath: FileCollection = config + + override fun buildCommandLineArgs(task: Task, extension: KoverTaskExtension): MutableList { val argsFile = File(task.temporaryDir, "intellijagent.args") argsFile.writeArgsToFile(extension) val jarFile = config.fileCollection { it.name == "intellij-coverage-agent" }.singleFile diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijCoverage.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt similarity index 82% rename from src/main/kotlin/kotlinx/kover/engines/intellij/IntellijCoverage.kt rename to src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt index 7eb0ed73..01f91182 100644 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijCoverage.kt +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt @@ -5,16 +5,16 @@ package kotlinx.kover.engines.intellij import kotlinx.kover.api.* +import kotlinx.kover.engines.commons.* +import kotlinx.kover.engines.commons.Report +import kotlinx.kover.engines.commons.ReportFiles import org.gradle.api.* import org.gradle.api.file.* import java.io.* import java.util.* internal fun Task.intellijReport( - binaryReportFiles: Iterable, - smapFiles: Iterable, - sources: Iterable, - outputs: Iterable, + report: Report, xmlFile: File?, htmlDir: File?, classpath: FileCollection @@ -29,7 +29,7 @@ internal fun Task.intellijReport( val argsFile = File(temporaryDir, "intellijreport.json") argsFile.printWriter().use { pw -> - pw.writeModuleReportJson(binaryReportFiles, smapFiles, sources, outputs, xmlFile, htmlDir) + pw.writeReportsJson(report, xmlFile, htmlDir) } project.javaexec { e -> @@ -78,11 +78,8 @@ JSON example: } ``` */ -private fun Writer.writeModuleReportJson( - binaryReportFiles: Iterable, - smapFiles: Iterable, - sources: Iterable, - outputs: Iterable, +private fun Writer.writeReportsJson( + report: Report, xmlFile: File?, htmlDir: File? ) { @@ -95,30 +92,35 @@ private fun Writer.writeModuleReportJson( appendLine(""" "html": "${it.safePath()}",""") } appendLine(""" "modules": [""") + report.modules.forEachIndexed { index, module -> + writeModuleReportJson(report.files, module, index == (report.modules.size - 1)) + } + appendLine(""" ]""") + appendLine("}") +} + +private fun Writer.writeModuleReportJson(reportFiles: Iterable, moduleInfo: ModuleInfo, isLast: Boolean) { appendLine(""" { "reports": [ """) - val smapIterator = smapFiles.iterator() - appendLine(binaryReportFiles.joinToString(",\n ", " ") { f -> - """{"ic": "${f.safePath()}", "smap": "${smapIterator.next().safePath()}"}""" + appendLine(reportFiles.joinToString(",\n ", " ") { f -> + """{"ic": "${f.binary.safePath()}", "smap": "${f.smap!!.safePath()}"}""" }) appendLine(""" ], """) appendLine(""" "output": [""") appendLine( - outputs.joinToString(",\n ", " ") { f -> '"' + f.safePath() + '"' }) + moduleInfo.outputs.joinToString(",\n ", " ") { f -> '"' + f.safePath() + '"' }) appendLine(""" ],""") appendLine(""" "sources": [""") appendLine( - sources.joinToString(",\n ", " ") { f -> '"' + f.safePath() + '"' }) + moduleInfo.sources.joinToString(",\n ", " ") { f -> '"' + f.safePath() + '"' }) appendLine(""" ]""") - appendLine(""" }""") - appendLine(""" ]""") - appendLine("}") + appendLine(""" }${if (isLast) "" else ","}""") } private fun File.safePath(): String { - return canonicalPath.replace("\\","\\\\").replace("\"", "\\\"") + return canonicalPath.replace("\\", "\\\\").replace("\"", "\\\"") } internal fun Task.intellijVerification( diff --git a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt index fbb1147a..71988042 100644 --- a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt +++ b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt @@ -5,17 +5,24 @@ package kotlinx.kover.engines.jacoco 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 java.io.* -internal fun Project.createJacocoAgent(koverExtension: KoverExtension): JacocoAgent { +internal fun Project.createJacocoAgent(koverExtension: KoverExtension): CoverageAgent { val jacocoConfig = createJacocoConfig(koverExtension) return JacocoAgent(jacocoConfig, this) } -internal class JacocoAgent(val config: Configuration, private val project: Project) { - fun buildCommandLineArgs(extension: KoverTaskExtension): MutableList { +private class JacocoAgent(private val config: Configuration, private val project: Project): CoverageAgent { + override val engine: CoverageEngine = CoverageEngine.JACOCO + + override val classpath: FileCollection = config + + override fun buildCommandLineArgs(task: Task, extension: KoverTaskExtension): MutableList { return mutableListOf("-javaagent:${getJacocoJar().canonicalPath}=${agentArgs(extension)}") } diff --git a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoCoverage.kt b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt similarity index 83% rename from src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoCoverage.kt rename to src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt index 38c3821a..df55b3bd 100644 --- a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoCoverage.kt +++ b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt @@ -6,6 +6,7 @@ package kotlinx.kover.engines.jacoco import groovy.lang.* import kotlinx.kover.api.* +import kotlinx.kover.engines.commons.Report import org.gradle.api.* import org.gradle.api.file.* import org.gradle.internal.reflect.* @@ -13,10 +14,8 @@ import java.io.* import java.math.* import java.util.* -internal fun Task.callJacocoAntReportTask( - binaryReportFiles: Iterable, - sources: Iterable, - outputs: Iterable, +private fun Task.callJacocoAntReportTask( + report: Report, classpath: FileCollection, block: GroovyObject.() -> Unit ) { @@ -30,10 +29,18 @@ internal fun Task.callJacocoAntReportTask( ) ) + val binaries: List = report.files.map(kotlinx.kover.engines.commons.ReportFiles::binary) + + val sources: MutableList = mutableListOf() + val outputs: MutableList = mutableListOf() + report.modules.forEach { module -> + sources.addAll(module.sources) + outputs.addAll(module.outputs) + } + builder.invokeWithBody("jacocoReport") { invokeWithBody("executiondata") { - val binaries = project.files(binaryReportFiles) - binaries.addToAntBuilder(this, "resources") + project.files(binaries).addToAntBuilder(this, "resources") } invokeWithBody("structure", mapOf("name" to project.name)) { invokeWithBody("sourcefiles") { @@ -48,14 +55,12 @@ internal fun Task.callJacocoAntReportTask( } internal fun Task.jacocoReport( - binaryReportFiles: Iterable, - sources: Iterable, - outputs: Iterable, - classpath: FileCollection, + report: Report, xmlFile: File?, - htmlDir: File? + htmlDir: File?, + classpath: FileCollection ) { - callJacocoAntReportTask(binaryReportFiles, sources, outputs, classpath) { + callJacocoAntReportTask(report, classpath) { if (xmlFile != null) { xmlFile.parentFile.mkdirs() invokeMethod("xml", mapOf("destfile" to xmlFile)) @@ -69,15 +74,11 @@ internal fun Task.jacocoReport( internal fun Task.jacocoVerification( - binaryReportFiles: Iterable, - sources: Iterable, - outputs: Iterable, - classpath: FileCollection, - rules: Iterable + report: Report, + rules: Iterable, + classpath: FileCollection ) { - - - callJacocoAntReportTask(binaryReportFiles, sources, outputs, classpath) { + callJacocoAntReportTask(report, classpath) { invokeWithBody("check", mapOf("failonviolation" to "false", "violationsproperty" to "jacocoErrors")) { rules.forEach { invokeWithBody("rule", mapOf("element" to "BUNDLE")) { diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverAggregateTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverAggregateTask.kt new file mode 100644 index 00000000..1dd8c7fc --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverAggregateTask.kt @@ -0,0 +1,84 @@ +/* + * 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 kotlinx.kover.engines.commons.ReportFiles +import kotlinx.kover.engines.intellij.* +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 java.io.* + +@CacheableTask +open class KoverAggregateTask : DefaultTask() { + @get:Nested + val binaryReportFiles: MapProperty = + project.objects.mapProperty(String::class.java, NestedFiles::class.java) + + @get:Nested + val smapFiles: 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) + + + internal fun report(): Report { + val binariesMap = binaryReportFiles.get() + val smapFilesMap = smapFiles.get() + val sourcesMap = srcDirs.get() + val outputsMap = outputDirs.get() + + val moduleNames = sourcesMap.keys + + val reportFiles: MutableList = mutableListOf() + val sourceFiles: MutableList = mutableListOf() + val outputFiles: MutableList = mutableListOf() + + // FIXME now all modules joined to one because of incorrect reporter's JSON format + moduleNames.map { name -> + val binaries = binariesMap.getValue(name) + + reportFiles += if (coverageEngine.get() == CoverageEngine.INTELLIJ) { + val smapFiles = smapFilesMap.getValue(name).files.get().iterator() + binaries.files.get().map { binary -> + ReportFiles(binary, smapFiles.next()) + } + } else { + binaries.files.get().map { binary -> + ReportFiles(binary) + } + } + + sourceFiles += sourcesMap.getValue(name).files.get() + outputFiles += outputsMap.getValue(name).files.get() + } + + return Report(reportFiles, listOf(ModuleInfo(sourceFiles, outputFiles))) + } +} + + +class NestedFiles(objects: ObjectFactory, files: Provider) { + @get:InputFiles + @get:PathSensitive(PathSensitivity.ABSOLUTE) + val files: Property = objects.property(FileCollection::class.java).also { it.set(files) } +} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverCollectingTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverCollectingModulesTask.kt similarity index 95% rename from src/main/kotlin/kotlinx/kover/tasks/KoverCollectingTask.kt rename to src/main/kotlin/kotlinx/kover/tasks/KoverCollectingModulesTask.kt index 715c4282..79400a9d 100644 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverCollectingTask.kt +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverCollectingModulesTask.kt @@ -4,7 +4,7 @@ import org.gradle.api.* import org.gradle.api.file.* import org.gradle.api.tasks.* -open class KoverCollectingTask : DefaultTask() { +open class KoverCollectingModulesTask : DefaultTask() { /** * Specifies directory path for collecting of all XML and HTML reports from all modules. */ diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReportTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReport.kt similarity index 52% rename from src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReportTask.kt rename to src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReport.kt index f672ef31..5b55cd6e 100644 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReportTask.kt +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReport.kt @@ -11,7 +11,7 @@ import org.gradle.api.file.* import org.gradle.api.tasks.* @CacheableTask -open class KoverHtmlReportTask : KoverCommonTask() { +open class KoverHtmlReportTask : KoverAggregateTask() { /** * Specifies directory path of generated HTML report. */ @@ -24,22 +24,48 @@ open class KoverHtmlReportTask : KoverCommonTask() { if (coverageEngine.get() == CoverageEngine.INTELLIJ) { intellijReport( - binaryReportFiles.get(), - smapFiles.get(), - srcDirs.get(), - outputDirs.get(), + report(), null, htmlDirFile, classpath.get() ) } else { jacocoReport( - binaryReportFiles.get(), - srcDirs.get(), - outputDirs.get(), + report(), + null, + htmlDirFile, classpath.get(), + ) + } + project.logger.lifecycle("Kover: aggregate HTML report file://${htmlDirFile.canonicalPath}/index.html") + } +} + +@CacheableTask +open class KoverHtmlModuleReportTask : KoverModuleTask() { + /** + * 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( + report(), null, htmlDirFile, + classpath.get() + ) + } else { + jacocoReport( + report(), + null, + htmlDirFile, + classpath.get(), ) } project.logger.lifecycle("Kover: HTML report for '${project.name}' file://${htmlDirFile.canonicalPath}/index.html") diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverCommonTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverModuleTask.kt similarity index 61% rename from src/main/kotlin/kotlinx/kover/tasks/KoverCommonTask.kt rename to src/main/kotlin/kotlinx/kover/tasks/KoverModuleTask.kt index 400a2d43..f1018acb 100644 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverCommonTask.kt +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverModuleTask.kt @@ -5,12 +5,15 @@ package kotlinx.kover.tasks import kotlinx.kover.api.* +import kotlinx.kover.engines.commons.ModuleInfo +import kotlinx.kover.engines.commons.Report +import kotlinx.kover.engines.commons.ReportFiles import org.gradle.api.* import org.gradle.api.file.* import org.gradle.api.provider.* import org.gradle.api.tasks.* -abstract class KoverCommonTask : DefaultTask() { +abstract class KoverModuleTask : DefaultTask() { @get:InputFiles @get:PathSensitive(PathSensitivity.ABSOLUTE) val binaryReportFiles: Property = project.objects.property(FileCollection::class.java) @@ -31,4 +34,21 @@ abstract class KoverCommonTask : DefaultTask() { @get:Classpath internal val classpath: Property = project.objects.property(FileCollection::class.java) + + internal fun report(): Report { + val binaries = binaryReportFiles.get() + val smapFiles = smapFiles.get().iterator() + + val reportFiles = if (coverageEngine.get() == CoverageEngine.INTELLIJ) { + binaries.map { binary -> + ReportFiles(binary, smapFiles.next()) + } + } else { + binaries.map { binary -> + ReportFiles(binary) + } + } + + return Report(reportFiles, listOf(ModuleInfo(srcDirs.get(), outputDirs.get()))) + } } diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverVerification.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverVerification.kt new file mode 100644 index 00000000..530c7838 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverVerification.kt @@ -0,0 +1,137 @@ +/* + * 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.api.KoverNames.XML_MODULE_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.XML_REPORT_TASK_NAME +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 java.io.* +import javax.inject.* + +open class KoverVerificationTask : KoverAggregateTask() { + 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, project.objects) + .also { configureRule.execute(it) } + } + + @TaskAction + fun verify() { + verify(report(), coverageEngine.get(), rulesInternal, classpath.get()) { + val xmlReport = + this.project.tasks.withType(KoverXmlReportTask::class.java) + .findByName(XML_REPORT_TASK_NAME) + ?: throw GradleException("Kover: task '$XML_REPORT_TASK_NAME' not exists but it is required for verification") + + var xmlFile = xmlReport.xmlReportFile.get().asFile + if (!xmlFile.exists()) { + xmlFile = File(temporaryDir, "counters.xml") + intellijReport( + it, + xmlFile, + null, + xmlReport.classpath.get() + ) + } + xmlFile + } + } + +} + +open class KoverModuleVerificationTask : KoverModuleTask() { + 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, project.objects) + .also { configureRule.execute(it) } + } + + @TaskAction + fun verify() { + verify(report(), coverageEngine.get(), rulesInternal, classpath.get()) { + val xmlReport = + this.project.tasks.withType(KoverXmlModuleReportTask::class.java) + .findByName(XML_MODULE_REPORT_TASK_NAME) + ?: throw GradleException("Kover: task '$XML_MODULE_REPORT_TASK_NAME' does not exist but it is required for verification") + + var xmlFile = xmlReport.xmlReportFile.get().asFile + if (!xmlFile.exists()) { + xmlFile = File(temporaryDir, "counters.xml") + intellijReport( + it, + xmlFile, + null, + xmlReport.classpath.get() + ) + } + xmlFile + } + } + +} + +private fun Task.verify( + report: Report, + engine: CoverageEngine, + rules: List, + classpath: FileCollection, + // Remove it after verification is implemented in the IntelliJ reporter + xmlFileProducer: (Report) -> File, +) { + if (engine == CoverageEngine.INTELLIJ) { + val xmlFile = xmlFileProducer(report) + this.intellijVerification(xmlFile, rules) + } else { + jacocoVerification(report, rules, classpath) + } +} + +private open class VerificationRuleImpl @Inject constructor(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).also { configureBound.execute(it) } + } +} + +private open class VerificationBoundImpl : 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 deleted file mode 100644 index 1438790a..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverVerificationTask.kt +++ /dev/null @@ -1,91 +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.api.KoverNames.XML_REPORT_TASK_NAME -import kotlinx.kover.engines.intellij.* -import kotlinx.kover.engines.jacoco.* -import org.gradle.api.* -import org.gradle.api.model.* -import org.gradle.api.tasks.* -import java.io.* -import javax.inject.* - -open class KoverVerificationTask : KoverCommonTask() { - 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, project.objects) - .also { configureRule.execute(it) } - } - - @TaskAction - fun verify() { - if (coverageEngine.get() == CoverageEngine.INTELLIJ) { - intellij() - } else { - jacoco() - } - } - - private fun intellij() { - val xmlReport = - this.project.tasks.withType(KoverXmlReportTask::class.java).findByName(XML_REPORT_TASK_NAME) - ?: throw GradleException("Kover: task '${XML_REPORT_TASK_NAME}' not exists but it is required for verification") - - var xmlFile = xmlReport.xmlReportFile.get().asFile - if (!xmlFile.exists()) { - xmlFile = File(temporaryDir, "counters.xml") - intellijReport( - xmlReport.binaryReportFiles.get(), - xmlReport.smapFiles.get(), - xmlReport.srcDirs.get(), - xmlReport.outputDirs.get(), - xmlFile, - null, - xmlReport.classpath.get() - ) - } - this.intellijVerification(xmlFile, rulesInternal) - } - - private fun jacoco() { - this.jacocoVerification( - binaryReportFiles.get(), - srcDirs.get(), - outputDirs.get(), - classpath.get(), - rulesInternal - ) - } - -} - -private open class VerificationRuleImpl @Inject constructor(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).also { configureBound.execute(it) } - } -} - -private open class VerificationBoundImpl : 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/KoverXmlReport.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverXmlReport.kt new file mode 100644 index 00000000..54d116af --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverXmlReport.kt @@ -0,0 +1,69 @@ +/* + * 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 KoverXmlReportTask : KoverAggregateTask() { + + /** + * 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( + report(), + xmlReportFile.get().asFile, + null, + classpath.get() + ) + } else { + jacocoReport( + report(), + xmlReportFile.get().asFile, + null, + classpath.get() + ) + } + } +} + +@CacheableTask +open class KoverXmlModuleReportTask : KoverModuleTask() { + + /** + * 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( + 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/KoverXmlReportTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverXmlReportTask.kt deleted file mode 100644 index effe90bd..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverXmlReportTask.kt +++ /dev/null @@ -1,45 +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 KoverXmlReportTask : KoverCommonTask() { - - /** - * 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( - binaryReportFiles.get(), - smapFiles.get(), - srcDirs.get(), - outputDirs.get(), - xmlReportFile.get().asFile, - null, - classpath.get() - ) - } else { - jacocoReport( - binaryReportFiles.get(), - srcDirs.get(), - outputDirs.get(), - classpath.get(), - xmlReportFile.get().asFile, - null - ) - } - } -}