package com.tryge.xocotl.io;
import com.tryge.xocotl.util.internal.Nop;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.mockito.stubbing.OngoingStubbing;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;
/**
* @author michael.zehender@me.com
*/
public class StreamedDuplexChannelTest {
private final List<byte[]> DUMMY = new ArrayList<byte[]>(1);
{
DUMMY.add(new byte[] { 1 });
}
private StreamedDuplexChannel channel;
private StreamSource source;
private Stream stream;
private Thread threadRead;
private Thread threadWrite;
private Responder responder;
private MessageDecoder decoder;
private ChannelListener channelListener;
private MessageListener messageListener;
@Before
public void setUp() throws IOException {
source = mock(StreamSource.class);
stream = mock(Stream.class);
threadRead = mock(Thread.class);
threadWrite = mock(Thread.class);
channelListener = mock(ChannelListener.class);
messageListener = mock(MessageListener.class);
decoder = mock(MessageDecoder.class);
responder = mock(Responder.class);
ThreadFactory factory = mock(ThreadFactory.class);
channel = new StreamedDuplexChannel(factory, responder, decoder, source, 32);
when(source.open()).thenReturn(stream);
when(factory.newThread(isA(StreamedDuplexChannel.Reader.class))).thenReturn(threadRead);
when(factory.newThread(isA(StreamedDuplexChannel.Writer.class))).thenReturn(threadWrite);
}
@Test(expected = NullPointerException.class)
public void testChannelListenerProtection() {
channel.setChannelListener(null);
}
@Test
public void testChannelListenerAcceptance() {
channel.setChannelListener(channelListener);
}
@Test(expected = NullPointerException.class)
public void testMessageListenerProtection() {
channel.setMessageListener(null);
}
@Test
public void testMessageListenerAcceptance() {
channel.setMessageListener(messageListener);
}
@Test
public void testOpenCloseCycle() throws Exception {
InOrder mocks = inOrder(source, stream, threadRead, threadWrite);
channel.open();
assertTrue(channel.isOpen());
channel.close();
assertTrue(channel.isClosed());
mocks.verify(source).open();
mocks.verify(threadRead).start();
mocks.verify(threadWrite).start();
mocks.verify(threadRead).interrupt();
mocks.verify(threadWrite).interrupt();
mocks.verify(stream).close();
// can't verify that join is called
// mocks.verify(threadRead).join();
// mocks.verify(threadWrite).join();
}
@Test(expected = IllegalStateException.class)
public void doubleOpenThrows() throws IOException {
when(source.open()).thenReturn(stream);
channel.open();
channel.open();
}
@Test
public void doubleCloseIsIgnored() throws IOException {
when(source.open()).thenReturn(stream);
channel.open();
channel.close();
channel.close();
}
@SuppressWarnings("unchecked")
@Test(expected = IOException.class)
public void testOpenThrowsIfStreamSourceThrows() throws IOException {
when(source.open()).thenThrow(IOException.class);
channel.open();
}
@Test(expected = IOException.class)
public void testCloseThrowsIfStreamThrows() throws IOException {
when(source.open()).thenReturn(stream);
doThrow(IOException.class).when(stream).close();
channel.open();
channel.close();
}
@Test(expected = IllegalStateException.class)
public void testSendWhenClosed1() throws IOException {
Message message = mock(Message.class);
when(source.open()).thenReturn(stream);
channel.open();
channel.close();
channel.send(message);
}
@Test
public void testSendMessageAndForget() throws Exception {
final CyclicBarrier barrier = new CyclicBarrier(2);
Message message = mock(Message.class);
OutputStream out = mock(OutputStream.class);
when(source.open()).thenReturn(stream);
when(stream.getOutputStream()).thenReturn(out);
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
barrier.await(1, TimeUnit.SECONDS);
return null;
}
}).when(message).writeTo(same(out));
channel.open();
Thread thread = new Thread(channel.writer);
thread.start();
channel.sendAndForget(message);
barrier.await(1, TimeUnit.SECONDS);
thread.interrupt();
channel.sendAndForget(StreamedDuplexChannel.POISON);
thread.join();
verify(message).writeTo(same(out));
}
@Test
public void testSendMessageEx() throws Exception {
final CyclicBarrier barrier = new CyclicBarrier(2);
Message message = mock(Message.class);
OutputStream out = mock(OutputStream.class);
when(source.open()).thenReturn(stream);
when(stream.getOutputStream()).thenReturn(out);
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
barrier.await(1, TimeUnit.SECONDS);
return null;
}
}).when(message).writeTo(same(out));
channel.open();
Thread thread = new Thread(channel.writer);
thread.start();
Future<Void> future = channel.sendEx(message);
barrier.await(1, TimeUnit.SECONDS);
assertTrue(future.isDone());
// if it doesn't throw an exception it was successful
future.get();
thread.interrupt();
channel.sendAndForget(StreamedDuplexChannel.POISON);
thread.join();
verify(message).writeTo(same(out));
}
@Test
public void testSendMessage() throws Exception {
final CyclicBarrier barrier = new CyclicBarrier(2);
Message message = mock(Message.class);
final Message response = mock(Message.class);
OutputStream out = mock(OutputStream.class);
final ResponderMessage responderMessage = new ResponderMessage(message, new Nop());
when(responder.register(same(channel), same(message))).thenReturn(responderMessage);
when(message.getId()).thenReturn("test");
when(response.isResponseTo()).thenReturn("test");
when(source.open()).thenReturn(stream);
when(stream.getOutputStream()).thenReturn(out);
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
responderMessage.future().succeeded(response);
barrier.await(1, TimeUnit.SECONDS);
return null;
}
}).when(message).writeTo(same(out));
channel.open();
Thread thread = new Thread(channel.writer);
thread.start();
Future<Message> future = channel.send(message);
barrier.await(1, TimeUnit.SECONDS);
assertTrue(future.isDone());
// if it doesn't throw an exception it was successful
Message received = future.get();
thread.interrupt();
channel.sendAndForget(StreamedDuplexChannel.POISON);
thread.join();
verify(message).writeTo(same(out));
assertSame(response, received);
}
/**
* This test case assumes that the read method of the input stream just
* returns -1 if the stream is closed.
*
* @throws InterruptedException
* @throws IOException
*/
@Test
public void testReadCycleEOF() throws Exception {
InputStream in = testReadCycleStartup(DUMMY, new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
try {
//noinspection InfiniteLoopStatement
for (; ; ) {
Thread.sleep(100);
}
} catch (InterruptedException ie) {
return -1;
}
}
});
//noinspection ResultOfMethodCallIgnored
verify(in, times(2)).read(isA(byte[].class));
}
/**
* This test case assumes that the read method of the input stream throws
* an exception if closed or something bad happens
*
* @throws InterruptedException
* @throws IOException
*/
@Test
public void testReadCycleException() throws Exception {
InputStream in = testReadCycleStartup(DUMMY, new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
try {
//noinspection InfiniteLoopStatement
for (;;) {
Thread.sleep(100);
}
} catch (InterruptedException ie) {
throw new IOException("test");
}
}
});
//noinspection ResultOfMethodCallIgnored
verify(in, times(2)).read(isA(byte[].class));
}
@Test
public void testReadMessage() throws Exception {
Message message = mock(Message.class);
when(decoder.decode(notNull(ByteBuffer.class)))
.thenReturn(message)
.thenReturn(null);
when(message.isResponse()).thenReturn(false);
testReadCycleStartup(DUMMY, eofAnswer);
verify(decoder, atLeast(1)).decode(isA(ByteBuffer.class));
verify(messageListener).onMessage(same(message));
verify(channelListener).onClose(same(channel));
verifyNoMoreInteractions(messageListener, channelListener);
}
@Test
public void testReadResponse() throws Exception {
Message message = mock(Message.class);
when(decoder.decode(notNull(ByteBuffer.class)))
.thenReturn(message)
.thenReturn(null);
when(message.isResponse()).thenReturn(true);
testReadCycleStartup(DUMMY, eofAnswer);
verify(decoder, atLeast(1)).decode(isA(ByteBuffer.class));
verify(responder).responseReceived(same(channel), same(message));
verify(channelListener).onClose(same(channel));
verifyNoMoreInteractions(messageListener, channelListener);
}
@Test
public void testReadMultiple() throws Exception {
List<byte[]> messages = new ArrayList<byte[]>(3);
messages.add(new byte[] {1, 2, 3, 4, 1});
messages.add(new byte[] {2, 3});
messages.add(new byte[] {4});
final Message message = mock(Message.class);
when(decoder.decode(notNull(ByteBuffer.class)))
.thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
ByteBuffer buffer = (ByteBuffer)invocation.getArguments()[0];
if (buffer.remaining() >= 4) {
byte[] check = new byte[4];
buffer.get(check);
if (check[0] != 1 || check[1] != 2 || check[2] != 3 || check[3] != 4) {
throw new IllegalArgumentException("buffer doesn't contain the correct data");
}
return message;
} else if (buffer.remaining() >= 1) {
buffer.get();
}
return null;
}
});
when(message.isResponse()).thenReturn(false);
testReadCycleStartup(messages, eofAnswer);
verify(messageListener, times(2)).onMessage(same(message));
}
private final Answer<Integer> eofAnswer = new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
return -1;
}
};
private InputStream testReadCycleStartup(List<byte[]> messages, final Answer<Integer> answer) throws Exception {
final CyclicBarrier barrier = new CyclicBarrier(2);
InputStream in = mock(InputStream.class);
when(stream.getInputStream()).thenReturn(in);
OngoingStubbing<Integer> stubbing = when(in.read(isA(byte[].class)));
// first return all the messages to be read
for (byte[] msg : messages) {
final byte[] message = msg;
stubbing = stubbing.thenAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
byte[] param = (byte[]) invocation.getArguments()[0];
if (param.length < message.length) {
throw new RuntimeException("Test Case Invalid: message must be smaller than param");
}
System.arraycopy(message, 0, param, 0, message.length);
return message.length;
}
});
}
// further reads execute the specified answer
stubbing.thenAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
if (!barrier.isBroken()) {
barrier.await();
}
return answer.answer(invocation);
}
});
// initialize the stream
channel.setChannelListener(channelListener);
channel.setMessageListener(messageListener);
channel.open();
Thread thread = new Thread(channel.reader);
thread.start();
barrier.await(1, TimeUnit.SECONDS);
thread.interrupt();
thread.join();
return in;
}
}