File: Language\IntegrationTests\RazorBaselineIntegrationTestBase.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.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Sdk;
 
namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests;
 
public abstract class RazorBaselineIntegrationTestBase : RazorIntegrationTestBase
{
    // UTF-8 with BOM
    private static readonly Encoding _baselineEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
 
    protected RazorBaselineIntegrationTestBase(TestProject.Layer layer)
    {
        TestProjectRoot = TestProject.GetProjectDirectory(GetType(), layer);
    }
 
    protected string TestProjectRoot { get; }
 
    // For consistent line endings because the character counts are going to be recorded in files.
    internal override string LineEnding => "\r\n";
 
    internal override bool NormalizeSourceLineEndings => true;
 
    internal override string PathSeparator => "\\";
 
    // Force consistent paths since they are going to be recorded in files.
    internal override string WorkingDirectory => ArbitraryWindowsPath;
 
    protected abstract string GetDirectoryPath(string testName);
 
    protected void AssertDocumentNodeMatchesBaseline(RazorCodeDocument codeDocument, [CallerMemberName]string testName = "")
    {
        var document = codeDocument.GetRequiredDocumentNode();
        var baselineFilePath = GetBaselineFilePath(codeDocument, ".ir.txt", testName);
 
        if (GenerateBaselines.ShouldGenerate)
        {
            var baselineFullPath = Path.Combine(TestProjectRoot, baselineFilePath);
            Directory.CreateDirectory(Path.GetDirectoryName(baselineFullPath));
            WriteBaseline(IntermediateNodeSerializer.Serialize(document), baselineFullPath);
 
            return;
        }
 
        var irFile = TestFile.Create(baselineFilePath, GetType().Assembly);
        if (!irFile.Exists())
        {
            throw new XunitException($"The resource {baselineFilePath} was not found.");
        }
 
        // Normalize newlines by splitting into an array.
        var irText = irFile.ReadAllText();
        var baseline = irText.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
        AssertEx.AssertEqualToleratingWhitespaceDifferences(irText, IntermediateNodeSerializer.Serialize(document));
        IntermediateNodeVerifier.Verify(document, baseline);
    }
 
    protected void AssertCSharpDocumentMatchesBaseline(RazorCodeDocument codeDocument, bool verifyLinePragmas = true, [CallerMemberName] string testName = "")
    {
        var document = codeDocument.GetRequiredCSharpDocument();
 
        // Normalize newlines to match those in the baseline.
        var actualCode = document.Text.ToString().Replace("\r", "").Replace("\n", "\r\n");
 
        var baselineFilePath = GetBaselineFilePath(codeDocument, ".codegen.cs", testName);
        var baselineDiagnosticsFilePath = GetBaselineFilePath(codeDocument, ".diagnostics.txt", testName);
        var baselineMappingsFilePath = GetBaselineFilePath(codeDocument, ".mappings.txt", testName);
 
        var serializedMappings = SourceMappingsSerializer.Serialize(document, codeDocument.Source);
 
        if (GenerateBaselines.ShouldGenerate)
        {
            var baselineFullPath = Path.Combine(TestProjectRoot, baselineFilePath);
            Directory.CreateDirectory(Path.GetDirectoryName(baselineFullPath));
            WriteBaseline(actualCode, baselineFullPath);
 
            var baselineDiagnosticsFullPath = Path.Combine(TestProjectRoot, baselineDiagnosticsFilePath);
            var lines = document.Diagnostics.Select(RazorDiagnosticSerializer.Serialize).ToArray();
            if (lines.Any())
            {
                WriteBaseline(lines, baselineDiagnosticsFullPath);
            }
            else if (File.Exists(baselineDiagnosticsFullPath))
            {
                File.Delete(baselineDiagnosticsFullPath);
            }
 
            var baselineMappingsFullPath = Path.Combine(TestProjectRoot, baselineMappingsFilePath);
            var text = SourceMappingsSerializer.Serialize(document, codeDocument.Source);
            if (!string.IsNullOrEmpty(text))
            {
                WriteBaseline(text, baselineMappingsFullPath);
            }
            else if (File.Exists(baselineMappingsFullPath))
            {
                File.Delete(baselineMappingsFullPath);
            }
 
            return;
        }
 
        var codegenFile = TestFile.Create(baselineFilePath, GetType().Assembly);
        if (!codegenFile.Exists())
        {
            throw new XunitException($"The resource {baselineFilePath} was not found.");
        }
 
        var baseline = codegenFile.ReadAllText();
        Assert.Equal(baseline, actualCode);
 
        var baselineDiagnostics = string.Empty;
        var diagnosticsFile = TestFile.Create(baselineDiagnosticsFilePath, GetType().Assembly);
        if (diagnosticsFile.Exists())
        {
            baselineDiagnostics = diagnosticsFile.ReadAllText();
        }
 
        var actualDiagnostics = string.Concat(document.Diagnostics.Select(d => RazorDiagnosticSerializer.Serialize(d) + "\r\n"));
        Assert.Equal(baselineDiagnostics, actualDiagnostics);
 
        var baselineMappings = string.Empty;
        var mappingsFile = TestFile.Create(baselineMappingsFilePath, GetType().Assembly);
        if (mappingsFile.Exists())
        {
            baselineMappings = mappingsFile.ReadAllText();
        }
 
        var actualMappings = SourceMappingsSerializer.Serialize(document, codeDocument.Source);
        actualMappings = actualMappings.Replace("\r", "").Replace("\n", "\r\n");
        Assert.Equal(baselineMappings, actualMappings);
 
        if (verifyLinePragmas)
        {
            AssertLinePragmas(codeDocument);
        }
    }
 
