File: RuntimeViewCompilerTest.cs
Web Access
Project: src\src\Mvc\Mvc.Razor.RuntimeCompilation\test\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj (Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Hosting;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using static Microsoft.AspNetCore.Razor.Hosting.TestRazorCompiledItem;
 
namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
 
public class RuntimeViewCompilerTest
{
    [Fact]
    public async Task CompileAsync_ReturnsResultWithNullAttribute_IfFileIsNotFoundInFileSystem()
    {
        // Arrange
        var path = "/file/does-not-exist";
        var fileProvider = new TestFileProvider();
        var viewCompiler = GetViewCompiler(fileProvider);
 
        // Act
        var result1 = await viewCompiler.CompileAsync(path);
        var result2 = await viewCompiler.CompileAsync(path);
 
        // Assert
        Assert.Same(result1, result2);
        Assert.Null(result1.Item);
        var token = Assert.Single(result1.ExpirationTokens);
        Assert.Same(fileProvider.GetChangeToken(path), token);
    }
 
    [Fact]
    public async Task CompileAsync_ReturnsResultWithExpirationToken()
    {
        // Arrange
        var path = "/file/does-not-exist";
        var fileProvider = new TestFileProvider();
        var viewCompiler = GetViewCompiler(fileProvider);
 
        // Act
        var result1 = await viewCompiler.CompileAsync(path);
        var result2 = await viewCompiler.CompileAsync(path);
 
        // Assert
        Assert.Same(result1, result2);
        Assert.Null(result1.Item);
        Assert.Collection(
            result1.ExpirationTokens,
            token => Assert.Equal(fileProvider.GetChangeToken(path), token));
    }
 
    [Fact]
    public async Task CompileAsync_AddsChangeTokensForViewStartsIfFileExists()
    {
        // Arrange
        var path = "/file/exists/FilePath.cshtml";
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
        var viewCompiler = GetViewCompiler(fileProvider);
 
        // Act
        var result = await viewCompiler.CompileAsync(path);
 
        // Assert
        Assert.NotNull(result.Item);
        Assert.Collection(
            result.ExpirationTokens,
            token => Assert.Same(fileProvider.GetChangeToken(path), token),
            token => Assert.Same(fileProvider.GetChangeToken("/_ViewImports.cshtml"), token),
            token => Assert.Same(fileProvider.GetChangeToken("/file/_ViewImports.cshtml"), token),
            token => Assert.Same(fileProvider.GetChangeToken("/file/exists/_ViewImports.cshtml"), token));
    }
 
    [Theory]
    [InlineData("/Areas/Finances/Views/Home/Index.cshtml")]
    [InlineData(@"Areas\Finances\Views\Home\Index.cshtml")]
    [InlineData(@"\Areas\Finances\Views\Home\Index.cshtml")]
    [InlineData(@"\Areas\Finances\Views/Home\Index.cshtml")]
    public async Task CompileAsync_NormalizesPathSeparatorForPaths(string relativePath)
    {
        // Arrange
        var viewPath = "/Areas/Finances/Views/Home/Index.cshtml";
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(viewPath, "some content");
        var viewCompiler = GetViewCompiler(fileProvider);
 
        // Act - 1
        var result1 = await viewCompiler.CompileAsync(@"Areas\Finances\Views\Home\Index.cshtml");
 
        // Act - 2
        viewCompiler.Compile = _ => throw new Exception("Can't call me");
        var result2 = await viewCompiler.CompileAsync(relativePath);
 
        // Assert - 2
        Assert.Same(result1, result2);
    }
 
    [Fact]
    public async Task CompileAsync_InvalidatesCache_IfChangeTokenExpires()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
        var fileProvider = new TestFileProvider();
        var fileNode = fileProvider.AddFile(path, "some content");
        var viewCompiler = GetViewCompiler(fileProvider);
 
        // Act 1
        var result1 = await viewCompiler.CompileAsync(path);
 
        // Assert 1
        Assert.NotNull(result1.Item);
 
        // Act 2
        // Simulate deleting the file
        fileProvider.GetChangeToken(path).HasChanged = true;
        fileProvider.DeleteFile(path);
 
        viewCompiler.Compile = _ => throw new Exception("Can't call me");
        var result2 = await viewCompiler.CompileAsync(path);
 
        // Assert 2
        Assert.NotSame(result1, result2);
        Assert.Null(result2.Item);
    }
 
    [Fact]
    public async Task CompileAsync_ReturnsNewResultIfFileWasModified()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
        var viewCompiler = GetViewCompiler(fileProvider);
        var expected2 = new CompiledViewDescriptor();
 
        // Act 1
        var result1 = await viewCompiler.CompileAsync(path);
 
        // Assert 1
        Assert.NotNull(result1.Item);
 
        // Act 2
        fileProvider.GetChangeToken(path).HasChanged = true;
        viewCompiler.Compile = _ => expected2;
        var result2 = await viewCompiler.CompileAsync(path);
 
        // Assert 2
        Assert.NotSame(result1, result2);
        Assert.Same(expected2, result2);
    }
 
    [Fact]
    public async Task CompileAsync_ReturnsNewResult_IfAncestorViewImportsWereModified()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
        var viewCompiler = GetViewCompiler(fileProvider);
        var expected2 = new CompiledViewDescriptor();
 
        // Act 1
        var result1 = await viewCompiler.CompileAsync(path);
 
        // Assert 1
        Assert.NotNull(result1.Item);
 
        // Act 2
        fileProvider.GetChangeToken("/Views/_ViewImports.cshtml").HasChanged = true;
        viewCompiler.Compile = _ => expected2;
        var result2 = await viewCompiler.CompileAsync(path);
 
        // Assert 2
        Assert.NotSame(result1, result2);
        Assert.Same(expected2, result2);
    }
 
    [Fact]
    public async Task CompileAsync_ReturnsPrecompiledViews()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
        };
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
 
        // Act
        var result = await viewCompiler.CompileAsync(path);
 
        // Assert
        Assert.Same(precompiledView, result);
 
        // This view doesn't have checksums so it can't be recompiled.
        Assert.Null(precompiledView.ExpirationTokens);
    }
 
    [Theory]
    [InlineData("/views/home/index.cshtml")]
    [InlineData("/VIEWS/HOME/INDEX.CSHTML")]
    [InlineData("/viEws/HoME/inDex.cshtml")]
    public async Task CompileAsync_PerformsCaseInsensitiveLookupsForPrecompiledViews(string lookupPath)
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
        };
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
 
        // Act
        var result = await viewCompiler.CompileAsync(lookupPath);
 
        // Assert
        Assert.Same(precompiledView, result);
    }
 
    [Fact]
    public async Task CompileAsync_PerformsCaseInsensitiveLookupsForPrecompiledViews_WithNonNormalizedPaths()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
        };
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
 
        // Act
        var result = await viewCompiler.CompileAsync("Views\\Home\\Index.cshtml");
 
        // Assert
        Assert.Same(precompiledView, result);
    }
 
    [Fact]
    public async Task CompileAsync_PrecompiledViewWithoutChecksumForMainSource_DoesNotSupportRecompilation()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
 
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
 
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
            Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[]
            {
                    new RazorSourceChecksumAttribute("sha1", GetChecksum("some content"), "/Views/Some-Other-View"),
            }),
        };
 
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
 
        // Act - 1
        var result = await viewCompiler.CompileAsync(path);
 
        // Assert - 1
        Assert.Same(precompiledView.Item, result.Item);
 
        // Act - 2
        fileProvider.Watch(path);
        fileProvider.GetChangeToken(path).HasChanged = true;
        result = await viewCompiler.CompileAsync(path);
 
        // Assert - 2
        Assert.Same(precompiledView.Item, result.Item);
 
        // This view doesn't have checksums so it can't be recompiled.
        Assert.Null(result.ExpirationTokens);
    }
 
    [Fact]
    public async Task CompileAsync_PrecompiledViewWithoutAnyChecksum_DoesNotSupportRecompilation()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
 
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
 
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
            Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[] { }),
        };
 
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
 
        // Act - 1
        var result = await viewCompiler.CompileAsync(path);
 
        // Assert - 1
        Assert.Same(precompiledView, result);
 
        // Act - 2
        fileProvider.Watch(path);
        fileProvider.GetChangeToken(path).HasChanged = true;
        result = await viewCompiler.CompileAsync(path);
 
        // Assert - 2
        Assert.Same(precompiledView, result);
 
        // This view doesn't have checksums so it can't be recompiled.
        Assert.Null(result.ExpirationTokens);
    }
 
    [Fact]
    public async Task CompileAsync_PrecompiledViewWithChecksum_UsesPrecompiledViewWhenChecksumIsMatch()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
 
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
 
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
            Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[]
            {
                    new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path),
            }),
        };
 
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
 
        // Act
        var result = await viewCompiler.CompileAsync(path);
 
        // Assert
        Assert.Same(precompiledView.Item, result.Item);
 
        // This view has checksums so it should also have tokens
        Assert.Collection(
             result.ExpirationTokens,
             token => Assert.Same(fileProvider.GetChangeToken(path), token));
    }
 
    [Fact]
    public async Task CompileAsync_PrecompiledViewWithChecksum_CanRejectWhenChecksumFails()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
 
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
 
        var expected = new CompiledViewDescriptor();
 
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
            Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[]
            {
                    new RazorSourceChecksumAttribute("SHA1", GetChecksum("some other content"), path),
            }),
        };
 
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
        viewCompiler.Compile = _ => expected;
 
        // Act
        var result = await viewCompiler.CompileAsync(path);
 
        // Assert
        Assert.Same(expected, result);
    }
 
    [Fact]
    public async Task CompileAsync_PrecompiledViewWithChecksum_AddsExpirationTokensForFilesInChecksumAttributes()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
 
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
 
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
            Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[]
            {
                    new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path),
            }),
        };
 
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
 
        // Act
        var result = await viewCompiler.CompileAsync(path);
 
        // Assert
        Assert.Same(precompiledView.Item, result.Item);
        var token = Assert.Single(result.ExpirationTokens);
        Assert.Same(fileProvider.GetChangeToken(path), token);
    }
 
    [Fact]
    public async Task CompileAsync_PrecompiledViewWithChecksum_CanRecompile()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
 
        var fileProvider = new TestFileProvider();
        var file = fileProvider.AddFile(path, "some content");
        var expected2 = new CompiledViewDescriptor();
 
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
            Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[]
            {
                    new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path),
            }),
        };
 
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
 
        // Act - 1
        var result = await viewCompiler.CompileAsync(path);
 
        // Assert - 1
        Assert.Same(precompiledView.Item, result.Item);
        Assert.NotEmpty(result.ExpirationTokens);
 
        // Act - 2
        file.Content = "different";
        fileProvider.GetChangeToken(path).HasChanged = true;
        viewCompiler.Compile = _ => expected2;
        result = await viewCompiler.CompileAsync(path);
 
        // Assert - 2
        Assert.Same(expected2, result);
    }
 
    [Fact]
    public async Task CompileAsync_PrecompiledViewWithChecksum_DoesNotRecompiledWithoutContentChange()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
 
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
 
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
            Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[]
            {
                    new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path),
            }),
        };
 
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
 
        // Act - 1
        var result = await viewCompiler.CompileAsync(path);
 
        // Assert - 1
        Assert.Same(precompiledView.Item, result.Item);
 
        // Act - 2
        fileProvider.GetChangeToken(path).HasChanged = true;
        result = await viewCompiler.CompileAsync(path);
 
        // Assert - 2
        Assert.Same(precompiledView.Item, result.Item);
    }
 
    [Fact]
    public async Task CompileAsync_PrecompiledViewWithChecksum_CanReusePrecompiledViewIfContentChangesToMatch()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
 
        var fileProvider = new TestFileProvider();
        var file = fileProvider.AddFile(path, "different content");
 
        var expected1 = new CompiledViewDescriptor();
 
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
            Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[]
            {
                    new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path),
            }),
        };
 
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
        viewCompiler.Compile = _ => expected1;
 
        // Act - 1
        var result = await viewCompiler.CompileAsync(path);
 
        // Assert - 1
        Assert.Same(expected1, result);
 
        // Act - 2
        file.Content = "some content";
        fileProvider.GetChangeToken(path).HasChanged = true;
        result = await viewCompiler.CompileAsync(path);
 
        // Assert - 2
        Assert.Same(precompiledView.Item, result.Item);
    }
 
    [Fact]
    public async Task CompileAsync_PrecompiledViewWithChecksum_CanRecompileWhenViewImportChanges()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
        var importPath = "/Views/_ViewImports.cshtml";
 
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
        var importFile = fileProvider.AddFile(importPath, "some import");
 
        var expected2 = new CompiledViewDescriptor();
 
        var precompiledView = new CompiledViewDescriptor
        {
            RelativePath = path,
            Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[]
            {
                    new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path),
                    new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), importPath),
            }),
        };
 
        var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
 
        // Act - 1
        var result = await viewCompiler.CompileAsync(path);
 
        // Assert - 1
        Assert.Same(precompiledView.Item, result.Item);
 
        // Act - 2
        importFile.Content = "different content";
        fileProvider.GetChangeToken(importPath).HasChanged = true;
        viewCompiler.Compile = _ => expected2;
        result = await viewCompiler.CompileAsync(path);
 
        // Assert - 2
        Assert.Same(expected2, result);
    }
 
    [Fact]
    public async Task GetOrAdd_AllowsConcurrentCompilationOfMultipleRazorPages()
    {
        // Arrange
        var path1 = "/Views/Home/Index.cshtml";
        var path2 = "/Views/Home/About.cshtml";
        var waitDuration = TimeSpan.FromSeconds(20);
 
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path1, "some content");
        fileProvider.AddFile(path2, "some content");
 
        var resetEvent1 = new AutoResetEvent(initialState: false);
        var resetEvent2 = new ManualResetEvent(initialState: false);
 
        var compilingOne = false;
        var compilingTwo = false;
 
        var result1 = new CompiledViewDescriptor();
        var result2 = new CompiledViewDescriptor();
 
        var compiler = GetViewCompiler(fileProvider);
 
        compiler.Compile = path =>
        {
            if (path == path1)
            {
                compilingOne = true;
 
                // Event 2
                Assert.True(resetEvent1.WaitOne(waitDuration));
 
                // Event 3
                Assert.True(resetEvent2.Set());
 
                // Event 6
                Assert.True(resetEvent1.WaitOne(waitDuration));
 
                Assert.True(compilingTwo);
 
                return result1;
            }
            else if (path == path2)
            {
                compilingTwo = true;
 
                // Event 4
                Assert.True(resetEvent2.WaitOne(waitDuration));
 
                // Event 5
                Assert.True(resetEvent1.Set());
 
                Assert.True(compilingOne);
 
                return result2;
            }
            else
            {
                throw new Exception();
            }
        };
 
        // Act
        var task1 = Task.Run(() => compiler.CompileAsync(path1));
        var task2 = Task.Run(() => compiler.CompileAsync(path2));
 
        // Event 1
        resetEvent1.Set();
 
        await Task.WhenAll(task1, task2);
 
        // Assert
        Assert.True(compilingOne);
        Assert.True(compilingTwo);
