chore: v1.5.0 - 提交测试代码、测试报告,更新 tasks.md 状态

This commit is contained in:
2026-06-06 12:52:51 +08:00
parent d8d669bba1
commit ce62d2f353
13 changed files with 2251 additions and 10 deletions

View File

@@ -561,6 +561,342 @@ def test_list_to_map(r: TestRunner):
shutil.rmtree(tmpdir, ignore_errors=True)
# ── Part 3: v1.5 Template / Style Integration Tests ────────────────
def test_v15_styles(r: TestRunner):
"""v1.5: 模板分离与样式应用集成测试方案B — 直接调用底层函数传入 fixture 路径)。"""
fixture_dir = os.path.join(os.path.dirname(__file__), 'fixtures')
tmpdir = tempfile.mkdtemp(prefix="pinmap_v15_")
from xlsx_writer import write_xlsx_with_style
try:
# ── TC-v1.5-001: MAP→List 加载 BallList 模板 ──
def _tc_v15_001(result):
template_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
assert os.path.exists(template_path), f"BallList 模板文件不存在: {template_path}"
style = read_template_styles(template_path)
assert style is not None, "BallList 模板样式应成功读取"
assert len(style.fonts) > 0, "应有字体定义"
assert len(style.borders) > 0, "应有边框定义"
assert 0 in style.column_widths, "应有列宽定义"
result.ok(f"模板加载成功: fonts={len(style.fonts)}, borders={len(style.borders)}, width_A={style.column_widths.get(0)}")
r.run("TC-v1.5-001: MAP->List 加载 BallList 模板", _tc_v15_001)
# ── TC-v1.5-002: MAP→List 无模板降级 ──
def _tc_v15_002(result):
style = read_template_styles('/nonexistent/nonexistent_template.xlsx')
assert style is None, "不存在的模板应返回 None"
result.ok("无模板文件时优雅返回 None")
r.run("TC-v1.5-002: MAP->List 无模板降级", _tc_v15_002)
# ── TC-v1.5-003: List→MAP 加载 BallMAP 模板 ──
def _tc_v15_003(result):
template_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
assert os.path.exists(template_path), f"BallMAP 模板文件不存在: {template_path}"
style = read_template_styles(template_path)
assert style is not None, "BallMAP 模板样式应成功读取"
assert len(style.fonts) > 0, "应有字体定义"
assert len(style.borders) > 0, "应有边框定义"
assert 0 in style.row_heights, "应有行高定义"
result.ok(f"模板加载成功: fonts={len(style.fonts)}, borders={len(style.borders)}, row_height={style.row_heights.get(0)}")
r.run("TC-v1.5-003: List->MAP 加载 BallMAP 模板", _tc_v15_003)
# ── TC-v1.5-004: List→MAP 无模板降级 ──
def _tc_v15_004(result):
style = read_template_styles('/nonexistent/nonexistent_template.xlsx')
assert style is None, "不存在的模板应返回 None"
result.ok("无模板文件时优雅返回 None")
r.run("TC-v1.5-004: List->MAP 无模板降级", _tc_v15_004)
# ── TC-v1.5-005: 两个方向独立使用各自模板 ──
def _tc_v15_005(result):
bl_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
bm_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
style_bl = read_template_styles(bl_path)
style_bm = read_template_styles(bm_path)
assert style_bl is not None, "BallList 模板应成功加载"
assert style_bm is not None, "BallMAP 模板应成功加载"
# BallList 有列宽BallMAP 有行高
assert 0 in style_bl.column_widths, "BallList 应有列宽"
assert 0 in style_bm.row_heights, "BallMAP 应有行高"
result.ok(f"两个模板独立: BL fonts={len(style_bl.fonts)}, BM fonts={len(style_bm.fonts)}")
r.run("TC-v1.5-005: 两个方向独立使用各自模板", _tc_v15_005)
# ── TC-v1.5-006: 模板损坏优雅降级 ──
def _tc_v15_006(result):
corrupt_path = os.path.join(fixture_dir, 'template_corrupt.xlsx')
assert os.path.exists(corrupt_path), f"损坏模板文件不存在: {corrupt_path}"
style = read_template_styles(corrupt_path)
assert style is None, "损坏模板应返回 None优雅降级"
result.ok("损坏模板优雅返回 None")
r.run("TC-v1.5-006: 模板损坏优雅降级", _tc_v15_006)
# ── TC-v1.5-007: 模板字体应用到输出文件 ──
def _tc_v15_007(result):
template_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
style = read_template_styles(template_path)
assert style is not None, "模板样式应成功读取"
# 用模板生成 3x3 PinMAP
entries = [PinListEntry(number=i+1, name=f"PIN{i+1:02d}") for i in range(12)]
outpath = os.path.join(tmpdir, 'v15_007_output.xlsx')
generate_pinmap(entries, 3, 3, "QFP-12", template_style=style, output_path=outpath)
# 验证输出 styles.xml 包含模板字体
import zipfile
with zipfile.ZipFile(outpath, 'r') as zf:
assert 'xl/styles.xml' in zf.namelist(), "输出应包含 styles.xml"
styles_xml = zf.read('xl/styles.xml').decode('utf-8')
assert '宋体' in styles_xml, f"输出 styles.xml 应包含宋体"
assert '14' in styles_xml, f"输出 styles.xml 应包含字号 14"
result.ok("输出 styles.xml 包含模板字体(宋体 14pt)")
r.run("TC-v1.5-007: 模板字体应用到输出文件", _tc_v15_007)
# ── TC-v1.5-008: 模板列宽应用到输出文件 ──
def _tc_v15_008(result):
template_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
style = read_template_styles(template_path)
assert style is not None, "模板样式应成功读取"
# 用模板生成 MAP->List 输出
data = {'A1': 'QFP-44', 'A2': 'Pin1', 'B2': '1', 'A3': 'Pin2', 'B3': '2'}
outpath = os.path.join(tmpdir, 'v15_008_output.xlsx')
write_xlsx_with_style(data, outpath, style)
# 验证列宽
import zipfile
import xml.etree.ElementTree as ET
_S = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
with zipfile.ZipFile(outpath, 'r') as zf:
sheet_xml = zf.read('xl/worksheets/sheet1.xml')
root = ET.fromstring(sheet_xml)
cols_elem = root.find(f'{{{_S}}}cols')
assert cols_elem is not None, "输出 sheet1.xml 应包含 cols"
cols = cols_elem.findall(f'{{{_S}}}col')
assert len(cols) >= 2, f"应有至少2列宽度定义实际: {len(cols)}"
width_a = float(cols[0].get('width', '0'))
width_b = float(cols[1].get('width', '0'))
assert abs(width_a - 25.0) < 0.5, f"A列宽 ({width_a}) 应与模板 25 接近"
assert abs(width_b - 18.0) < 0.5, f"B列宽 ({width_b}) 应与模板 18 接近"
result.ok(f"列宽验证通过: A={width_a:.1f}, B={width_b:.1f}")
r.run("TC-v1.5-008: 模板列宽应用到输出文件", _tc_v15_008)
# ── TC-v1.5-009: 模板行高应用到输出文件 ──
def _tc_v15_009(result):
template_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
style = read_template_styles(template_path)
assert style is not None, "模板样式应成功读取"
entries = [PinListEntry(number=i+1, name=f"PIN{i+1:02d}") for i in range(12)]
outpath = os.path.join(tmpdir, 'v15_009_output.xlsx')
generate_pinmap(entries, 3, 3, "QFP-12", template_style=style, output_path=outpath)
import zipfile
import xml.etree.ElementTree as ET
_S = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
with zipfile.ZipFile(outpath, 'r') as zf:
sheet_xml = zf.read('xl/worksheets/sheet1.xml')
root = ET.fromstring(sheet_xml)
sheet_data = root.find(f'{{{_S}}}sheetData')
rows = sheet_data.findall(f'{{{_S}}}row')
found_ht = False
for row in rows:
ht = row.get('ht')
if ht:
found_ht = True
ht_val = float(ht)
assert abs(ht_val - 25.0) < 0.5, f"行高 ({ht_val}) 应与模板 25 接近"
break
assert found_ht, "至少应有一个 row 包含 ht 属性"
result.ok("行高验证通过: ht=25")
r.run("TC-v1.5-009: 模板行高应用到输出文件", _tc_v15_009)
# ── TC-v1.5-010: 两个方向使用不同模板各自的格式 ──
def _tc_v15_010(result):
bl_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
bm_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
style_bl = read_template_styles(bl_path)
style_bm = read_template_styles(bm_path)
assert style_bl and style_bm, "两个模板都应该成功加载"
# MAP->List 方向:用 BallList 模板
pinlist_data = {'A1': 'QFP-44', 'A2': 'Pin1', 'B2': '1'}
pinlist_path = os.path.join(tmpdir, 'v15_010_pinlist.xlsx')
write_xlsx_with_style(pinlist_data, pinlist_path, style_bl)
# List->MAP 方向:用 BallMAP 模板
entries = [PinListEntry(number=i+1, name=f"PIN{i+1:02d}") for i in range(12)]
pinmap_path = os.path.join(tmpdir, 'v15_010_pinmap.xlsx')
generate_pinmap(entries, 3, 3, "QFP-12", template_style=style_bm, output_path=pinmap_path)
import zipfile
with zipfile.ZipFile(pinlist_path, 'r') as zf:
pl_styles = zf.read('xl/styles.xml').decode('utf-8')
with zipfile.ZipFile(pinmap_path, 'r') as zf:
pm_styles = zf.read('xl/styles.xml').decode('utf-8')
assert '楷体' in pl_styles, "BallList 输出应包含楷体"
assert '宋体' in pm_styles, "BallMAP 输出应包含宋体"
result.ok("两个方向输出字体不同: BL->楷体, BM->宋体")
r.run("TC-v1.5-010: 两个方向不同模板各自的格式", _tc_v15_010)
# ── TC-v1.5-011: 完整往返+模板隔离 ──
def _tc_v15_011(result):
bl_path = os.path.join(fixture_dir, 'BallList-Template.xlsx')
bm_path = os.path.join(fixture_dir, 'BallMAP-Template.xlsx')
style_bl = read_template_styles(bl_path)
style_bm = read_template_styles(bm_path)
assert style_bl and style_bm, "两个模板都应该成功加载"
fixture_4x4 = os.path.join(fixture_dir, 'sample_4x4.xlsx')
cells = read_xlsx_cells(fixture_4x4)
pinmap = parse_pinmap(cells)
validation = validate_pinmap(pinmap)
pinlist = generate_pinlist(pinmap, validation)
pinlist_data = {'A1': pinlist.package_info}
for i, (pin_name, pin_num) in enumerate(pinlist.rows):
pinlist_data[f'A{i+2}'] = pin_name
pinlist_data[f'B{i+2}'] = str(pin_num)
pinlist_path = os.path.join(tmpdir, 'v15_011_pinlist.xlsx')
write_xlsx_with_style(pinlist_data, pinlist_path, style_bl)
pkg2, entries2 = parse_pinlist(pinlist_path)
pinmap_path = os.path.join(tmpdir, 'v15_011_pinmap.xlsx')
generate_pinmap(entries2, 3, 3, pkg2, template_style=style_bm, output_path=pinmap_path)
rt_cells = read_xlsx_cells(pinmap_path)
rt_pinmap = parse_pinmap(rt_cells)
assert len(rt_pinmap.pins) == len(pinmap.pins), f"往返后引脚数应一致: {len(rt_pinmap.pins)} vs {len(pinmap.pins)}"
rt_numbers = sorted([p.number for p in rt_pinmap.pins])
orig_numbers = sorted([p.number for p in pinmap.pins])
assert rt_numbers == orig_numbers, f"往返引脚序号应一致"
import zipfile
with zipfile.ZipFile(pinlist_path, 'r') as zf:
pl_xml = zf.read('xl/styles.xml').decode('utf-8')
with zipfile.ZipFile(pinmap_path, 'r') as zf:
pm_xml = zf.read('xl/styles.xml').decode('utf-8')
assert '楷体' in pl_xml, "中间 PinList 应使用 BallList 的楷体"
assert '宋体' in pm_xml, "最终 PinMAP 应使用 BallMAP 的宋体"
result.ok(f"往返成功: {len(pinmap.pins)} pins, 楷体->PinList, 宋体->PinMAP")
r.run("TC-v1.5-011: 完整往返+模板隔离", _tc_v15_011)
# ── TC-v1.5-012: 无模板完整流程 ──
def _tc_v15_012(result):
fixture_4x4 = os.path.join(fixture_dir, 'sample_4x4.xlsx')
cells = read_xlsx_cells(fixture_4x4)
pinmap = parse_pinmap(cells)
validation = validate_pinmap(pinmap)
pinlist = generate_pinlist(pinmap, validation)
pinlist_data = {'A1': pinlist.package_info}
for i, (pin_name, pin_num) in enumerate(pinlist.rows):
pinlist_data[f'A{i+2}'] = pin_name
pinlist_data[f'B{i+2}'] = str(pin_num)
pinlist_path = os.path.join(tmpdir, 'v15_012_pinlist.xlsx')
write_xlsx_with_style(pinlist_data, pinlist_path, None)
assert os.path.exists(pinlist_path), "输出文件应存在"
rt_cells = read_xlsx_cells(pinlist_path)
assert (0, 0) in rt_cells, "A1 应有封装信息"
pkg2, entries2 = parse_pinlist(pinlist_path)
pinmap_path = os.path.join(tmpdir, 'v15_012_pinmap.xlsx')
generate_pinmap(entries2, 3, 3, pkg2, template_style=None, output_path=pinmap_path)
assert os.path.exists(pinmap_path), "PinMAP 输出文件应存在"
result.ok("无模板完整流程正常")
r.run("TC-v1.5-012: 无模板完整流程", _tc_v15_012)
# ── TC-v1.5-013: 极简模板(只有字体) ──
def _tc_v15_013(result):
template_path = os.path.join(fixture_dir, 'template_minimal.xlsx')
assert os.path.exists(template_path), f"极简模板文件不存在: {template_path}"
style = read_template_styles(template_path)
assert style is not None, "极简模板应成功加载"
assert len(style.fonts) > 0, "应有字体定义"
assert style.fonts[0].name == "Courier New", f"字体应为 Courier New, 实际: {style.fonts[0].name}"
assert len(style.borders) == 0, "极简模板不应有边框"
assert len(style.fills) == 0, "极简模板不应有填充"
pinlist_data = {'A1': 'QFP-8', 'A2': 'Pin1', 'B2': '1'}
outpath = os.path.join(tmpdir, 'v15_013_output.xlsx')
write_xlsx_with_style(pinlist_data, outpath, style)
import zipfile
with zipfile.ZipFile(outpath, 'r') as zf:
styles_xml = zf.read('xl/styles.xml').decode('utf-8')
assert 'Courier New' in styles_xml, "输出应包含 Courier New"
result.ok(f"极简模板: font={style.fonts[0].name}")
r.run("TC-v1.5-013: 极简模板(只有字体)", _tc_v15_013)
# ── TC-v1.5-014: 列宽扩展 ──
def _tc_v15_014(result):
template_path = os.path.join(fixture_dir, 'template_narrow.xlsx')
style = read_template_styles(template_path)
assert style is not None, "窄模板应成功加载"
assert 0 in style.column_widths, "应有第 0 列宽"
assert abs(style.column_widths[0] - 15.0) < 0.5
entries = [PinListEntry(number=i+1, name=f"PIN{i+1:02d}") for i in range(20)]
outpath = os.path.join(tmpdir, 'v15_014_output.xlsx')
generate_pinmap(entries, 5, 5, "QFN-20", template_style=style, output_path=outpath)
import zipfile
import xml.etree.ElementTree as ET
_S = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
with zipfile.ZipFile(outpath, 'r') as zf:
sheet_xml = zf.read('xl/worksheets/sheet1.xml')
root = ET.fromstring(sheet_xml)
cols_elem = root.find(f'{{{_S}}}cols')
assert cols_elem is not None, "输出应包含 cols"
cols = cols_elem.findall(f'{{{_S}}}col')
assert len(cols) >= 6, f"需要至少 6 列,实际 {len(cols)}"
widths = [float(c.get('width', '0')) for c in cols]
assert abs(widths[0] - 15.0) < 0.5, f"A列应为 15.0, 实际: {widths[0]}"
assert abs(widths[1] - 12.0) < 0.5, f"B列应为 12.0, 实际: {widths[1]}"
assert abs(widths[2] - 10.0) < 0.5, f"C列应为 10.0, 实际: {widths[2]}"
assert abs(widths[3] - 8.0) < 0.5, f"D列应为默认 8.0, 实际: {widths[3]}"
assert abs(widths[4] - 8.0) < 0.5, f"E列应为默认 8.0, 实际: {widths[4]}"
result.ok(f"列宽扩展正确: A={widths[0]}, B={widths[1]}, C={widths[2]}, D={widths[3]}, E={widths[4]}")
r.run("TC-v1.5-014: 列宽扩展", _tc_v15_014)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
# ── Main ────────────────────────────────────────────────────────────
def main():
@@ -579,6 +915,10 @@ def main():
test_list_to_map(runner)
print()
print("-- Part 3: v1.5 模板/样式集成测试 --")
test_v15_styles(runner)
print()
total, passed, failed = runner.summary()
print("=" * 60)
print(f" 总计: {total} | 通过: {passed} | 失败: {failed}")
@@ -611,13 +951,17 @@ def generate_report(runner: TestRunner, total: int, passed: int, failed: int):
# 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")]
v15_results = [r for r in runner.results if r.name.startswith("TC-v1.5")]
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
v15_pass = sum(1 for r in v15_results if r.passed)
v15_fail = len(v15_results) - v15_pass
lines.append(f"| MAPList 回归 | {len(map_results)} | {map_pass} | {map_fail} |")
lines.append(f"| ListMAP 新增 | {len(lm_results)} | {lm_pass} | {lm_fail} |")
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"| v1.5 模板/样式集成 | {len(v15_results)} | {v15_pass} | {v15_fail} |")
lines.append(f"| **总计** | **{total}** | **{passed}** | **{failed}** |")
lines.append("")
lines.append("---")
@@ -649,6 +993,19 @@ def generate_report(runner: TestRunner, total: int, passed: int, failed: int):
lines.append(f"- **错误**: {r.error}")
lines.append("")
# v1.5 details
lines.append("## Part 3: v1.5 模板/样式集成测试")
lines.append("")
for r in v15_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("")