/* * 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.android.mvc; import com.shipdream.lib.poke.Component; import com.shipdream.lib.poke.Consumer; import com.shipdream.lib.poke.Graph; import com.shipdream.lib.poke.Provider; import com.shipdream.lib.poke.Provides; import com.shipdream.lib.poke.exception.CircularDependenciesException; import com.shipdream.lib.poke.exception.ProvideException; import com.shipdream.lib.poke.exception.ProviderConflictException; import com.shipdream.lib.poke.exception.ProviderMissingException; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import javax.inject.Inject; import javax.inject.Qualifier; import javax.inject.Singleton; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class TestMvcGraph extends BaseTest{ interface Os { } @Qualifier @Documented @Retention(RUNTIME) @interface Apple { } @Qualifier @Documented @Retention(RUNTIME) @interface Google { } static class iOS implements Os { } static class Android implements Os { } static class DeviceModule { @Provides @Singleton public Os provide() { return new Android(); } @Provides @Singleton @Apple public Os provideIos() { return new iOS(); } @Provides @Singleton @Google public Os provideAndroid() { return new Android(); } } class Device { @Inject private Os android; @Inject @Apple private Os os; } @Override @Before public void setUp() throws Exception { super.setUp(); graph.uiThreadRunner = mock(UiThreadRunner.class); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(true); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Runnable runnable = (Runnable) invocation.getArguments()[0]; runnable.run(); return null; } }).when(graph.uiThreadRunner).post(any(Runnable.class)); } @Test public void should_be_able_to_post_on_default_ui_thread_runner() { new MvcGraph().uiThreadRunner.post(mock(Runnable.class)); new MvcGraph().uiThreadRunner.postDelayed(mock(Runnable.class), 100); } @Test public void should_throw_IllegalRootComponentException_on_graph_preparation_with_attached_component() throws Component.MultiParentException, ProviderConflictException { MvcComponent parent = new MvcComponent(""); MvcComponent child = new MvcComponent(""); parent.attach(child); boolean caught = false; try { graph.prepareInternalGraph(new Graph(), child); } catch (MvcGraphException e) { if (e.getCause() instanceof Graph.IllegalRootComponentException) { caught = true; } } Assert.assertTrue(caught); } @Test public void should_throw_ProviderConflictException_on_graph_preparation_by_duplicate_registration() throws Component.MultiParentException, ProviderConflictException, ProvideException { MvcComponent component = new MvcComponent(""); component.register(new Object() { @Provides public UiThreadRunner uiThreadRunner() { return mock(UiThreadRunner.class); } }); boolean caught = false; try { graph.prepareInternalGraph(new Graph(), component); } catch (MvcGraphException e) { if (e.getCause() instanceof ProviderConflictException) { caught = true; } } Assert.assertTrue(caught); } @Test public void use_method_should_retain_and_release_instance_without_qualifier_correctly() throws ProvideException, ProviderConflictException { graph.getRootComponent().register(new DeviceModule()); //OsReferenceCount = 0 graph.use(Os.class, new Consumer<Os>() { @Override public void consume(Os instance) { //First time to create the instance. //OsReferenceCount = 1 Assert.assertTrue(instance instanceof Android); } }); //Reference count decremented by use method automatically //OsReferenceCount = 0 final Device device = new Device(); graph.inject(device); //OsReferenceCount = 1 //New instance created and cached graph.use(Os.class, new Consumer<Os>() { @Override public void consume(Os instance) { //Since reference count is greater than 0, cached instance will be reused //OsReferenceCount = 2 Assert.assertTrue(device.android == instance); Assert.assertTrue(instance instanceof Android); } }); //Reference count decremented by use method automatically //OsReferenceCount = 1 graph.release(device); //OsReferenceCount = 0 //Last instance released, so next time a new instance will be created graph.use(Os.class, new Consumer<Os>() { @Override public void consume(Os instance) { //OsReferenceCount = 1 //Since the cached instance is cleared, the new instance is a newly created one. Assert.assertTrue(device.android != instance); Assert.assertTrue(instance instanceof Android); } }); //Reference count decremented by use method automatically //OsReferenceCount = 0 graph.use(Os.class, new Consumer<Os>() { @Override public void consume(Os instance) { //OsReferenceCount = 1 //Since the cached instance is cleared, the new instance is a newly created one. Assert.assertTrue(device.android != instance); Assert.assertTrue(instance instanceof Android); } }); //Reference count decremented by use method automatically //OsReferenceCount = 0 //Cached instance cleared again graph.use(Os.class, new Consumer<Os>() { @Override public void consume(Os instance) { //OsReferenceCount = 1 graph.inject(device); //Injection will reuse the cached instance and increment the reference count //OsReferenceCount = 2 //Since the cached instance is cleared, the new instance is a newly created one. Assert.assertTrue(device.android == instance); Assert.assertTrue(instance instanceof Android); } }); //Reference count decremented by use method automatically //OsReferenceCount = 1 graph.release(device); //OsReferenceCount = 0 } private void do_use_method_should_retain_and_release_instance_correctly() { @Apple class NeedIoS { } Annotation iosQualifier = NeedIoS.class.getAnnotation(Apple.class); //OsReferenceCount = 0 graph.use(Os.class, iosQualifier, new Consumer<Os>() { @Override public void consume(Os instance) { //First time to create the instance. //OsReferenceCount = 1 Assert.assertTrue(instance instanceof iOS); } }); //Reference count decremented by use method automatically //OsReferenceCount = 0 final Device device = new Device(); graph.inject(device); //OsReferenceCount = 1 //New instance created and cached graph.use(Os.class, iosQualifier, new Consumer<Os>() { @Override public void consume(Os instance) { //Since reference count is greater than 0, cached instance will be reused //OsReferenceCount = 2 Assert.assertTrue(device.os == instance); Assert.assertTrue(instance instanceof iOS); } }); //Reference count decremented by use method automatically //OsReferenceCount = 1 graph.release(device); //OsReferenceCount = 0 //Last instance released, so next time a new instance will be created graph.use(Os.class, iosQualifier, new Consumer<Os>() { @Override public void consume(Os instance) { //OsReferenceCount = 1 //Since the cached instance is cleared, the new instance is a newly created one. Assert.assertTrue(device.os != instance); Assert.assertTrue(instance instanceof iOS); } }); //Reference count decremented by use method automatically //OsReferenceCount = 0 graph.use(Os.class, iosQualifier, new Consumer<Os>() { @Override public void consume(Os instance) { //OsReferenceCount = 1 //Since the cached instance is cleared, the new instance is a newly created one. Assert.assertTrue(device.os != instance); Assert.assertTrue(instance instanceof iOS); } }); //Reference count decremented by use method automatically //OsReferenceCount = 0 //Cached instance cleared again graph.use(Os.class, iosQualifier, new Consumer<Os>() { @Override public void consume(Os instance) { //OsReferenceCount = 1 graph.inject(device); //Injection will reuse the cached instance and increment the reference count //OsReferenceCount = 2 //Since the cached instance is cleared, the new instance is a newly created one. Assert.assertTrue(device.os == instance); Assert.assertTrue(instance instanceof iOS); } }); //Reference count decremented by use method automatically //OsReferenceCount = 1 graph.release(device); //OsReferenceCount = 0 } @Test public void use_method_should_retain_and_release_instance_correctly() throws ProvideException, ProviderConflictException { graph.getRootComponent().register(new DeviceModule()); do_use_method_should_retain_and_release_instance_correctly(); } @Test public void should_delegate_mvc_graph_properly() throws ProvideException, ProviderConflictException { // Arrange Graph graphMock = mock(Graph.class); graph.graph = graphMock; // Act Graph.Monitor monitor = mock(Graph.Monitor.class); graph.registerMonitor(monitor); // Verify verify(graphMock).registerMonitor(eq(monitor)); // Arrange reset(graphMock); // Act graph.unregisterMonitor(monitor); // Verify verify(graphMock).unregisterMonitor(eq(monitor)); // Arrange reset(graphMock); // Act graph.clearMonitors(); // Verify verify(graphMock).clearMonitors(); // Arrange reset(graphMock); Provider.DereferenceListener providerFreedListener = mock(Provider.DereferenceListener.class); // Act graph.registerDereferencedListener(providerFreedListener); // Verify verify(graphMock).registerDereferencedListener(eq(providerFreedListener)); // Arrange reset(graphMock); // Act graph.unregisterDereferencedListener(providerFreedListener); // Verify verify(graphMock).unregisterDereferencedListener(eq(providerFreedListener)); // Arrange reset(graphMock); // Act graph.clearDereferencedListeners(); // Verify verify(graphMock).clearDereferencedListeners(); } @Test public void non_ui_thread_should_delegate_mvc_graph_properly() throws Exception { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); should_delegate_mvc_graph_properly(); } @Test (expected = IllegalStateException.class) public void should_throw_out_exceptions_when_registering_component() throws ProvideException, ProviderConflictException, Graph.IllegalRootComponentException { // Arrange MvcComponent badComponent = mock(MvcComponent.class); MvcGraph mvcGraph = new MvcGraph(); mvcGraph.setRootComponent(badComponent); Object obj = new Object(); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { throw new IllegalStateException(); } }).when(badComponent).register(any(Object.class)); // Act mvcGraph.getRootComponent().register(obj); } interface UnimplementedInterface{} @Test(expected = MvcGraphException.class) public void should_raise_mvc_graph_exception_when_inject_on_poke_exception() { class View { @Inject UnimplementedInterface unimplementedInterface; } graph.inject(new View()); } @Test(expected = MvcGraphException.class) public void should_raise_mvc_graph_exception_when_inject_on_poke_exception_on_non_ui_thread() { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); should_raise_mvc_graph_exception_when_inject_on_poke_exception(); } @Test(expected = MvcGraphException.class) public void should_raise_mvc_graph_exception_when_use_on_poke_exception_on_non_ui_thread() { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); class View { @Inject UnimplementedInterface unimplementedInterface; } graph.use(UnimplementedInterface.class, null, mock(Consumer.class)); } @Test(expected = MvcGraphException.class) public void should_raise_mvc_graph_exception_when_release_on_poke_exception() { class View { @Inject UnimplementedInterface unimplementedInterface; } View view = new View(); view.unimplementedInterface = new UnimplementedInterface() { }; graph.release(view); } @Test(expected = MvcGraphException.class) public void should_raise_mvc_graph_exception_when_release_on_poke_exception_on_non_ui_thread() { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); should_raise_mvc_graph_exception_when_release_on_poke_exception(); } @Test(expected = MvcGraphException.class) public void should_raise_mvc_graph_exception_when_use_on_poke_exception() { class View { @Inject UnimplementedInterface unimplementedInterface; } graph.use(UnimplementedInterface.class, new Consumer<UnimplementedInterface>() { @Override public void consume(UnimplementedInterface instance) { } }); } @Test(expected = MvcGraphException.class) public void should_raise_mvc_graph_exception_when_use_on_poke_exception_on_non_thread() { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); should_raise_mvc_graph_exception_when_use_on_poke_exception(); } @Test public void should_throw_exception_when_mvc_graph_use_consumer_on_non_main_thread() { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.use(String.class, mock(Consumer.class)); } @Test public void should_throw_exception_when_mvc_graph_use_consumer_without_annotation_on_non_main_thread() { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.use(String.class, null, mock(Consumer.class)); } @Test(expected = MvcGraphException.class) public void should_throw_exception_when_mvc_graph_reference_on_non_main_thread() throws ProvideException, CircularDependenciesException, ProviderMissingException { graph.reference(String.class, null); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.reference(String.class, null); } @Test public void should_throw_exception_when_mvc_graph_dreference_on_non_main_thread() throws ProvideException, CircularDependenciesException, ProviderMissingException { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.dereference(this, TestMvcGraph.class, null); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(true); graph.dereference(this, TestMvcGraph.class, null); } @Test public void should_throw_exception_when_mvc_graph_inject_on_non_main_thread() { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.inject(this); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(true); graph.inject(this); } @Test public void should_throw_exception_when_mvc_graph_release_on_non_main_thread() { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.release(this); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(true); graph.release(this); } @Test(expected = MvcGraphException.class) public void should_throw_exception_when_set_attached_root_component_to_graph_on_main_thread() throws Graph.IllegalRootComponentException, Component.MultiParentException, ProviderConflictException { MvcComponent parent = new MvcComponent("parent"); MvcComponent child = new MvcComponent(""); parent.attach(child); graph.setRootComponent(child); } @Test(expected = MvcGraphException.class) public void should_throw_exception_when_set_attached_root_component_to_graph_on_non_main_thread() throws Graph.IllegalRootComponentException, Component.MultiParentException, ProviderConflictException { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); MvcComponent parent = new MvcComponent("parent"); MvcComponent child = new MvcComponent(""); parent.attach(child); graph.setRootComponent(child); } @Test public void should_throw_exception_when_mvc_graph_set_rootComponent_on_non_main_thread() throws Graph.IllegalRootComponentException { graph.setRootComponent(new MvcComponent("")); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.setRootComponent(new MvcComponent("")); } @Test public void should_throw_exception_when_mvc_graph_register_deferenceListener_on_non_main_thread() throws Graph.IllegalRootComponentException { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.registerDereferencedListener(mock(Provider.DereferenceListener.class)); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(true); graph.registerDereferencedListener(mock(Provider.DereferenceListener.class)); } @Test public void should_throw_exception_when_mvc_graph_unregister_deferenceListener_on_non_main_thread() throws Graph.IllegalRootComponentException { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.unregisterDereferencedListener(mock(Provider.DereferenceListener.class)); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(true); graph.unregisterDereferencedListener(mock(Provider.DereferenceListener.class)); } @Test public void should_throw_exception_when_mvc_graph_clear_deferenceListeners_on_non_main_thread() throws Graph.IllegalRootComponentException { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.clearDereferencedListeners(); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(true); graph.clearDereferencedListeners(); } @Test public void should_throw_exception_when_mvc_graph_register_monitor_on_non_main_thread() throws Graph.IllegalRootComponentException { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.registerMonitor(mock(Graph.Monitor.class)); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(true); graph.registerMonitor(mock(Graph.Monitor.class)); } @Test public void should_throw_exception_when_mvc_graph_unregister_monitor_on_non_main_thread() throws Graph.IllegalRootComponentException { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.unregisterMonitor(mock(Graph.Monitor.class)); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(true); graph.unregisterMonitor(mock(Graph.Monitor.class)); } @Test public void should_throw_exception_when_mvc_graph_clear_monitors_on_non_main_thread() throws Graph.IllegalRootComponentException { when(graph.uiThreadRunner.isOnUiThread()).thenReturn(false); graph.clearMonitors(); when(graph.uiThreadRunner.isOnUiThread()).thenReturn(true); graph.clearMonitors(); } @Test public void default_uiThreadRunner_should_post_on_same_thread() { final Thread thread = Thread.currentThread(); graph.uiThreadRunner.post(new Runnable() { @Override public void run() { Assert.assertTrue(Thread.currentThread() == thread); } }); } @Test public void default_uiThreadRunner_should_post_with_delay_on_same_thread() { final Thread thread = Thread.currentThread(); graph.uiThreadRunner.postDelayed(new Runnable() { @Override public void run() { Assert.assertTrue(Thread.currentThread() == thread); } }, 100); } }