diff --git a/TerminalGuiFluentAssertions/Class1.cs b/TerminalGuiFluentAssertions/Class1.cs index 76148bfa6..7e0a078f4 100644 --- a/TerminalGuiFluentAssertions/Class1.cs +++ b/TerminalGuiFluentAssertions/Class1.cs @@ -8,8 +8,18 @@ using static Unix.Terminal.Curses; namespace TerminalGuiFluentAssertions; -class FakeInput(CancellationToken hardStopToken) : IConsoleInput +class FakeInput : IConsoleInput { + private readonly CancellationToken _hardStopToken; + + private readonly CancellationTokenSource _timeoutCts; + public FakeInput (CancellationToken hardStopToken) + { + _hardStopToken = hardStopToken; + + // Create a timeout-based cancellation token too to prevent tests ever fully hanging + _timeoutCts = new (With.Timeout); + } /// public void Dispose () { } @@ -22,7 +32,7 @@ class FakeInput(CancellationToken hardStopToken) : IConsoleInput public void Run (CancellationToken token) { // Blocks until either the token or the hardStopToken is cancelled. - WaitHandle.WaitAny (new [] { token.WaitHandle, hardStopToken.WaitHandle }); + WaitHandle.WaitAny (new [] { token.WaitHandle, _hardStopToken.WaitHandle, _timeoutCts.Token.WaitHandle }); } } @@ -92,13 +102,18 @@ public static class With /// public static GuiTestContext A (int width, int height) where T : Toplevel, new () { - return new GuiTestContext (width,height); + return new (width,height); } + + /// + /// The global timeout to allow for any given application to run for before shutting down. + /// + public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds (30); } public class GuiTestContext : IDisposable where T : Toplevel, new() { private readonly CancellationTokenSource _cts = new (); - private readonly CancellationTokenSource _hardStop = new (); + private readonly CancellationTokenSource _hardStop = new (With.Timeout); private readonly Task _runTask; private Exception _ex; private readonly FakeOutput _output = new (); @@ -120,6 +135,7 @@ public class GuiTestContext : IDisposable where T : Toplevel, new() () => winInput, () => _output); + var booting = new SemaphoreSlim (0, 1); // Start the application in a background thread _runTask = Task.Run (() => @@ -130,6 +146,8 @@ public class GuiTestContext : IDisposable where T : Toplevel, new() v2.Init (null,"v2win"); + booting.Release (); + Application.Run (); // This will block, but it's on a background thread now Application.Shutdown (); @@ -146,6 +164,12 @@ public class GuiTestContext : IDisposable where T : Toplevel, new() } }, _cts.Token); + // Wait for booting to complete with a timeout to avoid hangs + if (!booting.WaitAsync (TimeSpan.FromSeconds (5)).Result) + { + throw new TimeoutException ("Application failed to start within the allotted time."); + } + WaitIteration (); } @@ -183,6 +207,12 @@ public class GuiTestContext : IDisposable where T : Toplevel, new() public void Dispose () { Stop (); + + if (_hardStop.IsCancellationRequested) + { + throw new Exception ( + "Application was hard stopped, typically this means it timed out or did not shutdown gracefully. Ensure you call Stop in your test"); + } _hardStop.Cancel(); } diff --git a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs index a5ddec5ee..080b42464 100644 --- a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs @@ -77,11 +77,7 @@ public class BasicFluentAssertionTests .RightClick(1,1) .ScreenShot ("After open menu",_out) .LeftClick (3, 3) - /*.Assert (Application.Top.Focused.Should ().BeAssignableTo(typeof(MenuBarItem))) - .Down() - .Enter()*/ .Stop (); Assert.True (clicked); - } }