Skip to content

Commit c3fb4dd

Browse files
authored
Merge pull request #2828 from hongwei1/refactor/RemoveLiftwebCode
refactor: remove Lift Web framework — full http4s runtime + OIDC migration
2 parents 5f54b6f + 81e5bea commit c3fb4dd

230 files changed

Lines changed: 2162 additions & 12408 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 161 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,157 +1,222 @@
11
#!/usr/bin/env python3
22
"""
3-
Parse surefire XML reports from all shards and print an http4s-vs-Lift
4-
per-test speed table to stdout (plain text) and, if GITHUB_STEP_SUMMARY
5-
is set, append a markdown version to that file.
3+
Parse surefire XML reports from all shards and print a per-test speed table.
4+
5+
The Lift -> http4s migration is complete: *every* API version (v1.2.1 through
6+
v7.0.0) is served by http4s. There is no Lift HTTP code left, so the old
7+
"http4s vs Lift" split is meaningless. The meaningful axis now is **execution
8+
model**:
9+
10+
* unit/pure — no embedded server; pure logic / JSON-factory / route-matcher
11+
/ middleware tests. These are the speed win of the migration.
12+
* integration — boots a real server (test class extends ServerSetup) or a
13+
self-started http4s server; pays DB/HTTP cost per test.
14+
15+
Two tables are printed:
16+
1. By execution model (unit/pure vs integration) — the migration KPI.
17+
2. By API version (API v1 .. v7, all http4s) — per-version cost.
18+
19+
A suite counts as integration when its test class extends `ServerSetup`
20+
(detected by scanning obp-api/src/test/scala) or is one of the self-starting
21+
http4s server suites in HTTP4S_INTEGRATION_SUITES. Everything else is unit/pure.
622
723
Usage:
824
python3 test_speed_report.py <reports-root-dir>
925
10-
<reports-root-dir> should contain the extracted artifacts from all shards,
11-
e.g. after downloading test-reports-shard{1,2,3} into one directory.
26+
<reports-root-dir> should contain the extracted artifacts from all shards.
27+
Override the source root (used only for integration detection) with
28+
OBP_TEST_SRC_ROOT; if sources are not found, the execution-model split degrades
29+
gracefully (suites counted as integration) and the by-version table still prints.
1230
"""
1331

32+
from __future__ import annotations
33+
1434
import os
35+
import re
1536
import sys
1637
import glob
1738
import xml.etree.ElementTree as ET
1839
from collections import defaultdict
1940

20-
# ---------------------------------------------------------------------------
21-
# Classification
22-
# ---------------------------------------------------------------------------
23-
24-
# These suites run a real embedded server — they pay the same DB/HTTP cost
25-
# as Lift integration tests.
41+
# Self-starting http4s integration suites that boot a server WITHOUT extending
42+
# ServerSetup (so the source scan can't detect them) — list them explicitly.
2643
HTTP4S_INTEGRATION_SUITES = {
2744
"code.api.v7_0_0.Http4s700RoutesTest",
2845
"code.api.v7_0_0.Http4s700TransactionTest",
29-
"code.api.http4sbridge.Http4sLiftBridgePropertyTest",
3046
"code.api.http4sbridge.Http4sServerIntegrationTest",
3147
"code.api.v5_0_0.Http4s500SystemViewsTest",
3248
}
3349

