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>
This commit is contained in:
Thomas Nind
2025-09-10 17:01:57 +01:00
committed by GitHub
parent 00aaefb962
commit 51dda7e69f
85 changed files with 1783 additions and 1130 deletions

View File

@@ -0,0 +1,158 @@
using System.Collections.Concurrent;
using System.Drawing;
using TerminalGuiFluentTesting;
namespace Terminal.Gui.Drivers;
public class FakeApplicationFactory
{
/// <summary>
/// Creates an initialized fake application which will be cleaned up when result object
/// is disposed.
/// </summary>
/// <returns></returns>
public IDisposable SetupFakeApplication ()
{
var cts = new CancellationTokenSource ();
var fakeInput = new FakeNetInput (cts.Token);
FakeOutput _output = new ();
_output.Size = new (25, 25);
IApplication origApp = ApplicationImpl.Instance;
var sizeMonitor = new FakeSizeMonitor ();
var v2 = new ApplicationV2 (new FakeNetComponentFactory (fakeInput, _output, sizeMonitor));
ApplicationImpl.ChangeInstance (v2);
v2.Init (null,"v2net");
var d = (ConsoleDriverFacade<ConsoleKeyInfo>)Application.Driver;
sizeMonitor.SizeChanging += (_, e) =>
{
if (e.Size != null)
{
var s = e.Size.Value;
_output.Size = s;
d.OutputBuffer.SetWindowSize (s.Width, s.Height);
}
};
return new FakeApplicationLifecycle (origApp,cts);
}
}
class FakeApplicationLifecycle : IDisposable
{
private readonly IApplication _origApp;
private readonly CancellationTokenSource _hardStop;
public FakeApplicationLifecycle (IApplication origApp, CancellationTokenSource hardStop)
{
_origApp = origApp;
_hardStop = hardStop;
}
/// <inheritdoc />
public void Dispose ()
{
_hardStop.Cancel();
Application.Top?.Dispose ();
Application.Shutdown ();
ApplicationImpl.ChangeInstance (_origApp);
}
}
public class FakeDriverFactory
{
/// <summary>
/// Creates a new instance of <see cref="FakeDriverV2"/> using default options
/// </summary>
/// <returns></returns>
public IFakeDriverV2 Create ()
{
return new FakeDriverV2 (
new ConcurrentQueue<ConsoleKeyInfo> (),
new OutputBuffer (),
new FakeOutput (),
() => DateTime.Now,
new FakeSizeMonitor ());
}
}
public interface IFakeDriverV2 : IConsoleDriver, IConsoleDriverFacade
{
void SetBufferSize (int width, int height);
}
/// <summary>
/// Implementation of <see cref="IConsoleDriver"/> that uses fake input/output.
/// This is a lightweight alternative to <see cref="GuiTestContext"/> (if you don't
/// need the entire application main loop running).
/// </summary>
class FakeDriverV2 : ConsoleDriverFacade<ConsoleKeyInfo>, IFakeDriverV2
{
public ConcurrentQueue<ConsoleKeyInfo> InputBuffer { get; }
public FakeSizeMonitor SizeMonitor { get; }
public OutputBuffer OutputBuffer { get; }
public IConsoleOutput ConsoleOutput { get; }
private FakeOutput _fakeOutput;
internal FakeDriverV2 (
ConcurrentQueue<ConsoleKeyInfo> inputBuffer,
OutputBuffer outputBuffer,
FakeOutput fakeOutput,
Func<DateTime> datetimeFunc,
FakeSizeMonitor sizeMonitor) :
base (new NetInputProcessor (inputBuffer),
outputBuffer,
fakeOutput,
new (new AnsiResponseParser (), datetimeFunc),
sizeMonitor)
{
InputBuffer = inputBuffer;
SizeMonitor = sizeMonitor;
OutputBuffer = outputBuffer;
ConsoleOutput = _fakeOutput = fakeOutput;
SizeChanged += (_, e) =>
{
if (e.Size != null)
{
var s = e.Size.Value;
_fakeOutput.Size = s;
OutputBuffer.SetWindowSize (s.Width,s.Height);
}
};
}
public void SetBufferSize (int width, int height)
{
SizeMonitor.RaiseSizeChanging (new Size (width,height));
OutputBuffer.SetWindowSize (width,height);
}
}
public class FakeSizeMonitor : IWindowSizeMonitor
{
/// <inheritdoc />
public event EventHandler<SizeChangedEventArgs>? SizeChanging;
/// <inheritdoc />
public bool Poll ()
{
return false;
}
/// <summary>
/// Raises the <see cref="SizeChanging"/> event.
/// </summary>
/// <param name="newSize"></param>
public void RaiseSizeChanging (Size newSize)
{
SizeChanging?.Invoke (this,new (newSize));
}
}

