File: Language\Legacy\ParserTestBase.cs
Web Access
Project: src\src\Razor\src\Shared\Microsoft.AspNetCore.Razor.Test.Common\Microsoft.AspNetCore.Razor.Test.Common.csproj (Microsoft.AspNetCore.Razor.Test.Common)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Sdk;
 
namespace Microsoft.AspNetCore.Razor.Language.Legacy;
 
[InitializeTestFile]
public abstract class ParserTestBase : IParserTest
{
    private static readonly AsyncLocal<string> _fileName = new AsyncLocal<string>();
    private static readonly AsyncLocal<bool> _isTheory = new AsyncLocal<bool>();
 
    // UTF-8 with BOM
    private static readonly Encoding _baselineEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
    private readonly bool _validateSpanEditHandlers;
    private readonly bool _useLegacyTokenizer;
 
    internal ParserTestBase(TestProject.Layer layer, bool validateSpanEditHandlers = false, bool useLegacyTokenizer = false)
    {
        TestProjectRoot = TestProject.GetProjectDirectory(GetType(), layer);
        _validateSpanEditHandlers = validateSpanEditHandlers;
        _useLegacyTokenizer = useLegacyTokenizer;
    }
 
    /// <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; }
 
    protected string TestProjectRoot { get; }
 
    // Used by the test framework to set the 'base' name for test files.
    public static string FileName
    {
        get { return _fileName.Value; }
        set { _fileName.Value = value; }
    }
 
    public static bool IsTheory
    {
        get { return _isTheory.Value; }
        set { _isTheory.Value = value; }
    }
 
    protected int BaselineTestCount { get; set; }
 
