@@ -24,6 +24,11 @@ namespace Tvl.VisualStudio.MouseFastScroll.IntegrationTests.Threading
24
24
/// </summary>
25
25
public sealed class WpfTestRunner : XunitTestRunner
26
26
{
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
+
27
32
public WpfTestRunner (
28
33
WpfTestSharedData sharedData ,
29
34
ITest test ,
@@ -46,34 +51,72 @@ public WpfTestRunner(
46
51
protected override Task < decimal > InvokeTestMethodAsync ( ExceptionAggregator aggregator )
47
52
{
48
53
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 ) ;
50
85
var task = Task . Factory . StartNew (
51
86
async ( ) =>
52
87
{
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 " ) ;
54
89
55
90
using ( await SharedData . TestSerializationGate . DisposableWaitAsync ( CancellationToken . None ) )
56
91
{
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 ( ) ;
70
95
}
71
96
} ,
72
97
CancellationTokenSource . Token ,
73
98
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 ( ) ;
75
115
76
- return task . Unwrap ( ) ;
116
+ // Join the STA thread, which ensures shutdown is complete.
117
+ staThread . Join ( HangMitigatingTimeout ) ;
118
+ }
119
+ } ) ;
77
120
}
78
121
}
79
122
}
0 commit comments