Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if NETFRAMEWORK
Expand Down Expand Up @@ -117,14 +117,16 @@ private async Task<TestResult> ExecuteInternalAsync(object?[]? arguments, Cancel
// For any failure after this point, we must run TestCleanup
_isTestContextSet = true;

if (await RunTestInitializeMethodAsync(_classInstance!, result, timeoutTokenSource).ConfigureAwait(false))
// Intentionally using ConfigureAwait(true) here to ensure the continuation is posted to the synchronization context.
// In case of WinUI's default synchronization context, this will ensure that the test method runs on the UI thread.
if (await RunTestInitializeMethodAsync(_classInstance!, result, timeoutTokenSource).ConfigureAwait(true))
{
if (_executionContext is null)
{
Task? invokeResult = MethodInfo.GetInvokeResultAsync(_classInstance, arguments);
if (invokeResult is not null)
{
await invokeResult.ConfigureAwait(false);
await invokeResult.ConfigureAwait(true);
}
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,60 @@ public async Task TestMethodInfoInvokeShouldCallAsyncTestInitializeAndWaitForCom
result.Outcome.Should().Be(UnitTestOutcome.Passed);
}

public async Task TestMethodInfo_AsyncTestInitialize_PreservesSynchronizationContextForTestMethod()
Comment thread
Evangelink marked this conversation as resolved.
{
// Arrange
SynchronizationContext? capturedContextInTestMethod = null;

DummyTestClass.TestInitializeMethodBodyAsync = async _ => await Task.Delay(1);
Comment thread
Evangelink marked this conversation as resolved.
DummyTestClass.TestMethodBody = _ => capturedContextInTestMethod = SynchronizationContext.Current;
_testClassInfo.TestInitializeMethod = typeof(DummyTestClass).GetMethod("DummyTestInitializeMethodAsync")!;

// Create a TestMethodInfo without a timeout so we go through ExecuteInternalAsync directly
// (not the non-cooperative timeout path which runs on a separate thread pool thread).
var testMethodInfo = new TestMethodInfo(_methodInfo, _testClassInfo)
{
Executor = _testMethodAttribute,
};

using var syncContext = new SingleThreadedSynchronizationContextForTesting();
TestResult result = await RunInvokeAsyncOnContextAsync(testMethodInfo, syncContext);

// Assert: the SynchronizationContext should be preserved in the test method even after async TestInitialize
result.Outcome.Should().Be(UnitTestOutcome.Passed);
capturedContextInTestMethod.Should().BeSameAs(syncContext);
}

public async Task TestMethodInfo_AsyncTestCleanup_PreservesSynchronizationContextForTestCleanup()
{
// Arrange
SynchronizationContext? capturedContextInTestCleanup = null;

DummyTestClass.TestInitializeMethodBodyAsync = async _ => await Task.Delay(1);
DummyTestClass.TestCleanupMethodBodyAsync = async _ =>
{
capturedContextInTestCleanup = SynchronizationContext.Current;
Comment thread
Evangelink marked this conversation as resolved.
await Task.Delay(1);
};
DummyTestClass.TestMethodBody = _ => { };
_testClassInfo.TestInitializeMethod = typeof(DummyTestClass).GetMethod("DummyTestInitializeMethodAsync")!;
_testClassInfo.TestCleanupMethod = typeof(DummyTestClass).GetMethod("DummyTestCleanupMethodAsync")!;
Comment thread
Evangelink marked this conversation as resolved.

// Create a TestMethodInfo without a timeout so we go through ExecuteInternalAsync directly
// (not the non-cooperative timeout path which runs on a separate thread pool thread).
var testMethodInfo = new TestMethodInfo(_methodInfo, _testClassInfo)
{
Executor = _testMethodAttribute,
};

using var syncContext = new SingleThreadedSynchronizationContextForTesting();
TestResult result = await RunInvokeAsyncOnContextAsync(testMethodInfo, syncContext);

// Assert: the SynchronizationContext should be preserved at the start of TestCleanup
result.Outcome.Should().Be(UnitTestOutcome.Passed);
capturedContextInTestCleanup.Should().BeSameAs(syncContext);
}

public async Task TestMethodInfoInvokeShouldCallTestInitializeOfAllBaseClasses()
{
var callOrder = new List<string>();
Expand Down Expand Up @@ -1625,9 +1679,90 @@ private static async Task RunWithTestablePlatformService(TestablePlatformService
}
}

private static async Task<TestResult> RunInvokeAsyncOnContextAsync(
TestMethodInfo info,
SingleThreadedSynchronizationContextForTesting ctx)
{
var tcs = new TaskCompletionSource<TestResult>(TaskCreationOptions.RunContinuationsAsynchronously);
ctx.Post(
_ => _ = info.InvokeAsync(null).ContinueWith(
t =>
{
if (t.IsFaulted)
{
tcs.SetException(t.Exception!);
}
else if (t.IsCanceled)
{
tcs.SetCanceled();
}
else
{
tcs.SetResult(t.Result);
}
},
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.Default),
null);

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
Task completed = await Task.WhenAny(tcs.Task, Task.Delay(Timeout.Infinite, cts.Token));
completed.Should().BeSameAs(tcs.Task, "Timed out waiting for InvokeAsync to complete.");

return await tcs.Task;
}

#endregion

#region Test data

private sealed class SingleThreadedSynchronizationContextForTesting : SynchronizationContext, IDisposable
{
private readonly BlockingCollection<(SendOrPostCallback Callback, object? State)> _queue = [];
private readonly Thread _thread;

public SingleThreadedSynchronizationContextForTesting()
{
_thread = new Thread(() =>
{
SynchronizationContext.SetSynchronizationContext(this);
foreach ((SendOrPostCallback callback, object? state) in _queue.GetConsumingEnumerable())
{
callback(state);
}
})
Comment thread
Evangelink marked this conversation as resolved.
{
Comment thread
Evangelink marked this conversation as resolved.
IsBackground = true,
};
_thread.Start();
}

public override void Post(SendOrPostCallback d, object? state)
{
try
{
_queue.TryAdd((d, state));
}
catch (InvalidOperationException)
{
// Collection completed (after Dispose called CompleteAdding) or disposed
}
}

public void Dispose()
{
_queue.CompleteAdding();
if (!_thread.Join(TimeSpan.FromSeconds(5)))
{
// Thread is background, so it will be killed on process exit.
// Avoid hanging the test run indefinitely.
}

_queue.Dispose();
}
}

public class DummyTestClassBase
{
public static Action<DummyTestClassBase> BaseTestClassMethodBody { get; set; } = null!;
Expand Down