This commit is contained in:
Tig
2025-11-25 07:13:28 -08:00
163 changed files with 4892 additions and 2983 deletions

View File

@@ -1,41 +0,0 @@
using UnitTests;
namespace UnitTests_Parallelizable.ApplicationTests;
public class ApplicationForceDriverTests : FakeDriverBase
{
[Fact]
public void ForceDriver_Does_Not_Changes_If_It_Has_Valid_Value ()
{
Assert.False (Application.Initialized);
Assert.Null (Application.Driver);
Assert.Equal (string.Empty, Application.ForceDriver);
Application.ForceDriver = "fake";
Assert.Equal ("fake", Application.ForceDriver);
Application.ForceDriver = "dotnet";
Assert.Equal ("fake", Application.ForceDriver);
}
[Fact]
public void ForceDriver_Throws_If_Initialized_Changed_To_Another_Value ()
{
IDriver driver = CreateFakeDriver ();
Assert.False (Application.Initialized);
Assert.Null (Application.Driver);
Assert.Equal (string.Empty, Application.ForceDriver);
Application.Init (driverName: "fake");
Assert.True (Application.Initialized);
Assert.NotNull (Application.Driver);
Assert.Equal ("fake", Application.Driver.GetName ());
Assert.Equal (string.Empty, Application.ForceDriver);
Assert.Throws<InvalidOperationException> (() => Application.ForceDriver = "dotnet");
Application.ForceDriver = "fake";
Assert.Equal ("fake", Application.ForceDriver);
}
}

View File

