From 7e4253cf28428ea80a4773b137d4bd89cf321746 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 30 Mar 2025 12:16:46 +0100 Subject: [PATCH 01/34] Add class for detecting information about console in extensible way --- .../FeatureDetection/ConsoleFeatureFinder.cs | 59 +++++++++++++++++++ .../ConsoleFeatureFinderResults.cs | 16 +++++ .../FeatureDetection/WindowsFeatureSet.cs | 15 +++++ .../ConsoleDrivers/V2/ApplicationV2.cs | 8 ++- 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinder.cs create mode 100644 Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinderResults.cs create mode 100644 Terminal.Gui/ConsoleDrivers/FeatureDetection/WindowsFeatureSet.cs diff --git a/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinder.cs b/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinder.cs new file mode 100644 index 0000000000..1ce9c5af34 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinder.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Win32; + +namespace Terminal.Gui; + +/// +/// Attempts to determine information about the terminal and what features it +/// does/not support based on runtime operations e.g. registry etc +/// +internal class ConsoleFeatureFinder +{ + public ConsoleFeatureFinderResults GetResults () + { + var results = new ConsoleFeatureFinderResults (); + + PlatformID p = Environment.OSVersion.Platform; + results.IsWindows = p is PlatformID.Win32NT or PlatformID.Win32S or PlatformID.Win32Windows; + + if (results.IsWindows) + { + DetectWindowsSpecificFeatures (results.Windows); + } + + return results; + } + + private void DetectWindowsSpecificFeatures (WindowsFeatureSet windowsFeatures) + { + windowsFeatures.ConHostLegacyMode = IsLegacyConsoleEnabled (); + } + + bool IsLegacyConsoleEnabled () + { + try + { + using (RegistryKey key = Registry.CurrentUser.OpenSubKey (@"Console")) + { + if (key != null) + { + object value = key.GetValue ("ForceV2"); + if (value is int intValue) + { + return intValue == 0; // Legacy Mode enabled if ForceV2 is 0 + } + } + } + } + catch (Exception ex) + { + Logging.Warning ("Error reading registry: " + ex.Message); + } + + return false; // Assume new console mode if check fails + } +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinderResults.cs b/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinderResults.cs new file mode 100644 index 0000000000..fd9fa6e8e6 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinderResults.cs @@ -0,0 +1,16 @@ +namespace Terminal.Gui; + +/// +/// Results of console feature detection +/// +internal class ConsoleFeatureFinderResults +{ + public WindowsFeatureSet Windows { get; set; } = new WindowsFeatureSet(); + public bool IsWindows { get; set; } + + /// + public override string ToString () + { + return $"{nameof(IsWindows)}:{IsWindows} {nameof(Windows)}:{Windows}"; + } +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/FeatureDetection/WindowsFeatureSet.cs b/Terminal.Gui/ConsoleDrivers/FeatureDetection/WindowsFeatureSet.cs new file mode 100644 index 0000000000..1d54ab5ac7 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/FeatureDetection/WindowsFeatureSet.cs @@ -0,0 +1,15 @@ +using static Unix.Terminal.Curses; + +namespace Terminal.Gui; + +/// +/// Features specific to the windows operating system +/// +internal class WindowsFeatureSet +{ + + public bool ConHostLegacyMode { get; set; } + + + public override string ToString () { return $"{nameof(ConHostLegacyMode)}:{ConHostLegacyMode}"; } +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 9baeba301f..f4500eef09 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -85,11 +85,15 @@ public override void Init (IConsoleDriver? driver = null, string? driverName = n private void CreateDriver (string? driverName) { - PlatformID p = Environment.OSVersion.Platform; bool definetlyWin = driverName?.Contains ("win") ?? false; bool definetlyNet = driverName?.Contains ("net") ?? false; + var finder = new ConsoleFeatureFinder (); + var result = finder.GetResults (); + + Logging.Logger.LogInformation ($"Feature detection results:{ result}"); + if (definetlyWin) { _coordinator = CreateWindowsSubcomponents (); @@ -98,7 +102,7 @@ private void CreateDriver (string? driverName) { _coordinator = CreateNetSubcomponents (); } - else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) + else if (result.IsWindows) { _coordinator = CreateWindowsSubcomponents (); } From 564e6f1424a847ea2ce8cdcbf6c793ac98d8fff8 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 3 Apr 2025 12:13:07 +0100 Subject: [PATCH 02/34] WIP - Create test for reordering --- TerminalGuiFluentTesting/GuiTestContext.cs | 35 ++++++++- .../FluentTests/BasicFluentAssertionTests.cs | 14 +--- .../FluentTests/TestOutputWriter.cs | 15 ++++ .../FluentTests/TreeViewFluentTests.cs | 77 +++++++++++++++++++ 4 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 Tests/IntegrationTests/FluentTests/TestOutputWriter.cs create mode 100644 Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs diff --git a/TerminalGuiFluentTesting/GuiTestContext.cs b/TerminalGuiFluentTesting/GuiTestContext.cs index 9a1195df81..412fd8ed31 100644 --- a/TerminalGuiFluentTesting/GuiTestContext.cs +++ b/TerminalGuiFluentTesting/GuiTestContext.cs @@ -243,7 +243,18 @@ public GuiTestContext WaitIteration (Action? a = null) /// public GuiTestContext Then (Action doAction) { - doAction (); + try + { + doAction (); + } + catch(Exception) + { + Stop (); + _hardStop.Cancel(); + + throw; + + } return this; } @@ -360,6 +371,7 @@ public GuiTestContext Right () { SendNetKey (k); } + WaitIteration (); break; default: throw new ArgumentOutOfRangeException (); @@ -548,4 +560,25 @@ private void SendWindowsKey (ConsoleKeyMapping.VK specialKey) WaitIteration (); } + + /// + /// Sets the input focus to the given . + /// Throws if focus did not change due to system + /// constraints e.g. + /// is + /// + /// + /// + /// + public GuiTestContext Focus (View toFocus) + { + toFocus.FocusDeepest (NavigationDirection.Forward, TabBehavior.TabStop); + + if (!toFocus.HasFocus) + { + throw new ArgumentException ("Failed to set focus, FocusDeepest did not result in HasFocus becoming true. Ensure view is added and focusable"); + } + + return WaitIteration (); + } } diff --git a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs index 345d7acc19..f79bd1f97b 100644 --- a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs @@ -1,5 +1,4 @@ -using System.Text; -using Terminal.Gui; +using Terminal.Gui; using TerminalGuiFluentTesting; using Xunit.Abstractions; @@ -9,17 +8,6 @@ public class BasicFluentAssertionTests { private readonly TextWriter _out; - public class TestOutputWriter : TextWriter - { - private readonly ITestOutputHelper _output; - - public TestOutputWriter (ITestOutputHelper output) { _output = output; } - - public override void WriteLine (string? value) { _output.WriteLine (value ?? string.Empty); } - - public override Encoding Encoding => Encoding.UTF8; - } - public BasicFluentAssertionTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); } [Theory] diff --git a/Tests/IntegrationTests/FluentTests/TestOutputWriter.cs b/Tests/IntegrationTests/FluentTests/TestOutputWriter.cs new file mode 100644 index 0000000000..62e40e5aea --- /dev/null +++ b/Tests/IntegrationTests/FluentTests/TestOutputWriter.cs @@ -0,0 +1,15 @@ +using System.Text; +using Xunit.Abstractions; + +namespace IntegrationTests.FluentTests; + +public class TestOutputWriter : TextWriter +{ + private readonly ITestOutputHelper _output; + + public TestOutputWriter (ITestOutputHelper output) { _output = output; } + + public override void WriteLine (string? value) { _output.WriteLine (value ?? string.Empty); } + + public override Encoding Encoding => Encoding.UTF8; +} diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs new file mode 100644 index 0000000000..7d521f328f --- /dev/null +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Terminal.Gui; +using TerminalGuiFluentTesting; +using Xunit.Abstractions; + +namespace IntegrationTests.FluentTests; + +public class TreeViewFluentTests +{ + + private readonly TextWriter _out; + + public TreeViewFluentTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void TreeView_AllowReOrdering (V2TestDriver d) + { + var tv = new TreeView () + { + Width = Dim.Fill(), + Height = Dim.Fill() + }; + + TreeNode car; + TreeNode lorry; + TreeNode bike; + + var root = new TreeNode ("Root") + { + Children = [ + car = new TreeNode("Car"), + lorry = new TreeNode("Lorry"), + bike = new TreeNode("Bike") + ] + }; + + tv.AddObject (root); + + + using GuiTestContext context = + With.A (40, 10, d) + .Add (tv) + .Focus(tv) + .WaitIteration () + .ScreenShot ("Before expanding",_out) + .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) + .Then (() => Assert.Null (tv.GetObjectOnRow (1))) + .Right () + .ScreenShot ("After expanding",_out) + .Then (()=>Assert.Equal (root,tv.GetObjectOnRow (0))) + .Then (() => Assert.Equal (car, tv.GetObjectOnRow (1))) + .Then (() => Assert.Equal (lorry, tv.GetObjectOnRow (2))) + .Then (() => Assert.Equal (bike, tv.GetObjectOnRow (3))) + .Then ( + () => + { + // Re order + root.Children = [bike, car, lorry]; + tv.RefreshObject (root); + }) + .WaitIteration () + .ScreenShot ("After re-order",_out) + .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) + .Then (() => Assert.Equal (bike, tv.GetObjectOnRow (1))) + .Then (() => Assert.Equal (car, tv.GetObjectOnRow (2))) + .Then (() => Assert.Equal (lorry, tv.GetObjectOnRow (3))) + .WriteOutLogs (_out); + + context.Stop (); + } + +} From 75a7159a32e099481d9373b1f724f306bde038b9 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 3 Apr 2025 12:28:37 +0100 Subject: [PATCH 03/34] Change Dictionary to List and preserve TreeBuilder order --- Terminal.Gui/Views/TreeView/Branch.cs | 36 ++++++++++++++----------- Terminal.Gui/Views/TreeView/TreeView.cs | 4 +-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index e7a5eb4ca4..96df0b0b92 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -27,7 +27,7 @@ public Branch (TreeView tree, Branch parentBranchIfAny, T model) /// The children of the current branch. This is null until the first call to to avoid /// enumerating the entire underlying hierarchy. /// - public Dictionary> ChildBranches { get; set; } + public List> ChildBranches { get; set; } /// The depth of the current branch. Depth of 0 indicates root level branches. public int Depth { get; } @@ -275,14 +275,14 @@ public virtual void FetchChildren () if (Depth >= tree.MaxDepth) { - children = Enumerable.Empty (); + children = []; } else { - children = tree.TreeBuilder.GetChildren (Model) ?? Enumerable.Empty (); + children = tree.TreeBuilder.GetChildren (Model) ?? []; } - ChildBranches = children.ToDictionary (k => k, val => new Branch (tree, this, val)); + ChildBranches = children.Select (o=>new Branch (tree, this, o)).ToList (); } /// @@ -340,10 +340,10 @@ public void Refresh (bool startAtTop) // we already knew about some children so preserve the state of the old children // first gather the new Children - IEnumerable newChildren = tree.TreeBuilder?.GetChildren (Model) ?? Enumerable.Empty (); + T[] newChildren = tree.TreeBuilder?.GetChildren (Model).ToArray () ?? []; // Children who no longer appear need to go - foreach (T toRemove in ChildBranches.Keys.Except (newChildren).ToArray ()) + foreach (Branch toRemove in ChildBranches.Where (b=>!newChildren.Contains(b.Model)).ToArray ()) { ChildBranches.Remove (toRemove); @@ -357,17 +357,21 @@ public void Refresh (bool startAtTop) // New children need to be added foreach (T newChild in newChildren) { + Branch existingBranch = ChildBranches.FirstOrDefault (b => b.Model.Equals (newChild)); // If we don't know about the child, yet we need a new branch - if (!ChildBranches.ContainsKey (newChild)) + if (existingBranch == null) { - ChildBranches.Add (newChild, new Branch (tree, this, newChild)); + ChildBranches.Add (new (tree, this, newChild)); } else { //we already have this object but update the reference anyway in case Equality match but the references are new - ChildBranches [newChild].Model = newChild; + existingBranch.Model = newChild; } } + + // Order the list + ChildBranches = ChildBranches.OrderBy (b => newChildren.IndexOf (b.Model)).ToList (); } } @@ -381,9 +385,9 @@ internal void CollapseAll () if (ChildBranches is { }) { - foreach (KeyValuePair> child in ChildBranches) + foreach (Branch child in ChildBranches) { - child.Value.CollapseAll (); + child.CollapseAll (); } } } @@ -395,9 +399,9 @@ internal void ExpandAll () if (ChildBranches is { }) { - foreach (KeyValuePair> child in ChildBranches) + foreach (Branch child in ChildBranches) { - child.Value.ExpandAll (); + child.ExpandAll (); } } } @@ -487,9 +491,9 @@ internal void Rebuild () if (IsExpanded) { // if we are expanded we need to update the visible children - foreach (KeyValuePair> child in ChildBranches) + foreach (Branch child in ChildBranches) { - child.Value.Rebuild (); + child.Rebuild (); } } else @@ -526,7 +530,7 @@ private bool IsLast () return this == tree.roots.Values.LastOrDefault (); } - return Parent.ChildBranches.Values.LastOrDefault () == this; + return Parent.ChildBranches.LastOrDefault () == this; } private static Cell NewCell (Attribute attr, Rune r) { return new Cell { Rune = r, Attribute = new (attr) }; } diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index 4ff0a3c89f..a4b33bfe5f 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -847,7 +847,7 @@ public IEnumerable GetChildren (T o) return new T [0]; } - return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0]; + return branch.ChildBranches?.Select (b => b.Model)?.ToArray () ?? new T [0]; } /// Returns the maximum width line in the tree including prefix and expansion symbols. @@ -1488,7 +1488,7 @@ private IEnumerable> AddToLineMap (Branch currentBranch, bool paren if (currentBranch.IsExpanded) { - foreach (Branch subBranch in currentBranch.ChildBranches.Values) + foreach (Branch subBranch in currentBranch.ChildBranches) { foreach (Branch sub in AddToLineMap (subBranch, weMatch, out bool childMatch)) { From c3aa947b2a3c2bee2d101bfe858c07ff4ac4a64f Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 3 Apr 2025 12:47:02 +0100 Subject: [PATCH 04/34] Add test to ensure branch expansion/status remains consistent despite reorder --- .../FluentTests/TreeViewFluentTests.cs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs index 7d521f328f..847d2031b6 100644 --- a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -74,4 +74,91 @@ public void TreeView_AllowReOrdering (V2TestDriver d) context.Stop (); } + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void TreeViewReOrder_PreservesExpansion (V2TestDriver d) + { + var tv = new TreeView () + { + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + TreeNode car; + TreeNode lorry; + TreeNode bike; + + TreeNode mrA; + TreeNode mrB; + + TreeNode mrC; + + TreeNode mrD; + TreeNode mrE; + + var root = new TreeNode ("Root") + { + Children = [ + car = new TreeNode("Car") + { + Children = [ + mrA = new TreeNode("Mr A"), + mrB = new TreeNode("Mr B") + ] + }, + lorry = new TreeNode("Lorry") + { + Children = [ + mrC = new TreeNode("Mr C"), + ] + }, + bike = new TreeNode("Bike") + { + Children = [ + mrD = new TreeNode("Mr D"), + mrE = new TreeNode("Mr E") + ] + } + ] + }; + + tv.AddObject (root); + tv.ExpandAll(); + + using GuiTestContext context = + With.A (40, 13, d) + .Add (tv) + .WaitIteration () + .ScreenShot ("Initial State", _out) + .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) + .Then (() => Assert.Equal (car, tv.GetObjectOnRow (1))) + .Then (() => Assert.Equal (mrA, tv.GetObjectOnRow (2))) + .Then (() => Assert.Equal (mrB, tv.GetObjectOnRow (3))) + .Then (() => Assert.Equal (lorry, tv.GetObjectOnRow (4))) + .Then (() => Assert.Equal (mrC, tv.GetObjectOnRow (5))) + .Then (() => Assert.Equal (bike, tv.GetObjectOnRow (6))) + .Then (() => Assert.Equal (mrD, tv.GetObjectOnRow (7))) + .Then (() => Assert.Equal (mrE, tv.GetObjectOnRow (8))) + .Then ( + () => + { + // Re order + root.Children = [bike, car, lorry]; + tv.RefreshObject (root); + }) + .WaitIteration () + .ScreenShot ("After re-order", _out) + .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) + .Then (() => Assert.Equal (bike, tv.GetObjectOnRow (1))) + .Then (() => Assert.Equal (mrD, tv.GetObjectOnRow (2))) + .Then (() => Assert.Equal (mrE, tv.GetObjectOnRow (3))) + .Then (() => Assert.Equal (car, tv.GetObjectOnRow (4))) + .Then (() => Assert.Equal (mrA, tv.GetObjectOnRow (5))) + .Then (() => Assert.Equal (mrB, tv.GetObjectOnRow (6))) + .Then (() => Assert.Equal (lorry, tv.GetObjectOnRow (7))) + .Then (() => Assert.Equal (mrC, tv.GetObjectOnRow (8))) + .WriteOutLogs (_out); + + context.Stop (); + } } From 387372b8f36753c840b4df71788d52488af88ee9 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 3 Apr 2025 12:49:01 +0100 Subject: [PATCH 05/34] Cleanup code --- .../FluentTests/TreeViewFluentTests.cs | 88 +++++++++---------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs index 847d2031b6..ed931074d7 100644 --- a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Terminal.Gui; +using Terminal.Gui; using TerminalGuiFluentTesting; using Xunit.Abstractions; @@ -11,7 +6,6 @@ namespace IntegrationTests.FluentTests; public class TreeViewFluentTests { - private readonly TextWriter _out; public TreeViewFluentTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); } @@ -20,10 +14,10 @@ public class TreeViewFluentTests [ClassData (typeof (V2TestDrivers))] public void TreeView_AllowReOrdering (V2TestDriver d) { - var tv = new TreeView () + var tv = new TreeView { - Width = Dim.Fill(), - Height = Dim.Fill() + Width = Dim.Fill (), + Height = Dim.Fill () }; TreeNode car; @@ -32,27 +26,27 @@ public void TreeView_AllowReOrdering (V2TestDriver d) var root = new TreeNode ("Root") { - Children = [ - car = new TreeNode("Car"), - lorry = new TreeNode("Lorry"), - bike = new TreeNode("Bike") - ] + Children = + [ + car = new ("Car"), + lorry = new ("Lorry"), + bike = new ("Bike") + ] }; tv.AddObject (root); - using GuiTestContext context = With.A (40, 10, d) .Add (tv) - .Focus(tv) + .Focus (tv) .WaitIteration () - .ScreenShot ("Before expanding",_out) + .ScreenShot ("Before expanding", _out) .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) .Then (() => Assert.Null (tv.GetObjectOnRow (1))) .Right () - .ScreenShot ("After expanding",_out) - .Then (()=>Assert.Equal (root,tv.GetObjectOnRow (0))) + .ScreenShot ("After expanding", _out) + .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) .Then (() => Assert.Equal (car, tv.GetObjectOnRow (1))) .Then (() => Assert.Equal (lorry, tv.GetObjectOnRow (2))) .Then (() => Assert.Equal (bike, tv.GetObjectOnRow (3))) @@ -64,7 +58,7 @@ public void TreeView_AllowReOrdering (V2TestDriver d) tv.RefreshObject (root); }) .WaitIteration () - .ScreenShot ("After re-order",_out) + .ScreenShot ("After re-order", _out) .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) .Then (() => Assert.Equal (bike, tv.GetObjectOnRow (1))) .Then (() => Assert.Equal (car, tv.GetObjectOnRow (2))) @@ -78,7 +72,7 @@ public void TreeView_AllowReOrdering (V2TestDriver d) [ClassData (typeof (V2TestDrivers))] public void TreeViewReOrder_PreservesExpansion (V2TestDriver d) { - var tv = new TreeView () + var tv = new TreeView { Width = Dim.Fill (), Height = Dim.Fill () @@ -98,32 +92,36 @@ public void TreeViewReOrder_PreservesExpansion (V2TestDriver d) var root = new TreeNode ("Root") { - Children = [ - car = new TreeNode("Car") - { - Children = [ - mrA = new TreeNode("Mr A"), - mrB = new TreeNode("Mr B") - ] - }, - lorry = new TreeNode("Lorry") - { - Children = [ - mrC = new TreeNode("Mr C"), - ] - }, - bike = new TreeNode("Bike") - { - Children = [ - mrD = new TreeNode("Mr D"), - mrE = new TreeNode("Mr E") - ] - } - ] + Children = + [ + car = new ("Car") + { + Children = + [ + mrA = new ("Mr A"), + mrB = new ("Mr B") + ] + }, + lorry = new ("Lorry") + { + Children = + [ + mrC = new ("Mr C") + ] + }, + bike = new ("Bike") + { + Children = + [ + mrD = new ("Mr D"), + mrE = new ("Mr E") + ] + } + ] }; tv.AddObject (root); - tv.ExpandAll(); + tv.ExpandAll (); using GuiTestContext context = With.A (40, 13, d) From 222ff34429a50278e2465b5f3b72c826a4bf3afd Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 3 Apr 2025 12:56:16 +0100 Subject: [PATCH 06/34] Fix regression when removed child was the selected one --- Terminal.Gui/Views/TreeView/Branch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index 96df0b0b92..4bc24af6cf 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -348,7 +348,7 @@ public void Refresh (bool startAtTop) ChildBranches.Remove (toRemove); //also if the user has this node selected (its disappearing) so lets change selection to us (the parent object) to be helpful - if (Equals (tree.SelectedObject, toRemove)) + if (Equals (tree.SelectedObject, toRemove.Model)) { tree.SelectedObject = Model; } From 1bc647d2daac8276d6fed3eace1c7bf83d1ba320 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 3 Apr 2025 13:01:53 +0100 Subject: [PATCH 07/34] Revert "Add class for detecting information about console in extensible way" This reverts commit 7e4253cf28428ea80a4773b137d4bd89cf321746. --- .../FeatureDetection/ConsoleFeatureFinder.cs | 59 ------------------- .../ConsoleFeatureFinderResults.cs | 16 ----- .../FeatureDetection/WindowsFeatureSet.cs | 15 ----- .../ConsoleDrivers/V2/ApplicationV2.cs | 8 +-- 4 files changed, 2 insertions(+), 96 deletions(-) delete mode 100644 Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinder.cs delete mode 100644 Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinderResults.cs delete mode 100644 Terminal.Gui/ConsoleDrivers/FeatureDetection/WindowsFeatureSet.cs diff --git a/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinder.cs b/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinder.cs deleted file mode 100644 index 1ce9c5af34..0000000000 --- a/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinder.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Win32; - -namespace Terminal.Gui; - -/// -/// Attempts to determine information about the terminal and what features it -/// does/not support based on runtime operations e.g. registry etc -/// -internal class ConsoleFeatureFinder -{ - public ConsoleFeatureFinderResults GetResults () - { - var results = new ConsoleFeatureFinderResults (); - - PlatformID p = Environment.OSVersion.Platform; - results.IsWindows = p is PlatformID.Win32NT or PlatformID.Win32S or PlatformID.Win32Windows; - - if (results.IsWindows) - { - DetectWindowsSpecificFeatures (results.Windows); - } - - return results; - } - - private void DetectWindowsSpecificFeatures (WindowsFeatureSet windowsFeatures) - { - windowsFeatures.ConHostLegacyMode = IsLegacyConsoleEnabled (); - } - - bool IsLegacyConsoleEnabled () - { - try - { - using (RegistryKey key = Registry.CurrentUser.OpenSubKey (@"Console")) - { - if (key != null) - { - object value = key.GetValue ("ForceV2"); - if (value is int intValue) - { - return intValue == 0; // Legacy Mode enabled if ForceV2 is 0 - } - } - } - } - catch (Exception ex) - { - Logging.Warning ("Error reading registry: " + ex.Message); - } - - return false; // Assume new console mode if check fails - } -} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinderResults.cs b/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinderResults.cs deleted file mode 100644 index fd9fa6e8e6..0000000000 --- a/Terminal.Gui/ConsoleDrivers/FeatureDetection/ConsoleFeatureFinderResults.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Terminal.Gui; - -/// -/// Results of console feature detection -/// -internal class ConsoleFeatureFinderResults -{ - public WindowsFeatureSet Windows { get; set; } = new WindowsFeatureSet(); - public bool IsWindows { get; set; } - - /// - public override string ToString () - { - return $"{nameof(IsWindows)}:{IsWindows} {nameof(Windows)}:{Windows}"; - } -} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/FeatureDetection/WindowsFeatureSet.cs b/Terminal.Gui/ConsoleDrivers/FeatureDetection/WindowsFeatureSet.cs deleted file mode 100644 index 1d54ab5ac7..0000000000 --- a/Terminal.Gui/ConsoleDrivers/FeatureDetection/WindowsFeatureSet.cs +++ /dev/null @@ -1,15 +0,0 @@ -using static Unix.Terminal.Curses; - -namespace Terminal.Gui; - -/// -/// Features specific to the windows operating system -/// -internal class WindowsFeatureSet -{ - - public bool ConHostLegacyMode { get; set; } - - - public override string ToString () { return $"{nameof(ConHostLegacyMode)}:{ConHostLegacyMode}"; } -} diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index f4500eef09..9baeba301f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -85,15 +85,11 @@ public override void Init (IConsoleDriver? driver = null, string? driverName = n private void CreateDriver (string? driverName) { + PlatformID p = Environment.OSVersion.Platform; bool definetlyWin = driverName?.Contains ("win") ?? false; bool definetlyNet = driverName?.Contains ("net") ?? false; - var finder = new ConsoleFeatureFinder (); - var result = finder.GetResults (); - - Logging.Logger.LogInformation ($"Feature detection results:{ result}"); - if (definetlyWin) { _coordinator = CreateWindowsSubcomponents (); @@ -102,7 +98,7 @@ private void CreateDriver (string? driverName) { _coordinator = CreateNetSubcomponents (); } - else if (result.IsWindows) + else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { _coordinator = CreateWindowsSubcomponents (); } From bc42fc901e22b16d68e6ec998952a2a5571e36a8 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 4 Apr 2025 08:49:59 +0100 Subject: [PATCH 08/34] Code cleanup and enable nullable on Branch --- Terminal.Gui/Views/TreeView/Branch.cs | 130 +++++++++++++------------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index 4bc24af6cf..87d216df2d 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -1,8 +1,10 @@ -namespace Terminal.Gui; +#nullable enable + +namespace Terminal.Gui; internal class Branch where T : class { - private readonly TreeView tree; + private readonly TreeView _tree; /// /// Declares a new branch of in which the users object is @@ -11,9 +13,9 @@ internal class Branch where T : class /// The UI control in which the branch resides. /// Pass null for root level branches, otherwise pass the parent. /// The user's object that should be displayed. - public Branch (TreeView tree, Branch parentBranchIfAny, T model) + public Branch (TreeView tree, Branch? parentBranchIfAny, T model) { - this.tree = tree; + _tree = tree; Model = model; if (parentBranchIfAny is { }) @@ -27,7 +29,7 @@ public Branch (TreeView tree, Branch parentBranchIfAny, T model) /// The children of the current branch. This is null until the first call to to avoid /// enumerating the entire underlying hierarchy. /// - public List> ChildBranches { get; set; } + public List>? ChildBranches { get; set; } /// The depth of the current branch. Depth of 0 indicates root level branches. public int Depth { get; } @@ -39,7 +41,7 @@ public Branch (TreeView tree, Branch parentBranchIfAny, T model) public T Model { get; private set; } /// The parent or null if it is a root. - public Branch Parent { get; } + public Branch? Parent { get; } /// /// Returns true if the current branch can be expanded according to the or cached @@ -52,13 +54,13 @@ public bool CanExpand () if (ChildBranches is null) { //if there is a rapid method for determining whether there are children - if (tree.TreeBuilder.SupportsCanExpand) + if (_tree.TreeBuilder.SupportsCanExpand) { - return tree.TreeBuilder.CanExpand (Model); + return _tree.TreeBuilder.CanExpand (Model); } //there is no way of knowing whether we can expand without fetching the children - FetchChildren (); + ChildBranches = FetchChildren (); } //we fetched or already know the children, so return whether we have any @@ -80,21 +82,21 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, int indexOfModelText; // true if the current line of the tree is the selected one and control has focus - bool isSelected = tree.IsSelected (Model); + bool isSelected = _tree.IsSelected (Model); Attribute textColor = - isSelected ? tree.HasFocus ? colorScheme.Focus : colorScheme.HotNormal : colorScheme.Normal; - Attribute symbolColor = tree.Style.HighlightModelTextOnly ? colorScheme.Normal : textColor; + isSelected ? _tree.HasFocus ? colorScheme.Focus : colorScheme.HotNormal : colorScheme.Normal; + Attribute symbolColor = _tree.Style.HighlightModelTextOnly ? colorScheme.Normal : textColor; // Everything on line before the expansion run and branch text Rune [] prefix = GetLinePrefix (driver).ToArray (); Rune expansion = GetExpandableSymbol (driver); - string lineBody = tree.AspectGetter (Model) ?? ""; + string lineBody = _tree.AspectGetter (Model) ?? ""; - tree.Move (0, y); + _tree.Move (0, y); // if we have scrolled to the right then bits of the prefix will have disappeared off the screen - int toSkip = tree.ScrollOffsetHorizontal; + int toSkip = _tree.ScrollOffsetHorizontal; Attribute attr = symbolColor; // Draw the line prefix (all parallel lanes or whitespace and an expand/collapse/leaf symbol) @@ -112,20 +114,20 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, } // pick color for expanded symbol - if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors) + if (_tree.Style.ColorExpandSymbol || _tree.Style.InvertExpandSymbolColors) { - Attribute color = symbolColor; + Attribute color; - if (tree.Style.ColorExpandSymbol) + if (_tree.Style.ColorExpandSymbol) { if (isSelected) { - color = tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal : - tree.HasFocus ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal; + color = _tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal : + _tree.HasFocus ? _tree.GetHotFocusColor () : _tree.GetHotNormalColor (); } else { - color = tree.ColorScheme.HotNormal; + color = _tree.GetHotNormalColor (); } } else @@ -133,9 +135,9 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, color = symbolColor; } - if (tree.Style.InvertExpandSymbolColors) + if (_tree.Style.InvertExpandSymbolColors) { - color = new Attribute (color.Background, color.Foreground); + color = new (color.Background, color.Foreground); } attr = color; @@ -177,10 +179,10 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, if (lineBody.EnumerateRunes ().Sum (l => l.GetColumns ()) > availableWidth) { // remaining space is zero and truncate the line - lineBody = new string ( - lineBody.TakeWhile (c => (availableWidth -= ((Rune)c).GetColumns ()) >= 0) - .ToArray () - ); + lineBody = new ( + lineBody.TakeWhile (c => (availableWidth -= ((Rune)c).GetColumns ()) >= 0) + .ToArray () + ); availableWidth = 0; } else @@ -194,9 +196,9 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, Attribute modelColor = textColor; // if custom color delegate invoke it - if (tree.ColorGetter is { }) + if (_tree.ColorGetter is { }) { - ColorScheme modelScheme = tree.ColorGetter (Model); + ColorScheme modelScheme = _tree.ColorGetter (Model); // if custom color scheme is defined for this Model if (modelScheme is { }) @@ -206,12 +208,12 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, } else { - modelColor = new Attribute (); + modelColor = new (); } } attr = modelColor; - cells.AddRange (lineBody.Select (r => NewCell (attr, new Rune (r)))); + cells.AddRange (lineBody.Select (r => NewCell (attr, new (r)))); if (availableWidth > 0) { @@ -219,7 +221,7 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, cells.AddRange ( Enumerable.Repeat ( - NewCell (attr, new Rune (' ')), + NewCell (attr, new (' ')), availableWidth ) ); @@ -230,12 +232,12 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, Model = Model, Y = y, Cells = cells, - Tree = tree, + Tree = _tree, IndexOfExpandCollapseSymbol = indexOfExpandCollapseSymbol, IndexOfModelText = indexOfModelText }; - tree.OnDrawLine (e); + _tree.OnDrawLine (e); if (!e.Handled && driver != null) { @@ -252,10 +254,7 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, /// Expands the current branch if possible. public void Expand () { - if (ChildBranches is null) - { - FetchChildren (); - } + ChildBranches ??= FetchChildren (); if (ChildBranches.Any ()) { @@ -264,25 +263,25 @@ public void Expand () } /// Fetch the children of this branch. This method populates . - public virtual void FetchChildren () + private List> FetchChildren () { - if (tree.TreeBuilder is null) + if (_tree.TreeBuilder is null) { - return; + return []; } IEnumerable children; - if (Depth >= tree.MaxDepth) + if (Depth >= _tree.MaxDepth) { children = []; } else { - children = tree.TreeBuilder.GetChildren (Model) ?? []; + children = _tree.TreeBuilder.GetChildren (Model) ?? []; } - ChildBranches = children.Select (o=>new Branch (tree, this, o)).ToList (); + return children.Select (o => new Branch (_tree, this, o)).ToList (); } /// @@ -293,16 +292,16 @@ public virtual void FetchChildren () /// public Rune GetExpandableSymbol (IConsoleDriver driver) { - Rune leafSymbol = tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' '; + Rune leafSymbol = _tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' '; if (IsExpanded) { - return tree.Style.CollapseableSymbol ?? leafSymbol; + return _tree.Style.CollapseableSymbol ?? leafSymbol; } if (CanExpand ()) { - return tree.Style.ExpandableSymbol ?? leafSymbol; + return _tree.Style.ExpandableSymbol ?? leafSymbol; } return leafSymbol; @@ -316,7 +315,7 @@ public Rune GetExpandableSymbol (IConsoleDriver driver) public virtual int GetWidth (IConsoleDriver driver) { return - GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (tree.AspectGetter (Model) ?? "").Length; + GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (_tree.AspectGetter (Model) ?? "").Length; } /// Refreshes cached knowledge in this branch e.g. what children an object has. @@ -333,35 +332,36 @@ public void Refresh (bool startAtTop) //if we don't know about any children yet just use the normal method if (ChildBranches is null) { - FetchChildren (); + ChildBranches = FetchChildren (); } else { // we already knew about some children so preserve the state of the old children // first gather the new Children - T[] newChildren = tree.TreeBuilder?.GetChildren (Model).ToArray () ?? []; + T [] newChildren = _tree.TreeBuilder?.GetChildren (Model).ToArray () ?? []; // Children who no longer appear need to go - foreach (Branch toRemove in ChildBranches.Where (b=>!newChildren.Contains(b.Model)).ToArray ()) + foreach (Branch toRemove in ChildBranches.Where (b => !newChildren.Contains (b.Model)).ToArray ()) { ChildBranches.Remove (toRemove); //also if the user has this node selected (its disappearing) so lets change selection to us (the parent object) to be helpful - if (Equals (tree.SelectedObject, toRemove.Model)) + if (Equals (_tree.SelectedObject, toRemove.Model)) { - tree.SelectedObject = Model; + _tree.SelectedObject = Model; } } // New children need to be added foreach (T newChild in newChildren) { - Branch existingBranch = ChildBranches.FirstOrDefault (b => b.Model.Equals (newChild)); + Branch? existingBranch = ChildBranches.FirstOrDefault (b => b.Model.Equals (newChild)); + // If we don't know about the child, yet we need a new branch if (existingBranch == null) { - ChildBranches.Add (new (tree, this, newChild)); + ChildBranches.Add (new (_tree, this, newChild)); } else { @@ -415,11 +415,11 @@ internal void ExpandAll () internal IEnumerable GetLinePrefix (IConsoleDriver driver) { // If not showing line branches or this is a root object. - if (!tree.Style.ShowBranchLines) + if (!_tree.Style.ShowBranchLines) { for (var i = 0; i < Depth; i++) { - yield return new Rune (' '); + yield return new (' '); } yield break; @@ -430,14 +430,14 @@ internal IEnumerable GetLinePrefix (IConsoleDriver driver) { if (cur.IsLast ()) { - yield return new Rune (' '); + yield return new (' '); } else { yield return Glyphs.VLine; } - yield return new Rune (' '); + yield return new (' '); } if (IsLast ()) @@ -466,13 +466,13 @@ internal bool IsHitOnExpandableSymbol (IConsoleDriver driver, int x) } // if we could theoretically expand - if (!IsExpanded && tree.Style.ExpandableSymbol != default (Rune?)) + if (!IsExpanded && _tree.Style.ExpandableSymbol != default (Rune?)) { return x == GetLinePrefix (driver).Count (); } // if we could theoretically collapse - if (IsExpanded && tree.Style.CollapseableSymbol != default (Rune?)) + if (IsExpanded && _tree.Style.CollapseableSymbol != default (Rune?)) { return x == GetLinePrefix (driver).Count (); } @@ -508,7 +508,7 @@ internal void Rebuild () /// private IEnumerable> GetParentBranches () { - Branch cur = Parent; + Branch? cur = Parent; while (cur is { }) { @@ -527,11 +527,13 @@ private bool IsLast () { if (Parent is null) { - return this == tree.roots.Values.LastOrDefault (); + return this == _tree.roots.Values.LastOrDefault (); } + Parent.ChildBranches ??= Parent.FetchChildren (); + return Parent.ChildBranches.LastOrDefault () == this; } - private static Cell NewCell (Attribute attr, Rune r) { return new Cell { Rune = r, Attribute = new (attr) }; } + private static Cell NewCell (Attribute attr, Rune r) { return new() { Rune = r, Attribute = new (attr) }; } } From a3ab90aa52ac46df2c003142caf86127f61a5986 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 4 Apr 2025 08:55:56 +0100 Subject: [PATCH 09/34] Remove color scheme and driver from Branch draw --- .../Views/TableView/TreeTableSource.cs | 4 +-- Terminal.Gui/Views/TreeView/Branch.cs | 36 +++++++++---------- Terminal.Gui/Views/TreeView/TreeView.cs | 6 ++-- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/Terminal.Gui/Views/TableView/TreeTableSource.cs b/Terminal.Gui/Views/TableView/TreeTableSource.cs index 9125c0c953..06b304634a 100644 --- a/Terminal.Gui/Views/TableView/TreeTableSource.cs +++ b/Terminal.Gui/Views/TableView/TreeTableSource.cs @@ -87,8 +87,8 @@ private string GetColumnZeroRepresentationFromTree (int row) Branch branch = RowToBranch (row); // Everything on line before the expansion run and branch text - Rune [] prefix = branch.GetLinePrefix (Application.Driver).ToArray (); - Rune expansion = branch.GetExpandableSymbol (Application.Driver); + Rune [] prefix = branch.GetLinePrefix ().ToArray (); + Rune expansion = branch.GetExpandableSymbol (); string lineBody = _tree.AspectGetter (branch.Model) ?? ""; var sb = new StringBuilder (); diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index 87d216df2d..4c348eaff6 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -71,11 +71,9 @@ public bool CanExpand () public void Collapse () { IsExpanded = false; } /// Renders the current on the specified line . - /// - /// /// /// - public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth) + public virtual void Draw (int y, int availableWidth) { List cells = new (); int? indexOfExpandCollapseSymbol = null; @@ -85,12 +83,12 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, bool isSelected = _tree.IsSelected (Model); Attribute textColor = - isSelected ? _tree.HasFocus ? colorScheme.Focus : colorScheme.HotNormal : colorScheme.Normal; - Attribute symbolColor = _tree.Style.HighlightModelTextOnly ? colorScheme.Normal : textColor; + isSelected ? _tree.HasFocus ? _tree.GetFocusColor () : _tree.GetHotNormalColor () : _tree.GetNormalColor (); + Attribute symbolColor = _tree.Style.HighlightModelTextOnly ? _tree.GetNormalColor () : textColor; // Everything on line before the expansion run and branch text - Rune [] prefix = GetLinePrefix (driver).ToArray (); - Rune expansion = GetExpandableSymbol (driver); + Rune [] prefix = GetLinePrefix ().ToArray (); + Rune expansion = GetExpandableSymbol (); string lineBody = _tree.AspectGetter (Model) ?? ""; _tree.Move (0, y); @@ -122,7 +120,7 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, { if (isSelected) { - color = _tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal : + color = _tree.Style.HighlightModelTextOnly ? _tree.GetHotNormalColor () : _tree.HasFocus ? _tree.GetHotFocusColor () : _tree.GetHotNormalColor (); } else @@ -239,16 +237,16 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, }; _tree.OnDrawLine (e); - if (!e.Handled && driver != null) + if (!e.Handled) { foreach (Cell cell in cells) { - driver.SetAttribute ((Attribute)cell.Attribute!); - driver.AddRune (cell.Rune); + _tree.SetAttribute ((Attribute)cell.Attribute!); + _tree.AddRune (cell.Rune); } } - driver?.SetAttribute (colorScheme.Normal); + _tree.SetAttribute (_tree.GetNormalColor()); } /// Expands the current branch if possible. @@ -288,9 +286,8 @@ private List> FetchChildren () /// Returns an appropriate symbol for displaying next to the string representation of the /// object to indicate whether it or not (or it is a leaf). /// - /// /// - public Rune GetExpandableSymbol (IConsoleDriver driver) + public Rune GetExpandableSymbol () { Rune leafSymbol = _tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' '; @@ -312,10 +309,10 @@ public Rune GetExpandableSymbol (IConsoleDriver driver) /// line body). /// /// - public virtual int GetWidth (IConsoleDriver driver) + public virtual int GetWidth () { return - GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (_tree.AspectGetter (Model) ?? "").Length; + GetLinePrefix ().Sum (r => r.GetColumns ()) + GetExpandableSymbol ().GetColumns () + (_tree.AspectGetter (Model) ?? "").Length; } /// Refreshes cached knowledge in this branch e.g. what children an object has. @@ -410,9 +407,8 @@ internal void ExpandAll () /// Gets all characters to render prior to the current branches line. This includes indentation whitespace and /// any tree branches (if enabled). /// - /// /// - internal IEnumerable GetLinePrefix (IConsoleDriver driver) + internal IEnumerable GetLinePrefix () { // If not showing line branches or this is a root object. if (!_tree.Style.ShowBranchLines) @@ -468,13 +464,13 @@ internal bool IsHitOnExpandableSymbol (IConsoleDriver driver, int x) // if we could theoretically expand if (!IsExpanded && _tree.Style.ExpandableSymbol != default (Rune?)) { - return x == GetLinePrefix (driver).Count (); + return x == GetLinePrefix ().Count (); } // if we could theoretically collapse if (IsExpanded && _tree.Style.CollapseableSymbol != default (Rune?)) { - return x == GetLinePrefix (driver).Count (); + return x == GetLinePrefix ().Count (); } return false; diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index a4b33bfe5f..af2245efae 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -879,10 +879,10 @@ public int GetContentWidth (bool visible) return 0; } - return map.Skip (ScrollOffsetVertical).Take (Viewport.Height).Max (b => b.GetWidth (Driver)); + return map.Skip (ScrollOffsetVertical).Take (Viewport.Height).Max (b => b.GetWidth ()); } - return map.Max (b => b.GetWidth (Driver)); + return map.Max (b => b.GetWidth ()); } /// @@ -1171,7 +1171,7 @@ protected override bool OnDrawingContent () if (idxToRender < map.Count) { // Render the line - map.ElementAt (idxToRender).Draw (Driver, ColorScheme, line, Viewport.Width); + map.ElementAt (idxToRender).Draw (line, Viewport.Width); } else { From b801ff6284870b80523f3f42c6df6e614a1d3daa Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 4 Apr 2025 10:01:23 +0100 Subject: [PATCH 10/34] Add xunit context extensions --- Terminal.sln | 6 +++ .../TerminalGuiFluentTesting.Xunit.csproj | 14 +++++ .../XunitContextExtensions.cs | 25 +++++++++ .../FluentTests/TreeViewFluentTests.cs | 54 +++++++++---------- .../IntegrationTests/IntegrationTests.csproj | 1 + 5 files changed, 73 insertions(+), 27 deletions(-) create mode 100644 TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj create mode 100644 TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs diff --git a/Terminal.sln b/Terminal.sln index e15d8f3606..b255d94598 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj", "{2DBA7BDC-17AE-474B-A507-00807D087607}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting.Xunit", "TerminalGuiFluentTesting.Xunit\TerminalGuiFluentTesting.Xunit.csproj", "{231B9723-10F3-46DB-8EAE-50C0C0375AD3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -123,6 +125,10 @@ Global {2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.Build.0 = Debug|Any CPU {2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.ActiveCfg = Release|Any CPU {2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.Build.0 = Release|Any CPU + {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj b/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj new file mode 100644 index 0000000000..03c8b09d94 --- /dev/null +++ b/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs b/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs new file mode 100644 index 0000000000..53f81e37b3 --- /dev/null +++ b/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs @@ -0,0 +1,25 @@ +using Xunit; + +namespace TerminalGuiFluentTesting; + +public static class XunitContextExtensions +{ + public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition) + { + context.Then ( + () => + { + Assert.True (condition); + }); + return context; + } + public static GuiTestContext AssertEqual (this GuiTestContext context, object? expected, object? actual) + { + context.Then ( + () => + { + Assert.Equal (expected,actual); + }); + return context; + } +} diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs index ed931074d7..f735147973 100644 --- a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -42,14 +42,14 @@ public void TreeView_AllowReOrdering (V2TestDriver d) .Focus (tv) .WaitIteration () .ScreenShot ("Before expanding", _out) - .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) + .AssertEqual (root, tv.GetObjectOnRow (0)) .Then (() => Assert.Null (tv.GetObjectOnRow (1))) .Right () .ScreenShot ("After expanding", _out) - .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) - .Then (() => Assert.Equal (car, tv.GetObjectOnRow (1))) - .Then (() => Assert.Equal (lorry, tv.GetObjectOnRow (2))) - .Then (() => Assert.Equal (bike, tv.GetObjectOnRow (3))) + .AssertEqual (root, tv.GetObjectOnRow (0)) + .AssertEqual (car, tv.GetObjectOnRow (1)) + .AssertEqual (lorry, tv.GetObjectOnRow (2)) + .AssertEqual (bike, tv.GetObjectOnRow (3)) .Then ( () => { @@ -59,10 +59,10 @@ public void TreeView_AllowReOrdering (V2TestDriver d) }) .WaitIteration () .ScreenShot ("After re-order", _out) - .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) - .Then (() => Assert.Equal (bike, tv.GetObjectOnRow (1))) - .Then (() => Assert.Equal (car, tv.GetObjectOnRow (2))) - .Then (() => Assert.Equal (lorry, tv.GetObjectOnRow (3))) + .AssertEqual (root, tv.GetObjectOnRow (0)) + .AssertEqual (bike, tv.GetObjectOnRow (1)) + .AssertEqual (car, tv.GetObjectOnRow (2)) + .AssertEqual (lorry, tv.GetObjectOnRow (3)) .WriteOutLogs (_out); context.Stop (); @@ -128,15 +128,15 @@ public void TreeViewReOrder_PreservesExpansion (V2TestDriver d) .Add (tv) .WaitIteration () .ScreenShot ("Initial State", _out) - .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) - .Then (() => Assert.Equal (car, tv.GetObjectOnRow (1))) - .Then (() => Assert.Equal (mrA, tv.GetObjectOnRow (2))) - .Then (() => Assert.Equal (mrB, tv.GetObjectOnRow (3))) - .Then (() => Assert.Equal (lorry, tv.GetObjectOnRow (4))) - .Then (() => Assert.Equal (mrC, tv.GetObjectOnRow (5))) - .Then (() => Assert.Equal (bike, tv.GetObjectOnRow (6))) - .Then (() => Assert.Equal (mrD, tv.GetObjectOnRow (7))) - .Then (() => Assert.Equal (mrE, tv.GetObjectOnRow (8))) + .AssertEqual (root, tv.GetObjectOnRow (0)) + .AssertEqual (car, tv.GetObjectOnRow (1)) + .AssertEqual (mrA, tv.GetObjectOnRow (2)) + .AssertEqual (mrB, tv.GetObjectOnRow (3)) + .AssertEqual (lorry, tv.GetObjectOnRow (4)) + .AssertEqual (mrC, tv.GetObjectOnRow (5)) + .AssertEqual (bike, tv.GetObjectOnRow (6)) + .AssertEqual (mrD, tv.GetObjectOnRow (7)) + .AssertEqual (mrE, tv.GetObjectOnRow (8)) .Then ( () => { @@ -146,15 +146,15 @@ public void TreeViewReOrder_PreservesExpansion (V2TestDriver d) }) .WaitIteration () .ScreenShot ("After re-order", _out) - .Then (() => Assert.Equal (root, tv.GetObjectOnRow (0))) - .Then (() => Assert.Equal (bike, tv.GetObjectOnRow (1))) - .Then (() => Assert.Equal (mrD, tv.GetObjectOnRow (2))) - .Then (() => Assert.Equal (mrE, tv.GetObjectOnRow (3))) - .Then (() => Assert.Equal (car, tv.GetObjectOnRow (4))) - .Then (() => Assert.Equal (mrA, tv.GetObjectOnRow (5))) - .Then (() => Assert.Equal (mrB, tv.GetObjectOnRow (6))) - .Then (() => Assert.Equal (lorry, tv.GetObjectOnRow (7))) - .Then (() => Assert.Equal (mrC, tv.GetObjectOnRow (8))) + .AssertEqual (root, tv.GetObjectOnRow (0)) + .AssertEqual (bike, tv.GetObjectOnRow (1)) + .AssertEqual (mrD, tv.GetObjectOnRow (2)) + .AssertEqual (mrE, tv.GetObjectOnRow (3)) + .AssertEqual (car, tv.GetObjectOnRow (4)) + .AssertEqual (mrA, tv.GetObjectOnRow (5)) + .AssertEqual (mrB, tv.GetObjectOnRow (6)) + .AssertEqual (lorry, tv.GetObjectOnRow (7)) + .AssertEqual (mrC, tv.GetObjectOnRow (8)) .WriteOutLogs (_out); context.Stop (); diff --git a/Tests/IntegrationTests/IntegrationTests.csproj b/Tests/IntegrationTests/IntegrationTests.csproj index f279e21df2..80f067bf7d 100644 --- a/Tests/IntegrationTests/IntegrationTests.csproj +++ b/Tests/IntegrationTests/IntegrationTests.csproj @@ -26,6 +26,7 @@ + From 992cbcadb4e92daf1aed857898d082439d97dc93 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 4 Apr 2025 15:45:04 +0100 Subject: [PATCH 11/34] Investigate codegen for xunit --- Directory.Packages.props | 93 +++++++--------- Terminal.sln | 16 ++- .../XunitContextExtensions.cs | 25 ----- ...inalGuiFluentTestingXunit.Generator.csproj | 20 ++++ .../TheGenerator.cs | 105 ++++++++++++++++++ .../TerminalGuiFluentTestingXunit.csproj | 2 + .../XunitContextExtensions.cs | 9 ++ .../FluentTests/TreeViewFluentTests.cs | 1 + .../IntegrationTests/IntegrationTests.csproj | 2 +- 9 files changed, 192 insertions(+), 81 deletions(-) delete mode 100644 TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs create mode 100644 TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj create mode 100644 TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs rename TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj => TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj (63%) create mode 100644 TerminalGuiFluentTestingXunit/XunitContextExtensions.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 18afbe64a4..592cfc40d5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,52 +1,45 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Terminal.sln b/Terminal.sln index b255d94598..1456297b83 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -65,7 +65,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj", "{2DBA7BDC-17AE-474B-A507-00807D087607}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting.Xunit", "TerminalGuiFluentTesting.Xunit\TerminalGuiFluentTesting.Xunit.csproj", "{231B9723-10F3-46DB-8EAE-50C0C0375AD3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTestingXunit", "TerminalGuiFluentTestingXunit\TerminalGuiFluentTestingXunit.csproj", "{F56BAFFD-F227-4B0A-96F0-C800FAEF2036}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTestingXunit.Generator", "TerminalGuiFluentTestingXunit.Generator\TerminalGuiFluentTestingXunit.Generator.csproj", "{199F27D8-A905-4DDC-82CA-1FE1A90B1788}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -125,10 +127,14 @@ Global {2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.Build.0 = Debug|Any CPU {2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.ActiveCfg = Release|Any CPU {2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.Build.0 = Release|Any CPU - {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.Build.0 = Release|Any CPU + {F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Release|Any CPU.Build.0 = Release|Any CPU + {199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Debug|Any CPU.Build.0 = Debug|Any CPU + {199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Release|Any CPU.ActiveCfg = Release|Any CPU + {199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs b/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs deleted file mode 100644 index 53f81e37b3..0000000000 --- a/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Xunit; - -namespace TerminalGuiFluentTesting; - -public static class XunitContextExtensions -{ - public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition) - { - context.Then ( - () => - { - Assert.True (condition); - }); - return context; - } - public static GuiTestContext AssertEqual (this GuiTestContext context, object? expected, object? actual) - { - context.Then ( - () => - { - Assert.Equal (expected,actual); - }); - return context; - } -} diff --git a/TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj b/TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj new file mode 100644 index 0000000000..454cc7bf7f --- /dev/null +++ b/TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj @@ -0,0 +1,20 @@ + + + + + netstandard2.0 + Latest + enable + enable + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs new file mode 100644 index 0000000000..6390d5ea9b --- /dev/null +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -0,0 +1,105 @@ +using System.Collections.Immutable; +using System.Reflection; +using System.Text; +using System.Xml.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TerminalGuiFluentTestingXunit.Generator; + +[Generator] +public class TheGenerator : IIncrementalGenerator +{ + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var provider = context.SyntaxProvider.CreateSyntaxProvider( + + predicate: static (node, _) => IsClass(node,"XunitContextExtensions"), + transform: static (ctx, _) => + (ClassDeclarationSyntax)ctx.Node) + .Where(m => m is { }); + + var compilation = context.CompilationProvider.Combine(provider.Collect()); + context.RegisterSourceOutput(compilation, Execute); + } + + private static bool IsClass (SyntaxNode node, string named) + { + return node is ClassDeclarationSyntax c && c.Identifier.Text == named; + } + + private void Execute(SourceProductionContext context, (Compilation Left, ImmutableArray Right) arg2) + { + var sb = new StringBuilder (); + var assertType = arg2.Left.GetTypeByMetadataName ("Xunit.Assert"); + + var equalMethods = assertType + .GetMembers ("Equal") + .OfType () + .Where (m => m.Parameters.Length == 2) + .ToList (); + + /*foreach (var method in equalMethods) + { + var signature = string.Join (", ", method.Parameters.Select (p => p.Type.ToDisplayString ())); + context.ReportDiagnostic (Diagnostic.Create ( + new DiagnosticDescriptor ("GEN002", "Equal Overload", $"Equal({signature})", "Generator", DiagnosticSeverity.Info, true), + Location.None)); + }*/ + + + + string header = """" + using TerminalGuiFluentTesting; + using Xunit; + + namespace TerminalGuiFluentTestingXunit; + + public static partial class XunitContextExtensions + { + + public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition) + { + context.Then ( + () => + { + Assert.True (condition); + }); + return context; + } + """"; + + string tail = """ + + } + """; + + sb.AppendLine (header); + + foreach (var m in equalMethods) + //for (int i = 0; i < 1; i++) + { + string method = """ + public static GuiTestContext AssertEqual (this GuiTestContext context, object? expected, object? actual) + { + context.Then ( + () => + { + Assert.Equal (expected,actual); + }); + return context; + } + """; + + sb.AppendLine (method); + + break; + } + + sb.AppendLine (tail); + + context.AddSource("XunitContextExtensions.g.cs", sb.ToString()); + } + +} diff --git a/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj similarity index 63% rename from TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj rename to TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj index 03c8b09d94..24edd52115 100644 --- a/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj +++ b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj @@ -4,9 +4,11 @@ net8.0 enable enable + true + diff --git a/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs new file mode 100644 index 0000000000..e3c1c5e84a --- /dev/null +++ b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace TerminalGuiFluentTestingXunit; + +public static partial class XunitContextExtensions +{ + // Placeholder + +} diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs index f735147973..51e09ba4cd 100644 --- a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -1,5 +1,6 @@ using Terminal.Gui; using TerminalGuiFluentTesting; +using TerminalGuiFluentTestingXunit; using Xunit.Abstractions; namespace IntegrationTests.FluentTests; diff --git a/Tests/IntegrationTests/IntegrationTests.csproj b/Tests/IntegrationTests/IntegrationTests.csproj index 80f067bf7d..ef275b3eb8 100644 --- a/Tests/IntegrationTests/IntegrationTests.csproj +++ b/Tests/IntegrationTests/IntegrationTests.csproj @@ -26,7 +26,7 @@ - + From 2ad5f001da79a30abec975dbab44e4d8d7bde5a1 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 4 Apr 2025 16:43:05 +0100 Subject: [PATCH 12/34] Getting closer to something that works --- .../TheGenerator.cs | 82 +++++++++++++++---- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs index 6390d5ea9b..31a797ce67 100644 --- a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -1,8 +1,10 @@ using System.Collections.Immutable; using System.Reflection; +using System.Reflection.Metadata; using System.Text; using System.Xml.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace TerminalGuiFluentTestingXunit.Generator; @@ -40,16 +42,6 @@ private void Execute(SourceProductionContext context, (Compilation Left, Immutab .Where (m => m.Parameters.Length == 2) .ToList (); - /*foreach (var method in equalMethods) - { - var signature = string.Join (", ", method.Parameters.Select (p => p.Type.ToDisplayString ())); - context.ReportDiagnostic (Diagnostic.Create ( - new DiagnosticDescriptor ("GEN002", "Equal Overload", $"Equal({signature})", "Generator", DiagnosticSeverity.Info, true), - Location.None)); - }*/ - - - string header = """" using TerminalGuiFluentTesting; using Xunit; @@ -77,24 +69,23 @@ public static GuiTestContext AssertTrue (this GuiTestContext context, bool? cond sb.AppendLine (header); - foreach (var m in equalMethods) - //for (int i = 0; i < 1; i++) + foreach (IMethodSymbol? m in equalMethods) { - string method = """ - public static GuiTestContext AssertEqual (this GuiTestContext context, object? expected, object? actual) + var signature = GetModifiedMethodSignature (m,out var expected, out var actual); + + string method = $$""" + {{signature}} { context.Then ( () => { - Assert.Equal (expected,actual); + Assert.Equal ({{expected}},{{actual}}); }); return context; } """; sb.AppendLine (method); - - break; } sb.AppendLine (tail); @@ -102,4 +93,61 @@ public static GuiTestContext AssertEqual (this GuiTestContext context, object? e context.AddSource("XunitContextExtensions.g.cs", sb.ToString()); } + + private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, out string expectedParamName, out string actualParamName) + { + // Create the "this GuiTestContext context" parameter + var contextParam = SyntaxFactory.Parameter (SyntaxFactory.Identifier ("context")) + .WithType (SyntaxFactory.ParseTypeName ("GuiTestContext")) + .AddModifiers (SyntaxFactory.Token (SyntaxKind.ThisKeyword)); // Add the "this" keyword + + + // Extract the parameter names (expected and actual) + expectedParamName = methodSymbol.Parameters.FirstOrDefault ()?.Name ?? "expected"; + actualParamName = methodSymbol.Parameters.Skip (1).FirstOrDefault ()?.Name ?? "actual"; + + + // Get the current method parameters and add the context parameter at the start + var parameters = methodSymbol.Parameters.Select (p => + SyntaxFactory.Parameter (SyntaxFactory.Identifier (p.Name)) + .WithType (SyntaxFactory.ParseTypeName (p.Type.ToDisplayString ())) + ).ToList (); + + parameters.Insert (0, contextParam); // Insert 'context' as the first parameter + + // Change the return type to GuiTestContext + TypeSyntax returnType = SyntaxFactory.ParseTypeName ("GuiTestContext"); + + // Change the method name to AssertEqual + SyntaxToken methodName = SyntaxFactory.Identifier ("AssertEqual"); + + // Handle generic type parameters if the method is generic + var typeParameters = methodSymbol.TypeParameters.Select ( + tp => + SyntaxFactory.TypeParameter (SyntaxFactory.Identifier (tp.Name)) + ) + .ToArray (); + + MethodDeclarationSyntax dec = SyntaxFactory.MethodDeclaration (returnType, methodName) + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword), + SyntaxFactory.Token (SyntaxKind.StaticKeyword))) + .WithParameterList (SyntaxFactory.ParameterList (SyntaxFactory.SeparatedList (parameters))); + + if (typeParameters.Any ()) + { + // Add the here + dec = dec.WithTypeParameterList (SyntaxFactory.TypeParameterList (SyntaxFactory.SeparatedList (typeParameters))); + } + + // Build the method signature syntax tree + MethodDeclarationSyntax methodSyntax = dec.NormalizeWhitespace (); + + // Convert the method syntax to a string + string methodString = methodSyntax.ToString (); + + return methodString; + } + } From bb8a330d3424ed284a0b0f2e4ca1b9bd5e310ee3 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 4 Apr 2025 17:30:44 +0100 Subject: [PATCH 13/34] Fix code generation --- TerminalGuiFluentTesting/GuiTestContext.cs | 13 ++++- .../TheGenerator.cs | 58 +++++++++++++++---- .../XunitContextExtensions.cs | 1 - 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/TerminalGuiFluentTesting/GuiTestContext.cs b/TerminalGuiFluentTesting/GuiTestContext.cs index 1a3274cd32..06977c9775 100644 --- a/TerminalGuiFluentTesting/GuiTestContext.cs +++ b/TerminalGuiFluentTesting/GuiTestContext.cs @@ -134,6 +134,15 @@ public GuiTestContext Stop () return this; } + /// + /// Hard stops the application and waits for the background thread to exit. + /// + public void HardStop () + { + _hardStop.Cancel (); + Stop (); + } + /// /// Cleanup to avoid state bleed between tests /// @@ -249,8 +258,7 @@ public GuiTestContext Then (Action doAction) } catch(Exception) { - Stop (); - _hardStop.Cancel(); + HardStop (); throw; @@ -259,6 +267,7 @@ public GuiTestContext Then (Action doAction) return this; } + /// /// Simulates a right click at the given screen coordinates on the current driver. /// This is a raw input event that goes through entire processing pipeline as though diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs index 31a797ce67..65d62eb39c 100644 --- a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -36,7 +36,11 @@ private void Execute(SourceProductionContext context, (Compilation Left, Immutab var sb = new StringBuilder (); var assertType = arg2.Left.GetTypeByMetadataName ("Xunit.Assert"); - var equalMethods = assertType + + // Create a HashSet to track unique method signatures + var signaturesDone = new HashSet (); + + var equalMethods = assertType .GetMembers ("Equal") .OfType () .Where (m => m.Parameters.Length == 2) @@ -73,17 +77,30 @@ public static GuiTestContext AssertTrue (this GuiTestContext context, bool? cond { var signature = GetModifiedMethodSignature (m,out var expected, out var actual); + if (!signaturesDone.Add (signature)) + { + continue; + } + string method = $$""" - {{signature}} - { - context.Then ( - () => - { - Assert.Equal ({{expected}},{{actual}}); - }); - return context; - } - """; + {{signature}} + { + try + { + Assert.Equal ({{expected}},{{actual}}); + } + catch(Exception) + { + context.HardStop (); + + + throw; + + } + + return context; + } + """; sb.AppendLine (method); } @@ -139,6 +156,25 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, out strin { // Add the here dec = dec.WithTypeParameterList (SyntaxFactory.TypeParameterList (SyntaxFactory.SeparatedList (typeParameters))); + + // Handle type parameter constraints + var constraintClauses = methodSymbol.TypeParameters + .Where (tp => tp.ConstraintTypes.Length > 0) + .Select (tp => + SyntaxFactory.TypeParameterConstraintClause (tp.Name) + .WithConstraints ( + SyntaxFactory.SeparatedList ( + tp.ConstraintTypes.Select (constraintType => + SyntaxFactory.TypeConstraint (SyntaxFactory.ParseTypeName (constraintType.ToDisplayString ())) + ) + ) + ) + ).ToList (); + + if (constraintClauses.Any ()) + { + dec = dec.WithConstraintClauses (SyntaxFactory.List (constraintClauses)); + } } // Build the method signature syntax tree diff --git a/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs index e3c1c5e84a..1e133e1d0e 100644 --- a/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs +++ b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs @@ -5,5 +5,4 @@ namespace TerminalGuiFluentTestingXunit; public static partial class XunitContextExtensions { // Placeholder - } From 46cdc34b63d1beab9105903b6c02548eee505454 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 4 Apr 2025 20:02:52 +0100 Subject: [PATCH 14/34] Further explore code gen --- .../TheGenerator.cs | 99 +++++++++++++------ .../XunitContextExtensions.cs | 13 ++- .../FluentTests/TreeViewFluentTests.cs | 1 + 3 files changed, 83 insertions(+), 30 deletions(-) diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs index 65d62eb39c..e2ab0a0a83 100644 --- a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -1,8 +1,5 @@ using System.Collections.Immutable; -using System.Reflection; -using System.Reflection.Metadata; using System.Text; -using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -10,8 +7,25 @@ namespace TerminalGuiFluentTestingXunit.Generator; [Generator] -public class TheGenerator : IIncrementalGenerator +public class AssertIsTypeGenerator : TheGenerator { + public AssertIsTypeGenerator () : + base ("IsType", 2,true) + { + } +} + +[Generator] +public class EqualGenerator : TheGenerator +{ + public EqualGenerator (): + base("Equal",2,false) + { + } +} +public abstract class TheGenerator(string methodName, int paramCount, bool invokeTExplicitly) : IIncrementalGenerator +{ + /// public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -40,10 +54,10 @@ private void Execute(SourceProductionContext context, (Compilation Left, Immutab // Create a HashSet to track unique method signatures var signaturesDone = new HashSet (); - var equalMethods = assertType - .GetMembers ("Equal") + var methods = assertType + .GetMembers (methodName) .OfType () - .Where (m => m.Parameters.Length == 2) + .Where (m => m.Parameters.Length == paramCount) .ToList (); string header = """" @@ -55,15 +69,7 @@ namespace TerminalGuiFluentTestingXunit; public static partial class XunitContextExtensions { - public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition) - { - context.Then ( - () => - { - Assert.True (condition); - }); - return context; - } + """"; string tail = """ @@ -73,9 +79,9 @@ public static GuiTestContext AssertTrue (this GuiTestContext context, bool? cond sb.AppendLine (header); - foreach (IMethodSymbol? m in equalMethods) + foreach (IMethodSymbol? m in methods) { - var signature = GetModifiedMethodSignature (m,out var expected, out var actual); + var signature = GetModifiedMethodSignature (m,out var paramNames, out var typeParams); if (!signaturesDone.Add (signature)) { @@ -87,7 +93,7 @@ public static GuiTestContext AssertTrue (this GuiTestContext context, bool? cond { try { - Assert.Equal ({{expected}},{{actual}}); + Assert.{{methodName}}{{typeParams}} ({{string.Join(",",paramNames)}}); } catch(Exception) { @@ -107,12 +113,13 @@ public static GuiTestContext AssertTrue (this GuiTestContext context, bool? cond sb.AppendLine (tail); - context.AddSource("XunitContextExtensions.g.cs", sb.ToString()); + context.AddSource($"XunitContextExtensions{methodName}.g.cs", sb.ToString()); } - - private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, out string expectedParamName, out string actualParamName) + private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, out string[] paramNames, out string typeParams) { + typeParams = string.Empty; + // Create the "this GuiTestContext context" parameter var contextParam = SyntaxFactory.Parameter (SyntaxFactory.Identifier ("context")) .WithType (SyntaxFactory.ParseTypeName ("GuiTestContext")) @@ -120,15 +127,37 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, out strin // Extract the parameter names (expected and actual) - expectedParamName = methodSymbol.Parameters.FirstOrDefault ()?.Name ?? "expected"; - actualParamName = methodSymbol.Parameters.Skip (1).FirstOrDefault ()?.Name ?? "actual"; + paramNames = new string [paramCount]; + + for (int i = 0; i < paramCount; i++) + { + paramNames [i] = methodSymbol.Parameters.ElementAt (i).Name; + // Check if the parameter name is a reserved keyword and prepend "@" if it is + if (IsReservedKeyword (paramNames [i])) + { + paramNames [i] = "@" + paramNames [i]; + } + else + { + paramNames [i] = paramNames [i]; + } + } // Get the current method parameters and add the context parameter at the start var parameters = methodSymbol.Parameters.Select (p => - SyntaxFactory.Parameter (SyntaxFactory.Identifier (p.Name)) - .WithType (SyntaxFactory.ParseTypeName (p.Type.ToDisplayString ())) - ).ToList (); + { + var paramName = p.Name; + // Check if the parameter name is a reserved keyword and prepend "@" if it is + if (IsReservedKeyword (paramName)) + { + paramName = "@" + paramName; + } + + // Create the parameter syntax with the modified name + return SyntaxFactory.Parameter (SyntaxFactory.Identifier (paramName)) + .WithType (SyntaxFactory.ParseTypeName (p.Type.ToDisplayString ())); + }).ToList (); parameters.Insert (0, contextParam); // Insert 'context' as the first parameter @@ -136,7 +165,7 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, out strin TypeSyntax returnType = SyntaxFactory.ParseTypeName ("GuiTestContext"); // Change the method name to AssertEqual - SyntaxToken methodName = SyntaxFactory.Identifier ("AssertEqual"); + SyntaxToken newMethodName = SyntaxFactory.Identifier ($"Assert{methodName}"); // Handle generic type parameters if the method is generic var typeParameters = methodSymbol.TypeParameters.Select ( @@ -145,7 +174,7 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, out strin ) .ToArray (); - MethodDeclarationSyntax dec = SyntaxFactory.MethodDeclaration (returnType, methodName) + MethodDeclarationSyntax dec = SyntaxFactory.MethodDeclaration (returnType, newMethodName) .WithModifiers ( SyntaxFactory.TokenList ( SyntaxFactory.Token (SyntaxKind.PublicKeyword), @@ -174,6 +203,13 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, out strin if (constraintClauses.Any ()) { dec = dec.WithConstraintClauses (SyntaxFactory.List (constraintClauses)); + + } + + // Add the here + if (invokeTExplicitly) + { + typeParams = "<" + string.Join (", ", typeParameters.Select (tp => tp.Identifier.ValueText)) + ">"; } } @@ -186,4 +222,9 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, out strin return methodString; } + // Helper method to check if a parameter name is a reserved keyword + private bool IsReservedKeyword (string name) + { + return string.Equals (name, "object"); + } } diff --git a/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs index 1e133e1d0e..c4f9c38048 100644 --- a/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs +++ b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs @@ -1,8 +1,19 @@ -using Xunit; +using TerminalGuiFluentTesting; +using Xunit; namespace TerminalGuiFluentTestingXunit; public static partial class XunitContextExtensions { // Placeholder + + public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition) + { + context.Then ( + () => + { + Assert.True (condition); + }); + return context; + } } diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs index 51e09ba4cd..22380ac8fd 100644 --- a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -47,6 +47,7 @@ public void TreeView_AllowReOrdering (V2TestDriver d) .Then (() => Assert.Null (tv.GetObjectOnRow (1))) .Right () .ScreenShot ("After expanding", _out) + .AssertIsType(tv.SelectedObject,false) .AssertEqual (root, tv.GetObjectOnRow (0)) .AssertEqual (car, tv.GetObjectOnRow (1)) .AssertEqual (lorry, tv.GetObjectOnRow (2)) From 356d8f15b6caa130f53c4fa5206d9700b6ed0db6 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 5 Apr 2025 12:20:06 +0100 Subject: [PATCH 15/34] Generate all methods in single class for easier extensibility --- .../TheGenerator.cs | 41 ++++++++----------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs index e2ab0a0a83..9bf9c31203 100644 --- a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Reflection; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -6,24 +7,9 @@ namespace TerminalGuiFluentTestingXunit.Generator; -[Generator] -public class AssertIsTypeGenerator : TheGenerator -{ - public AssertIsTypeGenerator () : - base ("IsType", 2,true) - { - } -} [Generator] -public class EqualGenerator : TheGenerator -{ - public EqualGenerator (): - base("Equal",2,false) - { - } -} -public abstract class TheGenerator(string methodName, int paramCount, bool invokeTExplicitly) : IIncrementalGenerator +public class TheGenerator : IIncrementalGenerator { /// @@ -47,9 +33,15 @@ private static bool IsClass (SyntaxNode node, string named) private void Execute(SourceProductionContext context, (Compilation Left, ImmutableArray Right) arg2) { - var sb = new StringBuilder (); var assertType = arg2.Left.GetTypeByMetadataName ("Xunit.Assert"); + GenerateMethods (assertType,context, "IsType",true); + GenerateMethods (assertType, context, "Equal", false); + } + + private void GenerateMethods (INamedTypeSymbol? assertType, SourceProductionContext context, string methodName, bool invokeTExplicitly) + { + var sb = new StringBuilder (); // Create a HashSet to track unique method signatures var signaturesDone = new HashSet (); @@ -57,10 +49,10 @@ private void Execute(SourceProductionContext context, (Compilation Left, Immutab var methods = assertType .GetMembers (methodName) .OfType () - .Where (m => m.Parameters.Length == paramCount) .ToList (); string header = """" + #nullable enable using TerminalGuiFluentTesting; using Xunit; @@ -81,7 +73,7 @@ public static partial class XunitContextExtensions foreach (IMethodSymbol? m in methods) { - var signature = GetModifiedMethodSignature (m,out var paramNames, out var typeParams); + var signature = GetModifiedMethodSignature (m,methodName,invokeTExplicitly, out var paramNames, out var typeParams); if (!signaturesDone.Add (signature)) { @@ -93,7 +85,7 @@ public static partial class XunitContextExtensions { try { - Assert.{{methodName}}{{typeParams}} ({{string.Join(",",paramNames)}}); + Assert.{{methodName}}{{typeParams}} ({{string.Join (",", paramNames)}}); } catch(Exception) { @@ -110,13 +102,12 @@ public static partial class XunitContextExtensions sb.AppendLine (method); } - sb.AppendLine (tail); - context.AddSource($"XunitContextExtensions{methodName}.g.cs", sb.ToString()); + context.AddSource ($"XunitContextExtensions{methodName}.g.cs", sb.ToString ()); } - private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, out string[] paramNames, out string typeParams) + private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, string methodName, bool invokeTExplicitly, out string [] paramNames, out string typeParams) { typeParams = string.Empty; @@ -127,9 +118,9 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, out strin // Extract the parameter names (expected and actual) - paramNames = new string [paramCount]; + paramNames = new string [methodSymbol.Parameters.Length]; - for (int i = 0; i < paramCount; i++) + for (int i = 0; i < methodSymbol.Parameters.Length; i++) { paramNames [i] = methodSymbol.Parameters.ElementAt (i).Name; From cdb453fb36a46be61ef1f37dc425bc99c581ceb1 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 5 Apr 2025 12:25:56 +0100 Subject: [PATCH 16/34] Simplify code gen by moving parameter creation to its own method --- .../TheGenerator.cs | 55 ++++++++++++++----- .../FluentTests/TreeViewFluentTests.cs | 2 +- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs index 9bf9c31203..1c9e41e76b 100644 --- a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -136,19 +136,7 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, string me } // Get the current method parameters and add the context parameter at the start - var parameters = methodSymbol.Parameters.Select (p => - { - var paramName = p.Name; - // Check if the parameter name is a reserved keyword and prepend "@" if it is - if (IsReservedKeyword (paramName)) - { - paramName = "@" + paramName; - } - - // Create the parameter syntax with the modified name - return SyntaxFactory.Parameter (SyntaxFactory.Identifier (paramName)) - .WithType (SyntaxFactory.ParseTypeName (p.Type.ToDisplayString ())); - }).ToList (); + var parameters = methodSymbol.Parameters.Select (p=>CreateParameter(p)).ToList (); parameters.Insert (0, contextParam); // Insert 'context' as the first parameter @@ -213,6 +201,47 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, string me return methodString; } + /// + /// Creates a from a discovered parameter on real xunit method parameter + /// + /// + /// + private ParameterSyntax CreateParameter (IParameterSymbol p) + { + var paramName = p.Name; + // Check if the parameter name is a reserved keyword and prepend "@" if it is + if (IsReservedKeyword (paramName)) + { + paramName = "@" + paramName; + } + + // Create the basic parameter syntax with the modified name and type + var parameterSyntax = SyntaxFactory.Parameter (SyntaxFactory.Identifier (paramName)) + .WithType (SyntaxFactory.ParseTypeName (p.Type.ToDisplayString ())); + + // Add default value if one is present + if (p.HasExplicitDefaultValue) + { + var defaultValueExpression = p.ExplicitDefaultValue switch + { + null => SyntaxFactory.LiteralExpression (SyntaxKind.NullLiteralExpression), + bool b => SyntaxFactory.LiteralExpression (b ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression), + int i => SyntaxFactory.LiteralExpression (SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal (i)), + double d => SyntaxFactory.LiteralExpression (SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal (d)), + string s => SyntaxFactory.LiteralExpression (SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal (s)), + _ => SyntaxFactory.ParseExpression (p.ExplicitDefaultValue.ToString ()) // Fallback + }; + + parameterSyntax = parameterSyntax.WithDefault ( + SyntaxFactory.EqualsValueClause (defaultValueExpression) + ); + + } + + return parameterSyntax; + + } + // Helper method to check if a parameter name is a reserved keyword private bool IsReservedKeyword (string name) { diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs index 22380ac8fd..0493b88834 100644 --- a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -47,7 +47,7 @@ public void TreeView_AllowReOrdering (V2TestDriver d) .Then (() => Assert.Null (tv.GetObjectOnRow (1))) .Right () .ScreenShot ("After expanding", _out) - .AssertIsType(tv.SelectedObject,false) + .AssertIsType(tv.SelectedObject) .AssertEqual (root, tv.GetObjectOnRow (0)) .AssertEqual (car, tv.GetObjectOnRow (1)) .AssertEqual (lorry, tv.GetObjectOnRow (2)) From 5697a793b6c95c54b675c39f9db1b5cb8fc4605d Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 5 Apr 2025 13:03:55 +0100 Subject: [PATCH 17/34] Implement asserts A-I --- .../TheGenerator.cs | 18 +++++++++++++++++- .../FluentTests/TreeViewFluentTests.cs | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs index 1c9e41e76b..dd0b13b063 100644 --- a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -35,8 +35,24 @@ private void Execute(SourceProductionContext context, (Compilation Left, Immutab { var assertType = arg2.Left.GetTypeByMetadataName ("Xunit.Assert"); - GenerateMethods (assertType,context, "IsType",true); GenerateMethods (assertType, context, "Equal", false); + + GenerateMethods (assertType, context, "All", true); + GenerateMethods (assertType, context, "Collection", true); + GenerateMethods (assertType, context, "Contains", true); + GenerateMethods (assertType, context, "Distinct", true); + GenerateMethods (assertType, context, "DoesNotContain", true); + GenerateMethods (assertType, context, "DoesNotMatch", true); + GenerateMethods (assertType, context, "Empty", true); + GenerateMethods (assertType, context, "EndsWith", false); + GenerateMethods (assertType, context, "Equivalent", true); + GenerateMethods (assertType, context, "Fail", true); + GenerateMethods (assertType, context, "False", true); + GenerateMethods (assertType, context, "InRange", true); + GenerateMethods (assertType, context, "IsAssignableFrom", true); + GenerateMethods (assertType, context, "IsNotAssignableFrom", true); + GenerateMethods (assertType, context, "IsType", true); + GenerateMethods (assertType, context, "IsNotType", true); } private void GenerateMethods (INamedTypeSymbol? assertType, SourceProductionContext context, string methodName, bool invokeTExplicitly) diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs index 0493b88834..5c84580bac 100644 --- a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -34,7 +34,7 @@ public void TreeView_AllowReOrdering (V2TestDriver d) bike = new ("Bike") ] }; - + tv.AddObject (root); using GuiTestContext context = From e86a884877cb4329f0740f6ce561b9a69a3c4912 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 5 Apr 2025 13:18:39 +0100 Subject: [PATCH 18/34] Add remaining assert calls that are not obsolete --- .../TheGenerator.cs | 24 +++++++++++++++++++ .../TerminalGuiFluentTestingXunit.csproj | 1 + .../XunitContextExtensions.cs | 10 -------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs index dd0b13b063..1dafe16e54 100644 --- a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -53,6 +53,30 @@ private void Execute(SourceProductionContext context, (Compilation Left, Immutab GenerateMethods (assertType, context, "IsNotAssignableFrom", true); GenerateMethods (assertType, context, "IsType", true); GenerateMethods (assertType, context, "IsNotType", true); + + GenerateMethods (assertType, context, "Matches", true); + GenerateMethods (assertType, context, "Multiple", true); + GenerateMethods (assertType, context, "NotEmpty", true); + GenerateMethods (assertType, context, "NotEqual", true); + GenerateMethods (assertType, context, "NotInRange", true); + GenerateMethods (assertType, context, "NotNull", false); + GenerateMethods (assertType, context, "NotSame", true); + GenerateMethods (assertType, context, "NotStrictEqual", true); + GenerateMethods (assertType, context, "Null", false); + GenerateMethods (assertType, context, "ProperSubset", true); + GenerateMethods (assertType, context, "ProperSuperset", true); + GenerateMethods (assertType, context, "Raises", true); + GenerateMethods (assertType, context, "RaisesAny", true); + GenerateMethods (assertType, context, "Same", true); + GenerateMethods (assertType, context, "Single", true); + GenerateMethods (assertType, context, "StartsWith", false); + + GenerateMethods (assertType, context, "StrictEqual", true); + GenerateMethods (assertType, context, "Subset", true); + GenerateMethods (assertType, context, "Superset", true); +// GenerateMethods (assertType, context, "Throws", true); + // GenerateMethods (assertType, context, "ThrowsAny", true); + GenerateMethods (assertType, context, "True", false); } private void GenerateMethods (INamedTypeSymbol? assertType, SourceProductionContext context, string methodName, bool invokeTExplicitly) diff --git a/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj index 24edd52115..e9e661df26 100644 --- a/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj +++ b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj @@ -5,6 +5,7 @@ enable enable true + CS8714 diff --git a/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs index c4f9c38048..a007dbbc1b 100644 --- a/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs +++ b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs @@ -6,14 +6,4 @@ namespace TerminalGuiFluentTestingXunit; public static partial class XunitContextExtensions { // Placeholder - - public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition) - { - context.Then ( - () => - { - Assert.True (condition); - }); - return context; - } } From 1e2b0fa5993ef85f80221a6b5030547d6bd6cf09 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 5 Apr 2025 13:34:43 +0100 Subject: [PATCH 19/34] Fix unit test --- Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs index 5c84580bac..fc5282dc75 100644 --- a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -47,7 +47,7 @@ public void TreeView_AllowReOrdering (V2TestDriver d) .Then (() => Assert.Null (tv.GetObjectOnRow (1))) .Right () .ScreenShot ("After expanding", _out) - .AssertIsType(tv.SelectedObject) + .AssertIsAssignableFrom (tv.SelectedObject) .AssertEqual (root, tv.GetObjectOnRow (0)) .AssertEqual (car, tv.GetObjectOnRow (1)) .AssertEqual (lorry, tv.GetObjectOnRow (2)) From 6e350ffb32d282046d6b66954da6e83022c68fbc Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 5 Apr 2025 15:24:27 +0100 Subject: [PATCH 20/34] Roll back versions to be compatible with CI version of csharp --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 592cfc40d5..1a0afff7a4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,9 +8,9 @@ - - - + + + From 7250643b25bdf2af2859e089a760cd4fb68d0872 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 6 Apr 2025 06:37:29 +0100 Subject: [PATCH 21/34] Handle params and ref etc --- .../TheGenerator.cs | 252 ++++++++++-------- .../FluentTests/TreeViewFluentTests.cs | 68 +++-- 2 files changed, 189 insertions(+), 131 deletions(-) diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs index 1dafe16e54..49f4bd6c91 100644 --- a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -1,5 +1,4 @@ using System.Collections.Immutable; -using System.Reflection; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -7,33 +6,28 @@ namespace TerminalGuiFluentTestingXunit.Generator; - [Generator] public class TheGenerator : IIncrementalGenerator { - - /// - public void Initialize(IncrementalGeneratorInitializationContext context) + /// + public void Initialize (IncrementalGeneratorInitializationContext context) { - var provider = context.SyntaxProvider.CreateSyntaxProvider( - - predicate: static (node, _) => IsClass(node,"XunitContextExtensions"), - transform: static (ctx, _) => - (ClassDeclarationSyntax)ctx.Node) - .Where(m => m is { }); - - var compilation = context.CompilationProvider.Combine(provider.Collect()); - context.RegisterSourceOutput(compilation, Execute); + IncrementalValuesProvider provider = context.SyntaxProvider.CreateSyntaxProvider ( + static (node, _) => IsClass (node, "XunitContextExtensions"), + static (ctx, _) => + (ClassDeclarationSyntax)ctx.Node) + .Where (m => m is { }); + + IncrementalValueProvider<(Compilation Left, ImmutableArray Right)> compilation = + context.CompilationProvider.Combine (provider.Collect ()); + context.RegisterSourceOutput (compilation, Execute); } - private static bool IsClass (SyntaxNode node, string named) - { - return node is ClassDeclarationSyntax c && c.Identifier.Text == named; - } + private static bool IsClass (SyntaxNode node, string named) { return node is ClassDeclarationSyntax c && c.Identifier.Text == named; } - private void Execute(SourceProductionContext context, (Compilation Left, ImmutableArray Right) arg2) + private void Execute (SourceProductionContext context, (Compilation Left, ImmutableArray Right) arg2) { - var assertType = arg2.Left.GetTypeByMetadataName ("Xunit.Assert"); + INamedTypeSymbol? assertType = arg2.Left.GetTypeByMetadataName ("Xunit.Assert"); GenerateMethods (assertType, context, "Equal", false); @@ -74,8 +68,9 @@ private void Execute(SourceProductionContext context, (Compilation Left, Immutab GenerateMethods (assertType, context, "StrictEqual", true); GenerateMethods (assertType, context, "Subset", true); GenerateMethods (assertType, context, "Superset", true); + // GenerateMethods (assertType, context, "Throws", true); - // GenerateMethods (assertType, context, "ThrowsAny", true); + // GenerateMethods (assertType, context, "ThrowsAny", true); GenerateMethods (assertType, context, "True", false); } @@ -84,83 +79,89 @@ private void GenerateMethods (INamedTypeSymbol? assertType, SourceProductionCont var sb = new StringBuilder (); // Create a HashSet to track unique method signatures - var signaturesDone = new HashSet (); + HashSet signaturesDone = new (); + + List methods = assertType + .GetMembers (methodName) + .OfType () + .ToList (); - var methods = assertType - .GetMembers (methodName) - .OfType () - .ToList (); + var header = """" + #nullable enable + using TerminalGuiFluentTesting; + using Xunit; - string header = """" - #nullable enable - using TerminalGuiFluentTesting; - using Xunit; + namespace TerminalGuiFluentTestingXunit; - namespace TerminalGuiFluentTestingXunit; + public static partial class XunitContextExtensions + { - public static partial class XunitContextExtensions - { - - """"; + """"; - string tail = """ + var tail = """ - } - """; + } + """; sb.AppendLine (header); foreach (IMethodSymbol? m in methods) { - var signature = GetModifiedMethodSignature (m,methodName,invokeTExplicitly, out var paramNames, out var typeParams); + string signature = GetModifiedMethodSignature (m, methodName, invokeTExplicitly, out string [] paramNames, out string typeParams); if (!signaturesDone.Add (signature)) { continue; } - string method = $$""" - {{signature}} - { - try - { - Assert.{{methodName}}{{typeParams}} ({{string.Join (",", paramNames)}}); - } - catch(Exception) - { - context.HardStop (); - - - throw; - - } - - return context; - } - """; + var method = $$""" + {{signature}} + { + try + { + Assert.{{methodName}}{{typeParams}} ({{string.Join (",", paramNames)}}); + } + catch(Exception) + { + context.HardStop (); + + + throw; + + } + + return context; + } + """; sb.AppendLine (method); } + sb.AppendLine (tail); context.AddSource ($"XunitContextExtensions{methodName}.g.cs", sb.ToString ()); } - private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, string methodName, bool invokeTExplicitly, out string [] paramNames, out string typeParams) + private string GetModifiedMethodSignature ( + IMethodSymbol methodSymbol, + string methodName, + bool invokeTExplicitly, + out string [] paramNames, + out string typeParams + ) { typeParams = string.Empty; // Create the "this GuiTestContext context" parameter - var contextParam = SyntaxFactory.Parameter (SyntaxFactory.Identifier ("context")) - .WithType (SyntaxFactory.ParseTypeName ("GuiTestContext")) - .AddModifiers (SyntaxFactory.Token (SyntaxKind.ThisKeyword)); // Add the "this" keyword - + ParameterSyntax contextParam = SyntaxFactory.Parameter (SyntaxFactory.Identifier ("context")) + .WithType (SyntaxFactory.ParseTypeName ("GuiTestContext")) + .AddModifiers (SyntaxFactory.Token (SyntaxKind.ThisKeyword)); // Add the "this" keyword // Extract the parameter names (expected and actual) paramNames = new string [methodSymbol.Parameters.Length]; - for (int i = 0; i < methodSymbol.Parameters.Length; i++) + for (var i = 0; i < methodSymbol.Parameters.Length; i++) { paramNames [i] = methodSymbol.Parameters.ElementAt (i).Name; @@ -176,7 +177,7 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, string me } // Get the current method parameters and add the context parameter at the start - var parameters = methodSymbol.Parameters.Select (p=>CreateParameter(p)).ToList (); + List parameters = methodSymbol.Parameters.Select (p => CreateParameter (p)).ToList (); parameters.Insert (0, contextParam); // Insert 'context' as the first parameter @@ -187,11 +188,11 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, string me SyntaxToken newMethodName = SyntaxFactory.Identifier ($"Assert{methodName}"); // Handle generic type parameters if the method is generic - var typeParameters = methodSymbol.TypeParameters.Select ( - tp => - SyntaxFactory.TypeParameter (SyntaxFactory.Identifier (tp.Name)) - ) - .ToArray (); + TypeParameterSyntax [] typeParameters = methodSymbol.TypeParameters.Select ( + tp => + SyntaxFactory.TypeParameter (SyntaxFactory.Identifier (tp.Name)) + ) + .ToArray (); MethodDeclarationSyntax dec = SyntaxFactory.MethodDeclaration (returnType, newMethodName) .WithModifiers ( @@ -206,23 +207,29 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, string me dec = dec.WithTypeParameterList (SyntaxFactory.TypeParameterList (SyntaxFactory.SeparatedList (typeParameters))); // Handle type parameter constraints - var constraintClauses = methodSymbol.TypeParameters - .Where (tp => tp.ConstraintTypes.Length > 0) - .Select (tp => - SyntaxFactory.TypeParameterConstraintClause (tp.Name) - .WithConstraints ( - SyntaxFactory.SeparatedList ( - tp.ConstraintTypes.Select (constraintType => - SyntaxFactory.TypeConstraint (SyntaxFactory.ParseTypeName (constraintType.ToDisplayString ())) - ) - ) - ) - ).ToList (); + List constraintClauses = methodSymbol.TypeParameters + .Where (tp => tp.ConstraintTypes.Length > 0) + .Select ( + tp => + SyntaxFactory.TypeParameterConstraintClause (tp.Name) + .WithConstraints ( + SyntaxFactory + .SeparatedList ( + tp.ConstraintTypes.Select ( + constraintType => + SyntaxFactory.TypeConstraint ( + SyntaxFactory.ParseTypeName ( + constraintType + .ToDisplayString ())) + ) + ) + ) + ) + .ToList (); if (constraintClauses.Any ()) { dec = dec.WithConstraintClauses (SyntaxFactory.List (constraintClauses)); - } // Add the here @@ -236,19 +243,21 @@ private string GetModifiedMethodSignature (IMethodSymbol methodSymbol, string me MethodDeclarationSyntax methodSyntax = dec.NormalizeWhitespace (); // Convert the method syntax to a string - string methodString = methodSyntax.ToString (); + var methodString = methodSyntax.ToString (); return methodString; } /// - /// Creates a from a discovered parameter on real xunit method parameter + /// Creates a from a discovered parameter on real xunit method parameter + /// /// /// /// private ParameterSyntax CreateParameter (IParameterSymbol p) { - var paramName = p.Name; + string paramName = p.Name; + // Check if the parameter name is a reserved keyword and prepend "@" if it is if (IsReservedKeyword (paramName)) { @@ -256,35 +265,68 @@ private ParameterSyntax CreateParameter (IParameterSymbol p) } // Create the basic parameter syntax with the modified name and type - var parameterSyntax = SyntaxFactory.Parameter (SyntaxFactory.Identifier (paramName)) - .WithType (SyntaxFactory.ParseTypeName (p.Type.ToDisplayString ())); + ParameterSyntax parameterSyntax = SyntaxFactory.Parameter (SyntaxFactory.Identifier (paramName)) + .WithType (SyntaxFactory.ParseTypeName (p.Type.ToDisplayString ())); + + // Add 'params' keyword if the parameter has the Params modifier + var modifiers = new List (); + + if (p.IsParams) + { + modifiers.Add (SyntaxFactory.Token (SyntaxKind.ParamsKeyword)); + } + + // Handle ref/out/in modifiers + if (p.RefKind != RefKind.None) + { + SyntaxKind modifierKind = p.RefKind switch + { + RefKind.Ref => SyntaxKind.RefKeyword, + RefKind.Out => SyntaxKind.OutKeyword, + RefKind.In => SyntaxKind.InKeyword, + _ => throw new NotSupportedException ($"Unsupported RefKind: {p.RefKind}") + }; + + + modifiers.Add (SyntaxFactory.Token (modifierKind)); + } + + + if (modifiers.Any ()) + { + parameterSyntax = parameterSyntax.WithModifiers (SyntaxFactory.TokenList (modifiers)); + } // Add default value if one is present if (p.HasExplicitDefaultValue) { - var defaultValueExpression = p.ExplicitDefaultValue switch - { - null => SyntaxFactory.LiteralExpression (SyntaxKind.NullLiteralExpression), - bool b => SyntaxFactory.LiteralExpression (b ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression), - int i => SyntaxFactory.LiteralExpression (SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal (i)), - double d => SyntaxFactory.LiteralExpression (SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal (d)), - string s => SyntaxFactory.LiteralExpression (SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal (s)), - _ => SyntaxFactory.ParseExpression (p.ExplicitDefaultValue.ToString ()) // Fallback - }; + ExpressionSyntax defaultValueExpression = p.ExplicitDefaultValue switch + { + null => SyntaxFactory.LiteralExpression (SyntaxKind.NullLiteralExpression), + bool b => SyntaxFactory.LiteralExpression ( + b + ? SyntaxKind.TrueLiteralExpression + : SyntaxKind.FalseLiteralExpression), + int i => SyntaxFactory.LiteralExpression ( + SyntaxKind.NumericLiteralExpression, + SyntaxFactory.Literal (i)), + double d => SyntaxFactory.LiteralExpression ( + SyntaxKind.NumericLiteralExpression, + SyntaxFactory.Literal (d)), + string s => SyntaxFactory.LiteralExpression ( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal (s)), + _ => SyntaxFactory.ParseExpression (p.ExplicitDefaultValue.ToString ()) // Fallback + }; parameterSyntax = parameterSyntax.WithDefault ( - SyntaxFactory.EqualsValueClause (defaultValueExpression) - ); - + SyntaxFactory.EqualsValueClause (defaultValueExpression) + ); } return parameterSyntax; - } // Helper method to check if a parameter name is a reserved keyword - private bool IsReservedKeyword (string name) - { - return string.Equals (name, "object"); - } + private bool IsReservedKeyword (string name) { return string.Equals (name, "object"); } } diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs index cc762f2f1d..145f1337eb 100644 --- a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -46,11 +46,15 @@ public void TreeView_AllowReOrdering (V2TestDriver d) .Then (() => Assert.Null (tv.GetObjectOnRow (1))) .Right () .ScreenShot ("After expanding", _out) + .AssertMultiple ( + () => + { + Assert.Equal (root, tv.GetObjectOnRow (0)); + Assert.Equal (car, tv.GetObjectOnRow (1)); + Assert.Equal (lorry, tv.GetObjectOnRow (2)); + Assert.Equal (bike, tv.GetObjectOnRow (3)); + }) .AssertIsAssignableFrom (tv.SelectedObject) - .AssertEqual (root, tv.GetObjectOnRow (0)) - .AssertEqual (car, tv.GetObjectOnRow (1)) - .AssertEqual (lorry, tv.GetObjectOnRow (2)) - .AssertEqual (bike, tv.GetObjectOnRow (3)) .Then ( () => { @@ -60,10 +64,14 @@ public void TreeView_AllowReOrdering (V2TestDriver d) }) .WaitIteration () .ScreenShot ("After re-order", _out) - .AssertEqual (root, tv.GetObjectOnRow (0)) - .AssertEqual (bike, tv.GetObjectOnRow (1)) - .AssertEqual (car, tv.GetObjectOnRow (2)) - .AssertEqual (lorry, tv.GetObjectOnRow (3)) + .AssertMultiple ( + () => + { + Assert.Equal (root, tv.GetObjectOnRow (0)); + Assert.Equal (bike, tv.GetObjectOnRow (1)); + Assert.Equal (car, tv.GetObjectOnRow (2)); + Assert.Equal (lorry, tv.GetObjectOnRow (3)); + }) .WriteOutLogs (_out); context.Stop (); @@ -129,15 +137,19 @@ public void TreeViewReOrder_PreservesExpansion (V2TestDriver d) .Add (tv) .WaitIteration () .ScreenShot ("Initial State", _out) - .AssertEqual (root, tv.GetObjectOnRow (0)) - .AssertEqual (car, tv.GetObjectOnRow (1)) - .AssertEqual (mrA, tv.GetObjectOnRow (2)) - .AssertEqual (mrB, tv.GetObjectOnRow (3)) - .AssertEqual (lorry, tv.GetObjectOnRow (4)) - .AssertEqual (mrC, tv.GetObjectOnRow (5)) - .AssertEqual (bike, tv.GetObjectOnRow (6)) - .AssertEqual (mrD, tv.GetObjectOnRow (7)) - .AssertEqual (mrE, tv.GetObjectOnRow (8)) + .AssertMultiple ( + () => + { + Assert.Equal (root, tv.GetObjectOnRow (0)); + Assert.Equal (car, tv.GetObjectOnRow (1)); + Assert.Equal (mrA, tv.GetObjectOnRow (2)); + Assert.Equal (mrB, tv.GetObjectOnRow (3)); + Assert.Equal (lorry, tv.GetObjectOnRow (4)); + Assert.Equal (mrC, tv.GetObjectOnRow (5)); + Assert.Equal (bike, tv.GetObjectOnRow (6)); + Assert.Equal (mrD, tv.GetObjectOnRow (7)); + Assert.Equal (mrE, tv.GetObjectOnRow (8)); + }) .Then ( () => { @@ -147,15 +159,19 @@ public void TreeViewReOrder_PreservesExpansion (V2TestDriver d) }) .WaitIteration () .ScreenShot ("After re-order", _out) - .AssertEqual (root, tv.GetObjectOnRow (0)) - .AssertEqual (bike, tv.GetObjectOnRow (1)) - .AssertEqual (mrD, tv.GetObjectOnRow (2)) - .AssertEqual (mrE, tv.GetObjectOnRow (3)) - .AssertEqual (car, tv.GetObjectOnRow (4)) - .AssertEqual (mrA, tv.GetObjectOnRow (5)) - .AssertEqual (mrB, tv.GetObjectOnRow (6)) - .AssertEqual (lorry, tv.GetObjectOnRow (7)) - .AssertEqual (mrC, tv.GetObjectOnRow (8)) + .AssertMultiple ( + () => + { + Assert.Equal (root, tv.GetObjectOnRow (0)); + Assert.Equal (bike, tv.GetObjectOnRow (1)); + Assert.Equal (mrD, tv.GetObjectOnRow (2)); + Assert.Equal (mrE, tv.GetObjectOnRow (3)); + Assert.Equal (car, tv.GetObjectOnRow (4)); + Assert.Equal (mrA, tv.GetObjectOnRow (5)); + Assert.Equal (mrB, tv.GetObjectOnRow (6)); + Assert.Equal (lorry, tv.GetObjectOnRow (7)); + Assert.Equal (mrC, tv.GetObjectOnRow (8)); + }) .WriteOutLogs (_out); context.Stop (); From 4553624931d74aa240473ec4ac43137c15667fd9 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 6 Apr 2025 06:53:53 +0100 Subject: [PATCH 22/34] Fix null warning --- TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs index 49f4bd6c91..5c0427d115 100644 --- a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -27,7 +27,8 @@ public void Initialize (IncrementalGeneratorInitializationContext context) private void Execute (SourceProductionContext context, (Compilation Left, ImmutableArray Right) arg2) { - INamedTypeSymbol? assertType = arg2.Left.GetTypeByMetadataName ("Xunit.Assert"); + INamedTypeSymbol assertType = arg2.Left.GetTypeByMetadataName ("Xunit.Assert") + ?? throw new NotSupportedException("Referencing codebase does not include Xunit, could not find Xunit.Assert"); GenerateMethods (assertType, context, "Equal", false); @@ -74,7 +75,7 @@ private void Execute (SourceProductionContext context, (Compilation Left, Immuta GenerateMethods (assertType, context, "True", false); } - private void GenerateMethods (INamedTypeSymbol? assertType, SourceProductionContext context, string methodName, bool invokeTExplicitly) + private void GenerateMethods (INamedTypeSymbol assertType, SourceProductionContext context, string methodName, bool invokeTExplicitly) { var sb = new StringBuilder (); From d67edb8ed9c3e0d9e3a929bec508321652fdd56a Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 7 Apr 2025 08:43:09 +0100 Subject: [PATCH 23/34] WIP - start to add integration tests for FileDialog --- Terminal.Gui/Views/FileDialog.cs | 2 +- Terminal.Gui/Views/SaveDialog.cs | 10 +++- TerminalGuiFluentTesting/With.cs | 15 ++++++ .../FluentTests/FileDialogFluentTests.cs | 46 +++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index cdeb939a82..1e69cc9986 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -490,7 +490,7 @@ public override void OnLoaded () // if no path has been provided if (_tbPath.Text.Length <= 0) { - Path = Environment.CurrentDirectory; + Path = _fileSystem.Directory.GetCurrentDirectory (); } // to streamline user experience and allow direct typing of paths diff --git a/Terminal.Gui/Views/SaveDialog.cs b/Terminal.Gui/Views/SaveDialog.cs index 61ffb88e47..3c7f52be59 100644 --- a/Terminal.Gui/Views/SaveDialog.cs +++ b/Terminal.Gui/Views/SaveDialog.cs @@ -9,6 +9,7 @@ // * Use a line separator to show the file listing, so we can use same colors as the rest // * DirListView: Add mouse support +using System.IO.Abstractions; using Terminal.Gui.Resources; namespace Terminal.Gui; @@ -24,8 +25,15 @@ namespace Terminal.Gui; public class SaveDialog : FileDialog { /// Initializes a new . - public SaveDialog () { Style.OkButtonText = Strings.btnSave; } + public SaveDialog () + { + Style.OkButtonText = Strings.btnSave; + } + internal SaveDialog (IFileSystem fileSystem) : base (fileSystem) + { + Style.OkButtonText = Strings.btnSave; + } /// /// Gets the name of the file the user selected for saving, or null if the user canceled the /// . diff --git a/TerminalGuiFluentTesting/With.cs b/TerminalGuiFluentTesting/With.cs index b65d832385..078fdb1896 100644 --- a/TerminalGuiFluentTesting/With.cs +++ b/TerminalGuiFluentTesting/With.cs @@ -19,8 +19,23 @@ public static class With return new (() => new T (), width, height,v2TestDriver); } + /// + /// Overload that takes an existing instance + /// instead of creating one. + /// + /// + /// + /// + /// + /// + public static GuiTestContext A (Toplevel toplevel, int width, int height, V2TestDriver v2TestDriver) + { + return new (()=>toplevel, width, height, v2TestDriver); + } /// /// The global timeout to allow for any given application to run for before shutting down. /// public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds (30); + + } diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs new file mode 100644 index 0000000000..6f37f7dabe --- /dev/null +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -0,0 +1,46 @@ +using System.IO.Abstractions.TestingHelpers; +using Terminal.Gui; +using TerminalGuiFluentTesting; +using Xunit.Abstractions; + +namespace IntegrationTests.FluentTests; +public class FileDialogFluentTests +{ + private readonly TextWriter _out; + + public FileDialogFluentTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); } + + private MockFileSystem CreateExampleFileSystem () + { + + // Optional: use Ordinal to simulate Linux-style case sensitivity + var mockFileSystem = new MockFileSystem (new Dictionary ()); + + string testDir = mockFileSystem.Path.Combine ("test-dir"); + string subDir = mockFileSystem.Path.Combine (testDir, "sub-dir"); + string logsDir = "logs"; + string emptyDir = "empty-dir"; + + // Add files + mockFileSystem.AddFile (mockFileSystem.Path.Combine (testDir, "file1.txt"), new MockFileData ("Hello, this is file 1.")); + mockFileSystem.AddFile (mockFileSystem.Path.Combine (testDir, "file2.txt"), new MockFileData ("Hello, this is file 2.")); + mockFileSystem.AddFile (mockFileSystem.Path.Combine (subDir, "nested-file.txt"), new MockFileData ("This is a nested file.")); + mockFileSystem.AddFile (mockFileSystem.Path.Combine (logsDir, "log1.log"), new MockFileData ("Log entry 1")); + mockFileSystem.AddFile (mockFileSystem.Path.Combine (logsDir, "log2.log"), new MockFileData ("Log entry 2")); + + // Create an empty directory + mockFileSystem.AddDirectory (emptyDir); + + return mockFileSystem; + } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void CancelFileDialog (V2TestDriver d) + { + var sd = new SaveDialog ( CreateExampleFileSystem ()) + { }; + With.A (sd, 50, 10, d) + .ScreenShot ("Save dialog",_out); + } +} From 63ba43d1795200bf5ba601ed7bb8bb0ed570c4fa Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 7 Apr 2025 20:47:28 +0100 Subject: [PATCH 24/34] Add ability to tab focus to specific control with simple one line delegate --- Terminal.Gui/Views/FileDialog.cs | 2 + TerminalGuiFluentTesting/GuiTestContext.cs | 130 ++++++++++++++++++ .../FluentTests/FileDialogFluentTests.cs | 27 +++- 3 files changed, 154 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 1e69cc9986..03b8434054 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -103,6 +103,8 @@ internal FileDialog (IFileSystem fileSystem) return; } + e.Cancel = true; + if (Modal) { Application.RequestStop (); diff --git a/TerminalGuiFluentTesting/GuiTestContext.cs b/TerminalGuiFluentTesting/GuiTestContext.cs index 06977c9775..caed95f0c6 100644 --- a/TerminalGuiFluentTesting/GuiTestContext.cs +++ b/TerminalGuiFluentTesting/GuiTestContext.cs @@ -471,6 +471,75 @@ public GuiTestContext Enter () return this; } + + /// + /// Simulates pressing the Esc (Escape) key. + /// + /// + /// + public GuiTestContext Escape () + { + switch (_driver) + { + case V2TestDriver.V2Win: + SendWindowsKey ( + new WindowsConsole.KeyEventRecord + { + UnicodeChar = '\u001b', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed, + wRepeatCount = 1, + wVirtualKeyCode = ConsoleKeyMapping.VK.ESCAPE, + wVirtualScanCode = 1 + }); + break; + case V2TestDriver.V2Net: + + // Note that this accurately describes how Esc comes in. Typically, ConsoleKey is None + // even though you would think it would be Escape - it isn't + SendNetKey (new ('\u001b', ConsoleKey.None, false, false, false)); + break; + default: + throw new ArgumentOutOfRangeException (); + } + + return this; + } + + + + /// + /// Simulates pressing the Tab key. + /// + /// + /// + public GuiTestContext Tab () + { + switch (_driver) + { + case V2TestDriver.V2Win: + SendWindowsKey ( + new WindowsConsole.KeyEventRecord + { + UnicodeChar = '\t', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed, + wRepeatCount = 1, + wVirtualKeyCode = 0, + wVirtualScanCode = 0 + }); + break; + case V2TestDriver.V2Net: + + // Note that this accurately describes how Tab comes in. Typically, ConsoleKey is None + // even though you would think it would be Tab - it isn't + SendNetKey (new ('\t', ConsoleKey.None, false, false, false)); + break; + default: + throw new ArgumentOutOfRangeException (); + } + + return this; + } + /// /// Registers a right click handler on the added view (or root view) that /// will open the supplied . @@ -592,4 +661,65 @@ public GuiTestContext Focus (View toFocus) return WaitIteration (); } + + /// + /// Tabs through the UI until a View matching the + /// is found (of Type T) or all views are looped through (back to the beginning) + /// in which case triggers hard stop and Exception + /// + /// + /// + public GuiTestContext Focus (Func evaluator) where T:View + { + var t = Application.Top; + + HashSet seen = new (); + + if (t == null) + { + Fail ("Application.Top was null when trying to set focus"); + return this; + } + + do + { + var next = t.MostFocused; + + // Is view found? + if (next is T v && evaluator (v)) + { + return this; + } + + // No, try tab to the next (or first) + this.Tab (); + WaitIteration (); + next = t.MostFocused; + + if (next is null) + { + Fail ("Failed to tab to a view which matched the Type and evaluator constraints of the test because MostFocused became or was always null"); + return this; + } + + // Track the views we have seen + // We have looped around to the start again if it was already there + if (!seen.Add (next)) + { + Fail ("Failed to tab to a view which matched the Type and evaluator constraints of the test before looping back to the original View"); + + return this; + } + + } + while (true); + } + + private void Fail (string reason) + { + Stop (); + + throw new Exception (reason); + + } } diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs index 6f37f7dabe..e1772dbdc5 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -36,11 +36,28 @@ private MockFileSystem CreateExampleFileSystem () [Theory] [ClassData (typeof (V2TestDrivers))] - public void CancelFileDialog (V2TestDriver d) + public void CancelFileDialog_UsingEscape (V2TestDriver d) { - var sd = new SaveDialog ( CreateExampleFileSystem ()) - { }; - With.A (sd, 50, 10, d) - .ScreenShot ("Save dialog",_out); + var sd = new SaveDialog ( CreateExampleFileSystem ()); + using var c = With.A (sd, 100, 20, d) + .ScreenShot ("Save dialog",_out) + .Escape() + .Stop (); + + Assert.True (sd.Canceled); + } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void CancelFileDialog_UsingCancelButton (V2TestDriver d) + { + var sd = new SaveDialog (CreateExampleFileSystem ()); + using var c = With.A (sd, 100, 20, d) + .ScreenShot ("Save dialog", _out) + .Focus