1 module dyaml.testsuite;
2 
3 import dyaml;
4 import dyaml.event;
5 
6 import std.algorithm;
7 import std.conv;
8 import std.file;
9 import std.format;
10 import std.json;
11 import std.path;
12 import std.range;
13 import std.stdio;
14 import std.string;
15 import std.typecons;
16 import std.utf;
17 import std.uni;
18 
19 auto dumpEventString(string str) @safe
20 {
21     string[] output;
22     try
23     {
24         auto events = Loader.fromString(str).parse();
25         foreach (event; events)
26         {
27             string line;
28             final switch (event.id)
29             {
30                 case EventID.scalar:
31                     line = "=VAL ";
32                     if (event.anchor != "")
33                     {
34                         line ~= text("&", event.anchor, " ");
35                     }
36                     if (event.tag != "")
37                     {
38                         line ~= text("<", event.tag, "> ");
39                     }
40                     switch(event.scalarStyle)
41                     {
42                         case ScalarStyle.singleQuoted:
43                             line ~= "'";
44                             break;
45                         case ScalarStyle.doubleQuoted:
46                             line ~= '"';
47                             break;
48                         case ScalarStyle.literal:
49                             line ~= "|";
50                             break;
51                         case ScalarStyle.folded:
52                             line ~= ">";
53                             break;
54                         default:
55                             line ~= ":";
56                             break;
57                     }
58                     if (event.value != "")
59                     {
60                         line ~= text(event.value.substitute("\n", "\\n", `\`, `\\`, "\r", "\\r", "\t", "\\t", "\b", "\\b"));
61                     }
62                     break;
63                 case EventID.streamStart:
64                     line = "+STR";
65                     break;
66                 case EventID.documentStart:
67                     line = "+DOC";
68                     if (event.explicitDocument)
69                     {
70                         line ~= text(" ---");
71                     }
72                     break;
73                 case EventID.mappingStart:
74                     line = "+MAP";
75                     if (event.collectionStyle == CollectionStyle.flow)
76                     {
77                         line ~= text(" {}");
78                     }
79                     if (event.anchor != "")
80                     {
81                         line ~= text(" &", event.anchor);
82                     }
83                     if (event.tag != "")
84                     {
85                         line ~= text(" <", event.tag, ">");
86                     }
87                     break;
88                 case EventID.sequenceStart:
89                     line = "+SEQ";
90                     if (event.collectionStyle == CollectionStyle.flow)
91                     {
92                         line ~= text(" []");
93                     }
94                     if (event.anchor != "")
95                     {
96                         line ~= text(" &", event.anchor);
97                     }
98                     if (event.tag != "")
99                     {
100                         line ~= text(" <", event.tag, ">");
101                     }
102                     break;
103                 case EventID.streamEnd:
104                     line = "-STR";
105                     break;
106                 case EventID.documentEnd:
107                     line = "-DOC";
108                     if (event.explicitDocument)
109                     {
110                         line ~= " ...";
111                     }
112                     break;
113                 case EventID.mappingEnd:
114                     line = "-MAP";
115                     break;
116                 case EventID.sequenceEnd:
117                     line = "-SEQ";
118                     break;
119                 case EventID.alias_:
120                     line = text("=ALI *", event.anchor);
121                     break;
122                 case EventID.invalid:
123                     assert(0, "Invalid EventID produced");
124             }
125             output ~= line;
126         }
127     }
128     catch (Exception) {} //Exceptions should just stop adding output
129     return output.join("\n");
130 }
131 
132 enum TestState
133 {
134     success,
135     skipped,
136     failure
137 }
138 
139 struct TestResult
140 {
141     string name;
142     TestState state;
143     string failMsg;
144 
145     const void toString(OutputRange)(ref OutputRange writer)
146         if (isOutputRange!(OutputRange, char))
147     {
148         ubyte statusColour;
149         string statusString;
150         final switch (state) {
151             case TestState.success:
152                 statusColour = 32;
153                 statusString = "Succeeded";
154                 break;
155             case TestState.failure:
156                 statusColour = 31;
157                 statusString = "Failed";
158                 break;
159             case TestState.skipped:
160                 statusColour = 93;
161                 statusString = "Skipped";
162                 break;
163         }
164         writer.formattedWrite!"[\033[%s;1m%s\033[0m] %s"(statusColour, statusString, name);
165         if (state != TestState.success)
166         {
167             writer.formattedWrite!" (%s)"(failMsg.replace("\n", " "));
168         }
169     }
170 }
171 
172 TestResult runTests(string yaml) @safe
173 {
174     TestResult output;
175     output.state = TestState.success;
176     auto testDoc = Loader.fromString(yaml).load();
177     output.name = testDoc[0]["name"].as!string;
178     bool loadFailed, shouldFail;
179     string failMsg;
180     JSONValue json;
181     Node[] nodes;
182     string yamlString;
183     Nullable!string compareYAMLString;
184     Nullable!string events;
185     ulong testsRun;
186 
187     void fail(string msg) @safe
188     {
189         output.state = TestState.failure;
190         output.failMsg = msg;
191     }
192     void skip(string msg) @safe
193     {
194         output.state = TestState.skipped;
195         output.failMsg = msg;
196     }
197     void parseYAML(string yaml) @safe
198     {
199         yamlString = yaml;
200         try {
201             nodes = Loader.fromString(yamlString).array;
202         }
203         catch (Exception e)
204         {
205             loadFailed = true;
206             failMsg = e.msg;
207         }
208     }
209     void compareLineByLine(const string a, const string b, bool skipWhitespace, const string msg) @safe
210     {
211         foreach (line1, line2; zip(a.lineSplitter, b.lineSplitter))
212         {
213             if (skipWhitespace)
214             {
215                 line1.skipOver!isWhite;
216                 line2.skipOver!isWhite;
217             }
218             if (line1 != line2)
219             {
220                 fail(text(msg, " Got ", line1, ", expected ", line2));
221                 break;
222             }
223         }
224     }
225     foreach (Node test; testDoc)
226     {
227         if ("yaml" in test)
228         {
229             parseYAML(cleanup(test["yaml"].as!string));
230         }
231         if ("json" in test)
232         {
233             json = parseJSON(test["json"].as!string);
234         }
235         if ("tree" in test)
236         {
237             events = cleanup(test["tree"].as!string);
238         }
239         if ("fail" in test)
240         {
241             shouldFail = test["fail"].as!bool;
242             if (shouldFail)
243             {
244                 testsRun++;
245             }
246         }
247         if ("emit" in test)
248         {
249             compareYAMLString = test["emit"].as!string;
250         }
251     }
252     if (!loadFailed && !compareYAMLString.isNull && !shouldFail)
253     {
254         Appender!string buf;
255         dumper().dump(buf);
256         compareLineByLine(buf.data, compareYAMLString.get, false, "Dumped YAML mismatch");
257         testsRun++;
258     }
259     if (!loadFailed && !events.isNull && !shouldFail)
260     {
261         const compare = dumpEventString(yamlString);
262         compareLineByLine(compare, events.get, true, "Event mismatch");
263         testsRun++;
264     }
265     if (loadFailed && !shouldFail)
266     {
267         fail(failMsg);
268     }
269     if (shouldFail && !loadFailed)
270     {
271         fail("Invalid YAML accepted");
272     }
273     if ((testsRun == 0) && (output.state != TestState.failure))
274     {
275         skip("No tests run");
276     }
277     return output;
278 }
279 
280 // Can't be @safe due to dirEntries()
281 void main(string[] args) @system
282 {
283     string path = "yaml-test-suite/src";
284 
285     void printResult(string id, TestResult result)
286     {
287         writeln(id, " ", result);
288     }
289 
290     if (args.length > 1)
291     {
292         path = args[1];
293     }
294 
295     ulong total;
296     ulong successes;
297     foreach (file; dirEntries(path, "*.yaml", SpanMode.shallow))
298     {
299         auto result = runTests(readText(file));
300         if (result.state == TestState.success)
301         {
302             debug(verbose) printResult(file.baseName, result);
303             successes++;
304         }
305         else
306         {
307             printResult(file.baseName, result);
308         }
309         total++;
310     }
311     writefln!"%d/%d tests passed"(successes, total);
312 }
313 
314 string cleanup(string input) @safe
315 {
316     return input.substitute(
317         "␣", " ",
318         "————»", "\t",
319         "———»", "\t",
320         "——»", "\t",
321         "—»", "\t",
322         "»", "\t",
323         "↵", "\n",
324         "∎", "",
325         "←", "\r",
326         "⇔", "\uFEFF"
327     ).toUTF8;
328 }