Files
Terminal.Gui/Tests/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs
Thomas Nind 51dda7e69f Fixes #3947 Adds Fake driver and fixes fluent tests (iteration-zero) (#4225)
* Consider width2 chars that are not IsBmp

* Apply same fix in WindowsDriver

* Explicitly use type of local variable

* Revert changes to WindowsDriver

* Assume we are running in a terminal that supports true color by default unless user explicitly forces 16

* Switch to SetAttribute and WriteConsole instead of WriteConsoleOutput for 16 color mode

* Fix some cursor issues (WIP)

* Remove concept of 'dirty rows' from v2 as its never actually used

* Remove damageRegion as it does nothing

* Make string builder to console writing simpler

* Radically simplify Write method

* Simplify conditional logic

* Simplify restoring cursor position

* Reference local variable for console buffer

* Reduce calls to ConsoleWrite by accumulating till attribute changes

* When resizing v2 16 color mode on windows, recreate the back buffer to match its size

* Fixes for VTS enabled

* Fix _lastSize never being assigned

* Fixes VTS for Force16Colors

* Fixes force16Colors in VTS

* Fixes escape sequences always echoing in non-VTS

* Force Force16Colors in non-VTS. It have a bug in adding a newline in the last line

* WIP Add base class for NetOutput

* Abstract away how we change attribute

* WIP - Make WindowsOutput use base class

* WIP working to fix set cursor position

* Remove commented out code

* Fixes legacy output mode

* Fixes size with no alt buffer supported on VTS and size restore after maximized.

* Fix set cursor which also fixes the broken surrogate pairs

* Add force parameter

* Fixes an issue that only happens with Windows Terminal when paste surrogate pairs by press Ctrl+V

* In Windows escape sequences must be sent during the lifetime of the console which is created in input handle

* Ensure flush the input buffer before reset the console

* Flush input buffer before reset console in v2win

* Fixes issue in v2net not being refreshing the menu bar at start

* Only force layout and draw on size changed.

* Fix v2net issue not draw first line by forcing set cursor position

* Set _lastCursorPosition nullable and remove bool force from set cursor position

* Remove force parameter

* Add v2 version of fake driver attribute

* Make direct replacement and wire up window resizing events

* Update casts to use V2 fake driver instead

* Adjust interfaces to expose less internals

* Fix not raising iteration event in v2

* WIP investigate what it takes to do resize and redraw using TextAlignment_Centered as example

* Sketch adding component factory

* Create relevant fake component factories

* Add window size monitor into factory

* Fake size monitor injecting

* Add helper for faking console resize in AutoInitShutdown tests

* Fix size setting in FakeDriverV2

* Switch to new method

* Fix IsLegacy becoming false when using blank constructor

* Fix for Ready not being raised when showing same top twice also fixes garbage collection issue if running millions of top levels

* Fix tests

* Remove auto init

* Restore conditional compilation stuff

* Restore 'if running unit tests' logic

* Check only for the output being specific classes for the suppression

* Fix ShadowView blowing up with index out of bounds error

* Fix resize in fluent tests

* Fix for people using Iteration call directly

* Fix more calls to iteration to use
        AutoInitShutdownAttribute.RunIteration ();

* Add comment

* Remove assumption that Run with prior view not disposed should throw

* Fix timings in Dialog_Opened_From_Another_Dialog

* Fix Zero_Buttons_Works

* Standardize and fix Button_IsDefault_True_Return_His_Index_On_Accepting

* Fix iteration counts on MessageBoxTests

* Fix WizartTests and DrawTests_Ruler

* Implement SendKeys into ConsoleDriverFacade

* Fix SendKeys in console driver facade such that FileDialogTests works
Fix when Clip is null in popover

* Add missing dispose call to test

* Fix support for Esc in facade SendKeys

* Fix AutocompleteTests

* Fix various tests

* Replace LayoutAndDraw with run iteration

* Fix draw issues

* fix draw order

* Fix run iteration calls

* Fix unit tests

* Fix SendKeys in facade.

* Manipulate upper and lower cases.

* Add IsValidInput method to the interface.

