Files
pinmap-to-pinlist/Test/run_tests.py

687 lines
28 KiB
Python
Raw 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
"""
PinMAP ↔ PinList 双向转换器 — 完整测试脚本
测试范围:
1. MAP→List (原有功能回归测试)
2. List→MAP (新增功能测试)
"""
import sys
import os
import tempfile
import shutil
# 把 Code/src 加入 path
SRC_DIR = os.path.join(os.path.dirname(__file__), '..', 'Code', 'src')
sys.path.insert(0, SRC_DIR)
from models import PinListEntry, ValidationResult, ValidationError
from pinlist_parser import parse_pinlist
from pinlist_validator import validate_pinlist
from pinmap_layout import calculate_layout, get_name_cell
from pinmap_generator import generate_pinmap, generate_output_path
from template_reader import read_template_styles
from xlsx_reader import read_excel_cells as read_xlsx_cells
from xlsx_writer import write_xlsx
from pinmap_parser import parse_pinmap
from validator import validate_pinmap
from pinlist_generator import generate_pinlist
# ── Helpers ─────────────────────────────────────────────────────────
class TestResult:
def __init__(self, name):
self.name = name
self.passed = False
self.error = None
self.details = ""
def ok(self, detail=""):
self.passed = True
self.details = detail
def fail(self, error, detail=""):
self.passed = False
self.error = error
self.details = detail
class TestRunner:
def __init__(self):
self.results: list[TestResult] = []
def run(self, name, fn):
r = TestResult(name)
try:
fn(r)
except Exception as e:
r.fail(str(e))
self.results.append(r)
status = "" if r.passed else ""
detail = f"{r.details}" if r.details else ""
print(f" {status} {name}{detail}")
def summary(self):
total = len(self.results)
passed = sum(1 for r in self.results if r.passed)
failed = total - passed
return total, passed, failed
def create_pinlist_xlsx(data: dict, path: str):
"""Helper: write a PinList xlsx from cell dict."""
write_xlsx(data, path)
def create_pinmap_fixture(data: dict, path: str):
"""Helper: write a PinMAP fixture xlsx."""
write_xlsx(data, path)
# ── Part 1: MAP→List Regression Tests ──────────────────────────────
def test_map_to_list(r: TestRunner):
fixture_dir = os.path.join(os.path.dirname(__file__), 'fixtures')
# TC-MAP-001: 标准 4x4 PinMAP 转换
def _tc_map_001(result):
filepath = os.path.join(fixture_dir, 'sample_4x4.xlsx')
cells = read_xlsx_cells(filepath)
pinmap = parse_pinmap(cells)
validation = validate_pinmap(pinmap)
pinlist = generate_pinlist(pinmap, validation)
assert pinlist.package_info, "package_info 不应为空"
assert len(pinlist.rows) > 0, "应有引脚数据"
# 验证递增排序
nums = [num for _, num in pinlist.rows]
assert nums == sorted(nums), f"序号应递增,实际: {nums}"
result.ok(f"封装={pinlist.package_info}, Pin数={len(pinlist.rows)}, 序号递增")
r.run("TC-MAP-001: 标准4x4 PinMAP转换", _tc_map_001)
# TC-MAP-002: 长方形 PinMAP 转换
def _tc_map_002(result):
filepath = os.path.join(fixture_dir, 'sample_rect.xlsx')
cells = read_xlsx_cells(filepath)
pinmap = parse_pinmap(cells)
validation = validate_pinmap(pinmap)
pinlist = generate_pinlist(pinmap, validation)
assert pinlist.package_info, "package_info 不应为空"
assert len(pinlist.rows) > 0, "应有引脚数据"
nums = [num for _, num in pinlist.rows]
assert nums == sorted(nums), f"序号应递增,实际: {nums}"
result.ok(f"封装={pinlist.package_info}, Pin数={len(pinlist.rows)}, 序号递增")
r.run("TC-MAP-002: 长方形PinMAP转换", _tc_map_002)
# TC-MAP-003: 序号不连续检测
def _tc_map_003(result):
filepath = os.path.join(fixture_dir, 'error_gap.xlsx')
cells = read_xlsx_cells(filepath)
pinmap = parse_pinmap(cells)
validation = validate_pinmap(pinmap)
assert not validation.is_valid, "应验证失败"
assert any("不连续" in e.message for e in validation.errors), \
"应有'不连续'错误"
result.ok(f"错误: {validation.errors[0].message}{validation.errors[0].details}")
r.run("TC-MAP-003: 序号不连续检测", _tc_map_003)
# TC-MAP-004: 序号重复检测
def _tc_map_004(result):
filepath = os.path.join(fixture_dir, 'error_dup.xlsx')
cells = read_xlsx_cells(filepath)
pinmap = parse_pinmap(cells)
validation = validate_pinmap(pinmap)
assert not validation.is_valid, "应验证失败"
assert any("重复" in e.message for e in validation.errors), \
"应有'重复'错误"
result.ok(f"错误: {validation.errors[0].message}{validation.errors[0].details}")
r.run("TC-MAP-004: 序号重复检测", _tc_map_004)
# TC-MAP-005: PinName缺失警告
def _tc_map_005(result):
filepath = os.path.join(fixture_dir, 'warning_missing.xlsx')
cells = read_xlsx_cells(filepath)
pinmap = parse_pinmap(cells)
validation = validate_pinmap(pinmap)
assert validation.is_valid, "应验证通过缺失Name是warning"
assert len(validation.warnings) > 0, "应有警告"
assert any("缺少" in w.message or "NC" in w.details for w in validation.warnings), \
"应有PinName缺失警告"
result.ok(f"警告: {validation.warnings[0].message}{validation.warnings[0].details}")
r.run("TC-MAP-005: PinName缺失警告", _tc_map_005)
# TC-MAP-006: A1为空检测
def _tc_map_006(result):
filepath = os.path.join(fixture_dir, 'error_empty_a1.xlsx')
cells = read_xlsx_cells(filepath)
try:
parse_pinmap(cells)
result.fail("应抛出StructureError")
except Exception as e:
assert "A1" in str(e) or "" in str(e), f"错误信息应提及A1或空: {e}"
result.ok(f"正确报错: {e}")
r.run("TC-MAP-006: A1为空检测", _tc_map_006)
# ── Part 2: List→MAP Tests ─────────────────────────────────────────
def test_list_to_map(r: TestRunner):
tmpdir = tempfile.mkdtemp(prefix="pinmap_test_")
try:
# ── TC-LM-001: 5×5 PinList → PinMAP (20引脚) ──
# v1.3: (r+c)*2 = (5+5)*2 = 20 pins
def _tc_lm_001(result):
# 5×5 grid → (5+5)*2 = 20 pins
data = {'A1': 'QFP-20'}
for i in range(1, 21):
row = i + 1 # row 2..21
data[f'A{row}'] = f'Pin{i}'
data[f'B{row}'] = str(i)
filepath = os.path.join(tmpdir, 'test_5x5_pinlist.xlsx')
create_pinlist_xlsx(data, filepath)
# Parse
pkg, entries = parse_pinlist(filepath)
assert pkg == 'QFP-20', f"封装信息应为QFP-20, 实际: {pkg}"
assert len(entries) == 20, f"应有20个引脚, 实际: {len(entries)}"
# Validate with 5×5 grid
validation = validate_pinlist(entries, 5, 5)
assert validation.is_valid, f"验证应通过: {validation.errors}"
assert len(validation.errors) == 0
# Generate PinMAP
output = generate_pinmap(entries, 5, 5, pkg, output_path=None)
assert 'A1' in output, "A1应有封装信息"
assert output['A1'] == 'QFP-20'
result.ok(f"解析成功, 封装={pkg}, Pin数={len(entries)}, 5×5布局验证通过")
r.run("TC-LM-001: 5×5 PinList→PinMAP (20引脚)", _tc_lm_001)
# ── TC-LM-002: 长方形 PinList → 6×10 PinMAP (32引脚) ──
# v1.3: (r+c)*2 = (6+10)*2 = 32 pins
def _tc_lm_002(result):
data = {'A1': 'LQFP-32'}
for i in range(1, 33):
row = i + 1
data[f'A{row}'] = f'PIN_{i:02d}'
data[f'B{row}'] = str(i)
filepath = os.path.join(tmpdir, 'test_6x10_pinlist.xlsx')
create_pinlist_xlsx(data, filepath)
pkg, entries = parse_pinlist(filepath)
assert len(entries) == 32, f"应有32个引脚, 实际: {len(entries)}"
validation = validate_pinlist(entries, 6, 10)
assert validation.is_valid, f"验证应通过: {validation.errors}"
# Generate and write to file
outpath = os.path.join(tmpdir, 'test_6x10_pinmap.xlsx')
output = generate_pinmap(entries, 6, 10, pkg, output_path=outpath)
assert os.path.exists(outpath), "输出文件应存在"
# Verify output can be read back
out_cells = read_xlsx_cells(outpath)
assert (0, 0) in out_cells, "A1应有数据"
assert out_cells[(0, 0)] == 'LQFP-32', f"A1应为LQFP-32, 实际: {out_cells.get((0,0))}"
result.ok(f"解析成功, 封装={pkg}, Pin数={len(entries)}, 6×10布局+文件输出验证通过")
r.run("TC-LM-002: 6×10 PinList→PinMAP (32引脚)", _tc_lm_002)
# ── TC-LM-003: 带模板文件的转换 ──
# v1.3: 5×5 grid → (5+5)*2 = 20 pins
def _tc_lm_003(result):
# 创建模板 PinMAP
template_data = {'A1': 'QFP-20'}
for i in range(1, 21):
row = i + 1
template_data[f'A{row}'] = f'Pin{i}'
template_data[f'B{row}'] = str(i)
template_path = os.path.join(tmpdir, 'template_pinmap.xlsx')
# 用 styled writer 创建模板
from xlsx_writer import write_xlsx_with_style
write_xlsx_with_style(template_data, template_path)
# 创建 PinList 并写入模板同目录
data = {'A1': 'QFP-20'}
for i in range(1, 21):
row = i + 1
data[f'A{row}'] = f'Pin{i}'
data[f'B{row}'] = str(i)
pinlist_path = os.path.join(tmpdir, 'templated_pinlist.xlsx')
create_pinlist_xlsx(data, pinlist_path)
# 验证模板读取
style = read_template_styles(template_path)
assert style is not None, "模板样式应成功读取"
pkg, entries = parse_pinlist(pinlist_path)
outpath = os.path.join(tmpdir, 'templated_output.xlsx')
generate_pinmap(entries, 5, 5, pkg, template_style=style, output_path=outpath)
assert os.path.exists(outpath), "带模板的输出文件应存在"
# 验证输出文件包含 styles.xml
import zipfile
with zipfile.ZipFile(outpath, 'r') as zf:
assert 'xl/styles.xml' in zf.namelist(), "输出应包含styles.xml"
result.ok(f"模板样式读取成功, 带模板输出文件包含styles.xml")
r.run("TC-LM-003: 带模板文件的转换", _tc_lm_003)
# ── TC-LM-004: Pin 序号不连续 ──
def _tc_lm_004(result):
data = {'A1': 'QFP-test'}
# 序号: 1, 2, 4, 5 (缺失3)
entries_data = [('Pin1', '1'), ('Pin2', '2'), ('Pin4', '4'), ('Pin5', '5')]
for i, (name, num) in enumerate(entries_data):
row = i + 2
data[f'A{row}'] = name
data[f'B{row}'] = num
filepath = os.path.join(tmpdir, 'gap_pinlist.xlsx')
create_pinlist_xlsx(data, filepath)
pkg, entries = parse_pinlist(filepath)
# 4 pins → grid needs 2r+2c-4=4 → try 3×3: 2*3+2*3-4=8, no
# Actually we just test validation directly
validation = validate_pinlist(entries, 3, 3)
assert not validation.is_valid, "应验证失败"
assert any("不连续" in e.message for e in validation.errors), \
"应有'不连续'错误"
result.ok(f"正确报错: {validation.errors[0].message}{validation.errors[0].details}")
r.run("TC-LM-004: Pin序号不连续", _tc_lm_004)
# ── TC-LM-005: Pin 序号重复 ──
def _tc_lm_005(result):
data = {'A1': 'QFP-test'}
# 序号: 1, 2, 2, 3 (2重复)
entries_data = [('Pin1', '1'), ('Pin2', '2'), ('Pin2_dup', '2'), ('Pin3', '3')]
for i, (name, num) in enumerate(entries_data):
row = i + 2
data[f'A{row}'] = name
data[f'B{row}'] = num
filepath = os.path.join(tmpdir, 'dup_pinlist.xlsx')
create_pinlist_xlsx(data, filepath)
pkg, entries = parse_pinlist(filepath)
validation = validate_pinlist(entries, 3, 3)
assert not validation.is_valid, "应验证失败"
assert any("重复" in e.message for e in validation.errors), \
"应有'重复'错误"
result.ok(f"正确报错: {validation.errors[0].message}{validation.errors[0].details}")
r.run("TC-LM-005: Pin序号重复", _tc_lm_005)
# ── TC-LM-006: Pin 总数不匹配 ──
# v1.3: 3×4 grid → (3+4)*2 = 14 pins
def _tc_lm_006(result):
# 创建8个引脚的PinList但3×4网格需要14个引脚
data = {'A1': 'QFP-test'}
for i in range(1, 9): # 8 pins
row = i + 1
data[f'A{row}'] = f'Pin{i}'
data[f'B{row}'] = str(i)
filepath = os.path.join(tmpdir, 'mismatch_pinlist.xlsx')
create_pinlist_xlsx(data, filepath)
pkg, entries = parse_pinlist(filepath)
# 3×4 needs 14 pins, but we have 8
validation = validate_pinlist(entries, 3, 4)
assert not validation.is_valid, "应验证失败"
assert any("不匹配" in e.message for e in validation.errors), \
"应有'不匹配'错误"
result.ok(f"正确报错: {validation.errors[0].message}{validation.errors[0].details}")
r.run("TC-LM-006: Pin总数不匹配", _tc_lm_006)
# ── TC-LM-007: 缺少 PinName ──
# v1.3: 2×3 grid → (2+3)*2 = 10 pins
def _tc_lm_007(result):
data = {'A1': 'QFP-test'}
# 10个引脚其中第2个缺PinName
entries_data = [('Pin1', '1'), ('', '2'), ('Pin3', '3'), ('Pin4', '4'), ('Pin5', '5'),
('Pin6', '6'), ('Pin7', '7'), ('Pin8', '8'), ('Pin9', '9'), ('Pin10', '10')]
for i, (name, num) in enumerate(entries_data):
row = i + 2
data[f'A{row}'] = name
data[f'B{row}'] = num
filepath = os.path.join(tmpdir, 'missing_name_pinlist.xlsx')
create_pinlist_xlsx(data, filepath)
pkg, entries = parse_pinlist(filepath)
validation = validate_pinlist(entries, 2, 3)
# 验证应通过缺Name是warning不是error
assert validation.is_valid, f"应验证通过: {validation.errors}"
assert len(validation.warnings) > 0, "应有警告"
assert any("缺少" in w.message for w in validation.warnings), \
"应有PinName缺失警告"
result.ok(f"验证通过(有warning): {validation.warnings[0].message}{validation.warnings[0].details}")
r.run("TC-LM-007: 缺少PinName (warning)", _tc_lm_007)
# ── TC-LM-008: 非4倍数提示 ──
# v1.3: (r+c)*2 is always even, but may not be multiple of 4
# (3+4)*2 = 14, 14 % 4 = 2 → not a multiple of 4
def _tc_lm_008(result):
# 14个引脚 → 不是4的倍数
data = {'A1': 'QFP-test'}
for i in range(1, 15):
row = i + 1
data[f'A{row}'] = f'Pin{i}'
data[f'B{row}'] = str(i)
filepath = os.path.join(tmpdir, 'non4mult_pinlist.xlsx')
create_pinlist_xlsx(data, filepath)
pkg, entries = parse_pinlist(filepath)
validation = validate_pinlist(entries, 3, 4)
assert validation.is_valid, f"应验证通过: {validation.errors}"
# 14 % 4 != 0, 应有 info 提示
result.ok(f"验证通过, Pin数={len(entries)} (非4倍数)")
r.run("TC-LM-008: 非4倍数提示", _tc_lm_008)
# ── TC-LM-009: 布局计算正确性 ──
# v1.3: 3×3 grid → (3+3)*2 = 12 pins
def _tc_lm_009(result):
entries = [PinListEntry(number=i, name=f'P{i}') for i in range(1, 13)]
layout = calculate_layout(entries, 3, 3)
# 验证四条边都有引脚
assert 'left' in layout, "应有left边"
assert 'bottom' in layout, "应有bottom边"
assert 'right' in layout, "应有right边"
assert 'top' in layout, "应有top边"
# 验证引脚数量分配 (v1.3: 每条边独立)
# left: rows=3, bottom: cols=3, right: rows=3, top: cols=3
assert len(layout['left'].pins) == 3, f"left应有3个引脚, 实际: {len(layout['left'].pins)}"
assert len(layout['bottom'].pins) == 3, f"bottom应有3个引脚, 实际: {len(layout['bottom'].pins)}"
assert len(layout['right'].pins) == 3, f"right应有3个引脚, 实际: {len(layout['right'].pins)}"
assert len(layout['top'].pins) == 3, f"top应有3个引脚, 实际: {len(layout['top'].pins)}"
# 验证总引脚数
total = sum(len(e.pins) for e in layout.values())
assert total == 12, f"总引脚数应为12, 实际: {total}"
# 验证逆时针顺序: left(1,2,3) → bottom(4,5,6) → right(7,8,9) → top(10,11,12)
assert layout['left'].pins[0][0] == 1, "left第一个应为Pin1"
assert layout['left'].pins[-1][0] == 3, "left最后一个应为Pin3"
assert layout['bottom'].pins[0][0] == 4, "bottom第一个应为Pin4"
assert layout['right'].pins[0][0] == 7, "right应为Pin7"
assert layout['top'].pins[0][0] == 10, "top第一个应为Pin10"
result.ok(f"布局计算正确: left=3, bottom=3, right=3, top=3, 逆时针顺序正确")
r.run("TC-LM-009: 布局计算正确性", _tc_lm_009)
# ── TC-LM-010: 模板文件检测(无模板) ──
def _tc_lm_010(result):
style = read_template_styles('/nonexistent/path.xlsx')
assert style is None, "不存在的模板应返回None"
result.ok("无模板文件时优雅返回None")
r.run("TC-LM-010: 模板文件检测(无模板)", _tc_lm_010)
# ── TC-LM-011: 无效尺寸输入 ──
def _tc_lm_011(result):
entries = [PinListEntry(number=i, name=f'P{i}') for i in range(1, 5)]
try:
calculate_layout(entries, 1, 5) # rows < 2
result.fail("应抛出LayoutError")
except Exception as e:
assert "" in str(e) or "无效" in str(e), f"错误应提及行数: {e}"
result.ok(f"正确报错: {e}")
r.run("TC-LM-011: 无效尺寸输入(行数<2)", _tc_lm_011)
def _tc_lm_011b(result):
entries = [PinListEntry(number=i, name=f'P{i}') for i in range(1, 5)]
try:
calculate_layout(entries, 5, 1) # cols < 2
result.fail("应抛出LayoutError")
except Exception as e:
assert "" in str(e) or "无效" in str(e), f"错误应提及列数: {e}"
result.ok(f"正确报错: {e}")
r.run("TC-LM-011b: 无效尺寸输入(列数<2)", _tc_lm_011b)
# ── TC-LM-012: 输出文件正确性 ──
# v1.3: 3×3 grid → (3+3)*2 = 12 pins
def _tc_lm_012(result):
data = {'A1': 'QFP-12'}
for i in range(1, 13):
row = i + 1
data[f'A{row}'] = f'Pin{i}'
data[f'B{row}'] = str(i)
pinlist_path = os.path.join(tmpdir, 'verify_pinlist.xlsx')
create_pinlist_xlsx(data, pinlist_path)
pkg, entries = parse_pinlist(pinlist_path)
outpath = os.path.join(tmpdir, 'verify_pinmap.xlsx')
generate_pinmap(entries, 3, 3, pkg, output_path=outpath)
# 读取输出并验证
out_cells = read_xlsx_cells(outpath)
assert out_cells[(0, 0)] == 'QFP-12', f"A1应为QFP-12, 实际: {out_cells.get((0,0))}"
# 验证所有12个引脚序号都在输出中
found_nums = set()
for (row, col), val in out_cells.items():
for part in str(val).split("/"):
if part.isdigit() and int(part) >= 1:
found_nums.add(int(part))
assert found_nums == {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, \
f"应包含1-12所有序号, 实际: {sorted(found_nums)}"
result.ok(f"输出文件验证通过: A1={out_cells[(0,0)]}, 包含Pin1-Pin12")
r.run("TC-LM-012: 输出文件正确性", _tc_lm_012)
# ── TC-LM-013: 端到端 roundtrip (PinMAP → PinList → PinMAP) ──
# v1.3: 3×3 grid → (3+3)*2 = 12 pins
def _tc_lm_013(result):
data = {'A1': 'QFP-12'}
for i in range(1, 13):
row = i + 1
data[f'A{row}'] = f'Pin{i}'
data[f'B{row}'] = str(i)
pinlist_path = os.path.join(tmpdir, 'rt_pinlist.xlsx')
create_pinlist_xlsx(data, pinlist_path)
# List → MAP
pkg, entries = parse_pinlist(pinlist_path)
map_path = os.path.join(tmpdir, 'rt_pinmap.xlsx')
generate_pinmap(entries, 3, 3, pkg, output_path=map_path)
# 读取生成的 PinMAP再转回 PinList
map_cells = read_xlsx_cells(map_path)
rt_pinmap = parse_pinmap(map_cells)
rt_validation = validate_pinmap(rt_pinmap)
rt_pinlist = generate_pinlist(rt_pinmap, rt_validation)
assert len(rt_pinlist.rows) == 12, \
f"Roundtrip后应有12个引脚, 实际: {len(rt_pinlist.rows)}"
# 验证序号一致
orig_nums = [e.number for e in entries]
rt_nums = [num for _, num in rt_pinlist.rows]
assert orig_nums == rt_nums, f"序号应一致: {orig_nums} vs {rt_nums}"
result.ok(f"Roundtrip成功: PinList(12) → PinMAP(3×3) → PinList({len(rt_pinlist.rows)}), 序号一致")
r.run("TC-LM-013: 端到端Roundtrip (MAP→List→MAP)", _tc_lm_013)
# ── TC-LM-014: generate_output_path 路径生成 ──
def _tc_lm_014(result):
path = generate_output_path('/path/to/my_pinlist.xlsx')
assert path == '/path/to/my_pinlist_PinMAP.xlsx', f"路径应为..._PinMAP.xlsx, 实际: {path}"
result.ok(f"路径生成正确: {path}")
r.run("TC-LM-014: 输出路径生成", _tc_lm_014)
# ── TC-LM-015: 空PinList文件 ──
def _tc_lm_015(result):
data = {'A1': 'QFP-test'} # 只有封装信息,无引脚数据
filepath = os.path.join(tmpdir, 'empty_pinlist.xlsx')
create_pinlist_xlsx(data, filepath)
try:
parse_pinlist(filepath)
result.fail("应抛出StructureError")
except Exception as e:
assert "" in str(e) or "未找到" in str(e), f"错误应提及空/未找到: {e}"
result.ok(f"正确报错: {e}")
r.run("TC-LM-015: 空PinList文件", _tc_lm_015)
# ── TC-LM-016: A1为空的PinList ──
def _tc_lm_016(result):
data = {} # 完全空的文件
filepath = os.path.join(tmpdir, 'no_a1_pinlist.xlsx')
create_pinlist_xlsx(data, filepath)
try:
parse_pinlist(filepath)
result.fail("应抛出StructureError")
except Exception as e:
assert "A1" in str(e) or "" in str(e), f"错误应提及A1/空: {e}"
result.ok(f"正确报错: {e}")
r.run("TC-LM-016: A1为空的PinList", _tc_lm_016)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
# ── Main ────────────────────────────────────────────────────────────
def main():
runner = TestRunner()
print("=" * 60)
print(" PinMAP ↔ PinList 双向转换器 — 测试报告")
print("=" * 60)
print()
print("── Part 1: MAP→List 回归测试 ──")
test_map_to_list(runner)
print()
print("── Part 2: List→MAP 新增功能测试 ──")
test_list_to_map(runner)
print()
total, passed, failed = runner.summary()
print("=" * 60)
print(f" 总计: {total} | 通过: {passed} | 失败: {failed}")
print("=" * 60)
# 生成测试报告
generate_report(runner, total, passed, failed)
return 0 if failed == 0 else 1
def generate_report(runner: TestRunner, total: int, passed: int, failed: int):
"""生成 Markdown 测试报告"""
report_path = os.path.join(os.path.dirname(__file__), 'test_report.md')
lines = []
lines.append("# PinMAP ↔ PinList 双向转换器 测试报告")
lines.append("")
lines.append(f"> **日期**: {__import__('datetime').datetime.now().strftime('%Y-%m-%d')}")
lines.append("> **测试类型**: 集成测试 + 端到端测试")
lines.append("> **测试环境**: Python 3.x, Linux x64")
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 测试概览")
lines.append("")
lines.append("| 类别 | 用例数 | 通过 | 失败 |")
lines.append("|------|--------|------|------|")
# Count by category
map_results = [r for r in runner.results if r.name.startswith("TC-MAP")]
lm_results = [r for r in runner.results if r.name.startswith("TC-LM")]
map_pass = sum(1 for r in map_results if r.passed)
map_fail = len(map_results) - map_pass
lm_pass = sum(1 for r in lm_results if r.passed)
lm_fail = len(lm_results) - lm_pass
lines.append(f"| MAP→List 回归 | {len(map_results)} | {map_pass} | {map_fail} |")
lines.append(f"| List→MAP 新增 | {len(lm_results)} | {lm_pass} | {lm_fail} |")
lines.append(f"| **总计** | **{total}** | **{passed}** | **{failed}** |")
lines.append("")
lines.append("---")
lines.append("")
# Part 1 details
lines.append("## Part 1: MAP→List 回归测试")
lines.append("")
for r in map_results:
status = "✅ 通过" if r.passed else "❌ 失败"
lines.append(f"### {r.name}")
lines.append(f"- **结果**: {status}")
if r.details:
lines.append(f"- **详情**: {r.details}")
if r.error:
lines.append(f"- **错误**: {r.error}")
lines.append("")
# Part 2 details
lines.append("## Part 2: List→MAP 新增功能测试")
lines.append("")
for r in lm_results:
status = "✅ 通过" if r.passed else "❌ 失败"
lines.append(f"### {r.name}")
lines.append(f"- **结果**: {status}")
if r.details:
lines.append(f"- **详情**: {r.details}")
if r.error:
lines.append(f"- **错误**: {r.error}")
lines.append("")
# Summary
lines.append("---")
lines.append("")
lines.append("## 结论")
lines.append("")
if failed == 0:
lines.append("✅ **所有测试用例通过,项目可进入发布阶段。**")
else:
lines.append(f"❌ **{failed} 个测试用例失败,需要修复。**")
lines.append("")
# Issue summary
failed_results = [r for r in runner.results if not r.passed]
if failed_results:
lines.append("## 失败用例汇总")
lines.append("")
lines.append("| 用例 | 错误 |")
lines.append("|------|------|")
for r in failed_results:
lines.append(f"| {r.name} | {r.error} |")
lines.append("")
lines.append("---")
lines.append("")
lines.append("*测试完成*")
report = '\n'.join(lines)
with open(report_path, 'w', encoding='utf-8') as f:
f.write(report)
print(f"\n📄 测试报告已写入: {report_path}")
if __name__ == '__main__':
sys.exit(main())