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