Working with test format

This commit is contained in:
Dimitry Ivanov 2018-08-23 18:39:10 +03:00
parent 4ae310595a
commit 808349d565
10 changed files with 805 additions and 3 deletions

View File

@ -78,9 +78,14 @@ ext {
]
deps['test'] = [
'junit' : 'junit:junit:4.12',
'robolectric': 'org.robolectric:robolectric:3.8',
'ix-java' : 'com.github.akarnokd:ixjava:1.0.0'
'junit' : 'junit:junit:4.12',
'robolectric' : 'org.robolectric:robolectric:3.8',
'ix-java' : 'com.github.akarnokd:ixjava:1.0.0',
'jackson-yaml' : 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.0',
'jackson-databind': 'com.fasterxml.jackson.core:jackson-databind:2.9.6',
'gson' : 'com.google.code.gson:gson:2.8.5',
'commons-io' : 'commons-io:commons-io:2.6',
'mockito' : 'org.mockito:mockito-core:2.21.0'
]
}

View File

@ -29,6 +29,11 @@ dependencies {
testImplementation it['junit']
testImplementation it['robolectric']
testImplementation it['ix-java']
testImplementation it['jackson-yaml']
testImplementation it['jackson-databind']
testImplementation it['gson']
testImplementation it['commons-io']
testImplementation it['mockito']
}
}

View File

@ -0,0 +1,132 @@
package ru.noties.markwon.renderer.visitor;
import android.support.annotation.NonNull;
import android.text.SpannableStringBuilder;
import org.commonmark.node.Node;
import org.junit.Test;
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;
import ix.IxPredicate;
import ru.noties.markwon.LinkResolverDef;
import ru.noties.markwon.Markwon;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.renderer.SpannableMarkdownVisitor;
import ru.noties.markwon.spans.SpannableTheme;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
@RunWith(ParameterizedRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class SpannableMarkdownVisitorTest {
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
public static Collection<Object> parameters() {
return TestDataReader.testFiles();
}
private final String file;
public SpannableMarkdownVisitorTest(@NonNull String file) {
this.file = file;
}
@Test
public void test() {
final TestData data = TestDataReader.readTest(file);
final SpannableConfiguration configuration = configuration(data.config());
final SpannableBuilder builder = new SpannableBuilder();
final SpannableMarkdownVisitor visitor = new SpannableMarkdownVisitor(configuration, builder);
final Node node = Markwon.createParser().parse(data.input());
node.accept(visitor);
final SpannableStringBuilder stringBuilder = builder.spannableStringBuilder();
final String raw = stringBuilder.toString();
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());
}
}
}
@NonNull
private SpannableConfiguration configuration(@NonNull TestConfig config) {
final SpannableFactory factory = new TestFactory(config.hasOption(TestConfig.USE_PARAGRAPHS));
// todo: rest omitted for now
return SpannableConfiguration.builder(null)
.theme(mock(SpannableTheme.class))
.linkResolver(mock(LinkResolverDef.class))
.factory(factory)
.build();
// return configuration;
}
}

View File

@ -0,0 +1,31 @@
package ru.noties.markwon.renderer.visitor;
import android.support.annotation.NonNull;
import java.util.Map;
class TestConfig {
static final String USE_PARAGRAPHS = "use-paragraphs";
static final String USE_HTML = "use-html";
static final String SOFT_BREAK_ADDS_NEW_LINE = "soft-break-adds-new-line";
static final String HTML_ALLOW_NON_CLOSED_TAGS = "html-allow-non-closed-tags";
private final Map<String, Boolean> map;
TestConfig(@NonNull Map<String, Boolean> map) {
this.map = map;
}
boolean hasOption(@NonNull String option) {
final Boolean value = map.get(option);
return value != null && value;
}
@Override
public String toString() {
return "TestConfig{" +
"map=" + map +
'}';
}
}

View File

@ -0,0 +1,45 @@
package ru.noties.markwon.renderer.visitor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
class TestData {
private final String description;
private final String input;
private final TestConfig config;
private final List<TestEntry> output;
TestData(
@Nullable String description,
@NonNull String input,
@NonNull TestConfig config,
@NonNull List<TestEntry> output) {
this.description = description;
this.input = input;
this.config = config;
this.output = output;
}
@Nullable
public String description() {
return description;
}
@NonNull
public String input() {
return input;
}
@NonNull
public TestConfig config() {
return config;
}
@NonNull
public List<TestEntry> output() {
return output;
}
}

View File

