/* * Copyright (C) 2014 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.internal.codegen; import static com.google.testing.compile.CompilationSubject.assertThat; import static dagger.internal.codegen.Compilers.compilerWithOptions; import static dagger.internal.codegen.Compilers.daggerCompiler; import com.google.testing.compile.Compilation; import com.google.testing.compile.JavaFileObjects; import javax.tools.JavaFileObject; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Producer-specific validation tests. */ @RunWith(JUnit4.class) public class ProductionGraphValidationTest { private static final JavaFileObject EXECUTOR_MODULE = JavaFileObjects.forSourceLines( "test.ExecutorModule", "package test;", "", "import com.google.common.util.concurrent.MoreExecutors;", "import dagger.Module;", "import dagger.Provides;", "import dagger.producers.Production;", "import java.util.concurrent.Executor;", "", "@Module", "class ExecutorModule {", " @Provides @Production Executor executor() {", " return MoreExecutors.directExecutor();", " }", "}"); @Test public void componentWithUnprovidedInput() { JavaFileObject component = JavaFileObjects.forSourceLines("test.MyComponent", "package test;", "", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.producers.ProductionComponent;", "", "@ProductionComponent(modules = {ExecutorModule.class, FooModule.class})", "interface MyComponent {", " ListenableFuture getFoo();", "}"); JavaFileObject module = JavaFileObjects.forSourceLines("test.FooModule", "package test;", "", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "", "class Foo {}", "class Bar {}", "", "@ProducerModule", "class FooModule {", " @Produces Foo foo(Bar bar) {", " return null;", " }", "}"); Compilation compilation = daggerCompiler().compile(EXECUTOR_MODULE, module, component); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining( "Bar cannot be provided without an @Inject constructor or an @Provides- or " + "@Produces-annotated method.") .inFile(component) .onLineContaining("interface MyComponent"); } @Test public void componentProductionWithNoDependencyChain() { JavaFileObject component = JavaFileObjects.forSourceLines("test.TestClass", "package test;", "", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.producers.ProductionComponent;", "", "final class TestClass {", " interface A {}", "", " @ProductionComponent(modules = ExecutorModule.class)", " interface AComponent {", " ListenableFuture getA();", " }", "}"); Compilation compilation = daggerCompiler().compile(EXECUTOR_MODULE, component); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining( "TestClass.A cannot be provided without an @Provides- or @Produces-annotated " + "method.") .inFile(component) .onLineContaining("interface AComponent"); } @Test public void provisionDependsOnProduction() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.Provides;", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "import dagger.producers.ProductionComponent;", "", "final class TestClass {", " interface A {}", " interface B {}", "", " @ProducerModule(includes = BModule.class)", " final class AModule {", " @Provides A a(B b) {", " return null;", " }", " }", "", " @ProducerModule", " final class BModule {", " @Produces ListenableFuture b() {", " return null;", " }", " }", "", " @ProductionComponent(modules = {ExecutorModule.class, AModule.class})", " interface AComponent {", " ListenableFuture getA();", " }", "}"); Compilation compilation = daggerCompiler().compile(EXECUTOR_MODULE, component); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining("TestClass.A is a provision, which cannot depend on a production.") .inFile(component) .onLineContaining("interface AComponent"); compilation = compilerWithOptions("-Adagger.fullBindingGraphValidation=ERROR") .compile(EXECUTOR_MODULE, component); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining("TestClass.A is a provision, which cannot depend on a production.") .inFile(component) .onLineContaining("class AModule"); } @Test public void provisionEntryPointDependsOnProduction() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "import dagger.producers.ProductionComponent;", "", "final class TestClass {", " interface A {}", "", " @ProducerModule", " static final class AModule {", " @Produces ListenableFuture a() {", " return null;", " }", " }", "", " @ProductionComponent(modules = {ExecutorModule.class, AModule.class})", " interface AComponent {", " A getA();", " }", "}"); Compilation compilation = daggerCompiler().compile(EXECUTOR_MODULE, component); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining( "TestClass.A is a provision entry-point, which cannot depend on a production.") .inFile(component) .onLineContaining("interface AComponent"); } @Test public void providingMultibindingWithProductions() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.Module;", "import dagger.Provides;", "import dagger.multibindings.IntoMap;", "import dagger.multibindings.StringKey;", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "import dagger.producers.ProductionComponent;", "import java.util.Map;", "import javax.inject.Provider;", "", "final class TestClass {", " interface A {}", " interface B {}", "", " @Module", " static final class AModule {", " @Provides static A a(Map> map) {", " return null;", " }", "", " @Provides @IntoMap @StringKey(\"a\") static Object aEntry() {", " return \"a\";", " }", " }", "", " @ProducerModule", " static final class BModule {", " @Produces static B b(A a) {", " return null;", " }", "", " @Produces @IntoMap @StringKey(\"b\") static Object bEntry() {", " return \"b\";", " }", " }", "", " @ProductionComponent(", " modules = {ExecutorModule.class, AModule.class, BModule.class})", " interface AComponent {", " ListenableFuture b();", " }", "}"); Compilation compilation = daggerCompiler().compile(EXECUTOR_MODULE, component); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining("TestClass.A is a provision, which cannot depend on a production") .inFile(component) .onLineContaining("interface AComponent"); } @Test public void monitoringDependsOnUnboundType() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.Module;", "import dagger.Provides;", "import dagger.multibindings.IntoSet;", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "import dagger.producers.ProductionComponent;", "import dagger.producers.monitoring.ProductionComponentMonitor;", "", "final class TestClass {", " interface A {}", "", " @Module", " final class MonitoringModule {", " @Provides @IntoSet", " ProductionComponentMonitor.Factory monitorFactory(A unbound) {", " return null;", " }", " }", "", " @ProducerModule", " final class StringModule {", " @Produces ListenableFuture str() {", " return null;", " }", " }", "", " @ProductionComponent(", " modules = {ExecutorModule.class, MonitoringModule.class, StringModule.class}", " )", " interface StringComponent {", " ListenableFuture getString();", " }", "}"); Compilation compilation = daggerCompiler().compile(EXECUTOR_MODULE, component); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining( "TestClass.A cannot be provided without an @Provides-annotated method.") .inFile(component) .onLineContaining("interface StringComponent"); } @Test public void monitoringDependsOnProduction() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.Module;", "import dagger.Provides;", "import dagger.multibindings.IntoSet;", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "import dagger.producers.ProductionComponent;", "import dagger.producers.monitoring.ProductionComponentMonitor;", "", "final class TestClass {", " interface A {}", "", " @Module", " final class MonitoringModule {", " @Provides @IntoSet ProductionComponentMonitor.Factory monitorFactory(A a) {", " return null;", " }", " }", "", " @ProducerModule", " final class StringModule {", " @Produces A a() {", " return null;", " }", "", " @Produces ListenableFuture str() {", " return null;", " }", " }", "", " @ProductionComponent(", " modules = {ExecutorModule.class, MonitoringModule.class, StringModule.class}", " )", " interface StringComponent {", " ListenableFuture getString();", " }", "}"); Compilation compilation = daggerCompiler().compile(EXECUTOR_MODULE, component); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining( "Set" + " TestClass.MonitoringModule#monitorFactory is a provision," + " which cannot depend on a production.") .inFile(component) .onLineContaining("interface StringComponent"); } @Test public void cycleNotBrokenByMap() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestComponent", "package test;", "", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.producers.ProductionComponent;", "", "@ProductionComponent(modules = {ExecutorModule.class, TestModule.class})", "interface TestComponent {", " ListenableFuture string();", "}"); JavaFileObject module = JavaFileObjects.forSourceLines( "test.TestModule", "package test;", "", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "import dagger.multibindings.IntoMap;", "import dagger.multibindings.StringKey;", "import java.util.Map;", "", "@ProducerModule", "final class TestModule {", " @Produces static String string(Map map) {", " return \"string\";", " }", "", " @Produces @IntoMap @StringKey(\"key\")", " static String entry(String string) {", " return string;", " }", "}"); Compilation compilation = daggerCompiler().compile(EXECUTOR_MODULE, component, module); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining("cycle") .inFile(component) .onLineContaining("interface TestComponent"); } @Test public void cycleNotBrokenByProducerMap() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestComponent", "package test;", "", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.producers.ProductionComponent;", "", "@ProductionComponent(modules = {ExecutorModule.class, TestModule.class})", "interface TestComponent {", " ListenableFuture string();", "}"); JavaFileObject module = JavaFileObjects.forSourceLines( "test.TestModule", "package test;", "", "import dagger.producers.Producer;", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "import dagger.multibindings.StringKey;", "import dagger.multibindings.IntoMap;", "import java.util.Map;", "", "@ProducerModule", "final class TestModule {", " @Produces static String string(Map> map) {", " return \"string\";", " }", "", " @Produces @IntoMap @StringKey(\"key\")", " static String entry(String string) {", " return string;", " }", "}"); Compilation compilation = daggerCompiler().compile(EXECUTOR_MODULE, component, module); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining("cycle") .inFile(component) .onLineContaining("interface TestComponent"); } @Test public void componentWithBadModule() { JavaFileObject badModule = JavaFileObjects.forSourceLines( "test.BadModule", "package test;", "", "import dagger.BindsOptionalOf;", "import dagger.multibindings.Multibinds;", "import dagger.Module;", "import java.util.Set;", "", "@Module", "abstract class BadModule {", " @Multibinds", " @BindsOptionalOf", " abstract Set strings();", "}"); JavaFileObject badComponent = JavaFileObjects.forSourceLines( "test.BadComponent", "package test;", "", "import dagger.Component;", "import java.util.Optional;", "import java.util.Set;", "", "@Component(modules = BadModule.class)", "interface BadComponent {", " Set strings();", " Optional> optionalStrings();", "}"); Compilation compilation = daggerCompiler().compile(badModule, badComponent); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining("BadModule has errors") .inFile(badComponent) .onLine(7); } }