    internal virtual void AssertSyntaxTreeNodeMatchesBaseline(RazorSyntaxTree syntaxTree)
    {
        var root = syntaxTree.Root;
        var diagnostics = syntaxTree.Diagnostics;
        var filePath = syntaxTree.Source.FilePath;
        if (FileName == null)
        {
            var message = $"{nameof(AssertSyntaxTreeNodeMatchesBaseline)} should only be called from a parser test ({nameof(FileName)} is null).";
            throw new InvalidOperationException(message);
        }
 
        if (IsTheory)
        {
            var message = $"{nameof(AssertSyntaxTreeNodeMatchesBaseline)} should not be called from a [Theory] test.";
            throw new InvalidOperationException(message);
        }
 
        var fileName = BaselineTestCount > 0 ? FileName + $"_{BaselineTestCount}" : FileName;
        var baselineFileName = Path.ChangeExtension(fileName, ".stree.txt");
        var baselineDiagnosticsFileName = Path.ChangeExtension(fileName, ".diag.txt");
        var baselineClassifiedSpansFileName = Path.ChangeExtension(fileName, ".cspans.txt");
        var baselineTagHelperSpansFileName = Path.ChangeExtension(fileName, ".tspans.txt");
        BaselineTestCount++;
 
        if (GenerateBaselines.ShouldGenerate)
        {
            // Write syntax tree baseline
            var baselineFullPath = Path.Combine(TestProjectRoot, baselineFileName);
            Directory.CreateDirectory(Path.GetDirectoryName(baselineFullPath));
            File.WriteAllText(baselineFullPath, TestSyntaxSerializer.Serialize(root, _validateSpanEditHandlers), _baselineEncoding);
 
            // Write diagnostics baseline
            var baselineDiagnosticsFullPath = Path.Combine(TestProjectRoot, baselineDiagnosticsFileName);
            var lines = diagnostics.Select(SerializeDiagnostic).ToArray();
            if (lines.Any())
            {
                File.WriteAllLines(baselineDiagnosticsFullPath, lines, _baselineEncoding);
            }
            else if (File.Exists(baselineDiagnosticsFullPath))
            {
                File.Delete(baselineDiagnosticsFullPath);
            }
 
            // Write classified spans baseline
            var classifiedSpansBaselineFullPath = Path.Combine(TestProjectRoot, baselineClassifiedSpansFileName);
            File.WriteAllText(classifiedSpansBaselineFullPath, ClassifiedSpanSerializer.Serialize(syntaxTree, _validateSpanEditHandlers), _baselineEncoding);
 
            // Write tag helper spans baseline
            var tagHelperSpansBaselineFullPath = Path.Combine(TestProjectRoot, baselineTagHelperSpansFileName);
            var serializedTagHelperSpans = TagHelperSpanSerializer.Serialize(syntaxTree);
            if (!string.IsNullOrEmpty(serializedTagHelperSpans))
            {
                File.WriteAllText(tagHelperSpansBaselineFullPath, serializedTagHelperSpans, _baselineEncoding);
            }
            else if (File.Exists(tagHelperSpansBaselineFullPath))
            {
                File.Delete(tagHelperSpansBaselineFullPath);
            }
 
            return;
        }
 
        // Verify syntax tree
        var stFile = TestFile.Create(baselineFileName, GetType().GetTypeInfo().Assembly);
        if (!stFile.Exists())
        {
            throw new XunitException($"The resource {baselineFileName} was not found.");
        }
 
        var syntaxNodeBaseline = stFile.ReadAllText();
        var actualSyntaxNodes = TestSyntaxSerializer.Serialize(root, _validateSpanEditHandlers);
        AssertEx.AssertEqualToleratingWhitespaceDifferences(syntaxNodeBaseline, actualSyntaxNodes);
 
        // Verify diagnostics
        var baselineDiagnostics = string.Empty;
        var diagnosticsFile = TestFile.Create(baselineDiagnosticsFileName, GetType().GetTypeInfo().Assembly);
        if (diagnosticsFile.Exists())
        {
            baselineDiagnostics = diagnosticsFile.ReadAllText();
        }
 
        var actualDiagnostics = string.Concat(diagnostics.Select(d => SerializeDiagnostic(d) + "\r\n"));
        Assert.Equal(baselineDiagnostics, actualDiagnostics);
 
        // Verify classified spans
        var classifiedSpanFile = TestFile.Create(baselineClassifiedSpansFileName, GetType().GetTypeInfo().Assembly);
        if (!classifiedSpanFile.Exists())
        {
            throw new XunitException($"The resource {baselineClassifiedSpansFileName} was not found.");
        }
        else
        {
            var classifiedSpanBaseline = classifiedSpanFile.ReadAllText();
            var actualClassifiedSpans = ClassifiedSpanSerializer.Serialize(syntaxTree, _validateSpanEditHandlers);
            AssertEx.AssertEqualToleratingWhitespaceDifferences(classifiedSpanBaseline, actualClassifiedSpans);
        }
 
        // Verify tag helper spans
        var tagHelperSpanFile = TestFile.Create(baselineTagHelperSpansFileName, GetType().GetTypeInfo().Assembly);
        if (tagHelperSpanFile.Exists())
        {
            var tagHelperSpanBaseline = tagHelperSpanFile.ReadAllText();
            var actualTagHelperSpans = TagHelperSpanSerializer.Serialize(syntaxTree);
            AssertEx.AssertEqualToleratingWhitespaceDifferences(tagHelperSpanBaseline, actualTagHelperSpans);
        }
    }
 
    protected static string SerializeDiagnostic(RazorDiagnostic diagnostic)
    {
        var content = RazorDiagnosticSerializer.Serialize(diagnostic);
        var normalized = NormalizeNewLines(content);
 
        return normalized;
    }
 
    private static string NormalizeNewLines(string content)
    {
        return Regex.Replace(content, "(?<!\r)\n", "\r\n", RegexOptions.None, TimeSpan.FromSeconds(10));
    }
 
    internal virtual void BaselineTest(RazorSyntaxTree syntaxTree, bool verifySyntaxTree = true, bool ensureFullFidelity = true)
    {
        if (verifySyntaxTree)
        {
            SyntaxTreeVerifier.Verify(syntaxTree, ensureFullFidelity);
        }
 
        AssertSyntaxTreeNodeMatchesBaseline(syntaxTree);
    }
 
    internal RazorSyntaxTree ParseDocument(
        string document,
        bool designTime = false,
        ImmutableArray<DirectiveDescriptor> directives = default,
        RazorFileKind? fileKind = null,
        CSharpParseOptions csharpParseOptions = null,
        Action<RazorParserOptions.Builder> configureParserOptions = null)
    {
        return ParseDocument(RazorLanguageVersion.Latest, document, directives, designTime, fileKind, csharpParseOptions, configureParserOptions);
    }
 
