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 /** Testing utility class that sets up a simple Android project that applies the Hilt plugin. */ 24 class GradleTestRunner(val tempFolder: TemporaryFolder) { 25 private val pluginClasspaths = mutableListOf<String>() 26 private val pluginIds = mutableListOf<String>() 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 val additionalClosures = mutableListOf<String>() 32 private var appClassName: String? = null 33 private var buildFile: File? = null 34 private var gradlePropertiesFile: File? = null 35 private var manifestFile: File? = null 36 private var additionalTasks = mutableListOf<String>() 37 private var isAppProject: Boolean = true 38 39 init { 40 tempFolder.newFolder("src", "main", "java", "minimal") 41 tempFolder.newFolder("src", "test", "java", "minimal") 42 tempFolder.newFolder("src", "main", "res") 43 } 44 45 // Adds a Gradle plugin classpath to the test project, 46 // e.g. "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0" addPluginClasspathnull47 fun addPluginClasspath(pluginClasspath: String) { 48 pluginClasspaths.add(pluginClasspath) 49 } 50 51 // Adds a Gradle plugin id to the test project, e.g. "kotlin-android" addPluginIdnull52 fun addPluginId(pluginId: String) { 53 pluginIds.add(pluginId) 54 } 55 56 // Adds project dependencies, e.g. "implementation <group>:<id>:<version>" addDependenciesnull57 fun addDependencies(vararg deps: String) { 58 dependencies.addAll(deps) 59 } 60 61 // Adds an <activity> tag in the project's Android Manifest, e.g. "<activity name=".Foo"/> addActivitiesnull62 fun addActivities(vararg activityElements: String) { 63 activities.addAll(activityElements) 64 } 65 66 // Adds 'android' options to the project's build.gradle, e.g. "lintOptions.checkReleaseBuilds = 67 // false" addAndroidOptionnull68 fun addAndroidOption(vararg options: String) { 69 additionalAndroidOptions.addAll(options) 70 } 71 72 // Adds 'hilt' options to the project's build.gradle, e.g. "enableExperimentalClasspathAggregation 73 // = true" addHiltOptionnull74 fun addHiltOption(vararg options: String) { 75 hiltOptions.addAll(options) 76 } 77 addAdditionalClosurenull78 fun addAdditionalClosure(closure: String) { 79 additionalClosures.add(closure) 80 } 81 82 // Adds a source package to the project. The package path is relative to 'src/main/java'. addSrcPackagenull83 fun addSrcPackage(packagePath: String) { 84 File(tempFolder.root, "src/main/java/$packagePath").mkdirs() 85 } 86 87 // Adds a source file to the project. The source path is relative to 'src/main/java'. addSrcnull88 fun addSrc(srcPath: String, srcContent: String): File { 89 File(tempFolder.root, "src/main/java/${srcPath.substringBeforeLast(File.separator)}").mkdirs() 90 return tempFolder.newFile("/src/main/java/$srcPath").apply { writeText(srcContent) } 91 } 92 93 // Adds a test source file to the project. The source path is relative to 'src/test/java'. addTestSrcnull94 fun addTestSrc(srcPath: String, srcContent: String): File { 95 File(tempFolder.root, "src/test/java/${srcPath.substringBeforeLast(File.separator)}").mkdirs() 96 return tempFolder.newFile("/src/test/java/$srcPath").apply { writeText(srcContent) } 97 } 98 99 // Adds a resource file to the project. The source path is relative to 'src/main/res'. addResnull100 fun addRes(resPath: String, resContent: String): File { 101 File(tempFolder.root, "src/main/res/${resPath.substringBeforeLast(File.separator)}").mkdirs() 102 return tempFolder.newFile("/src/main/res/$resPath").apply { writeText(resContent) } 103 } 104 setAppClassNamenull105 fun setAppClassName(name: String) { 106 appClassName = name 107 } 108 setIsAppProjectnull109 fun setIsAppProject(flag: Boolean) { 110 isAppProject = flag 111 } 112 runAdditionalTasksnull113 fun runAdditionalTasks(taskName: String) { 114 additionalTasks.add(taskName) 115 } 116 117 // Executes a Gradle builds and expects it to succeed. buildnull118 fun build(): Result { 119 setupFiles() 120 return Result(tempFolder.root, createRunner().build()) 121 } 122 123 // Executes a Gradle build and expects it to fail. buildAndFailnull124 fun buildAndFail(): Result { 125 setupFiles() 126 return Result(tempFolder.root, createRunner().buildAndFail()) 127 } 128 setupFilesnull129 private fun setupFiles() { 130 writeBuildFile() 131 writeGradleProperties() 132 writeAndroidManifest() 133 } 134 writeBuildFilenull135 private fun writeBuildFile() { 136 buildFile?.delete() 137 buildFile = 138 tempFolder.newFile("build.gradle").apply { 139 writeText( 140 """ 141 buildscript { 142 repositories { 143 google() 144 mavenCentral() 145 } 146 dependencies { 147 classpath 'com.android.tools.build:gradle:7.1.2' 148 ${pluginClasspaths.joinToString(separator = "\n") { "classpath '$it'" }} 149 } 150 } 151 152 plugins { 153 id '${ if (isAppProject) "com.android.application" else "com.android.library" }' 154 id 'com.google.dagger.hilt.android' 155 ${pluginIds.joinToString(separator = "\n") { "id '$it'" }} 156 } 157 158 android { 159 compileSdkVersion 33 160 buildToolsVersion "33.0.0" 161 162 defaultConfig { 163 ${ if (isAppProject) "applicationId \"plugin.test\"" else "" } 164 minSdkVersion 21 165 targetSdkVersion 33 166 } 167 168 compileOptions { 169 sourceCompatibility JavaVersion.VERSION_11 170 targetCompatibility JavaVersion.VERSION_11 171 } 172 ${additionalAndroidOptions.joinToString(separator = "\n")} 173 } 174 175 allprojects { 176 repositories { 177 mavenLocal() 178 google() 179 mavenCentral() 180 } 181 } 182 183 dependencies { 184 implementation(platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')) 185 ${dependencies.joinToString(separator = "\n")} 186 } 187 188 hilt { 189 ${hiltOptions.joinToString(separator = "\n")} 190 } 191 ${additionalClosures.joinToString(separator = "\n")} 192 """ 193 .trimIndent() 194 ) 195 } 196 } 197 writeGradlePropertiesnull198 private fun writeGradleProperties() { 199 gradlePropertiesFile?.delete() 200 gradlePropertiesFile = 201 tempFolder.newFile("gradle.properties").apply { 202 writeText( 203 """ 204 android.useAndroidX=true 205 // TODO(b/296583777): See if there's a better way to fix the OOM error. 206 org.gradle.jvmargs=-XX:MaxMetaspaceSize=1g 207 """ 208 .trimIndent() 209 ) 210 } 211 } 212 writeAndroidManifestnull213 private fun writeAndroidManifest() { 214 manifestFile?.delete() 215 manifestFile = 216 tempFolder.newFile("/src/main/AndroidManifest.xml").apply { 217 writeText( 218 """ 219 <?xml version="1.0" encoding="utf-8"?> 220 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="minimal"> 221 <application 222 android:name="${appClassName ?: "android.app.Application"}" 223 android:theme="@style/Theme.AppCompat.Light.DarkActionBar"> 224 ${activities.joinToString(separator = "\n")} 225 </application> 226 </manifest> 227 """ 228 .trimIndent() 229 ) 230 } 231 } 232 createRunnernull233 private fun createRunner() = 234 GradleRunner.create() 235 .withProjectDir(tempFolder.root) 236 .withArguments(listOf("--stacktrace", "assembleDebug") + additionalTasks) 237 .withPluginClasspath() 238 // .withDebug(true) // Add this line to enable attaching a debugger to the gradle test 239 // invocation 240 .forwardOutput() 241 242 // Data class representing a Gradle Test run result. 243 data class Result(private val projectRoot: File, private val buildResult: BuildResult) { 244 245 val tasks: List<BuildTask> 246 get() = buildResult.tasks 247 248 // Finds a task by name. 249 fun getTask(name: String) = buildResult.task(name) ?: error("Task '$name' not found.") 250 251 // Gets the full build output. 252 fun getOutput() = buildResult.output 253 254 // Finds a transformed file. The srcFilePath is relative to the app's package. 255 fun getTransformedFile(srcFilePath: String): File { 256 val parentDir = 257 File(projectRoot, "build/intermediates/asm_instrumented_project_classes/debug") 258 return File(parentDir, srcFilePath).also { 259 if (!it.exists()) { 260 error("Unable to find transformed class ${it.path}") 261 } 262 } 263 } 264 } 265 } 266