#nullable enable using System.Collections.Concurrent; using Xunit.Abstractions; namespace DriverTests; /// /// Parallelizable unit tests for IInput.EnqueueKeyDownEvent and InputProcessor.EnqueueKeyDownEvent. /// Tests validate the entire pipeline: Key → TInputRecord → Queue → ProcessQueue → Events. /// [Trait ("Category", "Input")] public class EnqueueKeyEventTests (ITestOutputHelper output) { private readonly ITestOutputHelper _output = output; #region Helper Methods /// /// Simulates the input thread by manually draining FakeInput's internal queue /// and moving items to the InputBuffer. This is needed because tests don't /// start the actual input thread via Run(). /// private static void SimulateInputThread (FakeInput fakeInput, ConcurrentQueue inputBuffer) { // FakeInput's Peek() checks _testInput while (fakeInput.Peek ()) { // Read() drains _testInput and returns items foreach (ConsoleKeyInfo item in fakeInput.Read ()) { // Manually add to InputBuffer (simulating what Run() would do) inputBuffer.Enqueue (item); } } } /// /// Processes the input queue with support for keys that may be held by the ANSI parser (like Esc). /// The parser holds Esc for 50ms waiting to see if it's part of an escape sequence. /// private static void ProcessQueueWithEscapeHandling (FakeInputProcessor processor, int maxAttempts = 3) { // First attempt - process immediately processor.ProcessQueue (); // For escape sequences, we may need to wait and process again // The parser holds escape for 50ms before releasing for (var attempt = 1; attempt < maxAttempts; attempt++) { Thread.Sleep (60); // Wait longer than the 50ms escape timeout processor.ProcessQueue (); // This should release any held escape keys } } #endregion #region FakeInput EnqueueKeyDownEvent Tests [Fact] public void FakeInput_EnqueueKeyDownEvent_AddsSingleKeyToQueue () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; List receivedKeys = []; processor.KeyDown += (_, k) => receivedKeys.Add (k); Key key = Key.A; // Act processor.EnqueueKeyDownEvent (key); // Simulate the input thread moving items from _testInput to InputBuffer SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert - Verify the key made it through Assert.Single (receivedKeys); Assert.Equal (key, receivedKeys [0]); } [Fact] public void FakeInput_EnqueueKeyDownEvent_SupportsMultipleKeys () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; Key [] keys = [Key.A, Key.B, Key.C, Key.Enter]; List receivedKeys = []; processor.KeyDown += (_, k) => receivedKeys.Add (k); // Act foreach (Key key in keys) { processor.EnqueueKeyDownEvent (key); } SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert Assert.Equal (keys.Length, receivedKeys.Count); Assert.Equal (keys, receivedKeys); } [Theory] [InlineData (KeyCode.A, false, false, false)] [InlineData (KeyCode.A, true, false, false)] // Shift+A [InlineData (KeyCode.A, false, true, false)] // Ctrl+A [InlineData (KeyCode.A, false, false, true)] // Alt+A [InlineData (KeyCode.A, true, true, true)] // Ctrl+Shift+Alt+A public void FakeInput_EnqueueKeyDownEvent_PreservesModifiers (KeyCode keyCode, bool shift, bool ctrl, bool alt) { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; var key = new Key (keyCode); if (shift) { key = key.WithShift; } if (ctrl) { key = key.WithCtrl; } if (alt) { key = key.WithAlt; } Key? receivedKey = null; processor.KeyDown += (_, k) => receivedKey = k; // Act processor.EnqueueKeyDownEvent (key); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert Assert.NotNull (receivedKey); Assert.Equal (key.IsShift, receivedKey.IsShift); Assert.Equal (key.IsCtrl, receivedKey.IsCtrl); Assert.Equal (key.IsAlt, receivedKey.IsAlt); Assert.Equal (key.KeyCode, receivedKey.KeyCode); } [Theory] [InlineData (KeyCode.Enter)] [InlineData (KeyCode.Tab)] [InlineData (KeyCode.Esc)] [InlineData (KeyCode.Backspace)] [InlineData (KeyCode.Delete)] [InlineData (KeyCode.CursorUp)] [InlineData (KeyCode.CursorDown)] [InlineData (KeyCode.CursorLeft)] [InlineData (KeyCode.CursorRight)] [InlineData (KeyCode.F1)] [InlineData (KeyCode.F12)] public void FakeInput_EnqueueKeyDownEvent_SupportsSpecialKeys (KeyCode keyCode) { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; var key = new Key (keyCode); Key? receivedKey = null; processor.KeyDown += (_, k) => receivedKey = k; // Act processor.EnqueueKeyDownEvent (key); SimulateInputThread (fakeInput, queue); // Esc is special - the ANSI parser holds it waiting for potential escape sequences // We need to process with delay to let the parser release it after timeout if (keyCode == KeyCode.Esc) { ProcessQueueWithEscapeHandling (processor); } else { processor.ProcessQueue (); } // Assert Assert.NotNull (receivedKey); Assert.Equal (key.KeyCode, receivedKey.KeyCode); } [Fact] public void FakeInput_EnqueueKeyDownEvent_RaisesKeyDownAndKeyUpEvents () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; var keyDownCount = 0; var keyUpCount = 0; processor.KeyDown += (_, _) => keyDownCount++; processor.KeyUp += (_, _) => keyUpCount++; // Act processor.EnqueueKeyDownEvent (Key.A); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert - FakeDriver simulates KeyUp immediately after KeyDown Assert.Equal (1, keyDownCount); Assert.Equal (1, keyUpCount); } #endregion #region InputProcessor Pipeline Tests [Fact] public void InputProcessor_EnqueueKeyDownEvent_RequiresTestableInput () { // Arrange ConcurrentQueue queue = new (); var processor = new FakeInputProcessor (queue); // Don't set InputImpl (or set to non-testable) // Act & Assert - Should not throw, but also won't add to queue // (because InputImpl is null or not ITestableInput) processor.EnqueueKeyDownEvent (Key.A); processor.ProcessQueue (); // No events should be raised since no input was added var eventRaised = false; processor.KeyDown += (_, _) => eventRaised = true; processor.ProcessQueue (); Assert.False (eventRaised); } [Fact] public void InputProcessor_ProcessQueue_DrainsPendingInputRecords () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; List receivedKeys = []; processor.KeyDown += (_, k) => receivedKeys.Add (k); // Act - Enqueue multiple keys before processing processor.EnqueueKeyDownEvent (Key.A); processor.EnqueueKeyDownEvent (Key.B); processor.EnqueueKeyDownEvent (Key.C); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert - After processing, queue should be empty and all keys received Assert.Empty (queue); Assert.Equal (3, receivedKeys.Count); } #endregion #region Thread Safety Tests [Fact] public void FakeInput_EnqueueKeyDownEvent_IsThreadSafe () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; ConcurrentBag receivedKeys = []; processor.KeyDown += (_, k) => receivedKeys.Add (k); const int threadCount = 10; const int keysPerThread = 100; Thread [] threads = new Thread [threadCount]; // Act - Enqueue keys from multiple threads for (var t = 0; t < threadCount; t++) { threads [t] = new (() => { for (var i = 0; i < keysPerThread; i++) { processor.EnqueueKeyDownEvent (Key.A); } }); threads [t].Start (); } // Wait for all threads to complete foreach (Thread thread in threads) { thread.Join (); } SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert Assert.Equal (threadCount * keysPerThread, receivedKeys.Count); } #endregion #region Error Handling Tests [Fact] public void FakeInput_EnqueueKeyDownEvent_WithInvalidKey_DoesNotThrow () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; // Act & Assert - Empty/null key should not throw Exception? exception = Record.Exception (() => { processor.EnqueueKeyDownEvent (Key.Empty); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); }); Assert.Null (exception); } #endregion }