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;

View File

@@ -1,21 +0,0 @@
#nullable enable
namespace UnitTests_Parallelizable.ConfigurationTests;
public class ConfigurationManagerTests
{
[ConfigurationProperty (Scope = typeof (CMTestsScope))]
public static bool? TestProperty { get; set; }
private class CMTestsScope : Scope<CMTestsScope>
{
}
[Fact]
public void GetConfigPropertiesByScope_Gets ()
{
var props = ConfigurationManager.GetUninitializedConfigPropertiesByScope ("CMTestsScope");
Assert.NotNull (props);
Assert.NotEmpty (props);
}
}

View File

@@ -515,47 +515,6 @@ public class SourcesManagerTests
Assert.Equal (streamSource, sourcesManager.Sources [location3]);
}
[Fact]
public void Sources_StaysConsistentWhenUpdateFails ()
{
// Arrange
var sourcesManager = new SourcesManager ();
var settingsScope = new SettingsScope ();
// Add one successful source
var validSource = "valid.json";
var validLocation = ConfigLocations.Runtime;
sourcesManager.Load (settingsScope, """{"Application.QuitKey": "Ctrl+Z"}""", validSource, validLocation);
try
{
// Configure to throw on errors
ConfigurationManager.ThrowOnJsonErrors = true;
// Act & Assert - attempt to update with invalid JSON
var invalidSource = "invalid.json";
var invalidLocation = ConfigLocations.AppCurrent;
var invalidJson = "{ invalid json }";
Assert.Throws<JsonException> (
() =>
sourcesManager.Load (settingsScope, invalidJson, invalidSource, invalidLocation));
// The valid source should still be there
Assert.Single (sourcesManager.Sources);
Assert.Equal (validSource, sourcesManager.Sources [validLocation]);
// The invalid source should not have been added
Assert.DoesNotContain (invalidLocation, sourcesManager.Sources.Keys);
}
finally
{
// Reset for other tests
ConfigurationManager.ThrowOnJsonErrors = false;
}
}
#endregion
}

View File

@@ -0,0 +1,98 @@
#nullable enable
using UnitTests;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.DriverTests;
public class ClipRegionTests (ITestOutputHelper output) : FakeDriverBase
{
private readonly ITestOutputHelper _output = output;
[Fact]
public void AddRune_Is_Clipped ()
{
IDriver? driver = CreateFakeDriver ();
driver.Move (0, 0);
driver.AddRune ('x');
Assert.Equal ("x", driver.Contents! [0, 0].Grapheme);
driver.Move (5, 5);
driver.AddRune ('x');
Assert.Equal ("x", driver.Contents [5, 5].Grapheme);
// Clear the contents
driver.FillRect (new Rectangle (0, 0, driver.Rows, driver.Cols), ' ');
Assert.Equal (" ", driver.Contents [0, 0].Grapheme);
// Setup the region with a single rectangle, fill screen with 'x'
driver.Clip = new (new Rectangle (5, 5, 5, 5));
driver.FillRect (new Rectangle (0, 0, driver.Rows, driver.Cols), 'x');
Assert.Equal (" ", driver.Contents [0, 0].Grapheme);
Assert.Equal (" ", driver.Contents [4, 9].Grapheme);
Assert.Equal ("x", driver.Contents [5, 5].Grapheme);
Assert.Equal ("x", driver.Contents [9, 9].Grapheme);
Assert.Equal (" ", driver.Contents [10, 10].Grapheme);
}
[Fact]
public void Clip_Set_To_Empty_AllInvalid ()
{
IDriver? driver = CreateFakeDriver ();
// Define a clip rectangle
driver.Clip = new (Rectangle.Empty);
// negative
Assert.False (driver.IsValidLocation (null!, 4, 5));
Assert.False (driver.IsValidLocation (null!, 5, 4));
Assert.False (driver.IsValidLocation (null!, 10, 9));
Assert.False (driver.IsValidLocation (null!, 9, 10));
Assert.False (driver.IsValidLocation (null!, -1, 0));
Assert.False (driver.IsValidLocation (null!, 0, -1));
Assert.False (driver.IsValidLocation (null!, -1, -1));
Assert.False (driver.IsValidLocation (null!, driver.Cols, driver.Rows - 1));
Assert.False (driver.IsValidLocation (null!, driver.Cols, driver.Rows - 1));
Assert.False (driver.IsValidLocation (null!, driver.Cols, driver.Rows));
}
[Fact]
public void IsValidLocation ()
{
IDriver? driver = CreateFakeDriver ();
driver.Rows = 10;
driver.Cols = 10;
// positive
Assert.True (driver.IsValidLocation (null!, 0, 0));
Assert.True (driver.IsValidLocation (null!, 1, 1));
Assert.True (driver.IsValidLocation (null!, driver.Cols - 1, driver.Rows - 1));
// negative
Assert.False (driver.IsValidLocation (null!, -1, 0));
Assert.False (driver.IsValidLocation (null!, 0, -1));
Assert.False (driver.IsValidLocation (null!, -1, -1));
Assert.False (driver.IsValidLocation (null!, driver.Cols, driver.Rows - 1));
Assert.False (driver.IsValidLocation (null!, driver.Cols, driver.Rows - 1));
Assert.False (driver.IsValidLocation (null!, driver.Cols, driver.Rows));
// Define a clip rectangle
driver.Clip = new (new Rectangle (5, 5, 5, 5));
// positive
Assert.True (driver.IsValidLocation (null!, 5, 5));
Assert.True (driver.IsValidLocation (null!, 9, 9));
// negative
Assert.False (driver.IsValidLocation (null!, 4, 5));
Assert.False (driver.IsValidLocation (null!, 5, 4));
Assert.False (driver.IsValidLocation (null!, 10, 9));
Assert.False (driver.IsValidLocation (null!, 9, 10));
Assert.False (driver.IsValidLocation (null!, -1, 0));
Assert.False (driver.IsValidLocation (null!, 0, -1));
Assert.False (driver.IsValidLocation (null!, -1, -1));
Assert.False (driver.IsValidLocation (null!, driver.Cols, driver.Rows - 1));
Assert.False (driver.IsValidLocation (null!, driver.Cols, driver.Rows - 1));
Assert.False (driver.IsValidLocation (null!, driver.Cols, driver.Rows));
}
}

View File

@@ -1,12 +1,12 @@
using UnitTests;
#nullable enable
using UnitTests;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.DriverTests;
public class DriverTests : FakeDriverBase
public class DriverTests (ITestOutputHelper output) : FakeDriverBase
{
[Theory]
[InlineData (null, true)]
[InlineData ("", true)]
[InlineData ("a", true)]
[InlineData ("👩‍❤️‍💋‍👨", false)]
@@ -49,4 +49,58 @@ public class DriverTests : FakeDriverBase
driver.End ();
}
[Theory]
[InlineData ("fake")]
[InlineData ("windows")]
[InlineData ("dotnet")]
[InlineData ("unix")]
public void All_Drivers_Init_Shutdown_Cross_Platform (string driverName)
{
IApplication? app = Application.Create ();
app.Init (driverName);
app.Shutdown ();
}
[Theory]
[InlineData ("fake")]
[InlineData ("windows")]
[InlineData ("dotnet")]
[InlineData ("unix")]
public void All_Drivers_Run_Cross_Platform (string driverName)
{
IApplication? app = Application.Create ();
app.Init (driverName);
app.StopAfterFirstIteration = true;
app.Run ().Dispose ();
app.Shutdown ();
}
[Theory]
[InlineData ("fake")]
[InlineData ("windows")]
[InlineData ("dotnet")]
[InlineData ("unix")]
public void All_Drivers_LayoutAndDraw_Cross_Platform (string driverName)
{
IApplication? app = Application.Create ();
app.Init (driverName);
app.StopAfterFirstIteration = true;
app.Run<TestTop> ().Dispose ();
DriverAssert.AssertDriverContentsWithFrameAre (driverName!, output, app.Driver);
app.Shutdown ();
}
}
public class TestTop : Toplevel
{
/// <inheritdoc/>
public override void BeginInit ()
{
Text = Driver!.GetName ()!;
BorderStyle = LineStyle.None;
base.BeginInit ();
}
}

View File

