/*
 * Copyright (C) 2007 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.io.ByteStreams.copy;
import static com.google.common.io.ByteStreams.newInputStreamSupplier;

import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Bytes;
import com.google.common.testing.TestLogHandler;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Arrays;
import java.util.Random;

/**
 * Unit test for {@link ByteStreams}.
 *
 * @author Chris Nokleberg
 */
public class ByteStreamsTest extends IoTestCase {

  /** Provides an InputStream that throws an IOException on every read. */
  static final InputSupplier<InputStream> BROKEN_READ
      = new InputSupplier<InputStream>() {
        @Override
        public InputStream getInput() {
          return new InputStream() {
            @Override public int read() throws IOException {
              throw new IOException("broken read");
            }
          };
        }
      };

  /** Provides an OutputStream that throws an IOException on every write. */
  static final OutputSupplier<OutputStream> BROKEN_WRITE
      = new OutputSupplier<OutputStream>() {
        @Override
        public OutputStream getOutput() {
          return new OutputStream() {
            @Override public void write(int b) throws IOException {
              throw new IOException("broken write");
            }
          };
        }
      };

  /** Provides an InputStream that throws an IOException on close. */
  static final InputSupplier<InputStream> BROKEN_CLOSE_INPUT =
      new InputSupplier<InputStream>() {
        @Override
        public InputStream getInput() {
          return new FilterInputStream(new ByteArrayInputStream(new byte[10])) {
            @Override public void close() throws IOException {
              throw new IOException("broken close input");
            }
          };
        }
      };

  /** Provides an OutputStream that throws an IOException on every close. */
  static final OutputSupplier<OutputStream> BROKEN_CLOSE_OUTPUT =
      new OutputSupplier<OutputStream>() {
        @Override
        public OutputStream getOutput() {
          return new FilterOutputStream(new ByteArrayOutputStream()) {
            @Override public void close() throws IOException {
              throw new IOException("broken close output");
            }
          };
        }
      };

  /** Throws an IOException from getInput. */
  static final InputSupplier<InputStream> BROKEN_GET_INPUT =
      new InputSupplier<InputStream>() {
        @Override
        public InputStream getInput() throws IOException {
          throw new IOException("broken get input");
        }
      };

  /** Throws an IOException from getOutput. */
  static final OutputSupplier<OutputStream> BROKEN_GET_OUTPUT =
      new OutputSupplier<OutputStream>() {
        @Override
        public OutputStream getOutput() throws IOException {
          throw new IOException("broken get output");
        }
      };

  private static final ImmutableSet<InputSupplier<InputStream>> BROKEN_INPUTS =
      ImmutableSet.of(BROKEN_CLOSE_INPUT, BROKEN_GET_INPUT, BROKEN_READ);
  private static final ImmutableSet<OutputSupplier<OutputStream>> BROKEN_OUTPUTS
      = ImmutableSet.of(BROKEN_CLOSE_OUTPUT, BROKEN_GET_OUTPUT, BROKEN_WRITE);

  public void testByteSuppliers() throws IOException {
    byte[] range = newPreFilledByteArray(200);
    assertTrue(Arrays.equals(range,
        ByteStreams.toByteArray(ByteStreams.newInputStreamSupplier(range))));

    byte[] subRange = ByteStreams.toByteArray(
        ByteStreams.newInputStreamSupplier(range, 100, 50));
    assertEquals(50, subRange.length);
    assertEquals(100, subRange[0]);
    assertEquals((byte) 149, subRange[subRange.length - 1]);
  }

