Add TestSpan module

This commit is contained in:
Dimitry Ivanov 2018-12-17 14:40:23 +03:00
parent 5a18aa3a01
commit 448a620399
9 changed files with 482 additions and 0 deletions

View File

@ -0,0 +1,24 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion config['compile-sdk']
buildToolsVersion config['build-tools']
defaultConfig {
minSdkVersion config['min-sdk']
targetSdkVersion config['target-sdk']
versionCode 1
versionName version
}
}
dependencies {
api deps['support-annotations']
deps['test'].with {
api it['junit']
api it['ix-java']
}
}

View File

@ -0,0 +1 @@
<manifest package="ru.noties.markwon.test" />

View File

@ -0,0 +1,124 @@
package ru.noties.markwon.test;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Utility class to validate spannable content
*
* @since 3.0.0
*/
public abstract class TestSpan {
@NonNull
public static TestSpan.Document document(TestSpan... children) {
return new TestSpanDocument(children(children));
}
@NonNull
public static TestSpan.Span span(@NonNull String name, TestSpan... children) {
return span(name, Collections.<String, Object>emptyMap(), children);
}
@NonNull
public static TestSpan.Span span(@NonNull String name, @NonNull Map<String, Object> arguments, TestSpan... children) {
return new TestSpanSpan(name, children(children), arguments);
}
@NonNull
public static TestSpan.Text text(@NonNull String literal) {
return new TestSpanText(literal);
}
@NonNull
public static List<TestSpan> children(TestSpan... children) {
final int length = children.length;
final List<TestSpan> list;
if (length == 0) {
list = Collections.emptyList();
} else if (length == 1) {
list = Collections.singletonList(children[0]);
} else {
final List<TestSpan> spans = new ArrayList<>(length);
Collections.addAll(spans, children);
list = Collections.unmodifiableList(spans);
}
return list;
}
@NonNull
public static Map<String, Object> args(Object... args) {
final int length = args.length;
if (length == 0) {
return Collections.emptyMap();
}
// validate that length is even (k=v)
if ((length % 2) != 0) {
throw new IllegalStateException("Supplied key-values array must contain " +
"even number of arguments");
}
final Map<String, Object> map = new HashMap<>(length / 2 + 1);
String key;
Object value;
for (int i = 0; i < length; i += 2) {
// possible class-cast exception
key = (String) args[i];
value = args[i + 1];
map.put(key, value);
}
return Collections.unmodifiableMap(map);
}
@NonNull
public abstract List<TestSpan> children();
@Override
public abstract int hashCode();
@Override
public abstract boolean equals(Object o);
public static abstract class Document extends TestSpan {
@NonNull
public abstract String wholeText();
}
public static abstract class Text extends TestSpan {
@NonNull
public abstract String literal();
public abstract int length();
}
public static abstract class Span extends TestSpan {
@NonNull
public abstract String name();
@NonNull
public abstract Map<String, Object> arguments();
@NonNull
@Override
public abstract List<TestSpan> children();
}
// package-private constructor
TestSpan() {
}
}

View File

@ -0,0 +1,60 @@
package ru.noties.markwon.test;
import android.support.annotation.NonNull;
import java.util.List;
class TestSpanDocument extends TestSpan.Document {
private static void fillWholeText(@NonNull StringBuilder builder, @NonNull TestSpan span) {
if (span instanceof Text) {
builder.append(((Text) span).literal());
} else if (span instanceof Span) {
for (TestSpan child : span.children()) {
fillWholeText(builder, child);
}
} else {
throw new IllegalStateException("Unexpected state. Found unexpected TestSpan " +
"object of type `" + span.getClass().getName() + "`");
}
}
private final List<TestSpan> children;
TestSpanDocument(@NonNull List<TestSpan> children) {
this.children = children;
}
@NonNull
@Override
public List<TestSpan> children() {
return children;
}
@NonNull
@Override
public String wholeText() {
final StringBuilder builder = new StringBuilder();
for (TestSpan child : children) {
fillWholeText(builder, child);
}
return builder.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestSpanDocument that = (TestSpanDocument) o;
return children.equals(that.children);
}
@Override
public int hashCode() {
return children.hashCode();
}
}

View File

@ -0,0 +1,34 @@
package ru.noties.markwon.test;
import android.support.annotation.NonNull;
public class TestSpanEnumerator {
public interface Listener {
void onNext(int start, int end, @NonNull TestSpan span);
}
public void enumerate(@NonNull TestSpan.Document document, @NonNull Listener listener) {
visit(0, document, listener);
}
private int visit(int start, @NonNull TestSpan span, @NonNull Listener listener) {
if (span instanceof TestSpan.Text) {
final int end = start + ((TestSpan.Text) span).length();
listener.onNext(start, end, span);
return end;
}
// yeah, we will need end... and from recursive call also -> children can have text inside
int s = start;
for (TestSpan child : span.children()) {
s = visit(s, child, listener);
}
listener.onNext(start, s, span);
return s;
}
}

View File

