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