1 /** 2 Grammar testing module for Pegged. 3 4 */ 5 module pegged.tester.grammartester; 6 7 import std.stdio; 8 import std.array; 9 import std.ascii : whitespace; 10 import std.range : retro; 11 import std.algorithm : max, splitter; 12 import std..string : format, stripRight, removechars, squeeze; 13 14 import pegged.tester.testerparser; 15 import pegged.grammar; 16 17 class GrammarTester(grammar, string startSymbol) 18 { 19 bool soft = false; 20 21 private Appender!string errorTextStream; 22 private int numberTests; 23 private int numberErrors; 24 private string latestDiff; 25 26 @property int testCount() { return numberTests; } 27 @property int errorCount() { return numberErrors; } 28 @property string errorText() { return errorTextStream.data; } 29 @property string latestDiffText() { return latestDiff; } 30 31 this() 32 { 33 reset(); 34 } 35 36 void reset() 37 { 38 errorTextStream = appender!string(); 39 numberTests = 0; 40 numberErrors = 0; 41 } 42 43 private void consumeResults(string file, size_t lineNo, bool pass, string diffText ) 44 { 45 numberTests++; 46 47 if ( !pass ) 48 { 49 if ( !soft ) 50 { 51 assert(pass, diffText); 52 } 53 else 54 { 55 numberErrors++; 56 errorTextStream.put("\n"); 57 errorTextStream.put( "---------------------------\n"); 58 errorTextStream.put(format("Assertion failed at %s(%s):\n",file,lineNo)); 59 errorTextStream.put(diffText); 60 } 61 } 62 } 63 64 private bool runTest(string file, size_t lineNo, 65 string textToParse, string desiredTreeRepresentation, bool invert ) 66 { 67 auto treeGot = grammar.decimateTree(mixin("grammar."~startSymbol~"(textToParse)")); 68 auto treeChecker = TesterGrammar.decimateTree(TesterGrammar.Root(desiredTreeRepresentation)); 69 70 /+writefln("%s",treeGot); 71 writefln(""); 72 writefln("%s",treeChecker); 73 writefln("");+/ 74 75 if ( !treeGot.successful ) 76 consumeResults(file, lineNo, false, 77 grammar.stringof ~ " failed to parse (left-hand-side). Details:\n" ~ treeGot.toString()); 78 79 if ( !treeChecker.successful ) 80 consumeResults(file, lineNo, false, 81 "Failed to parse test expectation (right-hand-side). Details:\n" ~ treeChecker.toString()); 82 83 if ( treeGot.successful && treeChecker.successful ) 84 { 85 auto ctx = getDifferencer(treeGot, treeChecker); 86 latestDiff = ctx.diff(); 87 88 bool pass; 89 if ( invert ) 90 pass = (ctx.differences != 0); 91 else 92 pass = (ctx.differences == 0); 93 94 consumeResults(file, lineNo, pass, latestDiff); 95 return pass; 96 } 97 else 98 return false; 99 } 100 101 bool assertSimilar(string file = __FILE__, size_t lineNo = __LINE__) 102 ( string textToParse, string desiredTreeRepresentation ) 103 { 104 return runTest(file, lineNo, textToParse, desiredTreeRepresentation, false); 105 } 106 107 bool assertDifferent(string file = __FILE__, size_t lineNo = __LINE__) 108 ( string textToParse, string desiredTreeRepresentation ) 109 { 110 return runTest(file, lineNo, textToParse, desiredTreeRepresentation, true); 111 } 112 113 static auto getDifferencer(T,P)( auto ref T treeRoot, auto ref P patternRoot ) 114 { 115 Differencer!(T,P) t; 116 t.treeRoot = &treeRoot; 117 t.patternRoot = &patternRoot; 118 return t; 119 } 120 } 121 122 private struct Differencer(T,P) 123 { 124 size_t level = 0; 125 size_t differences = 0; 126 const(T)* treeRoot; 127 const(P)* patternRoot; 128 size_t max1stColumnWidth = 0; 129 Appender!(string[]) leftColumn; 130 Appender!(char[]) centerLine; 131 Appender!(string[]) rightColumn; 132 133 alias T TParseTree; 134 alias P PParseTree; 135 136 private string diff() 137 { 138 level = 0; 139 differences = 0; 140 max1stColumnWidth = 0; 141 142 leftColumn = appender!(string[])(); 143 centerLine = appender!(char[])(); 144 rightColumn = appender!(string[])(); 145 146 printColumns(" Nodes Found",'|'," Nodes Expected"); 147 printColumns("", '|',""); 148 149 diffNode(treeRoot,patternRoot); 150 151 string[] lcolumn = leftColumn.data; 152 char[] center = centerLine.data; 153 string[] rcolumn = rightColumn.data; 154 auto diffText = appender!string(); 155 for ( size_t i = 0; i < lcolumn.length; i++ ) 156 diffText.put(format("\n%-*s %c %s", max1stColumnWidth, 157 lcolumn[i], center[i], rcolumn[i]).stripRight()); 158 diffText.put("\n"); 159 160 return diffText.data; 161 } 162 163 private void diffNode( const(T*) node, const(P*) pattern ) 164 { 165 assert(pattern); 166 switch ( pattern.name ) 167 { 168 case "TesterGrammar.Root": 169 diffNode(node, &pattern.children[0]); 170 break; 171 172 case "TesterGrammar.Node": 173 174 lineDiff(node,pattern); 175 176 // These will track which node children we have visited. 177 size_t cursor = 0; 178 bool[] visited = []; 179 if ( node ) visited = new bool[node.children.length]; 180 foreach ( ref flag; visited ) flag = false; 181 182 // This will handle nodes that we expect to find as 183 // children of this one. If these nodes don't exist, 184 // then we throw out angry diff lines about them. 185 string uniformBranchType = null; 186 foreach ( branch; pattern.children ) 187 { 188 // Enforce uniform branch types. 189 if ( uniformBranchType == null ) 190 uniformBranchType = branch.name; 191 else if ( branch.name != uniformBranchType ) 192 throw new Exception( 193 "It is forbidden to have both unordered and "~ 194 "ordered branches from the same node."); 195 196 // Note that we don't dereference the node's children: 197 // the pattern child will be a Branch statement and 198 // it will do the dereference for us. 199 diffBranch(node, &branch, visited, cursor); 200 } 201 202 // This will handle nodes that the pattern doesn't expect 203 // to find, but that we find anyways. Anything here 204 // deserves a nice angry diff line about the extra node. 205 level++; 206 if ( node ) foreach( i, childNode; node.children ) 207 { 208 // TODO: rescanning the visit array this is probably slow. 209 if ( visited[i] ) 210 continue; 211 212 traverseUnexpected(&node.children[i]); 213 visited[i] = true; 214 } 215 level--; 216 217 break; 218 219 default: assert(0, "Unexpected element in pattern: "~pattern.name); 220 } 221 } 222 223 private void traverseUnexpected( const(T*) node ) 224 { 225 assert(node); 226 lineDiff(node, null); 227 level++; 228 foreach( child; node.children ) 229 traverseUnexpected(&child); 230 level--; 231 } 232 233 private void diffBranch( const(T*) node, const(P*) pattern, bool[] visited, ref size_t cursor ) 234 { 235 assert(pattern); 236 switch ( pattern.name ) 237 { 238 239 case "TesterGrammar.OrderedBranch": 240 241 level++; 242 if ( node ) foreach ( patternChild; pattern.children ) 243 { 244 if ( cursor >= node.children.length ) 245 { 246 diffNode(null, &patternChild); 247 } 248 else 249 { 250 diffNode(&node.children[cursor], &patternChild); 251 visited[cursor] = true; 252 cursor++; 253 } 254 } 255 level--; 256 257 break; 258 259 case "TesterGrammar.UnorderedBranch": 260 261 level++; 262 if ( node ) foreach ( i, patternChild; pattern.children ) 263 { 264 size_t foundIndex = size_t.max; 265 266 // Cheap hack for now: 0(n^2) scan-in-scan. 267 // 268 // Ideally the parser will generate hints for locating 269 // submatches: 270 // GrammarName.childLocationHint( 271 // GrammarName.ruleId!(RuleName), 272 // GrammarName.ruleId!(ChildRuleName)); 273 // Think about it... it should be possible to compile 274 // this into integer table lookups, thus allowing us 275 // to do what a compiler does with an AST: vtable 276 // lookups followed by offset indexing to typed 277 // fields which are sometimes (typed) arrays. 278 // At that point things stay nice and dynamic while 279 // being just as performant as oldschool methods. 280 // 281 foreach ( j, nodeChild; node.children ) 282 { 283 // TODO: rescanning the visit array this is probably slow. 284 if ( visited[j] ) 285 continue; 286 287 if ( checkIdentifierMatch( &nodeChild, &patternChild ) ) 288 { 289 foundIndex = j; 290 break; 291 } 292 } 293 294 if ( foundIndex == size_t.max ) 295 diffNode(null, &patternChild); 296 else 297 { 298 diffNode(&node.children[foundIndex], &patternChild); 299 visited[foundIndex] = true; 300 } 301 } 302 level--; 303 304 break; 305 306 default: assert(0, "Unexpected element in pattern: "~pattern.name); 307 } 308 } 309 310 private bool checkIdentifierMatch(const(T*) node, const(P*) pattern) 311 { 312 if ( !node || !pattern ) 313 return false; 314 315 auto splitNode = retro(splitter(node.name,'.')); 316 auto splitPattern = retro(splitter(pattern.matches[0],'.')); 317 318 while ( true ) 319 { 320 auto nodePathElem = splitNode.front; 321 auto patternPathElem = splitPattern.front; 322 splitNode.popFront(); 323 splitPattern.popFront(); 324 325 if ( nodePathElem != patternPathElem ) 326 return false; 327 328 if ( splitNode.empty || splitPattern.empty ) 329 break; 330 } 331 332 return true; 333 } 334 335 private void lineDiff(const(T*) node, const(P*) expected) 336 { 337 if ( !checkIdentifierMatch( node, expected ) ) 338 { 339 differences++; 340 printNodes(node, expected, true); 341 } 342 else 343 printNodes(node, expected, false); 344 } 345 346 private string getNodeTxt(const(T*) node) 347 { 348 string nodeTxt = ""; 349 if ( node ) 350 nodeTxt = node.name; 351 return replicate(" ",level) ~ nodeTxt; 352 } 353 354 private string getPatternTxt(const(P*) pattern) 355 { 356 357 string patternTxt = ""; 358 if ( pattern ) 359 patternTxt = pattern.matches[0]; 360 return replicate(" ",level) ~ patternTxt; 361 } 362 363 private void printNodes(const(T*) node, const(P*) pattern, bool different) 364 { 365 auto nodeTxt = getNodeTxt(node); 366 auto patternTxt = getPatternTxt(pattern); 367 368 char diffLine = '='; 369 if ( !node && pattern ) 370 diffLine = '>'; 371 else if ( node && !pattern ) 372 diffLine = '<'; 373 else if ( different ) 374 diffLine = '|'; 375 376 printColumns( nodeTxt, diffLine, patternTxt ); 377 } 378 379 private void printColumns( string ltext, char center, string rtext ) 380 { 381 max1stColumnWidth = max(max1stColumnWidth, ltext.length); 382 leftColumn.put(ltext); 383 centerLine.put(center); 384 rightColumn.put(rtext); 385 } 386 } 387 388 unittest 389 { 390 string normalizeStr(string str) 391 { 392 return removechars(str," \t").replace("\r","\n").squeeze("\n"); 393 } 394 395 auto tester = new GrammarTester!(TesterGrammar, "Root"); 396 tester.soft = true; 397 398 tester.assertSimilar(`foo`, `Root->Node`); 399 tester.assertDifferent(`foo`, `Root`); 400 401 tester.assertSimilar(`foo->bar`, `Root->Node->OrderedBranch->Node`); 402 tester.assertDifferent(`foo->bar`, `Root->Node`); 403 404 assert(normalizeStr(tester.latestDiff) == 405 normalizeStr(` 406 Nodes Found | Nodes Expected 407 | 408 TesterGrammar.Root = Root 409 TesterGrammar.Node = Node 410 TesterGrammar.OrderedBranch < 411 TesterGrammar.Node < 412 `)); 413 414 tester.assertSimilar(`foo->{bar}`, `Root->Node->OrderedBranch->Node`); 415 tester.assertDifferent(`foo->{bar}`, `Root->Node->UnorderedBranch->Node`); 416 417 tester.assertSimilar(`foo~>{bar}`, `Root->Node->UnorderedBranch->Node`); 418 419 tester.assertSimilar(`foo~>bar`, `Root->Node->UnorderedBranch->Node`); 420 421 tester.assertSimilar(`foo->bar->baz`, 422 `Root->Node 423 ->OrderedBranch->Node 424 ->OrderedBranch->Node 425 `); 426 427 tester.assertSimilar(`foo~>bar~>baz`, 428 `Root->Node 429 ->UnorderedBranch->Node 430 ->UnorderedBranch->Node 431 `); 432 433 tester.assertSimilar(`foo->^bar->baz`, 434 `Root->Node-> 435 { 436 OrderedBranch->Node 437 OrderedBranch->Node 438 } 439 `); 440 441 tester.assertSimilar(`foo->{^bar}->baz`, 442 `Root->Node-> 443 { 444 OrderedBranch->Node 445 OrderedBranch->Node 446 } 447 `); 448 449 tester.assertSimilar(`foo~>^bar~>baz`, 450 `Root->Node-> 451 { 452 UnorderedBranch->Node 453 UnorderedBranch->Node 454 } 455 `); 456 457 tester.assertSimilar(`foo~>{^bar}~>baz`, 458 `Root->Node-> 459 { 460 UnorderedBranch->Node 461 UnorderedBranch->Node 462 } 463 `); 464 465 tester.assertSimilar(` 466 FOO~> 467 { 468 Bar->baz 469 } 470 `,` 471 /* FOO */ Root->Node 472 /* Bar */ ->UnorderedBranch->Node 473 /* baz */ ->OrderedBranch->Node 474 `); 475 476 tester.assertSimilar(` 477 FOO~> 478 { 479 x->^y->^z 480 u 481 v 482 Bar->baz 483 } 484 `,` 485 /* FOO */ Root->Node-> 486 /* */ UnorderedBranch-> 487 /* */ { 488 /* x */ Node~> 489 /* */ { 490 /* y */ OrderedBranch->Node 491 /* z */ OrderedBranch->Node 492 /* */ } 493 /* u */ Node 494 /* v */ Node 495 /* Bar */ Node->OrderedBranch 496 /* baz */ ->Node 497 /* */ } 498 `); 499 500 tester.assertDifferent(` 501 FOO~> 502 { 503 x->^y->^z 504 u 505 v 506 Bar->baz 507 } 508 `,` 509 /* FOO */ Root->Node-> 510 /* */ UnorderedBranch-> 511 /* */ { 512 /* x */ Node->OrderedBranch~> 513 /* */ { // There should actually be two ordered branches. 514 /* y */ Node 515 /*bug:z*/ Node 516 /*bug:z*/ Node 517 /* */ } 518 /* */ } 519 `); 520 521 assert(normalizeStr(tester.latestDiff) == 522 normalizeStr(` 523 Nodes Found | Nodes Expected 524 | 525 TesterGrammar.Root = Root 526 TesterGrammar.Node = Node 527 TesterGrammar.UnorderedBranch = UnorderedBranch 528 TesterGrammar.Node = Node 529 TesterGrammar.OrderedBranch = OrderedBranch 530 TesterGrammar.Node = Node 531 > Node 532 > Node 533 TesterGrammar.OrderedBranch < 534 TesterGrammar.Node < 535 TesterGrammar.Node < 536 TesterGrammar.Node < 537 TesterGrammar.Node < 538 TesterGrammar.OrderedBranch < 539 TesterGrammar.Node < 540 `)); 541 542 assert(tester.testCount > 0); 543 assert(tester.errorCount == 0); 544 assert(tester.errorText == ""); 545 546 547 tester = new GrammarTester!(TesterGrammar, "Root"); 548 tester.soft = true; 549 550 tester.assertSimilar(` 551 FOO 552 { 553 x->^y->^z 554 u 555 v 556 Bar->baz 557 } 558 `,` 559 /* FOO */ Root->Node-> 560 /* */ UnorderedBranch-> 561 /* */ { 562 /* x */ Node->OrderedBranch~> 563 /* */ { // There should actually be two ordered branches. 564 /* y */ Node 565 /*not z*/ Node 566 /*not z*/ Node 567 /* */ } 568 /* */ } 569 `); 570 571 assert(tester.testCount == 1); 572 assert(tester.errorCount == 1); 573 assert(tester.errorText.length > 0); 574 } 575 576 unittest 577 { 578 mixin(grammar(` 579 Arithmetic: 580 Term < Factor (Add / Sub)* 581 Add < "+" Factor 582 Sub < "-" Factor 583 Factor < Primary (Mul / Div)* 584 Mul < "*" Primary 585 Div < "/" Primary 586 Primary < Parens / Neg / Number / Variable 587 Parens < :"(" Term :")" 588 Neg < "-" Primary 589 Number < ~([0-9]+) 590 Variable <- identifier 591 `)); 592 593 auto arithmeticTester = new GrammarTester!(Arithmetic, "Term"); 594 595 arithmeticTester.assertSimilar(`1 + 3`, 596 ` 597 Term-> 598 { 599 Factor->Primary->Number 600 Add-> 601 Factor->Primary->Number 602 } 603 `); 604 605 arithmeticTester.assertSimilar(`1*2 + 3/4`, 606 ` 607 Term-> 608 { 609 Factor-> 610 { 611 Primary->Number 612 Mul~> 613 Primary->Number 614 } 615 Add-> 616 Factor-> 617 { 618 Primary->Number 619 Div~> 620 Primary->Number 621 } 622 } 623 `); 624 } 625 626 /+ For reference: 627 628 +/ 629 630