v1.5.4 Bug 修复:模板文件名修正 + 布局重设计

BUG-005: 模板文件名改为 PinMAP-Template.xlsx / PinList-Template.xlsx
BUG-006: 布局改为 Number 外侧 + Name 里侧(v1.5.4 最终版)
- 从边界往中心:第1圈=Number,第2圈=Name
- 上边角点例外处理,15种网格无冲突
- 18/18 单元测试 + 37/37 集成测试全部通过
This commit is contained in:
2026-06-09 08:27:11 +08:00
parent 91e1d93e18
commit d635ddbebe
19 changed files with 647 additions and 237 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -33,44 +33,44 @@ def wait_for_exit():
# ── Path helpers ────────────────────────────────────────────────────
def _find_balllist_template_path() -> str | None:
"""查找根目录下的 BallList-Template.xlsx。
def _find_pinlist_template_path() -> str | None:
"""查找根目录下的 PinList-Template.xlsx。
MAP→List 输出使用 BallList 模板(而非旧 PinMAP 模板)
MAP→List 输出使用 PinList 模板。
搜索顺序:
1. 与 run.bat 同级的根目录
2. 当前工作目录
"""
src_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
template_path = os.path.join(root_dir, "BallList-Template.xlsx")
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
if os.path.exists(template_path):
return template_path
cwd_template = os.path.join(os.getcwd(), "BallList-Template.xlsx")
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
if os.path.exists(cwd_template):
return cwd_template
return None
def _find_ballmap_template_path() -> str | None:
"""查找根目录下的 BallMAP-Template.xlsx。
def _find_pinmap_template_path() -> str | None:
"""查找根目录下的 PinMAP-Template.xlsx。
List→MAP 输出使用 BallMAP 模板(而非旧 PinMAP 模板
List→MAP 输出使用 PinMAP 模板。
搜索顺序:
1. 与 run.bat 同级的根目录
2. 当前工作目录
"""
src_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
template_path = os.path.join(root_dir, "BallMAP-Template.xlsx")
template_path = os.path.join(root_dir, "PinMAP-Template.xlsx")
if os.path.exists(template_path):
return template_path
cwd_template = os.path.join(os.getcwd(), "BallMAP-Template.xlsx")
cwd_template = os.path.join(os.getcwd(), "PinMAP-Template.xlsx")
if os.path.exists(cwd_template):
return cwd_template
@@ -171,17 +171,17 @@ def run_map_to_list(filepath: str):
data[f'A{row}'] = pin_name
data[f'B{row}'] = str(pin_num)
# 尝试读取 BallList 模板样式F009
template_path = _find_balllist_template_path()
# 尝试读取 PinList 模板样式
template_path = _find_pinlist_template_path()
template_style = None
if template_path:
template_style = read_template_styles(template_path)
if template_style:
print(f"[INFO] 已加载 BallList 模板样式: {template_path}")
print(f"[INFO] 已加载 PinList 模板样式: {template_path}")
else:
print("[WARN] BallList 模板文件存在但解析失败,使用默认样式")
print("[WARN] PinList 模板文件存在但解析失败,使用默认样式")
else:
print("[INFO] 未检测到 BallList-Template.xlsx使用默认样式")
print("[INFO] 未检测到 PinList-Template.xlsx使用默认样式")
if template_style is not None:
write_xlsx_with_style(data, output_path, template_style)
@@ -281,17 +281,17 @@ def run_list_to_map(filepath: str):
print(f"[INFO] 正在生成 PinMAP 并写入: {output_path}")
try:
# 尝试读取 BallMAP 模板样式F010
template_path = _find_ballmap_template_path()
# 尝试读取 PinMAP 模板样式
template_path = _find_pinmap_template_path()
template_style = None
if template_path:
template_style = read_template_styles(template_path)
if template_style:
print(f"[INFO] 已加载 BallMAP 模板样式: {template_path}")
print(f"[INFO] 已加载 PinMAP 模板样式: {template_path}")
else:
print("[WARN] BallMAP 模板文件存在但解析失败,使用默认样式")
print("[WARN] PinMAP 模板文件存在但解析失败,使用默认样式")
else:
print("[INFO] 未检测到 BallMAP-Template.xlsx使用默认样式")
print("[INFO] 未检测到 PinMAP-Template.xlsx使用默认样式")
generate_pinmap(
entries=entries,

View File

@@ -56,12 +56,11 @@ def generate_pinmap(
# 先写入 PinName 单元格
for edge_name, edge in layout.items():
for (pin_num, pin_name), num_cell in zip(edge.pins, edge.cells):
name_cell = get_name_cell(num_cell, edge_name)
name_cell = get_name_cell(num_cell, edge_name, cols=cols)
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1])
data[name_ref] = pin_name if pin_name and pin_name.strip() else "NC"
# 再写入序号单元格(覆盖同位置的名字,确保序号优先
# v1.3: 角点单元格被两条边共享,需写入两个引脚序号
# 再写入序号单元格(v1.5.4:无边角共享,每个序号独占一个单元格
cell_pins: dict[str, list[str]] = {}
for edge_name, edge in layout.items():
for (pin_num, pin_name), num_cell in zip(edge.pins, edge.cells):