  public void testEqual() throws IOException {
    equalHelper(false, 0, 1);
    equalHelper(false, 400, 10000);
    equalHelper(false, 0x2000, 0x2001);
    equalHelper(false, new byte[]{ 0 }, new byte[]{ 1 });

    byte[] mutate = newPreFilledByteArray(10000);
    mutate[9000] = 0;
    equalHelper(false, mutate, newPreFilledByteArray(10000));

    equalHelper(true, 0, 0);
    equalHelper(true, 1, 1);
    equalHelper(true, 400, 400);

    final byte[] tenK = newPreFilledByteArray(10000);
    equalHelper(true, tenK, tenK);
    assertTrue(ByteStreams.equal(ByteStreams.newInputStreamSupplier(tenK),
        new InputSupplier<InputStream>() {
          @Override
          public InputStream getInput() {
            return new RandomAmountInputStream(new ByteArrayInputStream(tenK),
                new Random(301));
          }
        }));
  }

  private void equalHelper(boolean expect, int size1, int size2)
      throws IOException {
    equalHelper(expect, newPreFilledByteArray(size1),
        newPreFilledByteArray(size2));
  }

  private void equalHelper(boolean expect, byte[] a, byte[] b)
      throws IOException {
    assertEquals(expect, ByteStreams.equal(
        ByteStreams.newInputStreamSupplier(a),
        ByteStreams.newInputStreamSupplier(b)));
  }

  public void testAlwaysCloses() throws IOException {
    byte[] range = newPreFilledByteArray(100);
    CheckCloseSupplier.Input<InputStream> okRead
        = newCheckInput(ByteStreams.newInputStreamSupplier(range));
    CheckCloseSupplier.Output<OutputStream> okWrite
        = newCheckOutput(new OutputSupplier<OutputStream>() {
          @Override
          public OutputStream getOutput() {
            return new ByteArrayOutputStream();
          }
        });

    CheckCloseSupplier.Input<InputStream> brokenRead
        = newCheckInput(BROKEN_READ);
    CheckCloseSupplier.Output<OutputStream> brokenWrite
        = newCheckOutput(BROKEN_WRITE);

    // copy, both suppliers
    ByteStreams.copy(okRead, okWrite);
    assertTrue(okRead.areClosed());
    assertTrue(okWrite.areClosed());

    try {
      ByteStreams.copy(okRead, brokenWrite);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken write", e.getMessage());
    }
    assertTrue(okRead.areClosed());
    assertTrue(brokenWrite.areClosed());

    try {
      ByteStreams.copy(brokenRead, okWrite);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken read", e.getMessage());
    }
    assertTrue(brokenRead.areClosed());
    assertTrue(okWrite.areClosed());

    try {
      ByteStreams.copy(brokenRead, brokenWrite);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken read", e.getMessage());
    }
    assertTrue(brokenRead.areClosed());
    assertTrue(brokenWrite.areClosed());

    // copy, input supplier
    OutputStream out = okWrite.getOutput();
    ByteStreams.copy(okRead, out);
    assertTrue(okRead.areClosed());
    assertFalse(okWrite.areClosed());
    out.close();

    out = brokenWrite.getOutput();
    try {
      ByteStreams.copy(okRead, out);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken write", e.getMessage());
    }
    assertTrue(okRead.areClosed());
    assertFalse(brokenWrite.areClosed());
    out.close();

    out = okWrite.getOutput();
    try {
      ByteStreams.copy(brokenRead, out);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken read", e.getMessage());
    }
    assertTrue(brokenRead.areClosed());
    assertFalse(okWrite.areClosed());
    out.close();

    out = brokenWrite.getOutput();
    try {
      ByteStreams.copy(brokenRead, out);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken read", e.getMessage());
    }
    assertTrue(brokenRead.areClosed());
    assertFalse(brokenWrite.areClosed());
    out.close();

    // copy, output supplier
    InputStream in = okRead.getInput();
    ByteStreams.copy(in, okWrite);
    assertFalse(okRead.areClosed());
    assertTrue(okWrite.areClosed());
    in.close();

    in = okRead.getInput();
    try {
      ByteStreams.copy(in, brokenWrite);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken write", e.getMessage());
    }
    assertFalse(okRead.areClosed());
    assertTrue(brokenWrite.areClosed());
    in.close();

    in = brokenRead.getInput();
    try {
      ByteStreams.copy(in, okWrite);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken read", e.getMessage());
    }
    assertFalse(brokenRead.areClosed());
    assertTrue(okWrite.areClosed());
    in.close();

    in = brokenRead.getInput();
    try {
      ByteStreams.copy(in, brokenWrite);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken read", e.getMessage());
    }
    assertFalse(brokenRead.areClosed());
    assertTrue(brokenWrite.areClosed());
    in.close();

    // toByteArray
    assertTrue(Arrays.equals(range, ByteStreams.toByteArray(okRead)));
    assertTrue(okRead.areClosed());

    try {
      ByteStreams.toByteArray(brokenRead);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken read", e.getMessage());
    }
    assertTrue(brokenRead.areClosed());

    // equal
    try {
      ByteStreams.equal(brokenRead, okRead);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken read", e.getMessage());
    }
    assertTrue(brokenRead.areClosed());

    try {
      ByteStreams.equal(okRead, brokenRead);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken read", e.getMessage());
    }
    assertTrue(brokenRead.areClosed());

    // write
    try {
      ByteStreams.write(new byte[10], brokenWrite);
      fail("expected exception");
    } catch (IOException e) {
      assertEquals("broken write", e.getMessage());
    }
    assertTrue(brokenWrite.areClosed());
  }

