Back to Repositories

Testing Video Frame Decoder Implementation in Glide

This test suite validates the VideoDecoder component in Glide, focusing on video frame extraction and bitmap handling functionality. It comprehensively tests video frame decoding, metadata retrieval, and device-specific optimizations across different Android versions.

Test Coverage Overview

The test suite provides extensive coverage of the VideoDecoder functionality, including:

  • Frame retrieval and bitmap handling
  • Media metadata extraction and management
  • Device-specific optimizations (particularly for ARC devices)
  • HDR rotation handling across Android versions
  • Error handling and resource cleanup

Implementation Analysis

The testing approach utilizes JUnit and Robolectric for Android environment simulation. It employs extensive mocking via Mockito to isolate components and verify interactions. The tests demonstrate thorough verification of both successful scenarios and edge cases, with particular attention to Android version-specific behaviors.

Technical Details

Key technical components include:

  • RobolectricTestRunner for Android framework simulation
  • Mockito for dependency mocking
  • Build.VERSION_CODES testing across Android API levels
  • MediaMetadataRetriever integration testing
  • Custom configuration for device-specific testing

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Proper test setup and teardown with @Before and @After annotations
  • Comprehensive mock object initialization and verification
  • Explicit testing of API version-specific behaviors
  • Thorough error case handling and resource cleanup testing
  • Clear test method naming and organization

bumptech/glide

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

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

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.ParcelFileDescriptor;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.tests.Util;
import com.bumptech.glide.util.Preconditions;
import java.io.IOException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = VERSION_CODES.O_MR1)
public class VideoDecoderTest {
  @Mock private ParcelFileDescriptor resource;
  @Mock private VideoDecoder.MediaMetadataRetrieverFactory factory;
  @Mock private VideoDecoder.MediaInitializer<ParcelFileDescriptor> initializer;
  @Mock private MediaMetadataRetriever retriever;
  @Mock private BitmapPool bitmapPool;
  private VideoDecoder<ParcelFileDescriptor> decoder;
  private Options options;
  private int initialSdkVersion;
  private String initialMake;
  private String initialModel;
  private String initialBuildId;
  private String initialDevice;

  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
    when(factory.build()).thenReturn(retriever);
    decoder = new VideoDecoder<>(bitmapPool, initializer, factory);
    options = new Options();

