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 }