From af8713bf7f61c682de0cf2b000aad8233f5a43df Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Tue, 21 Dec 2021 17:17:56 +0300 Subject: [PATCH] Kotlin Multiplatform plugin adapter rewritten to use reflection Fixes #100 --- .../test/functional/cases/AdaptersTests.kt | 35 ++++++++ .../functional/core/BaseGradleScriptTest.kt | 4 + .../kover/test/functional/core/Loader.kt | 19 +++++ .../kover/test/functional/core/Runner.kt | 81 ++++++++++++++----- .../different-plugins/build.gradle.kts | 7 ++ .../different-plugins/settings.gradle.kts | 3 + .../submodule-multiplatform/build.gradle.kts | 19 +++++ .../src/commonMain/kotlin/CommonClass.kt | 7 ++ .../src/commonTest/kotlin/CommonTest.kt | 10 +++ .../src/jvmMain/kotlin/JvmClass.kt | 7 ++ .../src/jvmTest/kotlin/JvmTest.kt | 10 +++ .../KotlinMultiplatformPluginAdapter.kt | 54 ++++++++++++- 12 files changed, 233 insertions(+), 23 deletions(-) create mode 100644 src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt create mode 100644 src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Loader.kt create mode 100644 src/functionalTest/templates/projects/different-plugins/build.gradle.kts create mode 100644 src/functionalTest/templates/projects/different-plugins/settings.gradle.kts create mode 100644 src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/build.gradle.kts create mode 100644 src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/commonMain/kotlin/CommonClass.kt create mode 100644 src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/commonTest/kotlin/CommonTest.kt create mode 100644 src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/jvmMain/kotlin/JvmClass.kt create mode 100644 src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/jvmTest/kotlin/JvmTest.kt diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt new file mode 100644 index 00000000..465e13be --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/cases/AdaptersTests.kt @@ -0,0 +1,35 @@ +package kotlinx.kover.test.functional.cases + +import kotlinx.kover.test.functional.cases.utils.assertCounterFullyCovered +import kotlinx.kover.test.functional.cases.utils.defaultXmlModuleReport +import kotlinx.kover.test.functional.cases.utils.defaultXmlReport +import kotlinx.kover.test.functional.core.BaseGradleScriptTest +import kotlin.test.* + +internal class AdaptersTests : BaseGradleScriptTest() { + @Test + fun testSubmoduleHasAnotherPlugin() { + /* + Tests for https://github.com/Kotlin/kotlinx-kover/issues/100 + Classes from plugins applied in submodule not accessible for Kover in root module. + Therefore, Kover is forced to use reflection to work with extensions of the kotlin multiplatform plugin. + */ + internalProject("different-plugins") + .run("koverXmlReport") { + xml(defaultXmlReport()) { + assertCounterFullyCovered(classCounter("org.jetbrains.CommonClass")) + assertCounterFullyCovered(classCounter("org.jetbrains.JvmClass")) + } + } + + internalProject("different-plugins") + .run("koverXmlModuleReport") { + submodule("submodule-multiplatform") { + xml(defaultXmlModuleReport()) { + assertCounterFullyCovered(classCounter("org.jetbrains.CommonClass")) + assertCounterFullyCovered(classCounter("org.jetbrains.JvmClass")) + } + } + } + } +} 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 6f305822..835dafd1 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/BaseGradleScriptTest.kt @@ -12,4 +12,8 @@ internal open class BaseGradleScriptTest { fun builder(description: String): ProjectBuilder { return createBuilder(rootFolder.root, description) } + + fun internalProject(name: String): ProjectRunner { + return loadInternalProject(name, rootFolder.root) + } } diff --git a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Loader.kt b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Loader.kt new file mode 100644 index 00000000..3b6a4e0e --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Loader.kt @@ -0,0 +1,19 @@ +package kotlinx.kover.test.functional.core + +import java.io.* + +private const val INTERNAL_PROJECTS_PATH = "src/functionalTest/templates/projects" + + +internal fun loadInternalProject(name: String, rootDir: File): ProjectRunner { + val targetDir = File.createTempFile(name, null, rootDir) + + val srcDir = File(INTERNAL_PROJECTS_PATH, name) + if (!srcDir.exists()) { + throw IllegalArgumentException("Internal test project '$name' not found") + } + + srcDir.copyRecursively(targetDir, true) + + return SingleProjectRunnerImpl(targetDir) +} 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 96fd5e12..f252def5 100644 --- a/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt +++ b/src/functionalTest/kotlin/kotlinx/kover/test/functional/core/Runner.kt @@ -13,27 +13,36 @@ internal class ProjectRunnerImpl(private val projects: Map) override fun run(vararg args: String, checker: RunResult.() -> Unit): ProjectRunnerImpl { val argsList = listOf(*args) - projects.forEach { (slice, project) -> project.runGradle(argsList, slice, checker) } + projects.forEach { (slice, project) -> + try { + project.runGradle(argsList, checker) + } catch (e: Throwable) { + throw AssertionError("Assertion error occurred in test for project $slice", e) + } + } return this } +} - private fun File.runGradle(args: List, slice: ProjectSlice, checker: RunResult.() -> Unit) { - try { - val buildResult = GradleRunner.create() - .withProjectDir(this) - .withPluginClasspath() - .addPluginTestRuntimeClasspath() - .withArguments(args) - .build() - - RunResultImpl(buildResult, slice, this).apply { checkIntellijErrors() }.apply(checker) - } catch (e: Throwable) { - throw AssertionError("Assertion error occurred in test for project $slice", e) - } +internal class SingleProjectRunnerImpl(private val projectDir: File) : ProjectRunner { + override fun run(vararg args: String, checker: RunResult.() -> Unit): SingleProjectRunnerImpl { + projectDir.runGradle(listOf(*args), checker) + return this } } +private fun File.runGradle(args: List, checker: RunResult.() -> Unit) { + val buildResult = GradleRunner.create() + .withProjectDir(this) + .withPluginClasspath() + .addPluginTestRuntimeClasspath() + .withArguments(args) + .build() + + RunResultImpl(buildResult, this).apply { checkIntellijErrors() }.apply(checker) +} + private fun GradleRunner.addPluginTestRuntimeClasspath() = apply { val classpathFile = File(System.getProperty("plugin-classpath")) if (!classpathFile.exists()) { @@ -45,15 +54,42 @@ private fun GradleRunner.addPluginTestRuntimeClasspath() = apply { } -private class RunResultImpl(private val result: BuildResult, private val slice: ProjectSlice, private val dir: File) : - RunResult { +private class RunResultImpl(private val result: BuildResult, private val dir: File) : RunResult { val buildDir: File = File(dir, "build") - override val engine: CoverageEngine = slice.engine ?: CoverageEngine.INTELLIJ - override val projectType: ProjectType = slice.type + private val buildScriptFile: File = buildFile() + private val buildScript: String by lazy { buildScriptFile.readText() } + + override val engine: CoverageEngine by lazy { + if (buildScript.contains("set(kotlinx.kover.api.CoverageEngine.JACOCO)")) { + CoverageEngine.JACOCO + } else { + CoverageEngine.INTELLIJ + } + } + + override val projectType: ProjectType by lazy { + if (buildScriptFile.name.substringAfterLast(".") == "kts") { + if (buildScript.contains("""kotlin("jvm")""")) { + ProjectType.KOTLIN_JVM + } else if (buildScript.contains("""kotlin("multiplatform")""")) { + ProjectType.KOTLIN_MULTIPLATFORM + } else { + throw IllegalArgumentException("Impossible to determine the type of project") + } + } else { + if (buildScript.contains("""id 'org.jetbrains.kotlin.jvm'""")) { + ProjectType.KOTLIN_JVM + } else if (buildScript.contains("""id 'org.jetbrains.kotlin.multiplatform'""")) { + ProjectType.KOTLIN_MULTIPLATFORM + } else { + throw IllegalArgumentException("Impossible to determine the type of project") + } + } + } override fun submodule(name: String, checker: RunResult.() -> Unit) { - RunResultImpl(result, slice, File(dir, name)).also(checker) + RunResultImpl(result, File(dir, name)).also(checker) } override fun output(checker: String.() -> Unit) { @@ -74,6 +110,13 @@ private class RunResultImpl(private val result: BuildResult, private val slice: result.task(taskPath)?.outcome?.checker() ?: throw IllegalArgumentException("Task '$taskPath' not found in build result") } + + private fun buildFile(): File { + val file = File(dir, "build.gradle") + if (file.exists() && file.isFile) return file + + return File(dir, "build.gradle.kts") + } } diff --git a/src/functionalTest/templates/projects/different-plugins/build.gradle.kts b/src/functionalTest/templates/projects/different-plugins/build.gradle.kts new file mode 100644 index 00000000..095535e8 --- /dev/null +++ b/src/functionalTest/templates/projects/different-plugins/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("org.jetbrains.kotlinx.kover") version "DEV" +} + +repositories { + mavenCentral() +} diff --git a/src/functionalTest/templates/projects/different-plugins/settings.gradle.kts b/src/functionalTest/templates/projects/different-plugins/settings.gradle.kts new file mode 100644 index 00000000..d0d34e3b --- /dev/null +++ b/src/functionalTest/templates/projects/different-plugins/settings.gradle.kts @@ -0,0 +1,3 @@ +rootProject.name = "different-plugins" + +include(":submodule-multiplatform") diff --git a/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/build.gradle.kts b/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/build.gradle.kts new file mode 100644 index 00000000..15fecee9 --- /dev/null +++ b/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + kotlin("multiplatform") +} + +repositories { + mavenCentral() +} + +kotlin { + jvm() + + sourceSets { + commonTest { + dependencies { + implementation("org.jetbrains.kotlin:kotlin-test") + } + } + } +} diff --git a/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/commonMain/kotlin/CommonClass.kt b/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/commonMain/kotlin/CommonClass.kt new file mode 100644 index 00000000..ad84c0db --- /dev/null +++ b/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/commonMain/kotlin/CommonClass.kt @@ -0,0 +1,7 @@ +package org.jetbrains + +class CommonClass { + fun function() { + println("Function") + } +} diff --git a/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/commonTest/kotlin/CommonTest.kt b/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/commonTest/kotlin/CommonTest.kt new file mode 100644 index 00000000..cfa803b9 --- /dev/null +++ b/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/commonTest/kotlin/CommonTest.kt @@ -0,0 +1,10 @@ +package org.jetbrains + +import kotlin.test.Test + +class CommonTest { + @Test + fun testCommon() { + CommonClass().function() + } +} diff --git a/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/jvmMain/kotlin/JvmClass.kt b/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/jvmMain/kotlin/JvmClass.kt new file mode 100644 index 00000000..a00235ca --- /dev/null +++ b/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/jvmMain/kotlin/JvmClass.kt @@ -0,0 +1,7 @@ +package org.jetbrains + +class JvmClass { + fun jvmFunction() { + println("JVM function") + } +} diff --git a/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/jvmTest/kotlin/JvmTest.kt b/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/jvmTest/kotlin/JvmTest.kt new file mode 100644 index 00000000..ab38a613 --- /dev/null +++ b/src/functionalTest/templates/projects/different-plugins/submodule-multiplatform/src/jvmTest/kotlin/JvmTest.kt @@ -0,0 +1,10 @@ +package org.jetbrains + +import kotlin.test.Test + +class JvmTest { + @Test + fun jvmTest() { + JvmClass().jvmFunction() + } +} diff --git a/src/main/kotlin/kotlinx/kover/adapters/KotlinMultiplatformPluginAdapter.kt b/src/main/kotlin/kotlinx/kover/adapters/KotlinMultiplatformPluginAdapter.kt index 4d7a753f..4830b8b2 100644 --- a/src/main/kotlin/kotlinx/kover/adapters/KotlinMultiplatformPluginAdapter.kt +++ b/src/main/kotlin/kotlinx/kover/adapters/KotlinMultiplatformPluginAdapter.kt @@ -4,8 +4,11 @@ package kotlinx.kover.adapters +import groovy.lang.* import kotlinx.kover.adapters.api.* 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.* @@ -15,15 +18,23 @@ class KotlinMultiplatformPluginAdapter : CompilationPluginAdapter { return safe(project) { this.plugins.findPlugin("kotlin-multiplatform") ?: return@safe PluginDirs(emptyList(), emptyList()) - val extension = project.extensions.findByType( - KotlinMultiplatformExtension::class.java - ) ?: return@safe PluginDirs(emptyList(), emptyList()) + 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 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 sourceDirs = + compilations.asSequence().flatMap { it.allKotlinSourceSets }.map { it.kotlin }.flatMap { it.srcDirs } + .toList() val outputDirs = compilations.asSequence().flatMap { it.output.classesDirs }.toList() @@ -32,4 +43,39 @@ class KotlinMultiplatformPluginAdapter : CompilationPluginAdapter { } } + /* + * If Kotlin Multiplatform plugin if the plugin applied not in the same project where Kover is applied, + * then its classes are in another class loader, and they are not available to kover. + * Therefore, the only way to work with such an object is to use reflection. + */ + @Suppress("UNCHECKED_CAST") + fun findByReflection(project: Project): PluginDirs { + val extension = + project.extensions.findByName("kotlin")?.let { BeanDynamicObject(it) } ?: return PluginDirs( + emptyList(), + emptyList() + ) + + val targets = (extension.getProperty("targets") as NamedDomainObjectCollection).filter { + val platformTypeName = (it.getProperty("platformType") as Named).name + platformTypeName == "jvm" || platformTypeName == "androidJvm" + } + + val compilations = targets.flatMap { + (it.getProperty("compilations") as NamedDomainObjectCollection) + .filter { c -> c.name != "test" } + }.map { BeanDynamicObject(it) } + + val sourceDirs = compilations.asSequence() + .flatMap { it.getProperty("allKotlinSourceSets") as Collection<*> } + .map { BeanDynamicObject(it).getProperty("kotlin") as SourceDirectorySet }.flatMap { it.srcDirs } + .toList() + + val outputDirs = + compilations.asSequence().map { BeanDynamicObject(it.getProperty("output")) } + .flatMap { it.getProperty("classesDirs") as FileCollection }.toList() + + return PluginDirs(sourceDirs, outputDirs) + } + }