Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
46 changes: 45 additions & 1 deletion 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 @@ -88,7 +90,49 @@ protected async override ValueTask<TimeSpan> RunTest(UITestRunnerContext ctxt)

TaskCompletionSource<object?> finished = new();

if (executionContext is not null)
int timeout = ctxt.Test.Timeout;
if (timeout > 0 && !Debugger.IsAttached)
{
// Post the test to the UI thread rather than invoking it inline,
// so that a test that blocks the UI thread cannot prevent us from observing the timeout.
Stopwatch stopwatch = Stopwatch.StartNew();
Comment thread
AArnott marked this conversation as resolved.
Resolved
ExecutionContext? postedExecutionContext = executionContext ?? ExecutionContext.Capture();
ctxt.ThreadRental.SynchronizationContext.Post(
_ =>
{
if (postedExecutionContext is not null)
{
ExecutionContext.Run(postedExecutionContext, RunTest, null);
}
else
{
RunTest(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)
{
// Like xunit, we report the timeout without waiting for the test to finish.
// The test continues on the UI thread until it completes or observes the cancellation token.
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 elapsedTime + stopwatch.Elapsed;
}
}
else if (executionContext is not null)
{
ExecutionContext.Run(executionContext, RunTest, null);
}
Expand Down
28 changes: 28 additions & 0 deletions test/Xunit.StaFact.Tests/UIFactTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,34 @@ public async Task PassAfterDelay()
await Task.Delay(10);
}

[DesktopFact(Timeout = 30_000)]
public async Task Timeout_NotExceeded()
{
Assert.Equal(30_000, Assert.IsAssignableFrom<Xunit.v3.IXunitTestCase>(TestContext.Current.TestCase).Timeout);
Assert.Equal(this.ctorThreadId, Environment.CurrentManagedThreadId);
Assert.Same(this.ctorSyncContext, SynchronizationContext.Current);
await Task.Yield();
Assert.Equal(this.ctorThreadId, Environment.CurrentManagedThreadId);
Assert.Same(this.ctorSyncContext, SynchronizationContext.Current);
TestContext.Current.CancellationToken.ThrowIfCancellationRequested();
}

#pragma warning disable xUnit1069 // Deliberately ignore the CancellationToken to simulate a blocked UI thread.
[DesktopFact(Timeout = 100), Trait("TestCategory", "FailureExpected")]
Comment thread
AArnott marked this conversation as resolved.
Resolved
public void Timeout_Exceeded_Sync()
{
// Deliberately block the UI thread without honoring the CancellationToken.
Thread.Sleep(2000);
Assert.Fail("The test should have timed out.");
}
#pragma warning restore xUnit1069

[DesktopFact(Timeout = 100), Trait("TestCategory", "FailureExpected")]
public async Task Timeout_Exceeded_Async()
{
await Task.Delay(2000, TestContext.Current.CancellationToken);
}

[DesktopFact, Trait("TestCategory", "FailureExpected")]
public async Task FailAfterYield()
{
Expand Down
33 changes: 33 additions & 0 deletions test/Xunit.StaFact.Tests/UITheoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,39 @@ public async Task PassAfterDelay(int arg)
Assert.Equal(0, arg);
}

[DesktopTheory(Timeout = 30_000)]
[InlineData(0)]
public async Task Timeout_NotExceeded(int arg)
{
Assert.Equal(30_000, Assert.IsAssignableFrom<Xunit.v3.IXunitTestCase>(TestContext.Current.TestCase).Timeout);
Assert.Equal(this.ctorThreadId, Environment.CurrentManagedThreadId);
Assert.Same(this.ctorSyncContext, SynchronizationContext.Current);
await Task.Yield();
Assert.Equal(this.ctorThreadId, Environment.CurrentManagedThreadId);
Assert.Same(this.ctorSyncContext, SynchronizationContext.Current);
TestContext.Current.CancellationToken.ThrowIfCancellationRequested();
Assert.Equal(0, arg);
}

#pragma warning disable xUnit1069 // Deliberately ignore the CancellationToken to simulate a blocked UI thread.
[DesktopTheory(Timeout = 100), Trait("TestCategory", "FailureExpected")]
Comment thread
AArnott marked this conversation as resolved.
Resolved
[InlineData(0)]
public void Timeout_Exceeded_Sync(int arg)
{
// Deliberately block the UI thread without honoring the CancellationToken.
Thread.Sleep(2000);
Assert.Fail($"The test should have timed out. Arg: {arg}");
}
#pragma warning restore xUnit1069

[DesktopTheory(Timeout = 100), Trait("TestCategory", "FailureExpected")]
[InlineData(0)]
public async Task Timeout_Exceeded_Async(int arg)
{
await Task.Delay(2000, TestContext.Current.CancellationToken);
Assert.Equal(0, arg);
}

[DesktopTheory, Trait("TestCategory", "FailureExpected")]
[InlineData(0)]
public async Task FailAfterYield(int arg)
Expand Down
Loading