| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- #nullable enable
- using System.Collections.Concurrent;
- using Xunit.Abstractions;
- namespace UnitTests_Parallelizable.DriverTests;
- /// <summary>
- /// Parallelizable unit tests for IInput.EnqueueKeyDownEvent and InputProcessor.EnqueueKeyDownEvent.
- /// Tests validate the entire pipeline: Key → TInputRecord → Queue → ProcessQueue → Events.
- /// </summary>
- [Trait ("Category", "Input")]
- public class EnqueueKeyEventTests (ITestOutputHelper output)
- {
- private readonly ITestOutputHelper _output = output;
- #region Helper Methods
- /// <summary>
- /// 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().
- /// </summary>
- private static void SimulateInputThread (FakeInput fakeInput, ConcurrentQueue<ConsoleKeyInfo> 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);
- }
- }
- }
- /// <summary>
- /// 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.
- /// </summary>
- 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<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- List<Key> 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<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- Key [] keys = [Key.A, Key.B, Key.C, Key.Enter];
- List<Key> 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<ConsoleKeyInfo> 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<ConsoleKeyInfo> 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<ConsoleKeyInfo> 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<ConsoleKeyInfo> 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<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- List<Key> 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<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- ConcurrentBag<Key> 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<ConsoleKeyInfo> 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
- }
|