Skip to content

Commit

Permalink
Improve UI thread handling by leveraging JoinableTaskContext
Browse files Browse the repository at this point in the history
Before the first test is run, we retrieve the MEF-exported JTC
so we can use it when running tests.

Fixes #42
  • Loading branch information
kzu committed Aug 25, 2022
1 parent 6981f95 commit 86992cf
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 93 deletions.
4 changes: 2 additions & 2 deletions src/Xunit.Vsix/TaskExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Xunit
{
static class TaskExtensions
{
public static async Task<T> TimeoutAfter<T>(this Task<T> task, int millisecondsTimeout)
public static async Task<T> TimeoutAfterAsync<T>(this Task<T> task, int millisecondsTimeout)
{
var disableTimeout = bool.TryParse(Environment.GetEnvironmentVariable(Constants.DisableTimeoutsEnvironmentVariable), out var noTimeout) && noTimeout;
// Never timeout if a debugger is attached.
Expand All @@ -20,7 +20,7 @@ public static async Task<T> TimeoutAfter<T>(this Task<T> task, int millisecondsT
else
{
// Ignore errors on faulted but otherwise timed out test.
_ = task.ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnFaulted);
_ = task.ContinueWith(_ => { }, default, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
throw new TimeoutException(string.Format("Execution didn't complete within the required maximum {0} seconds.", millisecondsTimeout / 1000));
}
}
Expand Down
73 changes: 7 additions & 66 deletions src/Xunit.Vsix/VsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ public async Task<RunSummary> RunAsync(VsixTestCase vsixTest, IMessageBus messag
#if !DEBUG
if (Debugger.IsAttached)
{
return await RunAsyncCore(vsixTest, messageBus, aggregator);
return await RunCoreAsync(vsixTest, messageBus, aggregator);
}
#endif

using var bufferBus = new InterceptingMessageBus(messageBus);
var summary = await RunAsyncCore(vsixTest, messageBus, aggregator);
var summary = await RunCoreAsync(vsixTest, messageBus, aggregator);
var shouldRecycle = vsixTest.RecycleOnFailure.GetValueOrDefault();

// Special case for MEF cache corruption, clear cache and restart the test.
Expand All @@ -99,13 +99,13 @@ public async Task<RunSummary> RunAsync(VsixTestCase vsixTest, IMessageBus messag
{
Recycle();
aggregator.Clear();
return await RunAsyncCore(vsixTest, messageBus, aggregator);
return await RunCoreAsync(vsixTest, messageBus, aggregator);
}

return summary;
}

async Task<RunSummary> RunAsyncCore(VsixTestCase testCase, IMessageBus messageBus, ExceptionAggregator aggregator)
async Task<RunSummary> RunCoreAsync(VsixTestCase testCase, IMessageBus messageBus, ExceptionAggregator aggregator)
{
if (!EnsureConnected(testCase, messageBus))
{
Expand All @@ -128,7 +128,7 @@ async Task<RunSummary> RunAsyncCore(VsixTestCase testCase, IMessageBus messageBu
var outputBus = new TraceOutputMessageBus(remoteBus);
var summary = await Task.Run(
() => _runner.Run(testCase, outputBus))
.TimeoutAfter(testCase.TimeoutSeconds * 1000);
.TimeoutAfterAsync(testCase.TimeoutSeconds * 1000);

if (summary.Exception != null)
aggregator.Add(summary.Exception);
Expand All @@ -140,8 +140,8 @@ async Task<RunSummary> RunAsyncCore(VsixTestCase testCase, IMessageBus messageBu
if (ex is RemotingException || ex is TimeoutException)
Stop();

if (ex is RemotingException)
ex = new Exception("Connection to running IDE lost.");
if (ex is RemotingException rex)
ex = new Exception("Connection to running IDE lost: " + rex.Message, ex);

aggregator.Add(ex);
messageBus.QueueMessage(new TestFailed(xunitTest, 0, ex.Message, ex));
Expand Down Expand Up @@ -305,65 +305,6 @@ bool Start()
if (dte == null)
return false;

//var services = new OleServiceProvider(dte);
// These casts don't work on this side of the client, for some reason.
//IVsShell shell;
//while ((shell = services.GetService<SVsShell, IVsShell>()) == null)
//{
// Thread.Sleep(_settings.RetrySleepInterval);
//}

//object zombie;
//// __VSSPROPID.VSSPROPID_Zombie
//while ((int?)(zombie = shell.GetProperty(-9014, out zombie)) != 0)
//{
// Thread.Sleep(_settings.RetrySleepInterval);
//}

// Retrieve the component model service, which could also now take time depending on new
// extensions being installed or updated before the first launch.
//var components = services.GetService<Interop.SComponentModel, object>();

//if (Debugger.IsAttached)
//{
// // When attached via TD.NET, there will be an environment variable named DTE_MainWindow=2296172
// var mainWindow = Environment.GetEnvironmentVariable("DTE_MainWindow");
// if (!string.IsNullOrEmpty(mainWindow))
// {
// var attached = false;
// var retries = 0;
// var sleep = _settings.RetrySleepInterval;
// while (retries++ < _settings.DebuggerAttachRetries && !attached)
// {
// try
// {
// var mainHWnd = int.Parse(mainWindow);
// var mainDte = GetAllDtes().FirstOrDefault(x => x.MainWindow.HWnd == mainHWnd);
// if (mainDte != null)
// {
// var startedVs = mainDte.Debugger.LocalProcesses.OfType<EnvDTE.Process>().FirstOrDefault(x => x.ProcessID == Process.Id);
// if (startedVs != null)
// {
// startedVs.Attach();
// attached = true;
// break;
// }
// }
// }
// catch (Exception ex)
// {
// s_tracer.TraceEvent(TraceEventType.Warning, 0, Strings.VsClient.RetryAttach(retries, _settings.DebuggerAttachRetries) + Environment.NewLine + ex.ToString());
// }

// Thread.Sleep(sleep);
// sleep = sleep * retries;
// }

// if (!attached)
// s_tracer.TraceEvent(TraceEventType.Error, 0, Strings.VsClient.FailedToAttach(_visualStudioVersion, _rootSuffix));
// }
//}

try
{
NativeMethods.IsWow64Process(Process.Handle, out var isWow);
Expand Down
81 changes: 57 additions & 24 deletions src/Xunit.Vsix/VsRemoteRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using Xunit.Abstractions;
using Xunit.Sdk;
using Task = System.Threading.Tasks.Task;
Expand All @@ -26,7 +28,7 @@ class VsRemoteRunner : MarshalByRefObject, IVsRemoteRunner
{
string _pipeName;
IChannel _channel;
IServiceProvider _services;
JoinableTaskContext _jtc;

Dictionary<Type, object> _assemblyFixtureMappings = new Dictionary<Type, object>();
Dictionary<Type, object> _collectionFixtureMappings = new Dictionary<Type, object>();
Expand Down Expand Up @@ -56,14 +58,32 @@ public void Ping() { }

public VsixRunSummary Run(VsixTestCase testCase, IMessageBus messageBus)
{
// Before the first test is run, ensure we have initialized the global services
// which in turn requests the component model which ensures MEF is initialized.
_services ??= GlobalServiceProvider.Default;
// Before the first test is run, ensure VS is properly initialized.
if (_jtc == null)
{
IVsShell shell;
while ((shell = GlobalServiceProvider.Default.GetService<SVsShell, IVsShell>()) == null)
{
Thread.Sleep(100);
}

object zombie;
// __VSSPROPID.VSSPROPID_Zombie
while ((int?)(zombie = shell.GetProperty(-9014, out zombie)) != 0)
{
Thread.Sleep(100);
}

// Retrieve the component model service, which could also now take time depending on new
// extensions being installed or updated before the first launch.
_jtc = GlobalServiceProvider.GetExport<JoinableTaskContext>();
}

messageBus.QueueMessage(new DiagnosticMessage("Running {0}", testCase.DisplayName));

var aggregator = new ExceptionAggregator();
var runner = _collectionRunnerMap.GetOrAdd(testCase.TestMethod.TestClass.TestCollection, tc => new VsRemoteTestCollectionRunner(tc, _assemblyFixtureMappings, _collectionFixtureMappings));
var runner = _collectionRunnerMap.GetOrAdd(testCase.TestMethod.TestClass.TestCollection,
tc => new VsRemoteTestCollectionRunner(tc, _jtc.Factory, _assemblyFixtureMappings, _collectionFixtureMappings));

if (SynchronizationContext.Current == null)
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
Expand All @@ -72,14 +92,17 @@ public VsixRunSummary Run(VsixTestCase testCase, IMessageBus messageBus)
{
using (var bus = new TestMessageBus(messageBus))
{
var result = runner.RunAsync(testCase, bus, aggregator)
.Result
.ToVsixRunSummary();
var ev = new ManualResetEventSlim();

var t = _jtc.Factory.RunAsync(async () =>
(await runner.RunAsync(testCase, bus, aggregator)).ToVsixRunSummary());

if (aggregator.HasExceptions && result != null)
result.Exception = aggregator.ToException();
_ = t.Task.ContinueWith(_ => ev.Set(), TaskScheduler.Default);
ev.Wait();

return result;
#pragma warning disable VSTHRD002 // We're not waiting synchronously here, we have already done that above with the MRE
return t.Task.Result;
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}
}
catch (AggregateException aex)
Expand Down Expand Up @@ -130,13 +153,15 @@ public override object InitializeLifetimeService()
class VsRemoteTestCollectionRunner : XunitTestCollectionRunner
{
readonly Dictionary<Type, object> _assemblyFixtureMappings;
readonly JoinableTaskFactory _jtf;

public VsRemoteTestCollectionRunner(ITestCollection testCollection, Dictionary<Type, object> assemblyFixtureMappings, Dictionary<Type, object> collectionFixtureMappings)
public VsRemoteTestCollectionRunner(ITestCollection testCollection, JoinableTaskFactory factory, Dictionary<Type, object> assemblyFixtureMappings, Dictionary<Type, object> collectionFixtureMappings)
: base(testCollection, Enumerable.Empty<IXunitTestCase>(), new NullMessageSink(), null,
new DefaultTestCaseOrderer(new NullMessageSink()), new ExceptionAggregator(), new CancellationTokenSource())
{
_assemblyFixtureMappings = assemblyFixtureMappings;
CollectionFixtureMappings = collectionFixtureMappings;
_jtf = factory;
}

public Task<RunSummary> RunAsync(IXunitTestCase testCase, IMessageBus messageBus, ExceptionAggregator aggregator)
Expand Down Expand Up @@ -174,7 +199,14 @@ protected override Task<RunSummary> RunTestClassAsync(ITestClass testClass, IRef
{
var fixture = Activator.CreateInstance(fixtureType);
if (fixture is IAsyncLifetime asyncFixture)
Aggregator.RunAsync(asyncFixture.InitializeAsync);
{
var ev = new ManualResetEventSlim();

_ = _jtf.RunAsync(asyncFixture.InitializeAsync)
.Task.ContinueWith(_ => ev.Set(), TaskScheduler.Default);

ev.Wait();
}

_assemblyFixtureMappings.Add(fixtureType, fixture);
});
Expand All @@ -189,16 +221,18 @@ protected override Task<RunSummary> RunTestClassAsync(ITestClass testClass, IRef
combinedFixtures[kvp.Key] = kvp.Value;

// We've done everything we need, so let the built-in types do the rest of the heavy lifting
return new VsRemoteTestClassRunner(testClass, @class, Aggregator, combinedFixtures).RunAsync(testCases.Single(), MessageBus);
return new VsRemoteTestClassRunner(_jtf, testClass, @class, Aggregator, combinedFixtures).RunAsync(testCases.Single(), MessageBus);
}
}

class VsRemoteTestClassRunner : XunitTestClassRunner
{
public VsRemoteTestClassRunner(ITestClass testClass, IReflectionTypeInfo @class, ExceptionAggregator aggregator, Dictionary<Type, object> collectionFixtureMappings)
readonly JoinableTaskFactory _jtf;
public VsRemoteTestClassRunner(JoinableTaskFactory jtf, ITestClass testClass, IReflectionTypeInfo @class, ExceptionAggregator aggregator, Dictionary<Type, object> collectionFixtureMappings)
: base(testClass, @class, Enumerable.Empty<IXunitTestCase>(), new NullMessageSink(), null,
new DefaultTestCaseOrderer(new NullMessageSink()), aggregator, new CancellationTokenSource(), collectionFixtureMappings)
{
_jtf = jtf;
}

public Task<RunSummary> RunAsync(IXunitTestCase testCase, IMessageBus messageBus)
Expand All @@ -211,9 +245,11 @@ public Task<RunSummary> RunAsync(IXunitTestCase testCase, IMessageBus messageBus
protected override async Task<RunSummary> RunTestMethodAsync(ITestMethod testMethod, IReflectionMethodInfo method, IEnumerable<IXunitTestCase> testCases, object[] constructorArguments)
{
var vsixTest = testCases.OfType<VsixTestCase>().Single();
var cancellation = Debugger.IsAttached ?
new CancellationTokenSource(TimeSpan.FromSeconds(vsixTest.TimeoutSeconds)) :
new CancellationTokenSource();
var disableTimeout = bool.TryParse(Environment.GetEnvironmentVariable(Constants.DisableTimeoutsEnvironmentVariable), out var noTimeout) && noTimeout;
var cancellation = Debugger.IsAttached || disableTimeout ?
// Don't timeout if we have an attached debugger.
new CancellationTokenSource() :
new CancellationTokenSource(TimeSpan.FromSeconds(vsixTest.TimeoutSeconds));

try
{
Expand All @@ -233,18 +269,15 @@ protected override async Task<RunSummary> RunTestMethodAsync(ITestMethod testMet
.RunAsync();

// If the UI thread was requested, switch to the main dispatcher.
var result = await Application.Current.Dispatcher.InvokeAsync(async () =>
await new SyncTestCaseRunner(
await _jtf.SwitchToMainThreadAsync();
return await new SyncTestCaseRunner(
testCases.Single(),
testCases.Single().DisplayName,
testCases.Single().SkipReason,
constructorArguments,
testCases.Single().TestMethodArguments,
MessageBus,
Aggregator, new CancellationTokenSource())
.RunAsync(), DispatcherPriority.Background, cancellation.Token);

return await result;
Aggregator, new CancellationTokenSource()).RunAsync();
}
finally
{
Expand Down
3 changes: 3 additions & 0 deletions src/Xunit.Vsix/VsixTestFramework.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ public VsixTestFrameworkExecutor(AssemblyName assemblyName,
: base(assemblyName, sourceInformationProvider, diagnosticMessageSink)
{ }

#pragma warning disable VSTHRD100 // Avoid async void methods
// This is an inherited method, we cannot change the return type to Task
protected override async void RunTestCases(IEnumerable<IXunitTestCase> testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
#pragma warning restore VSTHRD100 // Avoid async void methods
{
SetupTracing(TestAssembly.Assembly);

Expand Down
3 changes: 2 additions & 1 deletion src/Xunit.Vsix/Xunit.Vsix.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" />
<!--<PackageReference Include="Microsoft.VisualStudio.OLE.Interop" Version="7.10.6072" />-->
<PackageReference Include="Microsoft.VisualStudio.Setup.Configuration.Interop" Version="3.3.2180" PrivateAssets="all" />
<!--<PackageReference Include="Microsoft.VisualStudio.Shell.Interop" Version="7.10.6073" />-->
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop" Version="7.10.6073" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="15.8.209" />
<PackageReference Include="ThisAssembly.Strings" Version="1.0.9" PrivateAssets="all" />
<PackageReference Include="Devlooped.Injector" Version="42.42.42-main.7" />
<PackageReference Include="xunit" Version="2.4.1" />
Expand Down

0 comments on commit 86992cf

Please sign in to comment.