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.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Tools
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
{
|
{
|
||||||
internal class Application : CommandLineApplication
|
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;
|
CancellationToken = cancellationToken;
|
||||||
Checker = checker;
|
Checker = checker;
|
||||||
Loader = loader;
|
Loader = loader;
|
||||||
|
AssemblyReferenceProvider = assemblyReferenceProvider;
|
||||||
|
|
||||||
Name = "rzc";
|
Name = "rzc";
|
||||||
FullName = "Microsoft ASP.NET Core Razor CLI tool";
|
FullName = "Microsoft ASP.NET Core Razor CLI tool";
|
||||||
|
|
@ -37,6 +39,8 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
|
||||||
public ExtensionDependencyChecker Checker { get; }
|
public ExtensionDependencyChecker Checker { get; }
|
||||||
|
|
||||||
|
public Func<string, MetadataReferenceProperties, PortableExecutableReference> AssemblyReferenceProvider { get; }
|
||||||
|
|
||||||
public new int Execute(params string[] args)
|
public new int Execute(params string[] args)
|
||||||
{
|
{
|
||||||
try
|
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.
|
// 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.
|
// 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.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Tools
|
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
|
// consistently reject a request that doesn't specify everything it needs. Otherwise the request
|
||||||
// could succeed sometimes if it relies on transient state.
|
// could succeed sometimes if it relies on transient state.
|
||||||
Loader = new DefaultExtensionAssemblyLoader(Path.Combine(Path.GetTempPath(), "Razor-Server"));
|
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 ExtensionAssemblyLoader Loader { get; }
|
||||||
|
|
||||||
public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken)
|
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 writer = ServerLogger.IsLoggingEnabled ? new StringWriter() : TextWriter.Null;
|
||||||
|
|
||||||
var checker = new DefaultExtensionDependencyChecker(Loader, writer);
|
var checker = new DefaultExtensionDependencyChecker(Loader, writer);
|
||||||
var app = new Application(cancellationToken, Loader, checker)
|
var app = new Application(cancellationToken, Loader, checker, AssemblyReferenceProvider)
|
||||||
{
|
{
|
||||||
Out = writer,
|
Out = writer,
|
||||||
Error = 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];
|
var metadataReferences = new MetadataReference[assemblies.Length];
|
||||||
for (var i = 0; i < assemblies.Length; i++)
|
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 =>
|
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;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Tools
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -19,7 +20,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
var loader = new DefaultExtensionAssemblyLoader(baseDirectory: null);
|
var loader = new DefaultExtensionAssemblyLoader(baseDirectory: null);
|
||||||
var checker = new DefaultExtensionDependencyChecker(loader, Console.Error);
|
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);
|
return application.Execute(args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
}
|
}
|
||||||
|
|
||||||
var host = ConnectionHost.Create(Pipe.Value());
|
var host = ConnectionHost.Create(Pipe.Value());
|
||||||
|
|
||||||
var compilerHost = CompilerHost.Create();
|
var compilerHost = CompilerHost.Create();
|
||||||
ExecuteServerCore(host, compilerHost, Cancelled, eventBus: null, keepAlive: keepAlive);
|
ExecuteServerCore(host, compilerHost, Cancelled, eventBus: null, keepAlive: keepAlive);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Write a Request to the stream.
|
/// Write a Request to the stream.
|
||||||
/// </summary>
|
/// </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 memoryStream = new MemoryStream())
|
||||||
using (var writer = new BinaryWriter(memoryStream, Encoding.Unicode))
|
using (var writer = new BinaryWriter(memoryStream, Encoding.Unicode))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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 System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
||||||
|
|
@ -20,6 +22,12 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
||||||
public Task Build_SimpleMvc_WithServer_UsingDotnetMSBuild_CanBuildSuccessfully()
|
public Task Build_SimpleMvc_WithServer_UsingDotnetMSBuild_CanBuildSuccessfully()
|
||||||
=> Build_SimpleMvc_CanBuildSuccessfully(MSBuildProcessKind.Dotnet);
|
=> 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)
|
private async Task Build_SimpleMvc_CanBuildSuccessfully(MSBuildProcessKind msBuildProcessKind)
|
||||||
{
|
{
|
||||||
var result = await DotnetMSBuild(
|
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.pdb");
|
||||||
Assert.FileExists(result, OutputPath, "SimpleMvc.Views.dll");
|
Assert.FileExists(result, OutputPath, "SimpleMvc.Views.dll");
|
||||||
Assert.FileExists(result, OutputPath, "SimpleMvc.Views.pdb");
|
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]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.Razor.Tools;
|
using Microsoft.AspNetCore.Razor.Tools;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
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.");
|
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);
|
var exitCode = application.Execute("shutdown", "-w", "-p", PipeName);
|
||||||
if (exitCode != 0)
|
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.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Tools
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
|
@ -117,7 +118,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
EventBus eventBus,
|
EventBus eventBus,
|
||||||
TimeSpan? keepAlive)
|
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;
|
_host = host;
|
||||||
_compilerHost = compilerHost;
|
_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
|
/// A shutdown request should not abort an existing compilation. It should be allowed to run to
|
||||||
/// completion.
|
/// completion.
|
||||||
/// </summary>
|
/// </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()
|
public async Task ServerRunning_ShutdownRequest_DoesNotAbortCompilation()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -152,7 +153,8 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Multiple clients should be able to send shutdown requests to the server.
|
/// Multiple clients should be able to send shutdown requests to the server.
|
||||||
/// </summary>
|
/// </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()
|
public async Task ServerRunning_MultipleShutdownRequests_HandlesSuccessfully()
|
||||||
{
|
{
|
||||||
// Arrange
|
// 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()
|
public async Task ServerRunning_CancelCompilation_CancelsSuccessfully()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue