From a9dca60a10b304c672cfc9900b1f50d2e0cc6b54 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Tue, 20 Feb 2018 18:50:53 -0800 Subject: [PATCH] Added support for AssemblyMetadata caching during build server TagHelper discovery --- .../Application.cs | 6 +- .../CachingMetadataReference.cs | 32 +++ .../CompilerHost.cs | 8 +- .../ConcurrentLruCache.cs | 207 ++++++++++++++++++ .../DiscoverCommand.cs | 2 +- .../MetadataCache.cs | 86 ++++++++ .../Program.cs | 8 +- .../ServerCommand.cs | 1 + .../ServerProtocol/ServerRequest.cs | 2 +- .../BuildServerIntegrationTest.cs | 19 ++ .../BuildServerTestFixture.cs | 3 +- .../ConcurrentLruCacheTest.cs | 120 ++++++++++ .../Infrastructure/ServerUtilities.cs | 3 +- .../MetadataCacheTest.cs | 103 +++++++++ .../ServerLifecycleTest.cs | 9 +- 15 files changed, 599 insertions(+), 10 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Tools/CachingMetadataReference.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Tools/ConcurrentLruCache.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Tools/MetadataCache.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Tools.Test/ConcurrentLruCacheTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Tools.Test/MetadataCacheTest.cs diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Application.cs b/src/Microsoft.AspNetCore.Razor.Tools/Application.cs index 06da6a9ec4..c9f569747a 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Application.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/Application.cs @@ -6,17 +6,19 @@ using System.Collections.Generic; using System.IO; using System.Reflection; using System.Threading; +using Microsoft.CodeAnalysis; using Microsoft.Extensions.CommandLineUtils; namespace Microsoft.AspNetCore.Razor.Tools { internal class Application : CommandLineApplication { - public Application(CancellationToken cancellationToken, ExtensionAssemblyLoader loader, ExtensionDependencyChecker checker) + public Application(CancellationToken cancellationToken, ExtensionAssemblyLoader loader, ExtensionDependencyChecker checker, Func assemblyReferenceProvider) { CancellationToken = cancellationToken; Checker = checker; Loader = loader; + AssemblyReferenceProvider = assemblyReferenceProvider; Name = "rzc"; FullName = "Microsoft ASP.NET Core Razor CLI tool"; @@ -37,6 +39,8 @@ namespace Microsoft.AspNetCore.Razor.Tools public ExtensionDependencyChecker Checker { get; } + public Func AssemblyReferenceProvider { get; } + public new int Execute(params string[] args) { try diff --git a/src/Microsoft.AspNetCore.Razor.Tools/CachingMetadataReference.cs b/src/Microsoft.AspNetCore.Razor.Tools/CachingMetadataReference.cs new file mode 100644 index 0000000000..0aeb381b21 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/CachingMetadataReference.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal sealed class CachingMetadataReference : PortableExecutableReference + { + private static readonly MetadataCache _metadataCache = new MetadataCache(); + + public CachingMetadataReference(string fullPath, MetadataReferenceProperties properties) + : base(properties, fullPath) + { + } + + protected override DocumentationProvider CreateDocumentationProvider() + { + return DocumentationProvider.Default; + } + + protected override Metadata GetMetadataImpl() + { + return _metadataCache.GetMetadata(FilePath); + } + + protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties) + { + return new CachingMetadataReference(FilePath, properties); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs b/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs index 553e9f5a87..a57b9e83a0 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs @@ -1,10 +1,12 @@ // 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.Linq; using System.Threading; +using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Razor.Tools { @@ -30,8 +32,12 @@ namespace Microsoft.AspNetCore.Razor.Tools // consistently reject a request that doesn't specify everything it needs. Otherwise the request // could succeed sometimes if it relies on transient state. Loader = new DefaultExtensionAssemblyLoader(Path.Combine(Path.GetTempPath(), "Razor-Server")); + + AssemblyReferenceProvider = (path, properties) => new CachingMetadataReference(path, properties); } + public Func AssemblyReferenceProvider { get; } + public ExtensionAssemblyLoader Loader { get; } public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken) @@ -48,7 +54,7 @@ namespace Microsoft.AspNetCore.Razor.Tools var writer = ServerLogger.IsLoggingEnabled ? new StringWriter() : TextWriter.Null; var checker = new DefaultExtensionDependencyChecker(Loader, writer); - var app = new Application(cancellationToken, Loader, checker) + var app = new Application(cancellationToken, Loader, checker, AssemblyReferenceProvider) { Out = writer, Error = writer, diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ConcurrentLruCache.cs b/src/Microsoft.AspNetCore.Razor.Tools/ConcurrentLruCache.cs new file mode 100644 index 0000000000..0220615ec2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/ConcurrentLruCache.cs @@ -0,0 +1,207 @@ +// 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.Diagnostics; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + /// + /// Cache with a fixed size that evicts the least recently used members. + /// Thread-safe. + /// This was taken from https://github.com/dotnet/roslyn/blob/749c0ec135d7d080658dc1aa794d15229c3d10d2/src/Compilers/Core/Portable/InternalUtilities/ConcurrentLruCache.cs. + /// + internal class ConcurrentLruCache + { + private readonly int _capacity; + + private readonly Dictionary _cache; + private readonly LinkedList _nodeList; + // This is a naive course-grained lock, it can probably be optimized + private readonly object _lockObject = new object(); + + public ConcurrentLruCache(int capacity) + : this (capacity, EqualityComparer.Default) + { + } + + public ConcurrentLruCache(int capacity, IEqualityComparer comparer) + { + if (capacity <= 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity)); + } + _capacity = capacity; + _cache = new Dictionary(capacity, comparer); + _nodeList = new LinkedList(); + } + + /// + /// Create cache from an array. The cache capacity will be the size + /// of the array. All elements of the array will be added to the + /// cache. If any duplicate keys are found in the array a + /// will be thrown. + /// + public ConcurrentLruCache(KeyValuePair[] array) + : this(array.Length) + { + foreach (var kvp in array) + { + UnsafeAdd(kvp.Key, kvp.Value); + } + } + + public int Count + { + get + { + lock (_lockObject) + { + return _cache.Count; + } + } + } + + public void Add(TKey key, TValue value) + { + lock (_lockObject) + { + UnsafeAdd(key, value); + } + } + + public TValue GetOrAdd(TKey key, TValue value) + { + lock (_lockObject) + { + if (UnsafeTryGetValue(key, out var result)) + { + return result; + } + else + { + UnsafeAdd(key, value); + return value; + } + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + lock (_lockObject) + { + return UnsafeTryGetValue(key, out value); + } + } + + public bool Remove(TKey key) + { + lock (_lockObject) + { + return UnsafeRemove(key); + } + } + + /// + /// For testing. Very expensive. + /// + internal IEnumerable> TestingEnumerable + { + get + { + lock (_lockObject) + { + foreach (var key in _nodeList) + { + var kvp = new KeyValuePair(key, _cache[key].Value); + yield return kvp; + } + } + } + } + + /// + /// Doesn't lock. + /// + private bool UnsafeTryGetValue(TKey key, out TValue value) + { + if (_cache.TryGetValue(key, out var result)) + { + MoveNodeToTop(result.Node); + value = result.Value; + return true; + } + else + { + value = default(TValue); + return false; + } + } + + private void MoveNodeToTop(LinkedListNode node) + { + if (!object.ReferenceEquals(_nodeList.First, node)) + { + _nodeList.Remove(node); + _nodeList.AddFirst(node); + } + } + + /// + /// Expects non-empty cache. Does not lock. + /// + private void UnsafeEvictLastNode() + { + Debug.Assert(_capacity > 0); + var lastNode = _nodeList.Last; + _nodeList.Remove(lastNode); + _cache.Remove(lastNode.Value); + } + + private void UnsafeAddNodeToTop(TKey key, TValue value) + { + var node = new LinkedListNode(key); + _cache.Add(key, new CacheValue(value, node)); + _nodeList.AddFirst(node); + } + + /// + /// Doesn't lock. + /// + private void UnsafeAdd(TKey key, TValue value) + { + if (_cache.TryGetValue(key, out var result)) + { + throw new ArgumentException("Key already exists", nameof(key)); + } + else + { + if (_cache.Count == _capacity) + { + UnsafeEvictLastNode(); + } + UnsafeAddNodeToTop(key, value); + } + } + + private bool UnsafeRemove(TKey key) + { + _nodeList.Remove(key); + return _cache.Remove(key); + } + + private struct CacheValue + { + public CacheValue(TValue value, LinkedListNode node) + { + Value = value; + Node = node; + } + + public TValue Value { get; } + + public LinkedListNode Node { get; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs index 2fedb87022..c846ef3692 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs @@ -133,7 +133,7 @@ namespace Microsoft.AspNetCore.Razor.Tools var metadataReferences = new MetadataReference[assemblies.Length]; for (var i = 0; i < assemblies.Length; i++) { - metadataReferences[i] = MetadataReference.CreateFromFile(assemblies[i]); + metadataReferences[i] = Parent.AssemblyReferenceProvider(assemblies[i], default(MetadataReferenceProperties)); } var engine = RazorProjectEngine.Create(configuration, RazorProjectFileSystem.Empty, b => diff --git a/src/Microsoft.AspNetCore.Razor.Tools/MetadataCache.cs b/src/Microsoft.AspNetCore.Razor.Tools/MetadataCache.cs new file mode 100644 index 0000000000..b0f9fd025e --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/MetadataCache.cs @@ -0,0 +1,86 @@ +// 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.Diagnostics; +using System.IO; +using System.Reflection.PortableExecutable; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal class MetadataCache + { + // Store 1000 entries -- arbitrary number + private const int CacheSize = 1000; + private readonly ConcurrentLruCache _metadataCache = + new ConcurrentLruCache(CacheSize, StringComparer.OrdinalIgnoreCase); + + // For testing purposes only. + internal ConcurrentLruCache Cache => _metadataCache; + + internal Metadata GetMetadata(string fullPath) + { + var timestamp = GetFileTimeStamp(fullPath); + + // Check if we have an entry in the dictionary. + if (_metadataCache.TryGetValue(fullPath, out var entry)) + { + if (timestamp.HasValue && timestamp.Value == entry.Timestamp) + { + // The file has not changed since we cached it. Return the cached entry. + return entry.Metadata; + } + else + { + // The file has changed recently. Remove the cache entry. + _metadataCache.Remove(fullPath); + } + } + + Metadata metadata; + using (var fileStream = File.OpenRead(fullPath)) + { + metadata = AssemblyMetadata.CreateFromStream(fileStream, PEStreamOptions.PrefetchMetadata); + } + + _metadataCache.GetOrAdd(fullPath, new MetadataCacheEntry(timestamp.Value, metadata)); + + return metadata; + } + + private static DateTime? GetFileTimeStamp(string fullPath) + { + try + { + Debug.Assert(Path.IsPathRooted(fullPath)); + + return File.GetLastWriteTimeUtc(fullPath); + } + catch (Exception e) + { + // There are several exceptions that can occur here: NotSupportedException or PathTooLongException + // for a bad path, UnauthorizedAccessException for access denied, etc. Rather than listing them all, + // just catch all exceptions and log. + ServerLogger.LogException(e, $"Error getting timestamp of file {fullPath}."); + + return null; + } + } + + internal struct MetadataCacheEntry + { + public MetadataCacheEntry(DateTime timestamp, Metadata metadata) + { + Debug.Assert(timestamp.Kind == DateTimeKind.Utc); + + Timestamp = timestamp; + Metadata = metadata; + } + + public DateTime Timestamp { get; } + + public Metadata Metadata { get; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Program.cs b/src/Microsoft.AspNetCore.Razor.Tools/Program.cs index 0b3b980c3a..386a8837c5 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Program.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/Program.cs @@ -3,6 +3,7 @@ using System; using System.Threading; +using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Razor.Tools { @@ -19,7 +20,12 @@ namespace Microsoft.AspNetCore.Razor.Tools var loader = new DefaultExtensionAssemblyLoader(baseDirectory: null); var checker = new DefaultExtensionDependencyChecker(loader, Console.Error); - var application = new Application(cancel.Token, loader, checker); + var application = new Application( + cancel.Token, + loader, + checker, + (path, properties) => MetadataReference.CreateFromFile(path, properties)); + return application.Execute(args); } } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ServerCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/ServerCommand.cs index f44487ab25..65a70b51ed 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/ServerCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/ServerCommand.cs @@ -57,6 +57,7 @@ namespace Microsoft.AspNetCore.Razor.Tools } var host = ConnectionHost.Create(Pipe.Value()); + var compilerHost = CompilerHost.Create(); ExecuteServerCore(host, compilerHost, Cancelled, eventBus: null, keepAlive: keepAlive); } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/ServerRequest.cs b/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/ServerRequest.cs index e1dd9308b0..34b6a39eb9 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/ServerRequest.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/ServerRequest.cs @@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Razor.Tools /// /// Write a Request to the stream. /// - public async Task WriteAsync(Stream outStream, CancellationToken cancellationToken = default) + public async Task WriteAsync(Stream outStream, CancellationToken cancellationToken = default(CancellationToken)) { using (var memoryStream = new MemoryStream()) using (var writer = new BinaryWriter(memoryStream, Encoding.Unicode)) diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerIntegrationTest.cs index 570deb91aa..4c54a0cb11 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerIntegrationTest.cs @@ -1,7 +1,9 @@ // 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.IO; using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests @@ -20,6 +22,12 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests public Task Build_SimpleMvc_WithServer_UsingDotnetMSBuild_CanBuildSuccessfully() => Build_SimpleMvc_CanBuildSuccessfully(MSBuildProcessKind.Dotnet); + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] + [InitializeTestProject("SimpleMvc")] + public Task Build_SimpleMvc_WithServer_UsingDesktopMSBuild_CanBuildSuccessfully() + => Build_SimpleMvc_CanBuildSuccessfully(MSBuildProcessKind.Desktop); + private async Task Build_SimpleMvc_CanBuildSuccessfully(MSBuildProcessKind msBuildProcessKind) { var result = await DotnetMSBuild( @@ -32,6 +40,17 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.FileExists(result, OutputPath, "SimpleMvc.pdb"); Assert.FileExists(result, OutputPath, "SimpleMvc.Views.dll"); Assert.FileExists(result, OutputPath, "SimpleMvc.Views.pdb"); + + // Verify RazorTagHelper works + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.TagHelpers.input.cache"); + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.TagHelpers.output.cache"); + Assert.FileContains( + result, + Path.Combine(IntermediateOutputPath, "SimpleMvc.TagHelpers.output.cache"), + @"""Name"":""SimpleMvc.SimpleTagHelper"""); + + // Verify RazorGenerate works + Assert.FileCountEquals(result, 8, RazorIntermediateOutputPath, "*.cs"); } [Fact] diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs index d2d0aa98e5..a628f5224a 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Threading; using Microsoft.AspNetCore.Razor.Tools; +using Microsoft.CodeAnalysis; using Moq; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests @@ -35,7 +36,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests throw new TimeoutException($"Shutting down the build server at pipe {PipeName} took longer than expected."); }); - var application = new Application(cts.Token, Mock.Of(), Mock.Of()); + var application = new Application(cts.Token, Mock.Of(), Mock.Of(), (path, properties) => Mock.Of()); var exitCode = application.Execute("shutdown", "-w", "-p", PipeName); if (exitCode != 0) { diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/ConcurrentLruCacheTest.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/ConcurrentLruCacheTest.cs new file mode 100644 index 0000000000..77ad2f0cf4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/ConcurrentLruCacheTest.cs @@ -0,0 +1,120 @@ +// 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.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + public class ConcurrentLruCacheTest + { + [Fact] + public void ConcurrentLruCache_HoldsCapacity() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var expected = input.Reverse(); + + // Act + var cache = new ConcurrentLruCache(input); + + // Assert + Assert.Equal(expected, cache.TestingEnumerable); + } + + [Fact] + public void Add_ThrowsIfKeyExists() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var cache = new ConcurrentLruCache(input); + + // Act & Assert + var exception = Assert.Throws(() => cache.Add(1, 1)); + Assert.StartsWith("Key already exists", exception.Message); + } + + [Fact] + public void GetOrAdd_AddsIfKeyDoesNotExist() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var expected = GetKeyValueArray(Enumerable.Range(2, 3)).Reverse(); + var cache = new ConcurrentLruCache(input); + + // Act + cache.GetOrAdd(4, 4); + + // Assert + Assert.Equal(expected, cache.TestingEnumerable); + } + + [Fact] + public void Remove_RemovesEntry() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var expected = GetKeyValueArray(Enumerable.Range(1, 2)).Reverse(); + var cache = new ConcurrentLruCache(input); + + // Act + var result = cache.Remove(3); + + // Assert + Assert.True(result); + Assert.Equal(expected, cache.TestingEnumerable); + } + + [Fact] + public void Remove_KeyNotFound_ReturnsFalse() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var cache = new ConcurrentLruCache(input); + + // Act + var result = cache.Remove(4); + + // Assert + Assert.False(result); + } + + [Fact] + public void Add_NoRead_EvictsLastNode() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var expected = GetKeyValueArray(Enumerable.Range(2, 3)).Reverse(); + var cache = new ConcurrentLruCache(input); + + // Act + cache.Add(4, 4); + + // Assert + Assert.Equal(expected, cache.TestingEnumerable); + } + + [Fact] + public void Add_ReadLastNode_EvictsSecondOldestNode() + { + // Arrange + var input = GetKeyValueArray(Enumerable.Range(1, 3)); + var expected = GetKeyValueArray(new int[] { 4, 1, 3 }); + var cache = new ConcurrentLruCache(input); + + // Act + cache.GetOrAdd(1, 1); // Read to make this MRU + cache.Add(4, 4); // Add a new node + + // Assert + Assert.Equal(expected, cache.TestingEnumerable); + } + + private KeyValuePair[] GetKeyValueArray(IEnumerable inputArray) + { + return inputArray.Select(v => new KeyValuePair(v, v)).ToArray(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs index b91723c9d0..e8b30514f9 100644 --- a/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis; using Moq; namespace Microsoft.AspNetCore.Razor.Tools @@ -117,7 +118,7 @@ namespace Microsoft.AspNetCore.Razor.Tools CancellationToken ct, EventBus eventBus, TimeSpan? keepAlive) - : base(new Application(ct, Mock.Of(), Mock.Of())) + : base(new Application(ct, Mock.Of(), Mock.Of(), (path, properties) => Mock.Of())) { _host = host; _compilerHost = compilerHost; diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/MetadataCacheTest.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/MetadataCacheTest.cs new file mode 100644 index 0000000000..fd4e54197c --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/MetadataCacheTest.cs @@ -0,0 +1,103 @@ +// 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.IO; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + public class MetadataCacheTest + { + [Fact] + public void GetMetadata_AddsToCache() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var metadataCache = new MetadataCache(); + var assemblyFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + // Act + var result = metadataCache.GetMetadata(assemblyFilePath); + + // Assert + Assert.NotNull(result); + Assert.Equal(1, metadataCache.Cache.Count); + } + } + + [Fact] + public void GetMetadata_UsesCache() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var metadataCache = new MetadataCache(); + var assemblyFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + // Act 1 + var result = metadataCache.GetMetadata(assemblyFilePath); + + // Assert 1 + Assert.NotNull(result); + Assert.Equal(1, metadataCache.Cache.Count); + + // Act 2 + var cacheResult = metadataCache.GetMetadata(assemblyFilePath); + + // Assert 2 + Assert.Same(result, cacheResult); + Assert.Equal(1, metadataCache.Cache.Count); + } + } + + [Fact] + public void GetMetadata_MultipleFiles_ReturnsDifferentResultsAndAddsToCache() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var metadataCache = new MetadataCache(); + var assemblyFilePath1 = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + var assemblyFilePath2 = LoaderTestResources.Gamma.WriteToFile(directory.DirectoryPath, "Gamma.dll"); + + // Act + var result1 = metadataCache.GetMetadata(assemblyFilePath1); + var result2 = metadataCache.GetMetadata(assemblyFilePath2); + + // Assert + Assert.NotSame(result1, result2); + Assert.Equal(2, metadataCache.Cache.Count); + } + } + + [Fact] + public void GetMetadata_ReplacesCache_IfFileTimestampChanged() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var metadataCache = new MetadataCache(); + var assemblyFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + // Act 1 + var result = metadataCache.GetMetadata(assemblyFilePath); + + // Assert 1 + Assert.NotNull(result); + var entry = Assert.Single(metadataCache.Cache.TestingEnumerable); + Assert.Same(result, entry.Value.Metadata); + + // Act 2 + // Update the timestamp of the file + File.SetLastWriteTimeUtc(assemblyFilePath, File.GetLastWriteTimeUtc(assemblyFilePath).AddSeconds(1)); + var cacheResult = metadataCache.GetMetadata(assemblyFilePath); + + // Assert 2 + Assert.NotSame(result, cacheResult); + entry = Assert.Single(metadataCache.Cache.TestingEnumerable); + Assert.Same(cacheResult, entry.Value.Metadata); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/ServerLifecycleTest.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/ServerLifecycleTest.cs index 27dd7e619f..7cb6b64ae0 100644 --- a/test/Microsoft.AspNetCore.Razor.Tools.Test/ServerLifecycleTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/ServerLifecycleTest.cs @@ -116,7 +116,8 @@ namespace Microsoft.AspNetCore.Razor.Tools /// A shutdown request should not abort an existing compilation. It should be allowed to run to /// completion. /// - [Fact(Skip = "Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991")] + [ConditionalFact(Skip = "Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991")] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] public async Task ServerRunning_ShutdownRequest_DoesNotAbortCompilation() { // Arrange @@ -152,7 +153,8 @@ namespace Microsoft.AspNetCore.Razor.Tools /// /// Multiple clients should be able to send shutdown requests to the server. /// - [Fact(Skip = "Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991")] + [ConditionalFact(Skip = "Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991")] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] public async Task ServerRunning_MultipleShutdownRequests_HandlesSuccessfully() { // Arrange @@ -189,7 +191,8 @@ namespace Microsoft.AspNetCore.Razor.Tools } } - [Fact(Skip = "Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991")] + [ConditionalFact(Skip = "Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991")] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] public async Task ServerRunning_CancelCompilation_CancelsSuccessfully() { // Arrange