""" PregoPal - 测试运行器 ===================== 无需 pytest,直接运行所有测试并生成 HTML 测试报告。 """ import sys import os import time import traceback import json from datetime import datetime from pathlib import Path # 确保项目根目录在路径中 sys.path.insert(0, str(Path(__file__).parent.parent)) # ============================================================ # 测试框架(极简版) # ============================================================ class TestResult: def __init__(self, class_name, method_name, doc, passed, message=""): self.class_name = class_name self.method_name = method_name self.doc = doc self.passed = passed self.message = message @property def full_name(self): return f"{self.class_name}.{self.method_name}" class TestRunner: def __init__(self): self.results = [] self.start_time = None self.end_time = None def run(self, test_module): """运行一个测试模块中的所有测试类""" self.start_time = time.time() for name in dir(test_module): obj = getattr(test_module, name) if isinstance(obj, type) and name.startswith("Test"): self._run_test_class(obj) self.end_time = time.time() def _run_test_class(self, cls): """运行一个测试类中的所有测试方法""" for name in dir(cls): if not name.startswith("test_"): continue method = getattr(cls, name) doc = method.__doc__ or name # 每个测试方法创建独立实例,确保 setup/teardown 隔离 instance = cls() # setup if hasattr(instance, "setup_method"): try: instance.setup_method() except Exception as e: print(f" [WARN] setup_method failed for {cls.__name__}.{name}: {e}") try: method(instance) self.results.append(TestResult(cls.__name__, name, doc, True)) except Exception as e: tb = traceback.format_exc() # 安全获取错误消息(避免 GBK UnicodeEncodeError) try: msg = str(e) if str(e) else tb[:500] except UnicodeEncodeError: msg = tb[:500] self.results.append(TestResult(cls.__name__, name, doc, False, msg)) # teardown if hasattr(instance, "teardown_method"): try: instance.teardown_method() except Exception as e: print(f" [WARN] teardown_method failed for {cls.__name__}.{name}: {e}") @property def total(self): return len(self.results) @property def passed(self): return sum(1 for r in self.results if r.passed) @property def failed(self): return sum(1 for r in self.results if not r.passed) @property def duration(self): if self.start_time and self.end_time: return round(self.end_time - self.start_time, 2) return 0 def generate_html_report(self, output_path="test_report.html"): """生成 HTML 测试报告""" now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rows = "" for r in self.results: status = "PASS" if r.passed else "FAIL" color = "#4CAF50" if r.passed else "#f44336" message = f"
{r.message}
" if not r.passed else "" rows += f""" {status} {r.class_name} {r.method_name} {r.doc} {'通过' if r.passed else '失败'} {message} """ html = f""" PregoPal 测试报告

PregoPal 测试报告

生成时间: {now} | 测试框架: 内置 TestRunner

{self.total}
总用例数
{self.passed}
通过
{self.failed}
失败
{self.pass_rate:.1f}%
通过率
{rows}
状态 测试类 测试方法 描述 结果 错误信息
""" output_path = Path(output_path) output_path.write_text(html, encoding='utf-8') return output_path @property def pass_rate(self): return (self.passed / self.total * 100) if self.total > 0 else 0 def print_summary(self): """打印控制台摘要""" print("=" * 60) print(" PregoPal 测试报告") print(" " + "=" * 56) print(f" 总用例: {self.total} | 通过: {self.passed} | 失败: {self.failed} | 通过率: {self.pass_rate:.1f}%") print(f" 耗时: {self.duration}秒") print("=" * 60) if self.failed > 0: print("\n 失败的测试:") for r in self.results: if not r.passed: print(f" - {r.full_name}: {r.message}") print() # ============================================================ # 主入口 # ============================================================ def main(): """运行所有测试并生成报告""" import importlib test_modules = [ "tests.test_nutrition_standards", "tests.test_family_manager", "tests.test_diet_extractor", "tests.test_loop", "tests.test_plugins", ] runner = TestRunner() for module_name in test_modules: try: mod = importlib.import_module(module_name) print(f"[TEST] 正在测试: {module_name}") runner.run(mod) except Exception as e: print(f"[ERROR] 导入模块失败 {module_name}: {e}") runner.print_summary() # 生成 HTML 报告 report_path = runner.generate_html_report("test_report.html") print(f"[DONE] HTML 报告已生成: {report_path.resolve()}") # 生成 JSON 报告 json_path = Path("test_report.json") json_data = { "total": runner.total, "passed": runner.passed, "failed": runner.failed, "pass_rate": runner.pass_rate, "duration": runner.duration, "timestamp": datetime.now().isoformat(), "results": [ { "class": r.class_name, "method": r.method_name, "doc": r.doc, "passed": r.passed, "message": r.message } for r in runner.results ] } json_path.write_text(json.dumps(json_data, ensure_ascii=False, indent=2), encoding='utf-8') print(f"[DONE] JSON 报告已生成: {json_path.resolve()}") return 0 if runner.failed == 0 else 1 if __name__ == "__main__": sys.exit(main())