#pragma warning disable xUnit1031 // Do not use blocking task operations in test method
        Assert.Same(result1, task1.Result);
        Assert.Same(result2, task2.Result);
#pragma warning restore xUnit1031 // Do not use blocking task operations in test method
    }
 
    [Fact]
    public async Task CompileAsync_DoesNotCreateMultipleCompilationResults_ForConcurrentInvocations()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
        var waitDuration = TimeSpan.FromSeconds(20);
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
        var resetEvent1 = new ManualResetEvent(initialState: false);
        var resetEvent2 = new ManualResetEvent(initialState: false);
        var compiler = GetViewCompiler(fileProvider);
 
        compiler.Compile = _ =>
        {
            // Event 2
            resetEvent1.WaitOne(waitDuration);
 
            // Event 3
            resetEvent2.Set();
            return new CompiledViewDescriptor();
        };
 
        // Act
        var task1 = Task.Run(() => compiler.CompileAsync(path));
        var task2 = Task.Run(() =>
        {
            // Event 4
            Assert.True(resetEvent2.WaitOne(waitDuration));
            return compiler.CompileAsync(path);
        });
 
        // Event 1
        resetEvent1.Set();
        await Task.WhenAll(task1, task2);
 
        // Assert
#pragma warning disable xUnit1031 // Do not use blocking task operations in test method
        var result1 = task1.Result;
        var result2 = task2.Result;
        Assert.Same(result1, result2);
