package com.flutter.gradle import com.android.build.api.dsl.ApplicationDefaultConfig import com.android.build.api.dsl.ApplicationExtension 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(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 { mockAndroidComponentsExtension.selector() } returns mockk { every { all() } returns mockk() } every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension 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) every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension val mockApplicationExtension = mockk(relaxed = true) every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension val mockApplicationDefaultConfig = mockk(relaxed = true) every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig every { project.rootProject } returns project every { project.state.failure } 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(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 { mockAndroidComponentsExtension.selector() } returns mockk { every { all() } returns mockk() } 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) every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension val mockApplicationExtension = mockk(relaxed = true) every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension val mockApplicationDefaultConfig = mockk(relaxed = true) every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig every { project.rootProject } returns project every { project.state.failure } 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" } }