Back to Repositories

Testing Multi-Request Image Loading Behavior in Glide

This test suite validates the behavior of multiple concurrent image loading requests in Glide, focusing on thumbnail generation and request state management. The tests ensure proper handling of request lifecycles and resource loading completion states.

Test Coverage Overview

The test suite provides comprehensive coverage of Glide’s multi-request functionality, particularly focusing on thumbnail operations and request state management.

  • Tests primary request completion states during thumbnail processing
  • Validates request lifecycle states during failed loads
  • Covers edge cases in concurrent request handling
  • Tests integration with Glide’s request listener system

Implementation Analysis

The implementation employs JUnit4 with AndroidJUnit4 runner for Android-specific testing scenarios. It uses atomic operations and countdown latches to manage concurrent request states and synchronization.

  • Uses ConcurrencyHelper for main thread operations
  • Implements custom RequestListener for state verification
  • Utilizes custom Target implementation for resource handling

Technical Details

  • Testing Framework: JUnit4 with AndroidJUnit4 runner
  • Concurrency Tools: CountDownLatch, AtomicBoolean
  • Mock Data: Dynamic bitmap generation
  • Configuration: Custom GlideBuilder with controlled thread count
  • Test Rules: TearDownGlide, ModelGeneratorRule, TemporaryFolder

Best Practices Demonstrated

The test suite exemplifies robust testing practices for concurrent image loading operations.

  • Proper resource cleanup with TearDownGlide rule
  • Controlled concurrent execution environment
  • Isolated test cases with clear assertions
  • Effective use of temporary file management
  • Thread-safe state verification

bumptech/glide

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

            
package com.bumptech.glide;

import static com.google.common.truth.Truth.assertThat;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.load.engine.executor.GlideExecutor;
import com.bumptech.glide.request.Request;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.request.transition.Transition;
import com.bumptech.glide.test.ModelGeneratorRule;
import com.bumptech.glide.testutil.ConcurrencyHelper;
import com.bumptech.glide.testutil.TearDownGlide;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class MultiRequestTest {
  private final Context context = ApplicationProvider.getApplicationContext();
  @Rule public final TearDownGlide tearDownGlide = new TearDownGlide();
  @Rule public final ModelGeneratorRule modelGeneratorRule = new ModelGeneratorRule();
  @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
  private final ConcurrencyHelper concurrency = new ConcurrencyHelper();

  @Test
  public void thumbnail_onResourceReady_forPrimary_isComplete_whenRequestListenerIsCalled()
      throws IOException, InterruptedException {

    // Make sure the requests complete in the same order
    Glide.init(
        context,
        new GlideBuilder()
            .setSourceExecutor(GlideExecutor.newSourceBuilder().setThreadCount(1).build()));

    AtomicBoolean isPrimaryRequestComplete = new AtomicBoolean(false);
    CountDownLatch countDownLatch = new CountDownLatch(1);

    RequestBuilder<Drawable> request =
        Glide.with(context)
            .load(newImageFile())
            .thumbnail(Glide.with(context).load(newImageFile()))
            .listener(
                new RequestListener<>() {
                  @Override
                  public boolean onLoadFailed(
                      @Nullable GlideException e,
                      Object model,
                      @NonNull Target<Drawable> target,
                      boolean isFirstResource) {
                    return false;
                  }

                  @Override
                  public boolean onResourceReady(
                      @NonNull Drawable resource,
                      @NonNull Object model,
                      Target<Drawable> target,
                      @NonNull DataSource dataSource,
                      boolean isFirstResource) {
                    isPrimaryRequestComplete.set(target.getRequest().isComplete());
                    countDownLatch.countDown();
                    return false;
                  }
                });
    concurrency.runOnMainThread(() -> request.into(new DoNothingTarget()));

    assertThat(countDownLatch.await(3, TimeUnit.SECONDS)).isTrue();
    assertThat(isPrimaryRequestComplete.get()).isTrue();
  }

  @Test
  public void thumbnail_onLoadFailed_forPrimary_isNotRunningOrComplete_whenRequestListenerIsCalled()
      throws IOException, InterruptedException {

    // Make sure the requests complete in the same order
    Glide.init(
        context,
        new GlideBuilder()
            .setSourceExecutor(GlideExecutor.newSourceBuilder().setThreadCount(1).build()));

    AtomicBoolean isNeitherRunningNorComplete = new AtomicBoolean(false);
    CountDownLatch countDownLatch = new CountDownLatch(1);

    int missingResourceId = 123;
    RequestBuilder<Drawable> requestBuilder =
        Glide.with(context)
            .load(missingResourceId)
            .thumbnail(Glide.with(context).load(newImageFile()))
            .listener(
                new RequestListener<>() {
                  @Override
                  public boolean onLoadFailed(
                      @Nullable GlideException e,
                      Object model,
                      @NonNull Target<Drawable> target,
                      boolean isFirstResource) {
                    Request request = target.getRequest();
                    isNeitherRunningNorComplete.set(!request.isComplete() && !request.isRunning());
                    countDownLatch.countDown();
                    return false;
                  }

                  @Override
                  public boolean onResourceReady(
                      @NonNull Drawable resource,
                      @NonNull Object model,
                      Target<Drawable> target,
                      @NonNull DataSource dataSource,
                      boolean isFirstResource) {
                    return false;
                  }
                });
    concurrency.runOnMainThread(() -> requestBuilder.into(new DoNothingTarget()));

    assertThat(countDownLatch.await(3, TimeUnit.SECONDS)).isTrue();
    assertThat(isNeitherRunningNorComplete.get()).isTrue();
  }

  private File newImageFile() throws IOException {
    Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.RED);
    File result = temporaryFolder.newFile();
    try (OutputStream os = new BufferedOutputStream(new FileOutputStream(result))) {
      bitmap.compress(CompressFormat.JPEG, 75, os);
    }
    return result;
  }

  // We don't store or do anything with the resource, so we don't need to do anything to release it
  // in onLoadCleared.
  private static final class DoNothingTarget extends CustomTarget<Drawable> {
    @Override
    public void onResourceReady(
        @NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {}

    @Override
    public void onLoadCleared(@Nullable Drawable placeholder) {}
  }
}