blob: e9929c4274726df4eaaa8819a0e9fd9504245c66 [file]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.flutter.gradle
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.Variant
import com.android.build.api.variant.VariantBuilder
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.dsl.CmakeOptions
import com.android.build.gradle.internal.dsl.DefaultConfig
import com.android.builder.model.BuildType
import com.flutter.gradle.FlutterPluginUtils.BUILT_IN_KOTLIN_DOCS
import com.flutter.gradle.FlutterPluginUtils.BUILT_IN_KOTLIN_DOCS_FOR_APPS
import com.flutter.gradle.FlutterPluginUtils.BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS
import com.flutter.gradle.FlutterPluginUtils.BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS
import com.flutter.gradle.FlutterPluginUtils.detectApplyingKotlinGradlePlugin
import com.flutter.gradle.plugins.PluginHandler
import com.flutter.gradle.tasks.PrintTask
import io.mockk.called
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.slot
import io.mockk.verify
import org.gradle.api.Action
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.UnknownTaskException
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.invocation.Gradle
import org.gradle.api.logging.Logger
import org.gradle.api.plugins.PluginManager
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.plugin.extraProperties
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import java.io.File
import java.nio.file.Path
import java.util.Properties
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class FlutterPluginUtilsTest {
companion object {
const val EXAMPLE_ENGINE_VERSION = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23"
val devDependency: Map<String?, Any?> =
mapOf(
Pair("name", "grays_fun_dev_dependency"),
Pair(
"path",
"/Users/someuser/.pub-cache/hosted/pub.dev/grays_fun_dev_dependency-1.1.1/"
),
Pair("native_build", true),
Pair("dependencies", emptyList<String>()),
Pair("dev_dependency", true)
)
val cameraDependency: Map<String?, Any?> =
mapOf(
Pair("name", "camera_android_camerax"),
Pair(
"path",
"/Users/someuser/.pub-cache/hosted/pub.dev/camera_android_camerax-0.6.14+1/"
),
Pair("native_build", true),
Pair("dependencies", emptyList<String>()),
Pair("dev_dependency", false)
)
val flutterPluginAndroidLifecycleDependency: Map<String?, Any?> =
mapOf(
Pair("name", "flutter_plugin_android_lifecycle"),
Pair(
"path",
"/Users/someuser/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.27/"
),
Pair("native_build", true),
Pair("dependencies", emptyList<String>()),
Pair("dev_dependency", false)
)
val pluginListWithoutDevDependency: List<Map<String?, Any?>> =
listOf(
cameraDependency,
flutterPluginAndroidLifecycleDependency,
mapOf(
Pair("name", "in_app_purchase_android"),
Pair(
"path",
"/Users/someuser/.pub-cache/hosted/pub.dev/in_app_purchase_android-0.4.0+1/"
),
Pair("native_build", true),
Pair("dependencies", emptyList<String>()),
Pair("dev_dependency", false)
)
)
val pluginListWithDevDependency: List<Map<String?, Any?>> =
listOf(
cameraDependency,
flutterPluginAndroidLifecycleDependency,
devDependency,
mapOf(
Pair("name", "in_app_purchase_android"),
Pair(
"path",
"/Users/someuser/.pub-cache/hosted/pub.dev/in_app_purchase_android-0.4.0+1/"
),
Pair("native_build", true),
Pair("dependencies", emptyList<String>()),
Pair("dev_dependency", false)
)
)
val manifestText =
"""
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions do not break parsing -->
<uses-permission android:name="android.permission.INTERNET"/>
<application android:label="Flutter Task Helper Test" android:icon="@mipmap/ic_launcher">
<activity android:name="com.example.FlutterActivity1"
android:exported="true"
android:theme="@android:style/Theme.Black.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name="com.example.FlutterActivity2"
android:exported="false"
android:theme="@android:style/Theme.Black.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="poc"
android:host="deeplink.flutter.dev"
android:pathPrefix="some.prefix"
/>
</intent-filter>
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
""".trimIndent()
}
// toCamelCase
@Test
fun `toCamelCase converts a list of strings to camel case`() {
val result = FlutterPluginUtils.toCamelCase(listOf("hello", "world"))
assertEquals("helloWorld", result)
}
@Test
fun `toCamelCase handles empty list`() {
val result = FlutterPluginUtils.toCamelCase(emptyList())
assertEquals("", result)
}
@Test
fun `toCamelCase handles single-element list`() {
val result = FlutterPluginUtils.toCamelCase(listOf("hello"))
assertEquals("hello", result)
}
// compareVersionStrings
@Test
fun `compareVersionStrings compares last element of version string correctly`() {
val result = FlutterPluginUtils.compareVersionStrings("1.2.3", "1.2.4")
assertEquals(-1, result)
}
@Test
fun `compareVersionStrings compares middle element of version string correctly`() {
val result = FlutterPluginUtils.compareVersionStrings("1.2.3", "1.1.4")
assertEquals(1, result)
}
@Test
fun `compareVersionStrings compares first element of version string correctly`() {
val result = FlutterPluginUtils.compareVersionStrings("1.2.3", "2.2.4")
assertEquals(-1, result)
}
@Test
fun `compareVersionStrings considers rc candidates the same`() {
val result = FlutterPluginUtils.compareVersionStrings("1.2.3-rc", "1.2.3")
assertEquals(0, result)
}
// formatPlatformString
@Test
fun `formatPlatformString returns correct string`() {
val result = FlutterPluginUtils.formatPlatformString("android-arm64")
assertEquals("arm64_v8a", result)
}
// shouldShrinkResources
@Test
fun `shouldShrinkResources returns true by default`() {
val project = mockk<Project>()
every { project.hasProperty(any()) } returns false
val result = FlutterPluginUtils.shouldShrinkResources(project)
assertEquals(true, result)
}
@Test
fun `shouldShrinkResources returns true when property is set to true`() {
val project = mockk<Project>()
every { project.hasProperty(FlutterPluginUtils.PROP_SHOULD_SHRINK_RESOURCES) } returns true
every { project.property(FlutterPluginUtils.PROP_SHOULD_SHRINK_RESOURCES) } returns true
val result = FlutterPluginUtils.shouldShrinkResources(project)
assertEquals(true, result)
}
// settingsGradleFile
@Test
fun `settingsGradleFile returns groovy settings gradle file when it exists`(
@TempDir tempDir: Path
) {
val projectDir = tempDir.resolve("android").resolve("app")
projectDir.toFile().mkdirs()
val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle")
settingsGradle.createNewFile()
val result =
FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockk())
assertEquals(settingsGradle, result)
}
@Test
fun `settingsGradleFile returns groovy settings file and logs when both groovy and kotlin exist`(
@TempDir tempDir: Path
) {
val projectDir = tempDir.resolve("android").resolve("app")
projectDir.toFile().mkdirs()
val groovySettingsGradle = File(projectDir.parent.toFile(), "settings.gradle")
groovySettingsGradle.createNewFile()
val kotlinSettingsGradle = File(projectDir.parent.toFile(), "settings.gradle.kts")
kotlinSettingsGradle.createNewFile()
val mockLogger = mockk<Logger>()
every { mockLogger.error(any()) } returns Unit
val result =
FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockLogger)
assertEquals(groovySettingsGradle, result)
verify { mockLogger.error(any()) }
}
// buildGradleFile
@Test
fun `buildGradleFile returns groovy build gradle file when it exists`(
@TempDir tempDir: Path
) {
val projectDir = tempDir.resolve("android").resolve("app")
projectDir.toFile().mkdirs()
val buildGradle = File(projectDir.parent.resolve("app").toFile(), "build.gradle")
buildGradle.createNewFile()
val result =
FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockk())
assertEquals(buildGradle, result)
}
@Test
fun `buildGradleFile returns groovy build file and logs when both groovy and kotlin exist`(
@TempDir tempDir: Path
) {
val projectDir = tempDir.resolve("android").resolve("app")
projectDir.toFile().mkdirs()
val groovyBuildGradle = File(projectDir.parent.resolve("app").toFile(), "build.gradle")
groovyBuildGradle.createNewFile()
val kotlinBuildGradle = File(projectDir.parent.resolve("app").toFile(), "build.gradle.kts")
kotlinBuildGradle.createNewFile()
val mockLogger = mockk<Logger>()
every { mockLogger.error(any()) } returns Unit
val result =
FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockLogger)
assertEquals(groovyBuildGradle, result)
verify { mockLogger.error(any()) }
}
// shouldProjectSplitPerAbi
@Test
fun `shouldProjectSplitPerAbi returns false by default`() {
val project = mockk<Project>()
every { project.findProperty(FlutterPluginUtils.PROP_SPLIT_PER_ABI) } returns null
val result = FlutterPluginUtils.shouldProjectSplitPerAbi(project)
assertEquals(false, result)
}
@Test
fun `shouldProjectSplitPerAbi returns true when property is set to true`() {
val project = mockk<Project>()
every { project.findProperty(FlutterPluginUtils.PROP_SPLIT_PER_ABI) } returns "true"
val result = FlutterPluginUtils.shouldProjectSplitPerAbi(project)
assertEquals(true, result)
}
// shouldProjectUseLocalEngine skipped as it is a wrapper for a single getter
// isProjectVerbose
@Test
fun `isProjectVerbose returns false by default`() {
val project = mockk<Project>()
every { project.findProperty(FlutterPluginUtils.PROP_IS_VERBOSE) } returns null
val result = FlutterPluginUtils.isProjectVerbose(project)
assertEquals(false, result)
}
// isProjectVerbose
@Test
fun `isProjectVerbose returns true when the property is set to true`() {
val project = mockk<Project>()
every { project.findProperty(FlutterPluginUtils.PROP_IS_VERBOSE) } returns true
val result = FlutterPluginUtils.isProjectVerbose(project)
assertEquals(true, result)
}
// shouldConfigureFlutterTask
@Test
fun `shouldConfigureFlutterTask returns true for assemble task`() {
val project = mockk<Project>()
val assembleTask = mockk<Task>()
every { project.gradle.startParameter.taskNames } returns listOf("assemble")
val result = FlutterPluginUtils.shouldConfigureFlutterTask(project, assembleTask)
assertEquals(true, result)
}
@Test
fun `shouldConfigureFlutterTask returns true when taskname and assembleTask end with Release`() {
val project = mockk<Project>()
val assembleTask = mockk<Task>()
every { project.gradle.startParameter.taskNames } returns listOf("assembleRelease")
every { assembleTask.name } returns "assembleSomethingElseRelease"
val result = FlutterPluginUtils.shouldConfigureFlutterTask(project, assembleTask)
assertEquals(true, result)
}
// getFlutterSourceDirectory
@Test
fun `getFlutterSourceDirectory returns the flutter source directory`() {
val flutterExtension = FlutterExtension()
val project = mockk<Project>()
flutterExtension.source = "my/flutter/source/directory"
every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension
every { project.file(any()) } returns mockk()
FlutterPluginUtils.getFlutterSourceDirectory(project)
verify { project.file("my/flutter/source/directory") }
}
@Test
fun `getFlutterSourceDirectory throws exception when flutter source directory is not set`() {
val flutterExtension = FlutterExtension()
val project = mockk<Project>()
flutterExtension.source = null
every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension
assertThrows<GradleException> {
FlutterPluginUtils.getFlutterSourceDirectory(project)
}
}
// getFlutterTarget
@Test
fun `getFlutterTarget returns the target when the project property is set`() {
val project = mockk<Project>()
every { project.hasProperty(FlutterPluginUtils.PROP_TARGET) } returns true
every { project.property(FlutterPluginUtils.PROP_TARGET) } returns "my/target"
val result = FlutterPluginUtils.getFlutterTarget(project)
assertEquals("my/target", result)
}
@Test
fun `getFlutterTarget returns the target from the FlutterExtension when it is set and project property is not set`() {
val flutterExtension = FlutterExtension()
val project = mockk<Project>()
flutterExtension.target = "my/target"
every { project.hasProperty(FlutterPluginUtils.PROP_TARGET) } returns false
every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension
val result = FlutterPluginUtils.getFlutterTarget(project)
assertEquals(flutterExtension.target, result)
}
@Test
fun `getFlutterTarget returns the default target when it is not set`() {
val project = mockk<Project>()
every { project.hasProperty(FlutterPluginUtils.PROP_TARGET) } returns false
every { project.extensions.findByType(FlutterExtension::class.java)!!.target } returns null
val result = FlutterPluginUtils.getFlutterTarget(project)
assertEquals("lib/main.dart", result)
}
// isBuiltAsApp skipped as it is a wrapper for a single getter
// addApiDependencies
@Test
fun `addApiDependencies adds the dependency with the correct name when no UnknownTaskException`() {
val project = mockk<Project>()
val variantName = "debug"
val dependency = mockk<Any>()
every { project.configurations.named("api") } returns mockk()
every { project.dependencies.add(any(), any()) } returns mockk()
FlutterPluginUtils.addApiDependencies(project, variantName, dependency)
verify { project.dependencies.add("debugApi", dependency) }
}
@Test
fun `addApiDependencies adds the dependency with the correct name when UnknownTaskException`() {
val project = mockk<Project>()
val variantName = "debug"
val dependency = mockk<Any>()
every { project.configurations.named("api") } throws
UnknownTaskException(
"message",
mockk()
)
every { project.dependencies.add(any(), any()) } returns mockk()
FlutterPluginUtils.addApiDependencies(project, variantName, dependency)
verify { project.dependencies.add("debugCompile", dependency) }
}
// buildModeFor
@Test
fun `buildModeFor returns profile if the BuildType has name profile`() {
val buildType = mockk<BuildType>()
every { buildType.name } returns "profile"
val result = FlutterPluginUtils.buildModeFor(buildType)
assertEquals("profile", result)
}
@Test
fun `buildModeFor returns debug if the BuildType is debuggable`() {
val buildType = mockk<BuildType>()
every { buildType.name } returns "something random"
every { buildType.isDebuggable } returns true
val result = FlutterPluginUtils.buildModeFor(buildType)
assertEquals("debug", result)
}
@Test
fun `buildModeFor returns release if the BuildType is not debuggable and not named profile`() {
val buildType = mockk<BuildType>()
every { buildType.isDebuggable } returns false
every { buildType.name } returns "something random"
val result = FlutterPluginUtils.buildModeFor(buildType)
assertEquals("release", result)
}
// supportsBuildMode
@Test
fun `supportsBuildMode returns true if project should not use local engine`() {
val project = mockk<Project>()
every { project.hasProperty(FlutterPluginUtils.PROP_LOCAL_ENGINE_REPO) } returns false
val result = FlutterPluginUtils.supportsBuildMode(project, "debug")
assertEquals(true, result)
}
@Test
fun `supportsBuildMode returns false if project should use local engine and build mode does not match`() {
val project = mockk<Project>()
every { project.hasProperty(FlutterPluginUtils.PROP_LOCAL_ENGINE_REPO) } returns true
every { project.hasProperty(FlutterPluginUtils.PROP_LOCAL_ENGINE_BUILD_MODE) } returns true
every { project.property(FlutterPluginUtils.PROP_LOCAL_ENGINE_BUILD_MODE) } returns "debug"
val result = FlutterPluginUtils.supportsBuildMode(project, "release")
assertEquals(false, result)
}
// getTargetPlatforms
@Test
fun `getTargetPlatforms the default if property is not set`() {
val project = mockk<Project>()
every { project.hasProperty(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns false
val result = FlutterPluginUtils.getTargetPlatforms(project)
assertEquals(listOf("android-arm", "android-arm64", "android-x64"), result)
}
@Test
fun `getTargetPlatforms the value if property is set`() {
val project = mockk<Project>()
every { project.hasProperty(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns true
every { project.property(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns "android-arm64,android-arm"
val result = FlutterPluginUtils.getTargetPlatforms(project)
assertEquals(listOf("android-arm64", "android-arm"), result)
}
@Test
fun `getTargetPlatforms throws GradleException if property is set to invalid value`() {
val project = mockk<Project>()
every { project.hasProperty(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns true
every { project.property(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns "android-invalid"
val gradleException: GradleException =
assertThrows<GradleException> {
FlutterPluginUtils.getTargetPlatforms(project)
}
assertContains(gradleException.message!!, "android-invalid")
}
// readPropertiesIfExist
@Test
fun `readPropertiesIfExist returns empty Properties when file does not exist`(
@TempDir tempDir: Path
) {
val propertiesFile = tempDir.resolve("file_that_doesnt_exist.properties")
val result = FlutterPluginUtils.readPropertiesIfExist(propertiesFile.toFile())
assertEquals(Properties(), result)
}
@Test
fun `readPropertiesIfExist returns Properties when file exists`(
@TempDir tempDir: Path
) {
val propertiesFile = tempDir.resolve("file_that_exists.properties").toFile()
propertiesFile.writeText(
"""
sdk.dir=/Users/someuser/Library/Android/sdk
flutter.sdk=/Users/someuser/development/flutter/flutter
flutter.buildMode=release
flutter.versionName=1.0.0
flutter.versionCode=1
""".trimIndent()
)
val result = FlutterPluginUtils.readPropertiesIfExist(propertiesFile)
assertEquals(5, result.size)
assertEquals("/Users/someuser/Library/Android/sdk", result["sdk.dir"])
assertEquals("/Users/someuser/development/flutter/flutter", result["flutter.sdk"])
assertEquals("release", result["flutter.buildMode"])
assertEquals("1.0.0", result["flutter.versionName"])
assertEquals("1", result["flutter.versionCode"])
}
// getCompileSdkFromProject
@Test
fun `getCompileSdkFromProject returns the compileSdk from the project`() {
val project = mockk<Project>()
every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
val result = FlutterPluginUtils.getCompileSdkFromProject(project)
assertEquals("35", result)
}
@Test
fun `detectLowCompileSdkVersionOrNdkVersion registers ValidateCompileSdkVersionTask`() {
val project = mockk<Project>()
val taskContainer = mockk<org.gradle.api.tasks.TaskContainer>()
val taskProvider = mockk<org.gradle.api.tasks.TaskProvider<com.flutter.gradle.tasks.ValidateCompileSdkVersionTask>>()
val preBuildTaskProvider = mockk<org.gradle.api.tasks.TaskProvider<org.gradle.api.Task>>()
val androidComponents = mockk<AndroidComponentsExtension<Any, VariantBuilder, Variant>>()
every { project.tasks } returns taskContainer
every {
taskContainer.register(
"validateCompileSdkVersion",
com.flutter.gradle.tasks.ValidateCompileSdkVersionTask::class.java,
any<org.gradle.api.Action<com.flutter.gradle.tasks.ValidateCompileSdkVersionTask>>()
)
} returns taskProvider
every {
project.extensions.getByType(AndroidComponentsExtension::class.java)
} returns androidComponents as AndroidComponentsExtension<*, *, *>
every { androidComponents.finalizeDsl(match<(Any) -> Unit> { true }) } returns Unit
every { taskContainer.named("preBuild") } returns preBuildTaskProvider
every { preBuildTaskProvider.configure(any()) } returns Unit
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, emptyList())
verify {
taskContainer.register(
"validateCompileSdkVersion",
com.flutter.gradle.tasks.ValidateCompileSdkVersionTask::class.java,
any()
)
}
verify { androidComponents.finalizeDsl(match<(Any) -> Unit> { true }) }
verify { taskContainer.named("preBuild") }
}
@Test
fun `detectLowCompileSdkVersionOrNdkVersion handles non-Android plugins safely`() {
val project = mockk<Project>()
val rootProject = mockk<Project>()
val taskContainer = mockk<org.gradle.api.tasks.TaskContainer>()
val taskProvider = mockk<org.gradle.api.tasks.TaskProvider<com.flutter.gradle.tasks.ValidateCompileSdkVersionTask>>()
val preBuildTaskProvider = mockk<org.gradle.api.tasks.TaskProvider<org.gradle.api.Task>>()
val androidComponents = mockk<AndroidComponentsExtension<Any, VariantBuilder, Variant>>()
val pluginProject = mockk<Project>()
val extensionContainer = mockk<org.gradle.api.plugins.ExtensionContainer>()
val pluginSdks = mockk<org.gradle.api.provider.MapProperty<String, Int>>(relaxed = true)
val pluginNdks = mockk<org.gradle.api.provider.MapProperty<String, String>>(relaxed = true)
val mapPropertyObjects = mockk<org.gradle.api.model.ObjectFactory>()
every { project.rootProject } returns rootProject
every { project.tasks } returns taskContainer
every { project.objects } returns mapPropertyObjects
every { mapPropertyObjects.mapProperty(String::class.java, Int::class.java) } returns pluginSdks
every { mapPropertyObjects.mapProperty(String::class.java, String::class.java) } returns pluginNdks
every { project.provider(any<java.util.concurrent.Callable<Any>>()) } answers {
val callable = firstArg<java.util.concurrent.Callable<Any>>()
mockk<org.gradle.api.provider.Provider<Any>> {
every { get() } answers { callable.call() }
}
}
every {
taskContainer.register(
"validateCompileSdkVersion",
com.flutter.gradle.tasks.ValidateCompileSdkVersionTask::class.java,
any<org.gradle.api.Action<com.flutter.gradle.tasks.ValidateCompileSdkVersionTask>>()
)
} returns taskProvider
every {
project.extensions.getByType(AndroidComponentsExtension::class.java)
} returns androidComponents as AndroidComponentsExtension<*, *, *>
every { androidComponents.finalizeDsl(match<(Any) -> Unit> { true }) } returns Unit
every { taskContainer.named("preBuild") } returns preBuildTaskProvider
every { preBuildTaskProvider.configure(any()) } returns Unit
val pluginList: List<Map<String?, Any?>> = listOf(mapOf("name" to "nonAndroidPlugin"))
every { rootProject.findProject(":nonAndroidPlugin") } returns pluginProject
every { pluginProject.extensions } returns extensionContainer
every { extensionContainer.findByName("android") } returns null // Simulates non-Android project
val actionSlot = slot<org.gradle.api.Action<com.flutter.gradle.tasks.ValidateCompileSdkVersionTask>>()
every {
taskContainer.register(
"validateCompileSdkVersion",
com.flutter.gradle.tasks.ValidateCompileSdkVersionTask::class.java,
capture(actionSlot)
)
} returns taskProvider
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, pluginList)
// We don't execute the action here as it requires complex stubbing of project.provider
// which is hard to do in this setup. The fact that this method completes without crashing
// is enough to verify that it handles non-Android plugins safely during registration.
verify {
taskContainer.register(
"validateCompileSdkVersion",
com.flutter.gradle.tasks.ValidateCompileSdkVersionTask::class.java,
any()
)
}
}
enum class DslType { GROOVY, KOTLIN }
@Nested
inner class SupportBuiltInKotlinTests {
@Nested
inner class TestApplyingPluginsRegexTests {
@Test
fun `AGP app regex on single line apply`() {
assertSingeLinePluginDetection(FlutterPluginUtils.appPluginRegexKotlin, "com.android.application", DslType.KOTLIN)
assertSingeLinePluginDetection(FlutterPluginUtils.appPluginRegexGroovy, "com.android.application", DslType.GROOVY)
}
@Test
fun `AGP lib regex on single line apply`() {
assertSingeLinePluginDetection(FlutterPluginUtils.libPluginRegexKotlin, "com.android.library", DslType.KOTLIN)
assertSingeLinePluginDetection(FlutterPluginUtils.libPluginRegexGroovy, "com.android.library", DslType.GROOVY)
}
@Test
fun `KGP regex on single line apply`() {
assertSingeLinePluginDetection(FlutterPluginUtils.kgpRegexKotlin, "kotlin-android", DslType.KOTLIN)
assertSingeLinePluginDetection(FlutterPluginUtils.kgpRegexGroovy, "kotlin-android", DslType.GROOVY)
assertSingeLinePluginDetection(FlutterPluginUtils.kgpRegexKotlin, "org.jetbrains.kotlin.android", DslType.KOTLIN)
assertSingeLinePluginDetection(FlutterPluginUtils.kgpRegexGroovy, "org.jetbrains.kotlin.android", DslType.GROOVY)
}
@Test
fun `Regexes are successful when multiple plugins are applied`() {
val appProjectBuildGradlePluginsBlock =
"""
plugins {
id("com.android.application")
id("kotlin-android")
}
""".trimIndent()
assertTrue(
FlutterPluginUtils.appPluginRegexKotlin.containsMatchIn(appProjectBuildGradlePluginsBlock)
)
assertTrue(
FlutterPluginUtils.kgpRegexKotlin.containsMatchIn(appProjectBuildGradlePluginsBlock)
)
assertTrue(
FlutterPluginUtils.appPluginRegexGroovy.containsMatchIn(appProjectBuildGradlePluginsBlock)
)
assertTrue(
FlutterPluginUtils.kgpRegexGroovy.containsMatchIn(appProjectBuildGradlePluginsBlock)
)
val libProjectBuildGradlePluginsBlock =
"""
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
""".trimIndent()
assertTrue(
FlutterPluginUtils.libPluginRegexKotlin.containsMatchIn(libProjectBuildGradlePluginsBlock)
)
assertTrue(
FlutterPluginUtils.kgpRegexKotlin.containsMatchIn(libProjectBuildGradlePluginsBlock)
)
assertTrue(
FlutterPluginUtils.libPluginRegexGroovy.containsMatchIn(libProjectBuildGradlePluginsBlock)
)
assertTrue(
FlutterPluginUtils.kgpRegexGroovy.containsMatchIn(libProjectBuildGradlePluginsBlock)
)
val appProjectBuildGradlePluginsBlockNoParens =
"""
plugins {
id 'com.android.application'
id 'kotlin-android'
}
""".trimIndent()
assertTrue(
FlutterPluginUtils.appPluginRegexGroovy.containsMatchIn(appProjectBuildGradlePluginsBlockNoParens)
)
assertTrue(
FlutterPluginUtils.kgpRegexGroovy.containsMatchIn(appProjectBuildGradlePluginsBlockNoParens)
)
assertFalse(
FlutterPluginUtils.appPluginRegexKotlin.containsMatchIn(appProjectBuildGradlePluginsBlockNoParens)
)
assertFalse(
FlutterPluginUtils.kgpRegexKotlin.containsMatchIn(appProjectBuildGradlePluginsBlockNoParens)
)
val appProjectBuildGradlePluginsBlockMixed =
"""
plugins {
id 'com.android.application'
alias 'kotlin-android'
}
""".trimIndent()
assertTrue(
FlutterPluginUtils.appPluginRegexGroovy.containsMatchIn(appProjectBuildGradlePluginsBlockMixed)
)
assertTrue(
FlutterPluginUtils.kgpRegexGroovy.containsMatchIn(appProjectBuildGradlePluginsBlockMixed)
)
assertFalse(
FlutterPluginUtils.appPluginRegexKotlin.containsMatchIn(appProjectBuildGradlePluginsBlockMixed)
)
assertFalse(
FlutterPluginUtils.kgpRegexKotlin.containsMatchIn(appProjectBuildGradlePluginsBlockMixed)
)
}
@Test
fun `Regexes fail when multiple plugins are applied`() {
val kotlinSameLine =
"""
plugins {
id("com.android.application") id("kotlin-android")
}
""".trimIndent()
assertFalse(
FlutterPluginUtils.appPluginRegexKotlin.containsMatchIn(kotlinSameLine),
"Should fail: multi-id on one line"
)
assertFalse(
FlutterPluginUtils.kgpRegexKotlin.containsMatchIn(kotlinSameLine),
"Should fail: multi-id on one line"
)
val groovySameLine =
"""
plugins {
id 'com.android.application' id 'kotlin-android'
}
""".trimIndent()
assertFalse(
FlutterPluginUtils.appPluginRegexGroovy.containsMatchIn(groovySameLine),
"Should fail: multi-id on one line"
)
assertFalse(
FlutterPluginUtils.kgpRegexGroovy.containsMatchIn(groovySameLine),
"Should fail: multi-id on one line"
)
val appProjectBuildGradlePluginsCommentOnePlugin =
"""
plugins {
id 'com.android.application'
// alias 'kotlin-android'
}
""".trimIndent()
assertTrue(
FlutterPluginUtils.appPluginRegexGroovy.containsMatchIn(appProjectBuildGradlePluginsCommentOnePlugin)
)
assertFalse(
FlutterPluginUtils.kgpRegexGroovy.containsMatchIn(appProjectBuildGradlePluginsCommentOnePlugin)
)
assertFalse(
FlutterPluginUtils.appPluginRegexKotlin.containsMatchIn(appProjectBuildGradlePluginsCommentOnePlugin)
)
assertFalse(
FlutterPluginUtils.kgpRegexKotlin.containsMatchIn(appProjectBuildGradlePluginsCommentOnePlugin)
)
val appProjectBuildGradlePluginsComment =
"""
// plugins {
// id 'com.android.application'
// alias 'kotlin-android'
// }
""".trimIndent()
assertFalse(
FlutterPluginUtils.appPluginRegexGroovy.containsMatchIn(appProjectBuildGradlePluginsComment)
)
assertFalse(
FlutterPluginUtils.kgpRegexGroovy.containsMatchIn(appProjectBuildGradlePluginsComment)
)
assertFalse(
FlutterPluginUtils.appPluginRegexKotlin.containsMatchIn(appProjectBuildGradlePluginsComment)
)
assertFalse(
FlutterPluginUtils.kgpRegexKotlin.containsMatchIn(appProjectBuildGradlePluginsComment)
)
}
}
fun assertSingeLinePluginDetection(
regex: Regex,
pluginId: String,
dslType: DslType
) {
if (dslType == DslType.GROOVY) {
assertTrue(regex.containsMatchIn("apply plugin: '$pluginId'"))
assertTrue(regex.containsMatchIn("apply plugin: \"$pluginId\""))
assertTrue(regex.containsMatchIn("plugins { id '$pluginId' }"))
assertTrue(regex.containsMatchIn("plugins { id \"$pluginId\" }"))
assertTrue(regex.containsMatchIn("plugins {\n id '$pluginId'\n}"))
assertTrue(regex.containsMatchIn("plugins { alias '$pluginId' }"))
assertFalse(regex.containsMatchIn("apply plugin\n:'$pluginId'"), "Newline before colon failure")
assertFalse(regex.containsMatchIn("plugins { id\n'$pluginId' }"), "newline before opening quote")
}
if (dslType == DslType.KOTLIN) {
assertTrue(regex.containsMatchIn("plugins {\n id(\"$pluginId\")\n}"))
assertTrue(regex.containsMatchIn("plugins { id(\n'$pluginId'\n) }"))
assertFalse(regex.containsMatchIn("plugins { id '$pluginId' }"), "Kotlin DSL requires parentheses")
assertFalse(regex.containsMatchIn("apply plugin: '$pluginId'"), "Kotlin DSL does not use apply plugin: for AGP/KGP")
}
assertTrue(regex.containsMatchIn("plugins { id('$pluginId') }"))
assertTrue(regex.containsMatchIn("plugins { id(\"$pluginId\") }"))
assertTrue(regex.containsMatchIn("plugins { alias('$pluginId') }"))
assertTrue(regex.containsMatchIn("plugins { alias(\"$pluginId\") }"))
assertFalse(regex.containsMatchIn("// id '$pluginId'"), "Failed to ignore single line comment")
assertFalse(regex.containsMatchIn("// id('$pluginId')"), "Failed to ignore single line comment")
// Check newline constraints
assertFalse(regex.containsMatchIn("plugins\n{ id('$pluginId') }"), "Newline before opening bracket should fail")
assertFalse(regex.containsMatchIn("plugins { id\n('$pluginId') }"), "Newline before opening parentheses should fail")
// Check spacing inside quotes
assertFalse(regex.containsMatchIn("id ' $pluginId '"), "Should fail when there are spaces in quotes")
}
@Nested
inner class DetectApplyingKotlinGradlePluginTests {
@Test
fun `logs app warning when KGP is only applied in app`(
@TempDir tempDir: Path
) {
val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
val appBuildGradleFile =
File(appDir, "build.gradle").apply {
createNewFile()
writeText(
"""
plugins {
id("com.android.application")
id("kotlin-android")
}
""".trimIndent()
)
}
val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
val pluginBuildGradleFile =
File(pluginDir, "build.gradle").apply {
createNewFile()
writeText(
"""
plugins {
id("com.android.library")
}
""".trimIndent()
)
}
val rootProject = mockk<Project>()
val mockGradle = mockk<Gradle>()
val mockLogger = mockk<Logger>(relaxed = true)
val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
val pluginProjectPluginManager = mockk<PluginManager>(relaxed = true)
val appProject =
createMockSubproject(
tempDir = tempDir,
buildFile = appBuildGradleFile,
projectName = "app",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = appProjectPluginManager
)
val pluginProject =
createMockSubproject(
tempDir = tempDir,
buildFile = pluginBuildGradleFile,
projectName = "plugin",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = pluginProjectPluginManager
)
val subprojectsActionSlot = slot<Action<Project>>()
val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
detectApplyingKotlinGradlePlugin(appProject)
verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
subprojectsActionSlot.captured.execute(appProject)
subprojectsActionSlot.captured.execute(pluginProject)
verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
projectsEvaluatedActionSlot.captured.execute(mockGradle)
verify {
mockLogger.error(
"""
WARNING: Your Android app project: app located at: ${appBuildGradleFile.absolutePath}
applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter.
Please migrate your app to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_APPS
""".trimIndent()
)
}
verify(exactly = 0) {
mockLogger.error(match { it.contains("Your app uses the following plugins") })
}
verify(exactly = 0) { appProjectPluginManager.apply("kotlin-android") }
verify(exactly = 1) { pluginProjectPluginManager.apply("kotlin-android") }
}
@Test
fun `logs plugin warning when KGP is only applied in one plugin`(
@TempDir tempDir: Path
) {
val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
val appBuildGradleFile =
File(appDir, "build.gradle").apply {
createNewFile()
writeText(
"""
plugins {
id("com.android.application")
}
""".trimIndent()
)
}
val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
val pluginBuildGradleFile =
File(pluginDir, "build.gradle").apply {
createNewFile()
writeText(
"""
plugins {
id("com.android.library")
id("kotlin-android")
}
""".trimIndent()
)
}
val rootProject = mockk<Project>()
val mockGradle = mockk<Gradle>()
val mockLogger = mockk<Logger>(relaxed = true)
val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
val pluginProjectPluginManager = mockk<PluginManager>(relaxed = true)
val appProject =
createMockSubproject(
tempDir = tempDir,
buildFile = appBuildGradleFile,
projectName = "app",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = appProjectPluginManager
)
val pluginProject =
createMockSubproject(
tempDir = tempDir,
buildFile = pluginBuildGradleFile,
projectName = "plugin",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = pluginProjectPluginManager
)
val subprojectsActionSlot = slot<Action<Project>>()
val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
detectApplyingKotlinGradlePlugin(appProject)
verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
subprojectsActionSlot.captured.execute(appProject)
subprojectsActionSlot.captured.execute(pluginProject)
verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
projectsEvaluatedActionSlot.captured.execute(mockGradle)
verify {
mockLogger.error(
"""
WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): plugin
Future versions of Flutter will fail to build if your app uses plugins that apply KGP.
Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin.
If no such version exists, report the issue to the plugin. If necessary, here is a guide on filing
an issue against a plugin: $BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS
If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS
""".trimIndent()
)
}
verify(exactly = 0) {
mockLogger.error(match { it.contains("Your Android app project") })
}
verify(exactly = 1) { appProjectPluginManager.apply("kotlin-android") }
verify(exactly = 0) { pluginProjectPluginManager.apply("kotlin-android") }
}
@Test
fun `logs app and plugin warning when KGP is applied in both app and plugins`(
@TempDir tempDir: Path
) {
val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
val appBuildGradleFile =
File(appDir, "build.gradle").apply {
createNewFile()
writeText(
"""
plugins {
id("com.android.application")
id("kotlin-android")
}
""".trimIndent()
)
}
val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
val pluginBuildGradleFile =
File(pluginDir, "build.gradle").apply {
createNewFile()
writeText(
"""
plugins {
id("com.android.library")
id("kotlin-android")
}
""".trimIndent()
)
}
val rootProject = mockk<Project>()
val mockGradle = mockk<Gradle>()
val mockLogger = mockk<Logger>(relaxed = true)
val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
val pluginProjectPluginManager = mockk<PluginManager>(relaxed = true)
val appProject =
createMockSubproject(
tempDir = tempDir,
buildFile = appBuildGradleFile,
projectName = "app",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = appProjectPluginManager
)
val pluginProject =
createMockSubproject(
tempDir = tempDir,
buildFile = pluginBuildGradleFile,
projectName = "plugin",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = pluginProjectPluginManager
)
val subprojectsActionSlot = slot<Action<Project>>()
val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
detectApplyingKotlinGradlePlugin(appProject)
verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
subprojectsActionSlot.captured.execute(appProject)
subprojectsActionSlot.captured.execute(pluginProject)
verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
projectsEvaluatedActionSlot.captured.execute(mockGradle)
verify {
mockLogger.error(
"""
WARNING: Your Android app project: app located at: ${appBuildGradleFile.absolutePath}
applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter.
Please migrate your app to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_APPS
""".trimIndent()
)
}
verify {
mockLogger.error(
"""
WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): plugin
Future versions of Flutter will fail to build if your app uses plugins that apply KGP.
Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin.
If no such version exists, report the issue to the plugin. If necessary, here is a guide on filing
an issue against a plugin: $BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS
If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS
""".trimIndent()
)
}
verify(exactly = 0) { appProjectPluginManager.apply("kotlin-android") }
verify(exactly = 0) { pluginProjectPluginManager.apply("kotlin-android") }
}
@Test
fun `logs app and plugin warning when legacy KGP configuration is applied in both app and plugins`(
@TempDir tempDir: Path
) {
val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
val appBuildGradleFile =
File(appDir, "build.gradle").apply {
createNewFile()
writeText(
"""
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
""".trimIndent()
)
}
val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
val pluginBuildGradleFile =
File(pluginDir, "build.gradle").apply {
createNewFile()
writeText(
"""
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
""".trimIndent()
)
}
val rootProject = mockk<Project>()
val mockGradle = mockk<Gradle>()
val mockLogger = mockk<Logger>(relaxed = true)
val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
val pluginProjectOnePluginManager = mockk<PluginManager>(relaxed = true)
val pluginProjectTwoPluginManager = mockk<PluginManager>(relaxed = true)
val appProject =
createMockSubproject(
tempDir = tempDir,
buildFile = appBuildGradleFile,
projectName = "app",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = appProjectPluginManager
)
val pluginProjectOne =
createMockSubproject(
tempDir = tempDir,
buildFile = pluginBuildGradleFile,
projectName = "plugin1",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = pluginProjectOnePluginManager
)
val pluginProjectTwo =
createMockSubproject(
tempDir = tempDir,
buildFile = pluginBuildGradleFile,
projectName = "plugin2",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = pluginProjectTwoPluginManager
)
val subprojectsActionSlot = slot<Action<Project>>()
val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
detectApplyingKotlinGradlePlugin(appProject)
verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
subprojectsActionSlot.captured.execute(appProject)
subprojectsActionSlot.captured.execute(pluginProjectOne)
subprojectsActionSlot.captured.execute(pluginProjectTwo)
verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
projectsEvaluatedActionSlot.captured.execute(mockGradle)
verify {
mockLogger.error(
"""
WARNING: Your Android app project: app located at: ${appBuildGradleFile.absolutePath}
applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter.
Please migrate your app to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_APPS
""".trimIndent()
)
}
verify {
mockLogger.error(
"""
WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): plugin1, plugin2
Future versions of Flutter will fail to build if your app uses plugins that apply KGP.
Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin.
If no such version exists, report the issue to the plugin. If necessary, here is a guide on filing
an issue against a plugin: $BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS
If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS
""".trimIndent()
)
}
verify(exactly = 0) { appProjectPluginManager.apply("kotlin-android") }
verify(exactly = 0) { pluginProjectOnePluginManager.apply("kotlin-android") }
verify(exactly = 0) { pluginProjectTwoPluginManager.apply("kotlin-android") }
}
@Test
fun `does not log when migrated to Built-in Kotlin`(
@TempDir tempDir: Path
) {
val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
val appBuildGradleFile =
File(appDir, "build.gradle").apply {
createNewFile()
writeText(
"""
apply plugin: 'com.android.application'
""".trimIndent()
)
}
val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
val pluginBuildGradleFile =
File(pluginDir, "build.gradle").apply {
createNewFile()
writeText(
"""
apply plugin: 'com.android.library'
""".trimIndent()
)
}
val rootProject = mockk<Project>()
val mockGradle = mockk<Gradle>()
val mockLogger = mockk<Logger>(relaxed = true)
val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
val pluginProjectPluginManager = mockk<PluginManager>(relaxed = true)
val appProject =
createMockSubproject(
tempDir = tempDir,
buildFile = appBuildGradleFile,
projectName = "app",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = appProjectPluginManager
)
val pluginProject =
createMockSubproject(
tempDir = tempDir,
buildFile = pluginBuildGradleFile,
projectName = "plugin",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = pluginProjectPluginManager
)
val subprojectsActionSlot = slot<Action<Project>>()
val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
detectApplyingKotlinGradlePlugin(appProject)
verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
subprojectsActionSlot.captured.execute(appProject)
subprojectsActionSlot.captured.execute(pluginProject)
verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
projectsEvaluatedActionSlot.captured.execute(mockGradle)
verify(exactly = 0) {
mockLogger.error(any())
}
verify(exactly = 1) { appProjectPluginManager.apply("kotlin-android") }
verify(exactly = 1) { pluginProjectPluginManager.apply("kotlin-android") }
}
@Test
fun `logs when KGP is applied but fails to apply`(
@TempDir tempDir: Path
) {
val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
val appBuildGradleFile =
File(appDir, "build.gradle").apply {
createNewFile()
writeText(
"""
plugins {
id("com.android.application")
}
""".trimIndent()
)
}
val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
val pluginBuildGradleFile =
File(pluginDir, "build.gradle").apply {
createNewFile()
writeText(
"""
plugins {
id("com.android.library")
}
""".trimIndent()
)
}
val rootProject = mockk<Project>()
val mockGradle = mockk<Gradle>()
val mockLogger = mockk<Logger>(relaxed = true)
val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
val pluginProjectPluginManager = mockk<PluginManager>(relaxed = true)
val appProject =
createMockSubproject(
tempDir = tempDir,
buildFile = appBuildGradleFile,
projectName = "app",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = appProjectPluginManager
)
val pluginProject =
createMockSubproject(
tempDir = tempDir,
buildFile = pluginBuildGradleFile,
projectName = "plugin",
mockLogger = mockLogger,
rootProjectMock = rootProject,
gradleMock = mockGradle,
pluginManager = pluginProjectPluginManager
)
val subprojectsActionSlot = slot<Action<Project>>()
val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
every { appProjectPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath")
every { pluginProjectPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath")
detectApplyingKotlinGradlePlugin(appProject)
verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
subprojectsActionSlot.captured.execute(appProject)
subprojectsActionSlot.captured.execute(pluginProject)
verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
projectsEvaluatedActionSlot.captured.execute(mockGradle)
verify(exactly = 0) {
mockLogger.error(any())
}
verify {
mockLogger.quiet(
"""
Applying the Kotlin Android Plugin (KGP) was unsuccessful. KGP was not found on the classpath.
If your project uses Kotlin, ensure KGP is declared in the root plugins block.
For more details check: $BUILT_IN_KOTLIN_DOCS
""".trimIndent()
)
}
}
}
private fun createMockSubproject(
tempDir: Path,
buildFile: File,
projectName: String,
mockLogger: Logger,
rootProjectMock: Project? = null,
gradleMock: Gradle? = null,
pluginManager: PluginManager
): Project {
val projectDir = tempDir.resolve(projectName).toFile().apply { mkdirs() }
val project = mockk<Project>()
every { project.name } returns projectName
every { project.projectDir } returns projectDir
every { project.buildFile } returns buildFile
every { project.logger } returns mockLogger
every { project.pluginManager } returns pluginManager
if (rootProjectMock != null) every { project.rootProject } returns rootProjectMock
if (gradleMock != null) every { project.gradle } returns gradleMock
return project
}
}
// forceNdkDownload
@Test
fun `forceNdkDownload skips projects which are already configuring a native build`(
@TempDir tempDir: Path
) {
val fakeCmakeFile = tempDir.resolve("CMakeLists.txt").toFile()
fakeCmakeFile.createNewFile()
val project = mockk<Project>()
val mockCmakeOptions = mockk<CmakeOptions>()
val mockDefaultConfig = mockk<DefaultConfig>()
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.externalNativeBuild.cmake
} returns mockCmakeOptions
every { project.extensions.findByType(BaseExtension::class.java)!!.defaultConfig } returns mockDefaultConfig
every { mockCmakeOptions.path } returns fakeCmakeFile
FlutterPluginUtils.forceNdkDownload(project, "ignored")
verify(exactly = 1) {
mockCmakeOptions.path
}
verify(exactly = 0) { mockCmakeOptions.setPath(any()) }
verify { mockDefaultConfig wasNot called }
}
@Test
fun `forceNdkDownload sets externalNativeBuild properties`() {
val project = mockk<Project>()
val mockCmakeOptions = mockk<CmakeOptions>()
val mockDefaultConfig = mockk<DefaultConfig>()
val mockDirectoryProperty = mockk<DirectoryProperty>()
val mockDirectory = mockk<Directory>()
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.externalNativeBuild.cmake
} returns mockCmakeOptions
every { project.extensions.findByType(BaseExtension::class.java)!!.defaultConfig } returns mockDefaultConfig
val basePath = "/base/path"
val fakeBuildPath = "/randomapp/build/app/"
every { mockCmakeOptions.path } returns null
every { mockCmakeOptions.path(any()) } returns Unit
every { mockCmakeOptions.buildStagingDirectory(any()) } returns Unit
every { project.layout.buildDirectory } returns mockDirectoryProperty
every { mockDirectoryProperty.dir(any<String>()) } returns mockDirectoryProperty
every { mockDirectoryProperty.get() } returns mockDirectory
every { mockDirectory.asFile.path } returns fakeBuildPath
val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.buildTypes
.iterator()
} returns mutableListOf(mockBuildType).iterator()
every { mockBuildType.name } returns "Debug"
every { mockBuildType.externalNativeBuild.cmake.arguments(any(), any(), any()) } returns Unit
FlutterPluginUtils.forceNdkDownload(project, basePath)
verify(exactly = 1) {
mockCmakeOptions.path
}
verify(exactly = 1) { mockCmakeOptions.path("$basePath/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt") }
verify(exactly = 1) { mockCmakeOptions.buildStagingDirectory(any()) }
verify(exactly = 1) {
mockBuildType.externalNativeBuild.cmake.arguments(
"-Wno-dev",
"--no-warn-unused-cli",
"-DCMAKE_BUILD_TYPE=Debug"
)
}
}
// isFlutterAppProject skipped as it is a wrapper for a single getter that we would have to mock
// addFlutterDependencies
@Test
fun `addFlutterDependencies returns early if buildMode is not supported`() {
val project = mockk<Project>()
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithoutDevDependency
val buildType: BuildType = mockk<BuildType>()
every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true
every { project.hasProperty("local-engine-repo") } returns true
every { project.hasProperty("local-engine-build-mode") } returns true
every { project.property("local-engine-build-mode") } returns "release"
every { project.logger.quiet(any()) } returns Unit
FlutterPluginUtils.addFlutterDependencies(
project = project,
buildType = buildType,
pluginHandler = pluginHandler,
engineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23"
)
verify(exactly = 1) {
project.logger.quiet(
"Project does not support Flutter build mode: debug, " +
"skipping adding Flutter dependencies"
)
}
}
@Test
fun `addFlutterDependencies adds libflutter dependency but not embedding dependency when is a flutter app`() {
val project = mockk<Project>()
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithoutDevDependency
val buildType: BuildType = mockk<BuildType>()
val engineVersion = EXAMPLE_ENGINE_VERSION
every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true
every { project.hasProperty("local-engine-repo") } returns false
every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockk<ApplicationExtension>()
every { project.hasProperty("target-platform") } returns false
every { project.configurations.named("api") } returns mockk()
every { project.dependencies.add(any(), any()) } returns mockk()
FlutterPluginUtils.addFlutterDependencies(
project = project,
buildType = buildType,
pluginHandler = pluginHandler,
engineVersion = engineVersion
)
verify(exactly = 3) { project.dependencies.add(any(), any()) }
verify {
project.dependencies.add(
"debugApi",
"io.flutter:armeabi_v7a_debug:$engineVersion"
)
}
verify { project.dependencies.add("debugApi", "io.flutter:arm64_v8a_debug:$engineVersion") }
verify { project.dependencies.add("debugApi", "io.flutter:x86_64_debug:$engineVersion") }
}
@Test
fun `addFlutterDependencies adds libflutter and embedding dep when only dep is dev dep in release mode`() {
val project = mockk<Project>()
val pluginListWithSingleDevDependency = listOf(devDependency)
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithSingleDevDependency
val buildType: BuildType = mockk<BuildType>()
val engineVersion = EXAMPLE_ENGINE_VERSION
every { buildType.name } returns "release"
every { buildType.isDebuggable } returns false
every { project.hasProperty("local-engine-repo") } returns false
every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockk<ApplicationExtension>()
every { project.hasProperty("target-platform") } returns false
every { project.configurations.named("api") } returns mockk()
every { project.dependencies.add(any(), any()) } returns mockk()
FlutterPluginUtils.addFlutterDependencies(
project = project,
buildType = buildType,
pluginHandler = pluginHandler,
engineVersion = engineVersion
)
verify(exactly = 4) { project.dependencies.add(any(), any()) }
verify {
project.dependencies.add(
"releaseApi",
"io.flutter:flutter_embedding_release:$engineVersion"
)
}
verify {
project.dependencies.add(
"releaseApi",
"io.flutter:armeabi_v7a_release:$engineVersion"
)
}
verify {
project.dependencies.add(
"releaseApi",
"io.flutter:arm64_v8a_release:$engineVersion"
)
}
verify {
project.dependencies.add(
"releaseApi",
"io.flutter:x86_64_release:$engineVersion"
)
}
}
@Test
fun `addFlutterDependencies adds libflutter dep but not embedding dep when only dep is dev dep in debug mode`() {
val project = mockk<Project>()
val pluginListWithSingleDevDependency = listOf(devDependency)
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithSingleDevDependency
val buildType: BuildType = mockk<BuildType>()
val engineVersion = EXAMPLE_ENGINE_VERSION
every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true
every { project.hasProperty("local-engine-repo") } returns false
every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockk<ApplicationExtension>()
every { project.hasProperty("target-platform") } returns false
every { project.configurations.named("api") } returns mockk()
every { project.dependencies.add(any(), any()) } returns mockk()
FlutterPluginUtils.addFlutterDependencies(
project = project,
buildType = buildType,
pluginHandler = pluginHandler,
engineVersion = engineVersion
)
verify(exactly = 3) { project.dependencies.add(any(), any()) }
verify {
project.dependencies.add(
"debugApi",
"io.flutter:armeabi_v7a_debug:$engineVersion"
)
}
verify {
project.dependencies.add(
"debugApi",
"io.flutter:arm64_v8a_debug:$engineVersion"
)
}
verify {
project.dependencies.add(
"debugApi",
"io.flutter:x86_64_debug:$engineVersion"
)
}
}
@Test
fun `addTaskForJavaVersion adds task for Java version`() {
val project = mockk<Project>()
every { project.tasks.register(any(), eq(PrintTask::class.java), any()) } returns mockk()
val captureSlot = slot<Action<PrintTask>>()
FlutterPluginUtils.addTaskForJavaVersion(project)
verify { project.tasks.register("javaVersion", eq(PrintTask::class.java), capture(captureSlot)) }
val mockPrintTask = mockk<PrintTask>(relaxed = true)
captureSlot.captured.execute(mockPrintTask)
verify {
mockPrintTask.description = "Print the current java version used by gradle. see: " +
"https://docs.gradle.org/current/javadoc/org/gradle/api/JavaVersion.html"
}
}
// addTaskForKGPVersion
@Test
fun `addTaskForKGPVersion adds task for KGP version`() {
val project = mockk<Project>(relaxed = true)
every { project.tasks.register(any(), eq(PrintTask::class.java), any()) } returns mockk()
val captureSlot = slot<Action<PrintTask>>()
mockkObject(VersionFetcher)
every { VersionFetcher.getKGPVersion(project) } returns Version(2, 2, 0)
try {
FlutterPluginUtils.addTaskForKGPVersion(project)
verify { project.tasks.register("kgpVersion", eq(PrintTask::class.java), capture(captureSlot)) }
val mockPrintTask = mockk<PrintTask>(relaxed = true)
captureSlot.captured.execute(mockPrintTask)
verify {
mockPrintTask.description = "Print the current kgp version used by the project."
}
} finally {
io.mockk.unmockkObject(VersionFetcher)
}
}
// addTaskForPrintBuildVariants
@Test
fun `addTaskForPrintBuildVariants adds task for printing build variants`() {
val project = mockk<Project>()
val androidComponents = mockk<AndroidComponentsExtension<*, *, *>>(relaxed = true)
val listProperty = mockk<org.gradle.api.provider.ListProperty<String>>(relaxed = true)
every { project.extensions } returns mockk()
every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns androidComponents
every { project.objects.listProperty(String::class.java) } returns listProperty
every { project.tasks.register(any(), eq(PrintTask::class.java), any()) } returns mockk()
val captureSlot = slot<Action<PrintTask>>()
FlutterPluginUtils.addTaskForPrintBuildVariants(project)
verify { project.tasks.register("printBuildVariants", eq(PrintTask::class.java), capture(captureSlot)) }
val mockPrintTask = mockk<PrintTask>(relaxed = true)
captureSlot.captured.execute(mockPrintTask)
verify {
mockPrintTask.description = "Prints out all build variants for this Android project"
}
}
}