* Fix SendKeys scenario

* Fixes surrogate pairs in the label

* Make tests more sensible - they are testing draw functionality.  Callbacks do not need to happen in Iteration method

* Fix tests and harden cleanup in AutoInitShutdownAttribute v2 lifecycle dispose

* Delete extra create input call

* Fix mocks and order of exceptions thrown in Run when things are not initialized

* Revert use of `MapConsoleKeyInfoToKeyCode`

* Ignore casing as it is not what test is really about

* Clear application top and top levels before each auto init shutdown test

* Fix for unstable tests

* Restore actually working SendKeys code

* option to pass logger in fluent ctor

* restore ToArray

* Fix SendKeys method and add extension to unit test

* Leverage the EscSeqUtils.MapConsoleKeyInfo method to avoid duplicate code

* Remove unnecessary hack

* Using only KeyCode for rKeys

* Recover modifier keys in surrogate pairs

* Reformat

* Remove iteration limit for benchmarking in v2

* remove iteration delay to identify bugs

* Remove nudge to unique key and make Then run on UI thread

* fix fluid assertions

* Ensure UI operations all happen on UI thread

* Add explicit error for WaitIteration during an invoke

* Remove timeout added for debug

* Catch failing asserts better

* Fix screenshot

* Fix null ref

* Fix race condition in processing input

* Test fixing

* Standardize asserts

* Remove calls to layout and draw, remove pointless lock and enable reading Cancelled from Dialog even if it is disposed

* fix bad merge

* Make logs access threadsafe

* add extra wait to remove race between iteration end and assert

* Code cleanup

* Remove test for crash on access Cancelled after dispose as this is no longer a restriction

* Change resize console to run on UI thread - fixing race condition with redrawing

* Restore original frame rate after test

* Restore nudge to unique key

* Code Cleanup

* Fix for cascading failures when an assert fails in a specific test

* fix for bad merge

* Address PR feedback

* Move classes to seperate files and add xmldoc

* xml doc warnings

* More xml comments docs

* Fix spelling

---------

Co-authored-by: BDisp <bd.bdisp@gmail.com>
2025-09-10 10:01:57 -06:00

334 lines
17 KiB
C#

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
{
/// <inheritdoc/>
public void Initialize (IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<ClassDeclarationSyntax> provider = context.SyntaxProvider.CreateSyntaxProvider (
static (node, _) => IsClass (node, "XunitContextExtensions"),
static (ctx, _) =>
(ClassDeclarationSyntax)ctx.Node)
.Where (m => m is { });
IncrementalValueProvider<(Compilation Left, ImmutableArray<ClassDeclarationSyntax> 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<ClassDeclarationSyntax> 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<string> signaturesDone = new ();
List<IMethodSymbol> methods = assertType
.GetMembers (methodName)
.OfType<IMethodSymbol> ()
.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 ex)
{
context.HardStop (ex);
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<ParameterSyntax> 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 <T> here
dec = dec.WithTypeParameterList (SyntaxFactory.TypeParameterList (SyntaxFactory.SeparatedList (typeParameters)));
// Handle type parameter constraints
List<TypeParameterConstraintClauseSyntax> constraintClauses = methodSymbol.TypeParameters
.Where (tp => tp.ConstraintTypes.Length > 0)
.Select (
tp =>
SyntaxFactory.TypeParameterConstraintClause (tp.Name)
.WithConstraints (
SyntaxFactory
.SeparatedList<TypeParameterConstraintSyntax> (
tp.ConstraintTypes.Select (
constraintType =>
SyntaxFactory.TypeConstraint (
SyntaxFactory.ParseTypeName (
constraintType
.ToDisplayString ()))
)
)
)
)
.ToList ();
if (constraintClauses.Any ())
{
dec = dec.WithConstraintClauses (SyntaxFactory.List (constraintClauses));
}
// Add the <T> 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;
}
/// <summary>
/// Creates a <see cref="ParameterSyntax"/> from a discovered parameter on real xunit method parameter
/// <paramref name="p"/>
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
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<SyntaxToken> ();
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"); }
}