package net.i2p.client.streaming.impl; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import net.i2p.I2PAppContext; import net.i2p.data.ByteArray; import net.i2p.data.DataHelper; import net.i2p.util.Log; /** * Stress test the MessageInputStream */ public class MessageInputStreamTest { @Rule public MockitoRule rule = MockitoJUnit.rule(); private I2PAppContext _context; private Log _log; private ConnectionOptions _options; private MessageInputStream in; @Mock private PacketLocal packetLocal; @Before public void setUp() { _context = I2PAppContext.getGlobalContext(); _log = _context.logManager().getLog(MessageInputStreamTest.class); _options = new ConnectionOptions(); in = new MessageInputStream(_context, _options.getMaxMessageSize(), _options.getMaxWindowSize(), _options.getInboundBufferSize()); } @After public void tearDown() { in.close(); } @Test public void testGetHighestReadyBlockId() { assertThat(in.getHighestReadyBlockId(), is((long) -1)); in.messageReceived(0, new ByteArray()); assertThat(in.getHighestReadyBlockId(), is((long) 0)); in.messageReceived(2, new ByteArray()); assertThat(in.getHighestReadyBlockId(), is((long) 0)); in.messageReceived(1, new ByteArray()); assertThat(in.getHighestReadyBlockId(), is((long) 2)); } @Test public void testGetHighestBlockId() { assertThat(in.getHighestBlockId(), is((long) -1)); in.messageReceived(0, new ByteArray()); assertThat(in.getHighestBlockId(), is((long) 0)); in.messageReceived(2, new ByteArray()); assertThat(in.getHighestBlockId(), is((long) 2)); in.messageReceived(1, new ByteArray()); assertThat(in.getHighestBlockId(), is((long) 2)); } @Test public void testCanAccept() { // Can always accept packets with no data assertTrue(in.canAccept(0, 0)); assertTrue(in.canAccept(0, -1)); // Can always accept packets with message ID < MIN_READY_BUFFERS (= 16) assertTrue(in.canAccept(0, 1024)); assertTrue(in.canAccept(1, 1024)); assertTrue(in.canAccept(15, 1024)); } @Test public void testCanAccept_locallyClosed() { // Fill the buffer int numMsgs = _options.getInboundBufferSize() / _options.getMaxMessageSize(); byte orig[] = new byte[_options.getInboundBufferSize()]; _context.random().nextBytes(orig); for (int i = 0; i < numMsgs; i++) { byte msg[] = new byte[_options.getMaxMessageSize()]; System.arraycopy(orig, i*_options.getMaxMessageSize(), msg, 0, _options.getMaxMessageSize()); in.messageReceived(i, new ByteArray(msg)); } // Check that new messages won't be accepted assertFalse(in.canAccept(numMsgs, 1)); // Close in.close(); // Check that new messages will now be accepted assertTrue(in.canAccept(numMsgs, 1)); } @Test public void testCanAccept_allMaxSize() { // Fill the buffer to one message under limit with max-size msgs int numMsgs = _options.getInboundBufferSize() / _options.getMaxMessageSize(); byte orig[] = new byte[_options.getInboundBufferSize()]; _context.random().nextBytes(orig); for (int i = 0; i < numMsgs - 1; i++) { byte msg[] = new byte[_options.getMaxMessageSize()]; System.arraycopy(orig, i*_options.getMaxMessageSize(), msg, 0, _options.getMaxMessageSize()); in.messageReceived(i, new ByteArray(msg)); } assertTrue(in.canAccept(numMsgs, 1)); byte msg[] = new byte[_options.getMaxMessageSize()]; System.arraycopy(orig, (numMsgs-1)*_options.getMaxMessageSize(), msg, 0, _options.getMaxMessageSize()); in.messageReceived(numMsgs - 1, new ByteArray(msg)); assertFalse(in.canAccept(numMsgs, 1)); } @Test public void testCanAccept_smallerMsgsWithMaxSizeCount() { // Fill the buffer to one message under count that would reach limit with max-size msgs int numMsgs = _options.getInboundBufferSize() / _options.getMaxMessageSize(); byte orig[] = new byte[numMsgs*1024]; _context.random().nextBytes(orig); for (int i = 0; i < numMsgs - 1; i++) { byte msg[] = new byte[1024]; System.arraycopy(orig, i*1024, msg, 0, 1024); in.messageReceived(i, new ByteArray(msg)); } assertTrue(in.canAccept(numMsgs, 1)); byte msg[] = new byte[1024]; System.arraycopy(orig, (numMsgs-1)*1024, msg, 0, 1024); in.messageReceived(numMsgs - 1, new ByteArray(msg)); assertTrue(in.canAccept(numMsgs, 1)); } @Test public void testCanAccept_readyDup() { // Fill the buffer int numMsgs = _options.getInboundBufferSize() / _options.getMaxMessageSize(); byte orig[] = new byte[_options.getInboundBufferSize()]; _context.random().nextBytes(orig); for (int i = 0; i < numMsgs; i++) { byte msg[] = new byte[_options.getMaxMessageSize()]; System.arraycopy(orig, i*_options.getMaxMessageSize(), msg, 0, _options.getMaxMessageSize()); in.messageReceived(i, new ByteArray(msg)); } // Check that new messages won't be accepted assertFalse(in.canAccept(numMsgs, 1)); // Check that duplicate messages will be accepted assertTrue(in.canAccept(numMsgs-1, 1)); } @Test public void testCanAccept_notReadyDup() { // Fill the buffer int numMsgs = _options.getInboundBufferSize() / _options.getMaxMessageSize(); byte orig[] = new byte[_options.getInboundBufferSize()]; _context.random().nextBytes(orig); for (int i = 0; i < numMsgs; i++) { byte msg[] = new byte[_options.getMaxMessageSize()]; System.arraycopy(orig, i*_options.getMaxMessageSize(), msg, 0, _options.getMaxMessageSize()); if (i == numMsgs-1) in.messageReceived(numMsgs, new ByteArray(msg)); else in.messageReceived(i, new ByteArray(msg)); } // Check that duplicate notReady messages will be accepted assertTrue(in.canAccept(numMsgs, 1)); } @Test public void testCanAccept_msgIdExceedsBuffer() { // Fill the buffer to one message under limit with max-size msgs int numMsgs = _options.getInboundBufferSize() / _options.getMaxMessageSize(); byte orig[] = new byte[_options.getInboundBufferSize()]; _context.random().nextBytes(orig); for (int i = 0; i < numMsgs - 2; i++) { byte msg[] = new byte[_options.getMaxMessageSize()]; System.arraycopy(orig, i*_options.getMaxMessageSize(), msg, 0, _options.getMaxMessageSize()); in.messageReceived(i, new ByteArray(msg)); } // Add two half-size messages (to get past shortcut) byte msg[] = new byte[_options.getMaxMessageSize()/2]; System.arraycopy(orig, (numMsgs-1)*_options.getMaxMessageSize(), msg, 0, _options.getMaxMessageSize()/2); in.messageReceived(numMsgs - 2, new ByteArray(msg)); in.messageReceived(numMsgs - 1, new ByteArray(msg)); // Check that it predicts only one more msgId will be accepted assertTrue(in.canAccept(numMsgs, 1)); assertFalse(in.canAccept(numMsgs+1, 1)); } @Test public void testCanAccept_inOrderSmallMsgsDoS() { // Fill the buffer to one message under count that would trip DoS protection int numMsgs = 4 * _options.getMaxWindowSize(); byte orig[] = new byte[numMsgs]; _context.random().nextBytes(orig); for (int i = 0; i < numMsgs - 1; i++) { byte msg[] = new byte[1]; System.arraycopy(orig, i, msg, 0, 1); in.messageReceived(i, new ByteArray(msg)); } assertTrue(in.canAccept(numMsgs, 1)); // Trip DoS protection byte msg[] = new byte[1]; System.arraycopy(orig, (numMsgs-1), msg, 0, 1); in.messageReceived(numMsgs - 1, new ByteArray(msg)); assertFalse(in.canAccept(numMsgs, 1)); } @Test public void testGetNacks() { assertThat(in.getNacks(), is(nullValue())); in.messageReceived(0, new ByteArray()); assertThat(in.getNacks(), is(nullValue())); in.messageReceived(2, new ByteArray()); assertThat(in.getNacks(), is(equalTo(new long[] {1}))); in.messageReceived(4, new ByteArray()); assertThat(in.getNacks(), is(equalTo(new long[] {1, 3}))); in.messageReceived(1, new ByteArray()); assertThat(in.getNacks(), is(equalTo(new long[] {3}))); in.messageReceived(3, new ByteArray()); assertThat(in.getNacks(), is(nullValue())); } @Test public void testUpdateAcks_noMsgs() { in.updateAcks(packetLocal); verify(packetLocal).setAckThrough(-1); verify(packetLocal).setNacks(null); } @Test public void testUpdateAcks_inOrderMsgs() { in.messageReceived(0, new ByteArray()); in.messageReceived(1, new ByteArray()); in.messageReceived(2, new ByteArray()); in.updateAcks(packetLocal); verify(packetLocal).setAckThrough(2); verify(packetLocal).setNacks(null); } @Test public void testUpdateAcks_missingMsgs() { in.messageReceived(0, new ByteArray()); in.messageReceived(2, new ByteArray()); in.updateAcks(packetLocal); verify(packetLocal).setAckThrough(2); verify(packetLocal).setNacks(new long[] {1}); } @Test public void testReadTimeout() { assertThat(in.getReadTimeout(), is(-1)); in.setReadTimeout(100); assertThat(in.getReadTimeout(), is(100)); } @Test public void testInOrder() throws IOException { byte orig[] = new byte[256*1024]; _context.random().nextBytes(orig); for (int i = 0; i < orig.length / 1024; i++) { byte msg[] = new byte[1024]; System.arraycopy(orig, i*1024, msg, 0, 1024); in.messageReceived(i, new ByteArray(msg)); } byte read[] = new byte[orig.length]; int howMany = DataHelper.read(in, read); if (howMany != orig.length) fail("not enough bytes read [" + howMany + "]"); if (!DataHelper.eq(orig, read)) fail("data read is not equal"); _log.info("Passed test: in order"); } @Test public void testRandomOrder() throws IOException { byte orig[] = new byte[256*1024]; _context.random().nextBytes(orig); ArrayList<Integer> order = new ArrayList<Integer>(32); for (int i = 0; i < orig.length / 1024; i++) order.add(new Integer(i)); Collections.shuffle(order); for (int i = 0; i < orig.length / 1024; i++) { byte msg[] = new byte[1024]; Integer cur = (Integer)order.get(i); System.arraycopy(orig, cur.intValue()*1024, msg, 0, 1024); in.messageReceived(cur.intValue(), new ByteArray(msg)); _log.debug("Injecting " + cur); } byte read[] = new byte[orig.length]; int howMany = DataHelper.read(in, read); if (howMany != orig.length) fail("not enough bytes read [" + howMany + "]"); if (!DataHelper.eq(orig, read)) fail("data read is not equal"); _log.info("Passed test: random order"); } @Test public void testRandomDups() throws IOException { byte orig[] = new byte[256*1024]; _context.random().nextBytes(orig); for (int n = 0; n < 3; n++) { ArrayList<Integer> order = new ArrayList<Integer>(32); for (int i = 0; i < orig.length / 1024; i++) order.add(new Integer(i)); Collections.shuffle(order); for (int i = 0; i < orig.length / 1024; i++) { byte msg[] = new byte[1024]; Integer cur = (Integer)order.get(i); System.arraycopy(orig, cur.intValue()*1024, msg, 0, 1024); in.messageReceived(cur.intValue(), new ByteArray(msg)); _log.debug("Injecting " + cur); } } byte read[] = new byte[orig.length]; int howMany = DataHelper.read(in, read); if (howMany != orig.length) fail("not enough bytes read [" + howMany + "]"); if (!DataHelper.eq(orig, read)) fail("data read is not equal"); _log.info("Passed test: random dups"); } @Test public void testStaggered() throws IOException { byte orig[] = new byte[256*1024]; byte read[] = new byte[orig.length]; _context.random().nextBytes(orig); ArrayList<Integer> order = new ArrayList<Integer>(32); for (int i = 0; i < orig.length / 1024; i++) order.add(new Integer(i)); Collections.shuffle(order); int offset = 0; for (int i = 0; i < orig.length / 1024; i++) { byte msg[] = new byte[1024]; Integer cur = (Integer)order.get(i); System.arraycopy(orig, cur.intValue()*1024, msg, 0, 1024); in.messageReceived(cur.intValue(), new ByteArray(msg)); _log.debug("Injecting " + cur); if (in.available() > 0) { int curRead = in.read(read, offset, read.length-offset); _log.debug("read " + curRead); if (curRead == -1) fail("EOF with offset " + offset); else offset += curRead; } } if (!DataHelper.eq(orig, read)) fail("data read is not equal"); _log.info("Passed test: staggered"); } }