  private static int getAndResetRecords(TestLogHandler logHandler) {
    int records = logHandler.getStoredLogRecords().size();
    logHandler.clear();
    return records;
  }

  private static void runFailureTest(
      InputSupplier<? extends InputStream> in, OutputSupplier<OutputStream> out) {
    try {
      copy(in, out);
      fail();
    } catch (IOException expected) {
    }
  }

  private static OutputSupplier<OutputStream> newByteArrayOutputStreamSupplier() {
    return new OutputSupplier<OutputStream>() {
      @Override public OutputStream getOutput() {
        return new ByteArrayOutputStream();
      }
    };
  }

  public void testWriteBytes() throws IOException {
    final ByteArrayOutputStream out = new ByteArrayOutputStream();
    byte[] expected = newPreFilledByteArray(100);
    ByteStreams.write(expected, new OutputSupplier<OutputStream>() {
      @Override public OutputStream getOutput() {
        return out;
      }
    });
    assertTrue(Arrays.equals(expected, out.toByteArray()));
  }

  public void testCopy() throws Exception {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    byte[] expected = newPreFilledByteArray(100);
    long num = ByteStreams.copy(new ByteArrayInputStream(expected), out);
    assertEquals(100, num);
    assertTrue(Arrays.equals(expected, out.toByteArray()));
  }

  public void testCopyChannel() throws IOException {
    byte[] expected = newPreFilledByteArray(100);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    WritableByteChannel outChannel = Channels.newChannel(out);

    ReadableByteChannel inChannel =
        Channels.newChannel(new ByteArrayInputStream(expected));
    ByteStreams.copy(inChannel, outChannel);
    assertTrue(Arrays.equals(expected, out.toByteArray()));
  }

  public void testReadFully() throws IOException {
    byte[] b = new byte[10];

    try {
      ByteStreams.readFully(newTestStream(10), null, 0, 10);
      fail("expected exception");
    } catch (NullPointerException e) {
    }

    try {
      ByteStreams.readFully(null, b, 0, 10);
      fail("expected exception");
    } catch (NullPointerException e) {
    }

    try {
      ByteStreams.readFully(newTestStream(10), b, -1, 10);
      fail("expected exception");
    } catch (IndexOutOfBoundsException e) {
    }

    try {
      ByteStreams.readFully(newTestStream(10), b, 0, -1);
      fail("expected exception");
    } catch (IndexOutOfBoundsException e) {
    }

    try {
      ByteStreams.readFully(newTestStream(10), b, 0, -1);
      fail("expected exception");
    } catch (IndexOutOfBoundsException e) {
    }

    try {
      ByteStreams.readFully(newTestStream(10), b, 2, 10);
      fail("expected exception");
    } catch (IndexOutOfBoundsException e) {
    }

    try {
      ByteStreams.readFully(newTestStream(5), b, 0, 10);
      fail("expected exception");
    } catch (EOFException e) {
    }

    Arrays.fill(b, (byte) 0);
    ByteStreams.readFully(newTestStream(10), b, 0, 0);
    assertTrue(Arrays.equals(new byte[10], b));

    Arrays.fill(b, (byte) 0);
    ByteStreams.readFully(newTestStream(10), b, 0, 10);
    assertTrue(Arrays.equals(newPreFilledByteArray(10), b));

    Arrays.fill(b, (byte) 0);
    ByteStreams.readFully(newTestStream(10), b, 0, 5);
    assertTrue(Arrays.equals(new byte[]{0, 1, 2, 3, 4, 0, 0, 0, 0, 0}, b));
  }