@@ -0,0 +1,477 @@
#nullable enable
using System.Collections.Concurrent;
using Moq;
namespace UnitTests_Parallelizable.ApplicationTests;
public class ApplicationImplTests
{
/// <summary>
/// Crates a new ApplicationImpl instance for testing. The input, output, and size monitor components are mocked.
/// </summary>
private IApplication? NewMockedApplicationImpl ()
{
Mock<INetInput> netInput = new ();
SetupRunInputMockMethodToBlock (netInput);
Mock<IComponentFactory<ConsoleKeyInfo>> m = new ();
m.Setup (f => f.CreateInput ()).Returns (netInput.Object);
m.Setup (f => f.CreateInputProcessor (It.IsAny<ConcurrentQueue<ConsoleKeyInfo>> ())).Returns (Mock.Of<IInputProcessor> ());
Mock<IOutput> consoleOutput = new ();
var size = new Size (80, 25);
consoleOutput.Setup (o => o.SetSize (It.IsAny<int> (), It.IsAny<int> ()))
.Callback<int, int> ((w, h) => size = new (w, h));
consoleOutput.Setup (o => o.GetSize ()).Returns (() => size);
m.Setup (f => f.CreateOutput ()).Returns (consoleOutput.Object);
m.Setup (f => f.CreateSizeMonitor (It.IsAny<IOutput> (), It.IsAny<IOutputBuffer> ())).Returns (Mock.Of<ISizeMonitor> ());
return new ApplicationImpl (m.Object);
}
private void SetupRunInputMockMethodToBlock (Mock<INetInput> netInput)
{
netInput.Setup (r => r.Run (It.IsAny<CancellationToken> ()))
.Callback<CancellationToken> (token =>
{
// Simulate an infinite loop that checks for cancellation
while (!token.IsCancellationRequested)
{
// Perform the action that should repeat in the loop
// This could be some mock behavior or just an empty loop depending on the context
}
})
.Verifiable (Times.Once);
}
[Fact]
public void Init_CreatesKeybindings ()
{
IApplication? app = NewMockedApplicationImpl ();
app?.Keyboard.KeyBindings.Clear ();
Assert.Empty (app?.Keyboard?.KeyBindings.GetBindings ()!);
app?.Init ("fake");
Assert.NotEmpty (app?.Keyboard?.KeyBindings.GetBindings ()!);
app?.Shutdown ();
}
[Fact]
public void NoInitThrowOnRun ()
{
IApplication? app = NewMockedApplicationImpl ();
var ex = Assert.Throws<NotInitializedException> (() => app?.Run (new Window ()));
Assert.Equal ("Run cannot be accessed before Initialization", ex.Message);
app?.Shutdown ();
}
[Fact]
public void InitRunShutdown_Top_Set_To_Null_After_Shutdown ()
{
IApplication? app = NewMockedApplicationImpl ();
app?.Init ("fake");
object? timeoutToken = app?.AddTimeout (
TimeSpan.FromMilliseconds (150),
() =>
{
if (app.TopRunnable is { })
{
app.RequestStop ();
return false;
}
return false;
}
);
Assert.Null (app?.TopRunnable);
// Blocks until the timeout call is hit
app?.Run (new Window ());
// We returned false above, so we should not have to remove the timeout
Assert.False (app?.RemoveTimeout (timeoutToken!));
Assert.NotNull (app?.TopRunnable);
app.TopRunnable?.Dispose ();
app.Shutdown ();
Assert.Null (app.TopRunnable);
}
[Fact]
public void InitRunShutdown_Running_Set_To_False ()
{
IApplication app = NewMockedApplicationImpl ()!;
app.Init ("fake");
Toplevel top = new Window
{
Title = "InitRunShutdown_Running_Set_To_False"
};
object timeoutToken = app.AddTimeout (
TimeSpan.FromMilliseconds (150),
() =>
{
Assert.True (top!.Running);
if (app.TopRunnable != null)
{
app.RequestStop ();
return false;
}
return false;
}
);
Assert.False (top!.Running);
// Blocks until the timeout call is hit
app.Run (top);
// We returned false above, so we should not have to remove the timeout
Assert.False (app.RemoveTimeout (timeoutToken));
Assert.False (top!.Running);
// BUGBUG: Shutdown sets Top to null, not End.
//Assert.Null (Application.TopRunnable);
app.TopRunnable?.Dispose ();
app.Shutdown ();
}
[Fact]
public void InitRunShutdown_StopAfterFirstIteration_Stops ()
{
IApplication app = NewMockedApplicationImpl ()!;
Assert.Null (app.TopRunnable);
Assert.Null (app.Driver);
app.Init ("fake");
Toplevel top = new Window ();
app.TopRunnable = top;
var closedCount = 0;
top.Closed
+= (_, a) => { closedCount++; };
var unloadedCount = 0;
top.Unloaded
+= (_, a) => { unloadedCount++; };
object timeoutToken = app.AddTimeout (
TimeSpan.FromMilliseconds (150),
() =>
{
Assert.Fail (@"Didn't stop after first iteration.");
return false;
}
);
Assert.Equal (0, closedCount);
Assert.Equal (0, unloadedCount);
app.StopAfterFirstIteration = true;
app.Run (top);
Assert.Equal (1, closedCount);
Assert.Equal (1, unloadedCount);
app.TopRunnable?.Dispose ();
app.Shutdown ();
Assert.Equal (1, closedCount);
Assert.Equal (1, unloadedCount);
}
[Fact]
public void InitRunShutdown_End_Is_Called ()
{
IApplication app = NewMockedApplicationImpl ()!;
Assert.Null (app.TopRunnable);
Assert.Null (app.Driver);
app.Init ("fake");
Toplevel top = new Window ();
// BUGBUG: Both Closed and Unloaded are called from End; what's the difference?
var closedCount = 0;
top.Closed
+= (_, a) => { closedCount++; };
var unloadedCount = 0;
top.Unloaded
+= (_, a) => { unloadedCount++; };
object timeoutToken = app.AddTimeout (
TimeSpan.FromMilliseconds (150),
() =>
{
Assert.True (top!.Running);
if (app.TopRunnable != null)
{
app.RequestStop ();
return false;
}
return false;
}
);
Assert.Equal (0, closedCount);
Assert.Equal (0, unloadedCount);
// Blocks until the timeout call is hit
app.Run (top);
Assert.Equal (1, closedCount);
Assert.Equal (1, unloadedCount);
// We returned false above, so we should not have to remove the timeout
Assert.False (app.RemoveTimeout (timeoutToken));
app.TopRunnable?.Dispose ();
app.Shutdown ();
Assert.Equal (1, closedCount);
Assert.Equal (1, unloadedCount);
}
[Fact]
public void InitRunShutdown_QuitKey_Quits ()
{
IApplication app = NewMockedApplicationImpl ()!;
app.Init ("fake");
Toplevel top = new Window
{
Title = "InitRunShutdown_QuitKey_Quits"
};
object timeoutToken = app.AddTimeout (
TimeSpan.FromMilliseconds (150),
() =>
{
Assert.True (top!.Running);
if (app.TopRunnable != null)
{
app.Keyboard.RaiseKeyDownEvent (app.Keyboard.QuitKey);
}
return false;
}
);
Assert.False (top!.Running);
// Blocks until the timeout call is hit
app.Run (top);
// We returned false above, so we should not have to remove the timeout
Assert.False (app.RemoveTimeout (timeoutToken));
Assert.False (top!.Running);
Assert.NotNull (app.TopRunnable);
top.Dispose ();
app.Shutdown ();
Assert.Null (app.TopRunnable);
}
[Fact (Skip = "Phase 2: Ambiguous method call after Toplevel implements IRunnable. Use non-generic Run() or explicit cast.")]
public void InitRunShutdown_Generic_IdleForExit ()
{
IApplication app = NewMockedApplicationImpl ()!;
app.Init ("fake");
app.AddTimeout (TimeSpan.Zero, () => IdleExit (app));
Assert.Null (app.TopRunnable);
// Blocks until the timeout call is hit
// Phase 2: Ambiguous method call - use non-generic Run()
Window window = new ();
app.Run (window);
Assert.NotNull (app.TopRunnable);
app.TopRunnable?.Dispose ();
app.Shutdown ();
Assert.Null (app.TopRunnable);
}
[Fact]
public void Shutdown_Closing_Closed_Raised ()
{
IApplication app = NewMockedApplicationImpl ()!;
app.Init ("fake");
var closing = 0;
var closed = 0;
var t = new Toplevel ();
t.Closing
+= (_, a) =>
{
// Cancel the first time
if (closing == 0)
{
a.Cancel = true;
}
closing++;
Assert.Same (t, a.RequestingTop);
};
t.Closed
+= (_, a) =>
{
closed++;
Assert.Same (t, a.Toplevel);
};
app.AddTimeout (TimeSpan.Zero, () => IdleExit (app));
// Blocks until the timeout call is hit
app.Run (t);
app.TopRunnable?.Dispose ();
app.Shutdown ();
Assert.Equal (2, closing);
Assert.Equal (1, closed);
}
private bool IdleExit (IApplication app)
{
if (app.TopRunnable != null)
{
app.RequestStop ();
return true;
}
return true;
}
[Fact]
public void Open_Calls_ContinueWith_On_UIThread ()
{
IApplication app = NewMockedApplicationImpl ()!;
app.Init ("fake");
var b = new Button ();
var result = false;
b.Accepting +=
(_, _) =>
{
Task.Run (() => { Task.Delay (300).Wait (); })
.ContinueWith (
(t, _) =>
{
// no longer loading
app.Invoke (() =>
{
result = true;
app.RequestStop ();
});
},
TaskScheduler.FromCurrentSynchronizationContext ());
};
app.AddTimeout (
TimeSpan.FromMilliseconds (150),
() =>
{
// Run asynchronous logic inside Task.Run
if (app.TopRunnable != null)
{
b.NewKeyDownEvent (Key.Enter);
b.NewKeyUpEvent (Key.Enter);
}
return false;
});
Assert.Null (app.TopRunnable);
var w = new Window
{
Title = "Open_CallsContinueWithOnUIThread"
};
w.Add (b);
// Blocks until the timeout call is hit
app.Run (w);
Assert.NotNull (app.TopRunnable);
app.TopRunnable?.Dispose ();
app.Shutdown ();
Assert.Null (app.TopRunnable);
Assert.True (result);
}
[Fact]
public void ApplicationImpl_UsesInstanceFields_NotStaticReferences ()
{
// This test verifies that ApplicationImpl uses instance fields instead of static Application references
IApplication v2 = NewMockedApplicationImpl ()!;
// Before Init, all fields should be null/default
Assert.Null (v2.Driver);
Assert.False (v2.Initialized);
//Assert.Null (v2.Popover);
//Assert.Null (v2.Navigation);
Assert.Null (v2.TopRunnable);
Assert.Empty (v2.SessionStack);
// Init should populate instance fields
v2.Init ("fake");
// After Init, Driver, Navigation, and Popover should be populated
Assert.NotNull (v2.Driver);
Assert.True (v2.Initialized);
Assert.NotNull (v2.Popover);
Assert.NotNull (v2.Navigation);
Assert.Null (v2.TopRunnable); // Top is still null until Run
// Shutdown should clean up instance fields
v2.Shutdown ();
Assert.Null (v2.Driver);
Assert.False (v2.Initialized);
//Assert.Null (v2.Popover);
//Assert.Null (v2.Navigation);
Assert.Null (v2.TopRunnable);
Assert.Empty (v2.SessionStack);
}
}

