blob: 3f77a3b06fe5986c19fb5630db9a10e5f61bc72f [file]
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.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 com.flutter.gradle.tasks.PrintTask
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.slot
import io.mockk.unmockkAll
import io.mockk.verify
import org.gradle.api.Action
import org.gradle.api.NamedDomainObjectContainer
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.AfterEach
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.io.TempDir
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import java.util.Base64
import kotlin.io.path.writeText
import kotlin.test.Test
import kotlin.test.assertContains
class FlutterPluginTest {
// Clear global singleton mocks to prevent mock state leaking into other tests in the same JVM.
@AfterEach
fun tearDown() {
unmockkAll()
}
@Test
fun `FlutterPlugin apply() adds expected tasks`(
@TempDir tempDir: Path
) {
val env = setupTestProjectEnvironment(tempDir)
setupMockApplicationExtension(env.project)
setupMockComponentsExtension(env.project)
setupMockNativePluginLoader(env.project, env.flutterExtension)
val flutterPlugin = FlutterPlugin()
flutterPlugin.apply(env.project)
verify { env.project.tasks.register("generateLockfiles", any()) }
val registeredPrintTasks = mutableListOf<String>()
verify {
env.project.tasks.register(capture(registeredPrintTasks), PrintTask::class.java, any())
}
assertContains(registeredPrintTasks, "javaVersion")
assertContains(registeredPrintTasks, "kgpVersion")
assertContains(registeredPrintTasks, "printBuildVariants")
assertContains(registeredPrintTasks, "printNdkVersion")
}
@Test
fun `FlutterPlugin apply wires flutter embedding dependencies on all build types`(
@TempDir tempDir: Path
) {
val env = setupTestProjectEnvironment(tempDir)
setupMockApplicationExtension(env.project)
setupMockComponentsExtension(env.project)
setupMockNativePluginLoader(env.project, env.flutterExtension)
val flutterPlugin = FlutterPlugin()
flutterPlugin.apply(env.project)
verify {
env.project.dependencies.add(
"debugApi",
"io.flutter:flutter_embedding_debug:1.0.0-$FAKE_ENGINE_STAMP"
)
}
verify {
env.project.dependencies.add(
"releaseApi",
"io.flutter:flutter_embedding_release:1.0.0-$FAKE_ENGINE_STAMP"
)
}
}
@Test
fun `copyFlutterAssets task sets filePermissions correctly`(
@TempDir tempDir: Path
) {
val env = setupTestProjectEnvironment(tempDir)
val project = env.project
setupMockApplicationExtension(project)
val mockAbstractAppExtension = project.extensions.getByType(AbstractAppExtension::class.java)
setupMockComponentsExtension(project)
setupMockNativePluginLoader(project, env.flutterExtension)
// Set up the task container and our task capture
val taskContainer = mockk<TaskContainer>(relaxed = true)
every { project.tasks } returns taskContainer
val copyTaskActionCaptor = slot<Action<Copy>>()
val copyTask = mockk<Copy>(relaxed = true)
val mockVariant = mockk<com.android.build.gradle.api.ApplicationVariant>(relaxed = true)
every { mockVariant.name } returns "debug"
every { mockVariant.buildType.name } returns "debug"
every { mockVariant.flavorName } returns ""
val mergedFlavor = mockk<InternalBaseVariant.MergedFlavor>(relaxed = true)
every { mockVariant.mergedFlavor } returns mergedFlavor
val apiLevel = mockk<com.android.builder.model.ApiVersion>(relaxed = true)
every { apiLevel.apiLevel } returns 21
every { mergedFlavor.minSdkVersion } returns apiLevel
val variantOutput = mockk<com.android.build.gradle.api.BaseVariantOutput>(relaxed = true)
val outputsIterator = mockk<MutableIterator<com.android.build.gradle.api.BaseVariantOutput>>()
every { outputsIterator.hasNext() } returns true andThen false
every { outputsIterator.next() } returns variantOutput
val variantOutputCollection = mockk<org.gradle.api.DomainObjectCollection<com.android.build.gradle.api.BaseVariantOutput>>()
every { variantOutputCollection.iterator() } returns outputsIterator
every { mockVariant.outputs } returns variantOutputCollection
val processResourcesProvider = mockk<TaskProvider<ProcessAndroidResources>>(relaxed = true)
every { processResourcesProvider.hint(ProcessAndroidResources::class).get() } returns mockk<ProcessAndroidResources>(relaxed = true)
every { variantOutput.processResourcesProvider } returns processResourcesProvider
val assembleTask = mockk<Task>(relaxed = true)
val assembleTaskProvider = mockk<TaskProvider<Task>>(relaxed = true)
every { assembleTaskProvider.get() } returns assembleTask
every { mockVariant.assembleProvider } returns assembleTaskProvider
val variants = listOf(mockVariant)
val variantsIterator = mockk<MutableIterator<com.android.build.gradle.api.ApplicationVariant>>()
every { variantsIterator.hasNext() } returns true andThen false
every { variantsIterator.next() } returns mockVariant
val variantCollection = mockk<org.gradle.api.DomainObjectSet<com.android.build.gradle.api.ApplicationVariant>>()
every { mockAbstractAppExtension.applicationVariants } returns variantCollection
every { variantCollection.iterator() } returns variantsIterator
every {
variantCollection.configureEach(any<Action<com.android.build.gradle.api.ApplicationVariant>>())
} answers {
variants.forEach { firstArg<Action<com.android.build.gradle.api.ApplicationVariant>>().execute(it) }
}
every { mockVariant.mergeAssetsProvider.hint(MergeSourceSetFolders::class).get() } returns
mockk<MergeSourceSetFolders>(relaxed = true)
val flutterTask = mockk<FlutterTask>(relaxed = true)
val copySpec = mockk<org.gradle.api.file.CopySpec>(relaxed = true)
every {
(flutterTask).assets
} returns copySpec
val flutterTaskProvider = mockk<TaskProvider<FlutterTask>>(relaxed = true)
every {
flutterTaskProvider.hint(FlutterTask::class).get()
} returns flutterTask
every {
taskContainer.register(
match { it.contains("compileFlutterBuild") },
any<Class<FlutterTask>>(),
any()
)
} answers {
flutterTaskProvider
}
// Actual task that should be captured to test if permissions have been set
val mockCopyTaskProvider = mockk<TaskProvider<Copy>>(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<TaskProvider<org.gradle.api.tasks.bundling.Jar>>(relaxed = true)
every { mockJarTaskProvider.hint(org.gradle.api.tasks.bundling.Jar::class).get() } returns
mockk<org.gradle.api.tasks.bundling.Jar>(relaxed = true)
every {
taskContainer.register(
match { it.contains("packJniLibs") },
eq(org.gradle.api.tasks.bundling.Jar::class.java),
any()
)
} answers {
mockJarTaskProvider
}
val mockTaskProvider = mockk<TaskProvider<Task>>(relaxed = true)
every { mockTaskProvider.hint(Task::class).get() } returns mockk<Task>(relaxed = true)
every {
taskContainer.named(any<String>())
} returns mockTaskProvider
val flutterPlugin = FlutterPlugin()
flutterPlugin.apply(project)
copyTaskActionCaptor.captured.execute(copyTask)
val filePermissionsActionCaptor = slot<Action<org.gradle.api.file.ConfigurableFilePermissions>>()
verify {
copyTask.filePermissions(capture(filePermissionsActionCaptor))
}
if (filePermissionsActionCaptor.isCaptured) {
val mockFilePermissionSet = mockk<org.gradle.api.file.ConfigurableFilePermissions>(relaxed = true)
filePermissionsActionCaptor.captured.execute(mockFilePermissionSet)
val userPermissionsActionCaptor = slot<Action<org.gradle.api.file.ConfigurableUserClassFilePermissions>>()
verify {
mockFilePermissionSet.user(capture(userPermissionsActionCaptor))
}
if (userPermissionsActionCaptor.isCaptured) {
val mockUserPermission = mockk<org.gradle.api.file.ConfigurableUserClassFilePermissions>(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")
}
}
@Test
fun `apply adds task for generating manifest with engine shell arguments`(
@TempDir tempDir: Path
) {
val env = setupTestProjectEnvironment(tempDir)
val project = env.project
val engineShellArgsJson = """["--enable-impeller=true"]"""
val base64EngineShellArgs =
Base64.getEncoder().encodeToString(engineShellArgsJson.toByteArray(StandardCharsets.UTF_8))
every { project.findProperty("flutter.engineShellArgs") } returns base64EngineShellArgs
setupMockApplicationExtension(project)
setupMockComponentsExtension(project)
setupMockNativePluginLoader(project, env.flutterExtension)
mockkObject(FlutterPluginUtils)
val flutterPlugin = FlutterPlugin()
flutterPlugin.apply(project)
verify {
FlutterPluginUtils.addTaskForGeneratingEngineShellArgumentManifest(project)
}
}
private data class TestProjectEnvironment(
val projectDir: File,
val fakeFlutterSdkDir: File,
val project: Project,
val flutterExtension: FlutterExtension
)
private fun setupTestProjectEnvironment(
tempDir: Path,
engineStamp: String = FAKE_ENGINE_STAMP,
engineRealm: String = FAKE_ENGINE_REALM
): TestProjectEnvironment {
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(engineStamp)
val fakeEngineRealmFile = fakeCacheDir.resolve("engine.realm")
fakeEngineRealmFile.writeText(engineRealm)
val project = mockk<Project>(relaxed = true)
every { project.projectDir } returns projectDir.toFile()
every { project.findProperty("flutter.sdk") } returns fakeFlutterSdkDir.toString()
every { project.file(fakeFlutterSdkDir.toString()) } returns fakeFlutterSdkDir.toFile()
every { project.plugins.hasPlugin("com.android.application") } returns true
every { project.rootProject } returns project
every { project.state.failure as Throwable? } returns null
every { project.configurations.named("api") } returns mockk()
val flutterExtension = FlutterExtension()
every { project.extensions.create("flutter", any<Class<*>>()) } returns flutterExtension
every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension
return TestProjectEnvironment(
projectDir.toFile(),
fakeFlutterSdkDir.toFile(),
project,
flutterExtension
)
}
private fun setupMockApplicationExtension(
project: Project,
mockDebugBuildType: ApplicationBuildType =
mockk<ApplicationBuildType>(relaxed = true) {
every { name } returns "debug"
every { isDebuggable } returns true
},
mockReleaseBuildType: ApplicationBuildType =
mockk<ApplicationBuildType>(relaxed = true) {
every { name } returns "release"
every { isDebuggable } returns false
}
): ApplicationExtension {
val mockAbstractAppExtension =
mockk<AbstractAppExtension>(
moreInterfaces = arrayOf(ApplicationExtension::class),
relaxed = true
)
val mockApplicationExtension = mockAbstractAppExtension as ApplicationExtension
val mockLibraryExtension = mockk<LibraryExtension>(relaxed = true)
every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension
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.extensions.findByType(BaseExtension::class.java) } returns mockk(relaxed = true)
every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension
every { project.extensions.getByType(ApplicationExtension::class.java) } returns mockApplicationExtension
val container = mockk<NamedDomainObjectContainer<ApplicationBuildType>>(relaxed = true)
every { container.getByName("debug") } returns mockDebugBuildType
every { container.getByName("release") } returns mockReleaseBuildType
every { container.all(any<Action<in ApplicationBuildType>>()) } answers {
val action = firstArg<Action<in ApplicationBuildType>>()
action.execute(mockDebugBuildType)
action.execute(mockReleaseBuildType)
}
every { mockApplicationExtension.buildTypes } returns container
val mockApplicationDefaultConfig =
mockk<com.android.build.gradle.internal.dsl.DefaultConfig>(
moreInterfaces = arrayOf(ApplicationDefaultConfig::class),
relaxed = true
)
every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig
val mockDirectory = mockk<Directory>(relaxed = true)
every { project.layout.buildDirectory.get() } returns mockDirectory
val mockAndroidSourceSet = mockk<com.android.build.gradle.api.AndroidSourceSet>(relaxed = true)
val mockAndroidSourceDirectorySet = mockk<AndroidSourceDirectorySet>(relaxed = true)
every { mockAndroidSourceSet.jniLibs.srcDir(any()) } returns mockAndroidSourceDirectorySet
every { mockAbstractAppExtension.sourceSets.getByName("main") } returns mockAndroidSourceSet
return mockApplicationExtension
}
private fun setupMockComponentsExtension(project: Project): AndroidComponentsExtension<*, *, *> {
val mockAndroidComponentsExtension = mockk<AndroidComponentsExtension<*, *, *>>(relaxed = true)
every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension
every { project.extensions.findByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension
val mockSelector = mockk<com.android.build.api.variant.VariantSelector>(relaxed = true)
every { mockAndroidComponentsExtension.selector() } returns mockSelector
every { mockSelector.all() } returns mockSelector
every { mockSelector.withName(any<String>()) } returns mockSelector
return mockAndroidComponentsExtension
}
private fun setupMockNativePluginLoader(
project: Project,
flutterExtension: FlutterExtension
) {
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns listOf()
every { project.extraProperties } returns mockk()
every { project.file(flutterExtension.source!!) } returns mockk()
}
companion object {
const val FAKE_ENGINE_STAMP = "901b0f1afe77c3555abee7b86a26aaa37f131379"
const val FAKE_ENGINE_REALM = "made_up_realm"
}
}