# -*- coding: utf-8 -*- """fix_selfcheck_exit.py —— 修复 selfcheck.py「打印 HAS FAILURES 但 rc=0」的门禁失真缺陷。 背景 ---- 自查脚本 scripts/selfcheck.py 原先在 __main__ 分支里无条件 sys.exit(0)(或不显式退出), 导致存在失败项时进程退出码仍为 0。QC / CI 以退出码为唯一门禁依据,等于门禁形同虚设。 本脚本对 selfcheck.py 的 `if __name__ == "__main__":` 尾块做**幂等外科替换**: 1. 捕获 main() 的返回值 / SystemExit.code / 未捕获异常 -> 基础退出码 2. 同时 tee 全部 stdout,扫描失败特征串(HAS FAILURES / FAILED / [FAIL] / 有失败项) 3. 基础退出码为 0 但输出含失败特征 -> 强制改写为 1 4. 注入标记 SELFCHECK_EXIT_PATCHED,重复执行不会二次改写 用法(机构工作空间根目录):: python3 modules/pbl_blueprint/scripts/fix_selfcheck_exit.py # 应用补丁 python3 modules/pbl_blueprint/scripts/fix_selfcheck_exit.py --check # 只检查是否已打补丁 """ from __future__ import annotations import io import os import re import sys HERE = os.path.dirname(os.path.abspath(__file__)) TARGET = os.path.join(HERE, "selfcheck.py") MARKER = "SELFCHECK_EXIT_PATCHED" MAIN_RE = re.compile(r'^if\s+__name__\s*==\s*[\'"]__main__[\'"]\s*:\s*$', re.MULTILINE) PATCH_BLOCK = '''if __name__ == "__main__": # {marker} —— 退出码门禁:有失败项必须非 0 # 本块由 scripts/fix_selfcheck_exit.py 幂等注入,勿手工编辑。 import sys as _sys import io as _io import re as _re import traceback as _tb class _Tee(object): """同时写原始 stdout 与捕获缓冲,保证控制台输出不变。""" def __init__(self, *streams): self._streams = streams def write(self, s): for _st in self._streams: try: _st.write(s) except Exception: pass return len(s) def flush(self): for _st in self._streams: try: _st.flush() except Exception: pass def __getattr__(self, name): return getattr(self._streams[0], name) _real_stdout = _sys.stdout _buf = _io.StringIO() _sys.stdout = _Tee(_real_stdout, _buf) _rc = 0 try: _entry = None for _name in ("main", "run", "selfcheck", "run_selfcheck"): _entry = globals().get(_name) if callable(_entry): break if callable(_entry): _rv = _entry() if isinstance(_rv, bool): _rc = 0 if _rv else 1 elif isinstance(_rv, int): _rc = _rv else: _rc = 2 _real_stdout.write("[selfcheck] 未找到 main()/run() 入口,无法执行自查\\n") except SystemExit as _e: if _e.code is None: _rc = 0 elif isinstance(_e.code, int): _rc = _e.code else: _rc = 1 _real_stdout.write("[selfcheck] SystemExit: %s\\n" % (_e.code,)) except BaseException: _rc = 1 _sys.stdout = _real_stdout _tb.print_exc() _sys.stdout = _Tee(_real_stdout, _buf) finally: try: _sys.stdout.flush() except Exception: pass _sys.stdout = _real_stdout _out = _buf.getvalue() _FAIL_PAT = _re.compile( r"HAS\\s+FAILURES|\\bFAILED\\b|^\\s*\\[FAIL\\]|\\bFAIL:|失败项\\s*[::]\\s*[1-9]|" r"存在失败|结论\\s*[::]\\s*FAIL", _re.MULTILINE | _re.IGNORECASE, ) _PASS_PAT = _re.compile( r"ALL\\s+PASSED|全部通过|结论\\s*[::]\\s*(OK|PASS|PASSED|SUCCESS)|失败项\\s*[::]\\s*0\\b", _re.IGNORECASE, ) if _rc == 0 and _FAIL_PAT.search(_out) and not ( _PASS_PAT.search(_out) and not _FAIL_PAT.search(_out) ): _real_stdout.write( "[selfcheck] 检测到失败特征但退出码为 0,已按门禁规则改写为 1\\n" ) _rc = 1 _sys.exit(_rc) ''' def read_target() -> str: with io.open(TARGET, "r", encoding="utf-8") as f: return f.read() def already_patched(text: str) -> bool: return MARKER in text def apply_patch() -> int: if not os.path.isfile(TARGET): sys.stderr.write("[fix] 目标不存在: %s\n" % TARGET) return 2 text = read_target() if already_patched(text): print("[fix] selfcheck.py 已含 %s 标记,跳过(幂等)" % MARKER) return 0 m = None for m in MAIN_RE.finditer(text): pass # 取最后一个 __main__ 块 if m is None: # 没有 __main__ 块 -> 直接追加 new = text.rstrip("\n") + "\n\n\n" + PATCH_BLOCK.format(marker=MARKER) action = "appended" else: new = text[: m.start()] + PATCH_BLOCK.format(marker=MARKER) action = "replaced" with io.open(TARGET, "w", encoding="utf-8", newline="\n") as f: f.write(new) print("[fix] selfcheck.py __main__ 块已%s,退出码门禁生效" % action) return 0 def main() -> int: if "--check" in sys.argv: if not os.path.isfile(TARGET): print("[check] MISSING %s" % TARGET) return 2 print("[check] PATCHED" if already_patched(read_target()) else "[check] NOT_PATCHED") return 0 if already_patched(read_target()) else 1 return apply_patch() if __name__ == "__main__": sys.exit(main())