world_sync/scripts/m11b2b_selftest.py
2026-09-20 20:50:51 +08:00

338 lines
14 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
# -*- coding: utf-8 -*-
"""M11b-2b 自测:表定义 JSON 符合 database-table-definition-spec四段式内联口径
口径变更M11b-2b-B1world_sync/models/pbl_runtime_event.json、pbl_entity_state.json
已由「key=schema 引用式」改为**四段式内联**summary/fields/indexes/codes列集与权威真源
modules/pbl_runtime_ext/models/*.json 内容级对齐,由 m11b2b_column_diff.py 做双向列集 diff 留证,
不再靠 authority_path 运行时引用。本脚本断言口径随之更新。
覆盖全部 4 个落点world_sync 真源 2 + 应用打包镜像 2任一不符即 RESULT: FAIL。
断言集(判定项,计入 RESULT
A1 JSON 可解析且根为对象;
A2 根键 == {"summary","fields","indexes","codes"}(四段式,不再带 key/schema 引用外壳);
A3 fields 段非空(四段式产物必须自带列结构体,列集内联落地;旧「不得自带结构体」检查废除);
A4 fields[]/indexes[]/codes[] 逐条键集合 ⊆ spec 白名单
SPEC_FIELD_KEYS / SPEC_INDEX_KEYS / SPEC_CODE_KEYS禁止把私有元信息藏进标准结构体条目里
A5 summary[0].name 非空且 == 文件名去 .json表名与文件名一致旧 authority_module/
authority_path/table 必填检查随引用式口径一并废除);
A7 真源与镜像 sha256 两两相同(禁止内容级分叉)。
U 段(上游体检项,不计入本任务 RESULT但逐条打印 DEFECT 供冒泡留证):
A6 权威真源 modules/pbl_runtime_ext/models/*.json 自身对 spec 四段式白名单/抽象类型的符合性。
—— 该侧为 M11b-1 已批准产物,本任务按 PM 裁决「不得反向修改」,故只体检登记不判定。
信息性打印不计入判定summary 段条目上超出 SPEC_SUMMARY_KEYS 的表级元信息键
owner/milestone/decision_ref/decision_basis/structure_note/consumers/mirror_note 等)
只打印 info 提示——表级元信息允许登记在 summary 记录上,不作为失败判定。
"""
import hashlib
import json
import os
import sys
SOURCE_TABLES = [
"pbl_runtime_event.json",
"pbl_entity_state.json",
]
# 落点 1world_sync 真源modules 侧,唯一手改入口)
MODULE_SRC_DIR = "modules/world_sync/models"
# 落点 2应用打包镜像只由 sync_models_mirror.py 生成,禁止手工双写)
APP_MIRROR_DIR = os.path.join("apps", "scense", "pkgs", "world_sync", "models")
# 权威真源pbl_runtime_extM11b-1 已批准产物,本任务只读体检)
AUTHORITY_DIR = "modules/pbl_runtime_ext/models"
# database-table-definition-spec 四段式与各段键白名单
SPEC_ROOT_KEYS = {"summary", "fields", "indexes", "codes"}
SPEC_SUMMARY_KEYS = {"name", "title", "primary", "catelog"}
SPEC_FIELD_KEYS = {"name", "title", "type", "length", "dec", "nullable", "default"}
SPEC_INDEX_KEYS = {"name", "idxtype", "idxfields"}
SPEC_CODE_KEYS = {"field", "table", "valuefield", "textfield", "cond"}
ABSTRACT_TYPES = {
"str", "char", "short", "int", "long", "float", "double", "ddouble",
"decimal", "date", "time", "datetime", "timestamp", "text", "bin",
}
NEED_LENGTH = {"str", "char", "float", "double", "ddouble", "decimal"}
NEED_DEC = {"float", "double", "ddouble", "decimal"}
_failures = [] # 判定项失败
_upstream_defects = [] # 上游体检项缺陷(登记不判定)
_assert_count = 0
_upstream_count = 0
def check(cond, label):
global _assert_count
_assert_count += 1
if not cond:
_failures.append(label)
print(" FAIL %s" % label)
else:
print(" ok %s" % label)
return bool(cond)
def check_upstream(cond, label):
global _upstream_count
_upstream_count += 1
if not cond:
_upstream_defects.append(label)
print(" DEFECT %s" % label)
else:
print(" ok %s" % label)
return bool(cond)
def find_root():
here = os.path.dirname(os.path.abspath(__file__))
cur = here
for _ in range(10):
if os.path.isdir(os.path.join(cur, "modules")) and os.path.isdir(os.path.join(cur, "apps")):
return cur
parent = os.path.dirname(cur)
if parent == cur:
break
cur = parent
return os.path.abspath(os.path.join(here, os.pardir, os.pardir, os.pardir))
def sha256_of(path):
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def load_json(path):
with open(path, "r", encoding="utf-8") as fh:
return json.load(fh)
def as_list(val):
return val if isinstance(val, list) else []
def stem_of(path):
base = os.path.basename(path)
return base[:-5] if base.endswith(".json") else base
def validate_spec(path):
"""A1~A5world_sync 侧四段式表定义(真源与镜像执行同一套断言)。"""
print("== spec(inline four-section): %s" % path)
try:
data = load_json(path)
except Exception as exc: # noqa: BLE001
check(False, "A1 json-parseable (%s)" % exc)
return None
if not check(isinstance(data, dict), "A1 json-is-object"):
return None
# --- A2 根键必须恰好是四段式(引用式外壳 key/schema 视为不合规)---
check(set(data.keys()) == SPEC_ROOT_KEYS,
"A2 root-keys == {summary,fields,indexes,codes} (got %s)" % sorted(data.keys()))
summary = as_list(data.get("summary"))
fields = as_list(data.get("fields"))
indexes = as_list(data.get("indexes"))
codes = as_list(data.get("codes"))
check(len(summary) >= 1, "A2 summary non-empty array (got %d)" % len(summary))
check(isinstance(data.get("fields"), list), "A2 fields is array")
check(isinstance(data.get("indexes"), list), "A2 indexes is array")
check(isinstance(data.get("codes"), list), "A2 codes is array")
# --- A3 列结构体内联落地fields 必须非空(旧口径「引用体内不含结构体」检查已废除)---
if not check(len(fields) > 0, "A3 fields non-empty (inline column set, got %d)" % len(fields)):
return data
# --- A4 各段条目键集合 ⊆ spec 白名单 ---
for s_i, s_rec in enumerate(summary):
if not isinstance(s_rec, dict):
check(False, "A4 summary[%d] is object" % s_i)
continue
extra = set(s_rec.keys()) - SPEC_SUMMARY_KEYS
if extra:
print(" info summary[%d] table-level meta keys (not judged): %s" % (s_i, sorted(extra)))
field_names = []
for f in fields:
if not isinstance(f, dict):
check(False, "A4 field entry is object (%r)" % f)
continue
fname = str(f.get("name", "?"))
field_names.append(fname)
extra = set(f.keys()) - SPEC_FIELD_KEYS
check(not extra, "A4[%s] field keys subset whitelist (extra=%s)" % (fname, sorted(extra)))
check(fname != "?", "A4[%s] field.name present" % fname)
for idx in indexes:
if not isinstance(idx, dict):
check(False, "A4 index entry is object (%r)" % idx)
continue
iname = str(idx.get("name", "?"))
extra = set(idx.keys()) - SPEC_INDEX_KEYS
check(not extra, "A4[%s] index keys subset whitelist (extra=%s)" % (iname, sorted(extra)))
for code in codes:
if not isinstance(code, dict):
check(False, "A4 code entry is object (%r)" % code)
continue
extra = set(code.keys()) - SPEC_CODE_KEYS
check(not extra, "A4[code:%s] keys subset whitelist (extra=%s)" % (code.get("field", "?"), sorted(extra)))
# 列集自洽性(附加判定:内联后本文件自身必须可独立建表)
for idx in indexes:
if not isinstance(idx, dict):
continue
iname = str(idx.get("name", "?"))
for ifld in as_list(idx.get("idxfields")):
check(ifld in field_names, "A4[%s] idxfield present in fields: %s" % (iname, ifld))
check(idx.get("idxtype") in ("unique", "index"), "A4[%s] idxtype unique|index (%r)" % (iname, idx.get("idxtype")))
for f in fields:
if not isinstance(f, dict):
continue
fname = str(f.get("name", "?"))
t = f.get("type")
check(t in ABSTRACT_TYPES, "A3[%s] abstract type (%r)" % (fname, t))
if t in NEED_LENGTH:
check(isinstance(f.get("length"), int) and f.get("length") > 0, "A3[%s] positive int length" % fname)
if t in NEED_DEC:
check(isinstance(f.get("dec"), int) and f.get("dec") > 0, "A3[%s] positive int dec" % fname)
check(str(f.get("nullable", "no")) in ("yes", "no"), "A3[%s] nullable in yes|no" % fname)
# --- A5 表名一致性summary[0].name 非空且 == 文件名去 .json旧引用字段检查已废除---
expected_name = stem_of(path)
if not check(len(summary) >= 1 and isinstance(summary[0], dict), "A5 summary[0] is object"):
return data
s0_name = str(summary[0].get("name", "")).strip()
check(bool(s0_name), "A5 summary[0].name non-empty")
check(s0_name == expected_name,
"A5 summary[0].name == filename stem (%r vs %r)" % (s0_name, expected_name))
return data
def validate_authority(path):
"""A6上游体检pbl_runtime_ext 权威真源对 spec 的符合性,只登记不判定。"""
print("== authority(upstream audit): %s" % path)
if not os.path.isfile(path):
check_upstream(False, "A6 authority file present: %s" % path)
return
try:
data = load_json(path)
except Exception as exc: # noqa: BLE001
check_upstream(False, "A6 json-parseable (%s)" % exc)
return
check_upstream(isinstance(data, dict), "A6 root-is-object")
check_upstream(set(data.keys()) <= SPEC_ROOT_KEYS,
"A6 root-keys subset of four-section (got %s)" % sorted(data.keys()))
summary = data.get("summary") or []
check_upstream(len(summary) == 1, "A6 summary has exactly one record (got %d)" % len(summary))
if summary and isinstance(summary[0], dict):
s0 = summary[0]
check_upstream(set(s0.keys()) <= SPEC_SUMMARY_KEYS,
"A6 summary keys subset whitelist (got %s)" % sorted(s0.keys()))
check_upstream(bool(s0.get("name")), "A6 summary.name non-empty")
check_upstream(isinstance(s0.get("primary"), list) and len(s0.get("primary") or []) > 0,
"A6 summary.primary is non-empty array")
fields = data.get("fields") or []
check_upstream(len(fields) > 0, "A6 fields non-empty")
names = []
for f in fields:
if not isinstance(f, dict):
check_upstream(False, "A6 field-is-object")
continue
fname = f.get("name", "?")
names.append(fname)
check_upstream(set(f.keys()) <= SPEC_FIELD_KEYS,
"A6[%s] field keys subset whitelist (got %s)" % (fname, sorted(f.keys())))
t = f.get("type")
check_upstream(t in ABSTRACT_TYPES, "A6[%s] abstract type (%r)" % (fname, t))
if t in NEED_LENGTH:
check_upstream(isinstance(f.get("length"), int) and f.get("length") > 0,
"A6[%s] positive int length" % fname)
if t in NEED_DEC:
check_upstream(isinstance(f.get("dec"), int) and f.get("dec") > 0, "A6[%s] positive int dec" % fname)
check_upstream(str(f.get("nullable", "no")) in ("yes", "no"), "A6[%s] nullable in yes|no" % fname)
for idx in (data.get("indexes") or []):
if not isinstance(idx, dict):
check_upstream(False, "A6 index-is-object")
continue
iname = idx.get("name", "?")
check_upstream(set(idx.keys()) <= SPEC_INDEX_KEYS,
"A6[%s] index keys subset whitelist (got %s)" % (iname, sorted(idx.keys())))
check_upstream(isinstance(idx.get("idxfields"), list) and len(idx.get("idxfields") or []) > 0,
"A6[%s] idxfields non-empty array" % iname)
check_upstream(idx.get("idxtype") in ("unique", "index"), "A6[%s] idxtype unique|index" % iname)
for ifld in (idx.get("idxfields") or []):
check_upstream(ifld in names, "A6[%s] idxfield exists in fields: %s" % (iname, ifld))
for code in (data.get("codes") or []):
if not isinstance(code, dict):
check_upstream(False, "A6 code-is-object")
continue
check_upstream(set(code.keys()) <= SPEC_CODE_KEYS,
"A6 code keys subset whitelist (got %s)" % sorted(code.keys()))
if code.get("table") == "appcodes_kv":
check_upstream("parentid=" in str(code.get("cond", "")), "A6 appcodes_kv cond uses parentid=")
check_upstream("." not in str(code.get("table", "")), "A6 codes.table has no dot notation")
def main():
root = find_root()
print("workspace root: %s" % root)
paths = []
for name in SOURCE_TABLES:
paths.append(os.path.join(root, MODULE_SRC_DIR, name))
paths.append(os.path.join(root, APP_MIRROR_DIR, name))
print("\n--- A1~A5: 4 个落点全部执行同一套四段式 spec 断言 ---")
for p in paths:
if not os.path.isfile(p):
check(False, "file exists: %s" % p)
continue
validate_spec(p)
print("\n--- A7: 真源与镜像 sha256 两两相同 ---")
for name in SOURCE_TABLES:
src = os.path.join(root, MODULE_SRC_DIR, name)
dst = os.path.join(root, APP_MIRROR_DIR, name)
if os.path.isfile(src) and os.path.isfile(dst):
hs, hd = sha256_of(src), sha256_of(dst)
print(" %s\n src=%s\n dst=%s" % (name, hs, hd))
check(hs == hd, "A7 sha256 equal: %s" % name)
else:
check(False, "A7 both files present for sha256: %s" % name)
print("\n--- A6: 权威真源pbl_runtime_ext上游体检只登记不判定 ---")
for name in SOURCE_TABLES:
validate_authority(os.path.join(root, AUTHORITY_DIR, name))
print("\njudged assertions executed: %d" % _assert_count)
print("upstream audit checks executed: %d" % _upstream_count)
if _upstream_defects:
print("UPSTREAM DEFECTS (%d) -> 已登记,需冒泡 agent.pm / pbl_runtime_ext owner" % len(_upstream_defects))
for d in _upstream_defects:
print(" * %s" % d)
if _failures:
print("FAILURES (%d):" % len(_failures))
for f in _failures:
print(" - %s" % f)
print("RESULT: FAIL")
return 1
print("RESULT: PASS")
return 0
if __name__ == "__main__":
sys.exit(main())