package gov.nasa.arc.mct.gui.actions; import gov.nasa.arc.mct.components.AbstractComponent; import gov.nasa.arc.mct.gui.ActionContext; import gov.nasa.arc.mct.gui.ContextAwareAction; import gov.nasa.arc.mct.gui.housing.MCTAbstractHousing; import gov.nasa.arc.mct.gui.housing.registry.UserEnvironmentRegistry; import gov.nasa.arc.mct.osgi.platform.OSGIRuntimeImpl; import gov.nasa.arc.mct.platform.spi.Platform; import gov.nasa.arc.mct.platform.spi.PlatformAccess; import gov.nasa.arc.mct.platform.spi.WindowManager; import java.awt.event.ActionEvent; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.Map; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; public class TestQuitAction { private OSGIRuntimeImpl originalRuntime; private Platform originalPlatform; @Mock Platform mockPlatform; @Mock OSGIRuntimeImpl mockRuntime; @Mock ActionContext mockActionContext; @Mock ActionEvent mockEvent; private boolean doRemoveHousings = true; @BeforeClass public void setupRuntime() throws Exception { // We want to verify interactions with OSGIRuntime & Platform, // so we'll need to mock them. // Store the originals so it can be restored after the test originalRuntime = getSingleton(OSGIRuntimeImpl.class); originalPlatform = PlatformAccess.getPlatform(); } @AfterClass public void restoreRuntime() throws Exception { // Restore original runtime/platform setSingleton(OSGIRuntimeImpl.class, originalRuntime); new PlatformAccess().setPlatform(originalPlatform); // Flush out the registry, since we can't mock it clearRegistry(); } @BeforeMethod public void setup() throws Exception { MockitoAnnotations.initMocks(this); // Mock OSGIRuntime and Platform setSingleton(OSGIRuntimeImpl.class, mockRuntime); new PlatformAccess().setPlatform(mockPlatform); // Some tests want housings to refuse to be closed, // but most presume that they are removed by closeHousing call. doRemoveHousings = true; clearRegistry(); } @Test (dataProvider = "generateTestCases") public void testQuitEnabled(boolean confirmed, int housings) { // Verifies that QuitAction is available in appropriate cases initializeRegistry(housings); ContextAwareAction quit = new QuitAction(); // Should only handle or be enabled when there are windows open Assert.assertEquals(quit.canHandle(mockActionContext), housings > 0); Assert.assertEquals(quit.isEnabled(), housings > 0); } @Test (dataProvider = "generateTestCases") public void testActionPerformed(final boolean confirmed, int housings) throws Exception { // Verifies that performing a QuitAction closes all windows, stops OSGI // (except where user input indicates otherwise) MCTAbstractHousing[] mockHousings = initializeRegistry(housings); ContextAwareAction quit = new QuitAction(); Mockito.reset(mockRuntime); // Set up window manager to support dialog call // Act as though the user clicked "OK" or "Cancel" (depending on argument "confirmed") WindowManager mockWindowManager = Mockito.mock(WindowManager.class); Mockito.when(mockPlatform.getWindowManager()).thenReturn(mockWindowManager); Mockito.when(mockWindowManager.showInputDialog(Mockito.anyString(), Mockito.anyString(), Mockito.<Object[]>any(), Mockito.any(), Mockito.<Map<String,Object>>any())).thenAnswer(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Object[] options = (Object[]) invocation.getArguments()[2]; return confirmed ? options[0] : options[1]; // options[0] presumed to mean "OK" } }); // Already tested in testQuitEnabled, but also need to obey action's life cycle Assert.assertEquals(quit.canHandle(mockActionContext), housings > 0); // Trigger the action - this is the method we are testing quit.actionPerformed(mockEvent); // A dialog should have been requested Mockito.verify(mockWindowManager, Mockito.times(1)).showInputDialog(Mockito.anyString(), Mockito.anyString(), Mockito.<Object[]>any(), Mockito.any(), Mockito.<Map<String,Object>>any()); // All housings should be closed, iff dialog was confirmed for (MCTAbstractHousing mockHousing : mockHousings) { Mockito.verify(mockHousing, confirmed ? Mockito.times(1) : Mockito.never()).closeHousing(); } // Verify an assumption of the test (that all housings are gone if confirmed, or there if not) Assert.assertEquals(UserEnvironmentRegistry.getHousingCount(), confirmed ? 0 : housings); // OSGI should have been stopped, iff dialog was confirmed Mockito.verify(mockRuntime, confirmed ? Mockito.times(1) : Mockito.never()).stopOSGI(); } @Test (dataProvider = "generateTestCases") public void testActionPerformedWindowsNotClosed(final boolean confirmed, int housings) throws Exception { // A user may interrupt a QuitAction by stopping a window closing // (for instance, by hitting cancel if a "Save" "Discard" "Cancel" dialog appears) // If this happens, we should NOT stop OSGI (which effectively stops MCT) // This is verified by this test. // Suppress housing removal (as though user had kept window open) doRemoveHousings = false; MCTAbstractHousing[] mockHousings = initializeRegistry(housings); ContextAwareAction quit = new QuitAction(); Mockito.reset(mockRuntime); // Set up window manager to support dialog call // Act as though the user clicked "OK" or "Cancel" (depending on argument "confirmed") WindowManager mockWindowManager = Mockito.mock(WindowManager.class); Mockito.when(mockPlatform.getWindowManager()).thenReturn(mockWindowManager); Mockito.when(mockWindowManager.showInputDialog(Mockito.anyString(), Mockito.anyString(), Mockito.<Object[]>any(), Mockito.any(), Mockito.<Map<String,Object>>any())).thenAnswer(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Object[] options = (Object[]) invocation.getArguments()[2]; return confirmed ? options[0] : options[1]; // options[0] presumed to mean "OK" } }); // Already tested in testQuitEnabled, but also need to obey action's life cycle Assert.assertEquals(quit.canHandle(mockActionContext), housings > 0); // Trigger the action - this is the method we are testing quit.actionPerformed(mockEvent); // A dialog should have been requested Mockito.verify(mockWindowManager, Mockito.times(1)).showInputDialog(Mockito.anyString(), Mockito.anyString(), Mockito.<Object[]>any(), Mockito.any(), Mockito.<Map<String,Object>>any()); // All housings should be closed, iff dialog was confirmed for (MCTAbstractHousing mockHousing : mockHousings) { Mockito.verify(mockHousing, confirmed ? Mockito.times(1) : Mockito.never()).closeHousing(); } // Verify an assumption of the test (that all housings are still "there") Assert.assertEquals(UserEnvironmentRegistry.getHousingCount(), housings); // OSGI should NOT have been stopped (except for corner case where // action was confirmed and there were no housings to begin with) Mockito.verify(mockRuntime, (!confirmed || housings > 0) ? Mockito.never() : Mockito.times(1)).stopOSGI(); } private void clearRegistry() { // Flush all housings from the UserEnvironmentRegistry // (it can't be mocked, so test must rely on its actual behavior) Collection<MCTAbstractHousing> housings = UserEnvironmentRegistry.getAllHousings(); for (MCTAbstractHousing housing : housings) { UserEnvironmentRegistry.removeHousing(housing); } } private MCTAbstractHousing[] initializeRegistry (int numberOfHousings) { // Initialize the UserEnvironmentRegistry with mock housings // UserEnvironmentRegistry can't be effectively mocked // (internal instance is static and final), so instead we populate // it directly. clearRegistry(); MCTAbstractHousing[] mockHousings = mockArray(numberOfHousings, MCTAbstractHousing.class); // Verify precondition (ensure UserEnvironmentRegistry is appropriately clear) Assert.assertEquals(UserEnvironmentRegistry.getHousingCount(), 0); for (MCTAbstractHousing h : mockHousings) { // UserEnvironmentRegistry keys on component id, so provide one AbstractComponent mockComp = Mockito.mock(AbstractComponent.class); Mockito.when(mockComp.getId()).thenReturn("component"); Mockito.when(h.getWindowComponent()).thenReturn(mockComp); // Also, Mockito.doAnswer(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { // Actually remove from the UserEnvironmentRegistry, since // QuitAction looks there to determine behavior. But allow this // to be suppressed by changing doRemoveHousings (to permit // testing of QuitAction's behavior when housings don't close.) if (doRemoveHousings) { UserEnvironmentRegistry.removeHousing((MCTAbstractHousing) invocation.getMock()); } return null; } }).when(h).closeHousing(); UserEnvironmentRegistry.registerHousing(h); } // Verify new precondition for tests, that UserEnvironmentRegistry contains this many housings Assert.assertEquals(UserEnvironmentRegistry.getHousingCount(), numberOfHousings); return mockHousings; } @DataProvider public Object[][] generateTestCases() { // We want to test along two axes: // - Whether or not the user confirms the shutdown when the dialog appears // - How many housings (windows) are open when the action is invoked int housingVariations = 5; Object[][] cases = new Object[2 * housingVariations][2]; int count = 0; for (boolean confirmed : new boolean[]{false,true}) { for (int housings = 0; housings < housingVariations; housings++) { cases[count][0] = confirmed; cases[count][1] = (housings == 0) ? 0 : (1 << (housings-1)); count++; } } return cases; } // Create an array of mocks of a given type private <T> T[] mockArray(int size, Class<T> classToMock) { @SuppressWarnings("unchecked") T[] mocks = (T[]) Array.newInstance(classToMock, size); for (int i = 0; i < size; i++) { mocks[i] = Mockito.mock(classToMock, Mockito.RETURNS_MOCKS); } return mocks; } // Use reflection to "break into" singletons so they can be mocked/spied private <T> T getSingleton(Class<T> singletonClass) throws Exception { for (Field f : singletonClass.getDeclaredFields()) { if (singletonClass.isAssignableFrom(f.getType()) && Modifier.isStatic(f.getModifiers()) ) { f.setAccessible(true); return singletonClass.cast(f.get(null)); } } return null; } private <T> void setSingleton(Class<T> singletonClass, T value) throws Exception { for (Field f : singletonClass.getDeclaredFields()) { if (singletonClass.isAssignableFrom(f.getType()) && Modifier.isStatic(f.getModifiers()) ) { f.setAccessible(true); if (Modifier.isFinal(f.getModifiers())) { int mods = f.getModifiers(); Field modField = Field.class.getDeclaredField("modifiers"); modField.setAccessible(true); modField.set(f, mods & ~Modifier.FINAL); } f.set(null, value); return; } } } }