From 9c843cc656f0355008e42b042dd00c17d864f567 Mon Sep 17 00:00:00 2001 From: Tim Froehlich Date: Thu, 23 Apr 2026 19:21:10 +0000 Subject: [PATCH 1/2] test(oss-licenses): add Issue 397 regression test and version logging --- .../testapp/app/build.gradle.kts | 34 +++++++++++++++++++ .../oss/licenses/testapp/OssLicensesV2Test.kt | 22 ++++++++++++ 2 files changed, 56 insertions(+) diff --git a/oss-licenses-plugin/testapp/app/build.gradle.kts b/oss-licenses-plugin/testapp/app/build.gradle.kts index bc5a60bb..a105a634 100644 --- a/oss-licenses-plugin/testapp/app/build.gradle.kts +++ b/oss-licenses-plugin/testapp/app/build.gradle.kts @@ -120,6 +120,40 @@ dependencies { debugImplementation(libs.androidx.compose.ui.test.manifest) } +// Log the actual resolved versions of the library and plugin. +tasks.register("logOssVersions") { + val runtimeConfig = configurations.getByName("releaseRuntimeClasspath") + val buildConfig = rootProject.buildscript.configurations.getByName("classpath") + + // Use providers to resolve versions safely. + val libVersion = provider { + runtimeConfig.incoming.resolutionResult.allComponents + .map { it.id } + .filterIsInstance() + .find { it.group == "com.google.android.gms" && it.module == "play-services-oss-licenses" } + ?.version ?: "UNKNOWN" + } + + val plugVersion = provider { + buildConfig.incoming.resolutionResult.allComponents + .map { it.id } + .filterIsInstance() + .find { it.group == "com.google.android.gms" && it.module == "oss-licenses-plugin" } + ?.version ?: "LOCAL" + } + + doFirst { + println("------------------------------------------------------------") + println("OSS Licenses Library (Resolved): ${libVersion.get()}") + println("OSS Licenses Plugin (Resolved): ${plugVersion.get()}") + println("------------------------------------------------------------") + } +} + +tasks.matching { it.name == "preBuild" || it.name == "test" }.configureEach { + dependsOn("logOssVersions") +} + abstract class GenerateVersionTask : DefaultTask() { @get:org.gradle.api.tasks.InputFile @get:org.gradle.api.tasks.Optional diff --git a/oss-licenses-plugin/testapp/app/src/testRelease/java/com/google/android/gms/oss/licenses/testapp/OssLicensesV2Test.kt b/oss-licenses-plugin/testapp/app/src/testRelease/java/com/google/android/gms/oss/licenses/testapp/OssLicensesV2Test.kt index 65997aa5..6770ebcc 100644 --- a/oss-licenses-plugin/testapp/app/src/testRelease/java/com/google/android/gms/oss/licenses/testapp/OssLicensesV2Test.kt +++ b/oss-licenses-plugin/testapp/app/src/testRelease/java/com/google/android/gms/oss/licenses/testapp/OssLicensesV2Test.kt @@ -18,6 +18,7 @@ package com.google.android.gms.oss.licenses.testapp import android.content.Intent import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.core.app.ActivityScenario @@ -45,6 +46,27 @@ class OssLicensesV2Test { } } + @Test + fun testV2DoubleTapUpButton() { + // Reproduces Issue 397: Rapid double-tap on Up button leads to IllegalArgumentException + ActivityScenario.launch(OssLicensesMenuActivity::class.java).use { + // Navigate to detail + composeTestRule.onNodeWithText("Activity", ignoreCase = true).performClick() + + // Find the "Up" button (usually the first IconButton in the TopAppBar or has "Back" content description) + // In Nav3 default TopAppBar, it might be the back button. + // Let's assume it has a content description "Back" or "Navigate up" + val backButton = composeTestRule.onNodeWithContentDescription("Back", ignoreCase = true) + + // Rapidly double tap + backButton.performClick() + backButton.performClick() + + // If it crashes, the test will fail with the exception. + composeTestRule.onNodeWithText("Open source licenses", ignoreCase = true).assertExists() + } + } + @Test fun testV2DetailNavigation() { ActivityScenario.launch(OssLicensesMenuActivity::class.java).use { From 488c708b62a9b650ad0d9ca1252d7ce0c9bbe6c5 Mon Sep 17 00:00:00 2001 From: Tim Froehlich Date: Thu, 23 Apr 2026 21:30:22 +0000 Subject: [PATCH 2/2] Fix issue with changes to testapp not invalidating e2e est build cache --- oss-licenses-plugin/build.gradle.kts | 28 +++++++++++++++++++ .../gms/oss/licenses/plugin/EndToEndTest.kt | 24 +++++----------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/oss-licenses-plugin/build.gradle.kts b/oss-licenses-plugin/build.gradle.kts index a2873e4c..b93ac140 100644 --- a/oss-licenses-plugin/build.gradle.kts +++ b/oss-licenses-plugin/build.gradle.kts @@ -181,6 +181,22 @@ dependencies { "e2eTestImplementation"(gradleTestKit()) } +// Pre-process the testapp into a clean directory using an allow-list. +// This excludes redundant build artifacts and IDE/Gradle internal folders. +// The Gradle wrapper is also excluded because E2E tests use GradleRunner.withGradleVersion(). +val prepareTestApp by tasks.registering(Sync::class) { + from("testapp") { + include("app/src/**") + include("app/build.gradle.kts") + include("gradle/*.toml") + include("gradle/*.properties") + include("build.gradle.kts") + include("settings.gradle.kts") + include("gradle.properties") + } + into(layout.buildDirectory.dir("testapp-prepared")) +} + val e2eTestTask by tasks.registering(Test::class) { description = "Runs end-to-end tests that build the full testapp against multiple AGP versions" group = "verification" @@ -189,6 +205,18 @@ val e2eTestTask by tasks.registering(Test::class) { configureTestKitDefaults() + // Wire the prepared directory to inputs and system properties via Providers. + // This implicitly handles the task dependency on prepareTestApp. + val testAppDirProvider = prepareTestApp.map { it.destinationDir } + inputs.dir(testAppDirProvider) + .withPathSensitivity(PathSensitivity.RELATIVE) + .withPropertyName("testapp") + + // Pass as a system property lazily to maintain configuration cache compatibility. + jvmArgumentProviders.add(CommandLineArgumentProvider { + listOf("-Dtestapp.dir=${testAppDirProvider.get().absolutePath}") + }) + // Inject AGP/Gradle version pairs as system properties for each e2e subclass. e2eTestVersions.forEach { (className, versions) -> systemProperties["$className.agpVersion"] = versions.first diff --git a/oss-licenses-plugin/src/e2eTest/kotlin/com/google/android/gms/oss/licenses/plugin/EndToEndTest.kt b/oss-licenses-plugin/src/e2eTest/kotlin/com/google/android/gms/oss/licenses/plugin/EndToEndTest.kt index 6d9cd727..453974d6 100644 --- a/oss-licenses-plugin/src/e2eTest/kotlin/com/google/android/gms/oss/licenses/plugin/EndToEndTest.kt +++ b/oss-licenses-plugin/src/e2eTest/kotlin/com/google/android/gms/oss/licenses/plugin/EndToEndTest.kt @@ -44,12 +44,6 @@ abstract class EndToEndTest { private val AGP_VERSION_REGEX = Regex("""agp = ".*"""") private val KOTLIN_VERSION_REGEX = Regex("""kotlin = ".*"""") - // Files to copy from the testapp source into the temp project directory - private val TESTAPP_ALLOW_LIST = listOf( - "app", "gradle", "build.gradle.kts", "settings.gradle.kts", "gradle.properties", - "gradlew", "gradlew.bat" - ) - // AGP 9+ has built-in Kotlin support; AGP 8.x requires the standalone KGP with legacy config. private val AGP_9_KOTLIN_BLOCK = """ kotlin { @@ -78,16 +72,19 @@ abstract class EndToEndTest { projectDir = tempDirectory.newFolder("testapp") val currentDir = File(System.getProperty("user.dir")!!) // if this is missing then something is very wrong - val testAppSourceDir = File(currentDir, "testapp") + val testappDirPath = System.getProperty("testapp.dir") + requireNotNull(testappDirPath) { "testapp.dir system property is missing" } + + val testAppSourceDir = File(testappDirPath) require(testAppSourceDir.exists()) { "Test app source not found at: ${testAppSourceDir.absolutePath}" } configureAndroidSdk(currentDir) - copyTestApp(testAppSourceDir) + testAppSourceDir.copyRecursively(projectDir, overwrite = true) - // Remove the Gradle daemon JVM file — the JAVA_HOME injection in createRunner() handles - // JVM selection more cleanly across all Gradle versions. + // Remove the Gradle daemon JVM file if present — the JAVA_HOME injection in createRunner() + // handles JVM selection more cleanly across all Gradle versions. File(projectDir, "gradle/gradle-daemon-jvm.properties").delete() patchVersions() @@ -102,13 +99,6 @@ abstract class EndToEndTest { File(projectDir, "local.properties").writeText("sdk.dir=${sdkDir.replace("\\", "\\\\")}\n") } - private fun copyTestApp(sourceDir: File) { - TESTAPP_ALLOW_LIST - .map { sourceDir.resolve(it) } - .filter { it.exists() } - .forEach { it.copyRecursively(projectDir.resolve(it.name), overwrite = true) } - } - private fun patchVersions() { val agpBundlesKgp = agpVersion.substringBefore('.').toIntOrNull()?.let { it >= 9 } ?: false