EnqueueMouseEventTests.cs 19 KB

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