Back to Repositories

Testing Resource Downsampling Implementation in Glide

This test suite validates Glide’s image resource loading capabilities with downsampling, focusing on handling various image formats and configurations. It ensures proper decoding of different image types including JPEG, PNG, and GIF while testing hardware bitmap support and transparency handling.

Test Coverage Overview

The test suite provides comprehensive coverage of Glide’s resource loading functionality:

  • JPEG resource loading with wide gamut support
  • PNG handling for both opaque and transparent images
  • GIF processing including transparent and opaque variants
  • Hardware bitmap configuration testing
  • Byte array loading verification

Implementation Analysis

The testing approach employs JUnit with AndroidJUnit4 runner, utilizing custom model loaders and data fetchers. The implementation features concurrent testing patterns through ConcurrencyHelper and systematic resource loading verification across different Android API levels.

  • Custom FakeModelLoader implementation for controlled testing
  • Systematic validation of bitmap configurations
  • API-level specific feature testing with assumeTrue conditions

Technical Details

Testing infrastructure includes:

  • AndroidJUnit4 test runner
  • ConcurrencyHelper for async operations
  • TearDownGlide rule for clean test isolation
  • Custom InputStream handling
  • BitmapFactory and ColorSpace validations

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Proper test isolation and cleanup
  • Comprehensive error handling
  • Platform version-specific test conditions
  • Resource cleanup in data fetchers
  • Systematic validation of image properties

bumptech/glide

instrumentation/src/androidTest/java/com/bumptech/glide/LoadResourcesWithDownsamplerTest.java

            
package com.bumptech.glide;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeTrue;