View File

@@ -1,4 +1,5 @@
using System.Drawing;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using Microsoft.Extensions.Logging;
@@ -13,106 +14,105 @@ public class GuiTestContext : IDisposable
private readonly CancellationTokenSource _cts = new ();
private readonly CancellationTokenSource _hardStop = new (With.Timeout);
private readonly Task _runTask;
private Exception _ex;
private Exception? _ex;
private readonly FakeOutput _output = new ();
private readonly FakeWindowsInput _winInput;
private readonly FakeNetInput _netInput;
private View? _lastView;
private readonly object _logsLock = new ();
private readonly StringBuilder _logsSb;
private readonly V2TestDriver _driver;
private bool _finished;
private readonly object _threadLock = new ();
private readonly FakeSizeMonitor _fakeSizeMonitor;
internal GuiTestContext (Func<Toplevel> topLevelBuilder, int width, int height, V2TestDriver driver)
internal GuiTestContext (Func<Toplevel> topLevelBuilder, int width, int height, V2TestDriver driver, TextWriter? logWriter = null)
{
lock (_threadLock)
{
IApplication origApp = ApplicationImpl.Instance;
ILogger? origLogger = Logging.Logger;
_logsSb = new ();
_driver = driver;
// Remove frame limit
Application.MaximumIterationsPerSecond = ushort.MaxValue;
_netInput = new (_cts.Token);
_winInput = new (_cts.Token);
IApplication origApp = ApplicationImpl.Instance;
ILogger? origLogger = Logging.Logger;
_logsSb = new ();
_driver = driver;
_output.Size = new (width, height);
_netInput = new (_cts.Token);
_winInput = new (_cts.Token);
var v2 = new ApplicationV2 (
() => _netInput,
() => _output,
() => _winInput,
() => _output);
_output.Size = new (width, height);
_fakeSizeMonitor = new ();
var booting = new SemaphoreSlim (0, 1);
IComponentFactory cf = driver == V2TestDriver.V2Net
? new FakeNetComponentFactory (_netInput, _output, _fakeSizeMonitor)
: (IComponentFactory)new FakeWindowsComponentFactory (_winInput, _output, _fakeSizeMonitor);
// Start the application in a background thread
_runTask = Task.Run (() =>
var v2 = new ApplicationV2 (cf);
var booting = new SemaphoreSlim (0, 1);
// Start the application in a background thread
_runTask = Task.Run (
() =>
{
try
{
while (Application.Top is { })
ApplicationImpl.ChangeInstance (v2);
ILogger logger = LoggerFactory.Create (
builder =>
builder.SetMinimumLevel (LogLevel.Trace)
.AddProvider (
new TextWriterLoggerProvider (
new ThreadSafeStringWriter (_logsSb, _logsLock))))
.CreateLogger ("Test Logging");
Logging.Logger = logger;
v2.Init (null, GetDriverName ());
booting.Release ();
Toplevel t = topLevelBuilder ();
t.Closed += (s, e) => { _finished = true; };
Application.Run (t); // This will block, but it's on a background thread now
t.Dispose ();
Application.Shutdown ();
_cts.Cancel ();
}
catch (OperationCanceledException)
{ }
catch (Exception ex)
{
_ex = ex;
if (logWriter != null)
{
Task.Delay (300).Wait ();
WriteOutLogs (logWriter);
}
})
.ContinueWith (
(task, _) =>
{
try
{
if (task.IsFaulted)
{
_ex = task.Exception ?? new Exception ("Unknown error in background task");
}
// Ensure we are not running on the main thread
if (ApplicationImpl.Instance != origApp)
{
throw new InvalidOperationException (
"Application instance is not the original one, this should not happen.");
}
_hardStop.Cancel ();
}
finally
{
ApplicationImpl.ChangeInstance (origApp);
Logging.Logger = origLogger;
_finished = true;
ApplicationImpl.ChangeInstance (v2);
Application.MaximumIterationsPerSecond = Application.DefaultMaximumIterationsPerSecond;
}
},
_cts.Token);
ILogger logger = LoggerFactory.Create (builder =>
builder.SetMinimumLevel (LogLevel.Trace)
.AddProvider (
new TextWriterLoggerProvider (
new StringWriter (_logsSb))))
.CreateLogger ("Test Logging");
Logging.Logger = logger;
// Wait for booting to complete with a timeout to avoid hangs
if (!booting.WaitAsync (TimeSpan.FromSeconds (10)).Result)
{
throw new TimeoutException ("Application failed to start within the allotted time.");
}
v2.Init (null, GetDriverName ());
ResizeConsole (width, height);
booting.Release ();
Toplevel t = topLevelBuilder ();
t.Closed += (s, e) => { _finished = true; };
Application.Run (t); // This will block, but it's on a background thread now
t.Dispose ();
Application.Shutdown ();
}
catch (OperationCanceledException)
{ }
catch (Exception ex)
{
_ex = ex;
}
finally
{
ApplicationImpl.ChangeInstance (origApp);
Logging.Logger = origLogger;
_finished = true;
}
},
_cts.Token);
// Wait for booting to complete with a timeout to avoid hangs
if (!booting.WaitAsync (TimeSpan.FromSeconds (10)).Result)
{
throw new TimeoutException ("Application failed to start within the allotted time.");
}
WaitIteration ();
if (_ex != null)
{
throw new ("Application crashed", _ex);
}
}
@@ -137,7 +137,7 @@ public class GuiTestContext : IDisposable
return this;
}
Application.Invoke (() => { Application.RequestStop (); });
WaitIteration (() => { Application.RequestStop (); });
// Wait for the application to stop, but give it a 1-second timeout
if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000)))
@@ -147,7 +147,19 @@ public class GuiTestContext : IDisposable
// Timeout occurred, force the task to stop
_hardStop.Cancel ();
throw new TimeoutException ("Application failed to stop within the allotted time.");
// App is having trouble shutting down, try sending some more shutdown stuff from this thread.
// If this doesn't work there will be test cascade failures as the main loop continues to run during next test.
try
{
Application.RequestStop ();
Application.Shutdown ();
}
catch (Exception)
{
throw new TimeoutException ("Application failed to stop within the allotted time.", _ex);
}
throw new TimeoutException ("Application failed to stop within the allotted time.", _ex);
}
_cts.Cancel ();
@@ -163,8 +175,13 @@ public class GuiTestContext : IDisposable
/// <summary>
/// Hard stops the application and waits for the background thread to exit.
/// </summary>
public void HardStop ()
public void HardStop (Exception? ex = null)
{
if (ex != null)
{
_ex = ex;
}
_hardStop.Cancel ();
Stop ();
}
@@ -179,7 +196,8 @@ public class GuiTestContext : IDisposable
if (_hardStop.IsCancellationRequested)
{
throw new (
"Application was hard stopped, typically this means it timed out or did not shutdown gracefully. Ensure you call Stop in your test");
"Application was hard stopped, typically this means it timed out or did not shutdown gracefully. Ensure you call Stop in your test",
_ex);
}
_hardStop.Cancel ();
@@ -213,19 +231,27 @@ public class GuiTestContext : IDisposable
/// <returns></returns>
public GuiTestContext ResizeConsole (int width, int height)
{
_output.Size = new (width, height);
return WaitIteration (
() =>
{
_output.Size = new (width, height);
_fakeSizeMonitor.RaiseSizeChanging (_output.Size);
return WaitIteration ();
var d = (IConsoleDriverFacade)Application.Driver!;
d.OutputBuffer.SetWindowSize (width, height);
});
}
public GuiTestContext ScreenShot (string title, TextWriter writer)
{
writer.WriteLine (title + ":");
var text = Application.ToString ();
return WaitIteration (
() =>
{
writer.WriteLine (title + ":");
var text = Application.ToString ();
writer.WriteLine (text);
return this; //WaitIteration();
writer.WriteLine (text);
});
}
/// <summary>
@@ -235,7 +261,10 @@ public class GuiTestContext : IDisposable
/// <returns></returns>
public GuiTestContext WriteOutLogs (TextWriter writer)
{
writer.WriteLine (_logsSb.ToString ());
lock (_logsLock)
{
writer.WriteLine (_logsSb.ToString ());
}
return this; //WaitIteration();
}
@@ -254,14 +283,27 @@ public class GuiTestContext : IDisposable
return this;
}
if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId)
{
throw new NotSupportedException ("Cannot WaitIteration during Invoke");
}
a ??= () => { };
var ctsLocal = new CancellationTokenSource ();
Application.Invoke (
() =>
{
a ();
ctsLocal.Cancel ();
try
{
a ();
ctsLocal.Cancel ();
}
catch (Exception e)
{
_ex = e;
_hardStop.Cancel ();
}
});
// Blocks until either the token or the hardStopToken is cancelled.
@@ -286,10 +328,11 @@ public class GuiTestContext : IDisposable
{
try
{
doAction ();
WaitIteration (doAction);
}
catch (Exception)
catch (Exception ex)
{
_ex = ex;
HardStop ();
throw;
@@ -322,10 +365,19 @@ public class GuiTestContext : IDisposable
private GuiTestContext Click<T> (WindowsConsole.ButtonState btn, Func<T, bool> evaluator) where T : View
{
T v = Find (evaluator);
Point screen = v.ViewportToScreen (new Point (0, 0));
T v;
var screen = Point.Empty;
return Click (btn, screen.X, screen.Y);
GuiTestContext ctx = WaitIteration (
() =>
{
v = Find (evaluator);
screen = v.ViewportToScreen (new Point (0, 0));
});
Click (btn, screen.X, screen.Y);
return ctx;
}
private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
@@ -356,6 +408,8 @@ public class GuiTestContext : IDisposable
}
});
return WaitUntil (() => _winInput.InputBuffer.IsEmpty);
break;
case V2TestDriver.V2Net:
@@ -370,17 +424,31 @@ public class GuiTestContext : IDisposable
foreach (ConsoleKeyInfo k in NetSequences.Click (netButton, screenX, screenY))
{
SendNetKey (k);
SendNetKey (k, false);
}
break;
return WaitIteration ();
default:
throw new ArgumentOutOfRangeException ();
}
}
return WaitIteration ();
private GuiTestContext WaitUntil (Func<bool> condition)
{
GuiTestContext? c = null;
var sw = Stopwatch.StartNew ();
;
while (!condition ())
{
if (sw.Elapsed > With.Timeout)
{
throw new TimeoutException ("Failed to reach condition within the time limit");
}
c = WaitIteration ();
}
return c ?? this;
}
public GuiTestContext Down ()
@@ -648,7 +716,15 @@ public class GuiTestContext : IDisposable
WaitIteration ();
}
private void SendNetKey (ConsoleKeyInfo consoleKeyInfo) { _netInput.InputBuffer.Enqueue (consoleKeyInfo); }
private void SendNetKey (ConsoleKeyInfo consoleKeyInfo, bool wait = true)
{
_netInput.InputBuffer.Enqueue (consoleKeyInfo);
if (wait)
{
WaitUntil (() => _netInput.InputBuffer.IsEmpty);
}
}
/// <summary>
/// Sends a special key e.g. cursor key that does not map to a specific character
@@ -697,7 +773,7 @@ public class GuiTestContext : IDisposable
/// <returns></returns>
public GuiTestContext RaiseKeyDownEvent (Key key)
{
Application.RaiseKeyDownEvent (key);
WaitIteration (() => Application.RaiseKeyDownEvent (key));
return this; //WaitIteration();
}
@@ -728,9 +804,11 @@ public class GuiTestContext : IDisposable
/// is found (of Type T) or all views are looped through (back to the beginning)
/// in which case triggers hard stop and Exception
/// </summary>
/// <param name="evaluator">Delegate that returns true if the passed View is the one
/// you are trying to focus. Leave <see langword="null"/> to focus the first view of type
/// <typeparamref name="T"/></param>
/// <param name="evaluator">
/// Delegate that returns true if the passed View is the one
/// you are trying to focus. Leave <see langword="null"/> to focus the first view of type
/// <typeparamref name="T"/>
/// </param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public GuiTestContext Focus<T> (Func<T, bool>? evaluator = null) where T : View
@@ -760,11 +838,14 @@ public class GuiTestContext : IDisposable
// No, try tab to the next (or first)
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");
Fail (
"Failed to tab to a view which matched the Type and evaluator constraints of the test because MostFocused became or was always null"
+ DescribeSeenViews (seen));
return this;
}
@@ -773,7 +854,9 @@ public class GuiTestContext : IDisposable
// 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");
Fail (
"Failed to tab to a view which matched the Type and evaluator constraints of the test before looping back to the original View"
+ DescribeSeenViews (seen));
return this;
}
@@ -781,6 +864,8 @@ public class GuiTestContext : IDisposable
while (true);
}
private string DescribeSeenViews (HashSet<View> seen) { return Environment.NewLine + string.Join (Environment.NewLine, seen); }
private T Find<T> (Func<T, bool> evaluator) where T : View
{
Toplevel? t = Application.Top;
@@ -830,25 +915,70 @@ public class GuiTestContext : IDisposable
public GuiTestContext Send (Key key)
{
if (Application.Driver is IConsoleDriverFacade facade)
{
facade.InputProcessor.OnKeyDown (key);
facade.InputProcessor.OnKeyUp (key);
}
else
{
Fail ("Expected Application.Driver to be IConsoleDriverFacade");
}
return this;
return WaitIteration (
() =>
{
if (Application.Driver is IConsoleDriverFacade facade)
{
facade.InputProcessor.OnKeyDown (key);
facade.InputProcessor.OnKeyUp (key);
}
else
{
Fail ("Expected Application.Driver to be IConsoleDriverFacade");
}
});
}
/// <summary>
/// Returns the last set position of the cursor.
/// Returns the last set position of the cursor.
/// </summary>
/// <returns></returns>
public Point GetCursorPosition ()
{
return _output.CursorPosition;
}
public Point GetCursorPosition () { return _output.CursorPosition; }
}
internal class FakeWindowsComponentFactory : WindowsComponentFactory
{
private readonly FakeWindowsInput _winInput;
private readonly FakeOutput _output;
private readonly FakeSizeMonitor _fakeSizeMonitor;
public FakeWindowsComponentFactory (FakeWindowsInput winInput, FakeOutput output, FakeSizeMonitor fakeSizeMonitor)
{
_winInput = winInput;
_output = output;
_fakeSizeMonitor = fakeSizeMonitor;
}
/// <inheritdoc/>
public override IConsoleInput<WindowsConsole.InputRecord> CreateInput () { return _winInput; }
/// <inheritdoc/>
public override IConsoleOutput CreateOutput () { return _output; }
/// <inheritdoc/>
public override IWindowSizeMonitor CreateWindowSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer) { return _fakeSizeMonitor; }
}
internal class FakeNetComponentFactory : NetComponentFactory
{
private readonly FakeNetInput _netInput;
private readonly FakeOutput _output;
private readonly FakeSizeMonitor _fakeSizeMonitor;
public FakeNetComponentFactory (FakeNetInput netInput, FakeOutput output, FakeSizeMonitor fakeSizeMonitor)
{
_netInput = netInput;
_output = output;
_fakeSizeMonitor = fakeSizeMonitor;
}
/// <inheritdoc/>
public override IConsoleInput<ConsoleKeyInfo> CreateInput () { return _netInput; }
/// <inheritdoc/>
public override IConsoleOutput CreateOutput () { return _output; }
/// <inheritdoc/>
public override IWindowSizeMonitor CreateWindowSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer) { return _fakeSizeMonitor; }
}