View File

@@ -12,7 +12,26 @@ Edge assignment (counter-clockwise, top-left = pin 1):
Total: rows + cols + rows + cols = 2×rows + 2×cols = (rows + cols) × 2
v1.3: 每条边独立包含其端点,角点单元格会被两条边共享
v1.5.4: 从网格边界往中心走,第一圈全是 Number第二圈全是 Name
每条边独立包含其端点,所有单元格互不冲突。
Coordinate system (0-based):
Number (outer ring, 1st circle from boundary):
left: (2..rows+1, 0)
bottom: (rows+3, 1..cols)
right: (rows+1..2, cols+1) [reverse order]
top: (1, cols..1) [reverse order]
Name (inner ring, 2nd circle from boundary):
left: (2..rows+1, 1)
bottom: (rows+2, 1..cols)
right: (rows+1..2, cols) [reverse order]
top: (2, cols-1..2) [interior, reverse order]
+ (1, 0) [top-left corner exception]
+ (1, cols+1) [top-right corner exception]
Pin1: Number (2,0), Name (2,1) — top-left of left edge
"""
from models import PinListEntry, EdgePins, LayoutError
@@ -86,28 +105,29 @@ def calculate_layout(
top_pins = entries[idx: idx + top_count]
# ── 计算单元格坐标 ────────────────────────────────────────────
# ── 计算单元格坐标v1.5.4Number 外侧 + Name 里侧,无冲突)──
#
# 网格坐标体系0-based
# 方形区域:行 [1..rows],列 [0..cols]
# 左边: 序号在 (r, 0), Name 在 (r, 1) 其中 r ∈ [1, rows]
# 下边: 序号在 (rows, c), Name 在 (rows-1, c) 其中 c ∈ [1, cols]
# 右边: 序号在 (r, cols), Name 在 (r, cols-1) 其中 r ∈ [rows, 1] 逆序
# 上边: 序号在 (1, c), Name 在 (2, c) 其中 c ∈ [cols, 1] 逆序
# 从网格边界往中心走,第一圈全是 Number第二圈全是 Name
#
# v1.3: 每条边独立包含其端点,角点单元格会被两条边共享
# 左边: Number (r, 0) r ∈ [2, rows+1] Name (r, 1)
# 下边: Number (rows+3, c) c ∈ [1, cols] Name (rows+2, c)
# 右边: Number (r, cols+1) r ∈ [rows+1, 2] Name (r, cols) 逆序
# 上边: Number (1, c) c ∈ [cols, 1] Name — 见 get_name_cell 逆序
# 上边 Name: (2, cols-1..2) 内部 + (1,0)(1,cols+1) 角点例外
#
# Pin1: Number (2,0) = A3, Name (2,1) = B3 — 左上角
# 左边:从上到下
left_cells = [(r, 0) for r in range(1, rows + 1)]
# 左边:从上到下 (rows 个)
left_cells = [(r, 0) for r in range(2, rows + 2)]
# 下边:从左到右
bottom_cells = [(rows, c) for c in range(1, cols + 1)]
# 下边:从左到右 (cols 个)Number 在最底行 rows+3
bottom_cells = [(rows + 3, c) for c in range(1, cols + 1)]
# 右边:从下到上(逆序
right_cells = [(r, cols) for r in range(rows, 0, -1)]
# 右边:从下到上 (rows 个)Number 在 cols+1 列(右扩一列
right_cells = [(r, cols + 1) for r in range(rows + 1, 1, -1)]
# 上边:从右到左(逆序)
# 上边:从右到左 (cols 个)
top_cells = [(1, c) for c in range(cols, 0, -1)]
# ── 构建 EdgePins ─────────────────────────────────────────────
@@ -124,16 +144,22 @@ def calculate_layout(
}
def get_name_cell(num_cell: tuple[int, int], edge_name: str) -> tuple[int, int]:
def get_name_cell(num_cell: tuple[int, int], edge_name: str,
cols: int = 0) -> tuple[int, int]:
"""
根据序号单元格坐标和边名称,计算对应的 PinName 单元格坐标。
v1.5.4: 第二圈 Name 紧挨第一圈 Number 内侧。
上边角点会有例外(位于 row 1以避免与左/右边 Name 冲突。
Parameters
----------
num_cell : tuple[int, int]
序号单元格坐标 (row, col) 0-based
edge_name : str
"left" | "bottom" | "right" | "top"
cols : int
网格列数(上边检测角点例外时需要),默认 0
Returns
-------
@@ -142,12 +168,19 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str) -> tuple[int, int]:
"""
r, c = num_cell
if edge_name == "left":
return (r, c + 1) # Name 在序号右侧
return (r, c + 1) # Name 在 Number 右侧 (col 1)
elif edge_name == "bottom":
return (r - 1, c) # Name 在序号上方
return (r - 1, c) # Name 在 Number 上方 (row rows+2)
elif edge_name == "right":
return (r, c - 1) # Name 在序号左侧
return (r, c - 1) # Name 在 Number 左侧 (col cols)
elif edge_name == "top":
return (r + 1, c) # Name 在序号下方
# Top Number 在 (1, c), c ∈ [cols..1]
# 内部列: Name 在 Number 下方 (2, c)
# 角点例外: 放在 row 1 例外位置,避免与左/右边 Name (2,1)/(2,cols) 冲突
if c == 1:
return (1, 0) # top-left corner → A2
elif c == cols:
return (1, cols + 1) # top-right corner → (1, cols+1)
return (r + 1, c) # 内部: Name 在 Number 下方 (row 2)
else:
raise LayoutError(f"未知的边名称: {edge_name}")

View File

@@ -4,6 +4,8 @@ Reads a dict of {(row, col): str} cells (as produced by xls_reader / xlsx_reader
detects the rectangular PinMAP boundary, and extracts pins in
counter-clockwise order starting from the top-left corner.
v1.5.4: 支持 Number-外侧 + Name-里侧的双圈布局解析。
Usage
-----
>>> from pinmap_parser import parse_pinmap
@@ -82,40 +84,55 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
if not package_info or not str(package_info).strip():
raise StructureError("A1 单元格为空,缺少封装信息")
# ── Step 3: build name lookup ────────────────────────────────
# ── Step 3: build name lookup (v1.5.4 layout) ──────────────
# For each edge, pin names live in the cell *adjacent inward*
# from the boundary cell that holds the pin number.
#
# v1.5.4 layout:
# left : number at (r, min_col), name at (r, min_col+1)
# bottom : number at (max_row, c), name at (max_row-1, c)
# right : number at (r, max_col), name at (r, max_col-1)
# top : number at (min_row, c), name at (min_row+1, c)
# BUT corner cols (c=min_col, c=max_col) have exceptional
# names at (min_row-1, min_col) and (min_row-1, max_col+1)
name_map: dict[tuple[int, int], str] = {}
# left edge names
# left edge names: adjacent inward (r, min_col+1)
for r in range(min_row, max_row + 1):
name = cells.get((r, min_col + 1), "")
if name and str(name).strip():
name_map[(r, min_col)] = str(name).strip()
# bottom edge names
# bottom edge names: adjacent upward (max_row-1, c)
for c in range(min_col, max_col + 1):
name = cells.get((max_row - 1, c), "")
if name and str(name).strip():
name_map[(max_row, c)] = str(name).strip()
# right edge names
# right edge names: adjacent inward (r, max_col-1)
for r in range(min_row, max_row + 1):
name = cells.get((r, max_col - 1), "")
if name and str(name).strip():
name_map[(r, max_col)] = str(name).strip()
# top edge names
# top edge names: standard lookup at (min_row+1, c) for interior cols.
# Corner names are at special positions:
# - top-left corner name at (min_row, min_col) → Number at (min_row, min_col+1)
# - top-right corner name at (min_row, max_col) → Number at (min_row, max_col-1)
for c in range(min_col, max_col + 1):
name = cells.get((min_row + 1, c), "")
if name and str(name).strip():
name_map[(min_row, c)] = str(name).strip()
# Override with corner exceptions if present
left_corner = cells.get((min_row, min_col), "")
if left_corner and str(left_corner).strip():
# Belongs to top-left Number at (min_row, min_col+1)
name_map[(min_row, min_col + 1)] = str(left_corner).strip()
right_corner = cells.get((min_row, max_col), "")
if right_corner and str(right_corner).strip():
# Belongs to top-right Number at (min_row, max_col-1)
name_map[(min_row, max_col - 1)] = str(right_corner).strip()
# ── Step 4: walk edges counter-clockwise (v1.3 formula) ──────
# Each edge independently includes its endpoints (corners).

View File

@@ -16,36 +16,41 @@ from pinlist_generator import generate_pinlist
from utils import rc_to_cell_ref
# ── 4x4 example from the task description ────────────────────────
# 1-based Excel coords → 0-based (row, col):
# A4:1 A5:2 B4:Pin1 B5:Pin2 → left edge
# C7:3 D7:4 C6:Pin3 D6:Pin4 → bottom edge
# F5:5 F4:6 E5:Pin5 E4:Pin6 → right edge
# D2:7 C2:8 D3:Pin7 C3:Pin8 → top edge
# A1: "QFP-44" → package info
# ── 4x4 example (v1.5.4 layout) ───────────────────────────────
# Layout: rows=4, cols=4, 16 pins
# Left: Number A3..A6 (rows 2..5), Name B3..B6 (rows 2..5)
# Bottom: Number B8..E8 (row 7), Name B7..E7 (row 6)
# Right: Number F6..F3 (rows 5..2), Name E6..E3 (rows 5..2)
# Top: Number E2..B2 (row 1), Name D3..C3 (row 2 interior)
# + A2 (top-left corner exception), + F2 (top-right exception)
# A1: "QFP-44" = package info
#
# Pin1: Number A3=(2,0), Name B3=(2,1)
cells_4x4 = {
(0, 0): "QFP-44",
# left edge
(3, 0): "1",
(4, 0): "2",
(3, 1): "Pin1",
(4, 1): "Pin2",
# bottom edge
(6, 2): "3",
(6, 3): "4",
(5, 2): "Pin3",
(5, 3): "Pin4",
# right edge
(4, 5): "5",
(3, 5): "6",
(4, 4): "Pin5",
(3, 4): "Pin6",
# top edge
(1, 3): "7",
(1, 2): "8",
(2, 3): "Pin7",
(2, 2): "Pin8",
# left edge (rows 2..5, cols 0..1)
(2, 0): "1", (2, 1): "Pin1",
(3, 0): "2", (3, 1): "Pin2",
(4, 0): "3", (4, 1): "Pin3",
(5, 0): "4", (5, 1): "Pin4",
# bottom edge (rows 6..7, cols 1..4)
(6, 1): "Pin5", (6, 2): "Pin6", (6, 3): "Pin7", (6, 4): "Pin8",
(7, 1): "5", (7, 2): "6", (7, 3): "7", (7, 4): "8",
# right edge (rows 5..2, cols 4..5)
(5, 4): "Pin9", (5, 5): "9",
(4, 4): "Pin10", (4, 5): "10",
(3, 4): "Pin11", (3, 5): "11",
(2, 4): "Pin12", (2, 5): "12",
# top edge (row 1 Number, row 2 interior Name + corner exceptions)
# pin13: (1,4) Number, Name corner exception at (1,5)
# pin14: (1,3) Number, Name at (2,3)
# pin15: (1,2) Number, Name at (2,2)
# pin16: (1,1) Number, Name corner exception at (1,0)
(1, 4): "13", (1, 5): "Pin13",
(1, 3): "14", (2, 3): "Pin14",
(1, 2): "15", (2, 2): "Pin15",
(1, 1): "16", (1, 0): "Pin16",
}
@@ -53,19 +58,27 @@ def test_4x4_parse():
pm = parse_pinmap(cells_4x4)
assert pm.package_info == "QFP-44", f"package_info={pm.package_info}"
assert len(pm.pins) == 8, f"expected 8 pins, got {len(pm.pins)}"
assert len(pm.pins) == 16, f"expected 16 pins, got {len(pm.pins)}"
# Counter-clockwise order: left(top→bot) → bottom(left→right)
# → right(bot→top) → top(right→left)
expected = [
(1, "Pin1", "left"),
(2, "Pin2", "left"),
(3, "Pin3", "bottom"),
(4, "Pin4", "bottom"),
(5, "Pin5", "right"),
(6, "Pin6", "right"),
(7, "Pin7", "top"),
(8, "Pin8", "top"),
(3, "Pin3", "left"),
(4, "Pin4", "left"),
(5, "Pin5", "bottom"),
(6, "Pin6", "bottom"),
(7, "Pin7", "bottom"),
(8, "Pin8", "bottom"),
(9, "Pin9", "right"),
(10, "Pin10", "right"),
(11, "Pin11", "right"),
(12, "Pin12", "right"),
(13, "Pin13", "top"),
(14, "Pin14", "top"),
(15, "Pin15", "top"),
(16, "Pin16", "top"),
]
for i, (num, name, edge) in enumerate(expected):
p = pm.pins[i]
@@ -104,7 +117,7 @@ def test_missing_names_warning():
def test_duplicate_numbers():
cells = dict(cells_4x4)
cells[(6, 3)] = "1" # duplicate pin 1
cells[(3, 0)] = "1" # duplicate pin 1 (original at (2,0))
pm = parse_pinmap(cells)
vr = validate_pinmap(pm)
assert not vr.is_valid
@@ -114,7 +127,7 @@ def test_duplicate_numbers():
def test_gap_in_numbers():
cells = dict(cells_4x4)
cells[(6, 2)] = "10" # skip 3
cells[(7, 2)] = "10" # skip pin 6 (was "6" at (7,2))
pm = parse_pinmap(cells)
vr = validate_pinmap(pm)
assert not vr.is_valid
@@ -224,24 +237,19 @@ def test_12pin_square():
# ── F012: PinMAP 生成中上/下边 PinName 位置验证 ────────────
def test_f012_pinname_position():
"""验证 PinList→PinMAP 时下/上边 PinName 位置正确。
"""验证 PinList→PinMAP 时边 PinName 位置正确v1.5.4 布局)
F012 要求
- 边 Name 在 max_row-1序号上方
- 边 Name 在 min_row+1序号下方
v1.5.4 布局
- 边 Name 在 (2..rows+1, 1)
- 边 Name 在 (rows+2, 1..cols) ← 倒数第二行
- 右边 Name 在 (rows+1..2, cols)
- 上边 Name: interior (2, cols-1..2) + corner exceptions (1,0)(1,cols+1)
测试策略:
1. 构建 5×520 PinPinList 数据
2. 生成 PinMAP
3. 检查输出 cell 位置
4. 将生成的 PinMAP 再解析回 PinList做往返一致性验证
注意:
- 3×3 及以上网格中,(1,1) 既是左边第 1 个引脚 (row=1) 的
Name 位置((1,0)→(1,1)),又是上边最后 1 个引脚(row=1, c=1)
序号位置。这是网格布局的固有限制,非 F012 Bug。
- 因此往返验证仅检查数量和序号正确性,不要求所有 Name 完全
一致(角点区域可能被序号覆盖)。
"""
# ── 1. 构建 5×5 PinList 数据20 个引脚) ──────────────────
rows, cols = 5, 5
@@ -251,89 +259,76 @@ def test_f012_pinname_position():
]
package_info = "QFN-20"
# ── 2. 生成 PinMAP(不使用模板,纯逻辑验证) ───────────────
# ── 2. 生成 PinMAP ────────────────────────────────────────
data = generate_pinmap(
entries=entries,
rows=rows,
cols=cols,
package_info=package_info,
template_style=None,
output_path=None, # 不写入文件
output_path=None,
)
# ── 3. 检查单元格位置 ───────────────────────────────────────
# F012 验证:
# 5×5 网格坐标0-based
# min_row=1, max_row=5, min_col=0, max_col=5
# 预期:
# 边: 序号 (r,0) Name (r,1) r ∈ [1,5]
# 下边: 序号 (5,c) Name (4,c) = max_row-1 c ∈ [1,5]
# 右边: 序号 (r,5) Name (r,4) r ∈ [5,1] 逆序
# 上边: 序号 (1,c) Name (2,c) = min_row+1 c ∈ [5,1] 逆序
# ── 3. 检查单元格位置 (v1.5.4) ─────────────────────────────
# 5×5: rows=5, cols=5, 20 pins
# 左边: Number (2..6, 0), Name (2..6, 1)
# 下边: Number (8, 1..5), Name (7, 1..5)
# 右边: Number (6..2, 6), Name (6..2, 5)
# 边: Number (1, 5..1), Name interior (2, 4..2),
# corner exceptions (1,0) and (1,6)
# ── 3a. 验证下边 Name 位置 ─────────────────────────────────
# 下边序号在 (5, 1..5)Name 应在 (4, 1..5) = max_row-1
# ── 3a. 验证下边 Name 位置 (rows+2=7, 1..cols) ──────────
for c in range(1, cols + 1):
num_cell = (rows, c) # (5, c)
name_cell = (rows - 1, c) # (4, c) = max_row-1
num_ref = rc_to_cell_ref(num_cell[0], num_cell[1])
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1])
# 序号单元格应有值
assert num_ref in data, f"F012: 下边序号 {num_ref} 缺失"
# Name 单元格应在 max_row-1
num_ref = rc_to_cell_ref(rows + 3, c) # Number at row 8
name_ref = rc_to_cell_ref(rows + 2, c) # Name at row 7
assert num_ref in data, f"下边 Number {num_ref} 缺失"
assert name_ref in data, (
f"F012: 下边 Name 应在 {name_ref} (max_row-1), "
f"但未找到。序号在 {num_ref}"
f"下边 Name 应在 {name_ref} (rows+2), 但未找到。Number 在 {num_ref}"
)
# ── 3b. 验证上边 Name 位置 ─────────────────────────────────
# 上边序号在 (1, 5..1)Name 应在 (2, 5..1) = min_row+1
for c in range(cols, 0, -1):
num_cell = (1, c) # min_row=1
name_cell = (2, c) # min_row+1=2
num_ref = rc_to_cell_ref(num_cell[0], num_cell[1])
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1])
assert num_ref in data, f"F012: 上边序号 {num_ref} 缺失"
# Top pin layout: c=5 → (1,0)? No, top-right exception at (1,6)
# c=4 → (2,4), c=3 → (2,3), c=2 → (2,2)
# c=1 → (1,0) corner exception
for idx, c in enumerate(range(cols, 0, -1)):
num_ref = rc_to_cell_ref(1, c) # Number at row 1
assert num_ref in data, f"上边 Number {num_ref} 缺失"
if c == cols: # rightmost = top-right corner
name_ref = rc_to_cell_ref(1, cols + 1) # exception
elif c == 1: # leftmost = top-left corner
name_ref = rc_to_cell_ref(1, 0) # exception
else: # interior
name_ref = rc_to_cell_ref(2, c)
assert name_ref in data, (
f"F012: 上边 Name 应在 {name_ref} (min_row+1), "
f"但未找到。序号在 {num_ref}"
f"上边 Name 应在 {name_ref}, 但未找到。Number 在 {num_ref}"
)
# ── 3c. 验证左边 Name 位置 ──────────────────────────────────
for r in range(1, rows + 1):
num_cell = (r, 0)
name_cell = (r, 1)
num_ref = rc_to_cell_ref(num_cell[0], num_cell[1])
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1])
# ── 3c. 验证左边 Name 位置 (2..6, 1) ───────────────────
for r in range(2, rows + 2):
num_ref = rc_to_cell_ref(r, 0)
name_ref = rc_to_cell_ref(r, 1)
assert num_ref in data, f"左边 Number {num_ref} 缺失"
assert name_ref in data, f"左边 Name {name_ref} 缺失"
assert num_ref in data, f"F012: 左边序号 {num_ref} 缺失"
assert name_ref in data, f"F012: 左边 Name {name_ref} 缺失"
# ── 3d. 验证右边 Name 位置 ──────────────────────────────────
for r in range(rows, 0, -1):
num_cell = (r, cols)
name_cell = (r, cols - 1)
num_ref = rc_to_cell_ref(num_cell[0], num_cell[1])
name_ref = rc_to_cell_ref(name_cell[0], name_cell[1])
assert num_ref in data, f"F012: 右边序号 {num_ref} 缺失"
assert name_ref in data, f"F012: 右边 Name {name_ref} 缺失"
# ── 3d. 验证右边 Name 位置 (6..2, 5) ────────────────────
for r in range(rows + 1, 1, -1):
num_ref = rc_to_cell_ref(r, cols + 1)
name_ref = rc_to_cell_ref(r, cols)
assert num_ref in data, f"右边 Number {num_ref} 缺失"
assert name_ref in data, f"右边 Name {name_ref} 缺失"
# ── 4. 往返一致性验证PinMAP → PinList────────────────────
from utils import cell_ref_to_rc
# 将 data dict 转换为 PinMAP 解析器可读的 {(row,col): value} 格式
cell_data = {}
for ref, value in data.items():
cell_data[cell_ref_to_rc(ref)] = value
# 解析回 PinMAP
pm = parse_pinmap(cell_data)
assert len(pm.pins) == 20, f"往返: 预期 20 引脚,实际 {len(pm.pins)}"
# 验证引脚序号正确20 个引脚全部恢复)
actual_numbers = sorted([p.number for p in pm.pins])
expected_numbers = list(range(1, 21))
assert actual_numbers == expected_numbers, (
@@ -342,7 +337,6 @@ def test_f012_pinname_position():
f" 实际: {actual_numbers}"
)
# 验证 20 个引脚全部恢复
validation = validate_pinmap(pm)
assert validation.is_valid, (
f"往返验证失败: 错误={[e.message for e in validation.errors]}"
@@ -353,7 +347,6 @@ def test_f012_pinname_position():
f"往返 PinList: 预期 20 行,实际 {len(pinlist.rows)}"
)
# 验证序号从 1 到 20
for i, (name, num) in enumerate(pinlist.rows):
expected_num = i + 1
assert num == expected_num, (
@@ -367,18 +360,18 @@ def test_f012_pinname_position():
def test_template_path_generation():
"""验证两个模板查找函数返回正确的路径格式。"""
from main import _find_balllist_template_path, _find_ballmap_template_path
from main import _find_pinlist_template_path, _find_pinmap_template_path
result1 = _find_balllist_template_path()
result2 = _find_ballmap_template_path()
result1 = _find_pinlist_template_path()
result2 = _find_pinmap_template_path()
# 返回值要么是 str 要么是 None
assert result1 is None or isinstance(result1, str)
assert result2 is None or isinstance(result2, str)
# 两者应该是不同路径
if result1 and result2:
assert "BallList" in result1
assert "BallMAP" in result2
assert "PinList" in result1
assert "PinMAP" in result2
assert result1 != result2
print("✓ test_template_path_generation passed")