Create Truth SpannedSubject for style assertions

This will be used in subtitle decoding tests

I followed this guide: https://truth.dev/extension.html

PiperOrigin-RevId: 284787298
This commit is contained in:
ibaker 2019-12-10 17:38:49 +00:00 committed by Oliver Woodman
parent c1573106fa
commit 7263699c2e
3 changed files with 385 additions and 0 deletions

View file

@ -22,6 +22,9 @@
* Add support for attaching DRM sessions to clear content in the demo app.
* UI: Exclude `DefaultTimeBar` region from system gesture detection
([#6685](https://github.com/google/ExoPlayer/issues/6685)).
* Add `SpannedSubject` to testutils, for assertions on
[Span-styled text]( https://developer.android.com/guide/topics/text/spans)
(e.g. subtitles).
### 2.11.0 (2019-12-11) ###

View file

@ -0,0 +1,237 @@
/*
* Copyright (C) 2019 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.testutil.truth;
import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Fact.simpleFact;
import static com.google.common.truth.Truth.assertAbout;
import android.graphics.Typeface;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import androidx.annotation.Nullable;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
import java.util.ArrayList;
import java.util.List;
/** A Truth {@link Subject} for assertions on {@link Spanned} instances containing text styling. */
// TODO: add support for more Spans i.e. all those used in com.google.android.exoplayer2.text.
public final class SpannedSubject extends Subject {
@Nullable private final Spanned actual;
private SpannedSubject(FailureMetadata metadata, @Nullable Spanned actual) {
super(metadata, actual);
this.actual = actual;
}
public static Factory<SpannedSubject, Spanned> spanned() {
return SpannedSubject::new;
}
/**
* Convenience method to create a SpannedSubject.
*
* <p>Can be statically imported alongside other Truth {@code assertThat} methods.
*
* @param spanned The subject under test.
* @return An object for conducting assertions on the subject.
*/
public static SpannedSubject assertThat(@Nullable Spanned spanned) {
return assertAbout(spanned()).that(spanned);
}
/**
* Checks that the subject has an italic span from {@code startIndex} to {@code endIndex}.
*
* @param startIndex The start of the expected span.
* @param endIndex The end of the expected span.
* @param flags The flags of the expected span. See constants on {@link Spanned} for more
* information.
*/
public void hasItalicSpan(int startIndex, int endIndex, int flags) {
hasStyleSpan(startIndex, endIndex, flags, Typeface.ITALIC);
}
/**
* Checks that the subject has a bold span from {@code startIndex} to {@code endIndex}.
*
* @param startIndex The start of the expected span.
* @param endIndex The end of the expected span.
* @param flags The flags of the expected span. See constants on {@link Spanned} for more
* information.
*/
public void hasBoldSpan(int startIndex, int endIndex, int flags) {
hasStyleSpan(startIndex, endIndex, flags, Typeface.BOLD);
}
private void hasStyleSpan(int startIndex, int endIndex, int flags, int style) {
if (actual == null) {
failWithoutActual(simpleFact("Spanned must not be null"));
return;
}
for (StyleSpan span : findMatchingSpans(startIndex, endIndex, flags, StyleSpan.class)) {
if (span.getStyle() == style) {
return;
}
}
failWithExpectedSpan(
startIndex,
endIndex,
flags,
new StyleSpan(style),
actual.toString().substring(startIndex, endIndex));
}
/**
* Checks that the subject has bold and italic styling from {@code startIndex} to {@code
* endIndex}.
*
* <p>This can either be:
*
* <ul>
* <li>A single {@link StyleSpan} with {@code span.getStyle() == Typeface.BOLD_ITALIC}.
* <li>Two {@link StyleSpan}s, one with {@code span.getStyle() == Typeface.BOLD} and the other
* with {@code span.getStyle() == Typeface.ITALIC}.
* </ul>
*
* @param startIndex The start of the expected span.
* @param endIndex The end of the expected span.
* @param flags The flags of the expected span. See constants on {@link Spanned} for more
* information.
*/
public void hasBoldItalicSpan(int startIndex, int endIndex, int flags) {
if (actual == null) {
failWithoutActual(simpleFact("Spanned must not be null"));
return;
}
List<Integer> styles = new ArrayList<>();
for (StyleSpan span : findMatchingSpans(startIndex, endIndex, flags, StyleSpan.class)) {
styles.add(span.getStyle());
}
if (styles.size() == 1 && styles.contains(Typeface.BOLD_ITALIC)) {
return;
} else if (styles.size() == 2
&& styles.contains(Typeface.BOLD)
&& styles.contains(Typeface.ITALIC)) {
return;
}
String spannedSubstring = actual.toString().substring(startIndex, endIndex);
String boldSpan =
spanToString(startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD), spannedSubstring);
String italicSpan =
spanToString(startIndex, endIndex, flags, new StyleSpan(Typeface.ITALIC), spannedSubstring);
String boldItalicSpan =
spanToString(
startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD_ITALIC), spannedSubstring);
failWithoutActual(
simpleFact("No matching span found"),
fact("in text", actual.toString()),
fact("expected either", boldItalicSpan),
fact("or both", boldSpan + "\n" + italicSpan),
fact("but found", actualSpansString()));
}
/**
* Checks that the subject has an underline span from {@code startIndex} to {@code endIndex}.
*
* @param startIndex The start of the expected span.
* @param endIndex The end of the expected span.
* @param flags The flags of the expected span. See constants on {@link Spanned} for more
* information.
*/
public void hasUnderlineSpan(int startIndex, int endIndex, int flags) {
if (actual == null) {
failWithoutActual(simpleFact("Spanned must not be null"));
return;
}
List<UnderlineSpan> underlineSpans =
findMatchingSpans(startIndex, endIndex, flags, UnderlineSpan.class);
if (underlineSpans.size() == 1) {
return;
}
failWithExpectedSpan(
startIndex,
endIndex,
flags,
new UnderlineSpan(),
actual.toString().substring(startIndex, endIndex));
}
private <T> List<T> findMatchingSpans(
int startIndex, int endIndex, int flags, Class<T> spanClazz) {
List<T> spans = new ArrayList<>();
for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) {
if (actual.getSpanStart(span) == startIndex
&& actual.getSpanEnd(span) == endIndex
&& actual.getSpanFlags(span) == flags) {
spans.add(span);
}
}
return spans;
}
private void failWithExpectedSpan(
int start, int end, int flags, Object span, String spannedSubstring) {
failWithoutActual(
simpleFact("No matching span found"),
fact("in text", actual),
fact("expected", spanToString(start, end, flags, span, spannedSubstring)),
fact("but found", actualSpansString()));
}
private String actualSpansString() {
List<String> actualSpanStrings = new ArrayList<>();
for (Object span : actual.getSpans(0, actual.length(), /* type= */ Object.class)) {
actualSpanStrings.add(spanToString(span, actual));
}
return TextUtils.join("\n", actualSpanStrings);
}
private static String spanToString(Object span, Spanned spanned) {
int spanStart = spanned.getSpanStart(span);
int spanEnd = spanned.getSpanEnd(span);
return spanToString(
spanStart,
spanEnd,
spanned.getSpanFlags(span),
span,
spanned.toString().substring(spanStart, spanEnd));
}
private static String spanToString(
int start, int end, int flags, Object span, String spannedSubstring) {
String suffix;
if (span instanceof StyleSpan) {
suffix = "\tstyle=" + ((StyleSpan) span).getStyle();
} else {
suffix = "";
}
return String.format(
"start=%s\tend=%s\tflags=%s\ttype=%s\tsubstring='%s'%s",
start, end, flags, span.getClass().getSimpleName(), spannedSubstring, suffix);
}
}

