Files
Terminal.Gui/Tests/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs
Copilot 2be8397203 Remove legacy drivers, simplify architecture, and reorganize codebase structure (#4270)
* 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>
2025-10-15 13:24:47 -06:00

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&lt;int&gt; (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);
}
}