// 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.Text; using System.Threading; using System.Web.WebPages.TestUtils; using Microsoft.AspNetCore.Razor.Chunks.Generators; using Microsoft.AspNetCore.Razor.CodeGenerators; using Microsoft.AspNetCore.Razor.Parser; using Microsoft.AspNetCore.Razor.Parser.Internal; using Microsoft.AspNetCore.Razor.Text; using Moq; using Xunit; namespace Microsoft.AspNetCore.Razor { public class RazorTemplateEngineTest { [Fact] public void InvalidRazorEngineHostReturnsParseErrorsAtDesignTime() { // Arrange var host = new InvalidRazorEngineHost(new CSharpRazorCodeLanguage()) { DesignTimeMode = true }; var razorEngine = new RazorTemplateEngine(host); var input = new StringTextBuffer("
Hello @(\"World\")
"); var exception = new InvalidOperationException("Hello World"); var expectedError = RazorResources.FormatFatalException("test", Environment.NewLine, exception.Message); // Act var result = razorEngine.GenerateCode(input, className: null, rootNamespace: null, sourceFileName: "test"); // Assert Assert.Empty(result.Document.Children); Assert.Empty(result.ChunkTree.Children); Assert.Empty(result.DesignTimeLineMappings); Assert.Empty(result.GeneratedCode); var error = Assert.Single(result.ParserErrors); Assert.Equal(expectedError, error.Message, StringComparer.Ordinal); Assert.Equal(SourceLocation.Undefined, error.Location); Assert.Equal(-1, error.Length); } [Fact] public void InvalidRazorEngineHostThrowsAtRuntime() { // Arrange var host = new InvalidRazorEngineHost(new CSharpRazorCodeLanguage()) { DesignTimeMode = false }; var razorEngine = new RazorTemplateEngine(host); var input = new StringTextBuffer("
Hello @(\"World\")
"); // Act var thrownException = Assert.Throws(() => razorEngine.GenerateCode(input, className: null, rootNamespace: null, sourceFileName: "test")); // Assert Assert.Equal("Hello World", thrownException.Message, StringComparer.Ordinal); } [Fact] public void ConstructorInitializesHost() { // Arrange var host = new RazorEngineHost(new CSharpRazorCodeLanguage()); // Act var engine = new RazorTemplateEngine(host); // Assert Assert.Same(host, engine.Host); } [Fact] public void CreateParserMethodIsConstructedFromHost() { // Arrange var host = CreateHost(); var engine = new RazorTemplateEngine(host); // Act var parser = engine.CreateParser("some-file"); // Assert Assert.IsType(parser.CodeParser); Assert.IsType(parser.MarkupParser); } [Fact] public void CreateParserMethodSetsParserContextToDesignTimeModeIfHostSetToDesignTimeMode() { // Arrange var host = CreateHost(); var engine = new RazorTemplateEngine(host); host.DesignTimeMode = true; // Act var parser = engine.CreateParser("some-file"); // Assert Assert.True(parser.DesignTimeMode); } [Fact] public void CreateParserMethodPassesParsersThroughDecoratorMethodsOnHost() { // Arrange var expectedCode = new Mock().Object; var expectedMarkup = new Mock().Object; var mockHost = new Mock(new CSharpRazorCodeLanguage()) { CallBase = true }; mockHost.Setup(h => h.DecorateCodeParser(It.IsAny())) .Returns(expectedCode); mockHost.Setup(h => h.DecorateMarkupParser(It.IsAny())) .Returns(expectedMarkup); var engine = new RazorTemplateEngine(mockHost.Object); // Act var actual = engine.CreateParser("some-file"); // Assert Assert.Equal(expectedCode, actual.CodeParser); Assert.Equal(expectedMarkup, actual.MarkupParser); } [Fact] public void CreateChunkGeneratorMethodPassesChunkGeneratorThroughDecorateMethodOnHost() { // Arrange var mockHost = new Mock(new CSharpRazorCodeLanguage()) { CallBase = true }; var expected = new Mock("Foo", "Bar", "Baz", mockHost.Object).Object; mockHost.Setup(h => h.DecorateChunkGenerator(It.IsAny())) .Returns(expected); var engine = new RazorTemplateEngine(mockHost.Object); // Act var actual = engine.CreateChunkGenerator("Foo", "Bar", "Baz"); // Assert Assert.Equal(expected, actual); } [Fact] public void CreateCodeGenerator_PassesChunkGeneratorThroughDecorateMethodOnHost() { // Arrange var mockHost = new Mock(new CSharpRazorCodeLanguage()) { CallBase = true }; var codeGeneratorContext = new CodeGeneratorContext( mockHost.Object, "different-class", "different-ns", string.Empty, shouldGenerateLinePragmas: true, errorSink: new ErrorSink()); var expected = new CSharpCodeGenerator(codeGeneratorContext); mockHost .Setup(h => h.DecorateCodeGenerator(It.IsAny(), codeGeneratorContext)) .Returns(expected); var engine = new RazorTemplateEngine(mockHost.Object); // Act var actual = engine.CreateCodeGenerator(codeGeneratorContext); // Assert Assert.Equal(expected, actual); } [Fact] public void ParseTemplateCopiesTextReaderContentToSeekableTextReaderAndPassesToParseTemplateCore() { // Arrange var mockEngine = new Mock(CreateHost()); var reader = new StringReader("foo"); var source = new CancellationTokenSource(); // Act mockEngine.Object.ParseTemplate(reader, cancelToken: source.Token); // Assert mockEngine.Verify(e => e.ParseTemplateCore( It.Is(l => l.ReadToEnd() == "foo"), null, source.Token)); } [Fact] public void GenerateCodeCopiesTextReaderContentToSeekableTextReaderAndPassesToGenerateCodeCore() { // Arrange var mockEngine = new Mock(CreateHost()); var reader = new StringReader("foo"); var source = new CancellationTokenSource(); var className = "Foo"; var ns = "Bar"; var src = "Baz"; // Act mockEngine.Object.GenerateCode( reader, className: className, rootNamespace: ns, sourceFileName: src, cancelToken: source.Token); // Assert mockEngine.Verify(e => e.GenerateCodeCore( It.Is(l => l.ReadToEnd() == "foo"), className, ns, src, null, source.Token)); } [Fact] public void ParseTemplateOutputsResultsOfParsingProvidedTemplateSource() { // Arrange var engine = new RazorTemplateEngine(CreateHost()); // Act var results = engine.ParseTemplate(new StringTextBuffer("foo @bar(")); // Assert Assert.False(results.Success); Assert.Single(results.ParserErrors); Assert.NotNull(results.Document); } [Fact] public void GenerateOutputsResultsOfParsingAndGeneration() { // Arrange var engine = new RazorTemplateEngine(CreateHost()); // Act var results = engine.GenerateCode(new StringTextBuffer("foo @bar(")); // Assert Assert.False(results.Success); Assert.Single(results.ParserErrors); Assert.NotNull(results.Document); Assert.NotNull(results.GeneratedCode); } [Fact] public void GenerateOutputsDesignTimeMappingsIfDesignTimeSetOnHost() { // Arrange var engine = new RazorTemplateEngine(CreateHost(designTime: true)); // Act var results = engine.GenerateCode( new StringTextBuffer("foo @bar()"), className: null, rootNamespace: null, sourceFileName: "foo.cshtml"); // Assert Assert.True(results.Success); Assert.Empty(results.ParserErrors); Assert.NotNull(results.Document); Assert.NotNull(results.GeneratedCode); Assert.NotNull(results.DesignTimeLineMappings); } public static IEnumerable GenerateCodeCalculatesLinePragma_IfStreamInputIsUsedData { get { // Seekable stream var content = Encoding.UTF8.GetBytes("Hello world"); var stream = new MemoryStream(content); yield return new[] { stream }; // Non seekable stream var mockStream = new Mock(content) { CallBase = true }; mockStream.Setup(m => m.CanSeek) .Returns(false); yield return new[] { mockStream.Object }; } } [Theory] [MemberData(nameof(GenerateCodeCalculatesLinePragma_IfStreamInputIsUsedData))] public void GenerateCodeCalculatesChecksum_IfStreamInputIsUsed(Stream stream) { // Arrange var engine = new TestableRazorTemplateEngine(); // Act var results = engine.GenerateCode(stream, "some-class", "some-ns", "foo.cshtml"); // Assert Assert.Equal("7b502c3a1f48c8609ae212cdfb639dee39673f5e", engine.Checksum); } [Fact] public void GenerateCode_DoesNotCalculateChecksum_InDesignTimeMode() { // Arrange var engine = new TestableRazorTemplateEngine(); engine.Host.DesignTimeMode = true; // Act var results = engine.GenerateCode(Stream.Null, "some-class", "some-ns", "foo.cshtml"); // Assert Assert.Null(engine.Checksum); } [Fact] public void GenerateCode_UsesDecoratedRazorParser() { // Arrange Mock parser = null; var host = new Mock(new CSharpRazorCodeLanguage()) { CallBase = true }; host.Setup(p => p.DecorateRazorParser(It.IsAny(), "foo.cshtml")) .Returns((RazorParser p, string file) => { parser = new Mock(p) { CallBase = true }; return parser.Object; }) .Verifiable(); var engine = new RazorTemplateEngine(host.Object); // Act var results = engine.GenerateCode(Stream.Null, "some-class", "some-ns", "foo.cshtml"); // Assert Assert.NotNull(parser); parser.Verify(v => v.Parse(It.IsAny()), Times.Once()); host.Verify(); } private static RazorEngineHost CreateHost(bool designTime = false) { return new RazorEngineHost(new CSharpRazorCodeLanguage()) { DesignTimeMode = designTime }; } private class TestableRazorTemplateEngine : RazorTemplateEngine { public TestableRazorTemplateEngine() : base(CreateHost()) { } public string Checksum { get; set; } protected internal override GeneratorResults GenerateCodeCore( ITextDocument input, string className, string rootNamespace, string sourceFileName, string checksum, CancellationToken? cancelToken) { Checksum = checksum; return null; } } private class InvalidRazorEngineHost : RazorEngineHost { public InvalidRazorEngineHost(RazorCodeLanguage codeLanguage) : base(codeLanguage) { } public override string DefaultClassName { get { throw new InvalidOperationException("Hello World"); } set { } } } } }