2
0

KeyboardImplThreadSafetyTests.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. // ReSharper disable AccessToDisposedClosure
  2. #nullable enable
  3. namespace ApplicationTests.Keyboard;
  4. /// <summary>
  5. /// Tests to verify that KeyboardImpl is thread-safe for concurrent access scenarios.
  6. /// </summary>
  7. public class KeyboardImplThreadSafetyTests
  8. {
  9. [Fact]
  10. public void AddCommand_ConcurrentAccess_NoExceptions ()
  11. {
  12. // Arrange
  13. var keyboard = new KeyboardImpl ();
  14. List<Exception> exceptions = [];
  15. const int NUM_THREADS = 10;
  16. const int OPERATIONS_PER_THREAD = 50;
  17. // Act
  18. List<Task> tasks = [];
  19. for (var i = 0; i < NUM_THREADS; i++)
  20. {
  21. tasks.Add (
  22. Task.Run (() =>
  23. {
  24. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  25. {
  26. try
  27. {
  28. // AddKeyBindings internally calls AddCommand multiple times
  29. keyboard.AddKeyBindings ();
  30. }
  31. catch (InvalidOperationException)
  32. {
  33. // Expected - AddKeyBindings tries to add keys that already exist
  34. }
  35. catch (Exception ex)
  36. {
  37. exceptions.Add (ex);
  38. }
  39. }
  40. }));
  41. }
  42. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  43. Task.WaitAll (tasks.ToArray ());
  44. #pragma warning restore xUnit1031
  45. // Assert
  46. Assert.Empty (exceptions);
  47. keyboard.Dispose ();
  48. }
  49. [Fact]
  50. public void Dispose_WhileOperationsInProgress_NoExceptions ()
  51. {
  52. // Arrange
  53. IApplication? app = Application.Create ();
  54. app.Init ("fake");
  55. var keyboard = new KeyboardImpl { App = app };
  56. keyboard.AddKeyBindings ();
  57. List<Exception> exceptions = [];
  58. var continueRunning = true;
  59. // Act
  60. Task operationsTask = Task.Run (() =>
  61. {
  62. while (continueRunning)
  63. {
  64. try
  65. {
  66. keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl);
  67. IEnumerable<KeyValuePair<Key, KeyBinding>> bindings = keyboard.KeyBindings.GetBindings ();
  68. int count = bindings.Count ();
  69. }
  70. catch (ObjectDisposedException)
  71. {
  72. // Expected - keyboard was disposed
  73. break;
  74. }
  75. catch (Exception ex)
  76. {
  77. exceptions.Add (ex);
  78. break;
  79. }
  80. }
  81. });
  82. // Give operations a chance to start
  83. Thread.Sleep (10);
  84. // Dispose while operations are running
  85. keyboard.Dispose ();
  86. continueRunning = false;
  87. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  88. operationsTask.Wait (TimeSpan.FromSeconds (2));
  89. #pragma warning restore xUnit1031
  90. // Assert
  91. Assert.Empty (exceptions);
  92. app.Dispose ();
  93. }
  94. [Fact]
  95. public void InvokeCommand_ConcurrentAccess_NoExceptions ()
  96. {
  97. // Arrange
  98. IApplication? app = Application.Create ();
  99. app.Init ("fake");
  100. var keyboard = new KeyboardImpl { App = app };
  101. keyboard.AddKeyBindings ();
  102. List<Exception> exceptions = [];
  103. const int NUM_THREADS = 10;
  104. const int OPERATIONS_PER_THREAD = 50;
  105. // Act
  106. List<Task> tasks = new ();
  107. for (var i = 0; i < NUM_THREADS; i++)
  108. {
  109. tasks.Add (
  110. Task.Run (() =>
  111. {
  112. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  113. {
  114. try
  115. {
  116. var binding = new KeyBinding ([Command.Quit]);
  117. keyboard.InvokeCommand (Command.Quit, Key.Q.WithCtrl, binding);
  118. }
  119. catch (Exception ex)
  120. {
  121. exceptions.Add (ex);
  122. }
  123. }
  124. }));
  125. }
  126. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  127. Task.WaitAll (tasks.ToArray ());
  128. #pragma warning restore xUnit1031
  129. // Assert
  130. Assert.Empty (exceptions);
  131. keyboard.Dispose ();
  132. app.Dispose ();
  133. }
  134. [Fact]
  135. public void InvokeCommandsBoundToKey_ConcurrentAccess_NoExceptions ()
  136. {
  137. // Arrange
  138. IApplication? app = Application.Create ();
  139. app.Init ("fake");
  140. var keyboard = new KeyboardImpl { App = app };
  141. keyboard.AddKeyBindings ();
  142. List<Exception> exceptions = [];
  143. const int NUM_THREADS = 10;
  144. const int OPERATIONS_PER_THREAD = 50;
  145. // Act
  146. List<Task> tasks = [];
  147. for (var i = 0; i < NUM_THREADS; i++)
  148. {
  149. tasks.Add (
  150. Task.Run (() =>
  151. {
  152. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  153. {
  154. try
  155. {
  156. keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl);
  157. }
  158. catch (Exception ex)
  159. {
  160. exceptions.Add (ex);
  161. }
  162. }
  163. }));
  164. }
  165. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  166. Task.WaitAll (tasks.ToArray ());
  167. #pragma warning restore xUnit1031
  168. // Assert
  169. Assert.Empty (exceptions);
  170. keyboard.Dispose ();
  171. app.Dispose ();
  172. }
  173. [Fact]
  174. public void KeyBindings_ConcurrentAdd_NoExceptions ()
  175. {
  176. // Arrange
  177. var keyboard = new KeyboardImpl ();
  178. // Don't call AddKeyBindings here to avoid conflicts
  179. List<Exception> exceptions = [];
  180. const int NUM_THREADS = 10;
  181. const int OPERATIONS_PER_THREAD = 50;
  182. // Act
  183. List<Task> tasks = new ();
  184. for (var i = 0; i < NUM_THREADS; i++)
  185. {
  186. int threadId = i;
  187. tasks.Add (
  188. Task.Run (() =>
  189. {
  190. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  191. {
  192. try
  193. {
  194. // Use unique keys per thread to avoid conflicts
  195. Key key = Key.F1 + threadId * OPERATIONS_PER_THREAD + j;
  196. keyboard.KeyBindings.Add (key, Command.Refresh);
  197. }
  198. catch (InvalidOperationException)
  199. {
  200. // Expected - duplicate key
  201. }
  202. catch (ArgumentException)
  203. {
  204. // Expected - invalid key
  205. }
  206. catch (Exception ex)
  207. {
  208. exceptions.Add (ex);
  209. }
  210. }
  211. }));
  212. }
  213. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  214. Task.WaitAll (tasks.ToArray ());
  215. #pragma warning restore xUnit1031
  216. // Assert
  217. Assert.Empty (exceptions);
  218. keyboard.Dispose ();
  219. }
  220. [Fact]
  221. public void KeyDown_KeyUp_Events_ConcurrentSubscription_NoExceptions ()
  222. {
  223. // Arrange
  224. var keyboard = new KeyboardImpl ();
  225. keyboard.AddKeyBindings ();
  226. List<Exception> exceptions = [];
  227. const int NUM_THREADS = 10;
  228. const int OPERATIONS_PER_THREAD = 20;
  229. var keyDownCount = 0;
  230. var keyUpCount = 0;
  231. // Act
  232. List<Task> tasks = new ();
  233. // Threads subscribing to events
  234. for (var i = 0; i < NUM_THREADS; i++)
  235. {
  236. tasks.Add (
  237. Task.Run (() =>
  238. {
  239. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  240. {
  241. try
  242. {
  243. EventHandler<Key> handler = (s, e) => { Interlocked.Increment (ref keyDownCount); };
  244. keyboard.KeyDown += handler;
  245. keyboard.KeyDown -= handler;
  246. EventHandler<Key> upHandler = (s, e) => { Interlocked.Increment (ref keyUpCount); };
  247. keyboard.KeyUp += upHandler;
  248. keyboard.KeyUp -= upHandler;
  249. }
  250. catch (Exception ex)
  251. {
  252. exceptions.Add (ex);
  253. }
  254. }
  255. }));
  256. }
  257. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  258. Task.WaitAll (tasks.ToArray ());
  259. #pragma warning restore xUnit1031
  260. // Assert
  261. Assert.Empty (exceptions);
  262. keyboard.Dispose ();
  263. }
  264. [Fact]
  265. public void KeyProperty_Setters_ConcurrentAccess_NoExceptions ()
  266. {
  267. // Arrange
  268. var keyboard = new KeyboardImpl ();
  269. // Initialize once before concurrent access
  270. keyboard.AddKeyBindings ();
  271. List<Exception> exceptions = [];
  272. const int NUM_THREADS = 10;
  273. const int OPERATIONS_PER_THREAD = 20;
  274. // Act
  275. List<Task> tasks = new ();
  276. for (var i = 0; i < NUM_THREADS; i++)
  277. {
  278. int threadId = i;
  279. tasks.Add (
  280. Task.Run (() =>
  281. {
  282. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  283. {
  284. try
  285. {
  286. // Cycle through different key combinations
  287. switch (j % 6)
  288. {
  289. case 0:
  290. keyboard.QuitKey = Key.Q.WithCtrl;
  291. break;
  292. case 1:
  293. keyboard.ArrangeKey = Key.F6.WithCtrl;
  294. break;
  295. case 2:
  296. keyboard.NextTabKey = Key.Tab;
  297. break;
  298. case 3:
  299. keyboard.PrevTabKey = Key.Tab.WithShift;
  300. break;
  301. case 4:
  302. keyboard.NextTabGroupKey = Key.F6;
  303. break;
  304. case 5:
  305. keyboard.PrevTabGroupKey = Key.F6.WithShift;
  306. break;
  307. }
  308. }
  309. catch (Exception ex)
  310. {
  311. exceptions.Add (ex);
  312. }
  313. }
  314. }));
  315. }
  316. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  317. Task.WaitAll (tasks.ToArray ());
  318. #pragma warning restore xUnit1031
  319. // Assert
  320. Assert.Empty (exceptions);
  321. keyboard.Dispose ();
  322. }
  323. [Fact]
  324. public void MixedOperations_ConcurrentAccess_NoExceptions ()
  325. {
  326. // Arrange
  327. IApplication? app = Application.Create ();
  328. app.Init ("fake");
  329. var keyboard = new KeyboardImpl { App = app };
  330. keyboard.AddKeyBindings ();
  331. List<Exception> exceptions = [];
  332. const int OPERATIONS_PER_THREAD = 30;
  333. // Act
  334. List<Task> tasks = new ();
  335. // Thread 1: Add bindings with unique keys
  336. tasks.Add (
  337. Task.Run (() =>
  338. {
  339. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  340. {
  341. try
  342. {
  343. // Use high key codes to avoid conflicts
  344. var key = new Key ((KeyCode)((int)KeyCode.F20 + j));
  345. keyboard.KeyBindings.Add (key, Command.Refresh);
  346. }
  347. catch (InvalidOperationException)
  348. {
  349. // Expected - duplicate
  350. }
  351. catch (ArgumentException)
  352. {
  353. // Expected - invalid key
  354. }
  355. catch (Exception ex)
  356. {
  357. exceptions.Add (ex);
  358. }
  359. }
  360. }));
  361. // Thread 2: Invoke commands
  362. tasks.Add (
  363. Task.Run (() =>
  364. {
  365. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  366. {
  367. try
  368. {
  369. keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl);
  370. }
  371. catch (Exception ex)
  372. {
  373. exceptions.Add (ex);
  374. }
  375. }
  376. }));
  377. // Thread 3: Read bindings
  378. tasks.Add (
  379. Task.Run (() =>
  380. {
  381. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  382. {
  383. try
  384. {
  385. IEnumerable<KeyValuePair<Key, KeyBinding>> bindings = keyboard.KeyBindings.GetBindings ();
  386. int count = bindings.Count ();
  387. Assert.True (count >= 0);
  388. }
  389. catch (Exception ex)
  390. {
  391. exceptions.Add (ex);
  392. }
  393. }
  394. }));
  395. // Thread 4: Change key properties
  396. tasks.Add (
  397. Task.Run (() =>
  398. {
  399. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  400. {
  401. try
  402. {
  403. keyboard.QuitKey = j % 2 == 0 ? Key.Q.WithCtrl : Key.Esc;
  404. }
  405. catch (Exception ex)
  406. {
  407. exceptions.Add (ex);
  408. }
  409. }
  410. }));
  411. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  412. Task.WaitAll (tasks.ToArray ());
  413. #pragma warning restore xUnit1031
  414. // Assert
  415. Assert.Empty (exceptions);
  416. keyboard.Dispose ();
  417. app.Dispose ();
  418. }
  419. [Fact]
  420. public void RaiseKeyDownEvent_ConcurrentAccess_NoExceptions ()
  421. {
  422. // Arrange
  423. IApplication? app = Application.Create ();
  424. app.Init ("fake");
  425. var keyboard = new KeyboardImpl { App = app };
  426. keyboard.AddKeyBindings ();
  427. List<Exception> exceptions = [];
  428. const int NUM_THREADS = 5;
  429. const int OPERATIONS_PER_THREAD = 20;
  430. // Act
  431. List<Task> tasks = new ();
  432. for (var i = 0; i < NUM_THREADS; i++)
  433. {
  434. tasks.Add (
  435. Task.Run (() =>
  436. {
  437. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  438. {
  439. try
  440. {
  441. keyboard.RaiseKeyDownEvent (Key.A);
  442. }
  443. catch (Exception ex)
  444. {
  445. exceptions.Add (ex);
  446. }
  447. }
  448. }));
  449. }
  450. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  451. Task.WaitAll (tasks.ToArray ());
  452. #pragma warning restore xUnit1031
  453. // Assert
  454. Assert.Empty (exceptions);
  455. keyboard.Dispose ();
  456. app.Dispose ();
  457. }
  458. }