// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. //#define PARSER_TRACE using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Web.Razor.Generator; using System.Web.Razor.Parser; using System.Web.Razor.Parser.SyntaxTree; using System.Web.Razor.Text; using Xunit; namespace System.Web.Razor.Test.Framework { public abstract class ParserTestBase { protected static Block IgnoreOutput = new IgnoreOutputBlock(); public SpanFactory Factory { get; private set; } protected ParserTestBase() { Factory = CreateSpanFactory(); } public abstract ParserBase CreateMarkupParser(); public abstract ParserBase CreateCodeParser(); protected abstract ParserBase SelectActiveParser(ParserBase codeParser, ParserBase markupParser); public virtual ParserContext CreateParserContext(ITextDocument input, ParserBase codeParser, ParserBase markupParser) { return new ParserContext(input, codeParser, markupParser, SelectActiveParser(codeParser, markupParser)); } protected abstract SpanFactory CreateSpanFactory(); protected virtual void ParseBlockTest(string document) { ParseBlockTest(document, null, false, new RazorError[0]); } protected virtual void ParseBlockTest(string document, bool designTimeParser) { ParseBlockTest(document, null, designTimeParser, new RazorError[0]); } protected virtual void ParseBlockTest(string document, params RazorError[] expectedErrors) { ParseBlockTest(document, false, expectedErrors); } protected virtual void ParseBlockTest(string document, bool designTimeParser, params RazorError[] expectedErrors) { ParseBlockTest(document, null, designTimeParser, expectedErrors); } protected virtual void ParseBlockTest(string document, Block expectedRoot) { ParseBlockTest(document, expectedRoot, false, null); } protected virtual void ParseBlockTest(string document, Block expectedRoot, bool designTimeParser) { ParseBlockTest(document, expectedRoot, designTimeParser, null); } protected virtual void ParseBlockTest(string document, Block expectedRoot, params RazorError[] expectedErrors) { ParseBlockTest(document, expectedRoot, false, expectedErrors); } protected virtual void ParseBlockTest(string document, Block expectedRoot, bool designTimeParser, params RazorError[] expectedErrors) { RunParseTest(document, parser => parser.ParseBlock, expectedRoot, (expectedErrors ?? new RazorError[0]).ToList(), designTimeParser); } protected virtual void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any) { SingleSpanBlockTest(document, blockType, spanType, acceptedCharacters, expectedError: null); } protected virtual void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any) { SingleSpanBlockTest(document, spanContent, blockType, spanType, acceptedCharacters, expectedErrors: null); } protected virtual void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, params RazorError[] expectedError) { SingleSpanBlockTest(document, document, blockType, spanType, expectedError); } protected virtual void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, params RazorError[] expectedErrors) { SingleSpanBlockTest(document, spanContent, blockType, spanType, AcceptedCharacters.Any, expectedErrors ?? new RazorError[0]); } protected virtual void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedError) { SingleSpanBlockTest(document, document, blockType, spanType, acceptedCharacters, expectedError); } protected virtual void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedErrors) { BlockBuilder builder = new BlockBuilder(); builder.Type = blockType; ParseBlockTest( document, ConfigureAndAddSpanToBlock( builder, Factory.Span(spanType, spanContent, spanType == SpanKind.Markup) .Accepts(acceptedCharacters)), expectedErrors ?? new RazorError[0]); } protected virtual ParserResults RunParse(string document, Func parserActionSelector, bool designTimeParser) { // Create the source ParserResults results = null; using (SeekableTextReader reader = new SeekableTextReader(document)) { try { ParserBase codeParser = CreateCodeParser(); ParserBase markupParser = CreateMarkupParser(); ParserContext context = CreateParserContext(reader, codeParser, markupParser); context.DesignTimeMode = designTimeParser; codeParser.Context = context; markupParser.Context = context; // Run the parser parserActionSelector(context.ActiveParser)(); results = context.CompleteParse(); } finally { if (results != null && results.Document != null) { WriteTraceLine(String.Empty); WriteTraceLine("Actual Parse Tree:"); WriteNode(0, results.Document); } } } return results; } protected virtual void RunParseTest(string document, Func parserActionSelector, Block expectedRoot, IList expectedErrors, bool designTimeParser) { // Create the source ParserResults results = RunParse(document, parserActionSelector, designTimeParser); // Evaluate the results if (!ReferenceEquals(expectedRoot, IgnoreOutput)) { EvaluateResults(results, expectedRoot, expectedErrors); } } [Conditional("PARSER_TRACE")] private void WriteNode(int indent, SyntaxTreeNode node) { string content = node.ToString().Replace("\r", "\\r") .Replace("\n", "\\n") .Replace("{", "{{") .Replace("}", "}}"); if (indent > 0) { content = new String('.', indent * 2) + content; } WriteTraceLine(content); Block block = node as Block; if (block != null) { foreach (SyntaxTreeNode child in block.Children) { WriteNode(indent + 1, child); } } } public static void EvaluateResults(ParserResults results, Block expectedRoot) { EvaluateResults(results, expectedRoot, null); } public static void EvaluateResults(ParserResults results, Block expectedRoot, IList expectedErrors) { EvaluateParseTree(results.Document, expectedRoot); EvaluateRazorErrors(results.ParserErrors, expectedErrors); } public static void EvaluateParseTree(Block actualRoot, Block expectedRoot) { // Evaluate the result ErrorCollector collector = new ErrorCollector(); // Link all the nodes expectedRoot.LinkNodes(); if (expectedRoot == null) { Assert.Null(actualRoot); } else { Assert.NotNull(actualRoot); EvaluateSyntaxTreeNode(collector, actualRoot, expectedRoot); if (collector.Success) { WriteTraceLine("Parse Tree Validation Succeeded:\r\n{0}", collector.Message); } else { Assert.True(false, String.Format("\r\n{0}", collector.Message)); } } } private static void EvaluateSyntaxTreeNode(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected) { if (actual == null) { AddNullActualError(collector, actual, expected); } if (actual.IsBlock != expected.IsBlock) { AddMismatchError(collector, actual, expected); } else { if (expected.IsBlock) { EvaluateBlock(collector, (Block)actual, (Block)expected); } else { EvaluateSpan(collector, (Span)actual, (Span)expected); } } } private static void EvaluateSpan(ErrorCollector collector, Span actual, Span expected) { if (!Equals(expected, actual)) { AddMismatchError(collector, actual, expected); } else { AddPassedMessage(collector, expected); } } private static void EvaluateBlock(ErrorCollector collector, Block actual, Block expected) { if (actual.Type != expected.Type || !expected.CodeGenerator.Equals(actual.CodeGenerator)) { AddMismatchError(collector, actual, expected); } else { AddPassedMessage(collector, expected); using (collector.Indent()) { IEnumerator expectedNodes = expected.Children.GetEnumerator(); IEnumerator actualNodes = actual.Children.GetEnumerator(); while (expectedNodes.MoveNext()) { if (!actualNodes.MoveNext()) { collector.AddError("{0} - FAILED :: No more elements at this node", expectedNodes.Current); } else { EvaluateSyntaxTreeNode(collector, actualNodes.Current, expectedNodes.Current); } } while (actualNodes.MoveNext()) { collector.AddError("End of Node - FAILED :: Found Node: {0}", actualNodes.Current); } } } } private static void AddPassedMessage(ErrorCollector collector, SyntaxTreeNode expected) { collector.AddMessage("{0} - PASSED", expected); } private static void AddMismatchError(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected) { collector.AddError("{0} - FAILED :: Actual: {1}", expected, actual); } private static void AddNullActualError(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected) { collector.AddError("{0} - FAILED :: Actual: << Null >>", expected); } public static void EvaluateRazorErrors(IList actualErrors, IList expectedErrors) { // Evaluate the errors if (expectedErrors == null || expectedErrors.Count == 0) { Assert.True(actualErrors.Count == 0, String.Format("Expected that no errors would be raised, but the following errors were:\r\n{0}", FormatErrors(actualErrors))); } else { Assert.True(expectedErrors.Count == actualErrors.Count, String.Format("Expected that {0} errors would be raised, but {1} errors were.\r\nExpected Errors: \r\n{2}\r\nActual Errors: \r\n{3}", expectedErrors.Count, actualErrors.Count, FormatErrors(expectedErrors), FormatErrors(actualErrors))); Assert.Equal(expectedErrors.ToArray(), actualErrors.ToArray()); } WriteTraceLine("Expected Errors were raised:\r\n{0}", FormatErrors(expectedErrors)); } public static string FormatErrors(IList errors) { if (errors == null) { return "\t<< No Errors >>"; } StringBuilder builder = new StringBuilder(); foreach (RazorError err in errors) { builder.AppendFormat("\t{0}", err); builder.AppendLine(); } return builder.ToString(); } [Conditional("PARSER_TRACE")] private static void WriteTraceLine(string format, params object[] args) { Trace.WriteLine(String.Format(format, args)); } protected virtual Block CreateSimpleBlockAndSpan(string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any) { SpanConstructor span = Factory.Span(spanType, spanContent, spanType == SpanKind.Markup).Accepts(acceptedCharacters); BlockBuilder b = new BlockBuilder() { Type = blockType }; return ConfigureAndAddSpanToBlock(b, span); } protected virtual Block ConfigureAndAddSpanToBlock(BlockBuilder block, SpanConstructor span) { switch (block.Type) { case BlockType.Markup: span.With(new MarkupCodeGenerator()); break; case BlockType.Statement: span.With(new StatementCodeGenerator()); break; case BlockType.Expression: block.CodeGenerator = new ExpressionCodeGenerator(); span.With(new ExpressionCodeGenerator()); break; } block.Children.Add(span); return block.Build(); } private class IgnoreOutputBlock : Block { public IgnoreOutputBlock() : base(BlockType.Template, Enumerable.Empty(), null) { } } } }