dyaml/testsuite/source/app.d

324 lines
9.2 KiB
D
Raw Normal View History

module dyaml.testsuite;
import dyaml;
import dyaml.event;
import std.algorithm;
import std.conv;
import std.file;
import std.format;
import std.json;
import std.path;
import std.range;
import std.stdio;
import std.string;
import std.typecons;
import std.utf;
auto dumpEventString(string str) @safe
{
string[] output;
try
{
auto events = Loader.fromString(str).parse();
foreach (event; events)
{
string line;
final switch (event.id)
{
case EventID.scalar:
line = "=VAL ";
if (event.anchor != "")
{
line ~= text("&", event.anchor, " ");
}
if (event.tag != "")
{
line ~= text("<", event.tag, "> ");
}
switch(event.scalarStyle)
{
case ScalarStyle.singleQuoted:
line ~= "'";
break;
case ScalarStyle.doubleQuoted:
line ~= '"';
break;
case ScalarStyle.literal:
line ~= "|";
break;
case ScalarStyle.folded:
line ~= ">";
break;
default:
line ~= ":";
break;
}
if (event.value != "")
{
line ~= text(event.value.substitute("\n", "\\n", `\`, `\\`, "\r", "\\r", "\t", "\\t", "\b", "\\b"));
}
break;
case EventID.streamStart:
line = "+STR";
break;
case EventID.documentStart:
line = "+DOC";
if (event.explicitDocument)
{
line ~= text(" ---");
}
break;
case EventID.mappingStart:
line = "+MAP";
if (event.anchor != "")
{
line ~= text(" &", event.anchor);
}
if (event.tag != "")
{
line ~= text(" <", event.tag, ">");
}
break;
case EventID.sequenceStart:
line = "+SEQ";
if (event.anchor != "")
{
line ~= text(" &", event.anchor);
}
if (event.tag != "")
{
line ~= text(" <", event.tag, ">");
}
break;
case EventID.streamEnd:
line = "-STR";
break;
case EventID.documentEnd:
line = "-DOC";
if (event.explicitDocument)
{
line ~= " ...";
}
break;
case EventID.mappingEnd:
line = "-MAP";
break;
case EventID.sequenceEnd:
line = "-SEQ";
break;
case EventID.alias_:
line = text("=ALI *", event.anchor);
break;
case EventID.invalid:
assert(0, "Invalid EventID produced");
}
output ~= line;
}
}
catch (Exception) {} //Exceptions should just stop adding output
return output.join("\n");
}
enum TestState
{
success,
skipped,
failure
}
struct TestResult
{
string name;
TestState state;
string failMsg;
const void toString(OutputRange)(ref OutputRange writer)
if (isOutputRange!(OutputRange, char))
{
ubyte statusColour;
string statusString;
final switch (state) {
case TestState.success:
statusColour = 32;
statusString = "Succeeded";
break;
case TestState.failure:
statusColour = 31;
statusString = "Failed";
break;
case TestState.skipped:
statusColour = 93;
statusString = "Skipped";
break;
}
writer.formattedWrite!"[\033[%s;1m%s\033[0m] %s"(statusColour, statusString, name);
if (state != TestState.success)
{
writer.formattedWrite!" (%s)"(failMsg.replace("\n", " "));
}
}
}
TestResult runTests(string tml) @safe
{
TestResult output;
output.state = TestState.success;
auto splitFile = tml.splitter("\n--- ");
output.name = splitFile.front.findSplit("=== ")[2];
bool loadFailed, shouldFail;
string failMsg;
JSONValue json;
Node[] nodes;
string yamlString;
Nullable!string compareYAMLString;
Nullable!string events;
ulong testsRun;
void fail(string msg) @safe
{
output.state = TestState.failure;
output.failMsg = msg;
}
void skip(string msg) @safe
{
output.state = TestState.skipped;
output.failMsg = msg;
}
void parseYAML(string yaml) @safe
{
yamlString = yaml;
try {
nodes = Loader.fromString(yamlString).array;
}
catch (Exception e)
{
loadFailed = true;
failMsg = e.msg;
}
}
void compareLineByLine(const string a, const string b, const string msg) @safe
{
foreach (line1, line2; zip(a.lineSplitter, b.lineSplitter))
{
if (line1 != line2)
{
fail(text(msg, " Got ", line1, ", expected ", line2));
break;
}
}
}
foreach (section; splitFile.drop(1))
{
auto splitSection = section.findSplit("\n");
auto splitSectionHeader = splitSection[0].findSplit(":");
const splitSectionName = splitSectionHeader[0].findSplit("(");
const sectionName = splitSectionName[0];
const sectionParams = splitSectionName[2].findSplit(")")[0];
string sectionData = splitSection[2];
if (sectionData != "")
{
//< means dedent.
if (sectionParams.canFind("<"))
{
sectionData = sectionData[4..$].substitute("\n ", "\n", "<SPC>", " ", "<TAB>", "\t").toUTF8;
}
else
{
sectionData = sectionData.substitute("<SPC>", " ", "<TAB>", "\t").toUTF8;
}
//Not sure what + means.
}
switch(sectionName)
{
case "in-yaml":
parseYAML(sectionData);
break;
case "in-json":
json = parseJSON(sectionData);
break;
case "test-event":
events = sectionData;
break;
case "error":
shouldFail = true;
testsRun++;
break;
case "out-yaml":
compareYAMLString = sectionData;
break;
case "emit-yaml":
// TODO: Figure out how/if to implement this
//fail("Unhandled test - emit-yaml");
break;
case "lex-token":
// TODO: Should this be implemented?
//fail("Unhandled test - lex-token");
break;
case "from": break;
case "tags": break;
default: assert(false, text("Unhandled section ", sectionName, "in ", output.name));
}
}
if (!loadFailed && !compareYAMLString.isNull && !shouldFail)
{
Appender!string buf;
dumper().dump(buf);
compareLineByLine(buf.data, compareYAMLString.get, "Dumped YAML mismatch");
testsRun++;
}
if (!loadFailed && !events.isNull && !shouldFail)
{
const compare = dumpEventString(yamlString);
compareLineByLine(compare, events.get, "Event mismatch");
testsRun++;
}
if (loadFailed && !shouldFail)
{
fail(failMsg);
}
if (shouldFail && !loadFailed)
{
fail("Invalid YAML accepted");
}
if ((testsRun == 0) && (output.state != TestState.failure))
{
skip("No tests run");
}
return output;
}
// Can't be @safe due to dirEntries()
void main(string[] args) @system
{
string path = "yaml-test-suite/test";
void printResult(string id, TestResult result)
{
writeln(id, " ", result);
}
if (args.length > 1)
{
path = args[1];
}
ulong total;
ulong successes;
foreach (file; dirEntries(path, "*.tml", SpanMode.shallow))
{
auto result = runTests(readText(file));
if (result.state == TestState.success)
{
debug(verbose) printResult(file.baseName, result);
successes++;
}
else
{
printResult(file.baseName, result);
}
total++;
}
writefln!"%d/%d tests passed"(successes, total);
}