| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531 |
- #nullable enable
- using System.Collections.Concurrent;
- using Xunit.Abstractions;
- namespace UnitTests_Parallelizable.DriverTests;
- /// <summary>
- /// Parallelizable unit tests for IInputProcessor.EnqueueMouseEvent.
- /// Tests validate the entire pipeline: MouseEventArgs → TInputRecord → Queue → ProcessQueue → Events.
- /// fully implemented in InputProcessorImpl (base class). Only WindowsInputProcessor has a working implementation.
- /// </summary>
- [Trait ("Category", "Input")]
- public class EnqueueMouseEventTests (ITestOutputHelper output)
- {
- private readonly ITestOutputHelper _output = output;
- #region Mouse Event Sequencing Tests
- [Fact]
- public void FakeInput_EnqueueMouseEvent_HandlesCompleteClickSequence ()
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- List<MouseEventArgs> receivedEvents = [];
- processor.MouseEvent += (_, e) => receivedEvents.Add (e);
- // Act - Simulate a complete click: press → release → click
- processor.EnqueueMouseEvent (
- new()
- {
- Position = new (10, 5),
- Flags = MouseFlags.Button1Pressed
- });
- processor.EnqueueMouseEvent (
- new()
- {
- Position = new (10, 5),
- Flags = MouseFlags.Button1Released
- });
- // The MouseInterpreter in the processor should generate a clicked event
- SimulateInputThread (fakeInput, queue);
- processor.ProcessQueue ();
- // Assert
- // We should see at least the pressed and released events
- Assert.True (receivedEvents.Count >= 2);
- Assert.Contains (receivedEvents, e => e.Flags.HasFlag (MouseFlags.Button1Pressed));
- Assert.Contains (receivedEvents, e => e.Flags.HasFlag (MouseFlags.Button1Released));
- }
- #endregion
- #region Thread Safety Tests
- [Fact]
- public void FakeInput_EnqueueMouseEvent_IsThreadSafe ()
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- ConcurrentBag<MouseEventArgs> receivedEvents = [];
- processor.MouseEvent += (_, e) => receivedEvents.Add (e);
- const int threadCount = 10;
- const int eventsPerThread = 100;
- Thread [] threads = new Thread [threadCount];
- // Act - Enqueue mouse events from multiple threads
- for (var t = 0; t < threadCount; t++)
- {
- int threadId = t;
- threads [t] = new (() =>
- {
- for (var i = 0; i < eventsPerThread; i++)
- {
- processor.EnqueueMouseEvent (
- new()
- {
- Position = new (threadId, i),
- Flags = MouseFlags.Button1Clicked
- });
- }
- });
- 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 * eventsPerThread, receivedEvents.Count);
- }
- #endregion
- #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);
- }
- }
- }
- #endregion
- #region FakeInputProcessor EnqueueMouseEvent Tests
- [Fact]
- public void FakeInput_EnqueueMouseEvent_AddsSingleMouseEventToQueue ()
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- List<MouseEventArgs> receivedEvents = [];
- processor.MouseEvent += (_, e) => receivedEvents.Add (e);
- MouseEventArgs mouseEvent = new ()
- {
- Position = new (10, 5),
- Flags = MouseFlags.Button1Clicked
- };
- // Act
- processor.EnqueueMouseEvent (mouseEvent);
- SimulateInputThread (fakeInput, queue);
- processor.ProcessQueue ();
- // Assert - Verify the mouse event made it through
- Assert.Single (receivedEvents);
- Assert.Equal (mouseEvent.Position, receivedEvents [0].Position);
- Assert.Equal (mouseEvent.Flags, receivedEvents [0].Flags);
- }
- [Fact]
- public void FakeInput_EnqueueMouseEvent_SupportsMultipleEvents ()
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- MouseEventArgs [] events =
- [
- new () { Position = new (10, 5), Flags = MouseFlags.Button1Pressed },
- new () { Position = new (10, 5), Flags = MouseFlags.Button1Released },
- new () { Position = new (15, 8), Flags = MouseFlags.ReportMousePosition },
- new () { Position = new (20, 10), Flags = MouseFlags.Button1Clicked }
- ];
- List<MouseEventArgs> receivedEvents = [];
- processor.MouseEvent += (_, e) => receivedEvents.Add (e);
- // Act
- foreach (MouseEventArgs mouseEvent in events)
- {
- processor.EnqueueMouseEvent (mouseEvent);
- }
- SimulateInputThread (fakeInput, queue);
- processor.ProcessQueue ();
- // Assert
- // The MouseInterpreter processes Button1Pressed followed by Button1Released and generates
- // an additional Button1Clicked event, so we expect 5 events total:
- // 1. Button1Pressed (original)
- // 2. Button1Released (original)
- // 3. Button1Clicked (generated by MouseInterpreter from press+release)
- // 4. ReportMousePosition (original)
- // 5. Button1Clicked (original)
- Assert.Equal (5, receivedEvents.Count);
- // Verify the original events are present
- Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Pressed && e.Position == new Point (10, 5));
- Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Released && e.Position == new Point (10, 5));
- Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.ReportMousePosition && e.Position == new Point (15, 8));
-
- // There should be two clicked events: one generated, one original
- var clickedEvents = receivedEvents.Where (e => e.Flags == MouseFlags.Button1Clicked).ToList ();
- Assert.Equal (2, clickedEvents.Count);
- Assert.Contains (clickedEvents, e => e.Position == new Point (10, 5)); // Generated from press+release
- Assert.Contains (clickedEvents, e => e.Position == new Point (20, 10)); // Original
- }
- [Theory]
- [InlineData (MouseFlags.Button1Clicked)]
- [InlineData (MouseFlags.Button2Clicked)]
- [InlineData (MouseFlags.Button3Clicked)]
- [InlineData (MouseFlags.Button4Clicked)]
- [InlineData (MouseFlags.Button1DoubleClicked)]
- [InlineData (MouseFlags.Button1TripleClicked)]
- public void FakeInput_EnqueueMouseEvent_SupportsAllButtonClicks (MouseFlags flags)
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- MouseEventArgs mouseEvent = new ()
- {
- Position = new (10, 5),
- Flags = flags
- };
- MouseEventArgs? receivedEvent = null;
- processor.MouseEvent += (_, e) => receivedEvent = e;
- // Act
- processor.EnqueueMouseEvent (mouseEvent);
- SimulateInputThread (fakeInput, queue);
- processor.ProcessQueue ();
- // Assert
- Assert.NotNull (receivedEvent);
- Assert.Equal (flags, receivedEvent.Flags);
- }
- [Theory]
- [InlineData (0, 0)]
- [InlineData (10, 5)]
- [InlineData (79, 24)] // Near screen edge (assuming 80x25)
- [InlineData (100, 100)] // Beyond typical screen
- public void FakeInput_EnqueueMouseEvent_PreservesPosition (int x, int y)
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- MouseEventArgs mouseEvent = new ()
- {
- Position = new (x, y),
- Flags = MouseFlags.Button1Clicked
- };
- MouseEventArgs? receivedEvent = null;
- processor.MouseEvent += (_, e) => receivedEvent = e;
- // Act
- processor.EnqueueMouseEvent (mouseEvent);
- SimulateInputThread (fakeInput, queue);
- processor.ProcessQueue ();
- // Assert
- Assert.NotNull (receivedEvent);
- Assert.Equal (x, receivedEvent.Position.X);
- Assert.Equal (y, receivedEvent.Position.Y);
- }
- [Theory]
- [InlineData (MouseFlags.ButtonShift)]
- [InlineData (MouseFlags.ButtonCtrl)]
- [InlineData (MouseFlags.ButtonAlt)]
- [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonCtrl)]
- [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonAlt)]
- [InlineData (MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt)]
- [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt)]
- public void FakeInput_EnqueueMouseEvent_PreservesModifiers (MouseFlags modifiers)
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- MouseEventArgs mouseEvent = new ()
- {
- Position = new (10, 5),
- Flags = MouseFlags.Button1Clicked | modifiers
- };
- MouseEventArgs? receivedEvent = null;
- processor.MouseEvent += (_, e) => receivedEvent = e;
- // Act
- processor.EnqueueMouseEvent (mouseEvent);
- SimulateInputThread (fakeInput, queue);
- processor.ProcessQueue ();
- // Assert
- Assert.NotNull (receivedEvent);
- Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.Button1Clicked));
- if (modifiers.HasFlag (MouseFlags.ButtonShift))
- {
- Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonShift));
- }
- if (modifiers.HasFlag (MouseFlags.ButtonCtrl))
- {
- Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonCtrl));
- }
- if (modifiers.HasFlag (MouseFlags.ButtonAlt))
- {
- Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonAlt));
- }
- }
- [Theory]
- [InlineData (MouseFlags.WheeledUp)]
- [InlineData (MouseFlags.WheeledDown)]
- [InlineData (MouseFlags.WheeledLeft)]
- [InlineData (MouseFlags.WheeledRight)]
- public void FakeInput_EnqueueMouseEvent_SupportsMouseWheel (MouseFlags wheelFlag)
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- MouseEventArgs mouseEvent = new ()
- {
- Position = new (10, 5),
- Flags = wheelFlag
- };
- MouseEventArgs? receivedEvent = null;
- processor.MouseEvent += (_, e) => receivedEvent = e;
- // Act
- processor.EnqueueMouseEvent (mouseEvent);
- SimulateInputThread (fakeInput, queue);
- processor.ProcessQueue ();
- // Assert
- Assert.NotNull (receivedEvent);
- Assert.True (receivedEvent.Flags.HasFlag (wheelFlag));
- }
- [Fact]
- public void FakeInput_EnqueueMouseEvent_SupportsMouseMove ()
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- List<MouseEventArgs> receivedEvents = [];
- processor.MouseEvent += (_, e) => receivedEvents.Add (e);
- MouseEventArgs [] events =
- [
- new () { Position = new (0, 0), Flags = MouseFlags.ReportMousePosition },
- new () { Position = new (5, 5), Flags = MouseFlags.ReportMousePosition },
- new () { Position = new (10, 10), Flags = MouseFlags.ReportMousePosition }
- ];
- // Act
- foreach (MouseEventArgs mouseEvent in events)
- {
- processor.EnqueueMouseEvent (mouseEvent);
- }
- SimulateInputThread (fakeInput, queue);
- processor.ProcessQueue ();
- // Assert
- Assert.Equal (3, receivedEvents.Count);
- Assert.Equal (new (0, 0), receivedEvents [0].Position);
- Assert.Equal (new (5, 5), receivedEvents [1].Position);
- Assert.Equal (new (10, 10), receivedEvents [2].Position);
- }
- #endregion
- #region InputProcessor Pipeline Tests
- [Fact]
- public void InputProcessor_EnqueueMouseEvent_DoesNotThrow ()
- {
- // Arrange
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- var processor = new FakeInputProcessor (queue);
- // Don't set InputImpl (or set to non-testable)
- // Act & Assert - Should not throw even if not implemented
- Exception? exception = Record.Exception (() =>
- {
- processor.EnqueueMouseEvent (
- new()
- {
- Position = new (10, 5),
- Flags = MouseFlags.Button1Clicked
- });
- processor.ProcessQueue ();
- });
- // The base implementation logs a critical message but doesn't throw
- Assert.Null (exception);
- }
- [Fact]
- public void InputProcessor_ProcessQueue_DrainsPendingMouseEvents ()
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- List<MouseEventArgs> receivedEvents = [];
- processor.MouseEvent += (_, e) => receivedEvents.Add (e);
- // Act - Enqueue multiple events before processing
- processor.EnqueueMouseEvent (new() { Position = new (1, 1), Flags = MouseFlags.Button1Pressed });
- processor.EnqueueMouseEvent (new() { Position = new (2, 2), Flags = MouseFlags.ReportMousePosition });
- processor.EnqueueMouseEvent (new() { Position = new (3, 3), Flags = MouseFlags.Button1Released });
- SimulateInputThread (fakeInput, queue);
- processor.ProcessQueue ();
- // Assert - After processing, all events should be received
- Assert.Empty (queue);
- Assert.Equal (3, receivedEvents.Count);
- }
- #endregion
- #region Error Handling Tests
- [Fact]
- public void FakeInput_EnqueueMouseEvent_WithInvalidEvent_DoesNotThrow ()
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- // Act & Assert - Empty/default mouse event should not throw
- Exception? exception = Record.Exception (() =>
- {
- processor.EnqueueMouseEvent (new ());
- SimulateInputThread (fakeInput, queue);
- processor.ProcessQueue ();
- });
- Assert.Null (exception);
- }
- [Fact]
- public void FakeInput_EnqueueMouseEvent_WithNegativePosition_DoesNotThrow ()
- {
- // Arrange
- var fakeInput = new FakeInput ();
- ConcurrentQueue<ConsoleKeyInfo> queue = new ();
- fakeInput.Initialize (queue);
- var processor = new FakeInputProcessor (queue);
- processor.InputImpl = fakeInput;
- // Act & Assert - Negative positions should not throw
- Exception? exception = Record.Exception (() =>
- {
- processor.EnqueueMouseEvent (
- new()
- {
- Position = new (-10, -5),
- Flags = MouseFlags.Button1Clicked
- });
- SimulateInputThread (fakeInput, queue);
- processor.ProcessQueue ();
- });
- Assert.Null (exception);
- }
- #endregion
- }
|