Added support for AssemblyMetadata caching during build server TagHelper discovery
This commit is contained in:
parent
00485d9f1b
commit
a9dca60a10
|
|
@ -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<string, MetadataReferenceProperties, PortableExecutableReference> 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<string, MetadataReferenceProperties, PortableExecutableReference> AssemblyReferenceProvider { get; }
|
||||
|
||||
public new int Execute(params string[] args)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, MetadataReferenceProperties, PortableExecutableReference> 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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal class ConcurrentLruCache<TKey, TValue>
|
||||
{
|
||||
private readonly int _capacity;
|
||||
|
||||
private readonly Dictionary<TKey, CacheValue> _cache;
|
||||
private readonly LinkedList<TKey> _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<TKey>.Default)
|
||||
{
|
||||
}
|
||||
|
||||
public ConcurrentLruCache(int capacity, IEqualityComparer<TKey> comparer)
|
||||
{
|
||||
if (capacity <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(capacity));
|
||||
}
|
||||
_capacity = capacity;
|
||||
_cache = new Dictionary<TKey, CacheValue>(capacity, comparer);
|
||||
_nodeList = new LinkedList<TKey>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="ArgumentException"/> will be thrown.
|
||||
/// </summary>
|
||||
public ConcurrentLruCache(KeyValuePair<TKey, TValue>[] 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For testing. Very expensive.
|
||||
/// </summary>
|
||||
internal IEnumerable<KeyValuePair<TKey, TValue>> TestingEnumerable
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
foreach (var key in _nodeList)
|
||||
{
|
||||
var kvp = new KeyValuePair<TKey, TValue>(key, _cache[key].Value);
|
||||
yield return kvp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Doesn't lock.
|
||||
/// </summary>
|
||||
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<TKey> node)
|
||||
{
|
||||
if (!object.ReferenceEquals(_nodeList.First, node))
|
||||
{
|
||||
_nodeList.Remove(node);
|
||||
_nodeList.AddFirst(node);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expects non-empty cache. Does not lock.
|
||||
/// </summary>
|
||||
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<TKey>(key);
|
||||
_cache.Add(key, new CacheValue(value, node));
|
||||
_nodeList.AddFirst(node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Doesn't lock.
|
||||
/// </summary>
|
||||
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<TKey> node)
|
||||
{
|
||||
Value = value;
|
||||
Node = node;
|
||||
}
|
||||
|
||||
public TValue Value { get; }
|
||||
|
||||
public LinkedListNode<TKey> Node { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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<string, MetadataCacheEntry> _metadataCache =
|
||||
new ConcurrentLruCache<string, MetadataCacheEntry>(CacheSize, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// For testing purposes only.
|
||||
internal ConcurrentLruCache<string, MetadataCacheEntry> 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
/// <summary>
|
||||
/// Write a Request to the stream.
|
||||
/// </summary>
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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<ExtensionAssemblyLoader>(), Mock.Of<ExtensionDependencyChecker>());
|
||||
var application = new Application(cts.Token, Mock.Of<ExtensionAssemblyLoader>(), Mock.Of<ExtensionDependencyChecker>(), (path, properties) => Mock.Of<PortableExecutableReference>());
|
||||
var exitCode = application.Execute("shutdown", "-w", "-p", PipeName);
|
||||
if (exitCode != 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<int, int>(input);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, cache.TestingEnumerable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_ThrowsIfKeyExists()
|
||||
{
|
||||
// Arrange
|
||||
var input = GetKeyValueArray(Enumerable.Range(1, 3));
|
||||
var cache = new ConcurrentLruCache<int, int>(input);
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentException>(() => 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<int, int>(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<int, int>(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<int, int>(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<int, int>(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<int, int>(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<int, int>[] GetKeyValueArray(IEnumerable<int> inputArray)
|
||||
{
|
||||
return inputArray.Select(v => new KeyValuePair<int, int>(v, v)).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ExtensionAssemblyLoader>(), Mock.Of<ExtensionDependencyChecker>()))
|
||||
: base(new Application(ct, Mock.Of<ExtensionAssemblyLoader>(), Mock.Of<ExtensionDependencyChecker>(), (path, properties) => Mock.Of<PortableExecutableReference>()))
|
||||
{
|
||||
_host = host;
|
||||
_compilerHost = compilerHost;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
/// </summary>
|
||||
[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
|
|||
/// <summary>
|
||||
/// Multiple clients should be able to send shutdown requests to the server.
|
||||
/// </summary>
|
||||
[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
|
||||
|
|
|
|||
Loading…
Reference in New Issue