mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-27 08:17:53 +01:00
* Initial plan * Remove legacy drivers and reorganize v2 architecture Co-authored-by: tig <585482+tig@users.noreply.github.com> * Extract Windows key helper utilities and fix build Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix all test references to legacy drivers Co-authored-by: tig <585482+tig@users.noreply.github.com> * Update documentation to reflect new driver architecture Co-authored-by: tig <585482+tig@users.noreply.github.com> * Remove V2.cd diagram file Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix test failures: support legacy drivers and update exception handling Co-authored-by: tig <585482+tig@users.noreply.github.com> * updated driver names * Move V2 tests from ConsoleDrivers/V2 to proper locations Co-authored-by: tig <585482+tig@users.noreply.github.com> * Rename ApplicationV2 to ModernApplicationImpl to remove v2 terminology Co-authored-by: tig <585482+tig@users.noreply.github.com> * Remove V2 terminology from test drivers and FakeDriver classes Co-authored-by: tig <585482+tig@users.noreply.github.com> * Merge ModernApplicationImpl into ApplicationImpl and move to App folder Co-authored-by: tig <585482+tig@users.noreply.github.com> * Create modern FakeDriver with component factory architecture in Terminal.Gui project Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor: Move non-platform-dependent code from /Drivers to /App Co-authored-by: tig <585482+tig@users.noreply.github.com> * Code cleanup and org * Unit test reorg * Refactor MainLoop architecture: rename classes and enhance documentation for clarity Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add comprehensive FakeDriver tests (WIP - some tests need fixes) Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fixed FakeDriver build failures * Fix all FakeDriver test failures - Application.Top creation and clipboard behaviors Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fixed FakeDriver build failures2 * Remove hanging legacy FakeDriver tests that use Console.MockKeyPresses Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fixed some tests * Fixed more tests * Fixed more tests * Fix bad copilot (#4277) * Update Terminal.Gui/Drivers/FakeDriver/FakeConsoleOutput.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor Application Init and Update Tests Refactored `Application.Init` to improve initialization logic: - Added fallback to `ForceDriver` when `driverName` is null. - Changed repeated `Init` calls to throw `InvalidOperationException`. - Updated `_driverName` assignment logic for robustness. Enhanced `IConsoleDriver` with detailed remarks on implementations. Revised test cases to align with updated `Application.Init` behavior: - Replaced `FakeDriver` with `null` and `driverName: "fake"`. - Skipped or commented out tests incompatible with new logic. - Improved formatting and removed redundant setup code. Improved code style and consistency across the codebase: - Standardized parameter formatting and spacing. - Removed outdated comments and unused code. General cleanup to enhance readability and maintainability. * Warp fix copilot (#4278) * More fixes (#4279) * Fixes/works around test failures and temporarily disable failing test Updated `FakeDriver` to set `RunningUnitTests` to `true` and initialize dimensions using `FakeConsole`. Modified `TestRespondersDisposedAttribute` to set `ConsoleDriver.RunningUnitTests` in the `Before` method, ensuring proper behavior during unit tests. Temporarily disabled the `Button_CanFocus_False_Raises_Accepted_Correctly` test in `ViewCommandTests` by adding a `Skip` parameter to the `[Fact]` attribute, referencing issue #4270. * Allow all tests to run despite failures in UnitTests Modified the `dotnet test` command in the `Run UnitTestsParallelizable` step to set `xunit.stopOnFail` to `false`. This ensures that the test runner does not stop execution on the first failure, allowing all tests to execute regardless of individual test outcomes. * Refactor ApplicationScreenTests for cleaner setup/teardown Refactored `ClearContents_Called_When_Top_Frame_Changes` test: - Added `[AutoInitShutdown]` attribute for automatic lifecycle management. - Replaced manual `Application.Init` and `Application.Top` setup with `Application.Begin` and `RunState`. - Simplified event handling by defining `ClearedContents` handler inline. - Removed explicit cleanup logic, relying on `Application.End` for teardown. Updated `using` directives to include `UnitTests` namespace. * Attempt to fix intermittent local test failures. Update ApplicationImpl initialization parameter Changed the second parameter of the `impl.Init` method in the `FakeApplicationFactory` class from `"dotnet"` to `"fake"`. * Code cleanup to cause Action to re-run. * Stop tests on first failure in UnitTestsParallelizable Updated the `dotnet test` command in `unit-tests.yml` to set the `xunit.stopOnFail` parameter to `true`. This change ensures that test execution halts immediately upon encountering a failure, allowing quicker identification and resolution of issues. Note that this may prevent the full test suite from running in the event of a failure. * Allow all tests to run despite failures in CI Updated `unit-tests.yml` to set `xunit.stopOnFail` to `false` in both `Run UnitTests` and `Run UnitTestsParallelizable` steps. This ensures that the test runner does not stop execution on the first test failure, allowing all tests to complete even if some fail. * Enhance RuneExtensions docs and update user dictionary Updated the `<remarks>` section in `RuneExtensions.GetColumns` to include details about the `wcwidth` implementation and improved readability with `<para>` tags. Added `wcwidth` to the user dictionary in `Terminal.sln.DotSettings` to avoid spelling errors. * Improve XML doc formatting in RuneExtensions.cs Updated the remarks section of the `GetColumns` method in the `RuneExtensions` class to enhance readability by reformatting and properly indenting `<para>` tags. The content remains unchanged, describing the method's implementation via `wcwidth` and its role as a Terminal.Gui extension for `System.Text.Rune`. * Refactor drivers and improve clipboard handling Replaced legacy drivers (`CursesDriver`, `NetDriver`) with `UnixDriver` and `DotNetDriver` across the codebase, including comments, method names, and test cases. Updated documentation and remarks to reflect the new driver names and platforms. Revamped clipboard handling with new platform-specific implementations: `UnixClipboard` for Unix, `MacOSXClipboard` for macOS, and `WSLClipboard` for Linux under WSL. Removed the old `CursesClipboard` and consolidated clipboard logic. Updated test cases to align with the new drivers and clipboard implementations. Improved naming consistency and cleaned up redundant code. Updated the README and documentation to reflect these changes. * Remove `PlatformColor` from `Attribute` struct This commit removes the `PlatformColor` property from the `Attribute` struct, simplifying the codebase by eliminating platform-specific color handling. The following changes were made: - Removed `PlatformColor` from the `Attribute` struct, including its initialization, usage, and related comments. - Updated constructors to no longer initialize or use `PlatformColor`. - Modified `Equals` and `GetHashCode` methods to exclude `PlatformColor`. - Updated `UnixComponentFactory` documentation to remove references to "v2unix." - Renamed `v2TestDriver` to `testDriver` in the `With` class for clarity. - Removed `PlatformColor` references in `DriverAssert` and related error messages. - Deleted test cases in `AttributeTests` that relied on `PlatformColor`. - Cleaned up comments and TODOs related to `PlatformColor` and `UnixDriver`. These changes reflect a shift away from platform-dependent color management, improving code clarity and reducing complexity. Remove `PlatformColor` and simplify `Attribute` logic The `PlatformColor` property has been removed from the `Attribute` struct, along with its associated logic, simplifying the codebase and eliminating platform-specific dependencies. Constructors, equality checks, and hash code generation in `Attribute` have been updated accordingly. The `CurrentAttribute` property in `ConsoleDriver` and `OutputBuffer` has been simplified, removing dependencies on `Application.Driver`. The `MakeColor` method logic has been removed or simplified in related classes. Tests in `AttributeTests` have been refactored to reflect these changes, focusing on `Foreground`, `Background`, and `Style`. Unix-specific logic tied to `PlatformColor` has been eliminated. Additional updates include renaming parameters in the `With` class for clarity, simplifying `DriverAssert` output, and performing minor code cleanups to improve readability and maintainability. * Refactor Terminal.Gui driver architecture for v2 Updated documentation to reflect the new modular driver architecture in Terminal.Gui v2. - Revised `namespace-drivers.md` to include new components (`IConsoleInput`, `IConsoleOutput`, `IInputProcessor`, `IOutputBuffer`, `IWindowSizeMonitor`) and terminal size monitoring. - Replaced "Key Components" with "Architecture Overview" and added details on the **Component Factory** pattern. - Documented the four driver implementations (`DotNetDriver`, `WindowsDriver`, `UnixDriver`, `FakeDriver`) and their platform-specific optimizations. - Added a "Threading Model" section to explain the multi-threaded design for responsive input handling. - Updated examples to demonstrate driver capabilities and explicit driver selection. In `drivers.md`: - Expanded the "Overview" to emphasize the modular, component-based architecture. - Reorganized "Drivers" into "Available Drivers" and added details on `FakeDriver` for unit testing. - Added sections on "Initialization Flow," "Shutdown Flow," and platform-specific driver details. - Provided examples for accessing driver components and creating custom drivers. In `index.md`: - Updated "Cross Platform" feature to reflect new driver names and clarified compatibility with SSH and monochrome terminals. * Moved files around --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> Co-authored-by: Tig <tig@users.noreply.github.com> Co-authored-by: Thomas Nind <31306100+tznind@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
820 lines
29 KiB
C#
820 lines
29 KiB
C#
#nullable enable
|
|
using System.Diagnostics;
|
|
using System.Text;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace Terminal.Gui.DriverTests;
|
|
|
|
public class AnsiResponseParserTests (ITestOutputHelper output)
|
|
{
|
|
private readonly AnsiResponseParser<int> _parser1 = new ();
|
|
private readonly AnsiResponseParser _parser2 = new ();
|
|
|
|
/// <summary>
|
|
/// Used for the T value in batches that are passed to the AnsiResponseParser<int> (parser1)
|
|
/// </summary>
|
|
private int _tIndex;
|
|
|
|
[Fact]
|
|
public void TestInputProcessing ()
|
|
{
|
|
string ansiStream = "\u001b[<0;10;20M"
|
|
+ // ANSI escape for mouse move at (10, 20)
|
|
"Hello"
|
|
+ // User types "Hello"
|
|
"\u001b[0c"; // Device Attributes response (e.g., terminal identification i.e. DAR)
|
|
|
|
string? response1 = null;
|
|
string? response2 = null;
|
|
|
|
var i = 0;
|
|
|
|
// Imagine that we are expecting a DAR
|
|
_parser1.ExpectResponse ("c", s => response1 = s, null, false);
|
|
_parser2.ExpectResponse ("c", s => response2 = s, null, false);
|
|
|
|
// First char is Escape which we must consume incase what follows is the DAR
|
|
AssertConsumed (ansiStream, ref i); // Esc
|
|
|
|
for (var c = 0; c < "[<0;10;20".Length; c++)
|
|
{
|
|
AssertConsumed (ansiStream, ref i);
|
|
}
|
|
|
|
// We see the M terminator
|
|
AssertReleased (ansiStream, ref i, "\u001b[<0;10;20M");
|
|
|
|
// Regular user typing
|
|
for (var c = 0; c < "Hello".Length; c++)
|
|
{
|
|
AssertIgnored (ansiStream, "Hello" [c], ref i);
|
|
}
|
|
|
|
// Now we have entered the actual DAR we should be consuming these
|
|
for (var c = 0; c < "\u001b[0".Length; c++)
|
|
{
|
|
AssertConsumed (ansiStream, ref i);
|
|
}
|
|
|
|
// Consume the terminator 'c' and expect this to call the above event
|
|
Assert.Null (response1);
|
|
Assert.Null (response1);
|
|
AssertConsumed (ansiStream, ref i);
|
|
Assert.NotNull (response2);
|
|
Assert.Equal ("\u001b[0c", response2);
|
|
Assert.NotNull (response2);
|
|
Assert.Equal ("\u001b[0c", response2);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData ("\u001b[<0;10;20MHi\u001b[0c", "c", "\u001b[0c", "\u001b[<0;10;20MHi")]
|
|
[InlineData ("\u001b[<1;15;25MYou\u001b[1c", "c", "\u001b[1c", "\u001b[<1;15;25MYou")]
|
|
[InlineData ("\u001b[0cHi\u001b[0c", "c", "\u001b[0c", "Hi\u001b[0c")]
|
|
[InlineData ("\u001b[<0;0;0MHe\u001b[3c", "c", "\u001b[3c", "\u001b[<0;0;0MHe")]
|
|
[InlineData ("\u001b[<0;1;2Da\u001b[0c\u001b[1c", "c", "\u001b[0c", "\u001b[<0;1;2Da\u001b[1c")]
|
|
[InlineData ("\u001b[1;1M\u001b[3cAn", "c", "\u001b[3c", "\u001b[1;1MAn")]
|
|
[InlineData ("hi\u001b[2c\u001b[<5;5;5m", "c", "\u001b[2c", "hi\u001b[<5;5;5m")]
|
|
[InlineData ("\u001b[3c\u001b[4c\u001b[<0;0;0MIn", "c", "\u001b[3c", "\u001b[4c\u001b[<0;0;0MIn")]
|
|
[InlineData ("\u001b[<1;2;3M\u001b[0c\u001b[<1;2;3M\u001b[2c", "c", "\u001b[0c", "\u001b[<1;2;3M\u001b[<1;2;3M\u001b[2c")]
|
|
[InlineData ("\u001b[<0;1;1MHi\u001b[6c\u001b[2c\u001b[<1;0;0MT", "c", "\u001b[6c", "\u001b[<0;1;1MHi\u001b[2c\u001b[<1;0;0MT")]
|
|
[InlineData ("Te\u001b[<2;2;2M\u001b[7c", "c", "\u001b[7c", "Te\u001b[<2;2;2M")]
|
|
[InlineData ("\u001b[0c\u001b[<0;0;0M\u001b[3c\u001b[0c\u001b[1;0MT", "c", "\u001b[0c", "\u001b[<0;0;0M\u001b[3c\u001b[0c\u001b[1;0MT")]
|
|
[InlineData ("\u001b[0;0M\u001b[<0;0;0M\u001b[3cT\u001b[1c", "c", "\u001b[3c", "\u001b[0;0M\u001b[<0;0;0MT\u001b[1c")]
|
|
[InlineData ("\u001b[3c\u001b[<0;0;0M\u001b[0c\u001b[<1;1;1MIn\u001b[1c", "c", "\u001b[3c", "\u001b[<0;0;0M\u001b[0c\u001b[<1;1;1MIn\u001b[1c")]
|
|
[InlineData ("\u001b[<5;5;5M\u001b[7cEx\u001b[8c", "c", "\u001b[7c", "\u001b[<5;5;5MEx\u001b[8c")]
|
|
|
|
// Random characters and mixed inputs
|
|
[InlineData ("\u001b[<1;1;1MJJ\u001b[9c", "c", "\u001b[9c", "\u001b[<1;1;1MJJ")] // Mixed text
|
|
[InlineData ("Be\u001b[0cAf", "c", "\u001b[0c", "BeAf")] // Escape in the middle of the string
|
|
[InlineData ("\u001b[<0;0;0M\u001b[2cNot e", "c", "\u001b[2c", "\u001b[<0;0;0MNot e")] // Unexpected sequence followed by text
|
|
[InlineData (
|
|
"Just te\u001b[<0;0;0M\u001b[3c\u001b[2c\u001b[4c",
|
|
"c",
|
|
"\u001b[3c",
|
|
"Just te\u001b[<0;0;0M\u001b[2c\u001b[4c")] // Multiple unexpected responses
|
|
[InlineData (
|
|
"\u001b[1;2;3M\u001b[0c\u001b[2;2M\u001b[0;0;0MTe",
|
|
"c",
|
|
"\u001b[0c",
|
|
"\u001b[1;2;3M\u001b[2;2M\u001b[0;0;0MTe")] // Multiple commands with responses
|
|
[InlineData ("\u001b[<3;3;3Mabc\u001b[4cde", "c", "\u001b[4c", "\u001b[<3;3;3Mabcde")] // Escape sequences mixed with regular text
|
|
|
|
// Edge cases
|
|
[InlineData ("\u001b[0c\u001b[0c\u001b[0c", "c", "\u001b[0c", "\u001b[0c\u001b[0c")] // Multiple identical responses
|
|
[InlineData ("", "c", "", "")] // Empty input
|
|
[InlineData ("Normal", "c", "", "Normal")] // No escape sequences
|
|
[InlineData ("\u001b[<0;0;0M", "c", "", "\u001b[<0;0;0M")] // Escape sequence only
|
|
[InlineData ("\u001b[1;2;3M\u001b[0c", "c", "\u001b[0c", "\u001b[1;2;3M")] // Last response consumed
|
|
[InlineData ("Inpu\u001b[0c\u001b[1;0;0M", "c", "\u001b[0c", "Inpu\u001b[1;0;0M")] // Single input followed by escape
|
|
[InlineData ("\u001b[2c\u001b[<5;6;7MDa", "c", "\u001b[2c", "\u001b[<5;6;7MDa")] // Multiple escape sequences followed by text
|
|
[InlineData ("\u001b[0cHi\u001b[1cGo", "c", "\u001b[0c", "Hi\u001b[1cGo")] // Normal text with multiple escape sequences
|
|
[InlineData ("\u001b[<1;1;1MTe", "c", "", "\u001b[<1;1;1MTe")]
|
|
|
|
// Add more test cases here...
|
|
public void TestInputSequences (string ansiStream, string? expectedTerminator, string expectedResponse, string expectedOutput)
|
|
{
|
|
var swGenBatches = Stopwatch.StartNew ();
|
|
var tests = 0;
|
|
|
|
string [] [] permutations = GetBatchPermutations (ansiStream, 5).ToArray ();
|
|
|
|
swGenBatches.Stop ();
|
|
var swRunTest = Stopwatch.StartNew ();
|
|
|
|
foreach (string [] batchSet in permutations)
|
|
{
|
|
_tIndex = 0;
|
|
var response1 = string.Empty;
|
|
var response2 = string.Empty;
|
|
|
|
// Register the expected response with the given terminator
|
|
_parser1.ExpectResponse (expectedTerminator, s => response1 = s, null, false);
|
|
_parser2.ExpectResponse (expectedTerminator, s => response2 = s, null, false);
|
|
|
|
// Process the input
|
|
var actualOutput1 = new StringBuilder ();
|
|
var actualOutput2 = new StringBuilder ();
|
|
|
|
foreach (string batch in batchSet)
|
|
{
|
|
IEnumerable<Tuple<char, int>> output1 = _parser1.ProcessInput (StringToBatch (batch));
|
|
actualOutput1.Append (BatchToString (output1));
|
|
|
|
string output2 = _parser2.ProcessInput (batch);
|
|
actualOutput2.Append (output2);
|
|
}
|
|
|
|
// Assert the final output minus the expected response
|
|
Assert.Equal (expectedOutput, actualOutput1.ToString ());
|
|
Assert.Equal (expectedResponse, response1);
|
|
Assert.Equal (expectedOutput, actualOutput2.ToString ());
|
|
Assert.Equal (expectedResponse, response2);
|
|
tests++;
|
|
}
|
|
|
|
output.WriteLine ($"Tested {tests} in {swRunTest.ElapsedMilliseconds} ms (gen batches took {swGenBatches.ElapsedMilliseconds} ms)");
|
|
}
|
|
|
|
public static IEnumerable<object? []> TestInputSequencesExact_Cases ()
|
|
{
|
|
yield return
|
|
[
|
|
"Esc Only",
|
|
null,
|
|
new []
|
|
{
|
|
new StepExpectation ('\u001b', AnsiResponseParserState.ExpectingEscapeSequence, string.Empty)
|
|
}
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"Esc Hi with intermediate",
|
|
'c',
|
|
new []
|
|
{
|
|
new StepExpectation ('\u001b', AnsiResponseParserState.ExpectingEscapeSequence, string.Empty),
|
|
new StepExpectation (
|
|
'H',
|
|
AnsiResponseParserState.InResponse,
|
|
string.Empty), // H is known terminator and not expected one so here we release both chars
|
|
new StepExpectation ('\u001b', AnsiResponseParserState.ExpectingEscapeSequence, "\u001bH"),
|
|
new StepExpectation ('[', AnsiResponseParserState.InResponse, string.Empty),
|
|
new StepExpectation ('0', AnsiResponseParserState.InResponse, string.Empty),
|
|
new StepExpectation (
|
|
'c',
|
|
AnsiResponseParserState.Normal,
|
|
string.Empty,
|
|
"\u001b[0c"), // c is expected terminator so here we swallow input and populate expected response
|
|
new StepExpectation ('\u001b', AnsiResponseParserState.ExpectingEscapeSequence, string.Empty)
|
|
}
|
|
];
|
|
}
|
|
|
|
public class StepExpectation ()
|
|
{
|
|
/// <summary>
|
|
/// The input character to feed into the parser at this step of the test
|
|
/// </summary>
|
|
public char Input { get; }
|
|
|
|
/// <summary>
|
|
/// What should the state of the parser be after the <see cref="Input"/>
|
|
/// is fed in.
|
|
/// </summary>
|
|
public AnsiResponseParserState ExpectedStateAfterOperation { get; }
|
|
|
|
/// <summary>
|
|
/// If this step should release one or more characters, put them here.
|
|
/// </summary>
|
|
public string ExpectedRelease { get; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// If this step should result in a completing of detection of ANSI response
|
|
/// then put the expected full response sequence here.
|
|
/// </summary>
|
|
public string ExpectedAnsiResponse { get; } = string.Empty;
|
|
|
|
public StepExpectation (
|
|
char input,
|
|
AnsiResponseParserState expectedStateAfterOperation,
|
|
string expectedRelease = "",
|
|
string expectedAnsiResponse = ""
|
|
) : this ()
|
|
{
|
|
Input = input;
|
|
ExpectedStateAfterOperation = expectedStateAfterOperation;
|
|
ExpectedRelease = expectedRelease;
|
|
ExpectedAnsiResponse = expectedAnsiResponse;
|
|
}
|
|
}
|
|
|
|
[MemberData (nameof (TestInputSequencesExact_Cases))]
|
|
[Theory]
|
|
public void TestInputSequencesExact (string caseName, char? terminator, IEnumerable<StepExpectation> expectedStates)
|
|
{
|
|
output.WriteLine ("Running test case:" + caseName);
|
|
|
|
var parser = new AnsiResponseParser ();
|
|
string? response = null;
|
|
|
|
if (terminator.HasValue)
|
|
{
|
|
parser.ExpectResponse (terminator.Value.ToString (), s => response = s, null, false);
|
|
}
|
|
|
|
var step = 0;
|
|
|
|
foreach (StepExpectation state in expectedStates)
|
|
{
|
|
step++;
|
|
|
|
// If we expect the response to be detected at this step
|
|
if (!string.IsNullOrWhiteSpace (state.ExpectedAnsiResponse))
|
|
{
|
|
// Then before passing input it should be null
|
|
Assert.Null (response);
|
|
}
|
|
|
|
string actual = parser.ProcessInput (state.Input.ToString ());
|
|
|
|
Assert.Equal (state.ExpectedRelease, actual);
|
|
Assert.Equal (state.ExpectedStateAfterOperation, parser.State);
|
|
|
|
// If we expect the response to be detected at this step
|
|
if (!string.IsNullOrWhiteSpace (state.ExpectedAnsiResponse))
|
|
{
|
|
// And after passing input it shuld be the expected value
|
|
Assert.Equal (state.ExpectedAnsiResponse, response);
|
|
}
|
|
|
|
output.WriteLine ($"Step {step} passed");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ReleasesEscapeAfterTimeout ()
|
|
{
|
|
var input = "\u001b";
|
|
var i = 0;
|
|
|
|
// Esc on its own looks like it might be an esc sequence so should be consumed
|
|
AssertConsumed (input, ref i);
|
|
|
|
// We should know when the state changed
|
|
Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence, _parser1.State);
|
|
Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence, _parser2.State);
|
|
|
|
Assert.Equal (DateTime.Now.Date, _parser1.StateChangedAt.Date);
|
|
Assert.Equal (DateTime.Now.Date, _parser2.StateChangedAt.Date);
|
|
|
|
AssertManualReleaseIs (input);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoEscapesInARow ()
|
|
{
|
|
// Example user presses Esc key then a DAR comes in
|
|
var input = "\u001b\u001b";
|
|
var i = 0;
|
|
|
|
// First Esc gets grabbed
|
|
AssertConsumed (input, ref i);
|
|
|
|
// Upon getting the second Esc we should release the first
|
|
AssertReleased (input, ref i, "\u001b", 0);
|
|
|
|
// Assume 50ms or something has passed, lets force release as no new content
|
|
|
|
// It should be the second escape that gets released (i.e. index 1)
|
|
AssertManualReleaseIs ("\u001b", 1);
|
|
}
|
|
|
|
[Fact]
|
|
public void TestLateResponses ()
|
|
{
|
|
var p = new AnsiResponseParser ();
|
|
|
|
string? responseA = null;
|
|
string? responseB = null;
|
|
|
|
p.ExpectResponse ("z", r => responseA = r, null, false);
|
|
|
|
// Some time goes by without us seeing a response
|
|
p.StopExpecting ("z", false);
|
|
|
|
// Send our new request
|
|
p.ExpectResponse ("z", r => responseB = r, null, false);
|
|
|
|
// Because we gave up on getting A, we should expect the response to be to our new request
|
|
Assert.Empty (p.ProcessInput ("\u001b[<1;2z"));
|
|
Assert.Null (responseA);
|
|
Assert.Equal ("\u001b[<1;2z", responseB);
|
|
|
|
// Oh looks like we got one late after all - swallow it
|
|
Assert.Empty (p.ProcessInput ("\u001b[0000z"));
|
|
|
|
// Do not expect late responses to be populated back to your variable
|
|
Assert.Null (responseA);
|
|
Assert.Equal ("\u001b[<1;2z", responseB);
|
|
|
|
// We now have no outstanding requests (late or otherwise) so new ansi codes should just fall through
|
|
Assert.Equal ("\u001b[111z", p.ProcessInput ("\u001b[111z"));
|
|
}
|
|
|
|
[Fact]
|
|
public void TestPersistentResponses ()
|
|
{
|
|
var p = new AnsiResponseParser ();
|
|
|
|
var m = 0;
|
|
var M = 1;
|
|
|
|
p.ExpectResponse ("m", _ => m++, null, true);
|
|
p.ExpectResponse ("M", _ => M++, null, true);
|
|
|
|
// Act - Feed input strings containing ANSI sequences
|
|
p.ProcessInput ("\u001b[<0;10;10m"); // Should match and increment `m`
|
|
p.ProcessInput ("\u001b[<0;20;20m"); // Should match and increment `m`
|
|
p.ProcessInput ("\u001b[<0;30;30M"); // Should match and increment `M`
|
|
p.ProcessInput ("\u001b[<0;40;40M"); // Should match and increment `M`
|
|
p.ProcessInput ("\u001b[<0;50;50M"); // Should match and increment `M`
|
|
|
|
// Assert - Verify that counters reflect the expected counts of each terminator
|
|
Assert.Equal (2, m); // Expected two `m` responses
|
|
Assert.Equal (4, M); // Expected three `M` responses plus the initial value of 1
|
|
}
|
|
|
|
[Fact]
|
|
public void TestPersistentResponses_WithMetadata ()
|
|
{
|
|
AnsiResponseParser<int> p = new ();
|
|
|
|
// ReSharper disable once NotAccessedVariable
|
|
var m = 0;
|
|
|
|
List<Tuple<char, int>> result = new ();
|
|
|
|
p.ExpectResponseT (
|
|
"m",
|
|
r =>
|
|
{
|
|
result = r.ToList ();
|
|
m++;
|
|
},
|
|
null,
|
|
true);
|
|
|
|
// Act - Feed input strings containing ANSI sequences
|
|
p.ProcessInput (StringToBatch ("\u001b[<0;10;10m")); // Should match and increment `m`
|
|
|
|
// Prepare expected result:
|
|
List<Tuple<char, int>> expected = new()
|
|
{
|
|
Tuple.Create ('\u001b', 0), // Escape character
|
|
Tuple.Create ('[', 1),
|
|
Tuple.Create ('<', 2),
|
|
Tuple.Create ('0', 3),
|
|
Tuple.Create (';', 4),
|
|
Tuple.Create ('1', 5),
|
|
Tuple.Create ('0', 6),
|
|
Tuple.Create (';', 7),
|
|
Tuple.Create ('1', 8),
|
|
Tuple.Create ('0', 9),
|
|
Tuple.Create ('m', 10)
|
|
};
|
|
|
|
Assert.Equal (expected.Count, result.Count); // Ensure the count is as expected
|
|
Assert.True (expected.SequenceEqual (result), "The result does not match the expected output."); // Check the actual content
|
|
}
|
|
|
|
[Fact]
|
|
public void ShouldSwallowUnknownResponses_WhenDelegateSaysSo ()
|
|
{
|
|
// Swallow all unknown escape codes
|
|
_parser1.UnexpectedResponseHandler = _ => true;
|
|
_parser2.UnknownResponseHandler = _ => true;
|
|
|
|
AssertReleased (
|
|
"Just te\u001b[<0;0;0M\u001b[3c\u001b[2c\u001b[4cst",
|
|
"Just test",
|
|
0,
|
|
1,
|
|
2,
|
|
3,
|
|
4,
|
|
5,
|
|
6,
|
|
28,
|
|
29);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnknownResponses_ParameterShouldMatch ()
|
|
{
|
|
// Track unknown responses passed to the UnexpectedResponseHandler
|
|
List<string> unknownResponses = new ();
|
|
|
|
// Set up the UnexpectedResponseHandler to log each unknown response
|
|
_parser1.UnexpectedResponseHandler = r1 =>
|
|
{
|
|
unknownResponses.Add (BatchToString (r1));
|
|
|
|
return true; // Return true to swallow unknown responses
|
|
};
|
|
|
|
_parser2.UnknownResponseHandler = r2 =>
|
|
{
|
|
// parsers should be agreeing on what these responses are!
|
|
Assert.Equal (unknownResponses.Last (), r2);
|
|
|
|
return true; // Return true to swallow unknown responses
|
|
};
|
|
|
|
// Input with known and unknown responses
|
|
AssertReleased (
|
|
"Just te\u001b[<0;0;0M\u001b[3c\u001b[2c\u001b[4cst",
|
|
"Just test");
|
|
|
|
// Expected unknown responses (ANSI sequences that are unknown)
|
|
List<string> expectedUnknownResponses = new()
|
|
{
|
|
"\u001b[<0;0;0M",
|
|
"\u001b[3c",
|
|
"\u001b[2c",
|
|
"\u001b[4c"
|
|
};
|
|
|
|
// Assert that the UnexpectedResponseHandler was called with the correct unknown responses
|
|
Assert.Equal (expectedUnknownResponses.Count, unknownResponses.Count);
|
|
Assert.Equal (expectedUnknownResponses, unknownResponses);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParserDetectsMouse ()
|
|
{
|
|
// ANSI escape sequence for mouse down (using a generic format example)
|
|
const string MOUSE_DOWN = "\u001B[<0;12;32M";
|
|
|
|
// ANSI escape sequence for Device Attribute Response (e.g., Terminal identifying itself)
|
|
const string DEVICE_ATTRIBUTE_RESPONSE = "\u001B[?1;2c";
|
|
|
|
// ANSI escape sequence for mouse up (using a generic format example)
|
|
const string MOUSE_UP = "\u001B[<0;25;50m";
|
|
|
|
var parser = new AnsiResponseParser ();
|
|
|
|
parser.HandleMouse = true;
|
|
string? foundDar = null;
|
|
List<MouseEventArgs> mouseEventArgs = new ();
|
|
|
|
parser.Mouse += (s, e) => mouseEventArgs.Add (e);
|
|
parser.ExpectResponse ("c", dar => foundDar = dar, null, false);
|
|
string released = parser.ProcessInput ("a" + MOUSE_DOWN + "asdf" + DEVICE_ATTRIBUTE_RESPONSE + "bbcc" + MOUSE_UP + "sss");
|
|
|
|
Assert.Equal ("aasdfbbccsss", released);
|
|
|
|
Assert.Equal (2, mouseEventArgs.Count);
|
|
|
|
Assert.NotNull (foundDar);
|
|
Assert.Equal (DEVICE_ATTRIBUTE_RESPONSE, foundDar);
|
|
|
|
Assert.True (mouseEventArgs [0].IsPressed);
|
|
|
|
// Mouse positions in ANSI are 1 based so actual Terminal.Gui Screen positions are x-1,y-1
|
|
Assert.Equal (11, mouseEventArgs [0].Position.X);
|
|
Assert.Equal (31, mouseEventArgs [0].Position.Y);
|
|
|
|
Assert.True (mouseEventArgs [1].IsReleased);
|
|
Assert.Equal (24, mouseEventArgs [1].Position.X);
|
|
Assert.Equal (49, mouseEventArgs [1].Position.Y);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParserDetectsKeyboard ()
|
|
{
|
|
// ANSI escape sequence for cursor left
|
|
const string LEFT = "\u001b[D";
|
|
|
|
// ANSI escape sequence for Device Attribute Response (e.g., Terminal identifying itself)
|
|
const string DEVICE_ATTRIBUTE_RESPONSE = "\u001B[?1;2c";
|
|
|
|
// ANSI escape sequence for cursor up (while shift held down)
|
|
const string SHIFT_UP = "\u001b[1;2A";
|
|
|
|
var parser = new AnsiResponseParser ();
|
|
|
|
parser.HandleKeyboard = true;
|
|
string? foundDar = null;
|
|
List<Key> keys = new ();
|
|
|
|
parser.Keyboard += (s, e) => keys.Add (e);
|
|
parser.ExpectResponse ("c", dar => foundDar = dar, null, false);
|
|
string released = parser.ProcessInput ("a" + LEFT + "asdf" + DEVICE_ATTRIBUTE_RESPONSE + "bbcc" + SHIFT_UP + "sss");
|
|
|
|
Assert.Equal ("aasdfbbccsss", released);
|
|
|
|
Assert.Equal (2, keys.Count);
|
|
|
|
Assert.NotNull (foundDar);
|
|
Assert.Equal (DEVICE_ATTRIBUTE_RESPONSE, foundDar);
|
|
|
|
Assert.Equal (Key.CursorLeft, keys [0]);
|
|
Assert.Equal (Key.CursorUp.WithShift, keys [1]);
|
|
}
|
|
|
|
public static IEnumerable<object []> ParserDetects_FunctionKeys_Cases ()
|
|
{
|
|
// These are VT100 escape codes for F1-4
|
|
yield return
|
|
[
|
|
"\u001bOP",
|
|
Key.F1
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001bOQ",
|
|
Key.F2
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001bOR",
|
|
Key.F3
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001bOS",
|
|
Key.F4
|
|
];
|
|
|
|
// These are also F keys
|
|
yield return
|
|
[
|
|
"\u001b[11~",
|
|
Key.F1
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001b[12~",
|
|
Key.F2
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001b[13~",
|
|
Key.F3
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001b[14~",
|
|
Key.F4
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001b[15~",
|
|
Key.F5
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001b[17~",
|
|
Key.F6
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001b[18~",
|
|
Key.F7
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001b[19~",
|
|
Key.F8
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001b[20~",
|
|
Key.F9
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001b[21~",
|
|
Key.F10
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001b[23~",
|
|
Key.F11
|
|
];
|
|
|
|
yield return
|
|
[
|
|
"\u001b[24~",
|
|
Key.F12
|
|
];
|
|
}
|
|
|
|
[MemberData (nameof (ParserDetects_FunctionKeys_Cases))]
|
|
[Theory]
|
|
public void ParserDetects_FunctionKeys (string input, Key expectedKey)
|
|
{
|
|
var parser = new AnsiResponseParser ();
|
|
|
|
parser.HandleKeyboard = true;
|
|
List<Key> keys = new ();
|
|
|
|
parser.Keyboard += (s, e) => keys.Add (e);
|
|
|
|
foreach (char ch in input)
|
|
{
|
|
parser.ProcessInput (new (ch, 1));
|
|
}
|
|
|
|
Key k = Assert.Single (keys);
|
|
|
|
Assert.Equal (k, expectedKey);
|
|
}
|
|
|
|
private Tuple<char, int> [] StringToBatch (string batch) { return batch.Select (k => Tuple.Create (k, _tIndex++)).ToArray (); }
|
|
|
|
public static IEnumerable<string []> GetBatchPermutations (string input, int maxDepth = 3)
|
|
{
|
|
// Call the recursive method to generate batches with an initial depth of 0
|
|
return GenerateBatches (input, 0, maxDepth, 0);
|
|
}
|
|
|
|
private static IEnumerable<string []> GenerateBatches (string input, int start, int maxDepth, int currentDepth)
|
|
{
|
|
// If we have reached the maximum recursion depth, return no results
|
|
if (currentDepth >= maxDepth)
|
|
{
|
|
yield break; // No more batches can be generated at this depth
|
|
}
|
|
|
|
// If we have reached the end of the string, return an empty list
|
|
if (start >= input.Length)
|
|
{
|
|
yield return new string [0];
|
|
|
|
yield break;
|
|
}
|
|
|
|
// Iterate over the input string to create batches
|
|
for (int i = start + 1; i <= input.Length; i++)
|
|
{
|
|
// Take a batch from 'start' to 'i'
|
|
string batch = input.Substring (start, i - start);
|
|
|
|
// Recursively get batches from the remaining substring, increasing the depth
|
|
foreach (string [] remainingBatches in GenerateBatches (input, i, maxDepth, currentDepth + 1))
|
|
{
|
|
// Combine the current batch with the remaining batches
|
|
var result = new string [1 + remainingBatches.Length];
|
|
result [0] = batch;
|
|
Array.Copy (remainingBatches, 0, result, 1, remainingBatches.Length);
|
|
|
|
yield return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AssertIgnored (string ansiStream, char expected, ref int i)
|
|
{
|
|
char c2 = ansiStream [i];
|
|
Tuple<char, int> [] c1 = NextChar (ansiStream, ref i);
|
|
|
|
// Parser does not grab this key (i.e. driver can continue with regular operations)
|
|
Assert.Equal (c1, _parser1.ProcessInput (c1));
|
|
Assert.Equal (expected, c1.Single ().Item1);
|
|
|
|
Assert.Equal (c2, _parser2.ProcessInput (c2.ToString ()).Single ());
|
|
Assert.Equal (expected, c2);
|
|
}
|
|
|
|
private void AssertConsumed (string ansiStream, ref int i)
|
|
{
|
|
// Parser grabs this key
|
|
char c2 = ansiStream [i];
|
|
Tuple<char, int> [] c1 = NextChar (ansiStream, ref i);
|
|
|
|
Assert.Empty (_parser1.ProcessInput (c1));
|
|
Assert.Empty (_parser2.ProcessInput (c2.ToString ()));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overload that fully exhausts <paramref name="ansiStream"/> and asserts
|
|
/// that the final released content across whole processing is <paramref name="expectedRelease"/>
|
|
/// </summary>
|
|
/// <param name="ansiStream"></param>
|
|
/// <param name="expectedRelease"></param>
|
|
/// <param name="expectedTValues"></param>
|
|
private void AssertReleased (string ansiStream, string expectedRelease, params int [] expectedTValues)
|
|
{
|
|
var sb = new StringBuilder ();
|
|
List<int> tValues = new ();
|
|
|
|
var i = 0;
|
|
|
|
while (i < ansiStream.Length)
|
|
{
|
|
char c2 = ansiStream [i];
|
|
Tuple<char, int> [] c1 = NextChar (ansiStream, ref i);
|
|
|
|
Tuple<char, int> [] released1 = _parser1.ProcessInput (c1).ToArray ();
|
|
tValues.AddRange (released1.Select (kv => kv.Item2));
|
|
|
|
string released2 = _parser2.ProcessInput (c2.ToString ());
|
|
|
|
// Both parsers should have same chars so release chars consistently with each other
|
|
Assert.Equal (BatchToString (released1), released2);
|
|
|
|
sb.Append (released2);
|
|
}
|
|
|
|
Assert.Equal (expectedRelease, sb.ToString ());
|
|
|
|
if (expectedTValues.Length > 0)
|
|
{
|
|
Assert.True (expectedTValues.SequenceEqual (tValues));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that <paramref name="i"/> index of <see cref="ansiStream"/> when consumed will release
|
|
/// <paramref name="expectedRelease"/>. Results in implicit increment of <paramref name="i"/>.
|
|
/// <remarks>Note that this does NOT iteratively consume all the stream, only 1 char at <paramref name="i"/></remarks>
|
|
/// </summary>
|
|
/// <param name="ansiStream"></param>
|
|
/// <param name="i"></param>
|
|
/// <param name="expectedRelease"></param>
|
|
/// <param name="expectedTValues"></param>
|
|
private void AssertReleased (string ansiStream, ref int i, string expectedRelease, params int [] expectedTValues)
|
|
{
|
|
char c2 = ansiStream [i];
|
|
Tuple<char, int> [] c1 = NextChar (ansiStream, ref i);
|
|
|
|
// Parser realizes it has grabbed content that does not belong to an outstanding request
|
|
// Parser returns false to indicate to continue
|
|
Tuple<char, int> [] released1 = _parser1.ProcessInput (c1).ToArray ();
|
|
Assert.Equal (expectedRelease, BatchToString (released1));
|
|
|
|
if (expectedTValues.Length > 0)
|
|
{
|
|
Assert.True (expectedTValues.SequenceEqual (released1.Select (kv => kv.Item2)));
|
|
}
|
|
|
|
Assert.Equal (expectedRelease, _parser2.ProcessInput (c2.ToString ()));
|
|
}
|
|
|
|
private string BatchToString (IEnumerable<Tuple<char, int>> processInput) { return new (processInput.Select (a => a.Item1).ToArray ()); }
|
|
|
|
private Tuple<char, int> [] NextChar (string ansiStream, ref int i) { return StringToBatch (ansiStream [i++].ToString ()); }
|
|
|
|
private void AssertManualReleaseIs (string expectedRelease, params int [] expectedTValues)
|
|
{
|
|
// Consumer is responsible for determining this based on e.g. after 50ms
|
|
Tuple<char, int> [] released1 = _parser1.Release ().ToArray ();
|
|
Assert.Equal (expectedRelease, BatchToString (released1));
|
|
|
|
if (expectedTValues.Length > 0)
|
|
{
|
|
Assert.True (expectedTValues.SequenceEqual (released1.Select (kv => kv.Item2)));
|
|
}
|
|
|
|
Assert.Equal (expectedRelease, _parser2.Release ());
|
|
|
|
Assert.Equal (AnsiResponseParserState.Normal, _parser1.State);
|
|
Assert.Equal (AnsiResponseParserState.Normal, _parser2.State);
|
|
}
|
|
}
|