/**
* Copyright 2016 LinkedIn Corp. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*/
package com.github.ambry.commons;
import com.github.ambry.router.AsyncWritableChannel;
import com.github.ambry.router.CopyingAsyncWritableChannel;
import com.github.ambry.utils.ByteBufferInputStream;
import com.github.ambry.utils.TestUtils;
import com.github.ambry.utils.Utils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Tests functionality of {@link InputStreamReadableStreamChannel}.
*/
public class InputStreamReadableStreamChannelTest {
private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
/**
* Tests different types of {@link InputStream} and different sizes of the stream and ensures that the data is read
* correctly.
* @throws Exception
*/
@Test
public void commonCasesTest() throws Exception {
int bufSize = InputStreamReadableStreamChannel.BUFFER_SIZE;
int randSizeLessThanBuffer = TestUtils.RANDOM.nextInt(bufSize - 2) + 2;
int randMultiplier = TestUtils.RANDOM.nextInt(10);
int[] testStreamSizes =
{0, 1, randSizeLessThanBuffer, bufSize, bufSize + 1, bufSize * randMultiplier, bufSize * randMultiplier + 1};
for (int size : testStreamSizes) {
byte[] src = TestUtils.getRandomBytes(size);
InputStream stream = new ByteBufferInputStream(ByteBuffer.wrap(src));
doReadTest(stream, src, src.length);
stream = new ByteBufferInputStream(ByteBuffer.wrap(src));
doReadTest(stream, src, -1);
stream = new HaltingInputStream(new ByteBufferInputStream(ByteBuffer.wrap(src)));
doReadTest(stream, src, src.length);
stream = new HaltingInputStream(new ByteBufferInputStream(ByteBuffer.wrap(src)));
doReadTest(stream, src, -1);
}
}
/**
* Verfies behavior of {@link InputStreamReadableStreamChannel#close()}.
* @throws IOException
*/
@Test
public void closeTest() throws IOException {
final AtomicBoolean streamOpen = new AtomicBoolean(true);
InputStream stream = new InputStream() {
@Override
public int read() throws IOException {
throw new IllegalStateException("Not implemented");
}
@Override
public void close() {
streamOpen.set(false);
}
};
InputStreamReadableStreamChannel channel = new InputStreamReadableStreamChannel(stream, EXECUTOR_SERVICE);
assertTrue("Channel should be open", channel.isOpen());
channel.close();
assertFalse("Channel should be closed", channel.isOpen());
assertFalse("Stream should be closed", streamOpen.get());
// close again is ok
channel.close();
}
/**
* Tests that the right exceptions are thrown when reading into {@link AsyncWritableChannel} fails.
* @throws Exception
*/
@Test
public void readIntoAWCFailureTest() throws Exception {
String errMsg = "@@ExpectedExceptionMessage@@";
InputStream stream = new ByteBufferInputStream(ByteBuffer.allocate(1));
// Bad AWC.
InputStreamReadableStreamChannel channel = new InputStreamReadableStreamChannel(stream, EXECUTOR_SERVICE);
ReadIntoCallback callback = new ReadIntoCallback();
try {
channel.readInto(new BadAsyncWritableChannel(new IOException(errMsg)), callback).get();
fail("Should have failed because BadAsyncWritableChannel would have thrown exception");
} catch (ExecutionException e) {
Exception exception = (Exception) Utils.getRootCause(e);
assertEquals("Exception message does not match expected (future)", errMsg, exception.getMessage());
callback.awaitCallback();
assertEquals("Exception message does not match expected (callback)", errMsg, callback.exception.getMessage());
}
// Read after close.
channel = new InputStreamReadableStreamChannel(stream, EXECUTOR_SERVICE);
channel.close();
CopyingAsyncWritableChannel writeChannel = new CopyingAsyncWritableChannel();
callback = new ReadIntoCallback();
try {
channel.readInto(writeChannel, callback).get();
fail("InputStreamReadableStreamChannel has been closed, so read should have thrown ClosedChannelException");
} catch (ExecutionException e) {
Exception exception = (Exception) Utils.getRootCause(e);
assertTrue("Exception is not ClosedChannelException", exception instanceof ClosedChannelException);
callback.awaitCallback();
assertEquals("Exceptions of callback and future differ", exception.getMessage(), callback.exception.getMessage());
}
// Reading more than once.
channel = new InputStreamReadableStreamChannel(stream, EXECUTOR_SERVICE);
writeChannel = new CopyingAsyncWritableChannel();
channel.readInto(writeChannel, null);
try {
channel.readInto(writeChannel, null);
fail("Should have failed because readInto cannot be called more than once");
} catch (IllegalStateException e) {
// expected. Nothing to do.
}
}
/**
* Tests that the right exceptions are thrown when the provided {@link InputStream} has unexpected behavior.
* @throws Exception
*/
@Test
public void badInputStreamTest() throws Exception {
final String errMsg = "@@ExpectedExceptionMessage@@";
InputStream stream = new InputStream() {
@Override
public int read() throws IOException {
// this represents any exception - bad behavior or closure before being read completely.
throw new IllegalStateException(errMsg);
}
};
InputStreamReadableStreamChannel channel = new InputStreamReadableStreamChannel(stream, EXECUTOR_SERVICE);
ReadIntoCallback callback = new ReadIntoCallback();
try {
channel.readInto(new BadAsyncWritableChannel(new IOException(errMsg)), callback).get();
fail("Should have failed because the InputStream would have thrown exception");
} catch (ExecutionException e) {
Exception exception = (Exception) Utils.getRootCause(e);
assertEquals("Exception message does not match expected (future)", errMsg, exception.getMessage());
callback.awaitCallback();
assertEquals("Exception message does not match expected (callback)", errMsg, callback.exception.getMessage());
}
}
// helpers
// commonCasesTest() helpers
/**
* Does the test for reading from a {@link InputStreamReadableStreamChannel} that wraps the provided {@code stream}.
* Ensures that the data used to construct the {@code stream} ({@code src}) matches the data that is read from the
* created {@link InputStreamReadableStreamChannel}.
* @param stream the {@link InputStream} to use.
* @param src the data that was used to construct {@code stream}.
* @param sizeToProvide the size to provide to the constructor of {@link InputStreamReadableStreamChannel}.
* @throws Exception
*/
private void doReadTest(InputStream stream, byte[] src, int sizeToProvide) throws Exception {
InputStreamReadableStreamChannel channel;
if (sizeToProvide >= 0) {
channel = new InputStreamReadableStreamChannel(stream, sizeToProvide, EXECUTOR_SERVICE);
assertEquals("Reported size of channel incorrect", sizeToProvide, channel.getSize());
} else {
channel = new InputStreamReadableStreamChannel(stream, EXECUTOR_SERVICE);
assertEquals("Reported size of channel incorrect", -1, channel.getSize());
}
assertTrue("Channel should be open", channel.isOpen());
CopyingAsyncWritableChannel writableChannel = new CopyingAsyncWritableChannel(src.length);
ReadIntoCallback callback = new ReadIntoCallback();
long bytesRead = channel.readInto(writableChannel, callback).get(1, TimeUnit.SECONDS);
callback.awaitCallback();
if (callback.exception != null) {
throw callback.exception;
}
assertEquals("Total bytes written does not match (callback)", src.length, callback.bytesRead);
assertEquals("Total bytes written does not match (future)", src.length, bytesRead);
assertArrayEquals("Data does not match", src, writableChannel.getData());
channel.close();
assertFalse("Channel should be closed", channel.isOpen());
writableChannel.close();
}
}
/**
* Implementation of {@link InputStream} that sleeps before perfoming a read to simulate "blocking".
* <p/>
* Also reads only a fixed size on {@link #read(byte[], int, int)} to simulate partial data availability.
*/
class HaltingInputStream extends InputStream {
private final InputStream stream;
/**
* Constructs a HaltingInputStream from the provided {@code stream}.
* </p>
* Sleeps before calling the read methods on the underlying stream. In the case of {@link #read(byte[], int, int)},
* reads only the min of the length provided and a fixed size.
* @param stream the {@link InputStream} to use to provide the actual data.
*/
HaltingInputStream(InputStream stream) {
this.stream = stream;
}
@Override
public int read() throws IOException {
sleep();
return stream.read();
}
@Override
public int read(byte b[], int off, int len) throws IOException {
sleep();
// simulate partial data availability.
int lenToRead = Math.min(len, InputStreamReadableStreamChannel.BUFFER_SIZE / 2 - 1);
return stream.read(b, off, lenToRead);
}
/**
* Sleeps for a millisecond.
*/
private void sleep() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException("Sleep was interrupted", e);
}
}
}