package edu.brown.protorpc;
import static org.junit.Assert.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CountDownLatch;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import sun.misc.Signal;
public class NIOEventLoopTest {
private static final Signal SIGINT = new Signal("INT");
private static final class SigIntSender implements Runnable {
public void run() {
Signal.raise(SIGINT);
}
}
protected NIOEventLoop eventLoop;
protected TestHandler serverHandler;
protected ServerSocketChannel acceptSocket;
protected int serverPort;
@Before
public void setUp() throws IOException {
eventLoop = new NIOEventLoop();
serverHandler = new TestHandler();
acceptSocket = ServerSocketChannel.open();
// Mac OS X: Must bind() before calling Selector.register, or you don't get accept() events
acceptSocket.socket().bind(null);
serverPort = acceptSocket.socket().getLocalPort();
}
@After
public void tearDown() throws IOException {
acceptSocket.close();
}
@Test(timeout=5000)
public void testSigInt() throws InterruptedException {
try {
eventLoop.setExitOnSigInt(false);
fail("expected exception");
} catch (IllegalStateException e) {}
eventLoop.setExitOnSigInt(true);
try {
eventLoop.setExitOnSigInt(true);
fail("expected exception");
} catch (IllegalStateException e) {}
eventLoop.setExitOnSigInt(false);
eventLoop.setExitOnSigInt(true);
// Deliver SIGINT before looping
Signal.raise(SIGINT);
eventLoop.run();
// Deliver SIGINT while looping
eventLoop.setExitOnSigInt(true);
SigIntSender sender = new SigIntSender();
eventLoop.runInEventThread(sender);
eventLoop.run();
class Waker extends Thread {
public void run() {
try {
sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
eventLoop.exitLoop();
}
}
Waker waker = new Waker();
waker.start();
// This should block until woken by something else
eventLoop.run();
waker.join(1);
assertFalse(waker.isAlive());
}
static final class TestHandler extends AbstractEventHandler {
SocketChannel client;
boolean write;
boolean connected;
int timerExpiredCount = 0;
@Override
public void acceptCallback(SelectableChannel channel) {
// accept the connection
assert client == null;
try {
client = ((ServerSocketChannel) channel).accept();
} catch (IOException e) {
throw new RuntimeException(e);
}
assert client != null;
}
@Override
public void connectCallback(SocketChannel channel) {
connected = true;
}
@Override
public boolean writeCallback(SelectableChannel channel) {
assert !write;
write = true;
return true;
}
@Override
public void timerCallback() {
timerExpiredCount += 1;
}
}
private static void writeAll(SocketChannel channel, String message) {
ByteBuffer b = ByteBuffer.allocate(4096);
b.position(b.limit());
while (b.remaining() == 0) {
b.clear();
try {
int bytes = channel.write(b);
assert bytes >= 0;
//~ System.out.println(message + " " + bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Test
public void testWriteUnblocking() throws IOException, InterruptedException {
eventLoop.registerAccept(acceptSocket, serverHandler);
// Run a client in a new thread: connect, wait for a latch, read as much as possible
final CountDownLatch startReadingLatch = new CountDownLatch(1);
Thread client = new Thread() {
public void run() {
try {
SocketChannel clientSocket = SocketChannel.open();
clientSocket.connect(
new InetSocketAddress(InetAddress.getLocalHost(), serverPort));
startReadingLatch.await();
ByteBuffer input = ByteBuffer.allocate(4096);
int bytes = clientSocket.read(input);
assert bytes > 0;
//~ System.out.println("read " + bytes);
clientSocket.configureBlocking(false);
while (input.position() != 0) {
input.clear();
bytes = clientSocket.read(input);
//~ System.out.println("read " + bytes);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
client.start();
assertNull(serverHandler.client);
eventLoop.runOnce();
serverHandler.client.configureBlocking(false);
// Write data into the socket until it blocks. We seem to need to do this twice on Linux
// and Mac OS X to actually fill the buffer.
for (int i = 0; i < 2; ++i) {
writeAll(serverHandler.client, "write" + i);
if (i == 0) {
// Register a write callback (can only do this once)
eventLoop.registerWrite(serverHandler.client, serverHandler);
try {
eventLoop.registerWrite(serverHandler.client, serverHandler);
fail("expected exception");
} catch (AssertionError e) {}
} else {
assert i == 1;
// trigger reading
startReadingLatch.countDown();
}
assertFalse(serverHandler.write);
eventLoop.runOnce();
assertTrue(serverHandler.write);
serverHandler.write = false;
}
int count = serverHandler.client.write(ByteBuffer.allocate(4096));
assertTrue(count > 0);
//~ System.out.println("final write " + count);
client.join();
}
@Test
public void testConnectCallback() throws IOException {
eventLoop.registerAccept(acceptSocket, serverHandler);
SocketChannel clientSocket = SocketChannel.open();
clientSocket.configureBlocking(false);
boolean connected = clientSocket.connect(
new InetSocketAddress(InetAddress.getLocalHost(), serverPort));
assertFalse(connected);
assertTrue(clientSocket.isConnectionPending());
eventLoop.registerConnect(clientSocket, serverHandler);
// registering it for reading as well is not permitted: causes problems on Linux
try {
eventLoop.registerRead(clientSocket, serverHandler);
fail("expected exception");
} catch (AssertionError e) {}
// The event loop will trigger the accept callback
assertNull(serverHandler.client);
eventLoop.runOnce();
assertNotNull(serverHandler.client);
assertTrue(clientSocket.isConnectionPending());
// The event loop will also have triggered the connect callback
assertTrue(serverHandler.connected);
connected = clientSocket.finishConnect();
assertTrue(connected);
assertTrue(clientSocket.isConnected());
// Registering some other handler in response to the connect event should work
eventLoop.registerRead(clientSocket, new TestHandler());
clientSocket.close();
}
// @Test(timeout=2000)
// public void testTimeout() {
// long start = System.nanoTime();
// int msDelay = 500;
// eventLoop.registerTimer(msDelay, serverHandler);
// int loopCount = 0;
// while (serverHandler.timerExpiredCount == 0) {
// eventLoop.runOnce();
// loopCount += 1;
// }
// long end = System.nanoTime();
// assertTrue(end - start >= msDelay * 1000000);
// // Linux typically expires timeouts a few ms early, but we shouldn't have to loop more
// // than twice. But we could, so this might need adjustment.
// assertTrue(loopCount <= 2);
// }
// If this times out, it probably means all the timers didn't get set
@Test(timeout=500)
public void testManyTimeouts() {
final int MS_DELAY = 100;
final int NUM_TIMERS = 10;
for (int i = 0; i < NUM_TIMERS; ++i) {
eventLoop.registerTimer(MS_DELAY, serverHandler);
}
while (serverHandler.timerExpiredCount < NUM_TIMERS) {
eventLoop.runOnce();
}
}
@Test(timeout=2000)
public void testCancelTimeout() {
final int REAL_DELAY = 100;
eventLoop.registerTimer(REAL_DELAY - 50, serverHandler);
TestHandler handler2 = new TestHandler();
eventLoop.registerTimer(REAL_DELAY, handler2);
eventLoop.cancelTimer(serverHandler);
while (handler2.timerExpiredCount == 0) {
eventLoop.runOnce();
}
assertEquals(0, serverHandler.timerExpiredCount);
}
}