First step in converting parser tests to use baselines

- Added the necessary infrastructure to serialize and verify the syntax
tree
- Updated ParserVisitor to be overrideable
- Made CSharpVerbatimBlockTest to use baselines
This commit is contained in:
Ajay Bhargav Baaskaran 2018-06-06 03:21:16 -07:00
parent 620105cc5e
commit b821ce8b8e
20 changed files with 880 additions and 126 deletions

View File

@ -47,10 +47,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
public void Accept(ParserVisitor visitor, Block block)
{
for (var i = 0; i < block.Children.Count; i++)
{
block.Children[i].Accept(visitor);
}
visitor.VisitDefault(block);
}
}
}

View File

@ -5,7 +5,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
internal abstract class ParserVisitor
{
protected virtual void VisitDefault(Block block)
public virtual void Visit(SyntaxTreeNode node)
{
node.Accept(this);
}
public virtual void VisitDefault(Block block)
{
for (var i = 0; i < block.Children.Count; i++)
{
@ -13,6 +18,10 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
}
public virtual void VisitDefault(Span span)
{
}
public virtual void VisitBlock(Block block)
{
if (block.ChunkGenerator != null)
@ -44,35 +53,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
VisitDefault(block);
}
public virtual void VisitExpressionSpan(ExpressionChunkGenerator chunkGenerator, Span span)
{
}
public virtual void VisitMarkupSpan(MarkupChunkGenerator chunkGenerator, Span span)
{
}
public virtual void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Span span)
{
}
public virtual void VisitStatementSpan(StatementChunkGenerator chunkGenerator, Span span)
{
}
public virtual void VisitLiteralAttributeSpan(LiteralAttributeChunkGenerator chunkGenerator, Span span)
{
}
public virtual void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span block)
{
}
public virtual void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block)
{
VisitDefault(block);
}
public virtual void VisitTemplateBlock(TemplateBlockChunkGenerator chunkGenerator, Block block)
{
VisitDefault(block);
@ -88,16 +68,54 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
VisitDefault(block);
}
public virtual void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block)
{
VisitDefault(block);
}
public virtual void VisitExpressionSpan(ExpressionChunkGenerator chunkGenerator, Span span)
{
VisitDefault(span);
}
public virtual void VisitMarkupSpan(MarkupChunkGenerator chunkGenerator, Span span)
{
VisitDefault(span);
}
public virtual void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Span span)
{
VisitDefault(span);
}
public virtual void VisitStatementSpan(StatementChunkGenerator chunkGenerator, Span span)
{
VisitDefault(span);
}
public virtual void VisitLiteralAttributeSpan(LiteralAttributeChunkGenerator chunkGenerator, Span span)
{
VisitDefault(span);
}
public virtual void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span)
{
VisitDefault(span);
}
public virtual void VisitAddTagHelperSpan(AddTagHelperChunkGenerator chunkGenerator, Span span)
{
VisitDefault(span);
}
public virtual void VisitRemoveTagHelperSpan(RemoveTagHelperChunkGenerator chunkGenerator, Span span)
{
VisitDefault(span);
}
public virtual void VisitTagHelperPrefixDirectiveSpan(TagHelperPrefixDirectiveChunkGenerator chunkGenerator, Span span)
{
VisitDefault(span);
}
}
}

View File

@ -32,6 +32,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
public void Accept(ParserVisitor visitor, Span span)
{
visitor.VisitDefault(span);
}
public void GenerateChunk(Span target, ChunkGeneratorContext context)

View File

