509 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Java
		
	
	
	
			
		
		
	
	
			509 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Java
		
	
	
	
| /*
 | |
|  * Copyright (C) 2018 The Dagger Authors.
 | |
|  *
 | |
|  * Licensed under the Apache License, Version 2.0 (the "License");
 | |
|  * you may not use this file except in compliance with the License.
 | |
|  * You may obtain a copy of the License at
 | |
|  *
 | |
|  * http://www.apache.org/licenses/LICENSE-2.0
 | |
|  *
 | |
|  * Unless required by applicable law or agreed to in writing, software
 | |
|  * distributed under the License is distributed on an "AS IS" BASIS,
 | |
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
|  * See the License for the specific language governing permissions and
 | |
|  * limitations under the License.
 | |
|  */
 | |
| 
 | |
| package dagger.spi;
 | |
| 
 | |
| import static com.google.testing.compile.CompilationSubject.assertThat;
 | |
| import static com.google.testing.compile.Compiler.javac;
 | |
| 
 | |
| import com.google.common.base.Joiner;
 | |
| import com.google.common.collect.ImmutableList;
 | |
| import com.google.testing.compile.Compilation;
 | |
| import com.google.testing.compile.JavaFileObjects;
 | |
| import dagger.internal.codegen.ComponentProcessor;
 | |
| import javax.tools.JavaFileObject;
 | |
| import org.junit.Test;
 | |
| import org.junit.runner.RunWith;
 | |
| import org.junit.runners.JUnit4;
 | |
| 
 | |
| @RunWith(JUnit4.class)
 | |
| public final class SpiPluginTest {
 | |
|   @Test
 | |
|   public void moduleBinding() {
 | |
|     JavaFileObject module =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.TestModule",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import dagger.Module;",
 | |
|             "import dagger.Provides;",
 | |
|             "",
 | |
|             "@Module",
 | |
|             "interface TestModule {",
 | |
|             "  @Provides",
 | |
|             "  static int provideInt() {",
 | |
|             "    return 0;",
 | |
|             "  }",
 | |
|             "}");
 | |
| 
 | |
|     Compilation compilation =
 | |
|         javac()
 | |
|             .withProcessors(new ComponentProcessor())
 | |
|             .withOptions(
 | |
|                 "-Aerror_on_binding=java.lang.Integer",
 | |
|                 "-Adagger.fullBindingGraphValidation=ERROR",
 | |
|                 "-Adagger.pluginsVisitFullBindingGraphs=ENABLED")
 | |
|             .compile(module);
 | |
|     assertThat(compilation).failed();
 | |
|     assertThat(compilation)
 | |
|         .hadErrorContaining(
 | |
|             message("[FailingPlugin] Bad Binding: @Provides int test.TestModule.provideInt()"))
 | |
|         .inFile(module)
 | |
|         .onLineContaining("interface TestModule");
 | |
|   }
 | |
| 
 | |
|   @Test
 | |
|   public void dependencyTraceAtBinding() {
 | |
|     JavaFileObject foo =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.Foo",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class Foo {",
 | |
|             "  @Inject Foo() {}",
 | |
|             "}");
 | |
|     JavaFileObject component =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.TestComponent",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import dagger.Component;",
 | |
|             "",
 | |
|             "@Component",
 | |
|             "interface TestComponent {",
 | |
|             "  Foo foo();",
 | |
|             "}");
 | |
| 
 | |
|     Compilation compilation =
 | |
|         javac()
 | |
|             .withProcessors(new ComponentProcessor())
 | |
|             .withOptions("-Aerror_on_binding=test.Foo")
 | |
|             .compile(component, foo);
 | |
|     assertThat(compilation).failed();
 | |
|     assertThat(compilation)
 | |
|         .hadErrorContaining(
 | |
|             message(
 | |
|                 "[FailingPlugin] Bad Binding: @Inject test.Foo()",
 | |
|                 "    test.Foo is requested at",
 | |
|                 "        test.TestComponent.foo()"))
 | |
|         .inFile(component)
 | |
|         .onLineContaining("interface TestComponent");
 | |
|   }
 | |
| 
 | |
|   @Test
 | |
|   public void dependencyTraceAtDependencyRequest() {
 | |
|     JavaFileObject foo =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.Foo",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class Foo {",
 | |
|             "  @Inject Foo(Duplicated inFooDep) {}",
 | |
|             "}");
 | |
|     JavaFileObject duplicated =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.Duplicated",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class Duplicated {",
 | |
|             "  @Inject Duplicated() {}",
 | |
|             "}");
 | |
|     JavaFileObject entryPoint =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.EntryPoint",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class EntryPoint {",
 | |
|             "  @Inject EntryPoint(Foo foo, Duplicated dup1, Duplicated dup2) {}",
 | |
