diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 9e501f22c..947cab271 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -975,7 +975,7 @@ public static partial class Application if (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded || OverlappedChildNeedsDisplay ()) { - state.Toplevel.SetNeedsDisplay(); + state.Toplevel.SetNeedsDisplay (); state.Toplevel.Draw (); Driver.UpdateScreen (); @@ -1439,4 +1439,62 @@ public static partial class Application } #endregion Toplevel handling + + /// + /// Gets a string representation of the Application as rendered by . + /// + /// A string representation of the Application + public new static string ToString () + { + ConsoleDriver driver = Driver; + + if (driver is null) + { + return string.Empty; + } + + return ToString (driver); + } + + /// + /// Gets a string representation of the Application rendered by the provided . + /// + /// The driver to use to render the contents. + /// A string representation of the Application + public static string ToString (ConsoleDriver driver) + { + var sb = new StringBuilder (); + + Cell [,] contents = driver.Contents; + + for (var r = 0; r < driver.Rows; r++) + { + for (var c = 0; c < driver.Cols; c++) + { + Rune rune = contents [r, c].Rune; + + if (rune.DecodeSurrogatePair (out char [] sp)) + { + sb.Append (sp); + } + else + { + sb.Append ((char)rune.Value); + } + + if (rune.GetColumns () > 1) + { + c++; + } + + // See Issue #2616 + //foreach (var combMark in contents [r, c].CombiningMarks) { + // sb.Append ((char)combMark.Value); + //} + } + + sb.AppendLine (); + } + return sb.ToString (); + } } diff --git a/UnitTests/TestHelpers.cs b/UnitTests/TestHelpers.cs index 32b68eb37..899a35986 100644 --- a/UnitTests/TestHelpers.cs +++ b/UnitTests/TestHelpers.cs @@ -1,32 +1,28 @@ -using System.Collections; -using System.Diagnostics; +using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using UICatalog; using Xunit.Abstractions; using Xunit.Sdk; namespace Terminal.Gui; -// This class enables test functions annotated with the [AutoInitShutdown] attribute to -// automatically call Application.Init at start of the test and Application.Shutdown after the -// test exits. -// -// This is necessary because a) Application is a singleton and Init/Shutdown must be called -// as a pair, and b) all unit test functions should be atomic.. +/// +/// This class enables test functions annotated with the [AutoInitShutdown] attribute to +/// automatically call Application.Init at start of the test and Application.Shutdown after the +/// test exits. +/// This is necessary because a) Application is a singleton and Init/Shutdown must be called +/// as a pair, and b) all unit test functions should be atomic.. +/// [AttributeUsage (AttributeTargets.Class | AttributeTargets.Method)] public class AutoInitShutdownAttribute : BeforeAfterTestAttribute { - private readonly Type _driverType; - /// /// Initializes a [AutoInitShutdown] attribute, which determines if/how Application.Init and Application.Shutdown /// are automatically called Before/After a test runs. /// /// If true, Application.Init will be called Before the test runs. - /// If true, Application.Shutdown will be called After the test runs. /// /// Determines which ConsoleDriver (FakeDriver, WindowsDriver, CursesDriver, NetDriver) /// will be used when Application.Init is called. If null FakeDriver will be used. Only valid if @@ -65,7 +61,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute ConfigurationManager.Locations = configLocation; } - private bool AutoInit { get; } + private readonly Type _driverType; public override void After (MethodInfo methodUnderTest) { @@ -102,6 +98,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute ConfigurationManager.Reset (); #if DEBUG_IDISPOSABLE + // Clear out any lingering Responder instances from previous tests if (Responder.Instances.Count == 0) { @@ -115,6 +112,8 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute Application.Init ((ConsoleDriver)Activator.CreateInstance (_driverType)); } } + + private bool AutoInit { get; } } [AttributeUsage (AttributeTargets.Class | AttributeTargets.Method)] @@ -178,8 +177,8 @@ public class SetupFakeDriverAttribute : BeforeAfterTestAttribute [AttributeUsage (AttributeTargets.Class | AttributeTargets.Method)] public class TestDateAttribute : BeforeAfterTestAttribute { - private readonly CultureInfo _currentCulture = CultureInfo.CurrentCulture; public TestDateAttribute () { CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; } + private readonly CultureInfo _currentCulture = CultureInfo.CurrentCulture; public override void After (MethodInfo methodUnderTest) { @@ -238,12 +237,12 @@ internal partial class TestHelpers switch (match.Count) { case 0: - throw new Exception ( - $"{DriverContentsToString (driver)}\n" - + $"Expected Attribute {val} (PlatformColor = {val.Value.PlatformColor}) at Contents[{line},{c}] {contents [line, c]} ((PlatformColor = {contents [line, c].Attribute.Value.PlatformColor}) was not found.\n" - + $" Expected: {string.Join (",", expectedAttributes.Select (c => c))}\n" - + $" But Was: " - ); + throw new ( + $"{Application.ToString (driver)}\n" + + $"Expected Attribute {val} (PlatformColor = {val.Value.PlatformColor}) at Contents[{line},{c}] {contents [line, c]} ((PlatformColor = {contents [line, c].Attribute.Value.PlatformColor}) was not found.\n" + + $" Expected: {string.Join (",", expectedAttributes.Select (c => c))}\n" + + $" But Was: " + ); case > 1: throw new ArgumentException ( $"Bad value for expectedColors, {match.Count} Attributes had the same Value" @@ -255,12 +254,12 @@ internal partial class TestHelpers if (colorUsed != userExpected) { - throw new Exception ( - $"{DriverContentsToString (driver)}\n" - + $"Unexpected Attribute at Contents[{line},{c}] {contents [line, c]}.\n" - + $" Expected: {userExpected} ({expectedAttributes [int.Parse (userExpected.ToString ())]})\n" - + $" But Was: {colorUsed} ({val})\n" - ); + throw new ( + $"{Application.ToString (driver)}\n" + + $"Unexpected Attribute at Contents[{line},{c}] {contents [line, c]}.\n" + + $" Expected: {userExpected} ({expectedAttributes [int.Parse (userExpected.ToString ())]})\n" + + $" But Was: {colorUsed} ({val})\n" + ); } } @@ -282,7 +281,7 @@ internal partial class TestHelpers ) { #pragma warning restore xUnit1013 // Public method should be marked as test - string actualLook = DriverContentsToString (driver); + var actualLook = Application.ToString (driver ?? Application.Driver); if (string.Equals (expectedLook, actualLook)) { @@ -314,8 +313,7 @@ internal partial class TestHelpers } /// - /// Asserts that the driver contents are equal to the expected look, and that the cursor is at the expected - /// position. + /// Asserts that the driver contents are equal to the provided string. /// /// /// @@ -337,7 +335,6 @@ internal partial class TestHelpers Cell [,] contents = driver.Contents; - for (var rowIndex = 0; rowIndex < driver.Rows; rowIndex++) { List runes = []; @@ -353,7 +350,7 @@ internal partial class TestHelpers x = colIndex; y = rowIndex; - for (int i = 0; i < colIndex; i++) + for (var i = 0; i < colIndex; i++) { runes.InsertRange (i, [SpaceRune]); } @@ -433,7 +430,7 @@ internal partial class TestHelpers if (string.Equals (expectedLook, actualLook)) { - return new Rectangle (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0); + return new (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0); } // standardize line endings for the comparison @@ -453,7 +450,7 @@ internal partial class TestHelpers Assert.Equal (expectedLook, actualLook); - return new Rectangle (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0); + return new (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0); } #pragma warning disable xUnit1013 // Public method should be marked as test @@ -483,278 +480,6 @@ internal partial class TestHelpers } #pragma warning restore xUnit1013 // Public method should be marked as test - public static string DriverContentsToString (ConsoleDriver driver = null) - { - var sb = new StringBuilder (); - driver ??= Application.Driver; - - Cell [,] contents = driver.Contents; - - for (var r = 0; r < driver.Rows; r++) - { - for (var c = 0; c < driver.Cols; c++) - { - Rune rune = contents [r, c].Rune; - - if (rune.DecodeSurrogatePair (out char [] sp)) - { - sb.Append (sp); - } - else - { - sb.Append ((char)rune.Value); - } - - if (rune.GetColumns () > 1) - { - c++; - } - - // See Issue #2616 - //foreach (var combMark in contents [r, c].CombiningMarks) { - // sb.Append ((char)combMark.Value); - //} - } - - sb.AppendLine (); - } - - return sb.ToString (); - } - - //// TODO: Update all tests that use GetALlViews to use GetAllViewsTheoryData instead - ///// Gets a list of instances of all classes derived from View. - ///// List of View objects - //public static List GetAllViews () - //{ - // return typeof (View).Assembly.GetTypes () - // .Where ( - // type => type.IsClass - // && !type.IsAbstract - // && type.IsPublic - // && type.IsSubclassOf (typeof (View)) - // ) - // .Select (type => CreateView (type, type.GetConstructor (Array.Empty ()))) - // .ToList (); - //} - - //public class AllViewsData : IEnumerable - //{ - // private Lazy> data; - - // public AllViewsData () - // { - // data = new Lazy> (GetTestData); - // } - - // public IEnumerator GetEnumerator () - // { - // return data.Value.GetEnumerator (); - // } - - // IEnumerator IEnumerable.GetEnumerator () => GetEnumerator (); - - // private List GetTestData () - // { - // var viewTypes = typeof (View).Assembly - // .GetTypes () - // .Where (type => type.IsClass && !type.IsAbstract && type.IsPublic && type.IsSubclassOf (typeof (View))); - - // var testData = new List (); - - // foreach (var type in viewTypes) - // { - // var view = CreateView (type, type.GetConstructor (Array.Empty ())); - // testData.Add (new object [] { view, type.Name }); - // } - - // return testData; - // } - //} - - - /// - /// Verifies the console used all the when rendering. If one or more of the - /// expected colors are not used then the failure will output both the colors that were found to be used and which of - /// your expectations was not met. - /// - /// if null uses - /// - internal static void AssertDriverUsedColors (ConsoleDriver driver = null, params Attribute [] expectedColors) - { - driver ??= Application.Driver; - Cell [,] contents = driver.Contents; - - List toFind = expectedColors.ToList (); - - // Contents 3rd column is an Attribute - HashSet colorsUsed = new (); - - for (var r = 0; r < driver.Rows; r++) - { - for (var c = 0; c < driver.Cols; c++) - { - Attribute? val = contents [r, c].Attribute; - - if (val.HasValue) - { - colorsUsed.Add (val.Value); - - Attribute match = toFind.FirstOrDefault (e => e == val); - - // need to check twice because Attribute is a struct and therefore cannot be null - if (toFind.Any (e => e == val)) - { - toFind.Remove (match); - } - } - } - } - - if (!toFind.Any ()) - { - return; - } - - var sb = new StringBuilder (); - sb.AppendLine ("The following colors were not used:" + string.Join ("; ", toFind.Select (a => a.ToString ()))); - sb.AppendLine ("Colors used were:" + string.Join ("; ", colorsUsed.Select (a => a.ToString ()))); - - throw new Exception (sb.ToString ()); - } - - private static void AddArguments (Type paramType, List pTypes) - { - if (paramType == typeof (Rectangle)) - { - pTypes.Add (Rectangle.Empty); - } - else if (paramType == typeof (string)) - { - pTypes.Add (string.Empty); - } - else if (paramType == typeof (int)) - { - pTypes.Add (0); - } - else if (paramType == typeof (bool)) - { - pTypes.Add (true); - } - else if (paramType.Name == "IList") - { - pTypes.Add (new List ()); - } - else if (paramType.Name == "View") - { - var top = new Toplevel (); - var view = new View (); - top.Add (view); - pTypes.Add (view); - } - else if (paramType.Name == "View[]") - { - pTypes.Add (new View [] { }); - } - else if (paramType.Name == "Stream") - { - pTypes.Add (new MemoryStream ()); - } - else if (paramType.Name == "String") - { - pTypes.Add (string.Empty); - } - else if (paramType.Name == "TreeView`1[T]") - { - pTypes.Add (string.Empty); - } - else - { - pTypes.Add (null); - } - } - - public static View CreateView (Type type, ConstructorInfo ctor) - { - View view = null; - - if (type.IsGenericType && type.IsTypeDefinition) - { - List gTypes = new (); - - foreach (Type args in type.GetGenericArguments ()) - { - gTypes.Add (typeof (object)); - } - - type = type.MakeGenericType (gTypes.ToArray ()); - - Assert.IsType (type, (View)Activator.CreateInstance (type)); - } - else - { - ParameterInfo [] paramsInfo = ctor.GetParameters (); - Type paramType; - List pTypes = new (); - - if (type.IsGenericType) - { - foreach (Type args in type.GetGenericArguments ()) - { - paramType = args.GetType (); - - if (args.Name == "T") - { - pTypes.Add (typeof (object)); - } - else - { - AddArguments (paramType, pTypes); - } - } - } - - foreach (ParameterInfo p in paramsInfo) - { - paramType = p.ParameterType; - - if (p.HasDefaultValue) - { - pTypes.Add (p.DefaultValue); - } - else - { - AddArguments (paramType, pTypes); - } - } - - if (type.IsGenericType && !type.IsTypeDefinition) - { - view = (View)Activator.CreateInstance (type); - Assert.IsType (type, view); - } - else - { - view = (View)ctor.Invoke (pTypes.ToArray ()); - Assert.IsType (type, view); - } - } - - return view; - } - - public static List GetAllViewClasses () - { - return typeof (View).Assembly.GetTypes () - .Where ( - myType => myType.IsClass - && !myType.IsAbstract - && myType.IsPublic - && myType.IsSubclassOf (typeof (View)) - ) - .ToList (); - } - public static View CreateViewFromType (Type type, ConstructorInfo ctor) { View viewType = null; @@ -824,6 +549,119 @@ internal partial class TestHelpers return viewType; } + public static List GetAllViewClasses () + { + return typeof (View).Assembly.GetTypes () + .Where ( + myType => myType.IsClass + && !myType.IsAbstract + && myType.IsPublic + && myType.IsSubclassOf (typeof (View)) + ) + .ToList (); + } + + /// + /// Verifies the console used all the when rendering. If one or more of the + /// expected colors are not used then the failure will output both the colors that were found to be used and which of + /// your expectations was not met. + /// + /// if null uses + /// + internal static void AssertDriverUsedColors (ConsoleDriver driver = null, params Attribute [] expectedColors) + { + driver ??= Application.Driver; + Cell [,] contents = driver.Contents; + + List toFind = expectedColors.ToList (); + + // Contents 3rd column is an Attribute + HashSet colorsUsed = new (); + + for (var r = 0; r < driver.Rows; r++) + { + for (var c = 0; c < driver.Cols; c++) + { + Attribute? val = contents [r, c].Attribute; + + if (val.HasValue) + { + colorsUsed.Add (val.Value); + + Attribute match = toFind.FirstOrDefault (e => e == val); + + // need to check twice because Attribute is a struct and therefore cannot be null + if (toFind.Any (e => e == val)) + { + toFind.Remove (match); + } + } + } + } + + if (!toFind.Any ()) + { + return; + } + + var sb = new StringBuilder (); + sb.AppendLine ("The following colors were not used:" + string.Join ("; ", toFind.Select (a => a.ToString ()))); + sb.AppendLine ("Colors used were:" + string.Join ("; ", colorsUsed.Select (a => a.ToString ()))); + + throw new (sb.ToString ()); + } + + private static void AddArguments (Type paramType, List pTypes) + { + if (paramType == typeof (Rectangle)) + { + pTypes.Add (Rectangle.Empty); + } + else if (paramType == typeof (string)) + { + pTypes.Add (string.Empty); + } + else if (paramType == typeof (int)) + { + pTypes.Add (0); + } + else if (paramType == typeof (bool)) + { + pTypes.Add (true); + } + else if (paramType.Name == "IList") + { + pTypes.Add (new List ()); + } + else if (paramType.Name == "View") + { + var top = new Toplevel (); + var view = new View (); + top.Add (view); + pTypes.Add (view); + } + else if (paramType.Name == "View[]") + { + pTypes.Add (new View [] { }); + } + else if (paramType.Name == "Stream") + { + pTypes.Add (new MemoryStream ()); + } + else if (paramType.Name == "String") + { + pTypes.Add (string.Empty); + } + else if (paramType.Name == "TreeView`1[T]") + { + pTypes.Add (string.Empty); + } + else + { + pTypes.Add (null); + } + } + [GeneratedRegex ("^\\s+", RegexOptions.Multiline)] private static partial Regex LeadingWhitespaceRegEx (); @@ -832,11 +670,11 @@ internal partial class TestHelpers string replaced = toReplace; replaced = Environment.NewLine.Length switch - { - 2 when !replaced.Contains ("\r\n") => replaced.Replace ("\n", Environment.NewLine), - 1 => replaced.Replace ("\r\n", Environment.NewLine), - var _ => replaced - }; + { + 2 when !replaced.Contains ("\r\n") => replaced.Replace ("\n", Environment.NewLine), + 1 => replaced.Replace ("\r\n", Environment.NewLine), + var _ => replaced + }; return replaced; } @@ -863,6 +701,4 @@ public class TestsAllViews return Activator.CreateInstance (type) as View; } - } -