From 1b0b09dd834fe1ec735a240d305201fb1f6d0a60 Mon Sep 17 00:00:00 2001 From: "sebastian.sellmair" Date: Tue, 19 Jul 2022 13:30:18 +0200 Subject: [PATCH] [Gradle][MPP] Implement KMP <-> AGP compatibility check ^KT-53138 Verification Pending --- ...oidGradlePluginCompatibilityHealthCheck.kt | 132 ++++++++++ ...latformProjectConfigurationHealthChecks.kt | 5 +- .../gradle/plugin/KotlinPluginWrapper.kt | 12 + .../kotlin/gradle/plugin/KotlinProperties.kt | 5 + .../kotlin/gradle/utils/androidPluginIds.kt | 6 + ...radlePluginCompatibilityHealthCheckTest.kt | 247 ++++++++++++++++++ 6 files changed, 404 insertions(+), 3 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheckTest.kt diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.kt new file mode 100644 index 00000000000..aa3d271b6d4 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.plugin + +import com.android.Version +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider +import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ANDROID_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN +import org.jetbrains.kotlin.gradle.utils.androidPluginIds +import org.jetbrains.kotlin.gradle.utils.findAppliedAndroidPluginIdOrNull +import org.jetbrains.kotlin.gradle.utils.getOrPutRootProjectProperty +import org.jetbrains.kotlin.gradle.utils.runProjectConfigurationHealthCheck +import java.util.concurrent.atomic.AtomicBoolean + +internal object KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck { + + const val MIN_SUPPORTED_AGP_MAJOR_VERSION = 4 + const val MAX_SUPPORTED_AGP_MAJOR_VERSION = 7 + + /** + * Used to store previously emitted messages in the build + */ + const val PROPERTY_KEY_EMITTED_MESSAGES = "KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.emittedMessages" + + /** + * Used to store project paths that executed this health check + */ + const val PROPERTY_KEY_EXECUTED_PROJECT_PATHS = "KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.executedProjects" + + data class AndroidGradlePluginVersion(val major: Int, val minor: Int) + + interface AndroidGradlePluginStringProvider { + fun getAndroidGradlePluginString(): String? + + object Default : AndroidGradlePluginStringProvider { + override fun getAndroidGradlePluginString(): String? = + runCatching { Version.ANDROID_GRADLE_PLUGIN_VERSION }.getOrElse { return null } + } + } + + interface AndroidGradlePluginVersionParser { + fun parseVersionString(version: String): AndroidGradlePluginVersion? + + object Default : AndroidGradlePluginVersionParser { + private val versionRegex = Regex("""(\d+)\.(\d+)(\.\d+)?(.*)""") + + override fun parseVersionString(version: String): AndroidGradlePluginVersion? { + val matchResult = versionRegex.matchEntire(version) ?: return null + val major = matchResult.groups[1]?.value?.toIntOrNull() ?: return null + val minor = matchResult.groups[2]?.value?.toIntOrNull() ?: return null + return AndroidGradlePluginVersion(major, minor) + } + } + } + + fun Project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheckWhenAndroidIsApplied( + warningLogger: (warningMessage: String) -> Unit = project.logger::warn, + androidGradlePluginStringProvider: AndroidGradlePluginStringProvider = AndroidGradlePluginStringProvider.Default, + androidGradlePluginVersionParser: AndroidGradlePluginVersionParser = AndroidGradlePluginVersionParser.Default + ) { + val emittedMessages = project.getOrPutRootProjectProperty(PROPERTY_KEY_EMITTED_MESSAGES) { mutableSetOf() } + + val deduplicateWarningMessageLogger = { message: String -> + if (emittedMessages.add(message)) { + warningLogger(message) + } + } + + val executed = AtomicBoolean(false) + androidPluginIds.forEach { id -> + plugins.withId(id) { + if (!executed.getAndSet(true)) { + runMultiplatformAndroidGradlePluginCompatibilityHealthCheck( + deduplicateWarningMessageLogger, androidGradlePluginStringProvider, androidGradlePluginVersionParser + ) + } + } + } + } + + fun Project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck( + warningLogger: (warningMessage: String) -> Unit = project.logger::warn, + androidGradlePluginStringProvider: AndroidGradlePluginStringProvider = AndroidGradlePluginStringProvider.Default, + androidGradlePluginVersionParser: AndroidGradlePluginVersionParser = AndroidGradlePluginVersionParser.Default + ) = runProjectConfigurationHealthCheck check@{ + if (project.kotlinPropertiesProvider.ignoreAndroidGradlePluginCompatibilityIssues) return@check + getOrPutRootProjectProperty(PROPERTY_KEY_EXECUTED_PROJECT_PATHS) { mutableSetOf() }.add(path) + + /* Return when no android plugin is applied */ + findAppliedAndroidPluginIdOrNull() ?: return@check + + val androidGradlePluginVersionString = androidGradlePluginStringProvider.getAndroidGradlePluginString() + ?: return warningLogger(Messages.FAILED_GETTING_ANDROID_GRADLE_PLUGIN_VERSION_STRING) + + val androidGradlePluginVersion = androidGradlePluginVersionParser.parseVersionString(androidGradlePluginVersionString) + ?: return warningLogger(Messages.failedParsingAndroidGradlePluginVersion(androidGradlePluginVersionString)) + + + if (androidGradlePluginVersion.major < MIN_SUPPORTED_AGP_MAJOR_VERSION) { + warningLogger(Messages.androidGradlePluginVersionTooLow(androidGradlePluginVersionString)) + } + + if (androidGradlePluginVersion.major > MAX_SUPPORTED_AGP_MAJOR_VERSION) { + warningLogger(Messages.androidGradlePluginVersionTooHigh(androidGradlePluginVersionString)) + } + } + + object Messages { + const val FAILED_GETTING_ANDROID_GRADLE_PLUGIN_VERSION_STRING = "Failed to get the version of the applied Android Gradle Plugin" + + fun failedParsingAndroidGradlePluginVersion(androidGradlePluginVersionString: String) = + "w: Failed to parse the version of the applied Android Gradle Plugin: $androidGradlePluginVersionString" + + fun androidGradlePluginVersionTooLow(androidGradlePluginVersionString: String) = createCompatibilityWarningMessage( + "The applied Android Gradle Plugin version ($androidGradlePluginVersionString) is lower than the minimum supported" + ) + + fun androidGradlePluginVersionTooHigh(androidGradlePluginVersionString: String) = createCompatibilityWarningMessage( + "The applied Android Gradle Plugin version ($androidGradlePluginVersionString) is higher than the maximum supported" + ) + + private fun createCompatibilityWarningMessage(warning: String) = buildString { + appendLine("w: Kotlin Multiplatform <-> Android Gradle Plugin compatibility issue: $warning") + appendLine("Minimum supported Android Gradle Plugin version: $MIN_SUPPORTED_AGP_MAJOR_VERSION.0") + appendLine("Maximum supported Android Gradle Plugin version: $MAX_SUPPORTED_AGP_MAJOR_VERSION.+") + appendLine("To suppress this message add $KOTLIN_MPP_ANDROID_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN=true to your gradle.properties") + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinMultiplatformProjectConfigurationHealthChecks.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinMultiplatformProjectConfigurationHealthChecks.kt index 8dfed101d71..c29dfe273f5 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinMultiplatformProjectConfigurationHealthChecks.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinMultiplatformProjectConfigurationHealthChecks.kt @@ -17,7 +17,7 @@ import org.jetbrains.kotlin.gradle.targets.android.findAndroidTarget import org.jetbrains.kotlin.gradle.targets.native.internal.CInteropCommonizerDependent import org.jetbrains.kotlin.gradle.targets.native.internal.from import org.jetbrains.kotlin.gradle.targets.native.internal.isAllowCommonizer -import org.jetbrains.kotlin.gradle.utils.androidPluginIds +import org.jetbrains.kotlin.gradle.utils.findAppliedAndroidPluginIdOrNull import org.jetbrains.kotlin.gradle.utils.runProjectConfigurationHealthCheck private class KotlinMultiplatformProjectConfigurationException(message: String) : Exception(message) @@ -44,8 +44,7 @@ internal fun Project.runMissingAndroidTargetProjectConfigurationHealthCheck( return@check } - val androidPluginId = androidPluginIds - .firstOrNull { androidPluginId -> plugins.findPlugin(androidPluginId) != null } ?: return@check + val androidPluginId = findAppliedAndroidPluginIdOrNull() ?: return@check if (findAndroidTarget() != null) return@check diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt index 95734e578a8..615910034cd 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt @@ -29,6 +29,8 @@ import org.jetbrains.kotlin.gradle.dsl.* import org.jetbrains.kotlin.gradle.internal.KOTLIN_COMPILER_EMBEDDABLE import org.jetbrains.kotlin.gradle.internal.KOTLIN_MODULE_GROUP import org.jetbrains.kotlin.gradle.logging.kotlinDebug +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.runMultiplatformAndroidGradlePluginCompatibilityHealthCheckWhenAndroidIsApplied import org.jetbrains.kotlin.gradle.plugin.internal.* import org.jetbrains.kotlin.gradle.plugin.internal.BasePluginConfiguration import org.jetbrains.kotlin.gradle.plugin.internal.DefaultJavaSourceSetsAccessorVariantFactory @@ -292,6 +294,11 @@ abstract class AbstractKotlinMultiplatformPluginWrapper : KotlinBasePluginWrappe override fun getPlugin(project: Project): Plugin = KotlinMultiplatformPlugin() + override fun apply(project: Project) { + super.apply(project) + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheckWhenAndroidIsApplied() + } + override val projectExtensionClass: KClass get() = KotlinMultiplatformExtension::class @@ -305,6 +312,11 @@ abstract class AbstractKotlinMultiplatformPluginWrapper : KotlinBasePluginWrappe abstract class AbstractKotlinPm20PluginWrapper( private val objectFactory: ObjectFactory ) : KotlinBasePluginWrapper() { + override fun apply(project: Project) { + super.apply(project) + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheckWhenAndroidIsApplied() + } + override fun getPlugin(project: Project): Plugin = objectFactory.newInstance(KotlinPm20GradlePlugin::class.java) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinProperties.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinProperties.kt index dfa5f1744cd..85e64863a11 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinProperties.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinProperties.kt @@ -12,6 +12,7 @@ import org.jetbrains.kotlin.gradle.dsl.NativeCacheKind import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessageOutputStreamHandler.Companion.IGNORE_TCSM_OVERFLOW import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType.Companion.jsCompilerProperty import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_ABI_SNAPSHOT +import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ANDROID_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ENABLE_CINTEROP_COMMONIZATION import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ENABLE_GRANULAR_SOURCE_SETS_METADATA import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ENABLE_OPTIMISTIC_NUMBER_COMMONIZATION @@ -192,6 +193,9 @@ internal class PropertiesProvider private constructor(private val project: Proje val ignoreAbsentAndroidMultiplatformTarget: Boolean get() = booleanProperty("kotlin.mpp.absentAndroidTarget.nowarn") ?: false + val ignoreAndroidGradlePluginCompatibilityIssues: Boolean + get() = booleanProperty(KOTLIN_MPP_ANDROID_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN) ?: false + val ignoreDisabledCInteropCommonization: Boolean get() = booleanProperty("$KOTLIN_MPP_ENABLE_CINTEROP_COMMONIZATION.nowarn") ?: false @@ -459,6 +463,7 @@ internal class PropertiesProvider private constructor(private val project: Proje const val KOTLIN_MPP_ENABLE_CINTEROP_COMMONIZATION = "kotlin.mpp.enableCInteropCommonization" const val KOTLIN_MPP_HIERARCHICAL_STRUCTURE_BY_DEFAULT = "kotlin.internal.mpp.hierarchicalStructureByDefault" const val KOTLIN_MPP_HIERARCHICAL_STRUCTURE_SUPPORT = "kotlin.mpp.hierarchicalStructureSupport" + const val KOTLIN_MPP_ANDROID_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN = "kotlin.mpp.androidGradlePluginCompatibility.nowarn" const val KOTLIN_NATIVE_DEPENDENCY_PROPAGATION = "kotlin.native.enableDependencyPropagation" const val KOTLIN_MPP_ENABLE_OPTIMISTIC_NUMBER_COMMONIZATION = "kotlin.mpp.enableOptimisticNumberCommonization" const val KOTLIN_KPM_EXPERIMENTAL_MODEL_MAPPING = "kotlin.kpm.experimentalModelMapping" diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/androidPluginIds.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/androidPluginIds.kt index a82919085c8..8c7f3a9c9f5 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/androidPluginIds.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/androidPluginIds.kt @@ -5,6 +5,8 @@ package org.jetbrains.kotlin.gradle.utils +import org.gradle.api.Project + internal val androidPluginIds = listOf( "com.android.application", "com.android.library", @@ -18,3 +20,7 @@ internal val androidPluginIds = listOf( //"com.android.asset-pack", //"com.android.asset-pack-bundle", ) + +internal fun Project.findAppliedAndroidPluginIdOrNull(): String? { + return androidPluginIds.firstOrNull { androidPluginId -> plugins.findPlugin(androidPluginId) != null } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheckTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheckTest.kt new file mode 100644 index 00000000000..c58e7344f81 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheckTest.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:Suppress("FunctionName") + +package org.jetbrains.kotlin.gradle + +import com.android.build.gradle.LibraryPlugin +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.kotlin.gradle.mpp.buildProjectWithKPM +import org.jetbrains.kotlin.gradle.mpp.buildProjectWithMPP +import org.jetbrains.kotlin.gradle.mpp.kotlin +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.AndroidGradlePluginStringProvider +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.AndroidGradlePluginVersion +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.AndroidGradlePluginVersionParser +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.MAX_SUPPORTED_AGP_MAJOR_VERSION +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.MIN_SUPPORTED_AGP_MAJOR_VERSION +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.Messages +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.PROPERTY_KEY_EXECUTED_PROJECT_PATHS +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.runMultiplatformAndroidGradlePluginCompatibilityHealthCheckWhenAndroidIsApplied +import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider +import org.jetbrains.kotlin.gradle.utils.getOrPutRootProjectProperty +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheckTest { + + val project by lazy { + buildProjectWithMPP { + plugins.apply(LibraryPlugin::class.java) + kotlin { android() } + } + } + + private val testLogger = object : (String) -> Unit { + val messages = mutableListOf() + override fun invoke(message: String) { + messages.add(message) + } + } + + class FixedAndroidGradlePluginVersionProvider(private val version: String?) : AndroidGradlePluginStringProvider { + override fun getAndroidGradlePluginString(): String? = version + } + + @Test + fun `test - version too low`() { + val androidGradlePluginVersion = "${MIN_SUPPORTED_AGP_MAJOR_VERSION - 1}.0" + + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck( + testLogger, FixedAndroidGradlePluginVersionProvider(androidGradlePluginVersion) + ) + + assertEquals(Messages.androidGradlePluginVersionTooLow(androidGradlePluginVersion), assertSingleWarningMessage()) + } + + @Test + fun `test - version too high`() { + val androidGradlePluginVersion = "${MAX_SUPPORTED_AGP_MAJOR_VERSION + 1}.0" + + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck( + testLogger, FixedAndroidGradlePluginVersionProvider(androidGradlePluginVersion) + ) + + assertEquals( + Messages.androidGradlePluginVersionTooHigh(androidGradlePluginVersion), assertSingleWarningMessage() + ) + } + + @Test + fun `test - missing Android Gradle Plugin version string`() { + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck( + testLogger, FixedAndroidGradlePluginVersionProvider(null) + ) + + assertEquals( + Messages.FAILED_GETTING_ANDROID_GRADLE_PLUGIN_VERSION_STRING, assertSingleWarningMessage() + ) + } + + @Test + fun `test - failed parsing Android Gradle Plugin version`() { + val androidGradlePluginVersion = "${MIN_SUPPORTED_AGP_MAJOR_VERSION}.0" + + val failingParser = object : AndroidGradlePluginVersionParser { + override fun parseVersionString(version: String): AndroidGradlePluginVersion? { + return null + } + } + + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck( + testLogger, FixedAndroidGradlePluginVersionProvider(androidGradlePluginVersion), failingParser + ) + + assertEquals(Messages.failedParsingAndroidGradlePluginVersion(androidGradlePluginVersion), assertSingleWarningMessage()) + } + + @Test + fun `test - nowarn property`() { + project.propertiesExtension.set(PropertiesProvider.PropertyNames.KOTLIN_MPP_ANDROID_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN, "true") + + /* Test with missing AGP version */ + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck(testLogger, FixedAndroidGradlePluginVersionProvider(null)) + assertNoWarningMessage() + + /* Test with too low AGP version */ + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck( + testLogger, FixedAndroidGradlePluginVersionProvider("${MIN_SUPPORTED_AGP_MAJOR_VERSION - 1}.0") + ) + assertNoWarningMessage() + + /* Test with too high AGP version */ + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck( + testLogger, FixedAndroidGradlePluginVersionProvider("${MAX_SUPPORTED_AGP_MAJOR_VERSION + 1}.0") + ) + assertNoWarningMessage() + + /* Re-enable the check and test with missing AGP version */ + project.propertiesExtension.set(PropertiesProvider.PropertyNames.KOTLIN_MPP_ANDROID_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN, "false") + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck(testLogger, FixedAndroidGradlePluginVersionProvider(null)) + assertEquals(Messages.FAILED_GETTING_ANDROID_GRADLE_PLUGIN_VERSION_STRING, assertSingleWarningMessage()) + } + + @Test + fun `test - compatible versions`() { + fun assertNoWarnings(androidGradlePluginVersion: String) { + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheck( + testLogger, FixedAndroidGradlePluginVersionProvider(androidGradlePluginVersion) + ) + assertNoWarningMessage() + } + + assertNoWarnings("$MIN_SUPPORTED_AGP_MAJOR_VERSION.0") + assertNoWarnings("$MIN_SUPPORTED_AGP_MAJOR_VERSION.0.0") + assertNoWarnings("$MIN_SUPPORTED_AGP_MAJOR_VERSION.0.0-alpha01") + assertNoWarnings("$MIN_SUPPORTED_AGP_MAJOR_VERSION.0.0-m-281") + + assertNoWarnings("$MIN_SUPPORTED_AGP_MAJOR_VERSION.1") + assertNoWarnings("$MIN_SUPPORTED_AGP_MAJOR_VERSION.1.0") + assertNoWarnings("$MIN_SUPPORTED_AGP_MAJOR_VERSION.1.0-alpha01") + assertNoWarnings("$MIN_SUPPORTED_AGP_MAJOR_VERSION.1.0-m-281") + + assertNoWarnings("$MIN_SUPPORTED_AGP_MAJOR_VERSION.0.1") + assertNoWarnings("$MIN_SUPPORTED_AGP_MAJOR_VERSION.0.1-alpha01") + + assertNoWarnings("$MAX_SUPPORTED_AGP_MAJOR_VERSION.0") + assertNoWarnings("$MAX_SUPPORTED_AGP_MAJOR_VERSION.0.0") + assertNoWarnings("$MAX_SUPPORTED_AGP_MAJOR_VERSION.0.0-alpha01") + assertNoWarnings("$MAX_SUPPORTED_AGP_MAJOR_VERSION.0.0-m-281") + + assertNoWarnings("$MAX_SUPPORTED_AGP_MAJOR_VERSION.1") + assertNoWarnings("$MAX_SUPPORTED_AGP_MAJOR_VERSION.1.0") + assertNoWarnings("$MAX_SUPPORTED_AGP_MAJOR_VERSION.1.0-alpha01") + assertNoWarnings("$MAX_SUPPORTED_AGP_MAJOR_VERSION.1.0-m-281") + + assertNoWarnings("$MAX_SUPPORTED_AGP_MAJOR_VERSION.0.1") + assertNoWarnings("$MAX_SUPPORTED_AGP_MAJOR_VERSION.0.1-alpha01") + } + + @Test + fun `test WhenAndroidIsApplied - android is applied after the health check call`() { + val project = ProjectBuilder.builder().build() + + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheckWhenAndroidIsApplied( + testLogger, FixedAndroidGradlePluginVersionProvider(null) + ) + + /* Not yet, executed, because Android is not applied yet */ + assertNoWarningMessage() + + project.plugins.apply(LibraryPlugin::class.java) + assertEquals(Messages.FAILED_GETTING_ANDROID_GRADLE_PLUGIN_VERSION_STRING, assertSingleWarningMessage()) + } + + @Test + fun `test - WhenAndroidIsApplied - android is applied before the health check call`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply(LibraryPlugin::class.java) + + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheckWhenAndroidIsApplied( + testLogger, FixedAndroidGradlePluginVersionProvider(null) + ) + + assertEquals(Messages.FAILED_GETTING_ANDROID_GRADLE_PLUGIN_VERSION_STRING, assertSingleWarningMessage()) + } + + @Test + fun `test - WhenAndroidIsApplied - called multiple times - still emits only a single message`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply(LibraryPlugin::class.java) + + repeat(10) { + project.runMultiplatformAndroidGradlePluginCompatibilityHealthCheckWhenAndroidIsApplied( + testLogger, FixedAndroidGradlePluginVersionProvider(null) + ) + } + + assertEquals(Messages.FAILED_GETTING_ANDROID_GRADLE_PLUGIN_VERSION_STRING, assertSingleWarningMessage()) + } + + @Test + fun `test - is automatically executed when android plugin is applied - mpp plugin`() { + val project = buildProjectWithMPP { } + val executedProjectPaths = project.getOrPutRootProjectProperty(PROPERTY_KEY_EXECUTED_PROJECT_PATHS) { mutableSetOf() } + if (executedProjectPaths.isNotEmpty()) fail("Expected 'executed' project paths to be empty") + project.plugins.apply(LibraryPlugin::class.java) + + /* Expect project was checked */ + assertEquals(setOf(project.path), executedProjectPaths) + } + + @Test + fun `test - is automatically executed when android plugin is applied - kpm plugin`() { + val project = buildProjectWithKPM { } + val executedProjectPaths = project.getOrPutRootProjectProperty(PROPERTY_KEY_EXECUTED_PROJECT_PATHS) { mutableSetOf() } + if (executedProjectPaths.isNotEmpty()) fail("Expected 'executed' project paths to be empty") + project.plugins.apply(LibraryPlugin::class.java) + + /* Expect project was checked */ + assertEquals(setOf(project.path), executedProjectPaths) + } + + @Test + fun `test - is not executed when android plugin is applied - kotlin-android plugin`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("kotlin-android") + project.plugins.apply(LibraryPlugin::class.java) + + val executedProjectPaths = project.getOrPutRootProjectProperty(PROPERTY_KEY_EXECUTED_PROJECT_PATHS) { mutableSetOf() } + if (executedProjectPaths.isNotEmpty()) fail("Expected 'executed' project paths to be empty") + } + + private fun assertSingleWarningMessage(): String { + if (testLogger.messages.size != 1) fail("Expected exactly one warning message logged. Found: ${testLogger.messages}") + return testLogger.messages.single() + } + + private fun assertNoWarningMessage() { + if (testLogger.messages.isNotEmpty()) { + fail("Expected no warning messages to be emitted. Found: ${testLogger.messages}") + } + } +}