Back to Repositories

Testing EBML Element Processing Implementation in SmartTube

This test suite validates the DefaultEbmlReader implementation for handling EBML (Extensible Binary Meta Language) elements in the ExoPlayer library. It thoroughly tests parsing and processing of various EBML element types including master elements, integers, strings, floats and binary data.

Test Coverage Overview

The test suite provides comprehensive coverage of EBML element parsing functionality:

  • Master element handling with nested content
  • Unsigned integer parsing with edge cases
  • String element processing with padding and empty values
  • Float element parsing for both 4-byte and 8-byte formats
  • Binary data extraction and validation

Implementation Analysis

The testing approach uses a TestProcessor implementation to verify EBML element processing:

The tests utilize mock ExtractorInput objects to simulate EBML data streams and verify correct parsing behavior. Each test case validates specific element types through the EbmlProcessor interface, with careful attention to element identification, size calculation, and content extraction.

Technical Details

Testing infrastructure includes:

  • JUnit 4 with AndroidJUnit4 runner
  • Custom FakeExtractorInput for controlled input simulation
  • TestUtil for byte array creation
  • Truth assertion library for verification
  • Custom TestProcessor class for event recording

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test cases for each element type
  • Comprehensive edge case coverage
  • Clear test method naming
  • Helper methods for test data creation
  • Structured event verification system

yuliskov/smarttube

exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java

            
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.android.exoplayer2.extractor.mkv;

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

import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Tests {@link DefaultEbmlReader}. */
@RunWith(AndroidJUnit4.class)
public class DefaultEbmlReaderTest {

  @Test
  public void testMasterElement() throws IOException, InterruptedException {
    ExtractorInput input = createTestInput(0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x85, 0x81, 0x01);
    TestProcessor expected = new TestProcessor();
    expected.startMasterElement(TestProcessor.ID_EBML, 5, 4);
    expected.integerElement(TestProcessor.ID_DOC_TYPE_READ_VERSION, 1);
    expected.endMasterElement(TestProcessor.ID_EBML);
    assertEvents(input, expected.events);
  }

  @Test
  public void testMasterElementEmpty() throws IOException, InterruptedException {
    ExtractorInput input = createTestInput(0x18, 0x53, 0x80, 0x67, 0x80);
    TestProcessor expected = new TestProcessor();
    expected.startMasterElement(TestProcessor.ID_SEGMENT, 5, 0);
    expected.endMasterElement(TestProcessor.ID_SEGMENT);
    assertEvents(input, expected.events);
  }

  @Test
  public void testUnsignedIntegerElement() throws IOException, InterruptedException {
    // 0xFE is chosen because for signed integers it should be interpreted as -2
    ExtractorInput input = createTestInput(0x42, 0xF7, 0x81, 0xFE);
    TestProcessor expected = new TestProcessor();
    expected.integerElement(TestProcessor.ID_EBML_READ_VERSION, 254);
    assertEvents(input, expected.events);
  }

  @Test
  public void testUnsignedIntegerElementLarge() throws IOException, InterruptedException {
    ExtractorInput input =
        createTestInput(0x42, 0xF7, 0x88, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);
    TestProcessor expected = new TestProcessor();
    expected.integerElement(TestProcessor.ID_EBML_READ_VERSION, Long.MAX_VALUE);
    assertEvents(input, expected.events);
  }

  @Test
  public void testUnsignedIntegerElementTooLargeBecomesNegative()
      throws IOException, InterruptedException {
    ExtractorInput input =
        createTestInput(0x42, 0xF7, 0x88, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);
    TestProcessor expected = new TestProcessor();
    expected.integerElement(TestProcessor.ID_EBML_READ_VERSION, -1);
    assertEvents(input, expected.events);
  }

  @Test
  public void testStringElement() throws IOException, InterruptedException {
    ExtractorInput input = createTestInput(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x31, 0x32, 0x33);
    TestProcessor expected = new TestProcessor();
    expected.stringElement(TestProcessor.ID_DOC_TYPE, "Abc123");
    assertEvents(input, expected.events);
  }

  @Test
  public void testStringElementWithZeroPadding() throws IOException, InterruptedException {
    ExtractorInput input = createTestInput(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x00, 0x00, 0x00);
    TestProcessor expected = new TestProcessor();
    expected.stringElement(TestProcessor.ID_DOC_TYPE, "Abc");
    assertEvents(input, expected.events);
  }

  @Test
  public void testStringElementEmpty() throws IOException, InterruptedException {
    ExtractorInput input = createTestInput(0x42, 0x82, 0x80);
    TestProcessor expected = new TestProcessor();
    expected.stringElement(TestProcessor.ID_DOC_TYPE, "");
    assertEvents(input, expected.events);
  }

