• 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 /**
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