EnqueueKeyEventTests.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using Xunit.Abstractions;
  4. namespace DriverTests;
  5. /// <summary>
  6. /// Parallelizable unit tests for IInput.EnqueueKeyDownEvent and InputProcessor.EnqueueKeyDownEvent.
  7. /// Tests validate the entire pipeline: Key → TInputRecord → Queue → ProcessQueue → Events.
  8. /// </summary>
  9. [Trait ("Category", "Input")]
  10. public class EnqueueKeyEventTests (ITestOutputHelper output)
  11. {
  12. private readonly ITestOutputHelper _output = output;
  13. #region Helper Methods
  14. /// <summary>
  15. /// Simulates the input thread by manually draining FakeInput's internal queue
  16. /// and moving items to the InputBuffer. This is needed because tests don't
  17. /// start the actual input thread via Run().
  18. /// </summary>
  19. private static void SimulateInputThread (FakeInput fakeInput, ConcurrentQueue<ConsoleKeyInfo> inputBuffer)
  20. {
  21. // FakeInput's Peek() checks _testInput
  22. while (fakeInput.Peek ())
  23. {
  24. // Read() drains _testInput and returns items
  25. foreach (ConsoleKeyInfo item in fakeInput.Read ())
  26. {
  27. // Manually add to InputBuffer (simulating what Run() would do)
  28. inputBuffer.Enqueue (item);
  29. }
  30. }
  31. }
  32. /// <summary>
  33. /// Processes the input queue with support for keys that may be held by the ANSI parser (like Esc).
  34. /// The parser holds Esc for 50ms waiting to see if it's part of an escape sequence.
  35. /// </summary>
  36. private static void ProcessQueueWithEscapeHandling (FakeInputProcessor processor, int maxAttempts = 3)
  37. {
  38. // First attempt - process immediately
  39. processor.ProcessQueue ();
  40. // For escape sequences, we may need to wait and process again
  41. // The parser holds escape for 50ms before releasing
  42. for (var attempt = 1; attempt < maxAttempts; attempt++)
  43. {
  44. Thread.Sleep (60); // Wait longer than the 50ms escape timeout
  45. processor.ProcessQueue (); // This should release any held escape keys
  46. }
  47. }
  48. #endregion
  49. #region FakeInput EnqueueKeyDownEvent Tests
  50. [Fact]
  51. public void FakeInput_EnqueueKeyDownEvent_AddsSingleKeyToQueue ()
  52. {
  53. // Arrange
  54. var fakeInput = new FakeInput ();
  55. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  56. fakeInput.Initialize (queue);
  57. var processor = new FakeInputProcessor (queue);
  58. processor.InputImpl = fakeInput;
  59. List<Key> receivedKeys = [];
  60. processor.KeyDown += (_, k) => receivedKeys.Add (k);
  61. Key key = Key.A;
  62. // Act
  63. processor.EnqueueKeyDownEvent (key);
  64. // Simulate the input thread moving items from _testInput to InputBuffer
  65. SimulateInputThread (fakeInput, queue);
  66. processor.ProcessQueue ();
  67. // Assert - Verify the key made it through
  68. Assert.Single (receivedKeys);
  69. Assert.Equal (key, receivedKeys [0]);
  70. }
  71. [Fact]
  72. public void FakeInput_EnqueueKeyDownEvent_SupportsMultipleKeys ()
  73. {
  74. // Arrange
  75. var fakeInput = new FakeInput ();
  76. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  77. fakeInput.Initialize (queue);
  78. var processor = new FakeInputProcessor (queue);
  79. processor.InputImpl = fakeInput;
  80. Key [] keys = [Key.A, Key.B, Key.C, Key.Enter];
  81. List<Key> receivedKeys = [];
  82. processor.KeyDown += (_, k) => receivedKeys.Add (k);
  83. // Act
  84. foreach (Key key in keys)
  85. {
  86. processor.EnqueueKeyDownEvent (key);
  87. }
  88. SimulateInputThread (fakeInput, queue);
  89. processor.ProcessQueue ();
  90. // Assert
  91. Assert.Equal (keys.Length, receivedKeys.Count);
  92. Assert.Equal (keys, receivedKeys);
  93. }
  94. [Theory]
  95. [InlineData (KeyCode.A, false, false, false)]
  96. [InlineData (KeyCode.A, true, false, false)] // Shift+A
  97. [InlineData (KeyCode.A, false, true, false)] // Ctrl+A
  98. [InlineData (KeyCode.A, false, false, true)] // Alt+A
  99. [InlineData (KeyCode.A, true, true, true)] // Ctrl+Shift+Alt+A
  100. public void FakeInput_EnqueueKeyDownEvent_PreservesModifiers (KeyCode keyCode, bool shift, bool ctrl, bool alt)
  101. {
  102. // Arrange
  103. var fakeInput = new FakeInput ();
  104. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  105. fakeInput.Initialize (queue);
  106. var processor = new FakeInputProcessor (queue);
  107. processor.InputImpl = fakeInput;
  108. var key = new Key (keyCode);
  109. if (shift)
  110. {
  111. key = key.WithShift;
  112. }
  113. if (ctrl)
  114. {
  115. key = key.WithCtrl;
  116. }
  117. if (alt)
  118. {
  119. key = key.WithAlt;
  120. }
  121. Key? receivedKey = null;
  122. processor.KeyDown += (_, k) => receivedKey = k;
  123. // Act
  124. processor.EnqueueKeyDownEvent (key);
  125. SimulateInputThread (fakeInput, queue);
  126. processor.ProcessQueue ();
  127. // Assert
  128. Assert.NotNull (receivedKey);
  129. Assert.Equal (key.IsShift, receivedKey.IsShift);
  130. Assert.Equal (key.IsCtrl, receivedKey.IsCtrl);
  131. Assert.Equal (key.IsAlt, receivedKey.IsAlt);
  132. Assert.Equal (key.KeyCode, receivedKey.KeyCode);
  133. }
  134. [Theory]
  135. [InlineData (KeyCode.Enter)]
  136. [InlineData (KeyCode.Tab)]
  137. [InlineData (KeyCode.Esc)]
  138. [InlineData (KeyCode.Backspace)]
  139. [InlineData (KeyCode.Delete)]
  140. [InlineData (KeyCode.CursorUp)]
  141. [InlineData (KeyCode.CursorDown)]
  142. [InlineData (KeyCode.CursorLeft)]
  143. [InlineData (KeyCode.CursorRight)]
  144. [InlineData (KeyCode.F1)]
  145. [InlineData (KeyCode.F12)]
  146. public void FakeInput_EnqueueKeyDownEvent_SupportsSpecialKeys (KeyCode keyCode)
  147. {
  148. // Arrange
  149. var fakeInput = new FakeInput ();
  150. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  151. fakeInput.Initialize (queue);
  152. var processor = new FakeInputProcessor (queue);
  153. processor.InputImpl = fakeInput;
  154. var key = new Key (keyCode);
  155. Key? receivedKey = null;
  156. processor.KeyDown += (_, k) => receivedKey = k;
  157. // Act
  158. processor.EnqueueKeyDownEvent (key);
  159. SimulateInputThread (fakeInput, queue);
  160. // Esc is special - the ANSI parser holds it waiting for potential escape sequences
  161. // We need to process with delay to let the parser release it after timeout
  162. if (keyCode == KeyCode.Esc)
  163. {
  164. ProcessQueueWithEscapeHandling (processor);
  165. }
  166. else
  167. {
  168. processor.ProcessQueue ();
  169. }
  170. // Assert
  171. Assert.NotNull (receivedKey);
  172. Assert.Equal (key.KeyCode, receivedKey.KeyCode);
  173. }
  174. [Fact]
  175. public void FakeInput_EnqueueKeyDownEvent_RaisesKeyDownAndKeyUpEvents ()
  176. {
  177. // Arrange
  178. var fakeInput = new FakeInput ();
  179. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  180. fakeInput.Initialize (queue);
  181. var processor = new FakeInputProcessor (queue);
  182. processor.InputImpl = fakeInput;
  183. var keyDownCount = 0;
  184. var keyUpCount = 0;
  185. processor.KeyDown += (_, _) => keyDownCount++;
  186. processor.KeyUp += (_, _) => keyUpCount++;
  187. // Act
  188. processor.EnqueueKeyDownEvent (Key.A);
  189. SimulateInputThread (fakeInput, queue);
  190. processor.ProcessQueue ();
  191. // Assert - FakeDriver simulates KeyUp immediately after KeyDown
  192. Assert.Equal (1, keyDownCount);
  193. Assert.Equal (1, keyUpCount);
  194. }
  195. #endregion
  196. #region InputProcessor Pipeline Tests
  197. [Fact]
  198. public void InputProcessor_EnqueueKeyDownEvent_RequiresTestableInput ()
  199. {
  200. // Arrange
  201. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  202. var processor = new FakeInputProcessor (queue);
  203. // Don't set InputImpl (or set to non-testable)
  204. // Act & Assert - Should not throw, but also won't add to queue
  205. // (because InputImpl is null or not ITestableInput)
  206. processor.EnqueueKeyDownEvent (Key.A);
  207. processor.ProcessQueue ();
  208. // No events should be raised since no input was added
  209. var eventRaised = false;
  210. processor.KeyDown += (_, _) => eventRaised = true;
  211. processor.ProcessQueue ();
  212. Assert.False (eventRaised);
  213. }
  214. [Fact]
  215. public void InputProcessor_ProcessQueue_DrainsPendingInputRecords ()
  216. {
  217. // Arrange
  218. var fakeInput = new FakeInput ();
  219. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  220. fakeInput.Initialize (queue);
  221. var processor = new FakeInputProcessor (queue);
  222. processor.InputImpl = fakeInput;
  223. List<Key> receivedKeys = [];
  224. processor.KeyDown += (_, k) => receivedKeys.Add (k);
  225. // Act - Enqueue multiple keys before processing
  226. processor.EnqueueKeyDownEvent (Key.A);
  227. processor.EnqueueKeyDownEvent (Key.B);
  228. processor.EnqueueKeyDownEvent (Key.C);
  229. SimulateInputThread (fakeInput, queue);
  230. processor.ProcessQueue ();
  231. // Assert - After processing, queue should be empty and all keys received
  232. Assert.Empty (queue);
  233. Assert.Equal (3, receivedKeys.Count);
  234. }
  235. #endregion
  236. #region Thread Safety Tests
  237. [Fact]
  238. public void FakeInput_EnqueueKeyDownEvent_IsThreadSafe ()
  239. {
  240. // Arrange
  241. var fakeInput = new FakeInput ();
  242. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  243. fakeInput.Initialize (queue);
  244. var processor = new FakeInputProcessor (queue);
  245. processor.InputImpl = fakeInput;
  246. ConcurrentBag<Key> receivedKeys = [];
  247. processor.KeyDown += (_, k) => receivedKeys.Add (k);
  248. const int threadCount = 10;
  249. const int keysPerThread = 100;
  250. Thread [] threads = new Thread [threadCount];
  251. // Act - Enqueue keys from multiple threads
  252. for (var t = 0; t < threadCount; t++)
  253. {
  254. threads [t] = new (() =>
  255. {
  256. for (var i = 0; i < keysPerThread; i++)
  257. {
  258. processor.EnqueueKeyDownEvent (Key.A);
  259. }
  260. });
  261. threads [t].Start ();
  262. }
  263. // Wait for all threads to complete
  264. foreach (Thread thread in threads)
  265. {
  266. thread.Join ();
  267. }
  268. SimulateInputThread (fakeInput, queue);
  269. processor.ProcessQueue ();
  270. // Assert
  271. Assert.Equal (threadCount * keysPerThread, receivedKeys.Count);
  272. }
  273. #endregion
  274. #region Error Handling Tests
  275. [Fact]
  276. public void FakeInput_EnqueueKeyDownEvent_WithInvalidKey_DoesNotThrow ()
  277. {
  278. // Arrange
  279. var fakeInput = new FakeInput ();
  280. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  281. fakeInput.Initialize (queue);
  282. var processor = new FakeInputProcessor (queue);
  283. processor.InputImpl = fakeInput;
  284. // Act & Assert - Empty/null key should not throw
  285. Exception? exception = Record.Exception (() =>
  286. {
  287. processor.EnqueueKeyDownEvent (Key.Empty);
  288. SimulateInputThread (fakeInput, queue);
  289. processor.ProcessQueue ();
  290. });
  291. Assert.Null (exception);
  292. }
  293. #endregion
  294. }