""" 测试运行器 提供统一的测试执行和报告生成 """ import unittest import sys import json from pathlib import Path from datetime import datetime from typing import Dict, List, Any # 添加项目根目录到路径 sys.path.insert(0, str(Path(__file__).parent.parent)) class TestMetricsCollector(unittest.TestResult): """ 测试指标收集器 收集测试执行的详细指标 """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.test_metrics = [] self.start_time = None self.current_test_start = None def startTest(self, test): super().startTest(test) self.current_test_start = datetime.now() def stopTest(self, test): super().stopTest(test) duration = (datetime.now() - self.current_test_start).total_seconds() # 确定测试状态 status = 'passed' error_msg = None if test in [t[0] for t in self.failures]: status = 'failed' error_msg = [e[1] for e in self.failures if e[0] == test][0] elif test in [t[0] for t in self.errors]: status = 'error' error_msg = [e[1] for e in self.errors if e[0] == test][0] elif test in self.skipped: status = 'skipped' # 记录指标 self.test_metrics.append({ 'test_name': str(test), 'test_class': test.__class__.__name__, 'test_method': test._testMethodName, 'status': status, 'duration_seconds': duration, 'error_message': error_msg }) def get_summary(self) -> Dict[str, Any]: """获取测试摘要""" total = self.testsRun passed = len([m for m in self.test_metrics if m['status'] == 'passed']) failed = len(self.failures) errors = len(self.errors) skipped = len(self.skipped) total_duration = sum(m['duration_seconds'] for m in self.test_metrics) return { 'total_tests': total, 'passed': passed, 'failed': failed, 'errors': errors, 'skipped': skipped, 'success_rate': passed / total if total > 0 else 0, 'total_duration_seconds': total_duration, 'timestamp': datetime.now().isoformat() } def run_test_suite(test_modules: List[str], output_dir: Path = None) -> Dict[str, Any]: """ 运行测试套件并生成报告 Args: test_modules: 测试模块名称列表 output_dir: 报告输出目录 Returns: 测试结果摘要 """ # 创建测试套件 loader = unittest.TestLoader() suite = unittest.TestSuite() for module_name in test_modules: try: module = __import__(module_name, fromlist=['']) suite.addTests(loader.loadTestsFromModule(module)) except ImportError as e: print(f"警告: 无法加载测试模块 {module_name}: {e}") # 运行测试 print(f"\n{'='*70}") print(f"开始运行测试套件 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print(f"{'='*70}\n") result = TestMetricsCollector() suite.run(result) # 生成摘要 summary = result.get_summary() # 打印结果 print(f"\n{'='*70}") print("测试执行摘要") print(f"{'='*70}") print(f"总测试数: {summary['total_tests']}") print(f"通过: {summary['passed']} ✅") print(f"失败: {summary['failed']} ❌") print(f"错误: {summary['errors']} ⚠️") print(f"跳过: {summary['skipped']} ⏭️") print(f"成功率: {summary['success_rate']:.1%}") print(f"总耗时: {summary['total_duration_seconds']:.2f}秒") print(f"{'='*70}\n") # 显示失败的测试 if result.failures: print("失败的测试:") for test, traceback in result.failures: print(f" ❌ {test}") print(f" {traceback.split(chr(10))[0]}") # 显示错误的测试 if result.errors: print("\n错误的测试:") for test, traceback in result.errors: print(f" ⚠️ {test}") print(f" {traceback.split(chr(10))[0]}") # 保存详细报告 if output_dir: output_dir.mkdir(parents=True, exist_ok=True) # JSON报告 report_data = { 'summary': summary, 'test_details': result.test_metrics, 'failures': [ { 'test': str(test), 'traceback': traceback } for test, traceback in result.failures ], 'errors': [ { 'test': str(test), 'traceback': traceback } for test, traceback in result.errors ] } report_file = output_dir / f"test_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" with open(report_file, 'w', encoding='utf-8') as f: json.dump(report_data, f, ensure_ascii=False, indent=2) print(f"\n详细报告已保存到: {report_file}") # Markdown报告 md_report = generate_markdown_report(summary, result) md_file = output_dir / f"test_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md" with open(md_file, 'w', encoding='utf-8') as f: f.write(md_report) print(f"Markdown报告已保存到: {md_file}") return summary def generate_markdown_report(summary: Dict[str, Any], result: TestMetricsCollector) -> str: """生成Markdown格式的测试报告""" md = f"""# 测试执行报告 **生成时间**: {summary['timestamp']} ## 执行摘要 | 指标 | 数值 | |------|------| | 总测试数 | {summary['total_tests']} | | 通过 | {summary['passed']} ✅ | | 失败 | {summary['failed']} ❌ | | 错误 | {summary['errors']} ⚠️ | | 跳过 | {summary['skipped']} ⏭️ | | 成功率 | {summary['success_rate']:.1%} | | 总耗时 | {summary['total_duration_seconds']:.2f}秒 | ## 测试覆盖矩阵 ### 关键路径覆盖 """ # 按测试类分组 test_by_class = {} for metric in result.test_metrics: class_name = metric['test_class'] if class_name not in test_by_class: test_by_class[class_name] = [] test_by_class[class_name].append(metric) for class_name, tests in test_by_class.items(): passed = len([t for t in tests if t['status'] == 'passed']) total = len(tests) md += f"\n#### {class_name}\n\n" md += f"- 覆盖率: {passed}/{total} ({passed/total:.1%})\n" md += f"- 测试用例:\n" for test in tests: status_icon = { 'passed': '✅', 'failed': '❌', 'error': '⚠️', 'skipped': '⏭️' }.get(test['status'], '❓') md += f" - {status_icon} `{test['test_method']}` ({test['duration_seconds']:.3f}s)\n" # 失败详情 if result.failures or result.errors: md += "\n## 失败详情\n\n" if result.failures: md += "### 失败的测试\n\n" for test, traceback in result.failures: md += f"#### {test}\n\n" md += "```\n" md += traceback md += "\n```\n\n" if result.errors: md += "### 错误的测试\n\n" for test, traceback in result.errors: md += f"#### {test}\n\n" md += "```\n" md += traceback md += "\n```\n\n" # 建议 md += "\n## 改进建议\n\n" if summary['success_rate'] < 1.0: md += "- ⚠️ 存在失败的测试,需要修复\n" if summary['success_rate'] >= 0.95: md += "- ✅ 测试覆盖率良好\n" elif summary['success_rate'] >= 0.8: md += "- ⚠️ 建议提高测试覆盖率\n" else: md += "- ❌ 测试覆盖率较低,需要补充测试用例\n" return md def run_critical_path_tests(): """运行关键路径测试""" test_modules = [ 'test_e2e_integration', 'test_security_regression', ] workspace_path = Path(__file__).parent.parent / "workspace" output_dir = workspace_path / "test_reports" summary = run_test_suite(test_modules, output_dir) # 返回退出码 return 0 if summary['failed'] == 0 and summary['errors'] == 0 else 1 def run_all_tests(): """运行所有测试""" test_modules = [ 'test_intent_classifier', 'test_rule_checker', 'test_history_manager', 'test_task_features', 'test_data_governance', 'test_config_refresh', 'test_retry_fix', 'test_e2e_integration', 'test_security_regression', ] workspace_path = Path(__file__).parent.parent / "workspace" output_dir = workspace_path / "test_reports" summary = run_test_suite(test_modules, output_dir) # 返回退出码 return 0 if summary['failed'] == 0 and summary['errors'] == 0 else 1 if __name__ == '__main__': import argparse parser = argparse.ArgumentParser(description='LocalAgent 测试运行器') parser.add_argument( '--mode', choices=['all', 'critical', 'unit'], default='critical', help='测试模式: all(全部), critical(关键路径), unit(单元测试)' ) args = parser.parse_args() if args.mode == 'all': exit_code = run_all_tests() elif args.mode == 'critical': exit_code = run_critical_path_tests() else: # unit test_modules = [ 'test_intent_classifier', 'test_rule_checker', 'test_history_manager', ] workspace_path = Path(__file__).parent.parent / "workspace" output_dir = workspace_path / "test_reports" summary = run_test_suite(test_modules, output_dir) exit_code = 0 if summary['failed'] == 0 and summary['errors'] == 0 else 1 sys.exit(exit_code)