72 lines
2.5 KiB
Python
72 lines
2.5 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""单测进程的联网护栏。导入即安装,联网失败即使被捕获,进程也必须失败。
|
||
|
||
完整隔离入口:python scripts/unit_test_guard.py scripts/test_batch3_units.py。
|
||
入口在空临时目录运行测试,只继承系统环境,不读取仓库 .env 或生产参数。
|
||
仅供测试使用,生产模块不得导入。
|
||
"""
|
||
import atexit
|
||
import os
|
||
from pathlib import Path
|
||
import runpy
|
||
import sys
|
||
import tempfile
|
||
import traceback
|
||
|
||
_violations = []
|
||
_ROOT = str(Path(__file__).resolve().parents[1]) + os.sep
|
||
|
||
|
||
def _audit(event, args):
|
||
if event in {"socket.connect", "socket.getaddrinfo", "socket.sendto", "socket.sendmsg"}:
|
||
# 不输出连接地址或凭据,只保留调用位置。
|
||
_violations.append((event, traceback.extract_stack()[:-1]))
|
||
raise RuntimeError("单测禁止联网: " + event)
|
||
|
||
|
||
def _finish():
|
||
if _violations:
|
||
print("\nTEST ISOLATION FAILED: 检测到未替换的外部调用", file=sys.stderr)
|
||
try:
|
||
for event, stack in _violations[:5]:
|
||
print(event, file=sys.stderr)
|
||
local = [frame for frame in stack if frame.filename.startswith(_ROOT)]
|
||
traceback.print_list(local[-12:] or stack[-12:], file=sys.stderr)
|
||
sys.stdout.flush()
|
||
sys.stderr.flush()
|
||
finally:
|
||
os._exit(1) # atexit 里抛异常不会改变进程退出码。
|
||
|
||
|
||
def run_suite(path):
|
||
path = Path(path).resolve()
|
||
sys.path.insert(0, str(path.parent))
|
||
keep = {k: v for k, v in os.environ.items()
|
||
if k in {"PATH", "SYSTEMROOT", "LANG", "LC_ALL"}}
|
||
keep.update(TZ="Asia/Shanghai", PYTHONDONTWRITEBYTECODE="1")
|
||
os.environ.clear()
|
||
os.environ.update(keep)
|
||
sys.dont_write_bytecode = True
|
||
import time
|
||
if hasattr(time, "tzset"):
|
||
time.tzset()
|
||
previous = Path.cwd()
|
||
with tempfile.TemporaryDirectory(prefix="pms-unit-") as isolated:
|
||
try:
|
||
os.chdir(isolated)
|
||
sys.argv = [str(path)]
|
||
runpy.run_path(str(path), run_name="__main__")
|
||
finally:
|
||
os.chdir(previous)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) != 2:
|
||
raise SystemExit("用法: python scripts/unit_test_guard.py 测试文件")
|
||
# 独立模块保存护栏状态,不与 runpy 临时使用的 __main__ 共用全局字典。
|
||
from unit_test_guard import run_suite
|
||
run_suite(sys.argv[1])
|
||
else:
|
||
sys.addaudithook(_audit)
|
||
atexit.register(_finish)
|