Back to Repositories

Testing WebVTT CSS Parser Implementation in SmartTube

This test suite validates the CSS parsing functionality in ExoPlayer’s WebVTT text rendering system. It ensures proper handling of CSS styles for WebVTT subtitles, including color parsing, style block processing, and selector specificity calculations.

Test Coverage Overview

The test suite provides comprehensive coverage of the CssParser class functionality, including:

  • Whitespace and comment handling in CSS blocks
  • Input limit detection and processing
  • Style block parsing with multiple properties
  • RGBA color expression parsing
  • Token parsing and validation
  • Style specificity scoring system

Implementation Analysis

The testing approach uses JUnit with AndroidJUnit4 runner for Android compatibility. Tests are structured to validate individual parser components through targeted test methods, using a combination of input strings and expected WebvttCssStyle objects for verification.

The implementation employs assertion-based testing with Truth assertions for precise validation of parsing results.

Technical Details

Testing tools and configuration:

  • JUnit test framework with AndroidJUnit4 runner
  • Google Truth assertion library
  • ParsableByteArray for byte-level parsing
  • Custom utility methods for validation
  • WebvttCssStyle class for style representation

Best Practices Demonstrated

The test suite demonstrates several testing best practices:

  • Thorough setup with @Before initialization
  • Isolated test cases for specific functionality
  • Comprehensive edge case coverage
  • Clear test method naming
  • Helper methods for common validation tasks
  • Structured input/output validation patterns

yuliskov/smarttube

exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.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.text.webvtt;

import static com.google.android.exoplayer2.text.webvtt.CssParser.parseNextToken;
import static com.google.common.truth.Truth.assertThat;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Unit test for {@link CssParser}. */
@RunWith(AndroidJUnit4.class)
public final class CssParserTest {

  private CssParser parser;

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

  @Test
  public void testSkipWhitespacesAndComments() {
    // Skip only whitespaces
    String skipOnlyWhitespaces = " \t\r\n\f End of skip\n /*  */";
    assertSkipsToEndOfSkip("End of skip", skipOnlyWhitespaces);

    // Skip only comments.
    String skipOnlyComments = "/*A comment***//*/It even has spaces in it*/End of skip";
    assertSkipsToEndOfSkip("End of skip", skipOnlyComments);

    // Skip interleaved.
    String skipInterleaved = " /* We have comments and */\t\n/* whitespaces*/ End of skip";
    assertSkipsToEndOfSkip("End of skip", skipInterleaved);

    // Skip nothing.
    String skipNothing = "End of skip\n \t \r";
    assertSkipsToEndOfSkip("End of skip", skipNothing);

    // Skip everything.
    String skipEverything = "\t/* Comment */\n\r/* And another */";
    assertSkipsToEndOfSkip(null, skipEverything);
  }

  @Test
  public void testGetInputLimit() {
    // \r After 3 lines.
    String threeLinesThen3Cr = "One Line\nThen other\rAnd finally\r\r\r";
    assertInputLimit("", threeLinesThen3Cr);

    // \r\r After 3 lines
    String threeLinesThen2Cr = "One Line\nThen other\r\nAnd finally\r\r";
    assertInputLimit(null, threeLinesThen2Cr);

    // \n\n After 3 lines.
    String threeLinesThen2Lf = "One Line\nThen other\r\nAnd finally\n\nFinal\n\n\nLine";
    assertInputLimit("Final", threeLinesThen2Lf);

    // \r\n\n After 3 lines.
    String threeLinesThenCr2Lf = " \n \r\n \r\n\nFinal\n\n\nLine";
    assertInputLimit("Final", threeLinesThenCr2Lf);

    // Limit immediately.
    String immediateEmptyLine = "\nLine\nEnd";
    assertInputLimit("Line", immediateEmptyLine);

    // Empty string.
    assertInputLimit(null, "");
  }

