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
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@ updates:
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 1
groups:
github-actions:
patterns:
- "*"

- package-ecosystem: "gradle"
directory: "/oss-licenses-plugin/testapp"
schedule:
interval: "weekly"
open-pull-requests-limit: 1
groups:
all-dependencies:
patterns:
- "*"
42 changes: 40 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

Expand Down Expand Up @@ -62,3 +60,43 @@ jobs:
- name: Perform a Gradle build
run: ./gradlew build
working-directory: ./${{ matrix.project-dir }}

# Publish the plugin to a local repo for testapp verification
- name: Publish to local repo
if: matrix.project-dir == 'oss-licenses-plugin'
run: ./gradlew publish
working-directory: ./oss-licenses-plugin

# Upload the built plugin as an artifact
- name: Upload local repo artifact
if: matrix.project-dir == 'oss-licenses-plugin'
uses: actions/upload-artifact@b4b15b8c7c6ac21ea23f011fb03a537e76e993c2 # v4.4.3
with:
name: oss-licenses-local-repo
path: oss-licenses-plugin/build/repo/

# Job to run high-signal tests using the standalone testapp.
testapp-verification:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Download local repo artifact
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: oss-licenses-local-repo
path: oss-licenses-plugin/build/repo/

- name: Set up JDK 17
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2

- name: Run Test App Tests
run: ./gradlew build
working-directory: oss-licenses-plugin/testapp
35 changes: 35 additions & 0 deletions oss-licenses-plugin/GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Gemini Developer Guide: OSS Licenses Plugin

This document provides essential information for AI agents and developers working on the `oss-licenses-plugin` and its tests.

## Test App Architecture

The project uses a single test application to test the plugin.

### Directory Structure
* `oss-licenses-plugin/testapp/`: The build environment.
* **Gradle:** 9.4.0
* **AGP:** 9.0.0
* **SDK:** 36
* **Target:** Current stable/bleeding-edge versions.
* `oss-licenses-plugin/testapp/app/`: The Source Code.
* Contains the actual Android application and Robolectric tests used by the environment.

### Running Tests Standalone

```bash
cd oss-licenses-plugin/testapp
./gradlew clean :app:test
```

### Local Repository Injection
The test app is configured to automatically pick up the locally built plugin if it has been published to the project's internal repository.
1. **Publish:** `cd oss-licenses-plugin && ./gradlew publish`
2. **Usage:** The app's `settings.gradle.kts` looks for `../build/repo`.
3. **Library Override:** To test with a local version of the `play-services-oss-licenses` runtime library, pass the `libraryRepoPath` property:
`./gradlew :app:test -PlibraryRepoPath=/path/to/your/m2repo`

