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.anchor != "")
76                     {
77                         line ~= text(" &", event.anchor);
78                     }
79                     if (event.tag != "")
80                     {
81                         line ~= text(" <", event.tag, ">");
82                     }
83                     break;
84                 case EventID.sequenceStart:
85                     line = "+SEQ";
86                     if (event.anchor != "")
87                     {
88                         line ~= text(" &", event.anchor);
89                     }
90                     if (event.tag != "")
91                     {
92                         line ~= text(" <", event.tag, ">");
93                     }
94                     break;
95                 case EventID.streamEnd:
96                     line = "-STR";
97                     break;
98                 case EventID.documentEnd:
99                     line = "-DOC";
100                     if (event.explicitDocument)
101                     {
102                         line ~= " ...";
103                     }
104                     break;
105                 case EventID.mappingEnd:
106                     line = "-MAP";
107                     break;
108                 case EventID.sequenceEnd:
109                     line = "-SEQ";
110                     break;
111                 case EventID.alias_:
112                     line = text("=ALI *", event.anchor);
113                     break;
114                 case EventID.invalid:
115                     assert(0, "Invalid EventID produced");
116             }
117             output ~= line;
118         }
119     }
120     catch (Exception) {} //Exceptions should just stop adding output
121     return output.join("\n");
122 }
123 
124 enum TestState
125 {
126     success,
127     skipped,
128     failure
129 }
130 
131 struct TestResult
132 {
133     string name;
134     TestState state;
135     string failMsg;
136 
137     const void toString(OutputRange)(ref OutputRange writer)
138         if (isOutputRange!(OutputRange, char))
139     {
140         ubyte statusColour;
141         string statusString;
142         final switch (state) {
143             case TestState.success:
144                 statusColour = 32;
145                 statusString = "Succeeded";
146                 break;
147             case TestState.failure:
148                 statusColour = 31;
149                 statusString = "Failed";
150                 break;
151             case TestState.skipped:
152                 statusColour = 93;
153                 statusString = "Skipped";
154                 break;
155         }
156         writer.formattedWrite!"[\033[%s;1m%s\033[0m] %s"(statusColour, statusString, name);
157         if (state != TestState.success)
158         {
159             writer.formattedWrite!" (%s)"(failMsg.replace("\n", " "));
160         }
161     }
162 }
163 
164 TestResult runTests(string yaml) @safe
165 {
166     TestResult output;
167     output.state = TestState.success;
168     auto testDoc = Loader.fromString(yaml).load();
169     output.name = testDoc[0]["name"].as!string;
170     bool loadFailed, shouldFail;
171     string failMsg;
172     JSONValue json;
173     Node[] nodes;
174     string yamlString;
175     Nullable!string compareYAMLString;
176     Nullable!string events;
177     ulong testsRun;
178 
179     void fail(string msg) @safe
180     {
181         output.state = TestState.failure;
182         output.failMsg = msg;
183     }
184     void skip(string msg) @safe
185     {
186         output.state = TestState.skipped;
187         output.failMsg = msg;
188     }
189     void parseYAML(string yaml) @safe
190     {
191         yamlString = yaml;
192         try {
193             nodes = Loader.fromString(yamlString).array;
194         }
195         catch (Exception e)
196         {
197             loadFailed = true;
198             failMsg = e.msg;
199         }
200     }
201     void compareLineByLine(const string a, const string b, bool skipWhitespace, const string msg) @safe
202     {
203         foreach (line1, line2; zip(a.lineSplitter, b.lineSplitter))
204         {
205             if (skipWhitespace)
206             {
207                 line1.skipOver!isWhite;
208                 line2.skipOver!isWhite;
209             }
210             if (line1 != line2)
211             {
212                 fail(text(msg, " Got ", line1, ", expected ", line2));
213                 break;
214             }
215         }
216     }
217     foreach (Node test; testDoc)
218     {
219         if ("yaml" in test)
220         {
221             parseYAML(test["yaml"].as!string);
222         }
223         if ("json" in test)
224         {
225             json = parseJSON(test["json"].as!string);
226         }
227         if ("tree" in test)
228         {
229             events = test["tree"].as!string;
230         }
231         if ("fail" in test)
232         {
233             shouldFail = test["fail"].as!bool;
234             if (shouldFail)
235             {
236                 testsRun++;
237             }
238         }
239         if ("emit" in test)
240         {
241             compareYAMLString = test["emit"].as!string;
242         }
243     }
244     if (!loadFailed && !compareYAMLString.isNull && !shouldFail)
245     {
246         Appender!string buf;
247         dumper().dump(buf);
248         compareLineByLine(buf.data, compareYAMLString.get, false, "Dumped YAML mismatch");
249         testsRun++;
250     }
251     if (!loadFailed && !events.isNull && !shouldFail)
252     {
253         const compare = dumpEventString(yamlString);
254         compareLineByLine(compare, events.get, true, "Event mismatch");
255         testsRun++;
256     }
257     if (loadFailed && !shouldFail)
258     {
259         fail(failMsg);
260     }
261     if (shouldFail && !loadFailed)
262     {
263         fail("Invalid YAML accepted");
264     }
265     if ((testsRun == 0) && (output.state != TestState.failure))
266     {
267         skip("No tests run");
268     }
269     return output;
270 }
271 
272 // Can't be @safe due to dirEntries()
273 void main(string[] args) @system
274 {
275     string path = "yaml-test-suite/src";
276 
277     void printResult(string id, TestResult result)
278     {
279         writeln(id, " ", result);
280     }
281 
282     if (args.length > 1)
283     {
284         path = args[1];
285     }
286 
287     ulong total;
288     ulong successes;
289     foreach (file; dirEntries(path, "*.yaml", SpanMode.shallow))
290     {
291         auto result = runTests(readText(file));
292         if (result.state == TestState.success)
293         {
294             debug(verbose) printResult(file.baseName, result);
295             successes++;
296         }
297         else
298         {
299             printResult(file.baseName, result);
300         }
301         total++;
302     }
303     writefln!"%d/%d tests passed"(successes, total);
304 }