#!/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) # Спец-кейс dataType: real=None, poly="string" → 🟢 BETTER if field == "dataType" and rv_norm is None and pv == "string": return "better" # HTML entities в real → 🟢 BETTER 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)