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