pbl_blueprint/tests/test_m1b_import_closure.py
2026-09-17 15:16:08 +08:00

225 lines
9.7 KiB
Python
Raw Permalink 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 -*-
"""M1b import 闭包测试QC 退回意见 #1/#2/#3/#4 的验收断言)。
真实执行三类核验:
1. 静态闭包tools/m1b_fix_import_closure.py 扫描 modules/pbl_* 全部 .py
跨文件符号引用断裂数必须为 0
2. 动态导入pbl_blueprint.m1b 全子模块 + pbl_blueprint.api +
pbl_agent_runtime.api 必须可 import不 ImportError
3. 符号存在性QC 点名的每个符号在对应模块上真实可取到且可调用。
运行:
python3 modules/pbl_blueprint/tests/test_m1b_import_closure.py -v
"""
import importlib
import os
import sys
# --- M1b sys.path bootstrap: modules/ 下各包互为兄弟仓库,需逐个入 path ---
_M1B_MOD_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
_M1B_MODULES_DIR = os.path.abspath(os.path.join(_M1B_MOD_ROOT, ".."))
_M1B_CANDIDATES = [_M1B_MOD_ROOT, _M1B_MODULES_DIR]
try:
for _d in sorted(os.listdir(_M1B_MODULES_DIR)):
_sub = os.path.join(_M1B_MODULES_DIR, _d)
if os.path.isdir(_sub) and not _d.startswith("."):
_M1B_CANDIDATES.append(_sub)
except OSError:
pass
for _p in _M1B_CANDIDATES:
if _p not in sys.path:
sys.path.insert(0, _p)
# --- end bootstrap ---
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
MOD_ROOT = os.path.dirname(HERE)
REPO_ROOT = os.path.abspath(os.path.join(MOD_ROOT, "..", ".."))
TOOLS = os.path.join(MOD_ROOT, "tools")
for p in (REPO_ROOT, MOD_ROOT, TOOLS):
if p not in sys.path:
sys.path.insert(0, p)
import m1b_fix_import_closure as closure # noqa: E402
#: QC 点名的符号 -> 应能取到它的模块列表
REQUIRED_SYMBOLS = {
"PblError": ["pbl_blueprint.m1b.errors", "pbl_common.errors",
"pbl_blueprint.db"],
"PblNotFound": ["pbl_blueprint.m1b.errors", "pbl_common.errors",
"pbl_blueprint.db"],
"PblValidationError": ["pbl_blueprint.m1b.errors", "pbl_common.errors",
"pbl_blueprint.db"],
"require_tenant": ["pbl_blueprint.m1b.tenant", "pbl_common.tenant",
"pbl_blueprint.db"],
"write_audit": ["pbl_blueprint.m1b.audit", "pbl_common.audit"],
"tenant_crud": ["pbl_blueprint.m1b.crud_factory", "pbl_common.crud_factory"],
"new_id": ["pbl_blueprint.m1b.util", "pbl_common.dbutil"],
"now_str": ["pbl_blueprint.m1b.util", "pbl_common.dbutil"],
"sql_exec": ["pbl_blueprint.m1b.dbutil", "pbl_common.dbutil"],
"sql_rows": ["pbl_blueprint.m1b.dbutil", "pbl_common.dbutil"],
"sql_scalar": ["pbl_blueprint.m1b.dbutil", "pbl_common.dbutil"],
"assert_not_write_protected": ["pbl_blueprint.m1b.tenant",
"pbl_common.tenant"],
"pbl_template_instantiate": ["pbl_blueprint.m1b.api", "pbl_blueprint.api"],
"pbl_blueprint_create": ["pbl_blueprint.m1b.api", "pbl_blueprint.api"],
}
M1B_SUBMODULES = [
"pbl_blueprint.m1b",
"pbl_blueprint.m1b.errors",
"pbl_blueprint.m1b.util",
"pbl_blueprint.m1b.tenant",
"pbl_blueprint.m1b.dbutil",
"pbl_blueprint.m1b.audit",
"pbl_blueprint.m1b.crud_factory",
"pbl_blueprint.m1b.tables",
"pbl_blueprint.m1b.subobject",
"pbl_blueprint.m1b.ref",
"pbl_blueprint.m1b.template",
"pbl_blueprint.m1b.api",
"pbl_blueprint.m1b.init",
]
class ImportClosureTestCase(unittest.TestCase):
"""import 闭包硬门禁断言。"""
@classmethod
def setUpClass(cls):
# 先执行幂等修复(补齐兼容导出层),再核验闭包
cls.fix_log = closure.apply_fix(dry=False)
cls.breaks = closure.scan_closure()
def test_01_static_closure_has_zero_breaks(self):
"""静态扫描:跨文件符号引用断裂数 = 0QC #1 硬门禁)。"""
self.assertEqual(
self.breaks, [],
"import 闭包仍有 %d 处断裂:\n%s" % (
len(self.breaks),
"\n".join(" %s:%s -> %s.%s (%s)" % (
b["file"], b["line"], b["module"], b["symbol"], b["reason"])
for b in self.breaks[:40])))
def test_02_scanned_files_cover_pbl_packages(self):
"""扫描确实覆盖了 pbl_* 包(不是空扫)。"""
files = list(closure.iter_py_files())
self.assertGreater(len(files), 20, "扫描文件数过少,核验无意义")
pkgs = set()
for f in files:
rel = os.path.relpath(f, closure.MODULES_DIR).replace(os.sep, "/")
pkgs.add(rel.split("/")[0])
self.assertIn("pbl_blueprint", pkgs)
self.assertIn("pbl_common", pkgs)
def test_03_m1b_submodules_importable(self):
"""M1b 全部子模块可真实 import动态闭包"""
for mod in M1B_SUBMODULES:
with self.subTest(module=mod):
m = importlib.import_module(mod)
self.assertIsNotNone(m)
def test_04_pbl_blueprint_api_importable(self):
"""pbl_blueprint.api 可 importQC #2/#4 断裂点)。"""
m = importlib.import_module("pbl_blueprint.api")
self.assertTrue(hasattr(m, "pbl_template_instantiate"),
"pbl_blueprint.api 缺 pbl_template_instantiate")
self.assertTrue(hasattr(m, "pbl_blueprint_create"),
"pbl_blueprint.api 缺 pbl_blueprint_create")
self.assertTrue(callable(m.pbl_template_instantiate))
self.assertTrue(callable(m.pbl_blueprint_create))
def test_05_pbl_blueprint_db_importable(self):
"""pbl_blueprint.db 可 import 且提供 QC #2 点名符号。"""
m = importlib.import_module("pbl_blueprint.db")
for sym in ("PblError", "PblNotFound", "PblValidationError",
"require_tenant"):
with self.subTest(symbol=sym):
self.assertTrue(hasattr(m, sym), "pbl_blueprint.db 缺 %s" % sym)
def test_06_pbl_common_compat_surface(self):
"""pbl_common 四个模块的兼容导出面齐备QC #3"""
targets = {
"pbl_common.audit": ["write_audit"],
"pbl_common.crud_factory": ["tenant_crud"],
"pbl_common.dbutil": ["new_id", "now_str", "sql_exec", "sql_rows",
"sql_scalar"],
}
for mod, syms in targets.items():
try:
m = importlib.import_module(mod)
except ImportError as exc:
self.skipTest("%s 不可导入(%s),跳过" % (mod, exc))
continue
for s in syms:
with self.subTest(module=mod, symbol=s):
self.assertTrue(hasattr(m, s), "%s%s" % (mod, s))
def test_07_required_symbols_resolvable(self):
"""QC 点名的每个符号在其目标模块上真实可取到。"""
missing = []
for sym, mods in REQUIRED_SYMBOLS.items():
for mod in mods:
try:
m = importlib.import_module(mod)
except ImportError as exc:
missing.append("%s.%s (module import failed: %s)"
% (mod, sym, exc))
continue
if not hasattr(m, sym):
missing.append("%s.%s" % (mod, sym))
self.assertEqual(missing, [], "符号缺失: %s" % missing)
def test_08_pbl_agent_runtime_dependency_satisfied(self):
"""pbl_agent_runtime.api 的 M1b 依赖面可满足QC #4"""
try:
m = importlib.import_module("pbl_agent_runtime.api")
except ImportError as exc:
# 该模块可能依赖运行期平台组件;退化为静态核验其 import 目标
src_path = os.path.join(closure.MODULES_DIR, "pbl_agent_runtime",
"pbl_agent_runtime", "api.py")
if not os.path.exists(src_path):
self.skipTest("pbl_agent_runtime 不在工作空间: %s" % exc)
bp_api = importlib.import_module("pbl_blueprint.api")
for sym in ("pbl_template_instantiate", "pbl_blueprint_create"):
self.assertTrue(hasattr(bp_api, sym),
"pbl_blueprint.api 缺 %sagent_runtime 依赖)" % sym)
return
self.assertIsNotNone(m)
def test_09_fix_is_idempotent(self):
"""兼容层修复可重复执行,不重复追加块(幂等)。"""
before = {}
for spec in closure.COMPAT_TARGETS:
p = os.path.join(REPO_ROOT, spec["file"])
before[p] = os.path.getsize(p) if os.path.exists(p) else 0
closure.apply_fix(dry=False)
closure.apply_fix(dry=False)
for spec in closure.COMPAT_TARGETS:
p = os.path.join(REPO_ROOT, spec["file"])
size = os.path.getsize(p) if os.path.exists(p) else 0
with self.subTest(file=spec["file"]):
self.assertLessEqual(size, before[p] + 4096,
"%s 兼容块疑似重复追加" % spec["file"])
if os.path.exists(p):
with open(p, "r", encoding="utf-8") as fh:
txt = fh.read()
self.assertLessEqual(txt.count(closure.MARK_BEGIN), 1,
"%s 存在多个兼容块" % spec["file"])
def test_10_registry_matches_exports(self):
"""API 注册表条目全部真实可调用(声明=实现)。"""
from pbl_blueprint.m1b.api import M1B_API_REGISTRY
self.assertGreaterEqual(len(M1B_API_REGISTRY), 20)
for name, fn in M1B_API_REGISTRY.items():
with self.subTest(api=name):
self.assertTrue(callable(fn), "%s 不可调用" % name)
self.assertEqual(getattr(fn, "__name__", name), name)
if __name__ == "__main__":
unittest.main(verbosity=2)