View file

@ -0,0 +1,145 @@
/*
* Copyright (C) 2019 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.testutil.truth;
import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat;
import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.spanned;
import static com.google.common.truth.ExpectFailure.assertThat;
import static com.google.common.truth.ExpectFailure.expectFailureAbout;
import android.graphics.Typeface;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.truth.ExpectFailure;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link SpannedSubject}. */
@RunWith(AndroidJUnit4.class)
public class SpannedSubjectTest {
@Test
public void italicSpan_success() {
SpannableString spannable = SpannableString.valueOf("test with italic section");
int start = "test with ".length();
int end = start + "italic".length();
spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
assertThat(spannable).hasItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
@Test
public void italicSpan_mismatchedFlags() {
SpannableString spannable = SpannableString.valueOf("test with italic section");
int start = "test with ".length();
int end = start + "italic".length();
spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
AssertionError failure =
expectFailure(
whenTesting ->
whenTesting
.that(spannable)
.hasItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE));
assertThat(failure).factKeys().contains("No matching span found");
assertThat(failure).factValue("in text").isEqualTo(spannable.toString());
assertThat(failure).factValue("expected").contains("flags=" + Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
assertThat(failure)
.factValue("but found")
.contains("flags=" + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
@Test
public void italicSpan_null() {
AssertionError failure =
expectFailure(
whenTesting ->
whenTesting.that(null).hasItalicSpan(0, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE));
assertThat(failure).factKeys().containsExactly("Spanned must not be null");
}
@Test
public void boldSpan_success() {
SpannableString spannable = SpannableString.valueOf("test with bold section");
int start = "test with ".length();
int end = start + "bold".length();
spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
assertThat(spannable).hasBoldSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
@Test
public void boldItalicSpan_withOneSpan() {
SpannableString spannable = SpannableString.valueOf("test with bold & italic section");
int start = "test with ".length();
int end = start + "bold & italic".length();
spannable.setSpan(
new StyleSpan(Typeface.BOLD_ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
assertThat(spannable).hasBoldItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
@Test
public void boldItalicSpan_withTwoSpans() {
SpannableString spannable = SpannableString.valueOf("test with bold & italic section");
int start = "test with ".length();
int end = start + "bold & italic".length();
spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
assertThat(spannable).hasBoldItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
@Test
public void boldItalicSpan_mismatchedStartIndex() {
SpannableString spannable = SpannableString.valueOf("test with bold & italic section");
int start = "test with ".length();
int end = start + "bold & italic".length();
spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
int incorrectStart = start - 2;
AssertionError expected =
expectFailure(
whenTesting ->
whenTesting
.that(spannable)
.hasBoldItalicSpan(incorrectStart, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE));
assertThat(expected).factValue("expected either").contains("start=" + incorrectStart);
assertThat(expected).factValue("but found").contains("start=" + start);
}
@Test
public void underlineSpan_success() {
SpannableString spannable = SpannableString.valueOf("test with underlined section");
int start = "test with ".length();
int end = start + "underlined".length();
spannable.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
assertThat(spannable).hasUnderlineSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
private static AssertionError expectFailure(
ExpectFailure.SimpleSubjectBuilderCallback<SpannedSubject, Spanned> callback) {
return expectFailureAbout(spanned(), callback);
}
}