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.GradleRunner 20 import org.junit.rules.TemporaryFolder 21 22 /** 23 * Testing utility class that sets up a simple Android project that applies the Hilt plugin. 24 */ 25 class GradleTestRunner(val tempFolder: TemporaryFolder) { 26 private val dependencies = mutableListOf<String>() 27 private val activities = mutableListOf<String>() 28 private val additionalAndroidOptions = mutableListOf<String>() 29 private val hiltOptions = mutableListOf<String>() 30 private var appClassName: String? = null 31 private var buildFile: File? = null 32 private var gradlePropertiesFile: File? = null 33 private var manifestFile: File? = null 34 35 init { 36 tempFolder.newFolder("src", "main", "java", "minimal") 37 tempFolder.newFolder("src", "main", "res") 38 } 39 40 // Adds project dependencies, e.g. "implementation <group>:<id>:<version>" addDependenciesnull41 fun addDependencies(vararg deps: String) { 42 dependencies.addAll(deps) 43 } 44 45 // Adds an <activity> tag in the project's Android Manifest, e.g. "<activity name=".Foo"/> addActivitiesnull46 fun addActivities(vararg activityElements: String) { 47 activities.addAll(activityElements) 48 } 49 50 // Adds 'android' options to the project's build.gradle, e.g. "lintOptions.checkReleaseBuilds = false" addAndroidOptionnull51 fun addAndroidOption(vararg options: String) { 52 additionalAndroidOptions.addAll(options) 53 } 54 55 // Adds 'hilt' options to the project's build.gradle, e.g. "enableExperimentalClasspathAggregation = true" addHiltOptionnull56 fun addHiltOption(vararg options: String) { 57 hiltOptions.addAll(options) 58 } 59 60 // Adds a source package to the project. The package path is relative to 'src/main/java'. addSrcPackagenull61 fun addSrcPackage(packagePath: String) { 62 File(tempFolder.root, "src/main/java/$packagePath").mkdirs() 63 } 64 65 // Adds a source file to the project. The source path is relative to 'src/main/java'. addSrcnull66 fun addSrc(srcPath: String, srcContent: String): File { 67 File(tempFolder.root, "src/main/java/${srcPath.substringBeforeLast(File.separator)}").mkdirs() 68 return tempFolder.newFile("/src/main/java/$srcPath").apply { writeText(srcContent) } 69 } 70 71 // Adds a resource file to the project. The source path is relative to 'src/main/res'. addResnull72 fun addRes(resPath: String, resContent: String): File { 73 File(tempFolder.root, "src/main/res/${resPath.substringBeforeLast(File.separator)}").mkdirs() 74 return tempFolder.newFile("/src/main/res/$resPath").apply { writeText(resContent) } 75 } 76 setAppClassNamenull77 fun setAppClassName(name: String) { 78 appClassName = name 79 } 80 81 // Executes a Gradle builds and expects it to succeed. buildnull82 fun build(): Result { 83 setupFiles() 84 return Result(tempFolder.root, createRunner().build()) 85 } 86 87 // Executes a Gradle build and expects it to fail. buildAndFailnull88 fun buildAndFail(): Result { 89 setupFiles() 90 return Result(tempFolder.root, createRunner().buildAndFail()) 91 } 92 setupFilesnull93 private fun setupFiles() { 94 writeBuildFile() 95 writeGradleProperties() 96 writeAndroidManifest() 97 } 98 writeBuildFilenull99 private fun writeBuildFile() { 100 buildFile?.delete() 101 buildFile = tempFolder.newFile("build.gradle").apply { 102 writeText( 103 """ 104 buildscript { 105 repositories { 106 google() 107 jcenter() 108 } 109 dependencies { 110 classpath 'com.android.tools.build:gradle:4.2.0-beta04' 111 } 112 } 113 114 plugins { 115 id 'com.android.application' 116 id 'dagger.hilt.android.plugin' 117 } 118 119 android { 120 compileSdkVersion 30 121 buildToolsVersion "30.0.2" 122 123 defaultConfig { 124 applicationId "plugin.test" 125 minSdkVersion 21 126 targetSdkVersion 30 127 } 128 129 compileOptions { 130 sourceCompatibility 1.8 131 targetCompatibility 1.8 132 } 133 ${additionalAndroidOptions.joinToString(separator = "\n")} 134 } 135 136 allprojects { 137 repositories { 138 mavenLocal() 139 google() 140 jcenter() 141 } 142 } 143 144 dependencies { 145 ${dependencies.joinToString(separator = "\n")} 146 } 147 148 hilt { 149 ${hiltOptions.joinToString(separator = "\n")} 150 } 151 """.trimIndent() 152 ) 153 } 154 } 155 writeGradlePropertiesnull156 private fun writeGradleProperties() { 157 gradlePropertiesFile?.delete() 158 gradlePropertiesFile = tempFolder.newFile("gradle.properties").apply { 159 writeText( 160 """ 161 android.useAndroidX=true 162 """.trimIndent() 163 ) 164 } 165 } 166 writeAndroidManifestnull167 private fun writeAndroidManifest() { 168 manifestFile?.delete() 169 manifestFile = tempFolder.newFile("/src/main/AndroidManifest.xml").apply { 170 writeText( 171 """ 172 <?xml version="1.0" encoding="utf-8"?> 173 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="minimal"> 174 <application 175 android:name="${appClassName ?: "android.app.Application"}" 176 android:theme="@style/Theme.AppCompat.Light.DarkActionBar"> 177 ${activities.joinToString(separator = "\n")} 178 </application> 179 </manifest> 180 """.trimIndent() 181 ) 182 } 183 } 184 createRunnernull185 private fun createRunner() = GradleRunner.create() 186 .withProjectDir(tempFolder.root) 187 .withArguments("assembleDebug", "--stacktrace") 188 .withPluginClasspath() 189 // .withDebug(true) // Add this line to enable attaching a debugger to the gradle test invocation 190 .forwardOutput() 191 192 // Data class representing a Gradle Test run result. 193 data class Result( 194 private val projectRoot: File, 195 private val buildResult: BuildResult 196 ) { 197 // Finds a task by name. 198 fun getTask(name: String) = buildResult.task(name) ?: error("Task '$name' not found.") 199 200 // Gets the full build output. 201 fun getOutput() = buildResult.output 202 203 // Finds a transformed file. The srcFilePath is relative to the app's package. 204 fun getTransformedFile(srcFilePath: String): File { 205 val parentDir = 206 File(projectRoot, "build/intermediates/asm_instrumented_project_classes/debug") 207 return File(parentDir, srcFilePath).also { 208 if (!it.exists()) { 209 error("Unable to find transformed class ${it.path}") 210 } 211 } 212 } 213 } 214 } 215