1044 lines
46 KiB
Python
1044 lines
46 KiB
Python
#!/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)
|
||
|
||
|
||
# ── 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():
|
||
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()
|
||
|
||
print("-- Part 3: v1.5 模板/样式集成测试 --")
|
||
test_v15_styles(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")]
|
||
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"| v1.5 模板/样式集成 | {len(v15_results)} | {v15_pass} | {v15_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("")
|
||
|
||
# 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("")
|
||
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())
|