I am pulling testballoon 0.8.2-K2.2.21 into a mult...
# testballoon
r
I am pulling testballoon 0.8.2-K2.2.21 into a multiplatform project (with android library and KSP), and am getting this error in the consuming project:
Copy code
e: File '/home/raman/source/myproject/shared/build/generated/testBalloon/src/commonTest/kotlin/JvmEntryPoint.kt' can be a part of only one module, but is listed as a source for both `androidUnitTestDebug` and `commonTest`, please check you -Xfragment-sources options.
o
Could it be that you are using
junit-vintage
? If so, does this help?
r
Not using junit vintage -- tried it anyway and does not help
o
OK, then it could be related to incremental compilation: Would running
gradlew clean
first make the error disappear?
r
Gradle
clean
then
build
also fails. When running at the terminal rather than in the IDE I see this:
Copy code
* What went wrong:
A problem was found with the configuration of task ':shared:generateDebugAndroidTestLintModel' (type 'LintModelWriterTask').
  - Gradle detected a problem with the following location: '/home/raman/source/devhaus/mobile-sdk/shared/build/generated/testBalloon/src/commonTest'.
    
    Reason: Task ':shared:generateDebugAndroidTestLintModel' uses this output of task ':shared:generateTestBalloonEntryPoint' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.
    
    Possible solutions:
      1. Declare task ':shared:generateTestBalloonEntryPoint' as an input of ':shared:generateDebugAndroidTestLintModel'.
      2. Declare an explicit dependency on ':shared:generateTestBalloonEntryPoint' from ':shared:generateDebugAndroidTestLintModel' using Task#dependsOn.
      3. Declare an explicit dependency on ':shared:generateTestBalloonEntryPoint' from ':shared:generateDebugAndroidTestLintModel' using Task#mustRunAfter.
    
    For more information, please refer to <https://docs.gradle.org/8.14.1/userguide/validation_problems.html#implicit_dependency> in the Gradle documentation.

