fix(perf): compare_responses — устойчивый dump, не-JSON тела, strip_response без result
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b3899cf6dc
commit
88c37f97fa
@ -65,8 +65,9 @@ def strip_nulls(value):
|
||||
def strip_response(body: dict) -> dict:
|
||||
"""Копия тела, где strip_nulls применён только к row['data'] строк result."""
|
||||
out = dict(body)
|
||||
out["result"] = [{**row, "data": strip_nulls(row["data"])} if isinstance(row, dict) and "data" in row else row
|
||||
for row in body.get("result") or []]
|
||||
if isinstance(body.get("result"), list):
|
||||
out["result"] = [{**row, "data": strip_nulls(row["data"])} if isinstance(row, dict) and "data" in row else row
|
||||
for row in body["result"]]
|
||||
return out
|
||||
|
||||
|
||||
@ -97,8 +98,8 @@ def first_diff(a, b, path: str = "$") -> str | None:
|
||||
return f"{path}: {a!r} != {b!r}"
|
||||
|
||||
|
||||
def _load(d: Path, name: str):
|
||||
return json.loads(gzip.decompress((d / f"{name}.json.gz").read_bytes()))
|
||||
def _raw(d: Path, name: str) -> bytes:
|
||||
return gzip.decompress((d / f"{name}.json.gz").read_bytes())
|
||||
|
||||
|
||||
def compare(before: Path, after: Path, mode: str) -> tuple[int, list[str]]:
|
||||
@ -107,14 +108,22 @@ def compare(before: Path, after: Path, mode: str) -> tuple[int, list[str]]:
|
||||
ai = json.loads(ai_p.read_text()) if ai_p.exists() else {}
|
||||
ok, bad = 0, []
|
||||
for name, st in bi.items():
|
||||
if name not in ai or not (after / f"{name}.json.gz").exists():
|
||||
bad.append(f"{name}: нет в after")
|
||||
if name not in ai or not (after / f"{name}.json.gz").exists() or not (before / f"{name}.json.gz").exists():
|
||||
bad.append(f"{name}: нет файла в before или after")
|
||||
continue
|
||||
if st != ai[name]:
|
||||
bad.append(f"{name}: статус {st} != {ai[name]}")
|
||||
continue
|
||||
b, a = _load(before, name), _load(after, name)
|
||||
if mode == "strip-nulls" and is_stripped(name):
|
||||
rb, ra = _raw(before, name), _raw(after, name)
|
||||
try:
|
||||
b, a = json.loads(rb), json.loads(ra)
|
||||
except ValueError:
|
||||
if rb == ra:
|
||||
ok += 1
|
||||
else:
|
||||
bad.append(f"{name}: тела не JSON и различаются ({len(rb)} и {len(ra)} байт)")
|
||||
continue
|
||||
if mode == "strip-nulls" and is_stripped(name) and isinstance(b, dict):
|
||||
b = strip_response(b)
|
||||
d = first_diff(b, a)
|
||||
if d:
|
||||
@ -124,22 +133,29 @@ def compare(before: Path, after: Path, mode: str) -> tuple[int, list[str]]:
|
||||
return ok, bad
|
||||
|
||||
|
||||
def dump(man: dict, api: str, out: Path) -> None:
|
||||
def dump(man: dict, c: httpx.Client, out: Path) -> dict:
|
||||
"""Снимок всех запросов через готовый клиент; сбой одного запроса не прерывает дамп, index.json пишется всегда."""
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
index, tokens = {}, {}
|
||||
reqs = build_requests(man)
|
||||
with httpx.Client(base_url=api, timeout=300) as c:
|
||||
try:
|
||||
for role in sorted({r for r, _, _ in reqs}):
|
||||
u = man["users"][role]
|
||||
r = c.post("/api/v1/auth/login", json={"username": u["username"], "password": u["password"]})
|
||||
r.raise_for_status()
|
||||
tokens[role] = r.json()["access_token"]
|
||||
for role, name, path in reqs:
|
||||
r = c.get(path, headers={"Authorization": f"Bearer {tokens[role]}"})
|
||||
(out / f"{name}.json.gz").write_bytes(gzip.compress(r.content))
|
||||
index[name] = r.status_code
|
||||
print(name, r.status_code, len(r.content), flush=True)
|
||||
(out / "index.json").write_text(json.dumps(index, ensure_ascii=False, indent=1))
|
||||
try:
|
||||
r = c.get(path, headers={"Authorization": f"Bearer {tokens[role]}"})
|
||||
(out / f"{name}.json.gz").write_bytes(gzip.compress(r.content))
|
||||
index[name] = r.status_code
|
||||
print(name, r.status_code, len(r.content), flush=True)
|
||||
except httpx.HTTPError as e:
|
||||
index[name] = f"error: {type(e).__name__}"
|
||||
print(name, index[name], flush=True)
|
||||
finally:
|
||||
(out / "index.json").write_text(json.dumps(index, ensure_ascii=False, indent=1))
|
||||
return index
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
@ -156,7 +172,8 @@ def main(argv=None) -> int:
|
||||
a = ap.parse_args(argv)
|
||||
if a.cmd == "dump":
|
||||
man = json.loads((PERF / "results" / f"manifest-s{a.scale}.json").read_text())
|
||||
dump(man, a.api, Path(a.out))
|
||||
with httpx.Client(base_url=a.api, timeout=300) as c:
|
||||
dump(man, c, Path(a.out))
|
||||
return 0
|
||||
ok, bad = compare(Path(a.before), Path(a.after), a.mode)
|
||||
print(f"совпало {ok} из {ok + len(bad)}")
|
||||
|
||||
@ -92,3 +92,42 @@ def test_build_requests_minimal_manifest():
|
||||
assert {"projects-plan-summary", "projects-fact-summary", "projects-detail-summary"} <= set(by)
|
||||
assert not any("rollup" in n for n in by)
|
||||
assert cr.is_stripped("sheet-x") and cr.is_stripped("svod2-GO") and not cr.is_stripped("report-1")
|
||||
|
||||
|
||||
def test_strip_response_without_result():
|
||||
assert cr.strip_response({"success": False}) == {"success": False}
|
||||
assert cr.strip_response({"result": None}) == {"result": None}
|
||||
|
||||
|
||||
def test_compare_non_json_bodies(tmp_path):
|
||||
b, a = tmp_path / "before", tmp_path / "after"
|
||||
for name, bb, ab in (("svod1-A", b"<html>502", b"<html>502"), ("svod1-B", b"<html>502", b"<html>503"),
|
||||
("svod1-C", b"<html>", b'{"result": []}')):
|
||||
for d, raw in ((b, bb), (a, ab)):
|
||||
d.mkdir(exist_ok=True)
|
||||
(d / f"{name}.json.gz").write_bytes(gzip.compress(raw))
|
||||
for d in (b, a):
|
||||
(d / "index.json").write_text(json.dumps({"svod1-A": 502, "svod1-B": 502, "svod1-C": 502}))
|
||||
n, bad = cr.compare(b, a, "strip-nulls")
|
||||
assert n == 1 and len(bad) == 2
|
||||
|
||||
|
||||
def test_dump_survives_network_error(tmp_path):
|
||||
import httpx
|
||||
|
||||
def handler(req: httpx.Request) -> httpx.Response:
|
||||
if req.url.path == "/api/v1/auth/login":
|
||||
return httpx.Response(200, json={"access_token": "t"})
|
||||
if req.url.path.startswith("/api/v1/svod/1/MAIN"):
|
||||
raise httpx.ConnectError("boom", request=req)
|
||||
return httpx.Response(200, json={"result": []})
|
||||
|
||||
man = {"users": {"rf": {"username": "u", "password": "p"}, "dfip": {"username": "u", "password": "p"}},
|
||||
"sheets": [{"form_id": 1, "form_type": "FORM_2", "sheet": "AHR", "direction": None, "year": 2026,
|
||||
"url": "", "rows": 1, "owner": "rf"}], "project_reports": []}
|
||||
with httpx.Client(base_url="http://x", transport=httpx.MockTransport(handler)) as c:
|
||||
idx = cr.dump(man, c, tmp_path / "o")
|
||||
assert idx["svod1-MAIN"] == "error: ConnectError"
|
||||
assert idx["sheet-FORM_2-AHR-none-1"] == 200
|
||||
assert json.loads((tmp_path / "o" / "index.json").read_text()) == idx
|
||||
assert not (tmp_path / "o" / "svod1-MAIN.json.gz").exists()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user