Back to Repositories

Testing Bitmap PreFill Operations in Glide Library

This test suite validates the BitmapPreFillRunner component in the Glide image loading library, focusing on bitmap pre-filling functionality and memory management. The tests verify bitmap allocation patterns, memory cache interactions, and performance optimization strategies.

Test Coverage Overview

The test suite provides comprehensive coverage of bitmap pre-filling operations in Glide.

Key areas tested include:
  • Bitmap allocation ordering and sequencing
  • Memory cache and bitmap pool interactions
  • Performance throttling and backoff mechanisms
  • Resource management and memory optimization

Implementation Analysis

The testing approach utilizes JUnit with Robolectric for Android environment simulation. Tests employ extensive mocking using Mockito to isolate bitmap handling and memory management components.

Notable patterns include:
  • Mock handlers for bitmap allocation verification
  • Custom answer implementations for tracking bitmap operations
  • Systematic validation of memory cache vs pool behaviors

Technical Details

Testing infrastructure includes:
  • JUnit 4 test framework
  • Robolectric for Android runtime simulation
  • Mockito for dependency mocking
  • Custom bitmap tracking mechanisms
  • Memory cache and pool simulators

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through systematic validation of complex bitmap operations.

Notable practices include:
  • Thorough setup and teardown management
  • Extensive edge case coverage
  • Clear test method naming and organization
  • Effective use of mocking and verification

bumptech/glide

library/test/src/test/java/com/bumptech/glide/load/engine/prefill/BitmapPreFillRunnerTest.java

            
package com.bumptech.glide.load.engine.prefill;

import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK;
import static com.bumptech.glide.tests.Util.anyResource;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.graphics.Bitmap;
import android.os.Handler;
import android.util.Log;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool;
import com.bumptech.glide.load.engine.cache.MemoryCache;
import com.bumptech.glide.load.engine.cache.MemoryCacheAdapter;
import com.bumptech.glide.load.resource.bitmap.BitmapResource;
import com.bumptech.glide.tests.Util.CreateBitmap;
import com.bumptech.glide.util.Util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = ROBOLECTRIC_SDK)
public class BitmapPreFillRunnerTest {
  @Mock private BitmapPreFillRunner.Clock clock;
  @Mock private BitmapPool pool;
  @Mock private MemoryCache cache;
  @Mock private Handler mainHandler;
  private final List<Bitmap> addedBitmaps = new ArrayList<>();

  @Before
  public void setUp() {
    MockitoAnnotations.initMocks(this);

    doAnswer(new AddBitmapPoolAnswer(addedBitmaps)).when(pool).put(any(Bitmap.class));
    when(pool.getDirty(anyInt(), anyInt(), any(Bitmap.Config.class)))
        .thenAnswer(new CreateBitmap());
    when(cache.put(any(Key.class), anyResource()))
        .thenAnswer(new AddBitmapCacheAnswer(addedBitmaps));
  }

  private BitmapPreFillRunner getHandler(Map<PreFillType, Integer> allocationOrder) {
    return new BitmapPreFillRunner(
        pool, cache, new PreFillQueue(allocationOrder), clock, mainHandler);
  }

  @Test
  public void testAllocatesABitmapPerSizeInAllocationOrder() {
    PreFillType size = new PreFillType.Builder(100).setConfig(Bitmap.Config.ARGB_8888).build();
    final int toAdd = 3;
    Map<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(size, toAdd);
    BitmapPreFillRunner handler = getHandler(allocationOrder);
    handler.run();

    Bitmap expected = Bitmap.createBitmap(size.getWidth(), size.getHeight(), size.getConfig());
    // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed
    // assertThat(addedBitmaps).containsExactly(expected, expected, expected);
  }

  @Test
  public void testAllocatesBitmapsInOrderGivenByAllocationOrder() {
    PreFillType smallWidth =
        new PreFillType.Builder(50, 100).setConfig(Bitmap.Config.ARGB_8888).build();
    PreFillType smallHeight =
        new PreFillType.Builder(100, 50).setConfig(Bitmap.Config.RGB_565).build();

    PreFillType[] expectedOrder =
        new PreFillType[] {
          smallWidth, smallHeight, smallWidth, smallHeight,
        };

    HashMap<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(smallWidth, 2);
    allocationOrder.put(smallHeight, 2);
    BitmapPreFillRunner handler = getHandler(allocationOrder);
    handler.run();

    Bitmap[] expectedBitmaps = new Bitmap[expectedOrder.length];
    for (int i = 0; i < expectedBitmaps.length; i++) {
      PreFillType current = expectedOrder[i];
      expectedBitmaps[i] =
          Bitmap.createBitmap(current.getWidth(), current.getHeight(), current.getConfig());
    }

    Bitmap current = addedBitmaps.get(0);
    for (int i = 1; i < addedBitmaps.size(); i++) {
      assertNotEquals(current, addedBitmaps.get(i));
      current = addedBitmaps.get(i);
    }

    assertThat(addedBitmaps).hasSize(4);
  }

