diff options
Diffstat (limited to 'src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common')
32 files changed, 4258 insertions, 0 deletions
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/CodeGeneration/TestCodeRenderingContext.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/CodeGeneration/TestCodeRenderingContext.cs new file mode 100644 index 0000000000..2791dbcf62 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/CodeGeneration/TestCodeRenderingContext.cs @@ -0,0 +1,99 @@ +// 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 Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration +{ + public static class TestCodeRenderingContext + { + public static CodeRenderingContext CreateDesignTime( + string newLineString = null, + string suppressUniqueIds = "test", + RazorSourceDocument source = null, + IntermediateNodeWriter nodeWriter = null) + { + var codeWriter = new CodeWriter(); + var documentNode = new DocumentIntermediateNode(); + var options = RazorCodeGenerationOptions.CreateDesignTimeDefault(); + + if (source == null) + { + source = TestRazorSourceDocument.Create(); + } + + var codeDocument = RazorCodeDocument.Create(source); + if (newLineString != null) + { + codeDocument.Items[CodeRenderingContext.NewLineString] = newLineString; + } + + if (suppressUniqueIds != null) + { + codeDocument.Items[CodeRenderingContext.SuppressUniqueIds] = suppressUniqueIds; + } + + if (nodeWriter == null) + { + nodeWriter = new DesignTimeNodeWriter(); + } + + var context = new DefaultCodeRenderingContext(codeWriter, nodeWriter, codeDocument, documentNode, options); + context.Visitor = new RenderChildrenVisitor(context); + + return context; + } + + public static CodeRenderingContext CreateRuntime( + string newLineString = null, + string suppressUniqueIds = "test", + RazorSourceDocument source = null, + IntermediateNodeWriter nodeWriter = null) + { + var codeWriter = new CodeWriter(); + var documentNode = new DocumentIntermediateNode(); + var options = RazorCodeGenerationOptions.CreateDefault(); + + if (source == null) + { + source = TestRazorSourceDocument.Create(); + } + + var codeDocument = RazorCodeDocument.Create(source); + if (newLineString != null) + { + codeDocument.Items[CodeRenderingContext.NewLineString] = newLineString; + } + + if (suppressUniqueIds != null) + { + codeDocument.Items[CodeRenderingContext.SuppressUniqueIds] = suppressUniqueIds; + } + + if (nodeWriter == null) + { + nodeWriter = new RuntimeNodeWriter(); + } + + var context = new DefaultCodeRenderingContext(codeWriter, nodeWriter, codeDocument, documentNode, options); + context.Visitor = new RenderChildrenVisitor(context); + + return context; + } + + private class RenderChildrenVisitor : IntermediateNodeVisitor + { + private readonly CodeRenderingContext _context; + public RenderChildrenVisitor(CodeRenderingContext context) + { + _context = context; + } + + public override void VisitDefault(IntermediateNode node) + { + _context.CodeWriter.WriteLine("Render Children"); + } + } + + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntegrationTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntegrationTestBase.cs new file mode 100644 index 0000000000..0d6dd241ce --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntegrationTestBase.cs @@ -0,0 +1,321 @@ +// 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.Reflection; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +#if NET46 +using System.Runtime.Remoting; +using System.Runtime.Remoting.Messaging; +#else +using System.Threading; +#endif +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests +{ + [IntializeTestFile] + public abstract class IntegrationTestBase + { +#if !NET46 + private static readonly AsyncLocal<string> _fileName = new AsyncLocal<string>(); +#endif + + protected IntegrationTestBase() + { + TestProjectRoot = TestProject.GetProjectDirectory(GetType()); + } + +#if GENERATE_BASELINES + protected bool GenerateBaselines { get; set; } = true; +#else + protected bool GenerateBaselines { get; set; } = false; +#endif + + protected string TestProjectRoot { get; } + + // 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("IntegrationTestBase_FileName"); + return (string)handle.Unwrap(); + } + set + { + CallContext.LogicalSetData("IntegrationTestBase_FileName", new ObjectHandle(value)); + } +#elif NETCOREAPP2_0 || NETCOREAPP2_1 + get { return _fileName.Value; } + set { _fileName.Value = value; } +#endif + } + + protected virtual RazorProjectEngine CreateProjectEngine() => CreateProjectEngine(configure: null); + + protected virtual RazorProjectEngine CreateProjectEngine(Action<RazorProjectEngineBuilder> configure) + { + if (FileName == null) + { + var message = $"{nameof(CreateProjectEngine)} should only be called from an integration test, ({nameof(FileName)} is null)."; + throw new InvalidOperationException(message); + } + + var assembly = GetType().GetTypeInfo().Assembly; + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, IntegrationTestFileSystem.Default, b => + { + configure?.Invoke(b); + + var existingImportFeature = b.Features.OfType<IImportProjectFeature>().Single(); + b.SetImportFeature(new IntegrationTestImportFeature(assembly, existingImportFeature)); + }); + var testProjectEngine = new IntegrationTestProjectEngine(projectEngine); + + return testProjectEngine; + } + + protected virtual RazorProjectItem CreateProjectItem() + { + if (FileName == null) + { + var message = $"{nameof(CreateProjectItem)} should only be called from an integration test, ({nameof(FileName)} is null)."; + throw new InvalidOperationException(message); + } + + var suffixIndex = FileName.LastIndexOf("_"); + var normalizedFileName = suffixIndex == -1 ? FileName : FileName.Substring(0, suffixIndex); + var sourceFileName = Path.ChangeExtension(normalizedFileName, ".cshtml"); + var testFile = TestFile.Create(sourceFileName, GetType().GetTypeInfo().Assembly); + if (!testFile.Exists()) + { + throw new XunitException($"The resource {sourceFileName} was not found."); + } + var fileContent = testFile.ReadAllText(); + var normalizedContent = NormalizeNewLines(fileContent); + + var projectItem = new TestRazorProjectItem(sourceFileName) + { + Content = normalizedContent, + }; + + return projectItem; + } + + protected void AssertDocumentNodeMatchesBaseline(DocumentIntermediateNode document) + { + if (FileName == null) + { + var message = $"{nameof(AssertDocumentNodeMatchesBaseline)} should only be called from an integration test ({nameof(FileName)} is null)."; + throw new InvalidOperationException(message); + } + + var baselineFileName = Path.ChangeExtension(FileName, ".ir.txt"); + + if (GenerateBaselines) + { + var baselineFullPath = Path.Combine(TestProjectRoot, baselineFileName); + File.WriteAllText(baselineFullPath, IntermediateNodeSerializer.Serialize(document)); + return; + } + + var irFile = TestFile.Create(baselineFileName, GetType().GetTypeInfo().Assembly); + if (!irFile.Exists()) + { + throw new XunitException($"The resource {baselineFileName} was not found."); + } + + var baseline = irFile.ReadAllText().Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + IntermediateNodeVerifier.Verify(document, baseline); + } + + protected void AssertCSharpDocumentMatchesBaseline(RazorCSharpDocument document) + { + if (FileName == null) + { + var message = $"{nameof(AssertCSharpDocumentMatchesBaseline)} should only be called from an integration test ({nameof(FileName)} is null)."; + throw new InvalidOperationException(message); + } + + var baselineFileName = Path.ChangeExtension(FileName, ".codegen.cs"); + var baselineDiagnosticsFileName = Path.ChangeExtension(FileName, ".diagnostics.txt"); + + if (GenerateBaselines) + { + var baselineFullPath = Path.Combine(TestProjectRoot, baselineFileName); + File.WriteAllText(baselineFullPath, document.GeneratedCode); + + var baselineDiagnosticsFullPath = Path.Combine(TestProjectRoot, baselineDiagnosticsFileName); + var lines = document.Diagnostics.Select(RazorDiagnosticSerializer.Serialize).ToArray(); + if (lines.Any()) + { + File.WriteAllLines(baselineDiagnosticsFullPath, lines); + } + else if (File.Exists(baselineDiagnosticsFullPath)) + { + File.Delete(baselineDiagnosticsFullPath); + } + + return; + } + + var codegenFile = TestFile.Create(baselineFileName, GetType().GetTypeInfo().Assembly); + if (!codegenFile.Exists()) + { + throw new XunitException($"The resource {baselineFileName} was not found."); + } + + var baseline = codegenFile.ReadAllText(); + + // Normalize newlines to match those in the baseline. + var actual = document.GeneratedCode.Replace("\r", "").Replace("\n", "\r\n"); + Assert.Equal(baseline, actual); + + var baselineDiagnostics = string.Empty; + var diagnosticsFile = TestFile.Create(baselineDiagnosticsFileName, GetType().GetTypeInfo().Assembly); + if (diagnosticsFile.Exists()) + { + baselineDiagnostics = diagnosticsFile.ReadAllText(); + } + + var actualDiagnostics = string.Concat(document.Diagnostics.Select(d => RazorDiagnosticSerializer.Serialize(d) + "\r\n")); + Assert.Equal(baselineDiagnostics, actualDiagnostics); + } + + protected void AssertSourceMappingsMatchBaseline(RazorCodeDocument document) + { + if (FileName == null) + { + var message = $"{nameof(AssertSourceMappingsMatchBaseline)} should only be called from an integration test ({nameof(FileName)} is null)."; + throw new InvalidOperationException(message); + } + + var csharpDocument = document.GetCSharpDocument(); + Assert.NotNull(csharpDocument); + + var baselineFileName = Path.ChangeExtension(FileName, ".mappings.txt"); + var serializedMappings = SourceMappingsSerializer.Serialize(csharpDocument, document.Source); + + if (GenerateBaselines) + { + var baselineFullPath = Path.Combine(TestProjectRoot, baselineFileName); + File.WriteAllText(baselineFullPath, serializedMappings); + return; + } + + var testFile = TestFile.Create(baselineFileName, GetType().GetTypeInfo().Assembly); + if (!testFile.Exists()) + { + throw new XunitException($"The resource {baselineFileName} was not found."); + } + + var baseline = testFile.ReadAllText(); + + // Normalize newlines to match those in the baseline. + var actual = serializedMappings.Replace("\r", "").Replace("\n", "\r\n"); + + Assert.Equal(baseline, actual); + } + + private static string NormalizeNewLines(string content) + { + return Regex.Replace(content, "(?<!\r)\n", "\r\n", RegexOptions.None, TimeSpan.FromSeconds(10)); + } + + private class IntegrationTestProjectEngine : DefaultRazorProjectEngine + { + public IntegrationTestProjectEngine( + RazorProjectEngine innerEngine) + : base(innerEngine.Configuration, innerEngine.Engine, innerEngine.FileSystem, innerEngine.ProjectFeatures) + { + } + + protected override void ProcessCore(RazorCodeDocument codeDocument) + { + // This will ensure that we're not putting any randomly generated data in a baseline. + codeDocument.Items[CodeRenderingContext.SuppressUniqueIds] = "test"; + + // This is to make tests work cross platform. + codeDocument.Items[CodeRenderingContext.NewLineString] = "\r\n"; + + base.ProcessCore(codeDocument); + } + } + + private class IntegrationTestImportFeature : RazorProjectEngineFeatureBase, IImportProjectFeature + { + private Assembly _assembly; + private IImportProjectFeature _existingImportFeature; + + public IntegrationTestImportFeature(Assembly assembly, IImportProjectFeature existingImportFeature) + { + _assembly = assembly; + _existingImportFeature = existingImportFeature; + } + + protected override void OnInitialized() + { + _existingImportFeature.ProjectEngine = ProjectEngine; + } + + public IReadOnlyList<RazorProjectItem> GetImports(RazorProjectItem projectItem) + { + var imports = new List<RazorProjectItem>(); + + while (true) + { + var importsFileName = Path.ChangeExtension(projectItem.FilePathWithoutExtension + "_Imports" + imports.Count.ToString(), ".cshtml"); + var importsFile = TestFile.Create(importsFileName, _assembly); + if (!importsFile.Exists()) + { + break; + } + + var importContent = importsFile.ReadAllText(); + var normalizedContent = NormalizeNewLines(importContent); + var importItem = new TestRazorProjectItem(importsFileName) + { + Content = normalizedContent + }; + imports.Add(importItem); + } + + imports.AddRange(_existingImportFeature.GetImports(projectItem)); + + return imports; + } + } + + private class IntegrationTestFileSystem : RazorProjectFileSystem + { + public static IntegrationTestFileSystem Default = new IntegrationTestFileSystem(); + + private IntegrationTestFileSystem() + { + } + + public override IEnumerable<RazorProjectItem> EnumerateItems(string basePath) + { + return Enumerable.Empty<RazorProjectItem>(); + } + + public override RazorProjectItem GetItem(string path) + { + return new NotFoundProjectItem(string.Empty, path); + } + + public override IEnumerable<RazorProjectItem> FindHierarchicalItems(string basePath, string path, string fileName) + { + return Enumerable.Empty<RazorProjectItem>(); + } + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeSerializer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeSerializer.cs new file mode 100644 index 0000000000..891a4849ed --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeSerializer.cs @@ -0,0 +1,46 @@ +// 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; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests +{ + public static class IntermediateNodeSerializer + { + public static string Serialize(IntermediateNode node) + { + using (var writer = new StringWriter()) + { + var walker = new Walker(writer); + walker.Visit(node); + + return writer.ToString(); + } + } + + private class Walker : IntermediateNodeWalker + { + private readonly IntermediateNodeWriter _visitor; + private readonly TextWriter _writer; + + public Walker(TextWriter writer) + { + _visitor = new IntermediateNodeWriter(writer); + _writer = writer; + } + + public TextWriter Writer { get; } + + public override void VisitDefault(IntermediateNode node) + { + _visitor.Visit(node); + _writer.WriteLine(); + + _visitor.Depth++; + base.VisitDefault(node); + _visitor.Depth--; + } + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeVerifier.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeVerifier.cs new file mode 100644 index 0000000000..a75da6a2fc --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeVerifier.cs @@ -0,0 +1,275 @@ +// 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 Microsoft.AspNetCore.Razor.Language.Intermediate; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests +{ + public static class IntermediateNodeVerifier + { + public static void Verify(IntermediateNode node, string[] baseline) + { + var walker = new Walker(baseline); + walker.Visit(node); + walker.AssertReachedEndOfBaseline(); + } + + private class Walker : IntermediateNodeWalker + { + private readonly string[] _baseline; + private readonly IntermediateNodeWriter _visitor; + private readonly StringWriter _writer; + + private int _index; + + public Walker(string[] baseline) + { + _writer = new StringWriter(); + + _visitor = new IntermediateNodeWriter(_writer); + _baseline = baseline; + + } + + public TextWriter Writer { get; } + + public override void VisitDefault(IntermediateNode 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); + + _visitor.Depth++; + base.VisitDefault(node); + _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(IntermediateNode node, IEnumerable<IntermediateNode> 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 IntermediateNodeBaselineException(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(IntermediateNode node, IEnumerable<IntermediateNode> 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 IntermediateNodeBaselineException(node, ancestors.ToArray(), expected, actual, message); + } + + charsVerified = j; + } + + private void AssertNameEqual(IntermediateNode node, IEnumerable<IntermediateNode> 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 IntermediateNodeBaselineException(node, ancestors.ToArray(), expected, actual, message); + } + + charsVerified += expectedName.Length; + } + + // Either both strings need to have a delimiter next or neither should. + private void AssertDelimiter(IntermediateNode 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(IntermediateNode node, IEnumerable<IntermediateNode> 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 IntermediateNodeBaselineException(node, ancestors.ToArray(), expected, actual, message); + } + + charsVerified += expectedLocation.Length; + } + + private void AssertContentEqual(IntermediateNode node, IEnumerable<IntermediateNode> 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 IntermediateNodeBaselineException(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 IntermediateNodeBaselineException : XunitException + { + public IntermediateNodeBaselineException(IntermediateNode node, IntermediateNode[] ancestors, string expected, string actual, string userMessage) + : base(Format(node, ancestors, expected, actual, userMessage)) + { + Node = node; + Expected = expected; + Actual = actual; + } + + public IntermediateNode Node { get; } + + public string Actual { get; } + + public string Expected { get; } + + private static string Format(IntermediateNode node, IntermediateNode[] 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(); + } + } + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeWriter.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeWriter.cs new file mode 100644 index 0000000000..4329c0d02d --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeWriter.cs @@ -0,0 +1,306 @@ +// 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.Security.Cryptography; +using System.Text; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests +{ + // Serializes single IR nodes (shallow). + public class IntermediateNodeWriter : + IntermediateNodeVisitor, + IExtensionIntermediateNodeVisitor<SectionIntermediateNode> + { + private readonly TextWriter _writer; + + public IntermediateNodeWriter(TextWriter writer) + { + _writer = writer; + } + + public int Depth { get; set; } + + public override void VisitDefault(IntermediateNode node) + { + WriteBasicNode(node); + } + + public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) + { + var entries = new List<string>() + { + string.Join(" ", node.Modifiers), + node.ClassName, + node.BaseType, + string.Join(", ", node.Interfaces ?? Array.Empty<string>()) + }; + + // Avoid adding the type parameters to the baseline if they aren't present. + if (node.TypeParameters != null && node.TypeParameters.Count > 0) + { + entries.Add(string.Join(", ", node.TypeParameters)); + } + + WriteContentNode(node, entries.ToArray()); + } + + public override void VisitCSharpExpressionAttributeValue(CSharpExpressionAttributeValueIntermediateNode node) + { + WriteContentNode(node, node.Prefix); + } + + public override void VisitCSharpCodeAttributeValue(CSharpCodeAttributeValueIntermediateNode node) + { + WriteContentNode(node, node.Prefix); + } + + public override void VisitToken(IntermediateToken node) + { + WriteContentNode(node, node.Kind.ToString(), node.Content); + } + + public override void VisitMalformedDirective(MalformedDirectiveIntermediateNode node) + { + WriteContentNode(node, node.DirectiveName); + } + + public override void VisitDirective(DirectiveIntermediateNode node) + { + WriteContentNode(node, node.DirectiveName); + } + + public override void VisitDirectiveToken(DirectiveTokenIntermediateNode node) + { + WriteContentNode(node, node.Content); + } + + public override void VisitFieldDeclaration(FieldDeclarationIntermediateNode node) + { + WriteContentNode(node, string.Join(" ", node.Modifiers), node.FieldType, node.FieldName); + } + + public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node) + { + WriteContentNode(node, node.Prefix, node.Suffix); + } + + public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node) + { + WriteContentNode(node, node.Prefix); + } + + public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node) + { + WriteContentNode(node, node.Content); + } + + public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode node) + { + WriteContentNode(node, string.Join(" ", node.Modifiers), node.ReturnType, node.MethodName); + } + + public override void VisitUsingDirective(UsingDirectiveIntermediateNode node) + { + WriteContentNode(node, node.Content); + } + + public override void VisitTagHelper(TagHelperIntermediateNode node) + { + WriteContentNode(node, node.TagName, string.Format("{0}.{1}", nameof(TagMode), node.TagMode)); + } + + public override void VisitTagHelperProperty(TagHelperPropertyIntermediateNode node) + { + WriteContentNode(node, node.AttributeName, node.BoundAttribute.DisplayName, string.Format("HtmlAttributeValueStyle.{0}", node.AttributeStructure)); + } + + public override void VisitTagHelperHtmlAttribute(TagHelperHtmlAttributeIntermediateNode node) + { + WriteContentNode(node, node.AttributeName, string.Format("HtmlAttributeValueStyle.{0}", node.AttributeStructure)); + } + + public override void VisitExtension(ExtensionIntermediateNode node) + { + switch (node) + { + case PreallocatedTagHelperHtmlAttributeIntermediateNode n: + WriteContentNode(n, n.VariableName); + break; + case PreallocatedTagHelperHtmlAttributeValueIntermediateNode n: + WriteContentNode(n, n.VariableName, n.AttributeName, n.Value, string.Format("HtmlAttributeValueStyle.{0}", n.AttributeStructure)); + break; + case PreallocatedTagHelperPropertyIntermediateNode n: + WriteContentNode(n, n.VariableName, n.AttributeName, n.PropertyName); + break; + case PreallocatedTagHelperPropertyValueIntermediateNode n: + WriteContentNode(n, n.VariableName, n.AttributeName, n.Value, string.Format("HtmlAttributeValueStyle.{0}", n.AttributeStructure)); + break; + case DefaultTagHelperCreateIntermediateNode n: + WriteContentNode(n, n.TypeName); + break; + case DefaultTagHelperExecuteIntermediateNode n: + WriteBasicNode(n); + break; + case DefaultTagHelperHtmlAttributeIntermediateNode n: + WriteContentNode(n, n.AttributeName, string.Format("HtmlAttributeValueStyle.{0}", n.AttributeStructure)); + break; + case DefaultTagHelperPropertyIntermediateNode n: + WriteContentNode(n, n.AttributeName, n.BoundAttribute.DisplayName, string.Format("HtmlAttributeValueStyle.{0}", n.AttributeStructure)); + break; + case DefaultTagHelperRuntimeIntermediateNode n: + WriteBasicNode(n); + break; + default: + base.VisitExtension(node); + break; + } + } + + public void VisitExtension(SectionIntermediateNode node) + { + WriteContentNode(node, node.SectionName); + } + + protected void WriteBasicNode(IntermediateNode node) + { + WriteIndent(); + WriteName(node); + WriteSeparator(); + WriteSourceRange(node); + } + + protected void WriteContentNode(IntermediateNode node, params string[] content) + { + WriteIndent(); + WriteName(node); + WriteSeparator(); + WriteSourceRange(node); + + for (var i = 0; i < content.Length; i++) + { + WriteSeparator(); + WriteContent(content[i]); + } + } + + protected void WriteIndent() + { + for (var i = 0; i < Depth; i++) + { + for (var j = 0; j < 4; j++) + { + _writer.Write(' '); + } + } + } + + protected void WriteSeparator() + { + _writer.Write(" - "); + } + + protected void WriteNewLine() + { + _writer.WriteLine(); + } + + protected void WriteName(IntermediateNode node) + { + var typeName = node.GetType().Name; + if (typeName.EndsWith("IntermediateNode")) + { + _writer.Write(typeName.Substring(0, typeName.Length - "IntermediateNode".Length)); + } + else + { + _writer.Write(typeName); + } + } + + protected void WriteSourceRange(IntermediateNode node) + { + if (node.Source != null) + { + WriteSourceRange(node.Source.Value); + } + } + + protected void WriteSourceRange(SourceSpan sourceRange) + { + _writer.Write("("); + _writer.Write(sourceRange.AbsoluteIndex); + _writer.Write(":"); + _writer.Write(sourceRange.LineIndex); + _writer.Write(","); + _writer.Write(sourceRange.CharacterIndex); + _writer.Write(" ["); + _writer.Write(sourceRange.Length); + _writer.Write("] "); + + if (sourceRange.FilePath != null) + { + var fileName = sourceRange.FilePath.Substring(sourceRange.FilePath.LastIndexOf('/') + 1); + _writer.Write(fileName); + } + + _writer.Write(")"); + } + + protected void WriteDiagnostics(IntermediateNode node) + { + if (node.HasDiagnostics) + { + _writer.Write("| "); + for (var i = 0; i < node.Diagnostics.Count; i++) + { + var diagnostic = node.Diagnostics[i]; + _writer.Write("{"); + WriteSourceRange(diagnostic.Span); + _writer.Write(": "); + _writer.Write(diagnostic.Severity); + _writer.Write(" "); + _writer.Write(diagnostic.Id); + _writer.Write(": "); + + // Purposefully not writing out the entire message to ensure readable IR and because messages + // can span multiple lines. Not using string.GetHashCode because we can't have any collisions. + using (var md5 = MD5.Create()) + { + var diagnosticMessage = diagnostic.GetMessage(); + var messageBytes = Encoding.UTF8.GetBytes(diagnosticMessage); + var messageHash = md5.ComputeHash(messageBytes); + var stringHashBuilder = new StringBuilder(); + + for (var j = 0; j < messageHash.Length; j++) + { + stringHashBuilder.Append(messageHash[j].ToString("x2")); + } + + var stringHash = stringHashBuilder.ToString(); + _writer.Write(stringHash); + } + _writer.Write("} "); + } + } + } + + protected void WriteContent(string content) + { + if (content == null) + { + return; + } + + // We explicitly escape newlines in node content so that the IR can be compared line-by-line. The escaped + // newline cannot be platform specific so we need to drop the windows \r. + // Also, escape our separator so we can search for ` - `to find delimiters. + _writer.Write(content.Replace("\r", string.Empty).Replace("\n", "\\n").Replace(" - ", "\\-")); + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntializeTestFileAttribute.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntializeTestFileAttribute.cs new file mode 100644 index 0000000000..0b1c78a53e --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntializeTestFileAttribute.cs @@ -0,0 +1,28 @@ +// 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.Sdk; + +namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests +{ + public class IntializeTestFileAttribute : BeforeAfterTestAttribute + { + public override void Before(MethodInfo methodUnderTest) + { + if (typeof(IntegrationTestBase).GetTypeInfo().IsAssignableFrom(methodUnderTest.DeclaringType.GetTypeInfo())) + { + var typeName = methodUnderTest.DeclaringType.Name; + IntegrationTestBase.FileName = $"TestFiles/IntegrationTests/{typeName}/{methodUnderTest.Name}"; + } + } + + public override void After(MethodInfo methodUnderTest) + { + if (typeof(IntegrationTestBase).GetTypeInfo().IsAssignableFrom(methodUnderTest.DeclaringType.GetTypeInfo())) + { + IntegrationTestBase.FileName = null; + } + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorDiagnosticSerializer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorDiagnosticSerializer.cs new file mode 100644 index 0000000000..bb92350872 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorDiagnosticSerializer.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Microsoft.AspNetCore.Razor.Language +{ + public class RazorDiagnosticSerializer + { + public static string Serialize(RazorDiagnostic diagnostic) + { + return diagnostic.ToString(); + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/SourceMappingsSerializer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/SourceMappingsSerializer.cs new file mode 100644 index 0000000000..40d95625f4 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/SourceMappingsSerializer.cs @@ -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.Text; +using Microsoft.AspNetCore.Razor.Language.Legacy; + +namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests +{ + public static class SourceMappingsSerializer + { + public static string Serialize(RazorCSharpDocument csharpDocument, RazorSourceDocument sourceDocument) + { + var builder = new StringBuilder(); + var charBuffer = new char[sourceDocument.Length]; + sourceDocument.CopyTo(0, charBuffer, 0, sourceDocument.Length); + var sourceContent = new string(charBuffer); + + for (var i = 0; i < csharpDocument.SourceMappings.Count; i++) + { + var sourceMapping = csharpDocument.SourceMappings[i]; + + builder.Append("Source Location: "); + AppendMappingLocation(builder, sourceMapping.OriginalSpan, sourceContent); + + builder.Append("Generated Location: "); + AppendMappingLocation(builder, sourceMapping.GeneratedSpan, csharpDocument.GeneratedCode); + + builder.AppendLine(); + } + + return builder.ToString(); + } + + private static void AppendMappingLocation(StringBuilder builder, SourceSpan location, string content) + { + builder + .AppendLine(location.ToString()) + .Append("|"); + + for (var i = 0; i < location.Length; i++) + { + builder.Append(content[location.AbsoluteIndex + i]); + } + + builder.AppendLine("|"); + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Intermediate/IntermediateNodeAssert.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Intermediate/IntermediateNodeAssert.cs new file mode 100644 index 0000000000..b4fc6f105e --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Intermediate/IntermediateNodeAssert.cs @@ -0,0 +1,495 @@ +// 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.Text; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Razor.Language.Intermediate +{ + public static class IntermediateNodeAssert + { + public static TNode SingleChild<TNode>(IntermediateNode node) + { + if (node.Children.Count == 0) + { + throw new IntermediateNodeAssertException(node, "The node has no children."); + } + else if (node.Children.Count > 1) + { + throw new IntermediateNodeAssertException(node, node.Children, "The node has multiple children"); + } + + var child = node.Children[0]; + return Assert.IsType<TNode>(child); + } + + public static void NoChildren(IntermediateNode node) + { + if (node.Children.Count > 0) + { + throw new IntermediateNodeAssertException(node, node.Children, "The node has children."); + } + } + + public static void Children(IntermediateNode node, params Action<IntermediateNode>[] validators) + { + var i = 0; + for (; i < validators.Length; i++) + { + if (node.Children.Count == i) + { + throw new IntermediateNodeAssertException(node, node.Children, $"The node only has {node.Children.Count} children."); + } + + try + { + validators[i].Invoke(node.Children[i]); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, node.Children, $"Failed while validating node {node.Children[i]} at {i}.", e); + } + } + + if (i < node.Children.Count) + { + throw new IntermediateNodeAssertException(node, node.Children, $"The node has extra child {node.Children[i]} at {i}."); + } + } + + public static void AnnotationEquals(IntermediateNode node, object value) + { + AnnotationEquals(node, value, value); + } + + public static void AnnotationEquals(IntermediateNode node, object key, object value) + { + try + { + Assert.NotNull(node); + Assert.Equal(value, node.Annotations[key]); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, node.Children, e.Message, e); + } + } + + public static void HasAnnotation(IntermediateNode node, object key) + { + try + { + Assert.NotNull(node); + Assert.NotNull(node.Annotations[key]); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, node.Children, e.Message, e); + } + } + + public static void Html(string expected, IntermediateNode node) + { + try + { + var html = Assert.IsType<HtmlContentIntermediateNode>(node); + var content = new StringBuilder(); + for (var i = 0; i < html.Children.Count; i++) + { + var token = Assert.IsType<IntermediateToken>(html.Children[i]); + Assert.Equal(TokenKind.Html, token.Kind); + content.Append(token.Content); + } + + Assert.Equal(expected, content.ToString()); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, node.Children, e.Message, e); + } + } + + public static void CSharpCode(string expected, IntermediateNode node) + { + try + { + var statement = Assert.IsType<CSharpCodeIntermediateNode>(node); + var content = new StringBuilder(); + for (var i = 0; i < statement.Children.Count; i++) + { + var token = Assert.IsType<IntermediateToken>(statement.Children[i]); + Assert.Equal(TokenKind.CSharp, token.Kind); + content.Append(token.Content); + } + + Assert.Equal(expected, content.ToString()); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, node.Children, e.Message, e); + } + } + + public static void Directive(string expectedName, IntermediateNode node, params Action<IntermediateNode>[] childValidators) + { + try + { + var directive = Assert.IsType<DirectiveIntermediateNode>(node); + Assert.Equal(expectedName, directive.DirectiveName); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, node.Children, e.Message, e); + } + + Children(node, childValidators); + } + + public static void DirectiveToken(DirectiveTokenKind expectedKind, string expectedContent, IntermediateNode node) + { + try + { + var token = Assert.IsType<DirectiveTokenIntermediateNode>(node); + Assert.Equal(expectedKind, token.DirectiveToken.Kind); + Assert.Equal(expectedContent, token.Content); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, node.Children, e.Message, e); + } + } + + public static void Using(string expected, IntermediateNode node) + { + try + { + var @using = Assert.IsType<UsingDirectiveIntermediateNode>(node); + Assert.Equal(expected, @using.Content); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, node.Children, e.Message, e); + } + } + + public static void ConditionalAttribute( + string prefix, + string name, + string suffix, + IntermediateNode node, + params Action<IntermediateNode>[] valueValidators) + { + var attribute = Assert.IsType<HtmlAttributeIntermediateNode>(node); + + try + { + Assert.Equal(prefix, attribute.Prefix); + Assert.Equal(name, attribute.AttributeName); + Assert.Equal(suffix, attribute.Suffix); + + Children(attribute, valueValidators); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(attribute, attribute.Children, e.Message, e); + } + } + + public static void CSharpExpressionAttributeValue(string prefix, string expected, IntermediateNode node) + { + var attributeValue = Assert.IsType<CSharpExpressionAttributeValueIntermediateNode>(node); + + try + { + var content = new StringBuilder(); + for (var i = 0; i < attributeValue.Children.Count; i++) + { + var token = Assert.IsType<IntermediateToken>(attributeValue.Children[i]); + Assert.True(token.IsCSharp); + content.Append(token.Content); + } + + Assert.Equal(prefix, attributeValue.Prefix); + Assert.Equal(expected, content.ToString()); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(attributeValue, attributeValue.Children, e.Message, e); + } + } + + public static void LiteralAttributeValue(string prefix, string expected, IntermediateNode node) + { + var attributeValue = Assert.IsType<HtmlAttributeValueIntermediateNode>(node); + + try + { + var content = new StringBuilder(); + for (var i = 0; i < attributeValue.Children.Count; i++) + { + var token = Assert.IsType<IntermediateToken>(attributeValue.Children[i]); + Assert.True(token.IsHtml); + content.Append(token.Content); + } + + Assert.Equal(prefix, attributeValue.Prefix); + Assert.Equal(expected, content.ToString()); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(attributeValue, e.Message); + } + } + + public static void CSharpExpression(string expected, IntermediateNode node) + { + try + { + var cSharp = Assert.IsType<CSharpExpressionIntermediateNode>(node); + + var content = new StringBuilder(); + for (var i = 0; i < cSharp.Children.Count; i++) + { + var token = Assert.IsType<IntermediateToken>(cSharp.Children[i]); + Assert.Equal(TokenKind.CSharp, token.Kind); + content.Append(token.Content); + } + + Assert.Equal(expected, content.ToString()); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, node.Children, e.Message, e); + } + } + + public static void BeginInstrumentation(string expected, IntermediateNode node) + { + try + { + var beginNode = Assert.IsType<CSharpCodeIntermediateNode>(node); + var content = new StringBuilder(); + for (var i = 0; i < beginNode.Children.Count; i++) + { + var token = Assert.IsType<IntermediateToken>(beginNode.Children[i]); + Assert.True(token.IsCSharp); + content.Append(token.Content); + } + + Assert.Equal($"BeginContext({expected});", content.ToString()); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, node.Children, e.Message, e); + } + } + + public static void EndInstrumentation(IntermediateNode node) + { + try + { + var endNode = Assert.IsType<CSharpCodeIntermediateNode>(node); + var content = new StringBuilder(); + for (var i = 0; i < endNode.Children.Count; i++) + { + var token = Assert.IsType<IntermediateToken>(endNode.Children[i]); + Assert.Equal(TokenKind.CSharp, token.Kind); + content.Append(token.Content); + } + Assert.Equal("EndContext();", content.ToString()); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, node.Children, e.Message, e); + } + } + + internal static void TagHelperFieldDeclaration(IntermediateNode node, string typeFullName) + { + try + { + var fieldNode = Assert.IsType<FieldDeclarationIntermediateNode>(node); + Assert.Equal(typeFullName, fieldNode.FieldType); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(node, e.Message); + } + } + + internal static void PreallocatedTagHelperPropertyValue( + IntermediateNode node, + string attributeName, + string value, + AttributeStructure valueStyle) + { + var propertyValueNode = Assert.IsType<PreallocatedTagHelperPropertyValueIntermediateNode>(node); + + try + { + Assert.Equal(attributeName, propertyValueNode.AttributeName); + Assert.Equal(value, propertyValueNode.Value); + Assert.Equal(valueStyle, propertyValueNode.AttributeStructure); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(propertyValueNode, e.Message); + } + } + + internal static void TagHelper(string tagName, TagMode tagMode, IEnumerable<TagHelperDescriptor> tagHelpers, IntermediateNode node, params Action<IntermediateNode>[] childValidators) + { + var tagHelperNode = Assert.IsType<TagHelperIntermediateNode>(node); + + try + { + Assert.Equal(tagName, tagHelperNode.TagName); + Assert.Equal(tagMode, tagHelperNode.TagMode); + + Assert.Equal(tagHelpers, tagHelperNode.TagHelpers, TagHelperDescriptorComparer.CaseSensitive); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(tagHelperNode, e.Message); + } + + Children(node, childValidators); + } + + internal static void TagHelperHtmlAttribute( + string name, + AttributeStructure valueStyle, + IntermediateNode node, + params Action<IntermediateNode>[] valueValidators) + { + var tagHelperHtmlAttribute = Assert.IsType<TagHelperHtmlAttributeIntermediateNode>(node); + + try + { + Assert.Equal(name, tagHelperHtmlAttribute.AttributeName); + Assert.Equal(valueStyle, tagHelperHtmlAttribute.AttributeStructure); + Children(tagHelperHtmlAttribute, valueValidators); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(tagHelperHtmlAttribute, tagHelperHtmlAttribute.Children, e.Message, e); + } + } + + internal static void SetPreallocatedTagHelperProperty(IntermediateNode node, string attributeName, string propertyName) + { + var setPreallocatedTagHelperProperty = Assert.IsType<PreallocatedTagHelperPropertyIntermediateNode>(node); + + try + { + Assert.Equal(attributeName, setPreallocatedTagHelperProperty.AttributeName); + Assert.Equal(propertyName, setPreallocatedTagHelperProperty.PropertyName); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(setPreallocatedTagHelperProperty, e.Message); + } + } + + internal static void SetTagHelperProperty( + string name, + string propertyName, + AttributeStructure valueStyle, + IntermediateNode node, + params Action<IntermediateNode>[] valueValidators) + { + var propertyNode = Assert.IsType<TagHelperPropertyIntermediateNode>(node); + + try + { + Assert.Equal(name, propertyNode.AttributeName); + Assert.Equal(propertyName, propertyNode.BoundAttribute.GetPropertyName()); + Assert.Equal(valueStyle, propertyNode.AttributeStructure); + Children(propertyNode, valueValidators); + } + catch (XunitException e) + { + throw new IntermediateNodeAssertException(propertyNode, propertyNode.Children, e.Message, e); + } + } + + private class IntermediateNodeAssertException : XunitException + { + public IntermediateNodeAssertException(IntermediateNode node, string userMessage) + : base(Format(node, null, null, userMessage)) + { + Node = node; + } + + public IntermediateNodeAssertException(IntermediateNode node, IEnumerable<IntermediateNode> nodes, string userMessage) + : base(Format(node, null, nodes, userMessage)) + { + Node = node; + Nodes = nodes; + } + + public IntermediateNodeAssertException( + IntermediateNode node, + IEnumerable<IntermediateNode> nodes, + string userMessage, + Exception innerException) + : base(Format(node, null, nodes, userMessage), innerException) + { + } + + public IntermediateNodeAssertException( + IntermediateNode node, + IntermediateNode[] ancestors, + IEnumerable<IntermediateNode> nodes, + string userMessage, + Exception innerException) + : base(Format(node, ancestors, nodes, userMessage), innerException) + { + } + + public IntermediateNode Node { get; } + + public IEnumerable<IntermediateNode> Nodes { get; } + + private static string Format(IntermediateNode node, IntermediateNode[] ancestors, IEnumerable<IntermediateNode> nodes, string userMessage) + { + var builder = new StringBuilder(); + builder.AppendLine(userMessage); + builder.AppendLine(); + + if (nodes != null) + { + builder.AppendLine("Nodes:"); + + foreach (var n in nodes) + { + builder.AppendLine(n.ToString()); + } + + builder.AppendLine(); + } + + + builder.AppendLine("Path:"); + + if (ancestors != null) + { + builder.AppendLine(); + builder.AppendLine("Path:"); + + foreach (var ancestor in ancestors) + { + builder.AppendLine(ancestor.ToString()); + } + } + + return builder.ToString(); + } + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs new file mode 100644 index 0000000000..2551fee917 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs @@ -0,0 +1,99 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + internal class BlockFactory + { + private SpanFactory _factory; + + public BlockFactory(SpanFactory factory) + { + _factory = factory; + } + + public Block EscapedMarkupTagBlock(string prefix, string suffix) + { + return EscapedMarkupTagBlock(prefix, suffix, AcceptedCharactersInternal.Any); + } + + public Block EscapedMarkupTagBlock(string prefix, string suffix, params SyntaxTreeNode[] children) + { + return EscapedMarkupTagBlock(prefix, suffix, AcceptedCharactersInternal.Any, children); + } + + public Block EscapedMarkupTagBlock( + string prefix, + string suffix, + AcceptedCharactersInternal acceptedCharacters, + params SyntaxTreeNode[] children) + { + var newChildren = new List<SyntaxTreeNode>( + new SyntaxTreeNode[] + { + _factory.Markup(prefix), + _factory.BangEscape(), + _factory.Markup(suffix).Accepts(acceptedCharacters) + }); + + newChildren.AddRange(children); + + return new MarkupTagBlock(newChildren.ToArray()); + } + + public Block MarkupTagBlock(string content) + { + return MarkupTagBlock(content, AcceptedCharactersInternal.Any); + } + + public Block MarkupTagBlock(string content, AcceptedCharactersInternal acceptedCharacters) + { + return new MarkupTagBlock( + _factory.Markup(content).Accepts(acceptedCharacters) + ); + } + + public HtmlCommentBlock HtmlCommentBlock(string content) + { + return HtmlCommentBlock(_factory, f => new SyntaxTreeNode[] { f.Markup(content).Accepts(AcceptedCharactersInternal.WhiteSpace) }); + } + + public static HtmlCommentBlock HtmlCommentBlock(SpanFactory factory, Func<SpanFactory, IEnumerable<SyntaxTreeNode>> nodesBuilder = null) + { + var nodes = new List<SyntaxTreeNode>(); + nodes.Add(factory.Markup("<!--").Accepts(AcceptedCharactersInternal.None)); + if (nodesBuilder != null) + { + nodes.AddRange(nodesBuilder(factory)); + } + nodes.Add(factory.Markup("-->").Accepts(AcceptedCharactersInternal.None)); + + return new HtmlCommentBlock(nodes.ToArray()); + } + + public Block TagHelperBlock( + string tagName, + TagMode tagMode, + SourceLocation start, + Block startTag, + SyntaxTreeNode[] children, + Block endTag) + { + var builder = new TagHelperBlockBuilder( + tagName, + tagMode, + attributes: new List<TagHelperAttributeNode>(), + children: children) + { + Start = start, + SourceStartTag = startTag, + SourceEndTag = endTag + }; + + return builder.Build(); + } + } +}
\ No newline at end of file diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs new file mode 100644 index 0000000000..56deb6b460 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs @@ -0,0 +1,230 @@ +// 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 +{ + // The product code doesn't need this, but having subclasses for the block types makes tests much cleaner :) + + internal class StatementBlock : Block + { + private const BlockKindInternal ThisBlockKind = BlockKindInternal.Statement; + + public StatementBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children) + : base(ThisBlockKind, children, chunkGenerator) + { + } + + public StatementBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children) + { + } + + public StatementBlock(params SyntaxTreeNode[] children) + : this(ParentChunkGenerator.Null, children) + { + } + } + + internal class DirectiveBlock : Block + { + private const BlockKindInternal ThisBlockKind = BlockKindInternal.Directive; + + public DirectiveBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children) + : base(ThisBlockKind, children, chunkGenerator) + { + } + + public DirectiveBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children) + { + } + + public DirectiveBlock(params SyntaxTreeNode[] children) + : this(ParentChunkGenerator.Null, children) + { + } + } + + internal class ExpressionBlock : Block + { + private const BlockKindInternal ThisBlockKind = BlockKindInternal.Expression; + + public ExpressionBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children) + : base(ThisBlockKind, children, chunkGenerator) + { + } + + public ExpressionBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children) + { + } + + public ExpressionBlock(params SyntaxTreeNode[] children) + : this(new ExpressionChunkGenerator(), children) + { + } + } + + internal class MarkupTagBlock : Block + { + private const BlockKindInternal ThisBlockKind = BlockKindInternal.Tag; + + public MarkupTagBlock(params SyntaxTreeNode[] children) + : base(ThisBlockKind, children, ParentChunkGenerator.Null) + { + } + } + + internal class MarkupBlock : Block + { + private const BlockKindInternal ThisBlockKind = BlockKindInternal.Markup; + + public MarkupBlock( + BlockKindInternal BlockKind, + IParentChunkGenerator chunkGenerator, + IReadOnlyList<SyntaxTreeNode> children) + : base(BlockKind, children, chunkGenerator) + { + } + + public MarkupBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children) + : this(ThisBlockKind, chunkGenerator, children) + { + } + + public MarkupBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children) + { + } + + public MarkupBlock(params SyntaxTreeNode[] children) + : this(ParentChunkGenerator.Null, children) + { + } + } + + internal class MarkupTagHelperBlock : TagHelperBlock + { + public MarkupTagHelperBlock(string tagName) + : this(tagName, tagMode: TagMode.StartTagAndEndTag, attributes: new List<TagHelperAttributeNode>()) + { + } + + public MarkupTagHelperBlock(string tagName, TagMode tagMode) + : this(tagName, tagMode, new List<TagHelperAttributeNode>()) + { + } + + public MarkupTagHelperBlock( + string tagName, + IList<TagHelperAttributeNode> attributes) + : this(tagName, TagMode.StartTagAndEndTag, attributes, children: new SyntaxTreeNode[0]) + { + } + + public MarkupTagHelperBlock( + string tagName, + TagMode tagMode, + IList<TagHelperAttributeNode> attributes) + : this(tagName, tagMode, attributes, new SyntaxTreeNode[0]) + { + } + + public MarkupTagHelperBlock(string tagName, params SyntaxTreeNode[] children) + : this( + tagName, + TagMode.StartTagAndEndTag, + attributes: new List<TagHelperAttributeNode>(), + children: children) + { + } + + public MarkupTagHelperBlock(string tagName, TagMode tagMode, params SyntaxTreeNode[] children) + : this(tagName, tagMode, new List<TagHelperAttributeNode>(), children) + { + } + + public MarkupTagHelperBlock( + string tagName, + IList<TagHelperAttributeNode> attributes, + params SyntaxTreeNode[] children) + : base(new TagHelperBlockBuilder( + tagName, + TagMode.StartTagAndEndTag, + attributes: attributes, + children: children)) + { + } + + public MarkupTagHelperBlock( + string tagName, + TagMode tagMode, + IList<TagHelperAttributeNode> attributes, + params SyntaxTreeNode[] children) + : base(new TagHelperBlockBuilder(tagName, tagMode, attributes, children)) + { + } + } + + internal class TemplateBlock : Block + { + private const BlockKindInternal ThisBlockKind = BlockKindInternal.Template; + + public TemplateBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children) + : base(ThisBlockKind, children, chunkGenerator) + { + } + + public TemplateBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children) + { + } + + public TemplateBlock(params SyntaxTreeNode[] children) + : this(new TemplateBlockChunkGenerator(), children) + { + } + + public TemplateBlock(IReadOnlyList<SyntaxTreeNode> children) + : this(new TemplateBlockChunkGenerator(), children) + { + } + } + + internal class CommentBlock : Block + { + private const BlockKindInternal ThisBlockKind = BlockKindInternal.Comment; + + public CommentBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children) + : base(ThisBlockKind, children, chunkGenerator) + { + } + + public CommentBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children) + { + } + + public CommentBlock(params SyntaxTreeNode[] children) + : this(new RazorCommentChunkGenerator(), children) + { + } + + public CommentBlock(IReadOnlyList<SyntaxTreeNode> children) + : this(new RazorCommentChunkGenerator(), children) + { + } + } + + internal class HtmlCommentBlock : Block + { + private const BlockKindInternal ThisBlockKind = BlockKindInternal.HtmlComment; + + public HtmlCommentBlock(params SyntaxTreeNode[] children) + : base(ThisBlockKind, children, ParentChunkGenerator.Null) + { + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ErrorCollector.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ErrorCollector.cs new file mode 100644 index 0000000000..13da12b61a --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ErrorCollector.cs @@ -0,0 +1,57 @@ +// 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.Text; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + public class ErrorCollector + { + private StringBuilder _message = new StringBuilder(); + private int _indent = 0; + + public bool Success { get; private set; } + + public string Message + { + get { return _message.ToString(); } + } + + public ErrorCollector() + { + Success = true; + } + + public void AddError(string msg, params object[] args) + { + Append("F", msg, args); + Success = false; + } + + public void AddMessage(string msg, params object[] args) + { + Append("P", msg, args); + } + + public IDisposable Indent() + { + _indent++; + return new DisposableAction(Unindent); + } + + public void Unindent() + { + _indent--; + } + + private void Append(string prefix, string msg, object[] args) + { + _message.Append(prefix); + _message.Append(":"); + _message.Append(new String('\t', _indent)); + _message.AppendFormat(msg, args); + _message.AppendLine(); + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs new file mode 100644 index 0000000000..4d80343ff3 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs @@ -0,0 +1,696 @@ +// 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.Diagnostics; +using System.Linq; +using System.Text; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + public abstract class ParserTestBase + { + internal static Block IgnoreOutput = new IgnoreOutputBlock(); + + internal ParserTestBase() + { + Factory = CreateSpanFactory(); + BlockFactory = CreateBlockFactory(); + } + + /// <summary> + /// Set to true to autocorrect the locations of spans to appear in document order with no gaps. + /// Use this when spans were not created in document order. + /// </summary> + protected bool FixupSpans { get; set; } + + internal SpanFactory Factory { get; private set; } + + internal BlockFactory BlockFactory { get; private set; } + + internal RazorSyntaxTree ParseBlock(string document, bool designTime) + { + return ParseBlock(RazorLanguageVersion.Latest, document, designTime); + } + + internal RazorSyntaxTree ParseBlock(RazorLanguageVersion version, string document, bool designTime) + { + return ParseBlock(version, document, null, designTime); + } + + internal RazorSyntaxTree ParseBlock(string document, IEnumerable<DirectiveDescriptor> directives, bool designTime) + { + return ParseBlock(RazorLanguageVersion.Latest, document, directives, designTime); + } + + internal abstract RazorSyntaxTree ParseBlock(RazorLanguageVersion version, string document, IEnumerable<DirectiveDescriptor> directives, bool designTime); + + internal RazorSyntaxTree ParseDocument(string document, bool designTime = false) + { + return ParseDocument(RazorLanguageVersion.Latest, document, designTime); + } + + internal RazorSyntaxTree ParseDocument(RazorLanguageVersion version, string document, bool designTime = false) + { + return ParseDocument(version, document, null, designTime); + } + + internal RazorSyntaxTree ParseDocument(string document, IEnumerable<DirectiveDescriptor> directives, bool designTime = false) + { + return ParseDocument(RazorLanguageVersion.Latest, document, directives, designTime); + } + + internal virtual RazorSyntaxTree ParseDocument(RazorLanguageVersion version, string document, IEnumerable<DirectiveDescriptor> directives, bool designTime = false) + { + directives = directives ?? Array.Empty<DirectiveDescriptor>(); + + var source = TestRazorSourceDocument.Create(document, filePath: null); + + var options = CreateParserOptions(version, directives, designTime); + var context = new ParserContext(source, options); + + var codeParser = new CSharpCodeParser(directives, context); + var markupParser = new HtmlMarkupParser(context); + + codeParser.HtmlParser = markupParser; + markupParser.CodeParser = codeParser; + + markupParser.ParseDocument(); + + var root = context.Builder.Build(); + var diagnostics = context.ErrorSink.Errors; + + var codeDocument = RazorCodeDocument.Create(source); + + var syntaxTree = RazorSyntaxTree.Create(root, source, diagnostics, options); + codeDocument.SetSyntaxTree(syntaxTree); + + var defaultDirectivePass = new DefaultDirectiveSyntaxTreePass(); + syntaxTree = defaultDirectivePass.Execute(codeDocument, syntaxTree); + + return syntaxTree; + } + + internal virtual RazorSyntaxTree ParseHtmlBlock(RazorLanguageVersion version, string document, IEnumerable<DirectiveDescriptor> directives, bool designTime = false) + { + directives = directives ?? Array.Empty<DirectiveDescriptor>(); + + var source = TestRazorSourceDocument.Create(document, filePath: null); + var options = CreateParserOptions(version, directives, designTime); + var context = new ParserContext(source, options); + + var parser = new HtmlMarkupParser(context); + parser.CodeParser = new CSharpCodeParser(directives, context) + { + HtmlParser = parser, + }; + + parser.ParseBlock(); + + var root = context.Builder.Build(); + var diagnostics = context.ErrorSink.Errors; + + return RazorSyntaxTree.Create(root, source, diagnostics, options); + } + + internal RazorSyntaxTree ParseCodeBlock(string document, bool designTime = false) + { + return ParseCodeBlock(RazorLanguageVersion.Latest, document, Enumerable.Empty<DirectiveDescriptor>(), designTime); + } + + internal virtual RazorSyntaxTree ParseCodeBlock( + RazorLanguageVersion version, + string document, + IEnumerable<DirectiveDescriptor> directives, + bool designTime) + { + directives = directives ?? Array.Empty<DirectiveDescriptor>(); + + var source = TestRazorSourceDocument.Create(document, filePath: null); + var options = CreateParserOptions(version, directives, designTime); + var context = new ParserContext(source, options); + + var parser = new CSharpCodeParser(directives, context); + parser.HtmlParser = new HtmlMarkupParser(context) + { + CodeParser = parser, + }; + + parser.ParseBlock(); + + var root = context.Builder.Build(); + var diagnostics = context.ErrorSink.Errors; + + return RazorSyntaxTree.Create(root, source, diagnostics, options); + } + + internal SpanFactory CreateSpanFactory() + { + return new SpanFactory(); + } + + internal abstract BlockFactory CreateBlockFactory(); + + internal virtual void ParseBlockTest(string document) + { + ParseBlockTest(document, null, false, new RazorDiagnostic[0]); + } + + internal virtual void ParseBlockTest(string document, bool designTime) + { + ParseBlockTest(document, null, designTime, new RazorDiagnostic[0]); + } + + internal virtual void ParseBlockTest(string document, params RazorDiagnostic[] expectedErrors) + { + ParseBlockTest(document, false, expectedErrors); + } + + internal virtual void ParseBlockTest(string document, bool designTime, params RazorDiagnostic[] expectedErrors) + { + ParseBlockTest(document, null, designTime, expectedErrors); + } + + internal virtual void ParseBlockTest(RazorLanguageVersion version, string document, Block expectedRoot) + { + ParseBlockTest(version, document, expectedRoot, false, null); + } + + internal virtual void ParseBlockTest(string document, Block expectedRoot) + { + ParseBlockTest(document, expectedRoot, false, null); + } + + internal virtual void ParseBlockTest(string document, IEnumerable<DirectiveDescriptor> directives, Block expectedRoot) + { + ParseBlockTest(document, directives, expectedRoot, false, null); + } + + internal virtual void ParseBlockTest(string document, Block expectedRoot, bool designTime) + { + ParseBlockTest(document, expectedRoot, designTime, null); + } + + internal virtual void ParseBlockTest(string document, Block expectedRoot, params RazorDiagnostic[] expectedErrors) + { + ParseBlockTest(document, expectedRoot, false, expectedErrors); + } + + internal virtual void ParseBlockTest(string document, IEnumerable<DirectiveDescriptor> directives, Block expectedRoot, params RazorDiagnostic[] expectedErrors) + { + ParseBlockTest(document, directives, expectedRoot, false, expectedErrors); + } + + internal virtual void ParseBlockTest(string document, Block expected, bool designTime, params RazorDiagnostic[] expectedErrors) + { + ParseBlockTest(document, null, expected, designTime, expectedErrors); + } + + internal virtual void ParseBlockTest(RazorLanguageVersion version, string document, Block expected, bool designTime, params RazorDiagnostic[] expectedErrors) + { + ParseBlockTest(version, document, null, expected, designTime, expectedErrors); + } + + internal virtual void ParseBlockTest(string document, IEnumerable<DirectiveDescriptor> directives, Block expected, bool designTime, params RazorDiagnostic[] expectedErrors) + { + ParseBlockTest(RazorLanguageVersion.Latest, document, directives, expected, designTime, expectedErrors); + } + + internal virtual void ParseBlockTest(RazorLanguageVersion version, string document, IEnumerable<DirectiveDescriptor> directives, Block expected, bool designTime, params RazorDiagnostic[] expectedErrors) + { + var result = ParseBlock(version, document, directives, designTime); + + if (FixupSpans) + { + SpancestryCorrector.Correct(expected); + + var span = expected.FindFirstDescendentSpan(); + span.ChangeStart(SourceLocation.Zero); + } + + SyntaxTreeVerifier.Verify(result); + SyntaxTreeVerifier.Verify(expected); + + if (!ReferenceEquals(expected, IgnoreOutput)) + { + EvaluateResults(result, expected, expectedErrors); + } + } + + internal virtual void SingleSpanBlockTest(string document, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters = AcceptedCharactersInternal.Any) + { + SingleSpanBlockTest(document, blockKind, spanType, acceptedCharacters, expectedError: null); + } + + internal virtual void SingleSpanBlockTest(string document, string spanContent, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters = AcceptedCharactersInternal.Any) + { + SingleSpanBlockTest(document, spanContent, blockKind, spanType, acceptedCharacters, expectedErrors: null); + } + + internal virtual void SingleSpanBlockTest(string document, BlockKindInternal blockKind, SpanKindInternal spanType, params RazorDiagnostic[] expectedError) + { + SingleSpanBlockTest(document, document, blockKind, spanType, expectedError); + } + + internal virtual void SingleSpanBlockTest(string document, string spanContent, BlockKindInternal blockKind, SpanKindInternal spanType, params RazorDiagnostic[] expectedErrors) + { + SingleSpanBlockTest(document, spanContent, blockKind, spanType, AcceptedCharactersInternal.Any, expectedErrors ?? new RazorDiagnostic[0]); + } + + internal virtual void SingleSpanBlockTest(string document, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters, params RazorDiagnostic[] expectedError) + { + SingleSpanBlockTest(document, document, blockKind, spanType, acceptedCharacters, expectedError); + } + + internal virtual void SingleSpanBlockTest(string document, string spanContent, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters, params RazorDiagnostic[] expectedErrors) + { + var result = ParseBlock(document, designTime: false); + + var builder = new BlockBuilder(); + builder.Type = blockKind; + var expected = ConfigureAndAddSpanToBlock(builder, Factory.Span(spanType, spanContent, spanType == SpanKindInternal.Markup).Accepts(acceptedCharacters)); + + if (FixupSpans) + { + SpancestryCorrector.Correct(expected); + + var span = expected.FindFirstDescendentSpan(); + span.ChangeStart(SourceLocation.Zero); + } + + SyntaxTreeVerifier.Verify(result); + SyntaxTreeVerifier.Verify(expected); + + if (!ReferenceEquals(expected, IgnoreOutput)) + { + EvaluateResults(result, expected, expectedErrors); + } + } + + internal virtual void ParseDocumentTest(string document) + { + ParseDocumentTest(document, null, false); + } + + internal virtual void ParseDocumentTest(string document, Block expectedRoot) + { + ParseDocumentTest(document, expectedRoot, false, null); + } + + internal virtual void ParseDocumentTest(string document, Block expectedRoot, params RazorDiagnostic[] expectedErrors) + { + ParseDocumentTest(document, expectedRoot, false, expectedErrors); + } + + internal virtual void ParseDocumentTest(string document, IEnumerable<DirectiveDescriptor> directives, Block expected, params RazorDiagnostic[] expectedErrors) + { + ParseDocumentTest(document, directives, expected, false, expectedErrors); + } + + internal virtual void ParseDocumentTest(string document, bool designTime) + { + ParseDocumentTest(document, null, designTime); + } + + internal virtual void ParseDocumentTest(string document, Block expectedRoot, bool designTime) + { + ParseDocumentTest(document, expectedRoot, designTime, null); + } + + internal virtual void ParseDocumentTest(string document, Block expected, bool designTime, params RazorDiagnostic[] expectedErrors) + { + ParseDocumentTest(document, null, expected, designTime, expectedErrors); + } + + internal virtual void ParseDocumentTest(string document, IEnumerable<DirectiveDescriptor> directives, Block expected, bool designTime, params RazorDiagnostic[] expectedErrors) + { + var result = ParseDocument(document, directives, designTime); + + if (FixupSpans) + { + SpancestryCorrector.Correct(expected); + + var span = expected.FindFirstDescendentSpan(); + span.ChangeStart(SourceLocation.Zero); + } + + SyntaxTreeVerifier.Verify(result); + SyntaxTreeVerifier.Verify(expected); + + if (!ReferenceEquals(expected, IgnoreOutput)) + { + EvaluateResults(result, expected, expectedErrors); + } + } + + [Conditional("PARSER_TRACE")] + private void WriteNode(int indent, SyntaxTreeNode node) + { + var content = node.ToString().Replace("\r", "\\r") + .Replace("\n", "\\n") + .Replace("{", "{{") + .Replace("}", "}}"); + if (indent > 0) + { + content = new String('.', indent * 2) + content; + } + WriteTraceLine(content); + var block = node as Block; + if (block != null) + { + foreach (SyntaxTreeNode child in block.Children) + { + WriteNode(indent + 1, child); + } + } + } + + internal static void EvaluateResults(RazorSyntaxTree result, Block expectedRoot) + { + EvaluateResults(result, expectedRoot, null); + } + + internal static void EvaluateResults(RazorSyntaxTree result, Block expectedRoot, IList<RazorDiagnostic> expectedErrors) + { + EvaluateParseTree(result.Root, expectedRoot); + EvaluateRazorErrors(result.Diagnostics, expectedErrors); + } + + internal static void EvaluateParseTree(Block actualRoot, Block expectedRoot) + { + // Evaluate the result + var collector = new ErrorCollector(); + + if (expectedRoot == null) + { + Assert.Null(actualRoot); + } + else + { + // Link all the nodes + expectedRoot.LinkNodes(); + Assert.NotNull(actualRoot); + EvaluateSyntaxTreeNode(collector, actualRoot, expectedRoot); + if (collector.Success) + { + WriteTraceLine("Parse Tree Validation Succeeded:" + Environment.NewLine + collector.Message); + } + else + { + Assert.True(false, Environment.NewLine + collector.Message); + } + } + } + + private static void EvaluateTagHelperAttribute( + ErrorCollector collector, + TagHelperAttributeNode actual, + TagHelperAttributeNode expected) + { + if (actual.Name != expected.Name) + { + collector.AddError("{0} - FAILED :: Attribute names do not match", expected.Name); + } + else + { + collector.AddMessage("{0} - PASSED :: Attribute names match", expected.Name); + } + + if (actual.AttributeStructure != expected.AttributeStructure) + { + collector.AddError("{0} - FAILED :: Attribute value styles do not match", expected.AttributeStructure.ToString()); + } + else + { + collector.AddMessage("{0} - PASSED :: Attribute value style match", expected.AttributeStructure); + } + + if (actual.AttributeStructure != AttributeStructure.Minimized) + { + EvaluateSyntaxTreeNode(collector, actual.Value, expected.Value); + } + } + + private static void EvaluateSyntaxTreeNode(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected) + { + if (actual == null) + { + AddNullActualError(collector, actual, expected); + return; + } + + 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.ChunkGenerator.Equals(actual.ChunkGenerator)) + { + AddMismatchError(collector, actual, expected); + } + else + { + if (actual is TagHelperBlock) + { + EvaluateTagHelperBlock(collector, actual as TagHelperBlock, expected as TagHelperBlock); + } + + AddPassedMessage(collector, expected); + using (collector.Indent()) + { + var expectedNodes = expected.Children.GetEnumerator(); + var 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 EvaluateTagHelperBlock(ErrorCollector collector, TagHelperBlock actual, TagHelperBlock expected) + { + if (expected == null) + { + AddMismatchError(collector, actual, expected); + } + else + { + if (!string.Equals(expected.TagName, actual.TagName, StringComparison.Ordinal)) + { + collector.AddError( + "{0} - FAILED :: TagName mismatch for TagHelperBlock :: ACTUAL: {1}", + expected.TagName, + actual.TagName); + } + + if (expected.TagMode != actual.TagMode) + { + collector.AddError( + $"{expected.TagMode} - FAILED :: {nameof(TagMode)} for {nameof(TagHelperBlock)} " + + $"{actual.TagName} :: ACTUAL: {actual.TagMode}"); + } + + var expectedAttributes = expected.Attributes.GetEnumerator(); + var actualAttributes = actual.Attributes.GetEnumerator(); + + while (expectedAttributes.MoveNext()) + { + if (!actualAttributes.MoveNext()) + { + collector.AddError("{0} - FAILED :: No more attributes on this node", expectedAttributes.Current); + } + else + { + EvaluateTagHelperAttribute(collector, actualAttributes.Current, expectedAttributes.Current); + } + } + while (actualAttributes.MoveNext()) + { + collector.AddError("End of Attributes - FAILED :: Found Attribute: {0}", actualAttributes.Current.Name); + } + } + } + + 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); + } + + internal static void EvaluateRazorErrors(IEnumerable<RazorDiagnostic> actualErrors, IList<RazorDiagnostic> expectedErrors) + { + var realCount = actualErrors.Count(); + + // Evaluate the errors + if (expectedErrors == null || expectedErrors.Count == 0) + { + Assert.True( + realCount == 0, + "Expected that no errors would be raised, but the following errors were:" + Environment.NewLine + FormatErrors(actualErrors)); + } + else + { + Assert.True( + expectedErrors.Count == realCount, + $"Expected that {expectedErrors.Count} errors would be raised, but {realCount} errors were." + + $"{Environment.NewLine}Expected Errors: {Environment.NewLine}{FormatErrors(expectedErrors)}" + + $"{Environment.NewLine}Actual Errors: {Environment.NewLine}{FormatErrors(actualErrors)}"); + Assert.Equal(expectedErrors, actualErrors); + } + WriteTraceLine("Expected Errors were raised:" + Environment.NewLine + FormatErrors(expectedErrors)); + } + + internal static string FormatErrors(IEnumerable<RazorDiagnostic> errors) + { + if (errors == null) + { + return "\t<< No Errors >>"; + } + + var builder = new StringBuilder(); + foreach (var error in errors) + { + builder.AppendFormat("\t{0}", error); + builder.AppendLine(); + } + return builder.ToString(); + } + + [Conditional("PARSER_TRACE")] + private static void WriteTraceLine(string format, params object[] args) + { + Trace.WriteLine(string.Format(format, args)); + } + + internal virtual Block CreateSimpleBlockAndSpan(string spanContent, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters = AcceptedCharactersInternal.Any) + { + var span = Factory.Span(spanType, spanContent, spanType == SpanKindInternal.Markup).Accepts(acceptedCharacters); + var b = new BlockBuilder() + { + Type = blockKind + }; + return ConfigureAndAddSpanToBlock(b, span); + } + + internal virtual Block ConfigureAndAddSpanToBlock(BlockBuilder block, SpanConstructor span) + { + switch (block.Type) + { + case BlockKindInternal.Markup: + span.With(new MarkupChunkGenerator()); + break; + case BlockKindInternal.Statement: + span.With(new StatementChunkGenerator()); + break; + case BlockKindInternal.Expression: + block.ChunkGenerator = new ExpressionChunkGenerator(); + span.With(new ExpressionChunkGenerator()); + break; + } + block.Children.Add(span); + return block.Build(); + } + + private static RazorParserOptions CreateParserOptions( + RazorLanguageVersion version, + IEnumerable<DirectiveDescriptor> directives, + bool designTime) + { + return new DefaultRazorParserOptions( + directives.ToArray(), + designTime, + parseLeadingDirectives: false, + version: version); + } + + private class IgnoreOutputBlock : Block + { + public IgnoreOutputBlock() : base(BlockKindInternal.Template, new SyntaxTreeNode[0], null) { } + } + + // Corrects the parents and previous/next information for spans + internal class SpancestryCorrector : ParserVisitor + { + private SpancestryCorrector() + { + } + + protected Block CurrentBlock { get; set; } + + protected Span LastSpan { get; set; } + + public static void Correct(Block block) + { + new SpancestryCorrector().VisitBlock(block); + } + + public override void VisitBlock(Block block) + { + CurrentBlock = block; + base.VisitBlock(block); + } + + public override void VisitSpan(Span span) + { + span.Parent = CurrentBlock; + + span.Previous = LastSpan; + if (LastSpan != null) + { + LastSpan.Next = span; + } + + LastSpan = span; + } + } + } +}
\ No newline at end of file diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/RawTextSymbol.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/RawTextSymbol.cs new file mode 100644 index 0000000000..e2c3ccff64 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/RawTextSymbol.cs @@ -0,0 +1,55 @@ +// 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.Globalization; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + internal class RawTextSymbol : ISymbol + { + public RawTextSymbol(SourceLocation start, string content) + { + Start = start; + Content = content; + } + + public SourceLocation Start { get; private set; } + public string Content { get; } + public Span Parent { get; set; } + + public override bool Equals(object obj) + { + var other = obj as RawTextSymbol; + return other != null && Equals(Start, other.Start) && Equals(Content, other.Content); + } + + internal bool EquivalentTo(ISymbol sym) + { + return Equals(Start, sym.Start) && Equals(Content, sym.Content); + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties. + return Content == null ? 0 : Content.GetHashCode(); + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0} RAW - [{1}]", Start, Content); + } + + internal void CalculateStart(Span prev) + { + if (prev == null) + { + Start = SourceLocation.Zero; + } + else + { + Start = new SourceLocationTracker(prev.Start).UpdateLocation(prev.Content).CurrentLocation; + } + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/TestSpanBuilder.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/TestSpanBuilder.cs new file mode 100644 index 0000000000..ed7b3aadde --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/TestSpanBuilder.cs @@ -0,0 +1,443 @@ +// 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.Linq; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + internal static class SpanFactoryExtensions + { + public static UnclassifiedCodeSpanConstructor EmptyCSharp(this SpanFactory self) + { + return new UnclassifiedCodeSpanConstructor( + self.Span( + SpanKindInternal.Code, + new CSharpSymbol(string.Empty, CSharpSymbolType.Unknown))); + } + + public static SpanConstructor EmptyHtml(this SpanFactory self) + { + return self + .Span( + SpanKindInternal.Markup, + new HtmlSymbol(string.Empty, HtmlSymbolType.Unknown)) + .With(new MarkupChunkGenerator()); + } + + public static UnclassifiedCodeSpanConstructor Code(this SpanFactory self, string content) + { + return new UnclassifiedCodeSpanConstructor( + self.Span(SpanKindInternal.Code, content, markup: false)); + } + + public static SpanConstructor CodeTransition(this SpanFactory self) + { + return self + .Span(SpanKindInternal.Transition, SyntaxConstants.TransitionString, markup: false) + .Accepts(AcceptedCharactersInternal.None); + } + + public static SpanConstructor CodeTransition(this SpanFactory self, string content) + { + return self.Span(SpanKindInternal.Transition, content, markup: false).Accepts(AcceptedCharactersInternal.None); + } + + public static SpanConstructor CodeTransition(this SpanFactory self, CSharpSymbolType type) + { + return self + .Span(SpanKindInternal.Transition, SyntaxConstants.TransitionString, type) + .Accepts(AcceptedCharactersInternal.None); + } + + public static SpanConstructor CodeTransition(this SpanFactory self, string content, CSharpSymbolType type) + { + return self.Span(SpanKindInternal.Transition, content, type).Accepts(AcceptedCharactersInternal.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self) + { + return self + .Span(SpanKindInternal.Transition, SyntaxConstants.TransitionString, markup: true) + .Accepts(AcceptedCharactersInternal.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self, string content) + { + return self.Span(SpanKindInternal.Transition, content, markup: true).Accepts(AcceptedCharactersInternal.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self, HtmlSymbolType type) + { + return self + .Span(SpanKindInternal.Transition, SyntaxConstants.TransitionString, type) + .Accepts(AcceptedCharactersInternal.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self, string content, HtmlSymbolType type) + { + return self.Span(SpanKindInternal.Transition, content, type).Accepts(AcceptedCharactersInternal.None); + } + + public static SpanConstructor MetaCode(this SpanFactory self, string content) + { + return self.Span(SpanKindInternal.MetaCode, content, markup: false); + } + + public static SpanConstructor MetaCode(this SpanFactory self, string content, CSharpSymbolType type) + { + return self.Span(SpanKindInternal.MetaCode, content, type); + } + + public static SpanConstructor MetaMarkup(this SpanFactory self, string content) + { + return self.Span(SpanKindInternal.MetaCode, content, markup: true); + } + + public static SpanConstructor MetaMarkup(this SpanFactory self, string content, HtmlSymbolType type) + { + return self.Span(SpanKindInternal.MetaCode, content, type); + } + + public static SpanConstructor Comment(this SpanFactory self, string content, CSharpSymbolType type) + { + return self.Span(SpanKindInternal.Comment, content, type); + } + + public static SpanConstructor Comment(this SpanFactory self, string content, HtmlSymbolType type) + { + return self.Span(SpanKindInternal.Comment, content, type); + } + + public static SpanConstructor BangEscape(this SpanFactory self) + { + return self + .Span(SpanKindInternal.MetaCode, "!", markup: true) + .With(SpanChunkGenerator.Null) + .Accepts(AcceptedCharactersInternal.None); + } + + public static SpanConstructor Markup(this SpanFactory self, string content) + { + return self.Span(SpanKindInternal.Markup, content, markup: true).With(new MarkupChunkGenerator()); + } + + public static SpanConstructor Markup(this SpanFactory self, params string[] content) + { + return self.Span(SpanKindInternal.Markup, content, markup: true).With(new MarkupChunkGenerator()); + } + + public static SpanConstructor CodeMarkup(this SpanFactory self, params string[] content) + { + return self + .Span(SpanKindInternal.Code, content, markup: true) + .AsCodeMarkup(); + } + + public static SpanConstructor CSharpCodeMarkup(this SpanFactory self, string content) + { + return self.Code(content) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .AsCodeMarkup(); + } + + public static SpanConstructor AsCodeMarkup(this SpanConstructor self) + { + return self + .With(new ImplicitExpressionEditHandler( + (content) => SpanConstructor.TestTokenizer(content), + CSharpCodeParser.DefaultKeywords, + acceptTrailingDot: true)) + .With(new MarkupChunkGenerator()) + .Accepts(AcceptedCharactersInternal.AnyExceptNewline); + } + + public static SpanConstructor AsDirectiveToken(this SpanConstructor self, DirectiveTokenDescriptor descriptor) + { + return self + .With(new DirectiveTokenChunkGenerator(descriptor)) + .With(new DirectiveTokenEditHandler((content) => SpanConstructor.TestTokenizer(content))) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace); + } + + public static SourceLocation GetLocationAndAdvance(this SourceLocationTracker self, string content) + { + var ret = self.CurrentLocation; + self.UpdateLocation(content); + return ret; + } + } + + internal class SpanFactory + { + public SpanFactory() + { + LocationTracker = new SourceLocationTracker(); + + MarkupTokenizerFactory = doc => new HtmlTokenizer(doc); + CodeTokenizerFactory = doc => new CSharpTokenizer(doc); + } + + public Func<ITextDocument, HtmlTokenizer> MarkupTokenizerFactory { get; } + public Func<ITextDocument, CSharpTokenizer> CodeTokenizerFactory { get; } + public SourceLocationTracker LocationTracker { get; } + + + public SpanConstructor Span(SpanKindInternal kind, string content, CSharpSymbolType type) + { + return CreateSymbolSpan(kind, content, () => new CSharpSymbol(content, type)); + } + + public SpanConstructor Span(SpanKindInternal kind, string content, HtmlSymbolType type) + { + return CreateSymbolSpan(kind, content, () => new HtmlSymbol(content, type)); + } + + public SpanConstructor Span(SpanKindInternal kind, string content, bool markup) + { + return new SpanConstructor(kind, LocationTracker.CurrentLocation, Tokenize(new[] { content }, markup)); + } + + public SpanConstructor Span(SpanKindInternal kind, string[] content, bool markup) + { + return new SpanConstructor(kind, LocationTracker.CurrentLocation, Tokenize(content, markup)); + } + + public SpanConstructor Span(SpanKindInternal kind, params ISymbol[] symbols) + { + var start = LocationTracker.CurrentLocation; + foreach (var symbol in symbols) + { + LocationTracker.UpdateLocation(symbol.Content); + } + + return new SpanConstructor(kind, start, symbols); + } + + private SpanConstructor CreateSymbolSpan(SpanKindInternal kind, string content, Func<ISymbol> ctor) + { + var start = LocationTracker.CurrentLocation; + LocationTracker.UpdateLocation(content); + + return new SpanConstructor(kind, start, new[] { ctor() }); + } + + public void Reset() + { + LocationTracker.CurrentLocation = SourceLocation.Zero; + } + + private IEnumerable<ISymbol> Tokenize(IEnumerable<string> contentFragments, bool markup) + { + return contentFragments.SelectMany(fragment => Tokenize(fragment, markup)); + } + + private IEnumerable<ISymbol> Tokenize(string content, bool markup) + { + var tokenizer = MakeTokenizer(markup, new SeekableTextReader(content, filePath: null)); + ISymbol symbol; + ISymbol last = null; + + while ((symbol = tokenizer.NextSymbol()) != null) + { + last = symbol; + yield return symbol; + } + + LocationTracker.UpdateLocation(content); + } + + private ITokenizer MakeTokenizer(bool markup, SeekableTextReader seekableTextReader) + { + if (markup) + { + return MarkupTokenizerFactory(seekableTextReader); + } + else + { + return CodeTokenizerFactory(seekableTextReader); + } + } + } + + internal static class SpanConstructorExtensions + { + public static SpanConstructor Accepts(this SpanConstructor self, AcceptedCharactersInternal accepted) + { + return self.With(eh => eh.AcceptedCharacters = accepted); + } + + public static SpanConstructor AutoCompleteWith(this SpanConstructor self, string autoCompleteString) + { + return AutoCompleteWith(self, autoCompleteString, atEndOfSpan: false); + } + + public static SpanConstructor AutoCompleteWith( + this SpanConstructor self, + string autoCompleteString, + bool atEndOfSpan) + { + return self.With(new AutoCompleteEditHandler( + (content) => SpanConstructor.TestTokenizer(content), + autoCompleteAtEndOfSpan: atEndOfSpan) + { + AutoCompleteString = autoCompleteString + }); + } + } + + internal class UnclassifiedCodeSpanConstructor + { + SpanConstructor _self; + + public UnclassifiedCodeSpanConstructor(SpanConstructor self) + { + _self = self; + } + + public SpanConstructor AsMetaCode() + { + _self.Builder.Kind = SpanKindInternal.MetaCode; + return _self; + } + + public SpanConstructor AsStatement() + { + return _self.With(new StatementChunkGenerator()); + } + + public SpanConstructor AsExpression() + { + return _self.With(new ExpressionChunkGenerator()); + } + + public SpanConstructor AsImplicitExpression(ISet<string> keywords) + { + return AsImplicitExpression(keywords, acceptTrailingDot: false); + } + + public SpanConstructor AsImplicitExpression(ISet<string> keywords, bool acceptTrailingDot) + { + return _self + .With(new ImplicitExpressionEditHandler((content) => SpanConstructor.TestTokenizer(content), keywords, acceptTrailingDot)) + .With(new ExpressionChunkGenerator()); + } + + public SpanConstructor AsNamespaceImport(string ns) + { + return _self.With(new AddImportChunkGenerator(ns)); + } + + public SpanConstructor Hidden() + { + return _self.With(SpanChunkGenerator.Null); + } + + public SpanConstructor AsAddTagHelper( + string lookupText, + string directiveText, + string typePattern = null, + string assemblyName = null, + params RazorDiagnostic[] errors) + { + var diagnostics = errors.ToList(); + return _self + .With(new AddTagHelperChunkGenerator(lookupText, directiveText, typePattern, assemblyName, diagnostics)) + .Accepts(AcceptedCharactersInternal.AnyExceptNewline); + } + + public SpanConstructor AsRemoveTagHelper( + string lookupText, + string directiveText, + string typePattern = null, + string assemblyName = null, + params RazorDiagnostic[] errors) + { + var diagnostics = errors.ToList(); + return _self + .With(new RemoveTagHelperChunkGenerator(lookupText, directiveText, typePattern, assemblyName, diagnostics)) + .Accepts(AcceptedCharactersInternal.AnyExceptNewline); + } + + public SpanConstructor AsTagHelperPrefixDirective(string prefix, string directiveText, params RazorDiagnostic[] errors) + { + var diagnostics = errors.ToList(); + return _self + .With(new TagHelperPrefixDirectiveChunkGenerator(prefix, directiveText, diagnostics)) + .Accepts(AcceptedCharactersInternal.AnyExceptNewline); + } + + public SpanConstructor As(ISpanChunkGenerator chunkGenerator) + { + return _self.With(chunkGenerator); + } + } + + internal class SpanConstructor + { + public SpanBuilder Builder { get; private set; } + + internal static IEnumerable<ISymbol> TestTokenizer(string str) + { + yield return new RawTextSymbol(SourceLocation.Zero, str); + } + + public SpanConstructor(SpanKindInternal kind, SourceLocation location, IEnumerable<ISymbol> symbols) + { + Builder = new SpanBuilder(location); + Builder.Kind = kind; + Builder.EditHandler = SpanEditHandler.CreateDefault((content) => SpanConstructor.TestTokenizer(content)); + foreach (ISymbol sym in symbols) + { + Builder.Accept(sym); + } + } + + private Span Build() + { + return Builder.Build(); + } + + public SpanConstructor As(SpanKindInternal spanKind) + { + Builder.Kind = spanKind; + return this; + } + + public SpanConstructor With(ISpanChunkGenerator generator) + { + Builder.ChunkGenerator = generator; + return this; + } + + public SpanConstructor With(SpanEditHandler handler) + { + Builder.EditHandler = handler; + return this; + } + + public SpanConstructor With(Action<ISpanChunkGenerator> generatorConfigurer) + { + generatorConfigurer(Builder.ChunkGenerator); + return this; + } + + public SpanConstructor With(Action<SpanEditHandler> handlerConfigurer) + { + handlerConfigurer(Builder.EditHandler); + return this; + } + + public static implicit operator Span(SpanConstructor self) + { + return self.Build(); + } + + public SpanConstructor Hidden() + { + Builder.ChunkGenerator = SpanChunkGenerator.Null; + return this; + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorEngineBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorEngineBuilderExtensions.cs new file mode 100644 index 0000000000..3d7977ea2d --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorEngineBuilderExtensions.cs @@ -0,0 +1,71 @@ +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language.IntegrationTests; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language +{ + [Obsolete("This class is obsolete and will be removed in a future version.")] + public static class RazorEngineBuilderExtensions + { + public static IRazorEngineBuilder AddTagHelpers(this IRazorEngineBuilder builder, params TagHelperDescriptor[] tagHelpers) + { + return AddTagHelpers(builder, (IEnumerable<TagHelperDescriptor>)tagHelpers); + } + + public static IRazorEngineBuilder AddTagHelpers(this IRazorEngineBuilder builder, IEnumerable<TagHelperDescriptor> tagHelpers) + { + var feature = (TestTagHelperFeature)builder.Features.OfType<ITagHelperFeature>().FirstOrDefault(); + if (feature == null) + { + feature = new TestTagHelperFeature(); + builder.Features.Add(feature); + } + + feature.TagHelpers.AddRange(tagHelpers); + return builder; + } + + public static IRazorEngineBuilder ConfigureDocumentClassifier(this IRazorEngineBuilder builder) + { + var feature = builder.Features.OfType<DefaultDocumentClassifierPassFeature>().FirstOrDefault(); + if (feature == null) + { + feature = new DefaultDocumentClassifierPassFeature(); + builder.Features.Add(feature); + } + + feature.ConfigureNamespace.Clear(); + feature.ConfigureClass.Clear(); + feature.ConfigureMethod.Clear(); + + feature.ConfigureNamespace.Add((RazorCodeDocument codeDocument, NamespaceDeclarationIntermediateNode node) => + { + node.Content = "Microsoft.AspNetCore.Razor.Language.IntegrationTests.TestFiles"; + }); + + feature.ConfigureClass.Add((RazorCodeDocument codeDocument, ClassDeclarationIntermediateNode node) => + { + node.ClassName = IntegrationTestBase.FileName.Replace('/', '_'); + node.Modifiers.Clear(); + node.Modifiers.Add("public"); + }); + + feature.ConfigureMethod.Add((RazorCodeDocument codeDocument, MethodDeclarationIntermediateNode node) => + { + node.Modifiers.Clear(); + node.Modifiers.Add("public"); + node.Modifiers.Add("async"); + node.MethodName = "ExecuteAsync"; + node.ReturnType = typeof(Task).FullName; + }); + + return builder; + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorProjectEngineBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorProjectEngineBuilderExtensions.cs new file mode 100644 index 0000000000..bcea84e918 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorProjectEngineBuilderExtensions.cs @@ -0,0 +1,70 @@ +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language.IntegrationTests; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public static class RazorProjectEngineBuilderExtensions + { + public static RazorProjectEngineBuilder AddTagHelpers(this RazorProjectEngineBuilder builder, params TagHelperDescriptor[] tagHelpers) + { + return AddTagHelpers(builder, (IEnumerable<TagHelperDescriptor>)tagHelpers); + } + + public static RazorProjectEngineBuilder AddTagHelpers(this RazorProjectEngineBuilder builder, IEnumerable<TagHelperDescriptor> tagHelpers) + { + var feature = (TestTagHelperFeature)builder.Features.OfType<ITagHelperFeature>().FirstOrDefault(); + if (feature == null) + { + feature = new TestTagHelperFeature(); + builder.Features.Add(feature); + } + + feature.TagHelpers.AddRange(tagHelpers); + return builder; + } + + public static RazorProjectEngineBuilder ConfigureDocumentClassifier(this RazorProjectEngineBuilder builder) + { + var feature = builder.Features.OfType<DefaultDocumentClassifierPassFeature>().FirstOrDefault(); + if (feature == null) + { + feature = new DefaultDocumentClassifierPassFeature(); + builder.Features.Add(feature); + } + + feature.ConfigureNamespace.Clear(); + feature.ConfigureClass.Clear(); + feature.ConfigureMethod.Clear(); + + feature.ConfigureNamespace.Add((RazorCodeDocument codeDocument, NamespaceDeclarationIntermediateNode node) => + { + node.Content = "Microsoft.AspNetCore.Razor.Language.IntegrationTests.TestFiles"; + }); + + feature.ConfigureClass.Add((RazorCodeDocument codeDocument, ClassDeclarationIntermediateNode node) => + { + node.ClassName = IntegrationTestBase.FileName.Replace('/', '_'); + node.Modifiers.Clear(); + node.Modifiers.Add("public"); + }); + + feature.ConfigureMethod.Add((RazorCodeDocument codeDocument, MethodDeclarationIntermediateNode node) => + { + node.Modifiers.Clear(); + node.Modifiers.Add("public"); + node.Modifiers.Add("async"); + node.MethodName = "ExecuteAsync"; + node.ReturnType = typeof(Task).FullName; + }); + + return builder; + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs new file mode 100644 index 0000000000..2e5f86d1cc --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs @@ -0,0 +1,42 @@ +// 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 Microsoft.AspNetCore.Razor.Language.Legacy; + +namespace Microsoft.AspNetCore.Razor.Language +{ + // Verifies recursively that a syntax tree has no gaps in terms of position/location. + internal class SyntaxTreeVerifier : ParserVisitor + { + private readonly SourceLocationTracker _tracker = new SourceLocationTracker(SourceLocation.Zero); + + private SyntaxTreeVerifier() + { + } + + public static void Verify(RazorSyntaxTree syntaxTree) + { + Verify(syntaxTree.Root); + } + + public static void Verify(Block block) + { + new SyntaxTreeVerifier().VisitBlock(block); + } + + public override void VisitSpan(Span span) + { + var start = span.Start; + if (!start.Equals(_tracker.CurrentLocation)) + { + throw new InvalidOperationException($"Span starting at {span.Start} should start at {_tracker.CurrentLocation} - {span} "); + } + + for (var i = 0; i < span.Symbols.Count; i++) + { + _tracker.UpdateLocation(span.Symbols[i].Content); + } + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestBoundAttributeDescriptorBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestBoundAttributeDescriptorBuilderExtensions.cs new file mode 100644 index 0000000000..23987d03bf --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestBoundAttributeDescriptorBuilderExtensions.cs @@ -0,0 +1,123 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public static class TestBoundAttributeDescriptorBuilderExtensions + { + public static BoundAttributeDescriptorBuilder Name(this BoundAttributeDescriptorBuilder builder, string name) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Name = name; + + return builder; + } + + public static BoundAttributeDescriptorBuilder TypeName(this BoundAttributeDescriptorBuilder builder, string typeName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.TypeName = typeName; + + return builder; + } + + public static BoundAttributeDescriptorBuilder PropertyName(this BoundAttributeDescriptorBuilder builder, string propertyName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.SetPropertyName(propertyName); + + return builder; + } + + public static BoundAttributeDescriptorBuilder DisplayName(this BoundAttributeDescriptorBuilder builder, string displayName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.DisplayName = displayName; + + return builder; + } + + public static BoundAttributeDescriptorBuilder AsEnum(this BoundAttributeDescriptorBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.IsEnum = true; + + return builder; + } + + public static BoundAttributeDescriptorBuilder AsDictionaryAttribute( + this BoundAttributeDescriptorBuilder builder, + string attributeNamePrefix, + string valueTypeName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.IsDictionary = true; + builder.IndexerAttributeNamePrefix = attributeNamePrefix; + builder.IndexerValueTypeName = valueTypeName; + + return builder; + } + + public static BoundAttributeDescriptorBuilder Documentation(this BoundAttributeDescriptorBuilder builder, string documentation) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Documentation = documentation; + + return builder; + } + + public static BoundAttributeDescriptorBuilder AddMetadata(this BoundAttributeDescriptorBuilder builder, string key, string value) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Metadata[key] = value; + + return builder; + } + + public static BoundAttributeDescriptorBuilder AddDiagnostic(this BoundAttributeDescriptorBuilder builder, RazorDiagnostic diagnostic) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Diagnostics.Add(diagnostic); + + return builder; + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestFile.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestFile.cs new file mode 100644 index 0000000000..c04b9243ff --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestFile.cs @@ -0,0 +1,89 @@ +// 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; +using System.Reflection; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public class TestFile + { + private TestFile(string resourceName, Assembly assembly) + { + Assembly = assembly; + ResourceName = Assembly.GetName().Name + "." + resourceName.Replace('/', '.'); + } + + public Assembly Assembly { get; } + + public string ResourceName { get; } + + public static TestFile Create(string resourceName, Type type) + { + return new TestFile(resourceName, type.GetTypeInfo().Assembly); + } + + public static TestFile Create(string resourceName, Assembly assembly) + { + return new TestFile(resourceName, assembly); + } + + public Stream OpenRead() + { + var stream = Assembly.GetManifestResourceStream(ResourceName); + if (stream == null) + { + Assert.True(false, string.Format("Manifest resource: {0} not found", ResourceName)); + } + + return stream; + } + + public bool Exists() + { + var resourceNames = Assembly.GetManifestResourceNames(); + foreach (var resourceName in resourceNames) + { + // Resource names are case-sensitive. + if (string.Equals(ResourceName, resourceName, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + public string ReadAllText() + { + using (var reader = new StreamReader(OpenRead())) + { + // The .Replace() calls normalize line endings, in case you get \n instead of \r\n + // since all the unit tests rely on the assumption that the files will have \r\n endings. + return reader.ReadToEnd().Replace("\r", "").Replace("\n", "\r\n"); + } + } + + /// <summary> + /// Saves the file to the specified path. + /// </summary> + public void Save(string filePath) + { + var directory = Path.GetDirectoryName(filePath); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + using (var outStream = File.Create(filePath)) + { + using (var inStream = OpenRead()) + { + inStream.CopyTo(outStream); + } + } + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestProject.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestProject.cs new file mode 100644 index 0000000000..4cc8d85d61 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestProject.cs @@ -0,0 +1,28 @@ +// 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; +using Microsoft.AspNetCore.Testing; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public static class TestProject + { + public static string GetProjectDirectory(Type type) + { + var solutionDir = TestPathUtilities.GetSolutionRootDirectory("Razor"); + + var assemblyName = type.Assembly.GetName().Name; + var projectDirectory = Path.Combine(solutionDir, "test", assemblyName); + if (!Directory.Exists(projectDirectory)) + { + throw new InvalidOperationException( +$@"Could not locate project directory for type {type.FullName}. +Directory probe path: {projectDirectory}."); + } + + return projectDirectory; + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorCodeDocument.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorCodeDocument.cs new file mode 100644 index 0000000000..4a6430d618 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorCodeDocument.cs @@ -0,0 +1,27 @@ +// 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 +{ + public static class TestRazorCodeDocument + { + public static RazorCodeDocument CreateEmpty() + { + var source = TestRazorSourceDocument.Create(content: string.Empty); + return new DefaultRazorCodeDocument(source, imports: null); + } + + public static RazorCodeDocument Create(string content) + { + var source = TestRazorSourceDocument.Create(content); + return new DefaultRazorCodeDocument(source, imports: null); + } + + public static RazorCodeDocument Create(RazorSourceDocument source, IEnumerable<RazorSourceDocument> imports) + { + return new DefaultRazorCodeDocument(source, imports); + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs new file mode 100644 index 0000000000..f16fff0274 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs @@ -0,0 +1,41 @@ +// 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.Linq; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class TestRazorProjectFileSystem : DefaultRazorProjectFileSystem + { + public new static RazorProjectFileSystem Empty = new TestRazorProjectFileSystem(); + + private readonly Dictionary<string, RazorProjectItem> _lookup; + + public TestRazorProjectFileSystem() + : this(new RazorProjectItem[0]) + { + } + + public TestRazorProjectFileSystem(IList<RazorProjectItem> items) : base("/") + { + _lookup = items.ToDictionary(item => item.FilePath); + } + + public override IEnumerable<RazorProjectItem> EnumerateItems(string basePath) + { + throw new NotImplementedException(); + } + + public override RazorProjectItem GetItem(string path) + { + if (!_lookup.TryGetValue(path, out var value)) + { + value = new NotFoundProjectItem("", path); + } + + return value; + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectItem.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectItem.cs new file mode 100644 index 0000000000..d4f03fbf0d --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectItem.cs @@ -0,0 +1,49 @@ +// 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; +using System.Text; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public class TestRazorProjectItem : RazorProjectItem + { + public TestRazorProjectItem( + string filePath, + string physicalPath = null, + string relativePhysicalPath = null, + string basePath = "/") + { + FilePath = filePath; + PhysicalPath = physicalPath; + RelativePhysicalPath = relativePhysicalPath; + BasePath = basePath; + } + + public override string BasePath { get; } + + public override string FilePath { get; } + + public override string PhysicalPath { get; } + + public override string RelativePhysicalPath { get; } + + public override bool Exists { get; } = true; + + public string Content { get; set; } = "Default content"; + + public override Stream Read() + { + // Act like a file and have a UTF8 BOM. + var preamble = Encoding.UTF8.GetPreamble(); + var contentBytes = Encoding.UTF8.GetBytes(Content); + var buffer = new byte[preamble.Length + contentBytes.Length]; + preamble.CopyTo(buffer, 0); + contentBytes.CopyTo(buffer, preamble.Length); + + var stream = new MemoryStream(buffer); + + return stream; + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorSourceDocument.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorSourceDocument.cs new file mode 100644 index 0000000000..e731b03594 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorSourceDocument.cs @@ -0,0 +1,113 @@ +// 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; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public static class TestRazorSourceDocument + { + public static RazorSourceDocument CreateResource(string resourcePath, Type type, Encoding encoding = null, bool normalizeNewLines = false) + { + return CreateResource(resourcePath, type.GetTypeInfo().Assembly, encoding, normalizeNewLines); + } + + public static RazorSourceDocument CreateResource(string resourcePath, Assembly assembly, Encoding encoding = null, bool normalizeNewLines = false) + { + var file = TestFile.Create(resourcePath, assembly); + + using (var input = file.OpenRead()) + using (var reader = new StreamReader(input)) + { + var content = reader.ReadToEnd(); + if (normalizeNewLines) + { + content = NormalizeNewLines(content); + } + + var properties = new RazorSourceDocumentProperties(resourcePath, resourcePath); + return new StringSourceDocument(content, encoding ?? Encoding.UTF8, properties); + } + } + + public static RazorSourceDocument CreateResource( + string path, + Assembly assembly, + Encoding encoding, + RazorSourceDocumentProperties properties, + bool normalizeNewLines = false) + { + var file = TestFile.Create(path, assembly); + + using (var input = file.OpenRead()) + using (var reader = new StreamReader(input)) + { + var content = reader.ReadToEnd(); + if (normalizeNewLines) + { + content = NormalizeNewLines(content); + } + + return new StringSourceDocument(content, encoding ?? Encoding.UTF8, properties); + } + } + + public static MemoryStream CreateStreamContent(string content = "Hello, World!", Encoding encoding = null, bool normalizeNewLines = false) + { + var stream = new MemoryStream(); + encoding = encoding ?? Encoding.UTF8; + using (var writer = new StreamWriter(stream, encoding, bufferSize: 1024, leaveOpen: true)) + { + if (normalizeNewLines) + { + content = NormalizeNewLines(content); + } + + writer.Write(content); + } + + stream.Seek(0L, SeekOrigin.Begin); + + return stream; + } + + public static RazorSourceDocument Create( + string content = "Hello, world!", + Encoding encoding = null, + bool normalizeNewLines = false, + string filePath = "test.cshtml", + string relativePath = "test.cshtml") + { + if (normalizeNewLines) + { + content = NormalizeNewLines(content); + } + + var properties = new RazorSourceDocumentProperties(filePath, relativePath); + return new StringSourceDocument(content, encoding ?? Encoding.UTF8, properties); + } + + public static RazorSourceDocument Create( + string content, + RazorSourceDocumentProperties properties, + Encoding encoding = null, + bool normalizeNewLines = false) + { + if (normalizeNewLines) + { + content = NormalizeNewLines(content); + } + + return new StringSourceDocument(content, encoding ?? Encoding.UTF8, properties); + } + + private static string NormalizeNewLines(string content) + { + return Regex.Replace(content, "(?<!\r)\n", "\r\n", RegexOptions.None, TimeSpan.FromSeconds(10)); + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs new file mode 100644 index 0000000000..da638e1750 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs @@ -0,0 +1,69 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public static class TestRequiredAttributeDescriptorBuilderExtensions + { + public static RequiredAttributeDescriptorBuilder Name(this RequiredAttributeDescriptorBuilder builder, string name) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Name = name; + + return builder; + } + + public static RequiredAttributeDescriptorBuilder NameComparisonMode( + this RequiredAttributeDescriptorBuilder builder, + RequiredAttributeDescriptor.NameComparisonMode nameComparison) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.NameComparisonMode = nameComparison; + + return builder; + } + + public static RequiredAttributeDescriptorBuilder Value(this RequiredAttributeDescriptorBuilder builder, string value) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Value = value; + + return builder; + } + + public static RequiredAttributeDescriptorBuilder ValueComparisonMode( + this RequiredAttributeDescriptorBuilder builder, + RequiredAttributeDescriptor.ValueComparisonMode valueComparison) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.ValueComparisonMode = valueComparison; + + return builder; + } + + public static RequiredAttributeDescriptorBuilder AddDiagnostic(this RequiredAttributeDescriptorBuilder builder, RazorDiagnostic diagnostic) + { + builder.Diagnostics.Add(diagnostic); + + return builder; + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperDescriptorBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperDescriptorBuilderExtensions.cs new file mode 100644 index 0000000000..5bc81a7792 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperDescriptorBuilderExtensions.cs @@ -0,0 +1,122 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public static class TestTagHelperDescriptorBuilderExtensions + { + public static TagHelperDescriptorBuilder TypeName(this TagHelperDescriptorBuilder builder, string typeName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.SetTypeName(typeName); + + return builder; + } + + public static TagHelperDescriptorBuilder DisplayName(this TagHelperDescriptorBuilder builder, string displayName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.DisplayName = displayName; + + return builder; + } + + public static TagHelperDescriptorBuilder AllowChildTag(this TagHelperDescriptorBuilder builder, string allowedChild) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AllowChildTag(childTagBuilder => childTagBuilder.Name = allowedChild); + + return builder; + } + + public static TagHelperDescriptorBuilder TagOutputHint(this TagHelperDescriptorBuilder builder, string hint) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.TagOutputHint = hint; + + return builder; + } + + public static TagHelperDescriptorBuilder Documentation(this TagHelperDescriptorBuilder builder, string documentation) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Documentation = documentation; + + return builder; + } + + public static TagHelperDescriptorBuilder AddMetadata(this TagHelperDescriptorBuilder builder, string key, string value) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Metadata[key] = value; + + return builder; + } + + public static TagHelperDescriptorBuilder AddDiagnostic(this TagHelperDescriptorBuilder builder, RazorDiagnostic diagnostic) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Diagnostics.Add(diagnostic); + + return builder; + } + + public static TagHelperDescriptorBuilder BoundAttributeDescriptor( + this TagHelperDescriptorBuilder builder, + Action<BoundAttributeDescriptorBuilder> configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.BindAttribute(configure); + + return builder; + } + + public static TagHelperDescriptorBuilder TagMatchingRuleDescriptor( + this TagHelperDescriptorBuilder builder, + Action<TagMatchingRuleDescriptorBuilder> configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.TagMatchingRule(configure); + + return builder; + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperFeature.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperFeature.cs new file mode 100644 index 0000000000..ad4039007e --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperFeature.cs @@ -0,0 +1,27 @@ +// 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 +{ + public class TestTagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature + { + public TestTagHelperFeature() + { + TagHelpers = new List<TagHelperDescriptor>(); + } + + public TestTagHelperFeature(IEnumerable<TagHelperDescriptor> tagHelpers) + { + TagHelpers = new List<TagHelperDescriptor>(tagHelpers); + } + + public List<TagHelperDescriptor> TagHelpers { get; } + + public IReadOnlyList<TagHelperDescriptor> GetDescriptors() + { + return TagHelpers.ToArray(); + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs new file mode 100644 index 0000000000..f052818fe6 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs @@ -0,0 +1,72 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public static class TestTagMatchingRuleDescriptorBuilderExtensions + { + public static TagMatchingRuleDescriptorBuilder RequireTagName(this TagMatchingRuleDescriptorBuilder builder, string tagName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.TagName = tagName; + + return builder; + } + + public static TagMatchingRuleDescriptorBuilder RequireParentTag(this TagMatchingRuleDescriptorBuilder builder, string parentTag) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.ParentTag = parentTag; + + return builder; + } + + public static TagMatchingRuleDescriptorBuilder RequireTagStructure(this TagMatchingRuleDescriptorBuilder builder, TagStructure tagStructure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.TagStructure = tagStructure; + + return builder; + } + + public static TagMatchingRuleDescriptorBuilder AddDiagnostic(this TagMatchingRuleDescriptorBuilder builder, RazorDiagnostic diagnostic) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Diagnostics.Add(diagnostic); + + return builder; + } + + public static TagMatchingRuleDescriptorBuilder RequireAttributeDescriptor( + this TagMatchingRuleDescriptorBuilder builder, + Action<RequiredAttributeDescriptorBuilder> configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Attribute(configure); + + return builder; + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Microsoft.AspNetCore.Razor.Test.Common.csproj b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Microsoft.AspNetCore.Razor.Test.Common.csproj new file mode 100644 index 0000000000..cf35d8f8d7 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Microsoft.AspNetCore.Razor.Test.Common.csproj @@ -0,0 +1,21 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <!-- To generate baselines, run tests with /p:GenerateBaselines=true --> + <DefineConstants Condition="'$(GenerateBaselines)'=='true'">$(DefineConstants);GENERATE_BASELINES</DefineConstants> + <DefineConstants>$(DefineConstants);__RemoveThisBitTo__GENERATE_BASELINES</DefineConstants> + <TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net46</TargetFrameworks> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Razor.Language\Microsoft.AspNetCore.Razor.Language.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisCSharpPackageVersion)" /> + <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftExtensionsDependencyModelPackageVersion)" /> + <PackageReference Include="xunit" Version="$(XunitPackageVersion)" /> + </ItemGroup> + +</Project> diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Properties/AssemblyInfo.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..576c4e4e44 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Properties/AssemblyInfo.cs @@ -0,0 +1,12 @@ +// 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.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Language.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Workspaces.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LanguageServices.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/TestCompilation.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/TestCompilation.cs new file mode 100644 index 0000000000..ffa7fd66d4 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/TestCompilation.cs @@ -0,0 +1,71 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.DependencyModel; +using Xunit; + +namespace Microsoft.CodeAnalysis +{ + public static class TestCompilation + { + private static readonly ConcurrentDictionary<Assembly, IEnumerable<MetadataReference>> _referenceCache = + new ConcurrentDictionary<Assembly, IEnumerable<MetadataReference>>(); + + public static IEnumerable<MetadataReference> GetMetadataReferences(Assembly assembly) + { + var dependencyContext = DependencyContext.Load(assembly); + + var metadataReferences = dependencyContext.CompileLibraries + .SelectMany(l => l.ResolveReferencePaths()) + .Select(assemblyPath => MetadataReference.CreateFromFile(assemblyPath)) + .ToArray(); + + return metadataReferences; + } + + public static string AssemblyName => "TestAssembly"; + + public static CSharpCompilation Create(Assembly assembly, SyntaxTree syntaxTree = null) + { + IEnumerable<SyntaxTree> syntaxTrees = null; + + if (syntaxTree != null) + { + syntaxTrees = new[] { syntaxTree }; + } + + if (!_referenceCache.TryGetValue(assembly, out IEnumerable<MetadataReference> metadataReferences)) + { + metadataReferences = GetMetadataReferences(assembly); + _referenceCache.TryAdd(assembly, metadataReferences); + } + + var compilation = CSharpCompilation.Create(AssemblyName, syntaxTrees, metadataReferences, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + EnsureValidCompilation(compilation); + + return compilation; + } + + private static void EnsureValidCompilation(CSharpCompilation compilation) + { + using (var stream = new MemoryStream()) + { + var emitResult = compilation + .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .Emit(stream); + var diagnostics = string.Join( + Environment.NewLine, + emitResult.Diagnostics.Select(d => CSharpDiagnosticFormatter.Instance.Format(d))); + Assert.True(emitResult.Success, $"Compilation is invalid : {Environment.NewLine}{diagnostics}"); + } + } + } +} |