View File

@@ -0,0 +1,520 @@
// ReSharper disable AccessToDisposedClosure
#nullable enable
namespace UnitTests_Parallelizable.ApplicationTests;
/// <summary>
/// Tests to verify that KeyboardImpl is thread-safe for concurrent access scenarios.
/// </summary>
public class KeyboardImplThreadSafetyTests
{
[Fact]
public void AddCommand_ConcurrentAccess_NoExceptions ()
{
// Arrange
var keyboard = new KeyboardImpl ();
List<Exception> exceptions = [];
const int NUM_THREADS = 10;
const int OPERATIONS_PER_THREAD = 50;
// Act
List<Task> 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<Exception> exceptions = [];
var continueRunning = true;
// Act
Task operationsTask = Task.Run (() =>
{
while (continueRunning)
{
try
{
keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl);
IEnumerable<KeyValuePair<Key, KeyBinding>> 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.Shutdown ();
}
[Fact]
public void InvokeCommand_ConcurrentAccess_NoExceptions ()
{
// Arrange
IApplication? app = Application.Create ();
app.Init ("fake");
var keyboard = new KeyboardImpl { App = app };
keyboard.AddKeyBindings ();
List<Exception> exceptions = [];
const int NUM_THREADS = 10;
const int OPERATIONS_PER_THREAD = 50;
// Act
List<Task> 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.Shutdown ();
}
[Fact]
public void InvokeCommandsBoundToKey_ConcurrentAccess_NoExceptions ()
{
// Arrange
IApplication? app = Application.Create ();
app.Init ("fake");
var keyboard = new KeyboardImpl { App = app };
keyboard.AddKeyBindings ();
List<Exception> exceptions = [];
const int NUM_THREADS = 10;
const int OPERATIONS_PER_THREAD = 50;
// Act
List<Task> 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.Shutdown ();
}
[Fact]
public void KeyBindings_ConcurrentAdd_NoExceptions ()
{
// Arrange
var keyboard = new KeyboardImpl ();
// Don't call AddKeyBindings here to avoid conflicts
List<Exception> exceptions = [];
const int NUM_THREADS = 10;
const int OPERATIONS_PER_THREAD = 50;
// Act
List<Task> 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<Exception> exceptions = [];
const int NUM_THREADS = 10;
const int OPERATIONS_PER_THREAD = 20;
var keyDownCount = 0;
var keyUpCount = 0;
// Act
List<Task> 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<Key> handler = (s, e) => { Interlocked.Increment (ref keyDownCount); };
keyboard.KeyDown += handler;
keyboard.KeyDown -= handler;
EventHandler<Key> 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<Exception> exceptions = [];
const int NUM_THREADS = 10;
const int OPERATIONS_PER_THREAD = 20;
// Act
List<Task> 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<Exception> exceptions = [];
const int OPERATIONS_PER_THREAD = 30;
// Act
List<Task> 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<KeyValuePair<Key, KeyBinding>> 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.Shutdown ();
}
[Fact]
public void RaiseKeyDownEvent_ConcurrentAccess_NoExceptions ()
{
// Arrange
IApplication? app = Application.Create ();
app.Init ("fake");
var keyboard = new KeyboardImpl { App = app };
keyboard.AddKeyBindings ();
List<Exception> exceptions = [];
const int NUM_THREADS = 5;
const int OPERATIONS_PER_THREAD = 20;
// Act
List<Task> 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.Shutdown ();
}
}

View File

@@ -1,3 +1,4 @@
#nullable enable
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests;

View File

@@ -1,6 +1,7 @@
#nullable enable
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests;
namespace UnitTests_Parallelizable.ApplicationTests;
/// <summary>
/// Integration tests for IApplication's IRunnable support.
@@ -11,17 +12,6 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
private readonly ITestOutputHelper _output = output;
private IApplication? _app;
private IApplication GetApp ()
{
if (_app is null)
{
_app = Application.Create ();
_app.Init ("fake");
}
return _app;
}
public void Dispose ()
{
_app?.Shutdown ();
@@ -50,64 +40,42 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
}
[Fact]
public void Begin_ThrowsOnNullRunnable ()
public void Begin_CanBeCanceled_ByIsRunningChanging ()
{
// Arrange
IApplication app = GetApp ();
// Act & Assert
Assert.Throws<ArgumentNullException> (() => app.Begin ((IRunnable)null!));
}
[Fact]
public void Begin_RaisesIsRunningChangingEvent ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable = new ();
var isRunningChangingRaised = false;
bool? oldValue = null;
bool? newValue = null;
runnable.IsRunningChanging += (s, e) =>
{
isRunningChangingRaised = true;
oldValue = e.CurrentValue;
newValue = e.NewValue;
};
CancelableRunnable runnable = new () { CancelStart = true };
// Act
RunnableSessionToken token = app.Begin (runnable);
// Assert
Assert.True (isRunningChangingRaised);
Assert.False (oldValue);
Assert.True (newValue);
// Assert - Should not be added to stack if canceled
Assert.False (runnable.IsRunning);
// Cleanup
app.End (token);
// Token is still created but runnable not added to stack
Assert.NotNull (token);
}
[Fact]
public void Begin_RaisesIsRunningChangedEvent ()
public void Begin_RaisesIsModalChangedEvent ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable = new ();
var isRunningChangedRaised = false;
var isModalChangedRaised = false;
bool? receivedValue = null;
runnable.IsRunningChanged += (s, e) =>
{
isRunningChangedRaised = true;
receivedValue = e.Value;
};
runnable.IsModalChanged += (s, e) =>
{
isModalChangedRaised = true;
receivedValue = e.Value;
};
// Act
RunnableSessionToken token = app.Begin (runnable);
// Assert
Assert.True (isRunningChangedRaised);
Assert.True (isModalChangedRaised);
Assert.True (receivedValue);
// Cleanup
@@ -144,25 +112,25 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
}
[Fact]
public void Begin_RaisesIsModalChangedEvent ()
public void Begin_RaisesIsRunningChangedEvent ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable = new ();
var isModalChangedRaised = false;
var isRunningChangedRaised = false;
bool? receivedValue = null;
runnable.IsModalChanged += (s, e) =>
{
isModalChangedRaised = true;
receivedValue = e.Value;
};
runnable.IsRunningChanged += (s, e) =>
{
isRunningChangedRaised = true;
receivedValue = e.Value;
};
// Act
RunnableSessionToken token = app.Begin (runnable);
// Assert
Assert.True (isModalChangedRaised);
Assert.True (isRunningChangedRaised);
Assert.True (receivedValue);
// Cleanup
@@ -170,17 +138,29 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
}
[Fact]
public void Begin_SetsIsRunningToTrue ()
public void Begin_RaisesIsRunningChangingEvent ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable = new ();
var isRunningChangingRaised = false;
bool? oldValue = null;
bool? newValue = null;
runnable.IsRunningChanging += (s, e) =>
{
isRunningChangingRaised = true;
oldValue = e.CurrentValue;
newValue = e.NewValue;
};
// Act
RunnableSessionToken token = app.Begin (runnable);
// Assert
Assert.True (runnable.IsRunning);
Assert.True (isRunningChangingRaised);
Assert.False (oldValue);
Assert.True (newValue);
// Cleanup
app.End (token);
@@ -204,29 +184,89 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
}
[Fact]
public void End_RemovesRunnableFromStack ()
public void Begin_SetsIsRunningToTrue ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable = new ();
RunnableSessionToken token = app.Begin (runnable);
int stackCountBefore = app.RunnableSessionStack?.Count ?? 0;
// Act
app.End (token);
RunnableSessionToken token = app.Begin (runnable);
// Assert
Assert.Equal (stackCountBefore - 1, app.RunnableSessionStack?.Count ?? 0);
Assert.True (runnable.IsRunning);
// Cleanup
app.End (token);
}
[Fact]
public void End_ThrowsOnNullToken ()
public void Begin_ThrowsOnNullRunnable ()
{
// Arrange
IApplication app = GetApp ();
// Act & Assert
Assert.Throws<ArgumentNullException> (() => app.End ((RunnableSessionToken)null!));
Assert.Throws<ArgumentNullException> (() => app.Begin ((IRunnable)null!));
}
[Fact]
public void End_CanBeCanceled_ByIsRunningChanging ()
{
// Arrange
IApplication app = GetApp ();
CancelableRunnable runnable = new () { CancelStop = true };
RunnableSessionToken token = app.Begin (runnable);
runnable.CancelStop = true; // Enable cancellation
// Act
app.End (token);
// Assert - Should still be running if canceled
Assert.True (runnable.IsRunning);
// Force end by disabling cancellation
runnable.CancelStop = false;
app.End (token);
}
[Fact]
public void End_ClearsTokenRunnable ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable = new ();
RunnableSessionToken token = app.Begin (runnable);
// Act
app.End (token);
// Assert
Assert.Null (token.Runnable);
}
[Fact]
public void End_RaisesIsRunningChangedEvent ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable = new ();
RunnableSessionToken token = app.Begin (runnable);
var isRunningChangedRaised = false;
bool? receivedValue = null;
runnable.IsRunningChanged += (s, e) =>
{
isRunningChangedRaised = true;
receivedValue = e.Value;
};
// Act
app.End (token);
// Assert
Assert.True (isRunningChangedRaised);
Assert.False (receivedValue);
}
[Fact]
@@ -257,42 +297,19 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
}
[Fact]
public void End_RaisesIsRunningChangedEvent ()
public void End_RemovesRunnableFromStack ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable = new ();
RunnableSessionToken token = app.Begin (runnable);
var isRunningChangedRaised = false;
bool? receivedValue = null;
runnable.IsRunningChanged += (s, e) =>
{
isRunningChangedRaised = true;
receivedValue = e.Value;
};
int stackCountBefore = app.RunnableSessionStack?.Count ?? 0;
// Act
app.End (token);
// Assert
Assert.True (isRunningChangedRaised);
Assert.False (receivedValue);
}
[Fact]
public void End_SetsIsRunningToFalse ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable = new ();
RunnableSessionToken token = app.Begin (runnable);
// Act
app.End (token);
// Assert
Assert.False (runnable.IsRunning);
Assert.Equal (stackCountBefore - 1, app.RunnableSessionStack?.Count ?? 0);
}
[Fact]
@@ -311,7 +328,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
}
[Fact]
public void End_ClearsTokenRunnable ()
public void End_SetsIsRunningToFalse ()
{
// Arrange
IApplication app = GetApp ();
@@ -322,7 +339,34 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
app.End (token);
// Assert
Assert.Null (token.Runnable);
Assert.False (runnable.IsRunning);
}
[Fact]
public void End_ThrowsOnNullToken ()
{
// Arrange
IApplication app = GetApp ();
// Act & Assert
Assert.Throws<ArgumentNullException> (() => app.End ((RunnableSessionToken)null!));
}
[Fact]
public void MultipleRunnables_IndependentResults ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable1 = new ();
Runnable<string> runnable2 = new ();
// Act
runnable1.Result = 42;
runnable2.Result = "test";
// Assert
Assert.Equal (42, runnable1.Result);
Assert.Equal ("test", runnable2.Result);
}
[Fact]
@@ -441,80 +485,15 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
app.Shutdown ();
}
[Fact]
public void Begin_CanBeCanceled_ByIsRunningChanging ()
private IApplication GetApp ()
{
// Arrange
IApplication app = GetApp ();
CancelableRunnable runnable = new () { CancelStart = true };
// Act
RunnableSessionToken token = app.Begin (runnable);
// Assert - Should not be added to stack if canceled
Assert.False (runnable.IsRunning);
// Token is still created but runnable not added to stack
Assert.NotNull (token);
}
[Fact]
public void End_CanBeCanceled_ByIsRunningChanging ()
{
// Arrange
IApplication app = GetApp ();
CancelableRunnable runnable = new () { CancelStop = true };
RunnableSessionToken token = app.Begin (runnable);
runnable.CancelStop = true; // Enable cancellation
// Act
app.End (token);
// Assert - Should still be running if canceled
Assert.True (runnable.IsRunning);
// Force end by disabling cancellation
runnable.CancelStop = false;
app.End (token);
}
[Fact]
public void MultipleRunnables_IndependentResults ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable1 = new ();
Runnable<string> runnable2 = new ();
// Act
runnable1.Result = 42;
runnable2.Result = "test";
// Assert
Assert.Equal (42, runnable1.Result);
Assert.Equal ("test", runnable2.Result);
}
/// <summary>
/// Test runnable that can be stopped.
/// </summary>
private class StoppableRunnable : Runnable<int>
{
public bool WasStopRequested { get; private set; }
public override void RequestStop ()
if (_app is null)
{
WasStopRequested = true;
base.RequestStop ();
_app = Application.Create ();
_app.Init ("fake");
}
}
/// <summary>
/// Test runnable for generic Run tests.
/// </summary>
private class TestRunnable : Runnable<int>
{
public TestRunnable () { Id = "TestRunnable"; }
return _app;
}
/// <summary>
@@ -540,4 +519,26 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
return base.OnIsRunningChanging (oldIsRunning, newIsRunning);
}
}
/// <summary>
/// Test runnable that can be stopped.
/// </summary>
private class StoppableRunnable : Runnable<int>
{
public override void RequestStop ()
{
WasStopRequested = true;
base.RequestStop ();
}
public bool WasStopRequested { get; private set; }
}
/// <summary>
/// Test runnable for generic Run tests.
/// </summary>
private class TestRunnable : Runnable<int>
{
public TestRunnable () { Id = "TestRunnable"; }
}
}

View File

@@ -1,3 +1,4 @@
#nullable enable
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests;