pbl_runtime_ext/tests/test_m11b1a_ddl_runtime.py
2026-09-19 21:51:09 +08:00

248 lines
13 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-1a 运行时缺陷验证 harness响应 QC #1 % 转义 / QC #6 verify_db 判定 / QC #5 落点)。
被验对象scripts/pbl_runtime_event_ddl.py唯一被改文件
两层:
A. 离线层(无依赖,必跑)
A1 maintenance_sql() 渲染不再抛 ValueErrorQC #1 崩溃点)
A2 渲染正文里 DATE_FORMAT 参数为**单 %** 形式('%Y-%m-01' / '%Y%m'),且无 '%%Y' 残留
A3 append_only_sql()/full_script() 既有转义语义未改坏:'%(dbname)s'/'%(app_user)s'
被 replace 掉、host 通配符输出为单 % 'pbls'@'%'),无 '@'%%'' 残留
A4 DDL 未被改动create_table_sql() 的 sha256 == 修复前基线(验收标准 4
A5 源码无 `EXPLAIN PARTITIONS ` 废弃语法残留、无「查询结果未使用」的空转 for 循环
A6 verify_db 判定改用 information_schema.PARTITIONS(按 ORDINAL 排序) +
EXPLAIN FORMAT=JSON + PARTITION(p) 反查(静态断言三要素齐备)
A7 CLI 冒烟:--verify / --print / --print-maintain / --emit 全部 rc=0
B. 实库层(仅当 M11B1A_LOCAL_DB=1由 tests/run_m11b1a_localdb.sh 拉起一次性
MariaDB 后运行;**不会**连项目 test/prod 库,避免 DROP PARTITION 误伤真数据)
B1 用 mariadb CLI(DELIMITER) 装载 full_script(),核对表/分区/过程/触发器齐备
B2 --verify-db 全项 PASS分区数/兜底/跨月落不同分区/分区裁剪/UPDATE·DELETE 被拒/探针清理)
B3 CALL pbl_runtime_event_ensure_partitions(9) 真能 REORGANIZE 出新月分区A2 的单 % SQL 在
服务端可执行,不只是渲染正确)
B4 CALL pbl_runtime_event_prune_partitions(2) 真能 DROP 旧月分区且保留 pmax
用法:
python3 tests/test_m11b1a_ddl_runtime.py # 只跑 A 层
bash tests/run_m11b1a_localdb.sh # A + B 层(自带一次性库)
退出码0 = 无 FAIL1 = 有 FAIL2 = B 层被跳过且 A 层通过时仍返回 0B 缺失会显式打印 SKIP 行)。
"""
import hashlib
import importlib.util
import os
import re
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
MODULE_DIR = os.path.dirname(HERE) # modules/pbl_runtime_ext
SCRIPT = os.path.join(MODULE_DIR, "scripts", "pbl_runtime_event_ddl.py")
WORKSPACE = os.path.dirname(os.path.dirname(MODULE_DIR)) # 机构工作空间根
ENV_JSON = os.path.join(WORKSPACE, "projects", "pbls", "env", "test.json")
# 修复前git 049e09a`--print` 输出的 sha256 —— DDL 未改动的机械证据(验收标准 4
DDL_BASELINE_SHA256 = "30a83c8a9d729db987f82de1a5bc30229dc4a7b1cc9881c7a5a45673b1a62619"
RESULTS = []
def chk(name, passed, detail=""):
RESULTS.append((name, bool(passed), detail))
print(" %-4s %-46s %s" % ("PASS" if passed else "FAIL", name, detail))
return bool(passed)
def load_gen():
spec = importlib.util.spec_from_file_location("pbl_runtime_event_ddl", SCRIPT)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# ------------------------------------------------------------------ A. 离线层
def layer_a():
print("[A] 离线层(% 转义 + 判定逻辑静态核验)")
src = open(SCRIPT, encoding="utf-8").read()
# A1/A2 —— QC #1 崩溃点
try:
maint = subprocess.run([sys.executable, SCRIPT, "--print-maintain"],
capture_output=True, text=True)
rc_maint = maint.returncode
body = maint.stdout
except Exception as exc: # noqa: BLE001
rc_maint, body = 99, ""
chk("A1 maintenance_sql 渲染不抛异常", False, str(exc))
chk("A1 maintenance_sql 渲染不抛异常(QC#1)", rc_maint == 0,
"rc=%s, %d bytes修复前 rc=1 ValueError: unsupported format character 'Y'"
% (rc_maint, len(body)))
for token in ("pbl_runtime_event_ensure_partitions", "pbl_runtime_event_prune_partitions",
"ev_pbl_runtime_event_partition_maintain"):
chk("A2 维护段含完整对象 %s" % token, token in body, "%d" % body.count(token))
chk("A2 DATE_FORMAT 参数为单 %% 形式('%Y-%m-01')",
"DATE_FORMAT(CURDATE(), '%Y-%m-01')" in body and "DATE_FORMAT(month_start, '%Y%m')" in body,
"命中 %d 处单 %% 形式" % (body.count("'%Y-%m-01'") + body.count("'%Y%m'")))
chk("A2 无 '%%Y' 双写转义残留", "%%Y" not in body and "%%'" not in body,
"grep -c '%%Y' = %d" % body.count("%%Y"))
# A3 —— append_only 既有转义语义
full = subprocess.run([sys.executable, SCRIPT, "--print-maintain"], capture_output=True, text=True)
assert full.returncode == 0
script_txt = subprocess.run([sys.executable, "-c",
"import sys;sys.path.insert(0,%r);"
"import importlib.util as u;"
"s=u.spec_from_file_location('m',%r);m=u.module_from_spec(s);s.loader.exec_module(m);"
"print(m.full_script(),end='')" % (MODULE_DIR, SCRIPT)],
capture_output=True, text=True)
chk("A3 full_script() rc=0", script_txt.returncode == 0, "rc=%s" % script_txt.returncode)
fs = script_txt.stdout
chk("A3 %(dbname)s/%(app_user)s 已被替换", "%(dbname)s" not in fs and "%(app_user)s" not in fs,
"残留 %d" % (fs.count("%(dbname)s") + fs.count("%(app_user)s")))
chk("A3 REVOKE 主机通配符为单 %%'pbls'@'%'", "FROM 'pbls'@'%'" in fs and "@'%%'" not in fs,
"命中 'pbls'@'%%' = %d'@%%' = %d" % (fs.count("FROM 'pbls'@'%'"), fs.count("@'%%'")))
chk("A3 break-glass ctx 原样输出", "append_only_admin" in fs, "%d" % fs.count("append_only_admin"))
# A4 —— DDL 逐字未动
gen = load_gen()
digest = hashlib.sha256((gen.create_table_sql() + "\n").encode("utf-8")).hexdigest()
chk("A4 create_table_sql sha256 == 修复前基线", digest == DDL_BASELINE_SHA256, digest[:16] + "")
# A5/A6 —— 判定逻辑QC #6
chk("A5 源码无 'EXPLAIN PARTITIONS ' 废弃语法", "EXPLAIN PARTITIONS " not in src,
"grep 命中 %d" % src.count("EXPLAIN PARTITIONS "))
chk("A5 无未使用查询结果的空转 for 循环",
not re.search(r"for\s+k\s+in\s*\(", src) and "row[3]" not in src,
"row[3] 命中 %dfor k in 命中 %d" % (src.count("row[3]"), len(re.findall(r"for\s+k\s+in\s*\(", src))))
vsrc = src[src.index("def verify_db("):]
chk("A6 分区清单来自 information_schema.PARTITIONS+ORDINAL",
"information_schema.PARTITIONS" in vsrc and "PARTITION_ORDINAL_POSITION" in vsrc, "OK")
chk("A6 落分区判定用 PARTITION(p) 反查 + EXPLAIN FORMAT=JSON",
"_row_in_partition" in src and "PARTITION (`%s`)" in src
and "EXPLAIN FORMAT=JSON" in src and "_locate_row_partition" in vsrc, "OK")
chk("A6 探针清理 break-glass 逻辑保留",
"SET @pbl_guard_ctx=" in vsrc and "GUARD_ADMIN_CTX" in vsrc
and "SET @pbl_guard_ctx=NULL" in vsrc,
"guard_ctx 设置 %d 处 / 复位 %d"
% (vsrc.count("SET @pbl_guard_ctx="), vsrc.count("SET @pbl_guard_ctx=NULL")))
# A7 —— CLI 冒烟
for args in (["--verify"], ["--print"], ["--print-guard"], ["--print-maintain"]):
r = subprocess.run([sys.executable, SCRIPT] + args, capture_output=True, text=True)
tail = (r.stdout.strip().splitlines() or [""])[-1][:40]
chk("A7 CLI %s rc=0" % " ".join(args), r.returncode == 0, "末行: %s" % tail)
tmp_emit = os.path.join(HERE, "_emit_check")
r = subprocess.run([sys.executable, SCRIPT, "--emit", tmp_emit, "--dbname", "pbls"],
capture_output=True, text=True)
files = sorted(os.listdir(tmp_emit)) if os.path.isdir(tmp_emit) else []
chk("A7 CLI --emit rc=0", r.returncode == 0 and len(files) >= 3,
"%d 文件: %s" % (len(files), ",".join(files)))
# 生成的维护 SQL 同样必须是单 % 形式emit 路径回归)
m3 = os.path.join(tmp_emit, "20260919_m11b_03_pbl_runtime_event_partition_maintenance.sql")
if os.path.exists(m3):
t = open(m3, encoding="utf-8").read()
chk("A7 --emit 维护段 DATE_FORMAT 单 %", "DATE_FORMAT(CURDATE(), '%Y-%m-01')" in t and "%%Y" not in t,
"%d bytes" % len(t))
subprocess.run(["rm", "-rf", tmp_emit])
return gen
# ------------------------------------------------------------------ B. 实库层
def layer_b(gen):
print("\n[B] 实库层(一次性本地 MariaDB不触碰项目 test/prod 库)")
if os.environ.get("M11B1A_LOCAL_DB") != "1":
print(" SKIP B 层:未设 M11B1A_LOCAL_DB=1用 bash tests/run_m11b1a_localdb.sh 跑全量)")
return True
import json
import pymysql
conf = json.load(open(ENV_JSON, encoding="utf-8"))["db"]
db = dict(conf)
db["host"] = os.environ.get("M11B1A_DB_HOST", "127.0.0.1")
db["port"] = int(os.environ.get("M11B1A_DB_PORT", "13306"))
os.environ["PBL_DDL_DB_HOST"] = db["host"]
os.environ["PBL_DDL_DB_PORT"] = str(db["port"])
conn0 = pymysql.connect(host=db["host"], port=db["port"], user=db["user"],
password=db["password"], database=db["dbname"],
charset="utf8mb4", autocommit=True)
cur0 = conn0.cursor()
cur0.execute("SELECT COUNT(*) FROM information_schema.PARTITIONS "
"WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s "
"AND PARTITION_NAME IS NOT NULL", (gen.TABLE,))
n0 = cur0.fetchone()[0]
cur0.execute("SELECT COUNT(*) FROM information_schema.ROUTINES "
"WHERE ROUTINE_SCHEMA=DATABASE() AND ROUTINE_NAME LIKE 'pbl_runtime_event%%'")
n_proc = cur0.fetchone()[0]
cur0.execute("SELECT COUNT(*) FROM information_schema.TRIGGERS "
"WHERE EVENT_OBJECT_TABLE=%s", (gen.TABLE,))
n_trg = cur0.fetchone()[0]
cur0.close(); conn0.close()
chk("B1 CLI(mariadb+DELIMITER) 装载 full_script() 后对象齐备",
n0 >= gen.MONTHS_PRECREATE + 1 and n_proc >= 2 and n_trg >= 2,
"分区 %d 个 / 维护过程 %d 个 / 触发器 %d" % (n0, n_proc, n_trg))
rc2 = gen.verify_db()
chk("B2 verify_db() rc=0 全项 PASS(QC#6)", rc2 == 0, "verify-db rc=%s" % rc2)
conn = pymysql.connect(host=db["host"], port=db["port"], user=db["user"],
password=db["password"], database=db["dbname"],
charset="utf8mb4", autocommit=True)
cur = conn.cursor()
def parts():
cur.execute("SELECT PARTITION_NAME FROM information_schema.PARTITIONS "
"WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s "
"AND PARTITION_NAME IS NOT NULL ORDER BY PARTITION_ORDINAL_POSITION", (gen.TABLE,))
return [r[0] for r in cur.fetchall()]
before = parts()
cur.execute("CALL pbl_runtime_event_ensure_partitions(9)")
after = parts()
new = [p for p in after if p not in before]
chk("B3 CALL ensure_partitions(9) 补建出新月分区", len(new) >= 3 and after.count("pmax") == 1,
"新增 %s(分区数 %d%d" % (",".join(new), len(before), len(after)))
cur.execute("CALL pbl_runtime_event_prune_partitions(2)")
pruned = parts()
dropped = [p for p in after if p not in pruned]
chk("B4 CALL prune_partitions(2) DROP 旧月分区且保留 pmax",
len(dropped) >= 1 and "pmax" in pruned,
"DROP %s(剩余 %d 个)" % (",".join(dropped), len(pruned)))
# 分区裁剪 + 落分区判定的服务端可执行性EXPLAIN FORMAT=JSON 解析)
planned = gen._explain_partitions(cur, "SELECT id FROM `%s` WHERE created_at>='2026-01-01'"
" AND created_at<'2026-02-01'" % gen.TABLE)
chk("B5 _explain_partitions 解析出分区清单", bool(planned) and len(planned) < len(pruned),
"命中 %s(表内共 %d 分区)" % (",".join(planned), len(pruned)))
cur.execute("INSERT INTO `%s` (tenant_id,event_uid,event_type,scene_id,seq,created_at)"
" VALUES ('t_harness',%%s,'probe',0,%%s,%%s)" % gen.TABLE,
("h_a", 1, "2026-01-05 00:00:00"))
located = gen._locate_row_partition(cur, "h_a", "2026-01-05 00:00:00", pruned)
chk("B6 _locate_row_partition 定位到 information_schema 清单内的分区",
located in pruned, "h_a -> %s(清单 %s" % (located, ",".join(pruned)))
cur.execute("SET @pbl_guard_ctx='%s'" % gen.GUARD_ADMIN_CTX)
cur.execute("DELETE FROM `%s` WHERE tenant_id='t_harness'" % gen.TABLE)
cur.execute("SET @pbl_guard_ctx=NULL")
cur.close(); conn.close()
return True
def main():
print("harness: %s" % SCRIPT)
gen = layer_a()
ok_b = layer_b(gen)
fails = [n for n, p, _ in RESULTS if not p]
print("\n[汇总] 共 %dPASS %dFAIL %d%s"
% (len(RESULTS), len(RESULTS) - len(fails), len(fails),
"" if ok_b else "B 层 SKIP"))
if fails:
print("FAIL 明细: %s" % " | ".join(fails))
print("HARNESS FAIL")
return 1
print("HARNESS PASS")
return 0
if __name__ == "__main__":
sys.exit(main())