Redefined test format

This commit is contained in:
Dimitry Ivanov 2018-08-24 17:09:23 +03:00
parent db4be0eee3
commit caf7f99335
9 changed files with 307 additions and 175 deletions

View File

@ -9,7 +9,6 @@ import org.junit.runner.RunWith;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.Collection;
import ix.Ix;
@ -54,67 +53,83 @@ public class SpannableMarkdownVisitorTest {
node.accept(visitor);
final SpannableStringBuilder stringBuilder = builder.spannableStringBuilder();
final String raw = stringBuilder.toString();
System.out.printf("%n%s%n", stringBuilder);
int index = 0;
int lastIndex = 0;
for (TestEntry entry : data.output()) {
final String expected = entry.text();
final boolean isText = "text".equals(entry.name());
final int start;
final int end;
if (isText) {
start = lastIndex;
end = start + expected.length();
index = lastIndex = end;
} else {
start = raw.indexOf(expected, index);
if (start < 0) {
throw new AssertionError(String.format("Cannot find `%s` starting at index: %d, raw: %n###%n%s%n###",
expected, start, raw
));
}
end = start + expected.length();
lastIndex = Math.max(end, lastIndex);
}
if (!expected.equals(raw.substring(start, end))) {
throw new AssertionError(String.format("Expected: `%s`, actual: `%s`, start: %d, raw: %n###%n%s%n###",
expected, raw.substring(start, end), start, raw
));
}
final Object[] spans = stringBuilder.getSpans(start, end, Object.class);
final int length = spans != null ? spans.length : 0;
if (isText) {
// validate no spans
assertEquals(Arrays.toString(spans), 0, length);
} else {
assertTrue(length > 0);
final Object span = Ix.fromArray(spans)
.filter(new IxPredicate<Object>() {
@Override
public boolean test(Object o) {
return start == stringBuilder.getSpanStart(o)
&& end == stringBuilder.getSpanEnd(o);
}
})
.first(null);
assertNotNull(span);
assertTrue(span instanceof TestSpan);
final TestSpan testSpan = (TestSpan) span;
assertEquals(entry.name(), testSpan.name());
assertEquals(entry.attributes(), testSpan.attributes());
}
for (TestNode testNode : data.output()) {
index = validate(stringBuilder, index, testNode);
}
}
private int validate(@NonNull SpannableStringBuilder builder, int index, @NonNull TestNode node) {
if (node.isText()) {
final String text;
{
final String content = node.getAsText().text();
// code is a special case as we wrap it around non-breakable spaces
final TestNode parent = node.parent();
if (parent != null) {
final TestNode.Span span = parent.getAsSpan();
if (TestSpan.CODE.equals(span.name())) {
text = "\u00a0" + content + "\u00a0";
} else if (TestSpan.CODE_BLOCK.equals(span.name())) {
text = "\u00a0\n" + content + "\n\u00a0";
} else {
text = content;
}
} else {
text = content;
}
}
assertEquals(text, builder.subSequence(index, index + text.length()).toString());
return index + text.length();
}
final TestNode.Span span = node.getAsSpan();
int out = index;
for (TestNode child : span.children()) {
out = validate(builder, out, child);
}
final String info = node.toString();
// we can possibly have parent spans here, should filter them
final Object[] spans = builder.getSpans(index, out, Object.class);
assertTrue(info, spans != null);
final TestSpan testSpan = Ix.fromArray(spans)
.filter(new IxPredicate<Object>() {
@Override
public boolean test(Object o) {
return o instanceof TestSpan;
}
})
.cast(TestSpan.class)
.filter(new IxPredicate<TestSpan>() {
@Override
public boolean test(TestSpan testSpan) {
return span.name().equals(testSpan.name());
}
})
.first(null);
assertNotNull(info, testSpan);
assertEquals(info, span.name(), testSpan.name());
assertEquals(info, span.attributes(), testSpan.attributes());
return out;
}
@NonNull
private SpannableConfiguration configuration(@NonNull TestConfig config) {
@ -126,7 +141,5 @@ public class SpannableMarkdownVisitorTest {
.linkResolver(mock(LinkResolverDef.class))
.factory(factory)
.build();
// return configuration;
}
}

View File

@ -10,13 +10,13 @@ class TestData {
private final String description;
private final String input;
private final TestConfig config;
private final List<TestEntry> output;
private final List<TestNode> output;
TestData(
@Nullable String description,
@NonNull String input,
@NonNull TestConfig config,
@NonNull List<TestEntry> output) {
@NonNull List<TestNode> output) {
this.description = description;
this.input = input;
this.config = config;
@ -39,7 +39,7 @@ class TestData {
}
@NonNull
public List<TestEntry> output() {
public List<TestNode> output() {
return output;
}
}

View File

@ -79,6 +79,9 @@ abstract class TestDataReader {
static class Reader {
private static final String ATTRS = "attrs";
private static final String TEXT = "text";
private final String file;
Reader(@NonNull String file) {
@ -120,95 +123,99 @@ abstract class TestDataReader {
final String input = jsonObject.get("input").getAsString();
if (TextUtils.isEmpty(input)) {
throw new RuntimeException(String.format("Test case file `%s` is missing input parameter", file));
throw new RuntimeException(String.format("Test case file `%s` is missing " +
"input parameter", file));
}
final List<TestEntry> testSpans = testEntries(jsonObject.get("output").getAsJsonArray());
if (testSpans.size() == 0) {
throw new RuntimeException(String.format("Test case file `%s` has no output specified", file));
final TestConfig testConfig = testConfig(jsonObject.get("config"));
final List<TestNode> testNodes = testNodes(jsonObject.get("output").getAsJsonArray());
if (testNodes.size() == 0) {
throw new RuntimeException(String.format("Test case file `%s` has no " +
"output specified", file));
}
return new TestData(
description,
input,
testConfig(jsonObject.get("config")),
testSpans
testConfig,
testNodes
);
}
// todo: rename TestNode -> it's not a node... but... what?
@NonNull
private List<TestNode> testNodes(@NonNull JsonArray array) {
return testNodes(null, array);
}
@NonNull
private List<TestEntry> testEntries(@NonNull JsonArray array) {
private List<TestNode> testNodes(@Nullable TestNode parent, @NonNull JsonArray array) {
// all items are contained in this array
// key is the name of an item
// item can be defined like this:
// link: "text-content-of-the-link"
// link:
// attributes:
// - href: "my-href-attribute"
// text: "and here is the text content"
// text node can be found only at root level
// an item in array is a JsonObject
final int length = array.size();
// it can be "b": "bold" -> means Span(name="b", children=[Text(bold)]
// or b:
// - text: "bold" -> which is the same as above
final List<TestEntry> testSpans = new ArrayList<>(length);
// it can additionally contain "attrs" key which is the attributes
// b:
// - text: "bold"
// attrs:
// href: "my-href"
for (int i = 0; i < length; i++) {
final int size = array.size();
final JsonElement element = array.get(i);
final List<TestNode> testNodes = new ArrayList<>(size);
if (element.isJsonObject()) {
for (int i = 0; i < size; i++) {
final JsonObject object = element.getAsJsonObject();
final JsonObject object = array.get(i).getAsJsonObject();
// objects must have exactly 1 key: name of the node
// it's value can be different: JsonPrimitive (String) or an JsonObject (with attributes and text)
final String name = object.keySet().iterator().next();
final JsonElement value = object.get(name);
final String text;
final Map<String, String> attributes;
if (value.isJsonObject()) {
final JsonObject valueObject = value.getAsJsonObject();
text = valueObject.get("text").getAsString();
attributes = attributes(valueObject.get("attrs"));
String name = null;
Map<String, String> attributes = null;
for (String key : object.keySet()) {
if (ATTRS.equals(key)) {
attributes = attributes(object.get(key));
} else if (name == null) {
name = key;
} else {
// we allow only 2 keys: span and/or attributes and no more
throw new RuntimeException("Unexpected key in object: " + object);
}
}
final JsonPrimitive primitive;
if (name == null) {
throw new RuntimeException("Object is missing tag name: " + object);
}
if (value.isJsonPrimitive()) {
primitive = value.getAsJsonPrimitive();
} else {
primitive = null;
}
if (attributes == null) {
attributes = Collections.emptyMap();
}
if (primitive == null
|| !primitive.isString()) {
throw new RuntimeException(String.format("Unexpected json element at index: `%d` in array: `%s`",
i, array
));
}
final JsonElement element = object.get(name);
text = primitive.getAsString();
attributes = Collections.emptyMap();
if (TEXT.equals(name)) {
testNodes.add(new TestNode.Text(parent, element.getAsString()));
} else {
final List<TestNode> children = new ArrayList<>(1);
final TestNode.Span span = new TestNode.Span(parent, name, children, attributes);
// if it's primitive string -> just append text node
if (element.isJsonPrimitive()) {
children.add(new TestNode.Text(span, element.getAsString()));
} else if (element.isJsonArray()) {
children.addAll(testNodes(span, element.getAsJsonArray()));
} else {
throw new RuntimeException("Unexpected element: " + object);
}
testSpans.add(new TestEntry(name, text, attributes));
} else {
throw new RuntimeException(String.format("Unexpected json element at index: `%d` in array: `%s`",
i, array
));
testNodes.add(span);
}
}
return testSpans;
return testNodes;
}
@NonNull

View File

@ -1,42 +0,0 @@
package ru.noties.markwon.renderer.visitor;
import android.support.annotation.NonNull;
import java.util.Map;
public class TestEntry {
private final String name;
private final String text;
private final Map<String, String> attributes;
TestEntry(@NonNull String name, @NonNull String text, @NonNull Map<String, String> attributes) {
this.name = name;
this.text = text;
this.attributes = attributes;
}
@NonNull
public String name() {
return name;
}
@NonNull
public String text() {
return text;
}
@NonNull
public Map<String, String> attributes() {
return attributes;
}
@Override
public String toString() {
return "TestEntry{" +
"name='" + name + '\'' +
", text='" + text + '\'' +
", attributes=" + attributes +
'}';
}
}

View File

@ -19,6 +19,7 @@ import ru.noties.markwon.spans.TableRowSpan;
import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE;
import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST;
import static ru.noties.markwon.renderer.visitor.TestSpan.CODE;
import static ru.noties.markwon.renderer.visitor.TestSpan.CODE_BLOCK;
import static ru.noties.markwon.renderer.visitor.TestSpan.EMPHASIS;
import static ru.noties.markwon.renderer.visitor.TestSpan.HEADING;
import static ru.noties.markwon.renderer.visitor.TestSpan.IMAGE;
@ -63,13 +64,16 @@ class TestFactory implements SpannableFactory {
@Nullable
@Override
public Object code(@NonNull SpannableTheme theme, boolean multiline) {
return new TestSpan(CODE, map("multiline", multiline));
final String name = multiline
? CODE_BLOCK
: CODE;
return new TestSpan(name);
}
@Nullable
@Override
public Object orderedListItem(@NonNull SpannableTheme theme, int startNumber) {
return new TestSpan(ORDERED_LIST, map("startNumber", startNumber));
return new TestSpan(ORDERED_LIST, map("start", startNumber));
}
@Nullable
@ -87,7 +91,7 @@ class TestFactory implements SpannableFactory {
@Nullable
@Override
public Object heading(@NonNull SpannableTheme theme, int level) {
return new TestSpan(HEADING, map("level", level));
return new TestSpan(HEADING + level);
}
@Nullable
@ -101,7 +105,7 @@ class TestFactory implements SpannableFactory {
public Object taskListItem(@NonNull SpannableTheme theme, int blockIndent, boolean isDone) {
return new TestSpan(TASK_LIST, map(
Pair.of("blockIdent", blockIndent),
Pair.of("isDone", isDone)
Pair.of("done", isDone)
));
}
@ -110,8 +114,8 @@ class TestFactory implements SpannableFactory {
public Object tableRow(@NonNull SpannableTheme theme, @NonNull List<TableRowSpan.Cell> cells, boolean isHeader, boolean isOdd) {
return new TestSpan(TABLE_ROW, map(
Pair.of("cells", cells),
Pair.of("isHeader", isHeader),
Pair.of("isOdd", isOdd)
Pair.of("header", isHeader),
Pair.of("odd", isOdd)
));
}
@ -127,7 +131,7 @@ class TestFactory implements SpannableFactory {
@Override
public Object image(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) {
return new TestSpan(IMAGE, map(
Pair.of("destination", destination),
Pair.of("src", destination),
Pair.of("imageSize", imageSize),
Pair.of("replacementTextIsLink", replacementTextIsLink)
));

View File

@ -0,0 +1,140 @@
package ru.noties.markwon.renderer.visitor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
import java.util.Map;
abstract class TestNode {
private final TestNode parent;
TestNode(@Nullable TestNode parent) {
this.parent = parent;
}
@Nullable
public TestNode parent() {
return parent;
}
abstract boolean isText();
abstract boolean isSpan();
@NonNull
abstract Text getAsText();
@NonNull
abstract Span getAsSpan();
static class Text extends TestNode {
private final String text;
Text(@Nullable TestNode parent, @NonNull String text) {
super(parent);
this.text = text;
}
@NonNull
public String text() {
return text;
}
@Override
boolean isText() {
return true;
}
@Override
boolean isSpan() {
return false;
}
@NonNull
@Override
Text getAsText() {
return this;
}
@NonNull
@Override
Span getAsSpan() {
throw new ClassCastException();
}
@Override
public String toString() {
return "Text{" +
"text='" + text + '\'' +
'}';
}
}
static class Span extends TestNode {
private final String name;
private final List<TestNode> children;
private final Map<String, String> attributes;
Span(
@Nullable TestNode parent,
@NonNull String name,
@NonNull List<TestNode> children,
@NonNull Map<String, String> attributes) {
super(parent);
this.name = name;
this.children = children;
this.attributes = attributes;
}
@NonNull
public String name() {
return name;
}
@NonNull
public List<TestNode> children() {
return children;
}
@NonNull
public Map<String, String> attributes() {
return attributes;
}
@Override
boolean isText() {
return false;
}
@Override
boolean isSpan() {
return true;
}
@NonNull
@Override
Text getAsText() {
throw new ClassCastException();
}
@NonNull
@Override
Span getAsSpan() {
return this;
}
@Override
public String toString() {
return "Span{" +
"name='" + name + '\'' +
", children=" + children +
", attributes=" + attributes +
'}';
}
}
}

View File

@ -11,6 +11,7 @@ class TestSpan {
static final String EMPHASIS = "i";
static final String BLOCK_QUOTE = "blockquote";
static final String CODE = "code";
static final String CODE_BLOCK = "code-block";
static final String ORDERED_LIST = "ol";
static final String BULLET_LIST = "ul";
static final String THEMATIC_BREAK = "hr";

View File

@ -13,11 +13,13 @@ config:
output:
- text: "Here is some "
- a:
attrs:
href: "https://my.href"
text: "link"
- text: "link"
attrs:
href: "https://my.href"
- text: " "
- b: "bold bold italic bold"
- i: "bold italic"
- b:
- text: "bold "
- i: "bold italic" #equals to: `- i: - text: "bold italic"`
- text: " bold"
- text: " normal"

View File

@ -16,10 +16,17 @@ input: |-
output:
- text: "First "
- b: "line"
- text: " "
- text: " is "
- i: "always"
- text: " "
- s: "strike"
- text: " down\n\n"
- blockquote: "Some quote here!"
- text: "\n\n"
- h1: "Header 1"
- text: "\n\n"
- h2: "Header 2"
- text: "\n\nand "
- code: "some code"
- text: " and more:\n\n"
- code-block: "the code in multiline"