Skip to content

Commit 864fc41

Browse files
authored
Merge pull request #24 from sharwell/update-harness
Update test harness
2 parents d27832e + ba45322 commit 864fc41

File tree

4 files changed

+80
-163
lines changed

4 files changed

+80
-163
lines changed

Tvl.VisualStudio.MouseFastScroll.IntegrationTests/Harness/VisualStudioInstanceFactory.cs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,20 @@ private async Task UpdateCurrentlyRunningInstanceAsync(Version version, Immutabl
124124
hostProcess = StartNewVisualStudioProcess(installationPath, version);
125125

126126
// We wait until the DTE instance is up before we're good
127-
dte = await IntegrationHelper.WaitForNotNullAsync(() => IntegrationHelper.TryLocateDteForProcess(hostProcess)).ConfigureAwait(false);
127+
dte = await IntegrationHelper.WaitForNotNullAsync(() => IntegrationHelper.TryLocateDteForProcess(hostProcess)).ConfigureAwait(true);
128128
}
129129
else
130130
{
131131
// We are going to reuse the currently running instance, so ensure that we grab the host Process and Dte
132132
// before cleaning up any hooks or remoting services created by the previous instance. We will then
133133
// create a new VisualStudioInstance from the previous to ensure that everything is in a 'clean' state.
134+
//
135+
// We create a new DTE instance in the current context since the COM object could have been separated
136+
// from its RCW during the previous test.
134137
Debug.Assert(_currentlyRunningInstance != null, "Assertion failed: _currentlyRunningInstance != null");
135138

136139
hostProcess = _currentlyRunningInstance.HostProcess;
137-
dte = _currentlyRunningInstance.Dte;
140+
dte = await IntegrationHelper.WaitForNotNullAsync(() => IntegrationHelper.TryLocateDteForProcess(hostProcess)).ConfigureAwait(true);
138141
actualVersion = _currentlyRunningInstance.Version;
139142
supportedPackageIds = _currentlyRunningInstance.SupportedPackageIds;
140143
installationPath = _currentlyRunningInstance.InstallationPath;
@@ -208,14 +211,24 @@ private static IEnumerable<ISetupInstance> EnumerateVisualStudioInstancesViaInst
208211

209212
private static Tuple<string, Version, ImmutableHashSet<string>, InstanceState> LocateVisualStudioInstance(Version version, ImmutableHashSet<string> requiredPackageIds)
210213
{
211-
var vsInstallDir = Environment.GetEnvironmentVariable("VSInstallDir");
214+
var vsInstallDir = Environment.GetEnvironmentVariable("__UNITTESTEXPLORER_VSINSTALLPATH__")
215+
?? Environment.GetEnvironmentVariable("VSAPPIDDIR");
216+
if (vsInstallDir != null)
217+
{
218+
vsInstallDir = Path.GetFullPath(Path.Combine(vsInstallDir, @"..\.."));
219+
}
220+
else
221+
{
222+
vsInstallDir = Environment.GetEnvironmentVariable("VSInstallDir");
223+
}
224+
212225
var haveVsInstallDir = !string.IsNullOrEmpty(vsInstallDir);
213226

214227
if (haveVsInstallDir)
215228
{
216229
vsInstallDir = Path.GetFullPath(vsInstallDir);
217230
vsInstallDir = vsInstallDir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
218-
Debug.WriteLine($"An environment variable named 'VSInstallDir' was found, adding this to the specified requirements. (VSInstallDir: {vsInstallDir})");
231+
Debug.WriteLine($"An environment variable named 'VSInstallDir' (or equivalent) was found, adding this to the specified requirements. (VSInstallDir: {vsInstallDir})");
219232
}
220233

221234
var instances = EnumerateVisualStudioInstances().Where((instance) =>
@@ -225,11 +238,12 @@ private static Tuple<string, Version, ImmutableHashSet<string>, InstanceState> L
225238
isMatch &= version.Major == instance.Item2.Major;
226239
isMatch &= instance.Item2 >= version;
227240

228-
if (haveVsInstallDir)
241+
if (haveVsInstallDir && version.Major == 15)
229242
{
230243
var installationPath = instance.Item1;
244+
installationPath = Path.GetFullPath(installationPath);
231245
installationPath = installationPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
232-
////isMatch &= installationPath.Equals(vsInstallDir, StringComparison.OrdinalIgnoreCase);
246+
isMatch &= installationPath.Equals(vsInstallDir, StringComparison.OrdinalIgnoreCase);
233247
}
234248
}
235249

Tvl.VisualStudio.MouseFastScroll.IntegrationTests/Threading/StaTaskScheduler.cs

Lines changed: 0 additions & 78 deletions
This file was deleted.

Tvl.VisualStudio.MouseFastScroll.IntegrationTests/Threading/TaskJoinExtensions.cs

Lines changed: 0 additions & 62 deletions
This file was deleted.

Tvl.VisualStudio.MouseFastScroll.IntegrationTests/Threading/WpfTestRunner.cs

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ namespace Tvl.VisualStudio.MouseFastScroll.IntegrationTests.Threading
2424
/// </summary>
2525
public sealed class WpfTestRunner : XunitTestRunner
2626
{
27+
/// <summary>
28+
/// A long timeout used to avoid hangs in tests, where a test failure manifests as an operation never occurring.
29+
/// </summary>
30+
private static readonly TimeSpan HangMitigatingTimeout = TimeSpan.FromMinutes(1);
31+
2732
public WpfTestRunner(
2833
WpfTestSharedData sharedData,
2934
ITest test,
@@ -46,34 +51,72 @@ public WpfTestRunner(
4651
protected override Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator)
4752
{
4853
SharedData.ExecutingTest(TestMethod);
49-
var sta = StaTaskScheduler.DefaultSta;
54+
55+
DispatcherSynchronizationContext synchronizationContext = null;
56+
Dispatcher dispatcher = null;
57+
Thread staThread;
58+
using (var staThreadStartedEvent = new ManualResetEventSlim(initialState: false))
59+
{
60+
staThread = new Thread((ThreadStart)(() =>
61+
{
62+
// All WPF Tests need a DispatcherSynchronizationContext and we don't want to block pending keyboard
63+
// or mouse input from the user. So use background priority which is a single level below user input.
64+
synchronizationContext = new DispatcherSynchronizationContext();
65+
dispatcher = Dispatcher.CurrentDispatcher;
66+
67+
// xUnit creates its own synchronization context and wraps any existing context so that messages are
68+
// still pumped as necessary. So we are safe setting it here, where we are not safe setting it in test.
69+
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
70+
71+
staThreadStartedEvent.Set();
72+
73+
Dispatcher.Run();
74+
}));
75+
76+
staThread.Name = $"{nameof(WpfTestRunner)} {TestMethod.Name}";
77+
staThread.SetApartmentState(ApartmentState.STA);
78+
staThread.Start();
79+
80+
staThreadStartedEvent.Wait();
81+
Debug.Assert(synchronizationContext != null, "Assertion failed: synchronizationContext != null");
82+
}
83+
84+
var taskScheduler = new SynchronizationContextTaskScheduler(synchronizationContext);
5085
var task = Task.Factory.StartNew(
5186
async () =>
5287
{
53-
Debug.Assert(sta.StaThread == Thread.CurrentThread, "Assertion failed: sta.StaThread == Thread.CurrentThread");
88+
Debug.Assert(SynchronizationContext.Current is DispatcherSynchronizationContext, "Assertion failed: SynchronizationContext.Current is DispatcherSynchronizationContext");
5489

5590
using (await SharedData.TestSerializationGate.DisposableWaitAsync(CancellationToken.None))
5691
{
57-
try
58-
{
59-
Debug.Assert(SynchronizationContext.Current is DispatcherSynchronizationContext, "Assertion failed: SynchronizationContext.Current is DispatcherSynchronizationContext");
60-
61-
// Just call back into the normal xUnit dispatch process now that we are on an STA Thread with no synchronization context.
62-
var invoker = new XunitTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource);
63-
return invoker.RunAsync().JoinUsingDispatcher(CancellationTokenSource.Token);
64-
}
65-
finally
66-
{
67-
// Cleanup the synchronization context even if the test is failing exceptionally
68-
SynchronizationContext.SetSynchronizationContext(null);
69-
}
92+
// Just call back into the normal xUnit dispatch process now that we are on an STA Thread with no synchronization context.
93+
var invoker = new XunitTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource);
94+
return await invoker.RunAsync();
7095
}
7196
},
7297
CancellationTokenSource.Token,
7398
TaskCreationOptions.None,
74-
new SynchronizationContextTaskScheduler(sta.DispatcherSynchronizationContext));
99+
taskScheduler).Unwrap();
100+
101+
return Task.Run(
102+
async () =>
103+
{
104+
try
105+
{
106+
return await task.ConfigureAwait(false);
107+
}
108+
finally
109+
{
110+
// Make sure to shut down the dispatcher. Certain framework types listed for the dispatcher
111+
// shutdown to perform cleanup actions. In the absence of an explicit shutdown, these actions
112+
// are delayed and run during AppDomain or process shutdown, where they can lead to crashes of
113+
// the test process.
114+
dispatcher.InvokeShutdown();
75115

76-
return task.Unwrap();
116+
// Join the STA thread, which ensures shutdown is complete.
117+
staThread.Join(HangMitigatingTimeout);
118+
}
119+
});
77120
}
78121
}
79122
}

0 commit comments

Comments
 (0)