mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-28 08:47:59 +01:00
merged
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 ();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
#nullable enable
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests;
|
||||
|
||||
@@ -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"; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#nullable enable
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests;
|
||||
|
||||
Reference in New Issue
Block a user