#!/usr/bin/env python3 from __future__ import annotations import argparse import json import sys from pathlib import Path from typing import Any from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen DEFAULT_FORM_FILE = "f96a0c45-3eff-11f1-8287-005056b0d483__25c39fbf-35a4-4b43-8e3f-cd1f91082c88.0" def request_json( method: str, base_url: str, path: str, *, payload: dict[str, Any] | None = None, timeout: float = 30.0, ) -> tuple[int, dict[str, Any] | str, bool]: body = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None request = Request( f"{base_url.rstrip('/')}{path}", data=body, method=method, headers={"Content-Type": "application/json"}, ) try: with urlopen(request, timeout=timeout) as response: raw = response.read().decode("utf-8") return response.status, json.loads(raw) if raw else {}, False except HTTPError as exc: raw = exc.read().decode("utf-8") return exc.code, json.loads(raw) if raw else {}, True except URLError as exc: return 0, str(exc), True def rpc(base_url: str, method: str, payload: dict[str, Any], timeout: float) -> dict[str, Any]: status, response, is_error = request_json( "POST", base_url, "/rpc", payload={"method": method, "payload": payload}, timeout=timeout, ) if is_error or status != 200 or not isinstance(response, dict): raise AssertionError(f"{method} failed: status={status}, response={response}") return response def compact_smoke_result(smoke: dict[str, Any], *, max_failures: int) -> dict[str, Any]: failures = [] for row in smoke.get("results") or []: if not isinstance(row, dict) or row.get("status") == "verified": continue entry = row.get("entry") if isinstance(row.get("entry"), dict) else {} prop = entry.get("property") if isinstance(entry.get("property"), dict) else {} requested = entry.get("requested_target") if isinstance(entry.get("requested_target"), dict) else {} effective = entry.get("effective_target") if isinstance(entry.get("effective_target"), dict) else {} failures.append( { "status": row.get("status"), "target": requested.get("name") or requested.get("path"), "requested_section": requested.get("section"), "effective_section": effective.get("section"), "property": prop.get("presentation") or prop.get("canonical_property") or prop.get("property"), "old": prop.get("old"), "test_value": prop.get("test_value"), "diagnostics": row.get("diagnostics"), } ) if len(failures) >= max_failures: break return { "schema": "onec_write_matrix_smoke_cli_summary.v1", "status": smoke.get("status"), "counts": smoke.get("counts"), "adapter_report_path": smoke.get("path"), "failures": failures, } def main() -> int: parser = argparse.ArgumentParser(description="Build and optionally smoke-test saved-state form write matrix.") parser.add_argument("--base-url", default="http://docker.cin.su:8011", help="1C REST adapter URL.") parser.add_argument("--base-id", default="upo_test", help="Configured adapter base id.") parser.add_argument("--table", default="ConfigCASSave", help="Saved-state SQL table.") parser.add_argument("--file-name", default=DEFAULT_FORM_FILE, help="Saved-state form file name.") parser.add_argument("--timeout", type=float, default=30.0, help="Request timeout in seconds.") parser.add_argument("--max-candidates", type=int, default=50, help="Maximum safe write candidates to smoke.") parser.add_argument("--learning-id", default="upo-test-write-matrix", help="Adapter-side write-learning id.") parser.add_argument("--build-only", action="store_true", help="Only build the matrix; do not run apply_and_rollback smoke.") parser.add_argument("--report", type=Path, help="Write full JSON report to file.") parser.add_argument("--max-failures", type=int, default=20, help="Maximum failures to include in console summary.") args = parser.parse_args() base_payload = { "base_id": args.base_id, "table": args.table, "file_name": args.file_name, "timeout_seconds": int(args.timeout), } report: dict[str, Any] = { "schema": "onec_write_matrix_smoke_cli_report.v1", "base_url": args.base_url, "base_id": args.base_id, "table": args.table, "file_name": args.file_name, "passed": False, } try: status, health, is_error = request_json("GET", args.base_url, f"/health?base_id={args.base_id}", timeout=args.timeout) if is_error or status != 200 or not isinstance(health, dict) or health.get("status") != "ok": raise AssertionError(f"health failed: status={status}, response={health}") report["health"] = {"status": health.get("status"), "live_sql": health.get("live_sql")} matrix = rpc(args.base_url, "metadata.form.write_matrix.build", base_payload, args.timeout) report["matrix"] = matrix if matrix.get("status") != "ok": raise AssertionError(f"matrix build failed: {matrix.get('status')}") if not args.build_only: smoke_payload = { **base_payload, "allow_sql_saved_state_apply": True, "allow_sql_saved_state_rollback": True, "max_candidates": args.max_candidates, "learning_id": args.learning_id, } smoke = rpc(args.base_url, "metadata.form.write_matrix.smoke", smoke_payload, max(args.timeout, args.max_candidates * args.timeout)) report["smoke"] = smoke report["passed"] = smoke.get("status") == "ok" else: report["passed"] = True except Exception as exc: report["error"] = str(exc) 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") print(json.dumps(report, ensure_ascii=False, indent=2), file=sys.stderr) return 1 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") summary = { "schema": "onec_write_matrix_cli_summary.v1", "passed": report["passed"], "matrix_counts": (report.get("matrix") or {}).get("counts"), "smoke": compact_smoke_result(report.get("smoke") or {}, max_failures=args.max_failures) if not args.build_only else None, "report": str(args.report) if args.report else None, } print(json.dumps(summary, ensure_ascii=False, indent=2)) return 0 if report["passed"] else 1 if __name__ == "__main__": raise SystemExit(main())