|             "}");
 | |
|     JavaFileObject chain1 =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.Chain1",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class Chain1 {",
 | |
|             "  @Inject Chain1(Chain2 chain) {}",
 | |
|             "}");
 | |
|     JavaFileObject chain2 =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.Chain2",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class Chain2 {",
 | |
|             "  @Inject Chain2(Chain3 chain) {}",
 | |
|             "}");
 | |
|     JavaFileObject chain3 =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.Chain3",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class Chain3 {",
 | |
|             "  @Inject Chain3(Foo foo) {}",
 | |
|             "}");
 | |
|     JavaFileObject component =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.TestComponent",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import dagger.Component;",
 | |
|             "",
 | |
|             "@Component",
 | |
|             "interface TestComponent {",
 | |
|             "  EntryPoint entryPoint();",
 | |
|             "  Chain1 chain();",
 | |
|             "}");
 | |
| 
 | |
|     CompilationFactory compilationFactory =
 | |
|         new CompilationFactory(component, foo, duplicated, entryPoint, chain1, chain2, chain3);
 | |
| 
 | |
|     assertThat(compilationFactory.compilationWithErrorOnDependency("entryPoint"))
 | |
|         .hadErrorContaining(
 | |
|             message(
 | |
|                 "[FailingPlugin] Bad Dependency: test.TestComponent.entryPoint() (entry point)",
 | |
|                 "    test.EntryPoint is requested at",
 | |
|                 "        test.TestComponent.entryPoint()"))
 | |
|         .inFile(component)
 | |
|         .onLineContaining("interface TestComponent");
 | |
|     assertThat(compilationFactory.compilationWithErrorOnDependency("dup1"))
 | |
|         .hadErrorContaining(
 | |
|             message(
 | |
|                 "[FailingPlugin] Bad Dependency: test.EntryPoint(…, dup1, …)",
 | |
|                 "    test.Duplicated is injected at",
 | |
|                 "        test.EntryPoint(…, dup1, …)",
 | |
|                 "    test.EntryPoint is requested at",
 | |
|                 "        test.TestComponent.entryPoint()"))
 | |
|         .inFile(component)
 | |
|         .onLineContaining("interface TestComponent");
 | |
|     assertThat(compilationFactory.compilationWithErrorOnDependency("dup2"))
 | |
|         .hadErrorContaining(
 | |
|             message(
 | |
|                 "[FailingPlugin] Bad Dependency: test.EntryPoint(…, dup2)",
 | |
|                 "    test.Duplicated is injected at",
 | |
|                 "        test.EntryPoint(…, dup2)",
 | |
|                 "    test.EntryPoint is requested at",
 | |
|                 "        test.TestComponent.entryPoint()"))
 | |
|         .inFile(component)
 | |
|         .onLineContaining("interface TestComponent");
 | |
| 
 | |
|     Compilation inFooDepCompilation =
 | |
|         compilationFactory.compilationWithErrorOnDependency("inFooDep");
 | |
|     assertThat(inFooDepCompilation)
 | |
|         .hadErrorContaining(
 | |
|             message(
 | |
|                 "[FailingPlugin] Bad Dependency: test.Foo(inFooDep)",
 | |
|                 "    test.Duplicated is injected at",
 | |
|                 "        test.Foo(inFooDep)",
 | |
|                 "    test.Foo is injected at",
 | |
|                 "        test.EntryPoint(foo, …)",
 | |
|                 "    test.EntryPoint is requested at",
 | |
|                 "        test.TestComponent.entryPoint()",
 | |
|                 "The following other entry points also depend on it:",
 | |
|                 "    test.TestComponent.chain()"))
 | |
|         .inFile(component)
 | |
|         .onLineContaining("interface TestComponent");
 | |
|   }
 | |
| 
 | |
|   @Test
 | |
|   public void dependencyTraceAtDependencyRequest_subcomponents() {
 | |
|     JavaFileObject foo =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.Foo",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class Foo {",
 | |
|             "  @Inject Foo() {}",
 | |
|             "}");
 | |
|     JavaFileObject entryPoint =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.EntryPoint",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class EntryPoint {",
 | |
|             "  @Inject EntryPoint(Foo foo) {}",
 | |
|             "}");
 | |
|     JavaFileObject component =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.TestComponent",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import dagger.Component;",
 | |
|             "",
 | |
|             "@Component",
 | |
|             "interface TestComponent {",
 | |
|             "  TestSubcomponent sub();",
 | |
|             "}");
 | |
|     JavaFileObject subcomponent =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.TestSubcomponent",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import dagger.Subcomponent;",
 | |
|             "",
 | |
|             "@Subcomponent",
 | |
|             "interface TestSubcomponent {",
 | |
