/*
* 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);
}
}