1 /*
<lambda>null2  * 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 package dagger.hilt.android.plugin
17 
18 import com.android.build.gradle.api.UnitTestVariant
19 import dagger.hilt.android.plugin.util.isClassFile
20 import dagger.hilt.android.plugin.util.isJarFile
21 import java.io.File
22 import javax.inject.Inject
23 import org.gradle.api.Action
24 import org.gradle.api.DefaultTask
25 import org.gradle.api.Project
26 import org.gradle.api.file.ConfigurableFileCollection
27 import org.gradle.api.file.DirectoryProperty
28 import org.gradle.api.file.FileCollection
29 import org.gradle.api.provider.Property
30 import org.gradle.api.tasks.Classpath
31 import org.gradle.api.tasks.OutputDirectory
32 import org.gradle.api.tasks.TaskAction
33 import org.gradle.api.tasks.TaskProvider
34 import org.gradle.api.tasks.compile.JavaCompile
35 import org.gradle.api.tasks.testing.Test
36 import org.gradle.workers.WorkAction
37 import org.gradle.workers.WorkParameters
38 import org.gradle.workers.WorkerExecutor
39 import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper
40 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
41 
42 /**
43  * Task that transform classes used by host-side unit tests. See b/37076369
44  */
45 @Suppress("UnstableApiUsage")
46 abstract class HiltTransformTestClassesTask @Inject constructor(
47   private val workerExecutor: WorkerExecutor
48 ) : DefaultTask() {
49 
50   @get:Classpath
51   abstract val compiledClasses: ConfigurableFileCollection
52 
53   @get:OutputDirectory
54   abstract val outputDir: DirectoryProperty
55 
56   internal interface Parameters : WorkParameters {
57     val name: Property<String>
58     val compiledClasses: ConfigurableFileCollection
59     val outputDir: DirectoryProperty
60   }
61 
62   abstract class WorkerAction : WorkAction<Parameters> {
63     override fun execute() {
64       val outputDir = parameters.outputDir.asFile.get()
65       outputDir.deleteRecursively()
66       outputDir.mkdirs()
67 
68       val allInputs = parameters.compiledClasses.files.toList()
69       val classTransformer = AndroidEntryPointClassTransformer(
70         taskName = parameters.name.get(),
71         allInputs = allInputs,
72         sourceRootOutputDir = outputDir,
73         copyNonTransformed = false
74       )
75       // Parse the classpath in reverse so that we respect overwrites, if it ever happens.
76       allInputs.reversed().forEach {
77         if (it.isDirectory) {
78           it.walkTopDown().forEach { file ->
79             if (file.isClassFile()) {
80               classTransformer.transformFile(file)
81             }
82           }
83         } else if (it.isJarFile()) {
84           classTransformer.transformJarContents(it)
85         }
86       }
87     }
88   }
89 
90   @TaskAction
91   fun transformClasses() {
92     workerExecutor.noIsolation().submit(WorkerAction::class.java) {
93       it.compiledClasses.from(compiledClasses)
94       it.outputDir.set(outputDir)
95       it.name.set(name)
96     }
97   }
98 
99   internal class ConfigAction(
100     private val outputDir: File,
101     private val inputClasspath: FileCollection
102   ) : Action<HiltTransformTestClassesTask> {
103     override fun execute(transformTask: HiltTransformTestClassesTask) {
104       transformTask.description = "Transforms AndroidEntryPoint annotated classes for JUnit tests."
105       transformTask.outputDir.set(outputDir)
106       transformTask.compiledClasses.from(inputClasspath)
107     }
108   }
109 
110   companion object {
111 
112     private const val TASK_PREFIX = "hiltTransformFor"
113 
114     fun create(
115       project: Project,
116       unitTestVariant: UnitTestVariant,
117       extension: HiltExtension
118     ) {
119       if (!extension.enableTransformForLocalTests) {
120         // Not enabled, nothing to do here.
121         return
122       }
123 
124       // TODO(danysantiago): Only use project compiled sources as input, and not all dependency jars
125       // Using 'null' key to obtain the full compile classpath since we are not using the
126       // registerPreJavacGeneratedBytecode() API that would have otherwise given us a key to get
127       // a classpath up to the generated bytecode associated with the key.
128       val inputClasspath =
129         project.objects.fileCollection().from(unitTestVariant.getCompileClasspath(null))
130 
131       // Find the test sources Java compile task and add its output directory into our input
132       // classpath file collection. This also makes the transform task depend on the test compile
133       // task.
134       @Suppress("UNCHECKED_CAST")
135       val testCompileTaskProvider = project.tasks.named(
136         "compile${unitTestVariant.name.capitalize()}JavaWithJavac"
137       ) as TaskProvider<JavaCompile>
138       inputClasspath.from(testCompileTaskProvider.map { it.destinationDirectory })
139 
140       // Similarly, if the Kotlin plugin is configured, find the test sources Kotlin compile task
141       // and add its output directory to our input classpath file collection.
142       project.plugins.withType(KotlinBasePluginWrapper::class.java) {
143         @Suppress("UNCHECKED_CAST")
144         val kotlinCompileTaskProvider = project.tasks.named(
145           "compile${unitTestVariant.name.capitalize()}Kotlin"
146         ) as TaskProvider<KotlinCompile>
147         inputClasspath.from(kotlinCompileTaskProvider.map { it.destinationDirectory })
148       }
149 
150       // Create and configure the transform task.
151       val outputDir =
152         project.buildDir.resolve("intermediates/hilt/${unitTestVariant.dirName}Output")
153       val hiltTransformProvider = project.tasks.register(
154         "$TASK_PREFIX${unitTestVariant.name.capitalize()}",
155         HiltTransformTestClassesTask::class.java,
156         ConfigAction(outputDir, inputClasspath)
157       )
158       // Map the transform task's output to a file collection.
159       val outputFileCollection =
160         project.objects.fileCollection().from(hiltTransformProvider.map { it.outputDir })
161 
162       // Configure test classpath by appending the transform output file collection to the start of
163       // the test classpath so they override the original ones. This also makes test task (the one
164       // that runs the tests) depend on the transform task.
165       @Suppress("UNCHECKED_CAST")
166       val testTaskProvider = project.tasks.named(
167         "test${unitTestVariant.name.capitalize()}"
168       ) as TaskProvider<Test>
169       testTaskProvider.configure {
170         it.classpath = outputFileCollection + it.classpath
171       }
172     }
173   }
174 }
175