@ -7,6 +7,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
public class CSharpReservedWordsTest : CsHtmlCodeParserTestBase
{
public CSharpReservedWordsTest()
{
UseBaselineTests = true;
}
[Theory]
[InlineData("namespace")]
[InlineData("class")]

View File

@ -10,125 +10,45 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
private const string TestExtraKeyword = "model";
public CSharpVerbatimBlockTest()
{
UseBaselineTests = true;
}
[Fact]
public void VerbatimBlock()
{
ParseBlockTest("@{ foo(); }",
new StatementBlock(
Factory.CodeTransition(),
Factory.MetaCode("{")
.Accepts(AcceptedCharactersInternal.None),
Factory.Code(" foo(); ")
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
Factory.MetaCode("}")
.Accepts(AcceptedCharactersInternal.None)
));
ParseBlockTest("@{ foo(); }");
}
[Fact]
public void InnerImplicitExpressionWithOnlySingleAtOutputsZeroLengthCodeSpan()
{
ParseBlockTest("{@}",
new StatementBlock(
Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
Factory.EmptyCSharp()
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
new ExpressionBlock(
Factory.CodeTransition(),
Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharactersInternal.NonWhiteSpace)
),
Factory.EmptyCSharp().AsStatement(),
Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
designTime: true,
expectedErrors: new[]
{
RazorDiagnosticFactory.CreateParsing_UnexpectedCharacterAtStartOfCodeBlock(
new SourceSpan(new SourceLocation(2, 0, 2), contentLength: 1),
"}")
});
ParseBlockTest("{@}");
}
[Fact]
public void InnerImplicitExpressionDoesNotAcceptDotAfterAt()
{
ParseBlockTest("{@.}",
new StatementBlock(
Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
Factory.EmptyCSharp()
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
new ExpressionBlock(
Factory.CodeTransition(),
Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharactersInternal.NonWhiteSpace)
),
Factory.Code(".").AsStatement(),
Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
designTime: true,
expectedErrors: new[]
{
RazorDiagnosticFactory.CreateParsing_UnexpectedCharacterAtStartOfCodeBlock(
new SourceSpan(new SourceLocation(2, 0, 2), contentLength: 1),
".")
});
ParseBlockTest("{@.}");
}
[Fact]
public void InnerImplicitExpressionWithOnlySingleAtAcceptsSingleSpaceOrNewlineAtDesignTime()
{
ParseBlockTest("{" + Environment.NewLine
+ " @" + Environment.NewLine
+ "}",
new StatementBlock(
Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
Factory.Code(Environment.NewLine + " ")
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
new ExpressionBlock(
Factory.CodeTransition(),
Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharactersInternal.NonWhiteSpace)
),
Factory.Code(Environment.NewLine).AsStatement(),
Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
/* designTimeParser */ true,
RazorDiagnosticFactory.CreateParsing_UnexpectedWhiteSpaceAtStartOfCodeBlock(
new SourceSpan(new SourceLocation(6 + Environment.NewLine.Length, 1, 5), Environment.NewLine.Length)));
ParseBlockTest("{" + Environment.NewLine + " @" + Environment.NewLine + "}", designTime: true);
}
[Fact]
public void InnerImplicitExpressionDoesNotAcceptTrailingNewlineInRunTimeMode()
{
ParseBlockTest("{@foo." + Environment.NewLine
+ "}",
new StatementBlock(
Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
Factory.EmptyCSharp()
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
new ExpressionBlock(
Factory.CodeTransition(),
Factory.Code("foo.").AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
Factory.Code(Environment.NewLine).AsStatement(),
Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)));
ParseBlockTest("{@foo." + Environment.NewLine + "}");
}
[Fact]
public void InnerImplicitExpressionAcceptsTrailingNewlineInDesignTimeMode()
{
ParseBlockTest("{@foo." + Environment.NewLine
+ "}",
new StatementBlock(
Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
Factory.EmptyCSharp()
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
new ExpressionBlock(
Factory.CodeTransition(),
Factory.Code("foo.").AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
Factory.Code(Environment.NewLine).AsStatement(),
Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
designTime: true);
ParseBlockTest("{@foo." + Environment.NewLine + "}", designTime: true);
}
}
}

View File

@ -0,0 +1,8 @@
Statement block - NullParentChunkGenerator - 9 - (0:0,0)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (0:0,0)
Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[<null>];AtEOL - - (1:0,1)
Expression block - ExpressionChunkGenerator - 5 - (1:0,1)
Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (1:0,1)
Code span - ExpressionChunkGenerator - ImplicitExpressionEditHandler;Accepts:NonWhiteSpace;ImplicitExpression[ATD];K14 - foo. - (2:0,2)
Code span - StatementChunkGenerator - SpanEditHandler;Accepts:Any - LF - (6:0,6)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (8:1,0)

View File

@ -0,0 +1 @@
(1,3): Error RZ1005: "." is not valid at the start of a code block. Only identifiers, keywords, comments, "(" and "{" are valid.

View File

@ -0,0 +1,8 @@
Statement block - NullParentChunkGenerator - 4 - (0:0,0)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (0:0,0)
Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[<null>];AtEOL - - (1:0,1)
Expression block - ExpressionChunkGenerator - 1 - (1:0,1)
Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (1:0,1)
Code span - ExpressionChunkGenerator - ImplicitExpressionEditHandler;Accepts:NonWhiteSpace;ImplicitExpression[ATD];K14 - - (2:0,2)
Code span - StatementChunkGenerator - SpanEditHandler;Accepts:Any - . - (2:0,2)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (3:0,3)

View File

@ -0,0 +1,8 @@
Statement block - NullParentChunkGenerator - 9 - (0:0,0)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (0:0,0)
Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[<null>];AtEOL - - (1:0,1)
Expression block - ExpressionChunkGenerator - 5 - (1:0,1)
Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (1:0,1)
Code span - ExpressionChunkGenerator - ImplicitExpressionEditHandler;Accepts:NonWhiteSpace;ImplicitExpression[ATD];K14 - foo. - (2:0,2)
Code span - StatementChunkGenerator - SpanEditHandler;Accepts:Any - LF - (6:0,6)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (8:1,0)

View File

@ -0,0 +1 @@
(2,6): Error RZ1003: A space or line break was encountered after the "@" character. Only valid identifiers, keywords, comments, "(" and "{" are valid at the start of a code block and they must occur immediately following "@" with no space in between.

View File

@ -0,0 +1,8 @@
Statement block - NullParentChunkGenerator - 11 - (0:0,0)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (0:0,0)
Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[<null>];AtEOL - LF - (1:0,1)
Expression block - ExpressionChunkGenerator - 1 - (7:1,4)
Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (7:1,4)
Code span - ExpressionChunkGenerator - ImplicitExpressionEditHandler;Accepts:NonWhiteSpace;ImplicitExpression[ATD];K14 - - (8:1,5)
Code span - StatementChunkGenerator - SpanEditHandler;Accepts:Any - LF - (8:1,5)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (10:2,0)

View File

@ -0,0 +1 @@
(1,3): Error RZ1005: "}" is not valid at the start of a code block. Only identifiers, keywords, comments, "(" and "{" are valid.

View File

@ -0,0 +1,8 @@
Statement block - NullParentChunkGenerator - 3 - (0:0,0)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (0:0,0)
Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[<null>];AtEOL - - (1:0,1)
Expression block - ExpressionChunkGenerator - 1 - (1:0,1)
Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (1:0,1)
Code span - ExpressionChunkGenerator - ImplicitExpressionEditHandler;Accepts:NonWhiteSpace;ImplicitExpression[ATD];K14 - - (2:0,2)
Code span - StatementChunkGenerator - SpanEditHandler;Accepts:Any - - (2:0,2)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (2:0,2)

View File

@ -0,0 +1,5 @@
Statement block - NullParentChunkGenerator - 11 - (0:0,0)
Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (0:0,0)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (1:0,1)
Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[<null>];AtEOL - foo(); - (2:0,2)
MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (10:0,10)

View File

@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Reflection;
using Xunit;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
public class IntializeTestFileAttribute : BeforeAfterTestAttribute
{
public override void Before(MethodInfo methodUnderTest)
{
if (typeof(ParserTestBase).GetTypeInfo().IsAssignableFrom(methodUnderTest.DeclaringType.GetTypeInfo()))
{
var typeName = methodUnderTest.DeclaringType.Name;
ParserTestBase.FileName = $"TestFiles/ParserTests/{typeName}/{methodUnderTest.Name}";
ParserTestBase.IsTheory = false;
if (methodUnderTest.GetCustomAttributes(typeof(TheoryAttribute), inherit: false).Length > 0)
{
ParserTestBase.IsTheory = true;
}
}
}
public override void After(MethodInfo methodUnderTest)
{
if (typeof(ParserTestBase).GetTypeInfo().IsAssignableFrom(methodUnderTest.DeclaringType.GetTypeInfo()))
{
ParserTestBase.FileName = null;
ParserTestBase.IsTheory = false;
}
}
}
}