50+
DEFAULT_SRC_ROOT = "obp-api/src/test/scala"
51+
52+
53+
# ---------------------------------------------------------------------------
54+
# Integration detection by source scan (does the test class extend ServerSetup?)
55+
# ---------------------------------------------------------------------------
56+
57+
def build_integration_map(src_root):
58+
"""Return ({fqClassName: extendsServerSetup}, scan_ok)."""
59+
fqmap = {}
60+
if not os.path.isdir(src_root):
61+
return fqmap, False
62+
for root, _dirs, files in os.walk(src_root):
63+
for fname in files:
64+
if not fname.endswith(".scala"):
65+
continue
66+
try:
67+
with open(os.path.join(root, fname), encoding="utf-8", errors="ignore") as fh:
68+
txt = fh.read()
69+
except OSError:
70+
continue
71+
pm = re.search(r'^\s*package\s+([\w.]+)', txt, re.M)
72+
pkg = pm.group(1) if pm else ""
73+
# For each `class X ... {`, inspect the parents portion (everything
74+
# up to the first brace) and check whether it mentions ServerSetup.
75+
for cm in re.finditer(r'\bclass\s+(\w+)\b(.*?)\{', txt, re.S):
76+
cls, parents = cm.group(1), cm.group(2)
77+
fqmap[f"{pkg}.{cls}"] = ("ServerSetup" in parents)
78+
return fqmap, True
79+
80+
81+
def is_integration(fq, fqmap):
82+
if fq in HTTP4S_INTEGRATION_SUITES:
83+
return True
84+
if fq in fqmap:
85+
return fqmap[fq]
86+
# Unknown (class/file-name mismatch or degraded scan): default to
87+
# integration so we never overstate the unit/pure win.
88+
return True
3489

35-
def categorize(suite_name: str) -> str | None:
36-
"""Return a display category or None to exclude from the table."""
37-
# http4s integration (real server)
38-
if suite_name in HTTP4S_INTEGRATION_SUITES:
39-
return "http4s v7 — integration"
4090

41-
# http4s unit/pure (no server) — everything http4s-flavoured that isn't
42-
# in the integration set above
43-
if (
44-
"Http4s" in suite_name
45-
or "http4s" in suite_name
46-
or "v7_0_0" in suite_name
47-
or suite_name.startswith("code.api.util.http4s.")
48-
or suite_name.startswith("code.api.berlin.group.v2.Http4sBGv2")
49-
):
50-
return "http4s v7 — unit/pure"
91+
# ---------------------------------------------------------------------------
92+
# API version from the suite's package
93+
# ---------------------------------------------------------------------------
5194

52-
# Lift versions
53-
for v in ("v6_0_0", "v5_1_0", "v5_0_0", "v4_0_0", "v3_1_0", "v3_0_0",
54-
"v2_2_0", "v2_1_0", "v2_0_0", "v1_4_0", "v1_3_0", "v1_2_1"):
55-
if v in suite_name:
56-
major = v[1] # "1" … "6"
57-
return f"Lift v{major}"
95+
_VERSIONS = ("v7_0_0", "v6_0_0", "v5_1_0", "v5_0_0", "v4_0_0", "v3_1_0", "v3_0_0",
96+
"v2_2_0", "v2_1_0", "v2_0_0", "v1_4_0", "v1_3_0", "v1_2_1")
5897

59-
return None # exclude (util, berlin group non-http4s, etc.)
98+
99+
def api_version(fq):
100+
for v in _VERSIONS:
101+
if v in fq:
102+
return f"API v{v[1]}" # "1" .. "7"
103+
return "other"
60104

61105

62106
# ---------------------------------------------------------------------------
63107
# Parse
64108
# ---------------------------------------------------------------------------
65109

66-
def collect(reports_root: str) -> dict:
67-
stats = defaultdict(lambda: {"tests": 0, "time": 0.0})
110+
def collect(reports_root, fqmap):
111+
by_model = defaultdict(lambda: {"tests": 0, "time": 0.0})
112+
by_version = defaultdict(lambda: {"tests": 0, "time": 0.0})
68113

