/* * Copyright 2016 Kejun Xia * * 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.shipdream.lib.poke; import com.shipdream.lib.poke.exception.CircularDependenciesException; import com.shipdream.lib.poke.exception.PokeException; import com.shipdream.lib.poke.exception.ProvideException; import com.shipdream.lib.poke.exception.ProviderConflictException; import com.shipdream.lib.poke.exception.ProviderMissingException; import com.shipdream.lib.poke.Provider.DereferenceListener; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import javax.inject.Singleton; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @SuppressWarnings("unchecked") public class TestInjectionReferenceCount extends BaseTestCases { interface Fruit {} static class Apple implements Fruit { } interface Container {} static class Fridge implements Container { @MyInject private Fruit a; @MyInject private Fruit b; } static class House { @MyInject private Container container; } private Graph graph; private Component component; @Before public void setUp() throws Exception { graph = new Graph(); component = new Component("AppSingleton"); component.register(new TestModule()); graph.setRootComponent(component); } @Test public void unscopedProvidesShouldHaveDifferentInstances() throws PokeException { class Module1 { @Provides public Fruit providesFruit() { return new Apple(); } @Provides public Container providesContainer() { return new Fridge(); } } Graph g = new Graph(); Component c; c = new Component(false); c.register(new Module1()); g.setRootComponent(c); House house = new House(); g.inject(house, MyInject.class); Assert.assertNotNull(house.container); Assert.assertNotNull(((Fridge) house.container).a); Assert.assertNotNull(((Fridge) house.container).b); Assert.assertTrue(((Fridge) house.container).a != ((Fridge) house.container).b); } static class TestModule { @Singleton @Provides public Fruit providesFruit() { return new Apple(); } @Singleton @Provides public Container providesContainer() { return new Fridge(); } } @Test public void referenceCountCanReduceCascadinglyFromRoot() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { House house = new House(); graph.inject(house, MyInject.class); Assert.assertNotNull(house.container); Assert.assertNotNull(((Fridge) house.container).a); Assert.assertNotNull(((Fridge) house.container).b); Assert.assertTrue(((Fridge) house.container).a == ((Fridge) house.container).b); Provider<Container> containerProvider = component.findProvider(Container.class, null); Provider<Fruit> fruitProvider = component.findProvider(Fruit.class, null); Assert.assertTrue(containerProvider.getReferenceCount() == 1); Assert.assertTrue(fruitProvider.getReferenceCount() == 2); graph.release(house, MyInject.class); Assert.assertTrue(component.scopeCache.getCachedInstances().isEmpty()); } static class Mansion extends House { @MyInject private Container container; } @Test public void should_be_able_to_release_inherited_fields_with_same_name() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { Mansion mansion = new Mansion(); graph.inject(mansion, MyInject.class); Assert.assertNotNull(mansion.container); Assert.assertNotNull(((Fridge) mansion.container).a); Assert.assertNotNull(((Fridge) mansion.container).b); Assert.assertTrue(((Fridge) mansion.container).a == ((Fridge) mansion.container).b); Provider<Container> containerProvider = component.findProvider(Container.class, null); Provider<Fruit> fruitProvider = component.findProvider(Fruit.class, null); //container has been referenced twice by the fields Mansion.container and Mansion.House.container Assert.assertEquals(2, containerProvider.getReferenceCount()); Assert.assertEquals(4, fruitProvider.getReferenceCount()); graph.release(mansion, MyInject.class); Assert.assertTrue(component.scopeCache.getCachedInstances().isEmpty()); } static class Kitchen { @MyInject private Container container; @MyInject private Fruit aOnFloor; @MyInject private Fruit bOnFloor; } @Test public void referenceCountCanReduceCascadinglyFromSubNode() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { Kitchen kitchen = new Kitchen(); graph.inject(kitchen, MyInject.class); Fridge fridge = (Fridge) kitchen.container; Assert.assertNotNull(kitchen.container); Assert.assertNotNull(fridge.a); Assert.assertNotNull(fridge.b); Assert.assertTrue((fridge).a == fridge.b); Assert.assertTrue(kitchen.aOnFloor == kitchen.bOnFloor); Provider<Container> containerProvider = component.findProvider(Container.class, null); Provider<Fruit> fruitProvider = component.findProvider(Fruit.class, null); Assert.assertEquals(1, containerProvider.getReferenceCount()); Assert.assertEquals(4, fruitProvider.getReferenceCount()); graph.release(kitchen.container, MyInject.class); Assert.assertNotNull(kitchen.container); Assert.assertNotNull(kitchen.aOnFloor); Assert.assertNotNull(kitchen.bOnFloor); Assert.assertNotNull(fridge.a); Assert.assertNotNull(fridge.b); Assert.assertEquals(1, containerProvider.getReferenceCount()); Assert.assertEquals(2, fruitProvider.getReferenceCount()); graph.release(kitchen.container, MyInject.class); Assert.assertNotNull(fridge.a); Assert.assertNotNull(fridge.b); Assert.assertEquals(1, containerProvider.getReferenceCount()); Assert.assertEquals(2, fruitProvider.getReferenceCount()); } @Test public void releaseInjectedFieldsShouldSetThemNull() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { Kitchen kitchen = new Kitchen(); graph.inject(kitchen, MyInject.class); Provider<Container> containerProvider = component.findProvider(Container.class, null); Provider<Fruit> fruitProvider = component.findProvider(Fruit.class, null); graph.release(kitchen.container, MyInject.class); Assert.assertEquals(1, containerProvider.getReferenceCount()); Assert.assertEquals(2, fruitProvider.getReferenceCount()); graph.release(kitchen.container, MyInject.class); Assert.assertNotNull(kitchen.aOnFloor); Assert.assertNotNull(kitchen.bOnFloor); Assert.assertTrue(containerProvider.getReferenceCount() == 1); Assert.assertTrue(fruitProvider.getReferenceCount() == 2); graph.release(kitchen, MyInject.class); Assert.assertTrue(component.scopeCache.getCachedInstances().isEmpty()); } @Test public void should_invoke_on_freed_callback_when_providers_are_freed_from_child_node() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { //As mockito can't mock annotation so we need a proxy to stub it class OnCacheFreedProxy { public void onFreed(Class<?> type) {} } Kitchen kitchen = new Kitchen(); graph.inject(kitchen, MyInject.class); final OnCacheFreedProxy proxy = mock(OnCacheFreedProxy.class); Provider.DereferenceListener onFreed = new DereferenceListener() { @Override public <T> void onDereferenced(Provider<T> provider, T instance) { if (provider.getReferenceCount() == 0) { proxy.onFreed(provider.type()); } } }; graph.registerDereferencedListener(onFreed); //Assert //Releasing fruits are still held by kitchen, so no callback should be seen, though //reference count should be decreased graph.release(kitchen.container, MyInject.class); verify(proxy, times(0)).onFreed(eq(Container.class)); verify(proxy, times(0)).onFreed(eq(Fruit.class)); //Releasing fruits are still held by kitchen, so no callback should be seen. graph.release(kitchen.container, MyInject.class); verify(proxy, times(0)).onFreed(eq(Container.class)); verify(proxy, times(0)).onFreed(eq(Fruit.class)); graph.release(kitchen, MyInject.class); verify(proxy, times(1)).onFreed(eq(Container.class)); verify(proxy, times(1)).onFreed(eq(Fruit.class)); } @Test public void should_invoke_on_freed_callback_when_providers_are_freed_from_root_node() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { //As mockito can't mock annotation so we need a proxy to stub it class OnCacheFreedProxy { public void onFreed(Class<?> type) {} } Kitchen kitchen = new Kitchen(); graph.inject(kitchen, MyInject.class); final OnCacheFreedProxy proxy = mock(OnCacheFreedProxy.class); DereferenceListener onFreed = new Provider.DereferenceListener() { @Override public <T> void onDereferenced(Provider<T> provider, T instance) { if (provider.getReferenceCount() == 0) { proxy.onFreed(provider.type()); } } }; graph.registerDereferencedListener(onFreed); //Assert //Releasing root should free providers and invoke callbacks graph.release(kitchen, MyInject.class); verify(proxy, times(1)).onFreed(eq(Container.class)); verify(proxy, times(1)).onFreed(eq(Fruit.class)); } @Test public void should_not_invoke_on_freed_callback_when_freedListeners_are_unregistered() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { //As mockito can't mock annotation so we need a proxy to stub it class OnCacheFreedProxy { public void onFreed(Class<?> type) {} } Kitchen kitchen = new Kitchen(); graph.inject(kitchen, MyInject.class); final OnCacheFreedProxy proxy = mock(OnCacheFreedProxy.class); DereferenceListener onFreed = new Provider.DereferenceListener() { @Override public <T> void onDereferenced(Provider<T> provider, T instance) { if (provider.getReferenceCount() == 0) { proxy.onFreed(provider.type()); } } }; graph.registerDereferencedListener(onFreed); graph.unregisterDereferencedListener(onFreed); //Assert graph.release(kitchen, MyInject.class); verify(proxy, times(0)).onFreed(eq(Container.class)); verify(proxy, times(0)).onFreed(eq(Fruit.class)); } @Test public void should_not_invoke_on_freed_callback_when_freedListeners_are_cleared() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { //As mockito can't mock annotation so we need a proxy to stub it class OnCacheFreedProxy { public void onFreed(Class<?> type) {} } Kitchen kitchen = new Kitchen(); graph.inject(kitchen, MyInject.class); final OnCacheFreedProxy proxy = mock(OnCacheFreedProxy.class); DereferenceListener onFreed = new Provider.DereferenceListener() { @Override public <T> void onDereferenced(Provider<T> provider, T instance) { if (provider.getReferenceCount() == 0) { proxy.onFreed(provider.type()); } } }; graph.registerDereferencedListener(onFreed); graph.clearDereferencedListeners(); //Assert graph.release(kitchen, MyInject.class); verify(proxy, times(0)).onFreed(eq(Container.class)); verify(proxy, times(0)).onFreed(eq(Fruit.class)); } @Test public void should_invoke_call_backs_of_registered_graph_monitors() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { //As mockito can't mock annotation so we need a proxy to stub it class MonitorProxy { public void onInject(Object target) {} public void onRelease(Object target) {} } final MonitorProxy proxy = mock(MonitorProxy.class); Graph.Monitor monitor = new Graph.Monitor() { @Override public void onInject(Object intoTarget) { proxy.onInject(intoTarget); } @Override public void onRelease(Object fromTarget) { proxy.onRelease(fromTarget); } }; graph.registerMonitor(monitor); Kitchen kitchen = new Kitchen(); //Act graph.inject(kitchen, MyInject.class); //Assert verify(proxy, times(1)).onInject(kitchen); reset(proxy); //Act graph.release(kitchen, MyInject.class); //Assert verify(proxy, times(1)).onRelease(kitchen); } @Test public void should_not_invoke_call_backs_of_unregistered_graph_monitors() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { //As mockito can't mock annotation so we need a proxy to stub it class MonitorProxy { public void onInject(Object target) {} public void onRelease(Object target) {} } final MonitorProxy proxy = mock(MonitorProxy.class); Graph.Monitor monitor = new Graph.Monitor() { @Override public void onInject(Object intoTarget) { proxy.onInject(intoTarget); } @Override public void onRelease(Object fromTarget) { proxy.onRelease(fromTarget); } }; graph.registerMonitor(monitor); Kitchen kitchen = new Kitchen(); graph.unregisterMonitor(monitor); //Act graph.inject(kitchen, MyInject.class); //Assert verify(proxy, times(0)).onInject(kitchen); reset(proxy); //Act graph.release(kitchen, MyInject.class); //Assert verify(proxy, times(0)).onRelease(kitchen); } @Test public void should_not_invoke_call_backs_after_clear_graph_monitors() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { //As mockito can't mock annotation so we need a proxy to stub it class MonitorProxy { public void onInject(Object target) {} public void onRelease(Object target) {} } final MonitorProxy proxy = mock(MonitorProxy.class); Graph.Monitor monitor = new Graph.Monitor() { @Override public void onInject(Object intoTarget) { proxy.onInject(intoTarget); } @Override public void onRelease(Object fromTarget) { proxy.onRelease(fromTarget); } }; graph.registerMonitor(monitor); Kitchen kitchen = new Kitchen(); graph.clearMonitors(); //Act graph.inject(kitchen, MyInject.class); //Assert verify(proxy, times(0)).onInject(kitchen); reset(proxy); //Act graph.release(kitchen, MyInject.class); //Assert verify(proxy, times(0)).onRelease(kitchen); } static class Orange implements Fruit { @MyInject private Container container; } static class TestComp3 { @Singleton @Provides public Fruit providesFruit() { return new Orange(); } @Singleton @Provides public Container providesContainer() { return new Fridge(); } } @Test public void should_hold_provider_until_no_objects_is_referencing_to_it() throws ProvideException, ProviderConflictException, CircularDependenciesException, ProviderMissingException { Kitchen kitchen1 = new Kitchen(); graph.inject(kitchen1, MyInject.class); Fridge fridge1 = (Fridge) kitchen1.container; Kitchen kitchen2 = new Kitchen(); graph.inject(kitchen2, MyInject.class); Fridge fridge2 = (Fridge) kitchen2.container; Assert.assertEquals(fridge1, fridge2); Assert.assertNotNull(kitchen1); Assert.assertNotNull(kitchen1.container); Assert.assertNotNull(kitchen1.aOnFloor); Assert.assertNotNull(kitchen1.bOnFloor); Assert.assertNotNull(fridge1.a); Assert.assertNotNull(fridge1.b); Assert.assertNotNull(kitchen2); Assert.assertNotNull(kitchen2.container); Assert.assertNotNull(kitchen2.aOnFloor); Assert.assertNotNull(kitchen2.bOnFloor); Assert.assertNotNull(fridge2.a); Assert.assertNotNull(fridge2.b); graph.release(kitchen1.container, MyInject.class); Assert.assertNotNull(kitchen1.container); Assert.assertNotNull(kitchen1.aOnFloor); Assert.assertNotNull(kitchen1.bOnFloor); Assert.assertNotNull(fridge2.a); Assert.assertNotNull(fridge2.b); Assert.assertNotNull(fridge1.a); Assert.assertNotNull(fridge1.b); Assert.assertNotNull(kitchen2.container); Assert.assertNotNull(kitchen2.aOnFloor); Assert.assertNotNull(kitchen2.bOnFloor); Assert.assertNotNull(fridge2.a); Assert.assertNotNull(fridge2.b); } }