@ -0,0 +1,272 @@
package ru.noties.markwon.renderer.visitor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ix.Ix;
import ix.IxFunction;
import ix.IxPredicate;
abstract class TestDataReader {
private static final String FOLDER = "tests/";
@NonNull
static Collection<Object> testFiles() {
final InputStream in = TestDataReader.class.getClassLoader().getResourceAsStream(FOLDER);
if (in == null) {
throw new RuntimeException("Cannot access test cases folder");
}
try {
//noinspection unchecked
return (Collection) Ix.from(IOUtils.readLines(in, StandardCharsets.UTF_8))
.filter(new IxPredicate<String>() {
@Override
public boolean test(String s) {
return s.endsWith(".yaml");
}
})
.map(new IxFunction<String, String>() {
@Override
public String apply(String s) {
return FOLDER + s;
}
})
.map(new IxFunction<String, Object[]>() {
@Override
public Object[] apply(String s) {
return new Object[]{
s
};
}
})
.toList();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@NonNull
static TestData readTest(@NonNull String file) {
return new Reader(file).read();
}
private TestDataReader() {
}
static class Reader {
private final String file;
Reader(@NonNull String file) {
this.file = file;
}
@NonNull
TestData read() {
return testData(jsonObject());
}
@NonNull
private JsonObject jsonObject() {
try {
final String input = IOUtils.resourceToString(file, StandardCharsets.UTF_8, TestDataReader.class.getClassLoader());
final ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
final Object object = objectMapper.readValue(input, Object.class);
final ObjectMapper jsonWriter = new ObjectMapper();
final String json = jsonWriter.writeValueAsString(object);
return new Gson().fromJson(json, JsonObject.class);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
@NonNull
private TestData testData(@NonNull JsonObject jsonObject) {
final String description = jsonObject.get("description").getAsString();
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));
}
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));
}
return new TestData(
description,
input,
testConfig(jsonObject.get("config")),
testSpans
);
}
// todo: rename TestNode -> it's not a node... but... what?
@NonNull
private List<TestEntry> testEntries(@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
final int length = array.size();
final List<TestEntry> testSpans = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
final JsonElement element = array.get(i);
if (element.isJsonObject()) {
final JsonObject object = element.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"));
} else {
final JsonPrimitive primitive;
if (value.isJsonPrimitive()) {
primitive = value.getAsJsonPrimitive();
} else {
primitive = null;
}
if (primitive == null
|| !primitive.isString()) {
throw new RuntimeException(String.format("Unexpected json element at index: `%d` in array: `%s`",
i, array
));
}
text = primitive.getAsString();
attributes = Collections.emptyMap();
}
testSpans.add(new TestEntry(name, text, attributes));
} else {
throw new RuntimeException(String.format("Unexpected json element at index: `%d` in array: `%s`",
i, array
));
}
}
return testSpans;
}
@NonNull
private TestConfig testConfig(@Nullable JsonElement element) {
final JsonObject object = element != null && element.isJsonObject()
? element.getAsJsonObject()
: null;
final Map<String, Boolean> map;
if (object != null) {
map = new HashMap<>(object.size());
for (String key : object.keySet()) {
final JsonElement value = object.get(key);
if (value.isJsonPrimitive()) {
final JsonPrimitive jsonPrimitive = value.getAsJsonPrimitive();
Boolean b = null;
if (jsonPrimitive.isBoolean()) {
b = jsonPrimitive.getAsBoolean();
} else if (jsonPrimitive.isString()) {
final String s = jsonPrimitive.getAsString();
if ("true".equalsIgnoreCase(s)) {
b = Boolean.TRUE;
} else if ("false".equalsIgnoreCase(s)) {
b = Boolean.FALSE;
}
}
if (b != null) {
map.put(key, b);
}
}
}
} else {
map = Collections.emptyMap();
}
return new TestConfig(map);
}
@NonNull
private static Map<String, String> attributes(@NonNull JsonElement element) {
final JsonObject object = element.isJsonObject()
? element.getAsJsonObject()
: null;
final Map<String, String> attributes;
if (object != null) {
attributes = new HashMap<>(object.size());
for (String key : object.keySet()) {
attributes.put(key, object.get(key).getAsString());
}
} else {
attributes = Collections.emptyMap();
}
return attributes;
}
}
}

View File

@ -0,0 +1,42 @@
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

