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 }