Spaces:
Runtime error
Runtime error
| """ | |
| 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 | |
| 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}") | |
| def total(self): | |
| return len(self.results) | |
| def passed(self): | |
| return sum(1 for r in self.results if r.passed) | |
| def failed(self): | |
| return sum(1 for r in self.results if not r.passed) | |
| 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"<pre>{r.message}</pre>" if not r.passed else "" | |
| rows += f""" | |
| <tr> | |
| <td>{status}</td> | |
| <td>{r.class_name}</td> | |
| <td>{r.method_name}</td> | |
| <td>{r.doc}</td> | |
| <td style="color:{color};font-weight:bold">{'通过' if r.passed else '失败'}</td> | |
| <td>{message}</td> | |
| </tr>""" | |
| html = f"""<!DOCTYPE html> | |
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>PregoPal 测试报告</title> | |
| <style> | |
| * {{ margin: 0; padding: 0; box-sizing: border-box; }} | |
| body {{ font-family: -apple-system, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }} | |
| .container {{ max-width: 1200px; margin: 0 auto; }} | |
| .header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 12px; margin-bottom: 20px; }} | |
| .header h1 {{ font-size: 28px; margin-bottom: 10px; }} | |
| .header p {{ opacity: 0.9; }} | |
| .summary {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-bottom: 20px; }} | |
| .summary-card {{ background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); text-align: center; }} | |
| .summary-card .number {{ font-size: 36px; font-weight: bold; }} | |
| .summary-card .label {{ color: #666; margin-top: 5px; }} | |
| .summary-card.total .number {{ color: #2196F3; }} | |
| .summary-card.passed .number {{ color: #4CAF50; }} | |
| .summary-card.failed .number {{ color: #f44336; }} | |
| .summary-card.rate .number {{ color: #FF9800; }} | |
| table {{ width: 100%; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-collapse: collapse; }} | |
| th {{ background: #f8f9fa; padding: 12px 15px; text-align: left; font-weight: 600; color: #333; border-bottom: 2px solid #dee2e6; }} | |
| td {{ padding: 10px 15px; border-bottom: 1px solid #eee; }} | |
| tr:hover {{ background: #f8f9fa; }} | |
| pre {{ background: #f5f5f5; padding: 8px; border-radius: 4px; font-size: 12px; max-height: 100px; overflow: auto; }} | |
| .footer {{ text-align: center; color: #999; margin-top: 20px; font-size: 14px; }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>PregoPal 测试报告</h1> | |
| <p>生成时间: {now} | 测试框架: 内置 TestRunner</p> | |
| </div> | |
| <div class="summary"> | |
| <div class="summary-card total"> | |
| <div class="number">{self.total}</div> | |
| <div class="label">总用例数</div> | |
| </div> | |
| <div class="summary-card passed"> | |
| <div class="number">{self.passed}</div> | |
| <div class="label">通过</div> | |
| </div> | |
| <div class="summary-card failed"> | |
| <div class="number">{self.failed}</div> | |
| <div class="label">失败</div> | |
| </div> | |
| <div class="summary-card rate"> | |
| <div class="number">{self.pass_rate:.1f}%</div> | |
| <div class="label">通过率</div> | |
| </div> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>状态</th> | |
| <th>测试类</th> | |
| <th>测试方法</th> | |
| <th>描述</th> | |
| <th>结果</th> | |
| <th>错误信息</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {rows} | |
| </tbody> | |
| </table> | |
| <div class="footer"> | |
| <p>PregoPal v5 | 测试耗时: {self.duration}秒 | {self.passed}/{self.total} 通过</p> | |
| </div> | |
| </div> | |
| </body> | |
| </html>""" | |
| output_path = Path(output_path) | |
| output_path.write_text(html, encoding='utf-8') | |
| return output_path | |
| 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()) | |