1 /* 2 * Copyright (C) 2020 The Dagger Authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 import java.io.File 18 import org.gradle.testkit.runner.BuildResult 19 import org.gradle.testkit.runner.BuildTask 20 import org.gradle.testkit.runner.GradleRunner 21 import org.junit.rules.TemporaryFolder 22 23 /** 24 * Testing utility class that sets up a simple Android project that applies the Hilt plugin. 25 */ 26 class GradleTestRunner(val tempFolder: TemporaryFolder) { 27 private val dependencies = mutableListOf<String>() 28 private val activities = mutableListOf<String>() 29 private val additionalAndroidOptions = mutableListOf<String>() 30 private val hiltOptions = mutableListOf<String>() 31 private var appClassName: String? = null 32 private var buildFile: File? = null 33 private var gradlePropertiesFile: File? = null 34 private var manifestFile: File? = null 35 private var additionalTasks = mutableListOf<String>() 36 37 init { 38 tempFolder.newFolder("src", "main", "java", "minimal") 39 tempFolder.newFolder("src", "test", "java", "minimal") 40 tempFolder.newFolder("src", "main", "res") 41 } 42 43 // Adds project dependencies, e.g. "implementation <group>:<id>:<version>" addDependenciesnull44 fun addDependencies(vararg deps: String) { 45 dependencies.addAll(deps) 46 } 47 48 // Adds an <activity> tag in the project's Android Manifest, e.g. "<activity name=".Foo"/> addActivitiesnull49 fun addActivities(vararg activityElements: String) { 50 activities.addAll(activityElements) 51 } 52 53 // Adds 'android' options to the project's build.gradle, e.g. "lintOptions.checkReleaseBuilds = false" addAndroidOptionnull54 fun addAndroidOption(vararg options: String) { 55 additionalAndroidOptions.addAll(options) 56 } 57 58 // Adds 'hilt' options to the project's build.gradle, e.g. "enableExperimentalClasspathAggregation = true" addHiltOptionnull59 fun addHiltOption(vararg options: String) { 60 hiltOptions.addAll(options) 61 } 62 63 // Adds a source package to the project. The package path is relative to 'src/main/java'. addSrcPackagenull64 fun addSrcPackage(packagePath: String) { 65 File(tempFolder.root, "src/main/java/$packagePath").mkdirs() 66 } 67 68 // Adds a source file to the project. The source path is relative to 'src/main/java'. addSrcnull69 fun addSrc(srcPath: String, srcContent: String): File { 70 File(tempFolder.root, "src/main/java/${srcPath.substringBeforeLast(File.separator)}").mkdirs() 71 return tempFolder.newFile("/src/main/java/$srcPath").apply { writeText(srcContent) } 72 } 73 74 // Adds a test source file to the project. The source path is relative to 'src/test/java'. addTestSrcnull75 fun addTestSrc(srcPath: String, srcContent: String): File { 76 File(tempFolder.root, "src/test/java/${srcPath.substringBeforeLast(File.separator)}").mkdirs() 77 return tempFolder.newFile("/src/test/java/$srcPath").apply { writeText(srcContent) } 78 } 79 80 // Adds a resource file to the project. The source path is relative to 'src/main/res'. addResnull81 fun addRes(resPath: String, resContent: String): File { 82 File(tempFolder.root, "src/main/res/${resPath.substringBeforeLast(File.separator)}").mkdirs() 83 return tempFolder.newFile("/src/main/res/$resPath").apply { writeText(resContent) } 84 } 85 setAppClassNamenull86 fun setAppClassName(name: String) { 87 appClassName = name 88 } 89 runAdditionalTasksnull90 fun runAdditionalTasks(taskName: String) { 91 additionalTasks.add(taskName) 92 } 93 94 // Executes a Gradle builds and expects it to succeed. buildnull95 fun build(): Result { 96 setupFiles() 97 return Result(tempFolder.root, createRunner().build()) 98 } 99 100 // Executes a Gradle build and expects it to fail. buildAndFailnull101 fun buildAndFail(): Result { 102 setupFiles() 103 return Result(tempFolder.root, createRunner().buildAndFail()) 104 } 105 setupFilesnull106 private fun setupFiles() { 107 writeBuildFile() 108 writeGradleProperties() 109 writeAndroidManifest() 110 } 111 writeBuildFilenull112 private fun writeBuildFile() { 113 buildFile?.delete() 114 buildFile = tempFolder.newFile("build.gradle").apply { 115 writeText( 116 """ 117 buildscript { 118 repositories { 119 google() 120 mavenCentral() 121 } 122 dependencies { 123 classpath 'com.android.tools.build:gradle:4.2.0' 124 } 125 } 126 127 plugins { 128 id 'com.android.application' 129 id 'dagger.hilt.android.plugin' 130 } 131 132 android { 133 compileSdkVersion 30 134 buildToolsVersion "30.0.2" 135 136 defaultConfig { 137 applicationId "plugin.test" 138 minSdkVersion 21 139 targetSdkVersion 30 140 } 141 142 compileOptions { 143 sourceCompatibility 1.8 144 targetCompatibility 1.8 145 } 146 ${additionalAndroidOptions.joinToString(separator = "\n")} 147 } 148 149 allprojects { 150 repositories { 151 mavenLocal() 152 google() 153 mavenCentral() 154 } 155 } 156 157 dependencies { 158 ${dependencies.joinToString(separator = "\n")} 159 } 160 161 hilt { 162 ${hiltOptions.joinToString(separator = "\n")} 163 } 164 """.trimIndent() 165 ) 166 } 167 } 168 writeGradlePropertiesnull169 private fun writeGradleProperties() { 170 gradlePropertiesFile?.delete() 171 gradlePropertiesFile = tempFolder.newFile("gradle.properties").apply { 172 writeText( 173 """ 174 android.useAndroidX=true 175 """.trimIndent() 176 ) 177 } 178 } 179 writeAndroidManifestnull180 private fun writeAndroidManifest() { 181 manifestFile?.delete() 182 manifestFile = tempFolder.newFile("/src/main/AndroidManifest.xml").apply { 183 writeText( 184 """ 185 <?xml version="1.0" encoding="utf-8"?> 186 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="minimal"> 187 <application 188 android:name="${appClassName ?: "android.app.Application"}" 189 android:theme="@style/Theme.AppCompat.Light.DarkActionBar"> 190 ${activities.joinToString(separator = "\n")} 191 </application> 192 </manifest> 193 """.trimIndent() 194 ) 195 } 196 } 197 createRunnernull198 private fun createRunner() = GradleRunner.create() 199 .withProjectDir(tempFolder.root) 200 .withArguments(listOf("--stacktrace", "assembleDebug") + additionalTasks) 201 .withPluginClasspath() 202 // .withDebug(true) // Add this line to enable attaching a debugger to the gradle test invocation 203 .forwardOutput() 204 205 // Data class representing a Gradle Test run result. 206 data class Result( 207 private val projectRoot: File, 208 private val buildResult: BuildResult 209 ) { 210 211 val tasks: List<BuildTask> get() = buildResult.tasks 212 213 // Finds a task by name. 214 fun getTask(name: String) = buildResult.task(name) ?: error("Task '$name' not found.") 215 216 // Gets the full build output. 217 fun getOutput() = buildResult.output 218 219 // Finds a transformed file. The srcFilePath is relative to the app's package. 220 fun getTransformedFile(srcFilePath: String): File { 221 val parentDir = 222 File(projectRoot, "build/intermediates/asm_instrumented_project_classes/debug") 223 return File(parentDir, srcFilePath).also { 224 if (!it.exists()) { 225 error("Unable to find transformed class ${it.path}") 226 } 227 } 228 } 229 } 230 } 231