• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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