    protected void AssertLinePragmas(RazorCodeDocument codeDocument)
    {
        var csharpDocument = codeDocument.GetCSharpDocument();
        Assert.NotNull(csharpDocument);
        var linePragmas = csharpDocument.LinePragmas;
        if (DesignTime)
        {
            var sourceMappings = csharpDocument.SourceMappingsSortedByOriginal;
            foreach (var sourceMapping in sourceMappings)
            {
                var content = codeDocument.Source.Text.GetSubText(new TextSpan(sourceMapping.OriginalSpan.AbsoluteIndex, sourceMapping.OriginalSpan.Length)).ToString();
                if (string.IsNullOrWhiteSpace(content))
                {
                    continue;
                }
 
                var foundMatchingPragma = false;
                foreach (var linePragma in linePragmas)
                {
                    if (sourceMapping.OriginalSpan.LineIndex >= linePragma.StartLineIndex &&
                        sourceMapping.OriginalSpan.LineIndex <= linePragma.EndLineIndex)
                    {
                        // Found a match.
                        foundMatchingPragma = true;
                        break;
                    }
                }
 
                Assert.True(foundMatchingPragma, $"No line pragma found for code at line {sourceMapping.OriginalSpan.LineIndex + 1}.");
            }
        }
        else
        {
            var syntaxTree = codeDocument.GetTagHelperRewrittenSyntaxTree() ?? codeDocument.GetRequiredSyntaxTree();
            var sourceContent = syntaxTree.Source.Text.ToString();
            var classifiedSpans = syntaxTree.GetClassifiedSpans();
            foreach (var classifiedSpan in classifiedSpans)
            {
                var content = sourceContent.Substring(classifiedSpan.Span.AbsoluteIndex, classifiedSpan.Span.Length);
                if (!string.IsNullOrWhiteSpace(content) &&
                    classifiedSpan.BlockKind != BlockKindInternal.Directive &&
                    classifiedSpan.SpanKind == SpanKindInternal.Code)
                {
                    var foundMatchingPragma = false;
                    foreach (var linePragma in linePragmas)
                    {
                        if (classifiedSpan.Span.LineIndex >= linePragma.StartLineIndex &&
                            classifiedSpan.Span.LineIndex <= linePragma.EndLineIndex)
                        {
                            // Found a match.
                            foundMatchingPragma = true;
                            break;
                        }
                    }
 
                    Assert.True(foundMatchingPragma, $"No line pragma found for code '{content}' at line {classifiedSpan.Span.LineIndex + 1}.");
                }
            }
 
            // check that the pragmas in the main document are enhanced
            Assert.All(linePragmas.Where(p => p.FilePath == codeDocument.Source.FilePath), p => Assert.True(p.IsEnhanced));
        }
    }
 
