/* * 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.InstanceBinding; import com.google.inject.spi.ProvisionListener; import com.google.inject.util.Providers; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import junit.framework.TestCase; /** * 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 testExceptionInFieldProvision() throws Exception { final CountAndCaptureExceptionListener listener = new CountAndCaptureExceptionListener(); Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( new AbstractMatcher<Binding<?>>() { @Override public boolean matches(Binding<?> binding) { return binding.getKey().getRawType().equals(DependsOnFooBombInField.class); } }, listener); } }); assertEquals(0, listener.beforeProvision); String expectedMsg = null; try { injector.getInstance(DependsOnFooBombInField.class); fail(); } catch (ProvisionException expected) { assertEquals(1, expected.getErrorMessages().size()); expectedMsg = expected.getMessage(); assertContains( listener.capture.get().getMessage(), "1) Error injecting constructor, java.lang.RuntimeException: Retry, Abort, Fail", " at " + FooBomb.class.getName(), " while locating " + FooBomb.class.getName(), " while locating " + DependsOnFooBombInField.class.getName()); } assertEquals(1, listener.beforeProvision); assertEquals(expectedMsg, listener.capture.get().getMessage()); assertEquals(0, listener.afterProvision); } public void testExceptionInCxtorProvision() throws Exception { final CountAndCaptureExceptionListener listener = new CountAndCaptureExceptionListener(); Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( new AbstractMatcher<Binding<?>>() { @Override public boolean matches(Binding<?> binding) { return binding.getKey().getRawType().equals(DependsOnFooBombInCxtor.class); } }, listener); } }); assertEquals(0, listener.beforeProvision); String expectedMsg = null; try { injector.getInstance(DependsOnFooBombInCxtor.class); fail(); } catch (ProvisionException expected) { assertEquals(1, expected.getErrorMessages().size()); expectedMsg = expected.getMessage(); assertContains( listener.capture.get().getMessage(), "1) Error injecting constructor, java.lang.RuntimeException: Retry, Abort, Fail", " at " + FooBomb.class.getName(), " while locating " + FooBomb.class.getName(), " while locating " + DependsOnFooBombInCxtor.class.getName()); } assertEquals(1, listener.beforeProvision); assertEquals(expectedMsg, listener.capture.get().getMessage()); assertEquals(0, listener.afterProvision); } 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 {} static 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> { @Override public Foo get() { return new Foo(); } } static class JitFoo2P implements Provider<JitFoo2> { @Override public JitFoo2 get() { return new JitFoo2(); } } static class FooBomb { FooBomb() { throw new RuntimeException("Retry, Abort, Fail"); } } static class DependsOnFooBombInField { @Inject FooBomb fooBomb; } static class DependsOnFooBombInCxtor { @Inject DependsOnFooBombInCxtor(FooBomb fooBomb) {} } private static class Counter implements ProvisionListener { int count = 0; @Override public <T> void onProvision(ProvisionInvocation<T> provision) { count++; } } private static class CountAndCaptureExceptionListener implements ProvisionListener { int beforeProvision = 0; int afterProvision = 0; AtomicReference<RuntimeException> capture = new AtomicReference<RuntimeException>(); @Override public <T> void onProvision(ProvisionInvocation<T> provision) { beforeProvision++; try { provision.provision(); } catch (RuntimeException re) { capture.set(re); throw re; } afterProvision++; } } private static class Capturer implements ProvisionListener { List<Key> keys = Lists.newArrayList(); @Override 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 { @Override public <T> void onProvision(ProvisionInvocation<T> provision) { throw new RuntimeException("boo"); } } private static class FailAfterProvision implements ProvisionListener { @Override public <T> void onProvision(ProvisionInvocation<T> provision) { provision.provision(); throw new RuntimeException("boo"); } } private static class JustProvision implements ProvisionListener { @Override public <T> void onProvision(ProvisionInvocation<T> provision) { provision.provision(); } } private static class NoProvision implements ProvisionListener { @Override public <T> void onProvision(ProvisionInvocation<T> provision) {} } private static class ProvisionTwice implements ProvisionListener { @Override 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); } @Override public <T> void onProvision(ProvisionInvocation<T> provision) { List<Class<?>> actual = Lists.newArrayList(); for (com.google.inject.spi.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() { @Override 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; @Override 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; } @Override public <T> void onProvision(ProvisionInvocation<T> provision) { notified.set(true); assertEquals(notifyType, provision.getBinding().getKey().getRawType()); assertEquals(2, provision.getDependencyChain().size()); assertNull(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; @Override 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 { static final 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(); } } public void testDeDuplicateProvisionListeners() { final Counter counter = new Counter(); Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener(Matchers.any(), counter); bindListener(Matchers.any(), counter); } }); injector.getInstance(Many.class); assertEquals("ProvisionListener not de-duplicated", 1, counter.count); } }