import sys from io import StringIO import pytest def test_run_command_passthrough_writes_to_sys_streams(monkeypatch: pytest.MonkeyPatch) -> None: from l4d2web.services.host_commands import run_command mock_stdout = StringIO() mock_stderr = StringIO() monkeypatch.setattr(sys, "stdout", mock_stdout) monkeypatch.setattr(sys, "stderr", mock_stderr) run_command( ["python3", "-c", "import sys; print('passed out'); print('passed err', file=sys.stderr)"], passthrough=True, ) assert mock_stdout.getvalue() == "passed out\n" assert mock_stderr.getvalue() == "passed err\n" def test_run_command_streams_stdout_and_stderr_callbacks() -> None: from l4d2web.services.host_commands import run_command stdout: list[str] = [] stderr: list[str] = [] result = run_command( ["python3", "-c", "import sys; print('ok'); print('warn', file=sys.stderr)"], on_stdout=stdout.append, on_stderr=stderr.append, ) assert stdout == ["ok"] assert stderr == ["warn"] assert result.returncode == 0 assert result.stdout == "ok" assert result.stderr == "warn" def test_run_command_raises_host_error_on_nonzero_exit() -> None: from l4d2web.services.host_commands import HostCommandError, run_command with pytest.raises(HostCommandError) as exc_info: run_command(["python3", "-c", "import sys; print('bad', file=sys.stderr); sys.exit(7)"]) assert exc_info.value.returncode == 7 assert exc_info.value.stderr == "bad" def test_run_command_raises_cancelled_error_when_cancel_requested() -> None: from l4d2web.services.host_commands import CommandCancelledError, run_command stdout: list[str] = [] with pytest.raises(CommandCancelledError): run_command( ["python3", "-c", "import time; print('ready', flush=True); time.sleep(5)"], on_stdout=stdout.append, should_cancel=lambda: bool(stdout), cancel_poll_seconds=0.01, cancel_terminate_timeout=0.2, ) assert stdout == ["ready"] def test_stream_command_yields_stdout_lines() -> None: from l4d2web.services.host_commands import stream_command lines = list(stream_command(["python3", "-c", "print('one'); print('two')"])) assert lines == ["one", "two"] def test_stream_command_emits_heartbeat_when_subprocess_silent() -> None: import time from l4d2web.services.host_commands import stream_command cmd = [ "python3", "-c", "import time; time.sleep(0.4); print('done')", ] started = time.monotonic() items: list[str] = [] for item in stream_command(cmd, heartbeat_interval=0.05): items.append(item) if time.monotonic() - started > 2.0: break assert "done" in items, items heartbeats = [i for i in items if i == ""] assert len(heartbeats) >= 2, f"expected ≥2 heartbeat ticks during the silent 0.4s window, got items={items!r}" def test_stream_command_close_releases_subprocess_promptly() -> None: import time from l4d2web.services.host_commands import stream_command cmd = [ "python3", "-c", "import time;\nwhile True:\n time.sleep(60)", ] gen = stream_command(cmd, heartbeat_interval=0.05) assert next(gen) == "" started = time.monotonic() gen.close() elapsed = time.monotonic() - started assert elapsed < 1.0, f"gen.close() took {elapsed:.2f}s; subprocess cleanup must not block"