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