@@ -67,7 +67,7 @@ public class ToAnsiTests : FakeDriverBase
Assert.Contains ("Blue", ansi);
}
[Theory]
[Theory (Skip = "Uses Application.")]
[InlineData (false, "\u001b[48;2;")]
[InlineData (true, "\u001b[41m")]
public void ToAnsi_With_Background_Colors (bool force16Colors, string expected)
@@ -204,7 +204,7 @@ public class ToAnsiTests : FakeDriverBase
Assert.Equal (50, ansi.Count (c => c == '\n'));
}
[Fact]
[Fact (Skip = "Use Application.")]
public void ToAnsi_RGB_Colors ()
{
IDriver driver = CreateFakeDriver (10, 1);
@@ -228,7 +228,7 @@ public class ToAnsiTests : FakeDriverBase
}
}
[Fact]
[Fact (Skip = "Use Application.")]
public void ToAnsi_Force16Colors ()
{
IDriver driver = CreateFakeDriver (10, 1);

View File

@@ -32,14 +32,16 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
// Act - Simulate a complete click: press → release → click
processor.EnqueueMouseEvent (
new()
null,
new ()
{
Position = new (10, 5),
Flags = MouseFlags.Button1Pressed
});
processor.EnqueueMouseEvent (
new()
null,
new ()
{
Position = new (10, 5),
Flags = MouseFlags.Button1Released
@@ -89,7 +91,8 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
for (var i = 0; i < eventsPerThread; i++)
{
processor.EnqueueMouseEvent (
new()
null,
new ()
{
Position = new (threadId, i),
Flags = MouseFlags.Button1Clicked
@@ -160,7 +163,7 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
};
// Act
processor.EnqueueMouseEvent (mouseEvent);
processor.EnqueueMouseEvent (null, mouseEvent);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
@@ -196,7 +199,7 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
// Act
foreach (MouseEventArgs mouseEvent in events)
{
processor.EnqueueMouseEvent (mouseEvent);
processor.EnqueueMouseEvent (null, mouseEvent);
}
SimulateInputThread (fakeInput, queue);
@@ -216,9 +219,9 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Pressed && e.Position == new Point (10, 5));
Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Released && e.Position == new Point (10, 5));
Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.ReportMousePosition && e.Position == new Point (15, 8));
// There should be two clicked events: one generated, one original
var clickedEvents = receivedEvents.Where (e => e.Flags == MouseFlags.Button1Clicked).ToList ();
List<MouseEventArgs> clickedEvents = receivedEvents.Where (e => e.Flags == MouseFlags.Button1Clicked).ToList ();
Assert.Equal (2, clickedEvents.Count);
Assert.Contains (clickedEvents, e => e.Position == new Point (10, 5)); // Generated from press+release
Assert.Contains (clickedEvents, e => e.Position == new Point (20, 10)); // Original
@@ -251,7 +254,7 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
processor.MouseEvent += (_, e) => receivedEvent = e;
// Act
processor.EnqueueMouseEvent (mouseEvent);
processor.EnqueueMouseEvent (null, mouseEvent);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
@@ -285,7 +288,7 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
processor.MouseEvent += (_, e) => receivedEvent = e;
// Act
processor.EnqueueMouseEvent (mouseEvent);
processor.EnqueueMouseEvent (null, mouseEvent);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
@@ -323,7 +326,7 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
processor.MouseEvent += (_, e) => receivedEvent = e;
// Act
processor.EnqueueMouseEvent (mouseEvent);
processor.EnqueueMouseEvent (null, mouseEvent);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
@@ -372,7 +375,7 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
processor.MouseEvent += (_, e) => receivedEvent = e;
// Act
processor.EnqueueMouseEvent (mouseEvent);
processor.EnqueueMouseEvent (null, mouseEvent);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
@@ -405,7 +408,7 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
// Act
foreach (MouseEventArgs mouseEvent in events)
{
processor.EnqueueMouseEvent (mouseEvent);
processor.EnqueueMouseEvent (null, mouseEvent);
}
SimulateInputThread (fakeInput, queue);
@@ -435,7 +438,8 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
Exception? exception = Record.Exception (() =>
{
processor.EnqueueMouseEvent (
new()
null,
new ()
{
Position = new (10, 5),
Flags = MouseFlags.Button1Clicked
@@ -462,9 +466,9 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
processor.MouseEvent += (_, e) => receivedEvents.Add (e);
// Act - Enqueue multiple events before processing
processor.EnqueueMouseEvent (new() { Position = new (1, 1), Flags = MouseFlags.Button1Pressed });
processor.EnqueueMouseEvent (new() { Position = new (2, 2), Flags = MouseFlags.ReportMousePosition });
processor.EnqueueMouseEvent (new() { Position = new (3, 3), Flags = MouseFlags.Button1Released });
processor.EnqueueMouseEvent (null, new () { Position = new (1, 1), Flags = MouseFlags.Button1Pressed });
processor.EnqueueMouseEvent (null, new () { Position = new (2, 2), Flags = MouseFlags.ReportMousePosition });
processor.EnqueueMouseEvent (null, new () { Position = new (3, 3), Flags = MouseFlags.Button1Released });
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
@@ -492,7 +496,7 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
// Act & Assert - Empty/default mouse event should not throw
Exception? exception = Record.Exception (() =>
{
processor.EnqueueMouseEvent (new ());
processor.EnqueueMouseEvent (null, new ());
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
});
@@ -515,7 +519,8 @@ public class EnqueueMouseEventTests (ITestOutputHelper output)
Exception? exception = Record.Exception (() =>
{
processor.EnqueueMouseEvent (
new()
null,
new ()
{
Position = new (-10, -5),
Flags = MouseFlags.Button1Clicked

View File

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

View File

@@ -1,8 +1,6 @@
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.InputTests;
namespace UnitTests_Parallelizable.InputTests;
public class KeyBindingsTests ()
public class KeyBindingsTests
{
[Fact]
public void Add_Adds ()
@@ -28,7 +26,7 @@ public class KeyBindingsTests ()
[Fact]
public void Add_Invalid_Key_Throws ()
{
var keyBindings = new KeyBindings (new View ());
var keyBindings = new KeyBindings (new ());
List<Command> commands = new ();
Assert.Throws<ArgumentException> (() => keyBindings.Add (Key.Empty, Command.Accept));
}
@@ -71,40 +69,39 @@ public class KeyBindingsTests ()
Assert.Contains (Command.HotKey, resultCommands);
}
// Add should not allow duplicates
[Fact]
public void Add_Throws_If_Exists ()
{
var keyBindings = new KeyBindings (new View ());
var keyBindings = new KeyBindings (new ());
keyBindings.Add (Key.A, Command.HotKey);
Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, Command.Accept));
Command [] resultCommands = keyBindings.GetCommands (Key.A);
Assert.Contains (Command.HotKey, resultCommands);
keyBindings = new (new View ());
keyBindings = new (new ());
keyBindings.Add (Key.A, Command.HotKey);
Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, Command.Accept));
resultCommands = keyBindings.GetCommands (Key.A);
Assert.Contains (Command.HotKey, resultCommands);
keyBindings = new (new View ());
keyBindings = new (new ());
keyBindings.Add (Key.A, Command.HotKey);
Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, Command.Accept));
resultCommands = keyBindings.GetCommands (Key.A);
Assert.Contains (Command.HotKey, resultCommands);
keyBindings = new (new View ());
keyBindings = new (new ());
keyBindings.Add (Key.A, Command.Accept);
Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, Command.ScrollDown));
resultCommands = keyBindings.GetCommands (Key.A);
Assert.Contains (Command.Accept, resultCommands);
keyBindings = new (new View ());
keyBindings = new (new ());
keyBindings.Add (Key.A, new KeyBinding ([Command.HotKey]));
Assert.Throws<InvalidOperationException> (() => keyBindings.Add (Key.A, new KeyBinding (new [] { Command.Accept })));
@@ -142,6 +139,23 @@ public class KeyBindingsTests ()
Assert.Throws<InvalidOperationException> (() => keyBindings.Get (Key.B));
}
[Fact]
public void Get_Gets ()
{
var keyBindings = new KeyBindings (new ());
Command [] commands = [Command.Right, Command.Left];
var key = new Key (Key.A);
keyBindings.Add (key, commands);
KeyBinding binding = keyBindings.Get (key);
Assert.Contains (Command.Right, binding.Commands);
Assert.Contains (Command.Left, binding.Commands);
binding = keyBindings.Get (key);
Assert.Contains (Command.Right, binding.Commands);
Assert.Contains (Command.Left, binding.Commands);
}
// GetCommands
[Fact]
public void GetCommands_Unknown_ReturnsEmpty ()
@@ -230,6 +244,27 @@ public class KeyBindingsTests ()
Assert.Equal (Key.A, resultKey);
}
[Fact]
public void ReplaceCommands_Replaces ()
{
var keyBindings = new KeyBindings (new ());
keyBindings.Add (Key.A, Command.Accept);
keyBindings.ReplaceCommands (Key.A, Command.Refresh);
bool result = keyBindings.TryGet (Key.A, out KeyBinding bindings);
Assert.True (result);
Assert.Contains (Command.Refresh, bindings.Commands);
}
[Fact]
public void ReplaceKey_Adds_If_DoesNotContain_Old ()
{
var keyBindings = new KeyBindings (new ());
keyBindings.Replace (Key.A, Key.B);
Assert.True (keyBindings.TryGet (Key.B, out _));
}
[Fact]
public void ReplaceKey_Replaces ()
{
@@ -268,14 +303,6 @@ public class KeyBindingsTests ()
Assert.Contains (Command.Accept, keyBindings.GetCommands (Key.C));
}
[Fact]
public void ReplaceKey_Adds_If_DoesNotContain_Old ()
{
var keyBindings = new KeyBindings (new ());
keyBindings.Replace (Key.A, Key.B);
Assert.True (keyBindings.TryGet (Key.B, out _));
}
[Fact]
public void ReplaceKey_Throws_If_New_Is_Empty ()
{
@@ -284,23 +311,6 @@ public class KeyBindingsTests ()
Assert.Throws<ArgumentException> (() => keyBindings.Replace (Key.A, Key.Empty));
}
[Fact]
public void Get_Gets ()
{
var keyBindings = new KeyBindings (new ());
Command [] commands = [Command.Right, Command.Left];
var key = new Key (Key.A);
keyBindings.Add (key, commands);
KeyBinding binding = keyBindings.Get (key);
Assert.Contains (Command.Right, binding.Commands);
Assert.Contains (Command.Left, binding.Commands);
binding = keyBindings.Get (key);
Assert.Contains (Command.Right, binding.Commands);
Assert.Contains (Command.Left, binding.Commands);
}
// TryGet
[Fact]
public void TryGet_Succeeds ()
@@ -309,7 +319,8 @@ public class KeyBindingsTests ()
keyBindings.Add (Key.Q.WithCtrl, Command.HotKey);
var key = new Key (Key.Q.WithCtrl);
bool result = keyBindings.TryGet (key, out KeyBinding _);
Assert.True (result); ;
Assert.True (result);
;
}
[Fact]
@@ -329,18 +340,4 @@ public class KeyBindingsTests ()
Assert.True (result);
Assert.Contains (Command.HotKey, bindings.Commands);
}
[Fact]
public void ReplaceCommands_Replaces ()
{
var keyBindings = new KeyBindings (new ());
keyBindings.Add (Key.A, Command.Accept);
keyBindings.ReplaceCommands (Key.A, Command.Refresh);
bool result = keyBindings.TryGet (Key.A, out KeyBinding bindings);
Assert.True (result);
Assert.Contains (Command.Refresh, bindings.Commands);
}
}