#pragma warning restore xUnit1031 // Do not use blocking task operations in test method
    }
 
    [Fact]
    public async Task GetOrAdd_CachesCompilationExceptions()
    {
        // Arrange
        var path = "/Views/Home/Index.cshtml";
        var fileProvider = new TestFileProvider();
        fileProvider.AddFile(path, "some content");
        var exception = new InvalidTimeZoneException();
        var compiler = GetViewCompiler(fileProvider);
        compiler.Compile = _ => throw exception;
 
        // Act and Assert - 1
        var actual = await Assert.ThrowsAsync<InvalidTimeZoneException>(
            () => compiler.CompileAsync(path));
        Assert.Same(exception, actual);
 
        // Act and Assert - 2
        compiler.Compile = _ => throw new Exception("Shouldn't be called");
 
        actual = await Assert.ThrowsAsync<InvalidTimeZoneException>(
            () => compiler.CompileAsync(path));
        Assert.Same(exception, actual);
    }
 
    [Fact]
    public void Compile_SucceedsForCSharp7()
    {
        // Arrange
        var content = @"
public class MyTestType
{
    private string _name;
 
    public string Name
    {
        get => _name;
        set => _name = value ?? throw new System.ArgumentNullException(nameof(value));
    }
}";
        var compiler = GetViewCompiler(new TestFileProvider());
        var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("razor-content", "filename"));
 
        // Act
        var result = compiler.CompileAndEmit(codeDocument, content);
 
        // Assert
        var exportedType = Assert.Single(result.ExportedTypes);
        Assert.Equal("MyTestType", exportedType.Name);
    }
 
    [Fact]
    public void Compile_ReturnsCompilationFailureWithPathsFromLinePragmas()
    {
        // Arrange
        var viewPath = "some-relative-path";
        var fileContent = "test file content";
        var content = $@"
#line 1 ""{viewPath}""
this should fail";
 
        var compiler = GetViewCompiler(new TestFileProvider());
        var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create(fileContent, viewPath));
 
        // Act & Assert
        var ex = Assert.Throws<CompilationFailedException>(() => compiler.CompileAndEmit(codeDocument, content));
 
        var compilationFailure = Assert.Single(ex.CompilationFailures);
        Assert.Equal(viewPath, compilationFailure.SourceFilePath);
        Assert.Equal(fileContent, compilationFailure.SourceFileContent);
    }
 
    [Fact]
    public void Compile_ReturnsGeneratedCodePath_IfLinePragmaIsNotAvailable()
    {
        // Arrange
        var viewPath = "some-relative-path";
        var fileContent = "file content";
        var content = "public class Bad { this should fail }";
 
        var compiler = GetViewCompiler(new TestFileProvider());
        var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create(fileContent, viewPath));
 
        // Act & Assert
        var ex = Assert.Throws<CompilationFailedException>(() => compiler.CompileAndEmit(codeDocument, content));
 
        var compilationFailure = Assert.Single(ex.CompilationFailures);
        Assert.Equal("Generated Code", compilationFailure.SourceFilePath);
        Assert.Equal(content, compilationFailure.SourceFileContent);
    }
 
    [Fact]
    public void CompileAndEmit_DoesNotThrowIfDebugTypeIsEmbedded()
    {
        // Arrange
        var referenceManager = CreateReferenceManager();
        var csharpCompiler = new TestCSharpCompiler(referenceManager, Mock.Of<IWebHostEnvironment>())
        {
            EmitOptionsSettable = new EmitOptions(debugInformationFormat: DebugInformationFormat.Embedded),
        };
 
        var compiler = GetViewCompiler(csharpCompiler: csharpCompiler);
        var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path.cshtml"));
 
        // Act
        var result = compiler.CompileAndEmit(codeDocument, "public class Test{}");
 
        // Assert
        Assert.NotNull(result);
    }
 
    [Fact]
    public void CompileAndEmit_WorksIfEmitPdbIsNotSet()
    {
        // Arrange
        var referenceManager = CreateReferenceManager();
        var csharpCompiler = new TestCSharpCompiler(referenceManager, Mock.Of<IWebHostEnvironment>())
        {
            EmitPdbSettable = false,
        };
 
        var compiler = GetViewCompiler(csharpCompiler: csharpCompiler);
        var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path.cshtml"));
 
        // Act
        var result = compiler.CompileAndEmit(codeDocument, "public class Test{}");
 
        // Assert
        Assert.NotNull(result);
    }
 
    private static TestRazorViewCompiler GetViewCompiler(
        TestFileProvider fileProvider = null,
        RazorReferenceManager referenceManager = null,
        IList<CompiledViewDescriptor> precompiledViews = null,
        CSharpCompiler csharpCompiler = null)
    {
        fileProvider = fileProvider ?? new TestFileProvider();
        var options = Options.Create(new MvcRazorRuntimeCompilationOptions
        {
            FileProviders = { fileProvider }
        });
        var compilationFileProvider = new RuntimeCompilationFileProvider(options);
 
        referenceManager = referenceManager ?? CreateReferenceManager();
        precompiledViews = precompiledViews ?? Array.Empty<CompiledViewDescriptor>();
 
        var hostingEnvironment = Mock.Of<IWebHostEnvironment>(e => e.ContentRootPath == "BasePath");
        var fileSystem = new FileProviderRazorProjectFileSystem(compilationFileProvider, hostingEnvironment);
        var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder =>
        {
            RazorExtensions.Register(builder);
        });
 
        csharpCompiler = csharpCompiler ?? new CSharpCompiler(referenceManager, hostingEnvironment);
 
        return new TestRazorViewCompiler(
            fileProvider,
            projectEngine,
            csharpCompiler,
            precompiledViews);
    }
 
    private static RazorReferenceManager CreateReferenceManager()
    {
        var applicationPartManager = new ApplicationPartManager();
        var assembly = typeof(RuntimeViewCompilerTest).Assembly;
        applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly));
 
        return new RazorReferenceManager(applicationPartManager, Options.Create(new MvcRazorRuntimeCompilationOptions()));
    }
 
    private class TestRazorViewCompiler : RuntimeViewCompiler
    {
        public TestRazorViewCompiler(
            TestFileProvider fileProvider,
            RazorProjectEngine projectEngine,
            CSharpCompiler csharpCompiler,
            IList<CompiledViewDescriptor> precompiledViews,
            Func<string, CompiledViewDescriptor> compile = null)
            : base(fileProvider, projectEngine, csharpCompiler, precompiledViews, NullLogger.Instance)
        {
            Compile = compile;
            if (Compile == null)
            {
                Compile = path => new CompiledViewDescriptor
                {
                    RelativePath = path,
                    Item = CreateForView(path),
                };
            }
        }
 
        public Func<string, CompiledViewDescriptor> Compile { get; set; }
 
        protected override CompiledViewDescriptor CompileAndEmit(string relativePath)
        {
            return Compile(relativePath);
        }
    }
 
    private class TestCSharpCompiler : CSharpCompiler
    {
        public TestCSharpCompiler(RazorReferenceManager manager, IWebHostEnvironment hostingEnvironment)
            : base(manager, hostingEnvironment)
        {
        }
 
        public EmitOptions EmitOptionsSettable { get; set; }
 
        public bool EmitPdbSettable { get; set; }
 
        public override EmitOptions EmitOptions => EmitOptionsSettable;
 
        public override bool EmitPdb => EmitPdbSettable;
    }
}