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 }