Skip to content

Commit aea689c

Browse files
[rel/3.9] Run the whole ExecuteInternal logic under the right execution context (#5646)
Co-authored-by: Youssef1313 <youssefvictor00@gmail.com>
1 parent 6bed8dd commit aea689c

File tree

3 files changed

+136
-80
lines changed

3 files changed

+136
-80
lines changed

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

Lines changed: 105 additions & 72 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-
});
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
{

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/GenericTestMethodTests.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,18 @@ public async Task TestDifferentGenericMethodTestCases()
3838
failed ParameterizedMethodSimple \(null\) \(\d+ms\)
3939
Test method TestClass\.ParameterizedMethodSimple threw exception:
4040
System\.InvalidOperationException: The type of the generic parameter 'T' could not be inferred\.
41-
.+?
4241
failed ParameterizedMethodTwoGenericParametersAndFourMethodParameters \(1,"Hello world",2,3\) \(\d+ms\)
4342
Test method TestClass\.ParameterizedMethodTwoGenericParametersAndFourMethodParameters threw exception:
4443
System\.InvalidOperationException: Found two conflicting types for generic parameter 'T2'\. The conflicting types are 'System\.Byte' and 'System\.Int32'\.
45-
.+?
4644
failed ParameterizedMethodTwoGenericParametersAndFourMethodParameters \(null,"Hello world","Hello again",3\) \(\d+ms\)
4745
Assert\.Fail failed\. Test method 'ParameterizedMethodTwoGenericParametersAndFourMethodParameters' did run with parameters '<null>', 'Hello world', 'Hello again', '3' and generic types 'System\.Int32', 'System\.String'\.
4846
.+?
4947
failed ParameterizedMethodTwoGenericParametersAndFourMethodParameters \("Hello hello","Hello world",null,null\) \(\d+ms\)
5048
Test method TestClass\.ParameterizedMethodTwoGenericParametersAndFourMethodParameters threw exception:
5149
System\.InvalidOperationException: The type of the generic parameter 'T1' could not be inferred\.
52-
.+?
5350
failed ParameterizedMethodTwoGenericParametersAndFourMethodParameters \(null,null,null,null\) \(\d+ms\)
5451
Test method TestClass\.ParameterizedMethodTwoGenericParametersAndFourMethodParameters threw exception:
5552
System\.InvalidOperationException: The type of the generic parameter 'T1' could not be inferred\.
56-
.+?
5753
failed ParameterizedMethodSimpleParams \(1\) \(\d+ms\)
5854
Cannot create an instance of T\[] because Type\.ContainsGenericParameters is true\.
5955
.+?

0 commit comments

Comments
 (0)