// 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.AndroidPluginVersion import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.variant.AndroidComponentsExtension 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 io.mockk.called import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject import io.mockk.slot import io.mockk.unmockkObject 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.internal.impldep.junit.framework.TestCase.assertFalse import org.gradle.internal.impldep.junit.framework.TestCase.assertTrue import org.jetbrains.kotlin.gradle.plugin.extraProperties import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach 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.io.path.createDirectory import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue /** * Configuration for a mock Gradle subproject. * * @property name The name of the subproject. * @property declarativelyAppliedPlugins Plugins applied via the modern Gradle `plugins {}` block. * For more details, see [Gradle Plugins Block Docs](https://docs.gradle.org/current/userguide/plugins_intermediate.html#sec:plugins_block). * @property imperativelyAppliedPlugins Plugins applied via the legacy Gradle `apply plugin:` statement. * For more details, see [Gradle Old Plugin Application Docs](https://docs.gradle.org/current/userguide/plugins_intermediate.html#sec:old_plugin_application). */ private data class SubprojectConfig( val name: String, val declarativelyAppliedPlugins: List = emptyList(), val imperativelyAppliedPlugins: List = emptyList() ) private class TestEnvironment( val appProject: Project, val plugins: List, val subprojectsActionSlot: io.mockk.CapturingSlot> = slot(), val projectsEvaluatedActionSlot: io.mockk.CapturingSlot> = slot() ) { val appPluginManager: PluginManager get() = appProject.pluginManager val plugin1Manager: PluginManager get() = plugins[0].pluginManager val plugin2Manager: PluginManager get() = plugins[1].pluginManager } class FlutterPluginUtilsTest { companion object { const val EXAMPLE_ENGINE_VERSION = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23" val devDependency: Map = 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()), Pair("dev_dependency", true) ) val cameraDependency: Map = 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()), Pair("dev_dependency", false) ) val flutterPluginAndroidLifecycleDependency: Map = 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()), Pair("dev_dependency", false) ) val pluginListWithoutDevDependency: List> = 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()), Pair("dev_dependency", false) ) ) val pluginListWithDevDependency: List> = 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()), Pair("dev_dependency", false) ) ) val manifestText = """ """.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() 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() 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() 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() 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() 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() 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() 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() 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() val assembleTask = mockk() 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() val assembleTask = mockk() 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() 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() flutterExtension.source = null every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension assertThrows { FlutterPluginUtils.getFlutterSourceDirectory(project) } } // getFlutterTarget @Test fun `getFlutterTarget returns the target when the project property is set`() { val project = mockk() 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() 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() 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() val variantName = "debug" val dependency = mockk() 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() val variantName = "debug" val dependency = mockk() 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() 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() 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() 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() 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() 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() 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() 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() every { project.hasProperty(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns true every { project.property(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns "android-invalid" val gradleException: GradleException = assertThrows { 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() every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" val result = FlutterPluginUtils.getCompileSdkFromProject(project) assertEquals("35", result) } // detectLowCompileSdkVersionOrNdkVersion @Test fun `detectLowCompileSdkVersionOrNdkVersion logs no warnings when no plugins have higher sdk or ndk`( @TempDir tempDir: Path ) { val projectDir = tempDir.resolve("app").toFile() val project = mockk() val mockLogger = mockk() every { project.logger } returns mockLogger every { project.projectDir } returns projectDir val cameraPluginProject = mockk() val projectActionSlot = slot>() val cameraPluginProjectActionSlot = slot>() every { project.afterEvaluate(any>()) } returns Unit every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264" every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns cameraPluginProject every { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } returns Unit every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264" FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, listOf(cameraDependency)) verify { project.afterEvaluate(capture(projectActionSlot)) } projectActionSlot.captured.execute(project) verify { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } cameraPluginProjectActionSlot.captured.execute(cameraPluginProject) verify { mockLogger wasNot called } } @Test fun `detectLowCompileSdkVersionOrNdkVersion logs warnings when plugins have higher sdk and ndk`( @TempDir tempDir: Path ) { val buildGradleFile = tempDir .resolve("app") .createDirectory() .resolve("build.gradle") .toFile() buildGradleFile.createNewFile() val projectDir = tempDir.resolve("app").toFile() val project = mockk() val mockLogger = mockk() every { project.logger } returns mockLogger every { mockLogger.error(any()) } returns Unit every { project.projectDir } returns projectDir val cameraPluginProject = mockk() val flutterPluginAndroidLifecycleDependencyPluginProject = mockk() val projectActionSlot = slot>() val cameraPluginProjectActionSlot = slot>() val flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot = slot>() every { project.afterEvaluate(any>()) } returns Unit every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-33" every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "24.3.11579264" every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns cameraPluginProject every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns flutterPluginAndroidLifecycleDependencyPluginProject every { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } returns Unit every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264" every { flutterPluginAndroidLifecycleDependencyPluginProject.afterEvaluate( capture( flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot ) ) } returns Unit every { flutterPluginAndroidLifecycleDependencyPluginProject.extensions .findByType( BaseExtension::class.java )!! .compileSdkVersion } returns "android-34" every { flutterPluginAndroidLifecycleDependencyPluginProject.extensions .findByType( BaseExtension::class.java )!! .ndkVersion } returns "25.3.11579264" val dependencyList: List> = listOf(cameraDependency, flutterPluginAndroidLifecycleDependency) FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion( project, dependencyList ) verify { project.afterEvaluate(capture(projectActionSlot)) } projectActionSlot.captured.execute(project) verify { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } cameraPluginProjectActionSlot.captured.execute(cameraPluginProject) verify { flutterPluginAndroidLifecycleDependencyPluginProject.afterEvaluate( capture( flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot ) ) } flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot.captured.execute( flutterPluginAndroidLifecycleDependencyPluginProject ) verify { mockLogger.error( "Your project is configured to compile against Android SDK 33, but the " + "following plugin(s) require to be compiled against a higher Android SDK version:" ) } verify { mockLogger.error( "- ${cameraDependency["name"]} compiles against Android SDK 35" ) } verify { mockLogger.error( "- ${flutterPluginAndroidLifecycleDependency["name"]} compiles against Android SDK 34" ) } verify { mockLogger.error( """ Fix this issue by compiling against the highest Android SDK version (they are backward compatible). Add the following to ${buildGradleFile.path}: android { compileSdk = 35 ... } """.trimIndent() ) } verify { mockLogger.error( "Your project is configured with Android NDK 24.3.11579264, but the following plugin(s) depend on a different Android NDK version:" ) } verify { mockLogger.error( "- ${cameraDependency["name"]} requires Android NDK 26.3.11579264" ) } verify { mockLogger.error( "- ${flutterPluginAndroidLifecycleDependency["name"]} requires Android NDK 25.3.11579264" ) } verify { mockLogger.error( """ Fix this issue by using the highest Android NDK version (they are backward compatible). Add the following to ${buildGradleFile.path}: android { ndkVersion = "26.3.11579264" ... } """.trimIndent() ) } } @Test fun `detectLowCompileSdkVersionOrNdkVersion throws IllegalArgumentException when plugin has no name`() { val project = mockk() val projectActionSlot = slot>() every { project.afterEvaluate(any>()) } returns Unit every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264" val pluginWithoutName: MutableMap = cameraDependency.toMutableMap() pluginWithoutName.remove("name") FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion( project, listOf(pluginWithoutName) ) verify { project.afterEvaluate(capture(projectActionSlot)) } assertThrows { projectActionSlot.captured.execute(project) } } 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 { private val rootProject = mockk() private val mockGradle = mockk() private val mockLogger = mockk(relaxed = true) // This AGP version will should match the Flutter create template values. // In //packages/flutter_tools/lib/src/android/gradle_utils.dart private val templateAgpVersion = AndroidPluginVersion(9, 0, 1) private val errorAgpVersion = DependencyVersionChecker.errorAGPVersion @BeforeEach fun setUp() { mockkObject(VersionFetcher) } @AfterEach fun tearDown() { unmockkObject(VersionFetcher) } private fun createSubproject( tempDir: Path, projectName: String, declarativelyAppliedPlugins: List = emptyList(), imperativelyAppliedPlugins: List = emptyList() ): Project { val projectDir = tempDir.resolve(projectName).toFile().apply { mkdirs() } val buildGradleFile = File(projectDir, "build.gradle").apply { createNewFile() // Expected output of declarativeBlock if declarativelyAppliedPlugins contains ["kotlin-android"]: // plugins { // id("kotlin-android") // } val declarativeBlock = if (declarativelyAppliedPlugins.isNotEmpty()) { "plugins {\n" + declarativelyAppliedPlugins.joinToString("\n") { " id(\"$it\")" } + "\n}\n" } else { "" } // Expected output of imperativeBlock if imperativelyAppliedPlugins contains ["kotlin-android"]: // apply plugin: 'kotlin-android' val imperativeBlock = if (imperativelyAppliedPlugins.isNotEmpty()) { imperativelyAppliedPlugins.joinToString("\n") { "apply plugin: '$it'" } + "\n" } else { "" } writeText(declarativeBlock + imperativeBlock) } val pluginManager = mockk(relaxed = true) val project = mockk() every { project.name } returns projectName every { project.projectDir } returns projectDir every { project.buildFile } returns buildGradleFile every { project.logger } returns mockLogger every { project.pluginManager } returns pluginManager every { project.rootProject } returns rootProject every { project.gradle } returns mockGradle val extensions = mockk() every { extensions.findByType(any>()) } returns null every { project.extensions } returns extensions return project } private fun setupTest( tempDir: Path, agpVersion: AndroidPluginVersion, appConfig: SubprojectConfig, pluginConfigs: List ): TestEnvironment { every { VersionFetcher.getAGPVersion(any()) } returns agpVersion val appProject = createSubproject( tempDir = tempDir, projectName = appConfig.name, declarativelyAppliedPlugins = appConfig.declarativelyAppliedPlugins, imperativelyAppliedPlugins = appConfig.imperativelyAppliedPlugins ) val pluginProjects = pluginConfigs.map { config -> createSubproject( tempDir = tempDir, projectName = config.name, declarativelyAppliedPlugins = config.declarativelyAppliedPlugins, imperativelyAppliedPlugins = config.imperativelyAppliedPlugins ) } val allProjects = setOf(appProject) + pluginProjects every { rootProject.subprojects } returns allProjects val testProject = TestEnvironment(appProject, pluginProjects) every { rootProject.subprojects(capture(testProject.subprojectsActionSlot)) } returns Unit every { mockGradle.projectsEvaluated(capture(testProject.projectsEvaluatedActionSlot)) } returns Unit return testProject } private fun executeDetectApplyingKotlinGradlePlugin(testProject: TestEnvironment) { detectApplyingKotlinGradlePlugin(testProject.appProject) verify { rootProject.subprojects(capture(testProject.subprojectsActionSlot)) } testProject.subprojectsActionSlot.captured.execute(testProject.appProject) for (plugin in testProject.plugins) { testProject.subprojectsActionSlot.captured.execute(plugin) } verify { mockGradle.projectsEvaluated(capture(testProject.projectsEvaluatedActionSlot)) } testProject.projectsEvaluatedActionSlot.captured.execute(mockGradle) } @Nested inner class TestLogWarningsWhenAGPis9OrHigher { @Test fun `logs app warning when KGP is only applied in app`( @TempDir tempDir: Path ) { val testProject = setupTest( tempDir = tempDir, agpVersion = templateAgpVersion, appConfig = SubprojectConfig( "app", declarativelyAppliedPlugins = listOf("com.android.application", "kotlin-android") ), pluginConfigs = listOf( SubprojectConfig( "plugin", declarativelyAppliedPlugins = listOf("com.android.library") ) ) ) val appPluginManager = testProject.appPluginManager val plugin1Manager = testProject.plugin1Manager executeDetectApplyingKotlinGradlePlugin(testProject) verify { mockLogger.error( match { it.contains("WARNING: Your Android app project") && it.contains("applies the Kotlin Gradle Plugin") && it.contains(BUILT_IN_KOTLIN_DOCS_FOR_APPS) } ) } verify(exactly = 0) { mockLogger.error(match { it.contains("WARNING: Your app uses the following plugins") }) } verify(exactly = 0) { appPluginManager.apply("kotlin-android") } verify(exactly = 1) { plugin1Manager.apply("kotlin-android") } } @Test fun `logs plugin warning when KGP is only applied in one plugin`( @TempDir tempDir: Path ) { val testProject = setupTest( tempDir = tempDir, agpVersion = templateAgpVersion, appConfig = SubprojectConfig( "app", declarativelyAppliedPlugins = listOf("com.android.application") ), pluginConfigs = listOf( SubprojectConfig( "plugin", declarativelyAppliedPlugins = listOf("com.android.library", "kotlin-android") ) ) ) val appPluginManager = testProject.appPluginManager val plugin1Manager = testProject.plugin1Manager executeDetectApplyingKotlinGradlePlugin(testProject) verify { mockLogger.error( match { it.contains("WARNING: Your app uses the following plugins") && it.contains(BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS) && it.contains(BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS) } ) } verify(exactly = 0) { mockLogger.error(match { it.contains("WARNING: Your Android app project") }) } verify(exactly = 1) { appPluginManager.apply("kotlin-android") } verify(exactly = 0) { plugin1Manager.apply("kotlin-android") } } @Test fun `logs app and plugin warning when KGP is applied in both app and plugins`( @TempDir tempDir: Path ) { val testProject = setupTest( tempDir = tempDir, agpVersion = templateAgpVersion, appConfig = SubprojectConfig( "app", declarativelyAppliedPlugins = listOf("com.android.application", "kotlin-android") ), pluginConfigs = listOf( SubprojectConfig( "plugin", declarativelyAppliedPlugins = listOf("com.android.library", "kotlin-android") ) ) ) val appPluginManager = testProject.appPluginManager val plugin1Manager = testProject.plugin1Manager executeDetectApplyingKotlinGradlePlugin(testProject) verify { mockLogger.error( match { it.contains("WARNING: Your Android app project") && it.contains("applies the Kotlin Gradle Plugin") && it.contains(BUILT_IN_KOTLIN_DOCS_FOR_APPS) } ) } verify { mockLogger.error( match { it.contains("WARNING: Your app uses the following plugins") && it.contains(BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS) && it.contains(BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS) } ) } verify(exactly = 0) { appPluginManager.apply("kotlin-android") } verify(exactly = 0) { plugin1Manager.apply("kotlin-android") } } @Test fun `logs app and plugin warning when imperative KGP configuration is applied in both app and plugins`( @TempDir tempDir: Path ) { val testProject = setupTest( tempDir = tempDir, agpVersion = templateAgpVersion, appConfig = SubprojectConfig( "app", imperativelyAppliedPlugins = listOf( "com.android.application", "kotlin-android" ) ), pluginConfigs = listOf( SubprojectConfig( "plugin1", imperativelyAppliedPlugins = listOf( "com.android.library", "kotlin-android" ) ), SubprojectConfig( "plugin2", imperativelyAppliedPlugins = listOf( "com.android.library", "kotlin-android" ) ) ) ) val appPluginManager = testProject.appPluginManager val plugin1Manager = testProject.plugin1Manager val plugin2Manager = testProject.plugin2Manager executeDetectApplyingKotlinGradlePlugin(testProject) verify { mockLogger.error( match { it.contains("WARNING: Your Android app project") && it.contains("applies the Kotlin Gradle Plugin") && it.contains(BUILT_IN_KOTLIN_DOCS_FOR_APPS) } ) } verify { mockLogger.error( match { it.contains("WARNING: Your app uses the following plugins") && it.contains(BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS) && it.contains(BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS) } ) } verify(exactly = 0) { appPluginManager.apply("kotlin-android") } verify(exactly = 0) { plugin1Manager.apply("kotlin-android") } verify(exactly = 0) { plugin2Manager.apply("kotlin-android") } } @Test fun `does not log when migrated to Built-in Kotlin`( @TempDir tempDir: Path ) { val testProject = setupTest( tempDir = tempDir, agpVersion = templateAgpVersion, appConfig = SubprojectConfig( "app", imperativelyAppliedPlugins = listOf("com.android.application") ), pluginConfigs = listOf( SubprojectConfig( "plugin", imperativelyAppliedPlugins = listOf("com.android.library") ) ) ) val appPluginManager = testProject.appPluginManager val plugin1Manager = testProject.plugin1Manager executeDetectApplyingKotlinGradlePlugin(testProject) verify(exactly = 0) { mockLogger.error(any()) } verify(exactly = 1) { appPluginManager.apply("kotlin-android") } verify(exactly = 1) { plugin1Manager.apply("kotlin-android") } } @Test fun `logs KGP warning when KGP attempts to but fails to apply`( @TempDir tempDir: Path ) { val testProject = setupTest( tempDir = tempDir, agpVersion = templateAgpVersion, appConfig = SubprojectConfig( "app", declarativelyAppliedPlugins = listOf("com.android.application") ), pluginConfigs = listOf( SubprojectConfig( "plugin", declarativelyAppliedPlugins = listOf("com.android.library") ) ) ) val appPluginManager = testProject.appPluginManager val plugin1Manager = testProject.plugin1Manager every { appPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath") every { plugin1Manager.apply("kotlin-android") } throws Exception("KGP not on classpath") executeDetectApplyingKotlinGradlePlugin(testProject) verify(exactly = 0) { mockLogger.error(any()) } verify { mockLogger.quiet( match { it.contains("Applying the Kotlin Android Plugin (KGP) was unsuccessful") && it.contains("ensure KGP is declared in the root plugins block") && it.contains(BUILT_IN_KOTLIN_DOCS) } ) } } } @Nested inner class TestLogWarningsWhenAGPIsLessThan9 { @Test fun `does not log warnings when migrated to built-in kotlin`( @TempDir tempDir: Path ) { val testProject = setupTest( tempDir = tempDir, agpVersion = errorAgpVersion, appConfig = SubprojectConfig( "app", declarativelyAppliedPlugins = listOf("com.android.application") ), pluginConfigs = listOf( SubprojectConfig( "plugin", declarativelyAppliedPlugins = listOf("com.android.library") ) ) ) val appPluginManager = testProject.appPluginManager val plugin1Manager = testProject.plugin1Manager executeDetectApplyingKotlinGradlePlugin(testProject) // No warnings should be logged because AGP version is < 9. verify(exactly = 0) { mockLogger.error(any()) } // KGP is still applied in case the entire project has been migrated. verify(exactly = 1) { appPluginManager.apply("kotlin-android") } verify(exactly = 1) { plugin1Manager.apply("kotlin-android") } } @Test fun `does not log warnings when KGP is applied`( @TempDir tempDir: Path ) { val testProject = setupTest( tempDir = tempDir, agpVersion = errorAgpVersion, appConfig = SubprojectConfig( "app", declarativelyAppliedPlugins = listOf("com.android.application", "kotlin-android") ), pluginConfigs = listOf( SubprojectConfig( "plugin", declarativelyAppliedPlugins = listOf("com.android.library", "kotlin-android") ) ) ) val appPluginManager = testProject.appPluginManager val plugin1Manager = testProject.plugin1Manager executeDetectApplyingKotlinGradlePlugin(testProject) // No warnings should be logged because AGP version is < 9. verify(exactly = 0) { mockLogger.error(any()) } verify(exactly = 0) { appPluginManager.apply("kotlin-android") } verify(exactly = 0) { plugin1Manager.apply("kotlin-android") } } @Test fun `logs KGP warning when KGP attempts to but fails to apply`( @TempDir tempDir: Path ) { val testProject = setupTest( tempDir = tempDir, agpVersion = errorAgpVersion, appConfig = SubprojectConfig( "app", declarativelyAppliedPlugins = listOf("com.android.application") ), pluginConfigs = listOf( SubprojectConfig( "plugin", declarativelyAppliedPlugins = listOf("com.android.library") ) ) ) val appPluginManager = testProject.appPluginManager val plugin1Manager = testProject.plugin1Manager every { appPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath") every { plugin1Manager.apply("kotlin-android") } throws Exception("KGP not on classpath") executeDetectApplyingKotlinGradlePlugin(testProject) verify(exactly = 0) { mockLogger.error(any()) } verify { mockLogger.quiet( match { it.contains("Applying the Kotlin Android Plugin (KGP) was unsuccessful") && it.contains("ensure KGP is declared in the root plugins block") && it.contains(BUILT_IN_KOTLIN_DOCS) } ) } } } } } // 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() val mockCmakeOptions = mockk() val mockDefaultConfig = mockk() 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() val mockCmakeOptions = mockk() val mockDefaultConfig = mockk() val mockDirectoryProperty = mockk() val mockDirectory = mockk() 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()) } returns mockDirectoryProperty every { mockDirectoryProperty.get() } returns mockDirectory every { mockDirectory.asFile.path } returns fakeBuildPath val mockBuildType = mockk() 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() 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() 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() 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() 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() 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() 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() 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() 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() 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() 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() 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" ) } } // addTaskForJavaVersion @Test fun `addTaskForJavaVersion adds task for Java version`() { val project = mockk() every { project.tasks.register(any(), any>()) } returns mockk() val captureSlot = slot>() FlutterPluginUtils.addTaskForJavaVersion(project) verify { project.tasks.register("javaVersion", capture(captureSlot)) } val mockTask = mockk() every { mockTask.description = any() } returns Unit every { mockTask.doLast(any>()) } returns mockk() captureSlot.captured.execute(mockTask) verify { mockTask.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() every { project.tasks.register(any(), any>()) } returns mockk() val captureSlot = slot>() FlutterPluginUtils.addTaskForKGPVersion(project) verify { project.tasks.register("kgpVersion", capture(captureSlot)) } val mockTask = mockk() every { mockTask.description = any() } returns Unit every { mockTask.doLast(any>()) } returns mockk() captureSlot.captured.execute(mockTask) verify { mockTask.description = "Print the current kgp version used by the project." } } // addTaskForPrintBuildVariants @Test fun `addTaskForPrintBuildVariants adds task for printing build variants`() { val project = mockk() val androidComponents = mockk>(relaxed = true) val listProperty = mockk>() every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns androidComponents every { project.objects.listProperty(String::class.java) } returns listProperty every { project.tasks.register(any(), any>()) } returns mockk() val captureSlot = slot>() FlutterPluginUtils.addTaskForPrintBuildVariants(project) verify { project.tasks.register("printBuildVariants", capture(captureSlot)) } val mockTask = mockk() every { mockTask.description = any() } returns Unit every { mockTask.doLast(any>()) } returns mockk() captureSlot.captured.execute(mockTask) verify { mockTask.description = "Prints out all build variants for this Android project" } } }