Skip to content

Commit f1393db

Browse files
Make OnIdle tests deterministic by polling instead of sleeping
`CanRunOnIdleTask` (and its twin `CanRunOnIdleInProfileTask`) were flaky on the net462 (Windows PowerShell 5.1) CI leg — the former was just caught failing on PR #2298's Windows job. The root cause is that `PsesInternalHost.OnPowerShellIdle` calls `Events.GenerateEvent(PSEngineEvent.OnIdle, ...)`, which only *enqueues* the event. For a subscriber registered with `-Action {...}`, PowerShell doesn't run the action scriptblock inline; it becomes a pending action that the engine dispatches asynchronously on the pipeline thread, around subsequent pipeline invocations. So the action's execution was never synchronized with the test's `$handled` read, and the fixed `Thread.Sleep(2000)` was just a timing guess — sometimes too short on the slower WinPS leg, leaving `$global:handled` still `$false` at the assertion. The key realization is that each *additional* pipeline execution gives the engine another chance to drain the pending action, so re-reading the handler variable in a loop both waits for *and* drives completion. I replaced the sleep with a shared `WaitForHandledAsync` helper that polls the variable (~200ms apart, ~15s ceiling) until it reports `$true`, returning the last observed value on timeout so the assertion still fails loudly. This keeps the tests' intent intact and isn't merely a longer sleep. I validated both tests on net8.0 (green across repeated runs, ~0.4s each vs. the old fixed 2s); net462 can't run on macOS, but the mechanism is identical across targets and the 15s ceiling self-terminates on success, so it's strictly safer on the slow leg without slowing the fast one. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6ad4f46 commit f1393db

1 file changed

Lines changed: 34 additions & 12 deletions

File tree

test/PowerShellEditorServices.Test/Session/PsesInternalHostTests.cs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,38 @@ namespace PowerShellEditorServices.Test.Session
1919
using System.Management.Automation;
2020
using System.Management.Automation.Runspaces;
2121

22+
// Shared helpers for the OnIdle engine-event tests, whose handler actions are
23+
// dispatched asynchronously by PowerShell's event manager.
24+
internal static class OnIdleTestHelpers
25+
{
26+
// The OnIdle engine event's -Action scriptblock is not run inline when
27+
// OnPowerShellIdle generates the event; PowerShell enqueues it as a pending
28+
// action and dispatches it asynchronously around subsequent pipeline executions.
29+
// So instead of sleeping a fixed amount, poll the handler variable until it
30+
// reports true (each read is itself a pipeline, giving the engine another chance
31+
// to drain the pending action). On timeout we return the last observed value so
32+
// the caller's assertion still fails loudly.
33+
internal static async Task<IReadOnlyList<bool>> WaitForHandledAsync(
34+
PsesInternalHost psesHost, string variableName)
35+
{
36+
using CancellationTokenSource cancellationSource = new(millisecondsDelay: 15000);
37+
IReadOnlyList<bool> handled = Array.Empty<bool>();
38+
while (true)
39+
{
40+
handled = await psesHost.ExecutePSCommandAsync<bool>(
41+
new PSCommand().AddScript(variableName),
42+
CancellationToken.None);
43+
44+
if ((handled.Count > 0 && handled[0]) || cancellationSource.IsCancellationRequested)
45+
{
46+
return handled;
47+
}
48+
49+
await Task.Delay(200);
50+
}
51+
}
52+
}
53+
2254
[Trait("Category", "PsesInternalHost")]
2355
public class PsesInternalHostTests : IAsyncLifetime
2456
{
@@ -203,12 +235,7 @@ await psesHost.ExecuteDelegateAsync(
203235
(_, _) => psesHost.OnPowerShellIdle(CancellationToken.None),
204236
CancellationToken.None);
205237

206-
// TODO: Why is this racy?
207-
Thread.Sleep(2000);
208-
209-
handled = await psesHost.ExecutePSCommandAsync<bool>(
210-
new PSCommand().AddScript("$handled"),
211-
CancellationToken.None);
238+
handled = await OnIdleTestHelpers.WaitForHandledAsync(psesHost, "$handled");
212239

213240
Assert.Collection(handled, Assert.True);
214241
}
@@ -303,12 +330,7 @@ await psesHost.ExecuteDelegateAsync(
303330
(_, _) => psesHost.OnPowerShellIdle(CancellationToken.None),
304331
CancellationToken.None);
305332

306-
// TODO: Why is this racy?
307-
Thread.Sleep(2000);
308-
309-
IReadOnlyList<bool> handled = await psesHost.ExecutePSCommandAsync<bool>(
310-
new PSCommand().AddScript("$handledInProfile"),
311-
CancellationToken.None);
333+
IReadOnlyList<bool> handled = await OnIdleTestHelpers.WaitForHandledAsync(psesHost, "$handledInProfile");
312334

313335
Assert.Collection(handled, Assert.True);
314336
}

0 commit comments

Comments
 (0)