EnqueueMouseEventTests.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using Xunit.Abstractions;
  4. namespace UnitTests_Parallelizable.DriverTests;
  5. /// <summary>
  6. /// Parallelizable unit tests for IInputProcessor.EnqueueMouseEvent.
  7. /// Tests validate the entire pipeline: MouseEventArgs → TInputRecord → Queue → ProcessQueue → Events.
  8. /// fully implemented in InputProcessorImpl (base class). Only WindowsInputProcessor has a working implementation.
  9. /// </summary>
  10. [Trait ("Category", "Input")]
  11. public class EnqueueMouseEventTests (ITestOutputHelper output)
  12. {
  13. private readonly ITestOutputHelper _output = output;
  14. #region Mouse Event Sequencing Tests
  15. [Fact]
  16. public void FakeInput_EnqueueMouseEvent_HandlesCompleteClickSequence ()
  17. {
  18. // Arrange
  19. var fakeInput = new FakeInput ();
  20. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  21. fakeInput.Initialize (queue);
  22. var processor = new FakeInputProcessor (queue);
  23. processor.InputImpl = fakeInput;
  24. List<MouseEventArgs> receivedEvents = [];
  25. processor.MouseEvent += (_, e) => receivedEvents.Add (e);
  26. // Act - Simulate a complete click: press → release → click
  27. processor.EnqueueMouseEvent (
  28. new()
  29. {
  30. Position = new (10, 5),
  31. Flags = MouseFlags.Button1Pressed
  32. });
  33. processor.EnqueueMouseEvent (
  34. new()
  35. {
  36. Position = new (10, 5),
  37. Flags = MouseFlags.Button1Released
  38. });
  39. // The MouseInterpreter in the processor should generate a clicked event
  40. SimulateInputThread (fakeInput, queue);
  41. processor.ProcessQueue ();
  42. // Assert
  43. // We should see at least the pressed and released events
  44. Assert.True (receivedEvents.Count >= 2);
  45. Assert.Contains (receivedEvents, e => e.Flags.HasFlag (MouseFlags.Button1Pressed));
  46. Assert.Contains (receivedEvents, e => e.Flags.HasFlag (MouseFlags.Button1Released));
  47. }
  48. #endregion
  49. #region Thread Safety Tests
  50. [Fact]
  51. public void FakeInput_EnqueueMouseEvent_IsThreadSafe ()
  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. ConcurrentBag<MouseEventArgs> receivedEvents = [];
  60. processor.MouseEvent += (_, e) => receivedEvents.Add (e);
  61. const int threadCount = 10;
  62. const int eventsPerThread = 100;
  63. Thread [] threads = new Thread [threadCount];
  64. // Act - Enqueue mouse events from multiple threads
  65. for (var t = 0; t < threadCount; t++)
  66. {
  67. int threadId = t;
  68. threads [t] = new (() =>
  69. {
  70. for (var i = 0; i < eventsPerThread; i++)
  71. {
  72. processor.EnqueueMouseEvent (
  73. new()
  74. {
  75. Position = new (threadId, i),
  76. Flags = MouseFlags.Button1Clicked
  77. });
  78. }
  79. });
  80. threads [t].Start ();
  81. }
  82. // Wait for all threads to complete
  83. foreach (Thread thread in threads)
  84. {
  85. thread.Join ();
  86. }
  87. SimulateInputThread (fakeInput, queue);
  88. processor.ProcessQueue ();
  89. // Assert
  90. Assert.Equal (threadCount * eventsPerThread, receivedEvents.Count);
  91. }
  92. #endregion
  93. #region Helper Methods
  94. /// <summary>
  95. /// Simulates the input thread by manually draining FakeInput's internal queue
  96. /// and moving items to the InputBuffer. This is needed because tests don't
  97. /// start the actual input thread via Run().
  98. /// </summary>
  99. private static void SimulateInputThread (FakeInput fakeInput, ConcurrentQueue<ConsoleKeyInfo> inputBuffer)
  100. {
  101. // FakeInput's Peek() checks _testInput
  102. while (fakeInput.Peek ())
  103. {
  104. // Read() drains _testInput and returns items
  105. foreach (ConsoleKeyInfo item in fakeInput.Read ())
  106. {
  107. // Manually add to InputBuffer (simulating what Run() would do)
  108. inputBuffer.Enqueue (item);
  109. }
  110. }
  111. }
  112. #endregion
  113. #region FakeInputProcessor EnqueueMouseEvent Tests
  114. [Fact]
  115. public void FakeInput_EnqueueMouseEvent_AddsSingleMouseEventToQueue ()
  116. {
  117. // Arrange
  118. var fakeInput = new FakeInput ();
  119. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  120. fakeInput.Initialize (queue);
  121. var processor = new FakeInputProcessor (queue);
  122. processor.InputImpl = fakeInput;
  123. List<MouseEventArgs> receivedEvents = [];
  124. processor.MouseEvent += (_, e) => receivedEvents.Add (e);
  125. MouseEventArgs mouseEvent = new ()
  126. {
  127. Position = new (10, 5),
  128. Flags = MouseFlags.Button1Clicked
  129. };
  130. // Act
  131. processor.EnqueueMouseEvent (mouseEvent);
  132. SimulateInputThread (fakeInput, queue);
  133. processor.ProcessQueue ();
  134. // Assert - Verify the mouse event made it through
  135. Assert.Single (receivedEvents);
  136. Assert.Equal (mouseEvent.Position, receivedEvents [0].Position);
  137. Assert.Equal (mouseEvent.Flags, receivedEvents [0].Flags);
  138. }
  139. [Fact]
  140. public void FakeInput_EnqueueMouseEvent_SupportsMultipleEvents ()
  141. {
  142. // Arrange
  143. var fakeInput = new FakeInput ();
  144. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  145. fakeInput.Initialize (queue);
  146. var processor = new FakeInputProcessor (queue);
  147. processor.InputImpl = fakeInput;
  148. MouseEventArgs [] events =
  149. [
  150. new () { Position = new (10, 5), Flags = MouseFlags.Button1Pressed },
  151. new () { Position = new (10, 5), Flags = MouseFlags.Button1Released },
  152. new () { Position = new (15, 8), Flags = MouseFlags.ReportMousePosition },
  153. new () { Position = new (20, 10), Flags = MouseFlags.Button1Clicked }
  154. ];
  155. List<MouseEventArgs> receivedEvents = [];
  156. processor.MouseEvent += (_, e) => receivedEvents.Add (e);
  157. // Act
  158. foreach (MouseEventArgs mouseEvent in events)
  159. {
  160. processor.EnqueueMouseEvent (mouseEvent);
  161. }
  162. SimulateInputThread (fakeInput, queue);
  163. processor.ProcessQueue ();
  164. // Assert
  165. // The MouseInterpreter processes Button1Pressed followed by Button1Released and generates
  166. // an additional Button1Clicked event, so we expect 5 events total:
  167. // 1. Button1Pressed (original)
  168. // 2. Button1Released (original)
  169. // 3. Button1Clicked (generated by MouseInterpreter from press+release)
  170. // 4. ReportMousePosition (original)
  171. // 5. Button1Clicked (original)
  172. Assert.Equal (5, receivedEvents.Count);
  173. // Verify the original events are present
  174. Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Pressed && e.Position == new Point (10, 5));
  175. Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Released && e.Position == new Point (10, 5));
  176. Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.ReportMousePosition && e.Position == new Point (15, 8));
  177. // There should be two clicked events: one generated, one original
  178. var clickedEvents = receivedEvents.Where (e => e.Flags == MouseFlags.Button1Clicked).ToList ();
  179. Assert.Equal (2, clickedEvents.Count);
  180. Assert.Contains (clickedEvents, e => e.Position == new Point (10, 5)); // Generated from press+release
  181. Assert.Contains (clickedEvents, e => e.Position == new Point (20, 10)); // Original
  182. }
  183. [Theory]
  184. [InlineData (MouseFlags.Button1Clicked)]
  185. [InlineData (MouseFlags.Button2Clicked)]
  186. [InlineData (MouseFlags.Button3Clicked)]
  187. [InlineData (MouseFlags.Button4Clicked)]
  188. [InlineData (MouseFlags.Button1DoubleClicked)]
  189. [InlineData (MouseFlags.Button1TripleClicked)]
  190. public void FakeInput_EnqueueMouseEvent_SupportsAllButtonClicks (MouseFlags flags)
  191. {
  192. // Arrange
  193. var fakeInput = new FakeInput ();
  194. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  195. fakeInput.Initialize (queue);
  196. var processor = new FakeInputProcessor (queue);
  197. processor.InputImpl = fakeInput;
  198. MouseEventArgs mouseEvent = new ()
  199. {
  200. Position = new (10, 5),
  201. Flags = flags
  202. };
  203. MouseEventArgs? receivedEvent = null;
  204. processor.MouseEvent += (_, e) => receivedEvent = e;
  205. // Act
  206. processor.EnqueueMouseEvent (mouseEvent);
  207. SimulateInputThread (fakeInput, queue);
  208. processor.ProcessQueue ();
  209. // Assert
  210. Assert.NotNull (receivedEvent);
  211. Assert.Equal (flags, receivedEvent.Flags);
  212. }
  213. [Theory]
  214. [InlineData (0, 0)]
  215. [InlineData (10, 5)]
  216. [InlineData (79, 24)] // Near screen edge (assuming 80x25)
  217. [InlineData (100, 100)] // Beyond typical screen
  218. public void FakeInput_EnqueueMouseEvent_PreservesPosition (int x, int y)
  219. {
  220. // Arrange
  221. var fakeInput = new FakeInput ();
  222. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  223. fakeInput.Initialize (queue);
  224. var processor = new FakeInputProcessor (queue);
  225. processor.InputImpl = fakeInput;
  226. MouseEventArgs mouseEvent = new ()
  227. {
  228. Position = new (x, y),
  229. Flags = MouseFlags.Button1Clicked
  230. };
  231. MouseEventArgs? receivedEvent = null;
  232. processor.MouseEvent += (_, e) => receivedEvent = e;
  233. // Act
  234. processor.EnqueueMouseEvent (mouseEvent);
  235. SimulateInputThread (fakeInput, queue);
  236. processor.ProcessQueue ();
  237. // Assert
  238. Assert.NotNull (receivedEvent);
  239. Assert.Equal (x, receivedEvent.Position.X);
  240. Assert.Equal (y, receivedEvent.Position.Y);
  241. }
  242. [Theory]
  243. [InlineData (MouseFlags.ButtonShift)]
  244. [InlineData (MouseFlags.ButtonCtrl)]
  245. [InlineData (MouseFlags.ButtonAlt)]
  246. [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonCtrl)]
  247. [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonAlt)]
  248. [InlineData (MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt)]
  249. [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt)]
  250. public void FakeInput_EnqueueMouseEvent_PreservesModifiers (MouseFlags modifiers)
  251. {
  252. // Arrange
  253. var fakeInput = new FakeInput ();
  254. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  255. fakeInput.Initialize (queue);
  256. var processor = new FakeInputProcessor (queue);
  257. processor.InputImpl = fakeInput;
  258. MouseEventArgs mouseEvent = new ()
  259. {
  260. Position = new (10, 5),
  261. Flags = MouseFlags.Button1Clicked | modifiers
  262. };
  263. MouseEventArgs? receivedEvent = null;
  264. processor.MouseEvent += (_, e) => receivedEvent = e;
  265. // Act
  266. processor.EnqueueMouseEvent (mouseEvent);
  267. SimulateInputThread (fakeInput, queue);
  268. processor.ProcessQueue ();
  269. // Assert
  270. Assert.NotNull (receivedEvent);
  271. Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.Button1Clicked));
  272. if (modifiers.HasFlag (MouseFlags.ButtonShift))
  273. {
  274. Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonShift));
  275. }
  276. if (modifiers.HasFlag (MouseFlags.ButtonCtrl))
  277. {
  278. Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonCtrl));
  279. }
  280. if (modifiers.HasFlag (MouseFlags.ButtonAlt))
  281. {
  282. Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonAlt));
  283. }
  284. }
  285. [Theory]
  286. [InlineData (MouseFlags.WheeledUp)]
  287. [InlineData (MouseFlags.WheeledDown)]
  288. [InlineData (MouseFlags.WheeledLeft)]
  289. [InlineData (MouseFlags.WheeledRight)]
  290. public void FakeInput_EnqueueMouseEvent_SupportsMouseWheel (MouseFlags wheelFlag)
  291. {
  292. // Arrange
  293. var fakeInput = new FakeInput ();
  294. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  295. fakeInput.Initialize (queue);
  296. var processor = new FakeInputProcessor (queue);
  297. processor.InputImpl = fakeInput;
  298. MouseEventArgs mouseEvent = new ()
  299. {
  300. Position = new (10, 5),
  301. Flags = wheelFlag
  302. };
  303. MouseEventArgs? receivedEvent = null;
  304. processor.MouseEvent += (_, e) => receivedEvent = e;
  305. // Act
  306. processor.EnqueueMouseEvent (mouseEvent);
  307. SimulateInputThread (fakeInput, queue);
  308. processor.ProcessQueue ();
  309. // Assert
  310. Assert.NotNull (receivedEvent);
  311. Assert.True (receivedEvent.Flags.HasFlag (wheelFlag));
  312. }
  313. [Fact]
  314. public void FakeInput_EnqueueMouseEvent_SupportsMouseMove ()
  315. {
  316. // Arrange
  317. var fakeInput = new FakeInput ();
  318. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  319. fakeInput.Initialize (queue);
  320. var processor = new FakeInputProcessor (queue);
  321. processor.InputImpl = fakeInput;
  322. List<MouseEventArgs> receivedEvents = [];
  323. processor.MouseEvent += (_, e) => receivedEvents.Add (e);
  324. MouseEventArgs [] events =
  325. [
  326. new () { Position = new (0, 0), Flags = MouseFlags.ReportMousePosition },
  327. new () { Position = new (5, 5), Flags = MouseFlags.ReportMousePosition },
  328. new () { Position = new (10, 10), Flags = MouseFlags.ReportMousePosition }
  329. ];
  330. // Act
  331. foreach (MouseEventArgs mouseEvent in events)
  332. {
  333. processor.EnqueueMouseEvent (mouseEvent);
  334. }
  335. SimulateInputThread (fakeInput, queue);
  336. processor.ProcessQueue ();
  337. // Assert
  338. Assert.Equal (3, receivedEvents.Count);
  339. Assert.Equal (new (0, 0), receivedEvents [0].Position);
  340. Assert.Equal (new (5, 5), receivedEvents [1].Position);
  341. Assert.Equal (new (10, 10), receivedEvents [2].Position);
  342. }
  343. #endregion
  344. #region InputProcessor Pipeline Tests
  345. [Fact]
  346. public void InputProcessor_EnqueueMouseEvent_DoesNotThrow ()
  347. {
  348. // Arrange
  349. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  350. var processor = new FakeInputProcessor (queue);
  351. // Don't set InputImpl (or set to non-testable)
  352. // Act & Assert - Should not throw even if not implemented
  353. Exception? exception = Record.Exception (() =>
  354. {
  355. processor.EnqueueMouseEvent (
  356. new()
  357. {
  358. Position = new (10, 5),
  359. Flags = MouseFlags.Button1Clicked
  360. });
  361. processor.ProcessQueue ();
  362. });
  363. // The base implementation logs a critical message but doesn't throw
  364. Assert.Null (exception);
  365. }
  366. [Fact]
  367. public void InputProcessor_ProcessQueue_DrainsPendingMouseEvents ()
  368. {
  369. // Arrange
  370. var fakeInput = new FakeInput ();
  371. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  372. fakeInput.Initialize (queue);
  373. var processor = new FakeInputProcessor (queue);
  374. processor.InputImpl = fakeInput;
  375. List<MouseEventArgs> receivedEvents = [];
  376. processor.MouseEvent += (_, e) => receivedEvents.Add (e);
  377. // Act - Enqueue multiple events before processing
  378. processor.EnqueueMouseEvent (new() { Position = new (1, 1), Flags = MouseFlags.Button1Pressed });
  379. processor.EnqueueMouseEvent (new() { Position = new (2, 2), Flags = MouseFlags.ReportMousePosition });
  380. processor.EnqueueMouseEvent (new() { Position = new (3, 3), Flags = MouseFlags.Button1Released });
  381. SimulateInputThread (fakeInput, queue);
  382. processor.ProcessQueue ();
  383. // Assert - After processing, all events should be received
  384. Assert.Empty (queue);
  385. Assert.Equal (3, receivedEvents.Count);
  386. }
  387. #endregion
  388. #region Error Handling Tests
  389. [Fact]
  390. public void FakeInput_EnqueueMouseEvent_WithInvalidEvent_DoesNotThrow ()
  391. {
  392. // Arrange
  393. var fakeInput = new FakeInput ();
  394. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  395. fakeInput.Initialize (queue);
  396. var processor = new FakeInputProcessor (queue);
  397. processor.InputImpl = fakeInput;
  398. // Act & Assert - Empty/default mouse event should not throw
  399. Exception? exception = Record.Exception (() =>
  400. {
  401. processor.EnqueueMouseEvent (new ());
  402. SimulateInputThread (fakeInput, queue);
  403. processor.ProcessQueue ();
  404. });
  405. Assert.Null (exception);
  406. }
  407. [Fact]
  408. public void FakeInput_EnqueueMouseEvent_WithNegativePosition_DoesNotThrow ()
  409. {
  410. // Arrange
  411. var fakeInput = new FakeInput ();
  412. ConcurrentQueue<ConsoleKeyInfo> queue = new ();
  413. fakeInput.Initialize (queue);
  414. var processor = new FakeInputProcessor (queue);
  415. processor.InputImpl = fakeInput;
  416. // Act & Assert - Negative positions should not throw
  417. Exception? exception = Record.Exception (() =>
  418. {
  419. processor.EnqueueMouseEvent (
  420. new()
  421. {
  422. Position = new (-10, -5),
  423. Flags = MouseFlags.Button1Clicked
  424. });
  425. SimulateInputThread (fakeInput, queue);
  426. processor.ProcessQueue ();
  427. });
  428. Assert.Null (exception);
  429. }
  430. #endregion
  431. }