mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 00:03:01 +03:00
test(exec): remove Windows process timing races
This commit is contained in:
@@ -331,7 +331,7 @@ def test_exec_rejects_unsupported_shell(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_exec_can_continue_with_stdin(tmp_path):
|
def test_exec_can_continue_with_stdin(tmp_path):
|
||||||
async def run() -> tuple[str, str]:
|
async def run() -> tuple[str, str, str]:
|
||||||
manager = ExecSessionManager()
|
manager = ExecSessionManager()
|
||||||
exec_tool = ExecTool(working_dir=str(tmp_path), timeout=5, session_manager=manager)
|
exec_tool = ExecTool(working_dir=str(tmp_path), timeout=5, session_manager=manager)
|
||||||
stdin_tool = WriteStdinTool(manager=manager)
|
stdin_tool = WriteStdinTool(manager=manager)
|
||||||
@@ -340,17 +340,21 @@ def test_exec_can_continue_with_stdin(tmp_path):
|
|||||||
"line=sys.stdin.readline(); print('got:' + line.strip(), flush=True)"
|
"line=sys.stdin.readline(); print('got:' + line.strip(), flush=True)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
initial = await exec_tool.execute(command=command, yield_time_ms=500)
|
initial = await exec_tool.execute(command=command, yield_time_ms=500)
|
||||||
sid = _session_id(initial)
|
sid = _session_id(initial)
|
||||||
result = await stdin_tool.execute(session_id=sid, chars="ping\n", yield_time_ms=1000)
|
result = await stdin_tool.execute(session_id=sid, chars="ping\n", yield_time_ms=1000)
|
||||||
return initial, result
|
observed, final = await _poll_if_running(result, stdin_tool)
|
||||||
|
return initial, observed, final
|
||||||
|
finally:
|
||||||
|
await manager.close_all()
|
||||||
|
|
||||||
initial, result = asyncio.run(run())
|
initial, result, final = asyncio.run(run())
|
||||||
assert "ready" in initial + result
|
assert "ready" in initial + result
|
||||||
assert "Process running" in initial
|
assert "Process running" in initial
|
||||||
assert "Elapsed:" in initial
|
assert "Elapsed:" in initial
|
||||||
assert "got:ping" in result
|
assert "got:ping" in result
|
||||||
assert "Exit code: 0" in result
|
assert "Exit code: 0" in final
|
||||||
assert "Elapsed:" in result
|
assert "Elapsed:" in result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -238,37 +238,51 @@ async def test_execute_timeout_kills_background_process_tree(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_execute_timeout_kills_descendant_after_root_exits(tmp_path):
|
async def test_kill_process_tree_kills_descendant_after_root_exits(tmp_path):
|
||||||
"""Tree ownership must outlive a root shell that exits before timeout."""
|
"""Tree ownership must outlive a root shell that has demonstrably exited."""
|
||||||
marker = tmp_path / "child-survived-root"
|
marker = tmp_path / "child-survived-root"
|
||||||
|
started = tmp_path / "child-started"
|
||||||
|
release = tmp_path / "release-child"
|
||||||
child_code = (
|
child_code = (
|
||||||
"import pathlib,time; time.sleep(3.5); "
|
"import pathlib,time\n"
|
||||||
f"pathlib.Path({str(marker)!r}).write_text('alive')"
|
f"started=pathlib.Path({str(started)!r})\n"
|
||||||
|
f"release=pathlib.Path({str(release)!r})\n"
|
||||||
|
f"marker=pathlib.Path({str(marker)!r})\n"
|
||||||
|
"started.write_text('ready')\n"
|
||||||
|
"while not release.exists():\n"
|
||||||
|
" time.sleep(0.01)\n"
|
||||||
|
"marker.write_text('alive')\n"
|
||||||
)
|
)
|
||||||
child_payload = base64.b64encode(child_code.encode()).decode()
|
child_payload = base64.b64encode(child_code.encode()).decode()
|
||||||
|
# Do not let the descendant keep the root's captured pipes open; the test
|
||||||
|
# must observe root exit independently from descendant lifetime.
|
||||||
parent_code = (
|
parent_code = (
|
||||||
"import base64,subprocess,sys; "
|
"import base64,subprocess,sys; "
|
||||||
f"child=base64.b64decode('{child_payload}').decode(); "
|
f"child=base64.b64decode('{child_payload}').decode(); "
|
||||||
"subprocess.Popen([sys.executable, '-c', child])"
|
"subprocess.Popen([sys.executable, '-c', child], "
|
||||||
|
"stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)"
|
||||||
)
|
)
|
||||||
spawned = []
|
tool = ExecTool(working_dir=str(tmp_path), timeout=10)
|
||||||
original_spawn = ExecTool._spawn
|
process = await tool._spawn(
|
||||||
|
_python_command(parent_code),
|
||||||
async def capture_spawn(*args, **kwargs):
|
str(tmp_path),
|
||||||
process = await original_spawn(*args, **kwargs)
|
tool._build_env(),
|
||||||
spawned.append(process)
|
process_tree=True,
|
||||||
return process
|
|
||||||
|
|
||||||
with patch.object(ExecTool, "_spawn", side_effect=capture_spawn):
|
|
||||||
result = await ExecTool(working_dir=str(tmp_path), timeout=2).execute(
|
|
||||||
command=_python_command(parent_code),
|
|
||||||
timeout=2,
|
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(process.wait(), timeout=10)
|
||||||
|
assert process.returncode == 0
|
||||||
|
async with asyncio.timeout(10):
|
||||||
|
while not started.exists():
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
assert "timed out" in result.lower()
|
await ExecTool._kill_process_tree(process)
|
||||||
assert spawned[0].returncode == 0
|
await asyncio.wait_for(process.communicate(), timeout=5)
|
||||||
await asyncio.sleep(2)
|
release.write_text("go")
|
||||||
|
await asyncio.sleep(1)
|
||||||
assert not marker.exists()
|
assert not marker.exists()
|
||||||
|
finally:
|
||||||
|
await ExecTool._kill_process_tree(process)
|
||||||
|
|
||||||
|
|
||||||
def _mock_session_process(*, pid: int, returncode: int | None):
|
def _mock_session_process(*, pid: int, returncode: int | None):
|
||||||
|
|||||||
Reference in New Issue
Block a user