feat(prepare-win): idle check waits for quiet, reports busiest process

The one-shot idle sample fired ~30s after the final reboot, but Windows
Update / CBS (TiWorker) keeps finalizing for minutes — so a still-busy
template was wrongly rejected. Poll until CPU and RAM are both under
threshold (up to --idle-timeout, default 600s) instead of sampling once,
and report the busiest process to aid diagnosis. Adds --idle-timeout.
This commit is contained in:
2026-06-09 21:52:29 +02:00
parent 54feb43513
commit c1be0f276f
2 changed files with 67 additions and 32 deletions
+14 -8
View File
@@ -433,7 +433,7 @@ def _make_transport() -> MagicMock:
def _run(script: str = "", **kwargs: object) -> WinRmResult:
if "PercentProcessorTime" in script: # pre-snapshot idle check
return WinRmResult(stdout='{"cpu":2.0,"ram":21.0}', stderr="", returncode=0)
return WinRmResult(stdout='{"cpu":2.0,"ram":21.0,"top":"idle"}', stderr="", returncode=0)
if "ConvertTo-Json" in script:
return WinRmResult(stdout=val_json, stderr="", returncode=0)
return WinRmResult(stdout="CI_EXITCODE:0", stderr="", returncode=0)
@@ -2220,26 +2220,32 @@ def test_clean_force_artifacts_noop_on_clean_dir(tmp_path: Path) -> None:
def test_check_guest_idle_ok() -> None:
transport = MagicMock()
transport.run.return_value = WinRmResult(stdout='{"cpu":3.0,"ram":22.5}', stderr="", returncode=0)
tmpl_mod._check_guest_idle(transport, max_cpu=10.0, max_ram=50.0, settle_s=0) # no raise
transport.run.return_value = WinRmResult(
stdout='{"cpu":3.0,"ram":22.5,"top":"idle"}', stderr="", returncode=0
)
tmpl_mod._check_guest_idle(transport, max_cpu=10.0, max_ram=50.0, settle_s=0, timeout_s=5)
def test_check_guest_idle_cpu_over() -> None:
transport = MagicMock()
transport.run.return_value = WinRmResult(stdout='{"cpu":42.0,"ram":20.0}', stderr="", returncode=0)
transport.run.return_value = WinRmResult(
stdout='{"cpu":42.0,"ram":20.0,"top":"TiWorker"}', stderr="", returncode=0
)
with pytest.raises(click.ClickException, match=r"CPU 42\.0% > 10\.0%"):
tmpl_mod._check_guest_idle(transport, max_cpu=10.0, max_ram=50.0, settle_s=0)
tmpl_mod._check_guest_idle(transport, max_cpu=10.0, max_ram=50.0, settle_s=0, timeout_s=0)
def test_check_guest_idle_ram_over() -> None:
transport = MagicMock()
transport.run.return_value = WinRmResult(stdout='{"cpu":1.0,"ram":80.0}', stderr="", returncode=0)
transport.run.return_value = WinRmResult(
stdout='{"cpu":1.0,"ram":80.0,"top":"x"}', stderr="", returncode=0
)
with pytest.raises(click.ClickException, match=r"RAM 80\.0% > 50\.0%"):
tmpl_mod._check_guest_idle(transport, max_cpu=10.0, max_ram=50.0, settle_s=0)
tmpl_mod._check_guest_idle(transport, max_cpu=10.0, max_ram=50.0, settle_s=0, timeout_s=0)
def test_check_guest_idle_unparseable() -> None:
transport = MagicMock()
transport.run.return_value = WinRmResult(stdout="not json", stderr="", returncode=0)
with pytest.raises(click.ClickException, match="could not parse"):
tmpl_mod._check_guest_idle(transport, max_cpu=10.0, max_ram=50.0, settle_s=0)
tmpl_mod._sample_guest_idle(transport)