    protected void AssertSequencePointsMatchBaseline(CompileToAssemblyResult result, RazorCodeDocument codeDocument, [CallerMemberName] string testName = "")
    {
        using var peReader = new PEReader(result.ExecutableStream);
        var metadataReader = peReader.GetMetadataReader();
 
        var debugDirectory = peReader.ReadDebugDirectory().First(d => d.Type == DebugDirectoryEntryType.EmbeddedPortablePdb);
        var debugReader = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(debugDirectory).GetMetadataReader();
 
        var builder = new StringBuilder();
        foreach (var methodHandle in debugReader.MethodDebugInformation)
        {
            var methodDebugInfo = debugReader.GetMethodDebugInformation(methodHandle);
            var sequencePoints = methodDebugInfo.GetSequencePoints();
            if (!sequencePoints.Any())
                continue;
 
            var methodDefinition = metadataReader.GetMethodDefinition(methodHandle.ToDefinitionHandle());
            builder.AppendLine($"{metadataReader.GetString(methodDefinition.Name)}: ");
 
            foreach (var sequencePoint in sequencePoints)
            {
                if (!sequencePoint.IsHidden)
                {
                    var documentName = debugReader.GetString(debugReader.GetDocument(sequencePoint.Document).Name);
                    builder.AppendLine($"\tIL_{sequencePoint.Offset:x4}: ({sequencePoint.StartLine},{sequencePoint.StartColumn})-({sequencePoint.EndLine},{sequencePoint.EndColumn}) \"{documentName}\"");
                }
            }
        }
 
        var actualSequencePoints = builder.ToString().ReplaceLineEndings();
 
        var baselineFilePath = GetBaselineFilePath(codeDocument, ".sp.txt", testName);
        if (GenerateBaselines.ShouldGenerate)
        {
            var baselineFullPath = Path.Combine(TestProjectRoot, baselineFilePath);
            Directory.CreateDirectory(Path.GetDirectoryName(baselineFullPath));
            WriteBaseline(actualSequencePoints, baselineFullPath);
        }
        else
        {
            var baselineSequencePoints = string.Empty;
            var spFile = TestFile.Create(baselineFilePath, GetType().Assembly);
            if (spFile.Exists())
            {
                baselineSequencePoints = spFile.ReadAllText().ReplaceLineEndings();
            }
 
            AssertEx.Equal(baselineSequencePoints, actualSequencePoints);
        }
    }
 
    private string GetBaselineFilePath(RazorCodeDocument codeDocument, string extension, string testName)
    {
        if (codeDocument == null)
        {
            throw new ArgumentNullException(nameof(codeDocument));
        }
 
        if (extension == null)
        {
            throw new ArgumentNullException(nameof(extension));
        }
 
        var lastSlash = codeDocument.Source.FilePath.LastIndexOfAny(['/', '\\']);
        var fileName = lastSlash == -1 ? null : codeDocument.Source.FilePath[(lastSlash + 1)..];
        if (string.IsNullOrEmpty(fileName))
        {
            var message = "Integration tests require a filename";
            throw new InvalidOperationException(message);
        }
 
        return Path.Combine(GetDirectoryPath(testName), Path.ChangeExtension(fileName, extension));
    }
 
    private static void WriteBaseline(string text, string filePath)
    {
        File.WriteAllText(filePath, text, _baselineEncoding);
    }
 
    private static void WriteBaseline(string[] lines, string filePath)
    {
        using (var writer = new StreamWriter(File.Open(filePath, FileMode.Create), _baselineEncoding))
        {
            // Force windows-style line endings so that we're consistent. This isn't
            // required for correctness, but will prevent churn when developing on OSX.
            writer.NewLine = "\r\n";
 
            for (var i = 0; i < lines.Length; i++)
            {
                writer.WriteLine(lines[i]);
            }
        }
    }
}