From 1475b5de5e8f0c6847401a4d4ec1d83f7de101ab Mon Sep 17 00:00:00 2001 From: Sergey Shanshin Date: Thu, 9 Dec 2021 00:07:47 +0300 Subject: [PATCH] Unified agent filters All filters for both engines now use only `*` and `?` wildcards. Resolves #21 --- README.md | 16 +- .../functional/cases/DefaultConfigTests.kt | 14 +- .../cases/InstrumentationFilteringTests.kt | 58 +++ .../functional/cases/ReportsCachingTests.kt | 34 +- .../test/functional/cases/utils/Commons.kt | 65 +++- .../functional/core/BaseGradleScriptTest.kt | 4 +- .../kover/test/functional/core/Builder.kt | 133 +++++++ .../kover/test/functional/core/Runner.kt | 344 ++++-------------- .../kover/test/functional/core/Types.kt | 82 +++-- .../kover/test/functional/core/Writer.kt | 182 +++++++++ .../scripts/buildscripts/groovy/kjvm/root | 2 +- .../scripts/buildscripts/groovy/kjvm/testTask | 5 + .../scripts/buildscripts/groovy/kmp/root | 2 +- .../scripts/buildscripts/groovy/kmp/testTask | 5 + .../scripts/buildscripts/kotlin/kjvm/root | 2 +- .../scripts/buildscripts/kotlin/kjvm/testTask | 5 + .../scripts/buildscripts/kotlin/kmp/root | 2 +- .../scripts/buildscripts/kotlin/kmp/testTask | 5 + .../main/kotlin/Sources.kt | 8 +- .../test/kotlin/TestClass.kt | 6 + .../kotlinx/kover/api/KoverTaskExtension.kt | 26 +- .../kover/engines/intellij/IntellijAgent.kt | 25 +- .../kover/engines/jacoco/JacocoAgent.kt | 12 +- 23 files changed, 656 insertions(+), 381 deletions(-) create mode 100644 src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt create mode 100644 src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt create mode 100644 src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt create mode 100644 src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/testTask create mode 100644 src/functionalTest/templates/scripts/buildscripts/groovy/kmp/testTask create mode 100644 src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/testTask create mode 100644 src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/testTask rename src/functionalTest/templates/sources/{simple-single => simple}/main/kotlin/Sources.kt (69%) rename src/functionalTest/templates/sources/{simple-single => simple}/test/kotlin/TestClass.kt (62%) diff --git a/README.md b/README.md index ac772d6d..344fd3a5 100644 --- a/README.md +++ b/README.md @@ -121,8 +121,8 @@ tasks.test { extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) { isEnabled = true binaryReportFile.set(file("$buildDir/custom/result.bin")) - includes = listOf("com\\.example\\..*") - excludes = listOf("com\\.example\\.subpackage\\..*") + includes = listOf("com.example.*") + excludes = listOf("com.example.subpackage.*") } } ``` @@ -136,8 +136,8 @@ tasks.test { kover { enabled = true binaryReportFile.set(file("$buildDir/custom/result.bin")) - includes = ['com\\.example\\..*'] - excludes = ['com\\.example\\.subpackage\\..*'] + includes = ['com.example.*'] + excludes = ['com.example.subpackage.*'] } } ``` @@ -159,8 +159,8 @@ android { extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) { isEnabled = true binaryReportFile.set(file("$buildDir/custom/debug-report.bin")) - includes = listOf("com\\.example\\..*") - excludes = listOf("com\\.example\\.subpackage\\..*") + includes = listOf("com.example.*") + excludes = listOf("com.example.subpackage.*") } } } @@ -183,8 +183,8 @@ android { kover { enabled = true binaryReportFile.set(file("$buildDir/custom/debug-report.bin")) - includes = ['com\\.example\\..*'] - excludes = ['com\\.example\\.subpackage\\..*'] + includes = ['com.example.*'] + excludes = ['com.example.subpackage.*'] } } } 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 c00272d7..7f912da8 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/DefaultConfigTests.kt @@ -7,12 +7,13 @@ import kotlin.test.* internal class DefaultConfigTests : BaseGradleScriptTest() { @Test fun testImplicitConfigsJvm() { - runner() + builder() .case("Test default setting for Kotlin/JVM") .languages(GradleScriptLanguage.GROOVY, GradleScriptLanguage.KOTLIN) .types(ProjectType.KOTLIN_JVM) - .sources("simple-single") - .check("build") { + .sources("simple") + .build() + .run("build") { checkIntellijBinaryReport(DEFAULT_INTELLIJ_KJVM_BINARY, DEFAULT_INTELLIJ_KJVM_SMAP) checkReports(DEFAULT_XML, DEFAULT_HTML) } @@ -20,12 +21,13 @@ internal class DefaultConfigTests : BaseGradleScriptTest() { @Test fun testImplicitConfigsKmp() { - runner() + builder() .case("Test default setting for Kotlin Multi-Platform") .languages(GradleScriptLanguage.GROOVY, GradleScriptLanguage.KOTLIN) .types(ProjectType.KOTLIN_MULTIPLATFORM) - .sources("simple-single") - .check("build") { + .sources("simple") + .build() + .run("build") { checkIntellijBinaryReport(DEFAULT_INTELLIJ_KMP_BINARY, DEFAULT_INTELLIJ_KMP_SMAP) checkReports(DEFAULT_XML, DEFAULT_HTML) } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt new file mode 100644 index 00000000..cb5f2a25 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/InstrumentationFilteringTests.kt @@ -0,0 +1,58 @@ +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 +import kotlin.test.* + +internal class InstrumentationFilteringTests : BaseGradleScriptTest() { + + @Test + fun testExclude() { + builder() + .case("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(DEFAULT_XML) { + assertCounterExcluded(classCounter("org.jetbrains.ExampleClass"), this@run.engine) + assertCounterCoveredAndIncluded(classCounter("org.jetbrains.SecondClass")) + } + } + } + + @Test + fun testExcludeInclude() { + builder() + .case("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(DEFAULT_XML) { + assertCounterExcluded(classCounter("org.jetbrains.ExampleClass"), this@run.engine) + assertCounterExcluded(classCounter("org.jetbrains.Unused"), this@run.engine) + assertCounterCoveredAndIncluded(classCounter("org.jetbrains.SecondClass")) + } + } + } + +} 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 29dfdc93..2bd8edac 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/ReportsCachingTests.kt @@ -9,48 +9,50 @@ import kotlin.test.* internal class ReportsCachingTests : BaseGradleScriptTest() { @Test fun testCachingForIntellij() { - runner() + builder() .case("Test caching reports for IntelliJ Coverage Engine") .engines(CoverageEngine.INTELLIJ) - .sources("simple-single") - .check("build", "--build-cache") { + .sources("simple") + .build() + .run("build", "--build-cache") { checkIntellijBinaryReport(DEFAULT_INTELLIJ_KJVM_BINARY, DEFAULT_INTELLIJ_KJVM_SMAP) checkReports(DEFAULT_XML, DEFAULT_HTML) } - .check("clean", "--build-cache") { + .run("clean", "--build-cache") { checkIntellijBinaryReport(DEFAULT_INTELLIJ_KJVM_BINARY, DEFAULT_INTELLIJ_KJVM_SMAP, false) checkReports(DEFAULT_XML, DEFAULT_HTML, false) } - .check("build", "--build-cache") { + .run("build", "--build-cache") { checkIntellijBinaryReport(DEFAULT_INTELLIJ_KJVM_BINARY, DEFAULT_INTELLIJ_KJVM_SMAP) checkReports(DEFAULT_XML, DEFAULT_HTML) - assertEquals(TaskOutcome.FROM_CACHE, outcome(":test")) - assertEquals(TaskOutcome.FROM_CACHE, outcome(":koverXmlReport")) - assertEquals(TaskOutcome.FROM_CACHE, outcome(":koverHtmlReport")) + outcome(":test") { assertEquals(TaskOutcome.FROM_CACHE, this)} + outcome(":koverXmlReport") { assertEquals(TaskOutcome.FROM_CACHE, this)} + outcome(":koverHtmlReport") { assertEquals(TaskOutcome.FROM_CACHE, this)} } } @Test fun testCachingForJacoco() { - runner() + builder() .case("Test caching reports for JaCoCo Coverage Engine") .engines(CoverageEngine.JACOCO) - .sources("simple-single") - .check("build", "--build-cache") { + .sources("simple") + .build() + .run("build", "--build-cache") { checkJacocoBinaryReport(DEFAULT_JACOCO_KJVM_BINARY) checkReports(DEFAULT_XML, DEFAULT_HTML) } - .check("clean", "--build-cache") { + .run("clean", "--build-cache") { checkJacocoBinaryReport(DEFAULT_JACOCO_KJVM_BINARY, false) checkReports(DEFAULT_XML, DEFAULT_HTML, false) } - .check("build", "--build-cache") { + .run("build", "--build-cache") { checkJacocoBinaryReport(DEFAULT_JACOCO_KJVM_BINARY) checkReports(DEFAULT_XML, DEFAULT_HTML) - assertEquals(TaskOutcome.FROM_CACHE, outcome(":test")) - assertEquals(TaskOutcome.FROM_CACHE, outcome(":koverXmlReport")) - assertEquals(TaskOutcome.FROM_CACHE, outcome(":koverHtmlReport")) + outcome(":test") { assertEquals(TaskOutcome.FROM_CACHE, this)} + outcome(":koverXmlReport") { assertEquals(TaskOutcome.FROM_CACHE, this)} + outcome(":koverHtmlReport") { assertEquals(TaskOutcome.FROM_CACHE, this)} } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Commons.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/utils/Commons.kt index 5119c42c..b3261098 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 @@ -1,38 +1,73 @@ package kotlinx.kover.test.functional.cases.utils +import kotlinx.kover.api.* +import kotlinx.kover.test.functional.core.* import kotlinx.kover.test.functional.core.RunResult import kotlin.test.* internal fun RunResult.checkIntellijBinaryReport(binary: String, smap: String, mustExists: Boolean = true) { if (mustExists) { - assertTrue { file(binary).exists() } - assertTrue { file(binary).length() > 0 } - assertTrue { file(smap).exists() } - assertTrue { file(smap).length() > 0 } + file(binary) { + assertTrue { exists() } + assertTrue { length() > 0 } + } + file(smap) { + assertTrue { exists() } + assertTrue { length() > 0 } + } } else { - assertFalse { file(binary).exists() } - assertFalse { file(smap).exists() } + file(binary) { + assertFalse { exists() } + } + file(smap) { + assertFalse { exists() } + } } } internal fun RunResult.checkJacocoBinaryReport(binary: String, mustExists: Boolean = true) { if (mustExists) { - assertTrue { file(binary).exists() } - assertTrue { file(binary).length() > 0 } + file(binary) { + assertTrue { exists() } + assertTrue { length() > 0 } + } } else { - assertFalse { file(binary).exists() } + file(binary) { + assertFalse { exists() } + } } } internal fun RunResult.checkReports(xmlPath: String, htmlPath: String, mustExists: Boolean = true) { if (mustExists) { - assertTrue { file(xmlPath).exists() } - assertTrue { file(xmlPath).length() > 0 } - assertTrue { file(htmlPath).exists() } - assertTrue { file(htmlPath).isDirectory } + file(xmlPath) { + assertTrue { exists() } + assertTrue { length() > 0 } + } + file(htmlPath) { + assertTrue { exists() } + assertTrue { isDirectory } + } } else { - assertFalse { file(xmlPath).exists() } - assertFalse { file(htmlPath).exists() } + file(xmlPath) { + assertFalse { exists() } + } + file(htmlPath) { + assertFalse { exists() } + } } +} + +internal fun assertCounterExcluded(counter: Counter?, engine: CoverageEngine) { + if (engine == CoverageEngine.INTELLIJ) { + assertNull(counter) + } else { + assertNotNull(counter) + assertEquals(0, counter.covered) + } +} +internal fun assertCounterCoveredAndIncluded(counter: Counter?) { + assertNotNull(counter) + assertTrue { counter.covered > 0 } } 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 96f39458..25567121 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt @@ -9,7 +9,7 @@ internal open class BaseGradleScriptTest { @JvmField internal val rootFolder: TemporaryFolder = TemporaryFolder() - fun runner(): ProjectRunner { - return createRunner(rootFolder.root) + fun builder(): ProjectBuilder { + return createBuilder(rootFolder.root) } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt new file mode 100644 index 00000000..6cb36f1e --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Builder.kt @@ -0,0 +1,133 @@ +package kotlinx.kover.test.functional.core + +import kotlinx.kover.api.* +import java.io.* + +internal fun createBuilder(rootDir: File): ProjectBuilder { + return ProjectBuilderImpl(rootDir) +} + +internal class ProjectBuilderState { + var description: String? = null + var pluginVersion: String? = null + val languages: MutableSet = mutableSetOf() + val types: MutableSet = mutableSetOf() + val engines: MutableSet = mutableSetOf() + val koverConfig: KoverRootConfig = KoverRootConfig() + val rootModule: ModuleBuilderState = ModuleBuilderState() + val submodules: MutableMap = mutableMapOf() +} + +internal class ModuleBuilderState { + val sourceTemplates: MutableList = mutableListOf() + val kotlinScripts: MutableList = mutableListOf() + val groovyScripts: MutableList = mutableListOf() + val testKotlinScripts: MutableList = mutableListOf() + val testGroovyScripts: MutableList = mutableListOf() + val rules: MutableList = mutableListOf() + val mainSources: MutableMap = mutableMapOf() + val testSources: MutableMap = mutableMapOf() +} + +private class ProjectBuilderImpl( + val rootDir: File, + private val state: ProjectBuilderState = ProjectBuilderState() +) : ModuleBuilderImpl(state.rootModule), ProjectBuilder { + + override fun case(description: String) = also { + state.description = description + } + + override fun languages(vararg languages: GradleScriptLanguage) = also { + state.languages += languages + } + + override fun engines(vararg engines: CoverageEngine) = also { + state.engines += engines + } + + override fun types(vararg types: ProjectType) = also { + state.types += types + } + + override fun configKover(config: KoverRootConfig.() -> Unit) = also { + state.koverConfig.config() + } + + override fun submodule(name: String, builder: ModuleBuilder<*>.() -> Unit) = also { + val moduleState = state.submodules.computeIfAbsent(name) { ModuleBuilderState() } + @Suppress("UPPER_BOUND_VIOLATED_WARNING") + ModuleBuilderImpl>(moduleState).builder() + } + + override fun build(): ProjectRunner { + 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.4.4" // TODO read from properties + } + + val projects: MutableMap = mutableMapOf() + + 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) + } + } + } + + return ProjectRunnerImpl(projects) + } + +} + + +@Suppress("UNCHECKED_CAST") +private open class ModuleBuilderImpl>(val moduleState: ModuleBuilderState) : ModuleBuilder { + + override fun verification(rules: Iterable): B { + moduleState.rules += rules + return this as B + } + + override fun configTest(script: String): B { + moduleState.testKotlinScripts += script + moduleState.testGroovyScripts += script + return this as B + } + + override fun configTest(kotlin: String, groovy: String): B { + moduleState.testKotlinScripts += kotlin + moduleState.testGroovyScripts += groovy + return this as B + } + + override fun config(script: String): B { + moduleState.kotlinScripts += script + moduleState.groovyScripts += script + return this as B + } + + override fun config(kotlin: String, groovy: String): B { + moduleState.kotlinScripts += kotlin + moduleState.groovyScripts += groovy + return this as B + } + + override fun sources(template: String): B { + moduleState.sourceTemplates += template + return this as B + } +} + + + diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt index a7124caf..dfb9c79f 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt @@ -2,168 +2,22 @@ package kotlinx.kover.test.functional.core import kotlinx.kover.api.* import org.gradle.testkit.runner.* +import org.w3c.dom.* import java.io.* +import javax.xml.parsers.* -internal fun createRunner(rootDir: File): ProjectRunner { - return ProjectRunnerImpl(rootDir) -} - -fun GradleRunner.addPluginTestRuntimeClasspath() = apply { - val classpathFile = File(System.getProperty("plugin-classpath")) - if (!classpathFile.exists()) { - throw IllegalStateException("Could not find classpath resource") - } - - val pluginClasspath = pluginClasspath + classpathFile.readLines().map { File(it) } - withPluginClasspath(pluginClasspath) -} - -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" - -private class ProjectRunnerImpl(val rootDir: File) : ProjectRunner { - private var initialized: Boolean = false - - private var description: String? = null - private var pluginVersion: String? = null - - private var intellijVersion: String? = null - private var jacocoVersion: String? = null - private var pluginEnabled: Boolean? = null - private var projects: MutableMap = mutableMapOf() - - private val languages: MutableSet = mutableSetOf() - private val types: MutableSet = mutableSetOf() - private val engines: MutableSet = mutableSetOf() - - - private val koverConfigs: MutableList = mutableListOf() - private val kotlinScripts: MutableList = mutableListOf() - private val groovyScripts: MutableList = mutableListOf() - private val rules: MutableList = mutableListOf() - private val mainSources: MutableMap = mutableMapOf() - private val testSources: MutableMap = mutableMapOf() - private val submodules: MutableMap = mutableMapOf() - - override fun case(description: String) = configure { - this.description = description - } - - override fun languages(vararg languages: GradleScriptLanguage) = configure { - this.languages += languages - } - - override fun engines(vararg engines: CoverageEngine) = configure { - this.engines += engines - } - - override fun types(vararg types: ProjectType) = configure { - this.types += types - } - - override fun setIntellijVersion(version: String) = configure { - this.intellijVersion = version - } - - override fun setJacocoVersion(version: String) = configure { - this.jacocoVersion = version - } - - override fun submodule(name: String, builder: ModuleBuilder<*>.() -> Unit) = configure { - submodules[name] = SubmoduleBuilderImpl().apply(builder) - } - - override fun kover(rootExtensionScript: String) = configure { - koverConfigs += rootExtensionScript - } - - override fun verification(rules: Iterable) = configure { - this.rules += rules - } - - override fun config(script: String) = configure { - kotlinScripts += script - groovyScripts += script - } - - override fun config(kotlin: String, groovy: String) = configure { - kotlinScripts += kotlin - groovyScripts += groovy - } - - override fun sources(template: String) = configure { - fun File.processDir(result: MutableMap, path: String = "") { - listFiles()?.forEach { file -> - val filePath = "$path/${file.name}" - if (file.isDirectory) { - file.processDir(result, filePath) - } else if (file.exists() && file.length() > 0) { - result += filePath to file.readText() - } - } - } - - File(SOURCES_PATH, "$template/main").processDir(mainSources) - File(SOURCES_PATH, "$template/test").processDir(testSources) - return this - } - - override fun check(vararg args: String, block: RunResult.() -> Unit): ProjectRunner { - if (!initialized) { - initialize() - initialized = true - } - - languages.forEach { language -> - types.forEach { type -> - engines.forEach { engine -> - val slice = ProjectSlice(language, type, engine ?: CoverageEngine.INTELLIJ) - projects[slice]?.run(listOf(*args), block) - ?: throw IllegalStateException("Internal runner error: no project was created for the $slice slice during initialization") - } - } - } - - return this - } +internal class ProjectRunnerImpl(private val projects: Map) : ProjectRunner { - private fun initialize() { - if (languages.isEmpty()) { - languages += GradleScriptLanguage.KOTLIN - } - if (types.isEmpty()) { - types += ProjectType.KOTLIN_JVM - } - if (engines.isEmpty()) { - engines += null - } - if (pluginVersion == null) { - pluginVersion = "0.4.4" // TODO read from properties - } - - languages.forEach { language -> - types.forEach { type -> - engines.forEach { engine -> - projects[ProjectSlice(language, type, engine ?: CoverageEngine.INTELLIJ)] = - createProject(language, type, engine) - } - } - } - } + override fun run(vararg args: String, checker: RunResult.() -> Unit): ProjectRunnerImpl { + val argsList = listOf(*args) + projects.forEach { (slice, project) -> project.runGradle(argsList, slice, checker) } - private inline fun configure(block: ProjectRunnerImpl.() -> Unit): ProjectRunnerImpl { - if (initialized) { - throw IllegalStateException("Runner can't be configured after first build") - } - block() return this } - private fun File.run(args: List, block: RunResult.() -> Unit) { + private fun File.runGradle(args: List, slice: ProjectSlice, checker: RunResult.() -> Unit) { val buildResult = GradleRunner.create() .withProjectDir(this) .withPluginClasspath() @@ -171,156 +25,92 @@ private class ProjectRunnerImpl(val rootDir: File) : ProjectRunner { .withArguments(args) .build() - RunResult(buildResult, this).apply(block) - } - - private fun createProject(language: GradleScriptLanguage, type: ProjectType, engine: CoverageEngine?): File { - val projectDir = File(rootDir, System.currentTimeMillis().toString()).also { it.mkdirs() } - - val extension = if (language == GradleScriptLanguage.KOTLIN) "gradle.kts" else "gradle" - - File(projectDir, "build.$extension").writeText( - scriptTemplate(true, language, type) - .replace("//PLUGIN_VERSION", pluginVersion!!) - .replace("//REPOSITORIES", "") - .replace("//DEPENDENCIES", "") - .replace("//KOVER", buildRootExtension(language, engine)) - .replace("//SCRIPTS", buildScripts(language)) - .replace("//VERIFICATIONS", buildVerifications(language, rules)) - ) - File(projectDir, "settings.$extension").writeText(buildSettings(language)) - - val srcDir: File - val testDir: File - when (type) { - ProjectType.KOTLIN_JVM -> { - srcDir = File(projectDir, "src/main") - testDir = File(projectDir, "src/test") - } - ProjectType.KOTLIN_MULTIPLATFORM -> { - srcDir = File(projectDir, "src/jvmMain") - testDir = File(projectDir, "src/jvmTest") - } - ProjectType.ANDROID -> { - srcDir = File(projectDir, "src/jvmMain") - testDir = File(projectDir, "src/jvmTest") - } - } - - mainSources.forEach { (name, content) -> - val srcFile = File(srcDir, name) - srcFile.parentFile.mkdirs() - srcFile.writeText(content) - } - testSources.forEach { (name, content) -> - val srcFile = File(testDir, name) - srcFile.parentFile.mkdirs() - srcFile.writeText(content) + try { + RunResultImpl(buildResult, slice, this).apply(checker) + } catch (e: Throwable) { + throw AssertionError("Assertion error occurred in test for project $slice", e) } - - return projectDir } +} - private fun buildRootExtension(language: GradleScriptLanguage, engine: CoverageEngine?): String { - if (engine == null && koverConfigs.isEmpty() && pluginEnabled == null) { - return "" - } +private fun GradleRunner.addPluginTestRuntimeClasspath() = apply { + val classpathFile = File(System.getProperty("plugin-classpath")) + if (!classpathFile.exists()) { + throw IllegalStateException("Could not find classpath resource $classpathFile") + } - val builder = StringBuilder() - builder.appendLine() - builder.appendLine("kover {") + val pluginClasspath = pluginClasspath + classpathFile.readLines().map { File(it) } + withPluginClasspath(pluginClasspath) +} - if (pluginEnabled != null) { - val property = if (language == GradleScriptLanguage.KOTLIN) "isEnabled" else "enabled" - builder.appendLine("$property = $pluginEnabled") - } - if (engine == CoverageEngine.INTELLIJ) { - builder.appendLine(" coverageEngine.set(kotlinx.kover.api.CoverageEngine.INTELLIJ)") - if (intellijVersion != null) { - builder.appendLine(""" intellijEngineVersion.set("$intellijVersion")""") - } - } - if (engine == CoverageEngine.JACOCO) { - builder.appendLine(" coverageEngine.set(kotlinx.kover.api.CoverageEngine.JACOCO)") - if (jacocoVersion != null) { - builder.appendLine(""" jacocoEngineVersion.set("$jacocoVersion")""") - } - } +private class RunResultImpl(private val result: BuildResult, private val slice: ProjectSlice, dir: File) : RunResult { + val buildDir: File = File(dir, "build") - koverConfigs.forEach { - builder.appendLine(" $it") - } - builder.appendLine("}") + override val engine: CoverageEngine = slice.engine ?: CoverageEngine.INTELLIJ - return builder.toString() + override fun output(checker: String.() -> Unit) { + result.output.checker() } - @Suppress("UNUSED_PARAMETER") - private fun buildVerifications(language: GradleScriptLanguage, verifications: List): String { - return "" + override fun file(name: String, checker: File.() -> Unit) { + File(buildDir, name).checker() } - private fun buildSettings(language: GradleScriptLanguage): String { - return settingsTemplate(language) - .replace("//SUBMODULES", buildSubmodulesIncludes(submodules.keys)) + override fun xml(filename: String, checker: XmlReport.() -> Unit) { + val xmlFile = File(buildDir, filename) + if (!xmlFile.exists()) throw IllegalStateException("XML file '$filename' not found") + XmlReportImpl(xmlFile).checker() } - private fun buildScripts(language: GradleScriptLanguage): String { - val scripts = if (language == GradleScriptLanguage.KOTLIN) kotlinScripts else groovyScripts - - return if (scripts.isNotEmpty()) { - scripts.joinToString("\n", "\n", "\n") - } else { - "" - } + override fun outcome(taskPath: String, checker: TaskOutcome.() -> Unit) { + result.task(taskPath)?.outcome?.checker() + ?: throw IllegalArgumentException("Task '$taskPath' not found in build result") } } -private data class ProjectSlice(val language: GradleScriptLanguage, val type: ProjectType, val engine: CoverageEngine) -private class SubmoduleBuilderImpl : ModuleBuilder { - override fun verification(rules: Iterable): SubmoduleBuilderImpl { - TODO("Not yet implemented") - } +private class XmlReportImpl(file: File) : XmlReport { + private val document = DocumentBuilderFactory.newInstance() + // This option disables checking the dtd file for JaCoCo XML file + .also { it.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) } + .newDocumentBuilder().parse(file) - override fun config(script: String): SubmoduleBuilderImpl { - TODO("Not yet implemented") - } + override fun classCounter(className: String, type: String): Counter? { + val correctedClassName = className.replace('.', '/') + val packageName = correctedClassName.substringBeforeLast('/') - override fun config(kotlin: String, groovy: String): SubmoduleBuilderImpl { - TODO("Not yet implemented") - } + val reportElement = ((document.getElementsByTagName("report").item(0)) as Element) - override fun sources(template: String): SubmoduleBuilderImpl { - TODO("Not yet implemented") - } - -} + var classElement: Element? = null -private fun buildSubmodulesIncludes(submodules: Set): String { - if (submodules.isEmpty()) return "" + reportElement.forEach("package") loop@{ + if (getAttribute("name") == packageName) { + forEach("class") { + if (getAttribute("name") == correctedClassName) { + classElement = this + return@loop + } + } + } + } - return submodules.joinToString("\n", "\n", "\n") { - """include("$it")""" + classElement?.forEach("counter") { + if (getAttribute("type") == type) { + return Counter(type, this.getAttribute("missed").toInt(), this.getAttribute("covered").toInt()) + } + } + return null } } +private inline fun Element.forEach(tag: String, block: Element.() -> Unit) { + val elements = getElementsByTagName(tag) -private fun scriptTemplate(root: Boolean, language: GradleScriptLanguage, type: ProjectType): String { - val languageString = if (language == GradleScriptLanguage.KOTLIN) "kotlin" else "groovy" - - val typeString = when (type) { - ProjectType.KOTLIN_JVM -> "kjvm" - ProjectType.KOTLIN_MULTIPLATFORM -> "kmp" - ProjectType.ANDROID -> "android" + for (i in 0 until elements.length) { + val element = elements.item(i) as Element + if (element.parentNode == this) { + element.block() + } } - val filename = if (root) "root" else "child" - return File("$BUILD_SCRIPTS_PATH/$languageString/$typeString/$filename").readText() } - -private fun settingsTemplate(language: GradleScriptLanguage): String { - val filename = if (language == GradleScriptLanguage.KOTLIN) "settings.gradle.kts" else "settings.gradle" - return File("$SETTINGS_PATH/$filename").readText() -} - 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 5a22d541..77830cb9 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Types.kt @@ -8,36 +8,68 @@ internal enum class GradleScriptLanguage { KOTLIN, GROOVY } internal enum class ProjectType { KOTLIN_JVM, KOTLIN_MULTIPLATFORM, ANDROID } -internal interface ModuleBuilder> { - fun sources(template: String): S - fun verification(rules: Iterable): S - fun config(script: String): S - fun config(kotlin: String, groovy: String): S -} +internal interface ModuleBuilder> { + fun sources(template: String): B + fun verification(rules: Iterable): B + + fun configTest(script: String): B + fun configTest(kotlin: String, groovy: String): B -internal interface ProjectRunner : ModuleBuilder { - fun case(description: String): ProjectRunner - fun languages(vararg languages: GradleScriptLanguage): ProjectRunner - fun engines(vararg engines: CoverageEngine): ProjectRunner - fun types(vararg types: ProjectType): ProjectRunner - fun setIntellijVersion(version: String): ProjectRunner - fun setJacocoVersion(version: String): ProjectRunner - fun submodule(name: String, builder: ModuleBuilder<*>.() -> Unit): ProjectRunner - fun kover(rootExtensionScript: String): ProjectRunner - fun check(vararg args: String, block: RunResult.() -> Unit): ProjectRunner + fun config(script: String): B + fun config(kotlin: String, groovy: String): B } -internal class RunResult(private val result: BuildResult, private val dir: File) { - val buildDir: File = File(dir, "build") +internal interface ProjectBuilder : ModuleBuilder { + fun case(description: String): ProjectBuilder + fun languages(vararg languages: GradleScriptLanguage): ProjectBuilder + fun engines(vararg engines: CoverageEngine): ProjectBuilder + fun types(vararg types: ProjectType): ProjectBuilder - val output = result.output + fun configKover(config: KoverRootConfig.() -> Unit): ProjectBuilder - fun file(name: String): File { - return File(buildDir, name) - } + fun submodule(name: String, builder: ModuleBuilder<*>.() -> Unit): ModuleBuilder<*> + + fun build(): ProjectRunner +} - fun outcome(taskPath: String): TaskOutcome { - return result.task(taskPath)?.outcome - ?: throw IllegalArgumentException("Task '$taskPath' not found in build result") +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 data class KoverRootConfig( + var disabled: Boolean? = null, + var intellijVersion: String? = null, + var jacocoVersion: String? = null, + var generateReportOnCheck: Boolean? = null +) { + val isDefault = + disabled == null && intellijVersion == null && jacocoVersion == null && generateReportOnCheck == null +} + +internal interface ProjectRunner { + fun run(vararg args: String, checker: RunResult.() -> Unit): ProjectRunner +} + +internal interface RunResult { + val engine: CoverageEngine + + fun output(checker: String.() -> Unit) + + fun file(name: String, checker: File.() -> Unit) + + fun xml(filename: String, checker: XmlReport.() -> Unit) + + fun outcome(taskPath: String, checker: TaskOutcome.() -> Unit) +} + + +internal class Counter(val type: String, val missed: Int, val covered: Int) { + val isEmpty: Boolean + get() = missed == 0 && covered == 0 +} + +internal interface XmlReport { + fun classCounter(className: String, type: String = "INSTRUCTION"): Counter? +} diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt new file mode 100644 index 00000000..ac70259f --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Writer.kt @@ -0,0 +1,182 @@ +package kotlinx.kover.test.functional.core + +import kotlinx.kover.api.* +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 ProjectBuilderState.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) + .processModuleBuildScript(rootModule, slice) + + val settings = buildSettings(slice) + + File(projectDir, "build.$extension").writeText(buildScript) + File(projectDir, "settings.$extension").writeText(settings) + + rootModule.writeSources(projectDir, slice) + + return projectDir +} + + +private val ProjectSlice.scriptExtension get() = if (language == GradleScriptLanguage.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: ProjectBuilderState, slice: ProjectSlice): String { + return replace("//PLUGIN_VERSION", state.pluginVersion!!) + .replace("//KOVER", state.buildRootExtension(slice)) +} + +private fun String.processModuleBuildScript(state: ModuleBuilderState, slice: ProjectSlice): String { + return replace("//REPOSITORIES", "") + .replace("//DEPENDENCIES", "") + .replace("//SCRIPTS", state.buildScripts(slice)) + .replace("//TEST_TASK", state.buildTestTask(slice)) + .replace("//VERIFICATIONS", state.buildVerifications(slice)) +} + + +private fun ModuleBuilderState.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 == GradleScriptLanguage.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 buildSubmodulesIncludes(submodules: Set): String { + if (submodules.isEmpty()) return "" + + return submodules.joinToString("\n", "\n", "\n") { + """include("$it")""" + } +} + +private fun ProjectBuilderState.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 == GradleScriptLanguage.KOTLIN) "isEnabled" else "enabled" + builder.appendLine("$property = ${koverConfig.disabled == false}") + } + + 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}")""") + } + } + + builder.appendLine("}") + + return builder.toString() +} + +@Suppress("UNUSED_PARAMETER") +private fun ModuleBuilderState.buildTestTask(slice: ProjectSlice): String { + val configs = if (slice.language == GradleScriptLanguage.KOTLIN) testKotlinScripts else testGroovyScripts + + if (configs.isEmpty()) { + return "" + } + + return loadTestTaskTemplate(slice).replace("//KOVER_TEST_CONFIG", configs.joinToString("\n")) +} + +@Suppress("UNUSED_PARAMETER") +private fun ModuleBuilderState.buildVerifications(slice: ProjectSlice): String { + return "" +} + +private fun ProjectBuilderState.buildSettings(slice: ProjectSlice): String { + return loadSettingsTemplate(slice) + .replace("//SUBMODULES", buildSubmodulesIncludes(submodules.keys)) +} + +private fun ModuleBuilderState.buildScripts(slice: ProjectSlice): String { + val scripts = if (slice.language == GradleScriptLanguage.KOTLIN) kotlinScripts else groovyScripts + + return if (scripts.isNotEmpty()) { + scripts.joinToString("\n", "\n", "\n") + } else { + "" + } +} + + +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 "child" + return File("${slice.scriptPath()}/$filename").readText() +} + +private fun loadTestTaskTemplate(slice: ProjectSlice): String { + return File("${slice.scriptPath()}/testTask").readText() +} diff --git a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/root b/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/root index 30ac860d..46b6b11a 100644 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/root +++ b/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/root @@ -10,4 +10,4 @@ repositories { dependencies {//DEPENDENCIES testImplementation 'org.jetbrains.kotlin:kotlin-test' } -//KOVER//SCRIPTS//VERIFICATIONS +//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 new file mode 100644 index 00000000..a9d184d8 --- /dev/null +++ b/src/functionalTest/templates/scripts/buildscripts/groovy/kjvm/testTask @@ -0,0 +1,5 @@ +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 index e0416809..0a8bcb40 100644 --- a/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/root +++ b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/root @@ -16,4 +16,4 @@ kotlin { commonTestImplementation 'org.jetbrains.kotlin:kotlin-test' } } -//KOVER//SCRIPTS//VERIFICATIONS +//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 new file mode 100644 index 00000000..2ccf87c9 --- /dev/null +++ b/src/functionalTest/templates/scripts/buildscripts/groovy/kmp/testTask @@ -0,0 +1,5 @@ +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 index 2cb189f7..423d59ba 100644 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/root +++ b/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/root @@ -10,4 +10,4 @@ repositories { dependencies {//DEPENDENCIES testImplementation(kotlin("test")) } -//KOVER//SCRIPTS//VERIFICATIONS +//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 new file mode 100644 index 00000000..3a225fde --- /dev/null +++ b/src/functionalTest/templates/scripts/buildscripts/kotlin/kjvm/testTask @@ -0,0 +1,5 @@ +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 index 2c5a59e9..5f8c9e09 100644 --- a/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/root +++ b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/root @@ -16,4 +16,4 @@ kotlin { commonTestImplementation(kotlin("test")) } } -//KOVER//SCRIPTS//VERIFICATIONS +//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 new file mode 100644 index 00000000..0c1f5175 --- /dev/null +++ b/src/functionalTest/templates/scripts/buildscripts/kotlin/kmp/testTask @@ -0,0 +1,5 @@ +tasks.named("jvmTest").configure { + extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) { + //KOVER_TEST_CONFIG + } +} diff --git a/src/functionalTest/templates/sources/simple-single/main/kotlin/Sources.kt b/src/functionalTest/templates/sources/simple/main/kotlin/Sources.kt similarity index 69% rename from src/functionalTest/templates/sources/simple-single/main/kotlin/Sources.kt rename to src/functionalTest/templates/sources/simple/main/kotlin/Sources.kt index dbc8515d..2a05258f 100644 --- a/src/functionalTest/templates/sources/simple-single/main/kotlin/Sources.kt +++ b/src/functionalTest/templates/sources/simple/main/kotlin/Sources.kt @@ -10,7 +10,13 @@ class ExampleClass { } } -class UnusedClass { +class SecondClass { + fun anotherUsed(value: Int): Int { + return value + 1 + } +} + +class Unused { fun functionInUsedClass() { println("unused") } diff --git a/src/functionalTest/templates/sources/simple-single/test/kotlin/TestClass.kt b/src/functionalTest/templates/sources/simple/test/kotlin/TestClass.kt similarity index 62% rename from src/functionalTest/templates/sources/simple-single/test/kotlin/TestClass.kt rename to src/functionalTest/templates/sources/simple/test/kotlin/TestClass.kt index c57b7b2d..3b6504cc 100644 --- a/src/functionalTest/templates/sources/simple-single/test/kotlin/TestClass.kt +++ b/src/functionalTest/templates/sources/simple/test/kotlin/TestClass.kt @@ -1,6 +1,7 @@ package org.jetbrains.serialuser import org.jetbrains.ExampleClass +import org.jetbrains.SecondClass import kotlin.test.Test class TestClass { @@ -8,4 +9,9 @@ class TestClass { fun simpleTest() { ExampleClass().used(-20) } + + @Test + fun secondTest() { + SecondClass().anotherUsed(-20) + } } diff --git a/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt b/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt index 0c188858..74d6746c 100644 --- a/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt +++ b/src/main/kotlin/kotlinx/kover/api/KoverTaskExtension.kt @@ -37,33 +37,19 @@ open class KoverTaskExtension(objects: ObjectFactory) { /** - * Specifies inclusion rules coverage engine. + * Specifies class inclusion rules coverage engine. Exclusion rules have priority over inclusion ones. * - * ### Inclusion rules for IntelliJ - * - * Inclusion rules are represented as set of regular expressions - * that are matched against fully-qualified names of the classes being instrumented. - * - * ### Inclusion rules for JaCoCo - * - * Inclusion rules are represented as set of [JaCoCo-specific](https://www.eclemma.org/jacoco/trunk/doc/report-mojo.html#includes) - * fully qualified name that also supports `*` and `?`. + * Inclusion rules are represented as a set of fully-qualified names of the classes being instrumented. + * It's possible to use `*` and `?` wildcards. */ @get:Input public var includes: List = emptyList() /** - * Specifies exclusion rules for coverage engine. - * - * ### Exclusion rules for IntelliJ - * - * Exclusion rules are represented as set of regular expressions - * that are matched against fully-qualified names of the classes being instrumented. - * - * ### Inclusion rules for JaCoCo + * Specifies class exclusion rules for coverage engine. Exclusion rules have priority over inclusion ones. * - * Exclusion rules are represented as set of [JaCoCo-specific](https://www.eclemma.org/jacoco/trunk/doc/report-mojo.html#excludes) - * fully qualified name that also supports `*` and `?`. + * Exclusion rules are represented as a set of fully-qualified names of the classes being instrumented. + * It's possible to use `*` and `?` wildcards. */ @get:Input public var excludes: List = emptyList() diff --git a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt index 8e665d93..c664c6ef 100644 --- a/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt +++ b/src/main/kotlin/kotlinx/kover/engines/intellij/IntellijAgent.kt @@ -7,7 +7,6 @@ package kotlinx.kover.engines.intellij import kotlinx.kover.api.* import org.gradle.api.* import org.gradle.api.artifacts.* -import org.gradle.api.file.* import java.io.* @@ -55,20 +54,38 @@ internal class IntellijAgent(val config: Configuration) { pw.appendLine(generateSmapFile.toString()) pw.appendLine(smapPath) extension.includes.forEach { i -> - pw.appendLine(i) + pw.appendLine(i.replaceWildcards()) } if (extension.excludes.isNotEmpty()) { pw.appendLine("-exclude") } - extension.excludes.forEach { i -> - pw.appendLine(i) + extension.excludes.forEach { e -> + pw.appendLine(e.replaceWildcards()) } } } + + private fun String.replaceWildcards(): String { + // in most cases, the characters `*` or `.` will be present therefore, we increase the capacity in advance + val builder = StringBuilder(length * 2) + + forEach { char -> + when (char) { + in regexMetacharactersSet -> builder.append('\\').append(char) + '*' -> builder.append('.').append("*") + '?' -> builder.append('.') + else -> builder.append(char) + } + } + + return builder.toString() + } } +private val regexMetacharactersSet = "<([{\\^-=$!|]})+.>".toSet() + private fun Project.createIntellijConfig(koverExtension: KoverExtension): Configuration { val config = project.configurations.create("IntellijKoverConfig") config.isVisible = false diff --git a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt index 39e1a454..fbb1147a 100644 --- a/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt +++ b/src/main/kotlin/kotlinx/kover/engines/jacoco/JacocoAgent.kt @@ -7,7 +7,6 @@ package kotlinx.kover.engines.jacoco import kotlinx.kover.api.* import org.gradle.api.* import org.gradle.api.artifacts.* -import org.gradle.api.file.* import java.io.* internal fun Project.createJacocoAgent(koverExtension: KoverExtension): JacocoAgent { @@ -29,17 +28,24 @@ internal class JacocoAgent(val config: Configuration, private val project: Proje val binary = extension.binaryReportFile.get() binary.parentFile.mkdirs() - return listOf( + return listOfNotNull( "destfile=${binary.canonicalPath}", "append=false", // Kover don't support parallel execution of one task "inclnolocationclasses=false", "dumponexit=true", "output=file", - "jmx=false" + "jmx=false", + extension.includes.filterString("includes"), + extension.excludes.filterString("excludes") ).joinToString(",") } } +private fun List.filterString(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