v1.3.0: 修复pinmap_layout周长公式,新增PinList→PinMAP反向转换完整支持

This commit is contained in:
2026-06-01 11:43:53 +08:00
parent 3228c1a2e6
commit 8ad31cbf04
16 changed files with 967 additions and 183 deletions

View File

@@ -175,16 +175,13 @@ def test_list_to_map(r: TestRunner):
tmpdir = tempfile.mkdtemp(prefix="pinmap_test_")
try:
# ── TC-LM-001: 4×4 PinList → 2×2 PinMAP (16引脚) ──
# 2×rows + 2×cols - 4 = 2*4 + 2*4 - 4 = 12, not 16
# Actually: for a 4×4 grid: 2*4 + 2*4 - 4 = 12 pins
# For 16 pins on a square: 2r + 2c - 4 = 16 → r=c=5 → 5×5 grid
# Let's use 5×5 grid for 16 pins
# ── 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 → 2*5 + 2*5 - 4 = 16 pins
data = {'A1': 'QFP-16'}
for i in range(1, 17):
row = i + 1 # row 2..17
# 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')
@@ -192,8 +189,8 @@ def test_list_to_map(r: TestRunner):
# Parse
pkg, entries = parse_pinlist(filepath)
assert pkg == 'QFP-16', f"封装信息应为QFP-16, 实际: {pkg}"
assert len(entries) == 16, f"应有16个引脚, 实际: {len(entries)}"
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)
@@ -203,35 +200,32 @@ def test_list_to_map(r: TestRunner):
# Generate PinMAP
output = generate_pinmap(entries, 5, 5, pkg, output_path=None)
assert 'A1' in output, "A1应有封装信息"
assert output['A1'] == 'QFP-16'
assert output['A1'] == 'QFP-20'
result.ok(f"解析成功, 封装={pkg}, Pin数={len(entries)}, 5×5布局验证通过")
r.run("TC-LM-001: 5×5 PinList→PinMAP (16引脚)", _tc_lm_001)
r.run("TC-LM-001: 5×5 PinList→PinMAP (20引脚)", _tc_lm_001)
# ── TC-LM-002: 长方形 PinList → 4×8 PinMAP (32引脚) ──
# 2*4 + 2*8 - 4 = 8 + 16 - 4 = 20, not 32
# For 32 pins: 2r + 2c - 4 = 32
# Try 4×16: 2*4 + 2*16 - 4 = 8 + 32 - 4 = 36, no
# Try 6×12: 2*6 + 2*12 - 4 = 12 + 24 - 4 = 32 ✓
# ── 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_6x12_pinlist.xlsx')
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, 12)
validation = validate_pinlist(entries, 6, 10)
assert validation.is_valid, f"验证应通过: {validation.errors}"
# Generate and write to file
outpath = os.path.join(tmpdir, 'test_6x12_pinmap.xlsx')
output = generate_pinmap(entries, 6, 12, pkg, output_path=outpath)
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
@@ -239,16 +233,16 @@ def test_list_to_map(r: TestRunner):
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×12布局+文件输出验证通过")
result.ok(f"解析成功, 封装={pkg}, Pin数={len(entries)}, 6×10布局+文件输出验证通过")
r.run("TC-LM-002: 6×12 PinList→PinMAP (32引脚)", _tc_lm_002)
r.run("TC-LM-002: 6×10 PinList→PinMAP (32引脚)", _tc_lm_002)
# ── TC-LM-003: 带模板文件的转换 ──
# 先用一个正常PinMAP作为模板
# v1.3: 5×5 grid → (5+5)*2 = 20 pins
def _tc_lm_003(result):
# 创建模板 PinMAP
template_data = {'A1': 'QFP-16'}
for i in range(1, 17):
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)
@@ -258,8 +252,8 @@ def test_list_to_map(r: TestRunner):
write_xlsx_with_style(template_data, template_path)
# 创建 PinList 并写入模板同目录
data = {'A1': 'QFP-16'}
for i in range(1, 17):
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)
@@ -329,10 +323,9 @@ def test_list_to_map(r: TestRunner):
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×3网格(需要8个引脚)
# 2*3 + 2*3 - 4 = 8 → 匹配!
# 改用3×4网格(需要2*3+2*4-4=10个引脚)
# 创建8个引脚的PinList但3×4网格需要14个引脚
data = {'A1': 'QFP-test'}
for i in range(1, 9): # 8 pins
row = i + 1
@@ -342,7 +335,7 @@ def test_list_to_map(r: TestRunner):
create_pinlist_xlsx(data, filepath)
pkg, entries = parse_pinlist(filepath)
# 3×4 needs 10 pins, but we have 8
# 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), \
@@ -352,11 +345,12 @@ def test_list_to_map(r: TestRunner):
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'}
# 6个引脚其中第2个缺PinName
# 2×4 grid: 2*2+2*4-4=6 pins ✓
entries_data = [('Pin1', '1'), ('', '2'), ('Pin3', '3'), ('Pin4', '4'), ('Pin5', '5'), ('Pin6', '6')]
# 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
@@ -376,14 +370,12 @@ def test_list_to_map(r: TestRunner):
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):
# 6个引脚 → 不是4的倍数
# 2r+2c-4=6 → try 3×4: 2*3+2*4-4=10, no
# try 2×5: 2*2+2*5-4=8, no
# try 4×3: 2*4+2*3-4=10, no
# Actually: 2r+2c-4=6 → r+c=5 → try r=2,c=3: 4+6-4=6 ✓
# 14个引脚 → 不是4的倍数
data = {'A1': 'QFP-test'}
for i in range(1, 7):
for i in range(1, 15):
row = i + 1
data[f'A{row}'] = f'Pin{i}'
data[f'B{row}'] = str(i)
@@ -391,22 +383,17 @@ def test_list_to_map(r: TestRunner):
create_pinlist_xlsx(data, filepath)
pkg, entries = parse_pinlist(filepath)
validation = validate_pinlist(entries, 2, 3)
validation = validate_pinlist(entries, 3, 4)
assert validation.is_valid, f"应验证通过: {validation.errors}"
# 6 % 4 != 0, 应有 info 提示
# 注意: validate_pinlist 返回的 ValidationResult 没有 infos 字段
# 但 info 消息是附加到 warnings 中的
# 实际上看代码infos 是单独列表,但 ValidationResult 只有 errors 和 warnings
# 所以 info 不会出现在 validation.warnings 中
# 让我们直接检查 validate_pinlist 的返回
# 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):
# 3×3 grid → 2*3 + 2*3 - 4 = 8 pins
entries = [PinListEntry(number=i, name=f'P{i}') for i in range(1, 9)]
entries = [PinListEntry(number=i, name=f'P{i}') for i in range(1, 13)]
layout = calculate_layout(entries, 3, 3)
# 验证四条边都有引脚
@@ -415,26 +402,25 @@ def test_list_to_map(r: TestRunner):
assert 'right' in layout, "应有right边"
assert 'top' in layout, "应有top边"
# 验证引脚数量分配
# left: rows=3, bottom: cols-1=2, right: rows-2=1, top: cols-1=2
# 验证引脚数量分配 (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) == 2, f"bottom应有2个引脚, 实际: {len(layout['bottom'].pins)}"
assert len(layout['right'].pins) == 1, f"right应有1个引脚, 实际: {len(layout['right'].pins)}"
assert len(layout['top'].pins) == 2, f"top应有2个引脚, 实际: {len(layout['top'].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 == 8, f"总引脚数应为8, 实际: {total}"
assert total == 12, f"总引脚数应为12, 实际: {total}"
# 验证逆时针顺序: left(1,2,3) → bottom(4,5) → right(6) → top(7,8)
# 验证逆时针顺序: 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] == 6, "right应为Pin6"
assert layout['top'].pins[0][0] == 7, "top第一个应为Pin7"
assert layout['top'].pins[1][0] == 8, "top第二个应为Pin8"
assert layout['right'].pins[0][0] == 7, "right应为Pin7"
assert layout['top'].pins[0][0] == 10, "top第一个应为Pin10"
result.ok(f"布局计算正确: left=3, bottom=2, right=1, top=2, 逆时针顺序正确")
result.ok(f"布局计算正确: left=3, bottom=3, right=3, top=3, 逆时针顺序正确")
r.run("TC-LM-009: 布局计算正确性", _tc_lm_009)
@@ -470,10 +456,10 @@ def test_list_to_map(r: TestRunner):
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):
# 创建4×4 PinList (8 pins, 3×3 grid)
data = {'A1': 'QFP-8'}
for i in range(1, 9):
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)
@@ -486,26 +472,26 @@ def test_list_to_map(r: TestRunner):
# 读取输出并验证
out_cells = read_xlsx_cells(outpath)
assert out_cells[(0, 0)] == 'QFP-8', f"A1应为QFP-8, 实际: {out_cells.get((0,0))}"
assert out_cells[(0, 0)] == 'QFP-12', f"A1应为QFP-12, 实际: {out_cells.get((0,0))}"
# 验证所有8个引脚序号都在输出中
# 验证所有12个引脚序号都在输出中
found_nums = set()
for (row, col), val in out_cells.items():
if val.isdigit() and int(val) >= 1:
found_nums.add(int(val))
assert found_nums == {1, 2, 3, 4, 5, 6, 7, 8}, \
f"应包含1-8所有序号, 实际: {sorted(found_nums)}"
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-Pin8")
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):
# 创建一个符合周长公式的 PinList → PinMAP → PinList roundtrip
# 3×3 grid → 2*3+2*3-4=8 pins
data = {'A1': 'QFP-8'}
for i in range(1, 9):
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)
@@ -523,15 +509,15 @@ def test_list_to_map(r: TestRunner):
rt_validation = validate_pinmap(rt_pinmap)
rt_pinlist = generate_pinlist(rt_pinmap, rt_validation)
assert len(rt_pinlist.rows) == 8, \
f"Roundtrip后应有8个引脚, 实际: {len(rt_pinlist.rows)}"
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(8) → PinMAP(3×3) → PinList({len(rt_pinlist.rows)}), 序号一致")
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)