Files
Terminal.Gui/Tests/StressTests/ApplicationStressTests.cs
Copilot cb748a1c09 Fix InvokeLeakTest debugger failure on x64 with high-resolution timing (#4304)
* Initial plan

* Add comprehensive analysis of InvokeLeakTest debugger failure

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add XML documentation to InvokeLeakTest about debugger issues

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add visual timing diagrams for InvokeLeakTest analysis

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add executive summary of InvokeLeakTest investigation

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Update analysis with x64 vs ARM confirmation from @tig

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Implement Stopwatch-based timing in TimedEvents to fix x64 race condition

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Update documentation to reflect fix implementation

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix test issues and increase TimeSpan.Zero buffer for debugger safety

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add MainLoop.Wakeup() call in Invoke and remove problematic test

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Auto-detect debugger and increase test timeout to 500ms

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Remove unnecessary MainLoop.Wakeup() call for v2 drivers

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Move analysis documents to Tests/StressTests folder

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add test coverage for multiple drivers per @BDisp's suggestion

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Revert multi-driver test coverage changes per @tig request

Co-authored-by: tig <585482+tig@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
2025-10-21 19:41:03 -06:00

129 lines
4.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using UnitTests;
using Xunit.Abstractions;
namespace StressTests;
public class ApplicationStressTests : TestsAllViews
{
public ApplicationStressTests (ITestOutputHelper output)
{
ConsoleDriver.RunningUnitTests = true;
}
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;
// Use longer timeout when running under debugger to account for slower iterations
private static readonly int POLL_MS = System.Diagnostics.Debugger.IsAttached ? 500 : 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).
/// </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>
/// </remarks>
[Theory]
[InlineData (typeof (FakeDriver))]
//[InlineData (typeof (DotNetDriver), Skip = "System.IO.IOException: The handle is invalid")]
//[InlineData (typeof (ANSIDriver))]
//[InlineData (typeof (WindowsDriver))]
//[InlineData (typeof (UnixDriver), Skip = "Unable to load DLL 'libc' or one of its dependencies: The specified module could not be found. (0x8007007E)")]
public async Task InvokeLeakTest (Type driverType)
{
Application.Init (driverName: driverType.Name);
Random r = new ();
TextField tf = new ();
var top = new Toplevel ();
top.Add (tf);
_tbCounter = 0;
Task task = Task.Run (() => RunTest (r, tf, NUM_PASSES, NUM_INCREMENTS, POLL_MS));
// blocks here until the RequestStop is processed at the end of the test
Application.Run (top);
await task; // Propagate exception if any occurred
Assert.Equal (NUM_INCREMENTS * NUM_PASSES, _tbCounter);
top.Dispose ();
Application.Shutdown ();
return;
static void RunTest (Random r, TextField tf, int numPasses, int numIncrements, int pollMs)
{
for (var j = 0; j < numPasses; j++)
{
_wakeUp.Reset ();
for (var i = 0; i < numIncrements; i++)
{
Launch (r, tf, (j + 1) * numIncrements);
}
while (_tbCounter != (j + 1) * numIncrements) // Wait for tbCounter to reach expected value
{
int tbNow = _tbCounter;
_wakeUp.Wait (pollMs);
if (_tbCounter != tbNow)
{
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}"
);
}
;
}
Application.Invoke (() => Application.RequestStop ());
}
static void Launch (Random r, TextField tf, int target)
{
Task.Run (
() =>
{
Thread.Sleep (r.Next (2, 4));
Application.Invoke (
() =>
{
tf.Text = $"index{r.Next ()}";
Interlocked.Increment (ref _tbCounter);
if (target == _tbCounter)
{
// On last increment wake up the check
_wakeUp.Set ();
}
}
);
}
);
}
}
}