69114
pattern = os.path.join(reports_root, "**", "TEST-*.xml")
70115
for path in glob.glob(pattern, recursive=True):
71116
try:
72117
root = ET.parse(path).getroot()
73-
name = root.get("name", "")
118+
name = root.get("name", "")
74119
tests = int(root.get("tests", 0))
75-
time = float(root.get("time", 0))
120+
t = float(root.get("time", 0))
76121
if tests == 0:
77122
continue
78-
cat = categorize(name)
79-
if cat is None:
80-
continue
81-
stats[cat]["tests"] += tests
82-
stats[cat]["time"] += time
123+
model = "integration" if is_integration(name, fqmap) else "unit/pure"
124+
by_model[model]["tests"] += tests
125+
by_model[model]["time"] += t
126+
ver = api_version(name)
127+
by_version[ver]["tests"] += tests
128+
by_version[ver]["time"] += t
83129
except Exception:
84130
pass
85131

86-
return stats
132+
return by_model, by_version
87133

88134

89135
# ---------------------------------------------------------------------------
90136
# Render
91137
# ---------------------------------------------------------------------------
92138

93-
CATEGORY_ORDER = [
94-
"http4s v7 — unit/pure",
95-
"http4s v7 — integration",
96-
"Lift v6",
97-
"Lift v5",
98-
"Lift v4",
99-
"Lift v3",
100-
"Lift v2",
101-
"Lift v1",
102-
]
103-
104-
105-
def render_plain(stats: dict) -> str:
106-
col_w = [25, 7, 12, 10]
107-
sep = "+-" + "-+-".join("-" * w for w in col_w) + "-+"
108-
hdr = "| " + " | ".join(
109-
h.center(w) for h, w in zip(
110-
["Category", "Tests", "Total time", "Avg/test"], col_w
111-
)
112-
) + " |"
139+
MODEL_ORDER = ["unit/pure", "integration"]
140+
VERSION_ORDER = ["API v7", "API v6", "API v5", "API v4", "API v3",
141+
"API v2", "API v1", "other"]
142+
113143

144+
def _table(stats, order, col_w=(24, 7, 12, 10)):
145+
sep = "+-" + "-+-".join("-" * w for w in col_w) + "-+"
146+
hdr = "| " + " | ".join(h.center(w) for h, w in zip(
147+
["Category", "Tests", "Total time", "Avg/test"], col_w)) + " |"
114148
lines = [sep, hdr, sep]
115-
for cat in CATEGORY_ORDER:
149+
for cat in order:
116150
if cat not in stats:
117151
continue
118-
d = stats[cat]
152+
d = stats[cat]
119153
avg = d["time"] / d["tests"] if d["tests"] else 0
120-
row = "| " + " | ".join([
154+
lines.append("| " + " | ".join([
121155
cat.ljust(col_w[0]),
122156
str(d["tests"]).rjust(col_w[1]),
123157
f"{d['time']:.1f}s".rjust(col_w[2]),
124158
f"{avg:.3f}s".rjust(col_w[3]),
125-
]) + " |"
126-
lines.append(row)
159+
]) + " |")
127160
lines.append(sep)
128161
return "\n".join(lines)
129162

130163

