267 lines
9.5 KiB
Python
267 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
compare_test.py — Polygon ↔ Nubes API (v2, трёхкатегорийная классификация).
|
|
|
|
Сравнивает ТОЛЬКО сервисы полигона (из терраформа) с реальным API.
|
|
Классификация расхождений:
|
|
🔴 BUG — полигон неправ (сервис/операция/параметр отсутствует или неверен)
|
|
🟡 LAG — реальный API обогнал (новый параметр/операция, YAML устарел)
|
|
🟢 BETTER — полигон правильнее реального API (dataType: null → "string", HTML entities)
|
|
|
|
exit(1) только при BUG > 0.
|
|
"""
|
|
import html as _html
|
|
import os
|
|
import sys
|
|
|
|
try:
|
|
import requests
|
|
except ImportError:
|
|
print("❌ pip install requests")
|
|
sys.exit(1)
|
|
|
|
TOKENS_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "secrets")
|
|
UA = "Mozilla/5.0"
|
|
TIMEOUT = 15
|
|
|
|
REAL = {
|
|
"dev": "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
|
|
"test": "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc",
|
|
"prod": "https://lk-api-gateway.ngcloud.ru/api/v1/svc",
|
|
}
|
|
POLYGON = {
|
|
"dev": "https://polygon.pythonk8s.dev.nubes.ru/dev/api/v1/svc",
|
|
"test": "https://polygon.pythonk8s.dev.nubes.ru/test/api/v1/svc",
|
|
"prod": "https://polygon.pythonk8s.dev.nubes.ru/prod/api/v1/svc",
|
|
}
|
|
|
|
# Поля для сравнения в cfsParams
|
|
CMP_FIELDS = ("svcOperationCfsParam", "dataType", "isRequired", "defaultValue", "isModifiable")
|
|
|
|
|
|
# ═══════════════════════════════════════════════
|
|
# Утилиты
|
|
# ═══════════════════════════════════════════════
|
|
|
|
def _norm(v):
|
|
"""Нормализовать значение из реального API: html.unescape для строк."""
|
|
if isinstance(v, str):
|
|
return _html.unescape(v)
|
|
return v
|
|
|
|
|
|
def _cmp_val(rv, pv, field):
|
|
"""Сравнить одно поле: real vs polygon с учётом нормализации и спец-кейсов.
|
|
|
|
Возвращает:
|
|
None — значения совпадают
|
|
'bug' — расхождение, полигон неправ
|
|
'better' — полигон правильнее (dataType null→string)
|
|
"""
|
|
rv_norm = _norm(rv)
|
|
|
|
# 🟢 BETTER: real API возвращает null, полигон — осмысленный дефолт
|
|
if rv_norm is None:
|
|
if field == "dataType" and pv == "string":
|
|
return "better"
|
|
if field == "defaultValue" and pv == "":
|
|
return "better"
|
|
if field == "isModifiable" and pv in ("False", "True"):
|
|
return "better"
|
|
|
|
# 🟢 BETTER: HTML entities в real API
|
|
if rv_norm != rv and rv_norm == pv:
|
|
return "better"
|
|
|
|
if rv_norm != pv:
|
|
return "bug"
|
|
|
|
return None
|
|
|
|
|
|
def _cmp_value_lists(rv, pv):
|
|
"""Сравнить valueList: нормализовать None→[], сравнить сортированные."""
|
|
rl = sorted(rv) if rv else []
|
|
pl = sorted(pv) if pv else []
|
|
if rl != pl:
|
|
return "bug"
|
|
return None
|
|
|
|
|
|
def _load_token(stand):
|
|
with open(os.path.join(TOKENS_DIR, f"{stand}.token")) as f:
|
|
return f.read().strip()
|
|
|
|
|
|
def _real_headers(stand):
|
|
return {
|
|
"Authorization": f"Bearer {_load_token(stand)}",
|
|
"User-Agent": UA,
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
|
|
def _poly_headers():
|
|
return {"Content-Type": "application/json"}
|
|
|
|
|
|
def _get(url, headers, timeout=TIMEOUT):
|
|
r = requests.get(url, headers=headers, timeout=timeout)
|
|
assert r.status_code == 200, f"{r.status_code}: {r.text[:200]}"
|
|
return r.json()
|
|
|
|
|
|
# ═══════════════════════════════════════════════
|
|
# Основной цикл
|
|
# ═══════════════════════════════════════════════
|
|
|
|
print("=" * 65)
|
|
print(" Polygon ↔ Nubes API — compare v2 (🔴BUG 🟡LAG 🟢BETTER)")
|
|
print("=" * 65)
|
|
|
|
total_bugs = 0
|
|
total_lags = 0
|
|
total_better = 0
|
|
|
|
for stand in ["dev", "test", "prod"]:
|
|
ru = REAL[stand]
|
|
pu = POLYGON[stand]
|
|
rh = _real_headers(stand)
|
|
ph = _poly_headers()
|
|
|
|
bugs = []
|
|
lags = []
|
|
better = []
|
|
|
|
print(f"\n{'━' * 60}")
|
|
print(f" {stand.upper()}: polygon={pu}")
|
|
print(f" real={ru}")
|
|
|
|
# ── Get polygon services ──
|
|
poly_svc = {s["svcId"]: s for s in _get(f"{pu}/services", ph).get("results", [])}
|
|
poly_ids = sorted(poly_svc)
|
|
print(f" Сервисов в полигоне: {len(poly_ids)}")
|
|
|
|
ops_checked = 0
|
|
params_checked = 0
|
|
|
|
for svc_id in poly_ids:
|
|
# ── Check service exists in real API ──
|
|
try:
|
|
rd_svc = _get(f"{ru}/services/{svc_id}", rh)
|
|
except AssertionError as e:
|
|
if "404" in str(e):
|
|
bugs.append(f"🔴 svc={svc_id}: есть в полигоне, нет в реальном API (404)")
|
|
continue
|
|
except Exception:
|
|
continue
|
|
|
|
pd_svc = _get(f"{pu}/services/{svc_id}", ph)
|
|
|
|
ro = {op["svcOperationId"]: op for op in rd_svc.get("svc", {}).get("operations", [])}
|
|
po = {op["svcOperationId"]: op for op in pd_svc.get("svc", {}).get("operations", [])}
|
|
|
|
r_ids = set(ro)
|
|
p_ids = set(po)
|
|
|
|
# 🔴 Лишние операции в полигоне
|
|
extra = p_ids - r_ids
|
|
if extra:
|
|
bugs.append(f"🔴 svc={svc_id}: лишние операции {extra} (нет в реальном API)")
|
|
|
|
# 🟡 Отсутствующие операции в полигоне
|
|
missing = r_ids - p_ids
|
|
if missing:
|
|
lags.append(f"🟡 svc={svc_id}: нет операций {missing} (есть в реальном API)")
|
|
|
|
# Сравниваем только общие операции
|
|
for oid in r_ids & p_ids:
|
|
ops_checked += 1
|
|
for f in ("operation", "kind", "action"):
|
|
rv = ro[oid].get(f)
|
|
pv = po[oid].get(f)
|
|
if rv != pv:
|
|
bugs.append(f"🔴 svc={svc_id} op={oid} {f}: '{rv}' ≠ '{pv}'")
|
|
|
|
# ── cfsParams ──
|
|
try:
|
|
rd_par = _get(f"{ru}/instanceOperations/default/{oid}", rh)
|
|
except Exception:
|
|
continue
|
|
pd_par = _get(f"{pu}/instanceOperations/default/{oid}", ph)
|
|
|
|
rp = {p["svcOperationCfsParamId"]: p for p in rd_par.get("svcOperation", {}).get("cfsParams", [])}
|
|
pp = {p["svcOperationCfsParamId"]: p for p in pd_par.get("svcOperation", {}).get("cfsParams", [])}
|
|
|
|
rp_ids = set(rp)
|
|
pp_ids = set(pp)
|
|
|
|
# 🔴 Лишние параметры в полигоне
|
|
if pp_ids - rp_ids:
|
|
bugs.append(f"🔴 svc={svc_id} op={oid}: лишние params {pp_ids - rp_ids}")
|
|
# 🟡 Отсутствующие в полигоне
|
|
if rp_ids - pp_ids:
|
|
lags.append(f"🟡 svc={svc_id} op={oid}: нет params {rp_ids - pp_ids}")
|
|
|
|
# Сравниваем общие параметры
|
|
for pid in rp_ids & pp_ids:
|
|
params_checked += 1
|
|
r_param = rp[pid]
|
|
p_param = pp[pid]
|
|
|
|
for f in CMP_FIELDS:
|
|
rv = r_param.get(f)
|
|
pv = p_param.get(f)
|
|
result = _cmp_val(rv, pv, f)
|
|
if result == "bug":
|
|
bugs.append(f"🔴 svc={svc_id} op={oid} param={pid} {f}: '{rv}' ≠ '{pv}'")
|
|
elif result == "better":
|
|
better.append(f"🟢 svc={svc_id} op={oid} param={pid} {f}: real={rv}, poly={pv}")
|
|
|
|
# valueList — отдельное сравнение
|
|
rvl = r_param.get("valueList")
|
|
pvl = p_param.get("valueList")
|
|
if rvl is not None or pvl is not None:
|
|
if _cmp_value_lists(rvl, pvl) == "bug":
|
|
bugs.append(f"🔴 svc={svc_id} op={oid} param={pid} valueList: {rvl} ≠ {pvl}")
|
|
|
|
# ── Report ──
|
|
print(f" Проверено: {ops_checked} операций, {params_checked} параметров")
|
|
print(f" 🔴 BUGS: {len(bugs)}")
|
|
print(f" 🟡 LAGS: {len(lags)}")
|
|
print(f" 🟢 BETTER: {len(better)}")
|
|
|
|
for b in bugs[:8]:
|
|
print(f" {b}")
|
|
if len(bugs) > 8:
|
|
print(f" ... +{len(bugs)-8}")
|
|
|
|
for l in lags[:5]:
|
|
print(f" {l}")
|
|
if len(lags) > 5:
|
|
print(f" ... +{len(lags)-5}")
|
|
|
|
for b in better[:3]:
|
|
print(f" {b}")
|
|
if len(better) > 3:
|
|
print(f" ... +{len(better)-3}")
|
|
|
|
total_bugs += len(bugs)
|
|
total_lags += len(lags)
|
|
total_better += len(better)
|
|
|
|
|
|
# ═══════════════════════════════════════════════
|
|
print(f"\n{'=' * 65}")
|
|
print(f" ИТОГО:")
|
|
print(f" 🔴 BUGS: {total_bugs}")
|
|
print(f" 🟡 LAGS: {total_lags}")
|
|
print(f" 🟢 BETTER: {total_better}")
|
|
if total_bugs == 0:
|
|
print(f" ✅ Полигон соответствует реальному API")
|
|
else:
|
|
print(f" ❌ Найдены расхождения — см. выше")
|
|
print(f"{'=' * 65}")
|
|
|
|
sys.exit(1 if total_bugs > 0 else 0)
|