Back to Repositories

Testing GIF Frame Loading and Transformation Management in bumptech/glide

This test suite validates the GifFrameLoader functionality in Glide, focusing on frame loading, transformation, and lifecycle management for GIF animations. The tests ensure proper handling of bitmap frames, frame transitions, and resource management within Android’s context.

Test Coverage Overview

The test suite provides comprehensive coverage of GifFrameLoader functionality including:
  • Frame transformation and bitmap handling
  • Resource lifecycle management and memory handling
  • Frame loading states and transitions
  • Handler message timing and delivery
  • Edge cases around loading, clearing, and visibility changes

Implementation Analysis

The testing approach utilizes JUnit and Robolectric frameworks for Android-specific testing. Mockito is extensively used for mocking dependencies and verifying interactions. The tests follow a systematic pattern of setup-execute-verify with careful attention to frame loading sequences and state management.

Key patterns include mock initialization, bitmap resource management, and handler message verification.

Technical Details

Testing tools and configuration:
  • JUnit 4 test runner
  • Robolectric for Android framework simulation
  • Mockito for mocking and verification
  • Custom TearDownGlide rule for cleanup
  • LEGACY LooperMode configuration
  • SDK version targeting via ROBOLECTRIC_SDK

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Proper test isolation using @Before setup
  • Comprehensive mock verification
  • Explicit error case testing
  • Resource cleanup handling
  • Clear test method naming conventions
  • Thorough edge case coverage

bumptech/glide

library/test/src/test/java/com/bumptech/glide/load/resource/gif/GifFrameLoaderTest.java

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

import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.annotation.LooperMode.Mode.LEGACY;

import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Message;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.gifdecoder.GifDecoder;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.resource.gif.GifFrameLoader.DelayTarget;
import com.bumptech.glide.load.resource.gif.GifFrameLoader.FrameCallback;
import com.bumptech.glide.request.Request;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.tests.TearDownGlide;
import com.bumptech.glide.tests.Util.ReturnsSelfAnswer;
import com.bumptech.glide.util.Util;
import java.nio.ByteBuffer;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;

@LooperMode(LEGACY)
@RunWith(RobolectricTestRunner.class)
@Config(sdk = ROBOLECTRIC_SDK)
public class GifFrameLoaderTest {
  @Rule public TearDownGlide tearDownGlide = new TearDownGlide();

  @Mock private GifFrameLoader.FrameCallback callback;
  @Mock private GifDecoder gifDecoder;
  @Mock private Handler handler;
  @Mock private Transformation<Bitmap> transformation;
  @Mock private RequestManager requestManager;
  private GifFrameLoader loader;
  private RequestBuilder<Bitmap> requestBuilder;
  private Bitmap firstFrame;

  @SuppressWarnings("unchecked")
  @Before
  public void setUp() {
    MockitoAnnotations.initMocks(this);
    when(handler.obtainMessage(anyInt(), isA(DelayTarget.class))).thenReturn(mock(Message.class));

    firstFrame = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);

    ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    when(gifDecoder.getData()).thenReturn(byteBuffer);

    requestBuilder = mock(RequestBuilder.class, new ReturnsSelfAnswer());

