466 lines
20 KiB
Python
466 lines
20 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""M1b 内容级自检工具(QC 取证用)。
|
||
|
||
用途:把 QC 退回意见 #3/#4/#6 要求的「内容级核查」变成可重复执行、可留证据的
|
||
机械检查。运行:
|
||
|
||
cd modules/pbl_blueprint && python3 tools/m1b_selfcheck.py
|
||
|
||
退出码 0 = 全部通过;非 0 = 有 FAIL 项(打印明细)。
|
||
|
||
检查项:
|
||
A. DDL 合规(sql/m1b_ddl.sql)
|
||
A1 恰好 4 条 CREATE TABLE IF NOT EXISTS,且为 4 张目标表
|
||
A2 零 FOREIGN KEY
|
||
A3 零 PostgreSQL 方言(BIGSERIAL / nextval / SERIAL)
|
||
A4 零对既有基表的 DDL/DML 动词(ALTER/UPDATE/DELETE/DROP/TRUNCATE/INSERT)
|
||
A5 每个 UNIQUE KEY 首列为 tenant_key(规避 NULL 不去重)
|
||
A6 mariadb 方言要素齐备(InnoDB + utf8mb4 + AUTO_INCREMENT)
|
||
B. 表模型合规(models/m1b/*.json)
|
||
B1 4 个文件均可 json.load
|
||
B2 四段式:summary(数组,含 primary) / fields(数组) / indexes(数组) / codes(数组)
|
||
B3 models 与 DDL 表名一一对应(双向)
|
||
B4 models 字段集合与 DDL 列集合一一对应(双向,逐表)
|
||
B5 models 索引集合与 DDL 索引集合一一对应(逐表,含 unique 标记)
|
||
C. CRUD 定义合规(json/m1b/*.json)
|
||
C1 4 个文件均可 json.load
|
||
C2 含 tblname 且指向本模块 4 表之一
|
||
C3 含 params.editable(QC#3 指出的格式偏差)
|
||
C4 new/update/delete_data_url 在 params 顶层(非仅嵌套在 editable 内)
|
||
C5 json/ 下不含表定义特征键(summary/fields/indexes)——即 json/ 只放 CRUD
|
||
D. Python 源码内容级核查(pbl_blueprint/m1b_*.py)
|
||
D1 零硬编码库名(无 DBNAME = 'xxx' / dbname = 'xxx' 字面量赋值)
|
||
D2 取库名走 get_module_dbname
|
||
D3 只用 sqlor 标准 API(sor.C/U/D/R/I/sqlExe/sqlPaging),无编造 API
|
||
D4 全部 py_compile 通过
|
||
D5 每个 .py 有效语句数 > 0(非空壳)
|
||
E. 三处同步注册核验(QC#4)
|
||
E1 m1b_api.py 定义 register_m1b_routes
|
||
E2 __init__.py 导出 register_m1b_routes(import 行)
|
||
E3 init.py 在 load_pbl_blueprint 内引用 register_m1b_routes(env 注册/调用)
|
||
E4 M1b 每个对外函数在三处齐备(实现 / __init__ 导出 / init.py 注册)
|
||
F. M1a 既有代码零越权改动(QC#6)
|
||
F1 git diff 中 M1a 文件(非 m1b_* / 非 models|json|sql|tests|docs/tools 的
|
||
既有 .py)只允许「追加式」改动:新增行不得删除既有 M1a 函数定义行
|
||
"""
|
||
|
||
import ast
|
||
import json
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
|
||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
M1B_TABLES = [
|
||
"pbl_blueprint_template",
|
||
"pbl_blueprint_ref",
|
||
"pbl_subobject_ext",
|
||
"pbl_ext_field_def",
|
||
]
|
||
|
||
DDL_REL = os.path.join("sql", "m1b_ddl.sql")
|
||
MODELS_REL = os.path.join("pbl_blueprint", "models", "m1b")
|
||
JSON_REL = os.path.join("pbl_blueprint", "json", "m1b")
|
||
|
||
# 编造的 sqlor API(module-development-spec 明确:只有 C/U/D/R/I/sqlExe)
|
||
FAKE_SQLOR = [
|
||
"sqlor.save", "sor.save", "sqlor.list", "sor.list", "sqlor.one", "sor.one",
|
||
"sqlor.delete", "sor.delete", "sqlor.insert", "sor.insert", "sqlor.query",
|
||
"sor.query", "sqlor.update", "sor.update", "sqlor.create", "sor.create",
|
||
"sqlor.select", "sor.select", "sqlor.find", "sor.find", "sqlor.get",
|
||
"sor.get_one", "sor.fetch", "sqlor.fetch",
|
||
]
|
||
|
||
# 允许的 sqlor 调用(白名单,用于 D3 正向取证)
|
||
OK_SQLOR = re.compile(r"\bsor\.(C|U|D|R|I|sqlExe|sqlPaging|sqlorContext)\s*\(")
|
||
|
||
results = []
|
||
|
||
|
||
def record(check_id, ok, detail):
|
||
results.append((check_id, bool(ok), detail))
|
||
flag = "PASS" if ok else "FAIL"
|
||
print("[%s] %s :: %s" % (flag, check_id, detail))
|
||
|
||
|
||
def p(*parts):
|
||
return os.path.join(REPO, *parts)
|
||
|
||
|
||
def read(rel):
|
||
with open(p(rel), "r", encoding="utf-8") as f:
|
||
return f.read()
|
||
|
||
|
||
def strip_sql_comments(sql):
|
||
"""去掉 -- 行注释与 /* */ 块注释,避免注释文本干扰动词扫描。"""
|
||
sql = re.sub(r"/\*.*?\*/", " ", sql, flags=re.S)
|
||
out = []
|
||
for line in sql.splitlines():
|
||
idx = line.find("--")
|
||
if idx >= 0:
|
||
line = line[:idx]
|
||
out.append(line)
|
||
return "\n".join(out)
|
||
|
||
|
||
# ---------------------------------------------------------------- A. DDL
|
||
def check_ddl():
|
||
if not os.path.exists(p(DDL_REL)):
|
||
record("A0", False, "sql/m1b_ddl.sql 不存在")
|
||
return None
|
||
raw = read(DDL_REL)
|
||
body = strip_sql_comments(raw)
|
||
|
||
creates = re.findall(
|
||
r"CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+`?(\w+)`?", body, flags=re.I)
|
||
record("A1", len(creates) == 4 and sorted(creates) == sorted(M1B_TABLES),
|
||
"CREATE TABLE IF NOT EXISTS 共 %d 条,表=%s" % (len(creates), creates))
|
||
|
||
fk = len(re.findall(r"FOREIGN\s+KEY", body, flags=re.I))
|
||
record("A2", fk == 0, "FOREIGN KEY 出现 %d 次(应为 0)" % fk)
|
||
|
||
pg = re.findall(r"\b(BIGSERIAL|SERIAL|nextval)\b", body, flags=re.I)
|
||
record("A3", not pg, "PostgreSQL 方言命中 %s(应为空)" % (pg or "无"))
|
||
|
||
# A4:任何针对既有基表的 DDL/DML 动词;同时全文(去注释)不得出现这些动词
|
||
verbs = re.findall(
|
||
r"\b(ALTER|UPDATE|DELETE|DROP|TRUNCATE|INSERT|REPLACE)\b", body, flags=re.I)
|
||
base_hits = re.findall(
|
||
r"\b(CREATE|ALTER|UPDATE|DELETE|DROP|TRUNCATE|INSERT)\b[^;]{0,120}?"
|
||
r"\b(world|scene|entity|script|world_snapshot|scense)\b",
|
||
body, flags=re.I)
|
||
record("A4", not verbs and not base_hits,
|
||
"去注释后 DDL/DML 动词命中 %s;基表语句命中 %s(均应为空)"
|
||
% (sorted(set(v.upper() for v in verbs)) or "无", base_hits or "无"))
|
||
|
||
# A5:每个 UNIQUE KEY 首列必须是 tenant_key
|
||
bad_uk = []
|
||
uks = re.findall(r"UNIQUE\s+KEY\s+`?(\w+)`?\s*\(([^)]*)\)", body, flags=re.I)
|
||
for name, cols in uks:
|
||
first = cols.split(",")[0].strip().strip("`").strip()
|
||
if first != "tenant_key":
|
||
bad_uk.append("%s(首列=%s)" % (name, first))
|
||
record("A5", uks and not bad_uk,
|
||
"UNIQUE KEY 共 %d 个,首列非 tenant_key 的:%s" % (len(uks), bad_uk or "无"))
|
||
|
||
need = ["ENGINE=InnoDB", "utf8mb4", "AUTO_INCREMENT"]
|
||
miss = [n for n in need if n.lower() not in body.lower()]
|
||
record("A6", not miss, "mariadb 方言要素缺失 %s(应为空)" % (miss or "无"))
|
||
|
||
return body
|
||
|
||
|
||
# ------------------------------------------------------- B. models 四段式
|
||
def parse_ddl_tables(body):
|
||
"""从 DDL 正文解析 {table: {"cols": [...], "indexes": {name: (cols, unique)}}}"""
|
||
tables = {}
|
||
for m in re.finditer(
|
||
r"CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+`?(\w+)`?\s*\((.*?)\n\)\s*ENGINE",
|
||
body, flags=re.I | re.S):
|
||
tname, inner = m.group(1), m.group(2)
|
||
cols, idx = [], {}
|
||
for line in inner.splitlines():
|
||
s = line.strip().rstrip(",").strip()
|
||
if not s:
|
||
continue
|
||
up = s.upper()
|
||
if up.startswith("PRIMARY KEY"):
|
||
continue
|
||
if up.startswith("UNIQUE KEY") or up.startswith("KEY ") or up.startswith("INDEX "):
|
||
nm = re.match(r"(?:UNIQUE\s+)?(?:KEY|INDEX)\s+`?(\w+)`?\s*\(([^)]*)\)",
|
||
s, flags=re.I)
|
||
if nm:
|
||
cs = [c.strip().strip("`") for c in nm.group(2).split(",")]
|
||
idx[nm.group(1)] = (cs, up.startswith("UNIQUE"))
|
||
continue
|
||
cm = re.match(r"`(\w+)`", s)
|
||
if cm:
|
||
cols.append(cm.group(1))
|
||
tables[tname] = {"cols": cols, "indexes": idx}
|
||
return tables
|
||
|
||
|
||
def check_models(ddl_tables):
|
||
mdir = p(MODELS_REL)
|
||
if not os.path.isdir(mdir):
|
||
record("B1", False, "models/m1b/ 目录不存在")
|
||
return
|
||
files = sorted(f for f in os.listdir(mdir) if f.endswith(".json"))
|
||
record("B1", len(files) == 4, "models/m1b/ JSON 文件 %d 个:%s" % (len(files), files))
|
||
|
||
model_tables = {}
|
||
four_ok, four_bad = True, []
|
||
for fn in files:
|
||
try:
|
||
with open(os.path.join(mdir, fn), "r", encoding="utf-8") as f:
|
||
d = json.load(f)
|
||
except Exception as e:
|
||
four_ok = False
|
||
four_bad.append("%s: json.load 失败 %s" % (fn, e))
|
||
continue
|
||
for seg in ("summary", "fields", "indexes", "codes"):
|
||
if not isinstance(d.get(seg), list):
|
||
four_ok = False
|
||
four_bad.append("%s: 缺 %s 数组" % (fn, seg))
|
||
sm = d.get("summary") or []
|
||
if not (sm and isinstance(sm[0], dict) and sm[0].get("primary")):
|
||
four_ok = False
|
||
four_bad.append("%s: summary[0].primary 缺失" % fn)
|
||
if sm:
|
||
model_tables[sm[0].get("table")] = d
|
||
record("B2", four_ok, "四段式(summary/fields/indexes/codes) 违规:%s" % (four_bad or "无"))
|
||
|
||
only_model = sorted(set(model_tables) - set(ddl_tables))
|
||
only_ddl = sorted(set(ddl_tables) - set(model_tables))
|
||
record("B3", not only_model and not only_ddl,
|
||
"models↔DDL 表名对应:仅 models 有 %s,仅 DDL 有 %s(均应为空)"
|
||
% (only_model or "无", only_ddl or "无"))
|
||
|
||
col_bad, idx_bad = [], []
|
||
for t, d in model_tables.items():
|
||
if t not in ddl_tables:
|
||
continue
|
||
mcols = [f.get("name") for f in d.get("fields", [])]
|
||
dcols = ddl_tables[t]["cols"]
|
||
if sorted(mcols) != sorted(dcols):
|
||
col_bad.append("%s: 仅models=%s 仅DDL=%s"
|
||
% (t, sorted(set(mcols) - set(dcols)),
|
||
sorted(set(dcols) - set(mcols))))
|
||
midx = {}
|
||
for ix in d.get("indexes", []):
|
||
midx[ix.get("name")] = (ix.get("fields"), bool(ix.get("unique")))
|
||
didx = ddl_tables[t]["indexes"]
|
||
if sorted(midx) != sorted(didx):
|
||
idx_bad.append("%s: 索引名不一致 仅models=%s 仅DDL=%s"
|
||
% (t, sorted(set(midx) - set(didx)),
|
||
sorted(set(didx) - set(midx))))
|
||
else:
|
||
for k in midx:
|
||
if list(midx[k][0]) != list(didx[k][0]) or midx[k][1] != didx[k][1]:
|
||
idx_bad.append("%s.%s: models=%s DDL=%s" % (t, k, midx[k], didx[k]))
|
||
record("B4", not col_bad, "models↔DDL 列一一对应违规:%s" % (col_bad or "无"))
|
||
record("B5", not idx_bad, "models↔DDL 索引一一对应违规:%s" % (idx_bad or "无"))
|
||
|
||
|
||
# ---------------------------------------------------------- C. json CRUD
|
||
def check_crud():
|
||
jdir = p(JSON_REL)
|
||
if not os.path.isdir(jdir):
|
||
record("C1", False, "json/m1b/ 目录不存在")
|
||
return
|
||
files = sorted(f for f in os.listdir(jdir) if f.endswith(".json"))
|
||
record("C1", len(files) == 4, "json/m1b/ JSON 文件 %d 个:%s" % (len(files), files))
|
||
|
||
bad_tbl, bad_edit, bad_url, bad_model = [], [], [], []
|
||
for fn in files:
|
||
try:
|
||
with open(os.path.join(jdir, fn), "r", encoding="utf-8") as f:
|
||
d = json.load(f)
|
||
except Exception as e:
|
||
bad_tbl.append("%s: json.load 失败 %s" % (fn, e))
|
||
continue
|
||
if d.get("tblname") not in M1B_TABLES:
|
||
bad_tbl.append("%s: tblname=%r" % (fn, d.get("tblname")))
|
||
prm = d.get("params") or {}
|
||
if not isinstance(prm.get("editable"), dict):
|
||
bad_edit.append("%s: 缺 params.editable" % fn)
|
||
for k in ("new_data_url", "update_data_url", "delete_data_url"):
|
||
if not prm.get(k):
|
||
bad_url.append("%s: params 顶层缺 %s" % (fn, k))
|
||
for seg in ("summary", "fields", "indexes"):
|
||
if seg in d:
|
||
bad_model.append("%s: 含表定义段 %s(json/ 只放 CRUD)" % (fn, seg))
|
||
record("C2", not bad_tbl, "tblname 违规:%s" % (bad_tbl or "无"))
|
||
record("C3", not bad_edit, "params.editable 违规:%s" % (bad_edit or "无"))
|
||
record("C4", not bad_url, "params 顶层 *_data_url 违规:%s" % (bad_url or "无"))
|
||
record("C5", not bad_model, "json/ 混入表定义:%s" % (bad_model or "无"))
|
||
|
||
|
||
# ------------------------------------------------------- D. Python 源码
|
||
def m1b_py_files():
|
||
pkg = p("pbl_blueprint")
|
||
return sorted(f for f in os.listdir(pkg)
|
||
if f.startswith("m1b_") and f.endswith(".py"))
|
||
|
||
|
||
def check_python():
|
||
files = m1b_py_files()
|
||
if not files:
|
||
record("D1", False, "未找到 pbl_blueprint/m1b_*.py")
|
||
return {}
|
||
|
||
hard, no_dbname, fake, compile_bad, empty = [], [], [], [], []
|
||
funcs = {}
|
||
hard_re = re.compile(
|
||
r"""^\s*(?:DBNAME|dbname|db_name|DATABASE|database)\s*=\s*['"][^'"]+['"]""",
|
||
re.M)
|
||
for fn in files:
|
||
src = read(os.path.join("pbl_blueprint", fn))
|
||
if hard_re.search(src):
|
||
hard.append(fn)
|
||
if "get_module_dbname" not in src and "dbname" in src.lower():
|
||
no_dbname.append(fn)
|
||
for api in FAKE_SQLOR:
|
||
if api in src:
|
||
fake.append("%s: %s" % (fn, api))
|
||
try:
|
||
compile(src, fn, "exec")
|
||
except SyntaxError as e:
|
||
compile_bad.append("%s: %s" % (fn, e))
|
||
try:
|
||
tree = ast.parse(src)
|
||
except SyntaxError:
|
||
continue
|
||
stmts = [n for n in ast.walk(tree)
|
||
if isinstance(n, ast.stmt) and not isinstance(n, (ast.Pass,))]
|
||
if len(stmts) < 3:
|
||
empty.append("%s: 有效语句 %d" % (fn, len(stmts)))
|
||
for n in tree.body:
|
||
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||
funcs.setdefault(n.name, []).append(fn)
|
||
|
||
record("D1", not hard, "硬编码库名文件:%s(应为空)" % (hard or "无"))
|
||
record("D2", not no_dbname, "未走 get_module_dbname 的文件:%s" % (no_dbname or "无"))
|
||
record("D3", not fake, "编造 sqlor API:%s(应为空;白名单 sor.C/U/D/R/I/sqlExe/sqlPaging)"
|
||
% (fake or "无"))
|
||
record("D4", not compile_bad, "py_compile 失败:%s" % (compile_bad or "无"))
|
||
record("D5", not empty, "疑似空壳文件:%s" % (empty or "无"))
|
||
return funcs
|
||
|
||
|
||
# --------------------------------------------------- E. 三处同步注册
|
||
def check_registration(funcs):
|
||
api = read(os.path.join("pbl_blueprint", "m1b_api.py")) if os.path.exists(
|
||
p("pbl_blueprint", "m1b_api.py")) else ""
|
||
initpkg = read(os.path.join("pbl_blueprint", "__init__.py"))
|
||
initpy = read(os.path.join("pbl_blueprint", "init.py"))
|
||
|
||
record("E1", "def register_m1b_routes" in api,
|
||
"m1b_api.py 定义 register_m1b_routes:%s"
|
||
% ("是" if "def register_m1b_routes" in api else "否"))
|
||
record("E2", "register_m1b_routes" in initpkg,
|
||
"__init__.py 导出 register_m1b_routes:%s"
|
||
% ("是" if "register_m1b_routes" in initpkg else "否"))
|
||
record("E3", "register_m1b_routes" in initpy,
|
||
"init.py 引用/注册 register_m1b_routes:%s"
|
||
% ("是" if "register_m1b_routes" in initpy else "否"))
|
||
|
||
# E4:M1b 对外函数(m1b_*.py 顶层定义)三处齐备
|
||
# ① 实现:m1b_*.py 中 def/async def
|
||
# ② 导出:__init__.py 出现该名(from .m1b_x import ... 或 __all__)
|
||
# ③ 注册:init.py 出现该名(import + env.xxx= 赋值 或 直接调用)
|
||
PUBLIC_PREFIX = ("m1b_", "init_m1b", "register_m1b", "ensure_m1b",
|
||
"load_m1b", "seed_platform", "ensure_platform")
|
||
public = sorted(n for n in funcs if n.startswith(PUBLIC_PREFIX))
|
||
miss_exp = [n for n in public if not re.search(r"\b%s\b" % re.escape(n), initpkg)]
|
||
miss_reg = [n for n in public if not re.search(r"\b%s\b" % re.escape(n), initpy)]
|
||
# 注册形态取证:env.xxx = 赋值 或 函数调用 或 import 行
|
||
reg_form = {}
|
||
for n in public:
|
||
forms = []
|
||
if re.search(r"env\.%s\s*=" % re.escape(n), initpy):
|
||
forms.append("env赋值")
|
||
if re.search(r'["\']%s["\']' % re.escape(n), initpy) and \
|
||
re.search(r"M1B_ENV_EXPORTS|setattr\(env", initpy):
|
||
forms.append("M1B_ENV_EXPORTS声明式env注册")
|
||
if re.search(r"\b%s\s*\(" % re.escape(n), initpy):
|
||
forms.append("调用")
|
||
if re.search(r"^\s*from\s+\.m1b_\w*\s+import[^\n]*\b%s\b" % re.escape(n),
|
||
initpy, re.M) or re.search(r"^\s*from\s+\.\s+import[^\n]*\b%s\b"
|
||
% re.escape(n), initpy, re.M):
|
||
forms.append("import")
|
||
reg_form[n] = forms or ["未注册"]
|
||
record("E4", not miss_exp and not miss_reg,
|
||
"M1b 对外函数 %d 个;__init__ 未导出 %s;init.py 未注册 %s"
|
||
% (len(public), miss_exp or "无", miss_reg or "无"))
|
||
for n in public:
|
||
record("E4.%s" % n, reg_form[n] != ["未注册"],
|
||
"init.py 注册形态=%s" % "/".join(reg_form[n]))
|
||
|
||
|
||
def check_m1a_untouched():
|
||
"""F1/F2:M1b 改动不得越权破坏 M1a 既有代码。
|
||
|
||
界定:
|
||
* M1a 业务文件 = pbl_blueprint/ 下非 m1b_* 的 .py,且排除共享入口
|
||
__init__.py / init.py(这两个文件是 module-development-spec 规定的
|
||
「三处同步」注册点,M1b 必须在此追加导出/注册,属合规追加而非越权)。
|
||
* 判定:M1a 业务文件不得被修改/删除(git diff 无 M/D);
|
||
共享入口只允许「追加式」改动——既有 def 行零删除。
|
||
"""
|
||
SHARED = ("__init__.py", "init.py")
|
||
try:
|
||
out = subprocess.run(["git", "diff", "--name-status", "HEAD", "--", "pbl_blueprint/"],
|
||
cwd=REPO, capture_output=True, text=True, timeout=30)
|
||
lines = [l for l in out.stdout.splitlines() if l.strip()]
|
||
except Exception as e:
|
||
lines = []
|
||
print(" (git diff 不可用:%s)" % e)
|
||
|
||
m1a_biz_bad, shared_del = [], []
|
||
for l in lines:
|
||
parts = l.split("\t")
|
||
if len(parts) < 2:
|
||
continue
|
||
st, path = parts[0], parts[-1]
|
||
base = os.path.basename(path)
|
||
if base.startswith("m1b_") or "/m1b/" in path or not base.endswith(".py"):
|
||
continue
|
||
if base in SHARED:
|
||
# 共享入口:只查「既有 def 行是否被删除」
|
||
try:
|
||
d = subprocess.run(["git", "diff", "-U0", "HEAD", "--", path],
|
||
cwd=REPO, capture_output=True, text=True, timeout=30)
|
||
removed_defs = [x for x in d.stdout.splitlines()
|
||
if x.startswith("-") and not x.startswith("---")
|
||
and re.match(r"-\s*(async\s+)?def\s+\w+", x)]
|
||
if removed_defs:
|
||
shared_del.append("%s 删除既有函数 %d 个: %s"
|
||
% (base, len(removed_defs), removed_defs[:3]))
|
||
except Exception:
|
||
pass
|
||
continue
|
||
if st.startswith("M") or st.startswith("D"):
|
||
m1a_biz_bad.append("%s %s" % (st, path))
|
||
|
||
# F2 静态核验:M1a 关键入口与 11 表定义仍在
|
||
initpy = read(os.path.join("pbl_blueprint", "init.py"))
|
||
initpkg = read(os.path.join("pbl_blueprint", "__init__.py"))
|
||
m1a_ok = ("load_pbl_blueprint" in initpy and "load_pbl_blueprint" in initpkg
|
||
and "TABLE_NAMES" in initpy)
|
||
record("F1", not m1a_biz_bad and not shared_del and m1a_ok,
|
||
"M1a 业务文件被改/删:%s;共享入口删除既有函数:%s;"
|
||
"load_pbl_blueprint+TABLE_NAMES 完好:%s"
|
||
% (m1a_biz_bad or "无", shared_del or "无", m1a_ok))
|
||
|
||
|
||
def main():
|
||
print("=" * 72)
|
||
print("M1b 内容级自检 repo=%s" % REPO)
|
||
print("=" * 72)
|
||
body = check_ddl()
|
||
ddl_tables = parse_ddl_tables(body) if body else {}
|
||
check_models(ddl_tables)
|
||
check_crud()
|
||
funcs = check_python()
|
||
check_registration(funcs)
|
||
check_m1a_untouched()
|
||
|
||
total = len(results)
|
||
passed = sum(1 for _, ok, _ in results if ok)
|
||
print("-" * 72)
|
||
print("结果:%d/%d PASS" % (passed, total))
|
||
for cid, ok, detail in results:
|
||
if not ok:
|
||
print(" FAIL %s :: %s" % (cid, detail))
|
||
print("-" * 72)
|
||
return 0 if passed == total else 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|