This commit is contained in:
2026-02-04 14:38:52 +08:00
commit a5147b1429
29 changed files with 4489 additions and 0 deletions

332
modules/output_formatter.py Normal file
View File

@@ -0,0 +1,332 @@
# @line_count 300
"""多格式输出模块"""
import json
from pathlib import Path
from typing import List, Dict, Any, Optional
from datetime import datetime
try:
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
OPENPYXL_AVAILABLE = True
except ImportError:
OPENPYXL_AVAILABLE = False
class OutputFormatter:
"""多格式输出格式化器"""
def __init__(self, test_items: List[Dict[str, Any]], test_cases: List[Dict[str, Any]],
document_info: Optional[Dict[str, Any]] = None):
"""
初始化输出格式化器
Args:
test_items: 测试项列表
test_cases: 测试用例列表
document_info: 文档信息
"""
self.test_items = test_items
self.test_cases = test_cases
self.document_info = document_info or {}
def to_json(self, output_path: Optional[str] = None) -> str:
"""
输出为JSON格式
Args:
output_path: 输出文件路径如果为None则返回JSON字符串
Returns:
JSON字符串或文件路径
"""
data = {
'document_info': self.document_info,
'generation_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'summary': {
'test_item_count': len(self.test_items),
'test_case_count': len(self.test_cases)
},
'test_items': self.test_items,
'test_cases': self.test_cases
}
json_str = json.dumps(data, ensure_ascii=False, indent=2)
if output_path:
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(json_str)
return str(output_path)
return json_str
def to_markdown(self, output_path: Optional[str] = None) -> str:
"""
输出为Markdown格式
Args:
output_path: 输出文件路径如果为None则返回Markdown字符串
Returns:
Markdown字符串或文件路径
"""
lines = []
# 标题和文档信息
lines.append("# 测试项和测试用例文档\n")
if self.document_info.get('title'):
lines.append(f"**文档标题**: {self.document_info['title']}\n")
if self.document_info.get('version'):
lines.append(f"**版本**: {self.document_info['version']}\n")
lines.append(f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
lines.append(f"**测试项数量**: {len(self.test_items)}\n")
lines.append(f"**测试用例数量**: {len(self.test_cases)}\n")
lines.append("\n---\n")
# 按模块分组
modules = {}
for item in self.test_items:
module_name = item.get('module_name', '未分类')
if module_name not in modules:
modules[module_name] = []
modules[module_name].append(item)
# 输出每个模块的测试项和测试用例
for module_name, items in modules.items():
lines.append(f"\n## {module_name}\n")
for item in items:
# 测试项
lines.append(f"\n### {item.get('id', '')} {item.get('name', '')}\n")
lines.append(f"- **测试类型**: {item.get('test_type', 'N/A')}")
lines.append(f"- **优先级**: {item.get('priority', 'N/A')}")
lines.append(f"- **测试目标**: {item.get('test_objective', 'N/A')}\n")
# 该测试项下的测试用例
item_cases = [case for case in self.test_cases
if case.get('test_item_id') == item.get('id')]
if item_cases:
lines.append("#### 测试用例\n")
for case in item_cases:
lines.append(f"\n**{case.get('id', '')} {case.get('name', '')}**\n")
lines.append(f"- **前置条件**: {case.get('preconditions', 'N/A')}")
lines.append(f"- **优先级**: {case.get('priority', 'N/A')}")
lines.append(f"- **测试类型**: {case.get('test_type', 'N/A')}\n")
lines.append("**测试步骤**:\n")
for idx, step in enumerate(case.get('test_steps', []), 1):
lines.append(f"{idx}. {step}\n")
lines.append(f"\n**预期结果**: {case.get('expected_result', 'N/A')}\n")
lines.append("---\n")
markdown_str = '\n'.join(lines)
if output_path:
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(markdown_str)
return str(output_path)
return markdown_str
def to_excel(self, output_path: str) -> str:
"""
输出为Excel格式
Args:
output_path: 输出文件路径
Returns:
文件路径
"""
if not OPENPYXL_AVAILABLE:
raise ImportError("openpyxl未安装无法生成Excel文件。请运行: pip install openpyxl")
wb = Workbook()
# 删除默认工作表
if 'Sheet' in wb.sheetnames:
wb.remove(wb['Sheet'])
# 创建测试项工作表
self._create_test_items_sheet(wb)
# 创建测试用例工作表
self._create_test_cases_sheet(wb)
# 创建摘要工作表
self._create_summary_sheet(wb)
# 保存文件
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
wb.save(output_path)
return str(output_path)
def _create_test_items_sheet(self, wb: Workbook):
"""创建测试项工作表"""
ws = wb.create_sheet("测试项", 0)
# 表头
headers = ['测试项ID', '测试项名称', '所属模块', '功能名称', '测试类型', '优先级', '测试目标']
ws.append(headers)
# 设置表头样式
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF")
border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
for col in range(1, len(headers) + 1):
cell = ws.cell(1, col)
cell.fill = header_fill
cell.font = header_font
cell.border = border
cell.alignment = Alignment(horizontal='center', vertical='center')
# 数据行
for item in self.test_items:
row = [
item.get('id', ''),
item.get('name', ''),
item.get('module_name', ''),
item.get('function_name', ''),
item.get('test_type', ''),
item.get('priority', ''),
item.get('test_objective', '')
]
ws.append(row)
# 设置边框
for col in range(1, len(headers) + 1):
ws.cell(ws.max_row, col).border = border
# 调整列宽
column_widths = [12, 30, 15, 15, 12, 8, 40]
for col, width in enumerate(column_widths, 1):
ws.column_dimensions[ws.cell(1, col).column_letter].width = width
# 冻结首行
ws.freeze_panes = 'A2'
def _create_test_cases_sheet(self, wb: Workbook):
"""创建测试用例工作表"""
ws = wb.create_sheet("测试用例", 1)
# 表头
headers = ['测试用例ID', '测试项ID', '测试用例名称', '所属模块', '前置条件',
'测试步骤', '预期结果', '优先级', '测试类型']
ws.append(headers)
# 设置表头样式
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF")
border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
for col in range(1, len(headers) + 1):
cell = ws.cell(1, col)
cell.fill = header_fill
cell.font = header_font
cell.border = border
cell.alignment = Alignment(horizontal='center', vertical='center')
# 数据行
for case in self.test_cases:
test_steps_str = '\n'.join([f"{idx}. {step}" for idx, step
in enumerate(case.get('test_steps', []), 1)])
row = [
case.get('id', ''),
case.get('test_item_id', ''),
case.get('name', ''),
case.get('module_name', ''),
case.get('preconditions', ''),
test_steps_str,
case.get('expected_result', ''),
case.get('priority', ''),
case.get('test_type', '')
]
ws.append(row)
# 设置边框和换行
for col in range(1, len(headers) + 1):
cell = ws.cell(ws.max_row, col)
cell.border = border
if col == 6: # 测试步骤列
cell.alignment = Alignment(wrap_text=True, vertical='top')
else:
cell.alignment = Alignment(vertical='top')
# 调整列宽
column_widths = [12, 12, 30, 15, 25, 40, 30, 8, 12]
for col, width in enumerate(column_widths, 1):
ws.column_dimensions[ws.cell(1, col).column_letter].width = width
# 设置行高(为测试步骤列预留空间)
for row in range(2, ws.max_row + 1):
ws.row_dimensions[row].height = 60
# 冻结首行
ws.freeze_panes = 'A2'
def _create_summary_sheet(self, wb: Workbook):
"""创建摘要工作表"""
ws = wb.create_sheet("摘要", 2)
# 文档信息
if self.document_info.get('title'):
ws.append(['文档标题', self.document_info['title']])
if self.document_info.get('version'):
ws.append(['版本', self.document_info['version']])
ws.append(['生成时间', datetime.now().strftime('%Y-%m-%d %H:%M:%S')])
ws.append([])
# 统计信息
ws.append(['统计项', '数量'])
ws.append(['测试项总数', len(self.test_items)])
ws.append(['测试用例总数', len(self.test_cases)])
ws.append([])
# 按模块统计
ws.append(['模块', '测试项数量', '测试用例数量'])
modules = {}
for item in self.test_items:
module_name = item.get('module_name', '未分类')
if module_name not in modules:
modules[module_name] = {'items': 0, 'cases': 0}
modules[module_name]['items'] += 1
for case in self.test_cases:
module_name = case.get('module_name', '未分类')
if module_name not in modules:
modules[module_name] = {'items': 0, 'cases': 0}
modules[module_name]['cases'] += 1
for module_name, stats in modules.items():
ws.append([module_name, stats['items'], stats['cases']])
# 设置样式
header_font = Font(bold=True)
for row in ws.iter_rows(min_row=1, max_row=1):
for cell in row:
cell.font = header_font
# 调整列宽
ws.column_dimensions['A'].width = 20
ws.column_dimensions['B'].width = 15
ws.column_dimensions['C'].width = 15