### Test Separation (V1 vs V2)
Tests are split:
* `OssLicensesV1Test.kt`: Standard Espresso tests for the original activity.
* `OssLicensesV2Test.kt`: Compose tests for the V2 activity.
16 changes: 16 additions & 0 deletions oss-licenses-plugin/settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
/*
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

rootProject.name = 'oss-licenses-plugin'
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.gms.oss.licenses.plugin

import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File

/**
* Robust E2E test that executes the standalone testapp.
*/
abstract class TestAppEndToEndTest(private val agpVersion: String, private val gradleVersion: String) {

@get:Rule
val tempDirectory: TemporaryFolder = TemporaryFolder()

private lateinit var projectDir: File
private lateinit var testAppSourceDir: File

@Before
fun setup() {
projectDir = tempDirectory.newFolder("testapp")

// Find the source testapp directory
val currentDir = File(System.getProperty("user.dir"))
testAppSourceDir = File(currentDir, "testapp")

if (!testAppSourceDir.exists()) {
throw IllegalStateException("Test app source not found at: ${testAppSourceDir.absolutePath}")
}

// Copy everything to the temporary directory
testAppSourceDir.copyRecursively(projectDir)

// The testapp template uses AGP 9.0+ and modern Kotlin.
// For older compatible AGP versions, we dynamically override the versions.
val isModernKotlin = agpVersion.startsWith("9.")

val tomlFile = File(projectDir, "gradle/libs.versions.toml")
var tomlContent = tomlFile.readText()
tomlContent = tomlContent.replace(Regex("agp = \".*\""), "agp = \"$agpVersion\"")

// If testing an older AGP, we also need to manually inject the Kotlin Gradle Plugin
// since older AGPs didn't have built-in Kotlin support and rely on the older KGP.
if (!isModernKotlin) {
tomlContent = tomlContent.replace(Regex("kotlin = \".*\""), "kotlin = \"2.1.10\"")
tomlContent += "\nkotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }"
}
tomlFile.writeText(tomlContent)

// Update root build.gradle.kts to include KGP so ktfmt can find KotlinCompile on the classpath
val rootBuildFile = File(projectDir, "build.gradle.kts")
var rootBuildContent = rootBuildFile.readText()
if (!isModernKotlin) {
rootBuildContent = rootBuildContent.replace(
"alias(libs.plugins.android.application) apply false",
"""
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
""".trimIndent()
)
rootBuildFile.writeText(rootBuildContent)
}

// Update build.gradle.kts to handle older Kotlin compiler configs vs modern built-in Kotlin
val buildFile = File(projectDir, "app/build.gradle.kts")
var buildContent = buildFile.readText()
if (!isModernKotlin) {
buildContent = buildContent.replace(
"alias(libs.plugins.android.application)",
"""
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
""".trimIndent()
)
buildContent = buildContent.replace(
"""
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
""".trimIndent(),
"""
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
@Suppress("DEPRECATION")
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs += listOf("-Xskip-metadata-version-check")
}
}
""".trimIndent()
)
}
buildFile.writeText(buildContent)
}

private fun createRunner(vararg arguments: String): GradleRunner {
return GradleRunner.create()
.withProjectDir(projectDir)
.withGradleVersion(gradleVersion)
.withTestKitDir(File(System.getProperty("testkit_path"), this.javaClass.simpleName))
.forwardOutput()
.withArguments(*arguments, "--configuration-cache", "--parallel", "-Dorg.gradle.configuration-cache.problems=fail", "-s")
}

@Test
fun testTestAppTestsPass() {
val result = createRunner(":app:testReleaseUnitTest").build()
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":app:testReleaseUnitTest")?.outcome)
}
}

// Due to the dependency requirements of the library, we can only test with recent versions of AGP
class TestAppEndToEndTest_AGP810_G811 : TestAppEndToEndTest("8.10.0", "8.11.1")
class TestAppEndToEndTest_AGP812_G814 : TestAppEndToEndTest("8.12.2", "8.14")
class TestAppEndToEndTest_AGP90_G90 : TestAppEndToEndTest("9.0.0-alpha03", "9.0.0")
class TestAppEndToEndTest_AGP91_G931 : TestAppEndToEndTest("9.1.0-alpha05", "9.3.1")
115 changes: 115 additions & 0 deletions oss-licenses-plugin/testapp/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import com.android.build.api.variant.HostTestBuilder
import org.gradle.api.tasks.testing.Test

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ktfmt)
alias(libs.plugins.oss.licenses)
}

android {
namespace = "com.google.android.gms.oss.licenses.testapp"
compileSdk = libs.versions.compileSdk.get().toInt()
testBuildType = "release"

defaultConfig {
applicationId = "com.google.android.gms.oss.licenses.testapp"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
debug {
isMinifyEnabled = false
}
release {
signingConfig = signingConfigs.getByName("debug")
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
lint {
abortOnError = true
checkDependencies = true
ignoreWarnings = false
}
}

tasks.withType<Test>().configureEach {
// Enable parallel execution for faster Robolectric runs
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)

testLogging {
events("passed", "skipped", "failed", "standardOut", "standardError")
showStandardStreams = true
}
}

androidComponents {
beforeVariants { variantBuilder ->
// AGP 9.0 only enables unit tests for the "tested build type" by default.
// We explicitly enable them for all variants to ensure both Debug and Release coverage.
variantBuilder.hostTests[HostTestBuilder.UNIT_TEST_TYPE]?.enable = true
}
}

kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}

ktfmt {
kotlinLangStyle()
}

dependencies {
implementation(libs.play.services.oss.licenses)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity)

// Test dependencies for predictable license testing
implementation(libs.gson) // Apache 2.0
implementation(libs.guava) // Apache 2.0

testImplementation(libs.junit)
testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.androidx.test.espresso.core)
testImplementation(libs.androidx.test.espresso.contrib)
testImplementation(libs.androidx.test.core)
testImplementation(libs.robolectric)

// Compose Test (required for testing the V2 activity)
testImplementation(platform(libs.androidx.compose.bom))
testImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}
17 changes: 17 additions & 0 deletions oss-licenses-plugin/testapp/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:label="OSS Licenses Test App"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Loading