Prechádzať zdrojové kódy

Merge branch 'v2_develop' into copilot/restructure-scenarios-standalone

Tig 1 týždeň pred
rodič
commit
95e55465b5

+ 3 - 16
.github/workflows/api-docs.yml

@@ -1,8 +1,8 @@
-name: Build and publish API docs
+name: Build and publish v2 API docs
 
 on:
   push:
-    branches: [v1_release, v2_develop]
+    branches: [v2_develop]
 
 permissions:
   id-token: write 
@@ -10,7 +10,7 @@ permissions:
 
 jobs:
   deploy:
-    name: Build and Deploy API docs to github-pages ${{ github.ref_name }}
+    name: Build and Deploy v2 API docs to github-pages ${{ github.ref_name }}
     environment:
       name: github-pages
       url: ${{ steps.deployment.outputs.page_url }}
@@ -20,7 +20,6 @@ jobs:
       uses: actions/checkout@v4
 
     - name: DocFX Build
-      #if: github.ref_name == 'v1_release' ||  github.ref_name == 'v1_develop'
       working-directory: docfx
       run: |
         dotnet tool install -g docfx
@@ -30,27 +29,15 @@ jobs:
       continue-on-error: false
 
     - name: Setup Pages
-      #if: github.ref_name == 'v1_release' ||  github.ref_name == 'v1_develop'
       uses: actions/configure-pages@v5
       
     - name: Upload artifact
-      #if: github.ref_name == 'v1_release' ||  github.ref_name == 'v1_develop'
       uses: actions/upload-pages-artifact@v3
       with:
         path: docfx/_site
        
     - name: Deploy to GitHub Pages
-      if: github.ref_name == 'v2_release' ||  github.ref_name == 'v2_develop'
       id: deployment
       uses: actions/deploy-pages@v4
       with:
         token: ${{ secrets.GITHUB_TOKEN }}
-
-    # - name: v1_release Repository Dispatch ${{ github.ref_name }}
-    #   if: github.ref_name == 'v2_develop'
-    #   uses: peter-evans/repository-dispatch@v3
-    #   with:
-    #     token: ${{ secrets.V2DOCS_TOKEN }}
-    #     repository: gui-cs/Terminal.GuiV1Docs
-    #     event-type: v2_develop_push
-    #     client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}'

+ 64 - 42
Tests/StressTests/ApplicationStressTests.cs

@@ -1,41 +1,44 @@
+using System.Diagnostics;
 using Xunit.Abstractions;
 
+// ReSharper disable AccessToDisposedClosure
+
 namespace StressTests;
 
