mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-29 09:18:01 +01:00
merged
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
using UnitTests;
|
||||
|
||||
namespace UnitTests_Parallelizable.ApplicationTests;
|
||||
|
||||
public class ApplicationForceDriverTests : FakeDriverBase
|
||||
{
|
||||
[Fact]
|
||||
public void ForceDriver_Does_Not_Changes_If_It_Has_Valid_Value ()
|
||||
{
|
||||
Assert.False (Application.Initialized);
|
||||
Assert.Null (Application.Driver);
|
||||
Assert.Equal (string.Empty, Application.ForceDriver);
|
||||
|
||||
Application.ForceDriver = "fake";
|
||||
Assert.Equal ("fake", Application.ForceDriver);
|
||||
|
||||
Application.ForceDriver = "dotnet";
|
||||
Assert.Equal ("fake", Application.ForceDriver);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForceDriver_Throws_If_Initialized_Changed_To_Another_Value ()
|
||||
{
|
||||
IDriver driver = CreateFakeDriver ();
|
||||
|
||||
Assert.False (Application.Initialized);
|
||||
Assert.Null (Application.Driver);
|
||||
Assert.Equal (string.Empty, Application.ForceDriver);
|
||||
|
||||
Application.Init (driverName: "fake");
|
||||
Assert.True (Application.Initialized);
|
||||
Assert.NotNull (Application.Driver);
|
||||
Assert.Equal ("fake", Application.Driver.GetName ());
|
||||
Assert.Equal (string.Empty, Application.ForceDriver);
|
||||
|
||||
Assert.Throws<InvalidOperationException> (() => Application.ForceDriver = "dotnet");
|
||||
|
||||
Application.ForceDriver = "fake";
|
||||
Assert.Equal ("fake", Application.ForceDriver);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
using Moq;
|
||||
|
||||
namespace UnitTests_Parallelizable.ApplicationTests;
|
||||
|
||||
public class ApplicationImplTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Crates a new ApplicationImpl instance for testing. The input, output, and size monitor components are mocked.
|
||||
/// </summary>
|
||||
private IApplication? NewMockedApplicationImpl ()
|
||||
{
|
||||
Mock<INetInput> netInput = new ();
|
||||
SetupRunInputMockMethodToBlock (netInput);
|
||||
|
||||
Mock<IComponentFactory<ConsoleKeyInfo>> m = new ();
|
||||
m.Setup (f => f.CreateInput ()).Returns (netInput.Object);
|
||||
m.Setup (f => f.CreateInputProcessor (It.IsAny<ConcurrentQueue<ConsoleKeyInfo>> ())).Returns (Mock.Of<IInputProcessor> ());
|
||||
|
||||
Mock<IOutput> consoleOutput = new ();
|
||||
var size = new Size (80, 25);
|
||||
|
||||
consoleOutput.Setup (o => o.SetSize (It.IsAny<int> (), It.IsAny<int> ()))
|
||||
.Callback<int, int> ((w, h) => size = new (w, h));
|
||||
consoleOutput.Setup (o => o.GetSize ()).Returns (() => size);
|
||||
m.Setup (f => f.CreateOutput ()).Returns (consoleOutput.Object);
|
||||
m.Setup (f => f.CreateSizeMonitor (It.IsAny<IOutput> (), It.IsAny<IOutputBuffer> ())).Returns (Mock.Of<ISizeMonitor> ());
|
||||
|
||||
return new ApplicationImpl (m.Object);
|
||||
}
|
||||
|
||||
private void SetupRunInputMockMethodToBlock (Mock<INetInput> netInput)
|
||||
{
|
||||
netInput.Setup (r => r.Run (It.IsAny<CancellationToken> ()))
|
||||
.Callback<CancellationToken> (token =>
|
||||
{
|
||||
// Simulate an infinite loop that checks for cancellation
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
// Perform the action that should repeat in the loop
|
||||
// This could be some mock behavior or just an empty loop depending on the context
|
||||
}
|
||||
})
|
||||
.Verifiable (Times.Once);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void Init_CreatesKeybindings ()
|
||||
{
|
||||
IApplication? app = NewMockedApplicationImpl ();
|
||||
|
||||
app?.Keyboard.KeyBindings.Clear ();
|
||||
|
||||
Assert.Empty (app?.Keyboard?.KeyBindings.GetBindings ()!);
|
||||
|
||||
app?.Init ("fake");
|
||||
|
||||
Assert.NotEmpty (app?.Keyboard?.KeyBindings.GetBindings ()!);
|
||||
|
||||
app?.Shutdown ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoInitThrowOnRun ()
|
||||
{
|
||||
IApplication? app = NewMockedApplicationImpl ();
|
||||
var ex = Assert.Throws<NotInitializedException> (() => app?.Run (new Window ()));
|
||||
Assert.Equal ("Run cannot be accessed before Initialization", ex.Message);
|
||||
app?.Shutdown ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitRunShutdown_Top_Set_To_Null_After_Shutdown ()
|
||||
{
|
||||
IApplication? app = NewMockedApplicationImpl ();
|
||||
|
||||
app?.Init ("fake");
|
||||
|
||||
object? timeoutToken = app?.AddTimeout (
|
||||
TimeSpan.FromMilliseconds (150),
|
||||
() =>
|
||||
{
|
||||
if (app.TopRunnable is { })
|
||||
{
|
||||
app.RequestStop ();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
Assert.Null (app?.TopRunnable);
|
||||
|
||||
// Blocks until the timeout call is hit
|
||||
|
||||
app?.Run (new Window ());
|
||||
|
||||
// We returned false above, so we should not have to remove the timeout
|
||||
Assert.False (app?.RemoveTimeout (timeoutToken!));
|
||||
|
||||
Assert.NotNull (app?.TopRunnable);
|
||||
app.TopRunnable?.Dispose ();
|
||||
app.Shutdown ();
|
||||
Assert.Null (app.TopRunnable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitRunShutdown_Running_Set_To_False ()
|
||||
{
|
||||
IApplication app = NewMockedApplicationImpl ()!;
|
||||
|
||||
app.Init ("fake");
|
||||
|
||||
Toplevel top = new Window
|
||||
{
|
||||
Title = "InitRunShutdown_Running_Set_To_False"
|
||||
};
|
||||
|
||||
object timeoutToken = app.AddTimeout (
|
||||
TimeSpan.FromMilliseconds (150),
|
||||
() =>
|
||||
{
|
||||
Assert.True (top!.Running);
|
||||
|
||||
if (app.TopRunnable != null)
|
||||
{
|
||||
app.RequestStop ();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
Assert.False (top!.Running);
|
||||
|
||||
// Blocks until the timeout call is hit
|
||||
app.Run (top);
|
||||
|
||||
// We returned false above, so we should not have to remove the timeout
|
||||
Assert.False (app.RemoveTimeout (timeoutToken));
|
||||
|
||||
Assert.False (top!.Running);
|
||||
|
||||
// BUGBUG: Shutdown sets Top to null, not End.
|
||||
//Assert.Null (Application.TopRunnable);
|
||||
app.TopRunnable?.Dispose ();
|
||||
app.Shutdown ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitRunShutdown_StopAfterFirstIteration_Stops ()
|
||||
{
|
||||
IApplication app = NewMockedApplicationImpl ()!;
|
||||
|
||||
Assert.Null (app.TopRunnable);
|
||||
Assert.Null (app.Driver);
|
||||
|
||||
app.Init ("fake");
|
||||
|
||||
Toplevel top = new Window ();
|
||||
app.TopRunnable = top;
|
||||
|
||||
var closedCount = 0;
|
||||
|
||||
top.Closed
|
||||
+= (_, a) => { closedCount++; };
|
||||
|
||||
var unloadedCount = 0;
|
||||
|
||||
top.Unloaded
|
||||
+= (_, a) => { unloadedCount++; };
|
||||
|
||||
object timeoutToken = app.AddTimeout (
|
||||
TimeSpan.FromMilliseconds (150),
|
||||
() =>
|
||||
{
|
||||
Assert.Fail (@"Didn't stop after first iteration.");
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
Assert.Equal (0, closedCount);
|
||||
Assert.Equal (0, unloadedCount);
|
||||
|
||||
app.StopAfterFirstIteration = true;
|
||||
app.Run (top);
|
||||
|
||||
Assert.Equal (1, closedCount);
|
||||
Assert.Equal (1, unloadedCount);
|
||||
|
||||
app.TopRunnable?.Dispose ();
|
||||
app.Shutdown ();
|
||||
Assert.Equal (1, closedCount);
|
||||
Assert.Equal (1, unloadedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitRunShutdown_End_Is_Called ()
|
||||
{
|
||||
IApplication app = NewMockedApplicationImpl ()!;
|
||||
|
||||
Assert.Null (app.TopRunnable);
|
||||
Assert.Null (app.Driver);
|
||||
|
||||
app.Init ("fake");
|
||||
|
||||
Toplevel top = new Window ();
|
||||
|
||||
// BUGBUG: Both Closed and Unloaded are called from End; what's the difference?
|
||||
var closedCount = 0;
|
||||
|
||||
top.Closed
|
||||
+= (_, a) => { closedCount++; };
|
||||
|
||||
var unloadedCount = 0;
|
||||
|
||||
top.Unloaded
|
||||
+= (_, a) => { unloadedCount++; };
|
||||
|
||||
object timeoutToken = app.AddTimeout (
|
||||
TimeSpan.FromMilliseconds (150),
|
||||
() =>
|
||||
{
|
||||
Assert.True (top!.Running);
|
||||
|
||||
if (app.TopRunnable != null)
|
||||
{
|
||||
app.RequestStop ();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
Assert.Equal (0, closedCount);
|
||||
Assert.Equal (0, unloadedCount);
|
||||
|
||||
// Blocks until the timeout call is hit
|
||||
app.Run (top);
|
||||
|
||||
Assert.Equal (1, closedCount);
|
||||
Assert.Equal (1, unloadedCount);
|
||||
|
||||
// We returned false above, so we should not have to remove the timeout
|
||||
Assert.False (app.RemoveTimeout (timeoutToken));
|
||||
|
||||
app.TopRunnable?.Dispose ();
|
||||
app.Shutdown ();
|
||||
Assert.Equal (1, closedCount);
|
||||
Assert.Equal (1, unloadedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitRunShutdown_QuitKey_Quits ()
|
||||
{
|
||||
IApplication app = NewMockedApplicationImpl ()!;
|
||||
|
||||
app.Init ("fake");
|
||||
|
||||
Toplevel top = new Window
|
||||
{
|
||||
Title = "InitRunShutdown_QuitKey_Quits"
|
||||
};
|
||||
|
||||
object timeoutToken = app.AddTimeout (
|
||||
TimeSpan.FromMilliseconds (150),
|
||||
() =>
|
||||
{
|
||||
Assert.True (top!.Running);
|
||||
|
||||
if (app.TopRunnable != null)
|
||||
{
|
||||
app.Keyboard.RaiseKeyDownEvent (app.Keyboard.QuitKey);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
Assert.False (top!.Running);
|
||||
|
||||
// Blocks until the timeout call is hit
|
||||
app.Run (top);
|
||||
|
||||
// We returned false above, so we should not have to remove the timeout
|
||||
Assert.False (app.RemoveTimeout (timeoutToken));
|
||||
|
||||
Assert.False (top!.Running);
|
||||
|
||||
Assert.NotNull (app.TopRunnable);
|
||||
top.Dispose ();
|
||||
app.Shutdown ();
|
||||
Assert.Null (app.TopRunnable);
|
||||
}
|
||||
|
||||
[Fact (Skip = "Phase 2: Ambiguous method call after Toplevel implements IRunnable. Use non-generic Run() or explicit cast.")]
|
||||
public void InitRunShutdown_Generic_IdleForExit ()
|
||||
{
|
||||
IApplication app = NewMockedApplicationImpl ()!;
|
||||
|
||||
app.Init ("fake");
|
||||
|
||||
app.AddTimeout (TimeSpan.Zero, () => IdleExit (app));
|
||||
Assert.Null (app.TopRunnable);
|
||||
|
||||
// Blocks until the timeout call is hit
|
||||
// Phase 2: Ambiguous method call - use non-generic Run()
|
||||
Window window = new ();
|
||||
app.Run (window);
|
||||
|
||||
Assert.NotNull (app.TopRunnable);
|
||||
app.TopRunnable?.Dispose ();
|
||||
app.Shutdown ();
|
||||
Assert.Null (app.TopRunnable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shutdown_Closing_Closed_Raised ()
|
||||
{
|
||||
IApplication app = NewMockedApplicationImpl ()!;
|
||||
|
||||
app.Init ("fake");
|
||||
|
||||
var closing = 0;
|
||||
var closed = 0;
|
||||
var t = new Toplevel ();
|
||||
|
||||
t.Closing
|
||||
+= (_, a) =>
|
||||
{
|
||||
// Cancel the first time
|
||||
if (closing == 0)
|
||||
{
|
||||
a.Cancel = true;
|
||||
}
|
||||
|
||||
closing++;
|
||||
Assert.Same (t, a.RequestingTop);
|
||||
};
|
||||
|
||||
t.Closed
|
||||
+= (_, a) =>
|
||||
{
|
||||
closed++;
|
||||
Assert.Same (t, a.Toplevel);
|
||||
};
|
||||
|
||||
app.AddTimeout (TimeSpan.Zero, () => IdleExit (app));
|
||||
|
||||
// Blocks until the timeout call is hit
|
||||
|
||||
app.Run (t);
|
||||
|
||||
app.TopRunnable?.Dispose ();
|
||||
app.Shutdown ();
|
||||
|
||||
Assert.Equal (2, closing);
|
||||
Assert.Equal (1, closed);
|
||||
}
|
||||
|
||||
private bool IdleExit (IApplication app)
|
||||
{
|
||||
if (app.TopRunnable != null)
|
||||
{
|
||||
app.RequestStop ();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Open_Calls_ContinueWith_On_UIThread ()
|
||||
{
|
||||
IApplication app = NewMockedApplicationImpl ()!;
|
||||
|
||||
app.Init ("fake");
|
||||
var b = new Button ();
|
||||
|
||||
var result = false;
|
||||
|
||||
b.Accepting +=
|
||||
(_, _) =>
|
||||
{
|
||||
Task.Run (() => { Task.Delay (300).Wait (); })
|
||||
.ContinueWith (
|
||||
(t, _) =>
|
||||
{
|
||||
// no longer loading
|
||||
app.Invoke (() =>
|
||||
{
|
||||
result = true;
|
||||
app.RequestStop ();
|
||||
});
|
||||
},
|
||||
TaskScheduler.FromCurrentSynchronizationContext ());
|
||||
};
|
||||
|
||||
app.AddTimeout (
|
||||
TimeSpan.FromMilliseconds (150),
|
||||
() =>
|
||||
{
|
||||
// Run asynchronous logic inside Task.Run
|
||||
if (app.TopRunnable != null)
|
||||
{
|
||||
b.NewKeyDownEvent (Key.Enter);
|
||||
b.NewKeyUpEvent (Key.Enter);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
Assert.Null (app.TopRunnable);
|
||||
|
||||
var w = new Window
|
||||
{
|
||||
Title = "Open_CallsContinueWithOnUIThread"
|
||||
};
|
||||
w.Add (b);
|
||||
|
||||
// Blocks until the timeout call is hit
|
||||
app.Run (w);
|
||||
|
||||
Assert.NotNull (app.TopRunnable);
|
||||
app.TopRunnable?.Dispose ();
|
||||
app.Shutdown ();
|
||||
Assert.Null (app.TopRunnable);
|
||||
|
||||
Assert.True (result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplicationImpl_UsesInstanceFields_NotStaticReferences ()
|
||||
{
|
||||
// This test verifies that ApplicationImpl uses instance fields instead of static Application references
|
||||
IApplication v2 = NewMockedApplicationImpl ()!;
|
||||
|
||||
// Before Init, all fields should be null/default
|
||||
Assert.Null (v2.Driver);
|
||||
Assert.False (v2.Initialized);
|
||||
|
||||
//Assert.Null (v2.Popover);
|
||||
//Assert.Null (v2.Navigation);
|
||||
Assert.Null (v2.TopRunnable);
|
||||
Assert.Empty (v2.SessionStack);
|
||||
|
||||
// Init should populate instance fields
|
||||
v2.Init ("fake");
|
||||
|
||||
// After Init, Driver, Navigation, and Popover should be populated
|
||||
Assert.NotNull (v2.Driver);
|
||||
Assert.True (v2.Initialized);
|
||||
Assert.NotNull (v2.Popover);
|
||||
Assert.NotNull (v2.Navigation);
|
||||
Assert.Null (v2.TopRunnable); // Top is still null until Run
|
||||
|
||||
// Shutdown should clean up instance fields
|
||||
v2.Shutdown ();
|
||||
|
||||
Assert.Null (v2.Driver);
|
||||
Assert.False (v2.Initialized);
|
||||
|
||||
//Assert.Null (v2.Popover);
|
||||
//Assert.Null (v2.Navigation);
|
||||
Assert.Null (v2.TopRunnable);
|
||||
Assert.Empty (v2.SessionStack);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
// ReSharper disable AccessToDisposedClosure
|
||||
|
||||
#nullable enable
|
||||
namespace UnitTests_Parallelizable.ApplicationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests to verify that KeyboardImpl is thread-safe for concurrent access scenarios.
|
||||
/// </summary>
|
||||
public class KeyboardImplThreadSafetyTests
|
||||
{
|
||||
[Fact]
|
||||
public void AddCommand_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 50;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = [];
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// AddKeyBindings internally calls AddCommand multiple times
|
||||
keyboard.AddKeyBindings ();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Expected - AddKeyBindings tries to add keys that already exist
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WhileOperationsInProgress_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication? app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
var keyboard = new KeyboardImpl { App = app };
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
var continueRunning = true;
|
||||
|
||||
// Act
|
||||
Task operationsTask = Task.Run (() =>
|
||||
{
|
||||
while (continueRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl);
|
||||
IEnumerable<KeyValuePair<Key, KeyBinding>> bindings = keyboard.KeyBindings.GetBindings ();
|
||||
int count = bindings.Count ();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Expected - keyboard was disposed
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Give operations a chance to start
|
||||
Thread.Sleep (10);
|
||||
|
||||
// Dispose while operations are running
|
||||
keyboard.Dispose ();
|
||||
continueRunning = false;
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
operationsTask.Wait (TimeSpan.FromSeconds (2));
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
app.Shutdown ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeCommand_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication? app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
var keyboard = new KeyboardImpl { App = app };
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 50;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var binding = new KeyBinding ([Command.Quit]);
|
||||
keyboard.InvokeCommand (Command.Quit, Key.Q.WithCtrl, binding);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
app.Shutdown ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeCommandsBoundToKey_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication? app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
var keyboard = new KeyboardImpl { App = app };
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 50;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = [];
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
app.Shutdown ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyBindings_ConcurrentAdd_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Don't call AddKeyBindings here to avoid conflicts
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 50;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
int threadId = i;
|
||||
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use unique keys per thread to avoid conflicts
|
||||
Key key = Key.F1 + threadId * OPERATIONS_PER_THREAD + j;
|
||||
keyboard.KeyBindings.Add (key, Command.Refresh);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Expected - duplicate key
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Expected - invalid key
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyDown_KeyUp_Events_ConcurrentSubscription_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 20;
|
||||
var keyDownCount = 0;
|
||||
var keyUpCount = 0;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
// Threads subscribing to events
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
EventHandler<Key> handler = (s, e) => { Interlocked.Increment (ref keyDownCount); };
|
||||
keyboard.KeyDown += handler;
|
||||
keyboard.KeyDown -= handler;
|
||||
|
||||
EventHandler<Key> upHandler = (s, e) => { Interlocked.Increment (ref keyUpCount); };
|
||||
keyboard.KeyUp += upHandler;
|
||||
keyboard.KeyUp -= upHandler;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyProperty_Setters_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Initialize once before concurrent access
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 20;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
int threadId = i;
|
||||
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Cycle through different key combinations
|
||||
switch (j % 6)
|
||||
{
|
||||
case 0:
|
||||
keyboard.QuitKey = Key.Q.WithCtrl;
|
||||
|
||||
break;
|
||||
case 1:
|
||||
keyboard.ArrangeKey = Key.F6.WithCtrl;
|
||||
|
||||
break;
|
||||
case 2:
|
||||
keyboard.NextTabKey = Key.Tab;
|
||||
|
||||
break;
|
||||
case 3:
|
||||
keyboard.PrevTabKey = Key.Tab.WithShift;
|
||||
|
||||
break;
|
||||
case 4:
|
||||
keyboard.NextTabGroupKey = Key.F6;
|
||||
|
||||
break;
|
||||
case 5:
|
||||
keyboard.PrevTabGroupKey = Key.F6.WithShift;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixedOperations_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication? app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
var keyboard = new KeyboardImpl { App = app };
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int OPERATIONS_PER_THREAD = 30;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
// Thread 1: Add bindings with unique keys
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use high key codes to avoid conflicts
|
||||
var key = new Key ((KeyCode)((int)KeyCode.F20 + j));
|
||||
keyboard.KeyBindings.Add (key, Command.Refresh);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Expected - duplicate
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Expected - invalid key
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Thread 2: Invoke commands
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Thread 3: Read bindings
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
IEnumerable<KeyValuePair<Key, KeyBinding>> bindings = keyboard.KeyBindings.GetBindings ();
|
||||
int count = bindings.Count ();
|
||||
Assert.True (count >= 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Thread 4: Change key properties
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
keyboard.QuitKey = j % 2 == 0 ? Key.Q.WithCtrl : Key.Esc;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
app.Shutdown ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseKeyDownEvent_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication? app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
var keyboard = new KeyboardImpl { App = app };
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 5;
|
||||
const int OPERATIONS_PER_THREAD = 20;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
keyboard.RaiseKeyDownEvent (Key.A);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
app.Shutdown ();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
#nullable enable
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests;
|
||||
namespace UnitTests_Parallelizable.ApplicationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for IApplication's IRunnable support.
|
||||
@@ -11,17 +12,6 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
|
||||
private readonly ITestOutputHelper _output = output;
|
||||
private IApplication? _app;
|
||||
|
||||
private IApplication GetApp ()
|
||||
{
|
||||
if (_app is null)
|
||||
{
|
||||
_app = Application.Create ();
|
||||
_app.Init ("fake");
|
||||
}
|
||||
|
||||
return _app;
|
||||
}
|
||||
|
||||
public void Dispose ()
|
||||
{
|
||||
_app?.Shutdown ();
|
||||
@@ -50,64 +40,42 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Begin_ThrowsOnNullRunnable ()
|
||||
public void Begin_CanBeCanceled_ByIsRunningChanging ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException> (() => app.Begin ((IRunnable)null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Begin_RaisesIsRunningChangingEvent ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
Runnable<int> runnable = new ();
|
||||
var isRunningChangingRaised = false;
|
||||
bool? oldValue = null;
|
||||
bool? newValue = null;
|
||||
|
||||
runnable.IsRunningChanging += (s, e) =>
|
||||
{
|
||||
isRunningChangingRaised = true;
|
||||
oldValue = e.CurrentValue;
|
||||
newValue = e.NewValue;
|
||||
};
|
||||
CancelableRunnable runnable = new () { CancelStart = true };
|
||||
|
||||
// Act
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
|
||||
// Assert
|
||||
Assert.True (isRunningChangingRaised);
|
||||
Assert.False (oldValue);
|
||||
Assert.True (newValue);
|
||||
// Assert - Should not be added to stack if canceled
|
||||
Assert.False (runnable.IsRunning);
|
||||
|
||||
// Cleanup
|
||||
app.End (token);
|
||||
// Token is still created but runnable not added to stack
|
||||
Assert.NotNull (token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Begin_RaisesIsRunningChangedEvent ()
|
||||
public void Begin_RaisesIsModalChangedEvent ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
Runnable<int> runnable = new ();
|
||||
var isRunningChangedRaised = false;
|
||||
var isModalChangedRaised = false;
|
||||
bool? receivedValue = null;
|
||||
|
||||
runnable.IsRunningChanged += (s, e) =>
|
||||
{
|
||||
isRunningChangedRaised = true;
|
||||
receivedValue = e.Value;
|
||||
};
|
||||
runnable.IsModalChanged += (s, e) =>
|
||||
{
|
||||
isModalChangedRaised = true;
|
||||
receivedValue = e.Value;
|
||||
};
|
||||
|
||||
// Act
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
|
||||
// Assert
|
||||
Assert.True (isRunningChangedRaised);
|
||||
Assert.True (isModalChangedRaised);
|
||||
Assert.True (receivedValue);
|
||||
|
||||
// Cleanup
|
||||
@@ -144,25 +112,25 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Begin_RaisesIsModalChangedEvent ()
|
||||
public void Begin_RaisesIsRunningChangedEvent ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
Runnable<int> runnable = new ();
|
||||
var isModalChangedRaised = false;
|
||||
var isRunningChangedRaised = false;
|
||||
bool? receivedValue = null;
|
||||
|
||||
runnable.IsModalChanged += (s, e) =>
|
||||
{
|
||||
isModalChangedRaised = true;
|
||||
receivedValue = e.Value;
|
||||
};
|
||||
runnable.IsRunningChanged += (s, e) =>
|
||||
{
|
||||
isRunningChangedRaised = true;
|
||||
receivedValue = e.Value;
|
||||
};
|
||||
|
||||
// Act
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
|
||||
// Assert
|
||||
Assert.True (isModalChangedRaised);
|
||||
Assert.True (isRunningChangedRaised);
|
||||
Assert.True (receivedValue);
|
||||
|
||||
// Cleanup
|
||||
@@ -170,17 +138,29 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Begin_SetsIsRunningToTrue ()
|
||||
public void Begin_RaisesIsRunningChangingEvent ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
Runnable<int> runnable = new ();
|
||||
var isRunningChangingRaised = false;
|
||||
bool? oldValue = null;
|
||||
bool? newValue = null;
|
||||
|
||||
runnable.IsRunningChanging += (s, e) =>
|
||||
{
|
||||
isRunningChangingRaised = true;
|
||||
oldValue = e.CurrentValue;
|
||||
newValue = e.NewValue;
|
||||
};
|
||||
|
||||
// Act
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
|
||||
// Assert
|
||||
Assert.True (runnable.IsRunning);
|
||||
Assert.True (isRunningChangingRaised);
|
||||
Assert.False (oldValue);
|
||||
Assert.True (newValue);
|
||||
|
||||
// Cleanup
|
||||
app.End (token);
|
||||
@@ -204,29 +184,89 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void End_RemovesRunnableFromStack ()
|
||||
public void Begin_SetsIsRunningToTrue ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
Runnable<int> runnable = new ();
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
int stackCountBefore = app.RunnableSessionStack?.Count ?? 0;
|
||||
|
||||
// Act
|
||||
app.End (token);
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
|
||||
// Assert
|
||||
Assert.Equal (stackCountBefore - 1, app.RunnableSessionStack?.Count ?? 0);
|
||||
Assert.True (runnable.IsRunning);
|
||||
|
||||
// Cleanup
|
||||
app.End (token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void End_ThrowsOnNullToken ()
|
||||
public void Begin_ThrowsOnNullRunnable ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException> (() => app.End ((RunnableSessionToken)null!));
|
||||
Assert.Throws<ArgumentNullException> (() => app.Begin ((IRunnable)null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void End_CanBeCanceled_ByIsRunningChanging ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
CancelableRunnable runnable = new () { CancelStop = true };
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
runnable.CancelStop = true; // Enable cancellation
|
||||
|
||||
// Act
|
||||
app.End (token);
|
||||
|
||||
// Assert - Should still be running if canceled
|
||||
Assert.True (runnable.IsRunning);
|
||||
|
||||
// Force end by disabling cancellation
|
||||
runnable.CancelStop = false;
|
||||
app.End (token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void End_ClearsTokenRunnable ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
Runnable<int> runnable = new ();
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
|
||||
// Act
|
||||
app.End (token);
|
||||
|
||||
// Assert
|
||||
Assert.Null (token.Runnable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void End_RaisesIsRunningChangedEvent ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
Runnable<int> runnable = new ();
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
var isRunningChangedRaised = false;
|
||||
bool? receivedValue = null;
|
||||
|
||||
runnable.IsRunningChanged += (s, e) =>
|
||||
{
|
||||
isRunningChangedRaised = true;
|
||||
receivedValue = e.Value;
|
||||
};
|
||||
|
||||
// Act
|
||||
app.End (token);
|
||||
|
||||
// Assert
|
||||
Assert.True (isRunningChangedRaised);
|
||||
Assert.False (receivedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -257,42 +297,19 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void End_RaisesIsRunningChangedEvent ()
|
||||
public void End_RemovesRunnableFromStack ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
Runnable<int> runnable = new ();
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
var isRunningChangedRaised = false;
|
||||
bool? receivedValue = null;
|
||||
|
||||
runnable.IsRunningChanged += (s, e) =>
|
||||
{
|
||||
isRunningChangedRaised = true;
|
||||
receivedValue = e.Value;
|
||||
};
|
||||
int stackCountBefore = app.RunnableSessionStack?.Count ?? 0;
|
||||
|
||||
// Act
|
||||
app.End (token);
|
||||
|
||||
// Assert
|
||||
Assert.True (isRunningChangedRaised);
|
||||
Assert.False (receivedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void End_SetsIsRunningToFalse ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
Runnable<int> runnable = new ();
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
|
||||
// Act
|
||||
app.End (token);
|
||||
|
||||
// Assert
|
||||
Assert.False (runnable.IsRunning);
|
||||
Assert.Equal (stackCountBefore - 1, app.RunnableSessionStack?.Count ?? 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -311,7 +328,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void End_ClearsTokenRunnable ()
|
||||
public void End_SetsIsRunningToFalse ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
@@ -322,7 +339,34 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
|
||||
app.End (token);
|
||||
|
||||
// Assert
|
||||
Assert.Null (token.Runnable);
|
||||
Assert.False (runnable.IsRunning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void End_ThrowsOnNullToken ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException> (() => app.End ((RunnableSessionToken)null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleRunnables_IndependentResults ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
Runnable<int> runnable1 = new ();
|
||||
Runnable<string> runnable2 = new ();
|
||||
|
||||
// Act
|
||||
runnable1.Result = 42;
|
||||
runnable2.Result = "test";
|
||||
|
||||
// Assert
|
||||
Assert.Equal (42, runnable1.Result);
|
||||
Assert.Equal ("test", runnable2.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -441,80 +485,15 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
|
||||
app.Shutdown ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Begin_CanBeCanceled_ByIsRunningChanging ()
|
||||
private IApplication GetApp ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
CancelableRunnable runnable = new () { CancelStart = true };
|
||||
|
||||
// Act
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
|
||||
// Assert - Should not be added to stack if canceled
|
||||
Assert.False (runnable.IsRunning);
|
||||
|
||||
// Token is still created but runnable not added to stack
|
||||
Assert.NotNull (token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void End_CanBeCanceled_ByIsRunningChanging ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
CancelableRunnable runnable = new () { CancelStop = true };
|
||||
RunnableSessionToken token = app.Begin (runnable);
|
||||
runnable.CancelStop = true; // Enable cancellation
|
||||
|
||||
// Act
|
||||
app.End (token);
|
||||
|
||||
// Assert - Should still be running if canceled
|
||||
Assert.True (runnable.IsRunning);
|
||||
|
||||
// Force end by disabling cancellation
|
||||
runnable.CancelStop = false;
|
||||
app.End (token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleRunnables_IndependentResults ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication app = GetApp ();
|
||||
Runnable<int> runnable1 = new ();
|
||||
Runnable<string> runnable2 = new ();
|
||||
|
||||
// Act
|
||||
runnable1.Result = 42;
|
||||
runnable2.Result = "test";
|
||||
|
||||
// Assert
|
||||
Assert.Equal (42, runnable1.Result);
|
||||
Assert.Equal ("test", runnable2.Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test runnable that can be stopped.
|
||||
/// </summary>
|
||||
private class StoppableRunnable : Runnable<int>
|
||||
{
|
||||
public bool WasStopRequested { get; private set; }
|
||||
|
||||
public override void RequestStop ()
|
||||
if (_app is null)
|
||||
{
|
||||
WasStopRequested = true;
|
||||
base.RequestStop ();
|
||||
_app = Application.Create ();
|
||||
_app.Init ("fake");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test runnable for generic Run tests.
|
||||
/// </summary>
|
||||
private class TestRunnable : Runnable<int>
|
||||
{
|
||||
public TestRunnable () { Id = "TestRunnable"; }
|
||||
return _app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -540,4 +519,26 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
|
||||
return base.OnIsRunningChanging (oldIsRunning, newIsRunning);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test runnable that can be stopped.
|
||||
/// </summary>
|
||||
private class StoppableRunnable : Runnable<int>
|
||||
{
|
||||
public override void RequestStop ()
|
||||
{
|
||||
WasStopRequested = true;
|
||||
base.RequestStop ();
|
||||
}
|
||||
|
||||
public bool WasStopRequested { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test runnable for generic Run tests.
|
||||
/// </summary>
|
||||
private class TestRunnable : Runnable<int>
|
||||
{
|
||||
public TestRunnable () { Id = "TestRunnable"; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#nullable enable
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
98
Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs
Normal file
98
Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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 ();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
20
Tests/UnitTestsParallelizable/TestDateAttribute.cs
Normal file
20
Tests/UnitTestsParallelizable/TestDateAttribute.cs
Normal 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); }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
367
Tests/UnitTestsParallelizable/Views/DateFieldTests.cs
Normal file
367
Tests/UnitTestsParallelizable/Views/DateFieldTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
625
Tests/UnitTestsParallelizable/Views/MessageBoxTests.cs
Normal file
625
Tests/UnitTestsParallelizable/Views/MessageBoxTests.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -679,7 +679,6 @@ public class ScrollSliderTests (ITestOutputHelper output) : FakeDriverBase
|
||||
|
||||
|
||||
[Theory]
|
||||
[SetupFakeApplication]
|
||||
[InlineData (
|
||||
3,
|
||||
10,
|
||||
|
||||
205
Tests/UnitTestsParallelizable/Views/TimeFieldTests.cs
Normal file
205
Tests/UnitTestsParallelizable/Views/TimeFieldTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,6 @@
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"parallelizeTestCollections": true,
|
||||
"parallelizeAssembly": true,
|
||||
"stopOnFail": false
|
||||
"stopOnFail": false,
|
||||
"maxParallelThreads": "2x"
|
||||
}
|
||||
Reference in New Issue
Block a user