Issue #4475 - improve testing for MessageInputStream

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2020-01-17 18:37:47 +11:00
parent 3d309a5797
commit 08b1be6ea8
1 changed files with 212 additions and 145 deletions

View File

@ -24,12 +24,15 @@ import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.websocket.api.SuspendToken;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -37,6 +40,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import static java.time.Duration.ofSeconds; import static java.time.Duration.ofSeconds;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@ExtendWith(WorkDirExtension.class) @ExtendWith(WorkDirExtension.class)
public class MessageInputStreamTest public class MessageInputStreamTest
@ -48,116 +53,107 @@ public class MessageInputStreamTest
@Test @Test
public void testBasicAppendRead() throws IOException public void testBasicAppendRead() throws IOException
{ {
try (MessageInputStream stream = new MessageInputStream(new EmptySession())) StreamTestSession session = new StreamTestSession();
{ MessageInputStream stream = new MessageInputStream(session);
Assertions.assertTimeoutPreemptively(ofSeconds(5), () -> session.setMessageInputStream(stream);
{
// Append a single message (simple, short)
ByteBuffer payload = BufferUtil.toBuffer("Hello World", StandardCharsets.UTF_8);
boolean fin = true;
stream.appendFrame(payload, fin);
// Read entire message it from the stream. // Append a single message (simple, short)
byte[] buf = new byte[32]; ByteBuffer payload = BufferUtil.toBuffer("Hello World!", StandardCharsets.UTF_8);
int len = stream.read(buf); session.addContent(payload, true);
String message = new String(buf, 0, len, StandardCharsets.UTF_8); session.provideContent();
// Test it // Read entire message it from the stream.
assertThat("Message", message, is("Hello World")); byte[] buf = new byte[32];
}); int len = stream.read(buf);
} String message = new String(buf, 0, len, StandardCharsets.UTF_8);
// Test it
assertThat("Message", message, is("Hello World!"));
} }
@Test @Test
public void testBlockOnRead() throws Exception public void testBlockOnRead() throws Exception
{ {
try (MessageInputStream stream = new MessageInputStream(new EmptySession())) StreamTestSession session = new StreamTestSession();
MessageInputStream stream = new MessageInputStream(session);
session.setMessageInputStream(stream);
new Thread(session::provideContent).start();
final AtomicBoolean hadError = new AtomicBoolean(false);
final CountDownLatch startLatch = new CountDownLatch(1);
// This thread fills the stream (from the "worker" thread)
// But slowly (intentionally).
new Thread(() ->
{ {
final AtomicBoolean hadError = new AtomicBoolean(false); try
final CountDownLatch startLatch = new CountDownLatch(1);
// This thread fills the stream (from the "worker" thread)
// But slowly (intentionally).
new Thread(new Runnable()
{ {
@Override startLatch.countDown();
public void run() TimeUnit.MILLISECONDS.sleep(200);
{ session.addContent("Saved", false);
try TimeUnit.MILLISECONDS.sleep(200);
{ session.addContent(" by ", false);
startLatch.countDown(); TimeUnit.MILLISECONDS.sleep(200);
boolean fin = false; session.addContent("Zero", false);
TimeUnit.MILLISECONDS.sleep(200); TimeUnit.MILLISECONDS.sleep(200);
stream.appendFrame(BufferUtil.toBuffer("Saved", StandardCharsets.UTF_8), fin); session.addContent("", true);
TimeUnit.MILLISECONDS.sleep(200); }
stream.appendFrame(BufferUtil.toBuffer(" by ", StandardCharsets.UTF_8), fin); catch (Throwable t)
fin = true;
TimeUnit.MILLISECONDS.sleep(200);
stream.appendFrame(BufferUtil.toBuffer("Zero", StandardCharsets.UTF_8), fin);
}
catch (IOException | InterruptedException e)
{
hadError.set(true);
e.printStackTrace(System.err);
}
}
}).start();
Assertions.assertTimeoutPreemptively(ofSeconds(5), () ->
{ {
// wait for thread to start hadError.set(true);
startLatch.await(); t.printStackTrace(System.err);
}
}).start();
// Read it from the stream. Assertions.assertTimeoutPreemptively(ofSeconds(5), () ->
byte[] buf = new byte[32]; {
int len = stream.read(buf); // wait for thread to start
String message = new String(buf, 0, len, StandardCharsets.UTF_8); startLatch.await();
// Test it // Read it from the stream.
assertThat("Error when appending", hadError.get(), is(false)); byte[] buf = new byte[32];
assertThat("Message", message, is("Saved by Zero")); int len = stream.read(buf);
}); String message = new String(buf, 0, len, StandardCharsets.UTF_8);
}
// Test it
assertThat("Error when appending", hadError.get(), is(false));
assertThat("Message", message, is("Saved by Zero"));
});
} }
@Test @Test
public void testBlockOnReadInitial() throws IOException public void testBlockOnReadInitial() throws IOException
{ {
try (MessageInputStream stream = new MessageInputStream(new EmptySession())) StreamTestSession session = new StreamTestSession();
MessageInputStream stream = new MessageInputStream(session);
session.setMessageInputStream(stream);
session.addContent("I will conquer", true);
AtomicReference<Throwable> error = new AtomicReference<>();
new Thread(() ->
{ {
final AtomicBoolean hadError = new AtomicBoolean(false); try
new Thread(new Runnable()
{ {
@Override // wait for a little bit before initiating write to stream
public void run() TimeUnit.MILLISECONDS.sleep(1000);
{ session.provideContent();
try }
{ catch (Throwable t)
boolean fin = true;
// wait for a little bit before populating buffers
TimeUnit.MILLISECONDS.sleep(400);
stream.appendFrame(BufferUtil.toBuffer("I will conquer", StandardCharsets.UTF_8), fin);
}
catch (IOException | InterruptedException e)
{
hadError.set(true);
e.printStackTrace(System.err);
}
}
}).start();
Assertions.assertTimeoutPreemptively(ofSeconds(10), () ->
{ {
// Read byte from stream. error.set(t);
int b = stream.read(); t.printStackTrace(System.err);
// Should be a byte, blocking till byte received. }
}).start();
// Test it Assertions.assertTimeoutPreemptively(ofSeconds(10), () ->
assertThat("Error when appending", hadError.get(), is(false)); {
assertThat("Initial byte", b, is((int)'I')); // Read byte from stream, block until byte received.
}); int b = stream.read();
} assertThat("Initial byte", b, is((int)'I'));
// No error occurred.
assertNull(error.get());
});
} }
@Test @Test
@ -167,89 +163,160 @@ public class MessageInputStreamTest
{ {
final AtomicBoolean hadError = new AtomicBoolean(false); final AtomicBoolean hadError = new AtomicBoolean(false);
new Thread(new Runnable() new Thread(() ->
{ {
@Override try
public void run()
{ {
try // wait for a little bit before sending input closed
{ TimeUnit.MILLISECONDS.sleep(1000);
// wait for a little bit before sending input closed stream.messageComplete();
TimeUnit.MILLISECONDS.sleep(400); }
stream.messageComplete(); catch (InterruptedException e)
} {
catch (InterruptedException e) hadError.set(true);
{ e.printStackTrace(System.err);
hadError.set(true);
e.printStackTrace(System.err);
}
} }
}).start(); }).start();
Assertions.assertTimeoutPreemptively(ofSeconds(10), () -> Assertions.assertTimeoutPreemptively(ofSeconds(10), () ->
{ {
// Read byte from stream. // Read byte from stream. Should be a -1, indicating the end of the stream.
int b = stream.read(); int b = stream.read();
// Should be a -1, indicating the end of the stream.
// Test it
assertThat("Error when appending", hadError.get(), is(false));
assertThat("Initial byte", b, is(-1)); assertThat("Initial byte", b, is(-1));
// No error occurred.
assertThat("Error when appending", hadError.get(), is(false));
}); });
} }
} }
@Test @Test
public void testAppendEmptyPayloadRead() throws IOException public void testSplitMessageWithEmptyPayloads() throws IOException
{ {
try (MessageInputStream stream = new MessageInputStream(new EmptySession())) StreamTestSession session = new StreamTestSession();
{ MessageInputStream stream = new MessageInputStream(session);
Assertions.assertTimeoutPreemptively(ofSeconds(10), () -> session.setMessageInputStream(stream);
{
// Append parts of message
ByteBuffer msg1 = BufferUtil.toBuffer("Hello ", StandardCharsets.UTF_8);
ByteBuffer msg2 = ByteBuffer.allocate(0); // what is being tested
ByteBuffer msg3 = BufferUtil.toBuffer("World", StandardCharsets.UTF_8);
stream.appendFrame(msg1, false); session.addContent("", false);
stream.appendFrame(msg2, false); session.addContent("Hello", false);
stream.appendFrame(msg3, true); session.addContent("", false);
session.addContent(" World", false);
session.addContent("!", false);
session.addContent("", true);
session.provideContent();
// Read entire message it from the stream. // Read entire message it from the stream.
byte[] buf = new byte[32]; byte[] buf = new byte[32];
int len = stream.read(buf); int len = stream.read(buf);
String message = new String(buf, 0, len, StandardCharsets.UTF_8); String message = new String(buf, 0, len, StandardCharsets.UTF_8);
// Test it // Test it
assertThat("Message", message, is("Hello World")); assertThat("Message", message, is("Hello World!"));
});
}
} }
@Test @Test
public void testAppendNullPayloadRead() throws IOException public void testReadBeforeFirstAppend() throws IOException
{ {
try (MessageInputStream stream = new MessageInputStream(new EmptySession())) StreamTestSession session = new StreamTestSession();
MessageInputStream stream = new MessageInputStream(session);
session.setMessageInputStream(stream);
// Append a single message (simple, short)
session.addContent(BufferUtil.EMPTY_BUFFER, false);
session.addContent("Hello World", true);
new Thread(() ->
{ {
Assertions.assertTimeoutPreemptively(ofSeconds(10), () -> try
{ {
// Append parts of message Thread.sleep(2000);
ByteBuffer msg1 = BufferUtil.toBuffer("Hello ", StandardCharsets.UTF_8); session.provideContent();
ByteBuffer msg2 = null; // what is being tested }
ByteBuffer msg3 = BufferUtil.toBuffer("World", StandardCharsets.UTF_8); catch (Exception e)
{
throw new RuntimeException(e);
}
}).start();
stream.appendFrame(msg1, false); // Read entire message it from the stream.
stream.appendFrame(msg2, false); byte[] buf = new byte[32];
stream.appendFrame(msg3, true); int len = stream.read(buf);
String message = new String(buf, 0, len, StandardCharsets.UTF_8);
// Read entire message it from the stream. // Test it
byte[] buf = new byte[32]; assertThat("Message", message, is("Hello World"));
int len = stream.read(buf); }
String message = new String(buf, 0, len, StandardCharsets.UTF_8);
// Test it public static class StreamTestSession extends EmptySession
assertThat("Message", message, is("Hello World")); {
}); private static final ByteBuffer EOF = BufferUtil.allocate(0);
private final AtomicBoolean suspended = new AtomicBoolean(false);
private BlockingArrayQueue<ByteBuffer> contentQueue = new BlockingArrayQueue<>();
private MessageInputStream stream;
public void setMessageInputStream(MessageInputStream stream)
{
this.stream = stream;
}
public void addContent(String content, boolean last)
{
addContent(BufferUtil.toBuffer(content, StandardCharsets.UTF_8), last);
}
public void addContent(ByteBuffer content, boolean last)
{
contentQueue.add(content);
if (last)
contentQueue.add(EOF);
}
public void provideContent()
{
pollAndAppendFrame();
}
@Override
public void resume()
{
if (!suspended.compareAndSet(true, false))
throw new IllegalStateException();
pollAndAppendFrame();
}
@Override
public SuspendToken suspend()
{
if (!suspended.compareAndSet(false, true))
throw new IllegalStateException();
return super.suspend();
}
private void pollAndAppendFrame()
{
try
{
while (true)
{
ByteBuffer content = contentQueue.poll(10, TimeUnit.SECONDS);
assertNotNull(content);
boolean eof = (content == EOF);
stream.appendFrame(content, eof);
if (eof)
{
stream.messageComplete();
break;
}
if (suspended.get())
break;
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
} }
} }
} }