|             "  EntryPoint childEntryPoint();",
 | |
|             "}");
 | |
| 
 | |
|     CompilationFactory compilationFactory =
 | |
|         new CompilationFactory(component, subcomponent, foo, entryPoint);
 | |
|     assertThat(compilationFactory.compilationWithErrorOnDependency("childEntryPoint"))
 | |
|         .hadErrorContaining(
 | |
|             message(
 | |
|                 "[FailingPlugin] Bad Dependency: "
 | |
|                     + "test.TestSubcomponent.childEntryPoint() (entry point)",
 | |
|                 "    test.EntryPoint is requested at",
 | |
|                 "        test.TestSubcomponent.childEntryPoint()"
 | |
|                     + " [test.TestComponent → test.TestSubcomponent]"))
 | |
|         .inFile(component)
 | |
|         .onLineContaining("interface TestComponent");
 | |
|     assertThat(compilationFactory.compilationWithErrorOnDependency("foo"))
 | |
|         .hadErrorContaining(
 | |
|             // TODO(ronshapiro): Maybe make the component path resemble a stack trace:
 | |
|             //     test.TestSubcomponent is a child of
 | |
|             //         test.TestComponent
 | |
|             // TODO(dpb): Or invert the order: Child → Parent
 | |
|             message(
 | |
|                 "[FailingPlugin] Bad Dependency: test.EntryPoint(foo)",
 | |
|                 "    test.Foo is injected at",
 | |
|                 "        test.EntryPoint(foo)",
 | |
|                 "    test.EntryPoint is requested at",
 | |
|                 "        test.TestSubcomponent.childEntryPoint() "
 | |
|                     + "[test.TestComponent → test.TestSubcomponent]"))
 | |
|         .inFile(component)
 | |
|         .onLineContaining("interface TestComponent");
 | |
|   }
 | |
| 
 | |
|   @Test
 | |
|   public void errorOnComponent() {
 | |
|     JavaFileObject component =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.TestComponent",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import dagger.Component;",
 | |
|             "",
 | |
|             "@Component",
 | |
|             "interface TestComponent {}");
 | |
| 
 | |
|     Compilation compilation =
 | |
|         javac()
 | |
|             .withProcessors(new ComponentProcessor())
 | |
|             .withOptions("-Aerror_on_component")
 | |
|             .compile(component);
 | |
|     assertThat(compilation).failed();
 | |
|     assertThat(compilation)
 | |
|         .hadErrorContaining("[FailingPlugin] Bad Component: test.TestComponent")
 | |
|         .inFile(component)
 | |
|         .onLineContaining("interface TestComponent");
 | |
|   }
 | |
| 
 | |
|   @Test
 | |
|   public void errorOnSubcomponent() {
 | |
|     JavaFileObject subcomponent =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.TestSubcomponent",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import dagger.Subcomponent;",
 | |
|             "",
 | |
|             "@Subcomponent",
 | |
|             "interface TestSubcomponent {}");
 | |
|     JavaFileObject component =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.TestComponent",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import dagger.Component;",
 | |
|             "",
 | |
|             "@Component",
 | |
|             "interface TestComponent {",
 | |
|             "  TestSubcomponent subcomponent();",
 | |
|             "}");
 | |
| 
 | |
|     Compilation compilation =
 | |
|         javac()
 | |
|             .withProcessors(new ComponentProcessor())
 | |
|             .withOptions("-Aerror_on_subcomponents")
 | |
|             .compile(component, subcomponent);
 | |
|     assertThat(compilation).failed();
 | |
|     assertThat(compilation)
 | |
|         .hadErrorContaining(
 | |
|             "[FailingPlugin] Bad Subcomponent: test.TestComponent → test.TestSubcomponent "
 | |
|                 + "[test.TestComponent → test.TestSubcomponent]")
 | |
|         .inFile(component)
 | |
|         .onLineContaining("interface TestComponent");
 | |
|   }
 | |
| 
 | |
|   // SpiDiagnosticReporter uses a shortest path algorithm to determine a dependency trace to a
 | |
|   // binding. Without modifications, this would produce a strange error if a shorter path exists
 | |
|   // from one entrypoint, through a @Module.subcomponents builder binding edge, and to the binding
 | |
|   // usage within the subcomponent. Therefore, when scanning for the shortest path, we only consider
 | |
|   // BindingNodes so we don't cross component boundaries. This test exhibits this case.
 | |
|   @Test
 | |
|   public void shortestPathToBindingExistsThroughSubcomponentBuilder() {
 | |
|     JavaFileObject chain1 =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.Chain1",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class Chain1 {",
 | |
|             "  @Inject Chain1(Chain2 chain) {}",
 | |
|             "}");
 | |
