xref: /aosp_15_r20/external/dagger2/javatests/dagger/spi/SpiPluginTest.java (revision f585d8a307d0621d6060bd7e80091fdcbf94fe27)
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