131-
def render_markdown(stats: dict) -> str:
132-
rows = ["## http4s v7 vs Lift — per-test speed",
133-
"",
134-
"| Category | Tests | Total time | Avg/test |",
135-
"|---|---:|---:|---:|"]
136-
for cat in CATEGORY_ORDER:
137-
if cat not in stats:
164+
def render_plain(by_model, by_version, scan_ok):
165+
out = ["All API versions (v1-v7) are served by http4s — the split is by",
166+
"execution model, not framework.\n",
167+
"By execution model (unit/pure = no server, integration = embedded server):"]
168+
if not scan_ok:
169+
out.append(" (source scan unavailable — suites counted as integration)")
170+
out.append(_table(by_model, MODEL_ORDER))
171+
out += ["", "By API version:", _table(by_version, VERSION_ORDER)]
172+
173+
u = by_model.get("unit/pure")
174+
i = by_model.get("integration")
175+
if u and i and u["tests"] and i["tests"]:
176+
ua = u["time"] / u["tests"]
177+
ia = i["time"] / i["tests"]
178+
if ua > 0:
179+
out += ["",
180+
f"--> unit/pure tests are {ia/ua:.0f}x faster per test than "
181+
f"integration tests ({ua:.3f}s vs {ia:.3f}s)."]
182+
return "\n".join(out)
183+
184+
185+
def render_markdown(by_model, by_version, scan_ok):
186+
rows = ["## Per-test speed — all endpoints served by http4s", "",
187+
"_All API versions (v1-v7) run on http4s. The split below is by "
188+
"execution model, not framework._", "",
189+
"### By execution model", ""]
190+
if not scan_ok:
191+
rows += ["> source scan unavailable — suites counted as integration", ""]
192+
rows += ["| Category | Tests | Total time | Avg/test |", "|---|---:|---:|---:|"]
193+
for cat in MODEL_ORDER:
194+
if cat not in by_model:
195+
continue
196+
d = by_model[cat]
197+
avg = d["time"] / d["tests"] if d["tests"] else 0
198+
rows.append(f"| {cat} | {d['tests']} | {d['time']:.1f}s | {avg:.3f}s |")
199+
200+
rows += ["", "### By API version", "",
201+
"| Category | Tests | Total time | Avg/test |", "|---|---:|---:|---:|"]
202+
for cat in VERSION_ORDER:
203+
if cat not in by_version:
138204
continue
139-
d = stats[cat]
205+
d = by_version[cat]
140206
avg = d["time"] / d["tests"] if d["tests"] else 0
141207
rows.append(f"| {cat} | {d['tests']} | {d['time']:.1f}s | {avg:.3f}s |")
142208

143-
# Highlight ratio
144-
u = stats.get("http4s v7 — unit/pure")
145-
lift_times = [stats[c]["time"] for c in CATEGORY_ORDER if c.startswith("Lift") and c in stats]
146-
lift_tests = [stats[c]["tests"] for c in CATEGORY_ORDER if c.startswith("Lift") and c in stats]
147-
if u and lift_tests:
148-
lift_avg = sum(lift_times) / sum(lift_tests)
149-
unit_avg = u["time"] / u["tests"]
150-
rows += [
151-
"",
152-
f"> **Unit/pure tests are {lift_avg/unit_avg:.0f}× faster than Lift integration tests** "
153-
f"({unit_avg:.3f}s vs {lift_avg:.3f}s per test).",
154-
]
209+
u = by_model.get("unit/pure")
210+
i = by_model.get("integration")
211+
if u and i and u["tests"] and i["tests"]:
212+
ua = u["time"] / u["tests"]
213+
ia = i["time"] / i["tests"]
214+
if ua > 0:
215+
rows += ["",
216+
f"> **Unit/pure tests are {ia/ua:.0f}x faster per test than "
217+
f"integration tests** ({ua:.3f}s vs {ia:.3f}s). This is the "
218+
f"migration win: logic that used to need a running server is "
219+
f"now pure unit-tested."]
155220
return "\n".join(rows)
156221

157222

@@ -164,16 +229,19 @@ def render_markdown(stats: dict) -> str:
164229
print(f"Usage: {sys.argv[0]} <reports-root-dir>", file=sys.stderr)
165230
sys.exit(1)
166231

167-
stats = collect(sys.argv[1])
168-
if not stats:
232+
src_root = os.environ.get("OBP_TEST_SRC_ROOT", DEFAULT_SRC_ROOT)
233+
fqmap, scan_ok = build_integration_map(src_root)
234+
235+
by_model, by_version = collect(sys.argv[1], fqmap)
236+
if not by_model and not by_version:
169237
print("No matching surefire XML reports found.", file=sys.stderr)
170238
sys.exit(0)
171239

172-
print(render_plain(stats))
240+
print(render_plain(by_model, by_version, scan_ok))
173241

174242
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
175243
if summary_path:
176244
with open(summary_path, "a") as f:
177245
f.write("\n")
178-
f.write(render_markdown(stats))
246+
f.write(render_markdown(by_model, by_version, scan_ok))
179247
f.write("\n")

0 commit comments

Comments
 (0)