Skip to content

Commit e38f226

Browse files
CopilotYoussef1313
authored andcommitted
Run the whole ExecuteInternal logic under the right execution context once
1 parent d1c3780 commit e38f226

File tree

2 files changed

+71
-103
lines changed

2 files changed

+71
-103
lines changed

src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs

Lines changed: 58 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
});
168169

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();
177-
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)
179176
{
180-
result.Duration = watch.Elapsed;
181-
if (listener is not null)
177+
tcs.SetException(e);
178+
}
179+
finally
180+
{
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,22 +400,17 @@ 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;
@@ -424,36 +425,19 @@ private async Task<TestResult> ExecuteInternalAsync(object?[]? arguments, Execut
424425
// For any failure after this point, we must run TestCleanup
425426
_isTestContextSet = true;
426427

427-
if (RunTestInitializeMethod(_classInstance, result, ref executionContext, timeoutTokenSource))
428+
if (RunTestInitializeMethod(_classInstance, result, timeoutTokenSource))
428429
{
429430
hasTestInitializePassed = true;
430-
var tcs = new TaskCompletionSource<object?>();
431-
#pragma warning disable VSTHRD101 // Avoid unsupported async delegates
432-
ExecutionContextHelpers.RunOnContext(executionContext, async () =>
433-
{
434-
try
435-
{
436-
object? invokeResult = TestMethod.GetInvokeResult(_classInstance, arguments);
437-
if (invokeResult is Task task)
438-
{
439-
await task;
440-
}
441-
else if (invokeResult is ValueTask valueTask)
442-
{
443-
await valueTask;
444-
}
445-
446-
executionContext = ExecutionContext.Capture();
447-
tcs.SetResult(null);
448-
}
449-
catch (Exception ex)
450-
{
451-
tcs.SetException(ex);
452-
}
453-
});
454-
#pragma warning restore VSTHRD101 // Avoid unsupported async delegates
455431

456-
await tcs.Task;
432+
object? invokeResult = TestMethod.GetInvokeResult(_classInstance, arguments);
433+
if (invokeResult is Task task)
434+
{
435+
await task;
436+
}
437+
else if (invokeResult is ValueTask valueTask)
438+
{
439+
await valueTask;
440+
}
457441

458442
result.Outcome = UTF.UnitTestOutcome.Passed;
459443
}
@@ -532,7 +516,7 @@ private async Task<TestResult> ExecuteInternalAsync(object?[]? arguments, Execut
532516
// Pulling it out so extension writers can abort custom cleanups if need be. Having this in a finally block
533517
// does not allow a thread abort exception to be raised within the block but throws one after finally is executed
534518
// crashing the process. This was blocking writing an extension for Dynamic Timeout in VSO.
535-
RunTestCleanupMethod(result, executionContext, timeoutTokenSource);
519+
RunTestCleanupMethod(result, timeoutTokenSource);
536520

537521
return testRunnerException != null ? throw testRunnerException : result;
538522
}
@@ -677,10 +661,9 @@ private static TestFailedException HandleMethodException(Exception ex, Exception
677661
/// Runs TestCleanup methods of parent TestClass and base classes.
678662
/// </summary>
679663
/// <param name="result">Instance of TestResult.</param>
680-
/// <param name="executionContext">The execution context to execute the test cleanup on.</param>
681664
/// <param name="timeoutTokenSource">The timeout token source.</param>
682665
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")]
683-
private void RunTestCleanupMethod(TestResult result, ExecutionContext? executionContext, CancellationTokenSource? timeoutTokenSource)
666+
private void RunTestCleanupMethod(TestResult result, CancellationTokenSource? timeoutTokenSource)
684667
{
685668
DebugEx.Assert(result != null, "result != null");
686669

@@ -708,13 +691,13 @@ private void RunTestCleanupMethod(TestResult result, ExecutionContext? execution
708691
// Test cleanups are called in the order of discovery
709692
// Current TestClass -> Parent -> Grandparent
710693
testCleanupException = testCleanupMethod is not null
711-
? InvokeCleanupMethod(testCleanupMethod, _classInstance, ref executionContext, timeoutTokenSource)
694+
? InvokeCleanupMethod(testCleanupMethod, _classInstance, timeoutTokenSource)
712695
: null;
713696
var baseTestCleanupQueue = new Queue<MethodInfo>(Parent.BaseTestCleanupMethodsQueue);
714697
while (baseTestCleanupQueue.Count > 0 && testCleanupException is null)
715698
{
716699
testCleanupMethod = baseTestCleanupQueue.Dequeue();
717-
testCleanupException = InvokeCleanupMethod(testCleanupMethod, _classInstance, ref executionContext, timeoutTokenSource);
700+
testCleanupException = InvokeCleanupMethod(testCleanupMethod, _classInstance, timeoutTokenSource);
718701
}
719702
}
720703
finally
@@ -723,20 +706,12 @@ private void RunTestCleanupMethod(TestResult result, ExecutionContext? execution
723706
if (_classInstance is IAsyncDisposable classInstanceAsAsyncDisposable)
724707
{
725708
// 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-
});
709+
classInstanceAsAsyncDisposable.DisposeAsync().AsTask().Wait();
731710
}
732711
#endif
733712
if (_classInstance is IDisposable classInstanceAsDisposable)
734713
{
735-
ExecutionContextHelpers.RunOnContext(executionContext, () =>
736-
{
737-
classInstanceAsDisposable.Dispose();
738-
executionContext = ExecutionContext.Capture();
739-
});
714+
classInstanceAsDisposable.Dispose();
740715
}
741716
}
742717
}
@@ -775,11 +750,10 @@ private void RunTestCleanupMethod(TestResult result, ExecutionContext? execution
775750
/// </summary>
776751
/// <param name="classInstance">Instance of TestClass.</param>
777752
/// <param name="result">Instance of TestResult.</param>
778-
/// <param name="executionContext">The execution context to execute the test initialize on.</param>
779753
/// <param name="timeoutTokenSource">The timeout token source.</param>
780754
/// <returns>True if the TestInitialize method(s) did not throw an exception.</returns>
781755
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")]
782-
private bool RunTestInitializeMethod(object classInstance, TestResult result, ref ExecutionContext? executionContext, CancellationTokenSource? timeoutTokenSource)
756+
private bool RunTestInitializeMethod(object classInstance, TestResult result, CancellationTokenSource? timeoutTokenSource)
783757
{
784758
DebugEx.Assert(classInstance != null, "classInstance != null");
785759
DebugEx.Assert(result != null, "result != null");
@@ -796,7 +770,7 @@ private bool RunTestInitializeMethod(object classInstance, TestResult result, re
796770
{
797771
testInitializeMethod = baseTestInitializeStack.Pop();
798772
testInitializeException = testInitializeMethod is not null
799-
? InvokeInitializeMethod(testInitializeMethod, classInstance, ref executionContext, timeoutTokenSource)
773+
? InvokeInitializeMethod(testInitializeMethod, classInstance, timeoutTokenSource)
800774
: null;
801775
if (testInitializeException is not null)
802776
{
@@ -808,7 +782,7 @@ private bool RunTestInitializeMethod(object classInstance, TestResult result, re
808782
{
809783
testInitializeMethod = Parent.TestInitializeMethod;
810784
testInitializeException = testInitializeMethod is not null
811-
? InvokeInitializeMethod(testInitializeMethod, classInstance, ref executionContext, timeoutTokenSource)
785+
? InvokeInitializeMethod(testInitializeMethod, classInstance, timeoutTokenSource)
812786
: null;
813787
}
814788
}
@@ -857,66 +831,49 @@ private bool RunTestInitializeMethod(object classInstance, TestResult result, re
857831
return false;
858832
}
859833

860-
private TestFailedException? InvokeInitializeMethod(MethodInfo methodInfo, object classInstance, ref ExecutionContext? executionContext, CancellationTokenSource? timeoutTokenSource)
834+
private TestFailedException? InvokeInitializeMethod(MethodInfo methodInfo, object classInstance, CancellationTokenSource? timeoutTokenSource)
861835
{
862836
TimeoutInfo? timeout = null;
863837
if (Parent.TestInitializeMethodTimeoutMilliseconds.TryGetValue(methodInfo, out TimeoutInfo localTimeout))
864838
{
865839
timeout = localTimeout;
866840
}
867841

868-
ExecutionContext? updatedExecutionContext = executionContext;
869-
870842
TestFailedException? result = FixtureMethodRunner.RunWithTimeoutAndCancellation(
871-
() =>
872-
{
873-
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();
877-
},
843+
() => methodInfo.InvokeAsSynchronousTask(classInstance, null),
878844
TestContext!.Context.CancellationTokenSource,
879845
timeout,
880846
methodInfo,
881-
executionContext,
847+
null,
882848
Resource.TestInitializeWasCancelled,
883849
Resource.TestInitializeTimedOut,
884850
timeoutTokenSource is null
885851
? null
886852
: (timeoutTokenSource, TimeoutInfo.Timeout));
887853

888-
executionContext = updatedExecutionContext;
889854
return result;
890855
}
891856

892-
private TestFailedException? InvokeCleanupMethod(MethodInfo methodInfo, object classInstance, ref ExecutionContext? executionContext, CancellationTokenSource? timeoutTokenSource)
857+
private TestFailedException? InvokeCleanupMethod(MethodInfo methodInfo, object classInstance, CancellationTokenSource? timeoutTokenSource)
893858
{
894859
TimeoutInfo? timeout = null;
895860
if (Parent.TestCleanupMethodTimeoutMilliseconds.TryGetValue(methodInfo, out TimeoutInfo localTimeout))
896861
{
897862
timeout = localTimeout;
898863
}
899864

900-
ExecutionContext? updatedExecutionContext = executionContext;
901865
TestFailedException? result = FixtureMethodRunner.RunWithTimeoutAndCancellation(
902-
() =>
903-
{
904-
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();
908-
},
866+
() => methodInfo.InvokeAsSynchronousTask(classInstance, null),
909867
TestContext!.Context.CancellationTokenSource,
910868
timeout,
911869
methodInfo,
912-
executionContext,
870+
null,
913871
Resource.TestCleanupWasCancelled,
914872
Resource.TestCleanupTimedOut,
915873
timeoutTokenSource is null
916874
? null
917875
: (timeoutTokenSource, TimeoutInfo.Timeout));
918876

919-
executionContext = updatedExecutionContext;
920877
return result;
921878
}
922879

@@ -1031,10 +988,9 @@ private bool SetTestContext(object classInstance, TestResult result)
1031988
/// Execute test with a timeout.
1032989
/// </summary>
1033990
/// <param name="arguments">The arguments to be passed.</param>
1034-
/// <param name="executionContext">The execution context to execute the test method on.</param>
1035991
/// <returns>The result of execution.</returns>
1036992
[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)
993+
private async Task<TestResult> ExecuteInternalWithTimeoutAsync(object?[]? arguments)
1038994
{
1039995
DebugEx.Assert(IsTimeoutSet, "Timeout should be set");
1040996

@@ -1058,7 +1014,7 @@ private async Task<TestResult> ExecuteInternalWithTimeoutAsync(object?[]? argume
10581014

10591015
try
10601016
{
1061-
return await ExecuteInternalAsync(arguments, executionContext, timeoutTokenSource);
1017+
return await ExecuteInternalAsync(arguments, timeoutTokenSource);
10621018
}
10631019
catch (OperationCanceledException)
10641020
{
@@ -1096,7 +1052,7 @@ private async Task<TestResult> ExecuteInternalWithTimeoutAsync(object?[]? argume
10961052

10971053
// It's possible that some failures happened and that the cleanup wasn't executed, so we need to run it here.
10981054
// The method already checks if the cleanup was already executed.
1099-
RunTestCleanupMethod(result, executionContext, null);
1055+
RunTestCleanupMethod(result, null);
11001056
return result;
11011057
}
11021058

@@ -1120,7 +1076,7 @@ private async Task<TestResult> ExecuteInternalWithTimeoutAsync(object?[]? argume
11201076

11211077
// We don't know when the cancellation happened so it's possible that the cleanup wasn't executed, so we need to run it here.
11221078
// The method already checks if the cleanup was already executed.
1123-
RunTestCleanupMethod(timeoutResult, executionContext, null);
1079+
RunTestCleanupMethod(timeoutResult, null);
11241080
return timeoutResult;
11251081

11261082
// Local functions
@@ -1135,7 +1091,7 @@ void ExecuteAsyncAction()
11351091
// dispatched back to the SynchronizationContext which offloads the work to the UI thread.
11361092
// However, the GetAwaiter().GetResult() here will block the current thread which is also the UI thread.
11371093
// So, the continuations will not be able, thus this task never completes.
1138-
result = ExecuteInternalAsync(arguments, executionContext, null).GetAwaiter().GetResult();
1094+
result = ExecuteInternalAsync(arguments, null).GetAwaiter().GetResult();
11391095
}
11401096
catch (Exception ex)
11411097
{

0 commit comments

Comments
 (0)