Files
llm/scripts/smoke_1c_saved_state_module_write.py
T
2026-08-14 09:40:51 +03:00

246 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import re
import urllib.request
from pathlib import Path
from typing import Any
DEFAULT_BASE_URL = "http://docker.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):
continue
preview = str(stream.get("preview") or "")
if not stream.get("has_bsl_marker") and not any(
marker in preview for marker in ("Процедура ", "Функция ", "&НаКлиенте", "&НаСервере")
):
continue
declaration = re.search(r"(?im)^\s*(?:Процедура|Функция)\s+[A-Za-zА-Яа-яЁё_][\wА-Яа-яЁё]*\s*\([^)]*\)", preview)
old = declaration.group(0).strip() if declaration 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(
"--module-ref",
default="",
help="Exact module_ref returned by discovery. Useful for embedded form modules without a #stream suffix.",
)
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 = str(
args.module_ref
or auto_target.get("module_ref")
or f"{args.table}:{args.file_name}#stream:{args.stream_index}"
)
saved_state = rpc(
args.base_url,
"metadata.saved_state.modules.search",
{
"base_id": args.base_id,
"tables": [args.table],
"file_name": args.file_name,
"limit": 1,
"scan_limit": 10,
"include_storage": True,
"timeout_seconds": args.timeout_seconds,
},
)
saved_state_preflight = {
"status": saved_state.get("status"),
"counts": saved_state.get("counts") or {},
}
if args.allow_empty_saved_state:
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",
"base_id": args.base_id,
"table": args.table,
"module_ref": module_ref,
"saved_state_preflight": saved_state_preflight,
"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": saved_state_preflight,
"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())