/* * Copyright (C) 2014 Google, Inc. * * 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 com.google.testing.compile.JavaFileObjects; import javax.tools.JavaFileObject; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import static com.google.common.truth.Truth.assert_; import static com.google.testing.compile.JavaSourcesSubjectFactory.javaSources; import static java.util.Arrays.asList; @RunWith(JUnit4.class) public class GraphValidationScopingTest { @Test public void componentWithoutScopeIncludesScopedBindings_Fail() { JavaFileObject componentFile = JavaFileObjects.forSourceLines("test.MyComponent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Component(modules = ScopedModule.class)", "interface MyComponent {", " ScopedType string();", "}"); JavaFileObject typeFile = JavaFileObjects.forSourceLines("test.ScopedType", "package test;", "", "import javax.inject.Inject;", "import javax.inject.Singleton;", "", "@Singleton", "class ScopedType {", " @Inject ScopedType(String s, long l, float f) {}", "}"); JavaFileObject moduleFile = JavaFileObjects.forSourceLines("test.ScopedModule", "package test;", "", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Singleton;", "", "@Module", "class ScopedModule {", " @Provides @Singleton String string() { return \"a string\"; }", " @Provides long integer() { return 0L; }", " @Provides float floatingPoint() { return 0.0f; }", "}"); String errorMessage = "test.MyComponent (unscoped) may not reference scoped bindings:\n" + " @Provides @Singleton String test.ScopedModule.string()\n" + " @Singleton class test.ScopedType"; assert_().about(javaSources()).that(asList(componentFile, typeFile, moduleFile)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(errorMessage); } @Test public void componentWithScopeIncludesIncompatiblyScopedBindings_Fail() { JavaFileObject componentFile = JavaFileObjects.forSourceLines("test.MyComponent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component(modules = ScopedModule.class)", "interface MyComponent {", " ScopedType string();", "}"); JavaFileObject scopeFile = JavaFileObjects.forSourceLines("test.PerTest", "package test;", "", "import javax.inject.Scope;", "", "@Scope", "@interface PerTest {}"); JavaFileObject typeFile = JavaFileObjects.forSourceLines("test.ScopedType", "package test;", "", "import javax.inject.Inject;", "", "@PerTest", // incompatible scope "class ScopedType {", " @Inject ScopedType(String s, long l, float f) {}", "}"); JavaFileObject moduleFile = JavaFileObjects.forSourceLines("test.ScopedModule", "package test;", "", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Singleton;", "", "@Module", "class ScopedModule {", " @Provides @PerTest String string() { return \"a string\"; }", // incompatible scope " @Provides long integer() { return 0L; }", // unscoped - valid " @Provides @Singleton float floatingPoint() { return 0.0f; }", // same scope - valid "}"); String errorMessage = "test.MyComponent scoped with @Singleton " + "may not reference bindings with different scopes:\n" + " @Provides @test.PerTest String test.ScopedModule.string()\n" + " @test.PerTest class test.ScopedType"; assert_().about(javaSources()).that(asList(componentFile, scopeFile, typeFile, moduleFile)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(errorMessage); } @Test public void componentWithScopeMayDependOnOnlyOneScopedComponent() { // If a scoped component will have dependencies, they must only include, at most, a single // scoped component JavaFileObject type = JavaFileObjects.forSourceLines("test.SimpleType", "package test;", "", "import javax.inject.Inject;", "", "class SimpleType {", " @Inject SimpleType() {}", " static class A { @Inject A() {} }", " static class B { @Inject B() {} }", "}"); JavaFileObject simpleScope = JavaFileObjects.forSourceLines("test.SimpleScope", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface SimpleScope {}"); JavaFileObject singletonScopedA = JavaFileObjects.forSourceLines("test.SingletonComponentA", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component", "interface SingletonComponentA {", " SimpleType.A type();", "}"); JavaFileObject singletonScopedB = JavaFileObjects.forSourceLines("test.SingletonComponentB", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component", "interface SingletonComponentB {", " SimpleType.B type();", "}"); JavaFileObject scopeless = JavaFileObjects.forSourceLines("test.ScopelessComponent", "package test;", "", "import dagger.Component;", "", "@Component", "interface ScopelessComponent {", " SimpleType type();", "}"); JavaFileObject simpleScoped = JavaFileObjects.forSourceLines("test.SimpleScopedComponent", "package test;", "", "import dagger.Component;", "", "@SimpleScope", "@Component(dependencies = {SingletonComponentA.class, SingletonComponentB.class})", "interface SimpleScopedComponent {", " SimpleType.A type();", "}"); String errorMessage = "@test.SimpleScope test.SimpleScopedComponent depends on more than one scoped component:\n" + " @Singleton test.SingletonComponentA\n" + " @Singleton test.SingletonComponentB"; assert_().about(javaSources()) .that( asList(type, simpleScope, simpleScoped, singletonScopedA, singletonScopedB, scopeless)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(errorMessage); } @Test public void componentWithoutScopeCannotDependOnScopedComponent() { JavaFileObject type = JavaFileObjects.forSourceLines("test.SimpleType", "package test;", "", "import javax.inject.Inject;", "", "class SimpleType {", " @Inject SimpleType() {}", "}"); JavaFileObject scopedComponent = JavaFileObjects.forSourceLines("test.ScopedComponent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component", "interface ScopedComponent {", " SimpleType type();", "}"); JavaFileObject unscopedComponent = JavaFileObjects.forSourceLines("test.UnscopedComponent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Component(dependencies = ScopedComponent.class)", "interface UnscopedComponent {", " SimpleType type();", "}"); String errorMessage = "test.UnscopedComponent (unscoped) cannot depend on scoped components:\n" + " @Singleton test.ScopedComponent"; assert_().about(javaSources()) .that(asList(type, scopedComponent, unscopedComponent)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(errorMessage); } @Test public void componentWithSingletonScopeMayNotDependOnOtherScope() { // Singleton must be the widest lifetime of present scopes. JavaFileObject type = JavaFileObjects.forSourceLines("test.SimpleType", "package test;", "", "import javax.inject.Inject;", "", "class SimpleType {", " @Inject SimpleType() {}", "}"); JavaFileObject simpleScope = JavaFileObjects.forSourceLines("test.SimpleScope", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface SimpleScope {}"); JavaFileObject simpleScoped = JavaFileObjects.forSourceLines("test.SimpleScopedComponent", "package test;", "", "import dagger.Component;", "", "@SimpleScope", "@Component", "interface SimpleScopedComponent {", " SimpleType type();", "}"); JavaFileObject singletonScoped = JavaFileObjects.forSourceLines("test.SingletonComponent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component(dependencies = SimpleScopedComponent.class)", "interface SingletonComponent {", " SimpleType type();", "}"); String errorMessage = "This @Singleton component cannot depend on scoped components:\n" + " @test.SimpleScope test.SimpleScopedComponent"; assert_().about(javaSources()) .that(asList(type, simpleScope, simpleScoped, singletonScoped)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(errorMessage); } @Test public void componentScopeAncestryMustNotCycle() { // The dependency relationship of components is necessarily from shorter lifetimes to // longer lifetimes. The scoping annotations must reflect this, and so one cannot declare // scopes on components such that they cycle. JavaFileObject type = JavaFileObjects.forSourceLines("test.SimpleType", "package test;", "", "import javax.inject.Inject;", "", "class SimpleType {", " @Inject SimpleType() {}", "}"); JavaFileObject scopeA = JavaFileObjects.forSourceLines("test.ScopeA", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface ScopeA {}"); JavaFileObject scopeB = JavaFileObjects.forSourceLines("test.ScopeB", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface ScopeB {}"); JavaFileObject longLifetime = JavaFileObjects.forSourceLines("test.ComponentLong", "package test;", "", "import dagger.Component;", "", "@ScopeA", "@Component", "interface ComponentLong {", " SimpleType type();", "}"); JavaFileObject mediumLifetime = JavaFileObjects.forSourceLines("test.ComponentMedium", "package test;", "", "import dagger.Component;", "", "@ScopeB", "@Component(dependencies = ComponentLong.class)", "interface ComponentMedium {", " SimpleType type();", "}"); JavaFileObject shortLifetime = JavaFileObjects.forSourceLines("test.ComponentShort", "package test;", "", "import dagger.Component;", "", "@ScopeA", "@Component(dependencies = ComponentMedium.class)", "interface ComponentShort {", " SimpleType type();", "}"); String errorMessage = "test.ComponentShort depends on scoped components in a non-hierarchical scope ordering:\n" + " @test.ScopeA test.ComponentLong\n" + " @test.ScopeB test.ComponentMedium\n" + " @test.ScopeA test.ComponentShort"; assert_().about(javaSources()) .that(asList(type, scopeA, scopeB, longLifetime, mediumLifetime, shortLifetime)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(errorMessage); } }