From 9ac346529c5b656b4860945f924fbe0e8cbcacea Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Sat, 21 Mar 2015 13:57:02 -0700 Subject: [PATCH] Add test to validate tooling path normalization. - Tooling passes in rooted paths when asking for a RazorParser for a file. This was problematic when resolving inherited code trees and ultimately this commit tests that. #2213 --- .../Directives/ChunkInheritanceUtility.cs | 2 +- .../Internal/DesignTimeRazorPathNormalizer.cs | 30 ++++++++++ .../Internal/RazorPathNormalizer.cs | 15 +++++ .../MvcRazorHost.cs | 60 +++++++++---------- .../MvcRazorHostTest.cs | 49 +++++++++++++++ 5 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/Internal/DesignTimeRazorPathNormalizer.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/Internal/RazorPathNormalizer.cs diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs index 0e3f5adce0..d278980cd9 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs @@ -47,7 +47,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives /// The path of the page to locate inherited chunks for. /// A of parsed _GlobalImport /// s. - public IReadOnlyList GetInheritedCodeTrees([NotNull] string pagePath) + public virtual IReadOnlyList GetInheritedCodeTrees([NotNull] string pagePath) { var inheritedCodeTrees = new List(); var templateEngine = new RazorTemplateEngine(_razorHost); diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Internal/DesignTimeRazorPathNormalizer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Internal/DesignTimeRazorPathNormalizer.cs new file mode 100644 index 0000000000..ac433aaee6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Internal/DesignTimeRazorPathNormalizer.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.Razor.Internal +{ + public class DesignTimeRazorPathNormalizer : RazorPathNormalizer + { + private readonly string _applicationRoot; + + public DesignTimeRazorPathNormalizer([NotNull] string applicationRoot) + { + _applicationRoot = applicationRoot; + } + + public override string NormalizePath([NotNull] string path) + { + // Need to convert path to application relative (rooted paths are passed in during design time). + if (Path.IsPathRooted(path) && path.StartsWith(_applicationRoot, StringComparison.Ordinal)) + { + path = path.Substring(_applicationRoot.Length); + } + + return path; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Internal/RazorPathNormalizer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Internal/RazorPathNormalizer.cs new file mode 100644 index 0000000000..f459534fb3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Internal/RazorPathNormalizer.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.Razor.Internal +{ + public class RazorPathNormalizer + { + public virtual string NormalizePath([NotNull] string path) + { + return path; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs index 86fa368baa..7c33a5015a 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.Mvc.Razor.Directives; +using Microsoft.AspNet.Mvc.Razor.Internal; using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Generator.Compiler; @@ -36,31 +37,13 @@ namespace Microsoft.AspNet.Mvc.Razor // This field holds the type name without the generic decoration (MyBaseType) private readonly string _baseType; private readonly ICodeTreeCache _codeTreeCache; + private readonly RazorPathNormalizer _pathNormalizer; private ChunkInheritanceUtility _chunkInheritanceUtility; -#if NET45 - /// - /// Initializes a new instance of with the specified - /// . - /// - /// The path to the application base. - // Note: This constructor is used by tooling and is created once for each - // Razor page that is loaded. Consequently, each loaded page has its own copy of - // the CodeTreeCache, but this ok - having a shared CodeTreeCache per application in tooling - // is problematic to manage. - public MvcRazorHost(string root) : - this(new DefaultCodeTreeCache(new PhysicalFileProvider(root))) - { - ApplicationRoot = root; - } -#endif - /// - /// Initializes a new instance of using the specified . - /// - /// A rooted at the application base path. - public MvcRazorHost(ICodeTreeCache codeTreeCache) + internal MvcRazorHost(ICodeTreeCache codeTreeCache, RazorPathNormalizer pathNormalizer) : base(new CSharpRazorCodeLanguage()) { + _pathNormalizer = pathNormalizer; _baseType = BaseType; _codeTreeCache = codeTreeCache; @@ -115,10 +98,26 @@ namespace Microsoft.AspNet.Mvc.Razor #if NET45 /// - /// The path to the application root. + /// Initializes a new instance of with the specified . /// - public string ApplicationRoot { get; } + /// The path to the application base. + // Note: This constructor is used by tooling and is created once for each + // Razor page that is loaded. Consequently, each loaded page has its own copy of + // the CodeTreeCache, but this ok - having a shared CodeTreeCache per application in tooling + // is problematic to manage. + public MvcRazorHost(string root) + : this(new DefaultCodeTreeCache(new PhysicalFileProvider(root)), new DesignTimeRazorPathNormalizer(root)) + { + } #endif + /// + /// Initializes a new instance of using the specified . + /// + /// An rooted at the application base path. + public MvcRazorHost(ICodeTreeCache codeTreeCache) + : this(codeTreeCache, new RazorPathNormalizer()) + { + } /// /// Gets the model type used by default when no model is specified. @@ -168,7 +167,8 @@ namespace Microsoft.AspNet.Mvc.Razor get { return "CreateModelExpression"; } } - private ChunkInheritanceUtility ChunkInheritanceUtility + // Internal for testing + internal ChunkInheritanceUtility ChunkInheritanceUtility { get { @@ -180,6 +180,10 @@ namespace Microsoft.AspNet.Mvc.Razor return _chunkInheritanceUtility; } + set + { + _chunkInheritanceUtility = value; + } } /// @@ -194,13 +198,7 @@ namespace Microsoft.AspNet.Mvc.Razor /// public override RazorParser DecorateRazorParser([NotNull] RazorParser razorParser, string sourceFileName) { -#if NET45 - // Need to convert sourceFileName to application relative (rooted paths are passed in by tooling). - if (Path.IsPathRooted(sourceFileName)) - { - sourceFileName = sourceFileName.Substring(ApplicationRoot.Length); - } -#endif + sourceFileName = _pathNormalizer.NormalizePath(sourceFileName); var inheritedCodeTrees = ChunkInheritanceUtility.GetInheritedCodeTrees(sourceFileName); return new MvcRazorParser(razorParser, inheritedCodeTrees, DefaultInheritedChunks); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorHostTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorHostTest.cs index ffef6acf74..ffb2a8a9d0 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorHostTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorHostTest.cs @@ -1,20 +1,52 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Mvc.Razor.Directives; +using Microsoft.AspNet.Mvc.Razor.Internal; using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Generator.Compiler.CSharp; +using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Text; +using Microsoft.Framework.Internal; using Xunit; namespace Microsoft.AspNet.Mvc.Razor { public class MvcRazorHostTest { + [Theory] + [InlineData("//")] + [InlineData("C:/")] + [InlineData(@"\\")] + [InlineData(@"C:\")] + public void DecorateRazorParser_DesignTimeRazorPathNormalizer_NormalizesChunkInheritanceUtilityPaths( + string rootPrefix) + { + // Arrange + var rootedAppPath = $"{rootPrefix}SomeComputer/Location/Project/"; + var rootedFilePath = $"{rootPrefix}SomeComputer/Location/Project/src/file.cshtml"; + var host = new MvcRazorHost( + codeTreeCache: null, + pathNormalizer: new DesignTimeRazorPathNormalizer(rootedAppPath)); + var parser = new RazorParser( + host.CodeLanguage.CreateCodeParser(), + host.CreateMarkupParser(), + tagHelperDescriptorResolver: null); + var chunkInheritanceUtility = new PathValidatingChunkInheritanceUtility(host); + host.ChunkInheritanceUtility = chunkInheritanceUtility; + + // Act + host.DecorateRazorParser(parser, rootedFilePath); + + // Assert + Assert.Equal("src/file.cshtml", chunkInheritanceUtility.InheritedCodeTreePagePath, StringComparer.Ordinal); + } + [Fact] public void MvcRazorHost_EnablesInstrumentationByDefault() { @@ -254,6 +286,23 @@ namespace Microsoft.AspNet.Mvc.Razor generatedLocation: new MappingLocation(generatedLocation, contentLength)); } + private class PathValidatingChunkInheritanceUtility : ChunkInheritanceUtility + { + public PathValidatingChunkInheritanceUtility(MvcRazorHost razorHost) + : base(razorHost, codeTreeCache: null, defaultInheritedChunks: new Chunk[0]) + { + } + + public string InheritedCodeTreePagePath { get; private set; } + + public override IReadOnlyList GetInheritedCodeTrees([NotNull] string pagePath) + { + InheritedCodeTreePagePath = pagePath; + + return new CodeTree[0]; + } + } + /// /// Used when testing Tag Helpers, it disables the unique ID generation feature. ///