chore: v1.5.0 - 提交测试代码、测试报告,更新 tasks.md 状态
This commit is contained in:
@@ -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"| MAP→List 回归 | {len(map_results)} | {map_pass} | {map_fail} |")
|
||||
lines.append(f"| List→MAP 新增 | {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("")
|
||||
|
||||
Reference in New Issue
Block a user