    internal virtual RazorSyntaxTree ParseDocument(
        RazorLanguageVersion version,
        string document,
        ImmutableArray<DirectiveDescriptor> directives,
        bool designTime = false,
        RazorFileKind? fileKind = null,
        CSharpParseOptions csharpParseOptions = null,
        Action<RazorParserOptions.Builder> configureParserOptions = null)
    {
        directives = directives.NullToEmpty();
 
        var source = TestRazorSourceDocument.Create(document, filePath: null, relativePath: null, normalizeNewLines: true);
        var parseOptions = CreateParserOptions(version, fileKind, designTime, directives, csharpParseOptions, configureParserOptions);
        var codeDocument = RazorCodeDocument.Create(source, parseOptions);
 
        using var context = new ParserContext(source, parseOptions);
        using var codeParser = new CSharpCodeParser(directives, context);
        using var markupParser = new HtmlMarkupParser(context);
 
        codeParser.HtmlParser = markupParser;
        markupParser.CodeParser = codeParser;
 
        var root = markupParser.ParseDocument().CreateRed();
 
        var diagnostics = context.ErrorSink.GetErrorsAndClear();
 
        var syntaxTree = new RazorSyntaxTree(root, source, diagnostics, parseOptions);
        codeDocument = codeDocument.WithSyntaxTree(syntaxTree);
 
        var defaultDirectivePass = new DefaultDirectiveSyntaxTreePass();
        syntaxTree = defaultDirectivePass.Execute(codeDocument, syntaxTree);
 
        return syntaxTree;
    }
 
    internal virtual void ParseDocumentTest(string document)
    {
        ParseDocumentTest(document, directives: default, designTime: false);
    }
 
    internal virtual void ParseDocumentTest(string document, RazorFileKind fileKind)
    {
        ParseDocumentTest(document, directives: default, designTime: false, fileKind);
    }
 
    internal virtual void ParseDocumentTest(string document, ImmutableArray<DirectiveDescriptor> directives)
    {
        ParseDocumentTest(document, directives, designTime: false);
    }
 
    internal virtual void ParseDocumentTest(string document, bool designTime)
    {
        ParseDocumentTest(document, directives: default, designTime);
    }
 
    internal void ParseDocumentTest(string document, CSharpParseOptions options)
    {
        ParseDocumentTest(document, directives: default, designTime: false, csharpParseOptions: options);
    }
 
    internal virtual void ParseDocumentTest(
        string document,
        ImmutableArray<DirectiveDescriptor> directives,
        bool designTime,
        RazorFileKind? fileKind = null,
        CSharpParseOptions csharpParseOptions = null)
    {
        ParseDocumentTest(RazorLanguageVersion.Latest, document, directives, designTime, fileKind, csharpParseOptions);
    }
 
    internal virtual void ParseDocumentTest(
        RazorLanguageVersion version,
        string document,
        ImmutableArray<DirectiveDescriptor> directives,
        bool designTime,
        RazorFileKind? fileKind = null,
        CSharpParseOptions csharpParseOptions = null)
    {
        var result = ParseDocument(version, document, directives, designTime, fileKind: fileKind, csharpParseOptions: csharpParseOptions);
 
        BaselineTest(result);
    }
 
    private RazorParserOptions CreateParserOptions(
        RazorLanguageVersion version,
        RazorFileKind? fileKind,
        bool designTime,
        ImmutableArray<DirectiveDescriptor> directives,
        CSharpParseOptions csharpParseOptions,
        Action<RazorParserOptions.Builder> configureParserOptions = null)
    {
        var builder = new RazorParserOptions.Builder(version, fileKind ?? RazorFileKind.Legacy)
        {
            DesignTime = designTime,
            Directives = directives,
            EnableSpanEditHandlers = _validateSpanEditHandlers,
            UseRoslynTokenizer = !_useLegacyTokenizer
        };
 
        if (csharpParseOptions is not null)
        {
            builder.CSharpParseOptions = csharpParseOptions;
        }
 
        configureParserOptions?.Invoke(builder);
 
        return builder.ToOptions();
    }
}