Back to Repositories

Validating GIF Header Parser Implementation in Bumptech Glide

This test suite validates the GifHeaderParser component in Glide’s GIF decoder, focusing on parsing and validating GIF file headers, frame delays, color tables, and animation properties. The tests ensure robust handling of various GIF format specifications and edge cases.

Test Coverage Overview

The test suite provides comprehensive coverage of GIF header parsing functionality including:

  • GIF format validation and error handling
  • Header and Logical Screen Descriptor parsing
  • Frame delay calculations and validation
  • Color table handling (both global and local)
  • Multi-frame animation detection
  • Netscape iteration count parsing

Implementation Analysis

The testing approach uses JUnit 4 with systematic validation of GIF binary structures. Tests leverage ByteBuffer for precise byte-level control and custom test utilities for generating GIF data structures. The implementation follows a methodical pattern of arranging test data, executing the parser, and asserting expected header properties.

Technical Details

Key technical components include:

  • JUnit 4 test framework
  • Custom GifBytesTestUtil for test data generation
  • ByteBuffer for binary data manipulation
  • Test resource files for real GIF scenarios
  • setUp method initializing fresh parser instance

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test cases with clear single-responsibility focus
  • Comprehensive edge case coverage
  • Proper test setup and resource management
  • Meaningful test names describing scenarios
  • Effective use of test utilities and helper methods

bumptech/glide

third_party/gif_decoder/src/test/java/com/bumptech/glide/gifdecoder/GifHeaderParserTest.java

            
package com.bumptech.glide.gifdecoder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import com.bumptech.glide.gifdecoder.test.GifBytesTestUtil;
import com.bumptech.glide.testutil.TestUtil;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests for {@link com.bumptech.glide.gifdecoder.GifHeaderParser}.
 */
@RunWith(JUnit4.class)
public class GifHeaderParserTest {
  private GifHeaderParser parser;

  @Before
  public void setUp() {
    parser = new GifHeaderParser();
  }

  @Test
  public void testReturnsHeaderWithFormatErrorIfDoesNotStartWithGifHeader() {
    parser.setData("wrong_header".getBytes());
    GifHeader result = parser.parseHeader();
    assertEquals(GifDecoder.STATUS_FORMAT_ERROR, result.status);
  }