View File

@@ -14,16 +14,13 @@ This project contains unit tests that can run in parallel without interference.
- ✅ Use `View.BeginInit()` / `View.EndInit()` for initialization
### Tests CANNOT be parallelized if they:
- ❌ Use `[AutoInitShutdown]` - requires `Application.Init/Shutdown` which creates global state
- ❌ Use `[AutoInitShutdown]` or `[SetupFakeApplication]`- requires `Application.Init/Shutdown` which creates global state
- ❌ Set `Application.Driver` (global singleton)
- ❌ Call `Application.Init()`, `Application.Run/Run<T>()`, or `Application.Begin()`
-Modify `ConfigurationManager` global state (Enable/Load/Apply/Disable)
-Enable `ConfigurationManager` (Enable/Load/Apply/Disable)
- ❌ Access `ConfigurationManager` including `ThemeManager` and `SchemeManager` - these rely on global state
- ❌ Access `SchemeManager.GetSchemes()` or dictionary lookups like `schemes["Base"]` - requires module initialization
- ❌ Access `View.Schemes` - there can be weird interactions with xunit and dotnet module initialization such that tests run before module initialization sets up the Schemes array
- ❌ Modify static properties like `Key.Separator`, `CultureInfo.CurrentCulture`, etc.
- ❌ Set static members on View subclasses (e.g., configuration properties like `Dialog.DefaultButtonAlignment`) or any static fields/properties - these are shared across all parallel tests
- ❌ Use `Application.Top`, `Application.Driver`, `Application.MainLoop`, or `Application.Navigation`
- ❌ Are true integration tests that test multiple components working together
### Important Notes
@@ -35,7 +32,7 @@ This project contains unit tests that can run in parallel without interference.
## How to Migrate Tests
1. **Identify** tests in `UnitTests` that don't actually need Application statics
2. **Rewrite** tests to remove `[AutoInitShutdown]`, `Application.Begin()`, etc. if not needed
2. **Rewrite** tests to remove `[AutoInitShutdown]` or `[SetupFakeApplication]`, `Application.Begin()`, etc. if not needed
3. **Move** the test to the equivalent file in `UnitTests.Parallelizable`
4. **Delete** the old test from `UnitTests` to avoid duplicates
5. **Verify** no duplicate test names exist (CI will check this)
@@ -62,11 +59,11 @@ public void Constructor_Sets_Defaults ()
}
```
### Remove Unnecessary [SetupFakeDriver]
### Remove Unnecessary [SetupFakeApplication]
```csharp
// Before (in UnitTests)
[Fact]
[SetupFakeDriver]
[SetupFakeApplication]
public void Event_Fires_When_Property_Changes ()
{
var view = new Button ();
@@ -127,5 +124,5 @@ dotnet test Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj
```
## See Also
- [Category A Migration Summary](../CATEGORY_A_MIGRATION_SUMMARY.md) - Detailed analysis and migration guidelines
- [.NET Unit Testing Best Practices](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices)

View File

@@ -0,0 +1,20 @@
using System.Globalization;
using System.Reflection;
using Xunit.Sdk;
namespace UnitTests_Parallelizable;
[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method)]
public class TestDateAttribute : BeforeAfterTestAttribute
{
public TestDateAttribute () { CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; }
private readonly CultureInfo _currentCulture = CultureInfo.CurrentCulture;
public override void After (MethodInfo methodUnderTest)
{
CultureInfo.CurrentCulture = _currentCulture;
Assert.Equal (CultureInfo.CurrentCulture, _currentCulture);
}
public override void Before (MethodInfo methodUnderTest) { Assert.Equal (CultureInfo.CurrentCulture, CultureInfo.InvariantCulture); }
}

View File

@@ -24,7 +24,7 @@ public class GlobalTestSetup : IDisposable
// Reset application state just in case a test changed something.
// TODO: Add an Assert to ensure none of the state of Application changed.
// TODO: Add an Assert to ensure none of the state of ConfigurationManager changed.
Application.ResetState (true);
//Application.ResetState (true);
CheckDefaultState ();
}
@@ -39,15 +39,15 @@ public class GlobalTestSetup : IDisposable
// Check that all Application fields and properties are set to their default values
// Public Properties
Assert.Null (Application.TopRunnable);
Assert.Null (Application.Mouse.MouseGrabView);
//Assert.Null (Application.TopRunnable);
//Assert.Null (Application.Mouse.MouseGrabView);
// Don't check Application.ForceDriver
Assert.Empty (Application.ForceDriver);
// Don't check Application.Force16Colors
//Assert.False (Application.Force16Colors);
Assert.Null (Application.Driver);
Assert.False (Application.StopAfterFirstIteration);
//// Don't check Application.ForceDriver
//Assert.Empty (Application.ForceDriver);
//// Don't check Application.Force16Colors
////Assert.False (Application.Force16Colors);
//Assert.Null (Application.Driver);
//Assert.False (Application.StopAfterFirstIteration);
Assert.Equal (Key.Tab.WithShift, Application.PrevTabKey);
Assert.Equal (Key.Tab, Application.NextTabKey);
Assert.Equal (Key.F6.WithShift, Application.PrevTabGroupKey);
@@ -55,12 +55,12 @@ public class GlobalTestSetup : IDisposable
Assert.Equal (Key.Esc, Application.QuitKey);
// Internal properties
Assert.False (Application.Initialized);
Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures);
Assert.Equal (Application.GetAvailableCulturesFromEmbeddedResources (), Application.SupportedCultures);
Assert.Null (Application.MainThreadId);
Assert.Empty (Application.SessionStack);
Assert.Empty (Application.CachedViewsUnderMouse);
//Assert.False (Application.Initialized);
//Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures);
//Assert.Equal (Application.GetAvailableCulturesFromEmbeddedResources (), Application.SupportedCultures);
//Assert.Null (Application.MainThreadId);
//Assert.Empty (Application.SessionStack);
//Assert.Empty (Application.CachedViewsUnderMouse);
// Mouse
// Do not reset _lastMousePosition

View File

@@ -1,4 +1,6 @@
using Moq;
using System.Collections;
using System.Collections.Concurrent;
using Moq;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.TextTests;
@@ -41,7 +43,7 @@ public class CollectionNavigatorTests
Assert.Equal (2, n.GetNextMatchingItem (4, 'b'));
// cycling with 'a'
n = new CollectionNavigator (simpleStrings);
n = new (simpleStrings);
Assert.Equal (0, n.GetNextMatchingItem (null, 'a'));
Assert.Equal (1, n.GetNextMatchingItem (0, 'a'));
@@ -340,6 +342,7 @@ public class CollectionNavigatorTests
current = n.GetNextMatchingItem (current, ' ')
); // match bates hotel
}
[Fact]
public void CustomMatcher_NeverMatches ()
{
@@ -347,7 +350,7 @@ public class CollectionNavigatorTests
int? current = 0;
var n = new CollectionNavigator (strings);
var matchNone = new Mock<ICollectionNavigatorMatcher> ();
Mock<ICollectionNavigatorMatcher> matchNone = new ();
matchNone.Setup (m => m.IsMatch (It.IsAny<string> (), It.IsAny<object> ()))
.Returns (false);
@@ -358,4 +361,246 @@ public class CollectionNavigatorTests
Assert.Equal (0, current = n.GetNextMatchingItem (current, 'a')); // no matches
Assert.Equal (0, current = n.GetNextMatchingItem (current, 't')); // no matches
}
#region Thread Safety Tests
[Fact]
public void ThreadSafety_ConcurrentSearchStringAccess ()
{
var strings = new [] { "apricot", "arm", "bat", "batman", "candle" };
var navigator = new CollectionNavigator (strings);
var numTasks = 20;
ConcurrentBag<Exception> exceptions = new ();
Parallel.For (
0,
numTasks,
i =>
{
try
{
// Read SearchString concurrently
string searchString = navigator.SearchString;
// Perform navigation operations concurrently
int? result = navigator.GetNextMatchingItem (0, 'a');
// Read SearchString again
searchString = navigator.SearchString;
}
catch (Exception ex)
{
exceptions.Add (ex);
}
});
Assert.Empty (exceptions);
}
[Fact]
public void ThreadSafety_ConcurrentCollectionAccess ()
{
var strings = new [] { "apricot", "arm", "bat", "batman", "candle" };
var navigator = new CollectionNavigator (strings);
var numTasks = 20;
ConcurrentBag<Exception> exceptions = new ();
Parallel.For (
0,
numTasks,
i =>
{
try
{
// Access Collection property concurrently
IList collection = navigator.Collection;
// Perform navigation
int? result = navigator.GetNextMatchingItem (0, (char)('a' + i % 3));
}
catch (Exception ex)
{
exceptions.Add (ex);
}
});
Assert.Empty (exceptions);
}
[Fact]
public void ThreadSafety_ConcurrentNavigationOperations ()
{
var strings = new [] { "apricot", "arm", "bat", "batman", "candle", "cat", "dog", "elephant" };
var navigator = new CollectionNavigator (strings);
var numTasks = 50;
ConcurrentBag<int?> results = new ();
ConcurrentBag<Exception> exceptions = new ();
Parallel.For (
0,
numTasks,
i =>
{
try
{
var searchChar = (char)('a' + i % 5);
int? result = navigator.GetNextMatchingItem (i % strings.Length, searchChar);
results.Add (result);
}
catch (Exception ex)
{
exceptions.Add (ex);
}
});
Assert.Empty (exceptions);
Assert.Equal (numTasks, results.Count);
}
[Fact]
public void ThreadSafety_ConcurrentCollectionModification ()
{
var strings = new [] { "apricot", "arm", "bat", "batman", "candle" };
var navigator = new CollectionNavigator (strings);
var numReaders = 10;
var numWriters = 5;
ConcurrentBag<Exception> exceptions = new ();
List<Task> tasks = new ();
// Reader tasks
for (var i = 0; i < numReaders; i++)
{
tasks.Add (
Task.Run (() =>
{
try
{
for (var j = 0; j < 100; j++)
{
int? result = navigator.GetNextMatchingItem (0, 'a');
string searchString = navigator.SearchString;
}
}
catch (Exception ex)
{
exceptions.Add (ex);
}
}));
}
// Writer tasks (change Collection reference)
for (var i = 0; i < numWriters; i++)
{
int writerIndex = i;
tasks.Add (
Task.Run (() =>
{
try
{
for (var j = 0; j < 50; j++)
{
var newStrings = new [] { $"item{writerIndex}_{j}_1", $"item{writerIndex}_{j}_2" };
navigator.Collection = newStrings;
Thread.Sleep (1); // Small delay to increase contention
}
}
catch (Exception ex)
{
exceptions.Add (ex);
}
}));
}
#pragma warning disable xUnit1031
Task.WaitAll (tasks.ToArray ());
#pragma warning restore xUnit1031
// Allow some exceptions due to collection being swapped during access
// but verify no deadlocks occurred (all tasks completed)
Assert.True (tasks.All (t => t.IsCompleted));
}
[Fact]
public void ThreadSafety_ConcurrentSearchStringChanges ()
{
var strings = new [] { "apricot", "arm", "bat", "batman", "candle", "cat", "dog", "elephant", "fox", "goat" };
var navigator = new CollectionNavigator (strings);
var numTasks = 30;
ConcurrentBag<Exception> exceptions = new ();
ConcurrentBag<string> searchStrings = new ();
Parallel.For (
0,
numTasks,
i =>
{
try
{
// Each task performs multiple searches rapidly
char [] chars = { 'a', 'b', 'c', 'd', 'e', 'f' };
foreach (char c in chars)
{
navigator.GetNextMatchingItem (0, c);
searchStrings.Add (navigator.SearchString);
}
}
catch (Exception ex)
{
exceptions.Add (ex);
}
});
Assert.Empty (exceptions);
Assert.NotEmpty (searchStrings);
}
[Fact]
public void ThreadSafety_StressTest_RapidOperations ()
{
var strings = new string [100];
for (var i = 0; i < 100; i++)
{
strings [i] = $"item_{i:D3}";
}
var navigator = new CollectionNavigator (strings);
var numTasks = 100;
var operationsPerTask = 1000;
ConcurrentBag<Exception> exceptions = new ();
Parallel.For (
0,
numTasks,
i =>
{
try
{
var random = new Random (i);
for (var j = 0; j < operationsPerTask; j++)
{
int? currentIndex = random.Next (0, strings.Length);
var searchChar = (char)('a' + random.Next (0, 26));
navigator.GetNextMatchingItem (currentIndex, searchChar);
if (j % 100 == 0)
{
string searchString = navigator.SearchString;
}
}
}
catch (Exception ex)
{
exceptions.Add (ex);
}
});
Assert.Empty (exceptions);
}
#endregion Thread Safety Tests
}

View File

@@ -65,23 +65,18 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Assert.Equal (original, previous);
Assert.NotEqual (original, driver.Clip);
Application.ResetState (true);
}
[Fact]
public void SetClipToScreen_SetsClipToScreen ()
{
IDriver driver = CreateFakeDriver (80, 25);
Application.Driver = driver;
View view = new () { Driver = driver };
view.SetClipToScreen ();
Assert.NotNull (driver.Clip);
Assert.Equal (driver.Screen, driver.Clip.GetBounds ());
Application.ResetState (true);
}
#endregion
@@ -94,8 +89,6 @@ public class ViewDrawingClippingTests () : FakeDriverBase
View view = new () { Driver = null };
var exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (5, 5, 10, 10)));
Assert.Null (exception);
Application.ResetState (true);
}
[Fact]
@@ -103,7 +96,6 @@ public class ViewDrawingClippingTests () : FakeDriverBase
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (new Rectangle (0, 0, 80, 25));
Application.Driver = driver;
View view = new () { Driver = driver };
var toExclude = new Rectangle (10, 10, 20, 20);
@@ -112,8 +104,6 @@ public class ViewDrawingClippingTests () : FakeDriverBase
// Verify the region was excluded
Assert.NotNull (driver.Clip);
Assert.False (driver.Clip.Contains (15, 15));
Application.ResetState (true);
}
[Fact]
@@ -123,8 +113,6 @@ public class ViewDrawingClippingTests () : FakeDriverBase
var exception = Record.Exception (() => view.ExcludeFromClip (new Region (new Rectangle (5, 5, 10, 10))));
Assert.Null (exception);
Application.ResetState (true);
}
[Fact]
@@ -141,8 +129,6 @@ public class ViewDrawingClippingTests () : FakeDriverBase
// Verify the region was excluded
Assert.NotNull (driver.Clip);
Assert.False (driver.Clip.Contains (15, 15));
Application.ResetState (true);
}
#endregion
@@ -538,7 +524,6 @@ public class ViewDrawingClippingTests () : FakeDriverBase
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
Application.Driver = driver;
var view = new View
{
@@ -558,7 +543,6 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Assert.NotNull (driver.Clip);
Assert.False (driver.Clip.Contains (20, 20)); // Point inside excluded rect should not be in clip
Application.ResetState (true);
}
[Fact]
@@ -566,7 +550,6 @@ public class ViewDrawingClippingTests () : FakeDriverBase
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = null!;
Application.Driver = driver;
var view = new View
{
@@ -581,7 +564,6 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Assert.Null (exception);
Application.ResetState (true);
}
#endregion

View File

@@ -613,7 +613,6 @@ public class ViewDrawingFlowTests () : FakeDriverBase
IDriver driver = CreateFakeDriver (80, 25);
var initialClip = new Region (driver.Screen);
driver.Clip = initialClip;
Application.Driver = driver;
var view = new View
{
@@ -636,8 +635,6 @@ public class ViewDrawingFlowTests () : FakeDriverBase
// Points inside the view should be excluded
// Note: This test depends on the DrawContext tracking, which may not exclude if nothing was actually drawn
// We're verifying the mechanism exists, not that it necessarily excludes in this specific case
Application.ResetState (true);
}
#endregion

View File

@@ -0,0 +1,367 @@
#nullable enable
using System.Globalization;
using System.Runtime.InteropServices;
namespace UnitTests_Parallelizable.ViewsTests;
public class DateFieldTests
{
[Fact]
[TestDate]
public void Constructors_Defaults ()
{
var df = new DateField ();
df.Layout ();
Assert.Equal (DateTime.MinValue, df.Date);
Assert.Equal (1, df.CursorPosition);
Assert.Equal (new (0, 0, 12, 1), df.Frame);
Assert.Equal (" 01/01/0001", df.Text);
DateTime date = DateTime.Now;
df = new (date);
df.Layout ();
Assert.Equal (date, df.Date);
Assert.Equal (1, df.CursorPosition);
Assert.Equal (new (0, 0, 12, 1), df.Frame);
Assert.Equal ($" {date.ToString (CultureInfo.InvariantCulture.DateTimeFormat.ShortDatePattern)}", df.Text);
df = new (date) { X = 1, Y = 2 };
df.Layout ();
Assert.Equal (date, df.Date);
Assert.Equal (1, df.CursorPosition);
Assert.Equal (new (1, 2, 12, 1), df.Frame);
Assert.Equal ($" {date.ToString (CultureInfo.InvariantCulture.DateTimeFormat.ShortDatePattern)}", df.Text);
}
[Fact]
[TestDate]
public void Copy_Paste ()
{
IApplication app = Application.Create();
app.Init("fake");
try
{
var df1 = new DateField (DateTime.Parse ("12/12/1971")) { App = app };
var df2 = new DateField (DateTime.Parse ("12/31/2023")) { App = app };
// Select all text
Assert.True (df2.NewKeyDownEvent (Key.End.WithShift));
Assert.Equal (1, df2.SelectedStart);
Assert.Equal (10, df2.SelectedLength);
Assert.Equal (11, df2.CursorPosition);
// Copy from df2
Assert.True (df2.NewKeyDownEvent (Key.C.WithCtrl));
// Paste into df1
Assert.True (df1.NewKeyDownEvent (Key.V.WithCtrl));
Assert.Equal (" 12/31/2023", df1.Text);
Assert.Equal (11, df1.CursorPosition);
}
finally
{
app.Shutdown();
}
}
[Fact]
[TestDate]
public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format ()
{
var df = new DateField ();
Assert.Equal (1, df.CursorPosition);
df.CursorPosition = 0;
Assert.Equal (1, df.CursorPosition);
df.CursorPosition = 11;
Assert.Equal (10, df.CursorPosition);
}
[Fact]
[TestDate]
public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format_After_Selection ()
{
var df = new DateField ();
// Start selection
Assert.True (df.NewKeyDownEvent (Key.CursorLeft.WithShift));
Assert.Equal (1, df.SelectedStart);
Assert.Equal (1, df.SelectedLength);
Assert.Equal (0, df.CursorPosition);
// Without selection
Assert.True (df.NewKeyDownEvent (Key.CursorLeft));
Assert.Equal (-1, df.SelectedStart);
Assert.Equal (0, df.SelectedLength);
Assert.Equal (1, df.CursorPosition);
df.CursorPosition = 10;
Assert.True (df.NewKeyDownEvent (Key.CursorRight.WithShift));
Assert.Equal (10, df.SelectedStart);
Assert.Equal (1, df.SelectedLength);
Assert.Equal (11, df.CursorPosition);
Assert.True (df.NewKeyDownEvent (Key.CursorRight));
Assert.Equal (-1, df.SelectedStart);
Assert.Equal (0, df.SelectedLength);
Assert.Equal (10, df.CursorPosition);
}
[Fact]
[TestDate]
public void Date_Start_From_01_01_0001_And_End_At_12_31_9999 ()
{
var df = new DateField (DateTime.Parse ("01/01/0001"));
Assert.Equal (" 01/01/0001", df.Text);
df.Date = DateTime.Parse ("12/31/9999");
Assert.Equal (" 12/31/9999", df.Text);
}
[Fact]
[TestDate]
public void KeyBindings_Command ()
{
var df = new DateField (DateTime.Parse ("12/12/1971")) { ReadOnly = true };
Assert.True (df.NewKeyDownEvent (Key.Delete));
Assert.Equal (" 12/12/1971", df.Text);
df.ReadOnly = false;
Assert.True (df.NewKeyDownEvent (Key.D.WithCtrl));
Assert.Equal (" 02/12/1971", df.Text);
df.CursorPosition = 4;
df.ReadOnly = true;
Assert.True (df.NewKeyDownEvent (Key.Delete));
Assert.Equal (" 02/12/1971", df.Text);
df.ReadOnly = false;
Assert.True (df.NewKeyDownEvent (Key.Backspace));
Assert.Equal (" 02/02/1971", df.Text);
Assert.True (df.NewKeyDownEvent (Key.Home));
Assert.Equal (1, df.CursorPosition);
Assert.True (df.NewKeyDownEvent (Key.End));
Assert.Equal (10, df.CursorPosition);
Assert.True (df.NewKeyDownEvent (Key.E.WithCtrl));
Assert.Equal (10, df.CursorPosition);
Assert.True (df.NewKeyDownEvent (Key.CursorLeft));
Assert.Equal (9, df.CursorPosition);
Assert.True (df.NewKeyDownEvent (Key.CursorRight));
Assert.Equal (10, df.CursorPosition);
// Non-numerics are ignored
Assert.False (df.NewKeyDownEvent (Key.A));
df.ReadOnly = true;
df.CursorPosition = 1;
Assert.True (df.NewKeyDownEvent (Key.D1));
Assert.Equal (" 02/02/1971", df.Text);
df.ReadOnly = false;
Assert.True (df.NewKeyDownEvent (Key.D1));
Assert.Equal (" 12/02/1971", df.Text);
Assert.Equal (2, df.CursorPosition);
#if UNIX_KEY_BINDINGS
Assert.True (df.NewKeyDownEvent (Key.D.WithAlt));
Assert.Equal (" 10/02/1971", df.Text);
#endif
}
[Fact]
[TestDate]
public void Typing_With_Selection_Normalize_Format ()
{
var df = new DateField (DateTime.Parse ("12/12/1971"))
{
// Start selection at before the first separator /
CursorPosition = 2
};
// Now select the separator /
Assert.True (df.NewKeyDownEvent (Key.CursorRight.WithShift));
Assert.Equal (2, df.SelectedStart);
Assert.Equal (1, df.SelectedLength);
Assert.Equal (3, df.CursorPosition);
// Type 3 over the separator
Assert.True (df.NewKeyDownEvent (Key.D3));
// The format was normalized and replaced again with /
Assert.Equal (" 12/12/1971", df.Text);
Assert.Equal (4, df.CursorPosition);
}
[Fact]
[TestDate]
public void Culture_Pt_Portuguese ()
{
CultureInfo cultureBackup = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new ("pt-PT");
var df = new DateField (DateTime.Parse ("12/12/1971"))
{
// Move to the first 2
CursorPosition = 2
};
// Type 3 over the separator
Assert.True (df.NewKeyDownEvent (Key.D3));
// If InvariantCulture was used this will fail but not with PT culture
Assert.Equal (" 13/12/1971", df.Text);
Assert.Equal ("13/12/1971", df.Date!.Value.ToString (CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern));
Assert.Equal (4, df.CursorPosition);
}
finally
{
CultureInfo.CurrentCulture = cultureBackup;
}
}
/// <summary>
/// Tests specific culture date formatting edge cases.
/// Split from the monolithic culture test for better isolation and maintainability.
/// </summary>
[Theory]
[TestDate]
[InlineData ("en-US", "01/01/1971", '/')]
[InlineData ("en-GB", "01/01/1971", '/')]
[InlineData ("de-DE", "01.01.1971", '.')]
[InlineData ("fr-FR", "01/01/1971", '/')]
[InlineData ("es-ES", "01/01/1971", '/')]
[InlineData ("it-IT", "01/01/1971", '/')]
[InlineData ("ja-JP", "1971/01/01", '/')]
[InlineData ("zh-CN", "1971/01/01", '/')]
[InlineData ("ko-KR", "1971.01.01", '.')]
[InlineData ("pt-PT", "01/01/1971", '/')]
[InlineData ("pt-BR", "01/01/1971", '/')]
[InlineData ("ru-RU", "01.01.1971", '.')]
[InlineData ("nl-NL", "01-01-1971", '-')]
[InlineData ("sv-SE", "1971-01-01", '-')]
[InlineData ("pl-PL", "01.01.1971", '.')]
[InlineData ("tr-TR", "01.01.1971", '.')]
public void Culture_SpecificCultures_ProducesExpectedFormat (string cultureName, string expectedDate, char expectedSeparator)
{
// Skip cultures that may have platform-specific issues
if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
{
// macOS has known issues with certain cultures - see #3592
string [] problematicOnMac = { "ar-SA", "en-SA", "en-TH", "th", "th-TH" };
if (problematicOnMac.Contains (cultureName))
{
return;
}
}
CultureInfo cultureBackup = CultureInfo.CurrentCulture;
try
{
var culture = new CultureInfo (cultureName);
// Parse date using InvariantCulture BEFORE changing CurrentCulture
DateTime date = DateTime.Parse ("1/1/1971", CultureInfo.InvariantCulture);
CultureInfo.CurrentCulture = culture;
var df = new DateField (date);
// Verify the text contains the expected separator
Assert.Contains (expectedSeparator, df.Text);
// Verify the date is formatted correctly (accounting for leading space)
Assert.Equal ($" {expectedDate}", df.Text);
}
catch (CultureNotFoundException)
{
// Skip cultures not available on this system
}
finally
{
CultureInfo.CurrentCulture = cultureBackup;
}
}
/// <summary>
/// Tests right-to-left cultures separately due to their complexity.
/// </summary>
[Theory]
[TestDate]
[InlineData ("ar-SA")] // Arabic (Saudi Arabia)
[InlineData ("he-IL")] // Hebrew (Israel)
[InlineData ("fa-IR")] // Persian (Iran)
public void Culture_RightToLeft_HandlesFormatting (string cultureName)
{
if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
{
// macOS has known issues with RTL cultures - see #3592
return;
}
CultureInfo cultureBackup = CultureInfo.CurrentCulture;
try
{
var culture = new CultureInfo (cultureName);
// Parse date using InvariantCulture BEFORE changing CurrentCulture
// This is critical because RTL cultures may use different calendars
DateTime date = DateTime.Parse ("1/1/1971", CultureInfo.InvariantCulture);
CultureInfo.CurrentCulture = culture;
var df = new DateField (date);
// Just verify DateField doesn't crash with RTL cultures
// and produces some text
Assert.NotEmpty (df.Text);
Assert.NotNull (df.Date);
}
catch (CultureNotFoundException)
{
// Skip cultures not available on this system
}
finally
{
CultureInfo.CurrentCulture = cultureBackup;
}
}
/// <summary>
/// Tests that DateField handles calendar systems that differ from Gregorian.
/// </summary>
[Theory]
[TestDate]
[InlineData ("th-TH")] // Thai Buddhist calendar
public void Culture_NonGregorianCalendar_HandlesFormatting (string cultureName)
{
if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
{
// macOS has known issues with certain calendars - see #3592
return;
}
CultureInfo cultureBackup = CultureInfo.CurrentCulture;
try
{
var culture = new CultureInfo (cultureName);
// Parse date using InvariantCulture BEFORE changing CurrentCulture
DateTime date = DateTime.Parse ("1/1/1971", CultureInfo.InvariantCulture);
CultureInfo.CurrentCulture = culture;
var df = new DateField (date);
// Buddhist calendar is 543 years ahead (1971 + 543 = 2514)
// Just verify it doesn't crash and produces valid output
Assert.NotEmpty (df.Text);
Assert.NotNull (df.Date);
}
catch (CultureNotFoundException)
{
// Skip cultures not available on this system
}
finally
{
CultureInfo.CurrentCulture = cultureBackup;
}
}
}

View File

@@ -0,0 +1,625 @@
#nullable enable
using System.Text;
using UICatalog;
using UnitTests;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.ViewsTests;
public class MessageBoxTests (ITestOutputHelper output)
{
[Fact]
public void KeyBindings_Enter_Causes_Focused_Button_Click_No_Accept ()
{
IApplication app = Application.Create ();
app.Init ("fake");
try
{
int? result = null;
var iteration = 0;
var btnAcceptCount = 0;
app.Iteration += OnApplicationOnIteration;
app.Run<Toplevel> ().Dispose ();
app.Iteration -= OnApplicationOnIteration;
Assert.Equal (1, result);
Assert.Equal (1, btnAcceptCount);
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a)
{
iteration++;
switch (iteration)
{
case 1:
result = MessageBox.Query (app, string.Empty, string.Empty, 0, false, "btn0", "btn1");
app.RequestStop ();
break;
case 2:
// Tab to btn2
app.Keyboard.RaiseKeyDownEvent (Key.Tab);
var btn = app.Navigation!.GetFocused () as Button;
btn!.Accepting += (sender, e) => { btnAcceptCount++; };
// Click
app.Keyboard.RaiseKeyDownEvent (Key.Enter);
break;
default:
Assert.Fail ();
break;
}
}
}
finally
{
app.Shutdown ();
}
}
[Fact]
public void KeyBindings_Esc_Closes ()
{
IApplication app = Application.Create ();
app.Init ("fake");
try
{
int? result = 999;
var iteration = 0;
app.Iteration += OnApplicationOnIteration;
app.Run<Toplevel> ().Dispose ();
app.Iteration -= OnApplicationOnIteration;
Assert.Null (result);
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a)
{
iteration++;
switch (iteration)
{
case 1:
result = MessageBox.Query (app, string.Empty, string.Empty, 0, false, "btn0", "btn1");
app.RequestStop ();
break;
case 2:
app.Keyboard.RaiseKeyDownEvent (Key.Esc);
break;
default:
Assert.Fail ();
break;
}
}
}
finally
{
app.Shutdown ();
}
}
[Fact]
public void KeyBindings_Space_Causes_Focused_Button_Click_No_Accept ()
{
IApplication app = Application.Create ();
app.Init ("fake");
try
{
int? result = null;
var iteration = 0;
var btnAcceptCount = 0;
app.Iteration += OnApplicationOnIteration;
app.Run<Toplevel> ().Dispose ();
app.Iteration -= OnApplicationOnIteration;
Assert.Equal (1, result);
Assert.Equal (1, btnAcceptCount);
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a)
{
iteration++;
switch (iteration)
{
case 1:
result = MessageBox.Query (app, string.Empty, string.Empty, 0, false, "btn0", "btn1");
app.RequestStop ();
break;
case 2:
// Tab to btn2
app.Keyboard.RaiseKeyDownEvent (Key.Tab);
var btn = app.Navigation!.GetFocused () as Button;
btn!.Accepting += (sender, e) => { btnAcceptCount++; };
app.Keyboard.RaiseKeyDownEvent (Key.Space);
break;
default:
Assert.Fail ();
break;
}
}
}
finally
{
app.Shutdown ();
}
}
[Theory]
[InlineData (@"", false, false, 6, 6, 2, 2)]
[InlineData (@"", false, true, 3, 6, 9, 3)]
[InlineData (@"01234\n-----\n01234", false, false, 1, 6, 13, 3)]
[InlineData (@"01234\n-----\n01234", true, false, 1, 5, 13, 4)]
[InlineData (@"0123456789", false, false, 1, 6, 12, 3)]
[InlineData (@"0123456789", false, true, 1, 5, 12, 4)]
[InlineData (@"01234567890123456789", false, true, 1, 5, 13, 4)]
[InlineData (@"01234567890123456789", true, true, 1, 5, 13, 5)]
[InlineData (@"01234567890123456789\n01234567890123456789", false, true, 1, 5, 13, 4)]
[InlineData (@"01234567890123456789\n01234567890123456789", true, true, 1, 4, 13, 7)]
public void Location_And_Size_Correct (string message, bool wrapMessage, bool hasButton, int expectedX, int expectedY, int expectedW, int expectedH)
{
IApplication app = Application.Create ();
app.Init ("fake");
try
{
int iterations = -1;
app.Driver!.SetScreenSize (15, 15); // 15 x 15 gives us enough room for a button with one char (9x1)
Dialog.DefaultShadow = ShadowStyle.None;
Button.DefaultShadow = ShadowStyle.None;
var mbFrame = Rectangle.Empty;
app.Iteration += OnApplicationOnIteration;
app.Run<Toplevel> ().Dispose ();
app.Iteration -= OnApplicationOnIteration;
Assert.Equal (new (expectedX, expectedY, expectedW, expectedH), mbFrame);
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a)
{
iterations++;
if (iterations == 0)
{
MessageBox.Query (app, string.Empty, message, 0, wrapMessage, hasButton ? ["0"] : []);
app.RequestStop ();
}
else if (iterations == 1)
{
mbFrame = app.TopRunnable!.Frame;
app.RequestStop ();
}
}
}
finally
{
app.Shutdown ();
}
}
[Fact]
public void Message_With_Spaces_WrapMessage_False ()
{
IApplication app = Application.Create ();
app.Init ("fake");
try
{
int iterations = -1;
var top = new Toplevel ();
top.BorderStyle = LineStyle.None;
app.Driver!.SetScreenSize (20, 10);
var btn =
$"{Glyphs.LeftBracket}{Glyphs.LeftDefaultIndicator} btn {Glyphs.RightDefaultIndicator}{Glyphs.RightBracket}";
// Override CM
MessageBox.DefaultButtonAlignment = Alignment.End;
MessageBox.DefaultBorderStyle = LineStyle.Double;
Dialog.DefaultShadow = ShadowStyle.None;
Button.DefaultShadow = ShadowStyle.None;
app.Iteration += OnApplicationOnIteration;
try
{
app.Run (top);
}
finally
{
app.Iteration -= OnApplicationOnIteration;
top.Dispose ();
}
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a)
{
iterations++;
if (iterations == 0)
{
var sb = new StringBuilder ();
for (var i = 0; i < 17; i++)
{
sb.Append ("ff ");
}
MessageBox.Query (app, string.Empty, sb.ToString (), 0, false, "btn");
app.RequestStop ();
}
else if (iterations == 2)
{
DriverAssert.AssertDriverContentsWithFrameAre (
@"
╔════════════════╗
║ ff ff ff ff ff ║
║ ⟦► btn ◄⟧║
╚════════════════╝",
output,
app.Driver);
app.RequestStop ();
// Really long text
MessageBox.Query (app, string.Empty, new ('f', 500), 0, false, "btn");
}
else if (iterations == 4)
{
DriverAssert.AssertDriverContentsWithFrameAre (
@"
╔════════════════╗
║ffffffffffffffff║
║ ⟦► btn ◄⟧║
╚════════════════╝",
output,
app.Driver);
app.RequestStop ();
}
}
}
finally
{
app.Shutdown ();
}
}
[Fact]
public void Message_With_Spaces_WrapMessage_True ()
{
IApplication app = Application.Create ();
app.Init ("fake");
try
{
int iterations = -1;
var top = new Toplevel ();
top.BorderStyle = LineStyle.None;
app.Driver!.SetScreenSize (20, 10);
var btn =
$"{Glyphs.LeftBracket}{Glyphs.LeftDefaultIndicator} btn {Glyphs.RightDefaultIndicator}{Glyphs.RightBracket}";
// Override CM
MessageBox.DefaultButtonAlignment = Alignment.End;
MessageBox.DefaultBorderStyle = LineStyle.Double;
Dialog.DefaultShadow = ShadowStyle.None;
Button.DefaultShadow = ShadowStyle.None;
app.Iteration += OnApplicationOnIteration;
try
{
app.Run (top);
}
finally
{
app.Iteration -= OnApplicationOnIteration;
top.Dispose ();
}
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a)
{
iterations++;
if (iterations == 0)
{
var sb = new StringBuilder ();
for (var i = 0; i < 17; i++)
{
sb.Append ("ff ");
}
MessageBox.Query (app, string.Empty, sb.ToString (), 0, true, "btn");
app.RequestStop ();
}
else if (iterations == 2)
{
DriverAssert.AssertDriverContentsWithFrameAre (
@"
╔══════════════╗
║ff ff ff ff ff║
║ff ff ff ff ff║
║ff ff ff ff ff║
║ ff ff ║
║ ⟦► btn ◄⟧║
╚══════════════╝",
output,
app.Driver);
app.RequestStop ();
// Really long text
MessageBox.Query (app, string.Empty, new ('f', 500), 0, true, "btn");
}
else if (iterations == 4)
{
DriverAssert.AssertDriverContentsWithFrameAre (
@"
╔════════════════╗
║ffffffffffffffff║
║ffffffffffffffff║
║ffffffffffffffff║
║ffffffffffffffff║
║ffffffffffffffff║
║ffffffffffffffff║
║fffffff⟦► btn ◄⟧║
╚════════════════╝",
output,
app.Driver);
app.RequestStop ();
}
}
}
finally
{
app.Shutdown ();
}
}
[Theory (Skip = "Bogus test: Never does anything")]
[InlineData (0, 0, "1")]
[InlineData (1, 1, "1")]
[InlineData (7, 5, "1")]
[InlineData (50, 50, "1")]
[InlineData (0, 0, "message")]
[InlineData (1, 1, "message")]
[InlineData (7, 5, "message")]
[InlineData (50, 50, "message")]
public void Size_Not_Default_Message (int height, int width, string message)
{
IApplication app = Application.Create ();
app.Init ("fake");
try
{
int iterations = -1;
app.Driver!.SetScreenSize (100, 100);
app.Iteration += (s, a) =>
{
iterations++;
if (iterations == 0)
{
MessageBox.Query (app, height, width, string.Empty, message);
app.RequestStop ();
}
else if (iterations == 1)
{
Assert.IsType<Dialog> (app.TopRunnable);
Assert.Equal (new (height, width), app.TopRunnable.Frame.Size);
app.RequestStop ();
}
};
}
finally
{
app.Shutdown ();
}
}
[Theory (Skip = "Bogus test: Never does anything")]
[InlineData (0, 0, "1")]
[InlineData (1, 1, "1")]
[InlineData (7, 5, "1")]
[InlineData (50, 50, "1")]
[InlineData (0, 0, "message")]
[InlineData (1, 1, "message")]
[InlineData (7, 5, "message")]
[InlineData (50, 50, "message")]
public void Size_Not_Default_Message_Button (int height, int width, string message)
{
IApplication app = Application.Create ();
app.Init ("fake");
try
{
int iterations = -1;
app.Iteration += (s, a) =>
{
iterations++;
if (iterations == 0)
{
MessageBox.Query (app, height, width, string.Empty, message, "_Ok");
app.RequestStop ();
}
else if (iterations == 1)
{
Assert.IsType<Dialog> (app.TopRunnable);
Assert.Equal (new (height, width), app.TopRunnable.Frame.Size);
app.RequestStop ();
}
};
}
finally
{
app.Shutdown ();
}
}
[Theory (Skip = "Bogus test: Never does anything")]
[InlineData (0, 0)]
[InlineData (1, 1)]
[InlineData (7, 5)]
[InlineData (50, 50)]
public void Size_Not_Default_No_Message (int height, int width)
{
IApplication app = Application.Create ();
app.Init ("fake");
try
{
int iterations = -1;
app.Iteration += (s, a) =>
{
iterations++;
if (iterations == 0)
{
MessageBox.Query (app, height, width, string.Empty, string.Empty);
app.RequestStop ();
}
else if (iterations == 1)
{
Assert.IsType<Dialog> (app.TopRunnable);
Assert.Equal (new (height, width), app.TopRunnable.Frame.Size);
app.RequestStop ();
}
};
}
finally
{
app.Shutdown ();
}
}
[Fact]
public void UICatalog_AboutBox ()
{
IApplication app = Application.Create ();
app.Init ("fake");
try
{
int iterations = -1;
app.Driver!.SetScreenSize (70, 15);
// Override CM
MessageBox.DefaultButtonAlignment = Alignment.End;
MessageBox.DefaultBorderStyle = LineStyle.Double;
Dialog.DefaultShadow = ShadowStyle.None;
Button.DefaultShadow = ShadowStyle.None;
app.Iteration += OnApplicationOnIteration;
var top = new Toplevel ();
top.BorderStyle = LineStyle.Single;
try
{
app.Run (top);
}
finally
{
app.Iteration -= OnApplicationOnIteration;
top.Dispose ();
}
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a)
{
iterations++;
if (iterations == 0)
{
MessageBox.Query (
app,
"",
UICatalogTop.GetAboutBoxMessage (),
wrapMessage: false,
buttons: "_Ok");
app.RequestStop ();
}
else if (iterations == 2)
{
var expectedText = """
UI Catalog: A comprehensive sample library and test app for
_______ _ _ _____ _
|__ __| (_) | | / ____| (_)
| | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _
| |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | |
| | __/ | | | | | | | | | | | (_| | || |__| | |_| | |
|_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_|
v2 - Pre-Alpha
Ok
""";
DriverAssert.AssertDriverContentsAre (expectedText, output, app.Driver);
app.RequestStop ();
}
}
}
finally
{
app.Shutdown ();
}
}
[Theory]
[MemberData (nameof (AcceptingKeys))]
public void Button_IsDefault_True_Return_His_Index_On_Accepting (Key key)
{
IApplication app = Application.Create ();
app.Init ("fake");
try
{
app.Iteration += OnApplicationOnIteration;
int? res = MessageBox.Query (app, "hey", "IsDefault", "Yes", "No");
app.Iteration -= OnApplicationOnIteration;
Assert.Equal (0, res);
void OnApplicationOnIteration (object? o, EventArgs<IApplication?> iterationEventArgs) { Assert.True (app.Keyboard.RaiseKeyDownEvent (key)); }
}
finally
{
app.Shutdown ();
}
}
public static IEnumerable<object []> AcceptingKeys ()
{
yield return [Key.Enter];
yield return [Key.Space];
}
}

View File

@@ -679,7 +679,6 @@ public class ScrollSliderTests (ITestOutputHelper output) : FakeDriverBase
[Theory]
[SetupFakeApplication]
[InlineData (
3,
10,

View File

@@ -0,0 +1,205 @@
namespace UnitTests_Parallelizable.ViewsTests;
public class TimeFieldTests
{
[Fact]
public void Constructors_Defaults ()
{
var tf = new TimeField ();
tf.Layout ();
Assert.False (tf.IsShortFormat);
Assert.Equal (TimeSpan.MinValue, tf.Time);
Assert.Equal (1, tf.CursorPosition);
Assert.Equal (new Rectangle (0, 0, 10, 1), tf.Frame);
TimeSpan time = DateTime.Now.TimeOfDay;
tf = new TimeField { Time = time };
tf.Layout ();
Assert.False (tf.IsShortFormat);
Assert.Equal (time, tf.Time);
Assert.Equal (1, tf.CursorPosition);
Assert.Equal (new Rectangle (0, 0, 10, 1), tf.Frame);
tf = new TimeField { X = 1, Y = 2, Time = time };
tf.Layout ();
Assert.False (tf.IsShortFormat);
Assert.Equal (time, tf.Time);
Assert.Equal (1, tf.CursorPosition);
Assert.Equal (new Rectangle (1, 2, 10, 1), tf.Frame);
tf = new TimeField { X = 3, Y = 4, Time = time, IsShortFormat = true };
tf.Layout ();
Assert.True (tf.IsShortFormat);
Assert.Equal (time, tf.Time);
Assert.Equal (1, tf.CursorPosition);
Assert.Equal (new Rectangle (3, 4, 7, 1), tf.Frame);
tf.IsShortFormat = false;
tf.Layout ();
Assert.Equal (new Rectangle (3, 4, 10, 1), tf.Frame);
Assert.Equal (10, tf.Width);
}
[Fact]
public void Copy_Paste ()
{
IApplication app = Application.Create();
app.Init("fake");
try
{
var tf1 = new TimeField { Time = TimeSpan.Parse ("12:12:19"), App = app };
var tf2 = new TimeField { Time = TimeSpan.Parse ("12:59:01"), App = app };
// Select all text
Assert.True (tf2.NewKeyDownEvent (Key.End.WithShift));
Assert.Equal (1, tf2.SelectedStart);
Assert.Equal (8, tf2.SelectedLength);
Assert.Equal (9, tf2.CursorPosition);
// Copy from tf2
Assert.True (tf2.NewKeyDownEvent (Key.C.WithCtrl));
// Paste into tf1
Assert.True (tf1.NewKeyDownEvent (Key.V.WithCtrl));
Assert.Equal (" 12:59:01", tf1.Text);
Assert.Equal (9, tf1.CursorPosition);
}
finally
{
app.Shutdown();
}
}
[Fact]
public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format ()
{
var tf = new TimeField ();
Assert.Equal (1, tf.CursorPosition);
tf.CursorPosition = 0;
Assert.Equal (1, tf.CursorPosition);
tf.CursorPosition = 9;
Assert.Equal (8, tf.CursorPosition);
tf.IsShortFormat = true;
tf.CursorPosition = 0;
Assert.Equal (1, tf.CursorPosition);
tf.CursorPosition = 6;
Assert.Equal (5, tf.CursorPosition);
}
[Fact]
public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format_After_Selection ()
{
var tf = new TimeField ();
// Start selection
Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift));
Assert.Equal (1, tf.SelectedStart);
Assert.Equal (1, tf.SelectedLength);
Assert.Equal (0, tf.CursorPosition);
// Without selection
Assert.True (tf.NewKeyDownEvent (Key.CursorLeft));
Assert.Equal (-1, tf.SelectedStart);
Assert.Equal (0, tf.SelectedLength);
Assert.Equal (1, tf.CursorPosition);
tf.CursorPosition = 8;
Assert.True (tf.NewKeyDownEvent (Key.CursorRight.WithShift));
Assert.Equal (8, tf.SelectedStart);
Assert.Equal (1, tf.SelectedLength);
Assert.Equal (9, tf.CursorPosition);
Assert.True (tf.NewKeyDownEvent (Key.CursorRight));
Assert.Equal (-1, tf.SelectedStart);
Assert.Equal (0, tf.SelectedLength);
Assert.Equal (8, tf.CursorPosition);
Assert.False (tf.IsShortFormat);
Assert.False (tf.IsInitialized);
tf.BeginInit ();
tf.EndInit ();
tf.IsShortFormat = true;
Assert.Equal (5, tf.CursorPosition);
// Start selection
Assert.True (tf.NewKeyDownEvent (Key.CursorRight.WithShift));
Assert.Equal (5, tf.SelectedStart);
Assert.Equal (1, tf.SelectedLength);
Assert.Equal (6, tf.CursorPosition);
Assert.True (tf.NewKeyDownEvent (Key.CursorRight));
Assert.Equal (-1, tf.SelectedStart);
Assert.Equal (0, tf.SelectedLength);
Assert.Equal (5, tf.CursorPosition);
}
[Fact]
public void KeyBindings_Command ()
{
var tf = new TimeField { Time = TimeSpan.Parse ("12:12:19") };
tf.BeginInit ();
tf.EndInit ();
Assert.Equal (9, tf.CursorPosition);
tf.CursorPosition = 1;
tf.ReadOnly = true;
Assert.True (tf.NewKeyDownEvent (Key.Delete));
Assert.Equal (" 12:12:19", tf.Text);
tf.ReadOnly = false;
Assert.True (tf.NewKeyDownEvent (Key.D.WithCtrl));
Assert.Equal (" 02:12:19", tf.Text);
tf.CursorPosition = 4;
tf.ReadOnly = true;
Assert.True (tf.NewKeyDownEvent (Key.Delete));
Assert.Equal (" 02:12:19", tf.Text);
tf.ReadOnly = false;
Assert.True (tf.NewKeyDownEvent (Key.Backspace));
Assert.Equal (" 02:02:19", tf.Text);
Assert.True (tf.NewKeyDownEvent (Key.Home));
Assert.Equal (1, tf.CursorPosition);
Assert.True (tf.NewKeyDownEvent (Key.End));
Assert.Equal (8, tf.CursorPosition);
Assert.True (tf.NewKeyDownEvent (Key.A.WithCtrl));
Assert.Equal (1, tf.CursorPosition);
Assert.Equal (9, tf.Text.Length);
Assert.True (tf.NewKeyDownEvent (Key.E.WithCtrl));
Assert.Equal (8, tf.CursorPosition);
Assert.True (tf.NewKeyDownEvent (Key.CursorLeft));
Assert.Equal (7, tf.CursorPosition);
Assert.True (tf.NewKeyDownEvent (Key.CursorRight));
Assert.Equal (8, tf.CursorPosition);
// Non-numerics are ignored
Assert.False (tf.NewKeyDownEvent (Key.A));
tf.ReadOnly = true;
tf.CursorPosition = 1;
Assert.True (tf.NewKeyDownEvent (Key.D1));
Assert.Equal (" 02:02:19", tf.Text);
tf.ReadOnly = false;
Assert.True (tf.NewKeyDownEvent (Key.D1));
Assert.Equal (" 12:02:19", tf.Text);
Assert.Equal (2, tf.CursorPosition);
#if UNIX_KEY_BINDINGS
Assert.True (tf.NewKeyDownEvent (Key.D.WithAlt));
Assert.Equal (" 10:02:19", tf.Text);
#endif
}
[Fact]
public void Typing_With_Selection_Normalize_Format ()
{
var tf = new TimeField { Time = TimeSpan.Parse ("12:12:19") };
// Start selection at before the first separator :
tf.CursorPosition = 2;
// Now select the separator :
Assert.True (tf.NewKeyDownEvent (Key.CursorRight.WithShift));
Assert.Equal (2, tf.SelectedStart);
Assert.Equal (1, tf.SelectedLength);
Assert.Equal (3, tf.CursorPosition);
// Type 3 over the separator
Assert.True (tf.NewKeyDownEvent (Key.D3));
// The format was normalized and replaced again with :
Assert.Equal (" 12:12:19", tf.Text);
Assert.Equal (4, tf.CursorPosition);
}
}

View File

@@ -2,5 +2,6 @@
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"parallelizeTestCollections": true,
"parallelizeAssembly": true,
"stopOnFail": false
"stopOnFail": false,
"maxParallelThreads": "2x"
}