import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ColorSpace;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import com.bumptech.glide.load.resource.bitmap.Downsampler;
import com.bumptech.glide.signature.ObjectKey;
import com.bumptech.glide.test.GlideApp;
import com.bumptech.glide.test.ResourceIds;
import com.bumptech.glide.testutil.ConcurrencyHelper;
import com.bumptech.glide.testutil.TearDownGlide;
import com.bumptech.glide.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * On API 26, decoding a variety of different images can cause {@link BitmapFactory} with {@link
 * BitmapFactory.Options#inJustDecodeBounds} set to {@code true} to set {@link
 * BitmapFactory.Options#outConfig} to null instead of a valid value, even though the image can be
 * decoded successfully. Glide can mask these failures by decoding some image sources (notably
 * including resource ids) using other data types and decoders.
 *
 * <p>This test ensures that we've worked around the framework issue by loading a variety of images
 * and image types without the normal fallback behavior.
 */
@RunWith(AndroidJUnit4.class)
public class LoadResourcesWithDownsamplerTest {
  @Rule public final TearDownGlide tearDownGlide = new TearDownGlide();
  private final ConcurrencyHelper concurrency = new ConcurrencyHelper();
  private final Context context = ApplicationProvider.getApplicationContext();

  @Test
  public void loadJpegResource_withNoOtherLoaders_decodesResource() {
    Glide.get(context)
        .getRegistry()
        .prepend(Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.canonical));

    Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit());
    assertThat(bitmap).isNotNull();
  }

  @Test
  public void loadWideGamutJpegResource_withNoOtherLoaders_decodesWideGamutBitmap() {
    assumeTrue(
        "Wide gamut is only available on O+", Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
    Glide.get(context)
        .getRegistry()
        .prepend(
            Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.webkit_logo_p3));

    Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit());
    assertThat(bitmap).isNotNull();
    assertThat(bitmap.getConfig()).isEqualTo(Bitmap.Config.RGBA_F16);

    // The exact value here depends on the emulator / device we're running on. On Pixel devices and
    // emulators it'll return DISPLAY_P3. On 'generic' emulators and some other devices, it'll
    // return LINEAR_EXTENDED_SRGB. It's unclear how else we can assert correctly based on the
    // device type, so I've just left this is isAnyOf for now.
    assertThat(bitmap.getColorSpace())
        .isAnyOf(
            ColorSpace.get(ColorSpace.Named.DISPLAY_P3),
            ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB));
  }

  @Test
  public void loadOpaquePngResource_withNoOtherLoaders_decodesResource() {
    Glide.get(context)
        .getRegistry()
        .prepend(
            Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.canonical_png));

    Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit());
    assertThat(bitmap).isNotNull();
  }

  @Test
  public void loadTransparentPngResource_withNoOtherLoaders_decodesResource() {
    Glide.get(context)
        .getRegistry()
        .prepend(
            Object.class,
            InputStream.class,
            new FakeModelLoader<>(ResourceIds.raw.canonical_transparent_png));

    Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit());
    assertThat(bitmap).isNotNull();
  }

  @Test
  public void loadTransparentGifResource_withNoOtherLoaders_decodesResource() {
    Glide.get(context)
        .getRegistry()
        .prepend(
            Object.class,
            InputStream.class,
            new FakeModelLoader<>(ResourceIds.raw.transparent_gif));

    Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit());
    assertThat(bitmap).isNotNull();
  }

  @Test
  public void loadTransparentGifResource_asHardware_withNoOtherLoaders_decodesResource()
      throws InterruptedException {
    assumeTrue(
        "Hardware Bitmaps are only supported on P+",
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);
    // enableHardwareBitmaps must be called on the main thread.
    final CountDownLatch latch = new CountDownLatch(1);
    Util.postOnUiThread(
        new Runnable() {
          @Override
          public void run() {
            Glide.enableHardwareBitmaps();
            latch.countDown();
          }
        });
    latch.await(5, TimeUnit.SECONDS);

    Glide.get(context)
        .getRegistry()
        .prepend(
            Object.class,
            InputStream.class,
            new FakeModelLoader<>(ResourceIds.raw.transparent_gif));

    Bitmap bitmap =
        concurrency.get(
            GlideApp.with(context)
                .asBitmap()
                .set(Downsampler.ALLOW_HARDWARE_CONFIG, true)
                .format(DecodeFormat.PREFER_ARGB_8888)
                .load(new Object())
                .submit());
    assertThat(bitmap).isNotNull();
    assertThat(bitmap.getConfig()).isEqualTo(Bitmap.Config.HARDWARE);
  }

  @Test
  public void loadTransparentGifResource_withNoOtherLoaders_fromBytes_decodesResource() {
    byte[] data = getBytes(ResourceIds.raw.transparent_gif);
    Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(data).submit());
    assertThat(bitmap).isNotNull();
  }

  @Test
  public void loadOpaqueGifResource_withNoOtherLoaders_decodesResource() {
    Glide.get(context)
        .getRegistry()
        .prepend(
            Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.opaque_gif));

    Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(new Object()).submit());
    assertThat(bitmap).isNotNull();
  }

  @Test
  public void loadOpaqueGifResource_asBytes_decodesResource() {
    byte[] data = getBytes(ResourceIds.raw.opaque_gif);
    Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(data).submit());
    assertThat(bitmap).isNotNull();
  }

  @Test
  public void loadOpaqueGifResource_asHardware_withNoOtherLoaders_decodesResource() {
    assumeTrue(
        "Hardware Bitmaps are only supported on P+",
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);

    Glide.get(context)
        .getRegistry()
        .prepend(
            Object.class, InputStream.class, new FakeModelLoader<>(ResourceIds.raw.opaque_gif));

    Bitmap bitmap =
        concurrency.get(
            GlideApp.with(context)
                .asBitmap()
                // Allow HARDWARE Bitmaps.
                .format(DecodeFormat.PREFER_ARGB_8888)
                .load(new Object())
                .submit());
    assertThat(bitmap).isNotNull();
  }

  private byte[] getBytes(int resourceId) {
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    InputStream is = null;
    try {
      is = context.getResources().openRawResource(resourceId);
      byte[] buffer = new byte[1024 * 1024];
      int read;
      while ((read = is.read(buffer)) != -1) {
        os.write(buffer, 0, read);
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    } finally {
      if (is != null) {
        try {
          is.close();
        } catch (IOException e) {
          // Ignored;
        }
      }
    }

    return os.toByteArray();
  }

  private class FakeModelLoader<T>
      implements ModelLoader<T, InputStream>, ModelLoaderFactory<T, InputStream> {

    private final int resourceId;

    FakeModelLoader(int resourceId) {
      this.resourceId = resourceId;
    }

    @androidx.annotation.Nullable
    @Override
    public LoadData<InputStream> buildLoadData(
        @NonNull Object o, int width, int height, @NonNull Options options) {
      return new LoadData<>(new ObjectKey(o), new Fetcher());
    }

    @Override
    public boolean handles(@NonNull Object o) {
      return true;
    }

    @NonNull
    @Override
    public ModelLoader<T, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
      return this;
    }

    @Override
    public void teardown() {}

    private final class Fetcher implements DataFetcher<InputStream> {
      private InputStream inputStream;

      @Override
      public void loadData(
          @NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
        inputStream = getInputStreamForResource(context, resourceId);
        callback.onDataReady(inputStream);
      }

      private InputStream getInputStreamForResource(Context context, @DrawableRes int resourceId) {
        Resources resources = context.getResources();
        try {
          Uri parse =
              Uri.parse(
                  String.format(
                      Locale.US,
                      "%s://%s/%s/%s",
                      ContentResolver.SCHEME_ANDROID_RESOURCE,
                      resources.getResourcePackageName(resourceId),
                      resources.getResourceTypeName(resourceId),
                      resources.getResourceEntryName(resourceId)));
          return context.getContentResolver().openInputStream(parse);
        } catch (Resources.NotFoundException | FileNotFoundException e) {
          throw new IllegalArgumentException("Resource ID " + resourceId + " not found", e);
        }
      }

      @Override
      public void cleanup() {
        InputStream local = inputStream;
        if (local != null) {
          try {
            local.close();
          } catch (IOException e) {
            // Ignored.
          }
        }
      }

      @Override
      public void cancel() {
        // Do nothing.
      }

      @NonNull
      @Override
      public Class<InputStream> getDataClass() {
        return InputStream.class;
      }

      @NonNull
      @Override
      public DataSource getDataSource() {
        return DataSource.LOCAL;
      }
    }
  }
}