View File

@ -4,20 +4,36 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
#if NET46
using System.Runtime.Remoting;
using System.Runtime.Remoting.Messaging;
#else
using System.Threading;
#endif
using System.Text;
using Xunit;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
[IntializeTestFile]
public abstract class ParserTestBase
{
#if !NET46
private static readonly AsyncLocal<string> _fileName = new AsyncLocal<string>();
private static readonly AsyncLocal<bool> _isTheory = new AsyncLocal<bool>();
#endif
internal static Block IgnoreOutput = new IgnoreOutputBlock();
internal ParserTestBase()
{
Factory = CreateSpanFactory();
BlockFactory = CreateBlockFactory();
TestProjectRoot = TestProject.GetProjectDirectory(GetType());
}
/// <summary>
@ -30,6 +46,105 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
internal BlockFactory BlockFactory { get; private set; }
#if GENERATE_BASELINES
protected bool GenerateBaselines { get; set; } = true;
#else
protected bool GenerateBaselines { get; set; } = false;
#endif
protected string TestProjectRoot { get; }
protected bool UseBaselineTests { get; set; }
// Used by the test framework to set the 'base' name for test files.
public static string FileName
{
#if NET46
get
{
var handle = (ObjectHandle)CallContext.LogicalGetData("ParserTestBase_FileName");
return (string)handle.Unwrap();
}
set
{
CallContext.LogicalSetData("ParserTestBase_FileName", new ObjectHandle(value));
}
#elif NETCOREAPP2_2
get { return _fileName.Value; }
set { _fileName.Value = value; }
#endif
}
public static bool IsTheory
{
#if NET46
get
{
var handle = (ObjectHandle)CallContext.LogicalGetData("ParserTestBase_IsTheory");
return (bool)handle.Unwrap();
}
set
{
CallContext.LogicalSetData("ParserTestBase_IsTheory", new ObjectHandle(value));
}
#elif NETCOREAPP2_2
get { return _isTheory.Value; }
set { _isTheory.Value = value; }
#endif
}
internal void AssertSyntaxTreeNodeMatchesBaseline(RazorSyntaxTree syntaxTree)
{
if (FileName == null)
{
var message = $"{nameof(AssertSyntaxTreeNodeMatchesBaseline)} should only be called from a parser test ({nameof(FileName)} is null).";
throw new InvalidOperationException(message);
}
var baselineFileName = Path.ChangeExtension(FileName, ".syntaxtree.txt");
var baselineDiagnosticsFileName = Path.ChangeExtension(FileName, ".diagnostics.txt");
var root = syntaxTree.Root;
var diagnostics = syntaxTree.Diagnostics;
if (GenerateBaselines)
{
var baselineFullPath = Path.Combine(TestProjectRoot, baselineFileName);
File.WriteAllText(baselineFullPath, SyntaxTreeNodeSerializer.Serialize(root));
var baselineDiagnosticsFullPath = Path.Combine(TestProjectRoot, baselineDiagnosticsFileName);
var lines = diagnostics.Select(RazorDiagnosticSerializer.Serialize).ToArray();
if (lines.Any())
{
File.WriteAllLines(baselineDiagnosticsFullPath, lines);
}
else if (File.Exists(baselineDiagnosticsFullPath))
{
File.Delete(baselineDiagnosticsFullPath);
}
return;
}
var stFile = TestFile.Create(baselineFileName, GetType().GetTypeInfo().Assembly);
if (!stFile.Exists())
{
throw new XunitException($"The resource {baselineFileName} was not found.");
}
var baseline = stFile.ReadAllText().Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
SyntaxTreeNodeVerifier.Verify(root, baseline);
var baselineDiagnostics = string.Empty;
var diagnosticsFile = TestFile.Create(baselineDiagnosticsFileName, GetType().GetTypeInfo().Assembly);
if (diagnosticsFile.Exists())
{
baselineDiagnostics = diagnosticsFile.ReadAllText();
}
var actualDiagnostics = string.Concat(diagnostics.Select(d => RazorDiagnosticSerializer.Serialize(d) + "\r\n"));
Assert.Equal(baselineDiagnostics, actualDiagnostics);
}
internal RazorSyntaxTree ParseBlock(string document, bool designTime)
{
return ParseBlock(RazorLanguageVersion.Latest, document, designTime);
@ -128,7 +243,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
directives = directives ?? Array.Empty<DirectiveDescriptor>();
var source = TestRazorSourceDocument.Create(document, filePath: null);
var source = TestRazorSourceDocument.Create(document, filePath: null, normalizeNewLines: UseBaselineTests);
var options = CreateParserOptions(version, directives, designTime);
var context = new ParserContext(source, options);
@ -222,6 +337,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
var result = ParseBlock(version, document, directives, designTime);
if (UseBaselineTests && !IsTheory)
{
AssertSyntaxTreeNodeMatchesBaseline(result);
return;
}
if (FixupSpans)
{
SpancestryCorrector.Correct(expected);

View File

@ -0,0 +1,48 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
public static class SyntaxTreeNodeSerializer
{
internal static string Serialize(SyntaxTreeNode node)
{
using (var writer = new StringWriter())
{
var walker = new Walker(writer);
walker.Visit(node);
return writer.ToString();
}
}
private class Walker : SyntaxTreeNodeWalker
{
private readonly SyntaxTreeNodeWriter _visitor;
private readonly TextWriter _writer;
public Walker(TextWriter writer)
{
_visitor = new SyntaxTreeNodeWriter(writer);
_writer = writer;
}
public TextWriter Writer { get; }
public override void Visit(SyntaxTreeNode node)
{
_visitor.Visit(node);
_writer.WriteLine();
if (node is Block block)
{
_visitor.Depth++;
base.VisitDefault(block);
_visitor.Depth--;
}
}
}
}
}

View File

@ -0,0 +1,276 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Xunit;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
public static class SyntaxTreeNodeVerifier
{
internal static void Verify(SyntaxTreeNode node, string[] baseline)
{
var walker = new Walker(baseline);
walker.Visit(node);
walker.AssertReachedEndOfBaseline();
}
private class Walker : SyntaxTreeNodeWalker
{
private readonly string[] _baseline;
private readonly SyntaxTreeNodeWriter _visitor;
private readonly StringWriter _writer;
private int _index;
public Walker(string[] baseline)
{
_writer = new StringWriter();
_visitor = new SyntaxTreeNodeWriter(_writer);
_baseline = baseline;
}
public TextWriter Writer { get; }
public override void Visit(SyntaxTreeNode node)
{
var expected = _index < _baseline.Length ? _baseline[_index++] : null;
// Write the node as text for comparison
_writer.GetStringBuilder().Clear();
_visitor.Visit(node);
var actual = _writer.GetStringBuilder().ToString();
AssertNodeEquals(node, Ancestors, expected, actual);
if (node is Block block)
{
_visitor.Depth++;
base.VisitDefault(block);
_visitor.Depth--;
}
}
public void AssertReachedEndOfBaseline()
{
// Since we're walking the nodes of our generated code there's the chance that our baseline is longer.
Assert.True(_baseline.Length == _index, "Not all lines of the baseline were visited!");
}
private void AssertNodeEquals(SyntaxTreeNode node, IEnumerable<SyntaxTreeNode> ancestors, string expected, string actual)
{
if (string.Equals(expected, actual))
{
// YAY!!! everything is great.
return;
}
if (expected == null)
{
var message = "The node is missing from baseline.";
throw new SyntaxTreeNodeBaselineException(node, Ancestors.ToArray(), expected, actual, message);
}
int charsVerified = 0;
AssertNestingEqual(node, ancestors, expected, actual, ref charsVerified);
AssertNameEqual(node, ancestors, expected, actual, ref charsVerified);
AssertDelimiter(node, expected, actual, true, ref charsVerified);
AssertLocationEqual(node, ancestors, expected, actual, ref charsVerified);
AssertDelimiter(node, expected, actual, false, ref charsVerified);
AssertContentEqual(node, ancestors, expected, actual, ref charsVerified);
throw new InvalidOperationException("We can't figure out HOW these two things are different. This is a bug.");
}
private void AssertNestingEqual(SyntaxTreeNode node, IEnumerable<SyntaxTreeNode> ancestors, string expected, string actual, ref int charsVerified)
{
var i = 0;
for (; i < expected.Length; i++)
{
if (expected[i] != ' ')
{
break;
}
}
var failed = false;
var j = 0;
for (; j < i; j++)
{
if (actual.Length <= j || actual[j] != ' ')
{
failed = true;
break;
}
}
if (actual.Length <= j + 1 || actual[j] == ' ')
{
failed = true;
}
if (failed)
{
var message = "The node is at the wrong level of nesting. This usually means a child is missing.";
throw new SyntaxTreeNodeBaselineException(node, ancestors.ToArray(), expected, actual, message);
}
charsVerified = j;
}
private void AssertNameEqual(SyntaxTreeNode node, IEnumerable<SyntaxTreeNode> ancestors, string expected, string actual, ref int charsVerified)
{
var expectedName = GetName(expected, charsVerified);
var actualName = GetName(actual, charsVerified);
if (!string.Equals(expectedName, actualName))
{
var message = $"Node names are not equal.";
throw new SyntaxTreeNodeBaselineException(node, ancestors.ToArray(), expected, actual, message);
}
charsVerified += expectedName.Length;
}
// Either both strings need to have a delimiter next or neither should.
private void AssertDelimiter(SyntaxTreeNode node, string expected, string actual, bool required, ref int charsVerified)
{
if (charsVerified == expected.Length && required)
{
throw new InvalidOperationException($"Baseline text is not well-formed: '{expected}'.");
}
if (charsVerified == actual.Length && required)
{
throw new InvalidOperationException($"Baseline text is not well-formed: '{actual}'.");
}
if (charsVerified == expected.Length && charsVerified == actual.Length)
{
return;
}
var expectedDelimiter = expected.IndexOf(" - ", charsVerified);
if (expectedDelimiter != charsVerified && expectedDelimiter != -1)
{
throw new InvalidOperationException($"Baseline text is not well-formed: '{actual}'.");
}
var actualDelimiter = actual.IndexOf(" - ", charsVerified);
if (actualDelimiter != charsVerified && actualDelimiter != -1)
{
throw new InvalidOperationException($"Baseline text is not well-formed: '{actual}'.");
}
Assert.Equal(expectedDelimiter, actualDelimiter);
charsVerified += 3;
}
private void AssertLocationEqual(SyntaxTreeNode node, IEnumerable<SyntaxTreeNode> ancestors, string expected, string actual, ref int charsVerified)
{
var expectedLocation = GetLocation(expected, charsVerified);
var actualLocation = GetLocation(actual, charsVerified);
if (!string.Equals(expectedLocation, actualLocation))
{
var message = $"Locations are not equal.";
throw new SyntaxTreeNodeBaselineException(node, ancestors.ToArray(), expected, actual, message);
}
charsVerified += expectedLocation.Length;
}
private void AssertContentEqual(SyntaxTreeNode node, IEnumerable<SyntaxTreeNode> ancestors, string expected, string actual, ref int charsVerified)
{
var expectedContent = GetContent(expected, charsVerified);
var actualContent = GetContent(actual, charsVerified);
if (!string.Equals(expectedContent, actualContent))
{
var message = $"Contents are not equal.";
throw new SyntaxTreeNodeBaselineException(node, ancestors.ToArray(), expected, actual, message);
}
charsVerified += expectedContent.Length;
}
private string GetName(string text, int start)
{
var delimiter = text.IndexOf(" - ", start);
if (delimiter == -1)
{
throw new InvalidOperationException($"Baseline text is not well-formed: '{text}'.");
}
return text.Substring(start, delimiter - start);
}
private string GetLocation(string text, int start)
{
var delimiter = text.IndexOf(" - ", start);
return delimiter == -1 ? text.Substring(start) : text.Substring(start, delimiter - start);
}
private string GetContent(string text, int start)
{
return start == text.Length ? string.Empty : text.Substring(start);
}
private class SyntaxTreeNodeBaselineException : XunitException
{
public SyntaxTreeNodeBaselineException(SyntaxTreeNode node, SyntaxTreeNode[] ancestors, string expected, string actual, string userMessage)
: base(Format(node, ancestors, expected, actual, userMessage))
{
Node = node;
Expected = expected;
Actual = actual;
}
public SyntaxTreeNode Node { get; }
public string Actual { get; }
public string Expected { get; }
private static string Format(SyntaxTreeNode node, SyntaxTreeNode[] ancestors, string expected, string actual, string userMessage)
{
var builder = new StringBuilder();
builder.AppendLine(userMessage);
builder.AppendLine();
if (expected != null)
{
builder.Append("Expected: ");
builder.AppendLine(expected);
}
if (actual != null)
{
builder.Append("Actual: ");
builder.AppendLine(actual);
}
if (ancestors != null)
{
builder.AppendLine();
builder.AppendLine("Path:");
foreach (var ancestor in ancestors)
{
builder.AppendLine(ancestor.ToString());
}
}
return builder.ToString();
}
}
}
}
}

View File

@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
internal class SyntaxTreeNodeWalker : ParserVisitor
{
private readonly List<SyntaxTreeNode> _ancestors = new List<SyntaxTreeNode>();
protected IReadOnlyList<SyntaxTreeNode> Ancestors => _ancestors;
protected SyntaxTreeNode Parent => _ancestors.Count > 0 ? _ancestors[0] : null;
public override void VisitDefault(Block block)
{
var children = block.Children;
if (block.Children.Count == 0)
{
return;
}
_ancestors.Insert(0, block);
try
{
for (var i = 0; i < block.Children.Count; i++)
{
var child = children[i];
Visit(child);
}
}
finally
{
_ancestors.RemoveAt(0);
}
}
}
}

