diff --git a/buildSrc/src/main/kotlin/PublicationMavenCentral.kt b/buildSrc/src/main/kotlin/PublicationMavenCentral.kt index ce2ee67c..14d0f56e 100644 --- a/buildSrc/src/main/kotlin/PublicationMavenCentral.kt +++ b/buildSrc/src/main/kotlin/PublicationMavenCentral.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ import org.gradle.api.* diff --git a/gradle.properties b/gradle.properties index 92f2dfd3..0a3cd728 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=0.5.1 +version=0.6.0-SNAPSHOT group=org.jetbrains.kotlinx kotlin.code.style=official diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt index 4b392e11..57440f2b 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt @@ -14,7 +14,7 @@ internal class AdaptersTests : BaseGradleScriptTest() { Classes from plugins applied in subproject not accessible for Kover in root project. Therefore, Kover is forced to use reflection to work with extensions of the kotlin multiplatform plugin. */ - internalSample("different-plugins") + sampleBuild("different-plugins") .run("koverMergedXmlReport") { xml(defaultMergedXmlReport()) { classCounter("org.jetbrains.CommonClass").assertFullyCovered() @@ -22,7 +22,7 @@ internal class AdaptersTests : BaseGradleScriptTest() { } } - internalSample("different-plugins") + sampleBuild("different-plugins") .run("koverXmlReport") { subproject("subproject-multiplatform") { xml(defaultXmlReport()) { diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ConfigurationCacheTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ConfigurationCacheTests.kt index 5c2d89bb..125a3be0 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ConfigurationCacheTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ConfigurationCacheTests.kt @@ -3,12 +3,25 @@ package kotlinx.kover.test.functional.cases import kotlinx.kover.test.functional.core.* import kotlin.test.* -internal class ConfigurationCacheTests: BaseGradleScriptTest() { +internal class ConfigurationCacheTests : BaseGradleScriptTest() { @Test fun testConfigCache() { - builder("Testing configuration cache support") - .sources("simple") - .build() - .run("build", "koverMergedReport", "koverMergedVerify", "koverReport", "koverVerify", "--configuration-cache") + val build = diverseBuild() + build.addKoverRootProject { + sourcesFrom("simple") + koverMerged { + enable() + } + } + + val runner = build.prepare() + runner.run( + "build", + "koverMergedReport", + "koverMergedVerify", + "koverReport", + "koverVerify", + "--configuration-cache" + ) } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt index a95f1a07..75486281 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt @@ -7,15 +7,20 @@ import kotlin.test.* internal class DefaultConfigTests : BaseGradleScriptTest() { @Test fun testImplicitConfigs() { - builder("Test implicit default settings") - .languages(GradleScriptLanguage.GROOVY, GradleScriptLanguage.KOTLIN) - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .sources("simple") - .build() - .run("build") { - checkDefaultBinaryReport() - checkDefaultMergedReports() - } + val build = diverseBuild( + languages = ALL_LANGUAGES, + types = ALL_TYPES + ) + build.addKoverRootProject { + sourcesFrom("simple") + } + val runner = build.prepare() + + runner.run("koverReport") { + checkDefaultBinaryReport() + checkDefaultReports() + } + } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt index b318bbed..2b2f7449 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt @@ -1,6 +1,5 @@ package kotlinx.kover.test.functional.cases -import kotlinx.kover.api.* import kotlinx.kover.test.functional.cases.utils.* import kotlinx.kover.test.functional.core.* import kotlinx.kover.test.functional.core.BaseGradleScriptTest @@ -10,47 +9,41 @@ internal class InstrumentationFilteringTests : BaseGradleScriptTest() { @Test fun testExclude() { - builder("Test exclusion of classes from instrumentation") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("simple") - .configTest( - """excludes = listOf("org.jetbrains.*Exa?ple*")""", - """excludes = ['org.jetbrains.*Exa?ple*']""" - ) - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.ExampleClass").assertFullyMissed() - classCounter("org.jetbrains.SecondClass").assertCovered() - } + val build = diverseBuild(ALL_LANGUAGES, ALL_ENGINES, ALL_TYPES) + build.addKoverRootProject { + sourcesFrom("simple") + testTasks { + excludes("org.jetbrains.*Exa?ple*") } + } + val runner = build.prepare() + runner.run("build", "koverXmlReport") { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.ExampleClass").assertFullyMissed() + classCounter("org.jetbrains.SecondClass").assertCovered() + } + } + } @Test fun testExcludeInclude() { - builder("Test inclusion and exclusion of classes in instrumentation") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("simple") - .configTest( - """includes = listOf("org.jetbrains.*Cla?s")""", - """includes = ['org.jetbrains.*Cla?s']""" - ) - .configTest( - """excludes = listOf("org.jetbrains.*Exa?ple*")""", - """excludes = ['org.jetbrains.*Exa?ple*']""" - ) - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.ExampleClass").assertFullyMissed() - classCounter("org.jetbrains.Unused").assertFullyMissed() - classCounter("org.jetbrains.SecondClass").assertCovered() - } + val build = diverseBuild(ALL_LANGUAGES, ALL_ENGINES, ALL_TYPES) + build.addKoverRootProject { + sourcesFrom("simple") + testTasks { + includes("org.jetbrains.*Cla?s") + excludes("org.jetbrains.*Exa?ple*") + } + } + val runner = build.prepare() + runner.run("build", "koverXmlReport") { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.ExampleClass").assertFullyMissed() + classCounter("org.jetbrains.Unused").assertFullyMissed() + classCounter("org.jetbrains.SecondClass").assertCovered() } + } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiProjectTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiProjectTests.kt index 48b7d475..e4cbdcc8 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiProjectTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/MultiProjectTests.kt @@ -1,175 +1,223 @@ package kotlinx.kover.test.functional.cases -import kotlinx.kover.api.* import kotlinx.kover.test.functional.cases.utils.* import kotlinx.kover.test.functional.cases.utils.defaultMergedXmlReport +import kotlinx.kover.test.functional.core.* import kotlinx.kover.test.functional.core.BaseGradleScriptTest -import kotlinx.kover.test.functional.core.ProjectType import org.gradle.testkit.runner.* import kotlin.test.* internal class MultiProjectTests : BaseGradleScriptTest() { private val subprojectName = "common" + private val rootName = "kover-functional-test" + @Test fun testMergedReports() { - builder("Testing the generation of merged reports") - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") - } - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.CommonClass").assertFullyCovered() - classCounter("org.jetbrains.CommonInternalClass").assertFullyCovered() - classCounter("org.jetbrains.UserClass").assertFullyCovered() - } - } - } + val build = diverseBuild(engines = ALL_ENGINES, types = ALL_TYPES) + val subPath = build.addKoverSubproject(subprojectName) { + sourcesFrom("multiproject-common") + } - @Test - fun testIsolatedProjectsReports() { - builder("Testing the generation of project reports with running tests only for this project") - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") + build.addKoverRootProject { + sourcesFrom("multiproject-user") + subproject(subPath) + + koverMerged { + enable() } - .build() - .run("koverReport") { - xml(defaultXmlReport()) { - classCounter("org.jetbrains.CommonClass").assertAbsent() - classCounter("org.jetbrains.CommonInternalClass").assertAbsent() - classCounter("org.jetbrains.UserClass").assertFullyCovered() - } + } - subproject(subprojectName) { - xml(defaultXmlReport()) { - classCounter("org.jetbrains.UserClass").assertAbsent() - // common class covered partially because calls from the root project are not counted - classCounter("org.jetbrains.CommonClass").assertCovered() - classCounter("org.jetbrains.CommonInternalClass").assertCovered() - } - } + val runner = build.prepare() + runner.run(":koverMergedXmlReport") { + xml(defaultMergedXmlReport()) { + classCounter("org.jetbrains.CommonClass").assertFullyCovered() + classCounter("org.jetbrains.CommonInternalClass").assertFullyCovered() + classCounter("org.jetbrains.UserClass").assertFullyCovered() } + } } @Test - fun testLinkedProjectsReports() { - builder("Testing the generation of project reports with running all tests for all projects") - .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .configKover { runAllTestsForProjectTask = true } - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") + fun testIsolatedProjectsReports() { + val build = diverseBuild(engines = ALL_ENGINES, types = ALL_TYPES) + val subPath = build.addKoverSubproject(subprojectName) { + sourcesFrom("multiproject-common") + } + + build.addKoverRootProject { + sourcesFrom("multiproject-user") + subproject(subPath) + } + + + val runner = build.prepare() + runner.run("koverXmlReport") { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.CommonClass").assertAbsent() + classCounter("org.jetbrains.CommonInternalClass").assertAbsent() + classCounter("org.jetbrains.UserClass").assertFullyCovered() } - .build() - .run("koverReport") { - xml(defaultXmlReport()) { - classCounter("org.jetbrains.CommonClass").assertAbsent() - classCounter("org.jetbrains.CommonInternalClass").assertAbsent() - classCounter("org.jetbrains.UserClass").assertFullyCovered() - } - subproject(subprojectName) { - xml(defaultXmlReport()) { - classCounter("org.jetbrains.UserClass").assertAbsent() + subproject(subprojectName) { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.UserClass").assertAbsent() - // common class fully covered because calls from the root project are counted too - classCounter("org.jetbrains.CommonClass").assertFullyCovered() - classCounter("org.jetbrains.CommonInternalClass").assertFullyCovered() - } + // common class covered partially because calls from the root project are not counted + classCounter("org.jetbrains.CommonClass").assertCovered() + classCounter("org.jetbrains.CommonInternalClass").assertCovered() } } + } } +// +// @Test +// fun testLinkedProjectsReports() { +// builder("Testing the generation of project reports with running all tests for all projects") +// .types(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) +// .engines(CoverageEngineVendor.INTELLIJ, CoverageEngineVendor.JACOCO) +// .configKover { runAllTestsForProjectTask = true } +// .sources("multiproject-user") +// .subproject(subprojectName) { +// sources("multiproject-common") +// } +// .build() +// .run("koverReport") { +// xml(defaultXmlReport()) { +// classCounter("org.jetbrains.CommonClass").assertAbsent() +// classCounter("org.jetbrains.CommonInternalClass").assertAbsent() +// classCounter("org.jetbrains.UserClass").assertFullyCovered() +// } +// +// subproject(subprojectName) { +// xml(defaultXmlReport()) { +// classCounter("org.jetbrains.UserClass").assertAbsent() +// +// // common class fully covered because calls from the root project are counted too +// classCounter("org.jetbrains.CommonClass").assertFullyCovered() +// classCounter("org.jetbrains.CommonInternalClass").assertFullyCovered() +// } +// } +// } +// } @Test fun testDisabledKover() { - builder("Testing disabling whole Kover") - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") + val build = diverseBuild(engines = ALL_ENGINES, types = ALL_TYPES) + val subPath = build.addKoverSubproject(subprojectName) { + sourcesFrom("multiproject-common") + kover { + isDisabled = true } - .configKover { disabled = true } - .build() - .run("build", "koverHtmlReport") { - checkDefaultBinaryReport(false) - checkOutcome("koverMergedHtmlReport", TaskOutcome.SKIPPED) - checkOutcome("koverMergedVerify", TaskOutcome.SKIPPED) + } + build.addKoverRootProject { + sourcesFrom("multiproject-user") + subproject(subPath) + kover { + isDisabled = true + } + } + + val runner = build.prepare() + runner.run("koverReport", "koverVerify") { + checkDefaultBinaryReport(false) + + checkOutcome("koverHtmlReport", TaskOutcome.SKIPPED) + checkOutcome("koverXmlReport", TaskOutcome.SKIPPED) + checkOutcome("koverVerify", TaskOutcome.SKIPPED) + + subproject(subprojectName) { + checkDefaultBinaryReport(false) checkOutcome("koverHtmlReport", TaskOutcome.SKIPPED) + checkOutcome("koverXmlReport", TaskOutcome.SKIPPED) checkOutcome("koverVerify", TaskOutcome.SKIPPED) - checkOutcome("koverMergedHtmlReport", TaskOutcome.SKIPPED) - - subproject(subprojectName) { - checkDefaultBinaryReport(false) - checkOutcome("koverHtmlReport", TaskOutcome.SKIPPED) - checkOutcome("koverVerify", TaskOutcome.SKIPPED) - } } + } } @Test - fun testDisableSubproject() { - builder("Testing disabling one of subproject") - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") - } - .configKover { disabledProjects += subprojectName } - .build() - .run("build", "koverReport") { - checkDefaultBinaryReport() - checkDefaultMergedReports() - checkDefaultReports() - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.UserClass").assertFullyCovered() - - // classes from disabled project should not be included in the merged report - classCounter("org.jetbrains.CommonClass").assertAbsent() - classCounter("org.jetbrains.CommonInternalClass").assertAbsent() + fun testIncludeProject() { + val build = diverseBuild(engines = ALL_ENGINES, types = ALL_TYPES) + val subPath = build.addKoverSubproject(subprojectName) { + sourcesFrom("multiproject-common") + } + + build.addKoverRootProject { + sourcesFrom("multiproject-user") + subproject(subPath) + + koverMerged { + enable() + filters { + projects { + includes += rootName + } } + } + } + + val runner = build.prepare() + runner.run("koverMergedReport") { + checkDefaultBinaryReport() + checkDefaultReports(false) + checkDefaultMergedReports() + xml(defaultMergedXmlReport()) { + classCounter("org.jetbrains.UserClass").assertFullyCovered() + + // classes from disabled project should not be included in the merged report + classCounter("org.jetbrains.CommonClass").assertAbsent() + classCounter("org.jetbrains.CommonInternalClass").assertAbsent() + } - subproject(subprojectName) { - checkDefaultBinaryReport(false) - checkDefaultMergedReports(false) - checkDefaultReports(false) - } + subproject(subprojectName) { + checkDefaultBinaryReport(false) + checkDefaultMergedReports(false) + checkDefaultReports(false) } + } } @Test - fun testDisableSubprojectByPath() { - builder("Testing disabling one of subproject by path") - .sources("multiproject-user") - .subproject(subprojectName) { - sources("multiproject-common") - } - .configKover { disabledProjects += ":$subprojectName" } - .build() - .run("build", "koverReport") { - checkDefaultBinaryReport() - checkDefaultMergedReports() - checkDefaultReports() - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.UserClass").assertFullyCovered() - - // classes from disabled project should not be included in the merged report - classCounter("org.jetbrains.CommonClass").assertAbsent() - classCounter("org.jetbrains.CommonInternalClass").assertAbsent() + fun testIncludeProjectByPath() { + val build = diverseBuild(engines = ALL_ENGINES, types = ALL_TYPES) + val subPath = build.addKoverSubproject(subprojectName) { + sourcesFrom("multiproject-common") + } + + build.addKoverRootProject { + sourcesFrom("multiproject-user") + subproject(subPath) + + koverMerged { + enable() + filters { + projects { + includes += ":" + } } + } + } + + val runner = build.prepare() + runner.run("koverMergedReport") { + checkDefaultBinaryReport() + checkDefaultReports(false) + checkDefaultMergedReports() + xml(defaultMergedXmlReport()) { + classCounter("org.jetbrains.UserClass").assertFullyCovered() + + // classes from disabled project should not be included in the merged report + classCounter("org.jetbrains.CommonClass").assertAbsent() + classCounter("org.jetbrains.CommonInternalClass").assertAbsent() + } - subproject(subprojectName) { - checkDefaultBinaryReport(false) - checkDefaultMergedReports(false) - checkDefaultReports(false) - } + subproject(subprojectName) { + checkDefaultBinaryReport(false) + checkDefaultMergedReports(false) + checkDefaultReports(false) } + } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt index f1491a82..f2ea6cd8 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt @@ -1,7 +1,8 @@ package kotlinx.kover.test.functional.cases -import kotlinx.kover.api.* import kotlinx.kover.test.functional.cases.utils.* +import kotlinx.kover.test.functional.core.* +import kotlinx.kover.test.functional.core.ALL_ENGINES import kotlinx.kover.test.functional.core.BaseGradleScriptTest import org.gradle.testkit.runner.* import kotlin.test.* @@ -9,56 +10,56 @@ import kotlin.test.* internal class ReportsCachingTests : BaseGradleScriptTest() { @Test fun testCaching() { - builder("Test caching of merged reports") - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("simple") - .withLocalCache() - .build() - .run("build", "--build-cache") { - checkDefaultBinaryReport() - checkDefaultMergedReports() - checkOutcome("test", TaskOutcome.SUCCESS) - checkOutcome("koverMergedXmlReport", TaskOutcome.SUCCESS) - checkOutcome("koverMergedHtmlReport", TaskOutcome.SUCCESS) - } - .run("clean", "--build-cache") { - checkDefaultBinaryReport(false) - checkDefaultMergedReports(false) - } - .run("build", "--build-cache") { - checkDefaultBinaryReport() - checkDefaultMergedReports() - checkOutcome("test", TaskOutcome.FROM_CACHE) - checkOutcome("koverMergedXmlReport", TaskOutcome.FROM_CACHE) - checkOutcome("koverMergedHtmlReport", TaskOutcome.FROM_CACHE) - } + val build = diverseBuild(engines = ALL_ENGINES, withCache = true) + build.addKoverRootProject { + sourcesFrom("simple") + } + val runner = build.prepare() + runner.run("koverReport") { + checkDefaultBinaryReport() + checkDefaultReports() + checkOutcome("test", TaskOutcome.SUCCESS) + checkOutcome("koverXmlReport", TaskOutcome.SUCCESS) + checkOutcome("koverHtmlReport", TaskOutcome.SUCCESS) + } + runner.run("clean") { + checkDefaultBinaryReport(false) + checkDefaultReports(false) + } + runner.run("koverReport") { + checkDefaultBinaryReport() + checkDefaultReports() + checkOutcome("test", TaskOutcome.FROM_CACHE) + checkOutcome("koverXmlReport", TaskOutcome.FROM_CACHE) + checkOutcome("koverHtmlReport", TaskOutcome.FROM_CACHE) + } } @Test fun testProjectReportCaching() { - builder("Test caching projects reports") - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("simple") - .withLocalCache() - .build() - .run("koverReport", "--build-cache") { - checkDefaultBinaryReport() - checkDefaultReports() - checkOutcome("test", TaskOutcome.SUCCESS) - checkOutcome("koverXmlReport", TaskOutcome.SUCCESS) - checkOutcome("koverHtmlReport", TaskOutcome.SUCCESS) - } - .run("clean", "--build-cache") { - checkDefaultBinaryReport(false) - checkDefaultReports(false) - } - .run("koverReport", "--build-cache") { - checkDefaultBinaryReport() - checkDefaultReports() - checkOutcome("test", TaskOutcome.FROM_CACHE) - checkOutcome("koverXmlReport", TaskOutcome.FROM_CACHE) - checkOutcome("koverHtmlReport", TaskOutcome.FROM_CACHE) - } + val build = diverseBuild(engines = ALL_ENGINES, withCache = true) + build.addKoverRootProject { + sourcesFrom("simple") + } + val runner = build.prepare() + runner.run("koverReport") { + checkDefaultBinaryReport() + checkDefaultReports() + checkOutcome("test", TaskOutcome.SUCCESS) + checkOutcome("koverXmlReport", TaskOutcome.SUCCESS) + checkOutcome("koverHtmlReport", TaskOutcome.SUCCESS) + } + runner.run("clean") { + checkDefaultBinaryReport(false) + checkDefaultReports(false) + } + runner.run("koverReport") { + checkDefaultBinaryReport() + checkDefaultReports() + checkOutcome("test", TaskOutcome.FROM_CACHE) + checkOutcome("koverXmlReport", TaskOutcome.FROM_CACHE) + checkOutcome("koverHtmlReport", TaskOutcome.FROM_CACHE) + } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsFilteringTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsFilteringTests.kt index c310e411..8d5e4ce2 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsFilteringTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsFilteringTests.kt @@ -9,54 +9,50 @@ internal class ReportsFilteringTests : BaseGradleScriptTest() { @Test fun testExclude() { - builder("Test exclusion of classes from XML report") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .sources("simple") - .config( - """ - tasks.koverMergedXmlReport { - excludes = listOf("org.jetbrains.*Exa?ple*") - }""".trimIndent(), - """ - tasks.koverMergedXmlReport { - excludes = ['org.jetbrains.*Exa?ple*'] - }""".trimIndent() - ) - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.ExampleClass").assertAbsent() - classCounter("org.jetbrains.SecondClass").assertCovered() + val build = diverseBuild(languages = ALL_LANGUAGES) + build.addKoverRootProject { + sourcesFrom("simple") + + kover { + filters { + classes { + excludes += "org.jetbrains.*Exa?ple*" + } } } + } + val runner = build.prepare() + runner.run("koverXmlReport") { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.ExampleClass").assertAbsent() + classCounter("org.jetbrains.SecondClass").assertCovered() + } + } } @Test fun testExcludeInclude() { - builder("Test inclusion and exclusion of classes in XML report") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .sources("simple") - .config( - """ - tasks.koverMergedXmlReport { - includes = listOf("org.jetbrains.*Cla?s") - excludes = listOf("org.jetbrains.*Exa?ple*") - }""".trimIndent(), + val build = diverseBuild(languages = ALL_LANGUAGES) + build.addKoverRootProject { + sourcesFrom("simple") - """ - tasks.koverMergedXmlReport { - includes = ['org.jetbrains.*Cla?s'] - excludes = ['org.jetbrains.*Exa?ple*'] - }""".trimIndent() - ) - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - classCounter("org.jetbrains.ExampleClass").assertAbsent() - classCounter("org.jetbrains.Unused").assertAbsent() - classCounter("org.jetbrains.SecondClass").assertFullyCovered() + kover { + filters { + classes { + excludes += "org.jetbrains.*Exa?ple*" + includes += "org.jetbrains.*Cla?s" + } } } + } + val runner = build.prepare() + runner.run("koverXmlReport") { + xml(defaultXmlReport()) { + classCounter("org.jetbrains.ExampleClass").assertAbsent() + classCounter("org.jetbrains.Unused").assertAbsent() + classCounter("org.jetbrains.SecondClass").assertFullyCovered() + } + } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt index b423b3b1..678599c5 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/VerificationTests.kt @@ -12,84 +12,102 @@ import kotlin.test.* internal class VerificationTests : BaseGradleScriptTest() { @Test fun testVerified() { - builder("Test verification passed") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .engines(CoverageEngine.INTELLIJ, CoverageEngine.JACOCO) - .sources("simple") - .rule("test rule") { - bound { - minValue = 50 - maxValue = 60 - } - bound { - valueType = VerificationValueType.COVERED_LINES_COUNT - minValue = 2 - maxValue = 10 + val build = diverseBuild(languages = ALL_LANGUAGES, engines = ALL_ENGINES) + build.addKoverRootProject { + sourcesFrom("simple") + + kover { + verify { + rule { + name = "test rule" + bound { + minValue = 50 + maxValue = 60 + } + bound { + valueType = VerificationValueType.COVERED_COUNT + minValue = 2 + maxValue = 10 + } + } } } - .build() - .run("koverVerify") + } + + build.prepare().run("koverVerify", "--stacktrace") } @Test fun testNotVerifiedIntelliJ() { - builder("Test verification failed for IntelliJ Engine") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .engines(CoverageEngine.INTELLIJ) - .sources("simple") - .rule("test rule") { - bound { - minValue = 58 - maxValue = 60 - } - bound { - valueType = VerificationValueType.COVERED_LINES_COUNT - minValue = 2 - maxValue = 3 + val build = diverseBuild(languages = ALL_LANGUAGES, engines = listOf(CoverageEngineVendor.INTELLIJ)) + build.addKoverRootProject { + sourcesFrom("simple") + + kover { + verify { + rule { + name = "test rule" + bound { + minValue = 58 + maxValue = 60 + } + bound { + valueType = VerificationValueType.COVERED_COUNT + minValue = 2 + maxValue = 3 + } + } } } - .build() - .runWithError("koverVerify") { - output { - assertTrue { - this.contains( - "> Rule 'test rule' violated:\n" + - " covered lines percentage is 57.142900, but expected minimum is 58\n" + - " covered lines count is 4, but expected maximum is 3" - ) - } + } + + build.prepare().runWithError("koverVerify") { + output { + assertTrue { + this.contains( + "> Rule 'test rule' violated:\n" + + " covered lines percentage is 57.142900, but expected minimum is 58\n" + + " covered lines count is 4, but expected maximum is 3" + ) } } + } } @Test fun testNotVerifiedJaCoCo() { - builder("Test verification failed for JaCoCo Engine") - .languages(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) - .engines(CoverageEngine.JACOCO) - .sources("simple") - .rule("test rule") { - bound { - minValue = 58 - maxValue = 60 - } - bound { - valueType = VerificationValueType.COVERED_LINES_COUNT - minValue = 2 - maxValue = 3 + val build = diverseBuild(languages = ALL_LANGUAGES, engines = listOf(CoverageEngineVendor.JACOCO)) + build.addKoverRootProject { + sourcesFrom("simple") + + kover { + verify { + rule { + name = "test rule" + bound { + minValue = 58 + maxValue = 60 + } + bound { + valueType = VerificationValueType.COVERED_COUNT + minValue = 2 + maxValue = 3 + } + } } } - .build() - .runWithError("koverVerify") { - output { - assertTrue { - this.contains( - "[ant:jacocoReport] Rule violated for bundle :: lines covered ratio is 0.50, but expected minimum is 0.58\n" + - "[ant:jacocoReport] Rule violated for bundle :: lines covered count is 4, but expected maximum is 3" - ) - } + } + + build.prepare().runWithError("koverVerify") { + output { + assertTrue { + this.contains( + "[ant:jacocoReport] Rule violated for bundle :: lines covered ratio is 0.50, but expected minimum is 0.58\n" + + "[ant:jacocoReport] Rule violated for bundle :: lines covered count is 4, but expected maximum is 3" + ) } } + } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/counters/CountersValueTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/counters/CountersValueTests.kt index 8a9e3bb1..792564fc 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/counters/CountersValueTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/counters/CountersValueTests.kt @@ -2,44 +2,49 @@ package kotlinx.kover.test.functional.cases.counters import kotlinx.kover.api.* import kotlinx.kover.test.functional.cases.utils.* +import kotlinx.kover.test.functional.core.* import kotlinx.kover.test.functional.core.BaseGradleScriptTest import kotlin.test.* internal class CountersValueTests : BaseGradleScriptTest() { @Test fun testBasicCounterCases() { - builder("Testing of basic counting capabilities by an IntelliJ Agent") - .engines(CoverageEngine.INTELLIJ) - .sources("counters") - .build() - .run("build") { - xml(defaultMergedXmlReport()) { - // test on branch counter - methodCounter("org.jetbrains.MyBranchedClass", "foo", type = "BRANCH").assertCovered(1, 3) - - // test on constructor of used sealed classes - methodCounter("org.jetbrains.Sealed", "").assertFullyCovered() - methodCounter("org.jetbrains.SealedWithInit", "").assertFullyCovered() - methodCounter("org.jetbrains.SealedWithConstructor", "").assertFullyCovered() - - // test on empty objects - classCounter("org.jetbrains.UnusedObject").assertFullyMissed() - classCounter("org.jetbrains.UsedObject").assertFullyCovered() - - // deprecated functions (ERROR and HIDDEN) should not be included in the report. - methodCounter("org.jetbrains.Different", "deprecatedError").assertAbsent() - methodCounter("org.jetbrains.Different", "deprecatedHidden").assertAbsent() - // WARNING deprecated functions should be included in the report. - methodCounter("org.jetbrains.Different", "deprecatedWarn").assertFullyMissed() - - // empty functions must be included in the report with line counter - methodCounter("org.jetbrains.Different", "emptyFun", type = "LINE").assertFullyMissed() - - // Instruction counters - // helloWorld contains 4 instructions. `return` ignored by reporter and the first instruction is not covered because of a bug in the IR compiler - // FIXME after https://youtrack.jetbrains.com/issue/KT-51080 is fixed, the number should become 3 - methodCounter("org.jetbrains.Different", "helloWorld").assertTotal(2) - } + val build = diverseBuild( + engines = listOf(CoverageEngineVendor.INTELLIJ) + ) + build.addKoverRootProject { + sourcesFrom("counters") + } + + val runner = build.prepare() + runner.run("koverXmlReport") { + xml(defaultXmlReport()) { + // test on branch counter + methodCounter("org.jetbrains.MyBranchedClass", "foo", type = "BRANCH").assertCovered(1, 3) + + // test on constructor of used sealed classes + methodCounter("org.jetbrains.Sealed", "").assertFullyCovered() + methodCounter("org.jetbrains.SealedWithInit", "").assertFullyCovered() + methodCounter("org.jetbrains.SealedWithConstructor", "").assertFullyCovered() + + // test on empty objects + classCounter("org.jetbrains.UnusedObject").assertFullyMissed() + classCounter("org.jetbrains.UsedObject").assertFullyCovered() + + // deprecated functions (ERROR and HIDDEN) should not be included in the report. + methodCounter("org.jetbrains.Different", "deprecatedError").assertAbsent() + methodCounter("org.jetbrains.Different", "deprecatedHidden").assertAbsent() + // WARNING deprecated functions should be included in the report. + methodCounter("org.jetbrains.Different", "deprecatedWarn").assertFullyMissed() + + // empty functions must be included in the report with line counter + methodCounter("org.jetbrains.Different", "emptyFun", type = "LINE").assertFullyMissed() + + // Instruction counters + // helloWorld contains 4 instructions. `return` ignored by reporter and the first instruction is not covered because of a bug in the IR compiler + // FIXME after https://youtrack.jetbrains.com/issue/KT-51080 is fixed, the number should become 3 + methodCounter("org.jetbrains.Different", "helloWorld").assertTotal(2) } + } } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Commons.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Commons.kt index 48383530..2d6b2b4a 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Commons.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Commons.kt @@ -60,7 +60,7 @@ internal fun RunResult.checkReports(xmlPath: String, htmlPath: String, mustExist } internal fun RunResult.checkIntellijErrors(errorExpected: Boolean = false) { - if (engine != CoverageEngine.INTELLIJ) return + if (engine != CoverageEngineVendor.INTELLIJ) return file(errorsDirectory()) { if (this.exists() && !errorExpected) { diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Defaults.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Defaults.kt index 947f69a3..0311dda9 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 @@ -4,8 +4,8 @@ import kotlinx.kover.api.* import kotlinx.kover.test.functional.core.ProjectType -internal fun defaultBinaryReport(engine: CoverageEngine, projectType: ProjectType): String { - val extension = if (engine == CoverageEngine.INTELLIJ) "ic" else "exec" +internal fun defaultBinaryReport(engine: CoverageEngineVendor, projectType: ProjectType): String { + val extension = if (engine == CoverageEngineVendor.INTELLIJ) "ic" else "exec" return when (projectType) { ProjectType.KOTLIN_JVM -> "kover/test.$extension" ProjectType.KOTLIN_MULTIPLATFORM -> "kover/jvmTest.$extension" @@ -13,10 +13,10 @@ internal fun defaultBinaryReport(engine: CoverageEngine, projectType: ProjectTyp } } -internal fun defaultMergedXmlReport() = "reports/kover/report.xml" -internal fun defaultMergedHtmlReport() = "reports/kover/html" +internal fun defaultMergedXmlReport() = "reports/kover/merged/xml/report.xml" +internal fun defaultMergedHtmlReport() = "reports/kover/merged/html" -internal fun defaultXmlReport() = "reports/kover/project-xml/report.xml" -internal fun defaultHtmlReport() = "reports/kover/project-html" +internal fun defaultXmlReport() = "reports/kover/xml/report.xml" +internal fun defaultHtmlReport() = "reports/kover/html" internal fun errorsDirectory() = "kover/errors" diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt index f5e2a3ba..de6b430b 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt @@ -1,19 +1,79 @@ package kotlinx.kover.test.functional.core +import kotlinx.kover.api.* import org.junit.* import org.junit.rules.* +/** + * Kotlin by default. + */ +internal val DEFAULT_LANGUAGE = listOf(GradleScriptLanguage.KOTLIN) + +/** + * Used engine from configuration if specified. Otherwise, used [DefaultIntellijEngine] engine. + */ +internal val DEFAULT_ENGINE = listOf() + +/** + * Kotlin JVM project by default. + */ +internal val DEFAULT_TYPE = listOf(ProjectType.KOTLIN_JVM) + +internal val ALL_LANGUAGES = listOf(GradleScriptLanguage.KOTLIN, GradleScriptLanguage.GROOVY) +internal val ALL_ENGINES = listOf(CoverageEngineVendor.INTELLIJ, CoverageEngineVendor.JACOCO) +internal val ALL_TYPES = listOf(ProjectType.KOTLIN_JVM, ProjectType.KOTLIN_MULTIPLATFORM) + internal open class BaseGradleScriptTest { @Rule @JvmField internal val rootFolder: TemporaryFolder = TemporaryFolder() - fun builder(description: String): TestCaseBuilder { - return createBuilder(rootFolder.root, description) + + internal fun diverseBuild( + languages: List = DEFAULT_LANGUAGE, + engines: List = DEFAULT_ENGINE, + types: List = DEFAULT_TYPE, + withCache: Boolean = false + ): DiverseBuild { + return DiverseBuildState(rootFolder.root, languages, engines, types, withCache) + } + + internal fun sampleBuild(templateName: String): GradleRunner { + return createInternalSample(templateName, rootFolder.root) + } +} + + +internal fun DiverseBuild.addKoverRootProject(builder: ProjectBuilder.() -> Unit) { + addProject("root", ":") { + plugins { + kotlin("1.6.20") + kover("DEV") + } + + repositories { + repository("mavenCentral()") + } + + builder() } - fun internalSample(name: String): GradleRunner { - return createInternalSample(name, rootFolder.root) +} + +internal fun DiverseBuild.addKoverSubproject(name: String, builder: ProjectBuilder.() -> Unit): String { + addProject(name, ":$name") { + plugins { + kotlin() + kover() + } + + repositories { + repository("mavenCentral()") + } + + builder() } + + return ":$name" } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt index b638381c..52388956 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt @@ -5,163 +5,247 @@ package kotlinx.kover.test.functional.core import kotlinx.kover.api.* +import kotlinx.kover.test.functional.core.writer.initSlice import org.gradle.api.* import java.io.* -internal fun createBuilder(rootDir: File, description: String): TestCaseBuilder { - return TestCaseBuilderImpl(rootDir, description) -} +internal class DiverseBuildState( + private val rootDir: File, + private val languages: List, + private val engines: List, + private val types: List, + private val withCache: Boolean +) : DiverseBuild { + private val projects: MutableMap = mutableMapOf() -internal class CommonBuilderState(val description: String) { - var pluginVersion: String? = null - val languages: MutableSet = mutableSetOf() - val types: MutableSet = mutableSetOf() - val engines: MutableSet = mutableSetOf() - val koverConfig: KoverRootConfig = KoverRootConfig() - val rootProject: ProjectBuilderState = ProjectBuilderState() - val subprojects: MutableMap = mutableMapOf() - var localCache: Boolean = false -} + override fun addProject(name: String, path: String, builder: ProjectBuilder.() -> Unit) { + projects[path] = ProjectBuilderState(name).also(builder) + } + + override fun prepare(): GradleRunner { + val initSlices: MutableMap = mutableMapOf() + + val targetEngines = engines.ifEmpty { listOf(null) } + + + languages.forEach { language -> + types.forEach { type -> + targetEngines.forEach { engine -> + val slice = ProjectSlice(language, type, engine) + initSlices[slice] = initSlice(rootDir, slice, projects, withCache) + } + } + } -internal class ProjectBuilderState { - val sourceTemplates: MutableList = mutableListOf() - val scripts: MutableList = mutableListOf() - val testScripts: MutableList = mutableListOf() - val dependencies: MutableList = mutableListOf() - val rules: MutableList = mutableListOf() - val mainSources: MutableMap = mutableMapOf() - val testSources: MutableMap = mutableMapOf() + val extraArgs = if (withCache) { + listOf("--build-cache") + } else { + listOf() + } + + return DiverseGradleRunner(initSlices, extraArgs) + } } -internal data class GradleScript(val kotlin: String, val groovy: String) -private class TestCaseBuilderImpl( - val rootDir: File, - description: String, - private val state: CommonBuilderState = CommonBuilderState(description) -) : ProjectBuilderImpl(state.rootProject), TestCaseBuilder { +internal class ProjectBuilderState(val name: String) : ProjectBuilder { + val sourceTemplates: MutableSet = mutableSetOf() + val plugins: PluginsState = PluginsState() + val repositories: RepositoriesState = RepositoriesState() + var kover: TestKoverProjectConfigState? = null + var merged: TestKoverMergedConfigState? = null + val testTasks: TestTaskConfigState = TestTaskConfigState() + val subprojects: MutableList = mutableListOf() - override fun languages(vararg languages: GradleScriptLanguage) = also { - state.languages += languages + override fun plugins(block: Plugins.() -> Unit) { + plugins.also(block) } - override fun engines(vararg engines: CoverageEngine) = also { - state.engines += engines + override fun repositories(block: Repositories.() -> Unit) { + repositories.also(block) } - override fun types(vararg types: ProjectType) = also { - state.types += types + override fun kover(config: TestKoverProjectConfig.() -> Unit) { + kover = TestKoverProjectConfigState().also(config) } - override fun withLocalCache(): TestCaseBuilder = also { - state.localCache = true + override fun koverMerged(config: TestKoverMergedConfig.() -> Unit) { + merged = TestKoverMergedConfigState().also(config) } - override fun configKover(config: KoverRootConfig.() -> Unit) = also { - state.koverConfig.config() + override fun subproject(path: String) { + subprojects += path } - override fun subproject(name: String, builder: ProjectBuilder<*>.() -> Unit) = also { - val projectState = state.subprojects.computeIfAbsent(name) { ProjectBuilderState() } - @Suppress("UPPER_BOUND_VIOLATED_WARNING") - ProjectBuilderImpl>(projectState).builder() + override fun testTasks(block: TestTaskConfig.() -> Unit) { + testTasks.also(block) } - override fun build(): GradleRunner { - if (state.languages.isEmpty()) { - state.languages += GradleScriptLanguage.KOTLIN - } - if (state.types.isEmpty()) { - state.types += ProjectType.KOTLIN_JVM - } - if (state.engines.isEmpty()) { - state.engines += null - } - if (state.pluginVersion == null) { - state.pluginVersion = "0.5.0" - } + override fun sourcesFrom(template: String) { + sourceTemplates += template + } +} - val projects: MutableMap = mutableMapOf() +internal class TestKoverProjectConfigState : TestKoverProjectConfig { + override var isDisabled: Boolean? = null + override var engine: CoverageEngineVariant? = null + val filters: TestKoverProjectFiltersState = TestKoverProjectFiltersState() + val instrumentation: KoverProjectInstrumentation = KoverProjectInstrumentation() + val xml: TestKoverProjectXmlConfigState = TestKoverProjectXmlConfigState() + val html: TestKoverProjectHtmlConfigState = TestKoverProjectHtmlConfigState() + val verify: TestKoverVerifyConfigState = TestKoverVerifyConfigState() + override fun filters(config: TestKoverProjectFilters.() -> Unit) { + filters.also(config) + } - state.languages.forEach { language -> - state.types.forEach { type -> - state.engines.forEach { engine -> - val slice = ProjectSlice(language, type, engine ?: CoverageEngine.INTELLIJ) - projects[slice] = state.createProject(rootDir, slice) - } - } - } + override fun instrumentation(config: KoverProjectInstrumentation.() -> Unit) { + instrumentation.also(config) + } - return GradleRunnerImpl(projects) + override fun xmlReport(config: TestKoverProjectXmlConfig.() -> Unit) { + xml.also(config) } + override fun htmlReport(config: TestKoverProjectHtmlConfig.() -> Unit) { + html.also(config) + } + + override fun verify(config: TestKoverVerifyConfig.() -> Unit) { + verify.also(config) + } } +internal class TestKoverProjectFiltersState : TestKoverProjectFilters { + var classes: KoverClassFilters? = null + var sourcesets: KoverSourceSetFilters? = null -@Suppress("UNCHECKED_CAST") -private open class ProjectBuilderImpl>(val projectState: ProjectBuilderState) : ProjectBuilder { + override fun classes(config: KoverClassFilters.() -> Unit) { + classes = KoverClassFilters().also(config) + } - override fun rule(name: String?, builder: RuleBuilder.() -> Unit): B { - projectState.rules += TestVerificationRule(projectState.rules.size, name).apply(builder) - return this as B + override fun sourcesets(config: KoverSourceSetFilters.() -> Unit) { + sourcesets = KoverSourceSetFilters().also(config) } +} + +internal class TestKoverProjectXmlConfigState : TestKoverProjectXmlConfig { + override var onCheck: Boolean? = null + override var reportFile: File? = null + var overrideFilters: TestKoverProjectFiltersState? = null - override fun configTest(script: String): B { - projectState.testScripts += GradleScript(script, script) - return this as B + override fun overrideFilters(config: TestKoverProjectFilters.() -> Unit) { + if (overrideFilters == null) { + overrideFilters = TestKoverProjectFiltersState() + } + overrideFilters!!.also(config) } +} - override fun configTest(kotlin: String, groovy: String): B { - projectState.testScripts += GradleScript(kotlin, groovy) - return this as B +internal class TestKoverProjectHtmlConfigState : TestKoverProjectHtmlConfig { + override var onCheck: Boolean? = null + override var reportDir: File? = null + var overrideFilters: TestKoverProjectFiltersState? = null + + override fun overrideFilters(config: TestKoverProjectFilters.() -> Unit) { + if (overrideFilters == null) { + overrideFilters = TestKoverProjectFiltersState() + } + overrideFilters!!.also(config) } +} - override fun config(script: String): B { - projectState.scripts += GradleScript(script, script) - return this as B +internal class TestKoverVerifyConfigState : TestKoverVerifyConfig { + override val onCheck: Boolean? = null + val rules: MutableList = mutableListOf() + + override fun rule(config: TestVerificationRule.() -> Unit) { + rules += TestVerificationRule().also(config) } +} + +internal class TestVerificationRule { + var isEnabled: Boolean? = null + var name: String? = null + var target: VerificationTarget? = null + + var overrideClassFilters: KoverClassFilters? = null + val bounds: MutableList = mutableListOf() - override fun config(kotlin: String, groovy: String): B { - projectState.scripts += GradleScript(kotlin, groovy) - return this as B + fun overrideClassFilters(config: Action) { + overrideClassFilters = KoverClassFilters().also { config.execute(it) } } - override fun dependency(script: String): B { - projectState.dependencies += GradleScript(script, script) - return this as B + fun bound(configureBound: VerificationBoundState.() -> Unit) { + bounds += VerificationBoundState().also(configureBound) } +} + +internal class VerificationBoundState { + var minValue: Int? = null + var maxValue: Int? = null + var counter: CounterType? = null + var valueType: VerificationValueType? = null +} - override fun dependency(kotlin: String, groovy: String): B { - projectState.dependencies += GradleScript(kotlin, groovy) - return this as B +internal class TestTaskConfigState : TestTaskConfig { + var excludes: List? = null + var includes: List? = null + override fun excludes(vararg classes: String) { + excludes = classes.toList() } - override fun sources(template: String): B { - projectState.sourceTemplates += template - return this as B + override fun includes(vararg classes: String) { + includes = classes.toList() } + } -private data class TestVerificationRule( - override val id: Int, - override var name: String? -) : VerificationRule, RuleBuilder { - override val bounds: MutableList = mutableListOf() - override fun bound(configureBound: Action) { - bounds += TestVerificationBound(bounds.size).also { configureBound.execute(it) } +internal class RepositoriesState : Repositories { + val repositories: MutableList = mutableListOf() + override fun repository(name: String) { + repositories += name } - override fun bound(builder: VerificationBound.() -> Unit) { - bounds += TestVerificationBound(bounds.size).apply(builder) +} + +internal class TestKoverMergedConfigState : TestKoverMergedConfig { + var enabled: Boolean = false + val filters: TestKoverMergedFiltersState = TestKoverMergedFiltersState() + override fun enable() { + enabled = true + } + + override fun filters(config: TestKoverMergedFilters.() -> Unit) { + filters.also(config) } } -private data class TestVerificationBound( - override val id: Int, - override var minValue: Int? = null, - override var maxValue: Int? = null, - override var valueType: VerificationValueType = VerificationValueType.COVERED_LINES_PERCENTAGE -) : VerificationBound +internal class TestKoverMergedFiltersState : TestKoverMergedFilters { + var classes: KoverClassFilters? = null + var projects: KoverProjectsFilters? = null + override fun classes(config: KoverClassFilters.() -> Unit) { + classes = KoverClassFilters().also(config) + } + override fun projects(config: KoverProjectsFilters.() -> Unit) { + projects = KoverProjectsFilters().also(config) + } +} + +internal class PluginsState : Plugins { + var useKotlin: Boolean = false + var useKover: Boolean = false + var kotlinVersion: String? = null + var koverVersion: String? = null + + override fun kotlin(version: String?) { + useKotlin = true + kotlinVersion = version + } + override fun kover(version: String?) { + useKover = true + koverVersion = version + } +} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt index 9966d57a..f90583f9 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt @@ -12,11 +12,11 @@ import java.io.* import javax.xml.parsers.* -internal class GradleRunnerImpl(private val projects: Map) : +internal class DiverseGradleRunner(private val projects: Map, private val extraArgs: List) : GradleRunner { - override fun run(vararg args: String, checker: RunResult.() -> Unit): GradleRunnerImpl { - val argsList = listOf(*args) + override fun run(vararg args: String, checker: RunResult.() -> Unit): DiverseGradleRunner { + val argsList = listOf(*args) + extraArgs projects.forEach { (slice, project) -> try { val buildResult = project.runGradle(argsList) @@ -27,8 +27,8 @@ internal class GradleRunnerImpl(private val projects: Map) : } return this } - override fun runWithError(vararg args: String, errorChecker: RunResult.() -> Unit): GradleRunnerImpl { - val argsList = listOf(*args) + override fun runWithError(vararg args: String, errorChecker: RunResult.() -> Unit): DiverseGradleRunner { + val argsList = listOf(*args) + extraArgs projects.forEach { (slice, project) -> try { project.runGradleWithError(argsList) @@ -98,11 +98,11 @@ private class RunResultImpl( private val buildScriptFile: File = buildFile() private val buildScript: String by lazy { buildScriptFile.readText() } - override val engine: CoverageEngine by lazy { + override val engine: CoverageEngineVendor by lazy { if (buildScript.contains("set(kotlinx.kover.api.CoverageEngine.JACOCO)")) { - CoverageEngine.JACOCO + CoverageEngineVendor.JACOCO } else { - CoverageEngine.INTELLIJ + CoverageEngineVendor.INTELLIJ } } @@ -116,9 +116,9 @@ private class RunResultImpl( throw IllegalArgumentException("Impossible to determine the type of project") } } else { - if (buildScript.contains("""id 'org.jetbrains.kotlin.jvm'""")) { + if (buildScript.contains("""id "org.jetbrains.kotlin.jvm"""")) { ProjectType.KOTLIN_JVM - } else if (buildScript.contains("""id 'org.jetbrains.kotlin.multiplatform'""")) { + } else if (buildScript.contains("""id "org.jetbrains.kotlin.multiplatform"""")) { ProjectType.KOTLIN_MULTIPLATFORM } else { throw IllegalArgumentException("Impossible to determine the type of project") diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt index 4ee1689a..b41be670 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt @@ -12,55 +12,109 @@ internal enum class GradleScriptLanguage { KOTLIN, GROOVY } internal enum class ProjectType { KOTLIN_JVM, KOTLIN_MULTIPLATFORM, ANDROID } -internal interface ProjectBuilder> { - fun sources(template: String): B +internal interface DiverseBuild { + fun addProject(name: String, path: String, builder: ProjectBuilder.() -> Unit) + fun prepare(): GradleRunner +} + +internal interface ProjectBuilder { + fun plugins(block: Plugins.() -> Unit) + + fun repositories(block: Repositories.() -> Unit) - fun rule(name: String? = null, builder: RuleBuilder.() -> Unit): B + fun kover(config: TestKoverProjectConfig.() -> Unit) - fun configTest(script: String): B - fun configTest(kotlin: String, groovy: String): B + fun koverMerged(config: TestKoverMergedConfig.() -> Unit) - fun config(script: String): B - fun config(kotlin: String, groovy: String): B + fun testTasks(block: TestTaskConfig.() -> Unit) - fun dependency(script: String): B - fun dependency(kotlin: String, groovy: String): B + fun subproject(path: String) + + fun sourcesFrom(template: String) } -internal interface RuleBuilder { - fun bound(builder: VerificationBound.() -> Unit) + +interface Plugins { + fun kotlin(version: String? = null) + fun kover(version: String? = null) } -internal interface TestCaseBuilder : ProjectBuilder { - fun languages(vararg languages: GradleScriptLanguage): TestCaseBuilder - fun engines(vararg engines: CoverageEngine): TestCaseBuilder - fun types(vararg types: ProjectType): TestCaseBuilder +interface Repositories { + fun repository(name: String) +} + +interface TestTaskConfig { + fun excludes(vararg classes: String) + fun includes(vararg classes: String) +} - fun withLocalCache(): TestCaseBuilder +/** + * Same as [KoverProjectConfig] + */ +internal interface TestKoverProjectConfig { + var isDisabled: Boolean? - fun configKover(config: KoverRootConfig.() -> Unit): TestCaseBuilder + var engine: CoverageEngineVariant? - fun subproject(name: String, builder: ProjectBuilder<*>.() -> Unit): TestCaseBuilder + fun filters(config: TestKoverProjectFilters.() -> Unit) - fun build(): GradleRunner + fun instrumentation(config: KoverProjectInstrumentation.() -> Unit) + + fun xmlReport(config: TestKoverProjectXmlConfig.() -> Unit) + + fun htmlReport(config: TestKoverProjectHtmlConfig.() -> Unit) + + fun verify(config: TestKoverVerifyConfig.() -> Unit) } -internal data class ProjectSlice(val language: GradleScriptLanguage, val type: ProjectType, val engine: CoverageEngine?) { - fun encodedString(): String { - return "${language.ordinal}_${type.ordinal}_${engine?.ordinal?:"default"}" - } + +internal interface TestKoverVerifyConfig { + val onCheck: Boolean? + + fun rule(config: TestVerificationRule.() -> Unit) +} + +internal interface TestKoverProjectXmlConfig { + var onCheck: Boolean? + var reportFile: File? + + fun overrideFilters(config: TestKoverProjectFilters.() -> Unit) +} + +internal interface TestKoverProjectHtmlConfig { + var onCheck: Boolean? + var reportDir: File? + + fun overrideFilters(config: TestKoverProjectFilters.() -> Unit) } -internal data class KoverRootConfig( - var disabled: Boolean? = null, - var intellijVersion: String? = null, - var jacocoVersion: String? = null, - var generateReportOnCheck: Boolean? = null, - var runAllTestsForProjectTask: Boolean? = null, - val disabledProjects: MutableSet = mutableSetOf() +internal interface TestKoverProjectFilters { + fun classes(config: KoverClassFilters.() -> Unit) + + fun sourcesets(config: KoverSourceSetFilters.() -> Unit) +} + +internal interface TestKoverMergedConfig { + public fun enable() + + public fun filters(config: TestKoverMergedFilters.() -> Unit) +} + +public interface TestKoverMergedFilters { + public fun classes(config: KoverClassFilters.() -> Unit) + + public fun projects(config: KoverProjectsFilters.() -> Unit) +} + + +internal data class ProjectSlice( + val language: GradleScriptLanguage, + val type: ProjectType, + val engine: CoverageEngineVendor? ) { - val isDefault = - disabled == null && intellijVersion == null && jacocoVersion == null && generateReportOnCheck == null + fun encodedString(): String { + return "${language.ordinal}_${type.ordinal}_${engine?.ordinal ?: "default"}" + } } internal interface GradleRunner { @@ -69,7 +123,7 @@ internal interface GradleRunner { } internal interface RunResult { - val engine: CoverageEngine + val engine: CoverageEngineVendor val projectType: ProjectType fun subproject(name: String, checker: RunResult.() -> Unit) diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt deleted file mode 100644 index 4e068906..00000000 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.test.functional.core - -import kotlinx.kover.api.* -import kotlinx.kover.test.functional.core.GradleScriptLanguage.KOTLIN -import java.io.* - -private const val TEMPLATES_PATH = "src/functionalTest/templates" -private const val BUILD_SCRIPTS_PATH = "$TEMPLATES_PATH/scripts/buildscripts" -private const val SETTINGS_PATH = "$TEMPLATES_PATH/scripts/settings" -private const val SOURCES_PATH = "$TEMPLATES_PATH/sources" - -internal fun CommonBuilderState.createProject(rootDir: File, slice: ProjectSlice): File { - val projectDir = File(rootDir, slice.encodedString()).also { it.mkdirs() } - - val extension = slice.scriptExtension - - val buildScript = loadScriptTemplate(true, slice) - .processRootBuildScript(this, slice) - .processProjectBuildScript(rootProject, slice, subprojects.keys) - - File(projectDir, "build.$extension").writeText(buildScript) - File(projectDir, "settings.$extension").writeText(buildSettings(slice)) - - rootProject.writeSources(projectDir, slice) - - subprojects.forEach { (name, state) -> state.writeSubproject(File(projectDir, name), slice) } - - return projectDir -} - -private fun ProjectBuilderState.writeSubproject(directory: File, slice: ProjectSlice) { - directory.mkdirs() - - val extension = slice.scriptExtension - - val buildScript = loadScriptTemplate(false, slice).processProjectBuildScript(this, slice) - - File(directory, "build.$extension").writeText(buildScript) - - writeSources(directory, slice) -} - - -private val ProjectSlice.scriptExtension get() = if (language == KOTLIN) "gradle.kts" else "gradle" - -private val ProjectSlice.srcPath: String - get() { - return when (type) { - ProjectType.KOTLIN_JVM -> "src/main" - ProjectType.KOTLIN_MULTIPLATFORM -> "src/jvmMain" - ProjectType.ANDROID -> "src/jvmMain" - } - } - -private val ProjectSlice.testPath: String - get() { - return when (type) { - ProjectType.KOTLIN_JVM -> "src/test" - ProjectType.KOTLIN_MULTIPLATFORM -> "src/jvmTest" - ProjectType.ANDROID -> "src/jvmTest" - } - } - - -private fun String.processRootBuildScript(state: CommonBuilderState, slice: ProjectSlice): String { - return replace("//PLUGIN_VERSION", state.pluginVersion!!) - .replace("//KOVER", state.buildRootExtension(slice)) -} - -private fun String.processProjectBuildScript( - state: ProjectBuilderState, - slice: ProjectSlice, - subprojects: Set = emptySet() -): String { - return replace("//REPOSITORIES", "") - .replace("//DEPENDENCIES", state.buildDependencies(slice, subprojects)) - .replace("//SCRIPTS", state.buildScripts(slice)) - .replace("//TEST_TASK", state.buildTestTask(slice)) - .replace("//VERIFICATIONS", state.buildVerifications(slice)) -} - - -private fun ProjectBuilderState.writeSources(projectDir: File, slice: ProjectSlice) { - fun File.processDir(result: MutableMap, targetRootPath: String, relativePath: String = "") { - listFiles()?.forEach { file -> - val filePath = "$relativePath/${file.name}" - if (file.isDirectory) { - file.processDir(result, targetRootPath, filePath) - } else if (file.exists() && file.length() > 0) { - val targetFile = File(projectDir, "$targetRootPath/$filePath") - targetFile.parentFile.mkdirs() - file.copyTo(targetFile) - } - } - } - - val srcPath = slice.srcPath - val testPath = slice.testPath - - sourceTemplates.forEach { template -> - File(SOURCES_PATH, "$template/main").processDir(mainSources, srcPath) - File(SOURCES_PATH, "$template/test").processDir(testSources, testPath) - } -} - -private fun ProjectSlice.scriptPath(): String { - val languageString = if (language == KOTLIN) "kotlin" else "groovy" - val typeString = when (type) { - ProjectType.KOTLIN_JVM -> "kjvm" - ProjectType.KOTLIN_MULTIPLATFORM -> "kmp" - ProjectType.ANDROID -> "android" - } - return "$BUILD_SCRIPTS_PATH/$languageString/$typeString" -} - -private fun buildSubprojectsIncludes(subprojects: Set): String { - if (subprojects.isEmpty()) return "" - - return subprojects.joinToString("\n", "\n", "\n") { - """include("$it")""" - } -} - -private fun CommonBuilderState.buildExtraSettings(): String { - return if (localCache) { - """ -buildCache { - local { - directory = "${"$"}settingsDir/build-cache" - } -} -""" - } else { - "" - } -} - -private fun CommonBuilderState.buildRootExtension(slice: ProjectSlice): String { - if (slice.engine == null && koverConfig.isDefault) { - return "" - } - - val builder = StringBuilder() - builder.appendLine() - builder.appendLine("kover {") - - if (koverConfig.disabled != null) { - val property = if (slice.language == KOTLIN) "isDisabled" else "disabled" - builder.appendLine("$property = ${koverConfig.disabled}") - } - - if (slice.engine == CoverageEngine.INTELLIJ) { - builder.appendLine(" coverageEngine.set(kotlinx.kover.api.CoverageEngine.INTELLIJ)") - if (koverConfig.intellijVersion != null) { - builder.appendLine(""" intellijEngineVersion.set("${koverConfig.intellijVersion}")""") - } - } - if (slice.engine == CoverageEngine.JACOCO) { - builder.appendLine(" coverageEngine.set(kotlinx.kover.api.CoverageEngine.JACOCO)") - if (koverConfig.jacocoVersion != null) { - builder.appendLine(""" jacocoEngineVersion.set("${koverConfig.jacocoVersion}")""") - } - } - - if (koverConfig.disabledProjects.isNotEmpty()) { - val prefix = if (slice.language == KOTLIN) "setOf(" else "[" - val postfix = if (slice.language == KOTLIN) ")" else "]" - val value = koverConfig.disabledProjects.joinToString(prefix = prefix, postfix = postfix) { "\"$it\"" } - builder.appendLine(" disabledProjects = $value") - } - - if (koverConfig.runAllTestsForProjectTask != null) { - builder.appendLine(" runAllTestsForProjectTask = ${koverConfig.runAllTestsForProjectTask}") - } - - builder.appendLine("}") - - return builder.toString() -} - -private fun ProjectBuilderState.buildTestTask(slice: ProjectSlice): String { - if (testScripts.isEmpty()) { - return "" - } - - val configs = testScripts.map { if (slice.language == KOTLIN) it.kotlin else it.groovy } - - return loadTestTaskTemplate(slice).replace("//KOVER_TEST_CONFIG", configs.joinToString("\n")) -} - -@Suppress("UNUSED_PARAMETER") -private fun ProjectBuilderState.buildVerifications(slice: ProjectSlice): String { - if (rules.isEmpty()) { - return "" - } - - val builder = StringBuilder() - builder.appendLine() - builder.appendLine("tasks.koverVerify {") - - for (rule in rules) { - builder.appendLine(" rule {") - rule.name?.also { builder.appendLine(""" name = "$it"""") } - for (bound in rule.bounds) { - builder.appendLine(" bound {") - bound.minValue?.let { builder.appendLine(" minValue = $it") } - bound.maxValue?.let { builder.appendLine(" maxValue = $it") } - if (bound.valueType != VerificationValueType.COVERED_LINES_PERCENTAGE) { - builder.appendLine(" valueType = kotlinx.kover.api.VerificationValueType.${bound.valueType}") - } - builder.appendLine(" }") - } - builder.appendLine(" }") - } - builder.appendLine("}") - - return builder.toString() -} - -private fun CommonBuilderState.buildSettings(slice: ProjectSlice): String { - return loadSettingsTemplate(slice) - .replace("//SUBPROJECTS", buildSubprojectsIncludes(subprojects.keys)) - .replace("//EXTRA_SETTINGS", buildExtraSettings()) -} - -private fun ProjectBuilderState.buildScripts(slice: ProjectSlice): String { - if (scripts.isEmpty()) { - return "" - } - val configs = scripts.map { if (slice.language == KOTLIN) it.kotlin else it.groovy } - return configs.joinToString("\n", "\n", "\n") -} - -private fun ProjectBuilderState.buildDependencies(slice: ProjectSlice, subprojects: Set): String { - if (dependencies.isEmpty() && subprojects.isEmpty()) { - return "" - } - - val configs: MutableList = mutableListOf(); - - configs += subprojects.map { - if (slice.language == KOTLIN) "implementation(project(\":$it\"))" else "implementation project(':$it')" - } - configs += dependencies.map { if (slice.language == KOTLIN) it.kotlin else it.groovy } - return configs.joinToString("\n", "\n", "\n") -} - - -private fun loadSettingsTemplate(slice: ProjectSlice): String { - return File("$SETTINGS_PATH/settings.${slice.scriptExtension}").readText() -} - -private fun loadScriptTemplate(root: Boolean, slice: ProjectSlice): String { - val filename = if (root) "root" else "subproject" - return File("${slice.scriptPath()}/$filename").readText() -} - -private fun loadTestTaskTemplate(slice: ProjectSlice): String { - return File("${slice.scriptPath()}/testTask").readText() -} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/CommonWriter.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/CommonWriter.kt new file mode 100644 index 00000000..c63f4738 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/CommonWriter.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.api.* +import kotlinx.kover.test.functional.core.* +import java.io.PrintWriter + +internal fun PrintWriter.printClassFilters(classFilters: KoverClassFilters, slice: ProjectSlice, indents: Int) { + if (classFilters.excludes.isNotEmpty()) { + indented(indents, "excludes".addAllList(classFilters.excludes, slice.language)) + } + if (classFilters.includes.isNotEmpty()) { + indented(indents, "includes".addAllList(classFilters.includes, slice.language)) + } +} + +internal fun PrintWriter.printVerify(state: TestKoverVerifyConfigState, slice: ProjectSlice, indents: Int) { + val onCheck = state.onCheck + val rules = state.rules + if (onCheck == null && rules.isEmpty()) return + + indented(indents, "verify {") + if (onCheck != null) { + indented(indents + 1, "onCheck".setProperty(onCheck.toString(), slice.language)) + } + rules.forEach { rule -> + indented(indents + 1, "rule {") + if (rule.isEnabled != null) { + indented(indents + 2, "isEnabled = ${rule.isEnabled}") + } + if (rule.name != null) { + indented(indents + 2, "name = ${rule.name?.text(slice.language)}") + } + if (rule.target != null) { + indented(indents + 2, "target = ${rule.target?.enum(slice.language)}") + } + if (rule.overrideClassFilters != null) { + indented(indents + 2, "overrideClassFilters {") + printClassFilters(rule.overrideClassFilters!!, slice, indents + 3) + indented(indents + 2, "}") + } + rule.bounds.forEach { bound -> + indented(indents + 2, "bound {") + if (bound.minValue != null) { + indented(indents + 3, "minValue = ${bound.minValue}") + } + if (bound.maxValue != null) { + indented(indents + 3, "maxValue = ${bound.maxValue}") + } + if (bound.counter != null) { + indented(indents + 3, "counter = ${bound.counter?.enum(slice.language)}") + } + if (bound.valueType != null) { + indented(indents + 3, "valueType = ${bound.valueType?.enum(slice.language)}") + } + indented(indents + 2, "}") + } + indented(indents + 1, "}") + } + indented(indents, "}") +} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/KoverExtensionWriter.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/KoverExtensionWriter.kt new file mode 100644 index 00000000..48ee9a30 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/KoverExtensionWriter.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.api.* +import kotlinx.kover.test.functional.core.* +import kotlinx.kover.test.functional.core.ProjectSlice +import kotlinx.kover.test.functional.core.TestKoverProjectConfigState +import java.io.* + +internal fun PrintWriter.printKover(kover: TestKoverProjectConfigState?, slice: ProjectSlice, indents: Int) { + if (kover == null) return + + indented(indents, "kover {") + printDisabled(kover.isDisabled, slice, indents + 1) + printEngine(kover.engine, slice, indents + 1) + printFilters(kover.filters, slice, indents + 1) + printVerify(kover.verify, slice, indents + 1) + indented(indents, "}") +} + + +private fun PrintWriter.printEngine(scriptEngine: CoverageEngineVariant?, slice: ProjectSlice, indents: Int) { + if (scriptEngine == null && slice.engine == null) return + + val value = if (slice.engine != null) { + val clazz = + if (slice.engine == CoverageEngineVendor.INTELLIJ) DefaultIntellijEngine::class else DefaultJacocoEngine::class + clazz.obj(slice.language) + } else { + val clazz = + if (scriptEngine!!.vendor == CoverageEngineVendor.INTELLIJ) IntellijEngine::class else JacocoEngine::class + val new = if (slice.language == GradleScriptLanguage.KOTLIN) { + clazz.qualifiedName + } else { + clazz.qualifiedName + } + "$new(\"${scriptEngine.version}\")" + } + + indented(indents, "engine".setProperty(value, slice.language)) +} + +private fun PrintWriter.printDisabled(isDisabled: Boolean?, slice: ProjectSlice, indents: Int) { + if (isDisabled == null) return + + if (slice.language == GradleScriptLanguage.KOTLIN) { + indented(indents, "isDisabled.set($isDisabled)") + } else { + indented(indents, "disabled = $isDisabled") + } +} + +private fun PrintWriter.printFilters(state: TestKoverProjectFiltersState, slice: ProjectSlice, indents: Int) { + val classes = state.classes + val sourcesets = state.sourcesets + if (sourcesets == null && classes == null) return + + indented(indents, "filters {") + if (classes != null && (classes.excludes.isNotEmpty() || classes.includes.isNotEmpty())) { + indented(indents + 1, "classes {") + printClassFilters(classes, slice, indents + 2) + indented(indents + 1, "}") + } + + if (sourcesets != null) { + indented(indents + 1, "sourcesets {") + if (sourcesets.excludes.isNotEmpty()) { + indented(indents + 2, "excludes".addAllList(sourcesets.excludes, slice.language)) + } + indented(indents + 2, "excludeTests = " + sourcesets.excludeTests) + indented(indents + 1, "}") + } + + indented(indents, "}") +} + diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Languages.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Languages.kt new file mode 100644 index 00000000..46efb89c --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Languages.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.test.functional.core.GradleScriptLanguage +import kotlinx.kover.test.functional.core.ProjectSlice +import java.io.PrintWriter +import kotlin.reflect.* + +internal val ProjectSlice.scriptExtension get() = if (language == GradleScriptLanguage.KOTLIN) "gradle.kts" else "gradle" + +internal fun Iterable.formatList(language: GradleScriptLanguage): String { + val prefix = if (language == GradleScriptLanguage.KOTLIN) "listOf(" else "[" + val postfix = if (language == GradleScriptLanguage.KOTLIN) ")" else "]" + + return prefix + this.joinToString(separator = ",") { "\"$it\"" } + postfix +} + +internal fun String.addAllList(list: Iterable, language: GradleScriptLanguage): String { + val listString = list.formatList(language) + return this + if (language == GradleScriptLanguage.KOTLIN) " += $listString" else ".addAll($listString)" +} + +internal fun KClass<*>.obj(language: GradleScriptLanguage): String { + return if (language == GradleScriptLanguage.KOTLIN) { + qualifiedName!! + } else { + "$qualifiedName.INSTANCE" + } +} + +internal fun String.setProperty(value: String, language: GradleScriptLanguage): String { + return this + if (language == GradleScriptLanguage.KOTLIN) ".set($value)" else " = $value" +} + +private fun indent(count: Int): String { + return when (count) { + 0 -> "" + 1 -> " " + 2 -> " " + 3 -> " " + 4 -> " " + 5 -> " " + else -> " ".repeat(count) + } +} + +@Suppress("UNUSED_PARAMETER") +internal fun String.text(language: GradleScriptLanguage): String { + return "\"$this\"" +} + +internal fun Enum<*>.enum(language: GradleScriptLanguage): String { + return if (language == GradleScriptLanguage.KOTLIN) { + this::class.qualifiedName + '.' + this.name + } else { + "\"$name\"" + } +} + +internal fun PrintWriter.indented(indents: Int, content: String) { + println(indent(indents) + content) +} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/MergedExtensionWriter.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/MergedExtensionWriter.kt new file mode 100644 index 00000000..e5768c83 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/MergedExtensionWriter.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.test.functional.core.* +import kotlinx.kover.test.functional.core.ProjectSlice +import java.io.* + +internal fun PrintWriter.printKoverMerged(merged: TestKoverMergedConfigState?, slice: ProjectSlice, indents: Int) { + if (merged == null) return + + indented(indents, "koverMerged {") + printEnabled(merged.enabled, indents + 1) + printFilters(merged.filters, slice, indents + 1) + indented(indents, "}") +} + +private fun PrintWriter.printEnabled(isEnabled: Boolean, indents: Int) { + if (isEnabled) { + indented(indents, "enable()") + } +} + +private fun PrintWriter.printFilters(state: TestKoverMergedFiltersState, slice: ProjectSlice, indents: Int) { + val classes = state.classes + val projects = state.projects + if (projects == null && classes == null) return + + indented(indents, "filters {") + if (classes != null && (classes.excludes.isNotEmpty() || classes.includes.isNotEmpty())) { + indented(indents + 1, "classes {") + printClassFilters(classes, slice, indents + 2) + indented(indents + 1, "}") + } + + if (projects != null) { + indented(indents + 1, "projects {") + if (projects.includes.isNotEmpty()) { + indented(indents + 2, "includes".addAllList(projects.includes, slice.language)) + } + indented(indents + 1, "}") + } + + indented(indents, "}") +} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/SettingsFileWriter.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/SettingsFileWriter.kt new file mode 100644 index 00000000..5af72278 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/SettingsFileWriter.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.test.functional.core.* +import java.io.* + +internal fun generateSettingsFile( + sliceDir: File, + slice: ProjectSlice, + projects: Map, + localCache: Boolean +) { + File(sliceDir, "settings.${slice.scriptExtension}").printWriter().use { + it.println("""rootProject.name = "kover-functional-test"""") + it.println() + projects.keys.forEach { path -> + if (path != ":") { + it.println("""include("$path")""") + } + } + + if (localCache) { + it.println(LOCAL_CACHE) + } + } +} + +private const val LOCAL_CACHE = """ +buildCache { + local { + directory = "${"$"}settingsDir/build-cache" + } +} +""" diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Writer.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Writer.kt new file mode 100644 index 00000000..3d83cb30 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/writer/Writer.kt @@ -0,0 +1,290 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.test.functional.core.writer + +import kotlinx.kover.test.functional.core.GradleScriptLanguage.GROOVY +import kotlinx.kover.test.functional.core.GradleScriptLanguage.KOTLIN +import kotlinx.kover.test.functional.core.PluginsState +import kotlinx.kover.test.functional.core.ProjectBuilderState +import kotlinx.kover.test.functional.core.ProjectSlice +import kotlinx.kover.test.functional.core.ProjectType +import kotlinx.kover.test.functional.core.RepositoriesState +import kotlinx.kover.test.functional.core.TestTaskConfigState +import java.io.* + +private const val SAMPLES_PATH = "src/functionalTest/templates" +private const val SAMPLES_SOURCES_PATH = "$SAMPLES_PATH/sources" + +internal fun initSlice( + rootDir: File, + slice: ProjectSlice, + projects: Map, + localCache: Boolean +): File { + val sliceDir = File(rootDir, slice.encodedString()).also { it.mkdirs() } + generateSettingsFile(sliceDir, slice, projects, localCache) + projects.forEach { (path, state) -> state.generateProject(sliceDir, path, slice) } + return sliceDir +} + +private fun ProjectBuilderState.generateProject(sliceDir: File, path: String, slice: ProjectSlice) { + val subpath = path.replace(':', '/') + val projectDir = File(sliceDir, subpath).also { it.mkdirs() } + copySources(projectDir, slice) + File(projectDir, "build.${slice.scriptExtension}").printWriter().use { + it.printPlugins(plugins, slice) + it.printRepositories(repositories) + it.printDependencies(subprojects, slice) + it.printKover(kover, slice, 0) + it.printKoverMerged(merged, slice, 0) + it.printTestTasks(testTasks, slice) + } +} + + +private fun PrintWriter.printPlugins(plugins: PluginsState, slice: ProjectSlice) { + if (!plugins.useKotlin && !plugins.useKover) return + + println("plugins {") + if (plugins.useKotlin) { + when { + slice.language == GROOVY && slice.type == ProjectType.KOTLIN_JVM -> print(""" id "org.jetbrains.kotlin.jvm"""") + slice.language == GROOVY && slice.type == ProjectType.KOTLIN_MULTIPLATFORM -> print(""" id "org.jetbrains.kotlin.multiplatform"""") + slice.language == KOTLIN && slice.type == ProjectType.KOTLIN_JVM -> print(""" kotlin("jvm")""") + slice.language == KOTLIN && slice.type == ProjectType.KOTLIN_MULTIPLATFORM -> print(""" kotlin("multiplatform")""") + else -> throw Exception("Unsupported test combination: language ${slice.language} and project type ${slice.type}") + } + plugins.kotlinVersion?.let { print(""" version "$it"""") } + println() + } + + if (plugins.useKover) { + if (slice.language == KOTLIN) { + print(""" id("org.jetbrains.kotlinx.kover")""") + } else { + print(""" id "org.jetbrains.kotlinx.kover"""") + } + plugins.koverVersion?.let { print(""" version "$it"""") } + println() + } + println("}") +} + +private fun PrintWriter.printRepositories(repositories: RepositoriesState) { + if (repositories.repositories.isEmpty()) return + + println("repositories {") + repositories.repositories.forEach { + print(" ") + println(it) + } + println("}") +} + +private fun PrintWriter.printDependencies(subprojects: List, slice: ProjectSlice) { + val subprojectsPart = subprojects.joinToString(separator = "\n") { + if (slice.language == KOTLIN) "implementation(project(\"$it\"))" else "implementation project('$it')" + } + + val template = when { + slice.language == GROOVY && slice.type == ProjectType.KOTLIN_JVM -> GROOVY_JVM_DEPS + slice.language == GROOVY && slice.type == ProjectType.KOTLIN_MULTIPLATFORM -> GROOVY_KMP_DEPS + slice.language == KOTLIN && slice.type == ProjectType.KOTLIN_JVM -> KOTLIN_JVM_DEPS + slice.language == KOTLIN && slice.type == ProjectType.KOTLIN_MULTIPLATFORM -> KOTLIN_KMP_DEPS + else -> throw Exception("Unsupported test combination: language ${slice.language} and project type ${slice.type}") + } + println(template.replace(DEPS_PLACEHOLDER, subprojectsPart)) +} + +private fun PrintWriter.printTestTasks(state: TestTaskConfigState, slice: ProjectSlice) { + if (state.excludes == null && state.includes == null) return + + val testTaskName = when { + slice.type == ProjectType.KOTLIN_JVM -> "test" + slice.type == ProjectType.KOTLIN_MULTIPLATFORM && slice.language == KOTLIN -> "named(\"jvmTest\").configure" + slice.type == ProjectType.KOTLIN_MULTIPLATFORM -> "jvmTest" + else -> throw Exception("Project with type ${slice.type} and language ${slice.language} not supported for test task configuring") + } + val extension = + if (slice.language == KOTLIN) "extensions.configure(kotlinx.kover.api.KoverTaskExtension::class)" else "kover" + + println("tasks.$testTaskName {") + println(" $extension {") + if (state.excludes != null) { + val excludesString = state.excludes!!.joinToString(separator = ",") { "\"$it\"" } + println(" excludes.addAll($excludesString)") + } + if (state.includes != null) { + val includesString = state.includes!!.joinToString(separator = ",") { "\"$it\"" } + println(" includes.addAll($includesString)") + } + println(" }") + println("}") +} + +private const val DEPS_PLACEHOLDER = "/*DEPS*/" + +private const val KOTLIN_JVM_DEPS = """ +dependencies { + $DEPS_PLACEHOLDER + testImplementation(kotlin("test")) +} +""" + +private const val KOTLIN_KMP_DEPS = """ +kotlin { + jvm() { + withJava() + } + dependencies { + commonTestImplementation(kotlin("test")) + } + sourceSets { + val jvmMain by getting { + dependencies { + $DEPS_PLACEHOLDER + } + } + } +} +""" + +private const val GROOVY_JVM_DEPS = """ +dependencies { + $DEPS_PLACEHOLDER + testImplementation 'org.jetbrains.kotlin:kotlin-test' +} +""" + +private const val GROOVY_KMP_DEPS = """ +kotlin { + jvm() { + withJava() + } + dependencies { + commonTestImplementation 'org.jetbrains.kotlin:kotlin-test' + } + sourceSets { + jvmMain { + dependencies { + $DEPS_PLACEHOLDER + } + } + } +} +""" + +private fun ProjectBuilderState.copySources(projectDir: File, slice: ProjectSlice) { + fun File.copyInto(targetFile: File) { + listFiles()?.forEach { src -> + val subTarget = File(targetFile, src.name) + if (src.isDirectory) { + subTarget.mkdirs() + src.copyInto(subTarget) + } else if (src.exists() && src.length() > 0) { + src.copyTo(subTarget) + } + } + } + + sourceTemplates.forEach { template -> + File(SAMPLES_SOURCES_PATH, "$template/main").copyInto(File(projectDir, slice.mainPath)) + File(SAMPLES_SOURCES_PATH, "$template/test").copyInto(File(projectDir, slice.testPath)) + } +} + +private val ProjectSlice.mainPath: String + get() { + return when (type) { + ProjectType.KOTLIN_JVM -> "src/main" + ProjectType.KOTLIN_MULTIPLATFORM -> "src/jvmMain" + ProjectType.ANDROID -> "src/jvmMain" + } + } + +private val ProjectSlice.testPath: String + get() { + return when (type) { + ProjectType.KOTLIN_JVM -> "src/test" + ProjectType.KOTLIN_MULTIPLATFORM -> "src/jvmTest" + ProjectType.ANDROID -> "src/jvmTest" + } + } + + +// +//private fun CommonBuilderState.buildRootExtension(slice: ProjectSlice): String { +// if (slice.engine == null && koverConfig.isDefault) { +// return "" +// } +// +// val builder = StringBuilder() +// builder.appendLine() +// builder.appendLine("kover {") +// +// if (koverConfig.disabled != null) { +// val property = if (slice.language == KOTLIN) "isDisabled" else "disabled" +// builder.appendLine("$property = ${koverConfig.disabled}") +// } +// +// if (slice.engine == CoverageEngineVendor.INTELLIJ) { +// builder.appendLine(" coverageEngine.set(kotlinx.kover.api.CoverageEngine.INTELLIJ)") +// if (koverConfig.intellijVersion != null) { +// builder.appendLine(""" intellijEngineVersion.set("${koverConfig.intellijVersion}")""") +// } +// } +// if (slice.engine == CoverageEngineVendor.JACOCO) { +// builder.appendLine(" coverageEngine.set(kotlinx.kover.api.CoverageEngine.JACOCO)") +// if (koverConfig.jacocoVersion != null) { +// builder.appendLine(""" jacocoEngineVersion.set("${koverConfig.jacocoVersion}")""") +// } +// } +// +// if (koverConfig.disabledProjects.isNotEmpty()) { +// val prefix = if (slice.language == KOTLIN) "setOf(" else "[" +// val postfix = if (slice.language == KOTLIN) ")" else "]" +// val value = koverConfig.disabledProjects.joinToString(prefix = prefix, postfix = postfix) { "\"$it\"" } +// builder.appendLine(" disabledProjects = $value") +// } +// +// if (koverConfig.runAllTestsForProjectTask != null) { +// builder.appendLine(" runAllTestsForProjectTask = ${koverConfig.runAllTestsForProjectTask}") +// } +// +// builder.appendLine("}") +// +// return builder.toString() +//} + +// +//@Suppress("UNUSED_PARAMETER") +//private fun ProjectBuilderState.buildVerifications(slice: ProjectSlice): String { +// if (rules.isEmpty()) { +// return "" +// } +// +// val builder = StringBuilder() +// builder.appendLine() +// builder.appendLine("tasks.koverVerify {") +// +// for (rule in rules) { +// builder.appendLine(" rule {") +// rule.name?.also { builder.appendLine(""" name = "$it"""") } +// for (bound in rule.bounds) { +// builder.appendLine(" bound {") +// bound.minValue?.let { builder.appendLine(" minValue = $it") } +// bound.maxValue?.let { builder.appendLine(" maxValue = $it") } +// if (bound.valueType != VerificationValueType.COVERED_PERCENTAGE) { +// builder.appendLine(" valueType = kotlinx.kover.api.VerificationValueType.${bound.valueType}") +// } +// builder.appendLine(" }") +// } +// builder.appendLine(" }") +// } +// builder.appendLine("}") +// +// return builder.toString() +//} + diff --git a/src/functionalTest/templates/samples/different-plugins/build.gradle.kts b/src/functionalTest/templates/samples/different-plugins/build.gradle.kts index 095535e8..4207547b 100644 --- a/src/functionalTest/templates/samples/different-plugins/build.gradle.kts +++ b/src/functionalTest/templates/samples/different-plugins/build.gradle.kts @@ -5,3 +5,5 @@ plugins { repositories { mavenCentral() } + +koverMerged.enable() diff --git a/src/functionalTest/templates/samples/different-plugins/subproject-multiplatform/build.gradle.kts b/src/functionalTest/templates/samples/different-plugins/subproject-multiplatform/build.gradle.kts index 15fecee9..2dc779a1 100644 --- a/src/functionalTest/templates/samples/different-plugins/subproject-multiplatform/build.gradle.kts +++ b/src/functionalTest/templates/samples/different-plugins/subproject-multiplatform/build.gradle.kts @@ -1,5 +1,6 @@ plugins { kotlin("multiplatform") + id("org.jetbrains.kotlinx.kover") } repositories { diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/root b/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/root deleted file mode 100644 index 8a66aba6..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/root +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.jvm' version '1.6.10' - id 'org.jetbrains.kotlinx.kover' version '//PLUGIN_VERSION' -} - -repositories { - mavenCentral()//REPOSITORIES -} - -dependencies {//DEPENDENCIES - testImplementation 'org.jetbrains.kotlin:kotlin-test' -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/subproject b/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/subproject deleted file mode 100644 index 9cc59b26..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/subproject +++ /dev/null @@ -1,12 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.jvm' -} - -repositories { - mavenCentral()//REPOSITORIES -} - -dependencies {//DEPENDENCIES - testImplementation 'org.jetbrains.kotlin:kotlin-test' -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/testTask b/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/testTask deleted file mode 100644 index a9d184d8..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/testTask +++ /dev/null @@ -1,5 +0,0 @@ -tasks.test { - kover { - //KOVER_TEST_CONFIG - } -} diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/root b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/root deleted file mode 100644 index 7c1727f6..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/root +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.multiplatform' version '1.6.10' - id 'org.jetbrains.kotlinx.kover' version '//PLUGIN_VERSION' -} - -repositories { - mavenCentral()//REPOSITORIES -} - -kotlin { - jvm() { - withJava() - } - - dependencies { - commonTestImplementation 'org.jetbrains.kotlin:kotlin-test' - } - - sourceSets { - jvmMain { - dependencies {//DEPENDENCIES - } - } - } -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/subproject b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/subproject deleted file mode 100644 index 3f710e4d..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/subproject +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.multiplatform' -} - -repositories { - mavenCentral()//REPOSITORIES -} - -kotlin { - jvm() { - withJava() - } - - dependencies { - commonTestImplementation 'org.jetbrains.kotlin:kotlin-test' - } - - sourceSets { - jvmMain { - dependencies {//DEPENDENCIES - } - } - } -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/testTask b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/testTask deleted file mode 100644 index 2ccf87c9..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/testTask +++ /dev/null @@ -1,5 +0,0 @@ -tasks.jvmTest { - kover { - //KOVER_TEST_CONFIG - } -} diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/root b/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/root deleted file mode 100644 index 2217fed6..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/root +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - kotlin("jvm") version "1.6.10" - id("org.jetbrains.kotlinx.kover") version "//PLUGIN_VERSION" -} - -repositories { - mavenCentral()//REPOSITORIES -} - -dependencies {//DEPENDENCIES - testImplementation(kotlin("test")) -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/subproject b/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/subproject deleted file mode 100644 index 3fa1382e..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/subproject +++ /dev/null @@ -1,12 +0,0 @@ -plugins { - kotlin("jvm") -} - -repositories { - mavenCentral()//REPOSITORIES -} - -dependencies {//DEPENDENCIES - testImplementation(kotlin("test")) -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/testTask b/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/testTask deleted file mode 100644 index 3a225fde..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/testTask +++ /dev/null @@ -1,5 +0,0 @@ -tasks.test { - extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) { - //KOVER_TEST_CONFIG - } -} diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/root b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/root deleted file mode 100644 index 410231c5..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/root +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - kotlin("multiplatform") version "1.6.10" - id("org.jetbrains.kotlinx.kover") version "//PLUGIN_VERSION" -} - -repositories { - mavenCentral()//REPOSITORIES -} - -kotlin { - jvm() { - withJava() - } - - dependencies { - commonTestImplementation(kotlin("test")) - } - - sourceSets { - val jvmMain by getting { - dependencies {//DEPENDENCIES - } - } - } -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/subproject b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/subproject deleted file mode 100644 index d826c33d..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/subproject +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - kotlin("multiplatform") -} - -repositories { - mavenCentral()//REPOSITORIES -} - -kotlin { - jvm() { - withJava() - } - - dependencies { - commonTestImplementation(kotlin("test")) - } - - sourceSets { - val jvmMain by getting { - dependencies {//DEPENDENCIES - } - } - } -} -//KOVER//SCRIPTS//TEST_TASK//VERIFICATIONS diff --git a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/testTask b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/testTask deleted file mode 100644 index 0c1f5175..00000000 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/testTask +++ /dev/null @@ -1,5 +0,0 @@ -tasks.named("jvmTest").configure { - extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) { - //KOVER_TEST_CONFIG - } -} diff --git a/src/functionalTest/templates/scripts/settings/settings.gradle b/src/functionalTest/templates/scripts/settings/settings.gradle deleted file mode 100644 index 368b66a0..00000000 --- a/src/functionalTest/templates/scripts/settings/settings.gradle +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } - mavenCentral() - maven { url 'https://plugins.gradle.org/m2/' } - } -} -rootProject.name = 'plugin-test-project-groovy' -//SUBPROJECTS//EXTRA_SETTINGS diff --git a/src/functionalTest/templates/scripts/settings/settings.gradle.kts b/src/functionalTest/templates/scripts/settings/settings.gradle.kts deleted file mode 100644 index 60f592ab..00000000 --- a/src/functionalTest/templates/scripts/settings/settings.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { - maven { url=uri("https://oss.sonatype.org/content/repositories/snapshots") } - mavenCentral() - maven { url=uri("https://plugins.gradle.org/m2/") } - } -} -rootProject.name = "plugin-test-project-kotlin" -//SUBPROJECTS//EXTRA_SETTINGS diff --git a/src/main/kotlin/kotlinx/kover/KoverPlugin.kt b/src/main/kotlin/kotlinx/kover/KoverPlugin.kt index 013bf80d..9aadc639 100644 --- a/src/main/kotlin/kotlinx/kover/KoverPlugin.kt +++ b/src/main/kotlin/kotlinx/kover/KoverPlugin.kt @@ -1,398 +1,15 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.kover -import kotlinx.kover.adapters.* -import kotlinx.kover.api.* -import kotlinx.kover.api.KoverPaths.MERGED_HTML_REPORT_DEFAULT_PATH -import kotlinx.kover.api.KoverNames.CHECK_TASK_NAME -import kotlinx.kover.api.KoverNames.COLLECT_REPORTS_TASK_NAME -import kotlinx.kover.api.KoverNames.MERGED_HTML_REPORT_TASK_NAME -import kotlinx.kover.api.KoverNames.HTML_REPORT_TASK_NAME -import kotlinx.kover.api.KoverNames.REPORT_TASK_NAME -import kotlinx.kover.api.KoverNames.VERIFY_TASK_NAME -import kotlinx.kover.api.KoverNames.MERGED_REPORT_TASK_NAME -import kotlinx.kover.api.KoverNames.XML_REPORT_TASK_NAME -import kotlinx.kover.api.KoverNames.ROOT_EXTENSION_NAME -import kotlinx.kover.api.KoverNames.TASK_EXTENSION_NAME -import kotlinx.kover.api.KoverNames.VERIFICATION_GROUP -import kotlinx.kover.api.KoverNames.MERGED_VERIFY_TASK_NAME -import kotlinx.kover.api.KoverNames.MERGED_XML_REPORT_TASK_NAME -import kotlinx.kover.api.KoverPaths.ALL_PROJECTS_REPORTS_DEFAULT_PATH -import kotlinx.kover.api.KoverPaths.PROJECT_HTML_REPORT_DEFAULT_PATH -import kotlinx.kover.api.KoverPaths.MERGED_XML_REPORT_DEFAULT_PATH -import kotlinx.kover.api.KoverPaths.PROJECT_XML_REPORT_DEFAULT_PATH -import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.CoverageAgent -import kotlinx.kover.engines.intellij.* -import kotlinx.kover.tasks.* +import kotlinx.kover.appliers.* import org.gradle.api.* -import org.gradle.api.provider.* -import org.gradle.api.tasks.* -import org.gradle.api.tasks.testing.* -import org.gradle.process.* -import java.io.File -import kotlin.reflect.* class KoverPlugin : Plugin { - private val defaultJacocoVersion = "0.8.7" - override fun apply(target: Project) { - val koverExtension = target.createKoverExtension() - val agents = AgentsFactory.createAgents(target, koverExtension) - - val providers = target.createProviders(agents) - - target.allprojects { - it.applyToProject(providers, agents) - } - target.createCollectingTask() - - target.createMergedTasks(providers) - } - - private fun Project.applyToProject(providers: BuildProviders, agents: Map) { - val projectProviders = - providers.projects[path] - ?: throw GradleException("Kover: Providers for project '$name' ('$path') was not found") - - val xmlReportTask = createKoverProjectTask( - XML_REPORT_TASK_NAME, - KoverXmlReportTask::class, - providers, - projectProviders - ) { - it.xmlReportFile.set(layout.buildDirectory.file(PROJECT_XML_REPORT_DEFAULT_PATH)) - it.description = "Generates code coverage XML report for all enabled test tasks in one project." - } - - val htmlReportTask = createKoverProjectTask( - HTML_REPORT_TASK_NAME, - KoverHtmlReportTask::class, - providers, - projectProviders - ) { - it.htmlReportDir.set(it.project.layout.buildDirectory.dir(PROJECT_HTML_REPORT_DEFAULT_PATH)) - it.description = "Generates code coverage HTML report for all enabled test tasks in one project." - } - - val verifyTask = createKoverProjectTask( - VERIFY_TASK_NAME, - KoverVerificationTask::class, - providers, - projectProviders - ) { - it.onlyIf { t -> (t as KoverVerificationTask).rules.isNotEmpty() } - it.description = "Verifies code coverage metrics of one project based on specified rules." - } - - tasks.create(REPORT_TASK_NAME) { - it.group = VERIFICATION_GROUP - it.dependsOn(xmlReportTask) - it.dependsOn(htmlReportTask) - it.description = "Generates code coverage HTML and XML reports for all enabled test tasks in one project." - } - - tasks.configureEach { - if (it.name == CHECK_TASK_NAME) { - it.dependsOn(verifyTask) - } - } - - tasks.withType(Test::class.java).configureEach { t -> - t.configTestTask(providers, agents) - } - } - - private fun Project.createMergedTasks(providers: BuildProviders) { - val xmlReportTask = createKoverMergedTask( - MERGED_XML_REPORT_TASK_NAME, - KoverMergedXmlReportTask::class, - providers - ) { - it.xmlReportFile.set(layout.buildDirectory.file(MERGED_XML_REPORT_DEFAULT_PATH)) - it.description = "Generates code coverage XML report for all enabled test tasks in all projects." - } - - val htmlReportTask = createKoverMergedTask( - MERGED_HTML_REPORT_TASK_NAME, - KoverMergedHtmlReportTask::class, - providers - ) { - it.htmlReportDir.set(layout.buildDirectory.dir(MERGED_HTML_REPORT_DEFAULT_PATH)) - it.description = "Generates code coverage HTML report for all enabled test tasks in all projects." - } - - val reportTask = tasks.create(MERGED_REPORT_TASK_NAME) { - it.group = VERIFICATION_GROUP - it.dependsOn(xmlReportTask) - it.dependsOn(htmlReportTask) - it.description = "Generates code coverage HTML and XML reports for all enabled test tasks in all projects." - } - - val verifyTask = createKoverMergedTask( - MERGED_VERIFY_TASK_NAME, - KoverMergedVerificationTask::class, - providers - ) { - it.onlyIf { t -> (t as KoverMergedVerificationTask).rules.isNotEmpty() } - it.description = "Verifies code coverage metrics of all projects based on specified rules." - } - - tasks.configureEach { - if (it.name == CHECK_TASK_NAME) { - it.dependsOn(provider { - val koverExtension = extensions.getByType(KoverExtension::class.java) - if (koverExtension.generateReportOnCheck) { - listOf(reportTask, verifyTask) - } else { - listOf(verifyTask) - } - }) - } - } - } - - - private fun Project.createKoverMergedTask( - taskName: String, - type: KClass, - providers: BuildProviders, - block: (T) -> Unit - ): T { - val task = tasks.create(taskName, type.java) - - task.group = VERIFICATION_GROUP - - providers.projects.forEach { (projectPath, m) -> - task.binaryReportFiles.put(projectPath, NestedFiles(task.project.objects, m.reports)) - task.srcDirs.put(projectPath, NestedFiles(task.project.objects, m.sources)) - task.outputDirs.put(projectPath, NestedFiles(task.project.objects, m.output)) - } - - task.coverageEngine.set(providers.engine) - task.classpath.set(providers.classpath) - task.dependsOn(providers.merged.tests) - - val disabledProvider = providers.merged.disabled - task.onlyIf { !disabledProvider.get() } - - block(task) - return task - } - - private fun Project.createCollectingTask() { - tasks.create(COLLECT_REPORTS_TASK_NAME, KoverCollectingTask::class.java) { task -> - task.group = VERIFICATION_GROUP - task.description = "Collects all projects reports into one directory." - task.outputDir.set(project.layout.buildDirectory.dir(ALL_PROJECTS_REPORTS_DEFAULT_PATH)) - // disable UP-TO-DATE check for task: it will be executed every time - task.outputs.upToDateWhen { false } - - allprojects { proj -> - val xmlReportTask = - proj.tasks.withType(KoverXmlReportTask::class.java).getByName(XML_REPORT_TASK_NAME) - val htmlReportTask = - proj.tasks.withType(KoverHtmlReportTask::class.java).getByName(HTML_REPORT_TASK_NAME) - - task.mustRunAfter(xmlReportTask) - task.mustRunAfter(htmlReportTask) - - task.xmlFiles[proj.path] = xmlReportTask.xmlReportFile - task.htmlDirs[proj.path] = htmlReportTask.htmlReportDir - } - } - } - - - private fun Project.createKoverProjectTask( - taskName: String, - type: KClass, - providers: BuildProviders, - projectProviders: ProjectProviders, - block: (T) -> Unit - ): T { - tasks.findByName(taskName)?.let { - throw GradleException("Kover task '$taskName' already exist. Plugin should not be applied in child project if it has already been applied in one of the parent projects.") - } - - val task = tasks.create(taskName, type.java) - task.group = VERIFICATION_GROUP - - task.coverageEngine.set(providers.engine) - task.classpath.set(providers.classpath) - task.srcDirs.set(projectProviders.sources) - task.outputDirs.set(projectProviders.output) - - // it is necessary to read all binary reports because project's classes can be invoked in another project - task.binaryReportFiles.set(projectProviders.reports) - task.dependsOn(projectProviders.tests) - - val disabledProvider = projectProviders.disabled - task.onlyIf { !disabledProvider.get() } - task.onlyIf { !(it as KoverProjectTask).binaryReportFiles.get().isEmpty } - - block(task) - - return task - } - - private fun Project.createKoverExtension(): KoverExtension { - val extension = extensions.create(ROOT_EXTENSION_NAME, KoverExtension::class.java, objects) - extension.isDisabled = false - extension.coverageEngine.set(CoverageEngine.INTELLIJ) - extension.intellijEngineVersion.set(defaultIntellijVersion.toString()) - extension.jacocoEngineVersion.set(defaultJacocoVersion) - - afterEvaluate(CollectDisabledProjectsPathsAction(extension)) - - return extension - } - - private fun Test.configTestTask( - providers: BuildProviders, - agents: Map - ) { - val taskExtension = extensions.create(TASK_EXTENSION_NAME, KoverTaskExtension::class.java, project.objects) - - taskExtension.isDisabled = false - taskExtension.binaryReportFile.set(project.provider { - val koverExtension = providers.koverExtension.get() - val suffix = if (koverExtension.coverageEngine.get() == CoverageEngine.INTELLIJ) ".ic" else ".exec" - project.layout.buildDirectory.get().file("kover/$name$suffix").asFile - }) - - val pluginContainer = project.plugins - val excludeAndroidPackages = - project.provider { pluginContainer.androidPluginIsApplied && !providers.koverExtension.get().instrumentAndroidPackage } - - jvmArgumentProviders.add( - CoverageArgumentProvider( - this, - agents, - taskExtension, - providers.koverExtension, - excludeAndroidPackages - ) - ) - - val sourceErrorProvider = project.provider { - File(taskExtension.binaryReportFile.get().parentFile, "coverage-error.log") - } - val targetErrorProvider = project.layout.buildDirectory.file("kover/errors/$name.log").map { it.asFile } - - doFirst(BinaryReportCleanupAction(project.path, providers.koverExtension, taskExtension)) - doLast(MoveIntellijErrorLogAction(sourceErrorProvider, targetErrorProvider)) - } -} - -/* - To support parallel tests, both Coverage Engines work in append to data file mode. - For this reason, before starting the tests, it is necessary to clear the file from the results of previous runs. -*/ -private class BinaryReportCleanupAction( - private val projectPath: String, - private val koverExtensionProvider: Provider, - private val taskExtension: KoverTaskExtension -) : Action { - override fun execute(task: Task) { - val koverExtension = koverExtensionProvider.get() - val file = taskExtension.binaryReportFile.get() - - // always delete previous data file - file.delete() - - if (!taskExtension.isDisabled - && !koverExtension.isDisabled - && !koverExtension.disabledProjectsPaths.contains(projectPath) - && koverExtension.coverageEngine.get() == CoverageEngine.INTELLIJ - ) { - // IntelliJ engine expected empty file for parallel test execution. - // Since it is impossible to know in advance whether the tests will be run in parallel, we always create an empty file. - file.createNewFile() - } - } -} - -private class CollectDisabledProjectsPathsAction( - private val koverExtension: KoverExtension, -) : Action { - override fun execute(project: Project) { - val allProjects = project.allprojects - val paths = allProjects.associate { it.name to mutableListOf() } - allProjects.forEach { paths.getValue(it.name) += it.path } - - val result: MutableSet = mutableSetOf() - - koverExtension.disabledProjects.map { - if (it.startsWith(':')) { - result += it - } else { - val projectPaths = paths[it] ?: return@map - if (projectPaths.size > 1) { - throw GradleException("Kover configuring error: ambiguous name of the excluded project '$it': suitable projects with paths $projectPaths. Consider using fully-qualified name starting with ':'") - } - result += projectPaths - } - } - - koverExtension.disabledProjectsPaths = result - } -} - -private class MoveIntellijErrorLogAction( - private val sourceFile: Provider, - private val targetFile: Provider -) : Action { - override fun execute(task: Task) { - val origin = sourceFile.get() - if (origin.exists() && origin.isFile) { - origin.copyTo(targetFile.get(), true) - origin.delete() - } - } -} - -private class CoverageArgumentProvider( - private val task: Task, - private val agents: Map, - @get:Nested val taskExtension: KoverTaskExtension, - @get:Nested val koverExtension: Provider, - @get:Input val excludeAndroidPackage: Provider -) : CommandLineArgumentProvider, Named { - - private val projectPath: String = task.project.path - - @Internal - override fun getName(): String { - return "koverArgumentsProvider" - } - - override fun asArguments(): MutableIterable { - val koverExtensionValue = koverExtension.get() - - if (taskExtension.isDisabled - || koverExtensionValue.isDisabled - || koverExtensionValue.disabledProjectsPaths.contains(projectPath) - ) { - return mutableListOf() - } - - if (excludeAndroidPackage.get()) { - /* - The instrumentation of android classes often causes errors when using third-party - frameworks (see https://github.com/Kotlin/kotlinx-kover/issues/89). - - Because android classes are not part of the project, in any case they do not get into the report, - and they can be excluded from instrumentation. - - FIXME Remove this code if the IntelliJ Agent stops changing project classes during instrumentation - */ - taskExtension.excludes = taskExtension.excludes + "android.*" + "com.android.*" - } - - return agents.getFor(koverExtensionValue.coverageEngine.get()).buildCommandLineArgs(task, taskExtension) + target.applyToProject() + target.applyMerged() } } diff --git a/src/main/kotlin/kotlinx/kover/Providers.kt b/src/main/kotlin/kotlinx/kover/Providers.kt deleted file mode 100644 index 6b33a3b7..00000000 --- a/src/main/kotlin/kotlinx/kover/Providers.kt +++ /dev/null @@ -1,135 +0,0 @@ -package kotlinx.kover - -import kotlinx.kover.adapters.* -import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.CoverageAgent -import org.gradle.api.* -import org.gradle.api.file.* -import org.gradle.api.provider.* -import org.gradle.api.tasks.testing.* -import java.io.* - - -internal fun Project.createProviders(agents: Map): BuildProviders { - val projects: MutableMap = mutableMapOf() - - allprojects { - projects[it.path] = ProjectProviders( - it.provider { it.files(if (runAllTests()) allBinaryReports() else it.binaryReports(this)) }, - it.provider { if (runAllTests()) allTestTasks() else it.testTasks(this) }, - it.provider { it.collectDirs(this).first }, - it.provider { it.collectDirs(this).second }, - it.provider { it.isDisabled(this) } - ) - } - - val engineProvider = provider { extensions.getByType(KoverExtension::class.java).coverageEngine.get() } - - val classpathProvider: Provider = provider { - val koverExtension = extensions.getByType(KoverExtension::class.java) - agents.getFor(koverExtension.coverageEngine.get()).classpath - } - - val extensionProvider = provider { extensions.getByType(KoverExtension::class.java) } - - - val allReportsProvider: Provider = provider { files(allBinaryReports()) } - val allTestsProvider = provider { allTestTasks() } - val koverDisabledProvider = provider { extensions.getByType(KoverExtension::class.java).isDisabled } - - - // all sources and all outputs providers are unused, so NOW it can return empty file collection - val emptyProvider: Provider = provider { files() } - val mergedProviders = - ProjectProviders( - allReportsProvider, - allTestsProvider, - emptyProvider, - emptyProvider, - koverDisabledProvider) - - return BuildProviders(projects, mergedProviders, engineProvider, classpathProvider, extensionProvider) -} - - -internal fun Project.allTestTasks(): List { - return allprojects.flatMap { it.testTasks(this) } -} - -internal fun Project.allBinaryReports(): List { - return allprojects.flatMap { it.binaryReports(this) } -} - - -internal fun Project.testTasks(rootProject: Project): List { - if (isDisabled(rootProject)) { - return emptyList() - } - - return tasks.withType(Test::class.java) - .filterNot { t -> t.extensions.getByType(KoverTaskExtension::class.java).isDisabled } -} - -internal fun Project.binaryReports(root: Project): List { - if (isDisabled(root)) { - return emptyList() - } - - return tasks.withType(Test::class.java).asSequence() - .map { t -> t.extensions.getByType(KoverTaskExtension::class.java) } - // process binary report only from tasks with enabled cover - .filterNot { e -> e.isDisabled } - .map { e -> e.binaryReportFile.get() } - // process binary report only from tasks with sources - .filter { f -> f.exists() } - .toList() -} - -private fun Project.collectDirs(root: Project): Pair { - if (isDisabled(root)) { - return files() to files() - } - - val srcDirs = HashMap() - val outDirs = HashMap() - - createAdapters().forEach { - val dirs = it.findDirs(this) - srcDirs += dirs.sources.asSequence().map { f -> f.canonicalPath to f } - outDirs += dirs.output.asSequence().map { f -> f.canonicalPath to f } - } - - val src = srcDirs.asSequence().map { it.value }.filter { it.exists() && it.isDirectory }.toList() - val out = outDirs.asSequence().map { it.value }.filter { it.exists() && it.isDirectory }.toList() - - return files(src) to files(out) -} - -private fun Project.isDisabled(root: Project): Boolean { - val koverExtension = root.extensions.getByType(KoverExtension::class.java) - return koverExtension.isDisabled || koverExtension.disabledProjectsPaths.contains(path) -} - -private fun Project.runAllTests(): Boolean { - return extensions.getByType(KoverExtension::class.java).runAllTestsForProjectTask -} - - -internal class BuildProviders( - val projects: Map, - val merged: ProjectProviders, - - val engine: Provider, - val classpath: Provider, - val koverExtension: Provider -) - -internal class ProjectProviders( - val reports: Provider, - val tests: Provider>, - val sources: Provider, - val output: Provider, - val disabled: Provider -) - diff --git a/src/main/kotlin/kotlinx/kover/adapters/AndroidPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/adapters/AndroidPluginAdapter.kt deleted file mode 100644 index d9228841..00000000 --- a/src/main/kotlin/kotlinx/kover/adapters/AndroidPluginAdapter.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.adapters - -import com.android.build.gradle.* -import kotlinx.kover.adapters.api.* -import org.gradle.api.* - -class AndroidPluginAdapter : CompilationPluginAdapter { - - override fun findDirs(project: Project): PluginDirs { - return safe(project) { - this.plugins.findPlugin("android") ?: return@safe PluginDirs(emptyList(), emptyList()) - - val extension = project.extensions.findByType(BaseExtension::class.java) ?: return@safe PluginDirs( - emptyList(), - emptyList() - ) - - val sourceDirs = extension.sourceSets.asSequence() - .filter { !it.name.startsWith("test") && !it.name.startsWith("androidTest") } - .map { it.java }.toList() - .flatMap { it.srcDirs } - - - - return@safe PluginDirs(sourceDirs, emptyList()) - } - } - -} diff --git a/src/main/kotlin/kotlinx/kover/adapters/KotlinAndroidPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/adapters/KotlinAndroidPluginAdapter.kt deleted file mode 100644 index 56aa6648..00000000 --- a/src/main/kotlin/kotlinx/kover/adapters/KotlinAndroidPluginAdapter.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.adapters - -import kotlinx.kover.adapters.api.* -import org.gradle.api.* -import org.jetbrains.kotlin.gradle.dsl.* - -class KotlinAndroidPluginAdapter : CompilationPluginAdapter { - - override fun findDirs(project: Project): PluginDirs { - return safe(project) { - this.plugins.findPlugin("kotlin-android") ?: return@safe PluginDirs(emptyList(), emptyList()) - - val extension = project.extensions.findByType(KotlinAndroidProjectExtension::class.java) ?: return@safe PluginDirs( - emptyList(), - emptyList() - ) - - - val sourceDirs = extension.target.compilations - .filter { !it.name.endsWith("Test") } - .flatMap { it.allKotlinSourceSets } - .map { it.kotlin } - .flatMap { it.srcDirs } - - val outputDirs = extension.target.compilations - .filter { !it.name.endsWith("Test") } - .flatMap { it.output.classesDirs } - - return@safe PluginDirs(sourceDirs, outputDirs) - } - } - -} diff --git a/src/main/kotlin/kotlinx/kover/adapters/OldJavaPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/adapters/OldJavaPluginAdapter.kt deleted file mode 100644 index 9640083c..00000000 --- a/src/main/kotlin/kotlinx/kover/adapters/OldJavaPluginAdapter.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.adapters - -import kotlinx.kover.adapters.api.* -import org.gradle.api.* -import org.gradle.api.tasks.* - -class OldJavaPluginAdapter : CompilationPluginAdapter { - - override fun findDirs(project: Project): PluginDirs { - return safe(project) { - this.plugins.findPlugin("java") ?: return@safe PluginDirs(emptyList(), emptyList()) - - val sourceSetContainer = project.extensions.findByType( - SourceSetContainer::class.java - ) ?: return@safe PluginDirs(emptyList(), emptyList()) - - val sourceSets = sourceSetContainer.filter { it.name != SourceSet.TEST_SOURCE_SET_NAME } - - val sourceDirs = sourceSets.flatMap { it.allSource.srcDirs } - val outputDirs = sourceSets.flatMap { it.output.classesDirs } - - return@safe PluginDirs(sourceDirs, outputDirs) - } - } - -} diff --git a/src/main/kotlin/kotlinx/kover/adapters/PluginAdaptersFactory.kt b/src/main/kotlin/kotlinx/kover/adapters/PluginAdaptersFactory.kt deleted file mode 100644 index ec4d2ca6..00000000 --- a/src/main/kotlin/kotlinx/kover/adapters/PluginAdaptersFactory.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.adapters - -import kotlinx.kover.adapters.api.* -import org.gradle.api.plugins.PluginContainer - -internal fun createAdapters(): List { - return listOf( - OldJavaPluginAdapter(), - KotlinMultiplatformPluginAdapter(), - AndroidPluginAdapter(), - KotlinAndroidPluginAdapter() - ) -} - -val PluginContainer.androidPluginIsApplied: Boolean - get() { - return findPlugin("android") != null || findPlugin("kotlin-android") != null - } diff --git a/src/main/kotlin/kotlinx/kover/adapters/api/CompilationPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/adapters/api/CompilationPluginAdapter.kt deleted file mode 100644 index 9555412e..00000000 --- a/src/main/kotlin/kotlinx/kover/adapters/api/CompilationPluginAdapter.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.adapters.api - -import org.gradle.api.* -import java.io.* - -interface CompilationPluginAdapter { - fun findDirs(project: Project): PluginDirs -} - - -data class PluginDirs(val sources: List, val output: List) - -internal inline fun safe(project: Project, block: Project.() -> PluginDirs): PluginDirs { - return try { - project.block() - } catch (e: Throwable) { - when (e) { - is NoSuchMethodError, is NoSuchFieldError, is ClassNotFoundException, is NoClassDefFoundError -> { - project.logger.info("Problem occurred in Kover source set adapter", e) - PluginDirs(emptyList(), emptyList()) - } - else -> throw e - } - } -} diff --git a/src/main/kotlin/kotlinx/kover/api/CoverageEngines.kt b/src/main/kotlin/kotlinx/kover/api/CoverageEngines.kt new file mode 100644 index 00000000..da9560bc --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/api/CoverageEngines.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("RedundantVisibilityModifier") + +package kotlinx.kover.api + +import kotlinx.kover.api.KoverVersions.DEFAULT_INTELLIJ_VERSION +import kotlinx.kover.api.KoverVersions.DEFAULT_JACOCO_VERSION +import org.gradle.api.tasks.* + +public interface CoverageEngineVariant { + @get:Input + public val vendor: CoverageEngineVendor + @get:Input + public val version: String +} + +public enum class CoverageEngineVendor { + INTELLIJ, + JACOCO +} + +public class IntellijEngine(override val version: String): CoverageEngineVariant { + override val vendor: CoverageEngineVendor = CoverageEngineVendor.INTELLIJ + override fun toString(): String = "IntelliJ Coverage Engine $version" +} +public object DefaultIntellijEngine: CoverageEngineVariant { + override val vendor: CoverageEngineVendor = CoverageEngineVendor.INTELLIJ + override val version: String = DEFAULT_INTELLIJ_VERSION +} + +public class JacocoEngine(override val version: String): CoverageEngineVariant { + override val vendor: CoverageEngineVendor = CoverageEngineVendor.JACOCO + override fun toString(): String = "JaCoCo Coverage Engine $version" +} +public object DefaultJacocoEngine: CoverageEngineVariant { + override val vendor: CoverageEngineVendor = CoverageEngineVendor.JACOCO + override val version: String = DEFAULT_JACOCO_VERSION +} diff --git a/src/main/kotlin/kotlinx/kover/api/KoverConfig.kt b/src/main/kotlin/kotlinx/kover/api/KoverConfig.kt new file mode 100644 index 00000000..1336268c --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/api/KoverConfig.kt @@ -0,0 +1,322 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("RedundantVisibilityModifier") + +package kotlinx.kover.api + +import org.gradle.api.* +import org.gradle.api.file.* +import org.gradle.api.model.* +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import javax.annotation.* +import javax.inject.* + +public open class KoverProjectConfig @Inject constructor(objects: ObjectFactory) { + public val isDisabled: Property = objects.property(Boolean::class.java) + + public val engine: Property = objects.property(CoverageEngineVariant::class.java) + + internal val filters: KoverProjectFilters = objects.newInstance(KoverProjectFilters::class.java, objects) + + internal val instrumentation: KoverProjectInstrumentation = + objects.newInstance(KoverProjectInstrumentation::class.java) + + internal val xmlReport: KoverProjectXmlConfig = objects.newInstance(KoverProjectXmlConfig::class.java, objects) + + internal val htmlReport: KoverProjectHtmlConfig = objects.newInstance(KoverProjectHtmlConfig::class.java, objects) + + internal val verify: KoverVerifyConfig = objects.newInstance(KoverVerifyConfig::class.java, objects) + + public fun filters(config: Action) { + config.execute(filters) + } + + public fun instrumentation(config: Action) { + config.execute(instrumentation) + } + + public fun xmlReport(config: Action) { + config.execute(xmlReport) + } + + public fun htmlReport(config: Action) { + config.execute(htmlReport) + } + + public fun verify(config: Action) { + config.execute(verify) + } +} + +public open class KoverProjectFilters @Inject constructor(private val objects: ObjectFactory) { + internal val classes: Property = objects.property(KoverClassFilters::class.java) + //it's ok to create an instance by a constructor because this object won't be used in Gradle + .value(KoverClassFilters()) + + internal val sourceSets: Property = objects.property(KoverSourceSetFilters::class.java) + //it's ok to create an instance by a constructor because this object won't be used in Gradle + .value(KoverSourceSetFilters()) + + public fun classes(config: Action) { + val classFilters = objects.newInstance(KoverClassFilters::class.java) + config.execute(classFilters) + classes.set(classFilters) + } + + public fun sourcesets(config: Action) { + val sourceSetFilters = objects.newInstance(KoverSourceSetFilters::class.java) + config.execute(sourceSetFilters) + sourceSets.set(sourceSetFilters) + } +} + +public open class KoverProjectInstrumentation { + public val excludeTasks: MutableSet = mutableSetOf() +} + +public open class KoverProjectXmlConfig @Inject constructor(objects: ObjectFactory) { + public val onCheck: Property = objects.property(Boolean::class.java) + public val reportFile: RegularFileProperty = objects.fileProperty() + internal val taskFilters: KoverProjectFilters = objects.newInstance(KoverProjectFilters::class.java, objects) + + public fun overrideFilters(config: Action) { + config.execute(taskFilters) + } +} + +public open class KoverProjectHtmlConfig @Inject constructor(private val objects: ObjectFactory) { + public val onCheck: Property = objects.property(Boolean::class.java) + + public val reportDir: DirectoryProperty = objects.directoryProperty() + internal val taskFilters: KoverProjectFilters = objects.newInstance(KoverProjectFilters::class.java, objects) + + public fun overrideFilters(config: Action) { + config.execute(taskFilters) + } +} + + +public open class KoverMergedConfig @Inject constructor(objects: ObjectFactory) { + internal var isEnabled: Property = objects.property(Boolean::class.java) + internal val filters: KoverMergedFilters = objects.newInstance(KoverMergedFilters::class.java, objects) + internal val xmlReport: KoverMergedXmlConfig = objects.newInstance(KoverMergedXmlConfig::class.java, objects) + internal val htmlReport: KoverMergedHtmlConfig = objects.newInstance(KoverMergedHtmlConfig::class.java, objects) + internal val verify: KoverVerifyConfig = objects.newInstance(KoverVerifyConfig::class.java, objects) + + public fun enable() { + isEnabled.set(true) + } + + public fun filters(config: Action) { + config.execute(filters) + } + + public fun xmlReport(config: Action) { + config.execute(xmlReport) + } + + public fun htmlReport(config: Action) { + config.execute(htmlReport) + } + + public fun verify(config: Action) { + config.execute(verify) + } +} + +public open class KoverMergedFilters @Inject constructor(private val objects: ObjectFactory) { + internal val classes: Property = + objects.property(KoverClassFilters::class.java).value(KoverClassFilters()) + + internal val projects: Property = + objects.property(KoverProjectsFilters::class.java).value(KoverProjectsFilters()) + + public fun classes(config: Action) { + val classFilters = objects.newInstance(KoverClassFilters::class.java) + config.execute(classFilters) + classes.set(classFilters) + } + + public fun projects(config: Action) { + val projectsFilters = objects.newInstance(KoverProjectsFilters::class.java) + config.execute(projectsFilters) + projects.set(projectsFilters) + } +} + + +public open class KoverMergedXmlConfig @Inject constructor(private val objects: ObjectFactory) { + public val onCheck: Property = objects.property(Boolean::class.java) + + /** + * Specifies file path of generated XML report file with coverage data. + */ + public val reportFile: RegularFileProperty = objects.fileProperty() + + internal val classes: Property = objects.property(KoverClassFilters::class.java) + + public fun overrideClassFilters(config: Action) { + val classFilters = objects.newInstance(KoverClassFilters::class.java) + config.execute(classFilters) + classes.set(classFilters) + } +} + +public open class KoverMergedHtmlConfig @Inject constructor(private val objects: ObjectFactory) { + + public val onCheck: Property = objects.property(Boolean::class.java) + + /** + * Specifies directory path of generated HTML report. + */ + public val reportDir: DirectoryProperty = objects.directoryProperty() + + internal val classes: Property = objects.property(KoverClassFilters::class.java) + + public fun overrideClassFilters(config: Action) { + val classFilters = objects.newInstance(KoverClassFilters::class.java) + config.execute(classFilters) + classes.set(classFilters) + } +} + +public open class KoverProjectsFilters { + @get:Input + public val includes: MutableList = mutableListOf() +} + + +public open class KoverVerifyConfig @Inject constructor(private val objects: ObjectFactory) { + public val onCheck: Property = objects.property(Boolean::class.java).value(true) + + internal val rules: ListProperty = objects.listProperty(VerificationRule::class.java) + + public fun rule(configureRule: Action) { + rules.add(objects.newInstance(VerificationRule::class.java, objects).also { configureRule.execute(it) }) + } +} + +public open class KoverSourceSetFilters { + @get:Input + public val excludes: MutableSet = mutableSetOf() + + @get:Input + public var excludeTests: Boolean = true +} + +public open class KoverClassFilters { + @get:Input + public val includes: MutableList = mutableListOf() + + @get:Input + public val excludes: MutableList = mutableListOf() +} + +public open class VerificationRule @Inject constructor(private val objects: ObjectFactory) { + @get:Input + public var isEnabled: Boolean = true + + @get:Input + @get:Nullable + @get:Optional + public var name: String? = null + + @get:Input + public var target: VerificationTarget = VerificationTarget.ALL + + /** + * Absent default value to indicate that class filters are not overridden for the rule. + */ + @get:Nested + @get:Optional + internal val filters: Property = objects.property(KoverClassFilters::class.java) + + @get:Nested + internal val bounds: ListProperty = objects.listProperty(VerificationBound::class.java) + + public fun overrideClassFilters(config: Action) { + if (!filters.isPresent) { + filters.set(objects.newInstance(KoverClassFilters::class.java)) + } + config.execute(filters.get()) + } + + public fun bound(configureBound: Action) { + bounds.add(objects.newInstance(VerificationBound::class.java).also { configureBound.execute(it) }) + } +} + +public open class VerificationBound { + /** + * Minimal value to compare with counter value. + */ + @get:Input + @get:Nullable + @get:Optional + public var minValue: Int? = null + + /** + * Maximal value to compare with counter value. + */ + @get:Input + @get:Nullable + @get:Optional + public var maxValue: Int? = null + + /** + * TODO + */ + @get:Input + public var counter: CounterType = CounterType.LINE + + /** + * Type of lines counter value to compare with minimal and maximal values if them defined. + * Default is [VerificationValueType.COVERED_PERCENTAGE] + */ + @get:Input + public var valueType: VerificationValueType = VerificationValueType.COVERED_PERCENTAGE +} + +/** + * TODO + */ +public enum class VerificationTarget { + /** + * TODO + */ + ALL, + + /** + * TODO + */ + CLASS, + + /** + * TODO + */ + PACKAGE +} + +/** + * TODO + */ +public enum class CounterType { + LINE, + INSTRUCTION, + BRANCH +} + + +/** + * Type of lines counter value to compare with minimal and maximal values if them defined. + */ +public enum class VerificationValueType { + COVERED_COUNT, + MISSED_COUNT, + COVERED_PERCENTAGE, + MISSED_PERCENTAGE +} diff --git a/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt b/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt index 8353963d..7d3e03f1 100644 --- a/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt +++ b/src/main/kotlin/kotlinx/kover/api/KoverConstants.kt @@ -1,10 +1,17 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("RedundantVisibilityModifier") + package kotlinx.kover.api public object KoverNames { public const val CHECK_TASK_NAME = "check" public const val VERIFICATION_GROUP = "verification" - public const val ROOT_EXTENSION_NAME = "kover" + public const val PROJECT_EXTENSION_NAME = "kover" + public const val MERGED_EXTENSION_NAME = "koverMerged" public const val TASK_EXTENSION_NAME = "kover" public const val MERGED_XML_REPORT_TASK_NAME = "koverMergedXmlReport" @@ -15,17 +22,24 @@ public object KoverNames { public const val XML_REPORT_TASK_NAME = "koverXmlReport" public const val HTML_REPORT_TASK_NAME = "koverHtmlReport" public const val REPORT_TASK_NAME = "koverReport" - public const val COLLECT_REPORTS_TASK_NAME = "koverCollectReports" public const val VERIFY_TASK_NAME = "koverVerify" + + public const val CONFIGURATION_NAME = "KoverEngineConfig" } public object KoverPaths { - public const val MERGED_HTML_REPORT_DEFAULT_PATH = "reports/kover/html" - public const val MERGED_XML_REPORT_DEFAULT_PATH = "reports/kover/report.xml" + public const val MERGED_HTML_REPORT_DEFAULT_PATH = "reports/kover/merged/html" + public const val MERGED_XML_REPORT_DEFAULT_PATH = "reports/kover/merged/xml/report.xml" + public const val MERGED_VERIFICATION_REPORT_DEFAULT_PATH = "reports/kover/merged/verification/errors.txt" - public const val PROJECT_HTML_REPORT_DEFAULT_PATH = "reports/kover/project-html" - public const val PROJECT_XML_REPORT_DEFAULT_PATH = "reports/kover/project-xml/report.xml" + public const val PROJECT_HTML_REPORT_DEFAULT_PATH = "reports/kover/html" + public const val PROJECT_XML_REPORT_DEFAULT_PATH = "reports/kover/xml/report.xml" + public const val PROJECT_VERIFICATION_REPORT_DEFAULT_PATH = "reports/kover/verification/errors.txt" +} - public const val ALL_PROJECTS_REPORTS_DEFAULT_PATH = "reports/kover/projects" +public object KoverVersions { + internal const val MINIMAL_INTELLIJ_VERSION = "1.0.668" + internal const val DEFAULT_INTELLIJ_VERSION = "1.0.668" + internal const val DEFAULT_JACOCO_VERSION = "0.8.8" } diff --git a/src/main/kotlin/kotlinx/kover/api/KoverExtension.kt b/src/main/kotlin/kotlinx/kover/api/KoverExtension.kt deleted file mode 100644 index d7fabfcb..00000000 --- a/src/main/kotlin/kotlinx/kover/api/KoverExtension.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.api - -import org.gradle.api.model.* -import org.gradle.api.provider.* -import org.gradle.api.tasks.* - -open class KoverExtension(objects: ObjectFactory) { - - /** - * Specifies whether instrumentation is disabled for all test tasks of all projects. - */ - @get:Input - public var isDisabled: Boolean = false - - /** - * Specifies the coverage engine to be used to collect execution data. - */ - @get:Input - public val coverageEngine: Property = objects.property(CoverageEngine::class.java) - - /** - * Specifies the version of Intellij-coverage dependency. - */ - @get:Input - public val intellijEngineVersion: Property = objects.property(String::class.java) - - /** - * Specifies the version of JaCoCo dependency. - */ - @get:Input - public val jacocoEngineVersion: Property = objects.property(String::class.java) - - /** - * Specifies whether the reports will be generated within 'check' task execution. - */ - @get:Input - public var generateReportOnCheck: Boolean = true - - /** - * Specifies the projects to be disabled from instrumentation and reportings. - */ - @get:Input - public var disabledProjects: Set = emptySet() - - /** - * Specifies whether the classes from 'android' and 'com.android' packages should be included if Android plugin is applied. - */ - @get:Input - public var instrumentAndroidPackage: Boolean = false - - /** - * Specifies whether to perform all test tasks from all projects for Kover single-project tasks. - * If the value is `false`, then executed only test tasks of the project for which its Kover task is called. - */ - @get:Input - public var runAllTestsForProjectTask: Boolean = false - - /** - * Fully-qualified path of disabled projects. - * It is filled in only automatically based on custom values in `disabledProjects'. - */ - @get:Internal - internal var disabledProjectsPaths: Set = emptySet() -} - -public enum class CoverageEngine { - INTELLIJ, - JACOCO -} diff --git a/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt b/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt index cf3caa4b..b8b2cd72 100644 --- a/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt +++ b/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt @@ -1,32 +1,34 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ @file:Suppress("RedundantVisibilityModifier") package kotlinx.kover.api +import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.* -import org.gradle.api.provider.* +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property import org.gradle.api.tasks.* -import java.io.* /** * Extension for Kover plugin that additionally configures test tasks and * runs them with coverage agent to generate coverage execution data. */ -open class KoverTaskExtension(objects: ObjectFactory) { +public open class KoverTaskExtension(objects: ObjectFactory) { /** * Specifies whether instrumentation is disabled for an extended test task. */ @get:Input - public var isDisabled: Boolean = false + @get:JvmName("getIsDisabled") + public val isDisabled: Property = objects.property(Boolean::class.java) /** * Specifies file path of generated binary file with coverage data. */ @get:OutputFile - public val binaryReportFile: Property = objects.property(File::class.java) + public val reportFile: RegularFileProperty = objects.fileProperty() /** * Specifies class instrumentation inclusion rules. @@ -37,7 +39,7 @@ open class KoverTaskExtension(objects: ObjectFactory) { * It's possible to use `*` and `?` wildcards. */ @get:Input - public var includes: List = emptyList() + public val includes: ListProperty = objects.listProperty(String::class.java) /** * Specifies class instrumentation exclusion rules. @@ -48,5 +50,16 @@ open class KoverTaskExtension(objects: ObjectFactory) { * It's possible to use `*` and `?` wildcards. */ @get:Input - public var excludes: List = emptyList() + public val excludes: ListProperty = objects.listProperty(String::class.java) + + + // DEPRECATIONS + // TODO delete in 0.9 version + @get:Internal + @Deprecated( + message = "Property was renamed in Kover API version 2", + replaceWith = ReplaceWith("reportFile"), + level = DeprecationLevel.ERROR + ) + public val binaryReportFile: RegularFileProperty? = null } diff --git a/src/main/kotlin/kotlinx/kover/api/Rules.kt b/src/main/kotlin/kotlinx/kover/api/Rules.kt deleted file mode 100644 index 18ff50da..00000000 --- a/src/main/kotlin/kotlinx/kover/api/Rules.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.api - -import org.gradle.api.* -import org.gradle.api.tasks.* -import javax.annotation.* - -/** - * Simple verification rule for code coverage. - * Works only with lines counter. - */ -public interface VerificationRule { - /** - * Unique ID of the rule. - */ - @get:Internal - public val id: Int - - /** - * Custom name of the rule. - */ - @get:Input - @get:Nullable - @get:Optional - public var name: String? - - /** - * Added constraints on the values of code coverage metrics. - */ - @get:Input - public val bounds: List - - /** - * Add a constraint on the value of the code coverage metric. - */ - public fun bound(configureBound: Action) -} - -public interface VerificationBound { - /** - * ID of the bound unique in the rule. - */ - @get:Internal - public val id: Int - - /** - * Minimal value to compare with counter value. - */ - @get:Input - @get:Nullable - @get:Optional - public var minValue: Int? - - /** - * Maximal value to compare with counter value. - */ - @get:Input - @get:Nullable - @get:Optional - public var maxValue: Int? - - /** - * Type of lines counter value to compare with minimal and maximal values if them defined. - * Default is [VerificationValueType.COVERED_LINES_PERCENTAGE] - */ - @get:Input - public var valueType: VerificationValueType -} - -/** - * Type of lines counter value to compare with minimal and maximal values if them defined. - */ -public enum class VerificationValueType { - COVERED_LINES_COUNT, - MISSED_LINES_COUNT, - COVERED_LINES_PERCENTAGE -} diff --git a/src/main/kotlin/kotlinx/kover/appliers/KoverMergedApplier.kt b/src/main/kotlin/kotlinx/kover/appliers/KoverMergedApplier.kt new file mode 100644 index 00000000..f4ac654d --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/appliers/KoverMergedApplier.kt @@ -0,0 +1,253 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.appliers + +import kotlinx.kover.api.* +import kotlinx.kover.api.KoverNames.CHECK_TASK_NAME +import kotlinx.kover.api.KoverNames.CONFIGURATION_NAME +import kotlinx.kover.api.KoverNames.MERGED_HTML_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.MERGED_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.MERGED_VERIFY_TASK_NAME +import kotlinx.kover.api.KoverNames.MERGED_XML_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.VERIFICATION_GROUP +import kotlinx.kover.api.KoverPaths.MERGED_HTML_REPORT_DEFAULT_PATH +import kotlinx.kover.api.KoverPaths.MERGED_VERIFICATION_REPORT_DEFAULT_PATH +import kotlinx.kover.api.KoverPaths.MERGED_XML_REPORT_DEFAULT_PATH +import kotlinx.kover.tasks.* +import org.gradle.api.* +import org.gradle.api.file.* +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.testing.* +import org.gradle.configurationcache.extensions.* + +internal fun Project.applyMerged() { + val extension = createMergedExtension() + afterEvaluate(ProcessMergeExtensionAction(extension)) +} + +private fun Project.createMergedExtension(): KoverMergedConfig { + val extension = extensions.create(KoverNames.MERGED_EXTENSION_NAME, KoverMergedConfig::class.java, objects) + extension.isEnabled.set(false) + extension.xmlReport.onCheck.set(false) + extension.xmlReport.reportFile.set(layout.buildDirectory.file(MERGED_XML_REPORT_DEFAULT_PATH)) + extension.xmlReport.classes.set(extension.filters.classes) + extension.htmlReport.onCheck.set(false) + extension.htmlReport.reportDir.set(layout.buildDirectory.dir(MERGED_HTML_REPORT_DEFAULT_PATH)) + extension.htmlReport.classes.set(extension.filters.classes) + extension.verify.onCheck.set(false) + return extension +} + +private class ProcessMergeExtensionAction(private val extension: KoverMergedConfig) : Action { + override fun execute(container: Project) { + // don't create tasks if merge wasn't enabled + if (!extension.isEnabled.get()) { + return + } + + val extensionByProject = container.projectsExtensionsProvider(extension, container.allprojects) + val engineProvider = container.engineProvider(extensionByProject) + val testsProvider = container.instrumentedTasksProvider(extensionByProject) + + val xmlTask = container.createMergedTask( + MERGED_XML_REPORT_TASK_NAME, + extension.xmlReport.classes, + extensionByProject, + engineProvider, + testsProvider, + { e -> e.xmlReport.taskFilters.sourceSets.get() } + ) { + it.reportFile.set(extension.xmlReport.reportFile) + it.description = "Generates code coverage XML report for all enabled test tasks in specified projects." + } + + val htmlTask = container.createMergedTask( + MERGED_HTML_REPORT_TASK_NAME, + extension.htmlReport.classes, + extensionByProject, + engineProvider, + testsProvider, + { e -> e.htmlReport.taskFilters.sourceSets.get() } + ) { + it.reportDir.set(extension.htmlReport.reportDir) + it.description = "Generates code coverage HTML report for all enabled test tasks in specified projects." + } + + val verifyTask = container.createMergedTask( + MERGED_VERIFY_TASK_NAME, + extension.filters.classes, + extensionByProject, + engineProvider, + testsProvider, + { e -> e.filters.sourceSets.get() } + ) { + it.rules.set(extension.verify.rules) + it.resultFile.set(container.layout.buildDirectory.file(MERGED_VERIFICATION_REPORT_DEFAULT_PATH)) + it.description = "Verifies code coverage metrics of specified projects based on specified rules." + } + // TODO `onlyIf` block moved out from config lambda because of bug in Kotlin compiler - it implicitly adds closure on `Project` inside onlyIf's lambda + verifyTask.onlyIf { extension.verify.hasActiveRules() } + + + container.tasks.create(MERGED_REPORT_TASK_NAME) { + it.group = VERIFICATION_GROUP + it.dependsOn(xmlTask) + it.dependsOn(htmlTask) + it.description = "Generates code coverage HTML and XML reports for all enabled test tasks in one project." + } + + container.tasks.configureEach { + if (it.name == CHECK_TASK_NAME) { + it.dependsOn(container.provider { + val tasks = mutableListOf() + if (extension.xmlReport.onCheck.get()) { + tasks += xmlTask + } + if (extension.htmlReport.onCheck.get()) { + tasks += htmlTask + } + if (extension.verify.onCheck.get() && extension.verify.hasActiveRules()) { + // don't add dependency if there is no active verification rules https://github.com/Kotlin/kotlinx-kover/issues/168 + tasks += verifyTask + } + tasks + }) + } + } + } +} + +private inline fun Project.createMergedTask( + taskName: String, + classFilters: Provider, + extensionByProject: Provider>, + engineProvider: Provider, + testsProvider: Provider>, + crossinline filterExtractor: (KoverProjectConfig) -> KoverSourceSetFilters, + crossinline block: (T) -> Unit +): T { + val task = tasks.create(taskName, T::class.java) { + it.files.set(mergedFilesProvider(extensionByProject, filterExtractor)) + it.engine.set(engineProvider) + it.dependsOn(testsProvider) + + it.classFilters.set(classFilters) + it.group = VERIFICATION_GROUP + block(it) + } + // TODO `onlyIf` block moved out from config lambda because of bug in Kotlin compiler - it implicitly adds closure on `Project` inside onlyIf's lambda + // execute task only if there is at least one binary report + task.onlyIf { t -> (t as KoverReportTask).files.get().any { f -> !f.value.binaryReportFiles.isEmpty } } + + return task +} + +private fun Project.projectsExtensionsProvider( + extension: KoverMergedConfig, + allProjects: Iterable +): Provider> { + return provider { + val projects = filterProjects(extension.filters.projects.get(), allProjects) + + val notAppliedProjects = + projects.filter { it.extensions.findByType(KoverProjectConfig::class.java) == null }.map { it.path } + if (notAppliedProjects.isNotEmpty()) { + throw GradleException("Can't create Kover merge tasks: Kover plugin not applied in projects $notAppliedProjects") + } + projects.associateWith { it.extensions.getByType(KoverProjectConfig::class.java) } + } +} + + +private inline fun Project.mergedFilesProvider( + extensionByProject: Provider>, + crossinline filterExtractor: (KoverProjectConfig) -> KoverSourceSetFilters +): Provider> { + return provider { + extensionByProject.get() + .map { (project, extension) -> project.path to project.projectFiles(filterExtractor(extension), extension) } + .associate { it } + } +} + + +private fun filterProjects(filters: KoverProjectsFilters, allProjects: Iterable): List { + if (filters.includes.isEmpty()) { + return allProjects.toList() + } + + val projectsByPath = allProjects.associateBy { p -> p.path } + val pathsByName = allProjects.associate { it.name to mutableListOf() } + allProjects.forEach { pathsByName.getValue(it.name) += it.path } + + return filters.includes.map { + if (it.startsWith(':')) { + projectsByPath[it] + ?: throw GradleException("Kover configuring error: not found project '$it' for merged tasks") + } else { + val paths = pathsByName[it] + ?: throw GradleException("Kover configuring error: not found project '$it' for merged tasks") + if (paths.size > 1) { + throw GradleException("Kover configuring error: ambiguous name of the project '$it' for merged tasks: suitable projects with paths $paths. Consider using fully-qualified name starting with ':'") + } + projectsByPath[paths[0]]!! + } + } +} + + +private fun Project.engineProvider(extensionByProject: Provider>): Provider { + val archiveOperations: ArchiveOperations = this.serviceOf() + // configuration already created in all projects at this moment because merge tasks creates in afterEvaluate step + val config = configurations.getByName(CONFIGURATION_NAME) + // the plugin is always applied to the containing project + val containerEngine = extensions.getByType(KoverProjectConfig::class.java).engine + + val containerPath = path + return provider { + val variants = extensionByProject.get().map { ComparableEngineVariant(it.value.engine.get()) }.toSet() + // check same engine variants are used + if (variants.size > 1) { + throw GradleException("Can't create Kover merge tasks: different coverage engines are used in projects") + } + val variant = variants.first() + if (variant != ComparableEngineVariant(containerEngine.get())) { + throw GradleException("Can't create Kover merge tasks: project engines are different from the engine from the containing project '$containerPath'") + } + engineByVariant(variant, config, archiveOperations) + } +} + + +private fun Project.instrumentedTasksProvider(extensionByProject: Provider>): Provider> { + return provider { extensionByProject.get().flatMap { it.key.instrumentedTasks(it.value) } } +} + + +/** + * Wrapper for [CoverageEngineVariant] to group applied engines. + */ +private class ComparableEngineVariant(origin: CoverageEngineVariant) : CoverageEngineVariant { + override val vendor: CoverageEngineVendor = origin.vendor + override val version: String = origin.version + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ComparableEngineVariant + + if (vendor != other.vendor) return false + if (version != other.version) return false + + return true + } + + override fun hashCode(): Int { + var result = vendor.hashCode() + result = 31 * result + version.hashCode() + return result + } +} diff --git a/src/main/kotlin/kotlinx/kover/appliers/KoverProjectApplier.kt b/src/main/kotlin/kotlinx/kover/appliers/KoverProjectApplier.kt new file mode 100644 index 00000000..1376daa7 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/appliers/KoverProjectApplier.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.appliers + +import kotlinx.kover.api.* +import kotlinx.kover.api.KoverNames.CONFIGURATION_NAME +import kotlinx.kover.api.KoverNames.HTML_REPORT_TASK_NAME +import kotlinx.kover.api.KoverNames.VERIFY_TASK_NAME +import kotlinx.kover.api.KoverNames.XML_REPORT_TASK_NAME +import kotlinx.kover.engines.commons.* +import kotlinx.kover.lookup.* +import kotlinx.kover.tasks.* +import org.gradle.api.* +import org.gradle.api.artifacts.* +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.provider.* +import org.gradle.api.tasks.testing.* +import org.gradle.configurationcache.extensions.* +import java.io.* + +internal fun Project.applyToProject() { + val extension = createProjectExtension() + val config = createEngineConfig(extension.engine) + + val engineProvider = engineProvider(config, extension.engine) + + tasks.withType(Test::class.java).configureEach { t -> + t.applyToTestTask(extension, engineProvider) + } + + val testsProvider = instrumentedTasksProvider(extension) + + val xmlTask = createTask( + XML_REPORT_TASK_NAME, + extension.xmlReport.taskFilters, + extension, + engineProvider, + testsProvider, + ) { + it.reportFile.set(extension.xmlReport.reportFile) + it.description = "Generates code coverage XML report for all enabled test tasks in one project." + } + + val htmlTask = createTask( + HTML_REPORT_TASK_NAME, + extension.htmlReport.taskFilters, + extension, + engineProvider, + testsProvider, + ) { + it.reportDir.set(extension.htmlReport.reportDir) + it.description = "Generates code coverage HTML report for all enabled test tasks in one project." + } + + val verifyTask = createTask( + VERIFY_TASK_NAME, + extension.filters, + extension, + engineProvider, + testsProvider, + ) { + it.rules.set(extension.verify.rules) + it.resultFile.set(layout.buildDirectory.file(KoverPaths.PROJECT_VERIFICATION_REPORT_DEFAULT_PATH)) + it.description = "Verifies code coverage metrics of one project based on specified rules." + } + // TODO `onlyIf` block moved out from config lambda because of bug in Kotlin compiler - it implicitly adds closure on `Project` inside onlyIf's lambda + verifyTask.onlyIf { extension.verify.hasActiveRules() } + + + tasks.create(KoverNames.REPORT_TASK_NAME) { + it.group = KoverNames.VERIFICATION_GROUP + it.dependsOn(xmlTask) + it.dependsOn(htmlTask) + it.description = "Generates code coverage HTML and XML reports for all enabled test tasks in one project." + } + + + tasks.configureEach { + if (it.name == KoverNames.CHECK_TASK_NAME) { + it.dependsOn(provider { + // don't add dependency if Kover is disabled + if (extension.isDisabled.get()) { + return@provider emptyList() + } + + val tasks = mutableListOf() + if (extension.xmlReport.onCheck.get()) { + tasks += xmlTask + } + if (extension.htmlReport.onCheck.get()) { + tasks += htmlTask + } + if (extension.verify.onCheck.get() && extension.verify.hasActiveRules()) { + // don't add dependency if there is no active verification rules https://github.com/Kotlin/kotlinx-kover/issues/168 + tasks += verifyTask + } + tasks + }) + } + } +} + +private inline fun Project.createTask( + taskName: String, + filters: KoverProjectFilters, + extension: KoverProjectConfig, + engineProvider: Provider, + testsProvider: Provider>, + crossinline block: (T) -> Unit +): T { + val task = tasks.create(taskName, T::class.java) { + it.files.put(path, projectFilesProvider(extension, filters.sourceSets)) + it.engine.set(engineProvider) + it.dependsOn(testsProvider) + it.classFilters.set(filters.classes) + it.group = KoverNames.VERIFICATION_GROUP + block(it) + } + + // TODO `onlyIf` block moved out from config lambda because of bug in Kotlin compiler - it implicitly adds closure on `Project` inside onlyIf's lambda + // execute task only if Kover not disabled for project + task.onlyIf { !extension.isDisabled.get() } + // execute task only if there is at least one binary report + task.onlyIf { t -> (t as KoverReportTask).files.get().any { f -> !f.value.binaryReportFiles.isEmpty } } + + return task +} + +private fun Project.createProjectExtension(): KoverProjectConfig { + val extension = extensions.create(KoverNames.PROJECT_EXTENSION_NAME, KoverProjectConfig::class.java, objects) + // default values + + extension.isDisabled.set(false) + extension.engine.set(DefaultIntellijEngine) + extension.xmlReport.reportFile.set(layout.buildDirectory.file(KoverPaths.PROJECT_XML_REPORT_DEFAULT_PATH)) + extension.xmlReport.onCheck.set(false) + extension.xmlReport.taskFilters.classes.set(extension.filters.classes) + extension.xmlReport.taskFilters.sourceSets.set(extension.filters.sourceSets) + extension.htmlReport.reportDir.set(layout.buildDirectory.dir(KoverPaths.PROJECT_HTML_REPORT_DEFAULT_PATH)) + extension.htmlReport.onCheck.set(false) + extension.htmlReport.taskFilters.classes.set(extension.filters.classes) + extension.htmlReport.taskFilters.sourceSets.set(extension.filters.sourceSets) + extension.verify.onCheck.set(true) + + return extension +} + +private fun Project.createEngineConfig(engineVariantProvider: Provider): Configuration { + val config = project.configurations.create(CONFIGURATION_NAME) + config.isVisible = false + config.isTransitive = true + config.description = "Kotlin Kover Plugin configuration for Coverage Engine" + + config.defaultDependencies { default -> + val variant = engineVariantProvider.get() + EngineManager.dependencies(variant).forEach { + default.add(dependencies.create(it)) + } + } + return config +} + +private fun Project.engineProvider( + config: Configuration, + engineVariantProvider: Provider +): Provider { + val archiveOperations: ArchiveOperations = project.serviceOf() + return project.provider { engineByVariant(engineVariantProvider.get(), config, archiveOperations) } +} + +internal fun engineByVariant( + variant: CoverageEngineVariant, + config: Configuration, + archiveOperations: ArchiveOperations +): EngineDetails { + val jarFile = EngineManager.findJarFile(variant, config, archiveOperations) + return EngineDetails(variant, jarFile, config) +} + +internal fun Project.projectFilesProvider( + extension: KoverProjectConfig, + sourceSetFiltersProvider: Provider +): Provider { + return provider { projectFiles(sourceSetFiltersProvider.get(), extension) } +} + +internal fun Project.projectFiles( + filters: KoverSourceSetFilters, + extension: KoverProjectConfig +): ProjectFiles { + val directories = DirsLookup.lookup(project, filters) + val reportFiles = files(binaryReports(extension)) + return ProjectFiles(reportFiles, directories.sources, directories.outputs) +} + +internal fun Project.instrumentedTasks(extension: KoverProjectConfig): List { + return tasks.withType(Test::class.java).asSequence() + // task can be disabled in the project extension + .filterNot { extension.instrumentation.excludeTasks.contains(it.name) } + .filterNot { t -> t.extensions.getByType(KoverTaskExtension::class.java).isDisabled.get() } + .toList() +} + +private fun Project.instrumentedTasksProvider(extension: KoverProjectConfig): Provider> { + return provider { instrumentedTasks(extension) } +} + + +private fun Project.binaryReports(extension: KoverProjectConfig): List { + if (extension.isDisabled.get()) { + return emptyList() + } + + return tasks.withType(Test::class.java).asSequence() + // task can be disabled in the project extension + .filterNot { extension.instrumentation.excludeTasks.contains(it.name) } + .map { t -> t.extensions.getByType(KoverTaskExtension::class.java) } + // process binary report only from tasks with enabled cover + .filterNot { e -> e.isDisabled.get() } + .map { e -> e.reportFile.get().asFile } + // process binary report only from tasks with sources + .filter { f -> f.exists() } + .toList() +} + + +internal fun KoverVerifyConfig.hasActiveRules(): Boolean { + return rules.get().any { rule -> rule.isEnabled } +} diff --git a/src/main/kotlin/kotlinx/kover/appliers/KoverTaskApplier.kt b/src/main/kotlin/kotlinx/kover/appliers/KoverTaskApplier.kt new file mode 100644 index 00000000..c2016635 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/appliers/KoverTaskApplier.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.appliers + +import kotlinx.kover.api.* +import kotlinx.kover.engines.commons.* +import kotlinx.kover.tasks.* +import org.gradle.api.* +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.* +import org.gradle.api.tasks.* +import org.gradle.api.tasks.testing.* +import org.gradle.configurationcache.extensions.* +import org.gradle.process.* +import java.io.* + + +internal fun Test.applyToTestTask( + projectExtension: KoverProjectConfig, + engineProvider: Provider +) { + val extension = createTaskExtension(projectExtension) + + val disabledProvider = project.provider { + (projectExtension.isDisabled.get() || extension.isDisabled.get() || + projectExtension.instrumentation.excludeTasks.contains(name)) + } + + jvmArgumentProviders.add(CoverageArgumentProvider(this, engineProvider, disabledProvider, extension)) + + val sourceErrorProvider = project.provider { + File(extension.reportFile.get().asFile.parentFile, "coverage-error.log") + } + val targetErrorProvider = project.layout.buildDirectory.file("kover/errors/$name.log").map { it.asFile } + + doFirst(BinaryReportCleanupAction(disabledProvider, extension.reportFile, engineProvider.map { e -> e.variant })) + doLast(MoveIntellijErrorLogAction(sourceErrorProvider, targetErrorProvider)) +} + +private fun Task.createTaskExtension(projectExtension: KoverProjectConfig): KoverTaskExtension { + val taskExtension = + extensions.create(KoverNames.TASK_EXTENSION_NAME, KoverTaskExtension::class.java, project.objects) + + taskExtension.isDisabled.set(false) + taskExtension.reportFile.set(project.provider { + val engine = projectExtension.engine.get() + val suffix = if (engine.vendor == CoverageEngineVendor.INTELLIJ) ".ic" else ".exec" + project.layout.buildDirectory.get().file("kover/$name$suffix") + }) + + return taskExtension +} + + +private class CoverageArgumentProvider( + private val task: Task, + @get:Nested val engineProvider: Provider, + @get:Input val disabledProvider: Provider, + @get:Nested val taskExtension: KoverTaskExtension +) : CommandLineArgumentProvider, Named { + @Internal + override fun getName(): String { + return "koverArgumentsProvider" + } + + override fun asArguments(): MutableIterable { + if (disabledProvider.get()) { + return mutableListOf() + } + + val reportFile = taskExtension.reportFile.get().asFile + val classFilters = KoverClassFilters() + + classFilters.includes += taskExtension.includes.get() + classFilters.excludes += taskExtension.excludes.get() + /* + The instrumentation of android classes often causes errors when using third-party + frameworks (see https://github.com/Kotlin/kotlinx-kover/issues/89). + + Because android classes are not part of the project, in any case they do not get into the report, + and they can be excluded from instrumentation. + + FIXME Remove this code if the IntelliJ Agent stops changing project classes during instrumentation + */ + classFilters.excludes += "android.*" + classFilters.excludes += "com.android.*" + + return EngineManager.buildAgentArgs(engineProvider.get(), task, reportFile, classFilters) + } +} + + +/* + To support parallel tests, both Coverage Engines work in append to data file mode. + For this reason, before starting the tests, it is necessary to clear the file from the results of previous runs. +*/ +private class BinaryReportCleanupAction( + private val disabledProvider: Provider, + private val reportFileProvider: Provider, + private val engineVariantProvider: Provider, +) : Action { + override fun execute(task: Task) { + if (disabledProvider.get()) { + return + } + val file = reportFileProvider.get().asFile + // always delete previous data file + file.delete() + if (engineVariantProvider.get().vendor == CoverageEngineVendor.INTELLIJ) { + // IntelliJ engine expected empty file for parallel test execution. + // Since it is impossible to know in advance whether the tests will be run in parallel, we always create an empty file. + file.createNewFile() + } + } +} + +private class MoveIntellijErrorLogAction( + private val sourceFile: Provider, + private val targetFile: Provider +) : Action { + override fun execute(task: Task) { + val origin = sourceFile.get() + if (origin.exists() && origin.isFile) { + origin.copyTo(targetFile.get(), true) + origin.delete() + } + } +} diff --git a/src/main/kotlin/kotlinx/kover/engines/commons/AgentsFactory.kt b/src/main/kotlin/kotlinx/kover/engines/commons/AgentsFactory.kt deleted file mode 100644 index 2bf4ccac..00000000 --- a/src/main/kotlin/kotlinx/kover/engines/commons/AgentsFactory.kt +++ /dev/null @@ -1,19 +0,0 @@ -package kotlinx.kover.engines.commons - -import kotlinx.kover.api.* -import kotlinx.kover.engines.intellij.* -import kotlinx.kover.engines.jacoco.* -import org.gradle.api.* - -internal object AgentsFactory { - fun createAgents(project: Project, koverExtension: KoverExtension): Map { - return mapOf( - CoverageEngine.INTELLIJ to project.createIntellijAgent(koverExtension), - CoverageEngine.JACOCO to project.createJacocoAgent(koverExtension), - ) - } -} - -internal fun Map.getFor(engine: CoverageEngine): CoverageAgent { - return this[engine] ?: throw GradleException("Coverage agent for Coverage Engine '$engine' not found") -} diff --git a/src/main/kotlin/kotlinx/kover/engines/commons/CoverageAgent.kt b/src/main/kotlin/kotlinx/kover/engines/commons/CoverageAgent.kt deleted file mode 100644 index 77197f25..00000000 --- a/src/main/kotlin/kotlinx/kover/engines/commons/CoverageAgent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package kotlinx.kover.engines.commons - -import kotlinx.kover.api.* -import org.gradle.api.* -import org.gradle.api.file.* - -internal interface CoverageAgent { - val engine: CoverageEngine - val classpath: FileCollection - fun buildCommandLineArgs(task: Task, extension: KoverTaskExtension): MutableList -} diff --git a/src/main/kotlin/kotlinx/kover/engines/commons/EngineManager.kt b/src/main/kotlin/kotlinx/kover/engines/commons/EngineManager.kt new file mode 100644 index 00000000..bf70fd68 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/engines/commons/EngineManager.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.engines.commons + +import kotlinx.kover.api.* +import kotlinx.kover.engines.intellij.* +import kotlinx.kover.engines.jacoco.* +import kotlinx.kover.tasks.* +import org.gradle.api.* +import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.* +import org.gradle.process.* +import java.io.* + + +@Suppress("UNUSED_PARAMETER") +internal object EngineManager { + fun buildAgentArgs( + details: EngineDetails, + task: Task, + reportFile: File, + classFilters: KoverClassFilters + ): MutableList { + return if (details.variant.vendor == CoverageEngineVendor.INTELLIJ) { + task.buildIntellijAgentJvmArgs(details.jarFile, reportFile, classFilters) + } else { + reportFile.parentFile.mkdirs() + task.buildJacocoAgentJvmArgs(details.jarFile, reportFile, classFilters) + } + } + + fun report( + details: EngineDetails, + task: Task, + exec: ExecOperations, + projectFiles: Map, + classFilters: KoverClassFilters, + xmlFile: File?, + htmlDir: File? + ) { + if (details.variant.vendor == CoverageEngineVendor.INTELLIJ) { + task.intellijReport(exec, projectFiles, classFilters, xmlFile, htmlDir, details.classpath) + } else { + task.jacocoReport(projectFiles, xmlFile, htmlDir, details.classpath) + } + } + + fun verify( + details: EngineDetails, + task: Task, + exec: ExecOperations, + projectFiles: Map, + classFilters: KoverClassFilters, + rules: List, + ): String? { + return if (details.variant.vendor == CoverageEngineVendor.INTELLIJ) { + task.intellijVerification(exec, projectFiles, classFilters, rules, details.classpath) + } else { + task.jacocoVerification(projectFiles, rules, details.classpath) + } + } + + + fun findJarFile(variant: CoverageEngineVariant, config: Configuration, archiveOperations: ArchiveOperations): File { + return if (variant.vendor == CoverageEngineVendor.INTELLIJ) { + config.fileCollection { it.name == "intellij-coverage-agent" }.singleFile + } else { + val fatJar = config.fileCollection { it.name == "org.jacoco.agent" }.singleFile + archiveOperations.zipTree(fatJar).filter { it.name == "jacocoagent.jar" }.singleFile + } + } + + fun dependencies(engineVariant: CoverageEngineVariant): List { + return if (engineVariant.vendor == CoverageEngineVendor.INTELLIJ) { + getIntellijDependencies(engineVariant.version) + } else { + getJacocoDependencies(engineVariant.version) + } + } +} diff --git a/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt b/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt index ee7d7749..d6566599 100644 --- a/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt +++ b/src/main/kotlin/kotlinx/kover/engines/commons/Reports.kt @@ -1,14 +1,28 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + package kotlinx.kover.engines.commons -import java.io.* +import kotlinx.kover.api.* +import java.math.BigDecimal + + +internal class ReportVerificationRule( + val id: Int, + val name: String?, + val target: VerificationTarget, + val filters: KoverClassFilters?, + val bounds: List +) -internal class Report( - val files: List, - val projects: List, - val includes: List = emptyList(), - val excludes: List = emptyList() +internal class ReportVerificationBound( + val id: Int, + val minValue: BigDecimal?, + val maxValue: BigDecimal?, + val counter: CounterType, + val valueType: VerificationValueType ) -internal class ProjectInfo(val sources: Iterable, val outputs: Iterable) private val regexMetacharactersSet = "<([{\\^-=$!|]})+.>".toSet() diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt index 5ade3b98..9fdb6824 100644 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt @@ -1,91 +1,52 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.kover.engines.intellij import kotlinx.kover.api.* import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.CoverageAgent -import org.gradle.api.* -import org.gradle.api.artifacts.* -import org.gradle.api.file.* -import org.gradle.api.provider.Provider +import org.gradle.api.Task import java.io.* - -internal fun Project.createIntellijAgent(koverExtension: KoverExtension): CoverageAgent { - val intellijConfig = createIntellijConfig(koverExtension) - val jarProvider = provider { intellijConfig.fileCollection { it.name == "intellij-coverage-agent" }.singleFile } - return IntellijAgent(intellijConfig, jarProvider) +private const val trackingPerTest = false // a flag to enable tracking per test coverage +private const val calculateForUnloadedClasses = false // a flag to calculate coverage for unloaded classes +private const val appendToDataFile = true // a flag to use data file as initial coverage +private const val samplingMode = false //a flag to run coverage in sampling mode or in tracing mode otherwise + +internal fun Task.buildIntellijAgentJvmArgs(jarFile: File, reportFile: File, classFilters: KoverClassFilters): MutableList { + val argsFile = File(temporaryDir, "intellijagent.args") + argsFile.writeAgentArgs(reportFile, classFilters) + + return mutableListOf( + "-javaagent:${jarFile.canonicalPath}=${argsFile.canonicalPath}", + "-Didea.new.sampling.coverage=true", + "-Didea.new.tracing.coverage=true", + "-Didea.coverage.log.level=error", + "-Dcoverage.ignore.private.constructor.util.class=true" + ) } -private class IntellijAgent(override val classpath: FileCollection, private val jarProvider: Provider): CoverageAgent { - private val trackingPerTest = false // a flag to enable tracking per test coverage - private val calculateForUnloadedClasses = false // a flag to calculate coverage for unloaded classes - private val appendToDataFile = true // a flag to use data file as initial coverage - private val samplingMode = false //a flag to run coverage in sampling mode or in tracing mode otherwise - - override val engine: CoverageEngine = CoverageEngine.INTELLIJ - - override fun buildCommandLineArgs(task: Task, extension: KoverTaskExtension): MutableList { - val argsFile = File(task.temporaryDir, "intellijagent.args") - argsFile.writeArgsToFile(extension) - - return mutableListOf( - "-javaagent:${jarProvider.get().canonicalPath}=${argsFile.canonicalPath}", - "-Didea.new.sampling.coverage=true", - "-Didea.new.tracing.coverage=true", - "-Didea.coverage.log.level=error", - "-Dcoverage.ignore.private.constructor.util.class=true" - ) - } - - private fun File.writeArgsToFile(extension: KoverTaskExtension) { - val binary = extension.binaryReportFile.get() - binary.parentFile.mkdirs() - val binaryPath = binary.canonicalPath - - printWriter().use { pw -> - pw.appendLine(binaryPath) - pw.appendLine(trackingPerTest.toString()) - pw.appendLine(calculateForUnloadedClasses.toString()) - pw.appendLine(appendToDataFile.toString()) - pw.appendLine(samplingMode.toString()) - extension.includes.forEach { i -> - pw.appendLine(i.wildcardsToRegex()) - } - - if (extension.excludes.isNotEmpty()) { - pw.appendLine("-exclude") - } - - extension.excludes.forEach { e -> - pw.appendLine(e.wildcardsToRegex()) - } +private fun File.writeAgentArgs(reportFile: File, classFilters: KoverClassFilters) { + reportFile.parentFile.mkdirs() + val binaryPath = reportFile.canonicalPath + + printWriter().use { pw -> + pw.appendLine(binaryPath) + pw.appendLine(trackingPerTest.toString()) + pw.appendLine(calculateForUnloadedClasses.toString()) + pw.appendLine(appendToDataFile.toString()) + pw.appendLine(samplingMode.toString()) + classFilters.includes.forEach { i -> + pw.appendLine(i.wildcardsToRegex()) } - } -} -private fun Project.createIntellijConfig(koverExtension: KoverExtension): Configuration { - val config = project.configurations.create("IntellijKoverConfig") - config.isVisible = false - config.isTransitive = true - config.description = "Kotlin Kover Plugin configuration for IntelliJ agent and reporter" - - config.defaultDependencies { dependencies -> - val agentVersion = koverExtension.intellijEngineVersion.get() - IntellijEngineVersion.parseOrNull(agentVersion)?.let { - if (it < minimalIntellijVersion) throw GradleException("IntelliJ engine version $it is too low, minimal version is $minimalIntellijVersion") + if (classFilters.excludes.isNotEmpty()) { + pw.appendLine("-exclude") } - dependencies.add( - this.dependencies.create("org.jetbrains.intellij.deps:intellij-coverage-agent:$agentVersion") - ) - - dependencies.add( - this.dependencies.create("org.jetbrains.intellij.deps:intellij-coverage-reporter:$agentVersion") - ) + classFilters.excludes.forEach { e -> + pw.appendLine(e.wildcardsToRegex()) + } } - return config } diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijEngine.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijEngine.kt new file mode 100644 index 00000000..e36ac608 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijEngine.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.kover.engines.intellij + +import kotlinx.kover.api.KoverVersions.MINIMAL_INTELLIJ_VERSION +import org.gradle.api.* + +internal fun getIntellijDependencies(engineVersion: String): List { + if (engineVersion.isLowVersion(MINIMAL_INTELLIJ_VERSION)) { + throw GradleException("IntelliJ engine version $engineVersion is too low, minimal version is $MINIMAL_INTELLIJ_VERSION") + } + return listOf( + "org.jetbrains.intellij.deps:intellij-coverage-agent:$engineVersion", + "org.jetbrains.intellij.deps:intellij-coverage-reporter:$engineVersion" + ) +} + + +private fun String.isLowVersion(minimalVersion: String): Boolean { + val customParts = this.split(".") + if (customParts.size != 3) { + throw GradleException("Invalid custom IntelliJ Coverage Engine version '$this'. Expected 'x.x.xxx'") + } + + val minimalParts = minimalVersion.split(".") + if (minimalParts.size != 3) { + throw GradleException("Invalid minimal IntelliJ Coverage Engine version '$this'") + } + + for (i in 0..2) { + if (customParts[i] > minimalParts[i]) { + return false + } else if (customParts[i] < minimalParts[i]) { + return true + } + } + + return false +} diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt index d5978aee..f064e639 100644 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijReports.kt @@ -4,9 +4,10 @@ package kotlinx.kover.engines.intellij +import kotlinx.kover.api.* import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.Report import kotlinx.kover.json.* +import kotlinx.kover.tasks.* import org.gradle.api.* import org.gradle.api.file.* import org.gradle.process.ExecOperations @@ -14,7 +15,8 @@ import java.io.* internal fun Task.intellijReport( exec: ExecOperations, - report: Report, + projectFiles: Map, + filters: KoverClassFilters, xmlFile: File?, htmlDir: File?, classpath: FileCollection @@ -28,7 +30,7 @@ internal fun Task.intellijReport( } val argsFile = File(temporaryDir, "intellijreport.json") - argsFile.writeReportsJson(report, xmlFile, htmlDir) + argsFile.writeReportsJson(projectFiles, filters, xmlFile, htmlDir) exec.javaexec { e -> e.mainClass.set("com.intellij.rt.coverage.report.Main") @@ -77,19 +79,20 @@ JSON example: ``` */ private fun File.writeReportsJson( - report: Report, + projectFiles: Map, + classFilters: KoverClassFilters, xmlFile: File?, htmlDir: File? ) { writeJsonObject(mutableMapOf( - "reports" to report.files.map { mapOf("ic" to it) }, - "modules" to report.projects.map { mapOf("sources" to it.sources, "output" to it.outputs) }, + "reports" to projectFiles.flatMap { it.value.binaryReportFiles }.map { mapOf("ic" to it) }, + "modules" to projectFiles.map { mapOf("sources" to it.value.sources, "output" to it.value.outputs) }, ).also { - if (report.includes.isNotEmpty()) { - it["include"] = mapOf("classes" to report.includes.map { c -> c.wildcardsToRegex() }) + if (classFilters.includes.isNotEmpty()) { + it["include"] = mapOf("classes" to classFilters.includes.map { c -> c.wildcardsToRegex() }) } - if (report.excludes.isNotEmpty()) { - it["exclude"] = mapOf("classes" to report.excludes.map { c -> c.wildcardsToRegex() }) + if (classFilters.excludes.isNotEmpty()) { + it["exclude"] = mapOf("classes" to classFilters.excludes.map { c -> c.wildcardsToRegex() }) } xmlFile?.also { f -> it["xml"] = f diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt index 5b90ddc6..38f6b1fe 100644 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijVerification.kt @@ -5,10 +5,10 @@ package kotlinx.kover.engines.intellij import kotlinx.kover.api.* -import kotlinx.kover.api.VerificationValueType.COVERED_LINES_PERCENTAGE +import kotlinx.kover.api.VerificationValueType.* import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.Report import kotlinx.kover.json.* +import kotlinx.kover.tasks.* import org.gradle.api.* import org.gradle.api.file.* import org.gradle.process.ExecOperations @@ -19,13 +19,14 @@ import java.math.RoundingMode @Suppress("UNUSED_PARAMETER") internal fun Task.intellijVerification( exec: ExecOperations, - report: Report, - rules: Iterable, + projectFiles: Map, + classFilters: KoverClassFilters, + rules: List, classpath: FileCollection -) { +): String? { val aggRequest = File(temporaryDir, "agg-request.json") - val aggFile = File(temporaryDir, "aggregated.ic") - aggRequest.writeAggVerifyJson(report, aggFile) + val groupedRules = groupRules(classFilters, rules) + aggRequest.writeAggJson(projectFiles, groupedRules) exec.javaexec { e -> e.mainClass.set("com.intellij.rt.coverage.aggregate.Main") e.classpath = classpath @@ -33,19 +34,46 @@ internal fun Task.intellijVerification( } val verifyRequest = File(temporaryDir, "verify-request.json") - val resultFile = File(temporaryDir, "verify-result.json") - verifyRequest.writeVerifyJson(aggFile, resultFile, rules) + val verifyResponseFile = File(temporaryDir, "verify-result.json") + verifyRequest.writeVerifyJson(groupedRules, verifyResponseFile) exec.javaexec { e -> e.mainClass.set("com.intellij.rt.coverage.verify.Main") e.classpath = classpath e.args = mutableListOf(verifyRequest.canonicalPath) } - val violations = resultFile.readJsonObject() - if (violations.isNotEmpty()) { + val violations = verifyResponseFile.readJsonObject() + return if (violations.isNotEmpty()) { val result = processViolationsModel(violations) raiseViolations(result, rules) + } else { + null + } +} + +private data class RulesGroup( + val aggFile: File, + val filters: KoverClassFilters, + val rules: List +) + +private fun Task.groupRules( + commonClassFilters: KoverClassFilters, + allRules: List +): List { + val result = mutableListOf() + val commonAggFile = File(temporaryDir, "aggregated-common.ic") + val commonRules = mutableListOf() + result += RulesGroup(commonAggFile, commonClassFilters, commonRules) + + allRules.forEach { + if (it.filters == null) { + commonRules += it + } else { + result += RulesGroup(File(temporaryDir, "aggregated-${result.size}.ic"), it.filters, listOf(it)) + } } + return result } /* @@ -68,24 +96,28 @@ internal fun Task.intellijVerification( } } */ -private fun File.writeAggVerifyJson( - report: Report, - aggReport: File +private fun File.writeAggJson( + projectFiles: Map, + groups: List ) { writeJsonObject(mapOf( - "reports" to report.files.map { mapOf("ic" to it) }, - "modules" to report.projects.map { mapOf("sources" to it.sources, "output" to it.outputs) }, - "result" to listOf(mapOf( - "aggregatedReportFile" to aggReport, - "filters" to mutableMapOf().also { - if (report.includes.isNotEmpty()) { - it["include"] = mapOf("classes" to report.includes.map { c -> c.wildcardsToRegex() }) - } - if (report.excludes.isNotEmpty()) { - it["exclude"] = mapOf("classes" to report.excludes.map { c -> c.wildcardsToRegex() }) + "reports" to projectFiles.flatMap { it.value.binaryReportFiles }.map { mapOf("ic" to it) }, + "modules" to projectFiles.map { mapOf("sources" to it.value.sources, "output" to it.value.outputs) }, + "result" to groups.map { group -> + mapOf( + "aggregatedReportFile" to group.aggFile, + "filters" to mutableMapOf().also { + if (group.filters.includes.isNotEmpty()) { + it["include"] = + mapOf("classes" to group.filters.includes.map { c -> c.wildcardsToRegex() }) + } + if (group.filters.excludes.isNotEmpty()) { + it["exclude"] = + mapOf("classes" to group.filters.excludes.map { c -> c.wildcardsToRegex() }) + } } - } - )) + ) + } )) } @@ -110,51 +142,70 @@ private fun File.writeAggVerifyJson( } */ private fun File.writeVerifyJson( - aggReport: File, + groups: List, result: File, - rules: Iterable ) { - writeJsonObject(mapOf( - "resultFile" to result, - "rules" to rules.map { rule -> - mapOf( + val rulesArray = mutableListOf>() + + groups.forEach { group -> + group.rules.forEach { rule -> + rulesArray += mapOf( "id" to rule.id, - "aggregatedReportFile" to aggReport, - "targetType" to "ALL", + "aggregatedReportFile" to group.aggFile, + "targetType" to rule.targetToReporter(), "bounds" to rule.bounds.map { b -> mutableMapOf( "id" to b.id, - "counter" to "LINE", - "valueType" to b.valueTypeConverted(), + "counter" to b.counterToReporter(), + "valueType" to b.valueTypeToReporter(), ).also { val minValue = b.minValue val maxValue = b.maxValue if (minValue != null) { - it["min"] = b.valueAligned(minValue) + it["min"] = b.valueToReporter(minValue) } if (maxValue != null) { - it["max"] = b.valueAligned(maxValue) + it["max"] = b.valueToReporter(maxValue) } } } ) } - )) + } + + writeJsonObject(mapOf("resultFile" to result, "rules" to rulesArray)) +} + +private fun ReportVerificationRule.targetToReporter(): String { + return when (target) { + VerificationTarget.ALL -> "ALL" + VerificationTarget.CLASS -> "CLASS" + VerificationTarget.PACKAGE -> "PACKAGE" + } +} + +private fun ReportVerificationBound.counterToReporter(): String { + return when (counter) { + CounterType.LINE -> "LINE" + CounterType.INSTRUCTION -> "INSTRUCTION" + CounterType.BRANCH -> "BRANCH" + } } -private fun VerificationBound.valueTypeConverted(): String { +private fun ReportVerificationBound.valueTypeToReporter(): String { return when (valueType) { - VerificationValueType.COVERED_LINES_COUNT -> "COVERED" - VerificationValueType.MISSED_LINES_COUNT -> "MISSED" - COVERED_LINES_PERCENTAGE -> "COVERED_RATE" + COVERED_COUNT -> "COVERED" + MISSED_COUNT -> "MISSED" + COVERED_PERCENTAGE -> "COVERED_RATE" + MISSED_PERCENTAGE -> "MISSED_RATE" } } -private fun VerificationBound.valueAligned(value: Int): BigDecimal { - return if (valueType == COVERED_LINES_PERCENTAGE) { - value.toBigDecimal().divide(ONE_HUNDRED, 6, RoundingMode.HALF_UP) +private fun ReportVerificationBound.valueToReporter(value: BigDecimal): BigDecimal { + return if (valueType == COVERED_PERCENTAGE || valueType == MISSED_PERCENTAGE) { + value.divide(ONE_HUNDRED, 6, RoundingMode.HALF_UP) } else { - value.toBigDecimal() + value } } @@ -180,31 +231,33 @@ private fun processViolationsModel(violations: Map): ViolationsResu rules[ruleId.toInt()] = RuleViolation(bounds) } } catch (e: Throwable) { - throw GradleException("Error occurred while parsing verifier result", e) + throw GradleException("Error occurred while parsing verifier result", e) } return ViolationsResult(rules) } -private fun raiseViolations(result: ViolationsResult, rules: Iterable) { +private fun raiseViolations(result: ViolationsResult, rules: Iterable): String { val messageBuilder = StringBuilder() val rulesMap = rules.associateBy { r -> r.id } result.ruleViolations.forEach { (ruleId, rv) -> - val rule = rulesMap[ruleId] ?: throw Exception("") + val rule = rulesMap[ruleId] + ?: throw Exception("Error occurred while parsing verification error: unmapped rule with ID $ruleId") val ruleName = if (rule.name != null) " '${rule.name}' " else " " val boundsMap = rule.bounds.associateBy { b -> b.id } val boundMessages = rv.boundViolations.mapNotNull { (boundId, v) -> - val bound = boundsMap[boundId] ?: throw Exception("") + val bound = boundsMap[boundId] + ?: throw Exception("Error occurred while parsing verification error: unmapped bound with ID $boundId") val minViolation = v.min["all"] val maxViolation = v.max["all"] if (minViolation != null) { - "${bound.readableValueType} is ${minViolation.toRateIfNeeded(bound)}, but expected minimum is ${bound.minValue}" + "${bound.readableValueType} is ${minViolation.fromRateIfNeeded(bound)}, but expected minimum is ${bound.minValue}" } else if (maxViolation != null) { - "${bound.readableValueType} is ${maxViolation.toRateIfNeeded(bound)}, but expected maximum is ${bound.maxValue}" + "${bound.readableValueType} is ${maxViolation.fromRateIfNeeded(bound)}, but expected maximum is ${bound.maxValue}" } else { null } @@ -219,22 +272,23 @@ private fun raiseViolations(result: ViolationsResult, rules: Iterable "covered lines count" - VerificationValueType.MISSED_LINES_COUNT -> "missed lines count" - COVERED_LINES_PERCENTAGE -> "covered lines percentage" + COVERED_COUNT -> "covered lines count" + MISSED_COUNT -> "missed lines count" + COVERED_PERCENTAGE -> "covered lines percentage" + MISSED_PERCENTAGE -> "missed lines percentage" } diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/Versions.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/Versions.kt deleted file mode 100644 index 320ee50d..00000000 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/Versions.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.engines.intellij - -val minimalIntellijVersion = IntellijEngineVersion(1, 0, 668) -val defaultIntellijVersion = minimalIntellijVersion - -data class IntellijEngineVersion(val major: Int, val minor: Int, val build: Int): Comparable { - override fun compareTo(other: IntellijEngineVersion): Int { - var compared = major.compareTo(other.major) - if (compared != 0) { - return compared - } - compared = minor.compareTo(other.minor) - if (compared != 0) { - return compared - } - return build.compareTo(other.build) - } - - override fun toString(): String { - return "$major.$minor.$build" - } - - companion object { - fun parseOrNull(string: String): IntellijEngineVersion? { - val parts = string.split('.') - if (parts.size != 3) return null - val major = parts[0].toIntOrNull() ?: return null - val minor = parts[1].toIntOrNull() ?: return null - val build = parts[2].toIntOrNull() ?: return null - - return IntellijEngineVersion(major, minor, build) - } - } -} - - diff --git a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt index c6d18b74..c2d40a3d 100644 --- a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt +++ b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt @@ -1,73 +1,24 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.kover.engines.jacoco import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.CoverageAgent -import org.gradle.api.* -import org.gradle.api.artifacts.* -import org.gradle.api.file.* -import org.gradle.api.provider.* -import org.gradle.configurationcache.extensions.* +import org.gradle.api.Task import java.io.* -internal fun Project.createJacocoAgent(koverExtension: KoverExtension): CoverageAgent { - val jacocoConfig = createJacocoConfig(koverExtension) - val archiveOperations: ArchiveOperations = serviceOf() - val jarProvider = provider { - val fatJar = jacocoConfig.fileCollection { it.name == "org.jacoco.agent" }.singleFile - archiveOperations.zipTree(fatJar).filter { it.name == "jacocoagent.jar" }.singleFile - } - return JacocoAgent(jacocoConfig, jarProvider) -} - -private class JacocoAgent( - override val classpath: FileCollection, - private val jarProvider: Provider -) : CoverageAgent { - override val engine: CoverageEngine = CoverageEngine.JACOCO - - override fun buildCommandLineArgs(task: Task, extension: KoverTaskExtension): MutableList { - return mutableListOf("-javaagent:${jarProvider.get().canonicalPath}=${agentArgs(extension)}") - } - - private fun agentArgs(extension: KoverTaskExtension): String { - val binary = extension.binaryReportFile.get() - binary.parentFile.mkdirs() +internal fun Task.buildJacocoAgentJvmArgs(jarFile: File, reportFile: File, classFilters: KoverClassFilters): MutableList { + val agentArgs = listOfNotNull( + "destfile=${reportFile.canonicalPath},append=true,inclnolocationclasses=false,dumponexit=true,output=file,jmx=false", + classFilters.includes.joinToFilterString("includes"), + classFilters.excludes.joinToFilterString("excludes") + ).joinToString(",") - return listOfNotNull( - "destfile=${binary.canonicalPath}", - "append=true", - "inclnolocationclasses=false", - "dumponexit=true", - "output=file", - "jmx=false", - extension.includes.joinToFilterString("includes"), - extension.excludes.joinToFilterString("excludes") - ).joinToString(",") - } + return mutableListOf("-javaagent:${jarFile.canonicalPath}=$agentArgs") } private fun List.joinToFilterString(name: String): String? { if (isEmpty()) return null return name + "=" + joinToString(":") } - -private fun Project.createJacocoConfig(koverExtension: KoverExtension): Configuration { - val config = project.configurations.create("JacocoKoverConfig") - config.isVisible = false - config.isTransitive = true - config.description = "Kotlin Kover Plugin configuration for JaCoCo agent and reporter" - - config.defaultDependencies { dependencies -> - dependencies.add( - this.dependencies.create("org.jacoco:org.jacoco.agent:${koverExtension.jacocoEngineVersion.get()}") - ) - dependencies.add( - this.dependencies.create("org.jacoco:org.jacoco.ant:${koverExtension.jacocoEngineVersion.get()}") - ) - } - return config -} diff --git a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoEngine.kt b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoEngine.kt new file mode 100644 index 00000000..0942c846 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoEngine.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.kover.engines.jacoco + +internal fun getJacocoDependencies(engineVersion: String): List { + return listOf("org.jacoco:org.jacoco.agent:$engineVersion", "org.jacoco:org.jacoco.ant:$engineVersion") +} + + diff --git a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt index 9cdb07ee..655f339a 100644 --- a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt +++ b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoReports.kt @@ -1,14 +1,14 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.kover.engines.jacoco import groovy.lang.* import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.* +import kotlinx.kover.engines.commons.ReportVerificationRule import kotlinx.kover.engines.commons.ONE_HUNDRED -import kotlinx.kover.engines.commons.Report +import kotlinx.kover.tasks.* import org.gradle.api.* import org.gradle.api.file.* import org.gradle.internal.reflect.* @@ -16,51 +16,13 @@ import java.io.* import java.math.* import java.util.* -private fun Task.callJacocoAntReportTask( - report: Report, - classpath: FileCollection, - block: GroovyObject.() -> Unit -) { - val builder = ant - builder.invokeMethod( - "taskdef", - mapOf( - "name" to "jacocoReport", - "classname" to "org.jacoco.ant.ReportTask", - "classpath" to classpath.asPath - ) - ) - - val sources: MutableList = mutableListOf() - val outputs: MutableList = mutableListOf() - report.projects.forEach { projectInfo -> - sources.addAll(projectInfo.sources) - outputs.addAll(projectInfo.outputs) - } - - builder.invokeWithBody("jacocoReport") { - invokeWithBody("executiondata") { - project.files(report.files).addToAntBuilder(this, "resources") - } - invokeWithBody("structure", mapOf("name" to project.path)) { - invokeWithBody("sourcefiles") { - project.files(sources).addToAntBuilder(this, "resources") - } - invokeWithBody("classfiles") { - project.files(outputs).addToAntBuilder(this, "resources") - } - } - block() - } -} - internal fun Task.jacocoReport( - report: Report, + projectFiles: Map, xmlFile: File?, htmlDir: File?, classpath: FileCollection ) { - callJacocoAntReportTask(report, classpath) { + callJacocoAntReportTask(projectFiles, classpath) { if (xmlFile != null) { xmlFile.parentFile.mkdirs() invokeMethod("xml", mapOf("destfile" to xmlFile)) @@ -74,30 +36,35 @@ internal fun Task.jacocoReport( internal fun Task.jacocoVerification( - report: Report, - rules: Iterable, + projectFiles: Map, + rules: List, classpath: FileCollection -) { - callJacocoAntReportTask(report, classpath) { +): String? { + callJacocoAntReportTask(projectFiles, classpath) { invokeWithBody("check", mapOf("failonviolation" to "false", "violationsproperty" to "jacocoErrors")) { rules.forEach { invokeWithBody("rule", mapOf("element" to "BUNDLE")) { it.bounds.forEach { b -> val limitArgs = mutableMapOf("counter" to "LINE") - var min: BigDecimal? = b.minValue?.toBigDecimal() - var max: BigDecimal? = b.maxValue?.toBigDecimal() + var min: BigDecimal? = b.minValue + var max: BigDecimal? = b.maxValue when (b.valueType) { - VerificationValueType.COVERED_LINES_COUNT -> { + VerificationValueType.COVERED_COUNT -> { limitArgs["value"] = "COVEREDCOUNT" } - VerificationValueType.MISSED_LINES_COUNT -> { + VerificationValueType.MISSED_COUNT -> { limitArgs["value"] = "MISSEDCOUNT" } - VerificationValueType.COVERED_LINES_PERCENTAGE -> { + VerificationValueType.COVERED_PERCENTAGE -> { limitArgs["value"] = "COVEREDRATIO" min = min?.divide(ONE_HUNDRED) max = max?.divide(ONE_HUNDRED) } + VerificationValueType.MISSED_PERCENTAGE -> { + limitArgs["value"] = "MISSEDRATIO" + min = min?.divide(ONE_HUNDRED) + max = max?.divide(ONE_HUNDRED) + } } if (min != null) { @@ -114,7 +81,47 @@ internal fun Task.jacocoVerification( } } - ant.violations?.let { throw GradleException(it) } + return ant.violations +} + +private fun Task.callJacocoAntReportTask( + projectFiles: Map, + classpath: FileCollection, + block: GroovyObject.() -> Unit +) { + val builder = ant + builder.invokeMethod( + "taskdef", + mapOf( + "name" to "jacocoReport", + "classname" to "org.jacoco.ant.ReportTask", + "classpath" to classpath.asPath + ) + ) + + val sources: MutableList = mutableListOf() + val outputs: MutableList = mutableListOf() + val binaries: MutableList = mutableListOf() + projectFiles.forEach { pf -> + binaries += pf.value.binaryReportFiles + sources += pf.value.sources + outputs += pf.value.outputs + } + + builder.invokeWithBody("jacocoReport") { + invokeWithBody("executiondata") { + project.files(binaries).addToAntBuilder(this, "resources") + } + invokeWithBody("structure", mapOf("name" to project.path)) { + invokeWithBody("sourcefiles") { + project.files(sources).addToAntBuilder(this, "resources") + } + invokeWithBody("classfiles") { + project.files(outputs).addToAntBuilder(this, "resources") + } + } + block() + } } private val GroovyObject.violations: String? diff --git a/src/main/kotlin/kotlinx/kover/lookup/DirsLookup.kt b/src/main/kotlin/kotlinx/kover/lookup/DirsLookup.kt new file mode 100644 index 00000000..16481d2b --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/lookup/DirsLookup.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.kover.lookup + +import kotlinx.kover.api.* +import kotlinx.kover.lookup.adapters.* +import org.gradle.api.Project +import org.gradle.api.file.* +import java.io.File + + +open class ProjectDirectories(val sources: FileCollection, val outputs: FileCollection) + +internal object DirsLookup { + private val adapters: List = listOf( + OldJavaPluginAdapter(), + AndroidPluginAdapter(), + KotlinMultiplatformPluginAdapter(), + KotlinAndroidPluginAdapter() + ) + + @Suppress("UNUSED_PARAMETER") + fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilters): ProjectDirectories { + val srcDirs = HashMap() + val outDirs = HashMap() + + adapters.forEach { + val (src, out) = it.lookupSafe(project, sourceSetFilters) + srcDirs += src.map { file -> file.canonicalPath to file } + outDirs += out.map { file -> file.canonicalPath to file } + } + + val src = srcDirs.values.filter { it.exists() && it.isDirectory } + val out = outDirs.values.filter { it.exists() && it.isDirectory } + + return ProjectDirectories(project.files(src), project.files(out)) + } + + +} + + +internal abstract class LookupAdapter { + protected abstract fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilters): Dirs + + fun lookupSafe(project: Project, sourceSetFilters: KoverSourceSetFilters): Dirs { + return try { + lookup(project, sourceSetFilters) + } catch (e: Throwable) { + when (e) { + is NoSuchMethodError, is NoSuchFieldError, is ClassNotFoundException, is NoClassDefFoundError -> { + project.logger.info("Problem occurred in Kover source set adapter", e) + Dirs() + } + else -> throw e + } + } + } + + protected fun filterSourceSet(name: String, sourceSetFilters: KoverSourceSetFilters): Boolean { + if (sourceSetFilters.excludes.contains(name)) { + return false + } + if (sourceSetFilters.excludeTests && (name == "test" || name.endsWith("Test") || name.startsWith("test") || name.startsWith("androidTest"))) { + return false + } + return true + } + + data class Dirs(val sources: List = emptyList(), val outputs: List = emptyList()) +} diff --git a/src/main/kotlin/kotlinx/kover/lookup/adapters/AndroidPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/lookup/adapters/AndroidPluginAdapter.kt new file mode 100644 index 00000000..01ef4d86 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/lookup/adapters/AndroidPluginAdapter.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.lookup.adapters + +import com.android.build.gradle.* +import kotlinx.kover.api.* +import kotlinx.kover.lookup.* +import org.gradle.api.* + +internal class AndroidPluginAdapter : LookupAdapter() { + override fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilters): Dirs { + project.plugins.findPlugin("android") ?: return Dirs() + + val extension = project.extensions.findByType(BaseExtension::class.java) ?: return Dirs() + + val sourceDirs = extension.sourceSets.asSequence() + .filter { filterSourceSet(it.name, sourceSetFilters) } + .map { it.java }.toList() + .flatMap { it.srcDirs } + + return Dirs(sourceDirs, emptyList()) + } +} diff --git a/src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinAndroidPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinAndroidPluginAdapter.kt new file mode 100644 index 00000000..c8302fff --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinAndroidPluginAdapter.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.lookup.adapters + +import kotlinx.kover.api.* +import kotlinx.kover.lookup.LookupAdapter +import org.gradle.api.* +import org.jetbrains.kotlin.gradle.dsl.* + +internal class KotlinAndroidPluginAdapter : LookupAdapter() { + + override fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilters): Dirs { + project.plugins.findPlugin("kotlin-android") ?: return Dirs() + + val extension = project.extensions.findByType(KotlinAndroidProjectExtension::class.java) ?: return Dirs() + + val sourceDirs = extension.target.compilations + .filter { !it.name.endsWith("Test") } + .flatMap { it.allKotlinSourceSets } + .map { it.kotlin } + .flatMap { it.srcDirs } + + val outputDirs = extension.target.compilations + .filter { !it.name.endsWith("Test") } + .flatMap { it.output.classesDirs } + + return Dirs(sourceDirs, outputDirs) + + } + +} diff --git a/src/main/kotlin/kotlinx/kover/adapters/KotlinMultiplatformPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinMultiplatformPluginAdapter.kt similarity index 50% rename from src/main/kotlin/kotlinx/kover/adapters/KotlinMultiplatformPluginAdapter.kt rename to src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinMultiplatformPluginAdapter.kt index 4830b8b2..3533c28c 100644 --- a/src/main/kotlin/kotlinx/kover/adapters/KotlinMultiplatformPluginAdapter.kt +++ b/src/main/kotlin/kotlinx/kover/lookup/adapters/KotlinMultiplatformPluginAdapter.kt @@ -1,46 +1,43 @@ /* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.kover.adapters +package kotlinx.kover.lookup.adapters import groovy.lang.* -import kotlinx.kover.adapters.api.* +import kotlinx.kover.api.* +import kotlinx.kover.lookup.LookupAdapter import org.gradle.api.* import org.gradle.api.file.* import org.gradle.internal.metaobject.* import org.jetbrains.kotlin.gradle.dsl.* import org.jetbrains.kotlin.gradle.plugin.* -class KotlinMultiplatformPluginAdapter : CompilationPluginAdapter { +internal class KotlinMultiplatformPluginAdapter : LookupAdapter() { - override fun findDirs(project: Project): PluginDirs { - return safe(project) { - this.plugins.findPlugin("kotlin-multiplatform") ?: return@safe PluginDirs(emptyList(), emptyList()) + override fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilters): Dirs { + project.plugins.findPlugin("kotlin-multiplatform") ?: return Dirs() - val extension = try { - project.extensions.findByType( - KotlinMultiplatformExtension::class.java - ) ?: return@safe PluginDirs(emptyList(), emptyList()) - } catch (e: ClassNotFoundException) { - return findByReflection(project) - } catch (e: NoClassDefFoundError) { - return findByReflection(project) - } + val extension = try { + project.extensions.findByType(KotlinMultiplatformExtension::class.java) ?: return Dirs() + } catch (e: ClassNotFoundException) { + return findByReflection(project) + } catch (e: NoClassDefFoundError) { + return findByReflection(project) + } - val targets = - extension.targets.filter { it.platformType == KotlinPlatformType.jvm || it.platformType == KotlinPlatformType.androidJvm } + val targets = + extension.targets.filter { it.platformType == KotlinPlatformType.jvm || it.platformType == KotlinPlatformType.androidJvm } - val compilations = targets.flatMap { it.compilations.filter { c -> c.name != "test" } } - val sourceDirs = - compilations.asSequence().flatMap { it.allKotlinSourceSets }.map { it.kotlin }.flatMap { it.srcDirs } - .toList() + val compilations = targets.flatMap { it.compilations.filter { c -> c.name != "test" } } + val sourceDirs = + compilations.asSequence().flatMap { it.allKotlinSourceSets }.map { it.kotlin }.flatMap { it.srcDirs } + .toList() - val outputDirs = - compilations.asSequence().flatMap { it.output.classesDirs }.toList() + val outputDirs = + compilations.asSequence().flatMap { it.output.classesDirs }.toList() - return@safe PluginDirs(sourceDirs, outputDirs) - } + return Dirs(sourceDirs, outputDirs) } /* @@ -49,12 +46,9 @@ class KotlinMultiplatformPluginAdapter : CompilationPluginAdapter { * Therefore, the only way to work with such an object is to use reflection. */ @Suppress("UNCHECKED_CAST") - fun findByReflection(project: Project): PluginDirs { + fun findByReflection(project: Project): Dirs { val extension = - project.extensions.findByName("kotlin")?.let { BeanDynamicObject(it) } ?: return PluginDirs( - emptyList(), - emptyList() - ) + project.extensions.findByName("kotlin")?.let { BeanDynamicObject(it) } ?: return Dirs() val targets = (extension.getProperty("targets") as NamedDomainObjectCollection).filter { val platformTypeName = (it.getProperty("platformType") as Named).name @@ -75,7 +69,7 @@ class KotlinMultiplatformPluginAdapter : CompilationPluginAdapter { compilations.asSequence().map { BeanDynamicObject(it.getProperty("output")) } .flatMap { it.getProperty("classesDirs") as FileCollection }.toList() - return PluginDirs(sourceDirs, outputDirs) + return Dirs(sourceDirs, outputDirs) } } diff --git a/src/main/kotlin/kotlinx/kover/lookup/adapters/OldJavaPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/lookup/adapters/OldJavaPluginAdapter.kt new file mode 100644 index 00000000..c9f44d1f --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/lookup/adapters/OldJavaPluginAdapter.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.lookup.adapters + +import kotlinx.kover.api.* +import kotlinx.kover.lookup.LookupAdapter +import org.gradle.api.* +import org.gradle.api.tasks.* + +internal class OldJavaPluginAdapter : LookupAdapter() { + + override fun lookup(project: Project, sourceSetFilters: KoverSourceSetFilters): Dirs { + project.plugins.findPlugin("java") ?: return Dirs() + + val sourceSetContainer = project.extensions.findByType( + SourceSetContainer::class.java + ) ?: return Dirs() + + val sourceSets = sourceSetContainer.filter { filterSourceSet(it.name, sourceSetFilters) } + + val sourceDirs = sourceSets.flatMap { it.allSource.srcDirs } + val outputDirs = sourceSets.flatMap { it.output.classesDirs } + + return Dirs(sourceDirs, outputDirs) + } + +} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverCollectingTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverCollectingTask.kt deleted file mode 100644 index b8cd2fab..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverCollectingTask.kt +++ /dev/null @@ -1,49 +0,0 @@ -package kotlinx.kover.tasks - -import org.gradle.api.* -import org.gradle.api.file.* -import org.gradle.api.tasks.* - -open class KoverCollectingTask : DefaultTask() { - /** - * Specifies directory path for collecting of all XML and HTML reports from all projects. - */ - @get:OutputDirectory - val outputDir: DirectoryProperty = project.objects.directoryProperty() - - @get:Internal - internal var xmlFiles: MutableMap = mutableMapOf() - - @get:Internal - internal var htmlDirs: MutableMap = mutableMapOf() - - - @TaskAction - fun collect() { - project.copy { - it.into(outputDir) - xmlFiles.forEach { (p, f) -> - it.from(f) { c -> - c.rename { "${p.pathAsFilename()}.xml" } - } - } - } - - - htmlDirs.forEach { (p, d) -> - val name = p.pathAsFilename() - - // delete directory for HTML reports so that the old reports do not overlap with the new ones - project.delete(outputDir.dir("html/$name")) - - project.copy { - it.from(d) - it.into(outputDir.dir("html/$name")) - } - } - } - - private fun String.pathAsFilename(): String { - return if (this == ":") "_root_" else replace(':', '_') - } -} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReport.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReport.kt deleted file mode 100644 index f1055831..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlReport.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.tasks - -import kotlinx.kover.api.* -import kotlinx.kover.engines.intellij.* -import kotlinx.kover.engines.jacoco.* -import org.gradle.api.file.* -import org.gradle.api.tasks.* - -@CacheableTask -open class KoverMergedHtmlReportTask : KoverMergedTask() { - /** - * Specifies directory path of generated HTML report. - */ - @get:OutputDirectory - val htmlReportDir: DirectoryProperty = project.objects.directoryProperty() - - @TaskAction - fun generate() { - val htmlDirFile = htmlReportDir.get().asFile - - if (coverageEngine.get() == CoverageEngine.INTELLIJ) { - intellijReport( - exec, - report(), - null, - htmlDirFile, - classpath.get() - ) - } else { - jacocoReport( - report(), - null, - htmlDirFile, - classpath.get(), - ) - } - logger.lifecycle("Kover: merged HTML report file://${htmlDirFile.canonicalPath}/index.html") - } -} - -@CacheableTask -open class KoverHtmlReportTask : KoverProjectTask() { - private val projectPath = project.path - - /** - * Specifies directory path of generated HTML report. - */ - @get:OutputDirectory - val htmlReportDir: DirectoryProperty = project.objects.directoryProperty() - - @TaskAction - fun generate() { - val htmlDirFile = htmlReportDir.get().asFile - - if (coverageEngine.get() == CoverageEngine.INTELLIJ) { - intellijReport( - exec, - report(), - null, - htmlDirFile, - classpath.get() - ) - } else { - jacocoReport( - report(), - null, - htmlDirFile, - classpath.get(), - ) - } - logger.lifecycle("Kover: HTML report for '$projectPath' file://${htmlDirFile.canonicalPath}/index.html") - } -} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlTask.kt new file mode 100644 index 00000000..66e29507 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverHtmlTask.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.tasks + +import kotlinx.kover.engines.commons.* +import org.gradle.api.file.* +import org.gradle.api.tasks.* + +@CacheableTask +internal open class KoverHtmlTask : KoverReportTask() { + @get:OutputDirectory + internal val reportDir: DirectoryProperty = project.objects.directoryProperty() + + @TaskAction + fun generate() { + val reportDirFile = reportDir.get().asFile + + val projectFiles = files.get() + EngineManager.report( + engine.get(), + this, + exec, + projectFiles, + classFilters.get(), + null, + reportDirFile + ) + + if (projectFiles.keys.size > 1) { + logger.lifecycle("Kover: HTML merged report for '$projectPath' file://${reportDirFile.canonicalPath}/index.html \n merged projects ${projectFiles.keys}") + } else { + logger.lifecycle("Kover: HTML report for '$projectPath' file://${reportDirFile.canonicalPath}/index.html") + } + } +} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverMergedTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverMergedTask.kt deleted file mode 100644 index 29753a0a..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverMergedTask.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.tasks - -import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.commons.Report -import org.gradle.api.* -import org.gradle.api.file.* -import org.gradle.api.model.* -import org.gradle.api.provider.* -import org.gradle.api.tasks.* -import org.gradle.configurationcache.extensions.* -import org.gradle.process.* -import java.io.* - -@CacheableTask -open class KoverMergedTask : DefaultTask() { - @get:Nested - val binaryReportFiles: MapProperty = - project.objects.mapProperty(String::class.java, NestedFiles::class.java) - - @get:Nested - val srcDirs: MapProperty = - project.objects.mapProperty(String::class.java, NestedFiles::class.java) - - @get:Nested - val outputDirs: MapProperty = - project.objects.mapProperty(String::class.java, NestedFiles::class.java) - - @get:Input - internal val coverageEngine: Property = project.objects.property(CoverageEngine::class.java) - - @get:Classpath - internal val classpath: Property = project.objects.property(FileCollection::class.java) - - /** - * Specifies class inclusion rules into report. - * Only the specified classes may be present in the report. - * Exclusion rules have priority over inclusion ones. - * - * Inclusion rules are represented as a set of fully-qualified names of the classes being instrumented. - * It's possible to use `*` and `?` wildcards. - * - * **Works only with IntelliJ Coverage Engine.** - */ - @get:Input - public var includes: List = emptyList() - - /** - * Specifies class exclusion rules into report. - * The specified classes will definitely be missing from report. - * Exclusion rules have priority over inclusion ones. - * - * Exclusion rules are represented as a set of fully-qualified names of the classes being instrumented. - * It's possible to use `*` and `?` wildcards. - * - * **Works only with IntelliJ Coverage Engine.** - */ - @get:Input - public var excludes: List = emptyList() - - // exec operations to launch Java applications - @get:Internal - protected val exec: ExecOperations = project.serviceOf() - - internal fun report(): Report { - val binariesMap = binaryReportFiles.get() - val sourcesMap = srcDirs.get() - val outputsMap = outputDirs.get() - - val projectsPaths = sourcesMap.keys - - val reportFiles: MutableList = mutableListOf() - val projects: MutableList = mutableListOf() - - projectsPaths.map { projectPath -> - reportFiles += binariesMap.getValue(projectPath).files.get() - projects += ProjectInfo( - sources = sourcesMap.getValue(projectPath).files.get(), - outputs = outputsMap.getValue(projectPath).files.get() - ) - } - - return Report(reportFiles, projects, includes, excludes) - } -} - - -class NestedFiles(objects: ObjectFactory, files: Provider) { - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - val files: Property = objects.property(FileCollection::class.java).also { it.set(files) } -} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverProjectTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverProjectTask.kt deleted file mode 100644 index a9b39d65..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverProjectTask.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.tasks - -import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.ProjectInfo -import kotlinx.kover.engines.commons.Report -import org.gradle.api.* -import org.gradle.api.file.* -import org.gradle.api.provider.* -import org.gradle.api.tasks.* -import org.gradle.configurationcache.extensions.* -import org.gradle.process.* - -abstract class KoverProjectTask : DefaultTask() { - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - val binaryReportFiles: Property = project.objects.property(FileCollection::class.java) - - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - val srcDirs: Property = project.objects.property(FileCollection::class.java) - - @get:Classpath - val outputDirs: Property = project.objects.property(FileCollection::class.java) - - @get:Input - internal val coverageEngine: Property = project.objects.property(CoverageEngine::class.java) - - @get:Classpath - internal val classpath: Property = project.objects.property(FileCollection::class.java) - - /** - * Specifies class inclusion rules into report. - * Only the specified classes may be present in the report. - * Exclusion rules have priority over inclusion ones. - * - * Inclusion rules are represented as a set of fully-qualified names of the classes being instrumented. - * It's possible to use `*` and `?` wildcards. - * - * **Works only with IntelliJ Coverage Engine.** - */ - @get:Input - public var includes: List = emptyList() - - /** - * Specifies class exclusion rules into report. - * The specified classes will definitely be missing from report. - * Exclusion rules have priority over inclusion ones. - * - * Exclusion rules are represented as a set of fully-qualified names of the classes being instrumented. - * It's possible to use `*` and `?` wildcards. - * - * **Works only with IntelliJ Coverage Engine.** - */ - @get:Input - public var excludes: List = emptyList() - - // exec operations to launch Java applications - @get:Internal - protected val exec: ExecOperations = project.serviceOf() - - internal fun report(): Report { - return Report( - binaryReportFiles.get().toList(), - listOf(ProjectInfo(srcDirs.get(), outputDirs.get())), - includes, - excludes - ) - } -} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverReportTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverReportTask.kt new file mode 100644 index 00000000..bde78c82 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverReportTask.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.tasks + +import kotlinx.kover.api.* +import org.gradle.api.* +import org.gradle.api.file.* +import org.gradle.api.provider.* +import org.gradle.api.tasks.* +import org.gradle.configurationcache.extensions.* +import org.gradle.process.* +import java.io.* + +internal abstract class KoverReportTask : DefaultTask() { + @get:Nested + internal val files: MapProperty = + project.objects.mapProperty(String::class.java, ProjectFiles::class.java) + + @get:Nested + internal val classFilters: Property = project.objects.property(KoverClassFilters::class.java) + + @get:Nested + internal val engine: Property = project.objects.property(EngineDetails::class.java) + + // exec operations to launch Java applications + @get:Internal + protected val exec: ExecOperations = project.serviceOf() + + @get:Internal + protected val projectPath: String = project.path + +} + +open class ProjectFiles( + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + val binaryReportFiles: FileCollection, + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + val sources: FileCollection, + + @get:Classpath + val outputs: FileCollection +) + +internal class EngineDetails( + @get:Nested val variant: CoverageEngineVariant, + @get:Internal val jarFile: File, + @get:Internal val classpath: FileCollection +) + + + + + + + + + + + diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverVerification.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverVerification.kt deleted file mode 100644 index 60681a87..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverVerification.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.tasks - -import kotlinx.kover.api.* -import kotlinx.kover.engines.commons.* -import kotlinx.kover.engines.intellij.* -import kotlinx.kover.engines.jacoco.* -import org.gradle.api.* -import org.gradle.api.file.* -import org.gradle.api.model.* -import org.gradle.api.tasks.* -import org.gradle.process.* -import javax.inject.* - -open class KoverMergedVerificationTask : KoverMergedTask() { - private val rulesInternal: MutableList = mutableListOf() - - /** - * Added verification rules for test task. - */ - @get:Nested - public val rules: List - get() = rulesInternal - - /** - * Add new coverage verification rule to check after test task execution. - */ - public fun rule(configureRule: Action) { - rulesInternal += project.objects.newInstance( - VerificationRuleImpl::class.java, - rulesInternal.size, - project.objects - ) - .also { configureRule.execute(it) } - } - - @TaskAction - fun verify() { - verify(exec, report(), coverageEngine.get(), rulesInternal, classpath.get()) - } - -} - -open class KoverVerificationTask : KoverProjectTask() { - private val rulesInternal: MutableList = mutableListOf() - - /** - * Added verification rules for test task. - */ - @get:Nested - public val rules: List - get() = rulesInternal - - /** - * Add new coverage verification rule to check after test task execution. - */ - public fun rule(configureRule: Action) { - rulesInternal += project.objects.newInstance( - VerificationRuleImpl::class.java, - rulesInternal.size, - project.objects - ) - .also { configureRule.execute(it) } - } - - @TaskAction - fun verify() { - verify(exec, report(), coverageEngine.get(), rulesInternal, classpath.get()) - } - -} - -private fun Task.verify( - exec: ExecOperations, - report: Report, - engine: CoverageEngine, - rules: List, - classpath: FileCollection -) { - if (engine == CoverageEngine.INTELLIJ) { - intellijVerification(exec, report, rules, classpath) - } else { - jacocoVerification(report, rules, classpath) - } -} - -private open class VerificationRuleImpl @Inject constructor(override val id: Int, private val objects: ObjectFactory) : - VerificationRule { - override var name: String? = null - override val bounds: MutableList = mutableListOf() - override fun bound(configureBound: Action) { - bounds += objects.newInstance(VerificationBoundImpl::class.java, bounds.size) - .also { configureBound.execute(it) } - } -} - -private open class VerificationBoundImpl @Inject constructor(override val id: Int) : VerificationBound { - override var minValue: Int? = null - override var maxValue: Int? = null - override var valueType: VerificationValueType = VerificationValueType.COVERED_LINES_PERCENTAGE -} - - - diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverVerificationTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverVerificationTask.kt new file mode 100644 index 00000000..feb2adab --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverVerificationTask.kt @@ -0,0 +1,58 @@ +package kotlinx.kover.tasks + +import kotlinx.kover.api.* +import kotlinx.kover.engines.commons.* +import org.gradle.api.* +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.* + +@CacheableTask +internal open class KoverVerificationTask : KoverReportTask() { + @get:Nested + internal val rules: ListProperty = project.objects.listProperty(VerificationRule::class.java) + + @get:OutputFile + internal val resultFile: RegularFileProperty = project.objects.fileProperty() + + @TaskAction + fun verify() { + val reportRules = convertRules() + val errors = EngineManager.verify( + engine.get(), + this, + exec, + files.get(), + classFilters.get(), + reportRules, + ) + resultFile.get().asFile.writeText(errors ?: "") + + if (errors != null) { + throw GradleException(errors) + } + } + + private fun convertRules(): List { + val result = mutableListOf() + + rules.get().forEach { rule -> + if (!rule.isEnabled) { + return@forEach + } + + val ruleBounds = mutableListOf() + rule.bounds.get().forEach { bound -> + val min = bound.minValue?.toBigDecimal() + val max = bound.maxValue?.toBigDecimal() + ruleBounds += ReportVerificationBound(ruleBounds.size, min, max, bound.counter, bound.valueType) + } + + result += ReportVerificationRule(result.size, rule.name, rule.target, rule.filters.orNull, ruleBounds) + } + + return result + } + +} + diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverXmlReport.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverXmlReport.kt deleted file mode 100644 index 5e582a9e..00000000 --- a/src/main/kotlin/kotlinx/kover/tasks/KoverXmlReport.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.kover.tasks - -import kotlinx.kover.api.* -import kotlinx.kover.engines.intellij.* -import kotlinx.kover.engines.jacoco.* -import org.gradle.api.file.* -import org.gradle.api.tasks.* - -@CacheableTask -open class KoverMergedXmlReportTask : KoverMergedTask() { - - /** - * Specifies file path of generated XML report file with coverage data. - */ - @get:OutputFile - val xmlReportFile: RegularFileProperty = project.objects.fileProperty() - - @TaskAction - fun generate() { - if (coverageEngine.get() == CoverageEngine.INTELLIJ) { - intellijReport( - exec, - report(), - xmlReportFile.get().asFile, - null, - classpath.get() - ) - } else { - jacocoReport( - report(), - xmlReportFile.get().asFile, - null, - classpath.get() - ) - } - } -} - -@CacheableTask -open class KoverXmlReportTask : KoverProjectTask() { - - /** - * Specifies file path of generated XML report file with coverage data. - */ - @get:OutputFile - val xmlReportFile: RegularFileProperty = project.objects.fileProperty() - - @TaskAction - fun generate() { - if (coverageEngine.get() == CoverageEngine.INTELLIJ) { - intellijReport( - exec, - report(), - xmlReportFile.get().asFile, - null, - classpath.get() - ) - } else { - jacocoReport( - report(), - xmlReportFile.get().asFile, - null, - classpath.get() - ) - } - } -} diff --git a/src/main/kotlin/kotlinx/kover/tasks/KoverXmlTask.kt b/src/main/kotlin/kotlinx/kover/tasks/KoverXmlTask.kt new file mode 100644 index 00000000..50bdb590 --- /dev/null +++ b/src/main/kotlin/kotlinx/kover/tasks/KoverXmlTask.kt @@ -0,0 +1,27 @@ +package kotlinx.kover.tasks + +import kotlinx.kover.engines.commons.* +import org.gradle.api.file.* +import org.gradle.api.tasks.* + +@CacheableTask +internal open class KoverXmlTask : KoverReportTask() { + @get:OutputFile + internal val reportFile: RegularFileProperty = project.objects.fileProperty() + + @TaskAction + fun generate() { + val projectFiles = files.get() + + EngineManager.report( + engine.get(), + this, + exec, + projectFiles, + classFilters.get(), + reportFile.get().asFile, + null + ) + } + +}