  @Test
  public void testParseMethodSimpleInput() {
    WebvttCssStyle expectedStyle = new WebvttCssStyle();
    String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }";
    expectedStyle.setFontColor(0xFF000000);
    expectedStyle.setBackgroundColor(0xFFFFEFD5);
    assertParserProduces(styleBlock1, expectedStyle);

    String styleBlock2 = " ::cue { color : black }\n\n::cue { color : invalid }";
    expectedStyle = new WebvttCssStyle();
    expectedStyle.setFontColor(0xFF000000);
    assertParserProduces(styleBlock2, expectedStyle);

    String styleBlock3 = "::cue {\n background-color\n:#00fFFe}";
    expectedStyle = new WebvttCssStyle();
    expectedStyle.setBackgroundColor(0xFF00FFFE);
    assertParserProduces(styleBlock3, expectedStyle);
  }

  @Test
  public void testParseMethodMultipleRulesInBlockInput() {
    String styleBlock =
        "::cue {\n background-color\n:#00fFFe}      \n::cue {\n background-color\n:#00000000}\n";
    WebvttCssStyle expectedStyle = new WebvttCssStyle();
    expectedStyle.setBackgroundColor(0xFF00FFFE);
    WebvttCssStyle secondExpectedStyle = new WebvttCssStyle();
    secondExpectedStyle.setBackgroundColor(0x000000);
    assertParserProduces(styleBlock, expectedStyle, secondExpectedStyle);
  }

  @Test
  public void testMultiplePropertiesInBlock() {
    String styleBlock = "::cue(#id){text-decoration:underline; background-color:green;"
        + "color:red; font-family:Courier; font-weight:bold}";
    WebvttCssStyle expectedStyle = new WebvttCssStyle();
    expectedStyle.setTargetId("id");
    expectedStyle.setUnderline(true);
    expectedStyle.setBackgroundColor(0xFF008000);
    expectedStyle.setFontColor(0xFFFF0000);
    expectedStyle.setFontFamily("courier");
    expectedStyle.setBold(true);

    assertParserProduces(styleBlock, expectedStyle);
  }

  @Test
  public void testRgbaColorExpression() {
    String styleBlock = "::cue(#rgb){background-color: rgba(\n10/* Ugly color */,11\t, 12\n,.1);"
        + "color:rgb(1,1,\n1)}";
    WebvttCssStyle expectedStyle = new WebvttCssStyle();
    expectedStyle.setTargetId("rgb");
    expectedStyle.setBackgroundColor(0x190A0B0C);
    expectedStyle.setFontColor(0xFF010101);

    assertParserProduces(styleBlock, expectedStyle);
  }

  @Test
  public void testGetNextToken() {
    String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n";
    ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput));
    StringBuilder builder = new StringBuilder();
    assertThat(parseNextToken(input, builder)).isEqualTo("lorem");
    assertThat(parseNextToken(input, builder)).isEqualTo(":");
    assertThat(parseNextToken(input, builder)).isEqualTo("ipsum");
    assertThat(parseNextToken(input, builder)).isEqualTo("{");
    assertThat(parseNextToken(input, builder)).isEqualTo("dolor");
    assertThat(parseNextToken(input, builder)).isEqualTo("}");
    assertThat(parseNextToken(input, builder)).isEqualTo("#sit");
    assertThat(parseNextToken(input, builder)).isEqualTo(",");
    assertThat(parseNextToken(input, builder)).isEqualTo("amet");
    assertThat(parseNextToken(input, builder)).isEqualTo(";");
    assertThat(parseNextToken(input, builder)).isEqualTo("lorem");
    assertThat(parseNextToken(input, builder)).isEqualTo(":");
    assertThat(parseNextToken(input, builder)).isEqualTo("ipsum");
    assertThat(parseNextToken(input, builder)).isEqualTo("dolor");
    assertThat(parseNextToken(input, builder)).isEqualTo("(");
    assertThat(parseNextToken(input, builder)).isEqualTo("(");
    assertThat(parseNextToken(input, builder)).isEqualTo(")");
    assertThat(parseNextToken(input, builder)).isEqualTo(")");
    assertThat(parseNextToken(input, builder)).isNull();
  }

  @Test
  public void testStyleScoreSystem() {
    WebvttCssStyle style = new WebvttCssStyle();
    // Universal selector.
    assertThat(style.getSpecificityScore("", "", new String[0], "")).isEqualTo(1);
    // Class match without tag match.
    style.setTargetClasses(new String[] { "class1", "class2"});
    assertThat(style.getSpecificityScore("", "", new String[]{"class1", "class2", "class3"},
        "")).isEqualTo(8);
    // Class and tag match
    style.setTargetTagName("b");
    assertThat(style.getSpecificityScore("", "b",
        new String[]{"class1", "class2", "class3"}, "")).isEqualTo(10);
    // Class insufficiency.
    assertThat(style.getSpecificityScore("", "b", new String[]{"class1", "class"}, ""))
        .isEqualTo(0);
    // Voice, classes and tag match.
    style.setTargetVoice("Manuel Cráneo");
    assertThat(style.getSpecificityScore("", "b",
        new String[]{"class1", "class2", "class3"}, "Manuel Cráneo")).isEqualTo(14);
    // Voice mismatch.
    assertThat(style.getSpecificityScore(null, "b",
        new String[]{"class1", "class2", "class3"}, "Manuel Craneo")).isEqualTo(0);
    // Id, voice, classes and tag match.
    style.setTargetId("id");
    assertThat(style.getSpecificityScore("id", "b",
        new String[]{"class1", "class2", "class3"}, "Manuel Cráneo")).isEqualTo(0x40000000 + 14);
    // Id mismatch.
    assertThat(style.getSpecificityScore("id1", "b",
        new String[]{"class1", "class2", "class3"}, "")).isEqualTo(0);
  }

  // Utility methods.

  private void assertSkipsToEndOfSkip(String expectedLine, String s) {
    ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s));
    CssParser.skipWhitespaceAndComments(input);
    assertThat(input.readLine()).isEqualTo(expectedLine);
  }

  private void assertInputLimit(String expectedLine, String s) {
    ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s));
    CssParser.skipStyleBlock(input);
    assertThat(input.readLine()).isEqualTo(expectedLine);
  }

  private void assertParserProduces(String styleBlock, WebvttCssStyle... expectedStyles) {
    ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(styleBlock));
    List<WebvttCssStyle> styles = parser.parseBlock(input);
    assertThat(styles.size()).isEqualTo(expectedStyles.length);
    for (int i = 0; i < expectedStyles.length; i++) {
      WebvttCssStyle expected = expectedStyles[i];
      WebvttCssStyle actualElem = styles.get(i);
      assertThat(actualElem.hasBackgroundColor()).isEqualTo(expected.hasBackgroundColor());
      if (expected.hasBackgroundColor()) {
        assertThat(actualElem.getBackgroundColor()).isEqualTo(expected.getBackgroundColor());
      }
      assertThat(actualElem.hasFontColor()).isEqualTo(expected.hasFontColor());
      if (expected.hasFontColor()) {
        assertThat(actualElem.getFontColor()).isEqualTo(expected.getFontColor());
      }
      assertThat(actualElem.getFontFamily()).isEqualTo(expected.getFontFamily());
      assertThat(actualElem.getFontSize()).isEqualTo(expected.getFontSize());
      assertThat(actualElem.getFontSizeUnit()).isEqualTo(expected.getFontSizeUnit());
      assertThat(actualElem.getStyle()).isEqualTo(expected.getStyle());
      assertThat(actualElem.isLinethrough()).isEqualTo(expected.isLinethrough());
      assertThat(actualElem.isUnderline()).isEqualTo(expected.isUnderline());
      assertThat(actualElem.getTextAlign()).isEqualTo(expected.getTextAlign());
    }
  }

}