// 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.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.Mvc.Razor.Internal; using Microsoft.AspNet.Mvc.Razor.Precompilation; using Microsoft.Framework.Runtime; using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.Razor.Compilation { public class CompilerCacheTest { private const string ViewPath = "view-path"; private readonly IAssemblyLoadContext TestLoadContext = Mock.Of(); [Fact] public void GetOrAdd_ReturnsFileNotFoundResult_IfFileIsNotFoundInFileSystem() { // Arrange var fileProvider = new TestFileProvider(); var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var type = GetType(); // Act var result = cache.GetOrAdd("/some/path", _ => { throw new Exception("Shouldn't be called"); }); // Assert Assert.Same(CompilerCacheResult.FileNotFound, result); Assert.Null(result.CompilationResult); } [Fact] public void GetOrAdd_ReturnsCompilationResultFromFactory() { // Arrange var fileProvider = new TestFileProvider(); fileProvider.AddFile(ViewPath, "some content"); var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var type = GetType(); var expected = UncachedCompilationResult.Successful(type, "hello world"); // Act var result = cache.GetOrAdd(ViewPath, _ => expected); // Assert Assert.NotSame(CompilerCacheResult.FileNotFound, result); var actual = Assert.IsType(result.CompilationResult); Assert.NotNull(actual); Assert.Same(expected, actual); Assert.Equal("hello world", actual.CompiledContent); Assert.Same(type, actual.CompiledType); } [Fact] public void GetOrAdd_ReturnsFileNotFoundIfFileWasDeleted() { // Arrange var fileProvider = new TestFileProvider(); fileProvider.AddFile(ViewPath, "some content"); var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var type = typeof(RuntimeCompileIdentical); var expected = UncachedCompilationResult.Successful(type, "hello world"); // Act 1 var result1 = cache.GetOrAdd(ViewPath, _ => expected); // Assert 1 Assert.NotSame(CompilerCacheResult.FileNotFound, result1); Assert.Same(expected, result1.CompilationResult); // Act 2 // Delete the file from the file system and set it's expiration trigger. fileProvider.DeleteFile(ViewPath); fileProvider.GetTrigger(ViewPath).IsExpired = true; var result2 = cache.GetOrAdd(ViewPath, _ => { throw new Exception("shouldn't be called."); }); // Assert 2 Assert.Same(CompilerCacheResult.FileNotFound, result2); Assert.Null(result2.CompilationResult); } [Fact] public void GetOrAdd_ReturnsNewResultIfFileWasModified() { // Arrange var fileProvider = new TestFileProvider(); fileProvider.AddFile(ViewPath, "some content"); var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var type = typeof(RuntimeCompileIdentical); var expected1 = UncachedCompilationResult.Successful(type, "hello world"); var expected2 = UncachedCompilationResult.Successful(type, "different content"); // Act 1 var result1 = cache.GetOrAdd(ViewPath, _ => expected1); // Assert 1 Assert.NotSame(CompilerCacheResult.FileNotFound, result1); Assert.Same(expected1, result1.CompilationResult); // Act 2 fileProvider.GetTrigger(ViewPath).IsExpired = true; var result2 = cache.GetOrAdd(ViewPath, _ => expected2); // Assert 2 Assert.NotSame(CompilerCacheResult.FileNotFound, result2); Assert.Same(expected2, result2.CompilationResult); } [Fact] public void GetOrAdd_DoesNotQueryFileSystem_IfCachedFileTriggerWasNotSet() { // Arrange var mockFileProvider = new Mock { CallBase = true }; var fileProvider = mockFileProvider.Object; fileProvider.AddFile(ViewPath, "some content"); var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var type = typeof(RuntimeCompileIdentical); var expected = UncachedCompilationResult.Successful(type, "hello world"); // Act 1 var result1 = cache.GetOrAdd(ViewPath, _ => expected); // Assert 1 Assert.NotSame(CompilerCacheResult.FileNotFound, result1); Assert.Same(expected, result1.CompilationResult); // Act 2 var result2 = cache.GetOrAdd(ViewPath, _ => { throw new Exception("shouldn't be called"); }); // Assert 2 Assert.NotSame(CompilerCacheResult.FileNotFound, result2); Assert.IsType(result2.CompilationResult); Assert.Same(type, result2.CompilationResult.CompiledType); mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once()); } private abstract class View { public abstract string Content { get; } } private class PreCompile : View { public override string Content { get { return "Hello World it's @DateTime.Now"; } } } private class RuntimeCompileIdentical : View { public override string Content { get { return new PreCompile().Content; } } } private class RuntimeCompileDifferent : View { public override string Content { get { return new PreCompile().Content.Substring(1) + " "; } } } private class RuntimeCompileDifferentLength : View { public override string Content { get { return new PreCompile().Content + " longer because it was modified at runtime"; } } } private static Stream GetMemoryStream(string content) { var bytes = Encoding.UTF8.GetBytes(content); return new MemoryStream(bytes); } [Theory] [InlineData(10000)] [InlineData(11000)] public void GetOrAdd_UsesFilesFromCache_IfTimestampDiffers_ButContentAndLengthAreTheSame(long fileTimeUTC) { // Arrange var instance = new RuntimeCompileIdentical(); var length = Encoding.UTF8.GetByteCount(instance.Content); var fileProvider = new TestFileProvider(); var cache = new CompilerCache(new[] { new ViewCollection() }, TestLoadContext, fileProvider); var fileInfo = new TestFileInfo { Length = length, LastModified = DateTime.FromFileTimeUtc(fileTimeUTC), Content = instance.Content }; fileProvider.AddFile(ViewPath, fileInfo); // Act var result = cache.GetOrAdd(ViewPath, compile: _ => { throw new Exception("Shouldn't be called."); }); // Assert Assert.NotSame(CompilerCacheResult.FileNotFound, result); var actual = result.CompilationResult; Assert.NotNull(actual); Assert.Equal(typeof(PreCompile), actual.CompiledType); } [Theory] [InlineData(typeof(RuntimeCompileDifferent), 11000)] [InlineData(typeof(RuntimeCompileDifferentLength), 10000)] [InlineData(typeof(RuntimeCompileDifferentLength), 11000)] public void GetOrAdd_RecompilesFile_IfContentAndLengthAreChanged( Type resultViewType, long fileTimeUTC) { // Arrange var instance = (View)Activator.CreateInstance(resultViewType); var length = Encoding.UTF8.GetByteCount(instance.Content); var fileProvider = new TestFileProvider(); var cache = new CompilerCache(new[] { new ViewCollection() }, TestLoadContext, fileProvider); var fileInfo = new TestFileInfo { Length = length, LastModified = DateTime.FromFileTimeUtc(fileTimeUTC), Content = instance.Content }; fileProvider.AddFile(ViewPath, fileInfo); // Act var result = cache.GetOrAdd(ViewPath, compile: _ => CompilationResult.Successful(resultViewType)); // Assert Assert.NotSame(CompilerCacheResult.FileNotFound, result); var actual = result.CompilationResult; Assert.NotNull(actual); Assert.Equal(resultViewType, actual.CompiledType); } [Fact] public void GetOrAdd_UsesValueFromCache_IfGlobalHasNotChanged() { // Arrange var instance = new PreCompile(); var length = Encoding.UTF8.GetByteCount(instance.Content); var fileProvider = new TestFileProvider(); var lastModified = DateTime.UtcNow; var fileInfo = new TestFileInfo { Length = length, LastModified = lastModified, Content = instance.Content }; fileProvider.AddFile(ViewPath, fileInfo); var globalContent = "global-content"; var globalFileInfo = new TestFileInfo { Content = globalContent, LastModified = DateTime.UtcNow }; fileProvider.AddFile("_ViewImports.cshtml", globalFileInfo); var globalRazorFileInfo = new RazorFileInfo { Hash = Crc32.Calculate(GetMemoryStream(globalContent)).ToString(CultureInfo.InvariantCulture), HashAlgorithmVersion = 1, LastModified = globalFileInfo.LastModified, Length = globalFileInfo.Length, RelativePath = "_ViewImports.cshtml", FullTypeName = typeof(RuntimeCompileIdentical).FullName }; var precompiledViews = new ViewCollection(); precompiledViews.Add(globalRazorFileInfo); var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider); // Act var result = cache.GetOrAdd(ViewPath, compile: _ => { throw new Exception("shouldn't be invoked"); }); // Assert Assert.NotSame(CompilerCacheResult.FileNotFound, result); var actual = result.CompilationResult; Assert.NotNull(actual); Assert.Equal(typeof(PreCompile), actual.CompiledType); } [Fact] public void GetOrAdd_ReturnsFileNotFoundResult_IfPrecompiledViewWasRemovedFromFileSystem() { // Arrange var precompiledViews = new ViewCollection(); var fileProvider = new TestFileProvider(); var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider); // Act var result = cache.GetOrAdd(ViewPath, compile: _ => { throw new Exception("shouldn't be invoked"); }); // Assert Assert.Same(CompilerCacheResult.FileNotFound, result); Assert.Null(result.CompilationResult); } [Fact] public void GetOrAdd_DoesNotReadFileFromFileSystemAfterPrecompiledViewIsVerified() { // Arrange var precompiledViews = new ViewCollection(); var mockFileProvider = new Mock { CallBase = true }; var fileProvider = mockFileProvider.Object; var precompiledView = precompiledViews.FileInfos[0]; var fileInfo = new TestFileInfo { Length = precompiledView.Length, LastModified = precompiledView.LastModified, }; fileProvider.AddFile(ViewPath, fileInfo); var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider); // Act 1 var result1 = cache.GetOrAdd(ViewPath, compile: _ => { throw new Exception("shouldn't be invoked"); }); // Assert 1 Assert.NotSame(CompilerCacheResult.FileNotFound, result1); var actual1 = result1.CompilationResult; Assert.NotNull(actual1); Assert.Equal(typeof(PreCompile), actual1.CompiledType); mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once()); // Act 2 var result2 = cache.GetOrAdd(ViewPath, compile: _ => { throw new Exception("shouldn't be invoked"); }); // Assert 2 Assert.NotSame(CompilerCacheResult.FileNotFound, result2); var actual2 = result2.CompilationResult; Assert.NotNull(actual2); Assert.Equal(typeof(PreCompile), actual2.CompiledType); mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once()); } [Fact] public void GetOrAdd_IgnoresCachedValueIfFileIsIdentical_ButViewImportsWasAdedSinceTheCacheWasCreated() { // Arrange var expectedType = typeof(RuntimeCompileDifferent); var fileProvider = new TestFileProvider(); var collection = new ViewCollection(); var precompiledFile = collection.FileInfos[0]; precompiledFile.RelativePath = "Views\\home\\index.cshtml"; var cache = new CompilerCache(new[] { collection }, TestLoadContext, fileProvider); var testFile = new TestFileInfo { Content = new PreCompile().Content, LastModified = precompiledFile.LastModified, PhysicalPath = precompiledFile.RelativePath }; fileProvider.AddFile(precompiledFile.RelativePath, testFile); var relativeFile = new RelativeFileInfo(testFile, testFile.PhysicalPath); // Act 1 var result1 = cache.GetOrAdd(testFile.PhysicalPath, compile: _ => { throw new Exception("should not be called"); }); // Assert 1 Assert.NotSame(CompilerCacheResult.FileNotFound, result1); var actual1 = result1.CompilationResult; Assert.NotNull(actual1); Assert.Equal(typeof(PreCompile), actual1.CompiledType); // Act 2 var globalTrigger = fileProvider.GetTrigger("Views\\_ViewImports.cshtml"); globalTrigger.IsExpired = true; var result2 = cache.GetOrAdd(testFile.PhysicalPath, compile: _ => CompilationResult.Successful(expectedType)); // Assert 2 Assert.NotSame(CompilerCacheResult.FileNotFound, result2); var actual2 = result2.CompilationResult; Assert.NotNull(actual2); Assert.Equal(expectedType, actual2.CompiledType); } [Fact] public void GetOrAdd_IgnoresCachedValueIfFileIsIdentical_ButGlobalWasDeletedSinceCacheWasCreated() { // Arrange var expectedType = typeof(RuntimeCompileDifferent); var lastModified = DateTime.UtcNow; var fileProvider = new TestFileProvider(); var viewCollection = new ViewCollection(); var precompiledView = viewCollection.FileInfos[0]; precompiledView.RelativePath = "Views\\Index.cshtml"; var viewFileInfo = new TestFileInfo { Content = new PreCompile().Content, LastModified = precompiledView.LastModified, PhysicalPath = precompiledView.RelativePath }; fileProvider.AddFile(viewFileInfo.PhysicalPath, viewFileInfo); var globalFileInfo = new TestFileInfo { PhysicalPath = "Views\\_ViewImports.cshtml", Content = "viewstart-content", LastModified = lastModified }; var globalFile = new RazorFileInfo { FullTypeName = typeof(RuntimeCompileIdentical).FullName, RelativePath = globalFileInfo.PhysicalPath, LastModified = globalFileInfo.LastModified, Hash = RazorFileHash.GetHash(globalFileInfo, hashAlgorithmVersion: 1), HashAlgorithmVersion = 1, Length = globalFileInfo.Length }; fileProvider.AddFile(globalFileInfo.PhysicalPath, globalFileInfo); viewCollection.Add(globalFile); var cache = new CompilerCache(new[] { viewCollection }, TestLoadContext, fileProvider); // Act 1 var result1 = cache.GetOrAdd(viewFileInfo.PhysicalPath, compile: _ => { throw new Exception("should not be called"); }); // Assert 1 Assert.NotSame(CompilerCacheResult.FileNotFound, result1); var actual1 = result1.CompilationResult; Assert.NotNull(actual1); Assert.Equal(typeof(PreCompile), actual1.CompiledType); // Act 2 var trigger = fileProvider.GetTrigger(globalFileInfo.PhysicalPath); trigger.IsExpired = true; var result2 = cache.GetOrAdd(viewFileInfo.PhysicalPath, compile: _ => CompilationResult.Successful(expectedType)); // Assert 2 Assert.NotSame(CompilerCacheResult.FileNotFound, result2); var actual2 = result2.CompilationResult; Assert.NotNull(actual2); Assert.Equal(expectedType, actual2.CompiledType); } public static IEnumerable GetOrAdd_IgnoresCachedValue_IfGlobalWasChangedSinceCacheWasCreatedData { get { var globalContent = "global-content"; var contentStream = GetMemoryStream(globalContent); var lastModified = DateTime.UtcNow; int length = Encoding.UTF8.GetByteCount(globalContent); var path = "Views\\_ViewImports.cshtml"; var razorFileInfo = new RazorFileInfo { Hash = Crc32.Calculate(contentStream).ToString(CultureInfo.InvariantCulture), HashAlgorithmVersion = 1, LastModified = lastModified, Length = length, RelativePath = path }; // Length does not match var testFileInfo1 = new TestFileInfo { Length = 7732 }; yield return new object[] { razorFileInfo, testFileInfo1 }; // Content and last modified do not match var testFileInfo2 = new TestFileInfo { Length = length, Content = "viewstart-modified", LastModified = lastModified.AddSeconds(100), }; yield return new object[] { razorFileInfo, testFileInfo2 }; } } [Theory] [MemberData(nameof(GetOrAdd_IgnoresCachedValue_IfGlobalWasChangedSinceCacheWasCreatedData))] public void GetOrAdd_IgnoresCachedValue_IfGlobalFileWasChangedSinceCacheWasCreated( RazorFileInfo viewStartRazorFileInfo, IFileInfo globalFileInfo) { // Arrange var expectedType = typeof(RuntimeCompileDifferent); var lastModified = DateTime.UtcNow; var viewStartLastModified = DateTime.UtcNow; var content = "some content"; var fileInfo = new TestFileInfo { Length = 1020, Content = content, LastModified = lastModified, PhysicalPath = "Views\\home\\index.cshtml" }; var fileProvider = new TestFileProvider(); fileProvider.AddFile(fileInfo.PhysicalPath, fileInfo); fileProvider.AddFile(viewStartRazorFileInfo.RelativePath, globalFileInfo); var viewCollection = new ViewCollection(); var cache = new CompilerCache(new[] { viewCollection }, TestLoadContext, fileProvider); // Act var result = cache.GetOrAdd(fileInfo.PhysicalPath, compile: _ => CompilationResult.Successful(expectedType)); // Assert Assert.NotSame(CompilerCacheResult.FileNotFound, result); var actual = result.CompilationResult; Assert.NotNull(actual); Assert.Equal(expectedType, actual.CompiledType); } [Fact] public void GetOrAdd_DoesNotCacheCompiledContent_OnCallsAfterInitial() { // Arrange var lastModified = DateTime.UtcNow; var fileProvider = new TestFileProvider(); var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var fileInfo = new TestFileInfo { PhysicalPath = "test", LastModified = lastModified }; fileProvider.AddFile("test", fileInfo); var type = GetType(); var uncachedResult = UncachedCompilationResult.Successful(type, "hello world"); // Act cache.GetOrAdd("test", _ => uncachedResult); var result1 = cache.GetOrAdd("test", _ => { throw new Exception("shouldn't be called."); }); var result2 = cache.GetOrAdd("test", _ => { throw new Exception("shouldn't be called."); }); // Assert Assert.NotSame(CompilerCacheResult.FileNotFound, result1); Assert.NotSame(CompilerCacheResult.FileNotFound, result2); var actual1 = Assert.IsType(result1.CompilationResult); var actual2 = Assert.IsType(result2.CompilationResult); Assert.NotSame(uncachedResult, actual1); Assert.NotSame(uncachedResult, actual2); Assert.Same(type, actual1.CompiledType); Assert.Same(type, actual2.CompiledType); } [Fact] public void Match_ReturnsFalse_IfTypeIsAbstract() { // Arrange var type = typeof(AbstractRazorFileInfoCollection); // Act var result = CompilerCache.Match(type); // Assert Assert.False(result); } [Fact] public void Match_ReturnsFalse_IfTypeHasGenericParameters() { // Arrange var type = typeof(GenericRazorFileInfoCollection<>); // Act var result = CompilerCache.Match(type); // Assert Assert.False(result); } [Fact] public void Match_ReturnsFalse_IfTypeDoesNotHaveDefaultConstructor() { // Arrange var type = typeof(ParameterConstructorRazorFileInfoCollection); // Act var result = CompilerCache.Match(type); // Assert Assert.False(result); } [Fact] public void Match_ReturnsFalse_IfTypeDoesNotDeriveFromRazorFileInfoCollection() { // Arrange var type = typeof(NonSubTypeRazorFileInfoCollection); // Act var result = CompilerCache.Match(type); // Assert Assert.False(result); } [Fact] public void Match_ReturnsTrue_IfTypeDerivesFromRazorFileInfoCollection() { // Arrange var type = typeof(ViewCollection); // Act var result = CompilerCache.Match(type); // Assert Assert.True(result); } private abstract class AbstractRazorFileInfoCollection : RazorFileInfoCollection { } private class GenericRazorFileInfoCollection : RazorFileInfoCollection { } private class ParameterConstructorRazorFileInfoCollection : RazorFileInfoCollection { public ParameterConstructorRazorFileInfoCollection(string value) { } } private class NonSubTypeRazorFileInfoCollection : Controller { } private class ViewCollection : RazorFileInfoCollection { private readonly List _fileInfos = new List(); public ViewCollection() { FileInfos = _fileInfos; var content = new PreCompile().Content; var length = Encoding.UTF8.GetByteCount(content); Add(new RazorFileInfo() { FullTypeName = typeof(PreCompile).FullName, Hash = Crc32.Calculate(GetMemoryStream(content)).ToString(CultureInfo.InvariantCulture), HashAlgorithmVersion = 1, LastModified = DateTime.FromFileTimeUtc(10000), Length = length, RelativePath = ViewPath, }); } public void Add(RazorFileInfo fileInfo) { _fileInfos.Add(fileInfo); } public override Assembly LoadAssembly(IAssemblyLoadContext loadContext) { return typeof(ViewCollection).Assembly; } } } }