Merge pull request #3587 from tig/v2_1845-Application-ToString

Fixes #1845. `Application.ToString` returns formatted driver contents
This commit is contained in:
Tig
2024-07-07 13:21:25 -06:00
committed by GitHub
2 changed files with 207 additions and 313 deletions

View File

@@ -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..
/// <summary>
/// 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..
/// </summary>
[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method)]
public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
{
private readonly Type _driverType;
/// <summary>
/// Initializes a [AutoInitShutdown] attribute, which determines if/how Application.Init and Application.Shutdown
/// are automatically called Before/After a test runs.
/// </summary>
/// <param name="autoInit">If true, Application.Init will be called Before the test runs.</param>
/// <param name="autoShutdown">If true, Application.Shutdown will be called After the test runs.</param>
/// <param name="consoleDriverType">
/// 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: <not found>"
);
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: <not found>"
);
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
}
/// <summary>
/// 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.
/// </summary>
/// <param name="expectedLook"></param>
/// <param name="output"></param>
@@ -337,7 +335,6 @@ internal partial class TestHelpers
Cell [,] contents = driver.Contents;
for (var rowIndex = 0; rowIndex < driver.Rows; rowIndex++)
{
List<Rune> 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
///// <summary>Gets a list of instances of all classes derived from View.</summary>
///// <returns>List of View objects</returns>
//public static List<View> 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<Type> ())))
// .ToList ();
//}
//public class AllViewsData : IEnumerable<object []>
//{
// private Lazy<List<object []>> data;
// public AllViewsData ()
// {
// data = new Lazy<List<object []>> (GetTestData);
// }
// public IEnumerator<object []> GetEnumerator ()
// {
// return data.Value.GetEnumerator ();
// }
// IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
// private List<object []> GetTestData ()
// {
// var viewTypes = typeof (View).Assembly
// .GetTypes ()
// .Where (type => type.IsClass && !type.IsAbstract && type.IsPublic && type.IsSubclassOf (typeof (View)));
// var testData = new List<object []> ();
// foreach (var type in viewTypes)
// {
// var view = CreateView (type, type.GetConstructor (Array.Empty<Type> ()));
// testData.Add (new object [] { view, type.Name });
// }
// return testData;
// }
//}
/// <summary>
/// Verifies the console used all the <paramref name="expectedColors"/> 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.
/// </summary>
/// <param name="driver">if null uses <see cref="Application.Driver"/></param>
/// <param name="expectedColors"></param>
internal static void AssertDriverUsedColors (ConsoleDriver driver = null, params Attribute [] expectedColors)
{
driver ??= Application.Driver;
Cell [,] contents = driver.Contents;
List<Attribute> toFind = expectedColors.ToList ();
// Contents 3rd column is an Attribute
HashSet<Attribute> 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<object> 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<object> ());
}
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<Type> 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<object> 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<Type> 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<Type> GetAllViewClasses ()
{
return typeof (View).Assembly.GetTypes ()
.Where (
myType => myType.IsClass
&& !myType.IsAbstract
&& myType.IsPublic
&& myType.IsSubclassOf (typeof (View))
)
.ToList ();
}
/// <summary>
/// Verifies the console used all the <paramref name="expectedColors"/> 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.
/// </summary>
/// <param name="driver">if null uses <see cref="Application.Driver"/></param>
/// <param name="expectedColors"></param>
internal static void AssertDriverUsedColors (ConsoleDriver driver = null, params Attribute [] expectedColors)
{
driver ??= Application.Driver;
Cell [,] contents = driver.Contents;
List<Attribute> toFind = expectedColors.ToList ();
// Contents 3rd column is an Attribute
HashSet<Attribute> 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<object> 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<object> ());
}
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;
}
}