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

This commit is contained in:
Tig
2025-12-03 10:31:18 -07:00
committed by GitHub
9 changed files with 1192 additions and 353 deletions

View File

@@ -1,8 +1,8 @@
name: Build and publish API docs name: Build and publish v2 API docs
on: on:
push: push:
branches: [v1_release, v2_develop] branches: [v2_develop]
permissions: permissions:
id-token: write id-token: write
@@ -10,7 +10,7 @@ permissions:
jobs: jobs:
deploy: 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: environment:
name: github-pages name: github-pages
url: ${{ steps.deployment.outputs.page_url }} url: ${{ steps.deployment.outputs.page_url }}
@@ -20,7 +20,6 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: DocFX Build - name: DocFX Build
#if: github.ref_name == 'v1_release' || github.ref_name == 'v1_develop'
working-directory: docfx working-directory: docfx
run: | run: |
dotnet tool install -g docfx dotnet tool install -g docfx
@@ -30,27 +29,15 @@ jobs:
continue-on-error: false continue-on-error: false
- name: Setup Pages - name: Setup Pages
#if: github.ref_name == 'v1_release' || github.ref_name == 'v1_develop'
uses: actions/configure-pages@v5 uses: actions/configure-pages@v5
- name: Upload artifact - name: Upload artifact
#if: github.ref_name == 'v1_release' || github.ref_name == 'v1_develop'
uses: actions/upload-pages-artifact@v3 uses: actions/upload-pages-artifact@v3
with: with:
path: docfx/_site path: docfx/_site
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
if: github.ref_name == 'v2_release' || github.ref_name == 'v2_develop'
id: deployment id: deployment
uses: actions/deploy-pages@v4 uses: actions/deploy-pages@v4
with: with:
token: ${{ secrets.GITHUB_TOKEN }} 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 }}"}'

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
#nullable enable
using Xunit.Abstractions; using Xunit.Abstractions;
namespace ApplicationTests.Timeout; namespace ApplicationTests.Timeout;
@@ -11,43 +10,143 @@ namespace ApplicationTests.Timeout;
public class NestedRunTimeoutTests (ITestOutputHelper output) public class NestedRunTimeoutTests (ITestOutputHelper output)
{ {
[Fact] [Fact]
public void Timeout_Fires_With_Single_Session () public void Multiple_Timeouts_Fire_In_Correct_Order_With_Nested_Run ()
{ {
// Arrange // Arrange
using IApplication? app = Application.Create (example: false); using IApplication? app = Application.Create ();
app.Init ("FakeDriver"); app.Init ("FakeDriver");
// Create a simple window for the main run loop List<string> executionOrder = new ();
var mainWindow = new Window { Title = "Main Window" }; 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 // Use iteration counter for safety instead of time-based timeout
var requestStopTimeoutFired = false; var iterations = 0;
app.AddTimeout ( app.Iteration += IterationHandler;
TimeSpan.FromMilliseconds (100),
() =>
{
output.WriteLine ($"RequestStop Timeout fired!");
requestStopTimeoutFired = true;
app.RequestStop ();
return false;
}
);
// Act - Start the main run loop try
app.Run (mainWindow); {
// Schedule multiple timeouts
app.AddTimeout (
TimeSpan.FromMilliseconds (100),
() =>
{
executionOrder.Add ("Timeout1-100ms");
output.WriteLine ("Timeout1 fired at 100ms");
// Assert return false;
Assert.True (requestStopTimeoutFired, "RequestStop Timeout should have fired"); }
);
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] [Fact]
public void Timeout_Fires_In_Nested_Run () public void Timeout_Fires_In_Nested_Run ()
{ {
// Arrange // Arrange
using IApplication? app = Application.Create (example: false); using IApplication? app = Application.Create ();
app.Init ("FakeDriver"); app.Init ("FakeDriver");
@@ -59,60 +158,61 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
var mainWindow = new Window { Title = "Main Window" }; var mainWindow = new Window { Title = "Main Window" };
// Create a dialog for the nested run loop // 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 // Schedule a safety timeout that will ensure the app quits if test hangs
var requestStopTimeoutFired = false; var requestStopTimeoutFired = false;
app.AddTimeout ( app.AddTimeout (
TimeSpan.FromMilliseconds (5000), 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; requestStopTimeoutFired = true;
app.RequestStop (); app.RequestStop ();
return false; return false;
} }
); );
// Schedule a timeout that will fire AFTER the nested run starts and stop the dialog // Schedule a timeout that will fire AFTER the nested run starts and stop the dialog
app.AddTimeout ( app.AddTimeout (
TimeSpan.FromMilliseconds (200), TimeSpan.FromMilliseconds (200),
() => () =>
{ {
output.WriteLine ($"DialogRequestStop Timeout fired! TopRunnable: {app.TopRunnableView?.Title ?? "null"}"); output.WriteLine ($"DialogRequestStop Timeout fired! TopRunnable: {app.TopRunnableView?.Title ?? "null"}");
timeoutFired = true; timeoutFired = true;
// Close the dialog when timeout fires // Close the dialog when timeout fires
if (app.TopRunnableView == dialog) if (app.TopRunnableView == dialog)
{ {
app.RequestStop (dialog); app.RequestStop (dialog);
} }
return false; return false;
} }
); );
// After 100ms, start the nested run loop // After 100ms, start the nested run loop
app.AddTimeout ( app.AddTimeout (
TimeSpan.FromMilliseconds (100), TimeSpan.FromMilliseconds (100),
() => () =>
{ {
output.WriteLine ("Starting nested run..."); output.WriteLine ("Starting nested run...");
nestedRunStarted = true; nestedRunStarted = true;
// This blocks until the dialog is closed (by the timeout at 200ms) // This blocks until the dialog is closed (by the timeout at 200ms)
app.Run (dialog); app.Run (dialog);
output.WriteLine ("Nested run ended"); output.WriteLine ("Nested run ended");
nestedRunEnded = true; nestedRunEnded = true;
// Stop the main window after nested run completes // Stop the main window after nested run completes
app.RequestStop (); app.RequestStop ();
return false; return false;
} }
); );
// Act - Start the main run loop // Act - Start the main run loop
app.Run (mainWindow); app.Run (mainWindow);
@@ -129,212 +229,37 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
} }
[Fact] [Fact]
public void Multiple_Timeouts_Fire_In_Correct_Order_With_Nested_Run () public void Timeout_Fires_With_Single_Session ()
{ {
// Arrange // Arrange
using IApplication? app = Application.Create (example: false); using IApplication? app = Application.Create ();
app.Init ("FakeDriver"); app.Init ("FakeDriver");
var executionOrder = new List<string> (); // Create a simple window for the main run loop
var mainWindow = new Window { Title = "Main Window" }; 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; var requestStopTimeoutFired = false;
app.AddTimeout ( 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; requestStopTimeoutFired = true;
app.RequestStop (); app.RequestStop ();
return false; return false;
} }
); );
// Schedule multiple timeouts // Act - Start the main run loop
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");
// Start nested run
app.Run (dialog);
executionOrder.Add ("Timeout2-NestedRunEnded");
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;
}
);
app.AddTimeout (
TimeSpan.FromMilliseconds (500),
() =>
{
executionOrder.Add ("Timeout5-500ms-StopMain");
output.WriteLine ("Timeout5 fired at 500ms - Stopping main window");
// 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.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]);
Assert.False (requestStopTimeoutFired, "Safety timeout should NOT have fired");
dialog.Dispose ();
mainWindow.Dispose ();
}
[Fact]
public void Timeout_Scheduled_Before_Nested_Run_Fires_During_Nested_Run ()
{
// This test specifically reproduces the ESC key issue scenario:
// - Timeouts are scheduled upfront (like demo keys)
// - A timeout fires and triggers a nested run (like Enter opening MessageBox)
// - A subsequent timeout should still fire during the nested run (like ESC closing MessageBox)
// Arrange
using IApplication? app = Application.Create (example: false);
app.Init ("FakeDriver");
var enterFired = false;
var escFired = false;
var messageBoxShown = false;
var messageBoxClosed = false;
var mainWindow = new Window { Title = "Login Window" };
var messageBox = new Dialog { Title = "Success", Buttons = [new Button { 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!");
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;
}
);
// 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;
}
);
// Act
app.Run (mainWindow); app.Run (mainWindow);
// Assert // Assert
Assert.True (enterFired, "Enter timeout should have fired"); Assert.True (requestStopTimeoutFired, "RequestStop 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");
messageBox.Dispose ();
mainWindow.Dispose (); mainWindow.Dispose ();
} }
@@ -344,75 +269,78 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
// Verify that the timeout queue is not cleared when nested runs start/end // Verify that the timeout queue is not cleared when nested runs start/end
// Arrange // Arrange
using IApplication? app = Application.Create (example: false); using IApplication? app = Application.Create ();
app.Init ("FakeDriver"); app.Init ("FakeDriver");
// Schedule a safety timeout that will ensure the app quits if test hangs // Schedule a safety timeout that will ensure the app quits if test hangs
var requestStopTimeoutFired = false; var requestStopTimeoutFired = false;
app.AddTimeout ( app.AddTimeout (
TimeSpan.FromMilliseconds (10000), 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; requestStopTimeoutFired = true;
app.RequestStop (); app.RequestStop ();
return false; return false;
} }
); );
var mainWindow = new Window { Title = "Main Window" }; var mainWindow = new Window { Title = "Main Window" };
var dialog = new Dialog { Title = "Dialog", Buttons = [new Button { Text = "Ok" }] }; var dialog = new Dialog { Title = "Dialog", Buttons = [new() { Text = "Ok" }] };
int initialTimeoutCount = 0; var initialTimeoutCount = 0;
int timeoutCountDuringNestedRun = 0; var timeoutCountDuringNestedRun = 0;
int timeoutCountAfterNestedRun = 0; var timeoutCountAfterNestedRun = 0;
// Schedule 5 timeouts at different times // Schedule 5 timeouts at different times with wider spacing
for (int i = 0; i < 5; i++) for (var i = 0; i < 5; i++)
{ {
int capturedI = i; int capturedI = i;
app.AddTimeout ( app.AddTimeout (
TimeSpan.FromMilliseconds (100 * (i + 1)), TimeSpan.FromMilliseconds (150 * (i + 1)), // Increased spacing from 100ms to 150ms
() => () =>
{ {
output.WriteLine ($"Timeout {capturedI} fired at {100 * (capturedI + 1)}ms"); output.WriteLine ($"Timeout {capturedI} fired at {150 * (capturedI + 1)}ms");
if (capturedI == 0) if (capturedI == 0)
{ {
initialTimeoutCount = app.TimedEvents!.Timeouts.Count; initialTimeoutCount = app.TimedEvents!.Timeouts.Count;
output.WriteLine ($"Initial timeout count: {initialTimeoutCount}"); output.WriteLine ($"Initial timeout count: {initialTimeoutCount}");
} }
if (capturedI == 1) if (capturedI == 1)
{ {
// Start nested run // Start nested run
output.WriteLine ("Starting nested run"); output.WriteLine ("Starting nested run");
app.Run (dialog); app.Run (dialog);
output.WriteLine ("Nested run ended"); output.WriteLine ("Nested run ended");
timeoutCountAfterNestedRun = app.TimedEvents!.Timeouts.Count; timeoutCountAfterNestedRun = app.TimedEvents!.Timeouts.Count;
output.WriteLine ($"Timeout count after nested run: {timeoutCountAfterNestedRun}"); output.WriteLine ($"Timeout count after nested run: {timeoutCountAfterNestedRun}");
} }
if (capturedI == 2) if (capturedI == 2)
{ {
// This fires during nested run // This fires during nested run
timeoutCountDuringNestedRun = app.TimedEvents!.Timeouts.Count; timeoutCountDuringNestedRun = app.TimedEvents!.Timeouts.Count;
output.WriteLine ($"Timeout count during nested run: {timeoutCountDuringNestedRun}"); output.WriteLine ($"Timeout count during nested run: {timeoutCountDuringNestedRun}");
// Close dialog // Close dialog
app.RequestStop (dialog); app.RequestStop (dialog);
} }
if (capturedI == 4) if (capturedI == 4)
{ {
// Stop main window // Stop main window
app.RequestStop (mainWindow); app.RequestStop (mainWindow);
} }
return false; return false;
} }
); );
} }
// Act // Act
@@ -431,4 +359,104 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
dialog.Dispose (); dialog.Dispose ();
mainWindow.Dispose (); mainWindow.Dispose ();
} }
[Fact]
public void Timeout_Scheduled_Before_Nested_Run_Fires_During_Nested_Run ()
{
// This test specifically reproduces the ESC key issue scenario:
// - Timeouts are scheduled upfront (like demo keys)
// - A timeout fires and triggers a nested run (like Enter opening MessageBox)
// - A subsequent timeout should still fire during the nested run (like ESC closing MessageBox)
// Arrange
using IApplication? app = Application.Create ();
app.Init ("FakeDriver");
var enterFired = false;
var escFired = false;
var messageBoxShown = false;
var messageBoxClosed = false;
var mainWindow = new Window { Title = "Login Window" };
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!");
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;
}
);
// 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;
}
);
// Increased delay from 300ms to 500ms to ensure nested run completes before stopping main
app.AddTimeout (
TimeSpan.FromMilliseconds (500),
() =>
{
output.WriteLine ("Stopping main window");
app.RequestStop (mainWindow);
return false;
}
);
// 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");
Assert.False (requestStopTimeoutFired, "Safety timeout should NOT have fired");
messageBox.Dispose ();
mainWindow.Dispose ();
}
} }

