AnsiResponseParserTests.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819
  1. #nullable enable
  2. using System.Diagnostics;
  3. using System.Text;
  4. using Xunit.Abstractions;
  5. namespace UnitTests.ConsoleDrivers;
  6. public class AnsiResponseParserTests (ITestOutputHelper output)
  7. {
  8. private readonly AnsiResponseParser<int> _parser1 = new ();
  9. private readonly AnsiResponseParser _parser2 = new ();
  10. /// <summary>
  11. /// Used for the T value in batches that are passed to the AnsiResponseParser&lt;int&gt; (parser1)
  12. /// </summary>
  13. private int _tIndex;
  14. [Fact]
  15. public void TestInputProcessing ()
  16. {
  17. string ansiStream = "\u001b[<0;10;20M"
  18. + // ANSI escape for mouse move at (10, 20)
  19. "Hello"
  20. + // User types "Hello"
  21. "\u001b[0c"; // Device Attributes response (e.g., terminal identification i.e. DAR)
  22. string? response1 = null;
  23. string? response2 = null;
  24. var i = 0;
  25. // Imagine that we are expecting a DAR
  26. _parser1.ExpectResponse ("c", s => response1 = s, null, false);
  27. _parser2.ExpectResponse ("c", s => response2 = s, null, false);
  28. // First char is Escape which we must consume incase what follows is the DAR
  29. AssertConsumed (ansiStream, ref i); // Esc
  30. for (var c = 0; c < "[<0;10;20".Length; c++)
  31. {
  32. AssertConsumed (ansiStream, ref i);
  33. }
  34. // We see the M terminator
  35. AssertReleased (ansiStream, ref i, "\u001b[<0;10;20M");
  36. // Regular user typing
  37. for (var c = 0; c < "Hello".Length; c++)
  38. {
  39. AssertIgnored (ansiStream, "Hello" [c], ref i);
  40. }
  41. // Now we have entered the actual DAR we should be consuming these
  42. for (var c = 0; c < "\u001b[0".Length; c++)
  43. {
  44. AssertConsumed (ansiStream, ref i);
  45. }
  46. // Consume the terminator 'c' and expect this to call the above event
  47. Assert.Null (response1);
  48. Assert.Null (response1);
  49. AssertConsumed (ansiStream, ref i);
  50. Assert.NotNull (response2);
  51. Assert.Equal ("\u001b[0c", response2);
  52. Assert.NotNull (response2);
  53. Assert.Equal ("\u001b[0c", response2);
  54. }
  55. [Theory]
  56. [InlineData ("\u001b[<0;10;20MHi\u001b[0c", "c", "\u001b[0c", "\u001b[<0;10;20MHi")]
  57. [InlineData ("\u001b[<1;15;25MYou\u001b[1c", "c", "\u001b[1c", "\u001b[<1;15;25MYou")]
  58. [InlineData ("\u001b[0cHi\u001b[0c", "c", "\u001b[0c", "Hi\u001b[0c")]
  59. [InlineData ("\u001b[<0;0;0MHe\u001b[3c", "c", "\u001b[3c", "\u001b[<0;0;0MHe")]
  60. [InlineData ("\u001b[<0;1;2Da\u001b[0c\u001b[1c", "c", "\u001b[0c", "\u001b[<0;1;2Da\u001b[1c")]
  61. [InlineData ("\u001b[1;1M\u001b[3cAn", "c", "\u001b[3c", "\u001b[1;1MAn")]
  62. [InlineData ("hi\u001b[2c\u001b[<5;5;5m", "c", "\u001b[2c", "hi\u001b[<5;5;5m")]
  63. [InlineData ("\u001b[3c\u001b[4c\u001b[<0;0;0MIn", "c", "\u001b[3c", "\u001b[4c\u001b[<0;0;0MIn")]
  64. [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")]
  65. [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")]
  66. [InlineData ("Te\u001b[<2;2;2M\u001b[7c", "c", "\u001b[7c", "Te\u001b[<2;2;2M")]
  67. [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")]
  68. [InlineData ("\u001b[0;0M\u001b[<0;0;0M\u001b[3cT\u001b[1c", "c", "\u001b[3c", "\u001b[0;0M\u001b[<0;0;0MT\u001b[1c")]
  69. [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")]
  70. [InlineData ("\u001b[<5;5;5M\u001b[7cEx\u001b[8c", "c", "\u001b[7c", "\u001b[<5;5;5MEx\u001b[8c")]
  71. // Random characters and mixed inputs
  72. [InlineData ("\u001b[<1;1;1MJJ\u001b[9c", "c", "\u001b[9c", "\u001b[<1;1;1MJJ")] // Mixed text
  73. [InlineData ("Be\u001b[0cAf", "c", "\u001b[0c", "BeAf")] // Escape in the middle of the string
  74. [InlineData ("\u001b[<0;0;0M\u001b[2cNot e", "c", "\u001b[2c", "\u001b[<0;0;0MNot e")] // Unexpected sequence followed by text
  75. [InlineData (
  76. "Just te\u001b[<0;0;0M\u001b[3c\u001b[2c\u001b[4c",
  77. "c",
  78. "\u001b[3c",
  79. "Just te\u001b[<0;0;0M\u001b[2c\u001b[4c")] // Multiple unexpected responses
  80. [InlineData (
  81. "\u001b[1;2;3M\u001b[0c\u001b[2;2M\u001b[0;0;0MTe",
  82. "c",
  83. "\u001b[0c",
  84. "\u001b[1;2;3M\u001b[2;2M\u001b[0;0;0MTe")] // Multiple commands with responses
  85. [InlineData ("\u001b[<3;3;3Mabc\u001b[4cde", "c", "\u001b[4c", "\u001b[<3;3;3Mabcde")] // Escape sequences mixed with regular text
  86. // Edge cases
  87. [InlineData ("\u001b[0c\u001b[0c\u001b[0c", "c", "\u001b[0c", "\u001b[0c\u001b[0c")] // Multiple identical responses
  88. [InlineData ("", "c", "", "")] // Empty input
  89. [InlineData ("Normal", "c", "", "Normal")] // No escape sequences
  90. [InlineData ("\u001b[<0;0;0M", "c", "", "\u001b[<0;0;0M")] // Escape sequence only
  91. [InlineData ("\u001b[1;2;3M\u001b[0c", "c", "\u001b[0c", "\u001b[1;2;3M")] // Last response consumed
  92. [InlineData ("Inpu\u001b[0c\u001b[1;0;0M", "c", "\u001b[0c", "Inpu\u001b[1;0;0M")] // Single input followed by escape
  93. [InlineData ("\u001b[2c\u001b[<5;6;7MDa", "c", "\u001b[2c", "\u001b[<5;6;7MDa")] // Multiple escape sequences followed by text
  94. [InlineData ("\u001b[0cHi\u001b[1cGo", "c", "\u001b[0c", "Hi\u001b[1cGo")] // Normal text with multiple escape sequences
  95. [InlineData ("\u001b[<1;1;1MTe", "c", "", "\u001b[<1;1;1MTe")]
  96. // Add more test cases here...
  97. public void TestInputSequences (string ansiStream, string? expectedTerminator, string expectedResponse, string expectedOutput)
  98. {
  99. var swGenBatches = Stopwatch.StartNew ();
  100. var tests = 0;
  101. string [] [] permutations = GetBatchPermutations (ansiStream, 5).ToArray ();
  102. swGenBatches.Stop ();
  103. var swRunTest = Stopwatch.StartNew ();
  104. foreach (string [] batchSet in permutations)
  105. {
  106. _tIndex = 0;
  107. var response1 = string.Empty;
  108. var response2 = string.Empty;
  109. // Register the expected response with the given terminator
  110. _parser1.ExpectResponse (expectedTerminator, s => response1 = s, null, false);
  111. _parser2.ExpectResponse (expectedTerminator, s => response2 = s, null, false);
  112. // Process the input
  113. var actualOutput1 = new StringBuilder ();
  114. var actualOutput2 = new StringBuilder ();
  115. foreach (string batch in batchSet)
  116. {
  117. IEnumerable<Tuple<char, int>> output1 = _parser1.ProcessInput (StringToBatch (batch));
  118. actualOutput1.Append (BatchToString (output1));
  119. string output2 = _parser2.ProcessInput (batch);
  120. actualOutput2.Append (output2);
  121. }
  122. // Assert the final output minus the expected response
  123. Assert.Equal (expectedOutput, actualOutput1.ToString ());
  124. Assert.Equal (expectedResponse, response1);
  125. Assert.Equal (expectedOutput, actualOutput2.ToString ());
  126. Assert.Equal (expectedResponse, response2);
  127. tests++;
  128. }
  129. output.WriteLine ($"Tested {tests} in {swRunTest.ElapsedMilliseconds} ms (gen batches took {swGenBatches.ElapsedMilliseconds} ms)");
  130. }
  131. public static IEnumerable<object? []> TestInputSequencesExact_Cases ()
  132. {
  133. yield return
  134. [
  135. "Esc Only",
  136. null,
  137. new []
  138. {
  139. new StepExpectation ('\u001b', AnsiResponseParserState.ExpectingEscapeSequence, string.Empty)
  140. }
  141. ];
  142. yield return
  143. [
  144. "Esc Hi with intermediate",
  145. 'c',
  146. new []
  147. {
  148. new StepExpectation ('\u001b', AnsiResponseParserState.ExpectingEscapeSequence, string.Empty),
  149. new StepExpectation (
  150. 'H',
  151. AnsiResponseParserState.InResponse,
  152. string.Empty), // H is known terminator and not expected one so here we release both chars
  153. new StepExpectation ('\u001b', AnsiResponseParserState.ExpectingEscapeSequence, "\u001bH"),
  154. new StepExpectation ('[', AnsiResponseParserState.InResponse, string.Empty),
  155. new StepExpectation ('0', AnsiResponseParserState.InResponse, string.Empty),
  156. new StepExpectation (
  157. 'c',
  158. AnsiResponseParserState.Normal,
  159. string.Empty,
  160. "\u001b[0c"), // c is expected terminator so here we swallow input and populate expected response
  161. new StepExpectation ('\u001b', AnsiResponseParserState.ExpectingEscapeSequence, string.Empty)
  162. }
  163. ];
  164. }
  165. public class StepExpectation ()
  166. {
  167. /// <summary>
  168. /// The input character to feed into the parser at this step of the test
  169. /// </summary>
  170. public char Input { get; }
  171. /// <summary>
  172. /// What should the state of the parser be after the <see cref="Input"/>
  173. /// is fed in.
  174. /// </summary>
  175. public AnsiResponseParserState ExpectedStateAfterOperation { get; }
  176. /// <summary>
  177. /// If this step should release one or more characters, put them here.
  178. /// </summary>
  179. public string ExpectedRelease { get; } = string.Empty;
  180. /// <summary>
  181. /// If this step should result in a completing of detection of ANSI response
  182. /// then put the expected full response sequence here.
  183. /// </summary>
  184. public string ExpectedAnsiResponse { get; } = string.Empty;
  185. public StepExpectation (
  186. char input,
  187. AnsiResponseParserState expectedStateAfterOperation,
  188. string expectedRelease = "",
  189. string expectedAnsiResponse = ""
  190. ) : this ()
  191. {
  192. Input = input;
  193. ExpectedStateAfterOperation = expectedStateAfterOperation;
  194. ExpectedRelease = expectedRelease;
  195. ExpectedAnsiResponse = expectedAnsiResponse;
  196. }
  197. }
  198. [MemberData (nameof (TestInputSequencesExact_Cases))]
  199. [Theory]
  200. public void TestInputSequencesExact (string caseName, char? terminator, IEnumerable<StepExpectation> expectedStates)
  201. {
  202. output.WriteLine ("Running test case:" + caseName);
  203. var parser = new AnsiResponseParser ();
  204. string? response = null;
  205. if (terminator.HasValue)
  206. {
  207. parser.ExpectResponse (terminator.Value.ToString (), s => response = s, null, false);
  208. }
  209. var step = 0;
  210. foreach (StepExpectation state in expectedStates)
  211. {
  212. step++;
  213. // If we expect the response to be detected at this step
  214. if (!string.IsNullOrWhiteSpace (state.ExpectedAnsiResponse))
  215. {
  216. // Then before passing input it should be null
  217. Assert.Null (response);
  218. }
  219. string actual = parser.ProcessInput (state.Input.ToString ());
  220. Assert.Equal (state.ExpectedRelease, actual);
  221. Assert.Equal (state.ExpectedStateAfterOperation, parser.State);
  222. // If we expect the response to be detected at this step
  223. if (!string.IsNullOrWhiteSpace (state.ExpectedAnsiResponse))
  224. {
  225. // And after passing input it shuld be the expected value
  226. Assert.Equal (state.ExpectedAnsiResponse, response);
  227. }
  228. output.WriteLine ($"Step {step} passed");
  229. }
  230. }
  231. [Fact]
  232. public void ReleasesEscapeAfterTimeout ()
  233. {
  234. var input = "\u001b";
  235. var i = 0;
  236. // Esc on its own looks like it might be an esc sequence so should be consumed
  237. AssertConsumed (input, ref i);
  238. // We should know when the state changed
  239. Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence, _parser1.State);
  240. Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence, _parser2.State);
  241. Assert.Equal (DateTime.Now.Date, _parser1.StateChangedAt.Date);
  242. Assert.Equal (DateTime.Now.Date, _parser2.StateChangedAt.Date);
  243. AssertManualReleaseIs (input);
  244. }
  245. [Fact]
  246. public void TwoEscapesInARow ()
  247. {
  248. // Example user presses Esc key then a DAR comes in
  249. var input = "\u001b\u001b";
  250. var i = 0;
  251. // First Esc gets grabbed
  252. AssertConsumed (input, ref i);
  253. // Upon getting the second Esc we should release the first
  254. AssertReleased (input, ref i, "\u001b", 0);
  255. // Assume 50ms or something has passed, lets force release as no new content
  256. // It should be the second escape that gets released (i.e. index 1)
  257. AssertManualReleaseIs ("\u001b", 1);
  258. }
  259. [Fact]
  260. public void TestLateResponses ()
  261. {
  262. var p = new AnsiResponseParser ();
  263. string? responseA = null;
  264. string? responseB = null;
  265. p.ExpectResponse ("z", r => responseA = r, null, false);
  266. // Some time goes by without us seeing a response
  267. p.StopExpecting ("z", false);
  268. // Send our new request
  269. p.ExpectResponse ("z", r => responseB = r, null, false);
  270. // Because we gave up on getting A, we should expect the response to be to our new request
  271. Assert.Empty (p.ProcessInput ("\u001b[<1;2z"));
  272. Assert.Null (responseA);
  273. Assert.Equal ("\u001b[<1;2z", responseB);
  274. // Oh looks like we got one late after all - swallow it
  275. Assert.Empty (p.ProcessInput ("\u001b[0000z"));
  276. // Do not expect late responses to be populated back to your variable
  277. Assert.Null (responseA);
  278. Assert.Equal ("\u001b[<1;2z", responseB);
  279. // We now have no outstanding requests (late or otherwise) so new ansi codes should just fall through
  280. Assert.Equal ("\u001b[111z", p.ProcessInput ("\u001b[111z"));
  281. }
  282. [Fact]
  283. public void TestPersistentResponses ()
  284. {
  285. var p = new AnsiResponseParser ();
  286. var m = 0;
  287. var M = 1;
  288. p.ExpectResponse ("m", _ => m++, null, true);
  289. p.ExpectResponse ("M", _ => M++, null, true);
  290. // Act - Feed input strings containing ANSI sequences
  291. p.ProcessInput ("\u001b[<0;10;10m"); // Should match and increment `m`
  292. p.ProcessInput ("\u001b[<0;20;20m"); // Should match and increment `m`
  293. p.ProcessInput ("\u001b[<0;30;30M"); // Should match and increment `M`
  294. p.ProcessInput ("\u001b[<0;40;40M"); // Should match and increment `M`
  295. p.ProcessInput ("\u001b[<0;50;50M"); // Should match and increment `M`
  296. // Assert - Verify that counters reflect the expected counts of each terminator
  297. Assert.Equal (2, m); // Expected two `m` responses
  298. Assert.Equal (4, M); // Expected three `M` responses plus the initial value of 1
  299. }
  300. [Fact]
  301. public void TestPersistentResponses_WithMetadata ()
  302. {
  303. AnsiResponseParser<int> p = new ();
  304. // ReSharper disable once NotAccessedVariable
  305. var m = 0;
  306. List<Tuple<char, int>> result = new ();
  307. p.ExpectResponseT (
  308. "m",
  309. r =>
  310. {
  311. result = r.ToList ();
  312. m++;
  313. },
  314. null,
  315. true);
  316. // Act - Feed input strings containing ANSI sequences
  317. p.ProcessInput (StringToBatch ("\u001b[<0;10;10m")); // Should match and increment `m`
  318. // Prepare expected result:
  319. List<Tuple<char, int>> expected = new()
  320. {
  321. Tuple.Create ('\u001b', 0), // Escape character
  322. Tuple.Create ('[', 1),
  323. Tuple.Create ('<', 2),
  324. Tuple.Create ('0', 3),
  325. Tuple.Create (';', 4),
  326. Tuple.Create ('1', 5),
  327. Tuple.Create ('0', 6),
  328. Tuple.Create (';', 7),
  329. Tuple.Create ('1', 8),
  330. Tuple.Create ('0', 9),
  331. Tuple.Create ('m', 10)
  332. };
  333. Assert.Equal (expected.Count, result.Count); // Ensure the count is as expected
  334. Assert.True (expected.SequenceEqual (result), "The result does not match the expected output."); // Check the actual content
  335. }
  336. [Fact]
  337. public void ShouldSwallowUnknownResponses_WhenDelegateSaysSo ()
  338. {
  339. // Swallow all unknown escape codes
  340. _parser1.UnexpectedResponseHandler = _ => true;
  341. _parser2.UnknownResponseHandler = _ => true;
  342. AssertReleased (
  343. "Just te\u001b[<0;0;0M\u001b[3c\u001b[2c\u001b[4cst",
  344. "Just test",
  345. 0,
  346. 1,
  347. 2,
  348. 3,
  349. 4,
  350. 5,
  351. 6,
  352. 28,
  353. 29);
  354. }
  355. [Fact]
  356. public void UnknownResponses_ParameterShouldMatch ()
  357. {
  358. // Track unknown responses passed to the UnexpectedResponseHandler
  359. List<string> unknownResponses = new ();
  360. // Set up the UnexpectedResponseHandler to log each unknown response
  361. _parser1.UnexpectedResponseHandler = r1 =>
  362. {
  363. unknownResponses.Add (BatchToString (r1));
  364. return true; // Return true to swallow unknown responses
  365. };
  366. _parser2.UnknownResponseHandler = r2 =>
  367. {
  368. // parsers should be agreeing on what these responses are!
  369. Assert.Equal (unknownResponses.Last (), r2);
  370. return true; // Return true to swallow unknown responses
  371. };
  372. // Input with known and unknown responses
  373. AssertReleased (
  374. "Just te\u001b[<0;0;0M\u001b[3c\u001b[2c\u001b[4cst",
  375. "Just test");
  376. // Expected unknown responses (ANSI sequences that are unknown)
  377. List<string> expectedUnknownResponses = new()
  378. {
  379. "\u001b[<0;0;0M",
  380. "\u001b[3c",
  381. "\u001b[2c",
  382. "\u001b[4c"
  383. };
  384. // Assert that the UnexpectedResponseHandler was called with the correct unknown responses
  385. Assert.Equal (expectedUnknownResponses.Count, unknownResponses.Count);
  386. Assert.Equal (expectedUnknownResponses, unknownResponses);
  387. }
  388. [Fact]
  389. public void ParserDetectsMouse ()
  390. {
  391. // ANSI escape sequence for mouse down (using a generic format example)
  392. const string MOUSE_DOWN = "\u001B[<0;12;32M";
  393. // ANSI escape sequence for Device Attribute Response (e.g., Terminal identifying itself)
  394. const string DEVICE_ATTRIBUTE_RESPONSE = "\u001B[?1;2c";
  395. // ANSI escape sequence for mouse up (using a generic format example)
  396. const string MOUSE_UP = "\u001B[<0;25;50m";
  397. var parser = new AnsiResponseParser ();
  398. parser.HandleMouse = true;
  399. string? foundDar = null;
  400. List<MouseEventArgs> mouseEventArgs = new ();
  401. parser.Mouse += (s, e) => mouseEventArgs.Add (e);
  402. parser.ExpectResponse ("c", dar => foundDar = dar, null, false);
  403. string released = parser.ProcessInput ("a" + MOUSE_DOWN + "asdf" + DEVICE_ATTRIBUTE_RESPONSE + "bbcc" + MOUSE_UP + "sss");
  404. Assert.Equal ("aasdfbbccsss", released);
  405. Assert.Equal (2, mouseEventArgs.Count);
  406. Assert.NotNull (foundDar);
  407. Assert.Equal (DEVICE_ATTRIBUTE_RESPONSE, foundDar);
  408. Assert.True (mouseEventArgs [0].IsPressed);
  409. // Mouse positions in ANSI are 1 based so actual Terminal.Gui Screen positions are x-1,y-1
  410. Assert.Equal (11, mouseEventArgs [0].Position.X);
  411. Assert.Equal (31, mouseEventArgs [0].Position.Y);
  412. Assert.True (mouseEventArgs [1].IsReleased);
  413. Assert.Equal (24, mouseEventArgs [1].Position.X);
  414. Assert.Equal (49, mouseEventArgs [1].Position.Y);
  415. }
  416. [Fact]
  417. public void ParserDetectsKeyboard ()
  418. {
  419. // ANSI escape sequence for cursor left
  420. const string LEFT = "\u001b[D";
  421. // ANSI escape sequence for Device Attribute Response (e.g., Terminal identifying itself)
  422. const string DEVICE_ATTRIBUTE_RESPONSE = "\u001B[?1;2c";
  423. // ANSI escape sequence for cursor up (while shift held down)
  424. const string SHIFT_UP = "\u001b[1;2A";
  425. var parser = new AnsiResponseParser ();
  426. parser.HandleKeyboard = true;
  427. string? foundDar = null;
  428. List<Key> keys = new ();
  429. parser.Keyboard += (s, e) => keys.Add (e);
  430. parser.ExpectResponse ("c", dar => foundDar = dar, null, false);
  431. string released = parser.ProcessInput ("a" + LEFT + "asdf" + DEVICE_ATTRIBUTE_RESPONSE + "bbcc" + SHIFT_UP + "sss");
  432. Assert.Equal ("aasdfbbccsss", released);
  433. Assert.Equal (2, keys.Count);
  434. Assert.NotNull (foundDar);
  435. Assert.Equal (DEVICE_ATTRIBUTE_RESPONSE, foundDar);
  436. Assert.Equal (Key.CursorLeft, keys [0]);
  437. Assert.Equal (Key.CursorUp.WithShift, keys [1]);
  438. }
  439. public static IEnumerable<object []> ParserDetects_FunctionKeys_Cases ()
  440. {
  441. // These are VT100 escape codes for F1-4
  442. yield return
  443. [
  444. "\u001bOP",
  445. Key.F1
  446. ];
  447. yield return
  448. [
  449. "\u001bOQ",
  450. Key.F2
  451. ];
  452. yield return
  453. [
  454. "\u001bOR",
  455. Key.F3
  456. ];
  457. yield return
  458. [
  459. "\u001bOS",
  460. Key.F4
  461. ];
  462. // These are also F keys
  463. yield return
  464. [
  465. "\u001b[11~",
  466. Key.F1
  467. ];
  468. yield return
  469. [
  470. "\u001b[12~",
  471. Key.F2
  472. ];
  473. yield return
  474. [
  475. "\u001b[13~",
  476. Key.F3
  477. ];
  478. yield return
  479. [
  480. "\u001b[14~",
  481. Key.F4
  482. ];
  483. yield return
  484. [
  485. "\u001b[15~",
  486. Key.F5
  487. ];
  488. yield return
  489. [
  490. "\u001b[17~",
  491. Key.F6
  492. ];
  493. yield return
  494. [
  495. "\u001b[18~",
  496. Key.F7
  497. ];
  498. yield return
  499. [
  500. "\u001b[19~",
  501. Key.F8
  502. ];
  503. yield return
  504. [
  505. "\u001b[20~",
  506. Key.F9
  507. ];
  508. yield return
  509. [
  510. "\u001b[21~",
  511. Key.F10
  512. ];
  513. yield return
  514. [
  515. "\u001b[23~",
  516. Key.F11
  517. ];
  518. yield return
  519. [
  520. "\u001b[24~",
  521. Key.F12
  522. ];
  523. }
  524. [MemberData (nameof (ParserDetects_FunctionKeys_Cases))]
  525. [Theory]
  526. public void ParserDetects_FunctionKeys (string input, Key expectedKey)
  527. {
  528. var parser = new AnsiResponseParser ();
  529. parser.HandleKeyboard = true;
  530. List<Key> keys = new ();
  531. parser.Keyboard += (s, e) => keys.Add (e);
  532. foreach (char ch in input)
  533. {
  534. parser.ProcessInput (new (ch, 1));
  535. }
  536. Key k = Assert.Single (keys);
  537. Assert.Equal (k, expectedKey);
  538. }
  539. private Tuple<char, int> [] StringToBatch (string batch) { return batch.Select (k => Tuple.Create (k, _tIndex++)).ToArray (); }
  540. public static IEnumerable<string []> GetBatchPermutations (string input, int maxDepth = 3)
  541. {
  542. // Call the recursive method to generate batches with an initial depth of 0
  543. return GenerateBatches (input, 0, maxDepth, 0);
  544. }
  545. private static IEnumerable<string []> GenerateBatches (string input, int start, int maxDepth, int currentDepth)
  546. {
  547. // If we have reached the maximum recursion depth, return no results
  548. if (currentDepth >= maxDepth)
  549. {
  550. yield break; // No more batches can be generated at this depth
  551. }
  552. // If we have reached the end of the string, return an empty list
  553. if (start >= input.Length)
  554. {
  555. yield return new string [0];
  556. yield break;
  557. }
  558. // Iterate over the input string to create batches
  559. for (int i = start + 1; i <= input.Length; i++)
  560. {
  561. // Take a batch from 'start' to 'i'
  562. string batch = input.Substring (start, i - start);
  563. // Recursively get batches from the remaining substring, increasing the depth
  564. foreach (string [] remainingBatches in GenerateBatches (input, i, maxDepth, currentDepth + 1))
  565. {
  566. // Combine the current batch with the remaining batches
  567. var result = new string [1 + remainingBatches.Length];
  568. result [0] = batch;
  569. Array.Copy (remainingBatches, 0, result, 1, remainingBatches.Length);
  570. yield return result;
  571. }
  572. }
  573. }
  574. private void AssertIgnored (string ansiStream, char expected, ref int i)
  575. {
  576. char c2 = ansiStream [i];
  577. Tuple<char, int> [] c1 = NextChar (ansiStream, ref i);
  578. // Parser does not grab this key (i.e. driver can continue with regular operations)
  579. Assert.Equal (c1, _parser1.ProcessInput (c1));
  580. Assert.Equal (expected, c1.Single ().Item1);
  581. Assert.Equal (c2, _parser2.ProcessInput (c2.ToString ()).Single ());
  582. Assert.Equal (expected, c2);
  583. }
  584. private void AssertConsumed (string ansiStream, ref int i)
  585. {
  586. // Parser grabs this key
  587. char c2 = ansiStream [i];
  588. Tuple<char, int> [] c1 = NextChar (ansiStream, ref i);
  589. Assert.Empty (_parser1.ProcessInput (c1));
  590. Assert.Empty (_parser2.ProcessInput (c2.ToString ()));
  591. }
  592. /// <summary>
  593. /// Overload that fully exhausts <paramref name="ansiStream"/> and asserts
  594. /// that the final released content across whole processing is <paramref name="expectedRelease"/>
  595. /// </summary>
  596. /// <param name="ansiStream"></param>
  597. /// <param name="expectedRelease"></param>
  598. /// <param name="expectedTValues"></param>
  599. private void AssertReleased (string ansiStream, string expectedRelease, params int [] expectedTValues)
  600. {
  601. var sb = new StringBuilder ();
  602. List<int> tValues = new ();
  603. var i = 0;
  604. while (i < ansiStream.Length)
  605. {
  606. char c2 = ansiStream [i];
  607. Tuple<char, int> [] c1 = NextChar (ansiStream, ref i);
  608. Tuple<char, int> [] released1 = _parser1.ProcessInput (c1).ToArray ();
  609. tValues.AddRange (released1.Select (kv => kv.Item2));
  610. string released2 = _parser2.ProcessInput (c2.ToString ());
  611. // Both parsers should have same chars so release chars consistently with each other
  612. Assert.Equal (BatchToString (released1), released2);
  613. sb.Append (released2);
  614. }
  615. Assert.Equal (expectedRelease, sb.ToString ());
  616. if (expectedTValues.Length > 0)
  617. {
  618. Assert.True (expectedTValues.SequenceEqual (tValues));
  619. }
  620. }
  621. /// <summary>
  622. /// Asserts that <paramref name="i"/> index of <see cref="ansiStream"/> when consumed will release
  623. /// <paramref name="expectedRelease"/>. Results in implicit increment of <paramref name="i"/>.
  624. /// <remarks>Note that this does NOT iteratively consume all the stream, only 1 char at <paramref name="i"/></remarks>
  625. /// </summary>
  626. /// <param name="ansiStream"></param>
  627. /// <param name="i"></param>
  628. /// <param name="expectedRelease"></param>
  629. /// <param name="expectedTValues"></param>
  630. private void AssertReleased (string ansiStream, ref int i, string expectedRelease, params int [] expectedTValues)
  631. {
  632. char c2 = ansiStream [i];
  633. Tuple<char, int> [] c1 = NextChar (ansiStream, ref i);
  634. // Parser realizes it has grabbed content that does not belong to an outstanding request
  635. // Parser returns false to indicate to continue
  636. Tuple<char, int> [] released1 = _parser1.ProcessInput (c1).ToArray ();
  637. Assert.Equal (expectedRelease, BatchToString (released1));
  638. if (expectedTValues.Length > 0)
  639. {
  640. Assert.True (expectedTValues.SequenceEqual (released1.Select (kv => kv.Item2)));
  641. }
  642. Assert.Equal (expectedRelease, _parser2.ProcessInput (c2.ToString ()));
  643. }
  644. private string BatchToString (IEnumerable<Tuple<char, int>> processInput) { return new (processInput.Select (a => a.Item1).ToArray ()); }
  645. private Tuple<char, int> [] NextChar (string ansiStream, ref int i) { return StringToBatch (ansiStream [i++].ToString ()); }
  646. private void AssertManualReleaseIs (string expectedRelease, params int [] expectedTValues)
  647. {
  648. // Consumer is responsible for determining this based on e.g. after 50ms
  649. Tuple<char, int> [] released1 = _parser1.Release ().ToArray ();
  650. Assert.Equal (expectedRelease, BatchToString (released1));
  651. if (expectedTValues.Length > 0)
  652. {
  653. Assert.True (expectedTValues.SequenceEqual (released1.Select (kv => kv.Item2)));
  654. }
  655. Assert.Equal (expectedRelease, _parser2.Release ());
  656. Assert.Equal (AnsiResponseParserState.Normal, _parser1.State);
  657. Assert.Equal (AnsiResponseParserState.Normal, _parser2.State);
  658. }
  659. }