@ -0,0 +1,189 @@
package ru.noties.markwon.renderer.visitor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.SpannableTheme;
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.EMPHASIS;
import static ru.noties.markwon.renderer.visitor.TestSpan.HEADING;
import static ru.noties.markwon.renderer.visitor.TestSpan.IMAGE;
import static ru.noties.markwon.renderer.visitor.TestSpan.LINK;
import static ru.noties.markwon.renderer.visitor.TestSpan.ORDERED_LIST;
import static ru.noties.markwon.renderer.visitor.TestSpan.PARAGRAPH;
import static ru.noties.markwon.renderer.visitor.TestSpan.STRIKE_THROUGH;
import static ru.noties.markwon.renderer.visitor.TestSpan.STRONG_EMPHASIS;
import static ru.noties.markwon.renderer.visitor.TestSpan.SUB_SCRIPT;
import static ru.noties.markwon.renderer.visitor.TestSpan.SUPER_SCRIPT;
import static ru.noties.markwon.renderer.visitor.TestSpan.TABLE_ROW;
import static ru.noties.markwon.renderer.visitor.TestSpan.TASK_LIST;
import static ru.noties.markwon.renderer.visitor.TestSpan.THEMATIC_BREAK;
import static ru.noties.markwon.renderer.visitor.TestSpan.UNDERLINE;
class TestFactory implements SpannableFactory {
private final boolean useParagraphs;
TestFactory(boolean useParagraphs) {
this.useParagraphs = useParagraphs;
}
@Nullable
@Override
public Object strongEmphasis() {
return new TestSpan(STRONG_EMPHASIS);
}
@Nullable
@Override
public Object emphasis() {
return new TestSpan(EMPHASIS);
}
@Nullable
@Override
public Object blockQuote(@NonNull SpannableTheme theme) {
return new TestSpan(BLOCK_QUOTE);
}
@Nullable
@Override
public Object code(@NonNull SpannableTheme theme, boolean multiline) {
return new TestSpan(CODE, map("multiline", multiline));
}
@Nullable
@Override
public Object orderedListItem(@NonNull SpannableTheme theme, int startNumber) {
return new TestSpan(ORDERED_LIST, map("startNumber", startNumber));
}
@Nullable
@Override
public Object bulletListItem(@NonNull SpannableTheme theme, int level) {
return new TestSpan(BULLET_LIST, map("level", level));
}
@Nullable
@Override
public Object thematicBreak(@NonNull SpannableTheme theme) {
return new TestSpan(THEMATIC_BREAK);
}
@Nullable
@Override
public Object heading(@NonNull SpannableTheme theme, int level) {
return new TestSpan(HEADING, map("level", level));
}
@Nullable
@Override
public Object strikethrough() {
return new TestSpan(STRIKE_THROUGH);
}
@Nullable
@Override
public Object taskListItem(@NonNull SpannableTheme theme, int blockIndent, boolean isDone) {
return new TestSpan(TASK_LIST, map(
Pair.of("blockIdent", blockIndent),
Pair.of("isDone", isDone)
));
}
@Nullable
@Override
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)
));
}
@Nullable
@Override
public Object paragraph(boolean inTightList) {
return !useParagraphs
? null
: new TestSpan(PARAGRAPH);
}
@Nullable
@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("imageSize", imageSize),
Pair.of("replacementTextIsLink", replacementTextIsLink)
));
}
@Nullable
@Override
public Object link(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) {
return new TestSpan(LINK, map("href", destination));
}
@Nullable
@Override
public Object superScript(@NonNull SpannableTheme theme) {
return new TestSpan(SUPER_SCRIPT);
}
@Nullable
@Override
public Object subScript(@NonNull SpannableTheme theme) {
return new TestSpan(SUB_SCRIPT);
}
@Nullable
@Override
public Object underline() {
return new TestSpan(UNDERLINE);
}
@NonNull
private static Map<String, String> map(@NonNull String key, @Nullable Object value) {
return Collections.singletonMap(key, String.valueOf(value));
}
private static class Pair {
static Pair of(@NonNull String key, @Nullable Object value) {
return new Pair(key, value);
}
final String key;
final Object value;
Pair(@NonNull String key, @Nullable Object value) {
this.key = key;
this.value = value;
}
}
@NonNull
private static Map<String, String> map(Pair... pairs) {
final int length = pairs.length;
final Map<String, String> map = new HashMap<>(length);
for (Pair pair : pairs) {
map.put(pair.key, String.valueOf(pair.value));
}
return map;
}
}

View File

@ -0,0 +1,58 @@
package ru.noties.markwon.renderer.visitor;
import android.support.annotation.NonNull;
import java.util.Collections;
import java.util.Map;
class TestSpan {
static final String STRONG_EMPHASIS = "b";
static final String EMPHASIS = "i";
static final String BLOCK_QUOTE = "blockquote";
static final String CODE = "code";
static final String ORDERED_LIST = "ol";
static final String BULLET_LIST = "ul";
static final String THEMATIC_BREAK = "hr";
static final String HEADING = "h";
static final String STRIKE_THROUGH = "s";
static final String TASK_LIST = "task-list";
static final String TABLE_ROW = "tr";
static final String PARAGRAPH = "p";
static final String IMAGE = "img";
static final String LINK = "a";
static final String SUPER_SCRIPT = "sup";
static final String SUB_SCRIPT = "sub";
static final String UNDERLINE = "u";
private final String name;
private final Map<String, String> attributes;
TestSpan(@NonNull String name) {
this(name, Collections.<String, String>emptyMap());
}
TestSpan(@NonNull String name, @NonNull Map<String, String> attributes) {
this.name = name;
this.attributes = attributes;
}
@NonNull
public String name() {
return name;
}
@NonNull
public Map<String, String> attributes() {
return attributes;
}
@Override
public String toString() {
return "TestSpan{" +
"name='" + name + '\'' +
", attributes=" + attributes +
'}';
}
}

View File

@ -0,0 +1,23 @@
description: Defining test case format
input: |-
Here is some [link](https://my.href)
**bold _bold italic_ bold** normal
config:
use-paragraphs: false
use-html: false
soft-break-adds-new-line: false
html-allow-non-closed-tags: false
output:
- text: "Here is some "
- a:
attrs:
href: "https://my.href"
text: "link"
- text: " "
- b: "bold bold italic bold"
- i: "bold italic"
- text: " normal"