-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsmoke_deployment.py
More file actions
executable file
·152 lines (131 loc) · 5.51 KB
/
Copy pathsmoke_deployment.py
File metadata and controls
executable file
·152 lines (131 loc) · 5.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/usr/bin/env python3
"""Smoke-test a deployed Python By Example origin.
Usage:
scripts/smoke_deployment.py https://www.pythonbyexample.dev
"""
from __future__ import annotations
import argparse
import html
import os
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from urllib.parse import urljoin
SMOKE_PATHS = [
"/",
"/about",
"/privacy",
"/examples/values",
"/examples/async-await",
"/examples/networking",
"/examples/subprocesses",
"/journeys/reliability",
"/prototyping/production-figures-gestalt",
]
POST_SMOKES = [
("values", "print('runtime-smoke-values')\n", "runtime-smoke-values"),
("values", "print('runtime-smoke-values-edited')\n", "runtime-smoke-values-edited"),
("async-await", "import asyncio\n\nasync def main():\n return 'runtime-smoke-async'\n\nprint(asyncio.run(main()))\n", "runtime-smoke-async"),
("networking", "print('runtime-smoke-networking-boundary')\n", "runtime-smoke-networking-boundary"),
("subprocesses", "print('runtime-smoke-subprocess-boundary')\n", "runtime-smoke-subprocess-boundary"),
]
ERROR_MARKERS = ["error code: 1101", "PythonError", "Traceback"]
def fetch(url: str) -> tuple[int, str]:
request = urllib.request.Request(url, headers={"User-Agent": "pythonbyexample-smoke/1.0"})
with urllib.request.urlopen(request, timeout=20) as response:
body = response.read().decode("utf-8", errors="replace")
return response.status, body
def post_code(url: str, code: str, smoke_bypass_secret: str = "") -> tuple[int, str]:
data = urllib.parse.urlencode({"code": code}).encode()
headers = {
"User-Agent": "pythonbyexample-smoke/1.0",
"Content-Type": "application/x-www-form-urlencoded",
}
if smoke_bypass_secret:
headers["x-pythonbyexample-smoke-secret"] = smoke_bypass_secret
request = urllib.request.Request(
url,
data=data,
headers=headers,
method="POST",
)
with urllib.request.urlopen(request, timeout=30) as response:
body = response.read().decode("utf-8", errors="replace")
return response.status, body
def has_exception_marker(body: str) -> str | None:
lowered = body.lower()
for marker in ERROR_MARKERS:
if marker.lower() in lowered:
return marker
return None
def validate_smoke_bypass_origin(base_url: str, smoke_bypass_secret: str) -> str | None:
if smoke_bypass_secret and urllib.parse.urlparse(base_url).scheme != "https":
return "PBE_SMOKE_BYPASS_SECRET may only be sent to an https:// deployment origin"
return None
def output_panel_text(body: str) -> str:
match = re.search(
r'<section[^>]*class="[^"]*output-panel[^"]*"[^>]*>.*?<pre><code>(.*?)</code></pre>',
body,
flags=re.DOTALL,
)
return html.unescape(match.group(1)) if match else ""
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("base_url", help="deployment origin, e.g. https://www.pythonbyexample.dev")
parser.add_argument("--path", action="append", dest="paths", help="additional path to check")
parser.add_argument("--skip-post", action="store_true", help="check rendered pages only")
args = parser.parse_args()
base = args.base_url.rstrip("/") + "/"
smoke_bypass_value = os.environ.get("PBE_SMOKE_BYPASS_SECRET", "")
if origin_error := validate_smoke_bypass_origin(base, smoke_bypass_value):
print(origin_error, file=sys.stderr)
return 1
paths = SMOKE_PATHS + (args.paths or [])
failures: list[str] = []
for path in paths:
url = urljoin(base, path.lstrip("/"))
try:
status, body = fetch(url)
except urllib.error.HTTPError as exc:
failures.append(f"{url}: HTTP {exc.code}")
continue
except Exception as exc: # noqa: BLE001 # pragma: no cover - report any transport failure
failures.append(f"{url}: {exc!r}")
continue
if status != 200:
failures.append(f"{url}: HTTP {status}")
marker = has_exception_marker(body)
if marker:
failures.append(f"{url}: rendered exception marker {marker!r}")
print(f"GET {status} {url}")
if not args.skip_post:
for slug, code, expected in POST_SMOKES:
url = urljoin(base, f"examples/{slug}")
try:
status, body = post_code(url, code, smoke_bypass_value)
except urllib.error.HTTPError as exc:
failures.append(f"POST {url}: HTTP {exc.code}")
continue
except Exception as exc: # noqa: BLE001 # pragma: no cover - report any transport failure
failures.append(f"POST {url}: {exc!r}")
continue
if status != 200:
failures.append(f"POST {url}: HTTP {status}")
marker = has_exception_marker(body)
if marker:
failures.append(f"POST {url}: rendered exception marker {marker!r}")
rendered_output = output_panel_text(body)
if expected not in rendered_output:
failures.append(f"POST {url}: missing edited-code output {expected!r}")
print(f"POST {status} {url} -> {expected}")
if failures:
for failure in failures:
print(failure, file=sys.stderr)
return 1
post_count = 0 if args.skip_post else len(POST_SMOKES)
print(f"Deployment smoke OK ({len(paths)} GETs, {post_count} POSTs).")
return 0
if __name__ == "__main__":
raise SystemExit(main())