xref: /aosp_15_r20/external/dagger2/java/dagger/hilt/android/plugin/main/src/test/kotlin/IncrementalProcessorTest.kt (revision f585d8a307d0621d6060bd7e80091fdcbf94fe27)
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 
17 import com.google.common.truth.Expect
18 import java.io.File
19 import org.gradle.testkit.runner.BuildResult
20 import org.gradle.testkit.runner.GradleRunner
21 import org.gradle.testkit.runner.TaskOutcome
22 import org.junit.Before
23 import org.junit.Rule
24 import org.junit.Test
25 import org.junit.rules.TemporaryFolder
26 import org.junit.runner.RunWith
27 import org.junit.runners.Parameterized
28 
29 /**
30  * Tests to verify Gradle annotation processor incremental compilation.
31  *
32  * To run these tests first deploy artifacts to local maven via util/install-local-snapshot.sh.
33  */
34 @RunWith(Parameterized::class)
35 class IncrementalProcessorTest(private val incapMode: String) {
36 
37   @get:Rule val testProjectDir = TemporaryFolder()
38 
39   @get:Rule val expect: Expect = Expect.create()
40 
41   // Original source files
42   private lateinit var srcApp: File
43   private lateinit var srcActivity1: File
44   private lateinit var srcActivity2: File
45   private lateinit var srcModule1: File
46   private lateinit var srcModule2: File
47   private lateinit var srcTest1: File
48   private lateinit var srcTest2: File
49 
50   // Generated source files
51   private lateinit var genHiltApp: File
52   private lateinit var genHiltActivity1: File
53   private lateinit var genHiltActivity2: File
54   private lateinit var genAppInjector: File
55   private lateinit var genActivityInjector1: File
56   private lateinit var genActivityInjector2: File
57   private lateinit var genAppInjectorDeps: File
58   private lateinit var genActivityInjectorDeps1: File
59   private lateinit var genActivityInjectorDeps2: File
60   private lateinit var genModuleDeps1: File
61   private lateinit var genModuleDeps2: File
62   private lateinit var genComponentTreeDeps: File
63   private lateinit var genHiltComponents: File
64   private lateinit var genDaggerHiltApplicationComponent: File
65   private lateinit var genTest1ComponentTreeDeps: File
66   private lateinit var genTest2ComponentTreeDeps: File
67   private lateinit var genTest1HiltComponents: File
68   private lateinit var genTest2HiltComponents: File
69   private lateinit var genTest1DaggerHiltApplicationComponent: File
70   private lateinit var genTest2DaggerHiltApplicationComponent: File
71 
72   // Compiled classes
73   private lateinit var classSrcApp: File
74   private lateinit var classSrcActivity1: File
75   private lateinit var classSrcActivity2: File
76   private lateinit var classSrcModule1: File
77   private lateinit var classSrcModule2: File
78   private lateinit var classSrcTest1: File
79   private lateinit var classSrcTest2: File
80   private lateinit var classGenHiltApp: File
81   private lateinit var classGenHiltActivity1: File
82   private lateinit var classGenHiltActivity2: File
83   private lateinit var classGenAppInjector: File
84   private lateinit var classGenActivityInjector1: File
85   private lateinit var classGenActivityInjector2: File
86   private lateinit var classGenAppInjectorDeps: File
87   private lateinit var classGenActivityInjectorDeps1: File
88   private lateinit var classGenActivityInjectorDeps2: File
89   private lateinit var classGenModuleDeps1: File
90   private lateinit var classGenModuleDeps2: File
91   private lateinit var classGenComponentTreeDeps: File
92   private lateinit var classGenHiltComponents: File
93   private lateinit var classGenDaggerHiltApplicationComponent: File
94   private lateinit var classGenTest1ComponentTreeDeps: File
95   private lateinit var classGenTest2ComponentTreeDeps: File
96   private lateinit var classGenTest1HiltComponents: File
97   private lateinit var classGenTest2HiltComponents: File
98   private lateinit var classGenTest1DaggerHiltApplicationComponent: File
99   private lateinit var classGenTest2DaggerHiltApplicationComponent: File
100 
101   // Timestamps of files
102   private lateinit var fileToTimestampMap: Map<File, Long>
103 
104   // Sets of files that have changed/not changed/deleted
105   private lateinit var changedFiles: Set<File>
106   private lateinit var unchangedFiles: Set<File>
107   private lateinit var deletedFiles: Set<File>
108 
109   private val compileTaskName =
110     if (incapMode == ISOLATING_MODE) {
111       ":hiltJavaCompileDebug"
112     } else {
113       ":compileDebugJavaWithJavac"
114     }
115   private val testCompileTaskName =
116     if (incapMode == ISOLATING_MODE) {
117       ":hiltJavaCompileDebugUnitTest"
118     } else {
119       ":compileDebugUnitTestJavaWithJavac"
120     }
121   private val aggregatingTaskName = ":hiltAggregateDepsDebug"
122   private val testAggregatingTaskName = ":hiltAggregateDepsDebugUnitTest"
123 
124   @Before
125   fun setup() {
126     val projectRoot = testProjectDir.root
127     // copy test project
128     File("src/test/data/simple-project").copyRecursively(projectRoot)
129 
130     // set up build file
131     File(projectRoot, "build.gradle")
132       .writeText(
133         """
134       buildscript {
135         repositories {
136           google()
137           mavenCentral()
138         }
139         dependencies {
140           classpath 'com.android.tools.build:gradle:7.1.2'
141         }
142       }
143 
144       plugins {
145         id 'com.android.application'
146         id 'com.google.dagger.hilt.android'
147       }
148 
149       android {
150         compileSdkVersion 33
151         buildToolsVersion "33.0.0"
152 
153         defaultConfig {
154           applicationId "hilt.simple"
155           minSdkVersion 21
156           targetSdkVersion 33
157           javaCompileOptions {
158             annotationProcessorOptions {
159                 arguments += ["dagger.hilt.shareTestComponents" : "true"]
160             }
161           }
162         }
163 
164         compileOptions {
165             sourceCompatibility JavaVersion.VERSION_11
166             targetCompatibility JavaVersion.VERSION_11
167         }
168       }
169 
170       repositories {
171         mavenLocal()
172         google()
173         mavenCentral()
174       }
175 
176       dependencies {
177         implementation 'androidx.appcompat:appcompat:1.1.0'
178         implementation 'com.google.dagger:dagger:LOCAL-SNAPSHOT'
179         annotationProcessor 'com.google.dagger:dagger-compiler:LOCAL-SNAPSHOT'
180         implementation 'com.google.dagger:hilt-android:LOCAL-SNAPSHOT'
181         annotationProcessor 'com.google.dagger:hilt-compiler:LOCAL-SNAPSHOT'
182 
183         testImplementation 'junit:junit:4.12'
184         testImplementation 'androidx.test.ext:junit:1.1.3'
185         testImplementation 'androidx.test:runner:1.4.0'
186         testImplementation 'org.robolectric:robolectric:4.4'
187         testImplementation 'com.google.dagger:hilt-android-testing:LOCAL-SNAPSHOT'
188         testAnnotationProcessor 'com.google.dagger:hilt-compiler:LOCAL-SNAPSHOT'
189       }
190 
191       hilt {
192         enableAggregatingTask = ${if (incapMode == ISOLATING_MODE) "true" else "false"}
193       }
194       """
195           .trimIndent()
196       )
197 
198     // Compute directory paths
199     val defaultGenSrcDir = "build/generated/ap_generated_sources/debug/out/"
200     fun getComponentTreeDepsGenSrcDir(variant: String) =
201       if (incapMode == ISOLATING_MODE) {
202         "build/generated/hilt/component_trees/$variant/"
203       } else {
204         "build/generated/ap_generated_sources/$variant/out/"
205       }
206     val componentTreeDepsGenSrcDir = getComponentTreeDepsGenSrcDir("debug")
207     val testComponentTreeDepsGenSrcDir = getComponentTreeDepsGenSrcDir("debugUnitTest")
208     fun getRootGenSrcDir(variant: String) =
209       if (incapMode == ISOLATING_MODE) {
210         "build/generated/hilt/component_sources/$variant/"
211       } else {
212         "build/generated/ap_generated_sources/$variant/out/"
213       }
214     val rootGenSrcDir = getRootGenSrcDir("debug")
215     val testRootGenSrcDir = getRootGenSrcDir("debugUnitTest")
216     val defaultClassesDir = "build/intermediates/javac/debug/classes"
217     val testDefaultClassesDir = "build/intermediates/javac/debugUnitTest/classes"
218     fun getRootClassesDir(variant: String) =
219       if (incapMode == ISOLATING_MODE) {
220         "build/intermediates/hilt/component_classes/$variant/"
221       } else {
222         "build/intermediates/javac/$variant/classes"
223       }
224     val rootClassesDir = getRootClassesDir("debug")
225     val testRootClassesDir = getRootClassesDir("debugUnitTest")
226 
227     // Compute file paths
228     srcApp = File(projectRoot, "$MAIN_SRC_DIR/simple/SimpleApp.java")
229     srcActivity1 = File(projectRoot, "$MAIN_SRC_DIR/simple/Activity1.java")
230     srcActivity2 = File(projectRoot, "$MAIN_SRC_DIR/simple/Activity2.java")
231     srcModule1 = File(projectRoot, "$MAIN_SRC_DIR/simple/Module1.java")
232     srcModule2 = File(projectRoot, "$MAIN_SRC_DIR/simple/Module2.java")
233     srcTest1 = File(projectRoot, "$TEST_SRC_DIR/simple/Test1.java")
234     srcTest2 = File(projectRoot, "$TEST_SRC_DIR/simple/Test2.java")
235 
236     genHiltApp = File(projectRoot, "$rootGenSrcDir/simple/Hilt_SimpleApp.java")
237     genHiltActivity1 = File(projectRoot, "$defaultGenSrcDir/simple/Hilt_Activity1.java")
238     genHiltActivity2 = File(projectRoot, "$defaultGenSrcDir/simple/Hilt_Activity2.java")
239     genAppInjector = File(projectRoot, "$defaultGenSrcDir/simple/SimpleApp_GeneratedInjector.java")
240     genActivityInjector1 =
241       File(projectRoot, "$defaultGenSrcDir/simple/Activity1_GeneratedInjector.java")
242     genActivityInjector2 =
243       File(projectRoot, "$defaultGenSrcDir/simple/Activity2_GeneratedInjector.java")
244     genAppInjectorDeps =
245       File(
246         projectRoot,
247         "$defaultGenSrcDir/hilt_aggregated_deps/_simple_SimpleApp_GeneratedInjector.java"
248       )
249     genActivityInjectorDeps1 =
250       File(
251         projectRoot,
252         "$defaultGenSrcDir/hilt_aggregated_deps/_simple_Activity1_GeneratedInjector.java"
253       )
254     genActivityInjectorDeps2 =
255       File(
256         projectRoot,
257         "$defaultGenSrcDir/hilt_aggregated_deps/_simple_Activity2_GeneratedInjector.java"
258       )
259     genModuleDeps1 =
260       File(projectRoot, "$defaultGenSrcDir/hilt_aggregated_deps/_simple_Module1.java")
261     genModuleDeps2 =
262       File(projectRoot, "$defaultGenSrcDir/hilt_aggregated_deps/_simple_Module2.java")
263     genComponentTreeDeps =
264       File(projectRoot, "$componentTreeDepsGenSrcDir/simple/SimpleApp_ComponentTreeDeps.java")
265     genHiltComponents = File(projectRoot, "$rootGenSrcDir/simple/SimpleApp_HiltComponents.java")
266     genDaggerHiltApplicationComponent =
267       File(projectRoot, "$rootGenSrcDir/simple/DaggerSimpleApp_HiltComponents_SingletonC.java")
268     genTest1ComponentTreeDeps =
269       File(
270         projectRoot,
271         testComponentTreeDepsGenSrcDir +
272           "/dagger/hilt/android/internal/testing/root/Test1_ComponentTreeDeps.java"
273       )
274     genTest2ComponentTreeDeps =
275       File(
276         projectRoot,
277         testComponentTreeDepsGenSrcDir +
278           "/dagger/hilt/android/internal/testing/root/Test2_ComponentTreeDeps.java"
279       )
280     genTest1HiltComponents =
281       File(
282         projectRoot,
283         "$testRootGenSrcDir/dagger/hilt/android/internal/testing/root/Test1_HiltComponents.java"
284       )
285     genTest2HiltComponents =
286       File(
287         projectRoot,
288         "$testRootGenSrcDir/dagger/hilt/android/internal/testing/root/Test2_HiltComponents.java"
289       )
290     genTest1DaggerHiltApplicationComponent =
291       File(
292         projectRoot,
293         testRootGenSrcDir +
294           "/dagger/hilt/android/internal/testing/root/DaggerTest1_HiltComponents_SingletonC.java"
295       )
296     genTest2DaggerHiltApplicationComponent =
297       File(
298         projectRoot,
299         testRootGenSrcDir +
300           "/dagger/hilt/android/internal/testing/root/DaggerTest2_HiltComponents_SingletonC.java"
301       )
302 
303     classSrcApp = File(projectRoot, "$defaultClassesDir/simple/SimpleApp.class")
304     classSrcActivity1 = File(projectRoot, "$defaultClassesDir/simple/Activity1.class")
305     classSrcActivity2 = File(projectRoot, "$defaultClassesDir/simple/Activity2.class")
306     classSrcModule1 = File(projectRoot, "$defaultClassesDir/simple/Module1.class")
307     classSrcModule2 = File(projectRoot, "$defaultClassesDir/simple/Module2.class")
308     classSrcTest1 = File(projectRoot, "$testDefaultClassesDir/simple/Test1.class")
309     classSrcTest2 = File(projectRoot, "$testDefaultClassesDir/simple/Test2.class")
310     classGenHiltApp = File(projectRoot, "$rootClassesDir/simple/Hilt_SimpleApp.class")
311     classGenHiltActivity1 = File(projectRoot, "$defaultClassesDir/simple/Hilt_Activity1.class")
312     classGenHiltActivity2 = File(projectRoot, "$defaultClassesDir/simple/Hilt_Activity2.class")
313     classGenAppInjector =
314       File(projectRoot, "$defaultClassesDir/simple/SimpleApp_GeneratedInjector.class")
315     classGenActivityInjector1 =
316       File(projectRoot, "$defaultClassesDir/simple/Activity1_GeneratedInjector.class")
317     classGenActivityInjector2 =
318       File(projectRoot, "$defaultClassesDir/simple/Activity2_GeneratedInjector.class")
319     classGenAppInjectorDeps =
320       File(
321         projectRoot,
322         "$defaultClassesDir/hilt_aggregated_deps/_simple_SimpleApp_GeneratedInjector.class"
323       )
324     classGenActivityInjectorDeps1 =
325       File(
326         projectRoot,
327         "$defaultClassesDir/hilt_aggregated_deps/_simple_Activity1_GeneratedInjector.class"
328       )
329     classGenActivityInjectorDeps2 =
330       File(
331         projectRoot,
332         "$defaultClassesDir/hilt_aggregated_deps/_simple_Activity2_GeneratedInjector.class"
333       )
334     classGenModuleDeps1 =
335       File(projectRoot, "$defaultClassesDir/hilt_aggregated_deps/_simple_Module1.class")
336     classGenModuleDeps2 =
337       File(projectRoot, "$defaultClassesDir/hilt_aggregated_deps/_simple_Module2.class")
338     classGenComponentTreeDeps =
339       File(projectRoot, "$rootClassesDir/simple/SimpleApp_ComponentTreeDeps.class")
340     classGenHiltComponents =
341       File(projectRoot, "$rootClassesDir/simple/SimpleApp_HiltComponents.class")
342     classGenDaggerHiltApplicationComponent =
343       File(projectRoot, "$rootClassesDir/simple/DaggerSimpleApp_HiltComponents_SingletonC.class")
344     classGenTest1ComponentTreeDeps =
345       File(
346         projectRoot,
347         testRootClassesDir +
348           "/dagger/hilt/android/internal/testing/root/Test1_ComponentTreeDeps.class"
349       )
350     classGenTest2ComponentTreeDeps =
351       File(
352         projectRoot,
353         testRootClassesDir +
354           "/dagger/hilt/android/internal/testing/root/Test2_ComponentTreeDeps.class"
355       )
356     classGenTest1HiltComponents =
357       File(
358         projectRoot,
359         "$testRootClassesDir/dagger/hilt/android/internal/testing/root/Test1_HiltComponents.class"
360       )
361     classGenTest2HiltComponents =
362       File(
363         projectRoot,
364         "$testRootClassesDir/dagger/hilt/android/internal/testing/root/Test2_HiltComponents.class"
365       )
366     classGenTest1DaggerHiltApplicationComponent =
367       File(
368         projectRoot,
369         testRootClassesDir +
370           "/dagger/hilt/android/internal/testing/root/DaggerTest1_HiltComponents_SingletonC.class"
371       )
372     classGenTest2DaggerHiltApplicationComponent =
373       File(
374         projectRoot,
375         testRootClassesDir +
376           "/dagger/hilt/android/internal/testing/root/DaggerTest2_HiltComponents_SingletonC.class"
377       )
378   }
379 
380   @Test
381   fun firstFullBuild() {
382     // This test verifies the results of the first full (non-incremental) build. The other tests
383     // verify the results of the second incremental build based on different change scenarios.
384     val result = runFullBuild()
385     expect.that(result.task(compileTaskName)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
386 
387     // Check annotation processing outputs
388     assertFilesExist(
389       listOf(
390         genHiltApp,
391         genHiltActivity1,
392         genHiltActivity2,
393         genAppInjector,
394         genActivityInjector1,
395         genActivityInjector2,
396         genAppInjectorDeps,
397         genActivityInjectorDeps1,
398         genActivityInjectorDeps2,
399         genModuleDeps1,
400         genModuleDeps2,
401         genComponentTreeDeps,
402         genHiltComponents,
403         genDaggerHiltApplicationComponent
404       )
405     )
406 
407     // Check compilation outputs
408     assertFilesExist(
409       listOf(
410         classSrcApp,
411         classSrcActivity1,
412         classSrcActivity2,
413         classSrcModule1,
414         classSrcModule2,
415         classGenHiltApp,
416         classGenHiltActivity1,
417         classGenHiltActivity2,
418         classGenAppInjector,
419         classGenActivityInjector1,
420         classGenActivityInjector2,
421         classGenAppInjectorDeps,
422         classGenActivityInjectorDeps1,
423         classGenActivityInjectorDeps2,
424         classGenModuleDeps1,
425         classGenModuleDeps2,
426         classGenComponentTreeDeps,
427         classGenHiltComponents,
428         classGenDaggerHiltApplicationComponent
429       )
430     )
431   }
432 
433   @Test
434   fun changeActivitySource_addPublicMethod() {
435     runFullBuild()
436     val componentTreeDepsFullBuild = genComponentTreeDeps.readText(Charsets.UTF_8)
437 
438     // Change Activity 1 source
439     searchAndReplace(
440       srcActivity1,
441       "// Insert-change",
442       """
443       @Override
444       public void onResume() {
445         super.onResume();
446       }
447       """
448         .trimIndent()
449     )
450 
451     val result = runIncrementalBuild()
452     expect.that(result.task(compileTaskName)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
453 
454     // Check annotation processing outputs
455     // * Only activity 1 sources are re-generated, isolation in modules and from other activities
456     val regeneratedSourceFiles =
457       if (incapMode == ISOLATING_MODE) {
458         // * Aggregating task did not run, no change in deps
459         expect.that(result.task(aggregatingTaskName)!!.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
460         // * Components are re-generated due to a recompilation of a dep
461         listOf(
462           genHiltApp, // Re-gen because components got re-gen
463           genHiltActivity1,
464           genActivityInjector1,
465           genActivityInjectorDeps1,
466           genHiltComponents,
467           genDaggerHiltApplicationComponent
468         )
469       } else {
470         // * Root classes along with components are always re-generated (aggregated processor)
471         listOf(
472           genHiltApp,
473           genHiltActivity1,
474           genAppInjector,
475           genActivityInjector1,
476           genAppInjectorDeps,
477           genActivityInjectorDeps1,
478           genComponentTreeDeps,
479           genHiltComponents,
480           genDaggerHiltApplicationComponent
481         )
482       }
483     assertChangedFiles(FileType.JAVA, regeneratedSourceFiles)
484 
485     val componentTreeDepsIncrementalBuild = genComponentTreeDeps.readText(Charsets.UTF_8)
486     expect
487       .withMessage("Full build")
488       .that(componentTreeDepsFullBuild)
489       .isEqualTo(componentTreeDepsIncrementalBuild)
490 
491     // Check compilation outputs
492     // * Gen sources from activity 1 are re-compiled
493     val recompiledClassFiles =
494       if (incapMode == ISOLATING_MODE) {
495         listOf(
496           classSrcActivity1,
497           classGenHiltApp,
498           classGenHiltActivity1,
499           classGenActivityInjector1,
500           classGenActivityInjectorDeps1,
501           classGenHiltComponents,
502           classGenDaggerHiltApplicationComponent,
503         )
504       } else {
505         // * All aggregating processor gen sources are re-compiled
506         listOf(
507           classSrcActivity1,
508           classGenHiltApp,
509           classGenHiltActivity1,
510           classGenAppInjector,
511           classGenActivityInjector1,
512           classGenAppInjectorDeps,
513           classGenActivityInjectorDeps1,
514           classGenHiltComponents,
515           classGenComponentTreeDeps,
516           classGenDaggerHiltApplicationComponent
517         )
518       }
519     assertChangedFiles(FileType.CLASS, recompiledClassFiles)
520   }
521 
522   @Test
523   fun changeActivitySource_addPrivateMethod() {
524     runFullBuild()
525     val componentTreeDepsFullBuild = genComponentTreeDeps.readText(Charsets.UTF_8)
526 
527     // Change Activity 1 source
528     searchAndReplace(
529       srcActivity1,
530       "// Insert-change",
531       """
532       private void foo() { }
533       """
534         .trimIndent()
535     )
536 
537     val result = runIncrementalBuild()
538     val expectedOutcome =
539       if (incapMode == ISOLATING_MODE) {
540         // In isolating mode, changes that do not affect ABI will not cause re-compilation.
541         TaskOutcome.UP_TO_DATE
542       } else {
543         TaskOutcome.SUCCESS
544       }
545     expect.that(result.task(compileTaskName)!!.outcome).isEqualTo(expectedOutcome)
546 
547     // Check annotation processing outputs
548     // * Only activity 1 sources are re-generated, isolation in modules and from other activities
549     val regeneratedSourceFiles =
550       if (incapMode == ISOLATING_MODE) {
551         // * Aggregating task did not run, no change in deps
552         expect.that(result.task(aggregatingTaskName)!!.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
553         listOf(
554           genHiltActivity1,
555           genActivityInjector1,
556           genActivityInjectorDeps1,
557         )
558       } else {
559         // * Root classes along with components are always re-generated (aggregated processor)
560         listOf(
561           genHiltApp,
562           genHiltActivity1,
563           genAppInjector,
564           genActivityInjector1,
565           genAppInjectorDeps,
566           genActivityInjectorDeps1,
567           genComponentTreeDeps,
568           genHiltComponents,
569           genDaggerHiltApplicationComponent
570         )
571       }
572     assertChangedFiles(FileType.JAVA, regeneratedSourceFiles)
573 
574     val componentTreeDepsIncrementalBuild = genComponentTreeDeps.readText(Charsets.UTF_8)
575     expect
576       .withMessage("Full build")
577       .that(componentTreeDepsFullBuild)
578       .isEqualTo(componentTreeDepsIncrementalBuild)
579 
580     // Check compilation outputs
581     // * Gen sources from activity 1 are re-compiled
582     val recompiledClassFiles =
583       if (incapMode == ISOLATING_MODE) {
584         listOf(
585           classSrcActivity1,
586           classGenHiltActivity1,
587           classGenActivityInjector1,
588           classGenActivityInjectorDeps1
589         )
590       } else {
591         // * All aggregating processor gen sources are re-compiled
592         listOf(
593           classSrcActivity1,
594           classGenHiltApp,
595           classGenHiltActivity1,
596           classGenAppInjector,
597           classGenActivityInjector1,
598           classGenAppInjectorDeps,
599           classGenActivityInjectorDeps1,
600           classGenComponentTreeDeps,
601           classGenHiltComponents,
602           classGenDaggerHiltApplicationComponent
603         )
604       }
605     assertChangedFiles(FileType.CLASS, recompiledClassFiles)
606   }
607 
608   @Test
609   fun changeModuleSource() {
610     runFullBuild()
611     val componentTreeDepsFullBuild = genComponentTreeDeps.readText(Charsets.UTF_8)
612 
613     // Change Module 1 source
614     searchAndReplace(
615       srcModule1,
616       "// Insert-change",
617       """
618       @Provides
619       static double provideDouble() {
620         return 10.10;
621       }
622       """
623         .trimIndent()
624     )
625 
626     val result = runIncrementalBuild()
627     expect.that(result.task(compileTaskName)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
628 
629     // Check annotation processing outputs
630     // * Only module 1 sources are re-generated, isolation from other modules
631     val regeneratedSourceFiles =
632       if (incapMode == ISOLATING_MODE) {
633         // * Aggregating task did not run, no change in deps
634         expect.that(result.task(aggregatingTaskName)!!.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
635         // * Components are re-generated due to a recompilation of a dep
636         listOf(
637           genHiltApp, // Re-generated because components got re-generated
638           genModuleDeps1,
639           genHiltComponents,
640           genDaggerHiltApplicationComponent
641         )
642       } else {
643         // * Root classes along with components are always re-generated (aggregated processor)
644         listOf(
645           genHiltApp,
646           genAppInjector,
647           genAppInjectorDeps,
648           genModuleDeps1,
649           genComponentTreeDeps,
650           genHiltComponents,
651           genDaggerHiltApplicationComponent
652         )
653       }
654     assertChangedFiles(FileType.JAVA, regeneratedSourceFiles)
655 
656     val componentTreeDepsIncrementalBuild = genComponentTreeDeps.readText(Charsets.UTF_8)
657     expect
658       .withMessage("Full build")
659       .that(componentTreeDepsFullBuild)
660       .isEqualTo(componentTreeDepsIncrementalBuild)
661 
662     // Check compilation outputs
663     // * Gen sources from module 1 are re-compiled
664     val recompiledClassFiles =
665       if (incapMode == ISOLATING_MODE) {
666         listOf(
667           classSrcModule1,
668           classGenHiltApp,
669           classGenModuleDeps1,
670           classGenHiltComponents,
671           classGenDaggerHiltApplicationComponent
672         )
673       } else {
674         // * All aggregating processor gen sources are re-compiled
675         listOf(
676           classSrcModule1,
677           classGenHiltApp,
678           classGenAppInjector,
679           classGenAppInjectorDeps,
680           classGenModuleDeps1,
681           classGenComponentTreeDeps,
682           classGenHiltComponents,
683           classGenDaggerHiltApplicationComponent
684         )
685       }
686     assertChangedFiles(FileType.CLASS, recompiledClassFiles)
687   }
688 
689   @Test
690   fun changeAppSource() {
691     runFullBuild()
692     val componentTreeDepsFullBuild = genComponentTreeDeps.readText(Charsets.UTF_8)
693 
694     // Change Application source
695     searchAndReplace(
696       srcApp,
697       "// Insert-change",
698       """
699       @Override
700       public void onCreate() {
701         super.onCreate();
702       }
703       """
704         .trimIndent()
705     )
706 
707     val result = runIncrementalBuild()
708     expect.that(result.task(compileTaskName)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
709 
710     // Check annotation processing outputs
711     // * No modules or activities (or any other non-root) classes should be generated
712     val regeneratedSourceFiles =
713       if (incapMode == ISOLATING_MODE) {
714         // * Aggregating task did not run, no change in deps
715         expect.that(result.task(aggregatingTaskName)!!.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
716         // * Components are re-generated due to a recompilation of a dep
717         listOf(
718           genHiltApp, // Re-generated because components got re-generated
719           genAppInjector,
720           genAppInjectorDeps,
721           genHiltComponents,
722           genDaggerHiltApplicationComponent
723         )
724       } else {
725         // * Root classes along with components are always re-generated (aggregated processor)
726         listOf(
727           genHiltApp,
728           genAppInjector,
729           genAppInjectorDeps,
730           genComponentTreeDeps,
731           genHiltComponents,
732           genDaggerHiltApplicationComponent
733         )
734       }
735     assertChangedFiles(FileType.JAVA, regeneratedSourceFiles)
736 
737     val componentTreeDepsIncrementalBuild = genComponentTreeDeps.readText(Charsets.UTF_8)
738     expect
739       .withMessage("Full build")
740       .that(componentTreeDepsFullBuild)
741       .isEqualTo(componentTreeDepsIncrementalBuild)
742 
743     // Check compilation outputs
744     val recompiledClassFiles =
745       if (incapMode == ISOLATING_MODE) {
746         listOf(
747           classSrcApp,
748           classGenHiltApp,
749           classGenAppInjector,
750           classGenAppInjectorDeps,
751           classGenHiltComponents,
752           classGenDaggerHiltApplicationComponent
753         )
754       } else {
755         // * All aggregating processor gen sources are re-compiled
756         listOf(
757           classSrcApp,
758           classGenHiltApp,
759           classGenAppInjector,
760           classGenAppInjectorDeps,
761           classGenComponentTreeDeps,
762           classGenHiltComponents,
763           classGenDaggerHiltApplicationComponent
764         )
765       }
766     assertChangedFiles(FileType.CLASS, recompiledClassFiles)
767   }
768 
769   @Test
770   fun deleteActivitySource() {
771     runFullBuild()
772 
773     srcActivity2.delete()
774 
775     val result = runIncrementalBuild()
776     expect.that(result.task(compileTaskName)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
777 
778     // Check annotation processing outputs
779     // * All related gen classes from activity 2 should be deleted
780     // * Unrelated activities and modules are in isolation and should be unchanged
781     // * Root classes along with components are always re-generated (aggregated processor)
782     assertDeletedFiles(listOf(genHiltActivity2, genActivityInjector2, genActivityInjectorDeps2))
783     val regeneratedSourceFiles =
784       if (incapMode == ISOLATING_MODE) {
785         // * Aggregating task ran due to a change in dep
786         expect.that(result.task(aggregatingTaskName)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
787         // * Components are re-generated since there was a change in dep
788         listOf(
789           genHiltApp, // Re-generated because components got re-generated
790           genComponentTreeDeps,
791           genHiltComponents,
792           genDaggerHiltApplicationComponent
793         )
794       } else {
795         listOf(
796           genHiltApp,
797           genAppInjector,
798           genAppInjectorDeps,
799           genComponentTreeDeps,
800           genHiltComponents,
801           genDaggerHiltApplicationComponent
802         )
803       }
804     assertChangedFiles(FileType.JAVA, regeneratedSourceFiles)
805 
806     // Check compilation outputs
807     // * All compiled classes from activity 2 should be deleted
808     // * Unrelated activities and modules are in isolation and should be unchanged
809     assertDeletedFiles(
810       listOf(
811         classSrcActivity2,
812         classGenHiltActivity2,
813         classGenActivityInjector2,
814         classGenActivityInjectorDeps2
815       )
816     )
817     val recompiledClassFiles =
818       if (incapMode == ISOLATING_MODE) {
819         listOf(
820           classGenHiltApp,
821           classGenComponentTreeDeps,
822           classGenHiltComponents,
823           classGenDaggerHiltApplicationComponent
824         )
825       } else {
826         listOf(
827           classGenHiltApp,
828           classGenAppInjector,
829           classGenAppInjectorDeps,
830           classGenComponentTreeDeps,
831           classGenHiltComponents,
832           classGenDaggerHiltApplicationComponent
833         )
834       }
835     assertChangedFiles(FileType.CLASS, recompiledClassFiles)
836   }
837 
838   @Test
839   fun deleteModuleSource() {
840     runFullBuild()
841 
842     srcModule2.delete()
843 
844     val result = runIncrementalBuild()
845     expect.that(result.task(compileTaskName)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
846 
847     // Check annotation processing outputs
848     // * All related gen classes from module 2 should be deleted
849     // * Unrelated activities and modules are in isolation and should be unchanged
850 
851     assertDeletedFiles(listOf(genModuleDeps2))
852     val regeneratedSourceFiles =
853       if (incapMode == ISOLATING_MODE) {
854         // * Aggregating task ran due to a change in dep
855         expect.that(result.task(aggregatingTaskName)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
856         // * Components are re-generated since there was a change in dep
857         listOf(
858           genHiltApp, // Re-generated because components got re-generated
859           genComponentTreeDeps,
860           genHiltComponents,
861           genDaggerHiltApplicationComponent
862         )
863       } else {
864         // * Root classes along with components are always re-generated (aggregated processor)
865         listOf(
866           genHiltApp,
867           genAppInjector,
868           genAppInjectorDeps,
869           genComponentTreeDeps,
870           genHiltComponents,
871           genDaggerHiltApplicationComponent
872         )
873       }
874     assertChangedFiles(FileType.JAVA, regeneratedSourceFiles)
875 
876     // Check compilation outputs
877     // * All compiled classes from module 2 should be deleted
878     // * Unrelated activities and modules are in isolation and should be unchanged
879     assertDeletedFiles(listOf(classSrcModule2, classGenModuleDeps2))
880     val recompiledClassFiles =
881       if (incapMode == ISOLATING_MODE) {
882         listOf(
883           classGenHiltApp,
884           classGenComponentTreeDeps,
885           classGenHiltComponents,
886           classGenDaggerHiltApplicationComponent
887         )
888       } else {
889         listOf(
890           classGenHiltApp,
891           classGenAppInjector,
892           classGenAppInjectorDeps,
893           classGenComponentTreeDeps,
894           classGenHiltComponents,
895           classGenDaggerHiltApplicationComponent
896         )
897       }
898     assertChangedFiles(FileType.CLASS, recompiledClassFiles)
899   }
900 
901   @Test
902   fun addNewSource() {
903     runFullBuild()
904 
905     val newSource = File(testProjectDir.root, "$MAIN_SRC_DIR/simple/Foo.java")
906     newSource.writeText(
907       """
908         package simple;
909 
910         public class Foo { }
911       """
912         .trimIndent()
913     )
914 
915     val result = runIncrementalBuild()
916     val expectedOutcome =
917       if (incapMode == ISOLATING_MODE) {
918         // In isolating mode, component compile task does not re-compile.
919         TaskOutcome.UP_TO_DATE
920       } else {
921         TaskOutcome.SUCCESS
922       }
923     expect.that(result.task(compileTaskName)!!.outcome).isEqualTo(expectedOutcome)
924 
925     val regeneratedSourceFiles =
926       if (incapMode == ISOLATING_MODE) {
927         // * Aggregating task did not run, no change in deps
928         expect.that(result.task(aggregatingTaskName)!!.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
929         // * Non-DI related source causes no files to be generated
930         emptyList()
931       } else {
932         // * Root classes are always re-generated (aggregated processor)
933         listOf(
934           genHiltApp,
935           genAppInjector,
936           genAppInjectorDeps,
937           genComponentTreeDeps,
938           genHiltComponents,
939           genDaggerHiltApplicationComponent
940         )
941       }
942     assertChangedFiles(FileType.JAVA, regeneratedSourceFiles)
943 
944     val recompiledClassFiles =
945       if (incapMode == ISOLATING_MODE) {
946         emptyList()
947       } else {
948         listOf(
949           classGenHiltApp,
950           classGenAppInjector,
951           classGenAppInjectorDeps,
952           classGenComponentTreeDeps,
953           classGenHiltComponents,
954           classGenDaggerHiltApplicationComponent
955         )
956       }
957     assertChangedFiles(FileType.CLASS, recompiledClassFiles)
958   }
959 
960   @Test
961   fun firstTestFullBuild() {
962     val result = runFullTestBuild()
963     expect.that(result.task(testCompileTaskName)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
964 
965     assertFilesExist(
966       listOf(
967         genTest1ComponentTreeDeps,
968         genTest2ComponentTreeDeps,
969         genTest1HiltComponents,
970         genTest2HiltComponents,
971         genTest1DaggerHiltApplicationComponent,
972         genTest2DaggerHiltApplicationComponent,
973       )
974     )
975 
976     assertFilesExist(
977       listOf(
978         classSrcTest1,
979         classSrcTest2,
980         classGenTest1ComponentTreeDeps,
981         classGenTest2ComponentTreeDeps,
982         classGenTest1HiltComponents,
983         classGenTest2HiltComponents,
984         classGenTest1DaggerHiltApplicationComponent,
985         classGenTest2DaggerHiltApplicationComponent,
986       )
987     )
988   }
989 
990   @Test
991   fun changeTestSource_addPublicMethod() {
992     runFullTestBuild()
993     val test1ComponentTreeDepsFullBuild = genTest1ComponentTreeDeps.readText(Charsets.UTF_8)
994     val test2ComponentTreeDepsFullBuild = genTest2ComponentTreeDeps.readText(Charsets.UTF_8)
995 
996     // Change Test 1 source
997     searchAndReplace(
998       srcTest1,
999       "// Insert-change",
1000       """
1001       @Test
1002       public void newTest() { }
1003       """
1004         .trimIndent()
1005     )
1006 
1007     val result = runIncrementalTestBuild()
1008     expect.that(result.task(testCompileTaskName)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
1009 
1010     // Check annotation processing outputs
1011     // * Unrelated test components should be unchanged
1012 
1013     val regeneratedSourceFiles =
1014       if (incapMode == ISOLATING_MODE) {
1015         listOf(
1016           genTest1HiltComponents,
1017           genTest1DaggerHiltApplicationComponent,
1018         )
1019       } else {
1020         listOf(
1021           genTest1ComponentTreeDeps,
1022           genTest2ComponentTreeDeps,
1023           genTest1HiltComponents,
1024           genTest2HiltComponents,
1025           genTest1DaggerHiltApplicationComponent,
1026           genTest2DaggerHiltApplicationComponent,
1027         )
1028       }
1029     assertChangedFiles(FileType.JAVA, regeneratedSourceFiles)
1030 
1031     val test1ComponentTreeDepsIncrementalBuild = genTest1ComponentTreeDeps.readText(Charsets.UTF_8)
1032     val test2ComponentTreeDepsIncrementalBuild = genTest2ComponentTreeDeps.readText(Charsets.UTF_8)
1033     expect
1034       .withMessage("Full build")
1035       .that(test1ComponentTreeDepsFullBuild)
1036       .isEqualTo(test1ComponentTreeDepsIncrementalBuild)
1037     expect
1038       .withMessage("Full build")
1039       .that(test2ComponentTreeDepsFullBuild)
1040       .isEqualTo(test2ComponentTreeDepsIncrementalBuild)
1041 
1042     val recompiledClassFiles =
1043       if (incapMode == ISOLATING_MODE) {
1044         listOf(
1045           classSrcTest1,
1046           classGenTest1HiltComponents,
1047           classGenTest1DaggerHiltApplicationComponent,
1048         )
1049       } else {
1050         listOf(
1051           classSrcTest1,
1052           classGenTest1ComponentTreeDeps,
1053           classGenTest2ComponentTreeDeps,
1054           classGenTest1HiltComponents,
1055           classGenTest2HiltComponents,
1056           classGenTest1DaggerHiltApplicationComponent,
1057           classGenTest2DaggerHiltApplicationComponent,
1058         )
1059       }
1060     assertChangedFiles(FileType.CLASS, recompiledClassFiles)
1061   }
1062 
1063   @Test
1064   fun changeTestSource_addPrivateMethod() {
1065     runFullTestBuild()
1066     val test1ComponentTreeDepsFullBuild = genTest1ComponentTreeDeps.readText(Charsets.UTF_8)
1067     val test2ComponentTreeDepsFullBuild = genTest2ComponentTreeDeps.readText(Charsets.UTF_8)
1068 
1069     // Change Test 1 source
1070     searchAndReplace(
1071       srcTest1,
1072       "// Insert-change",
1073       """
1074       private void newMethod() { }
1075       """
1076         .trimIndent()
1077     )
1078 
1079     val result = runIncrementalTestBuild()
1080     val expectedOutcome =
1081       if (incapMode == ISOLATING_MODE) {
1082         // In isolating mode, changes that do not affect ABI will not cause re-compilation.
1083         TaskOutcome.UP_TO_DATE
1084       } else {
1085         TaskOutcome.SUCCESS
1086       }
1087     expect.that(result.task(testCompileTaskName)!!.outcome).isEqualTo(expectedOutcome)
1088 
1089     // Check annotation processing outputs
1090     // * Unrelated test components should be unchanged
1091 
1092     val regeneratedSourceFiles =
1093       if (incapMode == ISOLATING_MODE) {
1094         emptyList()
1095       } else {
1096         listOf(
1097           genTest1ComponentTreeDeps,
1098           genTest2ComponentTreeDeps,
1099           genTest1HiltComponents,
1100           genTest2HiltComponents,
1101           genTest1DaggerHiltApplicationComponent,
1102           genTest2DaggerHiltApplicationComponent,
1103         )
1104       }
1105     assertChangedFiles(FileType.JAVA, regeneratedSourceFiles)
1106 
1107     val test1ComponentTreeDepsIncrementalBuild = genTest1ComponentTreeDeps.readText(Charsets.UTF_8)
1108     val test2ComponentTreeDepsIncrementalBuild = genTest2ComponentTreeDeps.readText(Charsets.UTF_8)
1109     expect
1110       .withMessage("Full build")
1111       .that(test1ComponentTreeDepsFullBuild)
1112       .isEqualTo(test1ComponentTreeDepsIncrementalBuild)
1113     expect
1114       .withMessage("Full build")
1115       .that(test2ComponentTreeDepsFullBuild)
1116       .isEqualTo(test2ComponentTreeDepsIncrementalBuild)
1117 
1118     val recompiledClassFiles =
1119       if (incapMode == ISOLATING_MODE) {
1120         listOf(classSrcTest1)
1121       } else {
1122         listOf(
1123           classSrcTest1,
1124           classGenTest1ComponentTreeDeps,
1125           classGenTest2ComponentTreeDeps,
1126           classGenTest1HiltComponents,
1127           classGenTest2HiltComponents,
1128           classGenTest1DaggerHiltApplicationComponent,
1129           classGenTest2DaggerHiltApplicationComponent,
1130         )
1131       }
1132     assertChangedFiles(FileType.CLASS, recompiledClassFiles)
1133   }
1134 
1135   private fun runGradleTasks(vararg args: String): BuildResult {
1136     return GradleRunner.create()
1137       .withProjectDir(testProjectDir.root)
1138       .withArguments(*args)
1139       .withPluginClasspath()
1140       .forwardOutput()
1141       .build()
1142   }
1143 
1144   private fun runFullBuild(): BuildResult {
1145     val result = runGradleTasks(CLEAN_TASK, compileTaskName)
1146     recordTimestamps()
1147     return result
1148   }
1149 
1150   private fun runFullTestBuild(): BuildResult {
1151     runFullBuild()
1152     val result = runIncrementalTestBuild()
1153     recordTimestamps()
1154     return result
1155   }
1156 
1157   private fun runIncrementalBuild(): BuildResult {
1158     val result = runGradleTasks(compileTaskName)
1159     recordFileChanges()
1160     return result
1161   }
1162 
1163   private fun runIncrementalTestBuild(): BuildResult {
1164     val result = runGradleTasks(testCompileTaskName)
1165     recordFileChanges()
1166     return result
1167   }
1168 
1169   private fun recordTimestamps() {
1170     val files =
1171       listOf(
1172         genHiltApp,
1173         genHiltActivity1,
1174         genHiltActivity2,
1175         genAppInjector,
1176         genActivityInjector1,
1177         genActivityInjector2,
1178         genAppInjectorDeps,
1179         genActivityInjectorDeps1,
1180         genActivityInjectorDeps2,
1181         genModuleDeps1,
1182         genModuleDeps2,
1183         genComponentTreeDeps,
1184         genHiltComponents,
1185         genDaggerHiltApplicationComponent,
1186         genTest1ComponentTreeDeps,
1187         genTest2ComponentTreeDeps,
1188         genTest1HiltComponents,
1189         genTest2HiltComponents,
1190         genTest1DaggerHiltApplicationComponent,
1191         genTest2DaggerHiltApplicationComponent,
1192         classSrcApp,
1193         classSrcActivity1,
1194         classSrcActivity2,
1195         classSrcModule1,
1196         classSrcModule2,
1197         classSrcTest1,
1198         classSrcTest2,
1199         classGenHiltApp,
1200         classGenHiltActivity1,
1201         classGenHiltActivity2,
1202         classGenAppInjector,
1203         classGenActivityInjector1,
1204         classGenActivityInjector2,
1205         classGenAppInjectorDeps,
1206         classGenActivityInjectorDeps1,
1207         classGenActivityInjectorDeps2,
1208         classGenModuleDeps1,
1209         classGenModuleDeps2,
1210         classGenComponentTreeDeps,
1211         classGenHiltComponents,
1212         classGenDaggerHiltApplicationComponent,
1213         classGenTest1ComponentTreeDeps,
1214         classGenTest2ComponentTreeDeps,
1215         classGenTest1HiltComponents,
1216         classGenTest2HiltComponents,
1217         classGenTest1DaggerHiltApplicationComponent,
1218         classGenTest2DaggerHiltApplicationComponent,
1219       )
1220 
1221     fileToTimestampMap =
1222       mutableMapOf<File, Long>().apply {
1223         for (file in files) {
1224           this[file] = file.lastModified()
1225         }
1226       }
1227   }
1228 
1229   private fun recordFileChanges() {
1230     changedFiles =
1231       fileToTimestampMap
1232         .filter { (file, previousTimestamp) ->
1233           file.exists() && file.lastModified() != previousTimestamp
1234         }
1235         .keys
1236 
1237     unchangedFiles =
1238       fileToTimestampMap
1239         .filter { (file, previousTimestamp) ->
1240           file.exists() && file.lastModified() == previousTimestamp
1241         }
1242         .keys
1243 
1244     deletedFiles = fileToTimestampMap.filter { (file, _) -> !file.exists() }.keys
1245   }
1246 
1247   private fun assertFilesExist(files: Collection<File>) {
1248     expect
1249       .withMessage("Existing files")
1250       .that(files.filter { it.exists() })
1251       .containsExactlyElementsIn(files)
1252   }
1253 
1254   private fun assertChangedFiles(type: FileType, files: Collection<File>) {
1255     expect
1256       .withMessage("Changed files")
1257       .that(changedFiles.filter { it.name.endsWith(type.extension) })
1258       .containsExactlyElementsIn(files)
1259   }
1260 
1261   private fun assertDeletedFiles(files: Collection<File>) {
1262     expect.withMessage("Deleted files").that(deletedFiles).containsAtLeastElementsIn(files)
1263   }
1264 
1265   private fun searchAndReplace(file: File, search: String, replace: String) {
1266     file.writeText(file.readText().replace(search, replace))
1267   }
1268 
1269   enum class FileType(val extension: String) {
1270     JAVA(".java"),
1271     CLASS(".class"),
1272   }
1273 
1274   companion object {
1275 
1276     @JvmStatic
1277     @Parameterized.Parameters(name = "{0}")
1278     fun parameters() = listOf(ISOLATING_MODE, AGGREGATING_MODE)
1279 
1280     private const val ISOLATING_MODE = "isolating"
1281     private const val AGGREGATING_MODE = "aggregating"
1282 
1283     private const val MAIN_SRC_DIR = "src/main/java"
1284     private const val TEST_SRC_DIR = "src/test/java"
1285     private const val CLEAN_TASK = ":clean"
1286   }
1287 }
1288