-public class ApplicationStressTests
+public class ApplicationStressTests (ITestOutputHelper output)
 {
-    public ApplicationStressTests (ITestOutputHelper output)
-    {
-    }
+    private const int NUM_INCREMENTS = 500;
+
+    private const int NUM_PASSES = 50;
+    private const int POLL_MS_DEBUGGER = 500;
+    private const int POLL_MS_NORMAL = 100;
 
     private static volatile int _tbCounter;
 #pragma warning disable IDE1006 // Naming Styles
     private static readonly ManualResetEventSlim _wakeUp = new (false);
 #pragma warning restore IDE1006 // Naming Styles
 
-    private const int NUM_PASSES = 50;
-    private const int NUM_INCREMENTS = 500;
-    private const int POLL_MS = 100;
 
     /// <summary>
-    /// Stress test for Application.Invoke to verify that invocations from background threads
-    /// are not lost or delayed indefinitely. Tests 25,000 concurrent invocations (50 passes × 500 increments).
+    ///     Stress test for Application.Invoke to verify that invocations from background threads
+    ///     are not lost or delayed indefinitely. Tests 25,000 concurrent invocations (50 passes × 500 increments).
     /// </summary>
     /// <remarks>
-    /// <para>
-    /// This test automatically adapts its timeout when running under a debugger (500ms vs 100ms)
-    /// to account for slower iteration times caused by debugger overhead.
-    /// </para>
-    /// <para>
-    /// See InvokeLeakTest_Analysis.md for technical details about the timing improvements made
-    /// to TimedEvents (Stopwatch-based timing) and Application.Invoke (MainLoop wakeup).
-    /// </para>
+    ///     <para>
+    ///         This test automatically adapts its timeout when running under a debugger (500ms vs 100ms)
+    ///         to account for slower iteration times caused by debugger overhead.
+    ///     </para>
+    ///     <para>
+    ///         See InvokeLeakTest_Analysis.md for technical details about the timing improvements made
+    ///         to TimedEvents (Stopwatch-based timing) and Application.Invoke (MainLoop wakeup).
+    ///     </para>
     /// </remarks>
     [Fact]
     public async Task InvokeLeakTest ()
     {
+        IApplication app = Application.Create ();
+        app.Init ("fake");
 
-        Application.Init (driverName: "fake");
         Random r = new ();
         TextField tf = new ();
         var top = new Window ();
@@ -43,20 +46,21 @@ public class ApplicationStressTests
 
         _tbCounter = 0;
 
-        Task task = Task.Run (() => RunTest (r, tf, NUM_PASSES, NUM_INCREMENTS, POLL_MS));
+        int pollMs = Debugger.IsAttached ? POLL_MS_DEBUGGER : POLL_MS_NORMAL;
+        Task task = Task.Run (() => RunTest (app, r, tf, NUM_PASSES, NUM_INCREMENTS, pollMs));
 
         // blocks here until the RequestStop is processed at the end of the test
-        Application.Run (top);
+        app.Run (top);
 
         await task; // Propagate exception if any occurred
 
         Assert.Equal (NUM_INCREMENTS * NUM_PASSES, _tbCounter);
         top.Dispose ();
-        Application.Shutdown ();
+        app.Dispose ();
 
         return;
 
-        static void RunTest (Random r, TextField tf, int numPasses, int numIncrements, int pollMs)
+        void RunTest (IApplication application, Random random, TextField textField, int numPasses, int numIncrements, int pollMsValue)
         {
             for (var j = 0; j < numPasses; j++)
             {
@@ -64,52 +68,70 @@ public class ApplicationStressTests
 
                 for (var i = 0; i < numIncrements; i++)
                 {
-                    Launch (r, tf, (j + 1) * numIncrements);
+                    Launch (application, random, textField, (j + 1) * numIncrements);
                 }
 
+                int maxWaitMs = pollMsValue * 50; // Maximum total wait time (5s normal, 25s debugger)
+                var elapsedMs = 0;
+
                 while (_tbCounter != (j + 1) * numIncrements) // Wait for tbCounter to reach expected value
                 {
                     int tbNow = _tbCounter;
 
                     // Wait for Application.TopRunnable to be running to ensure timed events can be processed
-                    while (Application.TopRunnableView is null || Application.TopRunnableView is IRunnable { IsRunning: false })
+                    var topRunnableWaitMs = 0;
+
+                    while (application.TopRunnableView is null or IRunnable { IsRunning: false })
                     {
                         Thread.Sleep (1);
+                        topRunnableWaitMs++;
+
+                        if (topRunnableWaitMs > maxWaitMs)
+                        {
+                            application.Invoke (application.Dispose);
+
+                            throw new TimeoutException (
+                                                        $"Timeout: TopRunnableView never started running on pass {j + 1}"
+                                                       );
+                        }
                     }
 
-                    _wakeUp.Wait (pollMs);
+                    _wakeUp.Wait (pollMsValue);
+                    elapsedMs += pollMsValue;
 
                     if (_tbCounter != tbNow)
                     {
+                        elapsedMs = 0; // Reset elapsed time on progress
+
                         continue;
                     }
 
-                    // No change after wait: Idle handlers added via Application.Invoke have gone missing
-                    Application.Invoke (() => Application.RequestStop ());
-
-                    throw new TimeoutException (
-                                                $"Timeout: Increment lost. _tbCounter ({_tbCounter}) didn't "
-                                                + $"change after waiting {pollMs} ms. Failed to reach {(j + 1) * numIncrements} on pass {j + 1}"
-                                               );
+                    if (elapsedMs > maxWaitMs)
+                    {
+                        // No change after maximum wait: Idle handlers added via Application.Invoke have gone missing
+                        application.Invoke (application.Dispose);
+
+                        throw new TimeoutException (
+                                                    $"Timeout: Increment lost. _tbCounter ({_tbCounter}) didn't "
+                                                    + $"change after waiting {maxWaitMs} ms (pollMs={pollMsValue}). "
+                                                    + $"Failed to reach {(j + 1) * numIncrements} on pass {j + 1}"
+                                                   );
+                    }
                 }
-
-                ;
             }
 
-            Application.Invoke (() => Application.RequestStop ());
+            application.Invoke (application.Dispose);
         }
 
-        static void Launch (Random r, TextField tf, int target)
+        static void Launch (IApplication application, Random random, TextField textField, int target)
         {
-            Task.Run (
-                      () =>
+            Task.Run (() =>
                       {
-                          Thread.Sleep (r.Next (2, 4));
+                          Thread.Sleep (random.Next (2, 4));
 
-                          Application.Invoke (
-                                              () =>
+                          application.Invoke (() =>
                                               {
-                                                  tf.Text = $"index{r.Next ()}";
+                                                  textField.Text = $"index{random.Next ()}";
                                                   Interlocked.Increment (ref _tbCounter);
 
                                                   if (target == _tbCounter)

+ 2 - 3
Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs

@@ -380,11 +380,10 @@ public class ApplicationImplTests
         if (app.TopRunnableView != null)
         {
             app.RequestStop ();
-
-            return true;
         }
 
-        return true;
+        // Return false so the timer does not repeat
+        return false;
     }
 
     [Fact]

+ 0 - 2
Tests/UnitTestsParallelizable/Application/ApplicationTests.cs

@@ -245,8 +245,6 @@ public class ApplicationTests (ITestOutputHelper output)
 
         void Application_Iteration (object? sender, EventArgs<IApplication?> e)
         {
-            //Assert.Equal (0, iteration);
-
             iteration++;
             app.RequestStop ();
         }

+ 291 - 263
Tests/UnitTestsParallelizable/Application/NestedRunTimeoutTests.cs

@@ -1,4 +1,3 @@
-#nullable enable
 using Xunit.Abstractions;
 
 namespace ApplicationTests.Timeout;
@@ -11,43 +10,143 @@ namespace ApplicationTests.Timeout;
 public class NestedRunTimeoutTests (ITestOutputHelper output)
 {
     [Fact]
-    public void Timeout_Fires_With_Single_Session ()
+    public void Multiple_Timeouts_Fire_In_Correct_Order_With_Nested_Run ()
     {
         // Arrange
-        using IApplication? app = Application.Create (example: false);
-
+        using IApplication? app = Application.Create ();
         app.Init ("FakeDriver");
 
-        // Create a simple window for the main run loop
+        List<string> executionOrder = new ();
+
         var mainWindow = new Window { Title = "Main Window" };
+        var dialog = new Dialog { Title = "Nested Dialog", Buttons = [new() { Text = "Ok" }] };
+        var nestedRunCompleted = false;
 
-        // Schedule a timeout that will ensure the app quits
-        var requestStopTimeoutFired = false;
-        app.AddTimeout (
-                        TimeSpan.FromMilliseconds (100),
-                        () =>
-                        {
-                            output.WriteLine ($"RequestStop Timeout fired!");
-                            requestStopTimeoutFired = true;
-                            app.RequestStop ();
-                            return false;
-                        }
-                       );
+        // Use iteration counter for safety instead of time-based timeout
+        var iterations = 0;
+        app.Iteration += IterationHandler;
 
-        // Act - Start the main run loop
-        app.Run (mainWindow);
+        try
+        {
+            // Schedule multiple timeouts
+            app.AddTimeout (
+                            TimeSpan.FromMilliseconds (100),
+                            () =>
+                            {
+                                executionOrder.Add ("Timeout1-100ms");
+                                output.WriteLine ("Timeout1 fired at 100ms");
 
-        // Assert
-        Assert.True (requestStopTimeoutFired, "RequestStop Timeout should have fired");
+                                return false;
+                            }
+                           );
 
-        mainWindow.Dispose ();
+            app.AddTimeout (
+                            TimeSpan.FromMilliseconds (200),
+                            () =>
+                            {
+                                executionOrder.Add ("Timeout2-200ms-StartNestedRun");
+                                output.WriteLine ("Timeout2 fired at 200ms - Starting nested run");
+
+                                // Start nested run
+                                app.Run (dialog);
+
+                                executionOrder.Add ("Timeout2-NestedRunEnded");
+                                nestedRunCompleted = true;
+                                output.WriteLine ("Nested run ended");
+
+                                return false;
+                            }
+                           );
+
+            app.AddTimeout (
+                            TimeSpan.FromMilliseconds (300),
+                            () =>
+                            {
+                                executionOrder.Add ("Timeout3-300ms-InNestedRun");
+                                output.WriteLine ($"Timeout3 fired at 300ms - TopRunnable: {app.TopRunnableView?.Title}");
+
+                                // This should fire while dialog is running
+                                Assert.Equal (dialog, app.TopRunnableView);
+
+                                return false;
+                            }
+                           );
+
+            app.AddTimeout (
+                            TimeSpan.FromMilliseconds (400),
+                            () =>
+                            {
+                                executionOrder.Add ("Timeout4-400ms-CloseDialog");
+                                output.WriteLine ("Timeout4 fired at 400ms - Closing dialog");
+
+                                // Close the dialog
+                                app.RequestStop (dialog);
+
+                                return false;
+                            }
+                           );
+
+            // Event-driven: Only stop main window AFTER nested run completes
+            // Use a repeating timeout that checks the condition
+            app.AddTimeout (
+                            TimeSpan.FromMilliseconds (50),
+                            () =>
+                            {
+                                // Keep checking until nested run completes
+                                if (nestedRunCompleted)
+                                {
+                                    executionOrder.Add ("Timeout5-AfterNestedRun-StopMain");
+                                    output.WriteLine ("Timeout5 fired after nested run completed - Stopping main window");
+                                    app.RequestStop (mainWindow);
+
+                                    return false; // Don't repeat
+                                }
+
+                                return true; // Keep checking
+                            }
+                           );
+
+            // Act
+            app.Run (mainWindow);
+
+            // Assert - Verify all timeouts fired in the correct order
+            output.WriteLine ($"Execution order: {string.Join (", ", executionOrder)}");
+
+            Assert.Equal (6, executionOrder.Count); // 5 timeout events + 1 nested run end marker
+            Assert.Equal ("Timeout1-100ms", executionOrder [0]);
+            Assert.Equal ("Timeout2-200ms-StartNestedRun", executionOrder [1]);
+            Assert.Equal ("Timeout3-300ms-InNestedRun", executionOrder [2]);
+            Assert.Equal ("Timeout4-400ms-CloseDialog", executionOrder [3]);
+            Assert.Equal ("Timeout2-NestedRunEnded", executionOrder [4]);
+            Assert.Equal ("Timeout5-AfterNestedRun-StopMain", executionOrder [5]);
+        }
+        finally
+        {
+            app.Iteration -= IterationHandler;
+            dialog.Dispose ();
+            mainWindow.Dispose ();
+        }
+
+        return;
+
+        void IterationHandler (object? s, EventArgs<IApplication?> e)
+        {
+            iterations++;
+
+            // Safety limit - should never be hit with event-driven logic
+            if (iterations > 2000)
+            {
+                output.WriteLine ($"SAFETY: Hit iteration limit. Execution order: {string.Join (", ", executionOrder)}");
+                app.RequestStop ();
+            }
+        }
     }
 
     [Fact]
     public void Timeout_Fires_In_Nested_Run ()
     {
         // Arrange
-        using IApplication? app = Application.Create (example: false);
+        using IApplication? app = Application.Create ();
 
         app.Init ("FakeDriver");
 
@@ -59,60 +158,61 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
         var mainWindow = new Window { Title = "Main Window" };
 
         // Create a dialog for the nested run loop
-        var dialog = new Dialog { Title = "Nested Dialog", Buttons = [new Button { Text = "Ok" }] };
+        var dialog = new Dialog { Title = "Nested Dialog", Buttons = [new() { Text = "Ok" }] };
 
         // Schedule a safety timeout that will ensure the app quits if test hangs
         var requestStopTimeoutFired = false;
+
         app.AddTimeout (
                         TimeSpan.FromMilliseconds (5000),
                         () =>
                         {
-                            output.WriteLine ($"SAFETY: RequestStop Timeout fired - test took too long!");
+                            output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long!");
                             requestStopTimeoutFired = true;
                             app.RequestStop ();
+
                             return false;
                         }
                        );
 
-
         // Schedule a timeout that will fire AFTER the nested run starts and stop the dialog
         app.AddTimeout (
-                         TimeSpan.FromMilliseconds (200),
-                         () =>
-                         {
-                             output.WriteLine ($"DialogRequestStop Timeout fired! TopRunnable: {app.TopRunnableView?.Title ?? "null"}");
-                             timeoutFired = true;
-
-                             // Close the dialog when timeout fires
-                             if (app.TopRunnableView == dialog)
-                             {
-                                 app.RequestStop (dialog);
-                             }
-
-                             return false;
-                         }
-                        );
+                        TimeSpan.FromMilliseconds (200),
+                        () =>
+                        {
+                            output.WriteLine ($"DialogRequestStop Timeout fired! TopRunnable: {app.TopRunnableView?.Title ?? "null"}");
+                            timeoutFired = true;
+
+                            // Close the dialog when timeout fires
+                            if (app.TopRunnableView == dialog)
+                            {
+                                app.RequestStop (dialog);
+                            }
+
+                            return false;
+                        }
+                       );
 
         // After 100ms, start the nested run loop
         app.AddTimeout (
-                         TimeSpan.FromMilliseconds (100),
-                         () =>
-                         {
-                             output.WriteLine ("Starting nested run...");
-                             nestedRunStarted = true;
+                        TimeSpan.FromMilliseconds (100),
+                        () =>
+                        {
+                            output.WriteLine ("Starting nested run...");
+                            nestedRunStarted = true;
 
-                             // This blocks until the dialog is closed (by the timeout at 200ms)
-                             app.Run (dialog);
+                            // This blocks until the dialog is closed (by the timeout at 200ms)
+                            app.Run (dialog);
 
-                             output.WriteLine ("Nested run ended");
-                             nestedRunEnded = true;
+                            output.WriteLine ("Nested run ended");
+                            nestedRunEnded = true;
 
-                             // Stop the main window after nested run completes
-                             app.RequestStop ();
+                            // Stop the main window after nested run completes
+                            app.RequestStop ();
 
-                             return false;
-                         }
-                        );
+                            return false;
+                        }
+                       );
 
         // Act - Start the main run loop
         app.Run (mainWindow);
@@ -129,112 +229,130 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
     }
 
     [Fact]
-    public void Multiple_Timeouts_Fire_In_Correct_Order_With_Nested_Run ()
+    public void Timeout_Fires_With_Single_Session ()
     {
         // Arrange
-        using IApplication? app = Application.Create (example: false);
-        app.Init ("FakeDriver");
+        using IApplication? app = Application.Create ();
 
-        var executionOrder = new List<string> ();
+        app.Init ("FakeDriver");
 
+        // Create a simple window for the main run loop
         var mainWindow = new Window { Title = "Main Window" };
-        var dialog = new Dialog { Title = "Nested Dialog", Buttons = [new Button { Text = "Ok" }] };
 
-        // Schedule a safety timeout that will ensure the app quits if test hangs
+        // Schedule a timeout that will ensure the app quits
         var requestStopTimeoutFired = false;
+
         app.AddTimeout (
-                        TimeSpan.FromMilliseconds (10000),
+                        TimeSpan.FromMilliseconds (100),
                         () =>
                         {
-                            output.WriteLine ($"SAFETY: RequestStop Timeout fired - test took too long!");
+                            output.WriteLine ("RequestStop Timeout fired!");
                             requestStopTimeoutFired = true;
                             app.RequestStop ();
+
                             return false;
                         }
                        );
 
-        // Schedule multiple timeouts
-        app.AddTimeout (
-                         TimeSpan.FromMilliseconds (100),
-                         () =>
-                         {
-                             executionOrder.Add ("Timeout1-100ms");
-                             output.WriteLine ("Timeout1 fired at 100ms");
-                             return false;
-                         }
-                        );
-
-        app.AddTimeout (
-                         TimeSpan.FromMilliseconds (200),
-                         () =>
-                         {
-                             executionOrder.Add ("Timeout2-200ms-StartNestedRun");
-                             output.WriteLine ("Timeout2 fired at 200ms - Starting nested run");
+        // Act - Start the main run loop
+        app.Run (mainWindow);
 
-                             // Start nested run
-                             app.Run (dialog);
+        // Assert
+        Assert.True (requestStopTimeoutFired, "RequestStop Timeout should have fired");
 
-                             executionOrder.Add ("Timeout2-NestedRunEnded");
-                             output.WriteLine ("Nested run ended");
-                             return false;
-                         }
-                        );
+        mainWindow.Dispose ();
+    }
 
-        app.AddTimeout (
-                         TimeSpan.FromMilliseconds (300),
-                         () =>
-                         {
-                             executionOrder.Add ("Timeout3-300ms-InNestedRun");
-                             output.WriteLine ($"Timeout3 fired at 300ms - TopRunnable: {app.TopRunnableView?.Title}");
+    [Fact]
+    public void Timeout_Queue_Persists_Across_Nested_Runs ()
+    {
+        // Verify that the timeout queue is not cleared when nested runs start/end
 
-                             // This should fire while dialog is running
-                             Assert.Equal (dialog, app.TopRunnableView);
+        // Arrange
+        using IApplication? app = Application.Create ();
+        app.Init ("FakeDriver");
 
-                             return false;
-                         }
-                        );
+        // Schedule a safety timeout that will ensure the app quits if test hangs
+        var requestStopTimeoutFired = false;
 
         app.AddTimeout (
-                         TimeSpan.FromMilliseconds (400),
-                         () =>
-                         {
-                             executionOrder.Add ("Timeout4-400ms-CloseDialog");
-                             output.WriteLine ("Timeout4 fired at 400ms - Closing dialog");
+                        TimeSpan.FromMilliseconds (10000),
+                        () =>
+                        {
+                            output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long!");
+                            requestStopTimeoutFired = true;
+                            app.RequestStop ();
 
-                             // Close the dialog
-                             app.RequestStop (dialog);
+                            return false;
+                        }
+                       );
 
-                             return false;
-                         }
-                        );
+        var mainWindow = new Window { Title = "Main Window" };
+        var dialog = new Dialog { Title = "Dialog", Buttons = [new() { Text = "Ok" }] };
 
-        app.AddTimeout (
-                         TimeSpan.FromMilliseconds (500),
-                         () =>
-                         {
-                             executionOrder.Add ("Timeout5-500ms-StopMain");
-                             output.WriteLine ("Timeout5 fired at 500ms - Stopping main window");
+        var initialTimeoutCount = 0;
+        var timeoutCountDuringNestedRun = 0;
+        var timeoutCountAfterNestedRun = 0;
 
-                             // Stop main window
-                             app.RequestStop (mainWindow);
+        // Schedule 5 timeouts at different times with wider spacing
+        for (var i = 0; i < 5; i++)
+        {
+            int capturedI = i;
 
-                             return false;
-                         }
-                        );
+            app.AddTimeout (
+                            TimeSpan.FromMilliseconds (150 * (i + 1)), // Increased spacing from 100ms to 150ms
+                            () =>
+                            {
+                                output.WriteLine ($"Timeout {capturedI} fired at {150 * (capturedI + 1)}ms");
+
+                                if (capturedI == 0)
+                                {
+                                    initialTimeoutCount = app.TimedEvents!.Timeouts.Count;
+                                    output.WriteLine ($"Initial timeout count: {initialTimeoutCount}");
+                                }
+
+                                if (capturedI == 1)
+                                {
+                                    // Start nested run
+                                    output.WriteLine ("Starting nested run");
+                                    app.Run (dialog);
+                                    output.WriteLine ("Nested run ended");
+
+                                    timeoutCountAfterNestedRun = app.TimedEvents!.Timeouts.Count;
+                                    output.WriteLine ($"Timeout count after nested run: {timeoutCountAfterNestedRun}");
+                                }
+
+                                if (capturedI == 2)
+                                {
+                                    // This fires during nested run
+                                    timeoutCountDuringNestedRun = app.TimedEvents!.Timeouts.Count;
+                                    output.WriteLine ($"Timeout count during nested run: {timeoutCountDuringNestedRun}");
+
+                                    // Close dialog
+                                    app.RequestStop (dialog);
+                                }
+
+                                if (capturedI == 4)
+                                {
+                                    // Stop main window
+                                    app.RequestStop (mainWindow);
+                                }
+
+                                return false;
+                            }
+                           );
+        }
 
         // Act
         app.Run (mainWindow);
 
-        // Assert - Verify all timeouts fired in the correct order
-        output.WriteLine ($"Execution order: {string.Join (", ", executionOrder)}");
+        // Assert
+        output.WriteLine ($"Final counts - Initial: {initialTimeoutCount}, During: {timeoutCountDuringNestedRun}, After: {timeoutCountAfterNestedRun}");
 
-        Assert.Equal (6, executionOrder.Count); // 5 timeouts + 1 nested run end marker
-        Assert.Equal ("Timeout1-100ms", executionOrder [0]);
-        Assert.Equal ("Timeout2-200ms-StartNestedRun", executionOrder [1]);
-        Assert.Equal ("Timeout3-300ms-InNestedRun", executionOrder [2]);
-        Assert.Equal ("Timeout4-400ms-CloseDialog", executionOrder [3]);
-        Assert.Equal ("Timeout2-NestedRunEnded", executionOrder [4]);
-        Assert.Equal ("Timeout5-500ms-StopMain", executionOrder [5]);
+        // The timeout queue should have pending timeouts throughout
+        Assert.True (initialTimeoutCount >= 0, "Should have timeouts in queue initially");
+        Assert.True (timeoutCountDuringNestedRun >= 0, "Should have timeouts in queue during nested run");
+        Assert.True (timeoutCountAfterNestedRun >= 0, "Should have timeouts in queue after nested run");
 
         Assert.False (requestStopTimeoutFired, "Safety timeout should NOT have fired");
 
@@ -251,7 +369,7 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
         // - A subsequent timeout should still fire during the nested run (like ESC closing MessageBox)
 
         // Arrange
-        using IApplication? app = Application.Create (example: false);
+        using IApplication? app = Application.Create ();
         app.Init ("FakeDriver");
 
         var enterFired = false;
@@ -260,175 +378,85 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
         var messageBoxClosed = false;
 
         var mainWindow = new Window { Title = "Login Window" };
-        var messageBox = new Dialog { Title = "Success", Buttons = [new Button { Text = "Ok" }] };
+        var messageBox = new Dialog { Title = "Success", Buttons = [new() { Text = "Ok" }] };
 
         // Schedule a safety timeout that will ensure the app quits if test hangs
         var requestStopTimeoutFired = false;
+
         app.AddTimeout (
                         TimeSpan.FromMilliseconds (10000),
                         () =>
                         {
-                            output.WriteLine ($"SAFETY: RequestStop Timeout fired - test took too long!");
+                            output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long!");
                             requestStopTimeoutFired = true;
                             app.RequestStop ();
+
                             return false;
                         }
                        );
-        
+
         // Schedule "Enter" timeout at 100ms
         app.AddTimeout (
-                         TimeSpan.FromMilliseconds (100),
-                         () =>
-                         {
-                             output.WriteLine ("Enter timeout fired - showing MessageBox");
-                             enterFired = true;
-
-                             // Simulate Enter key opening MessageBox
-                             messageBoxShown = true;
-                             app.Run (messageBox);
-                             messageBoxClosed = true;
-
-                             output.WriteLine ("MessageBox closed");
-                             return false;
-                         }
-                        );
+                        TimeSpan.FromMilliseconds (100),
+                        () =>
+                        {
+                            output.WriteLine ("Enter timeout fired - showing MessageBox");
+                            enterFired = true;
 
-        // Schedule "ESC" timeout at 200ms (should fire while MessageBox is running)
-        app.AddTimeout (
-                         TimeSpan.FromMilliseconds (200),
-                         () =>
-                         {
-                             output.WriteLine ($"ESC timeout fired - TopRunnable: {app.TopRunnableView?.Title}");
-                             escFired = true;
-
-                             // Simulate ESC key closing MessageBox
-                             if (app.TopRunnableView == messageBox)
-                             {
-                                 output.WriteLine ("Closing MessageBox with ESC");
-                                 app.RequestStop (messageBox);
-                             }
-
-                             return false;
-                         }
-                        );
-
-        // Stop main window after MessageBox closes
-        app.AddTimeout (
-                         TimeSpan.FromMilliseconds (300),
-                         () =>
-                         {
-                             output.WriteLine ("Stopping main window");
-                             app.RequestStop (mainWindow);
-                             return false;
-                         }
-                        );
+                            // Simulate Enter key opening MessageBox
+                            messageBoxShown = true;
+                            app.Run (messageBox);
+                            messageBoxClosed = true;
 
-        // Act
-        app.Run (mainWindow);
-
-        // Assert
-        Assert.True (enterFired, "Enter timeout should have fired");
-        Assert.True (messageBoxShown, "MessageBox should have been shown");
-        Assert.True (escFired, "ESC timeout should have fired during MessageBox"); // THIS WAS THE BUG - NOW FIXED!
-        Assert.True (messageBoxClosed, "MessageBox should have been closed");
+                            output.WriteLine ("MessageBox closed");
 
-        Assert.False (requestStopTimeoutFired, "Safety timeout should NOT have fired");
+                            return false;
+                        }
+                       );
 
-        messageBox.Dispose ();
-        mainWindow.Dispose ();
-    }
+        // Schedule "ESC" timeout at 200ms (should fire while MessageBox is running)
+        app.AddTimeout (
+                        TimeSpan.FromMilliseconds (200),
+                        () =>
+                        {
+                            output.WriteLine ($"ESC timeout fired - TopRunnable: {app.TopRunnableView?.Title}");
+                            escFired = true;
 
-    [Fact]
-    public void Timeout_Queue_Persists_Across_Nested_Runs ()
-    {
-        // Verify that the timeout queue is not cleared when nested runs start/end
+                            // Simulate ESC key closing MessageBox
+                            if (app.TopRunnableView == messageBox)
+                            {
+                                output.WriteLine ("Closing MessageBox with ESC");
+                                app.RequestStop (messageBox);
+                            }
 
-        // Arrange
-        using IApplication? app = Application.Create (example: false);
-        app.Init ("FakeDriver");
+                            return false;
+                        }
+                       );
 
-        // Schedule a safety timeout that will ensure the app quits if test hangs
-        var requestStopTimeoutFired = false;
+        // Increased delay from 300ms to 500ms to ensure nested run completes before stopping main
         app.AddTimeout (
-                        TimeSpan.FromMilliseconds (10000),
+                        TimeSpan.FromMilliseconds (500),
                         () =>
                         {
-                            output.WriteLine ($"SAFETY: RequestStop Timeout fired - test took too long!");
-                            requestStopTimeoutFired = true;
-                            app.RequestStop ();
+                            output.WriteLine ("Stopping main window");
+                            app.RequestStop (mainWindow);
+
                             return false;
                         }
                        );
 
-        var mainWindow = new Window { Title = "Main Window" };
-        var dialog = new Dialog { Title = "Dialog", Buttons = [new Button { Text = "Ok" }] };
-
-        int initialTimeoutCount = 0;
-        int timeoutCountDuringNestedRun = 0;
-        int timeoutCountAfterNestedRun = 0;
-
-        // Schedule 5 timeouts at different times
-        for (int i = 0; i < 5; i++)
-        {
-            int capturedI = i;
-            app.AddTimeout (
-                             TimeSpan.FromMilliseconds (100 * (i + 1)),
-                             () =>
-                             {
-                                 output.WriteLine ($"Timeout {capturedI} fired at {100 * (capturedI + 1)}ms");
-
-                                 if (capturedI == 0)
-                                 {
-                                     initialTimeoutCount = app.TimedEvents!.Timeouts.Count;
-                                     output.WriteLine ($"Initial timeout count: {initialTimeoutCount}");
-                                 }
-
-                                 if (capturedI == 1)
-                                 {
-                                     // Start nested run
-                                     output.WriteLine ("Starting nested run");
-                                     app.Run (dialog);
-                                     output.WriteLine ("Nested run ended");
-
-                                     timeoutCountAfterNestedRun = app.TimedEvents!.Timeouts.Count;
-                                     output.WriteLine ($"Timeout count after nested run: {timeoutCountAfterNestedRun}");
-                                 }
-
-                                 if (capturedI == 2)
-                                 {
-                                     // This fires during nested run
-                                     timeoutCountDuringNestedRun = app.TimedEvents!.Timeouts.Count;
-                                     output.WriteLine ($"Timeout count during nested run: {timeoutCountDuringNestedRun}");
-
-                                     // Close dialog
-                                     app.RequestStop (dialog);
-                                 }
-
-                                 if (capturedI == 4)
-                                 {
-                                     // Stop main window
-                                     app.RequestStop (mainWindow);
-                                 }
-
-                                 return false;
-                             }
-                            );
-        }
-
         // Act
         app.Run (mainWindow);
 
         // Assert
-        output.WriteLine ($"Final counts - Initial: {initialTimeoutCount}, During: {timeoutCountDuringNestedRun}, After: {timeoutCountAfterNestedRun}");
-
-        // The timeout queue should have pending timeouts throughout
-        Assert.True (initialTimeoutCount >= 0, "Should have timeouts in queue initially");
-        Assert.True (timeoutCountDuringNestedRun >= 0, "Should have timeouts in queue during nested run");
-        Assert.True (timeoutCountAfterNestedRun >= 0, "Should have timeouts in queue after nested run");
+        Assert.True (enterFired, "Enter timeout should have fired");
+        Assert.True (messageBoxShown, "MessageBox should have been shown");
+        Assert.True (escFired, "ESC timeout should have fired during MessageBox"); // THIS WAS THE BUG - NOW FIXED!
+        Assert.True (messageBoxClosed, "MessageBox should have been closed");
 
         Assert.False (requestStopTimeoutFired, "Safety timeout should NOT have fired");
 
-        dialog.Dispose ();
+        messageBox.Dispose ();
         mainWindow.Dispose ();
     }
 }

+ 811 - 6
Tests/UnitTestsParallelizable/Application/TimeoutTests.cs

@@ -1,19 +1,125 @@
-#nullable enable
 using Xunit.Abstractions;
+// ReSharper disable AccessToDisposedClosure
+#pragma warning disable xUnit1031
 
 namespace ApplicationTests.Timeout;
 
 /// <summary>
-///     Tests for timeout behavior with nested Application.Run() calls.
-///     These tests verify that timeouts scheduled in a parent run loop continue to fire
-///     correctly when a nested modal dialog is shown via Application.Run().
+///     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 ()
     {
-        IApplication app = Application.Create ();
+        using IApplication app = Application.Create ();
         app.Init ("fake");
 
         uint timeoutTime = 100;
@@ -43,9 +149,708 @@ public class TimeoutTests (ITestOutputHelper output)
 
         // 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");
 
-        app.Dispose ();
+        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 ();
+            }
+        }
+    }
 }

+ 1 - 1
Tests/UnitTestsParallelizable/runsettings.coverage.xml

@@ -21,7 +21,7 @@
 		<ParallelizeAssembly>true</ParallelizeAssembly>
 		<ParallelizeTestCollections>true</ParallelizeTestCollections>
 		<!-- Enable collection parallelism -->
-		<MaxParallelThreads>unlimited</MaxParallelThreads>
+		<MaxParallelThreads>2x</MaxParallelThreads>
 		<!-- Or 'unlimited' / '2x' for CPU multiplier -->
 		<StopOnFail>true</StopOnFail>
 		<!-- Still stop on first failure -->

+ 1 - 1
Tests/UnitTestsParallelizable/runsettings.xml

@@ -6,7 +6,7 @@
 		<ParallelizeAssembly>true</ParallelizeAssembly>
 		<ParallelizeTestCollections>true</ParallelizeTestCollections>
 		<!-- Enable collection parallelism -->
-		<MaxParallelThreads>unlimited</MaxParallelThreads>
+		<MaxParallelThreads>2x</MaxParallelThreads>
 		<!-- Or 'unlimited' / '2x' for CPU multiplier -->
 		<StopOnFail>true</StopOnFail>
 		<!-- Still stop on first failure -->

+ 1 - 1
Tests/UnitTestsParallelizable/xunit.runner.json

@@ -3,5 +3,5 @@
   "parallelizeTestCollections": true,
   "parallelizeAssembly": true,
   "stopOnFail": false,
-  "maxParallelThreads": "default"
+  "maxParallelThreads": "4x"
 }