| // 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.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.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.internal.project.ProjectInternal |
| import org.gradle.api.invocation.Gradle |
| import org.gradle.api.logging.Logger |
| import org.gradle.api.plugins.PluginManager |
| import org.gradle.api.provider.Provider |
| import org.gradle.api.provider.ProviderFactory |
| import org.gradle.kotlin.dsl.support.serviceOf |
| import org.gradle.process.ExecOperations |
| import org.gradle.process.ExecResult |
| import org.gradle.process.ExecSpec |
| 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.io.IOException |
| 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.assertNotNull |
| import kotlin.test.assertNull |
| 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 imperative `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<String> = emptyList(), |
| val imperativelyAppliedPlugins: List<String> = emptyList() |
| ) |
| |
| private class TestEnvironment( |
| val appProject: Project, |
| val plugins: List<Project>, |
| val subprojectsActionSlot: io.mockk.CapturingSlot<Action<Project>> = slot(), |
| val projectsEvaluatedActionSlot: io.mockk.CapturingSlot<Action<Gradle>> = 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<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() |
| ) |
| } |
| } |
| |
| private fun writeBuildFile( |
| buildFile: File, |
| declarativelyAppliedPlugins: List<String> = emptyList(), |
| imperativelyAppliedPlugins: List<String> = emptyList() |
| ) { |
| buildFile.apply { |
| parentFile.mkdirs() |
| if (!exists()) { |
| createNewFile() |
| } |
| val declarativeBlock = |
| if (declarativelyAppliedPlugins.isNotEmpty()) { |
| // Expected output of declarativeBlock if declarativelyAppliedPlugins contains ["kotlin-android"]: |
| // plugins { |
| // id("kotlin-android") |
| // } |
| "plugins {\n" + declarativelyAppliedPlugins.joinToString("\n") { " id(\"$it\")" } + "\n}\n" |
| } else { |
| "" |
| } |
| val imperativeBlock = |
| if (imperativelyAppliedPlugins.isNotEmpty()) { |
| // Expected output of imperativeBlock if imperativelyAppliedPlugins contains ["kotlin-android"]: |
| // apply plugin: 'kotlin-android' |
| imperativelyAppliedPlugins.joinToString("\n") { "apply plugin: '$it'" } + "\n" |
| } else { |
| "" |
| } |
| writeText(declarativeBlock + imperativeBlock) |
| } |
| } |
| |
| 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 GetSubprojectPluginStateTests { |
| @Test |
| fun `returns null if build file does not exist`() { |
| val subproject = mockk<Project>() |
| val file = mockk<File>() |
| every { subproject.buildFile } returns file |
| every { file.exists() } returns false |
| |
| val result = FlutterPluginUtils.getSubprojectPluginState(subproject) |
| |
| assertNull(result) |
| } |
| |
| @Test |
| fun `returns null if build file path contains ephemeral android directory`() { |
| val subproject = mockk<Project>() |
| val file = mockk<File>() |
| every { subproject.buildFile } returns file |
| every { file.exists() } returns true |
| every { file.absolutePath } returns "/path/to/.android/build.gradle" |
| |
| val result = FlutterPluginUtils.getSubprojectPluginState(subproject) |
| |
| assertNull(result) |
| } |
| |
| @Test |
| fun `returns null and logs error when IOException is thrown during read`( |
| @TempDir tempDir: Path |
| ) { |
| val subproject = mockk<Project>() |
| val mockBuildFile = mockk<File>() |
| val mockLogger = mockk<Logger>(relaxed = true) |
| |
| every { subproject.buildFile } returns mockBuildFile |
| every { mockBuildFile.exists() } returns true |
| every { mockBuildFile.absolutePath } returns "/some/path/build.gradle" |
| every { mockBuildFile.extension } returns "gradle" |
| every { mockBuildFile.path } throws IOException("Simulated I/O error") |
| every { subproject.projectDir } returns tempDir.toFile() |
| every { subproject.logger } returns mockLogger |
| |
| val result = FlutterPluginUtils.getSubprojectPluginState(subproject) |
| |
| assertNull(result) |
| verify(exactly = 1) { |
| mockLogger.error( |
| "Failed to read build file: /some/path/build.gradle", |
| any<IOException>() |
| ) |
| } |
| } |
| |
| @Test |
| fun `detects KGP and AGP in app subproject indicated by AGP app id in Groovy DSL`( |
| @TempDir tempDir: Path |
| ) { |
| val buildFile = tempDir.resolve("build.gradle").toFile() |
| writeBuildFile( |
| buildFile = buildFile, |
| imperativelyAppliedPlugins = listOf("com.android.application", "kotlin-android") |
| ) |
| val subproject = mockk<Project>() |
| every { subproject.buildFile } returns buildFile |
| every { subproject.projectDir } returns tempDir.toFile() |
| every { subproject.logger } returns mockk(relaxed = true) |
| |
| val state = FlutterPluginUtils.getSubprojectPluginState(subproject) |
| |
| assertNotNull(state) |
| assertTrue(state.hasAppPlugin) |
| assertTrue(state.hasKgpPlugin) |
| assertFalse(state.hasLibPlugin) |
| } |
| |
| @Test |
| fun `detects KGP and AGP in library subproject indicated by AGP library id in Groovy DSL`( |
| @TempDir tempDir: Path |
| ) { |
| val buildFile = tempDir.resolve("build.gradle").toFile() |
| writeBuildFile( |
| buildFile = buildFile, |
| imperativelyAppliedPlugins = listOf("com.android.library", "kotlin-android") |
| ) |
| val subproject = mockk<Project>() |
| every { subproject.buildFile } returns buildFile |
| every { subproject.projectDir } returns tempDir.toFile() |
| every { subproject.logger } returns mockk(relaxed = true) |
| |
| val state = FlutterPluginUtils.getSubprojectPluginState(subproject) |
| |
| assertNotNull(state) |
| assertFalse(state.hasAppPlugin) |
| assertTrue(state.hasKgpPlugin) |
| assertTrue(state.hasLibPlugin) |
| } |
| |
| @Test |
| fun `does not detect KGP or AGP in Groovy DSL`( |
| @TempDir tempDir: Path |
| ) { |
| val buildFile = tempDir.resolve("build.gradle").toFile() |
| writeBuildFile( |
| buildFile = buildFile, |
| imperativelyAppliedPlugins = listOf("java") |
| ) |
| val subproject = mockk<Project>() |
| every { subproject.buildFile } returns buildFile |
| every { subproject.projectDir } returns tempDir.toFile() |
| every { subproject.logger } returns mockk(relaxed = true) |
| |
| val state = FlutterPluginUtils.getSubprojectPluginState(subproject) |
| |
| assertNotNull(state) |
| assertFalse(state.hasAppPlugin) |
| assertFalse(state.hasKgpPlugin) |
| assertFalse(state.hasLibPlugin) |
| } |
| |
| @Test |
| fun `detects KGP and AGP in app subproject indicated by AGP app id in Kotlin DSL`( |
| @TempDir tempDir: Path |
| ) { |
| val buildFile = tempDir.resolve("build.gradle.kts").toFile() |
| writeBuildFile( |
| buildFile = buildFile, |
| declarativelyAppliedPlugins = listOf("com.android.application", "org.jetbrains.kotlin.android") |
| ) |
| val subproject = mockk<Project>() |
| every { subproject.buildFile } returns buildFile |
| every { subproject.projectDir } returns tempDir.toFile() |
| every { subproject.logger } returns mockk(relaxed = true) |
| |
| val state = FlutterPluginUtils.getSubprojectPluginState(subproject) |
| |
| assertNotNull(state) |
| assertTrue(state.hasAppPlugin) |
| assertTrue(state.hasKgpPlugin) |
| assertFalse(state.hasLibPlugin) |
| } |
| |
| @Test |
| fun `detects KGP and AGP in library subproject indicated by AGP library id in Kotlin DSL`( |
| @TempDir tempDir: Path |
| ) { |
| val buildFile = tempDir.resolve("build.gradle.kts").toFile() |
| writeBuildFile( |
| buildFile = buildFile, |
| declarativelyAppliedPlugins = listOf("com.android.library", "org.jetbrains.kotlin.android") |
| ) |
| val subproject = mockk<Project>() |
| every { subproject.buildFile } returns buildFile |
| every { subproject.projectDir } returns tempDir.toFile() |
| every { subproject.logger } returns mockk(relaxed = true) |
| |
| val state = FlutterPluginUtils.getSubprojectPluginState(subproject) |
| |
| assertNotNull(state) |
| assertFalse(state.hasAppPlugin) |
| assertTrue(state.hasKgpPlugin) |
| assertTrue(state.hasLibPlugin) |
| } |
| |
| @Test |
| fun `does not detect KGP or AGP in Kotlin DSL`( |
| @TempDir tempDir: Path |
| ) { |
| val buildFile = tempDir.resolve("build.gradle.kts").toFile() |
| writeBuildFile( |
| buildFile = buildFile, |
| declarativelyAppliedPlugins = listOf("kotlin(\"jvm\") version \"1.9.0\"") |
| ) |
| val subproject = mockk<Project>() |
| every { subproject.buildFile } returns buildFile |
| every { subproject.projectDir } returns tempDir.toFile() |
| every { subproject.logger } returns mockk(relaxed = true) |
| |
| val state = FlutterPluginUtils.getSubprojectPluginState(subproject) |
| |
| assertNotNull(state) |
| assertFalse(state.hasAppPlugin) |
| assertFalse(state.hasKgpPlugin) |
| assertFalse(state.hasLibPlugin) |
| } |
| } |
| |
| @Nested |
| inner class DetectApplyingKotlinGradlePluginTests { |
| private val rootProject = mockk<Project>() |
| private val mockGradle = mockk<Gradle>() |
| private val mockLogger = mockk<Logger>(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, 1, 0) |
| |
| private val errorAgpVersion = DependencyVersionChecker.errorAGPVersion |
| |
| private fun mockBuiltInKotlinProperty(value: String?) { |
| val mockProvider = mockk<Provider<String>>() |
| every { mockProvider.orNull } returns value |
| val mockProviders = mockk<ProviderFactory>() |
| every { mockProviders.gradleProperty("android.builtInKotlin") } returns mockProvider |
| every { rootProject.providers } returns mockProviders |
| every { rootProject.findProperty("android.builtInKotlin") } returns value |
| } |
| |
| @Nested |
| inner class IsBuiltInKotlinEnabledTests { |
| private fun setupProjectWithProperty(propertyValue: String?): Project { |
| val project = mockk<Project>() |
| every { project.rootProject } returns rootProject |
| every { project.providers } answers { rootProject.providers } |
| mockBuiltInKotlinProperty(propertyValue) |
| return project |
| } |
| |
| @Test |
| fun `returns false when AGP version is null`() { |
| val project = setupProjectWithProperty("true") |
| |
| val result = FlutterPluginUtils.isBuiltInKotlinEnabled(project, null) |
| |
| assertFalse(result) |
| } |
| |
| @Test |
| fun `returns false when AGP is less than 9 and builtInKotlin is set to true`() { |
| val subproject = setupProjectWithProperty("true") |
| |
| val result = FlutterPluginUtils.isBuiltInKotlinEnabled(subproject, errorAgpVersion) |
| |
| assertFalse(result) |
| } |
| |
| @Test |
| fun `returns false when AGP is less than 9 and builtInKotlin is set to TRUE`() { |
| val subproject = setupProjectWithProperty("TRUE") |
| |
| val result = FlutterPluginUtils.isBuiltInKotlinEnabled(subproject, errorAgpVersion) |
| |
| assertFalse(result) |
| } |
| |
| @Test |
| fun `returns false when AGP is less than 9 and builtInKotlin is set to false`() { |
| val subproject = setupProjectWithProperty("false") |
| |
| val result = FlutterPluginUtils.isBuiltInKotlinEnabled(subproject, errorAgpVersion) |
| |
| assertFalse(result) |
| } |
| |
| @Test |
| fun `returns false when AGP is less than 9 and builtInKotlin is set to FALSE`() { |
| val subproject = setupProjectWithProperty("FALSE") |
| |
| val result = FlutterPluginUtils.isBuiltInKotlinEnabled(subproject, errorAgpVersion) |
| |
| assertFalse(result) |
| } |
| |
| @Test |
| fun `returns true when AGP is 9 or higher and builtInKotlin is set to true`() { |
| val subproject = setupProjectWithProperty("true") |
| |
| val result = FlutterPluginUtils.isBuiltInKotlinEnabled(subproject, templateAgpVersion) |
| |
| assertTrue(result) |
| } |
| |
| @Test |
| fun `returns true when AGP is 9 or higher and builtInKotlin is set to TRUE`() { |
| val subproject = setupProjectWithProperty("TRUE") |
| |
| val result = FlutterPluginUtils.isBuiltInKotlinEnabled(subproject, templateAgpVersion) |
| |
| assertTrue(result) |
| } |
| |
| @Test |
| fun `returns false when AGP is 9 or higher and builtInKotlin is set to false`() { |
| val subproject = setupProjectWithProperty("false") |
| |
| val result = FlutterPluginUtils.isBuiltInKotlinEnabled(subproject, templateAgpVersion) |
| |
| assertFalse(result) |
| } |
| |
| @Test |
| fun `returns false when AGP is 9 or higher and builtInKotlin is set to FALSE`() { |
| val subproject = setupProjectWithProperty("FALSE") |
| |
| val result = FlutterPluginUtils.isBuiltInKotlinEnabled(subproject, templateAgpVersion) |
| |
| assertFalse(result) |
| } |
| |
| @Test |
| fun `defaults to true when property is null and AGP is 9 or higher`() { |
| val subproject = setupProjectWithProperty(null) |
| |
| val result = FlutterPluginUtils.isBuiltInKotlinEnabled(subproject, templateAgpVersion) |
| |
| assertTrue(result) |
| } |
| } |
| |
| @BeforeEach |
| fun setUp() { |
| mockkObject(VersionFetcher) |
| } |
| |
| @AfterEach |
| fun tearDown() { |
| unmockkObject(VersionFetcher) |
| } |
| |
| private fun writeGradleProperties( |
| rootDir: File, |
| content: String |
| ) { |
| File(rootDir, "gradle.properties").apply { |
| createNewFile() |
| writeText(content) |
| } |
| } |
| |
| private fun createSubproject( |
| tempDir: Path, |
| projectName: String, |
| declarativelyAppliedPlugins: List<String> = emptyList(), |
| imperativelyAppliedPlugins: List<String> = emptyList() |
| ): Project { |
| val rootDir = tempDir.toFile() |
| every { rootProject.file("gradle.properties") } returns File(rootDir, "gradle.properties") |
| every { rootProject.projectDir } returns rootDir |
| |
| val projectDir = tempDir.resolve(projectName).toFile().apply { mkdirs() } |
| val buildGradleFile = File(projectDir, "build.gradle") |
| writeBuildFile(buildGradleFile, declarativelyAppliedPlugins, imperativelyAppliedPlugins) |
| val pluginManager = mockk<PluginManager>(relaxed = true) |
| val project = mockk<Project>() |
| 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.providers } answers { rootProject.providers } |
| every { project.gradle } returns mockGradle |
| every { project.findProperty(any()) } answers { rootProject.findProperty(arg(0)) } |
| |
| val pluginContainer = mockk<org.gradle.api.plugins.PluginContainer>(relaxed = true) |
| every { pluginContainer.withId(any(), any()) } answers { |
| // Cast is required because star-projecting Action (a consumer) yields Action<Nothing>, making it unexecutable. |
| @Suppress("UNCHECKED_CAST") |
| val action = args[1] as Action<org.gradle.api.Plugin<*>> |
| action.execute(mockk(relaxed = true)) |
| } |
| every { project.plugins } returns pluginContainer |
| |
| return project |
| } |
| |
| private fun setupTest( |
| tempDir: Path, |
| agpVersion: AndroidPluginVersion, |
| builtInKotlin: String?, |
| appConfig: SubprojectConfig, |
| pluginConfigs: List<SubprojectConfig>, |
| captureActions: Boolean = true |
| ): TestEnvironment { |
| val rootDir = tempDir.toFile() |
| if (builtInKotlin != null) { |
| writeGradleProperties(rootDir, "android.builtInKotlin=$builtInKotlin\n") |
| } else { |
| writeGradleProperties(rootDir, "") |
| } |
| 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 |
| mockBuiltInKotlinProperty(builtInKotlin) |
| |
| val testProject = TestEnvironment(appProject, pluginProjects) |
| |
| if (captureActions) { |
| every { rootProject.subprojects(capture(testProject.subprojectsActionSlot)) } answers { |
| val action = firstArg<Action<Project>>() |
| allProjects.forEach { action.execute(it) } |
| } |
| every { mockGradle.projectsEvaluated(capture(testProject.projectsEvaluatedActionSlot)) } returns Unit |
| } |
| |
| return testProject |
| } |
| |
| private fun executeDetectApplyingKotlinGradlePlugin(testProject: TestEnvironment) { |
| detectApplyingKotlinGradlePlugin(testProject.appProject) |
| |
| verify { rootProject.subprojects(any<Action<Project>>()) } |
| |
| if (testProject.projectsEvaluatedActionSlot.isCaptured) { |
| verify { mockGradle.projectsEvaluated(capture(testProject.projectsEvaluatedActionSlot)) } |
| testProject.projectsEvaluatedActionSlot.captured.execute(mockGradle) |
| } else { |
| verify(exactly = 0) { mockGradle.projectsEvaluated(any<Action<Gradle>>()) } |
| } |
| } |
| |
| @Nested |
| inner class BuiltInKotlinIsEnabledAndAgpIs9OrHigher { |
| @Test |
| fun `does not log nor apply KGP`( |
| @TempDir tempDir: Path |
| ) { |
| val testProject = |
| setupTest( |
| tempDir = tempDir, |
| agpVersion = templateAgpVersion, |
| builtInKotlin = "true", |
| appConfig = SubprojectConfig("app", declarativelyAppliedPlugins = listOf("com.android.application")), |
| pluginConfigs = listOf(SubprojectConfig("plugin", declarativelyAppliedPlugins = listOf("com.android.library"))) |
| ) |
| |
| detectApplyingKotlinGradlePlugin(testProject.appProject) |
| |
| verify(exactly = 1) { rootProject.subprojects(any<Action<Project>>()) } |
| verify(exactly = 0) { testProject.appPluginManager.apply("kotlin-android") } |
| verify(exactly = 0) { testProject.plugin1Manager.apply("kotlin-android") } |
| } |
| |
| @Test |
| fun `does not log nor apply KGP when property is null`( |
| @TempDir tempDir: Path |
| ) { |
| val testProject = |
| setupTest( |
| tempDir = tempDir, |
| agpVersion = templateAgpVersion, |
| builtInKotlin = null, |
| appConfig = SubprojectConfig("app", declarativelyAppliedPlugins = listOf("com.android.application")), |
| pluginConfigs = listOf(SubprojectConfig("plugin", declarativelyAppliedPlugins = listOf("com.android.library"))) |
| ) |
| |
| detectApplyingKotlinGradlePlugin(testProject.appProject) |
| |
| verify(exactly = 1) { rootProject.subprojects(any<Action<Project>>()) } |
| verify(exactly = 0) { testProject.appPluginManager.apply("kotlin-android") } |
| verify(exactly = 0) { testProject.plugin1Manager.apply("kotlin-android") } |
| } |
| } |
| |
| @Nested |
| inner class BuiltInKotlinIsDisabled { |
| @Nested |
| inner class AgpIs9OrHigher { |
| @Test |
| fun `logs warning when KGP is only applied in app`( |
| @TempDir tempDir: Path |
| ) { |
| val testProject = |
| setupTest( |
| tempDir = tempDir, |
| agpVersion = templateAgpVersion, |
| builtInKotlin = "false", |
| appConfig = |
| SubprojectConfig( |
| "app", |
| declarativelyAppliedPlugins = listOf("com.android.application", "kotlin-android") |
| ), |
| pluginConfigs = |
| listOf( |
| SubprojectConfig("plugin", declarativelyAppliedPlugins = listOf("com.android.library")) |
| ) |
| ) |
| |
| 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") }) |
| } |
| val appPluginManager = testProject.appPluginManager |
| val plugin1Manager = testProject.plugin1Manager |
| verify(exactly = 0) { appPluginManager.apply("kotlin-android") } |
| verify(exactly = 1) { plugin1Manager.apply("kotlin-android") } |
| } |
| |
| @Test |
| fun `logs warning when KGP is only applied in one plugin`( |
| @TempDir tempDir: Path |
| ) { |
| val testProject = |
| setupTest( |
| tempDir = tempDir, |
| agpVersion = templateAgpVersion, |
| builtInKotlin = "false", |
| appConfig = SubprojectConfig("app", declarativelyAppliedPlugins = listOf("com.android.application")), |
| pluginConfigs = |
| listOf( |
| SubprojectConfig( |
| "plugin", |
| declarativelyAppliedPlugins = listOf("com.android.library", "kotlin-android") |
| ) |
| ) |
| ) |
| |
| 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") }) |
| } |
| val appPluginManager = testProject.appPluginManager |
| val plugin1Manager = testProject.plugin1Manager |
| verify(exactly = 1) { appPluginManager.apply("kotlin-android") } |
| verify(exactly = 0) { plugin1Manager.apply("kotlin-android") } |
| } |
| |
| @Test |
| fun `logs warning when KGP is applied in both app and plugin`( |
| @TempDir tempDir: Path |
| ) { |
| val testProject = |
| setupTest( |
| tempDir = tempDir, |
| agpVersion = templateAgpVersion, |
| builtInKotlin = "false", |
| appConfig = |
| SubprojectConfig( |
| "app", |
| declarativelyAppliedPlugins = listOf("com.android.application", "kotlin-android") |
| ), |
| pluginConfigs = |
| listOf( |
| SubprojectConfig( |
| "plugin", |
| declarativelyAppliedPlugins = listOf("com.android.library", "kotlin-android") |
| ) |
| ) |
| ) |
| |
| 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) { testProject.appPluginManager.apply("kotlin-android") } |
| verify(exactly = 0) { testProject.plugin1Manager.apply("kotlin-android") } |
| } |
| |
| @Test |
| fun `logs warning when KGP is imperatively applied in both app and plugins`( |
| @TempDir tempDir: Path |
| ) { |
| val testProject = |
| setupTest( |
| tempDir = tempDir, |
| agpVersion = templateAgpVersion, |
| builtInKotlin = "false", |
| 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" |
| ) |
| ) |
| ) |
| ) |
| |
| 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) { testProject.appPluginManager.apply("kotlin-android") } |
| verify(exactly = 0) { testProject.plugin1Manager.apply("kotlin-android") } |
| verify(exactly = 0) { testProject.plugin2Manager.apply("kotlin-android") } |
| } |
| |
| @Test |
| fun `does not log when migrated`( |
| @TempDir tempDir: Path |
| ) { |
| val testProject = |
| setupTest( |
| tempDir = tempDir, |
| agpVersion = templateAgpVersion, |
| builtInKotlin = "false", |
| appConfig = SubprojectConfig("app", imperativelyAppliedPlugins = listOf("com.android.application")), |
| pluginConfigs = |
| listOf( |
| SubprojectConfig("plugin", imperativelyAppliedPlugins = listOf("com.android.library")) |
| ) |
| ) |
| |
| executeDetectApplyingKotlinGradlePlugin(testProject) |
| |
| verify(exactly = 0) { |
| mockLogger.error(any()) |
| } |
| |
| val appPluginManager = testProject.appPluginManager |
| val plugin1Manager = testProject.plugin1Manager |
| verify(exactly = 1) { appPluginManager.apply("kotlin-android") } |
| verify(exactly = 1) { plugin1Manager.apply("kotlin-android") } |
| } |
| |
| @Test |
| fun `logs quiet warning when KGP fails to apply`( |
| @TempDir tempDir: Path |
| ) { |
| val testProject = |
| setupTest( |
| tempDir = tempDir, |
| agpVersion = templateAgpVersion, |
| builtInKotlin = "false", |
| 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 AgpIsLessThan9 { |
| @Test |
| fun `applies KGP without logging`( |
| @TempDir tempDir: Path |
| ) { |
| val testProject = |
| setupTest( |
| tempDir = tempDir, |
| agpVersion = errorAgpVersion, |
| builtInKotlin = "false", |
| 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 re-apply KGP and does not log`( |
| @TempDir tempDir: Path |
| ) { |
| val testProject = |
| setupTest( |
| tempDir = tempDir, |
| agpVersion = errorAgpVersion, |
| builtInKotlin = "false", |
| 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 quiet warning when KGP fails to apply`( |
| @TempDir tempDir: Path |
| ) { |
| val testProject = |
| setupTest( |
| tempDir = tempDir, |
| agpVersion = errorAgpVersion, |
| builtInKotlin = "false", |
| 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) |
| } |
| ) |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private fun captureFinalizeDslAction(project: Project) = |
| slot<(Any) -> Unit>().also { finalizeDslSlot -> |
| val androidComponents = |
| mockk<AndroidComponentsExtension<Any, VariantBuilder, Variant>>() |
| every { |
| project.extensions.findByType(AndroidComponentsExtension::class.java) |
| } returns androidComponents as AndroidComponentsExtension<*, *, *> |
| every { androidComponents.finalizeDsl(capture(finalizeDslSlot)) } returns Unit |
| } |
| |
| // 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(ApplicationExtension::class.java) } returns null |
| 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 installs a missing ndk when tool properties are provided`( |
| @TempDir tempDir: Path |
| ) { |
| val project = mockk<ProjectInternal>() |
| val finalizeDslSlot = captureFinalizeDslAction(project) |
| val execActionSlot = slot<Action<ExecSpec>>() |
| val mockExecSpec = mockk<ExecSpec>() |
| val mockExecResult = mockk<ExecResult>() |
| val mockExecOperations = mockk<ExecOperations>() |
| val mockCmakeOptions = mockk<CmakeOptions>() |
| val mockDefaultConfig = mockk<DefaultConfig>() |
| val mockBaseExtension = mockk<BaseExtension>() |
| every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension |
| every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions |
| every { mockBaseExtension.defaultConfig } returns mockDefaultConfig |
| every { mockBaseExtension.ndkVersion } returns "29.0.13846066" |
| every { mockCmakeOptions.path } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns "/sdkmanager" |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns tempDir.toString() |
| every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "" |
| every { project.gradle.startParameter.taskNames } returns emptyList() |
| every { project.gradle.startParameter.isOffline } returns false |
| every { project.extensions.findByType(ApplicationExtension::class.java) } returns null |
| every { project.serviceOf<ExecOperations>() } returns mockExecOperations |
| every { mockExecOperations.exec(capture(execActionSlot)) } answers { |
| File(tempDir.toFile(), "ndk/29.0.13846066/source.properties").apply { |
| parentFile.mkdirs() |
| createNewFile() |
| } |
| mockExecResult |
| } |
| every { mockExecResult.assertNormalExitValue() } returns mockExecResult |
| every { mockExecSpec.commandLine(any<List<String>>()) } returns mockExecSpec |
| |
| FlutterPluginUtils.forceNdkDownload(project, "/base/path") |
| finalizeDslSlot.captured.invoke(Any()) |
| execActionSlot.captured.execute(mockExecSpec) |
| |
| verify(exactly = 1) { mockExecOperations.exec(any<Action<ExecSpec>>()) } |
| verify { |
| mockExecSpec.commandLine( |
| listOf( |
| "/sdkmanager", |
| "--sdk_root=$tempDir", |
| "--install", |
| "ndk;29.0.13846066" |
| ) |
| ) |
| } |
| verify(exactly = 0) { mockCmakeOptions.path(any()) } |
| verify { mockDefaultConfig wasNot called } |
| } |
| |
| @Test |
| fun `forceNdkDownload skips sdkmanager install when the requested ndk is already installed`() { |
| val project = mockk<Project>() |
| val finalizeDslSlot = captureFinalizeDslAction(project) |
| val mockCmakeOptions = mockk<CmakeOptions>() |
| val mockDefaultConfig = mockk<DefaultConfig>() |
| val mockBaseExtension = mockk<BaseExtension>() |
| every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension |
| every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions |
| every { mockBaseExtension.defaultConfig } returns mockDefaultConfig |
| every { mockBaseExtension.ndkVersion } returns "29.0.13846066" |
| every { mockCmakeOptions.path } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns "/sdkmanager" |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" |
| every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "29.0.13846066" |
| every { project.gradle.startParameter.taskNames } returns emptyList() |
| every { project.extensions.findByType(ApplicationExtension::class.java) } returns null |
| |
| FlutterPluginUtils.forceNdkDownload(project, "/base/path") |
| finalizeDslSlot.captured.invoke(Any()) |
| |
| verify(exactly = 0) { mockCmakeOptions.path(any()) } |
| verify { mockDefaultConfig wasNot called } |
| } |
| |
| @Test |
| fun `forceNdkDownload does not overwrite native build configured before dsl finalization`( |
| @TempDir tempDir: Path |
| ) { |
| val project = mockk<Project>() |
| val finalizeDslSlot = captureFinalizeDslAction(project) |
| val mockCmakeOptions = mockk<CmakeOptions>() |
| val mockDefaultConfig = mockk<DefaultConfig>() |
| val mockDirectoryProperty = mockk<DirectoryProperty>() |
| val mockDirectory = mockk<Directory>() |
| val mockBaseExtension = mockk<BaseExtension>() |
| var cmakePath: File? = null |
| every { project.extensions.findByType(ApplicationExtension::class.java) } returns null |
| every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension |
| every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions |
| every { mockBaseExtension.defaultConfig } returns mockDefaultConfig |
| every { mockBaseExtension.ndkVersion } returns "29.0.13846066" |
| every { mockCmakeOptions.path } answers { cmakePath } |
| every { mockCmakeOptions.path(any()) } returns Unit |
| every { mockCmakeOptions.buildStagingDirectory(any()) } returns Unit |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" |
| every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "" |
| every { project.gradle.startParameter.taskNames } returns emptyList() |
| every { project.layout.buildDirectory } returns mockDirectoryProperty |
| every { mockDirectoryProperty.dir(any<String>()) } returns mockDirectoryProperty |
| every { mockDirectoryProperty.get() } returns mockDirectory |
| every { mockDirectory.asFile.path } returns "/randomapp/build/app/" |
| |
| val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>() |
| every { mockBaseExtension.buildTypes.iterator() } returns mutableListOf(mockBuildType).iterator() |
| every { mockBuildType.name } returns "Debug" |
| every { mockBuildType.externalNativeBuild.cmake.arguments(any(), any(), any()) } returns Unit |
| |
| FlutterPluginUtils.forceNdkDownload(project, "/base/path") |
| cmakePath = tempDir.resolve("CMakeLists.txt").toFile() |
| finalizeDslSlot.captured.invoke(Any()) |
| |
| verify(exactly = 0) { |
| mockCmakeOptions.path( |
| "/base/path/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt" |
| ) |
| } |
| verify(exactly = 0) { mockCmakeOptions.buildStagingDirectory(any()) } |
| verify { mockDefaultConfig wasNot called } |
| } |
| |
| @Test |
| fun `forceNdkDownload waits for finalized ndkVersion before checking installed versions`( |
| @TempDir tempDir: Path |
| ) { |
| val project = mockk<ProjectInternal>() |
| val finalizeDslSlot = captureFinalizeDslAction(project) |
| val execActionSlot = slot<Action<ExecSpec>>() |
| val mockExecSpec = mockk<ExecSpec>() |
| val mockExecResult = mockk<ExecResult>() |
| val mockExecOperations = mockk<ExecOperations>() |
| val mockCmakeOptions = mockk<CmakeOptions>() |
| val mockDefaultConfig = mockk<DefaultConfig>() |
| val mockBaseExtension = mockk<BaseExtension>() |
| var configuredNdkVersion = "26.3.11579264" |
| every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension |
| every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions |
| every { mockBaseExtension.defaultConfig } returns mockDefaultConfig |
| every { mockBaseExtension.ndkVersion } answers { configuredNdkVersion } |
| every { mockCmakeOptions.path } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns "/sdkmanager" |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns tempDir.toString() |
| every { |
| project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) |
| } returns "26.3.11579264" |
| every { project.gradle.startParameter.taskNames } returns emptyList() |
| every { project.gradle.startParameter.isOffline } returns false |
| every { project.extensions.findByType(ApplicationExtension::class.java) } returns null |
| every { project.serviceOf<ExecOperations>() } returns mockExecOperations |
| every { mockExecOperations.exec(capture(execActionSlot)) } answers { |
| File(tempDir.toFile(), "ndk/27.3.13750724/source.properties").apply { |
| parentFile.mkdirs() |
| createNewFile() |
| } |
| mockExecResult |
| } |
| every { mockExecResult.assertNormalExitValue() } returns mockExecResult |
| every { mockExecSpec.commandLine(any<List<String>>()) } returns mockExecSpec |
| |
| FlutterPluginUtils.forceNdkDownload(project, "/base/path") |
| configuredNdkVersion = "27.3.13750724" |
| finalizeDslSlot.captured.invoke(Any()) |
| execActionSlot.captured.execute(mockExecSpec) |
| |
| verify(exactly = 1) { mockExecOperations.exec(any<Action<ExecSpec>>()) } |
| verify { |
| mockExecSpec.commandLine( |
| listOf( |
| "/sdkmanager", |
| "--sdk_root=$tempDir", |
| "--install", |
| "ndk;27.3.13750724" |
| ) |
| ) |
| } |
| verify(exactly = 0) { mockCmakeOptions.path(any()) } |
| verify { mockDefaultConfig wasNot called } |
| } |
| |
| @Test |
| fun `forceNdkDownload waits for finalized ApplicationExtension ndkVersion before checking installed versions`( |
| @TempDir tempDir: Path |
| ) { |
| val project = mockk<ProjectInternal>() |
| val finalizeDslSlot = captureFinalizeDslAction(project) |
| val execActionSlot = slot<Action<ExecSpec>>() |
| val mockExecSpec = mockk<ExecSpec>() |
| val mockExecResult = mockk<ExecResult>() |
| val mockExecOperations = mockk<ExecOperations>() |
| val mockCmakeOptions = mockk<CmakeOptions>() |
| val mockDefaultConfig = mockk<DefaultConfig>() |
| val mockBaseExtension = mockk<BaseExtension>() |
| val mockApplicationExtension = mockk<ApplicationExtension>() |
| var configuredNdkVersion = "26.3.11579264" |
| every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension |
| every { |
| project.extensions.findByType(ApplicationExtension::class.java) |
| } returns mockApplicationExtension |
| every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions |
| every { mockBaseExtension.defaultConfig } returns mockDefaultConfig |
| every { mockBaseExtension.ndkVersion } answers { |
| throw AssertionError( |
| "legacy ndkVersion should not be read when ApplicationExtension is available" |
| ) |
| } |
| every { mockApplicationExtension.ndkVersion } answers { configuredNdkVersion } |
| every { mockCmakeOptions.path } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns "/sdkmanager" |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns tempDir.toString() |
| every { |
| project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) |
| } returns "26.3.11579264" |
| every { project.gradle.startParameter.taskNames } returns emptyList() |
| every { project.gradle.startParameter.isOffline } returns false |
| every { project.serviceOf<ExecOperations>() } returns mockExecOperations |
| every { mockExecOperations.exec(capture(execActionSlot)) } answers { |
| File(tempDir.toFile(), "ndk/27.3.13750724/source.properties").apply { |
| parentFile.mkdirs() |
| createNewFile() |
| } |
| mockExecResult |
| } |
| every { mockExecResult.assertNormalExitValue() } returns mockExecResult |
| every { mockExecSpec.commandLine(any<List<String>>()) } returns mockExecSpec |
| |
| FlutterPluginUtils.forceNdkDownload(project, "/base/path") |
| configuredNdkVersion = "27.3.13750724" |
| finalizeDslSlot.captured.invoke(Any()) |
| execActionSlot.captured.execute(mockExecSpec) |
| |
| verify(exactly = 1) { mockExecOperations.exec(any<Action<ExecSpec>>()) } |
| verify { |
| mockExecSpec.commandLine( |
| listOf( |
| "/sdkmanager", |
| "--sdk_root=$tempDir", |
| "--install", |
| "ndk;27.3.13750724" |
| ) |
| ) |
| } |
| verify(exactly = 0) { mockCmakeOptions.path(any()) } |
| verify { mockDefaultConfig wasNot called } |
| } |
| |
| @Test |
| fun `forceNdkDownload skips fallback when sdkmanager is unavailable but the requested ndk is already installed`() { |
| val project = mockk<Project>() |
| val finalizeDslSlot = captureFinalizeDslAction(project) |
| val mockCmakeOptions = mockk<CmakeOptions>() |
| val mockDefaultConfig = mockk<DefaultConfig>() |
| val mockBaseExtension = mockk<BaseExtension>() |
| every { project.extensions.findByType(ApplicationExtension::class.java) } returns null |
| every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension |
| every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions |
| every { mockBaseExtension.defaultConfig } returns mockDefaultConfig |
| every { mockBaseExtension.ndkVersion } returns "29.0.13846066" |
| every { mockCmakeOptions.path } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" |
| every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "29.0.13846066" |
| every { project.gradle.startParameter.taskNames } returns emptyList() |
| |
| FlutterPluginUtils.forceNdkDownload(project, "/base/path") |
| finalizeDslSlot.captured.invoke(Any()) |
| |
| verify(exactly = 0) { mockCmakeOptions.path(any()) } |
| verify { mockDefaultConfig wasNot called } |
| } |
| |
| @Test |
| fun `forceNdkDownload reads ndkVersion from ApplicationExtension when legacy extension does not expose it`() { |
| val project = mockk<Project>() |
| val finalizeDslSlot = captureFinalizeDslAction(project) |
| val mockCmakeOptions = mockk<CmakeOptions>() |
| val mockDefaultConfig = mockk<DefaultConfig>() |
| val mockBaseExtension = mockk<BaseExtension>() |
| val mockApplicationExtension = mockk<ApplicationExtension>() |
| every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension |
| every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension |
| every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions |
| every { mockBaseExtension.defaultConfig } returns mockDefaultConfig |
| every { mockBaseExtension.ndkVersion } answers { |
| throw AssertionError("legacy ndkVersion should not be read when ApplicationExtension is available") |
| } |
| every { mockApplicationExtension.ndkVersion } returns "29.0.13846066" |
| every { mockCmakeOptions.path } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns "/sdkmanager" |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" |
| every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "29.0.13846066" |
| every { project.gradle.startParameter.taskNames } returns emptyList() |
| |
| FlutterPluginUtils.forceNdkDownload(project, "/base/path") |
| finalizeDslSlot.captured.invoke(Any()) |
| |
| verify(exactly = 0) { mockCmakeOptions.path(any()) } |
| verify { mockDefaultConfig wasNot called } |
| } |
| |
| @Test |
| fun `forceNdkDownload throws when sdkmanager install does not produce the requested ndk`( |
| @TempDir tempDir: Path |
| ) { |
| val project = mockk<ProjectInternal>() |
| val finalizeDslSlot = captureFinalizeDslAction(project) |
| val mockExecResult = mockk<ExecResult>() |
| val mockExecOperations = mockk<ExecOperations>() |
| val mockCmakeOptions = mockk<CmakeOptions>() |
| val mockDefaultConfig = mockk<DefaultConfig>() |
| val mockBaseExtension = mockk<BaseExtension>() |
| every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension |
| every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions |
| every { mockBaseExtension.defaultConfig } returns mockDefaultConfig |
| every { mockBaseExtension.ndkVersion } returns "29.0.13846066" |
| every { mockCmakeOptions.path } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns "/sdkmanager" |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns tempDir.toString() |
| every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "" |
| every { project.gradle.startParameter.taskNames } returns emptyList() |
| every { project.gradle.startParameter.isOffline } returns false |
| every { project.extensions.findByType(ApplicationExtension::class.java) } returns null |
| every { project.serviceOf<ExecOperations>() } returns mockExecOperations |
| every { mockExecOperations.exec(any<Action<ExecSpec>>()) } returns mockExecResult |
| every { mockExecResult.assertNormalExitValue() } returns mockExecResult |
| |
| FlutterPluginUtils.forceNdkDownload(project, "/base/path") |
| |
| assertThrows<GradleException> { |
| finalizeDslSlot.captured.invoke(Any()) |
| } |
| |
| verify(exactly = 0) { mockCmakeOptions.path(any()) } |
| verify { mockDefaultConfig wasNot called } |
| } |
| |
| @Test |
| fun `forceNdkDownload skips when invoking the ndk metadata task`() { |
| val project = mockk<Project>() |
| val mockCmakeOptions = mockk<CmakeOptions>() |
| val mockDefaultConfig = mockk<DefaultConfig>() |
| val mockBaseExtension = mockk<BaseExtension>() |
| every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension |
| every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions |
| every { mockBaseExtension.defaultConfig } returns mockDefaultConfig |
| every { mockCmakeOptions.path } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns null |
| every { project.gradle.startParameter.taskNames } returns listOf(FlutterPluginUtils.TASK_PRINT_NDK_VERSION) |
| every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockk(relaxed = true) |
| |
| FlutterPluginUtils.forceNdkDownload(project, "/base/path") |
| |
| verify(exactly = 0) { mockCmakeOptions.path(any()) } |
| verify { mockDefaultConfig wasNot called } |
| } |
| |
| @Test |
| fun `forceNdkDownload falls back when tool properties are present but sdkmanager is unavailable`() { |
| val project = mockk<Project>() |
| val finalizeDslSlot = captureFinalizeDslAction(project) |
| val mockCmakeOptions = mockk<CmakeOptions>() |
| val mockDefaultConfig = mockk<DefaultConfig>() |
| val mockDirectoryProperty = mockk<DirectoryProperty>() |
| val mockDirectory = mockk<Directory>() |
| val mockBaseExtension = mockk<BaseExtension>() |
| every { project.extensions.findByType(ApplicationExtension::class.java) } returns null |
| every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension |
| every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions |
| every { mockBaseExtension.defaultConfig } returns mockDefaultConfig |
| every { mockBaseExtension.ndkVersion } returns "29.0.13846066" |
| every { mockCmakeOptions.path } returns null |
| every { mockCmakeOptions.path(any()) } returns Unit |
| every { mockCmakeOptions.buildStagingDirectory(any()) } returns Unit |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" |
| every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "" |
| every { project.gradle.startParameter.taskNames } returns emptyList() |
| every { project.layout.buildDirectory } returns mockDirectoryProperty |
| every { mockDirectoryProperty.dir(any<String>()) } returns mockDirectoryProperty |
| every { mockDirectoryProperty.get() } returns mockDirectory |
| every { mockDirectory.asFile.path } returns "/randomapp/build/app/" |
| val basePath = "/base/path" |
| |
| val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>() |
| every { mockBaseExtension.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) |
| finalizeDslSlot.captured.invoke(Any()) |
| |
| 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" |
| ) |
| } |
| } |
| |
| @Test |
| fun `forceNdkDownload falls back when Gradle is offline`() { |
| val project = mockk<Project>() |
| val finalizeDslSlot = captureFinalizeDslAction(project) |
| val mockCmakeOptions = mockk<CmakeOptions>() |
| val mockDefaultConfig = mockk<DefaultConfig>() |
| val mockDirectoryProperty = mockk<DirectoryProperty>() |
| val mockDirectory = mockk<Directory>() |
| val mockBaseExtension = mockk<BaseExtension>() |
| every { project.extensions.findByType(ApplicationExtension::class.java) } returns null |
| every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension |
| every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions |
| every { mockBaseExtension.defaultConfig } returns mockDefaultConfig |
| every { mockBaseExtension.ndkVersion } returns "29.0.13846066" |
| every { mockCmakeOptions.path } returns null |
| every { mockCmakeOptions.path(any()) } returns Unit |
| every { mockCmakeOptions.buildStagingDirectory(any()) } returns Unit |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns "/sdkmanager" |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" |
| every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "" |
| every { project.gradle.startParameter.taskNames } returns emptyList() |
| every { project.gradle.startParameter.isOffline } returns true |
| every { project.layout.buildDirectory } returns mockDirectoryProperty |
| every { mockDirectoryProperty.dir(any<String>()) } returns mockDirectoryProperty |
| every { mockDirectoryProperty.get() } returns mockDirectory |
| every { mockDirectory.asFile.path } returns "/randomapp/build/app/" |
| val basePath = "/base/path" |
| |
| val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>() |
| every { mockBaseExtension.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) |
| finalizeDslSlot.captured.invoke(Any()) |
| |
| 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" |
| ) |
| } |
| } |
| |
| @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(ApplicationExtension::class.java) } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns null |
| every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns null |
| 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" |
| ) |
| } |
| } |
| |
| @Test |
| fun `addTaskForPrintNdkVersion adds task for printing ndk version`() { |
| val project = mockk<Project>(relaxed = true) |
| val androidExtension = mockk<ApplicationExtension>() |
| every { androidExtension.ndkVersion } returns "29.0.13846066" |
| every { project.extensions.findByType(ApplicationExtension::class.java) } returns androidExtension |
| every { project.tasks.register(any(), eq(PrintTask::class.java), any()) } returns mockk() |
| val captureSlot = slot<Action<PrintTask>>() |
| |
| FlutterPluginUtils.addTaskForPrintNdkVersion(project) |
| |
| verify { project.tasks.register("printNdkVersion", eq(PrintTask::class.java), capture(captureSlot)) } |
| val mockPrintTask = mockk<PrintTask>(relaxed = true) |
| captureSlot.captured.execute(mockPrintTask) |
| |
| verify { |
| mockPrintTask.description = "Prints out the configured ndkVersion for this Android project" |
| } |
| } |
| |
| // 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" |
| } |
| } |
| } |