Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
5 changes: 3 additions & 2 deletions kover-cli/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

/*
Expand Down Expand Up @@ -50,8 +51,8 @@ dependencies {
}

tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "1.8"
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.kover.gradle.plugin.test.functional.cases

import kotlinx.kover.gradle.plugin.test.functional.framework.checker.CheckerContext
import kotlinx.kover.gradle.plugin.test.functional.framework.starter.TemplateTest

internal class AndroidKmpLibTests {
@TemplateTest("android-kmp-library", ["koverXmlReport"])
fun CheckerContext.testPresence() {
xmlReport {
classCounter("org.jetbrains.ExampleClass").assertPresent()
classCounter("org.jetbrains.AndroidClass").assertPresent()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

plugins {
kotlin("multiplatform") version "2.2.0"
id("com.android.kotlin.multiplatform.library") version "8.12.0"
id ("org.jetbrains.kotlinx.kover") version "0.9.1"
}

kotlin {
androidLibrary {
namespace = "org.jetbrains.kover.kml.lib"
compileSdk = 33
minSdk = 24

withJava()
withDeviceTestBuilder {
sourceSetTreeName = "test"
}

}

jvm()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}

rootProject.name = "android-kmp-library"

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.jetbrains

class AndroidClass {
fun a() {
println("Hello World!")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.jetbrains

class ExampleClass {
fun a() {
println("Hello World!")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@ package kotlinx.kover.gradle.plugin.locators
import kotlinx.kover.gradle.plugin.appliers.origin.AndroidVariantOrigin
import kotlinx.kover.gradle.plugin.appliers.origin.JvmVariantOrigin
import kotlinx.kover.gradle.plugin.appliers.origin.AllVariantOrigins
import kotlinx.kover.gradle.plugin.appliers.origin.CompilationDetails
import kotlinx.kover.gradle.plugin.appliers.origin.LanguageCompilation
import kotlinx.kover.gradle.plugin.commons.*
import kotlinx.kover.gradle.plugin.util.*
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.testing.*
import org.gradle.kotlin.dsl.*
import java.io.File
import kotlin.collections.filter
import kotlin.collections.toSet

/*
Since the Kover and Kotlin Multiplatform plug-ins can be in different class loaders (declared in different projects), the plug-ins are stored in a single instance in the loader of the project where the plug-in was used for the first time.
Expand Down Expand Up @@ -42,29 +51,59 @@ private fun Project.locateAndroidVariants(kotlinExtension: DynamicBean): List<An
}

private fun Project.locateJvmVariant(kotlinExtension: DynamicBean): JvmVariantOrigin? {
// only one JVM target is allowed, so we can take the first one
val jvmTarget = kotlinExtension.beanCollection("targets").firstOrNull {
val jvmTargets = kotlinExtension.beanCollection("targets").filter {
it["platformType"].value<String>("name") == "jvm"
} ?: return null

return extractJvmVariant(jvmTarget)
}


private fun Project.extractJvmVariant(target: DynamicBean): JvmVariantOrigin {
val targetName = target.value<String>("targetName")
}
if (jvmTargets.isEmpty()) {
return null
}

val names = jvmTargets.map { it.value<String>("targetName") }.toSet()
val tests = tasks.withType<Test>().matching {
it.hasSuperclass("KotlinJvmTest") && it.bean().value<String>("targetName") == targetName
it.hasSuperclass("KotlinJvmTest") && it.bean().value("targetName") in names
}

val compilations = provider {
target.beanCollection("compilations").jvmCompilations {
// exclude java classes from report. Expected java class files are placed in directories like
// build/classes/java/main
it.parentFile.name == "java"
}
val compilations: Provider<Map<String, CompilationDetails>> = provider {
jvmTargets.extractPlainJvmVariant() + jvmTargets.extractKmpAndroidLibraryVariant()
}

return JvmVariantOrigin(tests, compilations)
}


private fun List<DynamicBean>.extractPlainJvmVariant(): Map<String, CompilationDetails> {
return singleOrNull {
it.origin.hasSuperclass("KotlinJvmTarget")
}?.beanCollection("compilations")?.jvmCompilations {
// exclude java classes from report. Expected java class files are placed in directories like
// build/classes/java/main
it.parentFile.name == "java"
} ?: emptyMap()
}


private fun List<DynamicBean>.extractKmpAndroidLibraryVariant(): Map<String, CompilationDetails> {
return singleOrNull {
it.origin.hasSuperclass("KotlinMultiplatformAndroidLibraryTargetImpl")
}?.beanCollection("compilations")
?.filter {
// exclude test compilations
val compilationName = it.value<String>("name")
compilationName != "test" && !compilationName.endsWith("Test")
}
?.associate { compilation ->
val name = compilation.value<String>("name")
val sources = compilation.beanCollection("allKotlinSourceSets").flatMap<DynamicBean, File> {
it["kotlin"].valueCollection("srcDirs")
}.toSet()

val kotlinOutputs = compilation["output"].value<ConfigurableFileCollection>("classesDirs").files.toSet()
val kotlinCompileTask = compilation.valueOrNull<TaskProvider<Task>?>("compileTaskProvider")?.orNull
val kotlin = LanguageCompilation(kotlinOutputs, kotlinCompileTask)
// at the moment, there is no way to get a task and directives for javac from the compilation
val java = kotlin

// since we place compilations from different targets in one map, we should separate it because the original names may overlap (like `main`)
"${name}AndroidLibrary" to CompilationDetails(sources, kotlin, java)
} ?: emptyMap()
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.gradle.internal.metaobject.*

internal fun Any.bean(): DynamicBean = DynamicBean(this)

internal class DynamicBean(origin: Any) {
internal class DynamicBean(val origin: Any) {
private val wrappedOrigin = BeanDynamicObject(origin)

operator fun get(name: String): DynamicBean = bean(name)
Expand Down