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