blob: d32b40eac7dae56c3267419ef830895b7ebcb7a8 [file] [log] [blame]
/*
* Copyright (C) 2020 The Dagger Authors.
*
* 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.
*/
import java.io.File
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.BuildTask
import org.gradle.testkit.runner.GradleRunner
import org.junit.rules.TemporaryFolder
/**
* Testing utility class that sets up a simple Android project that applies the Hilt plugin.
*/
class GradleTestRunner(val tempFolder: TemporaryFolder) {
private val dependencies = mutableListOf<String>()
private val activities = mutableListOf<String>()
private val additionalAndroidOptions = mutableListOf<String>()
private val hiltOptions = mutableListOf<String>()
private var appClassName: String? = null
private var buildFile: File? = null
private var gradlePropertiesFile: File? = null
private var manifestFile: File? = null
private var additionalTasks = mutableListOf<String>()
init {
tempFolder.newFolder("src", "main", "java", "minimal")
tempFolder.newFolder("src", "test", "java", "minimal")
tempFolder.newFolder("src", "main", "res")
}
// Adds project dependencies, e.g. "implementation <group>:<id>:<version>"
fun addDependencies(vararg deps: String) {
dependencies.addAll(deps)
}
// Adds an <activity> tag in the project's Android Manifest, e.g. "<activity name=".Foo"/>
fun addActivities(vararg activityElements: String) {
activities.addAll(activityElements)
}
// Adds 'android' options to the project's build.gradle, e.g. "lintOptions.checkReleaseBuilds = false"
fun addAndroidOption(vararg options: String) {
additionalAndroidOptions.addAll(options)
}
// Adds 'hilt' options to the project's build.gradle, e.g. "enableExperimentalClasspathAggregation = true"
fun addHiltOption(vararg options: String) {
hiltOptions.addAll(options)
}
// Adds a source package to the project. The package path is relative to 'src/main/java'.
fun addSrcPackage(packagePath: String) {
File(tempFolder.root, "src/main/java/$packagePath").mkdirs()
}
// Adds a source file to the project. The source path is relative to 'src/main/java'.
fun addSrc(srcPath: String, srcContent: String): File {
File(tempFolder.root, "src/main/java/${srcPath.substringBeforeLast(File.separator)}").mkdirs()
return tempFolder.newFile("/src/main/java/$srcPath").apply { writeText(srcContent) }
}
// Adds a test source file to the project. The source path is relative to 'src/test/java'.
fun addTestSrc(srcPath: String, srcContent: String): File {
File(tempFolder.root, "src/test/java/${srcPath.substringBeforeLast(File.separator)}").mkdirs()
return tempFolder.newFile("/src/test/java/$srcPath").apply { writeText(srcContent) }
}
// Adds a resource file to the project. The source path is relative to 'src/main/res'.
fun addRes(resPath: String, resContent: String): File {
File(tempFolder.root, "src/main/res/${resPath.substringBeforeLast(File.separator)}").mkdirs()
return tempFolder.newFile("/src/main/res/$resPath").apply { writeText(resContent) }
}
fun setAppClassName(name: String) {
appClassName = name
}
fun runAdditionalTasks(taskName: String) {
additionalTasks.add(taskName)
}
// Executes a Gradle builds and expects it to succeed.
fun build(): Result {
setupFiles()
return Result(tempFolder.root, createRunner().build())
}
// Executes a Gradle build and expects it to fail.
fun buildAndFail(): Result {
setupFiles()
return Result(tempFolder.root, createRunner().buildAndFail())
}
private fun setupFiles() {
writeBuildFile()
writeGradleProperties()
writeAndroidManifest()
}
private fun writeBuildFile() {
buildFile?.delete()
buildFile = tempFolder.newFile("build.gradle").apply {
writeText(
"""
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.0'
}
}
plugins {
id 'com.android.application'
id 'dagger.hilt.android.plugin'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "plugin.test"
minSdkVersion 21
targetSdkVersion 30
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
${additionalAndroidOptions.joinToString(separator = "\n")}
}
allprojects {
repositories {
mavenLocal()
google()
mavenCentral()
}
}
dependencies {
${dependencies.joinToString(separator = "\n")}
}
hilt {
${hiltOptions.joinToString(separator = "\n")}
}
""".trimIndent()
)
}
}
private fun writeGradleProperties() {
gradlePropertiesFile?.delete()
gradlePropertiesFile = tempFolder.newFile("gradle.properties").apply {
writeText(
"""
android.useAndroidX=true
""".trimIndent()
)
}
}
private fun writeAndroidManifest() {
manifestFile?.delete()
manifestFile = tempFolder.newFile("/src/main/AndroidManifest.xml").apply {
writeText(
"""
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="minimal">
<application
android:name="${appClassName ?: "android.app.Application"}"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
${activities.joinToString(separator = "\n")}
</application>
</manifest>
""".trimIndent()
)
}
}
private fun createRunner() = GradleRunner.create()
.withProjectDir(tempFolder.root)
.withArguments(listOf("--stacktrace", "assembleDebug") + additionalTasks)
.withPluginClasspath()
// .withDebug(true) // Add this line to enable attaching a debugger to the gradle test invocation
.forwardOutput()
// Data class representing a Gradle Test run result.
data class Result(
private val projectRoot: File,
private val buildResult: BuildResult
) {
val tasks: List<BuildTask> get() = buildResult.tasks
// Finds a task by name.
fun getTask(name: String) = buildResult.task(name) ?: error("Task '$name' not found.")
// Gets the full build output.
fun getOutput() = buildResult.output
// Finds a transformed file. The srcFilePath is relative to the app's package.
fun getTransformedFile(srcFilePath: String): File {
val parentDir =
File(projectRoot, "build/intermediates/asm_instrumented_project_classes/debug")
return File(parentDir, srcFilePath).also {
if (!it.exists()) {
error("Unable to find transformed class ${it.path}")
}
}
}
}
}