InputBindingsThreadSafetyTests.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. namespace InputTests;
  2. /// <summary>
  3. /// Tests to verify that InputBindings (KeyBindings and MouseBindings) are thread-safe
  4. /// for concurrent access scenarios.
  5. /// </summary>
  6. public class InputBindingsThreadSafetyTests
  7. {
  8. [Fact]
  9. public void Add_ConcurrentAccess_NoExceptions ()
  10. {
  11. // Arrange
  12. var bindings = new TestInputBindings ();
  13. const int NUM_THREADS = 10;
  14. const int ITEMS_PER_THREAD = 100;
  15. // Act
  16. Parallel.For (
  17. 0,
  18. NUM_THREADS,
  19. i =>
  20. {
  21. for (var j = 0; j < ITEMS_PER_THREAD; j++)
  22. {
  23. var key = $"key_{i}_{j}";
  24. try
  25. {
  26. bindings.Add (key, Command.Accept);
  27. }
  28. catch (InvalidOperationException)
  29. {
  30. // Expected if duplicate key - this is OK
  31. }
  32. }
  33. });
  34. // Assert
  35. IEnumerable<KeyValuePair<string, KeyBinding>> allBindings = bindings.GetBindings ();
  36. Assert.NotEmpty (allBindings);
  37. Assert.True (allBindings.Count () <= NUM_THREADS * ITEMS_PER_THREAD);
  38. }
  39. [Fact]
  40. public void Clear_ConcurrentAccess_NoExceptions ()
  41. {
  42. // Arrange
  43. var bindings = new TestInputBindings ();
  44. const int NUM_THREADS = 10;
  45. // Populate initial data
  46. for (var i = 0; i < 100; i++)
  47. {
  48. bindings.Add ($"key_{i}", Command.Accept);
  49. }
  50. // Act - Multiple threads clearing simultaneously
  51. Parallel.For (
  52. 0,
  53. NUM_THREADS,
  54. i =>
  55. {
  56. try
  57. {
  58. bindings.Clear ();
  59. }
  60. catch (Exception ex)
  61. {
  62. Assert.Fail ($"Clear should not throw: {ex.Message}");
  63. }
  64. });
  65. // Assert
  66. Assert.Empty (bindings.GetBindings ());
  67. }
  68. [Fact]
  69. public void GetAllFromCommands_DuringModification_NoExceptions ()
  70. {
  71. // Arrange
  72. var bindings = new TestInputBindings ();
  73. var continueRunning = true;
  74. List<Exception> exceptions = new ();
  75. const int MAX_ADDITIONS = 200; // Limit total additions to prevent infinite loop
  76. // Populate initial data
  77. for (var i = 0; i < 50; i++)
  78. {
  79. bindings.Add ($"key_{i}", Command.Accept);
  80. }
  81. // Act - Modifier thread
  82. Task modifierTask = Task.Run (() =>
  83. {
  84. var counter = 50;
  85. while (continueRunning && counter < MAX_ADDITIONS)
  86. {
  87. try
  88. {
  89. bindings.Add ($"key_{counter++}", Command.Accept);
  90. Thread.Sleep (1); // Small delay to prevent CPU spinning
  91. }
  92. catch (InvalidOperationException)
  93. {
  94. // Expected
  95. }
  96. }
  97. });
  98. // Act - Reader threads
  99. List<Task> readerTasks = new ();
  100. for (var i = 0; i < 5; i++)
  101. {
  102. readerTasks.Add (
  103. Task.Run (() =>
  104. {
  105. for (var j = 0; j < 50; j++)
  106. {
  107. try
  108. {
  109. IEnumerable<string> results = bindings.GetAllFromCommands (Command.Accept);
  110. int count = results.Count ();
  111. Assert.True (count >= 0);
  112. }
  113. catch (Exception ex)
  114. {
  115. exceptions.Add (ex);
  116. }
  117. Thread.Sleep (1); // Small delay between iterations
  118. }
  119. }));
  120. }
  121. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  122. Task.WaitAll (readerTasks.ToArray ());
  123. continueRunning = false;
  124. modifierTask.Wait (TimeSpan.FromSeconds (5)); // Add timeout to prevent indefinite hang
  125. #pragma warning restore xUnit1031
  126. // Assert
  127. Assert.Empty (exceptions);
  128. }
  129. [Fact]
  130. public void GetBindings_DuringConcurrentModification_NoExceptions ()
  131. {
  132. // Arrange
  133. var bindings = new TestInputBindings ();
  134. var continueRunning = true;
  135. List<Exception> exceptions = new ();
  136. const int MAX_MODIFICATIONS = 200; // Limit total modifications
  137. // Populate some initial data
  138. for (var i = 0; i < 50; i++)
  139. {
  140. bindings.Add ($"initial_{i}", Command.Accept);
  141. }
  142. // Act - Start modifier thread
  143. Task modifierTask = Task.Run (() =>
  144. {
  145. var counter = 0;
  146. while (continueRunning && counter < MAX_MODIFICATIONS)
  147. {
  148. try
  149. {
  150. bindings.Add ($"key_{counter++}", Command.Cancel);
  151. }
  152. catch (InvalidOperationException)
  153. {
  154. // Expected - duplicate key
  155. }
  156. catch (Exception ex)
  157. {
  158. exceptions.Add (ex);
  159. }
  160. if (counter % 10 == 0)
  161. {
  162. bindings.Clear (Command.Accept);
  163. }
  164. Thread.Sleep (1); // Small delay to prevent CPU spinning
  165. }
  166. });
  167. // Act - Start reader threads
  168. List<Task> readerTasks = new ();
  169. for (var i = 0; i < 5; i++)
  170. {
  171. readerTasks.Add (
  172. Task.Run (() =>
  173. {
  174. for (var j = 0; j < 100; j++)
  175. {
  176. try
  177. {
  178. // This should never throw "Collection was modified" exception
  179. IEnumerable<KeyValuePair<string, KeyBinding>> snapshot = bindings.GetBindings ();
  180. int count = snapshot.Count ();
  181. Assert.True (count >= 0);
  182. }
  183. catch (InvalidOperationException ex) when (ex.Message.Contains ("Collection was modified"))
  184. {
  185. exceptions.Add (ex);
  186. }
  187. catch (Exception ex)
  188. {
  189. exceptions.Add (ex);
  190. }
  191. Thread.Sleep (1); // Small delay between iterations
  192. }
  193. }));
  194. }
  195. // Wait for readers to complete
  196. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  197. Task.WaitAll (readerTasks.ToArray ());
  198. continueRunning = false;
  199. modifierTask.Wait (TimeSpan.FromSeconds (5)); // Add timeout to prevent indefinite hang
  200. #pragma warning restore xUnit1031
  201. // Assert
  202. Assert.Empty (exceptions);
  203. }
  204. [Fact]
  205. public void KeyBindings_ConcurrentAccess_NoExceptions ()
  206. {
  207. // Arrange
  208. var view = new View ();
  209. KeyBindings keyBindings = view.KeyBindings;
  210. List<Exception> exceptions = new ();
  211. const int NUM_THREADS = 10;
  212. const int OPERATIONS_PER_THREAD = 50;
  213. // Act
  214. List<Task> tasks = new ();
  215. for (var i = 0; i < NUM_THREADS; i++)
  216. {
  217. int threadId = i;
  218. tasks.Add (
  219. Task.Run (() =>
  220. {
  221. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  222. {
  223. try
  224. {
  225. Key key = Key.A.WithShift.WithCtrl + threadId + j;
  226. keyBindings.Add (key, Command.Accept);
  227. }
  228. catch (InvalidOperationException)
  229. {
  230. // Expected - duplicate or invalid key
  231. }
  232. catch (ArgumentException)
  233. {
  234. // Expected - invalid key
  235. }
  236. catch (Exception ex)
  237. {
  238. exceptions.Add (ex);
  239. }
  240. }
  241. }));
  242. }
  243. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  244. Task.WaitAll (tasks.ToArray ());
  245. #pragma warning restore xUnit1031
  246. // Assert
  247. Assert.Empty (exceptions);
  248. IEnumerable<KeyValuePair<Key, KeyBinding>> bindings = keyBindings.GetBindings ();
  249. Assert.NotEmpty (bindings);
  250. view.Dispose ();
  251. }
  252. [Fact]
  253. public void MixedOperations_ConcurrentAccess_NoExceptions ()
  254. {
  255. // Arrange
  256. var bindings = new TestInputBindings ();
  257. List<Exception> exceptions = new ();
  258. const int OPERATIONS_PER_THREAD = 100;
  259. // Act - Multiple threads doing various operations
  260. List<Task> tasks = new ();
  261. // Adder threads
  262. for (var i = 0; i < 3; i++)
  263. {
  264. int threadId = i;
  265. tasks.Add (
  266. Task.Run (() =>
  267. {
  268. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  269. {
  270. try
  271. {
  272. bindings.Add ($"add_{threadId}_{j}", Command.Accept);
  273. }
  274. catch (InvalidOperationException)
  275. {
  276. // Expected - duplicate
  277. }
  278. catch (Exception ex)
  279. {
  280. exceptions.Add (ex);
  281. }
  282. }
  283. }));
  284. }
  285. // Reader threads
  286. for (var i = 0; i < 3; i++)
  287. {
  288. tasks.Add (
  289. Task.Run (() =>
  290. {
  291. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  292. {
  293. try
  294. {
  295. IEnumerable<KeyValuePair<string, KeyBinding>> snapshot = bindings.GetBindings ();
  296. int count = snapshot.Count ();
  297. Assert.True (count >= 0);
  298. }
  299. catch (Exception ex)
  300. {
  301. exceptions.Add (ex);
  302. }
  303. }
  304. }));
  305. }
  306. // Remover threads
  307. for (var i = 0; i < 2; i++)
  308. {
  309. int threadId = i;
  310. tasks.Add (
  311. Task.Run (() =>
  312. {
  313. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  314. {
  315. try
  316. {
  317. bindings.Remove ($"add_{threadId}_{j}");
  318. }
  319. catch (Exception ex)
  320. {
  321. exceptions.Add (ex);
  322. }
  323. }
  324. }));
  325. }
  326. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  327. Task.WaitAll (tasks.ToArray ());
  328. #pragma warning restore xUnit1031
  329. // Assert
  330. Assert.Empty (exceptions);
  331. }
  332. [Fact]
  333. public void MouseBindings_ConcurrentAccess_NoExceptions ()
  334. {
  335. // Arrange
  336. var view = new View ();
  337. MouseBindings mouseBindings = view.MouseBindings;
  338. List<Exception> exceptions = new ();
  339. const int NUM_THREADS = 10;
  340. const int OPERATIONS_PER_THREAD = 50;
  341. // Act
  342. List<Task> tasks = new ();
  343. for (var i = 0; i < NUM_THREADS; i++)
  344. {
  345. int threadId = i;
  346. tasks.Add (
  347. Task.Run (() =>
  348. {
  349. for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
  350. {
  351. try
  352. {
  353. MouseFlags flags = MouseFlags.Button1Clicked | (MouseFlags)(threadId * 1000 + j);
  354. mouseBindings.Add (flags, Command.Accept);
  355. }
  356. catch (InvalidOperationException)
  357. {
  358. // Expected - duplicate or invalid flags
  359. }
  360. catch (ArgumentException)
  361. {
  362. // Expected - invalid mouse flags
  363. }
  364. catch (Exception ex)
  365. {
  366. exceptions.Add (ex);
  367. }
  368. }
  369. }));
  370. }
  371. #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
  372. Task.WaitAll (tasks.ToArray ());
  373. #pragma warning restore xUnit1031
  374. // Assert
  375. Assert.Empty (exceptions);
  376. view.Dispose ();
  377. }
  378. [Fact]
  379. public void Remove_ConcurrentAccess_NoExceptions ()
  380. {
  381. // Arrange
  382. var bindings = new TestInputBindings ();
  383. const int NUM_ITEMS = 100;
  384. // Populate data
  385. for (var i = 0; i < NUM_ITEMS; i++)
  386. {
  387. bindings.Add ($"key_{i}", Command.Accept);
  388. }
  389. // Act - Multiple threads removing items
  390. Parallel.For (
  391. 0,
  392. NUM_ITEMS,
  393. i =>
  394. {
  395. try
  396. {
  397. bindings.Remove ($"key_{i}");
  398. }
  399. catch (Exception ex)
  400. {
  401. Assert.Fail ($"Remove should not throw: {ex.Message}");
  402. }
  403. });
  404. // Assert
  405. Assert.Empty (bindings.GetBindings ());
  406. }
  407. [Fact]
  408. public void Replace_ConcurrentAccess_NoExceptions ()
  409. {
  410. // Arrange
  411. var bindings = new TestInputBindings ();
  412. const string OLD_KEY = "old_key";
  413. const string NEW_KEY = "new_key";
  414. bindings.Add (OLD_KEY, Command.Accept);
  415. // Act - Multiple threads trying to replace
  416. List<Exception> exceptions = new ();
  417. Parallel.For (
  418. 0,
  419. 10,
  420. i =>
  421. {
  422. try
  423. {
  424. bindings.Replace (OLD_KEY, $"{NEW_KEY}_{i}");
  425. }
  426. catch (InvalidOperationException)
  427. {
  428. // Expected - key might already be replaced
  429. }
  430. catch (Exception ex)
  431. {
  432. exceptions.Add (ex);
  433. }
  434. });
  435. // Assert
  436. Assert.Empty (exceptions);
  437. }
  438. [Fact]
  439. public void TryGet_ConcurrentAccess_ReturnsConsistentResults ()
  440. {
  441. // Arrange
  442. var bindings = new TestInputBindings ();
  443. const string TEST_KEY = "test_key";
  444. bindings.Add (TEST_KEY, Command.Accept);
  445. // Act
  446. var results = new bool [100];
  447. Parallel.For (
  448. 0,
  449. 100,
  450. i => { results [i] = bindings.TryGet (TEST_KEY, out _); });
  451. // Assert - All threads should consistently find the binding
  452. Assert.All (results, result => Assert.True (result));
  453. }
  454. /// <summary>
  455. /// Test implementation of InputBindings for testing purposes.
  456. /// </summary>
  457. private class TestInputBindings () : InputBindings<string, KeyBinding> (
  458. (commands, evt) => new ()
  459. {
  460. Commands = commands,
  461. Key = Key.Empty
  462. },
  463. StringComparer.OrdinalIgnoreCase)
  464. {
  465. public override bool IsValid (string eventArgs) { return !string.IsNullOrEmpty (eventArgs); }
  466. }
  467. }