package org.limewire.nio.channel; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.Callable; import java.util.concurrent.Delayed; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import junit.framework.Test; import org.limewire.nio.observer.WriteObserver; import org.limewire.util.BaseTestCase; import org.limewire.util.PrivilegedAccessor; public class DelayedBufferWriterTest extends BaseTestCase { private WriteBufferChannel source; private WriteBufferChannel sink; public DelayedBufferWriterTest(String name) { super(name); } public static Test suite() { return buildTestSuite(DelayedBufferWriterTest.class); } @Override public void setUp() throws Exception { source = new WriteBufferChannel(); sink = new WriteBufferChannel(); } /** * tests that the chain is created properly */ public void testChainCreation() throws Exception { assertFalse(sink.status); assertNull(sink.observer); assertNull(source.channel); DelayedBufferWriter delayer = new DelayedBufferWriter(1,200, new StubScheduler()); setupChain(delayer); // test that when we set the sink we register that we are interested in events assertEquals(delayer, sink.observer); assertTrue(sink.status); assertEquals(delayer, source.channel); assertEquals(sink,delayer.getWriteChannel()); WriteObserver observer = (WriteObserver) PrivilegedAccessor.getValue(delayer, "observer"); assertEquals(source,observer); // test closing/opening assertTrue(delayer.isOpen()); delayer.close(); assertFalse(delayer.isOpen()); // closing of the sink should propagate to the delayer setUp(); delayer = new DelayedBufferWriter(1,200, new StubScheduler()); setupChain(delayer); assertTrue(delayer.isOpen()); sink.close(); assertFalse(delayer.isOpen()); } /** * tests that data is buffered until some time elapses */ public void testTimeFlush() throws Exception { byte [] data = new byte[]{(byte)1,(byte)2}; ByteBuffer buf = ByteBuffer.wrap(data); DelayedBufferWriter delayer = new DelayedBufferWriter(5,200, new StubScheduler()); setupChain(delayer); source.setBuffer(buf); sink.resize(5); // data should go from the source to the buffer, but not to the sink delayer.handleWrite(); assertEquals(2,source.position()); assertEquals(0,sink.position()); // after sleeping a while, data should move from the buffer to the sink Thread.sleep(300); delayer.handleWrite(); assertEquals(2,source.position()); assertEquals(2,sink.position()); // data should be 1,2 buf = sink.getBuffer(); assertEquals(1,buf.get()); assertEquals(2,buf.get()); } /** * tests that if the buffer is filled up it gets flushed immediately */ public void testForcedFlush() throws Exception { byte [] data = new byte[]{(byte)1,(byte)2,(byte)3}; ByteBuffer buf = ByteBuffer.wrap(data); DelayedBufferWriter delayer = new DelayedBufferWriter(2,200, new StubScheduler()); setupChain(delayer); source.setBuffer(buf); sink.resize(3); // data the size of the buffer should immediately be flushed, // and the rest should stay in the buffer delayer.handleWrite(); assertEquals(3,source.position()); assertEquals(2,sink.position()); // data should be 1,2 buf = sink.getBuffer(); assertEquals(1,buf.get()); assertEquals(2,buf.get()); } /** * Tests that excplicitly invoked flush writes as much data as it can. * @throws Exception */ public void testExplicitFlush() throws Exception { byte [] data = new byte[]{(byte)1,(byte)2}; ByteBuffer buf = ByteBuffer.wrap(data); DelayedBufferWriter delayer = new DelayedBufferWriter(5,200, new StubScheduler()); setupChain(delayer); source.setBuffer(buf); sink.resize(5); // data should go from the source to the buffer, but not to the sink delayer.handleWrite(); assertEquals(2,source.position()); assertEquals(0,sink.position()); // flushing should write out everything assertTrue(delayer.flush()); assertEquals(2, sink.position()); // flushing an empty buffer does nothing assertTrue(delayer.flush()); assertEquals(2, sink.position()); // add more data buf = ByteBuffer.allocate(5); source.setBuffer(buf); delayer.handleWrite(); // some should be written, not yet flushed assertEquals(5, source.position()); assertEquals(2, sink.position()); // a flush will not be able to write everything assertFalse(delayer.flush()); assertEquals(5, sink.position()); // no matter how many times we call it... assertFalse(delayer.flush()); assertFalse(delayer.flush()); assertFalse(delayer.flush()); } /** * tests that the buffer always looks if there's more data available * before writing */ public void testPartialBuffer() throws Exception { byte [] data = new byte[]{(byte)1}; ByteBuffer buf = ByteBuffer.wrap(data); DelayedBufferWriter delayer = new DelayedBufferWriter(2,200, new StubScheduler()); setupChain(delayer); source.setBuffer(buf); sink.resize(3); // data should be buffered, and not written out delayer.handleWrite(); assertEquals(1,source.position()); assertEquals(0,sink.position()); // add more data and sleep some time - // on the next write signal both new and old data should go out buf.rewind(); source.setBuffer(buf); Thread.sleep(300); delayer.handleWrite(); assertEquals(1,source.position()); assertEquals(2,sink.position()); // data should be 1,1 buf = sink.getBuffer(); assertEquals(1,buf.get()); assertEquals(1,buf.get()); } /** * tests that if nobody is interested in the buffer but its not empty, * it will turn its interest off but will schedule a flushing. * @throws Exception */ public void testFlushingScheduled() throws Exception { byte [] data = new byte[]{(byte)2}; ByteBuffer buf = ByteBuffer.wrap(data); StubScheduler scheduler = new StubScheduler(); DelayedBufferWriter delayer = new DelayedBufferWriter(2,200, scheduler); setupChain(delayer); source.setBuffer(buf); sink.resize(1); delayer.handleWrite(); // source->buf and source turns itself off // delayer should have turned itself off assertFalse(sink.status); // but there should be a scheduled flusher assertFalse(scheduler.tasks.isEmpty()); Runnable r = scheduler.tasks.get(0); // that should turn interest on Thread.sleep(201); r.run(); assertTrue(sink.status); // TODO: fix } public void testFlushingCancelled() throws Exception { byte [] data = new byte[]{(byte)2}; ByteBuffer buf = ByteBuffer.wrap(data); StubScheduler scheduler = new StubScheduler(); DelayedBufferWriter delayer = new DelayedBufferWriter(2,200, scheduler); setupChain(delayer); source.setBuffer(buf); sink.resize(1); delayer.handleWrite(); // source->buf and source turns itself off // delayer should have turned itself off assertFalse(sink.status); // but there should be a scheduled cleaner assertFalse(scheduler.tasks.isEmpty()); assertFalse(scheduler.futures.isEmpty()); assertFalse(scheduler.futures.get(0).isCancelled()); // buf if in the meantime someone becomes interested in the // delayer again the cleaner should be cancelled. delayer.interestWrite(source, true); assertTrue(scheduler.futures.get(0).isCancelled()); // even if the cancelling happens too late, the interester // should still do nothing. PrivilegedAccessor.setValue(sink,"status", Boolean.FALSE); Thread.sleep(200); scheduler.tasks.get(0).run(); assertFalse(sink.status); } public void testHasBufferedData() throws Exception { StubScheduler scheduler = new StubScheduler(); DelayedBufferWriter delayer = new DelayedBufferWriter(2, 100, scheduler); setupChain(delayer); sink.resize(2); assertFalse(delayer.hasBufferedOutput()); delayer.write(ByteBuffer.wrap(new byte[] { 1, 2 })); assertTrue(delayer.hasBufferedOutput()); delayer.handleWrite(); assertTrue(delayer.hasBufferedOutput()); assertTrue(delayer.flush()); assertTrue(sink.hasBufferedOutput()); assertTrue(delayer.hasBufferedOutput()); sink.getBuffer().clear(); assertFalse(delayer.hasBufferedOutput()); } private void setupChain(DelayedBufferWriter delayer) { source.setWriteChannel(delayer); delayer.interestWrite(source,true); delayer.setWriteChannel(sink); } private class StubScheduler extends AbstractExecutorService implements ScheduledExecutorService { List<Runnable> tasks = new ArrayList<Runnable>(); List<StubFuture> futures = new ArrayList<StubFuture>(); public ScheduledFuture<?> schedule(Runnable r, long delay, TimeUnit unit) { tasks.add(r); StubFuture f = new StubFuture(delay, r); futures.add(f); return f; } public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { throw new UnsupportedOperationException(); } public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { throw new UnsupportedOperationException(); } public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { throw new UnsupportedOperationException(); } public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { throw new UnsupportedOperationException(); } public boolean isShutdown() { throw new UnsupportedOperationException(); } public boolean isTerminated() { throw new UnsupportedOperationException(); } public void shutdown() { throw new UnsupportedOperationException(); } public List<Runnable> shutdownNow() { throw new UnsupportedOperationException(); } public void execute(Runnable command) { throw new UnsupportedOperationException(); } } private class StubFuture implements ScheduledFuture { final long delay; final Runnable r; boolean cancelled; StubFuture(long delay, Runnable r) { this.delay = delay; this.r = r; } public boolean cancel(boolean mayInterruptIfRunning) { cancelled = true; return false; } public Object get() throws InterruptedException, ExecutionException { return null; } public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return null; } public boolean isCancelled() { return cancelled; } public boolean isDone() { return false; } public long getDelay(TimeUnit unit) { return 0; } public int compareTo(Delayed o) { return 0; } } }