Skip to content

Commit 93357f1

Browse files
committed
Pin policyengine below 1.0 for local boot
1 parent 32b4b18 commit 93357f1

2 files changed

Lines changed: 156 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ dependencies = [
4141
"policyengine_uk==2.78.0",
4242
"policyengine_us==1.634.9",
4343
"policyengine_core>=3.16.6",
44-
"policyengine>=0.7.0",
44+
"policyengine>0.12.0,<1",
4545
"pydantic",
4646
"pymysql",
4747
"python-dotenv",

scripts/smoke_test_local_boot.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env python3
2+
"""Boot smoke test for the local PolicyEngine API app.
3+
4+
Starts the real Flask app in debug/local-db mode, waits for it to come up,
5+
hits health endpoints, then shuts it down. If startup fails, prints captured
6+
stdout/stderr to make the failure actionable.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import importlib.metadata
12+
import os
13+
from pathlib import Path
14+
import socket
15+
import subprocess
16+
import sys
17+
import tempfile
18+
import time
19+
import urllib.error
20+
import urllib.request
21+
22+
23+
REPO_ROOT = Path(__file__).resolve().parents[1]
24+
HEALTH_PATHS = ("/liveness-check", "/readiness-check")
25+
BOOT_TIMEOUT_SECONDS = 45
26+
HTTP_TIMEOUT_SECONDS = 2
27+
28+
29+
def find_free_port() -> int:
30+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
31+
sock.bind(("127.0.0.1", 0))
32+
return int(sock.getsockname()[1])
33+
34+
35+
def read_text(path: Path) -> str:
36+
if not path.exists():
37+
return ""
38+
return path.read_text(encoding="utf-8", errors="replace")
39+
40+
41+
def fetch(url: str) -> tuple[int, str]:
42+
request = urllib.request.Request(url, method="GET")
43+
with urllib.request.urlopen(request, timeout=HTTP_TIMEOUT_SECONDS) as response:
44+
body = response.read().decode("utf-8", errors="replace")
45+
return response.getcode(), body
46+
47+
48+
def print_section(title: str, body: str) -> None:
49+
print(f"\n=== {title} ===")
50+
print(body.rstrip() if body else "(empty)")
51+
52+
53+
def main() -> int:
54+
port = find_free_port()
55+
stdout_file = tempfile.NamedTemporaryFile(
56+
mode="w+", encoding="utf-8", delete=False, prefix="policyengine-api-boot-", suffix=".stdout.log"
57+
)
58+
stderr_file = tempfile.NamedTemporaryFile(
59+
mode="w+", encoding="utf-8", delete=False, prefix="policyengine-api-boot-", suffix=".stderr.log"
60+
)
61+
stdout_path = Path(stdout_file.name)
62+
stderr_path = Path(stderr_file.name)
63+
stdout_file.close()
64+
stderr_file.close()
65+
66+
env = os.environ.copy()
67+
env["FLASK_DEBUG"] = "1"
68+
env.setdefault("PYTHONUNBUFFERED", "1")
69+
70+
try:
71+
policyengine_version = importlib.metadata.version("policyengine")
72+
except importlib.metadata.PackageNotFoundError:
73+
policyengine_version = "not installed"
74+
75+
print(f"repo: {REPO_ROOT}")
76+
print(f"python: {sys.executable}")
77+
print(f"policyengine: {policyengine_version}")
78+
print(f"port: {port}")
79+
print(f"stdout log: {stdout_path}")
80+
print(f"stderr log: {stderr_path}")
81+
82+
with stdout_path.open("w", encoding="utf-8") as stdout_handle, stderr_path.open(
83+
"w", encoding="utf-8"
84+
) as stderr_handle:
85+
process = subprocess.Popen(
86+
[
87+
sys.executable,
88+
"-m",
89+
"flask",
90+
"--app",
91+
"policyengine_api.api",
92+
"run",
93+
"--without-threads",
94+
"--no-reload",
95+
"--port",
96+
str(port),
97+
],
98+
cwd=REPO_ROOT,
99+
env=env,
100+
stdout=stdout_handle,
101+
stderr=stderr_handle,
102+
text=True,
103+
)
104+
105+
try:
106+
deadline = time.time() + BOOT_TIMEOUT_SECONDS
107+
ready = False
108+
while time.time() < deadline:
109+
if process.poll() is not None:
110+
break
111+
112+
try:
113+
status, body = fetch(f"http://127.0.0.1:{port}/liveness-check")
114+
if status == 200 and body.strip() == "OK":
115+
ready = True
116+
break
117+
except (urllib.error.URLError, TimeoutError, ConnectionError):
118+
time.sleep(0.5)
119+
continue
120+
121+
time.sleep(0.5)
122+
123+
if not ready:
124+
process.wait(timeout=5) if process.poll() is not None else process.terminate()
125+
if process.poll() is None:
126+
process.wait(timeout=5)
127+
print("\nBoot smoke test failed: app did not become ready.")
128+
print_section("stdout", read_text(stdout_path))
129+
print_section("stderr", read_text(stderr_path))
130+
return 1
131+
132+
print("\nBoot smoke test reached a live app. Probing health endpoints:")
133+
for path in HEALTH_PATHS:
134+
status, body = fetch(f"http://127.0.0.1:{port}{path}")
135+
print(f"{path}: {status} {body.strip()}")
136+
if status != 200 or body.strip() != "OK":
137+
print("\nHealth endpoint probe failed.")
138+
print_section("stdout", read_text(stdout_path))
139+
print_section("stderr", read_text(stderr_path))
140+
return 1
141+
142+
print("\nBoot smoke test passed.")
143+
return 0
144+
finally:
145+
if "process" in locals() and process.poll() is None:
146+
process.terminate()
147+
try:
148+
process.wait(timeout=5)
149+
except subprocess.TimeoutExpired:
150+
process.kill()
151+
process.wait(timeout=5)
152+
153+
154+
if __name__ == "__main__":
155+
raise SystemExit(main())

0 commit comments

Comments
 (0)