• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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