// ReSharper disable AccessToDisposedClosure #nullable enable namespace ApplicationTests.Keyboard; /// /// Tests to verify that KeyboardImpl is thread-safe for concurrent access scenarios. /// public class KeyboardImplThreadSafetyTests { [Fact] public void AddCommand_ConcurrentAccess_NoExceptions () { // Arrange var keyboard = new KeyboardImpl (); List exceptions = []; const int NUM_THREADS = 10; const int OPERATIONS_PER_THREAD = 50; // Act List tasks = []; for (var i = 0; i < NUM_THREADS; i++) { tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { // AddKeyBindings internally calls AddCommand multiple times keyboard.AddKeyBindings (); } catch (InvalidOperationException) { // Expected - AddKeyBindings tries to add keys that already exist } 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); keyboard.Dispose (); } [Fact] public void Dispose_WhileOperationsInProgress_NoExceptions () { // Arrange IApplication? app = Application.Create (); app.Init ("fake"); var keyboard = new KeyboardImpl { App = app }; keyboard.AddKeyBindings (); List exceptions = []; var continueRunning = true; // Act Task operationsTask = Task.Run (() => { while (continueRunning) { try { keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl); IEnumerable> bindings = keyboard.KeyBindings.GetBindings (); int count = bindings.Count (); } catch (ObjectDisposedException) { // Expected - keyboard was disposed break; } catch (Exception ex) { exceptions.Add (ex); break; } } }); // Give operations a chance to start Thread.Sleep (10); // Dispose while operations are running keyboard.Dispose (); continueRunning = false; #pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing operationsTask.Wait (TimeSpan.FromSeconds (2)); #pragma warning restore xUnit1031 // Assert Assert.Empty (exceptions); app.Dispose (); } [Fact] public void InvokeCommand_ConcurrentAccess_NoExceptions () { // Arrange IApplication? app = Application.Create (); app.Init ("fake"); var keyboard = new KeyboardImpl { App = app }; keyboard.AddKeyBindings (); List exceptions = []; const int NUM_THREADS = 10; const int OPERATIONS_PER_THREAD = 50; // Act List tasks = new (); for (var i = 0; i < NUM_THREADS; i++) { tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { var binding = new KeyBinding ([Command.Quit]); keyboard.InvokeCommand (Command.Quit, Key.Q.WithCtrl, binding); } 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); keyboard.Dispose (); app.Dispose (); } [Fact] public void InvokeCommandsBoundToKey_ConcurrentAccess_NoExceptions () { // Arrange IApplication? app = Application.Create (); app.Init ("fake"); var keyboard = new KeyboardImpl { App = app }; keyboard.AddKeyBindings (); List exceptions = []; const int NUM_THREADS = 10; const int OPERATIONS_PER_THREAD = 50; // Act List tasks = []; for (var i = 0; i < NUM_THREADS; i++) { tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl); } 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); keyboard.Dispose (); app.Dispose (); } [Fact] public void KeyBindings_ConcurrentAdd_NoExceptions () { // Arrange var keyboard = new KeyboardImpl (); // Don't call AddKeyBindings here to avoid conflicts List exceptions = []; 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 { // Use unique keys per thread to avoid conflicts Key key = Key.F1 + threadId * OPERATIONS_PER_THREAD + j; keyboard.KeyBindings.Add (key, Command.Refresh); } catch (InvalidOperationException) { // Expected - duplicate 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); keyboard.Dispose (); } [Fact] public void KeyDown_KeyUp_Events_ConcurrentSubscription_NoExceptions () { // Arrange var keyboard = new KeyboardImpl (); keyboard.AddKeyBindings (); List exceptions = []; const int NUM_THREADS = 10; const int OPERATIONS_PER_THREAD = 20; var keyDownCount = 0; var keyUpCount = 0; // Act List tasks = new (); // Threads subscribing to events for (var i = 0; i < NUM_THREADS; i++) { tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { EventHandler handler = (s, e) => { Interlocked.Increment (ref keyDownCount); }; keyboard.KeyDown += handler; keyboard.KeyDown -= handler; EventHandler upHandler = (s, e) => { Interlocked.Increment (ref keyUpCount); }; keyboard.KeyUp += upHandler; keyboard.KeyUp -= upHandler; } 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); keyboard.Dispose (); } [Fact] public void KeyProperty_Setters_ConcurrentAccess_NoExceptions () { // Arrange var keyboard = new KeyboardImpl (); // Initialize once before concurrent access keyboard.AddKeyBindings (); List exceptions = []; const int NUM_THREADS = 10; const int OPERATIONS_PER_THREAD = 20; // 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 { // Cycle through different key combinations switch (j % 6) { case 0: keyboard.QuitKey = Key.Q.WithCtrl; break; case 1: keyboard.ArrangeKey = Key.F6.WithCtrl; break; case 2: keyboard.NextTabKey = Key.Tab; break; case 3: keyboard.PrevTabKey = Key.Tab.WithShift; break; case 4: keyboard.NextTabGroupKey = Key.F6; break; case 5: keyboard.PrevTabGroupKey = Key.F6.WithShift; break; } } 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); keyboard.Dispose (); } [Fact] public void MixedOperations_ConcurrentAccess_NoExceptions () { // Arrange IApplication? app = Application.Create (); app.Init ("fake"); var keyboard = new KeyboardImpl { App = app }; keyboard.AddKeyBindings (); List exceptions = []; const int OPERATIONS_PER_THREAD = 30; // Act List tasks = new (); // Thread 1: Add bindings with unique keys tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { // Use high key codes to avoid conflicts var key = new Key ((KeyCode)((int)KeyCode.F20 + j)); keyboard.KeyBindings.Add (key, Command.Refresh); } catch (InvalidOperationException) { // Expected - duplicate } catch (ArgumentException) { // Expected - invalid key } catch (Exception ex) { exceptions.Add (ex); } } })); // Thread 2: Invoke commands tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl); } catch (Exception ex) { exceptions.Add (ex); } } })); // Thread 3: Read bindings tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { IEnumerable> bindings = keyboard.KeyBindings.GetBindings (); int count = bindings.Count (); Assert.True (count >= 0); } catch (Exception ex) { exceptions.Add (ex); } } })); // Thread 4: Change key properties tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { keyboard.QuitKey = j % 2 == 0 ? Key.Q.WithCtrl : Key.Esc; } 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); keyboard.Dispose (); app.Dispose (); } [Fact] public void RaiseKeyDownEvent_ConcurrentAccess_NoExceptions () { // Arrange IApplication? app = Application.Create (); app.Init ("fake"); var keyboard = new KeyboardImpl { App = app }; keyboard.AddKeyBindings (); List exceptions = []; const int NUM_THREADS = 5; const int OPERATIONS_PER_THREAD = 20; // Act List tasks = new (); for (var i = 0; i < NUM_THREADS; i++) { tasks.Add ( Task.Run (() => { for (var j = 0; j < OPERATIONS_PER_THREAD; j++) { try { keyboard.RaiseKeyDownEvent (Key.A); } 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); keyboard.Dispose (); app.Dispose (); } }