blob: 1df8cd1b0f6f898ced9cd5459ed93670243a39cb [file] [log] [blame]
/*
* 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);
}
}