package com.flutter.gradle import com.android.build.api.dsl.ApplicationBuildType import com.android.build.api.dsl.ApplicationDefaultConfig import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.CommonExtension import com.android.build.api.dsl.LibraryExtension import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.gradle.AbstractAppExtension import com.android.build.gradle.BaseExtension import com.android.build.gradle.api.AndroidSourceDirectorySet import com.android.build.gradle.internal.core.InternalBaseVariant import com.android.build.gradle.tasks.MergeSourceSetFolders import com.android.build.gradle.tasks.ProcessAndroidResources import com.flutter.gradle.tasks.FlutterTask import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject import io.mockk.slot import io.mockk.verify import org.gradle.api.Action import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.file.Directory import org.gradle.api.tasks.Copy import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.gradle.plugin.extraProperties import org.junit.jupiter.api.Assertions.fail import org.junit.jupiter.api.io.TempDir import java.nio.file.Path import kotlin.io.path.writeText import kotlin.test.Test class FlutterPluginTest { @Test fun `FlutterPlugin apply() adds expected tasks`( @TempDir tempDir: Path ) { val projectDir = tempDir.resolve("project-dir").resolve("android").resolve("app") projectDir.toFile().mkdirs() val settingsFile = projectDir.parent.resolve("settings.gradle") settingsFile.writeText("empty for now") val fakeFlutterSdkDir = tempDir.resolve("fake-flutter-sdk") fakeFlutterSdkDir.toFile().mkdirs() val fakeCacheDir = fakeFlutterSdkDir.resolve("bin").resolve("cache") fakeCacheDir.toFile().mkdirs() val fakeEngineStampFile = fakeCacheDir.resolve("engine.stamp") fakeEngineStampFile.writeText(FAKE_ENGINE_STAMP) val fakeEngineRealmFile = fakeCacheDir.resolve("engine.realm") fakeEngineRealmFile.writeText(FAKE_ENGINE_REALM) val project = mockk(relaxed = true) val mockAbstractAppExtension = mockk( moreInterfaces = arrayOf(ApplicationExtension::class), relaxed = true ) val mockLibraryExtension = mockk(relaxed = true) every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension val mockAndroidComponentsExtension = mockk>(relaxed = true) every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension every { project.extensions.findByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension val mockSelector = mockk(relaxed = true) every { mockAndroidComponentsExtension.selector() } returns mockSelector every { mockSelector.all() } returns mockSelector every { mockSelector.withName(any()) } returns mockSelector every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension every { project.extensions.getByType(LibraryExtension::class.java) } returns mockLibraryExtension every { project.extensions.findByName("android") } returns mockAbstractAppExtension every { project.projectDir } returns projectDir.toFile() every { project.findProperty("flutter.sdk") } returns fakeFlutterSdkDir.toString() every { project.file(fakeFlutterSdkDir.toString()) } returns fakeFlutterSdkDir.toFile() val flutterExtension = FlutterExtension() every { project.extensions.create("flutter", any>()) } returns flutterExtension every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension val mockBaseExtension = mockk(relaxed = true) val mockCommonExtension = mockk>(relaxed = true) val mockDebugBuildType = mockk(relaxed = true) val mockReleaseBuildType = mockk(relaxed = true) // Cast our multi-interface mock instead of creating a brand new one val mockApplicationExtension = mockAbstractAppExtension as ApplicationExtension // Mock buildTypes on our new dual-purpose mock so AgpCommonExtensionWrapper can read them every { mockApplicationExtension.buildTypes.getByName("debug") } returns mockDebugBuildType every { mockApplicationExtension.buildTypes.getByName("release") } returns mockReleaseBuildType // Keep the CommonExtension mocks just in case other parts of the plugin look for it every { mockCommonExtension.buildTypes.getByName("debug") } returns mockDebugBuildType every { mockCommonExtension.buildTypes.getByName("release") } returns mockReleaseBuildType every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension every { project.extensions.findByType(CommonExtension::class.java) } returns mockCommonExtension // Pass the dual-purpose mock for any ApplicationExtension lookups every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension every { project.extensions.getByType(ApplicationExtension::class.java) } returns mockApplicationExtension val mockApplicationDefaultConfig = mockk( moreInterfaces = arrayOf(ApplicationDefaultConfig::class), relaxed = true ) every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig every { project.rootProject } returns project every { project.state.failure as Throwable? } returns null val mockDirectory = mockk(relaxed = true) every { project.layout.buildDirectory.get() } returns mockDirectory val mockAndroidSourceSet = mockk(relaxed = true) val mockAndroidSourceDirectorySet = mockk(relaxed = true) every { mockAndroidSourceSet.jniLibs.srcDir(any()) } returns mockAndroidSourceDirectorySet every { mockAbstractAppExtension.sourceSets.getByName("main") } returns mockAndroidSourceSet // mock return of NativePluginLoaderReflectionBridge.getPlugins mockkObject(NativePluginLoaderReflectionBridge) every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns listOf() // mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge every { project.extraProperties } returns mockk() every { project.file(flutterExtension.source!!) } returns mockk() val flutterPlugin = FlutterPlugin() flutterPlugin.apply(project) verify { project.tasks.register("generateLockfiles", any()) } verify { project.tasks.register("javaVersion", any()) } verify { project.tasks.register("printBuildVariants", any()) } } @Test fun `copyFlutterAssets task sets filePermissions correctly`( @TempDir tempDir: Path ) { val projectDir = tempDir.resolve("project-dir").resolve("android").resolve("app") projectDir.toFile().mkdirs() val settingsFile = projectDir.parent.resolve("settings.gradle") settingsFile.writeText("empty for now") val fakeFlutterSdkDir = tempDir.resolve("fake-flutter-sdk") fakeFlutterSdkDir.toFile().mkdirs() val fakeCacheDir = fakeFlutterSdkDir.resolve("bin").resolve("cache") fakeCacheDir.toFile().mkdirs() val fakeEngineStampFile = fakeCacheDir.resolve("engine.stamp") fakeEngineStampFile.writeText(FAKE_ENGINE_STAMP) val fakeEngineRealmFile = fakeCacheDir.resolve("engine.realm") fakeEngineRealmFile.writeText(FAKE_ENGINE_REALM) val project = mockk(relaxed = true) val mockAbstractAppExtension = mockk( moreInterfaces = arrayOf(ApplicationExtension::class), relaxed = true ) every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension every { project.extensions.findByName("android") } returns mockAbstractAppExtension val mockAndroidComponentsExtension = mockk>(relaxed = true) every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension every { project.extensions.findByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension val mockSelector = mockk(relaxed = true) every { mockAndroidComponentsExtension.selector() } returns mockSelector every { mockSelector.all() } returns mockSelector every { mockSelector.withName(any()) } returns mockSelector every { project.projectDir } returns projectDir.toFile() every { project.findProperty("flutter.sdk") } returns fakeFlutterSdkDir.toString() every { project.file(fakeFlutterSdkDir.toString()) } returns fakeFlutterSdkDir.toFile() val flutterExtension = FlutterExtension() every { project.extensions.create("flutter", any>()) } returns flutterExtension every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension val mockBaseExtension = mockk(relaxed = true) val mockCommonExtension = mockk>(relaxed = true) val mockDebugBuildType = mockk(relaxed = true) val mockReleaseBuildType = mockk(relaxed = true) // Cast our multi-interface mock instead of creating a brand new one val mockApplicationExtension = mockAbstractAppExtension as ApplicationExtension // Mock buildTypes on our new dual-purpose mock so AgpCommonExtensionWrapper can read them every { mockApplicationExtension.buildTypes.getByName("debug") } returns mockDebugBuildType every { mockApplicationExtension.buildTypes.getByName("release") } returns mockReleaseBuildType // Keep the CommonExtension mocks just in case other parts of the plugin look for it every { mockCommonExtension.buildTypes.getByName("debug") } returns mockDebugBuildType every { mockCommonExtension.buildTypes.getByName("release") } returns mockReleaseBuildType every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension every { project.extensions.findByType(CommonExtension::class.java) } returns mockCommonExtension // Pass the dual-purpose mock for any ApplicationExtension lookups every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension every { project.extensions.getByType(ApplicationExtension::class.java) } returns mockApplicationExtension val mockApplicationDefaultConfig = mockk( moreInterfaces = arrayOf(ApplicationDefaultConfig::class), relaxed = true ) every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig every { project.rootProject } returns project every { project.state.failure as Throwable? } returns null val mockDirectory = mockk(relaxed = true) every { project.layout.buildDirectory.get() } returns mockDirectory val mockAndroidSourceSet = mockk(relaxed = true) val mockAndroidSourceDirectorySet = mockk(relaxed = true) every { mockAndroidSourceSet.jniLibs.srcDir(any()) } returns mockAndroidSourceDirectorySet every { mockAbstractAppExtension.sourceSets.getByName("main") } returns mockAndroidSourceSet // mock return of NativePluginLoaderReflectionBridge.getPlugins mockkObject(NativePluginLoaderReflectionBridge) every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns listOf() // mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge every { project.extraProperties } returns mockk() every { project.file(flutterExtension.source!!) } returns mockk() // Set up the task container and our task capture val taskContainer = mockk(relaxed = true) every { project.tasks } returns taskContainer val copyTaskActionCaptor = slot>() val copyTask = mockk(relaxed = true) val mockVariant = mockk(relaxed = true) every { mockVariant.name } returns "debug" every { mockVariant.buildType.name } returns "debug" every { mockVariant.flavorName } returns "" val mergedFlavor = mockk(relaxed = true) every { mockVariant.mergedFlavor } returns mergedFlavor val apiLevel = mockk(relaxed = true) every { apiLevel.apiLevel } returns 21 every { mergedFlavor.minSdkVersion } returns apiLevel val variantOutput = mockk(relaxed = true) val outputsIterator = mockk>() every { outputsIterator.hasNext() } returns true andThen false every { outputsIterator.next() } returns variantOutput val variantOutputCollection = mockk>() every { variantOutputCollection.iterator() } returns outputsIterator every { mockVariant.outputs } returns variantOutputCollection val processResourcesProvider = mockk>(relaxed = true) every { processResourcesProvider.hint(ProcessAndroidResources::class).get() } returns mockk(relaxed = true) every { variantOutput.processResourcesProvider } returns processResourcesProvider val assembleTask = mockk(relaxed = true) val assembleTaskProvider = mockk>(relaxed = true) every { assembleTaskProvider.get() } returns assembleTask every { mockVariant.assembleProvider } returns assembleTaskProvider val variants = listOf(mockVariant) val variantsIterator = mockk>() every { variantsIterator.hasNext() } returns true andThen false every { variantsIterator.next() } returns mockVariant val variantCollection = mockk>() every { mockAbstractAppExtension.applicationVariants } returns variantCollection every { variantCollection.iterator() } returns variantsIterator every { variantCollection.configureEach(any>()) } answers { variants.forEach { firstArg>().execute(it) } } every { mockVariant.mergeAssetsProvider.hint(MergeSourceSetFolders::class).get() } returns mockk(relaxed = true) val flutterTask = mockk(relaxed = true) val copySpec = mockk(relaxed = true) every { (flutterTask).assets } returns copySpec val flutterTaskProvider = mockk>(relaxed = true) every { flutterTaskProvider.hint(FlutterTask::class).get() } returns flutterTask every { taskContainer.register( match { it.contains("compileFlutterBuild") }, any>(), any() ) } answers { flutterTaskProvider } // Actual task that should be captured to test if permissions have been set val mockCopyTaskProvider = mockk>(relaxed = true) every { mockCopyTaskProvider.hint(Copy::class).get() } returns copyTask every { taskContainer.register( match { it.startsWith("copyFlutterAssets") }, eq(Copy::class.java), capture(copyTaskActionCaptor) ) } answers { mockCopyTaskProvider } val mockJarTaskProvider = mockk>(relaxed = true) every { mockJarTaskProvider.hint(org.gradle.api.tasks.bundling.Jar::class).get() } returns mockk(relaxed = true) every { taskContainer.register( match { it.contains("packJniLibs") }, eq(org.gradle.api.tasks.bundling.Jar::class.java), any() ) } answers { mockJarTaskProvider } val mockTaskProvider = mockk>(relaxed = true) every { mockTaskProvider.hint(Task::class).get() } returns mockk(relaxed = true) every { taskContainer.named(any()) } returns mockTaskProvider val flutterPlugin = FlutterPlugin() flutterPlugin.apply(project) copyTaskActionCaptor.captured.execute(copyTask) val filePermissionsActionCaptor = slot>() verify { copyTask.filePermissions(capture(filePermissionsActionCaptor)) } if (filePermissionsActionCaptor.isCaptured) { val mockFilePermissionSet = mockk(relaxed = true) filePermissionsActionCaptor.captured.execute(mockFilePermissionSet) val userPermissionsActionCaptor = slot>() verify { mockFilePermissionSet.user(capture(userPermissionsActionCaptor)) } if (userPermissionsActionCaptor.isCaptured) { val mockUserPermission = mockk(relaxed = true) userPermissionsActionCaptor.captured.execute(mockUserPermission) verify { mockUserPermission.read = true mockUserPermission.write = true } } else { fail("User permissions configuration action was not captured") } } else { fail("FilePermissions configuration action was not captured") } } companion object { const val FAKE_ENGINE_STAMP = "901b0f1afe77c3555abee7b86a26aaa37f131379" const val FAKE_ENGINE_REALM = "made_up_realm" } }