package com.openxc.sources; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import com.openxc.TestUtils; import com.openxc.messages.SerializationException; import com.openxc.messages.SimpleVehicleMessage; import com.openxc.messages.VehicleMessage; import com.openxc.messages.streamers.BinaryStreamer; import com.openxc.messages.streamers.JsonStreamer; @RunWith(RobolectricTestRunner.class) public class BytestreamDataSourceTest { TestBytestreamSource source; SourceCallback callback = mock(SourceCallback.class); @Before public void setup() { source = new TestBytestreamSource(callback); } @After public void teardown() { source.stop(); } @Test public void startedIsRunning() { source.start(); assertTrue(source.isRunning()); } @Test public void initializedNotRunning() { assertFalse(source.isRunning()); } @Test public void stoppedNotRunning() { source.start(); source.stop(); assertFalse(source.isRunning()); } @Test public void errorOnReadDisconnects() { source.start(); source.connect(); source.nextReadIsError = true; source.inject(new byte[] {1,2,3,4}); TestUtils.pause(100); assertTrue(source.isRunning()); assertFalse(source.isConnected()); } @Test public void exceptionOnReadDisconnects() { source.start(); source.connect(); source.nextReadThrowsException = true; source.inject(new byte[] {1,2,3,4}); TestUtils.pause(50); assertTrue(source.isRunning()); assertFalse(source.isConnected()); } @Test public void readNotCalledUntilConnected() { source.start(); source.inject(new byte[] {1,2,3,4}); assertFalse(source.packets.isEmpty()); source.connect(); TestUtils.pause(20); assertTrue(source.packets.isEmpty()); } @Test public void receiveInvalidDataNoCallback() { source.start(); source.connect(); source.inject(new byte[] {1,2,3,4}); TestUtils.pause(20); verify(callback, never()).receive(Matchers.any(VehicleMessage.class)); } @Test public void receiveValidBinaryTriggersCallback() throws SerializationException { source.start(); source.connect(); SimpleVehicleMessage message = new SimpleVehicleMessage("foo", "bar"); source.inject(new BinaryStreamer().serializeForStream(message)); TestUtils.pause(100); ArgumentCaptor<VehicleMessage> argument = ArgumentCaptor.forClass( VehicleMessage.class); verify(callback).receive(argument.capture()); VehicleMessage received = argument.getValue(); received.untimestamp(); assertEquals(received, message); } @Test public void receiveValidJsonTriggersCallback() { source.start(); source.connect(); SimpleVehicleMessage message = new SimpleVehicleMessage("foo", "bar"); source.inject(new JsonStreamer().serializeForStream(message)); TestUtils.pause(100); ArgumentCaptor<VehicleMessage> argument = ArgumentCaptor.forClass( VehicleMessage.class); verify(callback).receive(argument.capture()); VehicleMessage received = argument.getValue(); received.untimestamp(); assertEquals(received, message); } @Test public void readMultipleMessageAtOnceReceivesAll() { source.start(); source.connect(); List<SimpleVehicleMessage> messages = new ArrayList<>(); messages.add(new SimpleVehicleMessage("1", "foo")); messages.add(new SimpleVehicleMessage("2", "bar")); messages.add(new SimpleVehicleMessage("3", "baz")); for(SimpleVehicleMessage message : messages) { source.inject(new JsonStreamer().serializeForStream(message), false); } source.signal(); TestUtils.pause(100); ArgumentCaptor<VehicleMessage> argument = ArgumentCaptor.forClass( VehicleMessage.class); verify(callback, times(3)).receive(argument.capture()); List<VehicleMessage> capturedMessages = argument.getAllValues(); for(int i = 0; i < messages.size(); i++) { VehicleMessage received = capturedMessages.get(i); received.untimestamp(); assertEquals(received, messages.get(i)); } } private class TestBytestreamSource extends BytestreamDataSource { public boolean connected = false; public ArrayList<byte[]> packets = new ArrayList<>(); private Lock mPacketLock = new ReentrantLock(); private Condition mPacketReceived = mPacketLock.newCondition(); public boolean nextReadIsError = false; public boolean nextReadThrowsException = false; public TestBytestreamSource(SourceCallback callback) { super(callback, RuntimeEnvironment.application); } @Override public boolean isConnected() { return connected; } public void signal() { try { mPacketLock.lock(); mPacketReceived.signal(); } finally { mPacketLock.unlock(); } } public void inject(byte[] bytes) { inject(bytes, true); } public void inject(byte[] bytes, boolean signal) { try { mPacketLock.lock(); packets.add(bytes); if(signal) { mPacketReceived.signal(); } } finally { mPacketLock.unlock(); } } @Override protected int read(byte[] bytes) throws IOException { try { mPacketLock.lock(); while(packets.isEmpty()) { mPacketReceived.await(); } if(nextReadIsError) { return -1; } else if(nextReadThrowsException) { throw new IOException(); } else { byte[] data = packets.remove(0); if(data != null) { System.arraycopy(data, 0, bytes, 0, data.length); return data.length; } else { return -1; } } } catch(InterruptedException e) { return -1; } finally { mPacketLock.unlock(); } } protected boolean write(byte[] bytes) { return true; } @Override protected void disconnect() { mConnectionLock.writeLock().lock(); connected = false; disconnected(); mConnectionLock.writeLock().unlock(); } @Override protected void connect() { mConnectionLock.writeLock().lock(); connected = true; disconnected(); mConnectionLock.writeLock().unlock(); } } }