mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-27 00:07:58 +01:00
365 lines
11 KiB
C#
365 lines
11 KiB
C#
#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
|
|
} |