namespace InputTests; /// /// Tests to verify that InputBindings (KeyBindings and MouseBindings) are thread-safe /// for concurrent access scenarios. /// public class InputBindingsThreadSafetyTests { [Fact] public void Add_ConcurrentAccess_NoExceptions () { // Arrange var bindings = new TestInputBindings (); const int NUM_THREADS = 10; const int ITEMS_PER_THREAD = 100; // Act Parallel.For ( 0, NUM_THREADS, i => { for (var j = 0; j < ITEMS_PER_THREAD; j++) { var key = $"key_{i}_{j}"; try { bindings.Add (key, Command.Accept); } catch (InvalidOperationException) { // Expected if duplicate key - this is OK } } }); // Assert IEnumerable> allBindings = bindings.GetBindings (); Assert.NotEmpty (allBindings); Assert.True (allBindings.Count () <= NUM_THREADS * ITEMS_PER_THREAD); } [Fact] public void Clear_ConcurrentAccess_NoExceptions () { // Arrange var bindings = new TestInputBindings (); const int NUM_THREADS = 10; // Populate initial data for (var i = 0; i < 100; i++) { bindings.Add ($"key_{i}", Command.Accept); } // Act - Multiple threads clearing simultaneously Parallel.For ( 0, NUM_THREADS, i => { try { bindings.Clear (); } catch (Exception ex) { Assert.Fail ($"Clear should not throw: {ex.Message}"); } }); // Assert Assert.Empty (bindings.GetBindings ()); } [Fact] public void GetAllFromCommands_DuringModification_NoExceptions () { // Arrange var bindings = new TestInputBindings (); var continueRunning = true; List exceptions = new (); const int MAX_ADDITIONS = 200; // Limit total additions to prevent infinite loop // Populate initial data for (var i = 0; i < 50; i++) { bindings.Add ($"key_{i}", Command.Accept); } // Act - Modifier thread Task modifierTask = Task.Run (() => { var counter = 50; while (continueRunning && counter < MAX_ADDITIONS) { try { bindings.Add ($"key_{counter++}", Command.Accept); Thread.Sleep (1); // Small delay to prevent CPU spinning } catch (InvalidOperationException) { // Expected } } }); // Act - Reader threads List readerTasks = new (); for (var i = 0; i < 5; i++) { readerTasks.Add ( Task.Run (() => { for (var j = 0; j < 50; j++) { try { IEnumerable results = bindings.GetAllFromCommands (Command.Accept); int count = results.Count (); Assert.True (count >= 0); } catch (Exception ex) { exceptions.Add (ex); } Thread.Sleep (1); // Small delay between iterations } })); } #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing Task.WaitAll (readerTasks.ToArray ()); continueRunning = false; modifierTask.Wait (TimeSpan.FromSeconds (5)); // Add timeout to prevent indefinite hang #pragma warning restore xUnit1031 // Assert Assert.Empty (exceptions); } [Fact] public void GetBindings_DuringConcurrentModification_NoExceptions () { // Arrange var bindings = new TestInputBindings (); var continueRunning = true; List exceptions = new (); const int MAX_MODIFICATIONS = 200; // Limit total modifications // Populate some initial data for (var i = 0; i < 50; i++) { bindings.Add ($"initial_{i}", Command.Accept); } // Act - Start modifier thread Task modifierTask = Task.Run (() => { var counter = 0; while (continueRunning && counter < MAX_MODIFICATIONS) { try { bindings.Add ($"key_{counter++}", Command.Cancel); } catch (InvalidOperationException) { // Expected - duplicate key } catch (Exception ex) { exceptions.Add (ex); } if (counter % 10 == 0) { bindings.Clear (Command.Accept); } Thread.Sleep (1); // Small delay to prevent CPU spinning } }); // Act - Start reader threads List readerTasks = new (); for (var i = 0; i < 5; i++) { readerTasks.Add ( Task.Run (() => { for (var j = 0; j < 100; j++) { try { // This should never throw "Collection was modified" exception IEnumerable> snapshot = bindings.GetBindings (); int count = snapshot.Count (); Assert.True (count >= 0); } catch (InvalidOperationException ex) when (ex.Message.Contains ("Collection was modified")) { exceptions.Add (ex); } catch (Exception ex) { exceptions.Add (ex); } Thread.Sleep (1); // Small delay between iterations } })); } // Wait for readers to complete #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing Task.WaitAll (readerTasks.ToArray ()); continueRunning = false; modifierTask.Wait (TimeSpan.FromSeconds (5)); // Add timeout to prevent indefinite hang #pragma warning restore xUnit1031 // Assert Assert.Empty (exceptions); } [Fact] public void KeyBindings_ConcurrentAccess_NoExceptions () { // Arrange var view = new View (); KeyBindings keyBindings = view.KeyBindings; List exceptions = new (); const int NUM_THREADS = 10; const int OPERATIONS_PER_THREAD = 50; // Act List tasks = new (); for (var i = 0; i < NUM_THREADS; i++) { int threadId = i; tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { Key key = Key.A.WithShift.WithCtrl + threadId + j; keyBindings.Add (key, Command.Accept); } catch (InvalidOperationException) { // Expected - duplicate or invalid key } catch (ArgumentException) { // Expected - invalid key } catch (Exception ex) { exceptions.Add (ex); } } })); } #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing Task.WaitAll (tasks.ToArray ()); #pragma warning restore xUnit1031 // Assert Assert.Empty (exceptions); IEnumerable> bindings = keyBindings.GetBindings (); Assert.NotEmpty (bindings); view.Dispose (); } [Fact] public void MixedOperations_ConcurrentAccess_NoExceptions () { // Arrange var bindings = new TestInputBindings (); List exceptions = new (); const int OPERATIONS_PER_THREAD = 100; // Act - Multiple threads doing various operations List tasks = new (); // Adder threads for (var i = 0; i < 3; i++) { int threadId = i; tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { bindings.Add ($"add_{threadId}_{j}", Command.Accept); } catch (InvalidOperationException) { // Expected - duplicate } catch (Exception ex) { exceptions.Add (ex); } } })); } // Reader threads for (var i = 0; i < 3; i++) { tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { IEnumerable> snapshot = bindings.GetBindings (); int count = snapshot.Count (); Assert.True (count >= 0); } catch (Exception ex) { exceptions.Add (ex); } } })); } // Remover threads for (var i = 0; i < 2; i++) { int threadId = i; tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { bindings.Remove ($"add_{threadId}_{j}"); } catch (Exception ex) { exceptions.Add (ex); } } })); } #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing Task.WaitAll (tasks.ToArray ()); #pragma warning restore xUnit1031 // Assert Assert.Empty (exceptions); } [Fact] public void MouseBindings_ConcurrentAccess_NoExceptions () { // Arrange var view = new View (); MouseBindings mouseBindings = view.MouseBindings; List exceptions = new (); const int NUM_THREADS = 10; const int OPERATIONS_PER_THREAD = 50; // Act List tasks = new (); for (var i = 0; i < NUM_THREADS; i++) { int threadId = i; tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { MouseFlags flags = MouseFlags.Button1Clicked | (MouseFlags)(threadId * 1000 + j); mouseBindings.Add (flags, Command.Accept); } catch (InvalidOperationException) { // Expected - duplicate or invalid flags } catch (ArgumentException) { // Expected - invalid mouse flags } catch (Exception ex) { exceptions.Add (ex); } } })); } #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing Task.WaitAll (tasks.ToArray ()); #pragma warning restore xUnit1031 // Assert Assert.Empty (exceptions); view.Dispose (); } [Fact] public void Remove_ConcurrentAccess_NoExceptions () { // Arrange var bindings = new TestInputBindings (); const int NUM_ITEMS = 100; // Populate data for (var i = 0; i < NUM_ITEMS; i++) { bindings.Add ($"key_{i}", Command.Accept); } // Act - Multiple threads removing items Parallel.For ( 0, NUM_ITEMS, i => { try { bindings.Remove ($"key_{i}"); } catch (Exception ex) { Assert.Fail ($"Remove should not throw: {ex.Message}"); } }); // Assert Assert.Empty (bindings.GetBindings ()); } [Fact] public void Replace_ConcurrentAccess_NoExceptions () { // Arrange var bindings = new TestInputBindings (); const string OLD_KEY = "old_key"; const string NEW_KEY = "new_key"; bindings.Add (OLD_KEY, Command.Accept); // Act - Multiple threads trying to replace List exceptions = new (); Parallel.For ( 0, 10, i => { try { bindings.Replace (OLD_KEY, $"{NEW_KEY}_{i}"); } catch (InvalidOperationException) { // Expected - key might already be replaced } catch (Exception ex) { exceptions.Add (ex); } }); // Assert Assert.Empty (exceptions); } [Fact] public void TryGet_ConcurrentAccess_ReturnsConsistentResults () { // Arrange var bindings = new TestInputBindings (); const string TEST_KEY = "test_key"; bindings.Add (TEST_KEY, Command.Accept); // Act var results = new bool [100]; Parallel.For ( 0, 100, i => { results [i] = bindings.TryGet (TEST_KEY, out _); }); // Assert - All threads should consistently find the binding Assert.All (results, result => Assert.True (result)); } /// /// Test implementation of InputBindings for testing purposes. /// private class TestInputBindings () : InputBindings ( (commands, evt) => new () { Commands = commands, Key = Key.Empty }, StringComparer.OrdinalIgnoreCase) { public override bool IsValid (string eventArgs) { return !string.IsNullOrEmpty (eventArgs); } } }