-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Add unit tests for gladstone extension handling service. #78034
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6d80411
f960333
90f09ee
a878668
4450466
b1991b2
25271ad
e79b2bc
9ea59ff
51cd35f
bd416cf
cdebbcd
c29d4ae
0cf7021
72043df
9e37ffe
6266869
77b5108
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,7 @@ private sealed class ExtensionFolder | |
/// <summary> | ||
/// Lazily computed assembly loader for this particular folder. | ||
/// </summary> | ||
private readonly AsyncLazy<(IAnalyzerAssemblyLoaderInternal? assemblyLoader, Exception? extensionException)> _lazyAssemblyLoader; | ||
private readonly AsyncLazy<(IExtensionAssemblyLoader? assemblyLoader, Exception? extensionException)> _lazyAssemblyLoader; | ||
|
||
/// <summary> | ||
/// Mapping from assembly file path to the handlers it contains. Should only be mutated while the <see | ||
|
@@ -43,52 +43,16 @@ public ExtensionFolder( | |
_extensionMessageHandlerService = extensionMessageHandlerService; | ||
_lazyAssemblyLoader = AsyncLazy.Create(cancellationToken => | ||
{ | ||
#if NET | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. moved to the default impl of the extension loading service. |
||
// These lines should always succeed. If they don't, they indicate a bug in our code that we want | ||
// to bubble out as it must be fixed. | ||
var analyzerAssemblyLoaderProvider = _extensionMessageHandlerService._solutionServices.GetRequiredService<IAnalyzerAssemblyLoaderProvider>(); | ||
var analyzerAssemblyLoader = analyzerAssemblyLoaderProvider.CreateNewShadowCopyLoader(); | ||
|
||
// Catch exceptions here related to working with the file system. If we can't properly enumerate, | ||
// we want to report that back to the client, while not blocking the entire extension service. | ||
try | ||
{ | ||
// Allow this assembly loader to load any dll in assemblyFolderPath. | ||
foreach (var dll in Directory.EnumerateFiles(assemblyFolderPath, "*.dll")) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
try | ||
{ | ||
// Check if the file is a valid .NET assembly. | ||
AssemblyName.GetAssemblyName(dll); | ||
} | ||
catch | ||
{ | ||
// The file is not a valid .NET assembly, skip it. | ||
continue; | ||
} | ||
|
||
analyzerAssemblyLoader.AddDependencyLocation(dll); | ||
} | ||
|
||
return ((IAnalyzerAssemblyLoaderInternal?)analyzerAssemblyLoader, (Exception?)null); | ||
} | ||
catch (Exception ex) when (ex is not OperationCanceledException) | ||
{ | ||
// Capture any exceptions here to be reported back in CreateAssemblyHandlersAsync. | ||
return (null, ex); | ||
} | ||
#else | ||
return ((IAnalyzerAssemblyLoaderInternal?)null, (Exception?)null); | ||
#endif | ||
var analyzerAssemblyLoaderProvider = _extensionMessageHandlerService._solutionServices.GetRequiredService<IExtensionAssemblyLoaderProvider>(); | ||
return analyzerAssemblyLoaderProvider.CreateNewShadowCopyLoader(assemblyFolderPath, cancellationToken); | ||
}); | ||
} | ||
|
||
public void Unload() | ||
{ | ||
// Only if we've created the assembly loader do we need to do anything. | ||
_lazyAssemblyLoader.TryGetValue(out var tuple); | ||
tuple.assemblyLoader?.Dispose(); | ||
tuple.assemblyLoader?.Unload(); | ||
} | ||
|
||
private async Task<AssemblyMessageHandlers> CreateAssemblyHandlersAsync( | ||
|
@@ -109,8 +73,14 @@ private async Task<AssemblyMessageHandlers> CreateAssemblyHandlersAsync( | |
} | ||
|
||
var assembly = analyzerAssemblyLoader.LoadFromPath(assemblyFilePath); | ||
var factory = _extensionMessageHandlerService._customMessageHandlerFactory; | ||
Contract.ThrowIfNull(factory); | ||
var factory = _extensionMessageHandlerService._solutionServices.GetService<IExtensionMessageHandlerFactory>(); | ||
if (factory is null) | ||
{ | ||
return new( | ||
DocumentMessageHandlers: ImmutableDictionary<string, IExtensionMessageHandlerWrapper>.Empty, | ||
WorkspaceMessageHandlers: ImmutableDictionary<string, IExtensionMessageHandlerWrapper>.Empty, | ||
ExtensionException: null); | ||
} | ||
|
||
// We're calling into code here to analyze the assembly at the specified file and to create handlers we | ||
// find within it. If this throws, then we will capture that exception and return it to the caller to | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
using System.Composition; | ||
using System.IO; | ||
using System.Reflection; | ||
using System.Threading; | ||
using Microsoft.CodeAnalysis.Host; | ||
using Microsoft.CodeAnalysis.Host.Mef; | ||
|
||
namespace Microsoft.CodeAnalysis.Extensions; | ||
|
||
/// <summary> | ||
/// Abstraction around <see cref="IAnalyzerAssemblyLoaderProvider"/> so that we can mock that behavior in tests. | ||
/// </summary> | ||
internal interface IExtensionAssemblyLoaderProvider : IWorkspaceService | ||
{ | ||
(IExtensionAssemblyLoader? assemblyLoader, Exception? extensionException) CreateNewShadowCopyLoader( | ||
string assemblyFolderPath, CancellationToken cancellationToken); | ||
} | ||
|
||
/// <summary> | ||
/// Abstraction around <see cref="IAnalyzerAssemblyLoader"/> so that we can mock that behavior in tests. | ||
/// </summary> | ||
internal interface IExtensionAssemblyLoader | ||
{ | ||
Assembly LoadFromPath(string assemblyFilePath); | ||
void Unload(); | ||
} | ||
|
||
[ExportWorkspaceServiceFactory(typeof(IExtensionAssemblyLoaderProvider)), Shared] | ||
[method: ImportingConstructor] | ||
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] | ||
internal sealed class DefaultExtensionAssemblyLoaderProviderFactory() : IWorkspaceServiceFactory | ||
{ | ||
public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) | ||
=> new DefaultExtensionAssemblyLoaderProvider(workspaceServices); | ||
|
||
private sealed class DefaultExtensionAssemblyLoaderProvider(HostWorkspaceServices workspaceServices) | ||
: IExtensionAssemblyLoaderProvider | ||
{ | ||
#pragma warning disable IDE0052 // Remove unread private members | ||
private readonly HostWorkspaceServices _workspaceServices = workspaceServices; | ||
#pragma warning restore IDE0052 // Remove unread private members | ||
|
||
public (IExtensionAssemblyLoader? assemblyLoader, Exception? extensionException) CreateNewShadowCopyLoader( | ||
string assemblyFolderPath, CancellationToken cancellationToken) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
|
||
#if NET | ||
// These lines should always succeed. If they don't, they indicate a bug in our code that we want | ||
// to bubble out as it must be fixed. | ||
var analyzerAssemblyLoaderProvider = _workspaceServices.GetRequiredService<IAnalyzerAssemblyLoaderProvider>(); | ||
var analyzerAssemblyLoader = analyzerAssemblyLoaderProvider.CreateNewShadowCopyLoader(); | ||
|
||
// Catch exceptions here related to working with the file system. If we can't properly enumerate, | ||
// we want to report that back to the client, while not blocking the entire extension service. | ||
try | ||
{ | ||
// Allow this assembly loader to load any dll in assemblyFolderPath. | ||
foreach (var dll in Directory.EnumerateFiles(assemblyFolderPath, "*.dll")) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
try | ||
{ | ||
// Check if the file is a valid .NET assembly. | ||
AssemblyName.GetAssemblyName(dll); | ||
} | ||
catch | ||
{ | ||
// The file is not a valid .NET assembly, skip it. | ||
continue; | ||
} | ||
|
||
analyzerAssemblyLoader.AddDependencyLocation(dll); | ||
} | ||
|
||
return (new DefaultExtensionAssemblyLoader(analyzerAssemblyLoader), null); | ||
} | ||
catch (Exception ex) when (ex is not OperationCanceledException) | ||
{ | ||
// Capture any exceptions here to be reported back in CreateAssemblyHandlersAsync. | ||
return (null, ex); | ||
} | ||
#else | ||
return default; | ||
#endif | ||
} | ||
} | ||
|
||
private sealed class DefaultExtensionAssemblyLoader( | ||
IAnalyzerAssemblyLoaderInternal assemblyLoader) : IExtensionAssemblyLoader | ||
{ | ||
public void Unload() => assemblyLoader.Dispose(); | ||
|
||
public Assembly LoadFromPath(string assemblyFilePath) | ||
=> assemblyLoader.LoadFromPath(assemblyFilePath); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,13 +5,14 @@ | |
using System.Collections.Immutable; | ||
using System.Reflection; | ||
using System.Threading; | ||
using Microsoft.CodeAnalysis.Host; | ||
|
||
namespace Microsoft.CodeAnalysis.Extensions; | ||
|
||
/// <summary> | ||
/// Factory for creating instances of extension message handlers. | ||
/// </summary> | ||
internal interface IExtensionMessageHandlerFactory | ||
internal interface IExtensionMessageHandlerFactory : IWorkspaceService | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. became a workspace service so we can override it in tests with our own version ot mock out exceptions it might through without actually loading assemblies and using reflection on them. |
||
{ | ||
/// <summary> | ||
/// Creates <see cref="IExtensionMessageHandlerWrapper{Solution}"/> instances for each | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
had to abstract out this type so we could test the varying things it could do with mocks.