Skip to content
Merged
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 firebase-dataconnect/firebase-dataconnect.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ plugins {
id("copy-google-services")
alias(libs.plugins.kotlinx.serialization)
id("com.google.firebase.dataconnect.gradle.plugin") apply false
id("com.google.firebase.dataconnect.sharedtest")
}

firebaseLibrary {
Expand Down
5 changes: 5 additions & 0 deletions firebase-dataconnect/gradleplugin/plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ gradlePlugin {
id = "com.google.firebase.dataconnect.gradle.plugin"
implementationClass = "com.google.firebase.dataconnect.gradle.plugin.DataConnectGradlePlugin"
}
create("sharedTest") {
id = "com.google.firebase.dataconnect.sharedtest"
implementationClass =
"com.google.firebase.dataconnect.gradle.sharedtest.SharedWithAndroidTestPlugin"
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* 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
*
* http://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.firebase.dataconnect.gradle.sharedtest

import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.logging.Logger
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import java.io.File

/**
* A Gradle task that copies Kotlin source files annotated with `@file:SharedWithAndroidTest` from
* an input directory to an output directory.
*
* This driving motivation behind creating this task is to share test utilities or fixtures between
* unit tests and Android instrumentation tests by copying the annotated files into the appropriate
* source set before compilation.
*/
abstract class CopySharedWithAndroidTestFiles : DefaultTask() {

@get:Internal abstract val inputBaseDirectory: DirectoryProperty

@get:InputDirectory abstract val inputDirectory: DirectoryProperty
Comment thread
dconeybe marked this conversation as resolved.

@get:OutputDirectory abstract val outputDirectory: DirectoryProperty

@TaskAction
fun run() {
val inputBaseDirectory: File = inputBaseDirectory.get().asFile
val inputDirectory: File = inputDirectory.get().asFile
val outputDirectory: File = outputDirectory.get().asFile

logger.info("inputBaseDirectory={}", inputBaseDirectory.absolutePath)
logger.info("inputDirectory={}", inputDirectory.absolutePath)
Comment thread
dconeybe marked this conversation as resolved.
logger.info("outputDirectory={}", outputDirectory.absolutePath)

copyAnnotatedFiles(
srcBaseDir = inputBaseDirectory,
srcDir = inputDirectory,
destDir = outputDirectory,
taskPath = path,
logger = logger,
)
}
Comment thread
dconeybe marked this conversation as resolved.
}

/**
* Recursively searches through [srcDir] for Kotlin source files (`.kt`) that contain the target
* annotation and copies them to [destDir], maintaining their relative directory structure.
*
* @param srcBaseDir The root directory to use when reporting file system paths in the generated
* files, rather than their absolute paths, to avoid defeating build caches; this **MUST** be a
* parent directory of [srcDir].
* @param srcDir The root directory to search for annotated Kotlin files.
* @param destDir The root directory where annotated files should be copied.
* @param taskPath The full Gradle path of the task performing the copy.
* @param logger The Gradle logger to use for logging the copy operations.
*/
private fun copyAnnotatedFiles(srcBaseDir: File, srcDir: File, destDir: File, taskPath: String, logger: Logger) {
if (!srcDir.exists()) {
logger.info("source directory was not found; no files copied")
return
}
if (!srcDir.isDirectory) {
logger.info("source directory is not a directory; no files copied")
return
}

val srcFilesToCopy = srcDir
.walk()
.filter { it.isFile && it.extension == "kt" && it.hasALineEqualToOneOf(sharedWithAndroidTestAnnotationLines) }
.toList()

logger.info(
"Found {} files in {} that have a line equal to one of: {}",
srcFilesToCopy.size,
srcDir,
sharedWithAndroidTestAnnotationLines.joinToString { "\"$it\"" }
)

if (srcFilesToCopy.isEmpty()) {
logger.info("No files found to copy; no files copied")
return
}

srcFilesToCopy.forEachIndexed { fileIndex, srcFile ->
val relativePath = srcFile.relativeTo(srcDir)
val destFile = File(destDir, relativePath.path)
logger.info("Copying file {}/{}: {} to {}", (fileIndex+1), srcFilesToCopy.size, srcFile, destFile)

destFile.parentFile?.mkdirs()

destFile.printWriter().use {
val srcFileRelative = srcFile.absoluteFile.relativeTo(srcBaseDir.absoluteFile)
it.println("// Generated from: $srcFileRelative")
it.println("// Generated by Gradle task: $taskPath")
it.println("//")
it.println("// WARNING: This file is generated!")
it.println("// Any changes made to this file will eventually be overwritten by the build.")
it.println("// If changes are desired, make them in the original file instead.")
it.println()

srcFile.useLines { lines ->
lines.forEach { line ->
it.println(line)
}
}
}
}
}

private val sharedWithAndroidTestAnnotationLines = listOf(
"@file:SharedWithAndroidTest",
"@file:com.google.firebase.dataconnect.testutil.SharedWithAndroidTest",
)

private fun File.hasALineEqualToOneOf(desiredLines: Collection<String>): Boolean =
useLines { lines ->
lines.any {
it.trim() in desiredLines
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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
*
* http://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.firebase.dataconnect.gradle.sharedtest

import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.HasAndroidTest
import com.android.build.api.variant.Variant
import java.util.Locale
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionContainer
import org.gradle.api.tasks.TaskContainer
import org.gradle.kotlin.dsl.register

/**
* A Gradle plugin that, when applied, makes all files in `src/test/kotlin` that are annotated with
* `@file:SharedWithAndroidTest` available to code in `src/androidTest/kotlin`, enabling test
* utilities written for unit tests to also be available to integration tests.
*
* This is achieved by adding a "code generation" step to the `androidTest` target which simply
* copies the appropriately-annotated files into the "generated code" directory.
*
* To apply this plugin to an Android application or library, simply register the plugin alongside
* other Gradle plugins in `build.gradle.kts`:
*
* ```
* plugins {
* // other plugins
* id("com.google.firebase.dataconnect.sharedtest")
* }
* ```
*/
@Suppress("unused")
abstract class SharedWithAndroidTestPlugin : Plugin<Project> {
override fun apply(project: Project) = applyPlugin(project.extensions, project.tasks)
}

private fun applyPlugin(extensions: ExtensionContainer, tasks: TaskContainer) {
val androidComponents = extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant -> handleVariant(variant, tasks) }
}

private fun handleVariant(variant: Variant, tasks: TaskContainer) {
val androidTest = (variant as? HasAndroidTest)?.androidTest ?: return
val variantNameTitleCase = variant.name.replaceFirstChar { it.titlecase(Locale.US) }

val task =
tasks.register<CopySharedWithAndroidTestFiles>(
"copy${variantNameTitleCase}SharedWithAndroidTestFiles"
) {
val projectDirectory = project.layout.projectDirectory
inputBaseDirectory.set(projectDirectory)
inputDirectory.set(projectDirectory.dir("src/test/kotlin"))
}

androidTest.sources.java!!.addGeneratedSourceDirectory(
task,
CopySharedWithAndroidTestFiles::outputDirectory
)
}
Comment thread
dconeybe marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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
*
* http://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.
*/

@file:SharedWithAndroidTest

package com.google.firebase.dataconnect.testutil

/**
* Annotation used to mark files that should be shared with the androidTest source set.
*
* This annotation is processed by the CopySharedWithAndroidTestFiles Gradle task. For performance
* and simplicity, the task performs a rudimentary string-based check on the file's contents. To be
* recognized, a line in the source file must, when trimmed, exactly equal either:
* - `@file:SharedWithAndroidTest`
* - `@file:com.google.firebase.dataconnect.testutil.SharedWithAndroidTest`
*
* Notably, "grouped syntax" like `@file:[JvmName("MyFile") SharedWithAndroidTest]` is NOT supported
* and will not be recognized by the task.
*/
@Target(AnnotationTarget.FILE)
@Retention(AnnotationRetention.SOURCE)
internal annotation class SharedWithAndroidTest
Loading