diff --git a/Directory.Packages.props b/Directory.Packages.props index 18afbe64a4..1a0afff7a4 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/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/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..5c0427d115 --- /dev/null +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -0,0 +1,333 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TerminalGuiFluentTestingXunit.Generator; + +[Generator] +public class TheGenerator : IIncrementalGenerator +{ + /// + public void Initialize (IncrementalGeneratorInitializationContext context) + { + 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 void Execute (SourceProductionContext context, (Compilation Left, ImmutableArray Right) arg2) + { + 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); + + 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); + + 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) + { + var sb = new StringBuilder (); + + // Create a HashSet to track unique method signatures + HashSet signaturesDone = new (); + + List methods = assertType + .GetMembers (methodName) + .OfType () + .ToList (); + + var header = """" + #nullable enable + using TerminalGuiFluentTesting; + using Xunit; + + namespace TerminalGuiFluentTestingXunit; + + public static partial class XunitContextExtensions + { + + + """"; + + var tail = """ + + } + """; + + sb.AppendLine (header); + + foreach (IMethodSymbol? m in methods) + { + string signature = GetModifiedMethodSignature (m, methodName, invokeTExplicitly, out string [] paramNames, out string typeParams); + + if (!signaturesDone.Add (signature)) + { + continue; + } + + 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 + ) + { + typeParams = string.Empty; + + // Create the "this GuiTestContext context" parameter + 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 (var i = 0; i < methodSymbol.Parameters.Length; 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 + List parameters = methodSymbol.Parameters.Select (p => CreateParameter (p)).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 newMethodName = SyntaxFactory.Identifier ($"Assert{methodName}"); + + // Handle generic type parameters if the method is generic + TypeParameterSyntax [] typeParameters = methodSymbol.TypeParameters.Select ( + tp => + SyntaxFactory.TypeParameter (SyntaxFactory.Identifier (tp.Name)) + ) + .ToArray (); + + MethodDeclarationSyntax dec = SyntaxFactory.MethodDeclaration (returnType, newMethodName) + .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))); + + // Handle type parameter constraints + 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 + if (invokeTExplicitly) + { + typeParams = "<" + string.Join (", ", typeParameters.Select (tp => tp.Identifier.ValueText)) + ">"; + } + } + + // Build the method signature syntax tree + MethodDeclarationSyntax methodSyntax = dec.NormalizeWhitespace (); + + // Convert the method syntax to a string + var methodString = methodSyntax.ToString (); + + return methodString; + } + + /// + /// Creates a from a discovered parameter on real xunit method parameter + /// + /// + /// + /// + private ParameterSyntax CreateParameter (IParameterSymbol p) + { + string 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 + 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) + { + 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) + ); + } + + return parameterSyntax; + } + + // 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/TerminalGuiFluentTestingXunit.csproj b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj new file mode 100644 index 0000000000..e9e661df26 --- /dev/null +++ b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + true + CS8714 + + + + + + + + + diff --git a/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs new file mode 100644 index 0000000000..a007dbbc1b --- /dev/null +++ b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs @@ -0,0 +1,9 @@ +using TerminalGuiFluentTesting; +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..145f1337eb 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; @@ -33,7 +34,6 @@ public void TreeView_AllowReOrdering (V2TestDriver d) bike = new ("Bike") ] }; - tv.AddObject (root); using GuiTestContext context = @@ -46,10 +46,15 @@ public void TreeView_AllowReOrdering (V2TestDriver d) .Then (() => Assert.Null (tv.GetObjectOnRow (1))) .Right () .ScreenShot ("After expanding", _out) - .AssertEqual (root, tv.GetObjectOnRow (0)) - .AssertEqual (car, tv.GetObjectOnRow (1)) - .AssertEqual (lorry, tv.GetObjectOnRow (2)) - .AssertEqual (bike, tv.GetObjectOnRow (3)) + .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) .Then ( () => { @@ -59,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 (); @@ -128,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 ( () => { @@ -146,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 (); 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 @@ - +