  @Test
  public void testCanReadValidHeaderAndLSD() {
    final int width = 10;
    final int height = 20;
    ByteBuffer buffer =
        ByteBuffer.allocate(GifBytesTestUtil.HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
    GifBytesTestUtil.writeHeaderAndLsd(buffer, width, height, false, 0);

    parser.setData(buffer.array());
    GifHeader header = parser.parseHeader();
    assertEquals(width, header.width);
    assertEquals(height, header.height);
    assertFalse(header.gctFlag);
    // 2^(1+0) == 2^1 == 2.
    assertEquals(2, header.gctSize);
    assertEquals(0, header.bgIndex);
    assertEquals(0, header.pixelAspect);
  }

  @Test
  public void testCanParseHeaderOfTestImageWithoutGraphicalExtension() throws IOException {
    byte[] data =
        TestUtil.resourceToBytes(getClass(), "gif_without_graphical_control_extension.gif");
    parser.setData(data);
    GifHeader header = parser.parseHeader();
    assertEquals(1, header.frameCount);
    assertNotNull(header.frames.get(0));
    assertEquals(GifDecoder.STATUS_OK, header.status);
  }

  @Test
  public void testCanReadNetscapeIterationCountIfNetscapeIterationCountIsZero() throws IOException {
    byte[] data = TestUtil.resourceToBytes(getClass(), "gif_netscape_iteration_0.gif");
    parser.setData(data);
    GifHeader header = parser.parseHeader();
    assertEquals(GifHeader.NETSCAPE_LOOP_COUNT_FOREVER, header.loopCount);
  }

  @Test
  public void testCanReadNetscapeIterationCountIfNetscapeIterationCountIs_1() throws IOException {
    byte[] data = TestUtil.resourceToBytes(getClass(), "gif_netscape_iteration_1.gif");
    parser.setData(data);
    GifHeader header = parser.parseHeader();
    assertEquals(1, header.loopCount);
  }

  @Test
  public void testCanReadNetscapeIterationCountIfNetscapeIterationCountIs_0x0F()
      throws IOException {
    byte[] data = TestUtil.resourceToBytes(getClass(), "gif_netscape_iteration_255.gif");
    parser.setData(data);
    GifHeader header = parser.parseHeader();
    assertEquals(255, header.loopCount);
  }

  @Test
  public void testCanReadNetscapeIterationCountIfNetscapeIterationCountIs_0x10()
      throws IOException {
    byte[] data = TestUtil.resourceToBytes(getClass(), "gif_netscape_iteration_256.gif");
    parser.setData(data);
    GifHeader header = parser.parseHeader();
    assertEquals(256, header.loopCount);
  }

  @Test
  public void testCanReadNetscapeIterationCountIfNetscapeIterationCountIs_0xFF()
      throws IOException {
    byte[] data = TestUtil.resourceToBytes(getClass(), "gif_netscape_iteration_65535.gif");
    parser.setData(data);
    GifHeader header = parser.parseHeader();
    assertEquals(65535, header.loopCount);
  }

  @Test
  public void testLoopCountReturnsMinusOneWithoutNetscapeIterationCount()
          throws IOException {
    byte[] data = TestUtil.resourceToBytes(getClass(), "gif_without_netscape_iteration.gif");
    parser.setData(data);
    GifHeader header = parser.parseHeader();
    assertEquals(GifHeader.NETSCAPE_LOOP_COUNT_DOES_NOT_EXIST, header.loopCount);
  }

  @Test
  public void testCanReadImageDescriptorWithoutGraphicalExtension() {
    final int lzwMinCodeSize = 2;
    ByteBuffer buffer = ByteBuffer.allocate(
        GifBytesTestUtil.HEADER_LENGTH + GifBytesTestUtil.IMAGE_DESCRIPTOR_LENGTH + GifBytesTestUtil
            .getImageDataSize()).order(ByteOrder.LITTLE_ENDIAN);
    GifBytesTestUtil.writeHeaderAndLsd(buffer, 1, 1, false, 0);
    GifBytesTestUtil.writeImageDescriptor(buffer, 0, 0, 1, 1, false /*hasLct*/, 0);
    GifBytesTestUtil.writeFakeImageData(buffer, lzwMinCodeSize);

    parser.setData(buffer.array());
    GifHeader header = parser.parseHeader();
    assertEquals(1, header.width);
    assertEquals(1, header.height);
    assertEquals(1, header.frameCount);
    assertNotNull(header.frames.get(0));
  }

  private static ByteBuffer writeHeaderWithGceAndFrameDelay(short frameDelay) {
    final int lzwMinCodeSize = 2;
    ByteBuffer buffer = ByteBuffer.allocate(
        GifBytesTestUtil.HEADER_LENGTH + GifBytesTestUtil.GRAPHICS_CONTROL_EXTENSION_LENGTH
            + GifBytesTestUtil.IMAGE_DESCRIPTOR_LENGTH + GifBytesTestUtil
            .getImageDataSize()).order(ByteOrder.LITTLE_ENDIAN);
    GifBytesTestUtil.writeHeaderAndLsd(buffer, 1, 1, false, 0);
    GifBytesTestUtil.writeGraphicsControlExtension(buffer, frameDelay);
    GifBytesTestUtil.writeImageDescriptor(buffer, 0, 0, 1, 1, false /*hasLct*/, 0);
    GifBytesTestUtil.writeFakeImageData(buffer, lzwMinCodeSize);
    return buffer;
  }

  @Test
  public void testCanParseFrameDelay() {
    final short frameDelay = 50;
    ByteBuffer buffer = writeHeaderWithGceAndFrameDelay(frameDelay);

    parser.setData(buffer.array());
    GifHeader header = parser.parseHeader();
    GifFrame frame = header.frames.get(0);

    // Convert delay in 100ths of a second to ms.
    assertEquals(frameDelay * 10, frame.delay);
  }

  @Test
  public void testSetsDefaultFrameDelayIfFrameDelayIsZero() {
    ByteBuffer buffer = writeHeaderWithGceAndFrameDelay((short) 0);

    parser.setData(buffer.array());
    GifHeader header = parser.parseHeader();
    GifFrame frame = header.frames.get(0);

    // Convert delay in 100ths of a second to ms.
    assertEquals(GifHeaderParser.DEFAULT_FRAME_DELAY * 10, frame.delay);
  }

  @Test
  public void testSetsDefaultFrameDelayIfFrameDelayIsLessThanMinimum() {
    final short frameDelay = GifHeaderParser.MIN_FRAME_DELAY - 1;
    ByteBuffer buffer = writeHeaderWithGceAndFrameDelay(frameDelay);

    parser.setData(buffer.array());
    GifHeader header = parser.parseHeader();
    GifFrame frame = header.frames.get(0);

    // Convert delay in 100ths of a second to ms.
    assertEquals(GifHeaderParser.DEFAULT_FRAME_DELAY * 10, frame.delay);
  }

  @Test
  public void testObeysFrameDelayIfFrameDelayIsAtMinimum() {
    final short frameDelay = GifHeaderParser.MIN_FRAME_DELAY;
    ByteBuffer buffer = writeHeaderWithGceAndFrameDelay(frameDelay);

    parser.setData(buffer.array());
    GifHeader header = parser.parseHeader();
    GifFrame frame = header.frames.get(0);

    // Convert delay in 100ths of a second to ms.
    assertEquals(frameDelay * 10, frame.delay);
  }

  @Test
  public void testSetsFrameLocalColorTableToNullIfNoColorTable() {
    final int lzwMinCodeSize = 2;
    ByteBuffer buffer = ByteBuffer.allocate(
        GifBytesTestUtil.HEADER_LENGTH + GifBytesTestUtil.IMAGE_DESCRIPTOR_LENGTH + GifBytesTestUtil
            .getImageDataSize()).order(ByteOrder.LITTLE_ENDIAN);
    GifBytesTestUtil.writeHeaderAndLsd(buffer, 1, 1, false, 0);
    GifBytesTestUtil.writeImageDescriptor(buffer, 0, 0, 1, 1, false /*hasLct*/, 0);
    GifBytesTestUtil.writeFakeImageData(buffer, lzwMinCodeSize);

    parser.setData(buffer.array());
    GifHeader header = parser.parseHeader();
    assertEquals(1, header.width);
    assertEquals(1, header.height);
    assertEquals(1, header.frameCount);
    assertNotNull(header.frames.get(0));
    assertNull(header.frames.get(0).lct);
  }

  @Test
  public void testSetsFrameLocalColorTableIfHasColorTable() {
    final int lzwMinCodeSize = 2;
    final int numColors = 4;
    ByteBuffer buffer = ByteBuffer.allocate(
        GifBytesTestUtil.HEADER_LENGTH + GifBytesTestUtil.IMAGE_DESCRIPTOR_LENGTH + GifBytesTestUtil
            .getImageDataSize() + GifBytesTestUtil.getColorTableLength(numColors))
        .order(ByteOrder.LITTLE_ENDIAN);
    GifBytesTestUtil.writeHeaderAndLsd(buffer, 1, 1, false, 0);
    GifBytesTestUtil.writeImageDescriptor(buffer, 0, 0, 1, 1, true /*hasLct*/, numColors);
    GifBytesTestUtil.writeColorTable(buffer, numColors);
    GifBytesTestUtil.writeFakeImageData(buffer, 2);

    parser.setData(buffer.array());
    GifHeader header = parser.parseHeader();
    assertEquals(1, header.width);
    assertEquals(1, header.height);
    assertEquals(1, header.frameCount);
    assertNotNull(header.frames.get(0));

    GifFrame frame = header.frames.get(0);
    assertNotNull(frame.lct);
  }

  @Test
  public void testCanParseMultipleFrames() {
    final int lzwMinCodeSize = 2;
    final int expectedFrames = 3;

    final int frameSize = GifBytesTestUtil.IMAGE_DESCRIPTOR_LENGTH + GifBytesTestUtil
        .getImageDataSize();
    ByteBuffer buffer =
        ByteBuffer.allocate(GifBytesTestUtil.HEADER_LENGTH + expectedFrames * frameSize)
            .order(ByteOrder.LITTLE_ENDIAN);

    GifBytesTestUtil.writeHeaderAndLsd(buffer, 1, 1, false, 0);
    for (int i = 0; i < expectedFrames; i++) {
      GifBytesTestUtil.writeImageDescriptor(buffer, 0, 0, 1, 1, false /*hasLct*/, 0 /*numColors*/);
      GifBytesTestUtil.writeFakeImageData(buffer, 2);
    }

    parser.setData(buffer.array());
    GifHeader header = parser.parseHeader();
    assertEquals(expectedFrames, header.frameCount);
    assertEquals(expectedFrames, header.frames.size());
  }

  @Test
  public void testIsAnimatedMultipleFrames() {
    final int lzwMinCodeSize = 2;
    final int numFrames = 3;

    final int frameSize =
        GifBytesTestUtil.IMAGE_DESCRIPTOR_LENGTH
            + GifBytesTestUtil.getImageDataSize();
    ByteBuffer buffer =
        ByteBuffer.allocate(GifBytesTestUtil.HEADER_LENGTH + numFrames * frameSize)
            .order(ByteOrder.LITTLE_ENDIAN);

    GifBytesTestUtil.writeHeaderAndLsd(buffer, 1, 1, false, 0);
    for (int i = 0; i < numFrames; i++) {
      GifBytesTestUtil.writeImageDescriptor(buffer, 0, 0, 1, 1, false /*hasLct*/, 0 /*numColors*/);
      GifBytesTestUtil.writeFakeImageData(buffer, 2);
    }

    parser.setData(buffer.array());
    assertTrue(parser.isAnimated());
  }

  @Test
  public void testIsNotAnimatedOneFrame() {
    final int lzwMinCodeSize = 2;

    final int frameSize =
        GifBytesTestUtil.IMAGE_DESCRIPTOR_LENGTH
            + GifBytesTestUtil.getImageDataSize();

    ByteBuffer buffer =
        ByteBuffer.allocate(GifBytesTestUtil.HEADER_LENGTH + frameSize)
            .order(ByteOrder.LITTLE_ENDIAN);

    GifBytesTestUtil.writeHeaderAndLsd(buffer, 1, 1, false, 0);
    GifBytesTestUtil.writeImageDescriptor(buffer, 0, 0, 1, 1, false /*hasLct*/, 0 /*numColors*/);
    GifBytesTestUtil.writeFakeImageData(buffer, 2);

    parser.setData(buffer.array());
    assertFalse(parser.isAnimated());
  }


  @Test(expected = IllegalStateException.class)
  public void testThrowsIfParseHeaderCalledBeforeSetData() {
    GifHeaderParser parser = new GifHeaderParser();
    parser.parseHeader();
  }
}