View File

@@ -0,0 +1,31 @@
using System.Text;
namespace TerminalGuiFluentTesting;
class ThreadSafeStringWriter : StringWriter
{
private readonly object _lock;
public ThreadSafeStringWriter (StringBuilder sb, object syncLock) : base (sb)
{
_lock = syncLock;
}
public override void Write (char value)
{
lock (_lock)
{
base.Write (value);
}
}
public override void Write (string? value)
{
lock (_lock)
{
base.Write (value);
}
}
// (override other Write* methods as needed)
}

View File

@@ -12,24 +12,24 @@ public static class With
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="v2TestDriver">Which v2 v2TestDriver to use for the test</param>
/// <param name="logWriter"></param>
/// <returns></returns>
public static GuiTestContext A<T> (int width, int height, V2TestDriver v2TestDriver) where T : Toplevel, new ()
public static GuiTestContext A<T> (int width, int height, V2TestDriver v2TestDriver, TextWriter? logWriter = null) where T : Toplevel, new ()
{
return new (() => new T (), width, height,v2TestDriver);
return new (() => new T (), width, height,v2TestDriver,logWriter);
}
/// <summary>
/// Overload that takes an existing instance <paramref name="toplevel"/>
/// instead of creating one.
/// Overload that takes a function to create instance <paramref name="toplevelFactory"/> after application is initialized.
/// </summary>
/// <param name="toplevel"></param>
/// <param name="toplevelFactory"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="v2TestDriver"></param>
/// <returns></returns>
public static GuiTestContext A (Toplevel toplevel, int width, int height, V2TestDriver v2TestDriver)
public static GuiTestContext A (Func<Toplevel> toplevelFactory, int width, int height, V2TestDriver v2TestDriver)
{
return new (()=>toplevel, width, height, v2TestDriver);
return new (toplevelFactory, width, height, v2TestDriver);
}
/// <summary>
/// The global timeout to allow for any given application to run for before shutting down.