mirror of
https://github.com/hpd840321/starRiverProperty.git
synced 2026-06-10 00:40:30 +08:00
418c7db202
- davinci-manager-storage:FilePart 路径与基址按 V1 JAR(/portal/file、/part/*、GET /download) - 启动类:扫描 cn.cloudwalk.serial 与 cn.cloudwalk.cwos.client.resource,补 UUIDSerial 与 ApplicationService - deploy:v1/v2 application 中 cloudwalk.serial.enabled、Kafka 指向 192.168.3.12:9092;deploy/.gitignore 忽略日志 - cloudwalk-common-serial:补充 META-INF/spring.factories(Boot 自动配置) - 电梯:Session 配置、Davinci Bean、Feign 包、MQTT/Visitor/Zone Feign;部署脚本与 API parity 工具更新 - 文档与根脚本若干;未纳入大体积 jar/zip 与 v1 CFR 对比目录 Made-with: Cursor Former-commit-id: b76d142d13ebb5c0898de2d9d11bc583876829c2
183 lines
5.3 KiB
Python
183 lines
5.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Any, Optional
|
|
|
|
import requests
|
|
|
|
from . import compare
|
|
from .health import probe_healthy
|
|
|
|
|
|
@dataclass
|
|
class ParityResult:
|
|
name: str
|
|
method: str
|
|
path: str
|
|
old_status: int
|
|
new_status: int
|
|
old_text: str
|
|
new_text: str
|
|
match: bool
|
|
message: str
|
|
|
|
|
|
def default_headers() -> dict[str, str]:
|
|
h: dict[str, str] = {
|
|
"Content-Type": "application/json",
|
|
}
|
|
m = {
|
|
"businessid": "ELEVATOR_HEADER_BUSINESSID",
|
|
"loginid": "ELEVATOR_HEADER_LOGINID",
|
|
"platformuserid": "ELEVATOR_HEADER_PLATFORMUSERID",
|
|
"authorization": "ELEVATOR_HEADER_AUTHORIZATION",
|
|
"applicationid": "ELEVATOR_HEADER_APPLICATIONID",
|
|
}
|
|
for hdr, evar in m.items():
|
|
v = os.environ.get(evar, "").strip()
|
|
if v:
|
|
h[hdr] = v
|
|
return h
|
|
|
|
|
|
def call_both(
|
|
name: str,
|
|
method: str,
|
|
path: str,
|
|
body: Any,
|
|
base_old: str,
|
|
base_new: str,
|
|
extra_headers: Optional[dict] = None,
|
|
compare_mode: str = "deep",
|
|
session: requests.Session | None = None,
|
|
) -> ParityResult:
|
|
s = session or requests.Session()
|
|
h = {**default_headers(), **(extra_headers or {})}
|
|
url_o = base_old.rstrip("/") + path
|
|
url_n = base_new.rstrip("/") + path
|
|
if body is None:
|
|
data = None
|
|
else:
|
|
data = body if isinstance(body, str) else json.dumps(body, ensure_ascii=False)
|
|
m = method.upper()
|
|
if m == "GET":
|
|
r_old = s.get(url_o, headers=h, timeout=60)
|
|
r_new = s.get(url_n, headers=h, timeout=60)
|
|
else:
|
|
r_old = s.post(url_o, headers=h, data=data, timeout=120)
|
|
r_new = s.post(url_n, headers=h, data=data, timeout=120)
|
|
ot, nt = r_old.text, r_new.text
|
|
st_o, st_n = r_old.status_code, r_new.status_code
|
|
msg = ""
|
|
|
|
if compare_mode == "status_only":
|
|
m_ok = st_o == st_n
|
|
if not m_ok:
|
|
msg = f"http {st_o} vs {st_n}"
|
|
elif compare_mode == "code_only":
|
|
msg = ""
|
|
oj, nj = _safe_json(ot), _safe_json(nt)
|
|
c1, c2 = compare.business_code(oj), compare.business_code(nj)
|
|
m_ok = st_o == st_n
|
|
if m_ok and c1 is not None and c2 is not None:
|
|
m_ok = c1 == c2
|
|
elif m_ok and c1 is None and c2 is None and st_o == st_n and 400 <= st_o < 600:
|
|
m_ok = True
|
|
msg = "http status only (no parseable JSON body)"
|
|
elif m_ok and (c1 is None) != (c2 is None):
|
|
m_ok = False
|
|
if not m_ok and not msg:
|
|
msg = compare.describe_diff(ot, nt, "code_only")
|
|
elif m_ok and not msg:
|
|
msg = ""
|
|
else:
|
|
m_ok = st_o == st_n
|
|
oj, nj = _safe_json(ot), _safe_json(nt)
|
|
if oj is not None and nj is not None and m_ok:
|
|
m_ok = compare.normalize(oj) == compare.normalize(nj)
|
|
elif m_ok and (ot or "") != (nt or ""):
|
|
m_ok = (ot or "").strip() == (nt or "").strip()
|
|
if not m_ok:
|
|
msg = compare.describe_diff(ot, nt, "deep") or f"http {st_o}/{st_n} or body mismatch"
|
|
|
|
return ParityResult(
|
|
name=name,
|
|
method=m,
|
|
path=path,
|
|
old_status=st_o,
|
|
new_status=st_n,
|
|
old_text=ot,
|
|
new_text=nt,
|
|
match=m_ok,
|
|
message=msg,
|
|
)
|
|
|
|
|
|
def _safe_json(text: str) -> Any:
|
|
if not (text or "").strip():
|
|
return None
|
|
try:
|
|
return compare.parse_json_loose(text)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def call_single(
|
|
name: str,
|
|
method: str,
|
|
path: str,
|
|
body: Any,
|
|
base_url: str,
|
|
session: requests.Session | None = None,
|
|
) -> dict[str, Any]:
|
|
"""One HTTP call for smoke coverage; returns a flat dict for reporting."""
|
|
s = session or requests.Session()
|
|
h = default_headers()
|
|
url = base_url.rstrip("/") + path
|
|
data = (
|
|
None
|
|
if body is None
|
|
else (body if isinstance(body, str) else json.dumps(body, ensure_ascii=False))
|
|
)
|
|
m = method.upper()
|
|
t0 = time.perf_counter()
|
|
if m == "GET":
|
|
r = s.get(url, headers=h, timeout=120)
|
|
else:
|
|
r = s.post(url, headers=h, data=data, timeout=120)
|
|
elapsed_ms = int((time.perf_counter() - t0) * 1000)
|
|
txt = r.text or ""
|
|
oj = _safe_json(txt)
|
|
bc = compare.business_code(oj) if oj is not None else None
|
|
head = txt[:400].replace("\n", " ")
|
|
return {
|
|
"name": name,
|
|
"method": m,
|
|
"path": path,
|
|
"http_status": r.status_code,
|
|
"elapsed_ms": elapsed_ms,
|
|
"business_code": bc,
|
|
"response_head": head,
|
|
"reachable": True,
|
|
}
|
|
|
|
|
|
def can_reach_one(base_url: str, s: requests.Session | None = None) -> tuple[bool, str]:
|
|
s = s or requests.Session()
|
|
_, ok, _ = probe_healthy(base_url, s)
|
|
return ok, base_url
|
|
|
|
|
|
def can_reach_both(
|
|
base_old: str, base_new: str, s: requests.Session | None = None
|
|
) -> tuple[bool, str]:
|
|
s = s or requests.Session()
|
|
p1, ok1, _ = probe_healthy(base_old, s)
|
|
p2, ok2, _ = probe_healthy(base_new, s)
|
|
if ok1 and ok2:
|
|
return True, f"ok old health path~{p1} new probed"
|
|
return False, f"health old ok={ok1} new ok={ok2} (paths like {p1})"
|