using Xunit.Abstractions; // ReSharper disable AccessToDisposedClosure #pragma warning disable xUnit1031 namespace ApplicationTests.Timeout; /// /// 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. /// public class TimeoutTests (ITestOutputHelper output) { [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 (); Assert.True (firstFired); Assert.True (secondFired); } finally { app.Iteration -= IterationHandler; } return; void IterationHandler (object? s, EventArgs 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 (() => app.Run ()); Assert.True (exceptionThrown, "Exception callback should have been invoked"); } finally { app.Iteration -= IterationHandler; } return; void IterationHandler (object? s, EventArgs 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 (); // 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 (); // 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 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 (); Assert.Equal (TIMEOUT_COUNT, firedCount); } finally { app.Iteration -= IterationHandler; } return; void IterationHandler (object? s, EventArgs 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 (); Assert.True (firstStarted); Assert.True (secondFired); Assert.True (firstCompleted); } finally { app.Iteration -= IterationHandler; } return; void IterationHandler (object? s, EventArgs 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 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 (); Assert.Equal (new [] { 1, 2, 3 }, executionOrder); } finally { app.Iteration -= IterationHandler; } return; void IterationHandler (object? s, EventArgs 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 (); Assert.Equal (TIMEOUT_COUNT, firedCount); } finally { app.Iteration -= IterationHandler; } return; void IterationHandler (object? s, EventArgs 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 (); 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 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 (); Assert.Equal (3, fireCount); } finally { app.Iteration -= IterationHandler; } return; void IterationHandler (object? s, EventArgs 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 (); 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 (); 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 (); Assert.False (timeoutFired); } finally { app.Iteration -= IterationHandler; } return; void IterationHandler (object? s, EventArgs 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 (); Assert.Equal (0, firedCount); } finally { app.Iteration -= IterationHandler; } return; void IterationHandler (object? s, EventArgs 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 (); // 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 e) { iterations++; // Stop when all tasks completed and all timeouts fired, or safety limit if ((tasksCompleted.IsSet && addedCount >= THREAD_COUNT) || iterations > 200) { app.RequestStop (); } } } }