blob: 7cf317df79599c46d60ca7208dbb47455112a862 [file] [log] [blame]
/**
* Copyright (C) 2011 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 com.google.inject;
import static com.google.common.collect.ImmutableList.of;
import static com.google.inject.Asserts.assertContains;
import static com.google.inject.name.Names.named;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.inject.matcher.AbstractMatcher;
import com.google.inject.matcher.Matcher;
import com.google.inject.matcher.Matchers;
import com.google.inject.name.Named;
import com.google.inject.spi.DependencyAndSource;
import com.google.inject.spi.InstanceBinding;
import com.google.inject.spi.ProvisionListener;
import com.google.inject.util.Providers;
import junit.framework.TestCase;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Tests for {@link Binder#bindListener(Matcher, ProvisionListener...)}
*
* @author sameb@google.com (Sam Berlin)
*/
// TODO(sameb): Add some tests for private modules & child injectors.
public class ProvisionListenerTest extends TestCase {
public void testExceptionInListenerBeforeProvisioning() {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), new FailBeforeProvision());
}
});
try {
injector.getInstance(Foo.class);
fail();
} catch(ProvisionException pe) {
assertEquals(1, pe.getErrorMessages().size());
assertContains(pe.getMessage(),
"1) Error notifying ProvisionListener " + FailBeforeProvision.class.getName()
+ " of " + Foo.class.getName(),
"Reason: java.lang.RuntimeException: boo",
"while locating " + Foo.class.getName());
assertEquals("boo", pe.getCause().getMessage());
}
}
public void testExceptionInListenerAfterProvisioning() {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), new FailAfterProvision());
}
});
try {
injector.getInstance(Foo.class);
fail();
} catch(ProvisionException pe) {
assertEquals(1, pe.getErrorMessages().size());
assertContains(pe.getMessage(),
"1) Error notifying ProvisionListener " + FailAfterProvision.class.getName()
+ " of " + Foo.class.getName(),
"Reason: java.lang.RuntimeException: boo",
"while locating " + Foo.class.getName());
assertEquals("boo", pe.getCause().getMessage());
}
}
public void testExceptionInProvisionExplicitlyCalled() {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), new JustProvision());
}
});
try {
injector.getInstance(FooBomb.class);
fail();
} catch(ProvisionException pe) {
assertEquals(1, pe.getErrorMessages().size());
assertContains(pe.getMessage(),
"1) Error injecting constructor, java.lang.RuntimeException: Retry, Abort, Fail",
" at " + FooBomb.class.getName(),
" while locating " + FooBomb.class.getName());
assertEquals("Retry, Abort, Fail", pe.getCause().getMessage());
}
}
public void testExceptionInProvisionAutomaticallyCalled() {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), new NoProvision());
}
});
try {
injector.getInstance(FooBomb.class);
fail();
} catch(ProvisionException pe) {
assertEquals(1, pe.getErrorMessages().size());
assertContains(pe.getMessage(),
"1) Error injecting constructor, java.lang.RuntimeException: Retry, Abort, Fail",
" at " + FooBomb.class.getName(),
" while locating " + FooBomb.class.getName());
assertEquals("Retry, Abort, Fail", pe.getCause().getMessage());
}
}
public void testListenerCallsProvisionTwice() {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), new ProvisionTwice());
}
});
try {
injector.getInstance(Foo.class);
fail();
} catch(ProvisionException pe) {
assertEquals(1, pe.getErrorMessages().size());
assertContains(pe.getMessage(),
"1) Error notifying ProvisionListener " + ProvisionTwice.class.getName()
+ " of " + Foo.class.getName(),
"Reason: java.lang.IllegalStateException: Already provisioned in this listener.",
"while locating " + Foo.class.getName());
assertEquals("Already provisioned in this listener.", pe.getCause().getMessage());
}
}
public void testCachedInScopePreventsProvisionNotify() {
final Counter count1 = new Counter();
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), count1);
bind(Foo.class).in(Scopes.SINGLETON);
}
});
Foo foo = injector.getInstance(Foo.class);
assertNotNull(foo);
assertEquals(1, count1.count);
// not notified the second time because nothing is provisioned
// (it's cached in the scope)
count1.count = 0;
assertSame(foo, injector.getInstance(Foo.class));
assertEquals(0, count1.count);
}
public void testCombineAllBindListenerCalls() {
final Counter count1 = new Counter();
final Counter count2 = new Counter();
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), count1);
bindListener(Matchers.any(), count2);
}
});
assertNotNull(injector.getInstance(Foo.class));
assertEquals(1, count1.count);
assertEquals(1, count2.count);
}
public void testNotifyEarlyListenersIfFailBeforeProvision() {
final Counter count1 = new Counter();
final Counter count2 = new Counter();
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), count1, new FailBeforeProvision(), count2);
}
});
try {
injector.getInstance(Foo.class);
fail();
} catch(ProvisionException pe) {
assertEquals(1, pe.getErrorMessages().size());
assertContains(pe.getMessage(),
"1) Error notifying ProvisionListener " + FailBeforeProvision.class.getName()
+ " of " + Foo.class.getName(),
"Reason: java.lang.RuntimeException: boo",
"while locating " + Foo.class.getName());
assertEquals("boo", pe.getCause().getMessage());
assertEquals(1, count1.count);
assertEquals(0, count2.count);
}
}
public void testNotifyLaterListenersIfFailAfterProvision() {
final Counter count1 = new Counter();
final Counter count2 = new Counter();
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), count1, new FailAfterProvision(), count2);
}
});
try {
injector.getInstance(Foo.class);
fail();
} catch(ProvisionException pe) {
assertEquals(1, pe.getErrorMessages().size());
assertContains(pe.getMessage(),
"1) Error notifying ProvisionListener " + FailAfterProvision.class.getName()
+ " of " + Foo.class.getName(),
"Reason: java.lang.RuntimeException: boo",
"while locating " + Foo.class.getName());
assertEquals("boo", pe.getCause().getMessage());
assertEquals(1, count1.count);
assertEquals(1, count2.count);
}
}
public void testNotifiedKeysOfAllBindTypes() {
final Capturer capturer = new Capturer();
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), capturer);
bind(Foo.class).annotatedWith(named("pk")).toProvider(FooP.class);
try {
bind(Foo.class).annotatedWith(named("cxtr")).toConstructor(Foo.class.getDeclaredConstructor());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
bind(LinkedFoo.class).to(Foo.class);
bind(Interface.class).toInstance(new Implementation());
bindConstant().annotatedWith(named("constant")).to("MyConstant");
}
@Provides @Named("pi") Foo provideFooBar() {
return new Foo();
}
});
// toInstance & constant bindings are notified in random order, at the very beginning.
assertEquals(
ImmutableSet.of(Key.get(Interface.class), Key.get(String.class, named("constant"))),
capturer.getAsSetAndClear());
// simple binding
assertNotNull(injector.getInstance(Foo.class));
assertEquals(of(Key.get(Foo.class)), capturer.getAndClear());
// provider key binding -- notifies about provider & the object, always
assertNotNull(injector.getInstance(Key.get(Foo.class, named("pk"))));
assertEquals(of(Key.get(FooP.class), Key.get(Foo.class, named("pk"))), capturer.getAndClear());
assertNotNull(injector.getInstance(Key.get(Foo.class, named("pk"))));
assertEquals(of(Key.get(FooP.class), Key.get(Foo.class, named("pk"))), capturer.getAndClear());
// JIT provider key binding -- notifies about provider & the object, always
assertNotNull(injector.getInstance(JitFoo2.class));
assertEquals(of(Key.get(JitFoo2P.class), Key.get(JitFoo2.class)), capturer.getAndClear());
assertNotNull(injector.getInstance(JitFoo2.class));
assertEquals(of(Key.get(JitFoo2P.class), Key.get(JitFoo2.class)), capturer.getAndClear());
// provider instance binding -- just the object (not the provider)
assertNotNull(injector.getInstance(Key.get(Foo.class, named("pi"))));
assertEquals(of(Key.get(Foo.class, named("pi"))), capturer.getAndClear());
// toConstructor binding
assertNotNull(injector.getInstance(Key.get(Foo.class, named("cxtr"))));
assertEquals(of(Key.get(Foo.class, named("cxtr"))), capturer.getAndClear());
// linked binding -- notifies about the target (that's what's provisioned), not the link
assertNotNull(injector.getInstance(LinkedFoo.class));
assertEquals(of(Key.get(Foo.class)), capturer.getAndClear());
// JIT linked binding -- notifies about the target (that's what's provisioned), not the link
assertNotNull(injector.getInstance(JitFoo.class));
assertEquals(of(Key.get(Foo.class)), capturer.getAndClear());
}
public void testSingletonMatcher() {
final Counter counter = new Counter();
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(new AbstractMatcher<Binding<?>>() {
@Override
public boolean matches(Binding<?> t) {
return Scopes.isSingleton(t);
}
}, counter);
}
});
assertEquals(0, counter.count);
// no increment for getting Many.
injector.getInstance(Many.class);
assertEquals(0, counter.count);
// but an increment for getting Sole, since it's a singleton.
injector.getInstance(Sole.class);
assertEquals(1, counter.count);
}
public void testCallingBindingDotGetProviderDotGet() {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), new ProvisionListener() {
@Override
public <T> void onProvision(ProvisionInvocation<T> provision) {
provision.getBinding().getProvider().get(); // AGH!
}
});
}
});
try {
injector.getInstance(Sole.class);
fail();
} catch(ProvisionException expected) {
// We don't really care what kind of error you get, we only care you get an error.
}
try {
injector.getInstance(Many.class);
fail();
} catch(ProvisionException expected) {
// We don't really care what kind of error you get, we only care you get an error.
}
}
interface Interface {}
class Implementation implements Interface {}
@Singleton static class Sole {}
static class Many {}
@ImplementedBy(Foo.class) static interface JitFoo {}
@ProvidedBy(JitFoo2P.class) static class JitFoo2 {}
static interface LinkedFoo {}
static class Foo implements JitFoo, LinkedFoo {}
static class FooP implements Provider<Foo> {
public Foo get() {
return new Foo();
}
}
static class JitFoo2P implements Provider<JitFoo2> {
public JitFoo2 get() {
return new JitFoo2();
}
}
static class FooBomb {
FooBomb() {
throw new RuntimeException("Retry, Abort, Fail");
}
}
private static class Counter implements ProvisionListener {
int count = 0;
public <T> void onProvision(ProvisionInvocation<T> provision) {
count++;
}
}
private static class Capturer implements ProvisionListener {
List<Key> keys = Lists.newArrayList();
public <T> void onProvision(ProvisionInvocation<T> provision) {
keys.add(provision.getBinding().getKey());
T provisioned = provision.provision();
// InstanceBindings are the only kind of binding where the key can
// be an instanceof the provisioned, because it isn't linked to any
// direct implementation. I guess maybe it'd also be possible
// with a toConstructor binding... but we don't use that in our tests.
if (provision.getBinding() instanceof InstanceBinding) {
Class<? super T> expected = provision.getBinding().getKey().getRawType();
assertTrue("expected instanceof: " + expected + ", but was: " + provisioned,
expected.isInstance(provisioned));
} else {
assertEquals(provision.getBinding().getKey().getRawType(), provisioned.getClass());
}
}
Set<Key> getAsSetAndClear() {
Set<Key> copy = ImmutableSet.copyOf(keys);
keys.clear();
return copy;
}
List<Key> getAndClear() {
List<Key> copy = ImmutableList.copyOf(keys);
keys.clear();
return copy;
}
}
private static class FailBeforeProvision implements ProvisionListener {
public <T> void onProvision(ProvisionInvocation<T> provision) {
throw new RuntimeException("boo");
}
}
private static class FailAfterProvision implements ProvisionListener {
public <T> void onProvision(ProvisionInvocation<T> provision) {
provision.provision();
throw new RuntimeException("boo");
}
}
private static class JustProvision implements ProvisionListener {
public <T> void onProvision(ProvisionInvocation<T> provision) {
provision.provision();
}
}
private static class NoProvision implements ProvisionListener {
public <T> void onProvision(ProvisionInvocation<T> provision) {
}
}
private static class ProvisionTwice implements ProvisionListener {
public <T> void onProvision(ProvisionInvocation<T> provision) {
provision.provision();
provision.provision();
}
}
private static class ChainAsserter implements ProvisionListener {
private final List<Class<?>> provisionList;
private final List<Class<?>> expected;
public ChainAsserter(List<Class<?>> provisionList, Iterable<Class<?>> expected) {
this.provisionList = provisionList;
this.expected = ImmutableList.copyOf(expected);
}
public <T> void onProvision(ProvisionInvocation<T> provision) {
List<Class<?>> actual = Lists.newArrayList();
for (DependencyAndSource dep : provision.getDependencyChain()) {
actual.add(dep.getDependency().getKey().getRawType());
}
assertEquals(expected, actual);
provisionList.add(provision.getBinding().getKey().getRawType());
}
}
private static Matcher<Binding<?>> keyMatcher(final Class<?> clazz) {
return new AbstractMatcher<Binding<?>>() {
@Override
public boolean matches(Binding<?> t) {
return t.getKey().equals(Key.get(clazz));
}
};
}
@SuppressWarnings("unchecked")
public void testDependencyChain() {
final List<Class<?>> pList = Lists.newArrayList();
final List<Class<?>> totalList = Lists.newArrayList();
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(Instance.class).toInstance(new Instance());
bind(B.class).to(BImpl.class);
bind(D.class).toProvider(DP.class);
bindListener(Matchers.any(), new ProvisionListener() {
public <T> void onProvision(ProvisionInvocation<T> provision) {
totalList.add(provision.getBinding().getKey().getRawType());
}
});
// Build up a list of asserters for our dependency chains.
ImmutableList.Builder<Class<?>> chain = ImmutableList.builder();
chain.add(Instance.class);
bindListener(keyMatcher(Instance.class), new ChainAsserter(pList, chain.build()));
chain.add(A.class);
bindListener(keyMatcher(A.class), new ChainAsserter(pList, chain.build()));
chain.add(B.class).add(BImpl.class);
bindListener(keyMatcher(BImpl.class), new ChainAsserter(pList, chain.build()));
chain.add(C.class);
bindListener(keyMatcher(C.class), new ChainAsserter(pList, chain.build()));
// the chain has D before DP even though DP is provisioned & notified first
// because we do DP because of D, and need DP to provision D.
chain.add(D.class).add(DP.class);
bindListener(keyMatcher(D.class), new ChainAsserter(pList, chain.build()));
bindListener(keyMatcher(DP.class), new ChainAsserter(pList, chain.build()));
chain.add(E.class);
bindListener(keyMatcher(E.class), new ChainAsserter(pList, chain.build()));
chain.add(F.class);
bindListener(keyMatcher(F.class), new ChainAsserter(pList, chain.build()));
}
@Provides C c(D d) {
return new C() {};
}
});
Instance instance = injector.getInstance(Instance.class);
// make sure we're checking all of the chain asserters..
assertEquals(
of(Instance.class, A.class, BImpl.class, C.class, DP.class, D.class, E.class, F.class),
pList);
// and make sure that nothing else was notified that we didn't expect.
assertEquals(totalList, pList);
}
public void testModuleRequestInjection() {
final AtomicBoolean notified = new AtomicBoolean();
Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
requestInjection(new Object() {
@Inject Foo foo;
});
bindListener(Matchers.any(),
new SpecialChecker(Foo.class, getClass().getName() + ".configure(", notified));
}
});
assertTrue(notified.get());
}
public void testToProviderInstance() {
final AtomicBoolean notified = new AtomicBoolean();
Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(Object.class).toProvider(new Provider<Object>() {
@Inject Foo foo;
public Object get() {
return null;
}
});
bindListener(Matchers.any(),
new SpecialChecker(Foo.class, getClass().getName() + ".configure(", notified));
}
});
assertTrue(notified.get());
}
public void testInjectorInjectMembers() {
final Object object = new Object() {
@Inject Foo foo;
};
final AtomicBoolean notified = new AtomicBoolean();
Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(),
new SpecialChecker(Foo.class, object.getClass().getName(), notified));
}
}).injectMembers(object);
assertTrue(notified.get());
}
private static class SpecialChecker implements ProvisionListener {
private final Class<?> notifyType;
private final String firstSource;
private final AtomicBoolean notified;
public SpecialChecker(Class<?> notifyType, String firstSource, AtomicBoolean notified) {
this.notifyType = notifyType;
this.firstSource = firstSource;
this.notified = notified;
}
public <T> void onProvision(ProvisionInvocation<T> provision) {
notified.set(true);
assertEquals(notifyType, provision.getBinding().getKey().getRawType());
assertEquals(2, provision.getDependencyChain().size());
assertEquals(null, provision.getDependencyChain().get(0).getDependency());
assertContains(provision.getDependencyChain().get(0).getBindingSource(), firstSource);
assertEquals(notifyType,
provision.getDependencyChain().get(1).getDependency().getKey().getRawType());
assertContains(provision.getDependencyChain().get(1).getBindingSource(),
notifyType.getName() + ".class(");
}
}
private static class Instance {
@Inject A a;
}
private static class A {
@Inject A(B b) {}
}
private interface B {}
private static class BImpl implements B {
@Inject void inject(C c) {}
}
private interface C {}
private interface D {}
private static class DP implements Provider<D> {
@Inject Provider<E> ep;
public D get() {
ep.get();
return new D() {};
}
}
private static class E {
@SuppressWarnings("unused")
@Inject F f;
}
private static class F {
}
public void testBindToInjectorWithListeningGivesSaneException() {
try {
Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), new Counter());
bind(Injector.class).toProvider(Providers.<Injector>of(null));
}
});
fail();
} catch (CreationException ce) {
assertContains(
ce.getMessage(), "Binding to core guice framework type is not allowed: Injector.");
}
}
public void testProvisionIsNotifiedAfterContextsClear() {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bindListener(Matchers.any(), new ProvisionListener() {
@Override
public <T> void onProvision(ProvisionInvocation<T> provision) {
Object provisioned = provision.provision();
if (provisioned instanceof X) {
((X)provisioned).init();
} else if (provisioned instanceof Y) {
X.createY = false;
((Y)provisioned).init();
}
}
});
}
});
X.createY = true;
X x = injector.getInstance(X.class);
assertNotSame(x, x.y.x);
assertFalse("x.ID: " + x.ID + ", x.y.x.iD: " + x.y.x.ID, x.ID == x.y.x.ID);
}
private static class X {
final static AtomicInteger COUNTER = new AtomicInteger();
static boolean createY;
final int ID = COUNTER.getAndIncrement();
final Provider<Y> yProvider;
Y y;
@Inject X(Provider<Y> yProvider) {
this.yProvider = yProvider;
}
void init() {
if (createY) {
this.y = yProvider.get();
}
}
}
private static class Y {
final Provider<X> xProvider;
X x;
@Inject Y(Provider<X> xProvider) {
this.xProvider = xProvider;
}
void init() {
this.x = xProvider.get();
}
}
}