/* * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of * the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright (c) 2014 Digi International Inc., All Rights Reserved. */ package com.digi.android.wva.test; import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.test.ServiceTestCase; import com.digi.android.wva.VehicleInfoService; import com.digi.android.wva.WvaApplication; import com.digi.android.wva.util.MessageCourier; import com.digi.wva.async.EventChannelStateListener; import com.digi.wva.async.WvaCallback; import com.digi.wva.WVA; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * Unit tests related to the VehicleInfoService. * * Created by mwadsten on 5/29/13. */ @SuppressWarnings("unchecked") public class VehicleInfoServiceTest extends ServiceTestCase<VehicleInfoService> { private WvaApplication app; private WVA device; private String[] names; private EventChannelStateListener listener; public VehicleInfoServiceTest(Class<VehicleInfoService> serviceClass) { super(serviceClass); } public VehicleInfoServiceTest() { this(VehicleInfoService.class); } @Override protected void setUp() throws Exception { super.setUp(); MessageCourier.clear(); app = mock(WvaApplication.class); setApplication(app); doNothing().when(app).subscribeToEndpoint(anyString(), anyInt(), any(WvaCallback.class)); doNothing().when(app).listNewEndpoint(anyString(), anyBoolean()); doCallRealMethod().when(app).getHandler(); doCallRealMethod().when(app).setDevice(any(WVA.class)); doCallRealMethod().when(app).getDevice(); doNothing().when(app).clearDevice(); when(app.isTesting()).thenReturn(true); final Set<String> endpoints = new HashSet<String>(); names = new String[] { "Speed", "RPM", "Temperature", "Light", "Humidity", "Seat Belt" }; endpoints.addAll(Arrays.asList(names)); device = mock(WVA.class); doNothing().when(device).unsubscribeFromVehicleData(anyString()); doNothing().when(device).disconnectEventChannel(); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { listener = (EventChannelStateListener) invocationOnMock.getArguments()[0]; return null; } }).when(device).setEventChannelStateListener(any(EventChannelStateListener.class)); setVehicleInitResponse(null, endpoints); setConnectDataStreamError(null); app.setDevice(device); // The ServiceTestCase code on Froyo does not like it when a service // is already started/created when startService is called. Calling // or not calling start() should have no effect on the actual behavior // of the app (and in fact, the whole APP_CREATE code might just be // removed in the end), but this is a workaround in the meantime. if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.FROYO) start(); } private void setVehicleInitResponse(final Exception e, final Set<String> endpoints) { doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { WvaCallback<Set<String>> cb = (WvaCallback<Set<String>>) invocationOnMock.getArguments()[0]; cb.onResponse(e, endpoints); return null; } }).when(device).fetchVehicleDataEndpoints(any(WvaCallback.class)); } private void setConnectDataStreamError(final IOException e) { if (e == null) { doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { listener.onConnected(device); return null; } }).when(device).connectEventChannel(anyInt()); } else { doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { listener.onError(device, e); return null; } }).when(device).connectEventChannel(anyInt()); } } /** * Call {@link #startService(android.content.Intent)} with * {@link VehicleInfoService#buildCreateIntent(android.content.Context)} * result, so as to make sure the service exists. */ private void start() { startService(VehicleInfoService.buildCreateIntent(getContext())); } public void testValidateCreateIntent() { Intent i = VehicleInfoService.buildCreateIntent(getContext()); assertEquals("Wrong intent command on create intent", VehicleInfoService.CMD_APPCREATE, i.getIntExtra(VehicleInfoService.INTENT_CMD, -1)); assertEquals("Wrong class", VehicleInfoService.class.getName(), i.getComponent().getClassName()); } public void testValidateConnectIntent() { Intent i = VehicleInfoService.buildConnectIntent(getContext(), "192.168.255.1"); assertEquals("Wrong IP address on intent", "192.168.255.1", i.getStringExtra(VehicleInfoService.INTENT_IP)); assertEquals("Wrong intent command on connect intent", VehicleInfoService.CMD_CONNECT, i.getIntExtra(VehicleInfoService.INTENT_CMD, -1)); assertEquals("Wrong class", VehicleInfoService.class.getName(), i.getComponent().getClassName()); } public void testValidateDisconnectIntent() { Intent i = VehicleInfoService.buildDisconnectIntent(getContext()); assertEquals("Wrong intent command on disconnect intent", VehicleInfoService.CMD_DISCONNECT, i.getIntExtra(VehicleInfoService.INTENT_CMD, -1)); assertEquals("Wrong class", VehicleInfoService.class.getName(), i.getComponent().getClassName()); } /** * Does a lot of mocking operations to make it so that calling * startService with a "connect" intent will execute immediately * (read: synchronously), thereby allowing us to validate the code * inside the initVehicleData callback and the connectDataStream callback * therein. */ public void testConnectNoErrors() { // Set up the preferences as needed. SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(getContext()).edit(); editor.putString("pref_device_port", "5000"); editor.putBoolean("pref_auto_subscribe", true); editor.putString("pref_default_interval", "10"); editor.commit(); MessageCourier.clear(); startService(VehicleInfoService.buildConnectIntent(getContext(), "0.0.0.0")); assertSame("Wrong device used", getService().getDevice(), device); verify(device).fetchVehicleDataEndpoints(any(WvaCallback.class)); verify(device).setEventChannelStateListener(any(EventChannelStateListener.class)); verify(device).connectEventChannel(eq(5000)); // Verify that subscribeToEndpoint was called for each endpoint for (int i = 0; i < names.length; i++) { verify(app).subscribeToEndpointFromService(eq(names[i]), eq(10), any(WvaCallback.class)); } MessageCourier.DashboardMessage[] msgs = MessageCourier.getDashboardMessages(); assertEquals("Incorrect number of dashboard messages", 1, msgs.length); assertFalse("Dashboard message that was sent is an error!", msgs[0].isError()); assertEquals("Wrong contents on connected message!", "0.0.0.0", msgs[0].getContents()); MessageCourier.ChartMessage[] cmsgs = MessageCourier.getChartMessages(); assertEquals("There are messages for the chart!", 0, cmsgs.length); assertEquals("Wrong connection IP address", "0.0.0.0", getService().getConnectionIpAddress()); assertTrue("Not 'connected'", getService().isConnected()); } public void testDisconnect() { // We almost wouldn't need to use mock devices and applications for // testing the disconnect call, since it doesn't do anything // asynchronously. But this is a useful way of testing that the // results of "connecting" to a mock device here can be undone // with the disconnect command. // No testing of this call needs to be done - that is in testConnectNoErrors startService(VehicleInfoService.buildConnectIntent(getContext(), "0.0.0.0")); startService(VehicleInfoService.buildDisconnectIntent(getContext())); verify(device).disconnectEventChannel(true); verify(app).setDevice(null); assertNull("Service still has old device", getService().getDevice()); assertFalse("Service still reports connected", getService().isConnected()); } public void testInitWithErrorMessage() { Exception e = mock(Exception.class); when(e.getMessage()).thenReturn("Useful Error Message"); setVehicleInitResponse(e, null); MessageCourier.clear(); startService(VehicleInfoService.buildConnectIntent(getContext(), "0.0.0.0")); verify(device, never()).connectEventChannel(anyInt()); verify(app, never()).subscribeToEndpoint(anyString(), anyInt(), any(WvaCallback.class)); verify(app).setDevice(null); verify(e).getMessage(); MessageCourier.DashboardMessage[] msgs = MessageCourier.getDashboardMessages(); assertEquals("No dashboard messages!", 1, msgs.length); assertTrue("Non-error dashboard message!", msgs[0].isError()); assertEquals("Wrong dashboard error!", "Useful Error Message", msgs[0].getContents()); } public void testInitWithErrorCause() { Exception e = mock(Exception.class); when(e.getMessage()).thenReturn(null); when(e.getCause()).thenReturn(new Exception("Another Error")); setVehicleInitResponse(e, null); MessageCourier.clear(); startService(VehicleInfoService.buildConnectIntent(getContext(), "0.0.0.0")); verify(device, never()).connectEventChannel(anyInt(), any(EventChannelStateListener.class)); verify(app, never()).subscribeToEndpoint(anyString(), anyInt(), any(WvaCallback.class)); verify(app).setDevice(null); verify(e).getMessage(); // Need to use atLeast(1), because (seemingly) the logging statement // invokes getCause() (or perhaps that is in printStackTrace or something) verify(e, atLeast(1)).getCause(); MessageCourier.DashboardMessage[] msgs = MessageCourier.getDashboardMessages(); assertEquals("No dashboard messages!", 1, msgs.length); assertTrue("Non-error dashboard message!", msgs[0].isError()); assertEquals("Wrong dashboard error!", "Another Error", msgs[0].getContents()); } public void testInitWithErrorToString() { Exception e = mock(Exception.class); when(e.getMessage()).thenReturn(null); when(e.getCause()).thenReturn(null); when(e.toString()).thenReturn("The Error"); setVehicleInitResponse(e, null); MessageCourier.clear(); startService(VehicleInfoService.buildConnectIntent(getContext(), "0.0.0.0")); verify(device, never()).connectEventChannel(anyInt(), any(EventChannelStateListener.class)); verify(app, never()).subscribeToEndpoint(anyString(), anyInt(), any(WvaCallback.class)); verify(app).setDevice(null); verify(e).getMessage(); // Need to use atLeast(1), because (seemingly) the logging statement // invokes getCause() (or perhaps that is in printStackTrace or something) verify(e, atLeast(1)).getCause(); // Mockito won't let us verify toString() MessageCourier.DashboardMessage[] msgs = MessageCourier.getDashboardMessages(); assertEquals("No dashboard messages!", 1, msgs.length); assertTrue("Non-error dashboard message!", msgs[0].isError()); assertEquals("Wrong dashboard error!", "The Error", msgs[0].getContents()); } // These tests worked, and made sense, when the connectDataStream API // was asynchronous. // public void testConnectDataStreamErrorMessage() { // IOException e = mock(IOException.class); // when(e.getMessage()).thenReturn("Error Message"); // // setConnectDataStreamError(e); // // startService(VehicleInfoService.buildConnectIntent(getContext(), "0.0.0.0")); // // verify(e).getMessage(); // verify(app).setDevice(null); // assertFalse("Service reports connected", getService().isConnected()); // MessageCourier.DashboardMessage[] msgs = MessageCourier.getDashboardMessages(); // assertEquals("No dashboard messages!", 1, msgs.length); // assertTrue("Non-error dashboard message!", msgs[0].isError()); // assertEquals("Wrong dashboard error!", "Error Message", msgs[0].getContents()); // } // // public void testConnectDataStreamErrorCause() { // IOException e = mock(IOException.class); // when(e.getMessage()).thenReturn(null); // when(e.getCause()).thenReturn(new Exception("The cause")); // // setConnectDataStreamError(e); // // startService(VehicleInfoService.buildConnectIntent(getContext(), "0.0.0.0")); // // verify(e).getMessage(); // verify(e, atLeast(1)).getCause(); // verify(app).setDevice(null); // assertNull("Service has old device", getService().getDevice()); // assertFalse("Service reports connected", getService().isConnected()); // MessageCourier.DashboardMessage[] msgs = MessageCourier.getDashboardMessages(); // assertEquals("No dashboard messages!", 1, msgs.length); // assertTrue("Non-error dashboard message!", msgs[0].isError()); // assertEquals("Wrong dashboard error!", "The cause", msgs[0].getContents()); // } // // public void testConnectDataStreamErrorToString() { // IOException e = mock(IOException.class); // when(e.getMessage()).thenReturn(null); // when(e.getCause()).thenReturn(null); // when(e.toString()).thenReturn("An Error"); // // setConnectDataStreamError(e); // // startService(VehicleInfoService.buildConnectIntent(getContext(), "0.0.0.0")); // // verify(e).getMessage(); // verify(e, atLeast(1)).getCause(); // // Mockito won't let us verify toString() // verify(app).setDevice(null); // assertNull("Service has old device", getService().getDevice()); // assertFalse("Service reports connected", getService().isConnected()); // MessageCourier.DashboardMessage[] msgs = MessageCourier.getDashboardMessages(); // assertEquals("No dashboard messages!", 1, msgs.length); // assertTrue("Non-error dashboard message!", msgs[0].isError()); // assertEquals("Wrong dashboard error!", "An Error", msgs[0].getContents()); // } // public void testCheckConnectionRunnable() { // Runnable r = getService().getConnectionLoopRunnable(); // assertEquals("MessageCourier should be empty!", 0, MessageCourier.getDashboardMessages().length); // assertEquals("MessageCourier should be empty!", 0, MessageCourier.getChartMessages().length); // assertFalse("Service should report false for isConnected()", getService().isConnected()); // // Service should start out in a "disconnected" state. // r.run(); // assertFalse("checkConnection set isConnected to true!", getService().isConnected()); // // // "Connect" the service so that we get to the second condition. // IOException e = new IOException("Fail."); // when(device.isDataStreamDisconnected()).thenReturn(true); // when(device.dataStreamException()).thenReturn(e); // startService(VehicleInfoService.buildConnectIntent(getContext(), "0.0.0.0")); // // r.run(); // MessageCourier.DashboardMessage[] dm = MessageCourier.getDashboardMessages(); // assertEquals("No error message for dashboard", 2, dm.length); // assertTrue("Non-error in dashboard messages", dm[1].isError()); // if (!dm[1].getContents().contains("Fail.")) { // fail("'Fail.' does not appear in error message: " + dm[1].getContents()); // } // } }