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
6 changes: 6 additions & 0 deletions docfx/docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ Xunit test attributes | Supported OS's | SynchronizationContext
We also offer a @Xunit.UISettingsAttribute that can be applied to individual test methods or test classes to control the behavior of the various UI test attributes.
This attribute offers a means to add automated retries to a test's execution for unstable tests.

UI fact and theory attributes support a per-test timeout in milliseconds,
covering construction, initialization, invocation, and disposal. A timeout fails the test and
cancels its test cancellation token without waiting for the UI thread to finish. Code that does
not observe cancellation may continue running after the failure is reported. Timeouts are
disabled while a debugger is attached.


[^1]: This is a private @System.Threading.SynchronizationContext that works cross-platform and effectively keeps code running on the test's starting thread the way a GUI application's main thread would do.

Expand Down
9 changes: 8 additions & 1 deletion src/Xunit.StaFact/Sdk/UITestCaseRunner.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using System.Text;

namespace Xunit.Sdk;
Expand Down Expand Up @@ -106,7 +107,13 @@ internal static ValueTask<RunSummary> Run(
async () =>
{
using ThreadRental threadRental = await ThreadRental.CreateAsync(adapter, testCase.TestMethod);
await threadRental.SynchronizationContext;

// Keep timeout reporting independent of a blocked UI thread.
if (testCase.Timeout <= 0 || Debugger.IsAttached)
{
await threadRental.SynchronizationContext;
}

var runner = new UITestCaseRunner(settings, threadRental);
return await runner.Run(
testCase,
Expand Down
65 changes: 65 additions & 0 deletions src/Xunit.StaFact/Sdk/UITestRunner.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information.

using System.Diagnostics;

namespace Xunit.Sdk;

public class UITestRunner : XunitTestRunnerBase<UITestRunnerContext, IXunitTest>
Expand Down Expand Up @@ -72,6 +74,69 @@ protected async override ValueTask<TimeSpan> RunTest(UITestRunnerContext ctxt)
uiSyncContext.SetExceptionAggregator(ctxt.Aggregator);
}

int timeout = ctxt.Test.Timeout;
if (ctxt.Aggregator.HasExceptions || timeout <= 0 || Debugger.IsAttached)
{
return await this.RunTestLifecycle(ctxt);
}

Stopwatch stopwatch = Stopwatch.StartNew();
TaskCompletionSource<TimeSpan> finished = new(TaskCreationOptions.RunContinuationsAsynchronously);
ExecutionContext? executionContext = ExecutionContext.Capture();
ctxt.ThreadRental.SynchronizationContext.Post(
_ =>
{
if (executionContext is not null)
{
ExecutionContext.Run(executionContext, RunLifecycle, null);
}
else
{
RunLifecycle(null);
}
},
null);

using CancellationTokenSource delayCancellation = new();
Task completedTask = await Task.WhenAny(finished.Task, Task.Delay(timeout, delayCancellation.Token)).ConfigureAwait(false);
delayCancellation.Cancel();
if (completedTask == finished.Task)
{
return await finished.Task.ConfigureAwait(false);
}

// Like xunit, report the timeout without waiting for the lifecycle to finish.
// The UI thread continues until the test completes or observes cancellation.
try
{
TestTimeoutException timeoutException = TestTimeoutException.ForTimedOutTest(timeout);
ctxt.Aggregator.Add(timeoutException);
this.UpdateTestContext(null, TestResultState.FromException((decimal)stopwatch.Elapsed.TotalSeconds, timeoutException));
}
finally
{
TestContext.Current.CancelCurrentTest();
}

return stopwatch.Elapsed;

async void RunLifecycle(object? state)
{
try
{
SynchronizationContext.SetSynchronizationContext(
ctxt.ThreadRental.SyncContextAdapter.ShouldSetAsCurrent ? ctxt.ThreadRental.SynchronizationContext : null);
finished.TrySetResult(await this.RunTestLifecycle(ctxt));
}
catch (Exception ex)
{
finished.TrySetException(ex);
}
}
}

private async ValueTask<TimeSpan> RunTestLifecycle(UITestRunnerContext ctxt)
{
object? testClassInstance = null;
TimeSpan elapsedTime = TimeSpan.Zero;

Expand Down
Loading
Loading