Back to Repositories

Testing RecyclableBufferedInputStream Implementation in Glide

This test suite validates the functionality of RecyclableBufferedInputStream, a custom implementation for efficient buffered input stream handling in the Glide library. It ensures proper stream operations, memory management, and error handling.

Test Coverage Overview

The test suite provides comprehensive coverage of RecyclableBufferedInputStream functionality including:
  • Basic stream operations (read, skip, mark, reset)
  • Buffer management and recycling
  • Edge cases handling for different buffer sizes
  • Error conditions and exception handling
  • Integration with ArrayPool for memory efficiency

Implementation Analysis

The testing approach uses JUnit with Robolectric for Android environment simulation. It employs systematic verification of stream operations through:
  • Byte-level read/write accuracy checks
  • Buffer boundary testing
  • Mock objects for wrapped stream verification
  • Explicit state validation after operations

Technical Details

Testing infrastructure includes:
  • JUnit 4 test framework
  • Robolectric for Android runtime simulation
  • Mockito for mock object creation
  • Custom byte array validation
  • LruArrayPool implementation for buffer management

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices including:
  • Thorough setup and teardown management
  • Comprehensive edge case coverage
  • Clear test method naming conventions
  • Proper exception testing
  • Efficient test data generation
  • Systematic buffer size validation

bumptech/glide

library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/RecyclableBufferedInputStreamTest.java

            
package com.bumptech.glide.load.resource.bitmap;

import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

// Not required in tests.
@SuppressWarnings("ResultOfMethodCallIgnored")
@RunWith(RobolectricTestRunner.class)
@Config(sdk = ROBOLECTRIC_SDK)
public class RecyclableBufferedInputStreamTest {

  private static final int DATA_SIZE = 30;
  private static final int BUFFER_SIZE = 10;

  private RecyclableBufferedInputStream stream;
  private byte[] data;
  private ArrayPool byteArrayPool;