|     JavaFileObject chain2 =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.Chain2",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class Chain2 {",
 | |
|             "  @Inject Chain2(Chain3 chain) {}",
 | |
|             "}");
 | |
|     JavaFileObject chain3 =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.Chain3",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class Chain3 {",
 | |
|             "  @Inject Chain3(ExposedOnSubcomponent exposedOnSubcomponent) {}",
 | |
|             "}");
 | |
|     JavaFileObject exposedOnSubcomponent =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.ExposedOnSubcomponent",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import javax.inject.Inject;",
 | |
|             "",
 | |
|             "class ExposedOnSubcomponent {",
 | |
|             "  @Inject ExposedOnSubcomponent() {}",
 | |
|             "}");
 | |
|     JavaFileObject subcomponent =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.TestSubcomponent",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import dagger.Subcomponent;",
 | |
|             "",
 | |
|             "@Subcomponent",
 | |
|             "interface TestSubcomponent {",
 | |
|             "  ExposedOnSubcomponent exposedOnSubcomponent();",
 | |
|             "",
 | |
|             "  @Subcomponent.Builder",
 | |
|             "  interface Builder {",
 | |
|             "    TestSubcomponent build();",
 | |
|             "  }",
 | |
|             "}");
 | |
|     JavaFileObject subcomponentModule =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.SubcomponentModule",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import dagger.Module;",
 | |
|             "",
 | |
|             "@Module(subcomponents = TestSubcomponent.class)",
 | |
|             "interface SubcomponentModule {}");
 | |
|     JavaFileObject component =
 | |
|         JavaFileObjects.forSourceLines(
 | |
|             "test.TestComponent",
 | |
|             "package test;",
 | |
|             "",
 | |
|             "import dagger.Component;",
 | |
|             "import javax.inject.Singleton;",
 | |
|             "",
 | |
|             "@Singleton",
 | |
|             "@Component(modules = SubcomponentModule.class)",
 | |
|             "interface TestComponent {",
 | |
|             "  Chain1 chain();",
 | |
|             "  TestSubcomponent.Builder subcomponent();",
 | |
|             "}");
 | |
| 
 | |
|     Compilation compilation =
 | |
|         javac()
 | |
|             .withProcessors(new ComponentProcessor())
 | |
|             .withOptions("-Aerror_on_binding=test.ExposedOnSubcomponent")
 | |
|             .compile(
 | |
|                 component,
 | |
|                 subcomponent,
 | |
|                 chain1,
 | |
|                 chain2,
 | |
|                 chain3,
 | |
|                 exposedOnSubcomponent,
 | |
|                 subcomponentModule);
 | |
|     assertThat(compilation)
 | |
|         .hadErrorContaining(
 | |
|             message(
 | |
|                 "[FailingPlugin] Bad Binding: @Inject test.ExposedOnSubcomponent()",
 | |
|                 "    test.ExposedOnSubcomponent is injected at",
 | |
|                 "        test.Chain3(exposedOnSubcomponent)",
 | |
|                 "    test.Chain3 is injected at",
 | |
|                 "        test.Chain2(chain)",
 | |
|                 "    test.Chain2 is injected at",
 | |
|                 "        test.Chain1(chain)",
 | |
|                 "    test.Chain1 is requested at",
 | |
|                 "        test.TestComponent.chain()",
 | |
|                 "The following other entry points also depend on it:",
 | |
|                 "    test.TestSubcomponent.exposedOnSubcomponent() "
 | |
|                     + "[test.TestComponent → test.TestSubcomponent]"))
 | |
|         .inFile(component)
 | |
|         .onLineContaining("interface TestComponent");
 | |
|   }
 | |
| 
 | |
|   // This works around an issue in the opensource compile testing where only one diagnostic is
 | |
|   // recorded per line. When multiple validation items resolve to the same entry point, we can
 | |
|   // only see the first. This helper class makes it easier to compile all of the files in the test
 | |
|   // multiple times with different options to single out each error
 | |
|   private static class CompilationFactory {
 | |
|     private final ImmutableList<JavaFileObject> javaFileObjects;
 | |
| 
 | |
|     CompilationFactory(JavaFileObject... javaFileObjects) {
 | |
|       this.javaFileObjects = ImmutableList.copyOf(javaFileObjects);
 | |
|     }
 | |
| 
 | |
|     private Compilation compilationWithErrorOnDependency(String dependencySimpleName) {
 | |
|       return javac()
 | |
|           .withProcessors(new ComponentProcessor())
 | |
|           .withOptions("-Aerror_on_dependency=" + dependencySimpleName)
 | |
|           .compile(javaFileObjects);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private static String message(String... lines) {
 | |
|     return Joiner.on("\n  ").join(lines);
 | |
|   }
 | |
| }
 |