@@ -157,43 +157,49 @@ public virtual async Task<TestResult> InvokeAsync(object?[]? arguments)
157157
158158 ExecutionContext ? executionContext = Parent . ExecutionContext ?? Parent . Parent . ExecutionContext ;
159159
160- try
160+ var tcs = new TaskCompletionSource < object ? > ( ) ;
161+
162+ #pragma warning disable VSTHRD101 // Avoid unsupported async delegates
163+ ExecutionContextHelpers . RunOnContext ( executionContext , async ( ) =>
161164 {
162- ExecutionContextHelpers . RunOnContext ( executionContext , ( ) =>
165+ try
163166 {
164167 ThreadSafeStringWriter . CleanState ( ) ;
165168 listener = new LogMessageListener ( MSTestSettings . CurrentSettings . CaptureDebugTraces ) ;
166- executionContext = ExecutionContext . Capture ( ) ;
167- } ) ;
168-
169- result = IsTimeoutSet
170- ? await ExecuteInternalWithTimeoutAsync ( arguments , executionContext )
171- : await ExecuteInternalAsync ( arguments , executionContext , null ) ;
172- }
173- finally
174- {
175- // Handle logs & debug traces.
176- watch . Stop ( ) ;
177169
178- if ( result != null )
170+ result = IsTimeoutSet
171+ ? await ExecuteInternalWithTimeoutAsync ( arguments )
172+ : await ExecuteInternalAsync ( arguments , null ) ;
173+ tcs . SetResult ( null ) ;
174+ }
175+ catch ( Exception e )
176+ {
177+ tcs . SetException ( e ) ;
178+ }
179+ finally
179180 {
180- result . Duration = watch . Elapsed ;
181- if ( listener is not null )
181+ // Handle logs & debug traces.
182+ watch . Stop ( ) ;
183+
184+ if ( result != null )
182185 {
183- ExecutionContextHelpers . RunOnContext ( executionContext , ( ) =>
186+ result . Duration = watch . Elapsed ;
187+ if ( listener is not null )
184188 {
185189 result . DebugTrace = listener . GetAndClearDebugTrace ( ) ;
186190 result . LogOutput = listener . GetAndClearStandardOutput ( ) ;
187191 result . LogError = listener . GetAndClearStandardError ( ) ;
188192 result . TestContextMessages = TestContext ? . GetAndClearDiagnosticMessages ( ) ;
189193 result . ResultFiles = TestContext ? . GetResultFiles ( ) ;
190194 listener . Dispose ( ) ;
191- } ) ;
195+ }
192196 }
193197 }
194- }
198+ } ) ;
199+ #pragma warning restore VSTHRD101 // Avoid unsupported async delegates
195200
196- return result ;
201+ await tcs . Task ;
202+ return result ! ;
197203 }
198204
199205 internal void SetArguments ( object ? [ ] ? arguments ) => Arguments = arguments == null ? null : ResolveArguments ( arguments ) ;
@@ -394,26 +400,22 @@ private void ThrowMultipleAttributesException(string attributeName)
394400 /// Execute test without timeout.
395401 /// </summary>
396402 /// <param name="arguments">Arguments to be passed to the method.</param>
397- /// <param name="executionContext">The execution context to execute the test method on.</param>
398403 /// <param name="timeoutTokenSource">The timeout token source.</param>
399404 /// <returns>The result of the execution.</returns>
400405 [ SuppressMessage ( "Microsoft.Design" , "CA1031:DoNotCatchGeneralExceptionTypes" , Justification = "Requirement is to handle all kinds of user exceptions and message appropriately." ) ]
401- private async Task < TestResult > ExecuteInternalAsync ( object ? [ ] ? arguments , ExecutionContext ? executionContext , CancellationTokenSource ? timeoutTokenSource )
406+ private async Task < TestResult > ExecuteInternalAsync ( object ? [ ] ? arguments , CancellationTokenSource ? timeoutTokenSource )
402407 {
403408 DebugEx . Assert ( TestMethod != null , "UnitTestExecuter.DefaultTestMethodInvoke: testMethod = null." ) ;
404409
405410 var result = new TestResult ( ) ;
406411
407412 // TODO remove dry violation with TestMethodRunner
408- ExecutionContextHelpers . RunOnContext ( executionContext , ( ) =>
409- {
410- _classInstance = CreateTestClassInstance ( result ) ;
411- executionContext = ExecutionContext . Capture ( ) ;
412- } ) ;
413+ _classInstance = CreateTestClassInstance ( result ) ;
413414 bool isExceptionThrown = false ;
414415 bool hasTestInitializePassed = false ;
415416 Exception ? testRunnerException = null ;
416417 _isTestCleanupInvoked = false ;
418+ ExecutionContext ? executionContext = null ;
417419
418420 try
419421 {
@@ -427,33 +429,57 @@ private async Task<TestResult> ExecuteInternalAsync(object?[]? arguments, Execut
427429 if ( RunTestInitializeMethod ( _classInstance , result , ref executionContext , timeoutTokenSource ) )
428430 {
429431 hasTestInitializePassed = true ;
430- var tcs = new TaskCompletionSource < object ? > ( ) ;
431- #pragma warning disable VSTHRD101 // Avoid unsupported async delegates
432- ExecutionContextHelpers . RunOnContext ( executionContext , async ( ) =>
432+
433+ if ( executionContext is null )
433434 {
434- try
435+ object ? invokeResult = TestMethod . GetInvokeResult ( _classInstance , arguments ) ;
436+ if ( invokeResult is Task task )
435437 {
436- object ? invokeResult = TestMethod . GetInvokeResult ( _classInstance , arguments ) ;
437- if ( invokeResult is Task task )
438+ await task ;
439+ }
440+ else if ( invokeResult is ValueTask valueTask )
441+ {
442+ await valueTask ;
443+ }
444+ }
445+ else
446+ {
447+ var tcs = new TaskCompletionSource < object ? > ( ) ;
448+ ExecutionContext ? updatedExecutionContext = executionContext ;
449+ #pragma warning disable VSTHRD101 // Avoid unsupported async delegates
450+ ExecutionContextHelpers . RunOnContext ( executionContext , async ( ) =>
451+ {
452+ try
438453 {
439- await task ;
454+ object ? invokeResult = TestMethod . GetInvokeResult ( _classInstance , arguments ) ;
455+ if ( invokeResult is Task task )
456+ {
457+ await task ;
458+ }
459+ else if ( invokeResult is ValueTask valueTask )
460+ {
461+ await valueTask ;
462+ }
440463 }
441- else if ( invokeResult is ValueTask valueTask )
464+ catch ( Exception e )
442465 {
443- await valueTask ;
466+ tcs . SetException ( e ) ;
444467 }
468+ finally
469+ {
470+ updatedExecutionContext = ExecutionContext . Capture ( ) ;
471+ tcs . TrySetResult ( null ) ;
472+ }
473+ } ) ;
474+ #pragma warning restore VSTHRD101 // Avoid unsupported async delegates
445475
446- executionContext = ExecutionContext . Capture ( ) ;
447- tcs . SetResult ( null ) ;
448- }
449- catch ( Exception ex )
476+ await tcs . Task ;
477+
478+ if ( updatedExecutionContext is not null )
450479 {
451- tcs . SetException ( ex ) ;
480+ executionContext = updatedExecutionContext ;
452481 }
453- } ) ;
454- #pragma warning restore VSTHRD101 // Avoid unsupported async delegates
455-
456- await tcs . Task ;
482+ }
457483
458484 result . Outcome = UTF . UnitTestOutcome . Passed ;
459485 }
@@ -677,7 +703,7 @@ private static TestFailedException HandleMethodException(Exception ex, Exception
677703 /// Runs TestCleanup methods of parent TestClass and base classes.
678704 /// </summary>
679705 /// <param name="result">Instance of TestResult.</param>
680- /// <param name="executionContext">The execution context to execute the test cleanup on.</param>
706+ /// <param name="executionContext">The execution context to run on.</param>
681707 /// <param name="timeoutTokenSource">The timeout token source.</param>
682708 [ SuppressMessage ( "Microsoft.Design" , "CA1031:DoNotCatchGeneralExceptionTypes" , Justification = "Requirement is to handle all kinds of user exceptions and message appropriately." ) ]
683709 private void RunTestCleanupMethod ( TestResult result , ExecutionContext ? executionContext , CancellationTokenSource ? timeoutTokenSource )
@@ -723,20 +749,12 @@ private void RunTestCleanupMethod(TestResult result, ExecutionContext? execution
723749 if ( _classInstance is IAsyncDisposable classInstanceAsAsyncDisposable )
724750 {
725751 // If you implement IAsyncDisposable without calling the DisposeAsync this would result a resource leak.
726- ExecutionContextHelpers . RunOnContext ( executionContext , ( ) =>
727- {
728- classInstanceAsAsyncDisposable . DisposeAsync ( ) . AsTask ( ) . Wait ( ) ;
729- executionContext = ExecutionContext . Capture ( ) ;
730- } ) ;
752+ classInstanceAsAsyncDisposable . DisposeAsync ( ) . AsTask ( ) . Wait ( ) ;
731753 }
732754#endif
733755 if ( _classInstance is IDisposable classInstanceAsDisposable )
734756 {
735- ExecutionContextHelpers . RunOnContext ( executionContext , ( ) =>
736- {
737- classInstanceAsDisposable . Dispose ( ) ;
738- executionContext = ExecutionContext . Capture ( ) ;
739- } ) ;
757+ classInstanceAsDisposable . Dispose ( ) ;
740758 }
741759 }
742760 }
@@ -775,7 +793,7 @@ private void RunTestCleanupMethod(TestResult result, ExecutionContext? execution
775793 /// </summary>
776794 /// <param name="classInstance">Instance of TestClass.</param>
777795 /// <param name="result">Instance of TestResult.</param>
778- /// <param name="executionContext">The execution context to execute the test initialize on.</param>
796+ /// <param name="executionContext">The execution context to run on.</param>
779797 /// <param name="timeoutTokenSource">The timeout token source.</param>
780798 /// <returns>True if the TestInitialize method(s) did not throw an exception.</returns>
781799 [ SuppressMessage ( "Microsoft.Design" , "CA1031:DoNotCatchGeneralExceptionTypes" , Justification = "Requirement is to handle all kinds of user exceptions and message appropriately." ) ]
@@ -865,15 +883,18 @@ private bool RunTestInitializeMethod(object classInstance, TestResult result, re
865883 timeout = localTimeout ;
866884 }
867885
868- ExecutionContext ? updatedExecutionContext = executionContext ;
869-
886+ int originalThreadId = Environment . CurrentManagedThreadId ;
887+ ExecutionContext ? updatedExecutionContext = null ;
870888 TestFailedException ? result = FixtureMethodRunner . RunWithTimeoutAndCancellation (
871889 ( ) =>
872890 {
873891 methodInfo . InvokeAsSynchronousTask ( classInstance , null ) ;
874- // **After** we have executed the current test initialize (it could be from the current class or from base class), we save the current context.
875- // This context will contain async locals set by the test initialize method.
876- updatedExecutionContext = ExecutionContext . Capture ( ) ;
892+ if ( originalThreadId != Environment . CurrentManagedThreadId )
893+ {
894+ // We ended up running on a different thread, because of use of non-cooperative timeout.
895+ // Re-capture the execution context.
896+ updatedExecutionContext = ExecutionContext . Capture ( ) ;
897+ }
877898 } ,
878899 TestContext ! . Context . CancellationTokenSource ,
879900 timeout ,
@@ -885,7 +906,11 @@ timeoutTokenSource is null
885906 ? null
886907 : ( timeoutTokenSource , TimeoutInfo . Timeout ) ) ;
887908
888- executionContext = updatedExecutionContext ;
909+ if ( updatedExecutionContext != null )
910+ {
911+ executionContext = updatedExecutionContext ;
912+ }
913+
889914 return result ;
890915 }
891916
@@ -897,14 +922,18 @@ timeoutTokenSource is null
897922 timeout = localTimeout ;
898923 }
899924
900- ExecutionContext ? updatedExecutionContext = executionContext ;
925+ int originalThreadId = Environment . CurrentManagedThreadId ;
926+ ExecutionContext ? updatedExecutionContext = null ;
901927 TestFailedException ? result = FixtureMethodRunner . RunWithTimeoutAndCancellation (
902928 ( ) =>
903929 {
904930 methodInfo . InvokeAsSynchronousTask ( classInstance , null ) ;
905- // **After** we have executed the current test cleanup (it could be from the current class or from base class), we save the current context.
906- // This context will contain async locals set by the test cleanup method.
907- updatedExecutionContext = ExecutionContext . Capture ( ) ;
931+ if ( originalThreadId != Environment . CurrentManagedThreadId )
932+ {
933+ // We ended up running on a different thread, because of use of non-cooperative timeout.
934+ // Re-capture the execution context.
935+ updatedExecutionContext = ExecutionContext . Capture ( ) ;
936+ }
908937 } ,
909938 TestContext ! . Context . CancellationTokenSource ,
910939 timeout ,
@@ -916,7 +945,11 @@ timeoutTokenSource is null
916945 ? null
917946 : ( timeoutTokenSource , TimeoutInfo . Timeout ) ) ;
918947
919- executionContext = updatedExecutionContext ;
948+ if ( updatedExecutionContext != null )
949+ {
950+ executionContext = updatedExecutionContext ;
951+ }
952+
920953 return result ;
921954 }
922955
@@ -1031,10 +1064,9 @@ private bool SetTestContext(object classInstance, TestResult result)
10311064 /// Execute test with a timeout.
10321065 /// </summary>
10331066 /// <param name="arguments">The arguments to be passed.</param>
1034- /// <param name="executionContext">The execution context to execute the test method on.</param>
10351067 /// <returns>The result of execution.</returns>
10361068 [ SuppressMessage ( "Microsoft.Design" , "CA1031:DoNotCatchGeneralExceptionTypes" , Justification = "Requirement is to handle all kinds of user exceptions and message appropriately." ) ]
1037- private async Task < TestResult > ExecuteInternalWithTimeoutAsync ( object ? [ ] ? arguments , ExecutionContext ? executionContext )
1069+ private async Task < TestResult > ExecuteInternalWithTimeoutAsync ( object ? [ ] ? arguments )
10381070 {
10391071 DebugEx . Assert ( IsTimeoutSet , "Timeout should be set" ) ;
10401072
@@ -1058,7 +1090,7 @@ private async Task<TestResult> ExecuteInternalWithTimeoutAsync(object?[]? argume
10581090
10591091 try
10601092 {
1061- return await ExecuteInternalAsync ( arguments , executionContext , timeoutTokenSource ) ;
1093+ return await ExecuteInternalAsync ( arguments , timeoutTokenSource ) ;
10621094 }
10631095 catch ( OperationCanceledException )
10641096 {
@@ -1084,6 +1116,7 @@ private async Task<TestResult> ExecuteInternalWithTimeoutAsync(object?[]? argume
10841116
10851117 TestResult ? result = null ;
10861118 Exception ? failure = null ;
1119+ ExecutionContext ? executionContext = null ;
10871120
10881121 if ( PlatformServiceProvider . Instance . ThreadOperations . Execute ( ExecuteAsyncAction , TimeoutInfo . Timeout , TestContext ! . Context . CancellationTokenSource . Token ) )
10891122 {
@@ -1135,7 +1168,7 @@ void ExecuteAsyncAction()
11351168 // dispatched back to the SynchronizationContext which offloads the work to the UI thread.
11361169 // However, the GetAwaiter().GetResult() here will block the current thread which is also the UI thread.
11371170 // So, the continuations will not be able, thus this task never completes.
1138- result = ExecuteInternalAsync ( arguments , executionContext , null ) . GetAwaiter ( ) . GetResult ( ) ;
1171+ result = ExecuteInternalAsync ( arguments , null ) . GetAwaiter ( ) . GetResult ( ) ;
11391172 }
11401173 catch ( Exception ex )
11411174 {
0 commit comments