feat: ManagedService spawns detached driver services that outlive python
This commit is contained in:
@@ -1,10 +1,22 @@
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from wabot.hosts import ExternalServer, build_options, normalize_browser, service_alive
|
||||
from wabot.hosts import (
|
||||
ExternalServer,
|
||||
ManagedService,
|
||||
build_options,
|
||||
find_driver_binary,
|
||||
normalize_browser,
|
||||
service_alive,
|
||||
stop_service,
|
||||
)
|
||||
|
||||
|
||||
class TestNormalizeBrowser:
|
||||
@@ -119,3 +131,74 @@ class TestServiceAliveNonHttpResponder:
|
||||
ExternalServer(f"http://127.0.0.1:{port}").ensure_running()
|
||||
finally:
|
||||
server.close()
|
||||
|
||||
|
||||
STUB_DRIVER = """\
|
||||
#!{python}
|
||||
import http.server, json, sys
|
||||
port = int(sys.argv[1].split("=", 1)[1])
|
||||
class H(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
body = json.dumps({{"value": {{"ready": True, "message": ""}}}}).encode()
|
||||
self.send_response(200); self.end_headers(); self.wfile.write(body)
|
||||
def log_message(self, *a): pass
|
||||
http.server.HTTPServer(("127.0.0.1", port), H).serve_forever()
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stub_driver(tmp_path):
|
||||
path = tmp_path / "chromedriver"
|
||||
path.write_text(STUB_DRIVER.format(python=sys.executable))
|
||||
path.chmod(path.stat().st_mode | stat.S_IEXEC)
|
||||
return path
|
||||
|
||||
|
||||
class TestFindDriverBinary:
|
||||
def test_env_var_override_wins(self, monkeypatch):
|
||||
monkeypatch.setenv("WABOT_CHROMEDRIVER", "/custom/chromedriver")
|
||||
assert find_driver_binary("chromium") == "/custom/chromedriver"
|
||||
|
||||
def test_path_lookup(self, monkeypatch, stub_driver):
|
||||
monkeypatch.delenv("WABOT_CHROMEDRIVER", raising=False)
|
||||
monkeypatch.setenv("PATH", str(stub_driver.parent))
|
||||
assert find_driver_binary("chromium") == str(stub_driver)
|
||||
|
||||
|
||||
class TestManagedService:
|
||||
def test_spawns_detached_and_answers_status(self, stub_driver, tmp_path):
|
||||
svc = ManagedService("chromium", log_dir=tmp_path, driver_binary=str(stub_driver))
|
||||
url = svc.ensure_running()
|
||||
try:
|
||||
assert service_alive(url)
|
||||
assert svc.pid is not None and svc.port is not None
|
||||
assert url == f"http://127.0.0.1:{svc.port}"
|
||||
# detached: the spawned process is in its own session, so it
|
||||
# would survive this python process exiting
|
||||
assert os.getsid(svc.pid) != os.getsid(os.getpid())
|
||||
assert (tmp_path / f"chromedriver-{svc.port}.log").exists()
|
||||
finally:
|
||||
stop_service(svc.pid)
|
||||
|
||||
def test_startup_timeout_raises(self, tmp_path):
|
||||
# /bin/true exits immediately and never serves /status
|
||||
svc = ManagedService(
|
||||
"chromium", log_dir=tmp_path, driver_binary="/bin/true", startup_timeout=1.0
|
||||
)
|
||||
with pytest.raises(TimeoutError, match="did not answer /status"):
|
||||
svc.ensure_running()
|
||||
|
||||
|
||||
class TestStopService:
|
||||
def test_stops_a_live_process_and_reports_dead_ones(self, stub_driver, tmp_path):
|
||||
svc = ManagedService("chromium", log_dir=tmp_path, driver_binary=str(stub_driver))
|
||||
svc.ensure_running()
|
||||
assert stop_service(svc.pid) is True
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
os.kill(svc.pid, 0)
|
||||
except ProcessLookupError:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
assert stop_service(999999) is False # no such pid
|
||||
|
||||
Reference in New Issue
Block a user