179 lines
7.9 KiB
Python
179 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
DEFAULT_BASE_URL = "http://docker-gpu.cin.su:8011"
|
|
DEFAULT_TABLES = ("ConfigCASSave", "ConfigSave")
|
|
ALLOWED_TABLES = {"ConfigCASSave", "ConfigSave"}
|
|
|
|
|
|
def request_json(method: str, url: str, *, payload: dict[str, Any] | None, timeout: float) -> tuple[int, dict[str, Any] | str]:
|
|
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
|
|
request = urllib.request.Request(
|
|
url,
|
|
data=data,
|
|
method=method,
|
|
headers={"Content-Type": "application/json; charset=utf-8"},
|
|
)
|
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
raw = response.read().decode("utf-8")
|
|
return response.status, json.loads(raw) if raw else {}
|
|
|
|
|
|
def rpc(base_url: str, method: str, payload: dict[str, Any], timeout: float) -> dict[str, Any]:
|
|
status, response = request_json(
|
|
"POST",
|
|
base_url.rstrip("/") + "/rpc",
|
|
payload={"method": method, "payload": payload},
|
|
timeout=timeout,
|
|
)
|
|
if status != 200 or not isinstance(response, dict):
|
|
raise RuntimeError(f"{method} failed: status={status}, response={response!r}")
|
|
return response
|
|
|
|
|
|
def health_summary(base_url: str, base_id: str, timeout: float) -> dict[str, Any]:
|
|
status, response = request_json("GET", f"{base_url.rstrip('/')}/health?base_id={base_id}", payload=None, timeout=timeout)
|
|
if status != 200 or not isinstance(response, dict):
|
|
return {"status": "error", "http_status": status, "response": response}
|
|
live_sql = response.get("live_sql") if isinstance(response.get("live_sql"), dict) else {}
|
|
return {
|
|
"status": response.get("status"),
|
|
"contract_version": response.get("contract_version"),
|
|
"live_sql": {
|
|
"configured": live_sql.get("configured"),
|
|
"server": live_sql.get("server"),
|
|
"database": live_sql.get("database"),
|
|
},
|
|
}
|
|
|
|
|
|
def saved_state_row_counts(base_url: str, base_id: str, tables: list[str], timeout: float) -> dict[str, Any]:
|
|
selects = [f"SELECT '{table}' AS TableName, COUNT(*) AS RowsCount FROM {table}" for table in tables]
|
|
result = rpc(
|
|
base_url,
|
|
"query.run",
|
|
{
|
|
"base_id": base_id,
|
|
"diagnostic": True,
|
|
"query": "\nUNION ALL\n".join(selects),
|
|
"timeout_seconds": int(timeout),
|
|
},
|
|
timeout,
|
|
)
|
|
counts: dict[str, int] = {table: 0 for table in tables}
|
|
for row in result.get("rows") or []:
|
|
if not isinstance(row, dict):
|
|
continue
|
|
table = str(row.get("TableName") or "")
|
|
if table in counts and isinstance(row.get("RowsCount"), int):
|
|
counts[table] = int(row["RowsCount"])
|
|
return {
|
|
"status": result.get("status"),
|
|
"validation": result.get("validation"),
|
|
"counts": counts,
|
|
"raw_counts": result.get("counts"),
|
|
}
|
|
|
|
|
|
def search_saved_state(base_url: str, base_id: str, tables: list[str], timeout: float) -> dict[str, Any]:
|
|
common = {"base_id": base_id, "tables": tables, "limit": 3, "timeout_seconds": int(timeout)}
|
|
forms = rpc(base_url, "metadata.saved_state.forms.search", common, timeout)
|
|
modules = rpc(base_url, "metadata.saved_state.modules.search", {**common, "scan_limit": 100}, timeout)
|
|
return {
|
|
"forms": {
|
|
"status": forms.get("status"),
|
|
"counts": forms.get("counts"),
|
|
"sample": forms.get("forms") or [],
|
|
},
|
|
"modules": {
|
|
"status": modules.get("status"),
|
|
"counts": modules.get("counts"),
|
|
"sample": modules.get("modules") or [],
|
|
},
|
|
}
|
|
|
|
|
|
def build_report(base_url: str, base_id: str, tables: list[str], timeout: float, saved_state_table: str | None) -> dict[str, Any]:
|
|
report: dict[str, Any] = {
|
|
"schema": "onec_saved_state_strict_readiness.v1",
|
|
"base_url": base_url,
|
|
"base_id": base_id,
|
|
"saved_state_table": saved_state_table,
|
|
"tables": tables,
|
|
"ready": False,
|
|
"status": "error",
|
|
"checks": {},
|
|
"recommendations": [],
|
|
}
|
|
report["checks"]["health"] = health_summary(base_url, base_id, timeout)
|
|
report["checks"]["row_counts"] = saved_state_row_counts(base_url, base_id, tables, timeout)
|
|
report["checks"]["saved_state_search"] = search_saved_state(base_url, base_id, tables, timeout)
|
|
|
|
row_counts = (report["checks"]["row_counts"] or {}).get("counts") or {}
|
|
forms_counts = (((report["checks"]["saved_state_search"] or {}).get("forms") or {}).get("counts") or {})
|
|
modules_counts = (((report["checks"]["saved_state_search"] or {}).get("modules") or {}).get("counts") or {})
|
|
total_rows = sum(value for value in row_counts.values() if isinstance(value, int))
|
|
forms = forms_counts.get("forms") if isinstance(forms_counts.get("forms"), int) else 0
|
|
modules = modules_counts.get("modules") if isinstance(modules_counts.get("modules"), int) else 0
|
|
|
|
report["summary"] = {
|
|
"saved_state_rows": total_rows,
|
|
"forms": forms,
|
|
"modules": modules,
|
|
}
|
|
if forms > 0 and modules > 0:
|
|
report["ready"] = True
|
|
report["status"] = "ready"
|
|
report["recommendations"].append("Strict saved-state smoke can be attempted: form and module saved-state candidates are present in the selected save layer.")
|
|
elif total_rows == 0:
|
|
report["status"] = "blocked_no_saved_state_rows"
|
|
report["recommendations"].append(
|
|
f"No unactivated Configurator changes are present in {', '.join(tables)}. To prepare a strict write test, copy the target object from Config/ConfigCAS into the selected save layer using the approved saved-state workflow, then rerun readiness."
|
|
)
|
|
else:
|
|
report["status"] = "blocked_no_strict_candidates"
|
|
report["recommendations"].append(
|
|
"Saved-state rows exist, but the adapter did not find both form and module candidates needed by strict write-and-rollback smoke."
|
|
)
|
|
return report
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Read-only readiness check for strict 1C saved-state smoke tests.")
|
|
parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
|
|
parser.add_argument("--base-id", default="upo_test")
|
|
parser.add_argument("--table", action="append", choices=sorted(ALLOWED_TABLES), help="Saved-state table to inspect. Repeatable.")
|
|
parser.add_argument("--saved-state-table", choices=sorted(ALLOWED_TABLES), help="Expected saved-state table for the strict smoke target.")
|
|
parser.add_argument("--timeout", type=float, default=30.0)
|
|
parser.add_argument("--report", type=Path)
|
|
parser.add_argument("--require-ready", action="store_true", help="Exit non-zero when strict saved-state smoke is not ready.")
|
|
parser.add_argument("--json", action="store_true", help="Print full JSON report.")
|
|
args = parser.parse_args()
|
|
|
|
if args.saved_state_table and args.table and any(table != args.saved_state_table for table in args.table):
|
|
parser.error("--saved-state-table must match --table when both are provided")
|
|
tables = args.table or ([args.saved_state_table] if args.saved_state_table else list(DEFAULT_TABLES))
|
|
report = build_report(args.base_url, args.base_id, tables, args.timeout, args.saved_state_table)
|
|
if args.report:
|
|
args.report.parent.mkdir(parents=True, exist_ok=True)
|
|
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
|
|
if args.json or args.require_ready or not report["ready"]:
|
|
print(json.dumps(report, ensure_ascii=True, indent=2), file=sys.stderr if args.require_ready and not report["ready"] else sys.stdout)
|
|
else:
|
|
print(f"OK: strict saved-state smoke readiness passed for {args.base_id}.")
|
|
return 0 if report["ready"] or not args.require_ready else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|
|
|