  @Test
  public void testStopsAllocatingBitmapsUntilNextIdleCallIfAllocationsTakeLongerThanLimit() {
    PreFillType size = new PreFillType.Builder(1).setConfig(Bitmap.Config.ARGB_8888).build();
    Map<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(size, 3);
    when(clock.now()).thenReturn(0L).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS);
    BitmapPreFillRunner handler = getHandler(allocationOrder);
    handler.run();

    assertThat(addedBitmaps).hasSize(1);

    handler.run();

    assertThat(addedBitmaps).hasSize(3);
  }

  @Test
  public void testPreFillHandlerDoesNotPostIfHasNoBitmapsToAllocate() {
    BitmapPreFillRunner handler = getHandler(new HashMap<PreFillType, Integer>());
    handler.run();
    verify(mainHandler, never()).postDelayed(any(Runnable.class), anyLong());
  }

  @Test
  public void testPreFillHandlerPostsIfHasBitmapsToAllocateAfterRunning() {
    PreFillType size = new PreFillType.Builder(1).setConfig(Bitmap.Config.ARGB_8888).build();
    Map<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(size, 2);
    BitmapPreFillRunner handler = getHandler(allocationOrder);
    when(clock.now()).thenReturn(0L).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS);

    handler.run();
    verify(mainHandler).postDelayed(eq(handler), anyLong());
  }

  @Test
  public void testPreFillHandlerPostsWithBackoffIfHasBitmapsToAllocateAfterRunning() {
    PreFillType size = new PreFillType.Builder(1).setConfig(Bitmap.Config.ARGB_8888).build();
    Map<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(size, 100);

    BitmapPreFillRunner handler = getHandler(allocationOrder);
    when(clock.now()).thenReturn(0L).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS);

    handler.run();
    verify(mainHandler).postDelayed(eq(handler), eq(BitmapPreFillRunner.INITIAL_BACKOFF_MS));

    when(clock.now())
        .thenReturn(BitmapPreFillRunner.MAX_DURATION_MS)
        .thenReturn(
            BitmapPreFillRunner.MAX_DURATION_MS
                + BitmapPreFillRunner.INITIAL_BACKOFF_MS * BitmapPreFillRunner.BACKOFF_RATIO);

    handler.run();

    verify(mainHandler)
        .postDelayed(
            eq(handler),
            eq(BitmapPreFillRunner.INITIAL_BACKOFF_MS * BitmapPreFillRunner.BACKOFF_RATIO));

    when(clock.now()).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS);
    handler.run();
    when(clock.now()).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS);
    handler.run();
    when(clock.now()).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS);
    handler.run();
    when(clock.now()).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS);
    handler.run();

    verify(mainHandler, atLeastOnce())
        .postDelayed(eq(handler), eq(BitmapPreFillRunner.MAX_BACKOFF_MS));
  }

  @Test
  public void testPreFillHandlerDoesNotPostIfHasBitmapsButIsCancelled() {
    PreFillType size = new PreFillType.Builder(1).setConfig(Bitmap.Config.ARGB_8888).build();
    Map<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(size, 2);

    BitmapPreFillRunner handler = getHandler(allocationOrder);
    when(clock.now()).thenReturn(0L).thenReturn(0L).thenReturn(BitmapPreFillRunner.MAX_DURATION_MS);
    handler.cancel();
    handler.run();

    verify(mainHandler, never()).postDelayed(any(Runnable.class), anyLong());
  }

  @Test
  public void testAddsBitmapsToMemoryCacheIfMemoryCacheHasEnoughSpaceRemaining() {
    Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
    when(cache.getMaxSize()).thenReturn(Long.valueOf(Util.getBitmapByteSize(bitmap)));

    PreFillType size =
        new PreFillType.Builder(bitmap.getWidth(), bitmap.getHeight())
            .setConfig(bitmap.getConfig())
            .build();
    Map<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(size, 1);

    getHandler(allocationOrder).run();

    verify(cache).put(any(Key.class), anyResource());
    verify(pool, never()).put(any(Bitmap.class));
    // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed
    // assertThat(addedBitmaps).containsExactly(bitmap);
  }

  @Test
  public void testAddsBitmapsToBitmapPoolIfMemoryCacheIsFull() {
    Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
    when(cache.getMaxSize()).thenReturn(0L);

    PreFillType size =
        new PreFillType.Builder(bitmap.getWidth(), bitmap.getHeight())
            .setConfig(bitmap.getConfig())
            .build();
    Map<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(size, 1);

    getHandler(allocationOrder).run();

    verify(cache, never()).put(any(Key.class), anyResource());
    // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed
    // verify(pool).put(eq(bitmap));
    // assertThat(addedBitmaps).containsExactly(bitmap);
  }

  @Test
  public void testAddsBitmapsToPoolIfMemoryCacheIsNotFullButCannotFitBitmap() {
    Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
    when(cache.getMaxSize()).thenReturn((long) Util.getBitmapByteSize(bitmap) / 2);

    PreFillType size =
        new PreFillType.Builder(bitmap.getWidth(), bitmap.getHeight())
            .setConfig(bitmap.getConfig())
            .build();
    Map<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(size, 1);

    getHandler(allocationOrder).run();

    verify(cache, never()).put(any(Key.class), anyResource());
    // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed
    // verify(pool).put(eq(bitmap));
    // assertThat(addedBitmaps).containsExactly(bitmap);
  }

  @Test
  public void testDoesAGetFromPoolBeforeAddingForEachSize() {
    Bitmap first = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_4444);
    PreFillType firstSize =
        new PreFillType.Builder(first.getWidth(), first.getHeight())
            .setConfig(first.getConfig())
            .build();

    Bitmap second = Bitmap.createBitmap(200, 200, Bitmap.Config.RGB_565);
    PreFillType secondSize =
        new PreFillType.Builder(second.getWidth(), second.getHeight())
            .setConfig(second.getConfig())
            .build();

    Map<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(firstSize, 1);
    allocationOrder.put(secondSize, 1);

    getHandler(allocationOrder).run();

    InOrder firstOrder = inOrder(pool);
    firstOrder
        .verify(pool)
        .getDirty(eq(first.getWidth()), eq(first.getHeight()), eq(first.getConfig()));
    // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed
    // firstOrder.verify(pool).put(eq(first));

    InOrder secondOrder = inOrder(pool);
    secondOrder
        .verify(pool)
        .getDirty(eq(second.getWidth()), eq(second.getHeight()), eq(second.getConfig()));
    // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed
    // secondOrder.verify(pool).put(eq(second));
  }

  @Test
  public void testDoesNotGetMoreThanOncePerSize() {
    Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_4444);
    PreFillType size =
        new PreFillType.Builder(bitmap.getWidth(), bitmap.getHeight())
            .setConfig(bitmap.getConfig())
            .build();

    final int numBitmaps = 5;
    Map<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(size, numBitmaps);

    getHandler(allocationOrder).run();

    InOrder order = inOrder(pool);
    order
        .verify(pool)
        .getDirty(eq(bitmap.getWidth()), eq(bitmap.getHeight()), eq(bitmap.getConfig()));
    // TODO(b/20335397): This code was relying on Bitmap equality which Robolectric removed
    // order.verify(pool, times(numBitmaps)).put(eq(bitmap));
  }

  @Test
  public void allocate_whenBitmapPoolIsAtCapacity_doesNotLogWithRecycledBitmap() {
    ShadowLog.setLoggable(BitmapPreFillRunner.TAG, Log.VERBOSE);

    int dimensions = 10;
    Bitmap.Config config = Bitmap.Config.ARGB_8888;
    int bitmapByteSize = Util.getBitmapByteSize(dimensions, dimensions, config);
    PreFillType preFillType = new PreFillType.Builder(dimensions).setConfig(config).build();
    Map<PreFillType, Integer> allocationOrder = new HashMap<>();
    allocationOrder.put(preFillType, 1);
    PreFillQueue queue = new PreFillQueue(allocationOrder);
    BitmapPreFillRunner runner =
        new BitmapPreFillRunner(
            new LruBitmapPool(bitmapByteSize - 1), new MemoryCacheAdapter(), queue);

    runner.allocate();
  }

  private static final class AddBitmapPoolAnswer implements Answer<Void> {
    private final List<Bitmap> bitmaps;

    AddBitmapPoolAnswer(List<Bitmap> bitmaps) {
      this.bitmaps = bitmaps;
    }

    @Override
    public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
      Bitmap bitmap = (Bitmap) invocationOnMock.getArguments()[0];
      bitmaps.add(bitmap);
      return null;
    }
  }

  private static final class AddBitmapCacheAnswer implements Answer<Resource<?>> {
    private final List<Bitmap> bitmaps;

    AddBitmapCacheAnswer(List<Bitmap> bitmaps) {
      this.bitmaps = bitmaps;
    }

    @Override
    public Resource<?> answer(InvocationOnMock invocationOnMock) throws Throwable {
      BitmapResource resource = (BitmapResource) invocationOnMock.getArguments()[1];
      bitmaps.add(resource.get());
      return null;
    }
  }
}