@ -0,0 +1,131 @@
package ru.noties.markwon.test;
import android.support.annotation.NonNull;
import android.text.Spanned;
import junit.framework.Assert;
import junit.framework.ComparisonFailure;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
import ix.Ix;
import ix.IxPredicate;
public abstract class TestSpanMatcher {
public static void matches(@NonNull final Spanned spanned, @NonNull TestSpan.Document document) {
// assert number for spans
// assert raw text
final TestSpanEnumerator enumerator = new TestSpanEnumerator();
// keep track of total spans encountered
final AtomicInteger counter = new AtomicInteger();
enumerator.enumerate(document, new TestSpanEnumerator.Listener() {
@Override
public void onNext(final int start, final int end, @NonNull TestSpan span) {
if (span instanceof TestSpan.Document) {
TestSpanMatcher.documentMatches(spanned, (TestSpan.Document) span);
} else if (span instanceof TestSpan.Span) {
// increment span count so after enumeration we match total number of spans
counter.incrementAndGet();
TestSpanMatcher.spanMatches(spanned, start, end, (TestSpan.Span) span);
} else if (span instanceof TestSpan.Text) {
TestSpanMatcher.textMatches(spanned, start, end, (TestSpan.Text) span);
} else {
// in case we add a new type
throw new IllegalStateException("Unexpected type of a TestSpan: `"
+ span.getClass().getName() + "`, " + span);
}
}
});
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
Assert.assertEquals("Total spans count", counter.get(), spans.length);
}
public static void documentMatches(
@NonNull Spanned spanned,
@NonNull TestSpan.Document document) {
// match full text
final String expected = document.wholeText();
final String actual = spanned.toString();
if (!expected.equals(actual)) {
throw new ComparisonFailure(
"Document text mismatch",
expected,
actual);
}
}
public static void spanMatches(
@NonNull final Spanned spanned,
final int start,
final int end,
@NonNull TestSpan.Span expected) {
// when queried multiple spans can be returned (for example if one span
// wraps another one. so [0 1 [2 3] 4 5] where [] represents start/end of
// a span of same type, when queried for spans at 2-3 position, both will be returned
final TestSpan.Span actual = Ix.fromArray(spanned.getSpans(start, end, expected.getClass()))
.filter(new IxPredicate<TestSpan.Span>() {
@Override
public boolean test(TestSpan.Span span) {
return start == spanned.getSpanStart(span)
&& end == spanned.getSpanEnd(span);
}
})
.first(null);
if (!expected.equals(actual)) {
final String expectedSpan = expected.arguments().isEmpty()
? expected.name()
: expected.name() + ": " + expected.arguments();
final String actualSpan;
if (actual == null) {
actualSpan = "null";
} else {
actualSpan = actual.arguments().isEmpty()
? actual.name()
: actual.name() + ": " + actual.arguments();
}
throw new AssertionError(
String.format(Locale.US, "Expected span{%s} at {start: %d, end: %d}, found: %s",
expectedSpan, start, end, actualSpan));
}
}
public static void textMatches(
@NonNull Spanned spanned,
int start,
int end,
@NonNull TestSpan.Text text) {
final String expected = text.literal();
final String actual = spanned.subSequence(start, end).toString();
if (!expected.equals(actual)) {
throw new ComparisonFailure(
String.format(Locale.US, "Text mismatch at {start: %d, end: %d}", start, end),
expected,
actual
);
}
}
private TestSpanMatcher() {
}
}

View File

@ -0,0 +1,60 @@
package ru.noties.markwon.test;
import android.support.annotation.NonNull;
import java.util.List;
import java.util.Map;
class TestSpanSpan extends TestSpan.Span {
private final String name;
private final List<TestSpan> children;
private final Map<String, Object> arguments;
public TestSpanSpan(
@NonNull String name,
@NonNull List<TestSpan> children,
@NonNull Map<String, Object> arguments) {
this.name = name;
this.children = children;
this.arguments = arguments;
}
@NonNull
@Override
public String name() {
return name;
}
@NonNull
@Override
public Map<String, Object> arguments() {
return arguments;
}
@NonNull
@Override
public List<TestSpan> children() {
return children;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestSpanSpan that = (TestSpanSpan) o;
if (!name.equals(that.name)) return false;
if (!children.equals(that.children)) return false;
return arguments.equals(that.arguments);
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + children.hashCode();
result = 31 * result + arguments.hashCode();
return result;
}
}

View File

@ -0,0 +1,47 @@
package ru.noties.markwon.test;
import android.support.annotation.NonNull;
import java.util.Collections;
import java.util.List;
class TestSpanText extends TestSpan.Text {
private final String literal;
TestSpanText(@NonNull String literal) {
this.literal = literal;
}
@NonNull
@Override
public String literal() {
return literal;
}
@Override
public int length() {
return literal.length();
}
@NonNull
@Override
public List<TestSpan> children() {
return Collections.emptyList();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestSpanText that = (TestSpanText) o;
return literal.equals(that.literal);
}
@Override
public int hashCode() {
return literal.hashCode();
}
}

View File

@ -10,5 +10,6 @@ include ':app',
':markwon-syntax-highlight',
':markwon-html',
':markwon-view',
':markwon-test-span',
':sample-custom-extension',
':sample-latex-math'