1 /* 2 * Copyright (C) 2018 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 package dagger.spi; 18 19 import static com.google.testing.compile.CompilationSubject.assertThat; 20 import static com.google.testing.compile.Compiler.javac; 21 22 import com.google.common.base.Joiner; 23 import com.google.common.collect.ImmutableList; 24 import com.google.testing.compile.Compilation; 25 import com.google.testing.compile.JavaFileObjects; 26 import dagger.internal.codegen.ComponentProcessor; 27 import javax.tools.JavaFileObject; 28 import javax.tools.StandardLocation; 29 import org.junit.Test; 30 import org.junit.runner.RunWith; 31 import org.junit.runners.JUnit4; 32 33 @RunWith(JUnit4.class) 34 public final class SpiPluginTest { 35 @Test moduleBinding()36 public void moduleBinding() { 37 JavaFileObject module = 38 JavaFileObjects.forSourceLines( 39 "test.TestModule", 40 "package test;", 41 "", 42 "import dagger.Module;", 43 "import dagger.Provides;", 44 "", 45 "@Module", 46 "interface TestModule {", 47 " @Provides", 48 " static int provideInt() {", 49 " return 0;", 50 " }", 51 "}"); 52 53 Compilation compilation = 54 javac() 55 .withProcessors(new ComponentProcessor()) 56 .withOptions( 57 "-Aerror_on_binding=java.lang.Integer", 58 "-Adagger.fullBindingGraphValidation=ERROR", 59 "-Adagger.pluginsVisitFullBindingGraphs=ENABLED") 60 .compile(module); 61 assertThat(compilation).failed(); 62 assertThat(compilation) 63 .hadErrorContaining( 64 message("[FailingPlugin] Bad Binding: @Provides int test.TestModule.provideInt()")) 65 .inFile(module) 66 .onLineContaining("interface TestModule"); 67 } 68 69 @Test dependencyTraceAtBinding()70 public void dependencyTraceAtBinding() { 71 JavaFileObject foo = 72 JavaFileObjects.forSourceLines( 73 "test.Foo", 74 "package test;", 75 "", 76 "import javax.inject.Inject;", 77 "", 78 "class Foo {", 79 " @Inject Foo() {}", 80 "}"); 81 JavaFileObject component = 82 JavaFileObjects.forSourceLines( 83 "test.TestComponent", 84 "package test;", 85 "", 86 "import dagger.Component;", 87 "", 88 "@Component", 89 "interface TestComponent {", 90 " Foo foo();", 91 "}"); 92 93 Compilation compilation = 94 javac() 95 .withProcessors(new ComponentProcessor()) 96 .withOptions("-Aerror_on_binding=test.Foo") 97 .compile(component, foo); 98 assertThat(compilation).failed(); 99 assertThat(compilation) 100 .hadErrorContaining( 101 message( 102 "[FailingPlugin] Bad Binding: @Inject test.Foo()", 103 " test.Foo is requested at", 104 " test.TestComponent.foo()")) 105 .inFile(component) 106 .onLineContaining("interface TestComponent"); 107 } 108 109 @Test dependencyTraceAtDependencyRequest()110 public void dependencyTraceAtDependencyRequest() { 111 JavaFileObject foo = 112 JavaFileObjects.forSourceLines( 113 "test.Foo", 114 "package test;", 115 "", 116 "import javax.inject.Inject;", 117 "", 118 "class Foo {", 119 " @Inject Foo(Duplicated inFooDep) {}", 120 "}"); 121 JavaFileObject duplicated = 122 JavaFileObjects.forSourceLines( 123 "test.Duplicated", 124 "package test;", 125 "", 126 "import javax.inject.Inject;", 127 "", 128 "class Duplicated {", 129 " @Inject Duplicated() {}", 130 "}"); 131 JavaFileObject entryPoint = 132 JavaFileObjects.forSourceLines( 133 "test.EntryPoint", 134 "package test;", 135 "", 136 "import javax.inject.Inject;", 137 "", 138 "class EntryPoint {", 139 " @Inject EntryPoint(Foo foo, Duplicated dup1, Duplicated dup2) {}", 140 "}"); 141 JavaFileObject chain1 = 142 JavaFileObjects.forSourceLines( 143 "test.Chain1", 144 "package test;", 145 "", 146 "import javax.inject.Inject;", 147 "", 148 "class Chain1 {", 149 " @Inject Chain1(Chain2 chain) {}", 150 "}"); 151 JavaFileObject chain2 = 152 JavaFileObjects.forSourceLines( 153 "test.Chain2", 154 "package test;", 155 "", 156 "import javax.inject.Inject;", 157 "", 158 "class Chain2 {", 159 " @Inject Chain2(Chain3 chain) {}", 160 "}"); 161 JavaFileObject chain3 = 162 JavaFileObjects.forSourceLines( 163 "test.Chain3", 164 "package test;", 165 "", 166 "import javax.inject.Inject;", 167 "", 168 "class Chain3 {", 169 " @Inject Chain3(Foo foo) {}", 170 "}"); 171 JavaFileObject component = 172 JavaFileObjects.forSourceLines( 173 "test.TestComponent", 174 "package test;", 175 "", 176 "import dagger.Component;", 177 "", 178 "@Component", 179 "interface TestComponent {", 180 " EntryPoint entryPoint();", 181 " Chain1 chain();", 182 "}"); 183 184 CompilationFactory compilationFactory = 185 new CompilationFactory(component, foo, duplicated, entryPoint, chain1, chain2, chain3); 186 187 assertThat(compilationFactory.compilationWithErrorOnDependency("entryPoint")) 188 .hadErrorContaining( 189 message( 190 "[FailingPlugin] Bad Dependency: test.TestComponent.entryPoint() (entry point)", 191 " test.EntryPoint is requested at", 192 " test.TestComponent.entryPoint()")) 193 .inFile(component) 194 .onLineContaining("interface TestComponent"); 195 assertThat(compilationFactory.compilationWithErrorOnDependency("dup1")) 196 .hadErrorContaining( 197 message( 198 "[FailingPlugin] Bad Dependency: test.EntryPoint(…, dup1, …)", 199 " test.Duplicated is injected at", 200 " test.EntryPoint(…, dup1, …)", 201 " test.EntryPoint is requested at", 202 " test.TestComponent.entryPoint()")) 203 .inFile(component) 204 .onLineContaining("interface TestComponent"); 205 assertThat(compilationFactory.compilationWithErrorOnDependency("dup2")) 206 .hadErrorContaining( 207 message( 208 "[FailingPlugin] Bad Dependency: test.EntryPoint(…, dup2)", 209 " test.Duplicated is injected at", 210 " test.EntryPoint(…, dup2)", 211 " test.EntryPoint is requested at", 212 " test.TestComponent.entryPoint()")) 213 .inFile(component) 214 .onLineContaining("interface TestComponent"); 215 216 Compilation inFooDepCompilation = 217 compilationFactory.compilationWithErrorOnDependency("inFooDep"); 218 assertThat(inFooDepCompilation) 219 .hadErrorContaining( 220 message( 221 "[FailingPlugin] Bad Dependency: test.Foo(inFooDep)", 222 " test.Duplicated is injected at", 223 " test.Foo(inFooDep)", 224 " test.Foo is injected at", 225 " test.EntryPoint(foo, …)", 226 " test.EntryPoint is requested at", 227 " test.TestComponent.entryPoint()", 228 "The following other entry points also depend on it:", 229 " test.TestComponent.chain()")) 230 .inFile(component) 231 .onLineContaining("interface TestComponent"); 232 } 233 234 @Test dependencyTraceAtDependencyRequest_subcomponents()235 public void dependencyTraceAtDependencyRequest_subcomponents() { 236 JavaFileObject foo = 237 JavaFileObjects.forSourceLines( 238 "test.Foo", 239 "package test;", 240 "", 241 "import javax.inject.Inject;", 242 "", 243 "class Foo {", 244 " @Inject Foo() {}", 245 "}"); 246 JavaFileObject entryPoint = 247 JavaFileObjects.forSourceLines( 248 "test.EntryPoint", 249 "package test;", 250 "", 251 "import javax.inject.Inject;", 252 "", 253 "class EntryPoint {", 254 " @Inject EntryPoint(Foo foo) {}", 255 "}"); 256 JavaFileObject component = 257 JavaFileObjects.forSourceLines( 258 "test.TestComponent", 259 "package test;", 260 "", 261 "import dagger.Component;", 262 "", 263 "@Component", 264 "interface TestComponent {", 265 " TestSubcomponent sub();", 266 "}"); 267 JavaFileObject subcomponent = 268 JavaFileObjects.forSourceLines( 269 "test.TestSubcomponent", 270 "package test;", 271 "", 272 "import dagger.Subcomponent;", 273 "", 274 "@Subcomponent", 275 "interface TestSubcomponent {", 276 " EntryPoint childEntryPoint();", 277 "}"); 278 279 CompilationFactory compilationFactory = 280 new CompilationFactory(component, subcomponent, foo, entryPoint); 281 assertThat(compilationFactory.compilationWithErrorOnDependency("childEntryPoint")) 282 .hadErrorContaining( 283 message( 284 "[FailingPlugin] Bad Dependency: " 285 + "test.TestSubcomponent.childEntryPoint() (entry point)", 286 " test.EntryPoint is requested at", 287 " test.TestSubcomponent.childEntryPoint()" 288 + " [test.TestComponent → test.TestSubcomponent]")) 289 .inFile(component) 290 .onLineContaining("interface TestComponent"); 291 assertThat(compilationFactory.compilationWithErrorOnDependency("foo")) 292 .hadErrorContaining( 293 // TODO(ronshapiro): Maybe make the component path resemble a stack trace: 294 // test.TestSubcomponent is a child of 295 // test.TestComponent 296 // TODO(dpb): Or invert the order: Child → Parent 297 message( 298 "[FailingPlugin] Bad Dependency: test.EntryPoint(foo)", 299 " test.Foo is injected at", 300 " test.EntryPoint(foo)", 301 " test.EntryPoint is requested at", 302 " test.TestSubcomponent.childEntryPoint() " 303 + "[test.TestComponent → test.TestSubcomponent]")) 304 .inFile(component) 305 .onLineContaining("interface TestComponent"); 306 } 307 308 @Test errorOnComponent()309 public void errorOnComponent() { 310 JavaFileObject component = 311 JavaFileObjects.forSourceLines( 312 "test.TestComponent", 313 "package test;", 314 "", 315 "import dagger.Component;", 316 "", 317 "@Component", 318 "interface TestComponent {}"); 319 320 Compilation compilation = 321 javac() 322 .withProcessors(new ComponentProcessor()) 323 .withOptions("-Aerror_on_component") 324 .compile(component); 325 assertThat(compilation).failed(); 326 assertThat(compilation) 327 .hadErrorContaining("[FailingPlugin] Bad Component: test.TestComponent") 328 .inFile(component) 329 .onLineContaining("interface TestComponent"); 330 } 331 332 @Test errorOnSubcomponent()333 public void errorOnSubcomponent() { 334 JavaFileObject subcomponent = 335 JavaFileObjects.forSourceLines( 336 "test.TestSubcomponent", 337 "package test;", 338 "", 339 "import dagger.Subcomponent;", 340 "", 341 "@Subcomponent", 342 "interface TestSubcomponent {}"); 343 JavaFileObject component = 344 JavaFileObjects.forSourceLines( 345 "test.TestComponent", 346 "package test;", 347 "", 348 "import dagger.Component;", 349 "", 350 "@Component", 351 "interface TestComponent {", 352 " TestSubcomponent subcomponent();", 353 "}"); 354 355 Compilation compilation = 356 javac() 357 .withProcessors(new ComponentProcessor()) 358 .withOptions("-Aerror_on_subcomponents") 359 .compile(component, subcomponent); 360 assertThat(compilation).failed(); 361 assertThat(compilation) 362 .hadErrorContaining( 363 "[FailingPlugin] Bad Subcomponent: test.TestComponent → test.TestSubcomponent " 364 + "[test.TestComponent → test.TestSubcomponent]") 365 .inFile(component) 366 .onLineContaining("interface TestComponent"); 367 } 368 369 // SpiDiagnosticReporter uses a shortest path algorithm to determine a dependency trace to a 370 // binding. Without modifications, this would produce a strange error if a shorter path exists 371 // from one entrypoint, through a @Module.subcomponents builder binding edge, and to the binding 372 // usage within the subcomponent. Therefore, when scanning for the shortest path, we only consider 373 // BindingNodes so we don't cross component boundaries. This test exhibits this case. 374 @Test shortestPathToBindingExistsThroughSubcomponentBuilder()375 public void shortestPathToBindingExistsThroughSubcomponentBuilder() { 376 JavaFileObject chain1 = 377 JavaFileObjects.forSourceLines( 378 "test.Chain1", 379 "package test;", 380 "", 381 "import javax.inject.Inject;", 382 "", 383 "class Chain1 {", 384 " @Inject Chain1(Chain2 chain) {}", 385 "}"); 386 JavaFileObject chain2 = 387 JavaFileObjects.forSourceLines( 388 "test.Chain2", 389 "package test;", 390 "", 391 "import javax.inject.Inject;", 392 "", 393 "class Chain2 {", 394 " @Inject Chain2(Chain3 chain) {}", 395 "}"); 396 JavaFileObject chain3 = 397 JavaFileObjects.forSourceLines( 398 "test.Chain3", 399 "package test;", 400 "", 401 "import javax.inject.Inject;", 402 "", 403 "class Chain3 {", 404 " @Inject Chain3(ExposedOnSubcomponent exposedOnSubcomponent) {}", 405 "}"); 406 JavaFileObject exposedOnSubcomponent = 407 JavaFileObjects.forSourceLines( 408 "test.ExposedOnSubcomponent", 409 "package test;", 410 "", 411 "import javax.inject.Inject;", 412 "", 413 "class ExposedOnSubcomponent {", 414 " @Inject ExposedOnSubcomponent() {}", 415 "}"); 416 JavaFileObject subcomponent = 417 JavaFileObjects.forSourceLines( 418 "test.TestSubcomponent", 419 "package test;", 420 "", 421 "import dagger.Subcomponent;", 422 "", 423 "@Subcomponent", 424 "interface TestSubcomponent {", 425 " ExposedOnSubcomponent exposedOnSubcomponent();", 426 "", 427 " @Subcomponent.Builder", 428 " interface Builder {", 429 " TestSubcomponent build();", 430 " }", 431 "}"); 432 JavaFileObject subcomponentModule = 433 JavaFileObjects.forSourceLines( 434 "test.SubcomponentModule", 435 "package test;", 436 "", 437 "import dagger.Module;", 438 "", 439 "@Module(subcomponents = TestSubcomponent.class)", 440 "interface SubcomponentModule {}"); 441 JavaFileObject component = 442 JavaFileObjects.forSourceLines( 443 "test.TestComponent", 444 "package test;", 445 "", 446 "import dagger.Component;", 447 "import javax.inject.Singleton;", 448 "", 449 "@Singleton", 450 "@Component(modules = SubcomponentModule.class)", 451 "interface TestComponent {", 452 " Chain1 chain();", 453 " TestSubcomponent.Builder subcomponent();", 454 "}"); 455 456 Compilation compilation = 457 javac() 458 .withProcessors(new ComponentProcessor()) 459 .withOptions("-Aerror_on_binding=test.ExposedOnSubcomponent") 460 .compile( 461 component, 462 subcomponent, 463 chain1, 464 chain2, 465 chain3, 466 exposedOnSubcomponent, 467 subcomponentModule); 468 assertThat(compilation) 469 .hadErrorContaining( 470 message( 471 "[FailingPlugin] Bad Binding: @Inject test.ExposedOnSubcomponent()", 472 " test.ExposedOnSubcomponent is injected at", 473 " test.Chain3(exposedOnSubcomponent)", 474 " test.Chain3 is injected at", 475 " test.Chain2(chain)", 476 " test.Chain2 is injected at", 477 " test.Chain1(chain)", 478 " test.Chain1 is requested at", 479 " test.TestComponent.chain()", 480 "The following other entry points also depend on it:", 481 " test.TestSubcomponent.exposedOnSubcomponent() " 482 + "[test.TestComponent → test.TestSubcomponent]")) 483 .inFile(component) 484 .onLineContaining("interface TestComponent"); 485 } 486 487 @Test onPluginEnd()488 public void onPluginEnd() { 489 JavaFileObject component = 490 JavaFileObjects.forSourceLines( 491 "test.TestComponent", 492 "package test;", 493 "", 494 "import dagger.Component;", 495 "", 496 "@Component", 497 "interface TestComponent {}"); 498 Compilation compilation = javac().withProcessors(new ComponentProcessor()).compile(component); 499 assertThat(compilation) 500 .generatedFile(StandardLocation.SOURCE_OUTPUT, "", "onPluginEndTest.txt"); 501 } 502 503 // This works around an issue in the opensource compile testing where only one diagnostic is 504 // recorded per line. When multiple validation items resolve to the same entry point, we can 505 // only see the first. This helper class makes it easier to compile all of the files in the test 506 // multiple times with different options to single out each error 507 private static class CompilationFactory { 508 private final ImmutableList<JavaFileObject> javaFileObjects; 509 CompilationFactory(JavaFileObject... javaFileObjects)510 CompilationFactory(JavaFileObject... javaFileObjects) { 511 this.javaFileObjects = ImmutableList.copyOf(javaFileObjects); 512 } 513 compilationWithErrorOnDependency(String dependencySimpleName)514 private Compilation compilationWithErrorOnDependency(String dependencySimpleName) { 515 return javac() 516 .withProcessors(new ComponentProcessor()) 517 .withOptions("-Aerror_on_dependency=" + dependencySimpleName) 518 .compile(javaFileObjects); 519 } 520 } 521 message(String... lines)522 private static String message(String... lines) { 523 return Joiner.on("\n ").join(lines); 524 } 525 } 526