  public void testSkipFully() throws IOException {
    byte[] bytes = newPreFilledByteArray(100);
    skipHelper(0, 0, new ByteArrayInputStream(bytes));
    skipHelper(50, 50, new ByteArrayInputStream(bytes));
    skipHelper(50, 50, new SlowSkipper(new ByteArrayInputStream(bytes), 1));
    skipHelper(50, 50, new SlowSkipper(new ByteArrayInputStream(bytes), 0));
    skipHelper(100, -1, new ByteArrayInputStream(bytes));
    try {
      skipHelper(101, 0, new ByteArrayInputStream(bytes));
      fail("expected exception");
    } catch (EOFException e) {
    }
  }

  private void skipHelper(long n, int expect, InputStream in)
      throws IOException {
    ByteStreams.skipFully(in, n);
    assertEquals(expect, in.read());
    in.close();
  }

  // TODO(user): rename; violates rule that only immutable things can be all caps
  private static final byte[] BYTES = new byte[] {
      0x12, 0x34, 0x56, 0x78, 0x76, 0x54, 0x32, 0x10 };

  public void testNewDataInput_empty() {
    byte[] b = new byte[0];
    ByteArrayDataInput in = ByteStreams.newDataInput(b);
    try {
      in.readInt();
      fail();
    } catch (IllegalStateException expected) {
    }
  }

  public void testNewDataInput_normal() {
    ByteArrayDataInput in = ByteStreams.newDataInput(BYTES);
    assertEquals(0x12345678, in.readInt());
    assertEquals(0x76543210, in.readInt());
    try {
      in.readInt();
      fail();
    } catch (IllegalStateException expected) {
    }
  }

  public void testNewDataInput_readFully() {
    ByteArrayDataInput in = ByteStreams.newDataInput(BYTES);
    byte[] actual = new byte[BYTES.length];
    in.readFully(actual);
    assertEquals(BYTES, actual);
  }

  public void testNewDataInput_readFullyAndThenSome() {
    ByteArrayDataInput in = ByteStreams.newDataInput(BYTES);
    byte[] actual = new byte[BYTES.length * 2];
    try {
      in.readFully(actual);
      fail();
    } catch (IllegalStateException ex) {
      assertTrue(ex.getCause() instanceof EOFException);
    }
  }

  public void testNewDataInput_readFullyWithOffset() {
    ByteArrayDataInput in = ByteStreams.newDataInput(BYTES);
    byte[] actual = new byte[4];
    in.readFully(actual, 2, 2);
    assertEquals(0, actual[0]);
    assertEquals(0, actual[1]);
    assertEquals(BYTES[0], actual[2]);
    assertEquals(BYTES[1], actual[3]);
  }

  public void testNewDataInput_readLine() {
    ByteArrayDataInput in = ByteStreams.newDataInput(
        "This is a line\r\nThis too\rand this\nand also this".getBytes(Charsets.UTF_8));
    assertEquals("This is a line", in.readLine());
    assertEquals("This too", in.readLine());
    assertEquals("and this", in.readLine());
    assertEquals("and also this", in.readLine());
  }