View File

@@ -1,19 +1,125 @@
#nullable enable
using Xunit.Abstractions; using Xunit.Abstractions;
// ReSharper disable AccessToDisposedClosure
#pragma warning disable xUnit1031
namespace ApplicationTests.Timeout; namespace ApplicationTests.Timeout;
/// <summary> /// <summary>
/// Tests for timeout behavior with nested Application.Run() calls. /// Tests for timeout behavior and functionality.
/// These tests verify that timeouts scheduled in a parent run loop continue to fire /// These tests verify that timeouts fire correctly, can be added/removed,
/// correctly when a nested modal dialog is shown via Application.Run(). /// handle exceptions properly, and work with Application.Run() calls.
/// </summary> /// </summary>
public class TimeoutTests (ITestOutputHelper output) 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] [Fact]
public void AddTimeout_Fires () public void AddTimeout_Fires ()
{ {
IApplication app = Application.Create (); using IApplication app = Application.Create ();
app.Init ("fake"); app.Init ("fake");
uint timeoutTime = 100; uint timeoutTime = 100;
@@ -43,9 +149,708 @@ public class TimeoutTests (ITestOutputHelper output)
// The timeout should have fired // The timeout should have fired
Assert.True (timeoutFired); Assert.True (timeoutFired);
app.Dispose ();
} }
[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 ();
}
}
}
} }

View File

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

View File

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

View File

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