From 323c6240461e80875af7696d447226aefb3452ac Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 14:50:58 +0000 Subject: [PATCH] WIP: Add fluent assertions and prototype the kind of things that should be possible --- Directory.Packages.props | 93 ++++++++-------- TerminalGuiFluentAssertions/Class1.cs | 103 ++++++++++++++---- .../TerminalGuiFluentAssertions.csproj | 4 + .../FluentTests/BasicFluentAssertionTests.cs | 29 +++-- 4 files changed, 147 insertions(+), 82 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 18afbe64a..fbed06377 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,52 +1,45 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TerminalGuiFluentAssertions/Class1.cs b/TerminalGuiFluentAssertions/Class1.cs index 596be8d47..0ab4296f5 100644 --- a/TerminalGuiFluentAssertions/Class1.cs +++ b/TerminalGuiFluentAssertions/Class1.cs @@ -1,10 +1,12 @@ using System.Collections.Concurrent; using System.Drawing; +using FluentAssertions; +using FluentAssertions.Numeric; using Terminal.Gui; namespace TerminalGuiFluentAssertions; -class FakeInput : IConsoleInput +class FakeInput(CancellationToken hardStopToken) : IConsoleInput { /// public void Dispose () { } @@ -15,17 +17,17 @@ class FakeInput : IConsoleInput /// public void Run (CancellationToken token) { - // Simulate an infinite loop that checks for cancellation - token.WaitHandle.WaitOne (); // Blocks until the token is cancelled + // Blocks until either the token or the hardStopToken is cancelled. + WaitHandle.WaitAny (new [] { token.WaitHandle, hardStopToken.WaitHandle }); } } -class FakeNetInput : FakeInput, INetInput +class FakeNetInput (CancellationToken hardStopToken) : FakeInput (hardStopToken), INetInput { } -class FakeWindowsInput : FakeInput, IWindowsInput +class FakeWindowsInput (CancellationToken hardStopToken) : FakeInput (hardStopToken), IWindowsInput { } @@ -87,29 +89,29 @@ public static class With return new GuiTestContext (width,height); } } -public class GuiTestContext where T : Toplevel, new() +public class GuiTestContext : IDisposable where T : Toplevel, new() { - private readonly CancellationTokenSource _cts; + private readonly CancellationTokenSource _cts = new (); + private readonly CancellationTokenSource _hardStop = new (); private readonly Task _runTask; + private Exception _ex; + private readonly FakeOutput _output = new (); internal GuiTestContext (int width, int height) { IApplication origApp = ApplicationImpl.Instance; - var netInput = new FakeNetInput (); - var winInput = new FakeWindowsInput (); - var output = new FakeOutput (); + var netInput = new FakeNetInput (_cts.Token); + var winInput = new FakeWindowsInput (_cts.Token); - output.Size = new (width, height); + _output.Size = new (width, height); var v2 = new ApplicationV2( () => netInput, - ()=>output, + ()=>_output, () => winInput, - () => output); + () => _output); - // Create a cancellation token - _cts = new (); // Start the application in a background thread _runTask = Task.Run (() => @@ -125,31 +127,90 @@ public class GuiTestContext where T : Toplevel, new() Application.Shutdown (); } catch (OperationCanceledException) + { } + catch (Exception ex) { - + _ex = ex; } finally { ApplicationImpl.ChangeInstance (origApp); } }, _cts.Token); - - Application.Shutdown (); } /// /// Stops the application and waits for the background thread to exit. /// - public void Stop () + public GuiTestContext Stop () { + if (_runTask.IsCompleted) + { + return this; + } + + Application.Invoke (()=> Application.RequestStop ()); + + // Wait for the application to stop, but give it a 1-second timeout + if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000))) + { + _cts.Cancel (); + // Timeout occurred, force the task to stop + _hardStop.Cancel (); + throw new TimeoutException ("Application failed to stop within the allotted time."); + } _cts.Cancel (); - Application.Invoke (()=>Application.RequestStop()); - _runTask.Wait (); // Ensure the background thread exits + + if (_ex != null) + { + throw _ex; // Propagate any exception that happened in the background task + } + + return this; } // Cleanup to avoid state bleed between tests public void Dispose () { + Stop (); + _hardStop.Cancel(); + } + + /// + /// Adds the given to the current top level view + /// and performs layout. + /// + /// + /// + public GuiTestContext Add (View v) + { + Application.Invoke ( + () => + { + var top = Application.Top ?? throw new Exception("Top was null so could not add view"); + top.Add (v); + top.Layout (); + }); + + return this; + } + + public GuiTestContext ResizeConsole (int width, int height) + { + _output.Size = new Size (width,height); + + return WaitIteration (); + } + public GuiTestContext WaitIteration () + { + Application.Invoke (() => { }); + + return this; + } + + public GuiTestContext Assert (AndConstraint be) + { + return this; } } diff --git a/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj b/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj index 629e31cbe..5654a7235 100644 --- a/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj +++ b/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj @@ -6,6 +6,10 @@ enable + + + + diff --git a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs index 1a3473c6c..0f35ede61 100644 --- a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using FluentAssertions; using TerminalGuiFluentAssertions; namespace UnitTests.FluentTests; @@ -11,25 +12,31 @@ public class BasicFluentAssertionTests [Fact] public void GuiTestContext_StartsAndStopsWithoutError () { - var context = With.A (40, 10); + using var context = With.A (40, 10); // No actual assertions are needed — if no exceptions are thrown, it's working context.Stop (); } [Fact] - public void Test () + public void GuiTestContext_ForgotToStop () { - var myView = new TextField () { Width = 10 }; + using var context = With.A (40, 10); + } + [Fact] + public void TestWindowsResize () + { + var lbl = new Label () + { + Width = Dim.Fill () + }; + using var c = With.A (40, 10) + .Add (lbl ) + .Assert (lbl.Frame.Width.Should().Be(40)) + .ResizeConsole (20,20) + .Assert (lbl.Frame.Width.Should ().Be (20)) + .Stop (); - - /* - using var ctx = With.A (20, 10) - .Add (myView, 3, 2) - .Focus (myView) - .Type ("Hello"); - - Assert.Equal ("Hello", myView.Text);*/ } }