diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ForegroundDispatcher.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ForegroundDispatcher.cs index e9f9ca61eb..7f7dac2130 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ForegroundDispatcher.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ForegroundDispatcher.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; namespace Microsoft.CodeAnalysis.Razor @@ -11,6 +12,10 @@ namespace Microsoft.CodeAnalysis.Razor { public abstract bool IsForegroundThread { get; } + public abstract TaskScheduler ForegroundScheduler { get; } + + public abstract TaskScheduler BackgroundScheduler { get; } + public virtual void AssertForegroundThread([CallerMemberName] string caller = null) { if (!IsForegroundThread) diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioForegroundDispatcher.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioForegroundDispatcher.cs index 72e27447a5..e0f8400e27 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioForegroundDispatcher.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioForegroundDispatcher.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Composition; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Razor; using Microsoft.VisualStudio.Shell; @@ -12,6 +13,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor [ExportWorkspaceService(typeof(ForegroundDispatcher), ServiceLayer.Host)] internal class VisualStudioForegroundDispatcher : ForegroundDispatcher { + public override TaskScheduler BackgroundScheduler { get; } = TaskScheduler.Default; + + public override TaskScheduler ForegroundScheduler { get; } = TaskScheduler.FromCurrentSynchronizationContext(); + public override bool IsForegroundThread => ThreadHelper.CheckAccess(); } } diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ForegroundDispatcherTestBase.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ForegroundDispatcherTestBase.cs deleted file mode 100644 index 3c7d626984..0000000000 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ForegroundDispatcherTestBase.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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.Threading; -using Microsoft.CodeAnalysis.Razor; - -namespace Microsoft.VisualStudio.LanguageServices.Razor -{ - public abstract class ForegroundDispatcherTestBase - { - internal ForegroundDispatcher Dispatcher { get; } = new SingleThreadedForegroundDispatcher(); - - private class SingleThreadedForegroundDispatcher : ForegroundDispatcher - { - private Thread Thread { get; } = Thread.CurrentThread; - - public override bool IsForegroundThread => Thread.CurrentThread == Thread; - } - } -} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj index 18d2b92a3a..29cf10f6e8 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj @@ -13,6 +13,10 @@ + + + + diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundDispatcherTestBase.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundDispatcherTestBase.cs new file mode 100644 index 0000000000..226b0b2211 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundDispatcherTestBase.cs @@ -0,0 +1,52 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor; + +namespace Xunit +{ + public abstract class ForegroundDispatcherTestBase + { + internal ForegroundDispatcher Dispatcher { get; } = new SingleThreadedForegroundDispatcher(); + + private class SingleThreadedForegroundDispatcher : ForegroundDispatcher + { + public SingleThreadedForegroundDispatcher() + { + ForegroundScheduler = SynchronizationContext.Current == null ? new ThrowingTaskScheduler() : TaskScheduler.FromCurrentSynchronizationContext(); + BackgroundScheduler = TaskScheduler.Default; + } + + public override TaskScheduler ForegroundScheduler { get; } + + public override TaskScheduler BackgroundScheduler { get; } + + private Thread Thread { get; } = Thread.CurrentThread; + + public override bool IsForegroundThread => Thread.CurrentThread == Thread; + } + + private class ThrowingTaskScheduler : TaskScheduler + { + protected override IEnumerable GetScheduledTasks() + { + return Enumerable.Empty(); + } + + protected override void QueueTask(Task task) + { + throw new InvalidOperationException("Use [ForegroundFactAttribute]"); + } + + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + throw new InvalidOperationException("Use [ForegroundFactAttribute]"); + } + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactAttribute.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactAttribute.cs new file mode 100644 index 0000000000..e111c5b330 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactAttribute.cs @@ -0,0 +1,16 @@ +// 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 Xunit; +using Xunit.Sdk; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Test +{ + // Similar to WpfFactAttribute https://github.com/xunit/samples.xunit/blob/969d9f7e887836f01a6c525324bf3db55658c28f/STAExamples/WpfFactAttribute.cs + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + [XunitTestCaseDiscoverer(nameof(ForegroundFactAttribute), nameof(Microsoft.VisualStudio.LanguageServices.Razor))] + internal class ForegroundFactAttribute : FactAttribute + { + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactDiscoverer.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactDiscoverer.cs new file mode 100644 index 0000000000..02b0e0c383 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactDiscoverer.cs @@ -0,0 +1,117 @@ +// 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.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Xunit +{ + internal class ForegroundFactTestCase : LongLivedMarshalByRefObject, IXunitTestCase + { + private IXunitTestCase _inner; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Called by the de-serializer", error: true)] + public ForegroundFactTestCase() + { + } + + public ForegroundFactTestCase(IXunitTestCase testCase) + { + _inner = testCase; + } + + public string DisplayName => _inner.DisplayName; + + public IMethodInfo Method => _inner.Method; + + public string SkipReason => _inner.SkipReason; + + public ISourceInformation SourceInformation + { + get => _inner.SourceInformation; + set => _inner.SourceInformation = value; + } + + public ITestMethod TestMethod => _inner.TestMethod; + + public object[] TestMethodArguments => _inner.TestMethodArguments; + + public Dictionary> Traits => _inner.Traits; + + public string UniqueID => _inner.UniqueID; + + public void Deserialize(IXunitSerializationInfo info) + { + _inner = info.GetValue("InnerTestCase"); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue("InnerTestCase", _inner); + } + + public Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + var tcs = new TaskCompletionSource(); + var thread = new Thread(() => + { + try + { + SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext()); + + var worker = _inner.RunAsync(diagnosticMessageSink, messageBus, constructorArguments, aggregator, cancellationTokenSource); + + Exception caught = null; + var frame = new DispatcherFrame(); + Task.Run(async () => + { + try + { + await worker; + } + catch (Exception ex) + { + caught = ex; + } + finally + { + frame.Continue = false; + } + }); + + Dispatcher.PushFrame(frame); + + if (caught == null) + { + tcs.SetException(caught); + } + else + { + tcs.SetResult(worker.Result); + } + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return tcs.Task; + } + } +}