Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased
* Minimum required Gradle version is now 9.1
* Fixed support for Android Gradle Plugin version 9.0.1
* Added `com.osacky.fulladle.settings` settings plugin for Gradle configuration cache compatible multi-module testing

## 0.19.0
* Minimum required JVM version is now 17.
Expand Down
29 changes: 26 additions & 3 deletions docs/multi-module-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,30 @@ Multi module testing can be done by manually specifying [additionalTestApks](/fl

## Fulladle Plugin

1. Apply the Fulladle plugin at the root of the project.
There are two ways to set up Fulladle: with or without the settings plugin.

**The settings plugin (`com.osacky.fulladle.settings`) is the recommended approach** for projects using Gradle's [configuration cache](https://docs.gradle.org/current/userguide/configuration_cache.html). It avoids cross-project configuration by passing module metadata through Gradle's dependency management system. Without the settings plugin, Fulladle falls back to a legacy approach that uses `subprojects {}` — this still works but is not compatible with future Gradle best practices.

### Setup

1. *(Recommended)* Apply the settings plugin in `settings.gradle`:

=== "Groovy"
``` groovy
plugins {
id 'com.osacky.fulladle.settings' version '{{ fladle.current_release }}'
}
```
=== "Kotlin"
``` kotlin
plugins {
id("com.osacky.fulladle.settings") version "{{ fladle.current_release }}"
}
```

The settings plugin automatically applies the module plugin to every subproject, so no per-module setup is needed.

2. Apply the Fulladle plugin at the root of the project.

=== "Groovy"
``` groovy
Expand All @@ -19,7 +42,7 @@ Multi module testing can be done by manually specifying [additionalTestApks](/fl
}
```

2. Configure the Fladle extension.
3. Configure the Fladle extension.

===! "Groovy"
``` groovy
Expand All @@ -37,7 +60,7 @@ Multi module testing can be done by manually specifying [additionalTestApks](/fl
!!! Warning
If using buildFlavors or testing against a non default variant, you will need to specify the variant you want to test in the fulladleModuleConfig block.

3. Run the tests.
4. Run the tests.
First assemble all your debug apks and test apks.
``` bash
./gradlew assembleDebug assembleDebugAndroidTest
Expand Down
14 changes: 14 additions & 0 deletions fladle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ gradlePlugin {
implementationClass = "com.osacky.flank.gradle.FulladlePlugin"
tags.set(listOf("flank", "testing", "android", "fladle"))
}
create("fulladleSettings") {
id = "com.osacky.fulladle.settings"
displayName = "Fulladle Settings"
description = project.description
implementationClass = "com.osacky.flank.gradle.FulladleSettingsPlugin"
tags.set(listOf("flank", "testing", "android", "fladle"))
}
create("fulladleModule") {
id = "com.osacky.fulladle.module"
displayName = "Fulladle Module"
description = project.description
implementationClass = "com.osacky.flank.gradle.FulladleModulePlugin"
tags.set(listOf("flank", "testing", "android", "fladle"))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,19 @@ class FladlePluginDelegate {
checkMinimumGradleVersion()

// Create Configuration to store flank dependency
target.configurations.create(FLADLE_CONFIG)
val fladleConfig = target.configurations.create(FLADLE_CONFIG)

val extension = target.extensions.create<FlankGradleExtension>("fladle", target.objects)

target.tasks.register("flankAuth", FlankJavaExec::class.java) {
doFirst {
target.layout.fladleDir
.get()
.asFile
.mkdirs()
workingDir.mkdirs()
}
classpath = project.fladleConfig
classpath = fladleConfig
args = listOf("auth", "login")
}

configureTasks(target, extension)
configureTasks(target, extension, fladleConfig)
}

private fun checkMinimumGradleVersion() {
Expand All @@ -46,23 +43,25 @@ class FladlePluginDelegate {
private fun configureTasks(
project: Project,
base: FlankGradleExtension,
fladleConfig: Configuration,
) {
base.flankVersion.finalizeValueOnRead()
base.flankCoordinates.finalizeValueOnRead()
base.serviceAccountCredentials.finalizeValueOnRead()

// Register onVariants callbacks before afterEvaluate for APK path detection
// Use defaultDependencies to lazily add the Flank dependency
fladleConfig.defaultDependencies {
add(project.dependencies.create("${base.flankCoordinates.get()}:${base.flankVersion.get()}"))
}

// Register onVariants callbacks before task registration for APK path detection
project.pluginManager.withPlugin("com.android.application") {
if (!base.debugApk.isPresent || !base.instrumentationApk.isPresent) {
findDebugAndInstrumentationApk(project, base)
}
}

project.afterEvaluate {
// Add Flank dependency to Fladle Configuration
// Must be done afterEvaluate otherwise extension values will not be set.
project.dependencies.add(FLADLE_CONFIG, "${base.flankCoordinates.get()}:${base.flankVersion.get()}")

tasks.apply {
createTasksForConfig(base, base, project, "")

Expand All @@ -79,17 +78,20 @@ class FladlePluginDelegate {
project: Project,
name: String,
) {
val fladleConfig = project.configurations.getByName(FLADLE_CONFIG)
val configName = name.toLowerCase()
// we want to use default dir only if user did not set own `localResultsDir`
val useDefaultDir = config.localResultsDir.isPresent.not()

val flankVersionProvider = base.flankVersion

val validateFladle =
register("validateFladleConfig$name") {
description = "Perform validation actions"
group = TASK_GROUP
doLast {
checkIfSanityAndValidateConfigs(config)
validateOptionsUsed(config = config, flank = base.flankVersion.get())
validateOptionsUsed(config = config, flank = flankVersionProvider.get())
checkForExclusionUsage(config)
}
}
Expand All @@ -102,78 +104,57 @@ class FladlePluginDelegate {
description = "Print the flank.yml file to the console."
group = TASK_GROUP
dependsOn(writeConfigProps)
val configFile = writeConfigProps.flatMap { it.fladleConfigFile }
inputs.file(configFile)
doLast {
println(
writeConfigProps
.get()
.fladleConfigFile
.get()
.asFile
.readText(),
)
println(configFile.get().asFile.readText())
}
}

register("flankDoctor$name", FlankJavaExec::class.java) {
if (useDefaultDir) setUpWorkingDir(configName)
description = "Finds problems with the current configuration."
classpath = project.fladleConfig
classpath = fladleConfig
val configFilePath = writeConfigProps.flatMap { it.fladleConfigFile }.map { it.asFile.absolutePath }
args =
listOf(
"firebase",
"test",
"android",
"doctor",
"-c",
writeConfigProps
.get()
.fladleConfigFile
.get()
.asFile.absolutePath,
)
argumentProviders.add {
listOf("-c", configFilePath.get())
}
dependsOn(writeConfigProps)
}

val dumpShards = project.providers.gradleProperty("dumpShards")

val execFlank = register("execFlank$name", FlankExecutionTask::class.java, config)
execFlank.configure {
if (useDefaultDir) setUpWorkingDir(configName)
description = "Runs instrumentation tests using flank on firebase test lab."
classpath = project.fladleConfig
args =
if (project.hasProperty("dumpShards")) {
listOf(
"firebase",
"test",
"android",
"run",
"-c",
writeConfigProps
.get()
.fladleConfigFile
.get()
.asFile.absolutePath,
"--dump-shards",
)
} else {
listOf(
"firebase",
"test",
"android",
"run",
"-c",
writeConfigProps
.get()
.fladleConfigFile
.get()
.asFile.absolutePath,
)
classpath = fladleConfig
val configFilePath = writeConfigProps.flatMap { it.fladleConfigFile }.map { it.asFile.absolutePath }
argumentProviders.add {
buildList {
add("firebase")
add("test")
add("android")
add("run")
add("-c")
add(configFilePath.get())
if (dumpShards.isPresent) {
add("--dump-shards")
}
}
}
if (config.serviceAccountCredentials.isPresent) {
environment(mapOf("GOOGLE_APPLICATION_CREDENTIALS" to config.serviceAccountCredentials.get()))
}
dependsOn(writeConfigProps)
if (config.dependOnAssemble.isPresent && config.dependOnAssemble.get()) {
// Find assemble tasks by convention name pattern
val variantName = config.variant.orNull
if (variantName != null) {
val capitalizedVariant = variantName.capitalize()
Expand All @@ -186,7 +167,6 @@ class FladlePluginDelegate {
}
if (config.localResultsDir.hasValue) {
this.outputs.dir("${workingDir.path}/${config.localResultsDir.get()}")
// This task is never upToDate since it relies on network connections and firebase test lab.
this.outputs.upToDateWhen { false }
}
}
Expand All @@ -209,15 +189,13 @@ class FladlePluginDelegate {
config: FladleConfig,
androidExtension: ApplicationExtension,
) {
project.afterEvaluate {
val execution = androidExtension.testOptions.execution.uppercase()
val useOrchestrator =
execution == "ANDROIDX_TEST_ORCHESTRATOR" ||
execution == "ANDROID_TEST_ORCHESTRATOR"
if (useOrchestrator) {
log("Automatically detected the use of Android Test Orchestrator")
config.useOrchestrator.set(true)
}
val execution = androidExtension.testOptions.execution.uppercase()
val useOrchestrator =
execution == "ANDROIDX_TEST_ORCHESTRATOR" ||
execution == "ANDROID_TEST_ORCHESTRATOR"
if (useOrchestrator) {
project.log("Automatically detected the use of Android Test Orchestrator")
config.useOrchestrator.set(true)
}
}

Expand All @@ -229,13 +207,13 @@ class FladlePluginDelegate {
requireNotNull(
project.extensions.findByType(ApplicationExtension::class.java),
) { "Could not find ApplicationExtension in ${project.name}" }
automaticallyConfigureTestOrchestrator(project, config, androidExtension)

val androidComponents =
requireNotNull(project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java)) {
"Could not find ApplicationAndroidComponentsExtension in ${project.name}"
}

automaticallyConfigureTestOrchestrator(project, config, androidExtension)

androidComponents.onVariants { variant ->
if (!variant.isExpectedVariant(config)) return@onVariants
val androidTest = (variant as? HasAndroidTest)?.androidTest ?: return@onVariants
Expand All @@ -250,7 +228,6 @@ class FladlePluginDelegate {
.get()
val buildDir = project.layout.buildDirectory

// Test APK path
val testApkDirPath = if (flavorPath.isNotEmpty()) "androidTest/$flavorPath/$buildType" else "androidTest/$buildType"
val testApkFileName =
if (flavorName.isNotEmpty()) {
Expand All @@ -262,8 +239,7 @@ class FladlePluginDelegate {
buildDir
.file("outputs/apk/$testApkDirPath/$testApkFileName")
.get()
.asFile
.absolutePath
.asFile.absolutePath

variant.outputs.forEach { output ->
if (!output.isExpectedAbiOutput(config)) return@forEach
Expand All @@ -283,26 +259,20 @@ class FladlePluginDelegate {
buildDir
.file("outputs/apk/$appApkDirPath/$appApkFileName")
.get()
.asFile
.absolutePath
.asFile.absolutePath

if (!config.debugApk.isPresent) {
// Don't set debug apk if not already set. #172
project.log("Configuring fladle.debugApk from variant ${variant.name}")
config.debugApk.set(appApkPath)
}
if (!config.roboScript.isPresent && !config.instrumentationApk.isPresent && !config.sanityRobo.get()) {
// Don't set instrumentation apk if not already set. #172
project.log("Configuring fladle.instrumentationApk from variant ${variant.name}")
config.instrumentationApk.set(testApkPath)
}
}
}
}

private val Project.fladleConfig: Configuration
get() = configurations.getByName(FLADLE_CONFIG)

companion object {
val GRADLE_MIN_VERSION: GradleVersion = GradleVersion.version("9.1")
const val TASK_GROUP = "fladle"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ abstract class FlankJavaExec
constructor(
projectLayout: ProjectLayout,
) : JavaExec() {
// Store only the build directory Provider, not the whole ProjectLayout
// (ProjectLayout may hold a Project reference which is not CC-serializable)
private val buildDir = projectLayout.buildDirectory

init {
group = FladlePluginDelegate.TASK_GROUP
mainClass.set("ftl.Main")
workingDir(projectLayout.fladleDir)
}

fun setUpWorkingDir(configName: String) = workingDir(project.layout.buildDirectory.dir("fladle/$configName"))
fun setUpWorkingDir(configName: String) = workingDir(buildDir.dir("fladle/$configName"))
}
Loading