From 96ead04f841e30106cc6be58a24676eaa788a19b Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Tue, 7 Dec 2021 23:16:11 +0300 Subject: [PATCH] Implemented aggregate reports Resolves #20, #43 --- .../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 | 59 ++-- .../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 | 294 ++++++++++-------- 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 --- 40 files changed, 1142 insertions(+), 475 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/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..896041c6 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) { +internal fun RunResult.checkDefaultBinaryReport(mustExists: Boolean = true) { + val binary: String = defaultBinaryReport(engine, projectType) + if (mustExists) { 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 (mustExists) { + file(smap) { + assertTrue { exists() } + assertTrue { length() > 0 } + } + } else { + file(smap) { + assertFalse { exists() } + } } } } -internal fun RunResult.checkReports(xmlPath: String, htmlPath: String, mustExists: Boolean = true) { +internal fun RunResult.checkDefaultReports(mustExists: Boolean = true) { + checkReports(defaultXmlReport(), defaultHtmlReport(), mustExists) +} + +internal fun RunResult.checkDefaultModuleReports(mustExists: Boolean = true) { + checkReports(defaultXmlModuleReport(), defaultHtmlModuleReport(), mustExists) +} + +internal fun RunResult.checkReports(xmlPath: String, htmlPath: String, mustExists: Boolean) { if (mustExists) { 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..cf6e0225 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.HTML_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.MODULE_HTML_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.* @@ -30,173 +40,191 @@ class KoverPlugin : Plugin { override fun apply(target: Project) { 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." + } - it.mustRunAfter(xmlReportTask) - it.mustRunAfter(htmlReportTask) + val htmlReportTask = createKoverModuleTask( + MODULE_HTML_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." + } - it.xmlFiles[proj.name] = xmlReportTask.xmlReportFile - it.htmlDirs[proj.name] = htmlReportTask.htmlReportDir + 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." + } + + 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." + } + + 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) + } + + private fun Project.createCollectingTask() { + tasks.create(COLLECT_TASK_NAME, KoverCollectingModulesTask::class.java) { + it.group = VERIFICATION_GROUP + it.description = "Collects reports from all modules in one directory." + it.outputDir.set(project.layout.buildDirectory.dir(ALL_MODULES_REPORTS_DEFAULT_PATH)) + // disable UP-TO-DATE check for task: it will be executed every time + it.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(MODULE_HTML_REPORT_TASK_NAME) + + it.mustRunAfter(xmlReportTask) + it.mustRunAfter(htmlReportTask) - 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." + it.xmlFiles[proj.name] = xmlReportTask.xmlReportFile + it.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 +240,56 @@ 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 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..ec1969f2 --- /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 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" + + public const val MODULE_REPORT_TASK_NAME = "koverModuleReport" + public const val XML_MODULE_REPORT_TASK_NAME = "koverXmlModuleReport" + public const val MODULE_HTML_REPORT_TASK_NAME = "koverHtmlModuleReport" + 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..e6e2f239 --- /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("Not found coverage agent for Coverage Engine '$engine'") +} 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..b1909528 --- /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' 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 + } + } + +} + +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..23a41d15 --- /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. + */ + val xmlReportFile: RegularFileProperty = project.objects.fileProperty() + @OutputFile get + + @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 - ) - } - } -}