pbl_blueprint/tools/m1b_run_tests.py
2026-09-17 15:16:08 +08:00

183 lines
7.2 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 测试执行器:真实跑 unittest 并把完整日志落盘QC #9 取证)。
对应 QC 退回意见 #9「仅 py_compile 通过,无真实执行通过证据」。
本工具用 unittest.TextTestRunner 真实执行 tests/test_m1b_*.py
把 stdout/stderr 全量写入
projects/pbls/deliverables/m1b/test_logs/{suite}.log
并产出汇总 JSON
projects/pbls/deliverables/m1b/test_report.json
退出码 = 失败数 + 错误数0 表示全绿)。
用法:
python3 tools/m1b_run_tests.py # 跑全部 M1b 测试
python3 tools/m1b_run_tests.py --suite ext_ref
python3 tools/m1b_run_tests.py -v 3
"""
import argparse
import io
import json
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 time
import traceback
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, "..", ".."))
TESTS_DIR = os.path.join(MOD_ROOT, "tests")
LOG_DIR = os.path.join(REPO_ROOT, "projects", "pbls", "deliverables",
"m1b", "test_logs")
REPORT = os.path.join(REPO_ROOT, "projects", "pbls", "deliverables",
"m1b", "test_report.json")
SUITES = {
"ext_ref": "test_m1b_ext_ref.py",
"realdb": "test_m1b_realdb.py",
"closure": "test_m1b_import_closure.py",
}
for p in (REPO_ROOT, MOD_ROOT, TESTS_DIR):
if p not in sys.path:
sys.path.insert(0, p)
def run_suite(name, filename, verbosity=2):
"""执行单个测试文件,返回 (result_dict, log_text)。"""
buf = io.StringIO()
old_out, old_err = sys.stdout, sys.stderr
started = time.time()
tests_run = failures = errors = skipped = 0
case_results = []
fatal = None
try:
sys.stdout = buf
sys.stderr = buf
loader = unittest.TestLoader()
suite = loader.discover(TESTS_DIR, pattern=filename, top_level_dir=TESTS_DIR)
runner = unittest.TextTestRunner(stream=buf, verbosity=verbosity)
res = runner.run(suite)
tests_run = res.testsRun
failures = len(res.failures)
errors = len(res.errors)
skipped = len(res.skipped)
for case, tb in res.failures:
case_results.append({"case": str(case), "status": "FAIL", "trace": tb})
for case, tb in res.errors:
case_results.append({"case": str(case), "status": "ERROR", "trace": tb})
for case, reason in res.skipped:
case_results.append({"case": str(case), "status": "SKIP",
"trace": str(reason)})
ok = res.wasSuccessful()
except Exception: # noqa: BLE001 - 装载失败也要留证据
ok = False
fatal = traceback.format_exc()
buf.write("\n[FATAL] suite load/run error:\n" + fatal)
finally:
sys.stdout, sys.stderr = old_out, old_err
elapsed = round(time.time() - started, 3)
log = buf.getvalue()
return {
"suite": name,
"file": "tests/%s" % filename,
"ok": bool(ok),
"tests_run": tests_run,
"failures": failures,
"errors": errors,
"skipped": skipped,
"passed": max(tests_run - failures - errors - skipped, 0),
"elapsed_sec": elapsed,
"fatal": fatal,
"cases": case_results,
}, log
def main(argv=None):
ap = argparse.ArgumentParser(description="M1b test runner (real execution)")
ap.add_argument("--suite", choices=list(SUITES.keys()),
help="只跑指定套件(缺省跑全部)")
ap.add_argument("-v", "--verbosity", type=int, default=2)
args = ap.parse_args(argv)
os.makedirs(LOG_DIR, exist_ok=True)
targets = ([args.suite] if args.suite else list(SUITES.keys()))
results = []
for name in targets:
fn = SUITES[name]
if not os.path.exists(os.path.join(TESTS_DIR, fn)):
results.append({"suite": name, "file": "tests/%s" % fn, "ok": False,
"tests_run": 0, "failures": 0, "errors": 1,
"skipped": 0, "passed": 0, "elapsed_sec": 0,
"fatal": "test file missing", "cases": []})
continue
res, log = run_suite(name, fn, verbosity=args.verbosity)
log_path = os.path.join(LOG_DIR, "%s.log" % name)
with open(log_path, "w", encoding="utf-8") as fh:
fh.write("=== M1b test suite: %s (%s) ===\n" % (name, fn))
fh.write("=== executed_at: %s ===\n" % time.strftime("%Y-%m-%d %H:%M:%S"))
fh.write("=== python: %s ===\n" % sys.version.replace("\n", " "))
fh.write("=== result: %s | run=%d pass=%d fail=%d error=%d skip=%d "
"| %.3fs ===\n\n" % (
"PASS" if res["ok"] else "FAIL", res["tests_run"],
res["passed"], res["failures"], res["errors"],
res["skipped"], res["elapsed_sec"]))
fh.write(log)
fh.write("\n=== END OF LOG ===\n")
res["log"] = os.path.relpath(log_path, REPO_ROOT)
res["log_bytes"] = os.path.getsize(log_path)
results.append(res)
print("[%s] %s run=%d pass=%d fail=%d error=%d skip=%d (%.3fs) -> %s" % (
"PASS" if res["ok"] else "FAIL", name, res["tests_run"], res["passed"],
res["failures"], res["errors"], res["skipped"], res["elapsed_sec"],
res["log"]))
total_run = sum(r["tests_run"] for r in results)
total_fail = sum(r["failures"] for r in results)
total_err = sum(r["errors"] for r in results)
total_skip = sum(r["skipped"] for r in results)
summary = {
"tool": "tools/m1b_run_tests.py",
"milestone": "M1b",
"executed_at": time.strftime("%Y-%m-%d %H:%M:%S"),
"python": sys.version.split()[0],
"real_execution": True,
"suites": results,
"totals": {"run": total_run,
"passed": total_run - total_fail - total_err - total_skip,
"failures": total_fail, "errors": total_err,
"skipped": total_skip},
"all_green": total_fail == 0 and total_err == 0 and total_run > 0,
"log_dir": os.path.relpath(LOG_DIR, REPO_ROOT),
}
os.makedirs(os.path.dirname(REPORT), exist_ok=True)
with open(REPORT, "w", encoding="utf-8") as fh:
fh.write(json.dumps(summary, ensure_ascii=False, indent=2, default=str) + "\n")
print(json.dumps({"all_green": summary["all_green"], "totals": summary["totals"],
"report": os.path.relpath(REPORT, REPO_ROOT)},
ensure_ascii=False, indent=2))
return 0 if summary["all_green"] else (total_fail + total_err)
if __name__ == "__main__":
sys.exit(main())