  public void testNewDataInput_readFloat() {
    byte[] data = {0x12, 0x34, 0x56, 0x78, 0x76, 0x54, 0x32, 0x10};
    ByteArrayDataInput in = ByteStreams.newDataInput(data);
    assertEquals(Float.intBitsToFloat(0x12345678), in.readFloat(), 0.0);
    assertEquals(Float.intBitsToFloat(0x76543210), in.readFloat(), 0.0);
  }

  public void testNewDataInput_readDouble() {
    byte[] data = {0x12, 0x34, 0x56, 0x78, 0x76, 0x54, 0x32, 0x10};
    ByteArrayDataInput in = ByteStreams.newDataInput(data);
    assertEquals(Double.longBitsToDouble(0x1234567876543210L), in.readDouble(), 0.0);
  }

  public void testNewDataInput_readUTF() {
    byte[] data = new byte[17];
    data[1] = 15;
    System.arraycopy("Kilroy was here".getBytes(Charsets.UTF_8), 0, data, 2, 15);
    ByteArrayDataInput in = ByteStreams.newDataInput(data);
    assertEquals("Kilroy was here", in.readUTF());
  }

  public void testNewDataInput_readChar() {
    byte[] data = "qed".getBytes(Charsets.UTF_16BE);
    ByteArrayDataInput in = ByteStreams.newDataInput(data);
    assertEquals('q', in.readChar());
    assertEquals('e', in.readChar());
    assertEquals('d', in.readChar());
  }

  public void testNewDataInput_readUnsignedShort() {
    byte[] data = {0, 0, 0, 1, (byte) 0xFF, (byte) 0xFF, 0x12, 0x34};
    ByteArrayDataInput in = ByteStreams.newDataInput(data);
    assertEquals(0, in.readUnsignedShort());
    assertEquals(1, in.readUnsignedShort());
    assertEquals(65535, in.readUnsignedShort());
    assertEquals(0x1234, in.readUnsignedShort());
  }

  public void testNewDataInput_readLong() {
    byte[] data = {0x12, 0x34, 0x56, 0x78, 0x76, 0x54, 0x32, 0x10};
    ByteArrayDataInput in = ByteStreams.newDataInput(data);
    assertEquals(0x1234567876543210L, in.readLong());
  }

  public void testNewDataInput_readBoolean() {
    ByteArrayDataInput in = ByteStreams.newDataInput(BYTES);
    assertTrue(in.readBoolean());
  }

  public void testNewDataInput_readByte() {
    ByteArrayDataInput in = ByteStreams.newDataInput(BYTES);
    for (int i = 0; i < BYTES.length; i++) {
      assertEquals(BYTES[i], in.readByte());
    }
    try {
      in.readByte();
      fail();
    } catch (IllegalStateException ex) {
      assertTrue(ex.getCause() instanceof EOFException);
    }
  }

  public void testNewDataInput_readUnsignedByte() {
    ByteArrayDataInput in = ByteStreams.newDataInput(BYTES);
    for (int i = 0; i < BYTES.length; i++) {
      assertEquals(BYTES[i], in.readUnsignedByte());
    }
    try {
      in.readUnsignedByte();
      fail();
    } catch (IllegalStateException ex) {
      assertTrue(ex.getCause() instanceof EOFException);
    }
  }

  public void testNewDataInput_offset() {
    ByteArrayDataInput in = ByteStreams.newDataInput(BYTES, 2);
    assertEquals(0x56787654, in.readInt());
    try {
      in.readInt();
      fail();
    } catch (IllegalStateException expected) {
    }
  }

  public void testNewDataInput_skip() {
    ByteArrayDataInput in = ByteStreams.newDataInput(new byte[2]);
    in.skipBytes(2);
    try {
      in.skipBytes(1);
    } catch (IllegalStateException expected) {
    }
  }

