#!/usr/bin/env python3 from __future__ import annotations import argparse import json import urllib.request from pathlib import Path from typing import Any DEFAULT_BASE_URL = "http://docker-gpu.cin.su:8011" def rpc(base_url: str, method: str, payload: dict[str, Any]) -> dict[str, Any]: body = json.dumps({"method": method, "payload": payload}, ensure_ascii=False).encode("utf-8") request = urllib.request.Request( base_url.rstrip("/") + "/rpc", data=body, headers={"Content-Type": "application/json; charset=utf-8"}, method="POST", ) with urllib.request.urlopen(request, timeout=int(payload.get("timeout_seconds") or 60)) as response: return json.loads(response.read().decode("utf-8")) def should_auto_select(file_name: str) -> bool: value = str(file_name or "").strip().lower() return not value or value.startswith("placeholder") def discover_module_target(base_url: str, base_id: str, table: str, timeout_seconds: int) -> dict[str, Any]: saved_state = rpc( base_url, "metadata.saved_state.modules.search", { "base_id": base_id, "tables": [table], "limit": 20, "scan_limit": 100, "include_storage": True, "timeout_seconds": timeout_seconds, }, ) if saved_state.get("status") != "ok": raise AssertionError(f"saved-state modules search failed: {saved_state.get('status')}") for module in saved_state.get("modules") or []: source = module.get("source") if isinstance(module.get("source"), dict) else {} file_name = str(source.get("file_name") or module.get("file_name") or "").strip() if not file_name: continue for stream in module.get("streams") or []: if not isinstance(stream, dict) or not stream.get("has_bsl_marker"): continue preview = str(stream.get("preview") or "") old = "#Если " if "#Если " in preview else "Процедура " if "Процедура " in preview else "" if not old: continue write_plan_target = stream.get("write_plan_target") if isinstance(stream.get("write_plan_target"), dict) else {} stream_index = int(stream.get("stream_index") or 0) return { "file_name": file_name, "stream_index": stream_index, "module_ref": stream.get("module_ref") or f"{table}:{file_name}#stream:{stream_index}", "old": old, "new": old + " ", "expected_contains": old.strip(), "expected_sha1": write_plan_target.get("expected_sha1") or "", "discovery": {"status": saved_state.get("status"), "counts": saved_state.get("counts")}, } raise AssertionError("no saved-state BSL module stream candidates found") def main() -> int: parser = argparse.ArgumentParser(description="Smoke saved-state BSL module stream write with apply_and_rollback.") parser.add_argument("--base-url", default=DEFAULT_BASE_URL) parser.add_argument("--base-id", default="upo_test") parser.add_argument("--table", default="ConfigCASSave") parser.add_argument("--file-name", default="", help="Saved-state module file name. Empty or placeholder* auto-selects a BSL stream.") parser.add_argument("--stream-index", type=int, default=4) parser.add_argument("--old", default="Перем Параметры; ") parser.add_argument("--new", default="Перем Параметры; ") parser.add_argument("--expected-contains", default="Перем Параметры;") parser.add_argument("--expected-sha1", default="") parser.add_argument("--report", type=Path, required=True) parser.add_argument("--timeout-seconds", type=int, default=30) parser.add_argument("--allow-empty-saved-state", action="store_true", help="Exit successfully with skipped status when the saved-state table has no modules.") args = parser.parse_args() auto_target: dict[str, Any] = {} if should_auto_select(args.file_name): try: auto_target = discover_module_target(args.base_url, args.base_id, args.table, args.timeout_seconds) except AssertionError as exc: if not args.allow_empty_saved_state: raise result = { "schema": "onec_module_stream_write_smoke.v1", "status": "skipped_no_saved_state_candidate", "skipped": True, "base_id": args.base_id, "table": args.table, "saved_state_table": args.table, "diagnostics": {"message": str(exc)}, } args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({"schema": result["schema"], "status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2)) return 0 args.file_name = auto_target["file_name"] args.stream_index = int(auto_target["stream_index"]) args.old = auto_target["old"] args.new = auto_target["new"] args.expected_contains = auto_target["expected_contains"] if auto_target.get("expected_sha1") and not args.expected_sha1: args.expected_sha1 = str(auto_target["expected_sha1"]) module_ref = f"{args.table}:{args.file_name}#stream:{args.stream_index}" if args.allow_empty_saved_state: saved_state = rpc( args.base_url, "metadata.saved_state.modules.search", {"base_id": args.base_id, "tables": [args.table], "limit": 1, "scan_limit": 100, "timeout_seconds": args.timeout_seconds}, ) if saved_state.get("status") == "ok" and ((saved_state.get("counts") or {}).get("modules") or 0) == 0: result = { "schema": "onec_module_stream_write_smoke.v1", "status": "skipped_no_saved_state", "skipped": True, "base_id": args.base_id, "table": args.table, "module_ref": module_ref, "saved_state_preflight": {"status": saved_state.get("status"), "counts": saved_state.get("counts")}, } args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({"schema": result["schema"], "status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2)) return 0 write_payload: dict[str, Any] = { "base_id": args.base_id, "target": {"kind": "module", "module_ref": module_ref}, "mode": "apply_and_rollback", "allow_sql_saved_state_apply": True, "allow_sql_saved_state_rollback": True, "old": args.old, "new": args.new, "expected_contains": args.expected_contains, "timeout_seconds": args.timeout_seconds, } if args.expected_sha1: write_payload["expected_sha1"] = args.expected_sha1 written = rpc(args.base_url, "metadata.write", write_payload) write_result = written.get("result") if isinstance(written.get("result"), dict) else {} write_plan = write_result.get("write_plan") if isinstance(write_result.get("write_plan"), dict) else {} if written.get("status") != "verified_and_rolled_back" or write_plan.get("allowed") is not True: result = { "schema": "onec_module_stream_write_smoke.v1", "status": "write_failed", "metadata_write": written, "write_plan": write_plan, } args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({"status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2)) return 1 final = rpc( args.base_url, "query.run", { "base_id": args.base_id, "diagnostic": True, "query": ( "SELECT FileName, DataSize, DATALENGTH(BinaryData) AS BinaryBytes, " "CONVERT(varchar(40), HASHBYTES('SHA1', BinaryData), 2) AS BinarySHA1 " f"FROM {args.table} WHERE FileName = '{args.file_name}'" ), "timeout_seconds": args.timeout_seconds, }, ) result = { "schema": "onec_module_stream_write_smoke.v1", "status": "verified_and_rolled_back", "base_id": args.base_id, "table": args.table, "module_ref": module_ref, **({"saved_state_preflight": auto_target.get("discovery")} if auto_target else {}), "write_plan": { "status": write_plan.get("status"), "allowed": write_plan.get("allowed"), "apply_method": (write_plan.get("route") or {}).get("apply_method"), "target_kind": (write_plan.get("target") or {}).get("target_kind"), }, "metadata_write": { "status": written.get("status"), "routed_method": written.get("routed_method"), "result_status": write_result.get("status"), "applied": write_result.get("applied"), "rolled_back": write_result.get("rolled_back"), }, "proposal": {key: (write_result.get("proposal") or {}).get(key) for key in ("status", "original", "encoded", "edits", "validation", "counts")}, "apply_result": write_result.get("apply_result"), "rollback_result": write_result.get("rollback_result"), "verify_after_rollback": final, } args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({"schema": result["schema"], "status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())