Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""``bounded_probe_run`` — deadlock-safe capture for fail-open probes (#87134).
|
||||
|
||||
On Windows, ``subprocess.run(..., capture_output=True, timeout=N)`` can hang
|
||||
FOREVER after its timeout fires: run()'s cleanup kills the direct child and
|
||||
then joins the pipe reader threads with an unbounded ``communicate()``. A
|
||||
descendant (``conhost.exe`` under wmic/powershell, ``git.exe`` under a
|
||||
launcher shim) holding duplicated pipe handles keeps the pipes from EOF and
|
||||
the join never returns. ``hermes update`` wedged exactly there inside
|
||||
``_scan_gateway_pids`` on machines where the full ``Win32_Process`` scan
|
||||
exceeds its budget.
|
||||
|
||||
``bounded_probe_run`` is the shared, generalized form of the fix that
|
||||
``bounded_git_probe`` already proved for git probes (#68609 / #66037):
|
||||
explicit ``communicate(timeout)``, tree-kill on failure, bounded 1s drain,
|
||||
then abandon.
|
||||
|
||||
These tests use REAL subprocesses (no mocks) for the semantics: a mock cannot
|
||||
reproduce pipe-handle inheritance or timeout behavior. The POSIX-only
|
||||
descendant-survival cases live in ``test_git_probe_tree_kill.py`` and now
|
||||
exercise the same code path through the ``bounded_git_probe`` delegation.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli._subprocess_compat import bounded_git_probe, bounded_probe_run
|
||||
|
||||
_PY = sys.executable
|
||||
|
||||
|
||||
def test_success_returns_completed_process():
|
||||
result = bounded_probe_run([_PY, "-c", "print('ok'); import sys; sys.exit(0)"], timeout=30)
|
||||
assert result is not None
|
||||
assert result.returncode == 0
|
||||
assert result.stdout.strip() == "ok"
|
||||
|
||||
|
||||
def test_nonzero_exit_is_returned_not_swallowed():
|
||||
"""Unlike bounded_git_probe, callers see the real returncode — the gateway
|
||||
scan branches on ``returncode != 0`` to trip the wmic→powershell fallback."""
|
||||
result = bounded_probe_run([_PY, "-c", "print('partial'); import sys; sys.exit(3)"], timeout=30)
|
||||
assert result is not None
|
||||
assert result.returncode == 3
|
||||
assert result.stdout.strip() == "partial"
|
||||
|
||||
|
||||
def test_spawn_failure_returns_none():
|
||||
result = bounded_probe_run(["definitely-not-a-real-binary-87134"], timeout=5)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_timeout_returns_none_within_bounded_time():
|
||||
"""A child that sleeps past the timeout must produce ``None`` promptly —
|
||||
timeout + tree-kill + 1s bounded drain, not an unbounded join."""
|
||||
start = time.monotonic()
|
||||
result = bounded_probe_run(
|
||||
[_PY, "-c", "import time; time.sleep(300)"],
|
||||
timeout=1.0,
|
||||
)
|
||||
elapsed = time.monotonic() - start
|
||||
assert result is None
|
||||
# 1s timeout + tree-kill + 1s drain + slack. The pre-fix failure mode is
|
||||
# an indefinite hang, so any bound proves the property; keep it loose for
|
||||
# slow CI runners.
|
||||
assert elapsed < 30
|
||||
|
||||
|
||||
def test_decode_errors_configurable():
|
||||
"""The process scans pass errors='ignore' (wmic emits system code page);
|
||||
undecodable bytes must not raise or None out stdout (#17049 class)."""
|
||||
result = bounded_probe_run(
|
||||
[_PY, "-c", "import sys; sys.stdout.buffer.write(b'ok\\xff\\xfe')"],
|
||||
timeout=30,
|
||||
errors="ignore",
|
||||
)
|
||||
assert result is not None
|
||||
assert result.returncode == 0
|
||||
assert "ok" in result.stdout
|
||||
|
||||
|
||||
def test_stdin_is_devnull_not_inherited():
|
||||
"""A probe must never block reading the caller's stdin."""
|
||||
result = bounded_probe_run(
|
||||
[_PY, "-c", "import sys; print(repr(sys.stdin.read()))"],
|
||||
timeout=30,
|
||||
)
|
||||
assert result is not None
|
||||
assert result.returncode == 0
|
||||
assert result.stdout.strip() == "''"
|
||||
|
||||
|
||||
def test_bounded_git_probe_delegates_same_contract():
|
||||
"""The historical git-probe wrapper keeps its exact contract on top of
|
||||
bounded_probe_run: stripped stdout on rc==0, '' on any failure."""
|
||||
assert bounded_git_probe([_PY, "-c", "print(' x ')"], timeout=30) == "x"
|
||||
assert bounded_git_probe([_PY, "-c", "import sys; sys.exit(1)"], timeout=30) == ""
|
||||
assert bounded_git_probe(["definitely-not-a-real-binary-87134"], timeout=5) == ""
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX process-group check")
|
||||
def test_posix_child_gets_own_process_group():
|
||||
"""POSIX spawns use process_group=0 so timeout cleanup can killpg the
|
||||
whole tree (same contract bounded_git_probe had)."""
|
||||
result = bounded_probe_run(
|
||||
[_PY, "-c", "import os; print(os.getpgid(0) == os.getpid())"],
|
||||
timeout=30,
|
||||
)
|
||||
assert result is not None
|
||||
assert result.stdout.strip() == "True"
|
||||
Reference in New Issue
Block a user