/* * Copyright (C) 2012 The Guava Authors * * 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. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.common.io; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.io.TestOption.AVAILABLE_ALWAYS_ZERO; import static com.google.common.io.TestOption.CLOSE_THROWS; import static com.google.common.io.TestOption.OPEN_THROWS; import static com.google.common.io.TestOption.READ_THROWS; import static com.google.common.io.TestOption.SKIP_THROWS; import static com.google.common.io.TestOption.WRITE_THROWS; import static org.junit.Assert.assertArrayEquals; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.hash.Hashing; import com.google.common.primitives.UnsignedBytes; import com.google.common.testing.TestLogHandler; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.EnumSet; import junit.framework.TestSuite; /** * Tests for the default implementations of {@code ByteSource} methods. * * @author Colin Decker */ public class ByteSourceTest extends IoTestCase { @AndroidIncompatible // Android doesn't understand suites whose tests lack default constructors. public static TestSuite suite() { TestSuite suite = new TestSuite(); for (boolean asCharSource : new boolean[] {false, true}) { suite.addTest(ByteSourceTester.tests("ByteSource.wrap[byte[]]", SourceSinkFactories.byteArraySourceFactory(), asCharSource)); suite.addTest(ByteSourceTester.tests("ByteSource.empty[]", SourceSinkFactories.emptyByteSourceFactory(), asCharSource)); } suite.addTestSuite(ByteSourceTest.class); return suite; } private static final byte[] bytes = newPreFilledByteArray(10000); private TestByteSource source; @Override protected void setUp() throws Exception { source = new TestByteSource(bytes); } public void testOpenBufferedStream() throws IOException { InputStream in = source.openBufferedStream(); assertTrue(source.wasStreamOpened()); assertFalse(source.wasStreamClosed()); ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteStreams.copy(in, out); in.close(); out.close(); assertTrue(source.wasStreamClosed()); assertArrayEquals(bytes, out.toByteArray()); } public void testSize() throws IOException { assertEquals(bytes.length, source.size()); assertTrue(source.wasStreamOpened() && source.wasStreamClosed()); // test that we can get the size even if skip() isn't supported assertEquals(bytes.length, new TestByteSource(bytes, SKIP_THROWS).size()); // test that we can get the size even if available() always returns zero assertEquals(bytes.length, new TestByteSource(bytes, AVAILABLE_ALWAYS_ZERO).size()); } public void testCopyTo_outputStream() throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); assertEquals(bytes.length, source.copyTo(out)); assertTrue(source.wasStreamOpened() && source.wasStreamClosed()); assertArrayEquals(bytes, out.toByteArray()); } public void testCopyTo_byteSink() throws IOException { TestByteSink sink = new TestByteSink(); assertFalse(sink.wasStreamOpened() || sink.wasStreamClosed()); assertEquals(bytes.length, source.copyTo(sink)); assertTrue(source.wasStreamOpened() && source.wasStreamClosed()); assertTrue(sink.wasStreamOpened() && sink.wasStreamClosed()); assertArrayEquals(bytes, sink.getBytes()); } public void testRead_toArray() throws IOException { assertArrayEquals(bytes, source.read()); assertTrue(source.wasStreamOpened() && source.wasStreamClosed()); } public void testRead_withProcessor() throws IOException { final byte[] processedBytes = new byte[bytes.length]; ByteProcessor<byte[]> processor = new ByteProcessor<byte[]>() { int pos; @Override public boolean processBytes(byte[] buf, int off, int len) throws IOException { System.arraycopy(buf, off, processedBytes, pos, len); pos += len; return true; } @Override public byte[] getResult() { return processedBytes; } }; source.read(processor); assertTrue(source.wasStreamOpened() && source.wasStreamClosed()); assertArrayEquals(bytes, processedBytes); } public void testRead_withProcessor_stopsOnFalse() throws IOException { ByteProcessor<Void> processor = new ByteProcessor<Void>() { boolean firstCall = true; @Override public boolean processBytes(byte[] buf, int off, int len) throws IOException { assertTrue("consume() called twice", firstCall); firstCall = false; return false; } @Override public Void getResult() { return null; } }; source.read(processor); assertTrue(source.wasStreamOpened() && source.wasStreamClosed()); } public void testHash() throws IOException { ByteSource byteSource = new TestByteSource("hamburger\n".getBytes(Charsets.US_ASCII)); // Pasted this expected string from `echo hamburger | md5sum` assertEquals("cfa0c5002275c90508338a5cdb2a9781", byteSource.hash(Hashing.md5()).toString()); } public void testContentEquals() throws IOException { assertTrue(source.contentEquals(source)); assertTrue(source.wasStreamOpened() && source.wasStreamClosed()); ByteSource equalSource = new TestByteSource(bytes); assertTrue(source.contentEquals(equalSource)); assertTrue(new TestByteSource(bytes).contentEquals(source)); ByteSource fewerBytes = new TestByteSource(newPreFilledByteArray(bytes.length / 2)); assertFalse(source.contentEquals(fewerBytes)); byte[] copy = bytes.clone(); copy[9876] = 1; ByteSource oneByteOff = new TestByteSource(copy); assertFalse(source.contentEquals(oneByteOff)); } public void testSlice() throws IOException { // Test preconditions try { source.slice(-1, 10); fail(); } catch (IllegalArgumentException expected) { } try { source.slice(0, -1); fail(); } catch (IllegalArgumentException expected) { } assertCorrectSlice(0, 0, 0, 0); assertCorrectSlice(0, 0, 1, 0); assertCorrectSlice(100, 0, 10, 10); assertCorrectSlice(100, 0, 100, 100); assertCorrectSlice(100, 5, 10, 10); assertCorrectSlice(100, 5, 100, 95); assertCorrectSlice(100, 100, 0, 0); assertCorrectSlice(100, 100, 10, 0); assertCorrectSlice(100, 101, 10, 0); } /** * Tests that the default slice() behavior is correct when the source is sliced starting at an * offset that is greater than the current length of the source, a stream is then opened to that * source, and finally additional bytes are appended to the source before the stream is read. * * <p>Without special handling, it's possible to have reads of the open stream start <i>before</i> * the offset at which the slice is supposed to start. */ // TODO(cgdecker): Maybe add a test for this to ByteSourceTester public void testSlice_appendingAfterSlicing() throws IOException { // Source of length 5 AppendableByteSource source = new AppendableByteSource(newPreFilledByteArray(5)); // Slice it starting at offset 10. ByteSource slice = source.slice(10, 5); // Open a stream to the slice. InputStream in = slice.openStream(); // Append 10 more bytes to the source. source.append(newPreFilledByteArray(5, 10)); // The stream reports no bytes... importantly, it doesn't read the byte at index 5 when it // should be reading the byte at index 10. // We could use a custom InputStream instead to make the read start at index 10, but since this // is a racy situation anyway, this behavior seems reasonable. assertEquals(-1, in.read()); } private static class AppendableByteSource extends ByteSource { private byte[] bytes; public AppendableByteSource(byte[] initialBytes) { this.bytes = initialBytes.clone(); } @Override public InputStream openStream() { return new In(); } public void append(byte[] b) { byte[] newBytes = Arrays.copyOf(bytes, bytes.length + b.length); System.arraycopy(b, 0, newBytes, bytes.length, b.length); bytes = newBytes; } private class In extends InputStream { private int pos; @Override public int read() throws IOException { byte[] b = new byte[1]; return read(b) == -1 ? -1 : UnsignedBytes.toInt(b[0]); } @Override public int read(byte[] b, int off, int len) { if (pos >= bytes.length) { return -1; } int lenToRead = Math.min(len, bytes.length - pos); System.arraycopy(bytes, pos, b, off, lenToRead); pos += lenToRead; return lenToRead; } } } /** * @param input the size of the input source * @param offset the first argument to {@link ByteSource#slice} * @param length the second argument to {@link ByteSource#slice} * @param expectRead the number of bytes we expect to read */ private static void assertCorrectSlice( int input, int offset, long length, int expectRead) throws IOException { checkArgument(expectRead == (int) Math.max(0, Math.min(input, offset + length) - offset)); byte[] expected = newPreFilledByteArray(offset, expectRead); ByteSource source = new TestByteSource(newPreFilledByteArray(input)); ByteSource slice = source.slice(offset, length); assertArrayEquals(expected, slice.read()); } public void testCopyToStream_doesNotCloseThatStream() throws IOException { TestOutputStream out = new TestOutputStream(ByteStreams.nullOutputStream()); assertFalse(out.closed()); source.copyTo(out); assertFalse(out.closed()); } public void testClosesOnErrors_copyingToByteSinkThatThrows() { for (TestOption option : EnumSet.of(OPEN_THROWS, WRITE_THROWS, CLOSE_THROWS)) { TestByteSource okSource = new TestByteSource(bytes); try { okSource.copyTo(new TestByteSink(option)); fail(); } catch (IOException expected) { } // ensure stream was closed IF it was opened (depends on implementation whether or not it's // opened at all if sink.newOutputStream() throws). assertTrue("stream not closed when copying to sink with option: " + option, !okSource.wasStreamOpened() || okSource.wasStreamClosed()); } } public void testClosesOnErrors_whenReadThrows() { TestByteSource failSource = new TestByteSource(bytes, READ_THROWS); try { failSource.copyTo(new TestByteSink()); fail(); } catch (IOException expected) { } assertTrue(failSource.wasStreamClosed()); } public void testClosesOnErrors_copyingToOutputStreamThatThrows() { TestByteSource okSource = new TestByteSource(bytes); try { OutputStream out = new TestOutputStream(ByteStreams.nullOutputStream(), WRITE_THROWS); okSource.copyTo(out); fail(); } catch (IOException expected) { } assertTrue(okSource.wasStreamClosed()); } public void testConcat() throws IOException { ByteSource b1 = ByteSource.wrap(new byte[] {0, 1, 2, 3}); ByteSource b2 = ByteSource.wrap(new byte[0]); ByteSource b3 = ByteSource.wrap(new byte[] {4, 5}); byte[] expected = {0, 1, 2, 3, 4, 5}; assertArrayEquals(expected, ByteSource.concat(ImmutableList.of(b1, b2, b3)).read()); assertArrayEquals(expected, ByteSource.concat(b1, b2, b3).read()); assertArrayEquals(expected, ByteSource.concat(ImmutableList.of(b1, b2, b3).iterator()).read()); assertEquals(expected.length, ByteSource.concat(b1, b2, b3).size()); assertFalse(ByteSource.concat(b1, b2, b3).isEmpty()); ByteSource emptyConcat = ByteSource.concat(ByteSource.empty(), ByteSource.empty()); assertTrue(emptyConcat.isEmpty()); assertEquals(0, emptyConcat.size()); } public void testConcat_infiniteIterable() throws IOException { ByteSource source = ByteSource.wrap(new byte[] {0, 1, 2, 3}); Iterable<ByteSource> cycle = Iterables.cycle(ImmutableList.of(source)); ByteSource concatenated = ByteSource.concat(cycle); byte[] expected = {0, 1, 2, 3, 0, 1, 2, 3}; assertArrayEquals(expected, concatenated.slice(0, 8).read()); } private static final ByteSource BROKEN_CLOSE_SOURCE = new TestByteSource(new byte[10], CLOSE_THROWS); private static final ByteSource BROKEN_OPEN_SOURCE = new TestByteSource(new byte[10], OPEN_THROWS); private static final ByteSource BROKEN_READ_SOURCE = new TestByteSource(new byte[10], READ_THROWS); private static final ByteSink BROKEN_CLOSE_SINK = new TestByteSink(CLOSE_THROWS); private static final ByteSink BROKEN_OPEN_SINK = new TestByteSink(OPEN_THROWS); private static final ByteSink BROKEN_WRITE_SINK = new TestByteSink(WRITE_THROWS); private static final ImmutableSet<ByteSource> BROKEN_SOURCES = ImmutableSet.of(BROKEN_CLOSE_SOURCE, BROKEN_OPEN_SOURCE, BROKEN_READ_SOURCE); private static final ImmutableSet<ByteSink> BROKEN_SINKS = ImmutableSet.of(BROKEN_CLOSE_SINK, BROKEN_OPEN_SINK, BROKEN_WRITE_SINK); public void testCopyExceptions() { if (!Closer.SuppressingSuppressor.isAvailable()) { // test that exceptions are logged TestLogHandler logHandler = new TestLogHandler(); Closeables.logger.addHandler(logHandler); try { for (ByteSource in : BROKEN_SOURCES) { runFailureTest(in, newNormalByteSink()); assertTrue(logHandler.getStoredLogRecords().isEmpty()); runFailureTest(in, BROKEN_CLOSE_SINK); assertEquals((in == BROKEN_OPEN_SOURCE) ? 0 : 1, getAndResetRecords(logHandler)); } for (ByteSink out : BROKEN_SINKS) { runFailureTest(newNormalByteSource(), out); assertTrue(logHandler.getStoredLogRecords().isEmpty()); runFailureTest(BROKEN_CLOSE_SOURCE, out); assertEquals(1, getAndResetRecords(logHandler)); } for (ByteSource in : BROKEN_SOURCES) { for (ByteSink out : BROKEN_SINKS) { runFailureTest(in, out); assertTrue(getAndResetRecords(logHandler) <= 1); } } } finally { Closeables.logger.removeHandler(logHandler); } } else { // test that exceptions are suppressed for (ByteSource in : BROKEN_SOURCES) { int suppressed = runSuppressionFailureTest(in, newNormalByteSink()); assertEquals(0, suppressed); suppressed = runSuppressionFailureTest(in, BROKEN_CLOSE_SINK); assertEquals((in == BROKEN_OPEN_SOURCE) ? 0 : 1, suppressed); } for (ByteSink out : BROKEN_SINKS) { int suppressed = runSuppressionFailureTest(newNormalByteSource(), out); assertEquals(0, suppressed); suppressed = runSuppressionFailureTest(BROKEN_CLOSE_SOURCE, out); assertEquals(1, suppressed); } for (ByteSource in : BROKEN_SOURCES) { for (ByteSink out : BROKEN_SINKS) { int suppressed = runSuppressionFailureTest(in, out); assertTrue(suppressed <= 1); } } } } private static int getAndResetRecords(TestLogHandler logHandler) { int records = logHandler.getStoredLogRecords().size(); logHandler.clear(); return records; } private static void runFailureTest(ByteSource in, ByteSink out) { try { in.copyTo(out); fail(); } catch (IOException expected) { } } /** * @return the number of exceptions that were suppressed on the expected thrown exception */ private static int runSuppressionFailureTest(ByteSource in, ByteSink out) { try { in.copyTo(out); fail(); } catch (IOException expected) { return CloserTest.getSuppressed(expected).length; } throw new AssertionError(); // can't happen } private static ByteSource newNormalByteSource() { return ByteSource.wrap(new byte[10]); } private static ByteSink newNormalByteSink() { return new ByteSink() { @Override public OutputStream openStream() { return new ByteArrayOutputStream(); } }; } }