/** * 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.rest.MockRestRequest; import com.github.ambry.router.AsyncWritableChannel; import com.github.ambry.router.Callback; import com.github.ambry.router.FutureResult; import com.github.ambry.router.ReadableStreamChannel; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; import static org.junit.Assert.*; /** * Tests functionality of {@link ReadableStreamChannelInputStream}. */ public class ReadableStreamChannelInputStreamTest { private static final int CONTENT_SPLIT_PART_COUNT = 5; // intentionally unequal private static final int READ_PART_COUNT = 4; /** * Tests the common cases i.e reading byte by byte, reading by parts and reading all at once. * @throws Exception */ @Test public void commonCaseTest() throws Exception { int[] sizes = {0, 1024 * CONTENT_SPLIT_PART_COUNT}; for (int size : sizes) { byte[] in = new byte[size]; new Random().nextBytes(in); readByteByByteTest(in); readPartByPartTest(in); readAllAtOnceTest(in); } } /** * Tests cases where read() methods get incorrect input. * @throws IOException */ @Test public void readErrorCasesTest() throws IOException { byte[] in = new byte[1024]; new Random().nextBytes(in); ReadableStreamChannel channel = new ByteBufferReadableStreamChannel(ByteBuffer.wrap(in)); InputStream dstInputStream = new ReadableStreamChannelInputStream(channel); try { dstInputStream.read(null, 0, in.length); fail("The read should have failed"); } catch (NullPointerException e) { // expected. nothing to do. } byte[] out = new byte[in.length]; try { dstInputStream.read(out, -1, out.length); fail("The read should have failed"); } catch (IndexOutOfBoundsException e) { // expected. nothing to do. } try { dstInputStream.read(out, 0, -1); fail("The read should have failed"); } catch (IndexOutOfBoundsException e) { // expected. nothing to do. } try { dstInputStream.read(out, 0, out.length + 1); fail("The read should have failed"); } catch (IndexOutOfBoundsException e) { // expected. nothing to do. } assertEquals("Bytes read should have been 0 because passed len was 0", 0, dstInputStream.read(out, 0, 0)); } /** * Tests correctness of {@link ReadableStreamChannelInputStream#available()}. * @throws Exception */ @Test public void availableTest() throws Exception { int[] sizes = {0, 1024 * CONTENT_SPLIT_PART_COUNT}; for (int size : sizes) { byte[] in = new byte[size]; new Random().nextBytes(in); // channel with size and one piece of content. ReadableStreamChannel channel = new ByteBufferReadableStreamChannel(ByteBuffer.wrap(in)); InputStream stream = new ReadableStreamChannelInputStream(channel); doAvailableTest(stream, in, in.length); stream.close(); // channel with no size and multiple pieces of content. channel = new NoSizeRSC(ByteBuffer.wrap(in)); stream = new ReadableStreamChannelInputStream(channel); doAvailableTest(stream, in, in.length); stream.close(); // channel with no size and multiple pieces of content. List<ByteBuffer> contents = splitContent(in, CONTENT_SPLIT_PART_COUNT); contents.add(null); // assuming all parts are the same length. int partLength = contents.get(0).remaining(); channel = new MockRestRequest(MockRestRequest.DUMMY_DATA, contents); stream = new ReadableStreamChannelInputStream(channel); doAvailableTest(stream, in, partLength); stream.close(); } } /** * Tests for the case when reads are incomplete either because exceptions were thrown or the read simply did not * complete. * @throws IOException */ @Test public void incompleteReadsTest() throws IOException { // Exception during read String exceptionMsg = "@@randomMsg@@"; Exception exceptionToThrow = new Exception(exceptionMsg); ReadableStreamChannel channel = new IncompleteReadReadableStreamChannel(exceptionToThrow); InputStream inputStream = new ReadableStreamChannelInputStream(channel); try { inputStream.read(); fail("The read should have failed"); } catch (Exception e) { while (e.getCause() != null) { e = (Exception) e.getCause(); } assertEquals("Exception messages do not match", exceptionMsg, e.getMessage()); } // incomplete read channel = new IncompleteReadReadableStreamChannel(null); inputStream = new ReadableStreamChannelInputStream(channel); try { inputStream.read(); fail("The read should have failed"); } catch (IllegalStateException e) { // expected. Nothing to do. } } // helpers // commonCaseTest() helpers /** * Tests reading {@link ReadableStreamChannelInputStream} byte by byte. * @param in the data that the {@link ReadableStreamChannelInputStream} should contain. * @throws Exception */ private void readByteByByteTest(byte[] in) throws Exception { // channel with size and one piece of content. ReadableStreamChannel channel = new ByteBufferReadableStreamChannel(ByteBuffer.wrap(in)); InputStream stream = new ReadableStreamChannelInputStream(channel); doReadByteByByteTest(stream, in); stream.close(); // channel with no size but one piece of content. channel = new NoSizeRSC(ByteBuffer.wrap(in)); stream = new ReadableStreamChannelInputStream(channel); doReadByteByByteTest(stream, in); stream.close(); // channel with no size and multiple pieces of content. List<ByteBuffer> contents = splitContent(in, CONTENT_SPLIT_PART_COUNT); contents.add(null); channel = new MockRestRequest(MockRestRequest.DUMMY_DATA, contents); stream = new ReadableStreamChannelInputStream(channel); doReadByteByByteTest(stream, in); stream.close(); } /** * Tests reading {@link ReadableStreamChannelInputStream} part by part. * @param in the data that the {@link ReadableStreamChannelInputStream} should contain. * @throws Exception */ private void readPartByPartTest(byte[] in) throws Exception { // channel with size and one piece of content. ReadableStreamChannel channel = new ByteBufferReadableStreamChannel(ByteBuffer.wrap(in)); InputStream stream = new ReadableStreamChannelInputStream(channel); doReadPartByPartTest(stream, in); stream.close(); // channel with no size but one piece of content. channel = new NoSizeRSC(ByteBuffer.wrap(in)); stream = new ReadableStreamChannelInputStream(channel); doReadPartByPartTest(stream, in); stream.close(); // channel with no size and multiple pieces of content. List<ByteBuffer> contents = splitContent(in, CONTENT_SPLIT_PART_COUNT); contents.add(null); channel = new MockRestRequest(MockRestRequest.DUMMY_DATA, contents); stream = new ReadableStreamChannelInputStream(channel); doReadPartByPartTest(stream, in); stream.close(); } /** * Tests reading {@link ReadableStreamChannelInputStream} all at once. * @param in the data that the {@link ReadableStreamChannelInputStream} should contain. * @throws Exception */ private void readAllAtOnceTest(byte[] in) throws Exception { // channel with size and one piece of content. ReadableStreamChannel channel = new ByteBufferReadableStreamChannel(ByteBuffer.wrap(in)); InputStream stream = new ReadableStreamChannelInputStream(channel); doReadAllAtOnceTest(stream, in); stream.close(); // channel with no size but one piece of content. channel = new NoSizeRSC(ByteBuffer.wrap(in)); stream = new ReadableStreamChannelInputStream(channel); doReadAllAtOnceTest(stream, in); stream.close(); // channel with no size and multiple pieces of content. List<ByteBuffer> contents = splitContent(in, CONTENT_SPLIT_PART_COUNT); contents.add(null); channel = new MockRestRequest(MockRestRequest.DUMMY_DATA, contents); stream = new ReadableStreamChannelInputStream(channel); doReadAllAtOnceTest(stream, in); stream.close(); } /** * Tests reading {@link InputStream} byte by byte. * @param stream the {@link InputStream} to test. * @param in the original data that is inside {@code stream}. * @throws IOException */ private void doReadByteByByteTest(InputStream stream, byte[] in) throws IOException { for (int i = 0; i < in.length; i++) { assertEquals("Byte [" + i + "] does not match expected", in[i], (byte) stream.read()); } assertEquals("Did not receive expected EOF", -1, stream.read()); } /** * Tests reading {@link InputStream} part by part. * @param stream the {@link InputStream} to test. * @param in the original data that is inside {@code stream}. * @throws IOException */ private void doReadPartByPartTest(InputStream stream, byte[] in) throws IOException { byte[] out = new byte[in.length]; for (int start = 0; start < in.length; ) { int end = Math.min(start + in.length / READ_PART_COUNT, in.length); int len = end - start; assertEquals("Bytes read did not match what was requested", len, stream.read(out, start, len)); assertArrayEquals("Byte array obtained from InputStream did not match source", Arrays.copyOfRange(in, start, end), Arrays.copyOfRange(out, start, end)); start = end; } if (out.length > 0) { assertEquals("Did not receive expected EOF", -1, stream.read(out, 0, out.length)); } assertEquals("Did not receive expected EOF", -1, stream.read()); } /** * Tests reading {@link InputStream} all at once. * @param stream the {@link InputStream} to test. * @param in the original data that is inside {@code stream}. * @throws IOException */ private void doReadAllAtOnceTest(InputStream stream, byte[] in) throws IOException { byte[] out = new byte[in.length]; assertEquals("Bytes read did not match size of source array", in.length, stream.read(out)); assertArrayEquals("Byte array obtained from InputStream did not match source", in, out); if (out.length > 0) { assertEquals("Did not receive expected EOF", -1, stream.read(out)); } assertEquals("Did not receive expected EOF", -1, stream.read()); } /** * Tests correctness of {@link ReadableStreamChannelInputStream#available()}. * @param stream the {@link InputStream} to read from. * @param in the original data that is inside {@code stream}. * @param partLength the length of each chunk that is inside the {@link ReadableStreamChannel} backing the * {@code stream} * @throws IOException */ private void doAvailableTest(InputStream stream, byte[] in, int partLength) throws IOException { byte[] out = new byte[in.length / READ_PART_COUNT]; int totalBytesRead = 0; for (int i = 0; totalBytesRead < in.length; i++) { int sourceStart = out.length * i; // available will be 0 when no chunks have been read. int expectedAvailable = sourceStart == 0 ? 0 : partLength - (sourceStart % partLength); assertEquals("Available differs from expected", expectedAvailable, stream.available()); int bytesRead = stream.read(out); assertArrayEquals("Byte array obtained from InputStream did not match source", Arrays.copyOfRange(in, sourceStart, sourceStart + bytesRead), Arrays.copyOfRange(out, 0, bytesRead)); totalBytesRead += bytesRead; } assertEquals("Available should be 0", 0, stream.available()); } /** * Splits {@code in} into {@code numParts} {@link ByteBuffer} instances. The ByteBuffer instances wrap {@code in}. * Assumption is that the length of in is perfectly divisible by {@code numParts}. * @param in the byte array to split. * @param numParts the number of parts to split {@code in} into. * @return a list of {@link ByteBuffer} instances of size {@code numParts} that share the content of {@code in} (in * order). */ private List<ByteBuffer> splitContent(byte[] in, int numParts) { assertTrue("This function works only when length of input is exactly divisible by number of parts required", in.length % numParts == 0); List<ByteBuffer> contents = new ArrayList<>(); int individualPartSize = in.length / numParts; for (int addedContentCount = 0; addedContentCount < numParts; addedContentCount++) { contents.add(ByteBuffer.wrap(in, addedContentCount * individualPartSize, individualPartSize)); } return contents; } } /** * {@link ReadableStreamChannel} implementation that either has an {@link Exception} on * {@link #readInto(AsyncWritableChannel, Callback)} or executes an incomplete read. */ class IncompleteReadReadableStreamChannel implements ReadableStreamChannel { private final AtomicBoolean channelOpen = new AtomicBoolean(true); private final Exception exceptionToThrow; /** * Create an instance of {@link IncompleteReadReadableStreamChannel} with an {@code exceptionToThrow}. * @param exceptionToThrow if desired, provide an exception that will thrown on read. Can be null. */ public IncompleteReadReadableStreamChannel(Exception exceptionToThrow) { this.exceptionToThrow = exceptionToThrow; } @Override public long getSize() { return 1; } /** * Either throws the exception provided or returns immediately saying no bytes were read. * @param asyncWritableChannel the {@link AsyncWritableChannel} to read the data into. * @param callback the {@link Callback} that will be invoked either when all the data in the channel has been emptied * into the {@code asyncWritableChannel} or if there is an exception in doing so. This can be null. * @return a {@link Future} that will eventually contain the result of the operation. */ @Override public Future<Long> readInto(AsyncWritableChannel asyncWritableChannel, Callback<Long> callback) { Exception exception; if (!channelOpen.get()) { exception = new ClosedChannelException(); } else { exception = exceptionToThrow; } FutureResult<Long> futureResult = new FutureResult<Long>(); futureResult.done(0L, exception); if (callback != null) { callback.onCompletion(0L, exception); } return futureResult; } @Override public boolean isOpen() { return channelOpen.get(); } @Override public void close() throws IOException { channelOpen.set(false); } } /** * Implementation of {@link ReadableStreamChannel} that doesn't export the size. */ class NoSizeRSC extends ByteBufferReadableStreamChannel { NoSizeRSC(ByteBuffer buffer) { super(buffer); } /** * @return -1 */ @Override public long getSize() { return -1; } }