diff --git a/automation/scripts/adaptability_check.py b/automation/scripts/adaptability_check.py index d843782f8d..1714342405 100644 --- a/automation/scripts/adaptability_check.py +++ b/automation/scripts/adaptability_check.py @@ -1207,7 +1207,7 @@ def _comments(source: str) -> list[tuple[int, int, str]]: for token in tokenize.generate_tokens(io.StringIO(source).readline): if token.type == tokenize.COMMENT: out.append((token.start[0], token.start[1], token.string)) - except (tokenize.TokenError, IndentationError, SyntaxError): + except tokenize.TokenError, IndentationError, SyntaxError: for number, line in enumerate(source.splitlines(), start=1): stripped = line.lstrip() if stripped.startswith("#"): diff --git a/changelog.d/executor-stuck-test.md b/changelog.d/executor-stuck-test.md new file mode 100644 index 0000000000..fecd6023a1 --- /dev/null +++ b/changelog.d/executor-stuck-test.md @@ -0,0 +1,9 @@ +### Fixed + +- **The stuck-launcher executor test no longer fails on a slow CI runner.** The fake + launcher execs the harness, so the process the test keeps alive carried the job's + one-second `RLIMIT_CPU`; when the harness start spent that second, the kernel killed + the "stuck" launcher inside the grace window and `stuck` read 0. The test now gives + the run the host's full CPU cap, which a real `sandbox do` outside the sandbox always + has, and asserts that limit on the survivor, so it fails every time without the fix + (#12260). diff --git a/core/adaptability.py b/core/adaptability.py index f4895fcaff..ec34cd3ab0 100644 --- a/core/adaptability.py +++ b/core/adaptability.py @@ -876,7 +876,7 @@ def _comments(source: str) -> list[tuple[int, int, str]]: for token in tokenize.generate_tokens(io.StringIO(source).readline): if token.type == tokenize.COMMENT: out.append((token.start[0], token.start[1], token.string)) - except (tokenize.TokenError, IndentationError, SyntaxError): + except tokenize.TokenError, IndentationError, SyntaxError: for number, line in enumerate(source.splitlines(), start=1): stripped = line.lstrip() if stripped.startswith("#"): diff --git a/tests/unit/agents/renderer/test_executor.py b/tests/unit/agents/renderer/test_executor.py index 4c54742259..24b88792ed 100644 --- a/tests/unit/agents/renderer/test_executor.py +++ b/tests/unit/agents/renderer/test_executor.py @@ -7,6 +7,7 @@ import asyncio import base64 import os +import resource import signal import stat import time @@ -148,13 +149,20 @@ async def test_a_launcher_that_outlives_its_kill_blocks_renders_until_it_exits( executor = SandboxExecutor(replace(config, kill_grace_s=0.2)) monkeypatch.setattr(executor_module, "READER_GRACE_S", 0.2) monkeypatch.setattr(executor_module.os, "killpg", lambda pid, sig: None) # the kill has no effect + # The fake launcher execs the harness, so the survivor carries the run's RLIMIT_CPU, which a real + # `sandbox do` on the host never does. With the one second of a one-second job, the kernel's SIGKILL + # ended the "stuck" launcher inside the grace window whenever the harness start (matplotlib, a fresh + # font cache) spent that second, as it did on a loaded CI runner (2026-10-23). The full host cap + # leaves the survivor nothing but the test's own kill to die of. + monkeypatch.setattr(executor_module, "run_cpu_seconds", lambda limit_s, timeout_s: limit_s) run = await executor.run_theme(job(SLEEPER, timeout_s=1.0), "light") assert run.timed_out and executor.stuck == 1 + survivor = executor._stuck[0] + assert resource.prlimit(survivor.pid, resource.RLIMIT_CPU) == (config.rlimit_cpu_s,) * 2 with pytest.raises(Unavailable, match="stuck"): await executor.run_theme(job(PLOT), "light") - survivor = executor._stuck[0] os.kill(survivor.pid, signal.SIGKILL) await survivor.wait() assert executor.stuck == 0