    loader = createGifFrameLoader(handler);
  }

  @NonNull
  private GifFrameLoader createGifFrameLoader(Handler handler) {
    Glide glide = getGlideSingleton();
    GifFrameLoader result =
        new GifFrameLoader(
            glide.getBitmapPool(),
            requestManager,
            gifDecoder,
            handler,
            requestBuilder,
            transformation,
            firstFrame);
    result.subscribe(callback);
    return result;
  }

  private static Glide getGlideSingleton() {
    return Glide.get(ApplicationProvider.getApplicationContext());
  }

  @SuppressWarnings("unchecked")
  @Test
  public void testSetFrameTransformationSetsTransformationOnRequestBuilder() {
    verify(requestBuilder, times(2)).apply(isA(RequestOptions.class));
    Transformation<Bitmap> transformation = mock(Transformation.class);
    loader.setFrameTransformation(transformation, firstFrame);

    verify(requestBuilder, times(3)).apply(isA(RequestOptions.class));
  }

  @Test(expected = NullPointerException.class)
  public void testSetFrameTransformationThrowsIfGivenNullTransformation() {
    loader.setFrameTransformation(null, null);
  }

  @Test
  public void testReturnsSizeFromGifDecoderAndCurrentFrame() {
    int decoderByteSize = 123456;
    when(gifDecoder.getByteSize()).thenReturn(decoderByteSize);
    assertThat(loader.getSize()).isEqualTo(decoderByteSize + Util.getBitmapByteSize(firstFrame));
  }

  @Test
  public void testStartGetsNextFrameIfNotStartedAndWithNoLoadPending() {
    verify(requestBuilder).into(aTarget());
  }

  @Test
  public void testGetNextFrameIncrementsSignatureAndAdvancesDecoderBeforeStartingLoad() {
    InOrder order = inOrder(gifDecoder, requestBuilder);
    order.verify(gifDecoder).advance();
    order.verify(requestBuilder).apply(isA(RequestOptions.class));
    order.verify(requestBuilder).into(aTarget());
  }

  @Test
  public void testGetCurrentFrameReturnsFirstFrameWHenNoLoadHasCompleted() {
    assertThat(loader.getCurrentFrame()).isEqualTo(firstFrame);
  }

  @Test
  public void testGetCurrentFrameReturnsCurrentBitmapAfterLoadHasCompleted() {
    final Bitmap result = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
    DelayTarget target = mock(DelayTarget.class);
    when(target.getResource()).thenReturn(result);
    loader.onFrameReady(target);

    assertEquals(result, loader.getCurrentFrame());
  }

  @Test
  public void testStartDoesNotStartIfAlreadyRunning() {
    loader.subscribe(mock(FrameCallback.class));

    verify(requestBuilder, times(1)).into(aTarget());
  }

  @Test
  public void testGetNextFrameDoesNotStartLoadIfLoaderIsNotRunning() {
    verify(requestBuilder, times(1)).into(aTarget());
    loader.unsubscribe(callback);
    loader.onFrameReady(mock(DelayTarget.class));

    verify(requestBuilder, times(1)).into(aTarget());
  }

  @Test
  public void testGetNextFrameDoesNotStartLoadIfLoadIsInProgress() {
    loader.unsubscribe(callback);
    loader.subscribe(callback);

    verify(requestBuilder, times(1)).into(aTarget());
  }

  @Test
  public void testGetNextFrameDoesStartLoadIfRestartedAndNoLoadIsInProgress() {
    loader.unsubscribe(callback);

    loader.onFrameReady(mock(DelayTarget.class));
    loader.subscribe(callback);

    verify(requestBuilder, times(2)).into(aTarget());
  }

  @Test
  public void testGetNextFrameDoesStartLoadAfterLoadCompletesIfStarted() {
    loader.onFrameReady(mock(DelayTarget.class));

    verify(requestBuilder, times(2)).into(aTarget());
  }

  @Test
  public void testOnFrameReadyClearsPreviousFrame() {
    // Force the loader to create a real Handler.
    loader = createGifFrameLoader(null);

    DelayTarget previous = newDelayTarget();
    Request previousRequest = mock(Request.class);
    previous.setRequest(previousRequest);
    previous.onResourceReady(
        Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), /* transition= */ null);

    DelayTarget current = mock(DelayTarget.class);
    when(current.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565));
    loader.onFrameReady(previous);
    loader.onFrameReady(current);

    verify(requestManager).clear(eq(previous));
  }

  @Test
  public void testOnFrameReadyWithNullResourceDoesNotClearPreviousFrame() {
    // Force the loader to create a real Handler by passing null.
    loader = createGifFrameLoader(null);

    DelayTarget previous = newDelayTarget();
    Request previousRequest = mock(Request.class);
    previous.setRequest(previousRequest);
    previous.onResourceReady(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), null);

    DelayTarget current = mock(DelayTarget.class);
    when(current.getResource()).thenReturn(null);
    loader.onFrameReady(previous);
    loader.onFrameReady(current);

    verify(previousRequest, never()).clear();
  }

  @Test
  public void testDelayTargetSendsMessageWithHandlerDelayed() {
    long targetTime = 1234;
    DelayTarget delayTarget = new DelayTarget(handler, 1, targetTime);
    delayTarget.onResourceReady(
        Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), null
        /*glideAnimation*/ );
    verify(handler).sendMessageAtTime(isA(Message.class), eq(targetTime));
  }

  @Test
  public void testDelayTargetSetsResourceOnResourceReady() {
    DelayTarget delayTarget = new DelayTarget(handler, 1, 1);
    Bitmap expected = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
    delayTarget.onResourceReady(expected, null /*glideAnimation*/);

    assertEquals(expected, delayTarget.getResource());
  }

  @Test
  public void testClearsCompletedLoadOnFrameReadyIfCleared() {
    // Force the loader to create a real Handler by passing null;
    loader = createGifFrameLoader(null);
    loader.clear();
    DelayTarget delayTarget = newDelayTarget();
    Request request = mock(Request.class);
    delayTarget.setRequest(request);

    loader.onFrameReady(delayTarget);

    verify(requestManager).clear(eq(delayTarget));
  }

  @Test
  public void
      testDoesNotReturnResourceForCompletedFrameInGetCurrentFrameIfLoadCompletesWhileCleared() {
    loader.clear();
    DelayTarget delayTarget = mock(DelayTarget.class);
    Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
    when(delayTarget.getResource()).thenReturn(bitmap);

    loader.onFrameReady(delayTarget);

    assertNull(loader.getCurrentFrame());
  }

  @Test
  public void onFrameReady_whenNotRunning_doesNotClearPreviouslyLoadedImage() {
    loader = createGifFrameLoader(/* handler= */ null);
    DelayTarget loaded = mock(DelayTarget.class);
    when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
    loader.onFrameReady(loaded);
    loader.unsubscribe(callback);

    DelayTarget nextFrame = mock(DelayTarget.class);
    when(nextFrame.getResource())
        .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
    loader.onFrameReady(nextFrame);
    verify(requestManager, never()).clear(loaded);
  }

  @Test
  public void onFrameReady_whenNotRunning_clearsPendingFrameOnClear() {
    loader = createGifFrameLoader(/* handler= */ null);
    DelayTarget loaded = mock(DelayTarget.class);
    when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
    loader.onFrameReady(loaded);
    loader.unsubscribe(callback);

    DelayTarget nextFrame = mock(DelayTarget.class);
    when(nextFrame.getResource())
        .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
    loader.onFrameReady(nextFrame);

    loader.clear();
    verify(requestManager).clear(loaded);
    verify(requestManager).clear(nextFrame);
  }

  @Test
  public void onFrameReady_whenNotRunning_clearsOldFrameOnStart() {
    loader = createGifFrameLoader(/* handler= */ null);
    DelayTarget loaded = mock(DelayTarget.class);
    when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
    loader.onFrameReady(loaded);
    loader.unsubscribe(callback);

    DelayTarget nextFrame = mock(DelayTarget.class);
    when(nextFrame.getResource())
        .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
    loader.onFrameReady(nextFrame);

    loader.subscribe(callback);
    verify(requestManager).clear(loaded);
  }

  @Test
  public void onFrameReady_whenNotRunning_callsFrameReadyWithNewFrameOnStart() {
    loader = createGifFrameLoader(/* handler= */ null);
    DelayTarget loaded = mock(DelayTarget.class);
    when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
    loader.onFrameReady(loaded);
    loader.unsubscribe(callback);

    DelayTarget nextFrame = mock(DelayTarget.class);
    Bitmap expected = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
    when(nextFrame.getResource()).thenReturn(expected);
    loader.onFrameReady(nextFrame);

    verify(callback, times(1)).onFrameReady();
    loader.subscribe(callback);
    verify(callback, times(2)).onFrameReady();
    assertThat(loader.getCurrentFrame()).isEqualTo(expected);
  }

  @Test
  public void onFrameReady_whenInvisible_setVisibleLater() {
    loader = createGifFrameLoader(/* handler= */ null);
    // The target is invisible at this point.
    loader.unsubscribe(callback);
    loader.setNextStartFromFirstFrame();
    DelayTarget loaded = mock(DelayTarget.class);
    when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
    loader.onFrameReady(loaded);
    loader.subscribe(callback);
  }

  @Test
  public void startFromFirstFrame_withPendingFrame_clearsPendingFrame() {
    loader = createGifFrameLoader(/* handler= */ null);
    DelayTarget loaded = mock(DelayTarget.class);
    when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
    loader.onFrameReady(loaded);
    loader.unsubscribe(callback);

    DelayTarget nextFrame = mock(DelayTarget.class);
    Bitmap expected = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
    when(nextFrame.getResource()).thenReturn(expected);
    loader.onFrameReady(nextFrame);

    loader.setNextStartFromFirstFrame();
    verify(requestManager).clear(nextFrame);

    loader.subscribe(callback);
    verify(callback, times(1)).onFrameReady();
  }

  private DelayTarget newDelayTarget() {
    return new DelayTarget(handler, /* index= */ 0, /* targetTime= */ 0);
  }

  @SuppressWarnings("unchecked")
  private static Target<Bitmap> aTarget() {
    return isA(Target.class);
  }
}