  public void testNewDataOutput_empty() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    assertEquals(0, out.toByteArray().length);
  }

  public void testNewDataOutput_writeInt() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.writeInt(0x12345678);
    out.writeInt(0x76543210);
    assertTrue(Arrays.equals(BYTES, out.toByteArray()));
  }

  public void testNewDataOutput_sized() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput(4);
    out.writeInt(0x12345678);
    out.writeInt(0x76543210);
    assertTrue(Arrays.equals(BYTES, out.toByteArray()));
  }

  public void testNewDataOutput_writeLong() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.writeLong(0x1234567876543210L);
    assertTrue(Arrays.equals(BYTES, out.toByteArray()));
  }

  public void testNewDataOutput_writeByteArray() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.write(BYTES);
    assertTrue(Arrays.equals(BYTES, out.toByteArray()));
  }

  public void testNewDataOutput_writeByte() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.write(0x12);
    out.writeByte(0x34);
    assertTrue(Arrays.equals(new byte[] {0x12, 0x34}, out.toByteArray()));
  }

  public void testNewDataOutput_writeByteOffset() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.write(BYTES, 4, 2);
    byte[] expected = {BYTES[4], BYTES[5]};
    assertEquals(expected, out.toByteArray());
  }

  public void testNewDataOutput_writeBoolean() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.writeBoolean(true);
    out.writeBoolean(false);
    byte[] expected = {(byte) 1, (byte) 0};
    assertEquals(expected, out.toByteArray());
  }

  public void testNewDataOutput_writeChar() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.writeChar('a');
    assertTrue(Arrays.equals(new byte[] {0, 97}, out.toByteArray()));
  }

  public void testNewDataOutput_writeChars() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.writeChars("r\u00C9sum\u00C9");
    // need to remove byte order mark before comparing
    byte[] expected = Arrays.copyOfRange("r\u00C9sum\u00C9".getBytes(Charsets.UTF_16), 2, 14);
    assertEquals(expected, out.toByteArray());
  }

  public void testNewDataOutput_writeUTF() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.writeUTF("r\u00C9sum\u00C9");
    byte[] expected ="r\u00C9sum\u00C9".getBytes(Charsets.UTF_8);
    byte[] actual = out.toByteArray();
    // writeUTF writes the length of the string in 2 bytes
    assertEquals(0, actual[0]);
    assertEquals(expected.length, actual[1]);
    assertEquals(expected, Arrays.copyOfRange(actual, 2, actual.length));
  }

  public void testNewDataOutput_writeShort() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.writeShort(0x1234);
    assertTrue(Arrays.equals(new byte[] {0x12, 0x34}, out.toByteArray()));
  }

  public void testNewDataOutput_writeDouble() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.writeDouble(Double.longBitsToDouble(0x1234567876543210L));
    assertEquals(BYTES, out.toByteArray());
  }

  public void testNewDataOutput_writeFloat() {
    ByteArrayDataOutput out = ByteStreams.newDataOutput();
    out.writeFloat(Float.intBitsToFloat(0x12345678));
    out.writeFloat(Float.intBitsToFloat(0x76543210));
    assertEquals(BYTES, out.toByteArray());
  }

  public void testLength() throws IOException {
    lengthHelper(Long.MAX_VALUE);
    lengthHelper(7);
    lengthHelper(1);
    lengthHelper(0);

    assertEquals(0, ByteStreams.length(
        ByteStreams.newInputStreamSupplier(new byte[0])));
  }

  private void lengthHelper(final long skipLimit) throws IOException {
    assertEquals(100, ByteStreams.length(new InputSupplier<InputStream>() {
      @Override
      public InputStream getInput() {
        return new SlowSkipper(new ByteArrayInputStream(new byte[100]),
            skipLimit);
      }
    }));
  }

  public void testSlice() throws IOException {
    // Test preconditions
    InputSupplier<? extends InputStream> supplier
        = ByteStreams.newInputStreamSupplier(newPreFilledByteArray(100));
    try {
      ByteStreams.slice(supplier, -1, 10);
      fail("expected exception");
    } catch (IllegalArgumentException expected) {
    }

    try {
      ByteStreams.slice(supplier, 0, -1);
      fail("expected exception");
    } catch (IllegalArgumentException expected) {
    }

    try {
      ByteStreams.slice(null, 0, 10);
      fail("expected exception");
    } catch (NullPointerException expected) {
    }

    sliceHelper(0, 0, 0, 0);
    sliceHelper(0, 0, 1, 0);
    sliceHelper(100, 0, 10, 10);
    sliceHelper(100, 0, 100, 100);
    sliceHelper(100, 5, 10, 10);
    sliceHelper(100, 5, 100, 95);
    sliceHelper(100, 100, 0, 0);
    sliceHelper(100, 100, 10, 0);

    try {
      sliceHelper(100, 101, 10, 0);
      fail("expected exception");
    } catch (EOFException expected) {
    }
  }

  /**
   * @param input the size of the input stream
   * @param offset the first argument to {@link ByteStreams#slice}
   * @param length the second argument to {@link ByteStreams#slice}
   * @param expectRead the number of bytes we expect to read
   */
  private static void sliceHelper(
      int input, int offset, long length, int expectRead) throws IOException {
    Preconditions.checkArgument(expectRead == (int)
        Math.max(0, Math.min(input, offset + length) - offset));
    InputSupplier<? extends InputStream> supplier
        = ByteStreams.newInputStreamSupplier(newPreFilledByteArray(input));
    assertTrue(Arrays.equals(
        newPreFilledByteArray(offset, expectRead),
        ByteStreams.toByteArray(ByteStreams.slice(supplier, offset, length))));
  }

  private static InputStream newTestStream(int n) {
    return new ByteArrayInputStream(newPreFilledByteArray(n));
  }

  private static CheckCloseSupplier.Input<InputStream> newCheckInput(
      InputSupplier<? extends InputStream> delegate) {
    return new CheckCloseSupplier.Input<InputStream>(delegate) {
      @Override protected InputStream wrap(InputStream object,
          final Callback callback) {
        return new FilterInputStream(object) {
          @Override public void close() throws IOException {
            callback.delegateClosed();
            super.close();
          }
        };
      }
    };
  }

  private static CheckCloseSupplier.Output<OutputStream> newCheckOutput(
      OutputSupplier<? extends OutputStream> delegate) {
    return new CheckCloseSupplier.Output<OutputStream>(delegate) {
      @Override protected OutputStream wrap(OutputStream object,
          final Callback callback) {
        return new FilterOutputStream(object) {
          @Override public void close() throws IOException {
            callback.delegateClosed();
            super.close();
          }
        };
      }
    };
  }

  /** Stream that will skip a maximum number of bytes at a time. */
  private static class SlowSkipper extends FilterInputStream {
    private final long max;

    public SlowSkipper(InputStream in, long max) {
      super(in);
      this.max = max;
    }

    @Override public long skip(long n) throws IOException {
      return super.skip(Math.min(max, n));
    }
  }

  public void testByteProcessorStopEarly() throws IOException {
    byte[] array = newPreFilledByteArray(6000);
    assertEquals((Integer) 42,
        ByteStreams.readBytes(ByteStreams.newInputStreamSupplier(array),
            new ByteProcessor<Integer>() {
              @Override
              public boolean processBytes(byte[] buf, int off, int len) {
                assertTrue(Arrays.equals(
                    copyOfRange(buf, off, off + len),
                    newPreFilledByteArray(4096)));
                return false;
              }

              @Override
              public Integer getResult() {
                return 42;
              }
            }));
  }

  private static byte[] copyOfRange(byte[] in, int from, int to) {
    byte[] out = new byte[to - from];
    for (int i = 0; i < to - from; i++) {
      out[i] = in[from + i];
    }
    return out;
  }

  private static void assertEquals(byte[] expected, byte[] actual) {
    assertEquals(Bytes.asList(expected), Bytes.asList(actual));
  }
}