From c2e4a58fa8697830a23956b0542d020e8f7f8624 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 03:44:17 +0000 Subject: [PATCH 1/7] Initial plan From 33e175309fc26ad3fdd3332e6805e19040a40894 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 03:52:59 +0000 Subject: [PATCH 2/7] Refactor AppTestHelper to RunAsync Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/259ae3cf-ff29-45bc-83a9-ba299adadaff Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Tests/AppTestHelpers/AppTestHelper.cs | 34 ++++++++++------ Tests/AppTestHelpers/With.cs | 39 ++++++++++++++----- .../FluentTests/TestContextKeyEventTests.cs | 16 +++++--- .../FluentTests/TestContextTests.cs | 26 +++++++++++++ 4 files changed, 89 insertions(+), 26 deletions(-) diff --git a/Tests/AppTestHelpers/AppTestHelper.cs b/Tests/AppTestHelpers/AppTestHelper.cs index 2056ca44df..7f6cff8739 100644 --- a/Tests/AppTestHelpers/AppTestHelper.cs +++ b/Tests/AppTestHelpers/AppTestHelper.cs @@ -108,7 +108,7 @@ public AppTestHelper (string driverName, TextWriter? logWriter = null, TimeSpan? } /// - /// Constructor for tests that need to run the application with Application.Run. + /// Constructor for tests that need to run the application with IApplication.RunAsync. /// internal AppTestHelper (Func runnableBuilder, int width, int height, string driverName, TextWriter? logWriter = null, TimeSpan? timeout = null) { @@ -120,7 +120,7 @@ internal AppTestHelper (Func runnableBuilder, int width, int height, CommonInit (width, height, timeout); // Start the application in a background thread - _runTask = Task.Run (() => + _runTask = Task.Run (async () => { _loggerScope = Logging.PushLogger (_testLogger!); @@ -140,18 +140,20 @@ internal AppTestHelper (Func runnableBuilder, int width, int height, _booting.Release (); } - if (App is { Initialized: true }) + if (App is { Initialized: true } app) { IRunnable runnable = runnableBuilder (); - runnable.IsRunningChanged += (s, e) => + runnable.IsRunningChanged += (_, e) => { if (!e.Value) { Finished = true; } }; - App?.Run (runnable); // This will block, but it's on a background thread now + + CancellationToken runToken = _ansiInput.ExternalCancellationTokenSource!.Token; + await app.RunAsync (runnable, runToken); if (runnable is View runnableView) { @@ -159,7 +161,7 @@ internal AppTestHelper (Func runnableBuilder, int width, int height, } //Logging.Trace ("Application.Run completed"); - App?.Dispose (); + app.Dispose (); _runCancellationTokenSource.Cancel (); } } @@ -321,7 +323,9 @@ public AppTestHelper WaitIteration (Action? action = null) { action = app => { }; } + CancellationTokenSource ctsActionCompleted = new (); + Exception? actionException = null; App?.Invoke (app => { @@ -336,14 +340,22 @@ public AppTestHelper WaitIteration (Action? action = null) { Logging.Warning ($"Action failed with exception: {e}"); _backgroundException = e; + actionException = e; _ansiInput.ExternalCancellationTokenSource?.Cancel (); } - }); + }); + + // Blocks until either the action completes, the run stops, or the timeout/hard-stop token is cancelled. + WaitHandle.WaitAny ([ + _runCancellationTokenSource.Token.WaitHandle, + ctsActionCompleted.Token.WaitHandle, + _ansiInput.ExternalCancellationTokenSource!.Token.WaitHandle + ]); - // Blocks until either the token or the hardStopToken is cancelled. - // With linked tokens, we only need to wait on _runCancellationTokenSource and ctsLocal - // ExternalCancellationTokenSource is redundant because it's linked to _runCancellationTokenSource - WaitHandle.WaitAny ([_runCancellationTokenSource.Token.WaitHandle, ctsActionCompleted.Token.WaitHandle]); + if (actionException is { }) + { + throw actionException; + } // Logging.Trace ($"Return from WaitIteration"); return this; diff --git a/Tests/AppTestHelpers/With.cs b/Tests/AppTestHelpers/With.cs index acfeac4274..56742c6ffb 100644 --- a/Tests/AppTestHelpers/With.cs +++ b/Tests/AppTestHelpers/With.cs @@ -13,29 +13,50 @@ public static class With /// /// /// + /// /// - public static AppTestHelper A (int width, int height, string driverName, TextWriter? logWriter = null) where T : IRunnable, new() + public static AppTestHelper A ( + int width, + int height, + string driverName, + TextWriter? logWriter = null, + TimeSpan? timeout = null + ) where T : IRunnable, new () { - return new (() => new T () - { - //Id = $"{typeof (T).Name}" - }, width, height, - driverName, logWriter, Timeout); + return new ( + () => new T () + { + //Id = $"{typeof (T).Name}" + }, + width, + height, + driverName, + logWriter, + timeout ?? Timeout); } /// - /// Overload that takes a function to create instance after application is initialized. + /// Overload that takes a function to create instance after application is initialized. /// /// /// /// /// /// + /// /// - public static AppTestHelper A (Func runnableFactory, int width, int height, string driverName, TextWriter? logWriter = null) + public static AppTestHelper A ( + Func runnableFactory, + int width, + int height, + string driverName, + TextWriter? logWriter = null, + TimeSpan? timeout = null + ) { - return new (runnableFactory, width, height, driverName, logWriter, Timeout); + return new (runnableFactory, width, height, driverName, logWriter, timeout ?? Timeout); } + /// /// The global timeout to allow for any given application to run for before shutting down. /// diff --git a/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs b/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs index a3965ca35e..e8e6468bc1 100644 --- a/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs +++ b/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs @@ -14,12 +14,16 @@ public class TestContextKeyEventTests (ITestOutputHelper outputHelper) : TestsAl [MemberData (nameof (GetAllDriverNames))] public void QuitKey_ViaApplication_Stops (string d) { + IRunnable? top = null; + using AppTestHelper helper = With.A (40, 10, d) - .Then ((app) => - { - app?.Keyboard.RaiseKeyDownEvent (Application.GetDefaultKey (Command.Quit)); - Assert.False (app!.TopRunnable!.IsRunning); - }); + .Then ((app) => + { + top = app!.TopRunnable; + app.Keyboard.RaiseKeyDownEvent (Application.GetDefaultKey (Command.Quit)); + }); + + Assert.True (SpinWait.SpinUntil (() => top is { IsRunning: false }, TimeSpan.FromSeconds (5))); } [Theory] @@ -317,4 +321,4 @@ public void WithTextField_UpdatesText (string d) //Assert.Equal ("Hello", textField.Text); } -} \ No newline at end of file +} diff --git a/Tests/IntegrationTests/FluentTests/TestContextTests.cs b/Tests/IntegrationTests/FluentTests/TestContextTests.cs index 3ae7aa57f0..0d22ec70d5 100644 --- a/Tests/IntegrationTests/FluentTests/TestContextTests.cs +++ b/Tests/IntegrationTests/FluentTests/TestContextTests.cs @@ -99,6 +99,32 @@ public void With_Starts_Stops_Without_Error (string d) // No actual assertions are needed — if no exceptions are thrown, it's working } + [Fact] + [Trait ("Category", "LowLevelDriver")] + public void RunAsync_Timeout_Stops_Application () + { + // Copilot + using AppTestHelper helper = With.A (40, 10, DriverRegistry.Names.ANSI, _out, TimeSpan.FromMilliseconds (50)); + + Assert.True (SpinWait.SpinUntil (() => helper.Finished, TimeSpan.FromSeconds (5))); + Assert.True (helper.Finished); + } + + [Fact] + [Trait ("Category", "LowLevelDriver")] + public void Then_Exception_HardStops_Without_Hanging () + { + // Copilot + using AppTestHelper helper = With.A (40, 10, DriverRegistry.Names.ANSI, _out); + InvalidOperationException expectedException = new ("Expected test failure"); + + InvalidOperationException exception = Assert.Throws ( + () => helper.Then (_ => throw expectedException)); + + Assert.Same (expectedException, exception); + Assert.True (SpinWait.SpinUntil (() => helper.Finished, TimeSpan.FromSeconds (5))); + } + [Theory] [MemberData (nameof (GetAllDriverNames))] public void With_Without_Stop_Still_Cleans_Up (string d) From 149bea69f65af768ec870bec876e596b80fddabb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 03:55:18 +0000 Subject: [PATCH 3/7] Address AppTestHelper review feedback Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/259ae3cf-ff29-45bc-83a9-ba299adadaff Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Tests/AppTestHelpers/AppTestHelper.cs | 45 ++++++++++--------- .../FluentTests/TestContextKeyEventTests.cs | 10 ++--- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/Tests/AppTestHelpers/AppTestHelper.cs b/Tests/AppTestHelpers/AppTestHelper.cs index 7f6cff8739..6b92cca20d 100644 --- a/Tests/AppTestHelpers/AppTestHelper.cs +++ b/Tests/AppTestHelpers/AppTestHelper.cs @@ -328,29 +328,32 @@ public AppTestHelper WaitIteration (Action? action = null) Exception? actionException = null; App?.Invoke (app => - { - try - { - action (app); - - //Logging.Trace ("Action completed"); - ctsActionCompleted.Cancel (); - } - catch (Exception e) - { - Logging.Warning ($"Action failed with exception: {e}"); - _backgroundException = e; - actionException = e; - _ansiInput.ExternalCancellationTokenSource?.Cancel (); - } - }); + { + try + { + action (app); + + //Logging.Trace ("Action completed"); + ctsActionCompleted.Cancel (); + } + catch (Exception e) + { + Logging.Warning ($"Action failed with exception: {e}"); + _backgroundException = e; + actionException = e; + _ansiInput.ExternalCancellationTokenSource?.Cancel (); + } + }); // Blocks until either the action completes, the run stops, or the timeout/hard-stop token is cancelled. - WaitHandle.WaitAny ([ - _runCancellationTokenSource.Token.WaitHandle, - ctsActionCompleted.Token.WaitHandle, - _ansiInput.ExternalCancellationTokenSource!.Token.WaitHandle - ]); + WaitHandle [] waitHandles = + [ + _runCancellationTokenSource.Token.WaitHandle, + ctsActionCompleted.Token.WaitHandle, + _ansiInput.ExternalCancellationTokenSource!.Token.WaitHandle + ]; + + WaitHandle.WaitAny (waitHandles); if (actionException is { }) { diff --git a/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs b/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs index e8e6468bc1..0a9d36869c 100644 --- a/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs +++ b/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs @@ -17,11 +17,11 @@ public void QuitKey_ViaApplication_Stops (string d) IRunnable? top = null; using AppTestHelper helper = With.A (40, 10, d) - .Then ((app) => - { - top = app!.TopRunnable; - app.Keyboard.RaiseKeyDownEvent (Application.GetDefaultKey (Command.Quit)); - }); + .Then (app => + { + top = app!.TopRunnable; + app.Keyboard.RaiseKeyDownEvent (Application.GetDefaultKey (Command.Quit)); + }); Assert.True (SpinWait.SpinUntil (() => top is { IsRunning: false }, TimeSpan.FromSeconds (5))); } From cdcaec994b633fe90596ee06556b5503346a6308 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 03:57:26 +0000 Subject: [PATCH 4/7] Address final AppTestHelper validation feedback Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/259ae3cf-ff29-45bc-83a9-ba299adadaff Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Tests/AppTestHelpers/AppTestHelper.cs | 6 ++++-- Tests/AppTestHelpers/With.cs | 2 +- Tests/IntegrationTests/FluentTests/TestContextTests.cs | 7 ++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Tests/AppTestHelpers/AppTestHelper.cs b/Tests/AppTestHelpers/AppTestHelper.cs index 6b92cca20d..0f75c44e49 100644 --- a/Tests/AppTestHelpers/AppTestHelper.cs +++ b/Tests/AppTestHelpers/AppTestHelper.cs @@ -1,5 +1,6 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Drawing; +using System.Runtime.ExceptionServices; using System.Text; using Microsoft.Extensions.Logging; using Terminal.Gui.Time; @@ -109,6 +110,7 @@ public AppTestHelper (string driverName, TextWriter? logWriter = null, TimeSpan? /// /// Constructor for tests that need to run the application with IApplication.RunAsync. + /// The runnable observes the helper's linked cancellation token, including timeout cancellation. /// internal AppTestHelper (Func runnableBuilder, int width, int height, string driverName, TextWriter? logWriter = null, TimeSpan? timeout = null) { @@ -357,7 +359,7 @@ public AppTestHelper WaitIteration (Action? action = null) if (actionException is { }) { - throw actionException; + ExceptionDispatchInfo.Capture (actionException).Throw (); } // Logging.Trace ($"Return from WaitIteration"); diff --git a/Tests/AppTestHelpers/With.cs b/Tests/AppTestHelpers/With.cs index 56742c6ffb..73904f89e3 100644 --- a/Tests/AppTestHelpers/With.cs +++ b/Tests/AppTestHelpers/With.cs @@ -36,7 +36,7 @@ public static AppTestHelper A ( } /// - /// Overload that takes a function to create instance after application is initialized. + /// Overload that takes a function to create instance after application is initialized. /// /// /// diff --git a/Tests/IntegrationTests/FluentTests/TestContextTests.cs b/Tests/IntegrationTests/FluentTests/TestContextTests.cs index 0d22ec70d5..cb4bce1eb8 100644 --- a/Tests/IntegrationTests/FluentTests/TestContextTests.cs +++ b/Tests/IntegrationTests/FluentTests/TestContextTests.cs @@ -104,7 +104,12 @@ public void With_Starts_Stops_Without_Error (string d) public void RunAsync_Timeout_Stops_Application () { // Copilot - using AppTestHelper helper = With.A (40, 10, DriverRegistry.Names.ANSI, _out, TimeSpan.FromMilliseconds (50)); + using AppTestHelper helper = With.A ( + 40, + 10, + DriverRegistry.Names.ANSI, + _out, + TimeSpan.FromMilliseconds (50)); Assert.True (SpinWait.SpinUntil (() => helper.Finished, TimeSpan.FromSeconds (5))); Assert.True (helper.Finished); From 66546aa9ebc584c2be5190c0a2b5cfa2d9935e0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 03:59:02 +0000 Subject: [PATCH 5/7] Clarify AppTestHelper cancellation token Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/259ae3cf-ff29-45bc-83a9-ba299adadaff Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Tests/AppTestHelpers/AppTestHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/AppTestHelpers/AppTestHelper.cs b/Tests/AppTestHelpers/AppTestHelper.cs index 0f75c44e49..0db80e2d55 100644 --- a/Tests/AppTestHelpers/AppTestHelper.cs +++ b/Tests/AppTestHelpers/AppTestHelper.cs @@ -154,8 +154,8 @@ internal AppTestHelper (Func runnableBuilder, int width, int height, } }; - CancellationToken runToken = _ansiInput.ExternalCancellationTokenSource!.Token; - await app.RunAsync (runnable, runToken); + CancellationToken helperCancellationToken = _ansiInput.ExternalCancellationTokenSource!.Token; + await app.RunAsync (runnable, helperCancellationToken); if (runnable is View runnableView) { From 2616a0b0094d654b42a52f4fa9964bdc09fc5541 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 04:00:50 +0000 Subject: [PATCH 6/7] Clarify AppTestHelper assertions Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/259ae3cf-ff29-45bc-83a9-ba299adadaff Co-authored-by: tig <585482+tig@users.noreply.github.com> --- .../FluentTests/TestContextKeyEventTests.cs | 4 +++- Tests/IntegrationTests/FluentTests/TestContextTests.cs | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs b/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs index 0a9d36869c..2a039ba2b6 100644 --- a/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs +++ b/Tests/IntegrationTests/FluentTests/TestContextKeyEventTests.cs @@ -23,7 +23,9 @@ public void QuitKey_ViaApplication_Stops (string d) app.Keyboard.RaiseKeyDownEvent (Application.GetDefaultKey (Command.Quit)); }); - Assert.True (SpinWait.SpinUntil (() => top is { IsRunning: false }, TimeSpan.FromSeconds (5))); + Assert.True ( + SpinWait.SpinUntil (() => top is { IsRunning: false }, TimeSpan.FromSeconds (5)), + "TopRunnable did not stop within timeout."); } [Theory] diff --git a/Tests/IntegrationTests/FluentTests/TestContextTests.cs b/Tests/IntegrationTests/FluentTests/TestContextTests.cs index cb4bce1eb8..3074b1d050 100644 --- a/Tests/IntegrationTests/FluentTests/TestContextTests.cs +++ b/Tests/IntegrationTests/FluentTests/TestContextTests.cs @@ -111,8 +111,9 @@ public void RunAsync_Timeout_Stops_Application () _out, TimeSpan.FromMilliseconds (50)); - Assert.True (SpinWait.SpinUntil (() => helper.Finished, TimeSpan.FromSeconds (5))); - Assert.True (helper.Finished); + Assert.True ( + SpinWait.SpinUntil (() => helper.Finished, TimeSpan.FromSeconds (5)), + "AppTestHelper did not finish after timeout cancellation."); } [Fact] @@ -127,7 +128,9 @@ public void Then_Exception_HardStops_Without_Hanging () () => helper.Then (_ => throw expectedException)); Assert.Same (expectedException, exception); - Assert.True (SpinWait.SpinUntil (() => helper.Finished, TimeSpan.FromSeconds (5))); + Assert.True ( + SpinWait.SpinUntil (() => helper.Finished, TimeSpan.FromSeconds (5)), + "AppTestHelper did not finish after action failure."); } [Theory] From e4a7ca82c25f9c96c9a8e346cb7f830af020b151 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 20:18:26 +0000 Subject: [PATCH 7/7] Address AppTestHelper RunAsync review feedback Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/d5ca1a27-c203-4ff2-9a39-cbbe1beca629 Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Tests/AppTestHelpers/AppTestHelper.cs | 52 +++++++++++++------ .../FluentTests/TestContextTests.cs | 12 ++--- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/Tests/AppTestHelpers/AppTestHelper.cs b/Tests/AppTestHelpers/AppTestHelper.cs index 0db80e2d55..d68aaf8268 100644 --- a/Tests/AppTestHelpers/AppTestHelper.cs +++ b/Tests/AppTestHelpers/AppTestHelper.cs @@ -144,27 +144,34 @@ internal AppTestHelper (Func runnableBuilder, int width, int height, if (App is { Initialized: true } app) { - IRunnable runnable = runnableBuilder (); + IRunnable? runnable = null; - runnable.IsRunningChanged += (_, e) => - { - if (!e.Value) - { - Finished = true; - } - }; - - CancellationToken helperCancellationToken = _ansiInput.ExternalCancellationTokenSource!.Token; - await app.RunAsync (runnable, helperCancellationToken); + try + { + runnable = runnableBuilder (); - if (runnable is View runnableView) + runnable.IsRunningChanged += (_, e) => + { + if (!e.Value) + { + Finished = true; + } + }; + + CancellationToken helperCancellationToken = _ansiInput.ExternalCancellationTokenSource!.Token; + await app.RunAsync (runnable, helperCancellationToken); + } + finally { - runnableView.Dispose (); + if (runnable is View runnableView) + { + runnableView.Dispose (); + } + + //Logging.Trace ("Application.Run completed"); + app.Dispose (); + _runCancellationTokenSource.Cancel (); } - - //Logging.Trace ("Application.Run completed"); - app.Dispose (); - _runCancellationTokenSource.Cancel (); } } catch (OperationCanceledException) @@ -432,6 +439,17 @@ public AppTestHelper AnsiScreenShot (string title, TextWriter? writer) => writer?.WriteLine (text); }); + /// + /// Cancels the linked token observed by the running application. + /// + /// + public AppTestHelper CancelRun () + { + _ansiInput.ExternalCancellationTokenSource?.Cancel (); + + return this; + } + /// /// Stops the application and waits for the background thread to exit. /// diff --git a/Tests/IntegrationTests/FluentTests/TestContextTests.cs b/Tests/IntegrationTests/FluentTests/TestContextTests.cs index 3074b1d050..a41bc261a6 100644 --- a/Tests/IntegrationTests/FluentTests/TestContextTests.cs +++ b/Tests/IntegrationTests/FluentTests/TestContextTests.cs @@ -101,19 +101,15 @@ public void With_Starts_Stops_Without_Error (string d) [Fact] [Trait ("Category", "LowLevelDriver")] - public void RunAsync_Timeout_Stops_Application () + public void RunAsync_Cancellation_After_Boot_Stops_Application () { // Copilot - using AppTestHelper helper = With.A ( - 40, - 10, - DriverRegistry.Names.ANSI, - _out, - TimeSpan.FromMilliseconds (50)); + using AppTestHelper helper = With.A (40, 10, DriverRegistry.Names.ANSI, _out); + helper.CancelRun (); Assert.True ( SpinWait.SpinUntil (() => helper.Finished, TimeSpan.FromSeconds (5)), - "AppTestHelper did not finish after timeout cancellation."); + "AppTestHelper did not finish after cancellation."); } [Fact]