Skip to content

Commit 3c40fc5

Browse files
committed
Test examples
1 parent f12ae8f commit 3c40fc5

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

tests/test_examples.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""Run every script in ``examples/`` end-to-end against a real worker.
2+
3+
Spawns one ``offwork worker --backend local://localhost:9749 --tmp`` worker
4+
process, then for each example runs ``offwork run examples/<file>.py`` and
5+
asserts the script exits cleanly.
6+
7+
Runs as a normal pytest test (``pytest tests/test_examples.py``) and also
8+
directly (``python tests/test_examples.py``).
9+
10+
This is slow: each ``--tmp`` invocation builds a fresh venv. Use
11+
``-k <name>`` to run a single example, or ``OFFWORK_EXAMPLES_PORT`` to pick
12+
a different broker port.
13+
"""
14+
from __future__ import annotations
15+
16+
import os
17+
import socket
18+
import subprocess
19+
import sys
20+
import time
21+
from contextlib import contextmanager
22+
from pathlib import Path
23+
from typing import Iterator
24+
25+
import pytest
26+
27+
EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "examples"
28+
BACKEND = f"local://localhost:9748"
29+
WORKER_BOOT_TIMEOUT = 60.0 # building the worker's --tmp venv can take a while
30+
EXAMPLE_TIMEOUT = 60.0 # each example also builds its own --tmp venv
31+
32+
33+
def _example_scripts() -> list[Path]:
34+
return sorted(p for p in EXAMPLES_DIR.glob("*.py") if not p.name.startswith("_"))
35+
36+
37+
def _wait_for_port(host: str, port: int, timeout: float) -> None:
38+
deadline = time.monotonic() + timeout
39+
while time.monotonic() < deadline:
40+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
41+
s.settimeout(0.5)
42+
try:
43+
s.connect((host, port))
44+
return
45+
except OSError:
46+
time.sleep(0.5)
47+
raise TimeoutError(f"worker did not open {host}:{port} within {timeout}s")
48+
49+
50+
@contextmanager
51+
def _spawn_worker() -> Iterator[subprocess.Popen[bytes]]:
52+
cmd = [sys.executable, "-m", "offwork", "worker", "--backend", BACKEND, "--tmp"]
53+
proc = subprocess.Popen(
54+
cmd,
55+
stdout=subprocess.PIPE,
56+
stderr=subprocess.STDOUT,
57+
cwd=str(EXAMPLES_DIR.parent),
58+
)
59+
try:
60+
_wait_for_port("localhost", 9748, WORKER_BOOT_TIMEOUT)
61+
yield proc
62+
finally:
63+
proc.terminate()
64+
try:
65+
proc.wait(timeout=10)
66+
except subprocess.TimeoutExpired:
67+
proc.kill()
68+
proc.wait(timeout=5)
69+
70+
71+
def _run_example(script: Path) -> subprocess.CompletedProcess[str]:
72+
cmd = [sys.executable, "-m", "offwork", "run", str(script)]
73+
return subprocess.run(
74+
cmd,
75+
cwd=str(EXAMPLES_DIR.parent),
76+
capture_output=True,
77+
text=True,
78+
timeout=EXAMPLE_TIMEOUT,
79+
)
80+
81+
82+
@pytest.fixture(scope="module")
83+
def worker() -> Iterator[None]:
84+
with _spawn_worker():
85+
yield
86+
87+
88+
@pytest.mark.parametrize("script", _example_scripts(), ids=lambda p: p.name)
89+
def test_example_runs(worker: None, script: Path) -> None:
90+
result = _run_example(script)
91+
if result.returncode != 0:
92+
pytest.fail(
93+
f"{script.name} exited with {result.returncode}\n"
94+
f"--- stdout ---\n{result.stdout}\n"
95+
f"--- stderr ---\n{result.stderr}"
96+
)
97+
98+
99+
def main() -> int:
100+
scripts = _example_scripts()
101+
print(f"Running {len(scripts)} example(s) against {BACKEND}")
102+
failures: list[tuple[str, subprocess.CompletedProcess[str]]] = []
103+
with _spawn_worker():
104+
for script in scripts:
105+
print(f"--- {script.name} ", end="", flush=True)
106+
t0 = time.monotonic()
107+
result = _run_example(script)
108+
elapsed = time.monotonic() - t0
109+
if result.returncode == 0:
110+
print(f"OK ({elapsed:.1f}s)")
111+
else:
112+
print(f"FAIL ({elapsed:.1f}s, exit {result.returncode})")
113+
failures.append((script.name, result))
114+
115+
if failures:
116+
print(f"\n{len(failures)} failure(s):", file=sys.stderr)
117+
for name, res in failures:
118+
print(f"\n=== {name} ===", file=sys.stderr)
119+
print("--- stdout ---", file=sys.stderr)
120+
print(res.stdout, file=sys.stderr)
121+
print("--- stderr ---", file=sys.stderr)
122+
print(res.stderr, file=sys.stderr)
123+
return 1
124+
return 0
125+
126+
127+
if __name__ == "__main__":
128+
sys.exit(main())

0 commit comments

Comments
 (0)