  @Before
  public void setUp() {
    data = new byte[DATA_SIZE];
    for (int i = 0; i < DATA_SIZE; i++) {
      data[i] = (byte) i;
    }

    byteArrayPool = new LruArrayPool();
    InputStream wrapped = new ByteArrayInputStream(data);
    stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool, BUFFER_SIZE);
  }

  @Test
  public void testReturnsTrueForMarkSupported() {
    assertTrue(stream.markSupported());
  }

  @Test
  public void testCanReadIndividualBytes() throws IOException {
    for (int i = 0; i < data.length; i++) {
      assertEquals(i, stream.read());
    }
    assertEquals(-1, stream.read());
  }

  @Test
  public void testCanReadBytesInBulkLargerThanBufferSize() throws IOException {
    byte[] buffer = new byte[DATA_SIZE];
    assertEquals(DATA_SIZE, stream.read(buffer, 0, DATA_SIZE));
    for (int i = 0; i < DATA_SIZE; i++) {
      assertEquals(i, buffer[i]);
    }
  }

  @Test
  public void testCanReadBytesInBulkSmallerThanBufferSize() throws IOException {
    int toRead = BUFFER_SIZE / 2;
    byte[] buffer = new byte[toRead];
    assertEquals(toRead, stream.read(buffer, 0, toRead));
    for (int i = 0; i < toRead; i++) {
      assertEquals(i, buffer[i]);
    }
  }

  @Test
  public void testReadingZeroBytesIntoBufferReadsZeroBytes() throws IOException {
    // Make sure the next value is not 0.
    stream.read();
    byte[] buffer = new byte[BUFFER_SIZE];
    assertEquals(0, stream.read(buffer, 0, 0));

    for (int i = 0; i < BUFFER_SIZE; i++) {
      assertEquals(0, buffer[i]);
    }
  }

  @Test
  public void testCanReadIntoBufferLargerThanDataSize() throws IOException {
    int toRead = DATA_SIZE * 2;
    byte[] buffer = new byte[toRead];
    assertEquals(DATA_SIZE, stream.read(buffer, 0, toRead));
    for (int i = 0; i < DATA_SIZE; i++) {
      assertEquals(i, buffer[i]);
    }
    for (int i = DATA_SIZE; i < toRead; i++) {
      assertEquals(0, buffer[i]);
    }
  }

  @Test
  public void testCanReadBytesInBulkWithLimit() throws IOException {
    int toRead = BUFFER_SIZE / 2;
    byte[] buffer = new byte[BUFFER_SIZE];
    assertEquals(toRead, stream.read(buffer, 0, toRead));

    // 0, 1, 2, 3, 4, 0, 0, 0, 0, 0
    for (int i = 0; i < toRead; i++) {
      assertEquals(i, buffer[i]);
    }
    for (int i = toRead; i < BUFFER_SIZE; i++) {
      assertEquals(0, buffer[i]);
    }
  }

  @Test
  public void testCanReadBytesInBulkWithOffset() throws IOException {
    int toRead = BUFFER_SIZE / 2;
    byte[] buffer = new byte[BUFFER_SIZE];
    assertEquals(toRead, stream.read(buffer, BUFFER_SIZE - toRead, toRead));
    // 0, 0, 0, 0, 0, 0, 1, 2, 3, 4
    for (int i = 0; i < toRead; i++) {
      assertEquals(0, buffer[i]);
    }
    for (int i = toRead; i < BUFFER_SIZE; i++) {
      assertEquals(i - toRead, buffer[i]);
    }
  }

  @Test
  public void testCanReadBytesInBulkWhenSomeButNotAllBytesAreInBuffer() throws IOException {
    stream.read();
    byte[] buffer = new byte[BUFFER_SIZE];
    assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE));
    for (int i = 1; i < BUFFER_SIZE + 1; i++) {
      assertEquals(i, buffer[i - 1]);
    }
  }

  @Test
  public void testCanSkipBytes() throws IOException {
    int toSkip = data.length / 2;
    assertEquals(toSkip, stream.skip(toSkip));
    for (int i = toSkip; i < data.length; i++) {
      assertEquals(i, stream.read());
    }
    assertEquals(-1, stream.read());
  }

  @Test
  public void testSkipReturnsZeroIfSkipByteCountIsZero() throws IOException {
    assertEquals(0, stream.skip(0));
    assertEquals(0, stream.read());
  }

  @Test
  public void testSkipReturnsZeroIfSkipByteCountIsNegative() throws IOException {
    assertEquals(0, stream.skip(-13));
    assertEquals(0, stream.read());
  }

  @Test
  public void testCloseClosesWrappedStream() throws IOException {
    InputStream wrapped = mock(InputStream.class);
    stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool);
    stream.close();
    verify(wrapped).close();
  }

  @Test
  public void testCanSafelyBeClosedMultipleTimes() throws IOException {
    InputStream wrapped = mock(InputStream.class);
    stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool);
    stream.close();
    stream.close();
    stream.close();

    verify(wrapped, times(1)).close();
  }

  @Test
  public void testCanMarkAndReset() throws IOException {
    byte[] buffer = new byte[BUFFER_SIZE];
    stream.mark(BUFFER_SIZE);
    assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE));
    for (int i = 0; i < BUFFER_SIZE; i++) {
      assertEquals(i, buffer[i]);
    }
    Arrays.fill(buffer, (byte) 0);
    stream.reset();

    assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE));
    for (int i = 0; i < BUFFER_SIZE; i++) {
      assertEquals(i, buffer[i]);
    }
  }

  @Test
  public void testCanResetRepeatedlyAfterMarking() throws IOException {
    byte[] buffer = new byte[BUFFER_SIZE];
    stream.mark(BUFFER_SIZE);
    for (int repeat = 0; repeat < 10; repeat++) {
      assertEquals(BUFFER_SIZE, stream.read(buffer, 0, BUFFER_SIZE));
      for (int i = 0; i < BUFFER_SIZE; i++) {
        assertEquals(i, buffer[i]);
      }
      stream.reset();
    }
  }

  @Test
  public void testCanMarkInMiddleOfBufferAndStillReadUpToBufferLengthBeforeResetting()
      throws IOException {
    int markPos = BUFFER_SIZE / 2;
    for (int i = 0; i < markPos; i++) {
      stream.read();
    }
    stream.mark(BUFFER_SIZE);

    for (int i = 0; i < BUFFER_SIZE; i++) {
      stream.read();
    }

    stream.reset();
    assertEquals(markPos, stream.read());
  }

  @Test
  public void testAvailableReturnsWrappedAvailableIfNoBytesRead() throws IOException {
    assertEquals(DATA_SIZE, stream.available());
  }

  @Test
  public void testAvailableIncludesDataInBufferAndWrappedAvailableIfBytesRead() throws IOException {
    stream.read();
    assertEquals(DATA_SIZE - 1, stream.available());
  }

  @Test(expected = IOException.class)
  public void testCloseThrowsIfWrappedStreamThrowsOnClose() throws IOException {
    InputStream wrapped = mock(InputStream.class);
    doThrow(new IOException()).when(wrapped).close();
    stream = new RecyclableBufferedInputStream(wrapped, byteArrayPool);
    stream.close();
  }

  @Test(expected = IOException.class)
  public void testAvailableThrowsIfStreamIsClosed() throws IOException {
    stream.close();
    stream.available();
  }

  @Test(expected = IOException.class)
  public void testReadThrowsIfStreamIsClosed() throws IOException {
    stream.close();
    stream.read();
  }

  @Test(expected = IOException.class)
  public void testReadBulkThrowsIfStreamIsClosed() throws IOException {
    stream.close();
    stream.read(new byte[1], 0, 1);
  }

  @Test(expected = IOException.class)
  public void testResetThrowsIfStreamIsClosed() throws IOException {
    stream.close();
    stream.reset();
  }

  @Test(expected = IOException.class)
  public void testSkipThrowsIfStreamIsClosed() throws IOException {
    stream.close();
    stream.skip(10);
  }

  @Test(expected = RecyclableBufferedInputStream.InvalidMarkException.class)
  public void testResetThrowsIfMarkNotSet() throws IOException {
    stream.reset();
  }

  @Test(expected = RecyclableBufferedInputStream.InvalidMarkException.class)
  public void testResetThrowsIfMarkIsInvalid() throws IOException {
    stream.mark(1);
    stream.read(new byte[BUFFER_SIZE], 0, BUFFER_SIZE);
    stream.read();
    stream.reset();
  }
}