/* * 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. */ // TODO(beder): Merge the error-handling tests with the ModuleFactoryGeneratorTest. package dagger.internal.codegen; import static com.google.common.truth.Truth.assertAbout; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; import static dagger.internal.codegen.Compilers.daggerCompiler; import static dagger.internal.codegen.DaggerModuleMethodSubject.Factory.assertThatMethodInUnannotatedClass; import static dagger.internal.codegen.DaggerModuleMethodSubject.Factory.assertThatProductionModuleMethod; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.util.concurrent.ListenableFuture; import com.google.testing.compile.Compilation; import com.google.testing.compile.JavaFileObjects; import java.lang.annotation.Retention; import javax.inject.Qualifier; import javax.tools.JavaFileObject; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ProducerModuleFactoryGeneratorTest { @Test public void producesMethodNotInModule() { assertThatMethodInUnannotatedClass("@Produces String produceString() { return null; }") .hasError("@Produces methods can only be present within a @ProducerModule"); } @Test public void producesMethodAbstract() { assertThatProductionModuleMethod("@Produces abstract String produceString();") .hasError("@Produces methods cannot be abstract"); } @Test public void producesMethodPrivate() { assertThatProductionModuleMethod("@Produces private String produceString() { return null; }") .hasError("@Produces methods cannot be private"); } @Test public void producesMethodReturnVoid() { assertThatProductionModuleMethod("@Produces void produceNothing() {}") .hasError("@Produces methods must return a value (not void)"); } @Test public void producesProvider() { assertThatProductionModuleMethod("@Produces Provider produceProvider() {}") .hasError("@Produces methods must not return framework types"); } @Test public void producesLazy() { assertThatProductionModuleMethod("@Produces Lazy produceLazy() {}") .hasError("@Produces methods must not return framework types"); } @Test public void producesMembersInjector() { assertThatProductionModuleMethod( "@Produces MembersInjector produceMembersInjector() {}") .hasError("@Produces methods must not return framework types"); } @Test public void producesProducer() { assertThatProductionModuleMethod("@Produces Producer produceProducer() {}") .hasError("@Produces methods must not return framework types"); } @Test public void producesProduced() { assertThatProductionModuleMethod("@Produces Produced produceProduced() {}") .hasError("@Produces methods must not return framework types"); } @Test public void producesMethodReturnRawFuture() { assertThatProductionModuleMethod("@Produces ListenableFuture produceRaw() {}") .importing(ListenableFuture.class) .hasError("@Produces methods cannot return a raw ListenableFuture"); } @Test public void producesMethodReturnWildcardFuture() { assertThatProductionModuleMethod("@Produces ListenableFuture produceRaw() {}") .importing(ListenableFuture.class) .hasError( "@Produces methods can return only a primitive, an array, a type variable, " + "a declared type, or a ListenableFuture of one of those types"); } @Test public void producesMethodWithTypeParameter() { assertThatProductionModuleMethod("@Produces String produceString() { return null; }") .hasError("@Produces methods may not have type parameters"); } @Test public void producesMethodSetValuesWildcard() { assertThatProductionModuleMethod( "@Produces @ElementsIntoSet Set produceWildcard() { return null; }") .hasError( "@Produces methods can return only a primitive, an array, a type variable, " + "a declared type, or a ListenableFuture of one of those types"); } @Test public void producesMethodSetValuesRawSet() { assertThatProductionModuleMethod( "@Produces @ElementsIntoSet Set produceSomething() { return null; }") .hasError("@Produces methods annotated with @ElementsIntoSet cannot return a raw Set"); } @Test public void producesMethodSetValuesNotASet() { assertThatProductionModuleMethod( "@Produces @ElementsIntoSet List produceStrings() { return null; }") .hasError( "@Produces methods of type set values must return a Set or ListenableFuture of Set"); } @Test public void producesMethodSetValuesWildcardInFuture() { assertThatProductionModuleMethod( "@Produces @ElementsIntoSet " + "ListenableFuture> produceWildcard() { return null; }") .importing(ListenableFuture.class) .hasError( "@Produces methods can return only a primitive, an array, a type variable, " + "a declared type, or a ListenableFuture of one of those types"); } @Test public void producesMethodSetValuesFutureRawSet() { assertThatProductionModuleMethod( "@Produces @ElementsIntoSet ListenableFuture produceSomething() { return null; }") .importing(ListenableFuture.class) .hasError("@Produces methods annotated with @ElementsIntoSet cannot return a raw Set"); } @Test public void producesMethodSetValuesFutureNotASet() { assertThatProductionModuleMethod( "@Produces @ElementsIntoSet " + "ListenableFuture> produceStrings() { return null; }") .importing(ListenableFuture.class) .hasError( "@Produces methods of type set values must return a Set or ListenableFuture of Set"); } @Test public void multipleProducesMethodsWithSameName() { JavaFileObject moduleFile = JavaFileObjects.forSourceLines("test.TestModule", "package test;", "", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "", "@ProducerModule", "final class TestModule {", " @Produces Object produce(int i) {", " return i;", " }", "", " @Produces String produce() {", " return \"\";", " }", "}"); String errorMessage = "Cannot have more than one binding method with the same name in a single module"; Compilation compilation = daggerCompiler().compile(moduleFile); assertThat(compilation).failed(); assertThat(compilation).hadErrorContaining(errorMessage).inFile(moduleFile).onLine(8); assertThat(compilation).hadErrorContaining(errorMessage).inFile(moduleFile).onLine(12); } @Test public void producesMethodThrowsThrowable() { assertThatProductionModuleMethod("@Produces int produceInt() throws Throwable { return 0; }") .hasError( "@Produces methods may only throw unchecked exceptions or exceptions subclassing " + "Exception"); } @Test public void producesMethodWithScope() { assertThatProductionModuleMethod("@Produces @Singleton String str() { return \"\"; }") .hasError("@Produces methods cannot be scoped"); } @Test public void privateModule() { JavaFileObject moduleFile = JavaFileObjects.forSourceLines("test.Enclosing", "package test;", "", "import dagger.producers.ProducerModule;", "", "final class Enclosing {", " @ProducerModule private static final class PrivateModule {", " }", "}"); Compilation compilation = daggerCompiler().compile(moduleFile); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining("Modules cannot be private") .inFile(moduleFile) .onLine(6); } @Test public void enclosedInPrivateModule() { JavaFileObject moduleFile = JavaFileObjects.forSourceLines("test.Enclosing", "package test;", "", "import dagger.producers.ProducerModule;", "", "final class Enclosing {", " private static final class PrivateEnclosing {", " @ProducerModule static final class TestModule {", " }", " }", "}"); Compilation compilation = daggerCompiler().compile(moduleFile); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining("Modules cannot be enclosed in private types") .inFile(moduleFile) .onLine(7); } @Test public void includesNonModule() { JavaFileObject xFile = JavaFileObjects.forSourceLines("test.X", "package test;", "", "public final class X {}"); JavaFileObject moduleFile = JavaFileObjects.forSourceLines( "test.FooModule", "package test;", "", "import dagger.producers.ProducerModule;", "", "@ProducerModule(includes = X.class)", "public final class FooModule {", "}"); Compilation compilation = daggerCompiler().compile(xFile, moduleFile); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining( "X is listed as a module, but is not annotated with one of @Module, @ProducerModule"); } // TODO(ronshapiro): merge this with the equivalent test in ModuleFactoryGeneratorTest and make it // parameterized @Test public void publicModuleNonPublicIncludes() { JavaFileObject publicModuleFile = JavaFileObjects.forSourceLines("test.PublicModule", "package test;", "", "import dagger.producers.ProducerModule;", "", "@ProducerModule(includes = {", " BadNonPublicModule.class, OtherPublicModule.class, OkNonPublicModule.class", "})", "public final class PublicModule {", "}"); JavaFileObject badNonPublicModuleFile = JavaFileObjects.forSourceLines( "test.BadNonPublicModule", "package test;", "", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "", "@ProducerModule", "final class BadNonPublicModule {", " @Produces", " int produceInt() {", " return 42;", " }", "}"); JavaFileObject okNonPublicModuleFile = JavaFileObjects.forSourceLines("test.OkNonPublicModule", "package test;", "", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "", "@ProducerModule", "final class OkNonPublicModule {", " @Produces", " static String produceString() {", " return \"foo\";", " }", "}"); JavaFileObject otherPublicModuleFile = JavaFileObjects.forSourceLines("test.OtherPublicModule", "package test;", "", "import dagger.producers.ProducerModule;", "", "@ProducerModule", "public final class OtherPublicModule {", "}"); Compilation compilation = daggerCompiler() .compile( publicModuleFile, badNonPublicModuleFile, okNonPublicModuleFile, otherPublicModuleFile); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining( "This module is public, but it includes non-public (or effectively non-public) modules " + "(test.BadNonPublicModule) that have non-static, non-abstract binding methods. " + "Either reduce the visibility of this module, make the included modules public, " + "or make all of the binding methods on the included modules abstract or static.") .inFile(publicModuleFile) .onLine(8); } @Test public void argumentNamedModuleCompiles() { JavaFileObject moduleFile = JavaFileObjects.forSourceLines("test.TestModule", "package test;", "", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "", "@ProducerModule", "final class TestModule {", " @Produces String produceString(int module) {", " return null;", " }", "}"); Compilation compilation = daggerCompiler().compile(moduleFile); assertThat(compilation).succeeded(); } @Test public void singleProducesMethodNoArgsFuture() { JavaFileObject moduleFile = JavaFileObjects.forSourceLines("test.TestModule", "package test;", "", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "", "@ProducerModule", "final class TestModule {", " @Produces ListenableFuture produceString() {", " return null;", " }", "}"); JavaFileObject factoryFile = JavaFileObjects.forSourceLines( "TestModule_ProduceStringFactory", "package test;", "", GeneratedLines.generatedImports( "import com.google.common.util.concurrent.Futures;", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.producers.internal.AbstractProducesMethodProducer;", "import dagger.producers.monitoring.ProducerToken;", "import dagger.producers.monitoring.ProductionComponentMonitor;", "import java.util.concurrent.Executor;", "import javax.inject.Provider;"), "", GeneratedLines.generatedAnnotationsWithoutSuppressWarnings(), "@SuppressWarnings({\"FutureReturnValueIgnored\", \"unchecked\", \"rawtypes\"})", "public final class TestModule_ProduceStringFactory", " extends AbstractProducesMethodProducer {", " private final TestModule module;", "", " private TestModule_ProduceStringFactory(", " TestModule module,", " Provider executorProvider,", " Provider productionComponentMonitorProvider) {", " super(", " productionComponentMonitorProvider,", " ProducerToken.create(TestModule_ProduceStringFactory.class),", " executorProvider);", " this.module = module;", " }", "", " public static TestModule_ProduceStringFactory create(", " TestModule module,", " Provider executorProvider,", " Provider productionComponentMonitorProvider) {", " return new TestModule_ProduceStringFactory(", " module, executorProvider, productionComponentMonitorProvider);", " }", "", " @Override protected ListenableFuture collectDependencies() {", " return Futures.immediateFuture(null);", " }", "", " @Override public ListenableFuture callProducesMethod(Void ignoredVoidArg) {", " return module.produceString();", " }", "}"); assertAbout(javaSource()) .that(moduleFile) .processedWith(new ComponentProcessor()) .compilesWithoutError() .and() .generatesSources(factoryFile); } @Test public void singleProducesMethodNoArgsFutureWithProducerName() { JavaFileObject moduleFile = JavaFileObjects.forSourceLines( "test.TestModule", "package test;", "", "import com.google.common.util.concurrent.Futures;", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.producers.ProducerModule;", "import dagger.producers.Produces;", "", "@ProducerModule", "final class TestModule {", " @Produces ListenableFuture produceString() {", " return Futures.immediateFuture(\"\");", " }", "}"); JavaFileObject factoryFile = JavaFileObjects.forSourceLines( "TestModule_ProduceStringFactory", "package test;", "", GeneratedLines.generatedImports( "import com.google.common.util.concurrent.Futures;", "import com.google.common.util.concurrent.ListenableFuture;", "import dagger.producers.internal.AbstractProducesMethodProducer;", "import dagger.producers.monitoring.ProducerToken;", "import dagger.producers.monitoring.ProductionComponentMonitor;", "import java.util.concurrent.Executor;", "import javax.inject.Provider;"), "", GeneratedLines.generatedAnnotationsWithoutSuppressWarnings(), "@SuppressWarnings({\"FutureReturnValueIgnored\", \"unchecked\", \"rawtypes\"})", "public final class TestModule_ProduceStringFactory", " extends AbstractProducesMethodProducer {", " private final TestModule module;", "", " private TestModule_ProduceStringFactory(", " TestModule module,", " Provider executorProvider,", " Provider productionComponentMonitorProvider) {", " super(", " productionComponentMonitorProvider,", " ProducerToken.create(\"test.TestModule#produceString\"),", " executorProvider);", " this.module = module;", " }", "", " public static TestModule_ProduceStringFactory create(", " TestModule module,", " Provider executorProvider,", " Provider productionComponentMonitorProvider) {", " return new TestModule_ProduceStringFactory(", " module, executorProvider, productionComponentMonitorProvider);", " }", "", " @Override protected ListenableFuture collectDependencies() {", " return Futures.immediateFuture(null);", " }", "", " @Override public ListenableFuture callProducesMethod(Void ignoredVoidArg) {", " return module.produceString();", " }", "}"); assertAbout(javaSource()) .that(moduleFile) .withCompilerOptions("-Adagger.writeProducerNameInToken=ENABLED") .processedWith(new ComponentProcessor()) .compilesWithoutError() .and() .generatesSources(factoryFile); } @Test public void producesMethodMultipleQualifiersOnMethod() { assertThatProductionModuleMethod( "@Produces @QualifierA @QualifierB static String produceString() { return null; }") .importing(ListenableFuture.class, QualifierA.class, QualifierB.class) .hasError("may not use more than one @Qualifier"); } @Test public void producesMethodMultipleQualifiersOnParameter() { assertThatProductionModuleMethod( "@Produces static String produceString(@QualifierA @QualifierB Object input) " + "{ return null; }") .importing(ListenableFuture.class, QualifierA.class, QualifierB.class) .hasError("may not use more than one @Qualifier"); } @Test public void producesMethodWildcardDependency() { assertThatProductionModuleMethod( "@Produces static String produceString(Provider numberProvider) " + "{ return null; }") .importing(ListenableFuture.class, QualifierA.class, QualifierB.class) .hasError( "Dagger does not support injecting Provider, Lazy, Producer, or Produced " + "when T is a wildcard type such as ? extends java.lang.Number"); } @Qualifier @Retention(RUNTIME) public @interface QualifierA {} @Qualifier @Retention(RUNTIME) public @interface QualifierB {} }