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