Add TestSpan module
This commit is contained in:
parent
5a18aa3a01
commit
448a620399
24
markwon-test-span/build.gradle
Normal file
24
markwon-test-span/build.gradle
Normal 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']
|
||||
}
|
||||
}
|
1
markwon-test-span/src/main/AndroidManifest.xml
Normal file
1
markwon-test-span/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="ru.noties.markwon.test" />
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -10,5 +10,6 @@ include ':app',
|
||||
':markwon-syntax-highlight',
|
||||
':markwon-html',
|
||||
':markwon-view',
|
||||
':markwon-test-span',
|
||||
':sample-custom-extension',
|
||||
':sample-latex-math'
|
||||
|
Loading…
x
Reference in New Issue
Block a user