| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856 |
- 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 (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<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 ();
- }
- }
- }
- }
|