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