-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use non-LSP Roslyn code via package ref instead of source
Some of the LSP code depends on Roslyn workspaces etc. but importing these as source got messy fast due to the web of dependencies. Instead, import them as package references and hope we don't run into any issues with lack of internals visibility.
- Loading branch information
Showing
16 changed files
with
393 additions
and
1,481 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
263 changes: 263 additions & 0 deletions
263
MSBuildLanguageServer.Tests/Import/UseExportProviderAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
// modified copy of | ||
// https://raw.githubusercontent.com/dotnet/roslyn/1d8582e04c5af87883eab0a5ba7d0788d19a3eb4/src/Workspaces/CoreTestUtilities/MEF/UseExportProviderAttribute.cs | ||
// portions commented out using /* */ comments | ||
// and other changes annotated inline with // MODIFICATION | ||
|
||
// 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.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Composition.Hosting; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
using System.Text; | ||
using System.Threading; | ||
using Microsoft.CodeAnalysis.Host; | ||
using Microsoft.CodeAnalysis.Host.Mef; | ||
using Microsoft.CodeAnalysis.Remote; | ||
using Microsoft.CodeAnalysis.Shared.TestHooks; | ||
using Microsoft.VisualStudio.Composition; | ||
using Roslyn.Test.Utilities; | ||
using Xunit.Sdk; | ||
|
||
namespace Microsoft.CodeAnalysis.Test.Utilities | ||
{ | ||
/// <summary> | ||
/// This attribute supports tests that need to use a MEF container (<see cref="ExportProvider"/>) directly or | ||
/// indirectly during the test sequence. It ensures production code uniformly handles the export provider created | ||
/// during a test, and cleans up the state before the test completes. | ||
/// </summary> | ||
/// <remarks> | ||
/// <para>This attribute serves several important functions for tests that use state variables which are otherwise | ||
/// shared at runtime:</para> | ||
/// <list type="bullet"> | ||
/// <item>Ensures <see cref="HostServices"/> implementations all use the same <see cref="ExportProvider"/>, which is | ||
/// the one created by the test.</item> | ||
/// <item>Clears static cached values in production code holding instances of <see cref="HostServices"/>, or any | ||
/// object obtained from it or one of its related interfaces such as <see cref="HostLanguageServices"/>.</item> | ||
/// <item>Isolates tests by waiting for asynchronous operations to complete before a test is considered | ||
/// complete.</item> | ||
/// <item>When required, provides a separate <see cref="ExportProvider"/> for the <see cref="RemoteWorkspace"/> | ||
/// executing in the test process. If this provider is created during testing, it is cleaned up with the primary | ||
/// export provider during test teardown.</item> | ||
/// </list> | ||
/// </remarks> | ||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | ||
public class UseExportProviderAttribute : BeforeAfterTestAttribute | ||
{ | ||
/// <summary> | ||
/// Asynchronous operations are expected to be cancelled at the end of the test that started them. Operations | ||
/// cancelled by the test are cleaned up immediately. The remaining operations are given an opportunity to run | ||
/// to completion. If this timeout is exceeded by the asynchronous operations running after a test completes, | ||
/// the test is failed. | ||
/// </summary> | ||
private static readonly TimeSpan CleanupTimeout = TimeSpan.FromMinutes(1); | ||
|
||
private MefHostServices? _hostServices; | ||
|
||
static UseExportProviderAttribute() | ||
{ | ||
// Make sure we run the module initializer for Roslyn.Test.Utilities. C# projects do this via a | ||
// build-injected module initializer, but VB projects can ensure initialization occurs by applying the | ||
// UseExportProviderAttribute to test classes that rely on it. | ||
RuntimeHelpers.RunModuleConstructor(typeof(TestBase).Module.ModuleHandle); | ||
|
||
testHookMethod = typeof(MefHostServices) | ||
.GetNestedType("TestAccessor", BindingFlags.NonPublic) | ||
?.GetMethod("TestAccessor", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static) | ||
?? throw new InvalidOperationException("Could not get test accessor method for MefHostServices.TestAccessor.HookServiceCreation"); | ||
} | ||
|
||
static MethodInfo testHookMethod; | ||
|
||
static void HookServiceCreationViaReflection(Func<IEnumerable<Assembly>, MefHostServices> hook) | ||
{ | ||
testHookMethod.Invoke(null, [hook]); | ||
} | ||
|
||
public override void Before(MethodInfo? methodUnderTest) | ||
{ | ||
// Need to clear cached MefHostServices between test runs. | ||
// MODIFICATION: use reflection helper to access internal method | ||
HookServiceCreationViaReflection(CreateMefHostServices); | ||
|
||
// make sure we enable this for all unit tests | ||
AsynchronousOperationListenerProvider.Enable(enable: true, diagnostics: true); | ||
ExportProviderCache.SetEnabled_OnlyUseExportProviderAttributeCanCall(true); | ||
} | ||
|
||
/// <summary> | ||
/// To the extent reasonably possible, this method resets the state of the test environment to the same state as | ||
/// it started, ensuring that tests running in sequence cannot influence the outcome of later tests. | ||
/// </summary> | ||
/// <remarks> | ||
/// <para>The test cleanup runs in two primary steps:</para> | ||
/// <list type="number"> | ||
/// <item>Waiting for asynchronous operations started by the test to complete.</item> | ||
/// <item>Disposing of mutable resources created by the test.</item> | ||
/// <item>Clearing static state variables related to the use of MEF during a test.</item> | ||
/// </list> | ||
/// </remarks> | ||
public override void After(MethodInfo? methodUnderTest) | ||
{ | ||
try | ||
{ | ||
DisposeExportProvider(ExportProviderCache.LocalExportProviderForCleanup); | ||
DisposeExportProvider(ExportProviderCache.RemoteExportProviderForCleanup); | ||
} | ||
finally | ||
{ | ||
// Replace hooks with ones that always throw exceptions. These hooks detect cases where code executing | ||
// after the end of a test attempts to create an ExportProvider. | ||
// MODIFICATION: use reflection helper to access internal method | ||
HookServiceCreationViaReflection(DenyMefHostServicesCreationBetweenTests); | ||
|
||
// Reset static state variables. | ||
_hostServices = null; | ||
ExportProviderCache.SetEnabled_OnlyUseExportProviderAttributeCanCall(false); | ||
} | ||
} | ||
|
||
private static void DisposeExportProvider(ExportProvider? exportProvider) | ||
{ | ||
if (exportProvider == null) | ||
{ | ||
return; | ||
} | ||
|
||
// Dispose of the export provider, including calling Dispose for any IDisposable services created during the test. | ||
using var _ = exportProvider; | ||
|
||
if (exportProvider.GetExportedValues<IAsynchronousOperationListenerProvider>().SingleOrDefault() is { } listenerProvider) | ||
{ | ||
// Verify the synchronization context was not used incorrectly | ||
var testExportJoinableTaskContext = exportProvider.GetExportedValues<TestExportJoinableTaskContext>().SingleOrDefault(); | ||
var denyExecutionSynchronizationContext = testExportJoinableTaskContext?.SynchronizationContext as TestExportJoinableTaskContext.DenyExecutionSynchronizationContext; | ||
|
||
// Join remaining operations with a timeout | ||
using (var timeoutTokenSource = new CancellationTokenSource(CleanupTimeout)) | ||
{ | ||
if (denyExecutionSynchronizationContext is object) | ||
{ | ||
// Immediately cancel the test if the synchronization context is improperly used | ||
denyExecutionSynchronizationContext.InvalidSwitch += delegate { timeoutTokenSource.CancelAfter(0); }; | ||
denyExecutionSynchronizationContext.ThrowIfSwitchOccurred(); | ||
} | ||
|
||
try | ||
{ | ||
// This attribute cleans up the in-process and out-of-process export providers separately, so we | ||
// don't need to provide a workspace when waiting for operations to complete. | ||
var waiter = ((AsynchronousOperationListenerProvider)listenerProvider).WaitAllDispatcherOperationAndTasksAsync(workspace: null); | ||
|
||
if (testExportJoinableTaskContext?.DispatcherTaskJoiner is { } taskJoiner) | ||
{ | ||
taskJoiner.JoinUsingDispatcher(waiter, timeoutTokenSource.Token); | ||
} | ||
else | ||
{ | ||
waiter.GetAwaiter().GetResult(); | ||
} | ||
} | ||
catch (OperationCanceledException ex) when (timeoutTokenSource.IsCancellationRequested) | ||
{ | ||
// If the failure was caused by an invalid thread change, throw that exception | ||
denyExecutionSynchronizationContext?.ThrowIfSwitchOccurred(); | ||
|
||
var messageBuilder = new StringBuilder("Failed to clean up listeners in a timely manner."); | ||
foreach (var token in ((AsynchronousOperationListenerProvider)listenerProvider).GetTokens()) | ||
{ | ||
messageBuilder.AppendLine().Append($" {token}"); | ||
} | ||
|
||
throw new TimeoutException(messageBuilder.ToString(), ex); | ||
} | ||
} | ||
|
||
denyExecutionSynchronizationContext?.ThrowIfSwitchOccurred(); | ||
|
||
foreach (var testErrorHandler in exportProvider.GetExportedValues<ITestErrorHandler>()) | ||
{ | ||
var exceptions = testErrorHandler.Exceptions; | ||
if (exceptions.Count > 0) | ||
{ | ||
throw new AggregateException("Tests threw unexpected exceptions", exceptions); | ||
} | ||
} | ||
} | ||
} | ||
|
||
private MefHostServices CreateMefHostServices(IEnumerable<Assembly> assemblies) | ||
{ | ||
ExportProvider exportProvider; | ||
|
||
if (assemblies is ImmutableArray<Assembly> array && | ||
array == MefHostServices.DefaultAssemblies && | ||
ExportProviderCache.LocalExportProviderForCleanup != null) | ||
{ | ||
if (_hostServices != null) | ||
{ | ||
return _hostServices; | ||
} | ||
|
||
exportProvider = ExportProviderCache.LocalExportProviderForCleanup; | ||
} | ||
else | ||
{ | ||
exportProvider = ExportProviderCache.GetOrCreateExportProviderFactory(assemblies).CreateExportProvider(); | ||
} | ||
|
||
Interlocked.CompareExchange( | ||
ref _hostServices, | ||
new ExportProviderMefHostServices(exportProvider), | ||
null); | ||
|
||
return _hostServices; | ||
} | ||
|
||
private static MefHostServices DenyMefHostServicesCreationBetweenTests(IEnumerable<Assembly> assemblies) | ||
{ | ||
// If you hit this, one of three situations occurred: | ||
// | ||
// 1. A test method that uses ExportProvider is not marked with UseExportProviderAttribute (can also be | ||
// applied to the containing type or a base type. | ||
// 2. A test attempted to create an ExportProvider during the test cleanup operations after the | ||
// ExportProvider was already disposed. | ||
// 3. A test attempted to use an ExportProvider in the constructor of the test, or during the initialization | ||
// of a field in the test class. | ||
throw new InvalidOperationException("Cannot create host services after test tear down."); | ||
} | ||
|
||
private class ExportProviderMefHostServices : MefHostServices, IMefHostExportProvider | ||
{ | ||
private readonly VisualStudioMefHostServices _vsHostServices; | ||
|
||
public ExportProviderMefHostServices(ExportProvider exportProvider) | ||
: base(new ContainerConfiguration().CreateContainer()) | ||
{ | ||
_vsHostServices = VisualStudioMefHostServices.Create(exportProvider); | ||
} | ||
|
||
// MODIFICATION: made protected instead of protected internal, use reflection to call internal method | ||
protected override HostWorkspaceServices CreateWorkspaceServices(Workspace workspace) | ||
{ | ||
//=> _vsHostServices.CreateWorkspaceServices(workspace); | ||
return (HostWorkspaceServices?) _vsHostServices | ||
.GetType() | ||
.GetMethod("CreateWorkspaceServices", BindingFlags.NonPublic | BindingFlags.Instance) | ||
?.Invoke(_vsHostServices, [workspace]) | ||
?? throw new InvalidOperationException(); | ||
} | ||
|
||
IEnumerable<Lazy<TExtension, TMetadata>> IMefHostExportProvider.GetExports<TExtension, TMetadata>() | ||
=> _vsHostServices.GetExports<TExtension, TMetadata>(); | ||
|
||
IEnumerable<Lazy<TExtension>> IMefHostExportProvider.GetExports<TExtension>() | ||
=> _vsHostServices.GetExports<TExtension>(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.