mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
* Initial plan * Delete AnsiColorNameResolver and MultiStandardColorNameResolver, add legacy 16-color names to StandardColor Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor and enhance tests for Color, Region, and Lines Refactored `Color` struct by removing unused methods and simplifying logic. Updated namespaces for better organization. Enhanced test coverage for `Color`, `Region`, and `LineCanvas` with new test cases, parameterized tests, and edge case handling. Added `StraightLineExtensionsTests`, `StraightLineTests`, and `RegionClassTests` to validate behavior under various scenarios. Improved `MergeRectangles` stability and addressed crash patterns. Removed legacy features and unused code. Enhanced documentation and optimized performance in key methods. * Improve Color struct and StandardColors functionality Enhanced the Color struct to fully support the alpha channel for rendering intent while maintaining semantic color identity. Updated TryNameColor to ignore alpha when matching colors, ensuring transparency does not affect color resolution. Expanded XML documentation to clarify alpha channel usage and future alpha blending support. Improved drawing documentation to explain the lifecycle, deferred rendering, and color support, including 24-bit true color and legacy 16-color compatibility. Added a new section on transparency and its role in rendering. Revised StandardColors implementation to use modern C# features and ensure consistent ARGB mapping. Added comprehensive tests for StandardColors and Color, covering alpha handling, color parsing, thread safety, and aliased color resolution. Removed outdated tests relying on legacy behavior. Enhanced code readability, maintainability, and test coverage to ensure correctness and backward compatibility. * Code cleanup * Code cleanup * Fix warnings. Code cleanup * Add comprehensive unit tests for ColorStrings class Introduced a new test class `ColorStringsTests` under the `DrawingTests.ColorTests` namespace to validate the functionality of the `ColorStrings` class. Key changes include: - Added tests for `GetColorName` to verify behavior for standard and non-standard colors, ignoring alpha channels, and handling known colors. - Added tests for `GetStandardColorNames` to ensure the method returns a non-empty, alphabetically sorted collection containing all `StandardColor` enum values. - Implemented tests for `TryParseStandardColorName` to validate case-insensitive parsing, hex color support, handling invalid input, and `ReadOnlySpan<char>` compatibility. - Added tests for `TryParseNamedColor` to verify parsing of named and hex colors, handling of aliases, and `ReadOnlySpan<char>` support. - Added round-trip tests to ensure consistency between `GetColorName`, `TryParseNamedColor`, `GetStandardColorNames`, and `TryParseStandardColorName`. These tests ensure robust validation of color parsing and naming functionality. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> Co-authored-by: Tig <tig@users.noreply.github.com>
857 lines
24 KiB
C#
857 lines
24 KiB
C#
using Xunit.Abstractions;
|
|
// ReSharper disable AccessToDisposedClosure
|
|
#pragma warning disable xUnit1031
|
|
|
|
namespace ApplicationTests.Timeout;
|
|
|
|
/// <summary>
|
|
/// Tests for timeout behavior and functionality.
|
|
/// These tests verify that timeouts fire correctly, can be added/removed,
|
|
/// handle exceptions properly, and work with Application.Run() calls.
|
|
/// </summary>
|
|
public class TimeoutTests
|
|
{
|
|
[Fact]
|
|
public void AddTimeout_Callback_Can_Add_New_Timeout ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
var firstFired = false;
|
|
var secondFired = false;
|
|
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (50),
|
|
() =>
|
|
{
|
|
firstFired = true;
|
|
|
|
// Add another timeout from within callback
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (50),
|
|
() =>
|
|
{
|
|
secondFired = true;
|
|
app.RequestStop ();
|
|
|
|
return false;
|
|
}
|
|
);
|
|
|
|
return false;
|
|
}
|
|
);
|
|
|
|
// Defensive: use iteration counter instead of time-based safety timeout
|
|
var iterations = 0;
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
app.Run<Runnable> ();
|
|
|
|
Assert.True (firstFired);
|
|
Assert.True (secondFired);
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Stop if test objectives met or safety limit reached
|
|
if ((firstFired && secondFired) || iterations > 1000)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AddTimeout_Exception_In_Callback_Propagates ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
var exceptionThrown = false;
|
|
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (50),
|
|
() =>
|
|
{
|
|
exceptionThrown = true;
|
|
throw new InvalidOperationException ("Test exception");
|
|
});
|
|
|
|
// Defensive: use iteration counter
|
|
var iterations = 0;
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
Assert.Throws<InvalidOperationException> (() => app.Run<Runnable> ());
|
|
Assert.True (exceptionThrown, "Exception callback should have been invoked");
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Safety stop if exception not thrown after many iterations
|
|
if (iterations > 1000 && !exceptionThrown)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AddTimeout_Fires ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
uint timeoutTime = 100;
|
|
var timeoutFired = false;
|
|
|
|
// Setup a timeout that will fire
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (timeoutTime),
|
|
() =>
|
|
{
|
|
timeoutFired = true;
|
|
|
|
// Return false so the timer does not repeat
|
|
return false;
|
|
}
|
|
);
|
|
|
|
// The timeout has not fired yet
|
|
Assert.False (timeoutFired);
|
|
|
|
// Block the thread to prove the timeout does not fire on a background thread
|
|
Thread.Sleep ((int)timeoutTime * 2);
|
|
Assert.False (timeoutFired);
|
|
|
|
app.StopAfterFirstIteration = true;
|
|
app.Run<Runnable> ();
|
|
|
|
// The timeout should have fired
|
|
Assert.True (timeoutFired);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddTimeout_From_Background_Thread_Fires ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
var timeoutFired = false;
|
|
using var taskCompleted = new ManualResetEventSlim (false);
|
|
|
|
Task.Run (() =>
|
|
{
|
|
Thread.Sleep (50); // Ensure we're on background thread
|
|
|
|
app.Invoke (() =>
|
|
{
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (100),
|
|
() =>
|
|
{
|
|
timeoutFired = true;
|
|
taskCompleted.Set ();
|
|
app.RequestStop ();
|
|
|
|
return false;
|
|
}
|
|
);
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
// Use iteration counter for safety instead of time
|
|
var iterations = 0;
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
app.Run<Runnable> ();
|
|
|
|
// Defensive: wait with timeout
|
|
Assert.True (taskCompleted.Wait (TimeSpan.FromSeconds (5)), "Timeout from background thread should have completed");
|
|
Assert.True (timeoutFired);
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Safety stop
|
|
if (iterations > 1000)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AddTimeout_High_Frequency_All_Fire ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
const int TIMEOUT_COUNT = 50; // Reduced from 100 for performance
|
|
var firedCount = 0;
|
|
|
|
for (var i = 0; i < TIMEOUT_COUNT; i++)
|
|
{
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (10 + i * 5),
|
|
() =>
|
|
{
|
|
Interlocked.Increment (ref firedCount);
|
|
|
|
return false;
|
|
}
|
|
);
|
|
}
|
|
|
|
// Use iteration counter and event completion instead of time-based safety
|
|
var iterations = 0;
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
app.Run<Runnable> ();
|
|
|
|
Assert.Equal (TIMEOUT_COUNT, firedCount);
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Stop when all timeouts fired or safety limit reached
|
|
if (firedCount >= TIMEOUT_COUNT || iterations > 2000)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Long_Running_Callback_Delays_Subsequent_Timeouts ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
var firstStarted = false;
|
|
var secondFired = false;
|
|
var firstCompleted = false;
|
|
|
|
// Long-running timeout
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (50),
|
|
() =>
|
|
{
|
|
firstStarted = true;
|
|
Thread.Sleep (200); // Simulate long operation
|
|
firstCompleted = true;
|
|
|
|
return false;
|
|
}
|
|
);
|
|
|
|
// This should fire even though first is still running
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (100),
|
|
() =>
|
|
{
|
|
secondFired = true;
|
|
|
|
return false;
|
|
}
|
|
);
|
|
|
|
// Use iteration counter instead of time-based timeout
|
|
var iterations = 0;
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
app.Run<Runnable> ();
|
|
|
|
Assert.True (firstStarted);
|
|
Assert.True (secondFired);
|
|
Assert.True (firstCompleted);
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Stop when both complete or safety limit
|
|
if ((firstCompleted && secondFired) || iterations > 2000)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AddTimeout_Multiple_Fire_In_Order ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
List<int> executionOrder = new ();
|
|
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (300),
|
|
() =>
|
|
{
|
|
executionOrder.Add (3);
|
|
|
|
return false;
|
|
});
|
|
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (100),
|
|
() =>
|
|
{
|
|
executionOrder.Add (1);
|
|
|
|
return false;
|
|
});
|
|
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (200),
|
|
() =>
|
|
{
|
|
executionOrder.Add (2);
|
|
|
|
return false;
|
|
});
|
|
|
|
var iterations = 0;
|
|
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
app.Run<Runnable> ();
|
|
|
|
Assert.Equal (new [] { 1, 2, 3 }, executionOrder);
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Stop after timeouts fire or max iterations (defensive)
|
|
if (executionOrder.Count == 3 || iterations > 1000)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AddTimeout_Multiple_TimeSpan_Zero_All_Fire ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
const int TIMEOUT_COUNT = 10;
|
|
var firedCount = 0;
|
|
|
|
for (var i = 0; i < TIMEOUT_COUNT; i++)
|
|
{
|
|
app.AddTimeout (
|
|
TimeSpan.Zero,
|
|
() =>
|
|
{
|
|
Interlocked.Increment (ref firedCount);
|
|
|
|
return false;
|
|
}
|
|
);
|
|
}
|
|
|
|
var iterations = 0;
|
|
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
app.Run<Runnable> ();
|
|
|
|
Assert.Equal (TIMEOUT_COUNT, firedCount);
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Defensive: stop after timeouts fire or max iterations
|
|
if (firedCount == TIMEOUT_COUNT || iterations > 100)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AddTimeout_Nested_Run_Parent_Timeout_Fires ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
var parentTimeoutFired = false;
|
|
var childTimeoutFired = false;
|
|
var nestedRunCompleted = false;
|
|
|
|
// Parent timeout - fires after child modal opens
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (200),
|
|
() =>
|
|
{
|
|
parentTimeoutFired = true;
|
|
|
|
return false;
|
|
}
|
|
);
|
|
|
|
// After 100ms, open nested modal
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (100),
|
|
() =>
|
|
{
|
|
var childRunnable = new Runnable ();
|
|
|
|
// Child timeout
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (50),
|
|
() =>
|
|
{
|
|
childTimeoutFired = true;
|
|
app.RequestStop (childRunnable);
|
|
|
|
return false;
|
|
}
|
|
);
|
|
|
|
app.Run (childRunnable);
|
|
nestedRunCompleted = true;
|
|
childRunnable.Dispose ();
|
|
|
|
return false;
|
|
}
|
|
);
|
|
|
|
// Use iteration counter instead of time-based safety
|
|
var iterations = 0;
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
app.Run<Runnable> ();
|
|
|
|
Assert.True (childTimeoutFired, "Child timeout should fire during nested Run");
|
|
Assert.True (parentTimeoutFired, "Parent timeout should continue firing during nested Run");
|
|
Assert.True (nestedRunCompleted, "Nested run should have completed");
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Stop when objectives met or safety limit
|
|
if ((parentTimeoutFired && nestedRunCompleted) || iterations > 2000)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AddTimeout_Repeating_Fires_Multiple_Times ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
var fireCount = 0;
|
|
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (50),
|
|
() =>
|
|
{
|
|
fireCount++;
|
|
|
|
return fireCount < 3; // Repeat 3 times
|
|
}
|
|
);
|
|
|
|
var iterations = 0;
|
|
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
app.Run<Runnable> ();
|
|
|
|
Assert.Equal (3, fireCount);
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Stop after 3 fires or max iterations (defensive)
|
|
if (fireCount >= 3 || iterations > 1000)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AddTimeout_StopAfterFirstIteration_Immediate_Fires ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
var timeoutFired = false;
|
|
|
|
app.AddTimeout (
|
|
TimeSpan.Zero,
|
|
() =>
|
|
{
|
|
timeoutFired = true;
|
|
|
|
return false;
|
|
}
|
|
);
|
|
|
|
app.StopAfterFirstIteration = true;
|
|
app.Run<Runnable> ();
|
|
|
|
Assert.True (timeoutFired);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddTimeout_TimeSpan_Zero_Fires ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
var timeoutFired = false;
|
|
|
|
app.AddTimeout (
|
|
TimeSpan.Zero,
|
|
() =>
|
|
{
|
|
timeoutFired = true;
|
|
|
|
return false;
|
|
});
|
|
|
|
app.StopAfterFirstIteration = true;
|
|
app.Run<Runnable> ();
|
|
|
|
Assert.True (timeoutFired);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveTimeout_Already_Removed_Returns_False ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
object? token = app.AddTimeout (TimeSpan.FromMilliseconds (100), () => false);
|
|
|
|
// Remove once
|
|
bool removed1 = app.RemoveTimeout (token!);
|
|
Assert.True (removed1);
|
|
|
|
// Try to remove again
|
|
bool removed2 = app.RemoveTimeout (token!);
|
|
Assert.False (removed2);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveTimeout_Cancels_Timeout ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
var timeoutFired = false;
|
|
|
|
object? token = app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (100),
|
|
() =>
|
|
{
|
|
timeoutFired = true;
|
|
|
|
return false;
|
|
}
|
|
);
|
|
|
|
// Remove timeout before it fires
|
|
bool removed = app.RemoveTimeout (token!);
|
|
Assert.True (removed);
|
|
|
|
// Use iteration counter instead of time-based timeout
|
|
var iterations = 0;
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
app.Run<Runnable> ();
|
|
|
|
Assert.False (timeoutFired);
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Since timeout was removed, just need enough iterations to prove it won't fire
|
|
// With 100ms timeout, give ~50 iterations which is more than enough
|
|
if (iterations > 50)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveTimeout_Invalid_Token_Returns_False ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
var fakeToken = new object ();
|
|
bool removed = app.RemoveTimeout (fakeToken);
|
|
|
|
Assert.False (removed);
|
|
}
|
|
|
|
[Fact]
|
|
public void TimedEvents_GetTimeout_Invalid_Token_Returns_Null ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
var fakeToken = new object ();
|
|
TimeSpan? actualTimeSpan = app.TimedEvents?.GetTimeout (fakeToken);
|
|
|
|
Assert.Null (actualTimeSpan);
|
|
}
|
|
|
|
[Fact]
|
|
public void TimedEvents_GetTimeout_Returns_Correct_TimeSpan ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
TimeSpan expectedTimeSpan = TimeSpan.FromMilliseconds (500);
|
|
object? token = app.AddTimeout (expectedTimeSpan, () => false);
|
|
|
|
TimeSpan? actualTimeSpan = app.TimedEvents?.GetTimeout (token!);
|
|
|
|
Assert.NotNull (actualTimeSpan);
|
|
Assert.Equal (expectedTimeSpan, actualTimeSpan.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void TimedEvents_StopAll_Clears_Timeouts ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
var firedCount = 0;
|
|
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
app.AddTimeout (
|
|
TimeSpan.FromMilliseconds (100),
|
|
() =>
|
|
{
|
|
Interlocked.Increment (ref firedCount);
|
|
|
|
return false;
|
|
}
|
|
);
|
|
}
|
|
|
|
Assert.NotEmpty (app.TimedEvents!.Timeouts);
|
|
|
|
app.TimedEvents.StopAll ();
|
|
|
|
Assert.Empty (app.TimedEvents.Timeouts);
|
|
|
|
// Use iteration counter for safety
|
|
var iterations = 0;
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
app.Run<Runnable> ();
|
|
|
|
Assert.Equal (0, firedCount);
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Since all timeouts were cleared, just need enough iterations to prove they won't fire
|
|
// With 100ms timeouts, give ~50 iterations which is more than enough
|
|
if (iterations > 50)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TimedEvents_Timeouts_Property_Is_Thread_Safe ()
|
|
{
|
|
using IApplication app = Application.Create ();
|
|
app.Init ("fake");
|
|
|
|
const int THREAD_COUNT = 10;
|
|
var addedCount = 0;
|
|
var tasksCompleted = new CountdownEvent (THREAD_COUNT);
|
|
|
|
// Add timeouts from multiple threads using Invoke
|
|
for (var i = 0; i < THREAD_COUNT; i++)
|
|
{
|
|
Task.Run (() =>
|
|
{
|
|
app.Invoke (() =>
|
|
{
|
|
// Add timeout with immediate execution
|
|
app.AddTimeout (
|
|
TimeSpan.Zero,
|
|
() =>
|
|
{
|
|
Interlocked.Increment (ref addedCount);
|
|
|
|
return false;
|
|
}
|
|
);
|
|
|
|
tasksCompleted.Signal ();
|
|
}
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
// Use iteration counter to stop when all tasks complete
|
|
var iterations = 0;
|
|
app.Iteration += IterationHandler;
|
|
|
|
try
|
|
{
|
|
app.Run<Runnable> ();
|
|
|
|
// Verify we can safely access the Timeouts property from main thread
|
|
int timeoutCount = app.TimedEvents?.Timeouts.Count ?? 0;
|
|
|
|
// Verify no exceptions occurred
|
|
Assert.True (timeoutCount >= 0, "Should be able to access Timeouts property without exception");
|
|
|
|
// Verify all tasks completed and all timeouts fired
|
|
Assert.True (tasksCompleted.IsSet, "All background tasks should have completed");
|
|
Assert.Equal (THREAD_COUNT, addedCount);
|
|
}
|
|
finally
|
|
{
|
|
app.Iteration -= IterationHandler;
|
|
tasksCompleted.Dispose ();
|
|
}
|
|
|
|
return;
|
|
|
|
void IterationHandler (object? s, EventArgs<IApplication?> e)
|
|
{
|
|
iterations++;
|
|
|
|
// Stop when all tasks completed and all timeouts fired, or safety limit
|
|
if ((tasksCompleted.IsSet && addedCount >= THREAD_COUNT) || iterations > 200)
|
|
{
|
|
app.RequestStop ();
|
|
}
|
|
}
|
|
}
|
|
}
|