    initialSdkVersion = Build.VERSION.SDK_INT;
    initialMake = Build.MANUFACTURER;
    initialModel = Build.MODEL;
    initialBuildId = Build.ID;
    initialDevice = Build.DEVICE;
  }

  @After
  public void tearDown() {
    Util.setSdkVersionInt(initialSdkVersion);
    resetBuildInfo(initialMake, initialModel, initialBuildId, initialDevice);
  }

  @Test
  public void testReturnsRetrievedFrameForResource() throws IOException {
    Util.setSdkVersionInt(19);
    Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
    when(retriever.getFrameAtTime(VideoDecoder.DEFAULT_FRAME, VideoDecoder.DEFAULT_FRAME_OPTION))
        .thenReturn(expected);

    Resource<Bitmap> result =
        Preconditions.checkNotNull(decoder.decode(resource, 100, 100, options));

    verify(initializer).initializeRetriever(retriever, resource);
    assertEquals(expected, result.get());
  }

  @Test
  public void testReleasesMediaMetadataRetriever() {
    Util.setSdkVersionInt(19);
    assertThrows(
        RuntimeException.class,
        new ThrowingRunnable() {
          @Override
          public void run() throws IOException {
            decoder.decode(resource, 1, 2, options);
          }
        });
    try {
      verify(retriever).release();
    } catch (Exception e) {
      // Ignore failures while cleaning up.
    }
  }

  @Test(expected = IllegalArgumentException.class)
  public void testThrowsExceptionIfCalledWithInvalidFrame() throws IOException {
    Util.setSdkVersionInt(19);
    options.set(VideoDecoder.TARGET_FRAME, -5L);
    new VideoDecoder<>(bitmapPool, initializer, factory).decode(resource, 100, 100, options);
  }

  @Test
  public void testSpecifiesThumbnailFrameIfICalledWithFrameNumber() {
    Util.setSdkVersionInt(19);
    long frame = 5;
    options.set(VideoDecoder.TARGET_FRAME, frame);
    decoder = new VideoDecoder<>(bitmapPool, initializer, factory);

    assertThrows(
        RuntimeException.class,
        new ThrowingRunnable() {
          @Override
          public void run() throws IOException {
            decoder.decode(resource, 1, 2, options);
          }
        });

    verify(retriever).getFrameAtTime(frame, VideoDecoder.DEFAULT_FRAME_OPTION);
  }

  @Test
  public void testDoesNotSpecifyThumbnailFrameIfCalledWithoutFrameNumber() {
    Util.setSdkVersionInt(19);
    decoder = new VideoDecoder<>(bitmapPool, initializer, factory);
    assertThrows(
        RuntimeException.class,
        new ThrowingRunnable() {
          @Override
          public void run() throws IOException {
            decoder.decode(resource, 100, 100, options);
          }
        });

    verify(retriever).getFrameAtTime(VideoDecoder.DEFAULT_FRAME, VideoDecoder.DEFAULT_FRAME_OPTION);
  }

  @Test
  public void getScaledFrameAtTime() throws IOException {
    // Anything other than NONE.
    options.set(DownsampleStrategy.OPTION, DownsampleStrategy.AT_LEAST);

    Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
    when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH))
        .thenReturn("100");
    when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT))
        .thenReturn("100");
    when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION))
        .thenReturn("0");
    when(retriever.getScaledFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, 100, 100))
        .thenReturn(expected);

    assertThat(decoder.decode(resource, 100, 100, options).get()).isSameInstanceAs(expected);
  }

  @Test
  public void decodeFrame_withTargetSizeOriginal_onApi27_doesNotThrow() throws IOException {
    Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
    when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC))
        .thenReturn(expected);

    verify(retriever, never()).getScaledFrameAtTime(anyLong(), anyInt(), anyInt(), anyInt());
    assertThat(decoder.decode(resource, Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL, options).get())
        .isSameInstanceAs(expected);
  }

  @Test
  public void decodeFrame_withTargetSizeOriginalWidthOnly_onApi27_doesNotThrow()
      throws IOException {
    Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
    when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC))
        .thenReturn(expected);

    verify(retriever, never()).getScaledFrameAtTime(anyLong(), anyInt(), anyInt(), anyInt());
    assertThat(decoder.decode(resource, Target.SIZE_ORIGINAL, 100, options).get())
        .isSameInstanceAs(expected);
  }

  @Test
  public void decodeFrame_withTargetSizeOriginalHeightOnly_onApi27_doesNotThrow()
      throws IOException {
    Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
    when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC))
        .thenReturn(expected);

    verify(retriever, never()).getScaledFrameAtTime(anyLong(), anyInt(), anyInt(), anyInt());
    assertThat(decoder.decode(resource, 100, Target.SIZE_ORIGINAL, options).get())
        .isSameInstanceAs(expected);
  }

  @Test
  public void decodeFrame_notArcDeviceButWebm_doesNotInitializeMediaExtractor() throws IOException {
    setDevice("notArc");
    when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE))
        .thenReturn("video/webm");
    when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC))
        .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));

    decoder.decode(resource, Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL, options).get();

    verify(initializer, never()).initializeExtractor(any(), any());
  }

  @Test
  public void decodeFrame_arcDeviceButNotWebm_doesNotInitializeMediaExtractor() throws IOException {
    setDevice("arc_cheets");
    when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE))
        .thenReturn("video/mp4");
    when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC))
        .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));

    decoder.decode(resource, Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL, options).get();

    verify(initializer, never()).initializeExtractor(any(), any());
  }

  @Test
  public void decodeFrame_arcDeviceAndWebm_initializesMediaExtractor() throws IOException {
    setDevice("arc_cheets");
    when(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE))
        .thenReturn("video/webm");
    when(retriever.getFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC))
        .thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));

    decoder.decode(resource, Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL, options).get();

    verify(initializer).initializeExtractor(any(), any());
  }

  @Test
  @Config(sdk = VERSION_CODES.M)
  public void isHdr180RotationFixRequired_androidM_returnsFalse() {
    assertThat(VideoDecoder.isHdr180RotationFixRequired()).isFalse();
  }

  @Test
  @Config(sdk = VERSION_CODES.Q)
  public void isHdr180RotationFixRequired_androidQ_returnsFalse() {
    assertThat(VideoDecoder.isHdr180RotationFixRequired()).isFalse();
  }

  @Test
  @Config(sdk = VERSION_CODES.R)
  public void isHdr180RotationFixRequired_androidR_returnsTrue() {
    assertThat(VideoDecoder.isHdr180RotationFixRequired()).isTrue();
  }

  @Test
  @Config(sdk = VERSION_CODES.S)
  public void isHdr180RotationFixRequired_androidS_returnsTrue() {
    assertThat(VideoDecoder.isHdr180RotationFixRequired()).isTrue();
  }

  private void resetBuildInfo(String make, String model, String buildId, String device) {
    ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", make);
    ReflectionHelpers.setStaticField(Build.class, "MODEL", model);
    ReflectionHelpers.setStaticField(Build.class, "ID", buildId);
    setDevice(device);
  }

  private void setDevice(String device) {
    ReflectionHelpers.setStaticField(Build.class, "DEVICE", device);
  }
}