// 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 ();
}
}