View File

@ -0,0 +1,244 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
internal class SyntaxTreeNodeWriter : ParserVisitor
{
private readonly TextWriter _writer;
public int Depth { get; set; }
public SyntaxTreeNodeWriter(TextWriter writer)
{
_writer = writer;
}
public override void VisitDefault(Block block)
{
WriteBlock(block);
}
public override void VisitDefault(Span span)
{
WriteSpan(span);
}
public override void VisitTagHelperBlock(TagHelperChunkGenerator chunkGenerator, Block block)
{
WriteBlock(block);
if (block is TagHelperBlock tagHelperBlock)
{
WriteSeparator();
Write(tagHelperBlock.TagName);
WriteSeparator();
Write(tagHelperBlock.TagMode);
foreach (var descriptor in tagHelperBlock.Binding.Descriptors)
{
WriteSeparator();
// Get the type name without the namespace.
var typeName = descriptor.Name.Substring(descriptor.Name.LastIndexOf('.') + 1);
Write(typeName);
}
}
}
public override void VisitAttributeBlock(AttributeBlockChunkGenerator chunkGenerator, Block block)
{
WriteBlock(block);
WriteSeparator();
Write(chunkGenerator.Name);
WriteSeparator();
WriteLocationTaggedString(chunkGenerator.Prefix);
WriteSeparator();
WriteLocationTaggedString(chunkGenerator.Suffix);
}
public override void VisitCommentBlock(RazorCommentChunkGenerator chunkGenerator, Block block)
{
WriteBlock(block);
}
public override void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block)
{
WriteBlock(block);
WriteSeparator();
Write(chunkGenerator.Descriptor.Directive);
WriteSeparator();
Write(chunkGenerator.Descriptor.Kind);
WriteSeparator();
Write(chunkGenerator.Descriptor.Usage);
}
public override void VisitDynamicAttributeBlock(DynamicAttributeBlockChunkGenerator chunkGenerator, Block block)
{
WriteBlock(block);
WriteSeparator();
WriteLocationTaggedString(chunkGenerator.Prefix);
WriteSeparator();
WriteSourceLocation(chunkGenerator.ValueStart);
}
public override void VisitExpressionBlock(ExpressionChunkGenerator chunkGenerator, Block block)
{
WriteBlock(block);
}
public override void VisitTemplateBlock(TemplateBlockChunkGenerator chunkGenerator, Block block)
{
WriteBlock(block);
}
public override void VisitMarkupSpan(MarkupChunkGenerator chunkGenerator, Span span)
{
WriteSpan(span);
}
public override void VisitAddTagHelperSpan(AddTagHelperChunkGenerator chunkGenerator, Span span)
{
WriteSpan(span);
WriteSeparator();
Write(chunkGenerator.LookupText);
WriteSeparator();
Write(chunkGenerator.DirectiveText);
WriteSeparator();
Write(chunkGenerator.TypePattern);
WriteSeparator();
Write(chunkGenerator.AssemblyName);
}
public override void VisitExpressionSpan(ExpressionChunkGenerator chunkGenerator, Span span)
{
WriteSpan(span);
}
public override void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Span span)
{
WriteSpan(span);
WriteSeparator();
Write(chunkGenerator.Namespace);
}
public override void VisitLiteralAttributeSpan(LiteralAttributeChunkGenerator chunkGenerator, Span span)
{
WriteSpan(span);
WriteSeparator();
WriteLocationTaggedString(chunkGenerator.Prefix);
WriteSeparator();
WriteLocationTaggedString(chunkGenerator.Value);
}
public override void VisitRemoveTagHelperSpan(RemoveTagHelperChunkGenerator chunkGenerator, Span span)
{
WriteSpan(span);
WriteSeparator();
Write(chunkGenerator.LookupText);
WriteSeparator();
Write(chunkGenerator.DirectiveText);
WriteSeparator();
Write(chunkGenerator.TypePattern);
WriteSeparator();
Write(chunkGenerator.AssemblyName);
}
public override void VisitTagHelperPrefixDirectiveSpan(TagHelperPrefixDirectiveChunkGenerator chunkGenerator, Span span)
{
WriteSpan(span);
WriteSeparator();
Write(chunkGenerator.Prefix);
WriteSeparator();
Write(chunkGenerator.DirectiveText);
}
public override void VisitStatementSpan(StatementChunkGenerator chunkGenerator, Span span)
{
WriteSpan(span);
}
public override void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span)
{
WriteSpan(span);
WriteSeparator();
Write(chunkGenerator.Descriptor.Kind);
WriteSeparator();
Write(chunkGenerator.Descriptor.Name);
WriteSeparator();
Write($"Optional: {chunkGenerator.Descriptor.Optional}");
}
protected void WriteBlock(Block block)
{
WriteIndent();
Write($"{block.Type} block");
WriteSeparator();
Write(block.ChunkGenerator.GetType().Name);
WriteSeparator();
Write(block.Length);
WriteSeparator();
WriteSourceLocation(block.Start);
}
protected void WriteSpan(Span span)
{
WriteIndent();
Write($"{span.Kind} span");
WriteSeparator();
Write(span.ChunkGenerator.GetType().Name);
WriteSeparator();
Write(span.EditHandler);
WriteSeparator();
Write(span.Content);
WriteSeparator();
WriteSourceLocation(span.Start);
}
protected void WriteSourceLocation(SourceLocation location)
{
Write(location);
}
protected void WriteLocationTaggedString(LocationTagged<string> item)
{
Write(item.ToString("F", null));
}
protected void WriteIndent()
{
for (var i = 0; i < Depth; i++)
{
for (var j = 0; j < 4; j++)
{
Write(' ');
}
}
}
protected void WriteSeparator()
{
Write(" - ");
}
protected void WriteNewLine()
{
_writer.WriteLine();
}
protected void Write(object value)
{
if (value is string stringValue)
{
stringValue = stringValue.Replace("\r\n", "LF");
_writer.Write(stringValue);
return;
}
_writer.Write(value);
}
}
}