* Try:
> Declare task ':shared:generateTestBalloonEntryPoint' as an input of ':shared:generateDebugAndroidTestLintModel'
> Declare an explicit dependency on ':shared:generateTestBalloonEntryPoint' from ':shared:generateDebugAndroidTestLintModel' using Task#dependsOn
> Declare an explicit dependency on ':shared:generateTestBalloonEntryPoint' from ':shared:generateDebugAndroidTestLintModel' using Task#mustRunAfter
> Run with --scan to get full insights.
But that seems more like a symptom of the problem than the problem itself
o
Yep. It's a source set hierarchy configuration issue. Let's find out the root cause. Could you add the code below to your build script for the
shared
module, run
testBalloonDiagnostics
and send the output (here or via DM)?
Copy code
afterEvaluate {
    val taskName = "testBalloonDiagnostics"
    val kotlinMultiplatformExtension =
        runCatching { extensions.findByName("kotlin") as? KotlinMultiplatformExtension }.getOrNull()

    tasks.register(taskName) {
        group = "help"
        description = "Shows diagnostics for the TestBalloon test framework"

        fun <Result : Any> safeProvider(value: () -> Result?): Provider<Result> = project.provider {
            try {
                value()
            } catch (throwable: Throwable) {
                println("WARNING: Problem configuring task step in '$taskName': $throwable")
                throwable.printStackTrace()
                null
            }
        }

        fun doLastSafely(block: () -> Unit) = doLast {
            try {
                block()
            } catch (throwable: Throwable) {
                println("WARNING: Problem executing task step in '$taskName': $throwable")
                throwable.printStackTrace()
            }
        }

        val projectPath = project.path
        val gradleVersion = gradle.gradleVersion

        doLastSafely {
            println()
            println("--- BEGIN TestBalloon diagnostics for $projectPath ---------------------------------")
            println()
            println("Project path: $projectPath")
            println("Gradle version: $gradleVersion")
        }

        val buildScriptPluginsBlock = safeProvider {
            val buildScriptText = project.buildscript.sourceFile?.readText()
            Regex("""plugins \{.*?\}""", option = RegexOption.DOT_MATCHES_ALL).find(buildScriptText ?: "")?.value
                ?: "no plugins block found in ${project.buildscript.sourceFile?.path ?: "(unknown build script)"}"
        }

        doLastSafely {
            println()
            println("Build script plugins block:")
            println(buildScriptPluginsBlock.get().prependIndent("  "))
        }

        val catalogsExtension = safeProvider {
            project.extensions.getByType(VersionCatalogsExtension::class.java)
        }
        val versionCatalogNames = catalogsExtension.map { it.catalogNames.toList() }
        val relevantPlugins = safeProvider {
            val catalogsExtension = catalogsExtension.get()
            val versionCatalogs = catalogsExtension.catalogNames.map { catalogsExtension.named(it) }
            val catalogPluginVersions = versionCatalogs.flatMap { catalog ->
                catalog.pluginAliases.map {
                    val (artifactId, version) = catalog.findPlugin(it).get().get().toString().split(':', limit = 2)
                    artifactId to version
                }
            }.toMap()

            mapOf(
                Constants.COMPILER_PLUGIN_NAME to null,
                "org.jetbrains.kotlin.jvm" to null,
                "org.jetbrains.kotlin.multiplatform" to null,
                "com.android.application" to "Sharing a module with KMP is unsupported.",
                "com.android.library" to "deprecated",
                "org.jetbrains.kotlin.android" to "Must not be used with built-in Kotlin of AGP 9.",
                "com.android.kotlin.multiplatform.library" to null
            ).mapNotNull { (pluginId, notice) ->
                pluginManager.findPlugin(pluginId)?.let {
                    val pluginAndVersion = "$pluginId:${catalogPluginVersions[pluginId]}"
                    if (notice != null) "$pluginAndVersion  (*) $notice" else pluginAndVersion
                }
            }
        }

        doLast {
            println()
            println("Gradle plugins (excerpt, versions from catalog(s) ${versionCatalogNames.get()}):")
            println(relevantPlugins.get().joinToString(separator = "\n  ", prefix = "  "))
        }

        val relevantProperties = safeProvider {
            providers.gradlePropertiesPrefixedBy("testBalloon.").get() +
                providers.gradlePropertiesPrefixedBy("org.gradle.").get() +
                providers.gradlePropertiesPrefixedBy("kotlin.").get()
        }

        doLastSafely {
            println()
            println("Gradle properties (excerpt):")
            relevantProperties.get().toSortedMap(String::compareTo).forEach { (name, value) ->
                println("  $name=$value")
            }
        }

        val testBalloonVersionsUsage = safeProvider {
            configurations
                .filter { it.isCanBeResolved }
                .flatMap { configuration ->
                    val visitedComponentIds = mutableSetOf<ComponentIdentifier>()

                    fun ResolvedComponentResult.withAllChildren(): Sequence<ResolvedComponentResult> = sequence {
                        if (!visitedComponentIds.add(id)) return@sequence
                        yield(this@withAllChildren)
                        dependencies
                            .filterIsInstance<ResolvedDependencyResult>()
                            .forEach { dep ->
                                yieldAll(dep.selected.withAllChildren())
                            }
                    }

                    val rootComponent = configuration.incoming.resolutionResult.rootComponent.get()
                    val testBalloonVersions = rootComponent.dependencies
                        .filterIsInstance<ResolvedDependencyResult>()
                        .flatMap { resolvedDependencyResult ->
                            resolvedDependencyResult.selected.withAllChildren()
                        }
                        .mapNotNull { component ->
                            (component.id as? ModuleComponentIdentifier)
                                ?.takeIf { it.group == Constants.ARTIFACT_GROUP_ID }
                                ?.let { VersionNumber.parse(it.version) }
                        }
                        .toSet()

                    testBalloonVersions.map {
                        Pair(it, configuration.name)
                    }
                }
                .groupBy({ it.first }) {
                    it.second
                }
        }

        doLastSafely {
            val testBalloonVersionsUsage = testBalloonVersionsUsage.get()

            println()
            println("TestBalloon dependencies:")
            for ((version, configurations) in testBalloonVersionsUsage) {
                println("  $version: ${configurations.joinToString(limit = 3)}")
            }
            when (testBalloonVersionsUsage.size) {
                0 -> println("  (*) External TestBalloon dependencies are not present in this module.")
                1 -> {}
                else -> println("  (*) TestBalloon dependencies with different versions are unsupported and may fail.")
            }
        }

        val testSourceSetsDiagram = safeProvider {
            kotlinMultiplatformExtension?.let { kotlin ->
                val testSourceSetRegex = Regex("[tT]est")

                val testSourceSets = kotlin.sourceSets.filter { testSourceSetRegex.containsMatchIn(it.name) }

                buildString {
                    appendLine()
                    appendLine("Diagram for display on <https://mermaid.live/>")
                    appendLine()
                    appendLine("---")
                    appendLine("title: KMP test source sets of ${project.path}")
                    appendLine("---")
                    appendLine("classDiagram")
                    for (sourceSet in testSourceSets) {
                        appendLine("    class ${sourceSet.name}")
                    }
                    for (sourceSet in testSourceSets) {
                        for (dependsOnSourceSet in sourceSet.dependsOn.map { it.name }) {
                            appendLine("    $dependsOnSourceSet <|-- ${sourceSet.name}")
                        }
                    }
                }
            }
        }

        doLastSafely {
            testSourceSetsDiagram.orNull?.let { print(it) }
        }

        doLastSafely {
            println()
            println("--- END TestBalloon diagnostics for $projectPath -----------------------------------")
        }
    }
}
r
What is the correct import for
Constants
?
o
Ah sorry, time travel, the script was copied from the current dev version and is no longer self-contained. Let me send you a better version...
👍 1
This should work (tested with 2.3.20-RC, but hopefully works on 2.2.21 as well):
Copy code
afterEvaluate {
    val taskName = "testBalloonDiagnostics"
    val kotlinMultiplatformExtension =
        runCatching { extensions.findByName("kotlin") as? KotlinMultiplatformExtension }.getOrNull()

    tasks.register(taskName) {
        group = "help"
        description = "Shows diagnostics for the TestBalloon test framework"

        fun <Result : Any> safeProvider(value: () -> Result?): Provider<Result> = project.provider {
            try {
                value()
            } catch (throwable: Throwable) {
                println("WARNING: Problem configuring task step in '$taskName': $throwable")
                throwable.printStackTrace()
                null
            }
        }

        fun doLastSafely(block: () -> Unit) = doLast {
            try {
                block()
            } catch (throwable: Throwable) {
                println("WARNING: Problem executing task step in '$taskName': $throwable")
                throwable.printStackTrace()
            }
        }

        val projectPath = project.path
        val gradleVersion = gradle.gradleVersion

        doLastSafely {
            println()
            println("--- BEGIN TestBalloon diagnostics for $projectPath ---------------------------------")
            println()
            println("Project path: $projectPath")
            println("Gradle version: $gradleVersion")
        }

        val buildScriptPluginsBlock = safeProvider {
            val buildScriptText = project.buildscript.sourceFile?.readText()
            Regex("""plugins \{.*?\}""", option = RegexOption.DOT_MATCHES_ALL).find(buildScriptText ?: "")?.value
                ?: "no plugins block found in ${project.buildscript.sourceFile?.path ?: "(unknown build script)"}"
        }

        doLastSafely {
            println()
            println("Build script plugins block:")
            println(buildScriptPluginsBlock.get().prependIndent("  "))
        }

        val catalogsExtension = safeProvider {
            project.extensions.getByType(VersionCatalogsExtension::class.java)
        }
        val versionCatalogNames = catalogsExtension.map { it.catalogNames.toList() }
        val relevantPlugins = safeProvider {
            val catalogsExtension = catalogsExtension.get()
            val versionCatalogs = catalogsExtension.catalogNames.map { catalogsExtension.named(it) }
            val catalogPluginVersions = versionCatalogs.flatMap { catalog ->
                catalog.pluginAliases.map {
                    val (artifactId, version) = catalog.findPlugin(it).get().get().toString().split(':', limit = 2)
                    artifactId to version
                }
            }.toMap()

            mapOf(
                "de.infix.testBalloon" to null,
                "org.jetbrains.kotlin.jvm" to null,
                "org.jetbrains.kotlin.multiplatform" to null,
                "com.android.application" to "Sharing a module with KMP is unsupported.",
                "com.android.library" to "deprecated",
                "org.jetbrains.kotlin.android" to "Must not be used with built-in Kotlin of AGP 9.",
                "com.android.kotlin.multiplatform.library" to null
            ).mapNotNull { (pluginId, notice) ->
                pluginManager.findPlugin(pluginId)?.let {
                    val pluginAndVersion = "$pluginId:${catalogPluginVersions[pluginId]}"
                    if (notice != null) "$pluginAndVersion  (*) $notice" else pluginAndVersion
                }
            }
        }

        doLast {
            println()
            println("Gradle plugins (excerpt, versions from catalog(s) ${versionCatalogNames.get()}):")
            println(relevantPlugins.get().joinToString(separator = "\n  ", prefix = "  "))
        }

        val relevantProperties = safeProvider {
            providers.gradlePropertiesPrefixedBy("testBalloon.").get() +
                providers.gradlePropertiesPrefixedBy("org.gradle.").get() +
                providers.gradlePropertiesPrefixedBy("kotlin.").get()
        }

        doLastSafely {
            println()
            println("Gradle properties (excerpt):")
            relevantProperties.get().toSortedMap(String::compareTo).forEach { (name, value) ->
                println("  $name=$value")
            }
        }

        val testBalloonVersionsUsage = safeProvider {
            configurations
                .filter { it.isCanBeResolved }
                .flatMap { configuration ->
                    val visitedComponentIds = mutableSetOf<ComponentIdentifier>()

                    fun ResolvedComponentResult.withAllChildren(): Sequence<ResolvedComponentResult> = sequence {
                        if (!visitedComponentIds.add(id)) return@sequence
                        yield(this@withAllChildren)
                        dependencies
                            .filterIsInstance<ResolvedDependencyResult>()
                            .forEach { dep ->
                                yieldAll(dep.selected.withAllChildren())
                            }
                    }

                    val rootComponent = configuration.incoming.resolutionResult.rootComponent.get()
                    val testBalloonVersions = rootComponent.dependencies
                        .filterIsInstance<ResolvedDependencyResult>()
                        .flatMap { resolvedDependencyResult ->
                            resolvedDependencyResult.selected.withAllChildren()
                        }
                        .mapNotNull { component ->
                            (component.id as? ModuleComponentIdentifier)
                                ?.takeIf { it.group == "de.infix.testBalloon" }
                                ?.let { VersionNumber.parse(it.version) }
                        }
                        .toSet()

                    testBalloonVersions.map {
                        Pair(it, configuration.name)
                    }
                }
                .groupBy({ it.first }) {
                    it.second
                }
        }

        doLastSafely {
            val testBalloonVersionsUsage = testBalloonVersionsUsage.get()

            println()
            println("TestBalloon dependencies:")
            for ((version, configurations) in testBalloonVersionsUsage) {
                println("  $version: ${configurations.joinToString(limit = 3)}")
            }
            when (testBalloonVersionsUsage.size) {
                0 -> println("  (*) External TestBalloon dependencies are not present in this module.")
                1 -> {}
                else -> println("  (*) TestBalloon dependencies with different versions are unsupported and may fail.")
            }
        }

        val testSourceSetsDiagram = safeProvider {
            kotlinMultiplatformExtension?.let { kotlin ->
                val testSourceSetRegex = Regex("[tT]est")

                val testSourceSets = kotlin.sourceSets.filter { testSourceSetRegex.containsMatchIn(it.name) }

                buildString {
                    appendLine()
                    appendLine("Diagram for display on <https://mermaid.live/>")
                    appendLine()
                    appendLine("---")
                    appendLine("title: KMP test source sets of ${project.path}")
                    appendLine("---")
                    appendLine("classDiagram")
                    for (sourceSet in testSourceSets) {
                        appendLine("    class ${sourceSet.name}")
                    }
                    for (sourceSet in testSourceSets) {
                        for (dependsOnSourceSet in sourceSet.dependsOn.map { it.name }) {
                            appendLine("    $dependsOnSourceSet <|-- ${sourceSet.name}")
                        }
                    }
                }
            }
        }

        doLastSafely {
            testSourceSetsDiagram.orNull?.let { print(it) }
        }

        doLastSafely {
            println()
            println("--- END TestBalloon diagnostics for $projectPath -----------------------------------")
        }
    }
}
r
Sent via DM
👍 1
For future readers: solution (thanks to @Oliver.O) was migrating from the old Android plugin
com.android.library
to the new multiplatform plugin
com.android.kotlin.multiplatform.library
(see https://developer.android.com/kotlin/multiplatform/plugin).
kodee happy 1