From 448a6203999a0a8f7b573e7b2aeb65f1cf403b92 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 17 Dec 2018 14:40:23 +0300 Subject: [PATCH] Add TestSpan module --- markwon-test-span/build.gradle | 24 ++++ .../src/main/AndroidManifest.xml | 1 + .../java/ru/noties/markwon/test/TestSpan.java | 124 +++++++++++++++++ .../noties/markwon/test/TestSpanDocument.java | 60 ++++++++ .../markwon/test/TestSpanEnumerator.java | 34 +++++ .../noties/markwon/test/TestSpanMatcher.java | 131 ++++++++++++++++++ .../ru/noties/markwon/test/TestSpanSpan.java | 60 ++++++++ .../ru/noties/markwon/test/TestSpanText.java | 47 +++++++ settings.gradle | 1 + 9 files changed, 482 insertions(+) create mode 100644 markwon-test-span/build.gradle create mode 100644 markwon-test-span/src/main/AndroidManifest.xml create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanDocument.java create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanEnumerator.java create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanText.java diff --git a/markwon-test-span/build.gradle b/markwon-test-span/build.gradle new file mode 100644 index 00000000..cba39010 --- /dev/null +++ b/markwon-test-span/build.gradle @@ -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'] + } +} diff --git a/markwon-test-span/src/main/AndroidManifest.xml b/markwon-test-span/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c2e8a267 --- /dev/null +++ b/markwon-test-span/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java new file mode 100644 index 00000000..bc2bd524 --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java @@ -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.emptyMap(), children); + } + + @NonNull + public static TestSpan.Span span(@NonNull String name, @NonNull Map 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 children(TestSpan... children) { + final int length = children.length; + final List list; + if (length == 0) { + list = Collections.emptyList(); + } else if (length == 1) { + list = Collections.singletonList(children[0]); + } else { + final List spans = new ArrayList<>(length); + Collections.addAll(spans, children); + list = Collections.unmodifiableList(spans); + } + return list; + } + + @NonNull + public static Map 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 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 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 arguments(); + + @NonNull + @Override + public abstract List children(); + } + + // package-private constructor + TestSpan() { + } +} diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanDocument.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanDocument.java new file mode 100644 index 00000000..ef876311 --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanDocument.java @@ -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 children; + + TestSpanDocument(@NonNull List children) { + this.children = children; + } + + @NonNull + @Override + public List 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(); + } +} diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanEnumerator.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanEnumerator.java new file mode 100644 index 00000000..1c2172db --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanEnumerator.java @@ -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; + } +} diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java new file mode 100644 index 00000000..e9bf190b --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java @@ -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() { + @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() { + } +} diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java new file mode 100644 index 00000000..5dc60acd --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java @@ -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 children; + private final Map arguments; + + public TestSpanSpan( + @NonNull String name, + @NonNull List children, + @NonNull Map arguments) { + this.name = name; + this.children = children; + this.arguments = arguments; + } + + @NonNull + @Override + public String name() { + return name; + } + + @NonNull + @Override + public Map arguments() { + return arguments; + } + + @NonNull + @Override + public List 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; + } +} diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanText.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanText.java new file mode 100644 index 00000000..8a34a410 --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanText.java @@ -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 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(); + } +} diff --git a/settings.gradle b/settings.gradle index 10d8564b..d5f56c45 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,5 +10,6 @@ include ':app', ':markwon-syntax-highlight', ':markwon-html', ':markwon-view', + ':markwon-test-span', ':sample-custom-extension', ':sample-latex-math'