  @Test
  public void testFloatElementFourBytes() throws IOException, InterruptedException {
    ExtractorInput input =
        createTestInput(0x44, 0x89, 0x84, 0x3F, 0x80, 0x00, 0x00);
    TestProcessor expected = new TestProcessor();
    expected.floatElement(TestProcessor.ID_DURATION, 1.0);
    assertEvents(input, expected.events);
  }

  @Test
  public void testFloatElementEightBytes() throws IOException, InterruptedException {
    ExtractorInput input =
        createTestInput(0x44, 0x89, 0x88, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
    TestProcessor expected = new TestProcessor();
    expected.floatElement(TestProcessor.ID_DURATION, -2.0);
    assertEvents(input, expected.events);
  }

  @Test
  public void testBinaryElement() throws IOException, InterruptedException {
    ExtractorInput input =
        createTestInput(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08);
    TestProcessor expected = new TestProcessor();
    expected.binaryElement(
        TestProcessor.ID_SIMPLE_BLOCK,
        8,
        createTestInput(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08));
    assertEvents(input, expected.events);
  }

  private static void assertEvents(ExtractorInput input, List<String> expectedEvents)
      throws IOException, InterruptedException {
    DefaultEbmlReader reader = new DefaultEbmlReader();
    TestProcessor output = new TestProcessor();
    reader.init(output);

    // We expect the number of successful reads to equal the number of expected events.
    for (int i = 0; i < expectedEvents.size(); i++) {
      assertThat(reader.read(input)).isTrue();
    }
    // The next read should be unsuccessful.
    assertThat(reader.read(input)).isFalse();
    // Check that we really did get to the end of input.
    assertThat(input.readFully(new byte[1], 0, 1, true)).isFalse();

    assertThat(output.events).containsExactlyElementsIn(expectedEvents).inOrder();
  }

  /**
   * Helper to build an {@link ExtractorInput} from byte data.
   *
   * @param data Zero or more integers with values between {@code 0x00} and {@code 0xFF}.
   * @return An {@link ExtractorInput} from which the data can be read.
   */
  private static ExtractorInput createTestInput(int... data) {
    return new FakeExtractorInput.Builder()
        .setData(TestUtil.createByteArray(data))
        .setSimulateUnknownLength(true)
        .build();
  }

  /** An {@link EbmlProcessor} that records each event callback. */
  private static final class TestProcessor implements EbmlProcessor {

    // Element IDs
    private static final int ID_EBML = 0x1A45DFA3;
    private static final int ID_EBML_READ_VERSION = 0x42F7;
    private static final int ID_DOC_TYPE = 0x4282;
    private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;

    private static final int ID_SEGMENT = 0x18538067;
    private static final int ID_DURATION = 0x4489;
    private static final int ID_SIMPLE_BLOCK = 0xA3;

    private final List<String> events = new ArrayList<>();

    @Override
    @EbmlProcessor.ElementType
    public int getElementType(int id) {
      switch (id) {
        case ID_EBML:
        case ID_SEGMENT:
          return EbmlProcessor.ELEMENT_TYPE_MASTER;
        case ID_EBML_READ_VERSION:
        case ID_DOC_TYPE_READ_VERSION:
          return EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT;
        case ID_DOC_TYPE:
          return EbmlProcessor.ELEMENT_TYPE_STRING;
        case ID_SIMPLE_BLOCK:
          return EbmlProcessor.ELEMENT_TYPE_BINARY;
        case ID_DURATION:
          return EbmlProcessor.ELEMENT_TYPE_FLOAT;
        default:
          return EbmlProcessor.ELEMENT_TYPE_UNKNOWN;
      }
    }

    @Override
    public boolean isLevel1Element(int id) {
      return false;
    }

    @Override
    public void startMasterElement(int id, long contentPosition, long contentSize) {
      events.add(formatEvent(id, "start contentPosition=" + contentPosition
          + " contentSize=" + contentSize));
    }

    @Override
    public void endMasterElement(int id) {
      events.add(formatEvent(id, "end"));
    }

    @Override
    public void integerElement(int id, long value) {
      events.add(formatEvent(id, "integer=" + value));
    }

    @Override
    public void floatElement(int id, double value) {
      events.add(formatEvent(id, "float=" + value));
    }

    @Override
    public void stringElement(int id, String value) {
      events.add(formatEvent(id, "string=" + value));
    }

    @Override
    public void binaryElement(int id, int contentSize, ExtractorInput input)
        throws IOException, InterruptedException {
      byte[] bytes = new byte[contentSize];
      input.readFully(bytes, 0, contentSize);
      events.add(formatEvent(id, "bytes=" + Arrays.toString(bytes)));
    }

    private static String formatEvent(int id, String event) {
      return "[" + Integer.toHexString(id) + "] " + event;
    }

  }

}