Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88a231424c | |||
| e582b454d3 | |||
| d635ddbebe | |||
| 91e1d93e18 | |||
| ce62d2f353 | |||
| d8d669bba1 | |||
| 22fc8b6228 | |||
| c271e6e807 | |||
| 13e7b8c4a5 | |||
| 351f56ecb5 |
12
.gitignore
vendored
12
.gitignore
vendored
@@ -16,6 +16,18 @@ build/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Agent metadata
|
||||||
|
.openclaw/
|
||||||
|
AGENTS.md
|
||||||
|
HEARTBEAT.md
|
||||||
|
IDENTITY.md
|
||||||
|
SOUL.md
|
||||||
|
TOOLS.md
|
||||||
|
USER.md
|
||||||
|
|
||||||
|
# Release archives (keep versioned release notes only)
|
||||||
|
Releases/*.zip
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
89
CHANGELOG.md
89
CHANGELOG.md
@@ -1,5 +1,94 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [v1.5.5] - 2026-06-12
|
||||||
|
|
||||||
|
### 🐛 Bug 修复(深度修复)
|
||||||
|
|
||||||
|
#### BUG-005 【高】模板文件名/路径错误
|
||||||
|
|
||||||
|
- **根因**:v1.5.4 只改了模板文件名(`BallList-Template.xlsx` → `PinList-Template.xlsx`),但未修正搜索路径
|
||||||
|
- **修复**:模板搜索路径优先查找 `Code/src/Template/` 目录,再回退项目根目录和当前工作目录
|
||||||
|
- 模板样式现在可正确应用到输出文件
|
||||||
|
|
||||||
|
#### BUG-006 【高】PinList→PinMAP 上边 Name 与左边 Name 同行
|
||||||
|
|
||||||
|
- **根因**:v1.5.4 将上边 Name 放在 row 2(Excel 第 3 行),与左边 Name/Number 起始行相同,导致 3 条边数据混在同一行
|
||||||
|
- **修复**:将上边 Name 移至 **row 0**(Excel 第 1 行),上边 Number 保持在 row 1(第 2 行),使上边完全独立于其他边
|
||||||
|
- 不再需要角点例外逻辑,代码更简洁
|
||||||
|
- 每条边数据独立分隔,肉眼可读性大幅提升
|
||||||
|
|
||||||
|
### 🔧 修改文件
|
||||||
|
|
||||||
|
- `Code/src/main.py` — BUG-005: 模板搜索路径修正(优先 Code/src/Template/)
|
||||||
|
- `Code/src/pinmap_layout.py` — BUG-006: 上边 Name 坐标改为 `(0, c)`,移除角点例外
|
||||||
|
- `Code/src/pinmap_parser.py` — BUG-006: 上边 Name 从 row 0 读取,Number 从 row 1 读取
|
||||||
|
- `Test/fixtures/sample_4x4.xlsx` — BUG-006: 更新为 v1.5.5 新布局
|
||||||
|
- `Code/src/test_pinmap.py` — BUG-006: 测试数据适配新布局
|
||||||
|
|
||||||
|
### ✅ 测试
|
||||||
|
- 全部 37 个测试通过
|
||||||
|
|
||||||
|
## [v1.5.4] - 2026-06-09
|
||||||
|
|
||||||
|
### 🐛 Bug 修复
|
||||||
|
|
||||||
|
#### BUG-005 【高】模板文件名错误
|
||||||
|
|
||||||
|
- 模板文件重命名:`BallList-Template.xlsx` → `PinList-Template.xlsx`,`BallMAP-Template.xlsx` → `PinMAP-Template.xlsx`
|
||||||
|
- 同步更新 `main.py` 中的函数名和模板引用路径
|
||||||
|
|
||||||
|
#### BUG-006 【高】布局重设计(Number 外侧 + Name 里侧)
|
||||||
|
|
||||||
|
- 重新设计 PinMAP 布局:从网格边界往中心走,第 1 圈 = Number(数字),第 2 圈 = Name(引脚名)
|
||||||
|
- **上边**:Number row 1(最顶行),Name row 2(第二行;角点例外:最左/右上边 Name 在 (1,0)/(1,cols+1))
|
||||||
|
- **左边**:Number col 0(最左列),Name col 1(第二列)
|
||||||
|
- **下边**:Number row rows+3(最底行),Name row rows+2(倒数第二行)
|
||||||
|
- **右边**:Number col cols+1(最右列),Name col cols(右二列)
|
||||||
|
- Pin1 保持在左上角(A3=1, B3=Pin1)
|
||||||
|
- 不再需要角点 "//" 合并,每条边不共享任何单元格
|
||||||
|
- 全部 15 种网格大小验证无冲突
|
||||||
|
- 18/18 单元测试 + 37/37 集成测试全部通过
|
||||||
|
|
||||||
|
### 🔧 修改文件
|
||||||
|
|
||||||
|
- `Code/src/main.py` — BUG-005: 模板函数和引用改名;BUG-006: 传递 cols 参数
|
||||||
|
- `Code/src/pinmap_layout.py` — BUG-006: 重写坐标公式 + `get_name_cell()` 支持 cols 参数 + 角点例外
|
||||||
|
- `Code/src/pinmap_generator.py` — BUG-006: 传递 cols 参数 + 更新注释
|
||||||
|
- `Code/src/pinmap_parser.py` — BUG-006: 重写边界检测、Name 读取(角点例外检测)
|
||||||
|
- `Code/src/test_pinmap.py` — BUG-006: 更新测试数据适配新布局
|
||||||
|
- `Test/fixtures/PinList-Template.xlsx` + `PinMAP-Template.xlsx` — BUG-005: 模板文件重命名
|
||||||
|
|
||||||
|
### 📝 文档
|
||||||
|
|
||||||
|
- 更新 `CHANGELOG.md` 追加 v1.5.4 版本日志
|
||||||
|
- 更新 `README.md` 追加 v1.5.4 版本说明
|
||||||
|
- 生成 `Releases/v1.5.4/CHANGELOG.md`
|
||||||
|
|
||||||
|
## [v1.5.0] - 2026-06-06
|
||||||
|
|
||||||
|
### ✨ 功能新增
|
||||||
|
|
||||||
|
- **F009 MAP→List 使用 balllist 模板**:`run_map_to_list()` 改查 `BallList-Template.xlsx`,不再共用旧模板
|
||||||
|
- **F010 List→MAP 使用 ballmap 模板**:`run_list_to_map()` 改查 `BallMAP-Template.xlsx`,模板完全分离
|
||||||
|
- **F011 模板格式提取式应用**:从模板的 cellXfs/fonts/borders/fills 提取实际样式定义,替换硬编码边框和对齐;无模板时完全回退到默认样式
|
||||||
|
- **F012 验证+回归测试**:新增 `test_f012_pinname_position()` 验证下边 Name 在 max_row-1、上边 Name 在 min_row+1,添加 5×5 往返一致性测试
|
||||||
|
|
||||||
|
### 🗑️ 废弃
|
||||||
|
|
||||||
|
- `_find_template_path()` (PinMAP-Template.xlsx) — 不再自动查找,由 `_find_balllist_template_path()` 和 `_find_ballmap_template_path()` 替代
|
||||||
|
|
||||||
|
### 📝 文档
|
||||||
|
|
||||||
|
- 更新 `docs/tasks.md` T015 状态为已完成
|
||||||
|
- 更新 `docs/features.md` F009-F012 状态
|
||||||
|
|
||||||
|
### 🔧 修改文件
|
||||||
|
|
||||||
|
- `Code/src/main.py` — 新增两个模板查找函数,修改两个方向的模板调用
|
||||||
|
- `Code/src/xlsx_writer.py` — 重写 `_styles_xml()` 支持模板样式提取
|
||||||
|
- `Code/src/template_reader.py` — 增强 cellXfs 提取(xfId、applyAlignment、wrapText)
|
||||||
|
- `Code/src/test_pinmap.py` — 新增 F012 回归测试
|
||||||
|
|
||||||
## [v1.3.15] - 2026-06-01
|
## [v1.3.15] - 2026-06-01
|
||||||
|
|
||||||
### 🐛 Bug 修复
|
### 🐛 Bug 修复
|
||||||
|
|||||||
@@ -204,13 +204,24 @@ PinMAP 的引脚分布在四条边上,总引脚数由网格尺寸决定:
|
|||||||
|
|
||||||
> **提示**:如果不确定尺寸,可以先用公式反推:`行数 + 列数 = (引脚数 + 4) / 2`,然后根据需要调整行和列的比例。
|
> **提示**:如果不确定尺寸,可以先用公式反推:`行数 + 列数 = (引脚数 + 4) / 2`,然后根据需要调整行和列的比例。
|
||||||
|
|
||||||
### 模板文件说明
|
### 模板文件说明(v1.5.0)
|
||||||
|
|
||||||
PinList → PinMAP 转换时,程序会自动尝试从输入文件所在目录读取模板样式:
|
从 v1.5.0 开始,两个方向的转换使用各自独立的模板文件:
|
||||||
|
|
||||||
- **模板来源**:程序会尝试解析与输入文件同名的 `.xlsx` 模板文件中的样式信息
|
| 转换方向 | 模板文件 | 查找位置 |
|
||||||
- **提取内容**:字体(名称、大小、粗体、斜体、颜色)、填充、边框、列宽、行高
|
|----------|----------|----------|
|
||||||
- **优雅降级**:如果模板不存在或解析失败,程序会自动使用默认样式,不影响转换流程
|
| **MAP→List** | `BallList-Template.xlsx` | 项目根目录 → 当前工作目录 |
|
||||||
|
| **List→MAP** | `BallMAP-Template.xlsx` | 项目根目录 → 当前工作目录 |
|
||||||
|
|
||||||
|
#### 模板格式提取
|
||||||
|
|
||||||
|
程序从模板的 OOXML 中**提取**具体的样式定义(字体、边框、填充、对齐、列宽、行高),然后应用到输出文件。这种方式确保即使模板结构复杂也能正确提取关键样式属性。
|
||||||
|
|
||||||
|
#### 优雅降级
|
||||||
|
|
||||||
|
- 模板文件不存在 → 使用硬编码默认样式(Calibri 11pt、thin 边框、居中)
|
||||||
|
- 模板解析失败(损坏/格式异常)→ 优雅回退到默认样式
|
||||||
|
- 模板中某些样式属性缺失 → 仅应用可用属性,其余保持默认
|
||||||
|
|
||||||
### 使用示例
|
### 使用示例
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
在 IC 封装设计中,PinMAP 以方形/长方形矩阵形式展示引脚分布,而 PinList 则以线性列表形式提供引脚序号对照。本项目通过纯 Python 实现,自动完成 PinMAP 与 PinList 之间的双向转换,支持 `.xls` 和 `.xlsx` 两种格式。
|
在 IC 封装设计中,PinMAP 以方形/长方形矩阵形式展示引脚分布,而 PinList 则以线性列表形式提供引脚序号对照。本项目通过纯 Python 实现,自动完成 PinMAP 与 PinList 之间的双向转换,支持 `.xls` 和 `.xlsx` 两种格式。
|
||||||
|
|
||||||
**版本**: v1.2.0
|
**版本**: v1.5.0
|
||||||
**发布日期**: 2026-05-28
|
**发布日期**: 2026-06-06
|
||||||
**运行平台**: Windows(tkinter GUI)/ Linux(命令行回退)
|
**运行平台**: Windows(tkinter GUI)/ Linux(命令行回退)
|
||||||
**技术栈**: Python 标准库,零第三方依赖
|
**技术栈**: Python 标准库,零第三方依赖
|
||||||
|
|
||||||
@@ -27,7 +27,8 @@
|
|||||||
| **PinMAP → PinList** | 自动识别方形/长方形结构,沿四条边(左→下→右→上)逆时针提取引脚,生成 PinList |
|
| **PinMAP → PinList** | 自动识别方形/长方形结构,沿四条边(左→下→右→上)逆时针提取引脚,生成 PinList |
|
||||||
| **PinList → PinMAP** | 根据引脚列表和网格尺寸,自动计算布局并生成 PinMAP |
|
| **PinList → PinMAP** | 根据引脚列表和网格尺寸,自动计算布局并生成 PinMAP |
|
||||||
| **数据验证** | 双向验证:检测序号不连续、序号重复、PinName 缺失、A1 封装信息缺失、周长匹配 |
|
| **数据验证** | 双向验证:检测序号不连续、序号重复、PinName 缺失、A1 封装信息缺失、周长匹配 |
|
||||||
| **模板样式** | PinList → PinMAP 时自动读取模板文件的字体、填充、边框、列宽、行高等样式 |
|
| **模板样式** | MAP→List 使用 **BallList-Template.xlsx**,List→MAP 使用 **BallMAP-Template.xlsx**,模板完全分离 |
|
||||||
|
| **模板格式提取** | 从模板的 cellXfs/fonts/borders/fills 提取实际样式定义,替换硬编码边框和对齐;无模板时完全回退到默认样式 |
|
||||||
| **双格式支持** | 同时支持 `.xls`(BIFF8 引擎)和 `.xlsx`(OOXML 引擎) |
|
| **双格式支持** | 同时支持 `.xls`(BIFF8 引擎)和 `.xlsx`(OOXML 引擎) |
|
||||||
| **双模式运行** | GUI 文件选择对话框 + 命令行参数模式 |
|
| **双模式运行** | GUI 文件选择对话框 + 命令行参数模式 |
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
| `pinlist_validator.py` | PinList 数据验证 | `collections.Counter` |
|
| `pinlist_validator.py` | PinList 数据验证 | `collections.Counter` |
|
||||||
| `pinlist_generator.py` | PinList 生成 | 纯 Python |
|
| `pinlist_generator.py` | PinList 生成 | 纯 Python |
|
||||||
| `validator.py` | PinMAP 数据验证 | `collections.Counter` |
|
| `validator.py` | PinMAP 数据验证 | `collections.Counter` |
|
||||||
| `template_reader.py` | 模板样式提取 | `zipfile`, `xml.etree.ElementTree` |
|
| `template_reader.py` | 模板样式提取(含 cellXfs/xfId/applyAlignment/wrapText) | `zipfile`, `xml.etree.ElementTree` |
|
||||||
| `models.py` | 数据模型 | `dataclasses` |
|
| `models.py` | 数据模型 | `dataclasses` |
|
||||||
| `utils.py` | 工具函数 | 纯 Python |
|
| `utils.py` | 工具函数 | 纯 Python |
|
||||||
|
|
||||||
@@ -85,6 +86,38 @@
|
|||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
|
### 模板使用说明(v1.5.0)
|
||||||
|
|
||||||
|
从 v1.5.0 开始,两个方向的转换使用各自独立的模板文件:
|
||||||
|
|
||||||
|
| 转换方向 | 模板文件 | 查找位置 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| **MAP→List** | `BallList-Template.xlsx` | 项目根目录 → 当前工作目录 |
|
||||||
|
| **List→MAP** | `BallMAP-Template.xlsx` | 项目根目录 → 当前工作目录 |
|
||||||
|
|
||||||
|
#### 模板格式提取机制
|
||||||
|
|
||||||
|
程序从模板的 OOXML styles.xml 和 sheet1.xml 中**提取**具体的样式定义(字体、边框、填充、对齐、列宽、行高),然后写入输出的 `<styleSheet>` 中。这种方式是**提取式**(读取具体属性值)而非直接复制 cellXf 引用,确保即使模板结构复杂也能正确提取关键样式属性。
|
||||||
|
|
||||||
|
```
|
||||||
|
xl/styles.xml:
|
||||||
|
├── fonts: name, size, bold, italic, color
|
||||||
|
├── fills: pattern_type, fg_color
|
||||||
|
├── borders: top, bottom, left, right (style + color)
|
||||||
|
└── cellXfs: numFmtId, fontId, fillId, borderId, alignment
|
||||||
|
(含 xfId, applyAlignment, wrapText)
|
||||||
|
|
||||||
|
xl/worksheets/sheet1.xml:
|
||||||
|
├── cols: column width (min, max, width)
|
||||||
|
└── sheetData: row height
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 优雅降级
|
||||||
|
|
||||||
|
- 模板文件不存在 → 使用硬编码默认样式(Calibri 11pt、thin 边框、居中)
|
||||||
|
- 模板解析失败(损坏/格式异常)→ 优雅回退到默认样式
|
||||||
|
- 模板中某些样式属性缺失 → 仅应用可用属性,其余保持默认
|
||||||
|
|
||||||
### 前提条件
|
### 前提条件
|
||||||
|
|
||||||
- Python 3.6+(推荐 3.8+)
|
- Python 3.6+(推荐 3.8+)
|
||||||
@@ -326,7 +359,12 @@ pinmap-to-pinlist/
|
|||||||
│ │ ├── error_gap.xlsx # 序号不连续测试
|
│ │ ├── error_gap.xlsx # 序号不连续测试
|
||||||
│ │ ├── error_dup.xlsx # 序号重复测试
|
│ │ ├── error_dup.xlsx # 序号重复测试
|
||||||
│ │ ├── error_empty_a1.xlsx # A1 为空测试
|
│ │ ├── error_empty_a1.xlsx # A1 为空测试
|
||||||
│ │ └── warning_missing.xlsx # PinName 缺失测试
|
│ │ ├── warning_missing.xlsx # PinName 缺失测试
|
||||||
|
│ │ ├── BallList-Template.xlsx # MAP→List 样式模板(测试用)
|
||||||
|
│ │ ├── BallMAP-Template.xlsx # List→MAP 样式模板(测试用)
|
||||||
|
│ │ ├── template_corrupt.xlsx # 损坏模板回退测试
|
||||||
|
│ │ ├── template_minimal.xlsx # 最小模板测试
|
||||||
|
│ │ └── template_narrow.xlsx # 窄列宽模板测试
|
||||||
│ └── test_report.md # 测试报告
|
│ └── test_report.md # 测试报告
|
||||||
├── README.md # 项目根目录 README
|
├── README.md # 项目根目录 README
|
||||||
├── CHANGELOG.md # 变更日志
|
├── CHANGELOG.md # 变更日志
|
||||||
@@ -341,6 +379,8 @@ pinmap-to-pinlist/
|
|||||||
|
|
||||||
运行 `python test_pinmap.py`(在 `Code/src/` 目录下):
|
运行 `python test_pinmap.py`(在 `Code/src/` 目录下):
|
||||||
|
|
||||||
|
#### 基础功能测试(v1.0−v1.2)
|
||||||
|
|
||||||
| 测试用例 | 说明 | 状态 |
|
| 测试用例 | 说明 | 状态 |
|
||||||
|----------|------|------|
|
|----------|------|------|
|
||||||
| `test_4x4_parse` | 4×4 方形 PinMAP 解析 | ✅ 通过 |
|
| `test_4x4_parse` | 4×4 方形 PinMAP 解析 | ✅ 通过 |
|
||||||
@@ -350,8 +390,29 @@ pinmap-to-pinlist/
|
|||||||
| `test_gap_in_numbers` | 序号不连续检测 | ✅ 通过 |
|
| `test_gap_in_numbers` | 序号不连续检测 | ✅ 通过 |
|
||||||
| `test_empty_cells` | 空单元格处理 | ✅ 通过 |
|
| `test_empty_cells` | 空单元格处理 | ✅ 通过 |
|
||||||
| `test_no_pins` | 无引脚数据检测 | ✅ 通过 |
|
| `test_no_pins` | 无引脚数据检测 | ✅ 通过 |
|
||||||
|
| `test_rectangular_parse` | 长方形 PinMAP 解析 | ✅ 通过 |
|
||||||
| `test_12pin_square` | 12 引脚方形解析 | ✅ 通过 |
|
| `test_12pin_square` | 12 引脚方形解析 | ✅ 通过 |
|
||||||
|
|
||||||
|
#### F012 回归测试(v1.5.0 新增)
|
||||||
|
|
||||||
|
| 测试用例 | 说明 | 状态 |
|
||||||
|
|----------|------|------|
|
||||||
|
| `test_f012_pinname_position` | 5×5 往返一致性 + 上/下边 PinName 位置验证 | ✅ 通过 |
|
||||||
|
|
||||||
|
#### F011 模板格式提取测试(v1.5.0 新增)
|
||||||
|
|
||||||
|
| 测试用例 | 说明 | 状态 |
|
||||||
|
|----------|------|------|
|
||||||
|
| `test_template_path_generation` | 两个模板查找函数返回正确路径格式 | ✅ 通过 |
|
||||||
|
| `test_f011_default_styles_xml` | 无模板时回退到硬编码默认样式 | ✅ 通过 |
|
||||||
|
| `test_f011_template_fonts_in_styles_xml` | 有模板时使用模板字体信息 | ✅ 通过 |
|
||||||
|
| `test_f011_output_dims_determined_by_pins` | 输出行列由引脚数决定,非模板 | ✅ 通过 |
|
||||||
|
| `test_f011_template_borders_in_styles_xml` | 有模板时使用模板边框信息 | ✅ 通过 |
|
||||||
|
| `test_f011_template_fills_in_styles_xml` | 有模板时使用模板填充信息 | ✅ 通过 |
|
||||||
|
| `test_template_empty_fonts_fallback` | 空字体回退到默认 | ✅ 通过 |
|
||||||
|
| `test_template_color_prefix_auto_fix` | 颜色值 `#` 前缀自动修复 | ✅ 通过 |
|
||||||
|
| `test_template_no_styles_xml` | 无 styles.xml 时优雅降级 | ✅ 通过 |
|
||||||
|
|
||||||
### 集成测试
|
### 集成测试
|
||||||
|
|
||||||
| 测试用例 | 输入文件 | 说明 | 状态 |
|
| 测试用例 | 输入文件 | 说明 | 状态 |
|
||||||
@@ -363,7 +424,7 @@ pinmap-to-pinlist/
|
|||||||
| TC005 | `warning_missing.xlsx` | PinName 缺失警告 | ✅ 通过 |
|
| TC005 | `warning_missing.xlsx` | PinName 缺失警告 | ✅ 通过 |
|
||||||
| TC006 | `error_empty_a1.xlsx` | A1 为空检测 | ✅ 通过 |
|
| TC006 | `error_empty_a1.xlsx` | A1 为空检测 | ✅ 通过 |
|
||||||
|
|
||||||
**结论**:所有测试用例通过,无阻塞性问题。详见 `Test/test_report.md`。
|
**结论**:所有 18 个单元测试 + 6 个集成测试全部通过,无阻塞性问题。详见 `Test/test_report.md`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,165 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v1.5.0 — 2026-06-06
|
||||||
|
|
||||||
|
### ✨ 模板分离与格式提取增强
|
||||||
|
|
||||||
|
v1.5.0 将两个方向的模板完全分离,并实现了**提取式**模板格式应用机制,不再依赖硬编码的边框和对齐属性。新增 F012 回归测试确保上/下边 PinName 位置正确。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 新增功能
|
||||||
|
|
||||||
|
#### F009:MAP→List 使用 BallList-Template(独立模板)
|
||||||
|
- `run_map_to_list()` 改查 `BallList-Template.xlsx`
|
||||||
|
- 不再共用旧模板 `PinMAP-Template.xlsx`
|
||||||
|
- 新增 `_find_balllist_template_path()` 查找函数
|
||||||
|
|
||||||
|
#### F010:List→MAP 使用 BallMAP-Template(独立模板)
|
||||||
|
- `run_list_to_map()` 改查 `BallMAP-Template.xlsx`
|
||||||
|
- 模板完全分离,互不影响
|
||||||
|
- 新增 `_find_ballmap_template_path()` 查找函数
|
||||||
|
- 废弃 `_find_template_path()`(PinMAP-Template.xlsx)
|
||||||
|
|
||||||
|
#### F011:模板格式提取式应用
|
||||||
|
- 从模板的 cellXfs/fonts/borders/fills 提取实际样式定义
|
||||||
|
- 替换之前硬编码的 thin 边框和 center 对齐
|
||||||
|
- 支持 xfId、applyAlignment、wrapText 等属性的提取
|
||||||
|
- 无模板时完全回退到默认样式(Calibri 11pt、thin 边框、居中)
|
||||||
|
|
||||||
|
#### F012:上/下边 PinName 位置回归测试
|
||||||
|
- 新增 `test_f012_pinname_position()` 验证下边 Name 在 `max_row-1`、上边 Name 在 `min_row+1`
|
||||||
|
- 新增 5×5 往返一致性测试(PinList → PinMAP 后再解析验证)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
|
||||||
|
| 文件 | 变更说明 |
|
||||||
|
|------|----------|
|
||||||
|
| `Code/src/main.py` | 新增 `_find_balllist_template_path()` 和 `_find_ballmap_template_path()`;修改两个方向的模板调用 |
|
||||||
|
| `Code/src/xlsx_writer.py` | 重写 `_styles_xml()` 支持模板样式提取(fonts/fills/borders/cellXfs 动态生成) |
|
||||||
|
| `Code/src/template_reader.py` | 增强 cellXfs 提取(xfId、applyAlignment、wrapText),颜色 `#` 前缀自动修复 |
|
||||||
|
| `Code/src/test_pinmap.py` | 新增 F012 回归测试 + F011 模板格式提取测试共 12 个测试用例 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 技术实现
|
||||||
|
|
||||||
|
#### 模板查找逻辑
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _find_balllist_template_path() -> str | None:
|
||||||
|
"""查找顺序:项目根目录 → 当前工作目录"""
|
||||||
|
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
# 1. 项目根目录
|
||||||
|
template_path = os.path.join(root_dir, "BallList-Template.xlsx")
|
||||||
|
if os.path.isfile(template_path):
|
||||||
|
return template_path
|
||||||
|
# 2. 当前工作目录
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "BallList-Template.xlsx")
|
||||||
|
if os.path.isfile(cwd_template):
|
||||||
|
return cwd_template
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
`_find_ballmap_template_path()` 同理,查找 `BallMAP-Template.xlsx`。
|
||||||
|
|
||||||
|
#### 样式提取式应用
|
||||||
|
|
||||||
|
```
|
||||||
|
模板 styles.xml
|
||||||
|
│
|
||||||
|
▼ 读取字体、填充、边框定义
|
||||||
|
▼ 读取 cellXfs 引用
|
||||||
|
▼ 读取列宽、行高
|
||||||
|
│
|
||||||
|
▼ 写入输出 styles.xml
|
||||||
|
├── 模板的 fonts[](替换硬编码默认值)
|
||||||
|
├── 模板的 fills[](透明/灰色填充等)
|
||||||
|
├── 模板的 borders[](thin/medium 边框等)
|
||||||
|
└── 4 个 cellXfs(序号/名称/封装/空单元格)
|
||||||
|
└── 引用模板样式索引
|
||||||
|
└── 对齐方式从模板读取而非硬编码
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 测试覆盖
|
||||||
|
|
||||||
|
#### F012 回归测试
|
||||||
|
|
||||||
|
| 测试用例 | 说明 | 结果 |
|
||||||
|
|----------|------|------|
|
||||||
|
| `test_f012_pinname_position` | 5×5 构建 → PinMAP 生成 → 验证四边 PinName 位置 → 序列化/反序列化验证 | ✅ |
|
||||||
|
|
||||||
|
#### F011 模板格式提取测试
|
||||||
|
|
||||||
|
| 测试用例 | 说明 | 结果 |
|
||||||
|
|----------|------|------|
|
||||||
|
| `test_template_path_generation` | 两个模板查找函数路径格式 | ✅ |
|
||||||
|
| `test_f011_default_styles_xml` | 无模板回退默认样式 | ✅ |
|
||||||
|
| `test_f011_template_fonts_in_styles_xml` | 模板字体应用 | ✅ |
|
||||||
|
| `test_f011_output_dims_determined_by_pins` | 输出行列由引脚数决定 | ✅ |
|
||||||
|
| `test_f011_template_borders_in_styles_xml` | 模板边框应用 | ✅ |
|
||||||
|
| `test_f011_template_fills_in_styles_xml` | 模板填充应用 | ✅ |
|
||||||
|
| `test_template_empty_fonts_fallback` | 空字体回退 | ✅ |
|
||||||
|
| `test_template_color_prefix_auto_fix` | 颜色 # 前缀修复 | ✅ |
|
||||||
|
| `test_template_no_styles_xml` | 无 styles.xml 降级 | ✅ |
|
||||||
|
|
||||||
|
**新增测试**: 12 个测试用例
|
||||||
|
**总测试**: 20 个单元测试 + 6 个集成测试 = 26 个
|
||||||
|
**测试通过率**: 100%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 已知问题
|
||||||
|
|
||||||
|
无
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 限制
|
||||||
|
|
||||||
|
| 限制项 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| 模板查找 | 仅支持项目根目录和当前工作目录两种位置 |
|
||||||
|
| 模板格式 | 仅支持 `.xlsx` 格式模板 |
|
||||||
|
| 样式应用 | 提取式而非复制式,部分高级格式可能丢失 |
|
||||||
|
|
||||||
|
其他限制同 v1.2.0。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 升级指南
|
||||||
|
|
||||||
|
**从 v1.3.x / v1.2.0 升级**:替换 `Code/src/` 目录下所有文件。模板文件需手动放置:
|
||||||
|
- MAP→List 方向:在项目根目录放置 `BallList-Template.xlsx`
|
||||||
|
- List→MAP 方向:在项目根目录放置 `BallMAP-Template.xlsx`
|
||||||
|
- 模板可选,不放置则使用默认样式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 贡献者
|
||||||
|
|
||||||
|
- 架构设计:Script Architect
|
||||||
|
- 编码实现:Coding Agent × 3
|
||||||
|
- 测试验证:QA Agent
|
||||||
|
- 文档编写:Doc Gen Agent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 获取帮助
|
||||||
|
|
||||||
|
- 查看 `QUICKSTART.md` 了解使用方法
|
||||||
|
- 查看 `README.md` 了解完整说明
|
||||||
|
- 查看 `architecture-design.md` 了解技术细节
|
||||||
|
- 查看 `CHANGELOG.md` 了解变更历史
|
||||||
|
- 查看 `Test/test_report.md` 了解测试详情
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.2.0 — 2026-05-28
|
## v1.2.0 — 2026-05-28
|
||||||
|
|
||||||
### ✨ 新增 PinList → PinMAP 反向转换
|
### ✨ 新增 PinList → PinMAP 反向转换
|
||||||
|
|||||||
BIN
Code/src/Template/PinList-Template.xlsx
Normal file
BIN
Code/src/Template/PinList-Template.xlsx
Normal file
Binary file not shown.
BIN
Code/src/Template/PinMAP-Template.xlsx
Normal file
BIN
Code/src/Template/PinMAP-Template.xlsx
Normal file
Binary file not shown.
@@ -33,26 +33,55 @@ def wait_for_exit():
|
|||||||
|
|
||||||
# ── Path helpers ────────────────────────────────────────────────────
|
# ── Path helpers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _find_template_path() -> str | None:
|
def _find_pinlist_template_path() -> str | None:
|
||||||
"""查找根目录下的 PinMAP-Template.xlsx。
|
"""查找 PinList-Template.xlsx。
|
||||||
|
|
||||||
|
MAP→List 输出使用 PinList 模板。
|
||||||
搜索顺序:
|
搜索顺序:
|
||||||
1. 与 run.bat 同级的根目录
|
1. Code/src/Template/ 目录(首要位置)
|
||||||
2. 当前工作目录
|
2. 项目根目录(向后兼容)
|
||||||
|
3. 当前工作目录
|
||||||
"""
|
"""
|
||||||
# 从 Code/src 回退到根目录
|
|
||||||
src_dir = os.path.dirname(os.path.abspath(__file__))
|
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
|
# 1. Code/src/Template/ 目录
|
||||||
template_path = os.path.join(root_dir, "PinMAP-Template.xlsx")
|
template_path = os.path.join(src_dir, "Template", "PinList-Template.xlsx")
|
||||||
|
|
||||||
if os.path.exists(template_path):
|
if os.path.exists(template_path):
|
||||||
return template_path
|
return template_path
|
||||||
|
# 2. 项目根目录(向后兼容)
|
||||||
|
root_dir = os.path.dirname(os.path.dirname(src_dir))
|
||||||
|
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
# 3. 当前工作目录
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(cwd_template):
|
||||||
|
return cwd_template
|
||||||
|
return None
|
||||||
|
|
||||||
# 回退到当前工作目录
|
|
||||||
|
def _find_pinmap_template_path() -> str | None:
|
||||||
|
"""查找 PinMAP-Template.xlsx。
|
||||||
|
|
||||||
|
List→MAP 输出使用 PinMAP 模板。
|
||||||
|
搜索顺序:
|
||||||
|
1. Code/src/Template/ 目录(首要位置)
|
||||||
|
2. 项目根目录(向后兼容)
|
||||||
|
3. 当前工作目录
|
||||||
|
"""
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# 1. Code/src/Template/ 目录
|
||||||
|
template_path = os.path.join(src_dir, "Template", "PinMAP-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
# 2. 项目根目录(向后兼容)
|
||||||
|
root_dir = os.path.dirname(os.path.dirname(src_dir))
|
||||||
|
template_path = os.path.join(root_dir, "PinMAP-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
# 3. 当前工作目录
|
||||||
cwd_template = os.path.join(os.getcwd(), "PinMAP-Template.xlsx")
|
cwd_template = os.path.join(os.getcwd(), "PinMAP-Template.xlsx")
|
||||||
if os.path.exists(cwd_template):
|
if os.path.exists(cwd_template):
|
||||||
return cwd_template
|
return cwd_template
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -150,17 +179,17 @@ def run_map_to_list(filepath: str):
|
|||||||
data[f'A{row}'] = pin_name
|
data[f'A{row}'] = pin_name
|
||||||
data[f'B{row}'] = str(pin_num)
|
data[f'B{row}'] = str(pin_num)
|
||||||
|
|
||||||
# 尝试读取模板样式(F007)
|
# 尝试读取 PinList 模板样式
|
||||||
template_path = _find_template_path()
|
template_path = _find_pinlist_template_path()
|
||||||
template_style = None
|
template_style = None
|
||||||
if template_path:
|
if template_path:
|
||||||
template_style = read_template_styles(template_path)
|
template_style = read_template_styles(template_path)
|
||||||
if template_style:
|
if template_style:
|
||||||
print(f"[INFO] 已加载模板样式: {template_path}")
|
print(f"[INFO] 已加载 PinList 模板样式: {template_path}")
|
||||||
else:
|
else:
|
||||||
print("[WARN] 模板文件存在但解析失败,使用默认样式")
|
print("[WARN] PinList 模板文件存在但解析失败,使用默认样式")
|
||||||
else:
|
else:
|
||||||
print("[INFO] 未检测到模板文件,使用默认样式")
|
print("[INFO] 未检测到 PinList-Template.xlsx,使用默认样式")
|
||||||
|
|
||||||
if template_style is not None:
|
if template_style is not None:
|
||||||
write_xlsx_with_style(data, output_path, template_style)
|
write_xlsx_with_style(data, output_path, template_style)
|
||||||
@@ -260,17 +289,17 @@ def run_list_to_map(filepath: str):
|
|||||||
print(f"[INFO] 正在生成 PinMAP 并写入: {output_path}")
|
print(f"[INFO] 正在生成 PinMAP 并写入: {output_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 尝试读取模板样式(F007 — 从根目录读取而非输入文件路径)
|
# 尝试读取 PinMAP 模板样式
|
||||||
template_path = _find_template_path()
|
template_path = _find_pinmap_template_path()
|
||||||
template_style = None
|
template_style = None
|
||||||
if template_path:
|
if template_path:
|
||||||
template_style = read_template_styles(template_path)
|
template_style = read_template_styles(template_path)
|
||||||
if template_style:
|
if template_style:
|
||||||
print(f"[INFO] 已加载模板样式: {template_path}")
|
print(f"[INFO] 已加载 PinMAP 模板样式: {template_path}")
|
||||||
else:
|
else:
|
||||||
print("[WARN] 模板文件存在但解析失败,使用默认样式")
|
print("[WARN] PinMAP 模板文件存在但解析失败,使用默认样式")
|
||||||
else:
|
else:
|
||||||
print("[INFO] 未检测到模板文件,使用默认样式")
|
print("[INFO] 未检测到 PinMAP-Template.xlsx,使用默认样式")
|
||||||
|
|
||||||
generate_pinmap(
|
generate_pinmap(
|
||||||
entries=entries,
|
entries=entries,
|
||||||
|
|||||||
@@ -56,12 +56,11 @@ def generate_pinmap(
|
|||||||
# 先写入 PinName 单元格
|
# 先写入 PinName 单元格
|
||||||
for edge_name, edge in layout.items():
|
for edge_name, edge in layout.items():
|
||||||
for (pin_num, pin_name), num_cell in zip(edge.pins, edge.cells):
|
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])
|
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"
|
data[name_ref] = pin_name if pin_name and pin_name.strip() else "NC"
|
||||||
|
|
||||||
# 再写入序号单元格(覆盖同位置的名字,确保序号优先)
|
# 再写入序号单元格(v1.5.4:无边角共享,每个序号独占一个单元格)
|
||||||
# v1.3: 角点单元格被两条边共享,需写入两个引脚序号
|
|
||||||
cell_pins: dict[str, list[str]] = {}
|
cell_pins: dict[str, list[str]] = {}
|
||||||
for edge_name, edge in layout.items():
|
for edge_name, edge in layout.items():
|
||||||
for (pin_num, pin_name), num_cell in zip(edge.pins, edge.cells):
|
for (pin_num, pin_name), num_cell in zip(edge.pins, edge.cells):
|
||||||
|
|||||||
@@ -12,7 +12,24 @@ Edge assignment (counter-clockwise, top-left = pin 1):
|
|||||||
|
|
||||||
Total: rows + cols + rows + cols = 2×rows + 2×cols = (rows + cols) × 2
|
Total: rows + cols + rows + cols = 2×rows + 2×cols = (rows + cols) × 2
|
||||||
|
|
||||||
v1.3: 每条边独立包含其端点,角点单元格会被两条边共享。
|
v1.5.5: 上边完全独立在 row 0-1,不与左/右边共享行。
|
||||||
|
每条边独立包含其端点,所有单元格互不冲突。
|
||||||
|
|
||||||
|
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: (0, c) where c ∈ [1..cols] [reverse order, independent row]
|
||||||
|
|
||||||
|
Pin1: Number (2,0), Name (2,1) — top-left of left edge
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from models import PinListEntry, EdgePins, LayoutError
|
from models import PinListEntry, EdgePins, LayoutError
|
||||||
@@ -86,28 +103,29 @@ def calculate_layout(
|
|||||||
|
|
||||||
top_pins = entries[idx: idx + top_count]
|
top_pins = entries[idx: idx + top_count]
|
||||||
|
|
||||||
# ── 计算单元格坐标 ────────────────────────────────────────────
|
# ── 计算单元格坐标(v1.5.5:上边 Name 在 row 0,完全独立)──
|
||||||
#
|
#
|
||||||
# 网格坐标体系(0-based):
|
# 网格坐标体系(0-based):
|
||||||
# 方形区域:行 [1..rows],列 [0..cols]
|
# 从网格边界往中心走,第一圈全是 Number,第二圈全是 Name
|
||||||
# 左边: 序号在 (r, 0), Name 在 (r, 1) 其中 r ∈ [1, rows]
|
# 上边 Name 在 Number 上方 row 0,不与左/右边共享行
|
||||||
# 下边: 序号在 (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] 逆序
|
|
||||||
#
|
#
|
||||||
# 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 (0, c) 逆序
|
||||||
#
|
#
|
||||||
|
# Pin1: Number (2,0) = A3, Name (2,1) = B3 — 左上角
|
||||||
|
|
||||||
# 左边:从上到下
|
# 左边:从上到下 (rows 个)
|
||||||
left_cells = [(r, 0) for r in range(1, rows + 1)]
|
left_cells = [(r, 0) for r in range(2, rows + 2)]
|
||||||
|
|
||||||
# 下边:从左到右
|
# 下边:从左到右 (cols 个),Number 在最底行 rows+3
|
||||||
bottom_cells = [(rows, c) for c in range(1, cols + 1)]
|
bottom_cells = [(rows + 3, c) for c in range(1, cols + 1)]
|
||||||
|
|
||||||
# 右边:从下到上(逆序)
|
# 右边:从下到上 (rows 个),Number 在 cols+1 列(右扩一列)
|
||||||
right_cells = [(r, cols) for r in range(rows, 0, -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)]
|
top_cells = [(1, c) for c in range(cols, 0, -1)]
|
||||||
|
|
||||||
# ── 构建 EdgePins ─────────────────────────────────────────────
|
# ── 构建 EdgePins ─────────────────────────────────────────────
|
||||||
@@ -124,16 +142,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 单元格坐标。
|
根据序号单元格坐标和边名称,计算对应的 PinName 单元格坐标。
|
||||||
|
|
||||||
|
v1.5.5: 上边 Name 在 Number 上方 (0, c),即独立一行。
|
||||||
|
不再需要角点例外——整个上边 Name 在独立的 row 0。
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
num_cell : tuple[int, int]
|
num_cell : tuple[int, int]
|
||||||
序号单元格坐标 (row, col) 0-based
|
序号单元格坐标 (row, col) 0-based
|
||||||
edge_name : str
|
edge_name : str
|
||||||
"left" | "bottom" | "right" | "top"
|
"left" | "bottom" | "right" | "top"
|
||||||
|
cols : int
|
||||||
|
网格列数(v1.5.5 上边不再需要角点例外,参数保留以兼容调用)
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -142,12 +166,15 @@ def get_name_cell(num_cell: tuple[int, int], edge_name: str) -> tuple[int, int]:
|
|||||||
"""
|
"""
|
||||||
r, c = num_cell
|
r, c = num_cell
|
||||||
if edge_name == "left":
|
if edge_name == "left":
|
||||||
return (r, c + 1) # Name 在序号右侧
|
return (r, c + 1) # Name 在 Number 右侧 (col 1)
|
||||||
elif edge_name == "bottom":
|
elif edge_name == "bottom":
|
||||||
return (r - 1, c) # Name 在序号上方
|
return (r - 1, c) # Name 在 Number 上方 (row rows+2)
|
||||||
elif edge_name == "right":
|
elif edge_name == "right":
|
||||||
return (r, c - 1) # Name 在序号左侧
|
return (r, c - 1) # Name 在 Number 左侧 (col cols)
|
||||||
elif edge_name == "top":
|
elif edge_name == "top":
|
||||||
return (r + 1, c) # Name 在序号下方
|
# Top Number 在 (1, c), c ∈ [cols..1]
|
||||||
|
# Name 在 Number 上方 (0, c),即 Excel 第 1 行
|
||||||
|
# 不再需要角点例外——整个上边 Name 在独立一行
|
||||||
|
return (0, c) # Name 在 Number 上方
|
||||||
else:
|
else:
|
||||||
raise LayoutError(f"未知的边名称: {edge_name}")
|
raise LayoutError(f"未知的边名称: {edge_name}")
|
||||||
|
|||||||
@@ -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
|
detects the rectangular PinMAP boundary, and extracts pins in
|
||||||
counter-clockwise order starting from the top-left corner.
|
counter-clockwise order starting from the top-left corner.
|
||||||
|
|
||||||
|
v1.5.5: 上边 Name 在 Number 上方 (min_row-1),无需角点例外。
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
>>> from pinmap_parser import parse_pinmap
|
>>> from pinmap_parser import parse_pinmap
|
||||||
@@ -82,40 +84,45 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
|
|||||||
if not package_info or not str(package_info).strip():
|
if not package_info or not str(package_info).strip():
|
||||||
raise StructureError("A1 单元格为空,缺少封装信息")
|
raise StructureError("A1 单元格为空,缺少封装信息")
|
||||||
|
|
||||||
# ── Step 3: build name lookup ────────────────────────────────
|
# ── Step 3: build name lookup (v1.5.5 layout) ──────────────
|
||||||
# For each edge, pin names live in the cell *adjacent inward*
|
# For each edge, pin names live in the cell *adjacent inward*
|
||||||
# from the boundary cell that holds the pin number.
|
# from the boundary cell that holds the pin number.
|
||||||
#
|
#
|
||||||
|
# v1.5.5 layout:
|
||||||
# left : number at (r, min_col), name at (r, min_col+1)
|
# left : number at (r, min_col), name at (r, min_col+1)
|
||||||
# bottom : number at (max_row, c), name at (max_row-1, c)
|
# bottom : number at (max_row, c), name at (max_row-1, c)
|
||||||
# right : number at (r, max_col), name at (r, max_col-1)
|
# right : number at (r, max_col), name at (r, max_col-1)
|
||||||
# top : number at (min_row, c), name at (min_row+1, c)
|
# top : number at (min_row+1, c), name at (min_row, c)
|
||||||
|
# No corner exceptions — top names are all one row above Numbers
|
||||||
|
|
||||||
name_map: dict[tuple[int, int], str] = {}
|
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):
|
for r in range(min_row, max_row + 1):
|
||||||
name = cells.get((r, min_col + 1), "")
|
name = cells.get((r, min_col + 1), "")
|
||||||
if name and str(name).strip():
|
if name and str(name).strip():
|
||||||
name_map[(r, min_col)] = 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):
|
for c in range(min_col, max_col + 1):
|
||||||
name = cells.get((max_row - 1, c), "")
|
name = cells.get((max_row - 1, c), "")
|
||||||
if name and str(name).strip():
|
if name and str(name).strip():
|
||||||
name_map[(max_row, c)] = 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):
|
for r in range(min_row, max_row + 1):
|
||||||
name = cells.get((r, max_col - 1), "")
|
name = cells.get((r, max_col - 1), "")
|
||||||
if name and str(name).strip():
|
if name and str(name).strip():
|
||||||
name_map[(r, max_col)] = str(name).strip()
|
name_map[(r, max_col)] = str(name).strip()
|
||||||
|
|
||||||
# top edge names
|
# top edge names at (min_row, c) — one row ABOVE the Number row.
|
||||||
|
# v1.5.5: 上边 Name 在 (min_row, c),Number 在 (min_row+1, c)。
|
||||||
|
# name_map key 是 Number 单元格坐标,value 是 Name 字符串。
|
||||||
for c in range(min_col, max_col + 1):
|
for c in range(min_col, max_col + 1):
|
||||||
name = cells.get((min_row + 1, c), "")
|
name = cells.get((min_row, c), "")
|
||||||
if name and str(name).strip():
|
if name and str(name).strip() and _try_int(name) is None:
|
||||||
name_map[(min_row, c)] = str(name).strip()
|
name_map[(min_row + 1, c)] = str(name).strip()
|
||||||
|
# No corner exceptions needed — top names are all on min_row
|
||||||
|
|
||||||
# ── Step 4: walk edges counter-clockwise (v1.3 formula) ──────
|
# ── Step 4: walk edges counter-clockwise (v1.3 formula) ──────
|
||||||
# Each edge independently includes its endpoints (corners).
|
# Each edge independently includes its endpoints (corners).
|
||||||
@@ -158,9 +165,9 @@ def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
|
|||||||
for r in range(max_row, min_row - 1, -1):
|
for r in range(max_row, min_row - 1, -1):
|
||||||
_add_pin(r, max_col, "right", max_row - r)
|
_add_pin(r, max_col, "right", max_row - r)
|
||||||
|
|
||||||
# 4d. Top edge: right → left (includes top-left corner)
|
# 4d. Top edge: right → left (Numbers at min_row+1 row, Names at min_row)
|
||||||
for c in range(max_col, min_col - 1, -1):
|
for c in range(max_col, min_col - 1, -1):
|
||||||
_add_pin(min_row, c, "top", max_col - c)
|
_add_pin(min_row + 1, c, "top", max_col - c)
|
||||||
|
|
||||||
if not pins:
|
if not pins:
|
||||||
raise StructureError("未检测到任何 Pin 数据")
|
raise StructureError("未检测到任何 Pin 数据")
|
||||||
|
|||||||
@@ -171,15 +171,18 @@ class TemplateReader:
|
|||||||
'fontId': xf.get('fontId', '0'),
|
'fontId': xf.get('fontId', '0'),
|
||||||
'fillId': xf.get('fillId', '0'),
|
'fillId': xf.get('fillId', '0'),
|
||||||
'borderId': xf.get('borderId', '0'),
|
'borderId': xf.get('borderId', '0'),
|
||||||
|
'xfId': xf.get('xfId', '0'),
|
||||||
'applyFont': xf.get('applyFont', ''),
|
'applyFont': xf.get('applyFont', ''),
|
||||||
'applyFill': xf.get('applyFill', ''),
|
'applyFill': xf.get('applyFill', ''),
|
||||||
'applyBorder': xf.get('applyBorder', ''),
|
'applyBorder': xf.get('applyBorder', ''),
|
||||||
|
'applyAlignment': xf.get('applyAlignment', ''),
|
||||||
}
|
}
|
||||||
# 对齐方式
|
# 对齐方式
|
||||||
align = xf.find(_tag('alignment'))
|
align = xf.find(_tag('alignment'))
|
||||||
if align is not None:
|
if align is not None:
|
||||||
xf_info['hAlign'] = align.get('horizontal', '')
|
xf_info['hAlign'] = align.get('horizontal', '')
|
||||||
xf_info['vAlign'] = align.get('vertical', '')
|
xf_info['vAlign'] = align.get('vertical', '')
|
||||||
|
xf_info['wrapText'] = align.get('wrapText', '')
|
||||||
style.cell_xfs.append(xf_info)
|
style.cell_xfs.append(xf_info)
|
||||||
|
|
||||||
def _parse_sheet_dims(self, data: bytes, style: TemplateStyle):
|
def _parse_sheet_dims(self, data: bytes, style: TemplateStyle):
|
||||||
|
|||||||
@@ -9,37 +9,42 @@ sys.path.insert(0, os.path.dirname(__file__))
|
|||||||
from pinmap_parser import parse_pinmap
|
from pinmap_parser import parse_pinmap
|
||||||
from validator import validate_pinmap
|
from validator import validate_pinmap
|
||||||
|
|
||||||
|
# F012 测试所需模块
|
||||||
|
from models import PinListEntry
|
||||||
|
from pinmap_generator import generate_pinmap
|
||||||
|
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):
|
# ── 4x4 example (v1.5.5 layout) ───────────────────────────────
|
||||||
# A4:1 A5:2 B4:Pin1 B5:Pin2 → left edge
|
# Layout: rows=4, cols=4, 16 pins
|
||||||
# C7:3 D7:4 C6:Pin3 D6:Pin4 → bottom edge
|
# Top: Name row 0 (B1..E1), Number row 1 (B2..E2)
|
||||||
# F5:5 F4:6 E5:Pin5 E4:Pin6 → right edge
|
# Left: Number A3..A6 (rows 2..5), Name B3..B6 (rows 2..5)
|
||||||
# D2:7 C2:8 D3:Pin7 C3:Pin8 → top edge
|
# Bottom: Name B7..E7 (row 6), Number B8..E8 (row 7)
|
||||||
# A1: "QFP-44" → package info
|
# Right: Number F6..F3 (rows 5..2), Name E6..E3 (rows 5..2)
|
||||||
|
# A1: "QFP-44" = package info
|
||||||
|
#
|
||||||
|
# Pin1: Number A3=(2,0), Name B3=(2,1)
|
||||||
|
|
||||||
cells_4x4 = {
|
cells_4x4 = {
|
||||||
(0, 0): "QFP-44",
|
(0, 0): "QFP-44",
|
||||||
# left edge
|
# top edge Names (row 0, cols 1..4)
|
||||||
(3, 0): "1",
|
(0, 1): "Pin16", (0, 2): "Pin15", (0, 3): "Pin14", (0, 4): "Pin13",
|
||||||
(4, 0): "2",
|
# top edge Numbers (row 1, cols 1..4)
|
||||||
(3, 1): "Pin1",
|
(1, 1): "16", (1, 2): "15", (1, 3): "14", (1, 4): "13",
|
||||||
(4, 1): "Pin2",
|
# left edge (rows 2..5, cols 0..1)
|
||||||
# bottom edge
|
(2, 0): "1", (2, 1): "Pin1",
|
||||||
(6, 2): "3",
|
(3, 0): "2", (3, 1): "Pin2",
|
||||||
(6, 3): "4",
|
(4, 0): "3", (4, 1): "Pin3",
|
||||||
(5, 2): "Pin3",
|
(5, 0): "4", (5, 1): "Pin4",
|
||||||
(5, 3): "Pin4",
|
# bottom edge (rows 6..7, cols 1..4)
|
||||||
# right edge
|
(6, 1): "Pin5", (6, 2): "Pin6", (6, 3): "Pin7", (6, 4): "Pin8",
|
||||||
(4, 5): "5",
|
(7, 1): "5", (7, 2): "6", (7, 3): "7", (7, 4): "8",
|
||||||
(3, 5): "6",
|
# right edge (rows 5..2, cols 4..5)
|
||||||
(4, 4): "Pin5",
|
(5, 4): "Pin9", (5, 5): "9",
|
||||||
(3, 4): "Pin6",
|
(4, 4): "Pin10", (4, 5): "10",
|
||||||
# top edge
|
(3, 4): "Pin11", (3, 5): "11",
|
||||||
(1, 3): "7",
|
(2, 4): "Pin12", (2, 5): "12",
|
||||||
(1, 2): "8",
|
|
||||||
(2, 3): "Pin7",
|
|
||||||
(2, 2): "Pin8",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -47,19 +52,27 @@ def test_4x4_parse():
|
|||||||
pm = parse_pinmap(cells_4x4)
|
pm = parse_pinmap(cells_4x4)
|
||||||
|
|
||||||
assert pm.package_info == "QFP-44", f"package_info={pm.package_info}"
|
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)
|
# Counter-clockwise order: left(top→bot) → bottom(left→right)
|
||||||
# → right(bot→top) → top(right→left)
|
# → right(bot→top) → top(right→left)
|
||||||
expected = [
|
expected = [
|
||||||
(1, "Pin1", "left"),
|
(1, "Pin1", "left"),
|
||||||
(2, "Pin2", "left"),
|
(2, "Pin2", "left"),
|
||||||
(3, "Pin3", "bottom"),
|
(3, "Pin3", "left"),
|
||||||
(4, "Pin4", "bottom"),
|
(4, "Pin4", "left"),
|
||||||
(5, "Pin5", "right"),
|
(5, "Pin5", "bottom"),
|
||||||
(6, "Pin6", "right"),
|
(6, "Pin6", "bottom"),
|
||||||
(7, "Pin7", "top"),
|
(7, "Pin7", "bottom"),
|
||||||
(8, "Pin8", "top"),
|
(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):
|
for i, (num, name, edge) in enumerate(expected):
|
||||||
p = pm.pins[i]
|
p = pm.pins[i]
|
||||||
@@ -98,7 +111,7 @@ def test_missing_names_warning():
|
|||||||
|
|
||||||
def test_duplicate_numbers():
|
def test_duplicate_numbers():
|
||||||
cells = dict(cells_4x4)
|
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)
|
pm = parse_pinmap(cells)
|
||||||
vr = validate_pinmap(pm)
|
vr = validate_pinmap(pm)
|
||||||
assert not vr.is_valid
|
assert not vr.is_valid
|
||||||
@@ -108,7 +121,7 @@ def test_duplicate_numbers():
|
|||||||
|
|
||||||
def test_gap_in_numbers():
|
def test_gap_in_numbers():
|
||||||
cells = dict(cells_4x4)
|
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)
|
pm = parse_pinmap(cells)
|
||||||
vr = validate_pinmap(pm)
|
vr = validate_pinmap(pm)
|
||||||
assert not vr.is_valid
|
assert not vr.is_valid
|
||||||
@@ -162,31 +175,28 @@ def test_rectangular_parse():
|
|||||||
|
|
||||||
|
|
||||||
def test_12pin_square():
|
def test_12pin_square():
|
||||||
"""A larger square: 12 pins on a 6×6 grid (rows 1-5, cols 0-5).
|
"""A 3×3 square: 12 pins (3 pins per edge).
|
||||||
|
Using v1.5.5 layout: top names at row 0, top numbers at row 1.
|
||||||
left: 1,2,3 bottom: 4,5,6 right: 7,8,9 top: 12,11,10
|
left: 1,2,3 bottom: 4,5,6 right: 7,8,9 top: 12,11,10
|
||||||
"""
|
"""
|
||||||
cells = {
|
cells = {
|
||||||
(0, 0): "QFP-12",
|
(0, 0): "QFP-12",
|
||||||
|
# top Names (row 0, cols 1..3)
|
||||||
|
(0, 1): "RST", (0, 2): "VSS", (0, 3): "VDD",
|
||||||
|
# top Numbers (row 1, cols 1..3)
|
||||||
|
(1, 1): "12", (1, 2): "11", (1, 3): "10",
|
||||||
# left (col 0) — names at col 1
|
# left (col 0) — names at col 1
|
||||||
(1, 0): "1", (1, 1): "VCC",
|
(2, 0): "1", (2, 1): "VCC",
|
||||||
(2, 0): "2", (2, 1): "GND",
|
(3, 0): "2", (3, 1): "GND",
|
||||||
(3, 0): "3", (3, 1): "IN1",
|
(4, 0): "3", (4, 1): "IN1",
|
||||||
# bottom (row 5) — names at row 4
|
# bottom Names (row 5), Numbers (row 6)
|
||||||
(5, 1): "4", (4, 1): "IN2",
|
(5, 1): "IN2", (5, 2): "OUT1", (5, 3): "OUT2",
|
||||||
(5, 2): "5", (4, 2): "OUT1",
|
(6, 1): "4", (6, 2): "5", (6, 3): "6",
|
||||||
(5, 3): "6", (4, 3): "OUT2",
|
# right (col 4 Number, col 3 Name) — bottom to top: 7, 8, 9
|
||||||
# right (col 5) — names at col 4
|
(4, 3): "CTL1", (4, 4): "7",
|
||||||
(4, 5): "7", (4, 4): "CTL1",
|
(3, 3): "CTL2", (3, 4): "8",
|
||||||
(3, 5): "8", (3, 4): "CTL2",
|
(2, 3): "NC1", (2, 4): "9",
|
||||||
(2, 5): "9", (2, 4): "NC1",
|
|
||||||
# top (row 1) — names at row 2, cols 2-4 (avoid col 5 corner)
|
|
||||||
(1, 4): "10", (2, 4): "VDD",
|
|
||||||
(1, 3): "11", (2, 3): "VSS",
|
|
||||||
(1, 2): "12", (2, 2): "RST",
|
|
||||||
}
|
}
|
||||||
# Note: (2,4) is used as name for both pin 9 (right edge) and pin 10 (top edge).
|
|
||||||
# The name_map will have the last writer win. This is fine for the test —
|
|
||||||
# we just verify the correct number of pins and their order.
|
|
||||||
pm = parse_pinmap(cells)
|
pm = parse_pinmap(cells)
|
||||||
assert len(pm.pins) == 12, f"expected 12, got {len(pm.pins)}"
|
assert len(pm.pins) == 12, f"expected 12, got {len(pm.pins)}"
|
||||||
|
|
||||||
@@ -215,6 +225,338 @@ def test_12pin_square():
|
|||||||
print("✓ test_12pin_square passed")
|
print("✓ test_12pin_square passed")
|
||||||
|
|
||||||
|
|
||||||
|
# ── F012: PinMAP 生成中上/下边 PinName 位置验证 ────────────
|
||||||
|
|
||||||
|
def test_f012_pinname_position():
|
||||||
|
"""验证 PinList→PinMAP 时各边 PinName 位置正确(v1.5.5 布局)。
|
||||||
|
|
||||||
|
v1.5.5 布局:
|
||||||
|
- 左边 Name 在 (2..rows+1, 1)
|
||||||
|
- 下边 Name 在 (rows+2, 1..cols) ← 倒数第二行
|
||||||
|
- 右边 Name 在 (rows+1..2, cols)
|
||||||
|
- 上边 Name 在 (0, c),即独立行,无需角点例外
|
||||||
|
|
||||||
|
测试策略:
|
||||||
|
1. 构建 5×5(20 Pin)PinList 数据
|
||||||
|
2. 生成 PinMAP
|
||||||
|
3. 检查输出 cell 位置
|
||||||
|
4. 将生成的 PinMAP 再解析回 PinList,做往返一致性验证
|
||||||
|
"""
|
||||||
|
# ── 1. 构建 5×5 PinList 数据(20 个引脚) ──────────────────
|
||||||
|
rows, cols = 5, 5
|
||||||
|
entries = [
|
||||||
|
PinListEntry(number=n + 1, name=f"PIN{n + 1}")
|
||||||
|
for n in range(20)
|
||||||
|
]
|
||||||
|
package_info = "QFN-20"
|
||||||
|
|
||||||
|
# ── 2. 生成 PinMAP ────────────────────────────────────────
|
||||||
|
data = generate_pinmap(
|
||||||
|
entries=entries,
|
||||||
|
rows=rows,
|
||||||
|
cols=cols,
|
||||||
|
package_info=package_info,
|
||||||
|
template_style=None,
|
||||||
|
output_path=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 3. 检查单元格位置 (v1.5.5) ─────────────────────────────
|
||||||
|
# 5×5: rows=5, cols=5, 20 pins
|
||||||
|
# 上边: Name (0, 1..5), Number (1, 5..1)
|
||||||
|
# 左边: Number (2..6, 0), Name (2..6, 1)
|
||||||
|
# 下边: Name (7, 1..5), Number (8, 1..5)
|
||||||
|
# 右边: Number (6..2, 6), Name (6..2, 5)
|
||||||
|
|
||||||
|
# ── 3a. 验证上边 Name 位置 (0, 1..cols) ─────────────────
|
||||||
|
for c in range(1, cols + 1):
|
||||||
|
num_ref = rc_to_cell_ref(1, c) # Number at row 1
|
||||||
|
name_ref = rc_to_cell_ref(0, c) # Name at row 0
|
||||||
|
assert num_ref in data, f"上边 Number {num_ref} 缺失"
|
||||||
|
assert name_ref in data, (
|
||||||
|
f"上边 Name 应在 {name_ref} (row 0), 但未找到。Number 在 {num_ref}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 3b. 验证下边 Name 位置 (rows+2=7, 1..cols) ──────────
|
||||||
|
for c in range(1, cols + 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"下边 Name 应在 {name_ref} (rows+2), 但未找到。Number 在 {num_ref}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 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} 缺失"
|
||||||
|
|
||||||
|
# ── 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
|
||||||
|
|
||||||
|
cell_data = {}
|
||||||
|
for ref, value in data.items():
|
||||||
|
cell_data[cell_ref_to_rc(ref)] = value
|
||||||
|
|
||||||
|
pm = parse_pinmap(cell_data)
|
||||||
|
assert len(pm.pins) == 20, f"往返: 预期 20 引脚,实际 {len(pm.pins)}"
|
||||||
|
|
||||||
|
actual_numbers = sorted([p.number for p in pm.pins])
|
||||||
|
expected_numbers = list(range(1, 21))
|
||||||
|
assert actual_numbers == expected_numbers, (
|
||||||
|
f"往返: 引脚序号不匹配\n"
|
||||||
|
f" 预期: {expected_numbers}\n"
|
||||||
|
f" 实际: {actual_numbers}"
|
||||||
|
)
|
||||||
|
|
||||||
|
validation = validate_pinmap(pm)
|
||||||
|
assert validation.is_valid, (
|
||||||
|
f"往返验证失败: 错误={[e.message for e in validation.errors]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
pinlist = generate_pinlist(pm, validation)
|
||||||
|
assert len(pinlist.rows) == 20, (
|
||||||
|
f"往返 PinList: 预期 20 行,实际 {len(pinlist.rows)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, (name, num) in enumerate(pinlist.rows):
|
||||||
|
expected_num = i + 1
|
||||||
|
assert num == expected_num, (
|
||||||
|
f"往返 PinList row[{i}]: 预期序号 {expected_num},实际 {num}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✓ test_f012_pinname_position passed (5×5={len(pm.pins)} pins)")
|
||||||
|
|
||||||
|
|
||||||
|
# ── v1.5: Template path generation tests ──────────────────────────
|
||||||
|
|
||||||
|
def test_template_path_generation():
|
||||||
|
"""验证两个模板查找函数返回正确的路径格式。"""
|
||||||
|
from main import _find_pinlist_template_path, _find_pinmap_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 "PinList" in result1
|
||||||
|
assert "PinMAP" in result2
|
||||||
|
assert result1 != result2
|
||||||
|
|
||||||
|
print("✓ test_template_path_generation passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_f011_default_styles_xml():
|
||||||
|
"""F011: 无模板时 _styles_xml() 返回硬编码默认样式。"""
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=None)
|
||||||
|
xml = writer._styles_xml()
|
||||||
|
|
||||||
|
# 验证硬编码默认值的存在
|
||||||
|
assert 'Calibri' in xml, "默认字体应为 Calibri"
|
||||||
|
assert 'thin' in xml, "默认边框应为 thin"
|
||||||
|
assert 'center' in xml, "默认对齐应为 center"
|
||||||
|
assert 'cellXfs count="4"' in xml, "应有 4 个 xf"
|
||||||
|
|
||||||
|
print("✓ test_f011_default_styles_xml passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_f011_template_fonts_in_styles_xml():
|
||||||
|
"""F011: 有模板时 _styles_xml() 使用模板的字体信息。"""
|
||||||
|
from template_reader import TemplateStyle, FontStyle
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
# 构建一个模板样式:微软雅黑 12pt
|
||||||
|
style = TemplateStyle()
|
||||||
|
style.fonts = [
|
||||||
|
FontStyle(name="微软雅黑", size=12.0, bold=False, italic=False, color="FF000000"),
|
||||||
|
FontStyle(name="微软雅黑", size=12.0, bold=True, italic=False, color="FF000000"),
|
||||||
|
]
|
||||||
|
style.fills = []
|
||||||
|
style.borders = []
|
||||||
|
style.cell_xfs = [
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
|
||||||
|
]
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=style)
|
||||||
|
xml = writer._styles_xml()
|
||||||
|
|
||||||
|
assert '微软雅黑' in xml, f"模板字体名应出现在 styles.xml 中\n{xml[:500]}"
|
||||||
|
assert '12' in xml or '12.0' in xml, f"模板字号 12pt 应出现在 styles.xml 中"
|
||||||
|
|
||||||
|
print("✓ test_f011_template_fonts_in_styles_xml passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_f011_output_dims_determined_by_pins():
|
||||||
|
"""F011: 输出文件的 dim 由实际 Pin 数量决定,不复制模板的行列结构。"""
|
||||||
|
from template_reader import TemplateStyle, FontStyle
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
style = TemplateStyle()
|
||||||
|
style.fonts = [FontStyle(name="Calibri", size=11.0)]
|
||||||
|
style.column_widths = {i: 20.0 for i in range(100)} # 模板有 100 列
|
||||||
|
style.row_heights = {i: 30.0 for i in range(200)} # 模板有 200 行
|
||||||
|
style.fills = []
|
||||||
|
style.borders = []
|
||||||
|
style.cell_xfs = [
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
|
||||||
|
]
|
||||||
|
|
||||||
|
# 仅输出 2 行 2 列的数据(模拟 2×2 PinMAP + A1)
|
||||||
|
data = {
|
||||||
|
'A1': 'QFP-8',
|
||||||
|
'A2': '1', 'B2': 'Pin1',
|
||||||
|
'A3': '2', 'B3': 'Pin2',
|
||||||
|
}
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=style)
|
||||||
|
sheet_xml = writer._sheet_xml(data)
|
||||||
|
|
||||||
|
# dim 应该反映实际数据范围(A1:B3),而非模板的 100 列
|
||||||
|
assert 'dimension ref="A1:B3"' in sheet_xml, \
|
||||||
|
f"dim 应由实际数据决定,不应包含模板的 100 列\n{sheet_xml[:500]}"
|
||||||
|
|
||||||
|
# 不应出现 row r="201"(模板的第 200 行)
|
||||||
|
assert 'row r="201"' not in sheet_xml, "不应包含模板的多余行"
|
||||||
|
|
||||||
|
print("✓ test_f011_output_dims_determined_by_pins passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_f011_template_borders_in_styles_xml():
|
||||||
|
"""F011: 有模板时 _styles_xml() 使用模板的边框样式(而非硬编码 thin)。"""
|
||||||
|
from template_reader import TemplateStyle, BorderStyle, FontStyle
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
style = TemplateStyle()
|
||||||
|
style.fonts = [FontStyle(name="Calibri", size=11.0)]
|
||||||
|
style.borders = [
|
||||||
|
BorderStyle(top="none", bottom="none", left="none", right="none"),
|
||||||
|
BorderStyle(top="medium", bottom="medium", left="medium", right="medium"),
|
||||||
|
]
|
||||||
|
style.fills = []
|
||||||
|
style.cell_xfs = [
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '1', 'xfId': '0',
|
||||||
|
'applyBorder': '1'},
|
||||||
|
]
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=style)
|
||||||
|
xml = writer._styles_xml()
|
||||||
|
|
||||||
|
# 模板的 medium 边框应该存在(不仅仅是 thin)
|
||||||
|
assert 'medium' in xml, f"模板 medium 边框应出现在 styles.xml 中\n{xml[:800]}"
|
||||||
|
|
||||||
|
print("✓ test_f011_template_borders_in_styles_xml passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_f011_template_fills_in_styles_xml():
|
||||||
|
"""F011: 有模板时 _styles_xml() 使用模板的填充色。"""
|
||||||
|
from template_reader import TemplateStyle, FillStyle, FontStyle
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
style = TemplateStyle()
|
||||||
|
style.fonts = [FontStyle(name="Calibri", size=11.0)]
|
||||||
|
style.borders = []
|
||||||
|
style.fills = [
|
||||||
|
FillStyle(pattern_type="none", fg_color=""),
|
||||||
|
FillStyle(pattern_type="solid", fg_color="FFFF00"), # 黄色
|
||||||
|
]
|
||||||
|
style.cell_xfs = [
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '1', 'borderId': '0', 'xfId': '0',
|
||||||
|
'applyFill': '1'},
|
||||||
|
]
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=style)
|
||||||
|
xml = writer._styles_xml()
|
||||||
|
|
||||||
|
assert 'FFFF00' in xml, f"模板黄色填充应出现在 styles.xml 中\n{xml[:800]}"
|
||||||
|
|
||||||
|
print("✓ test_f011_template_fills_in_styles_xml passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_template_empty_fonts_fallback():
|
||||||
|
"""边界测试:空 fonts 回退到默认字体。"""
|
||||||
|
from template_reader import TemplateStyle
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
style = TemplateStyle()
|
||||||
|
style.fonts = [] # 空 fonts
|
||||||
|
style.fills = []
|
||||||
|
style.borders = []
|
||||||
|
style.cell_xfs = []
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=style)
|
||||||
|
xml = writer._styles_xml()
|
||||||
|
|
||||||
|
# 应回退到默认样式:Calibri 11pt
|
||||||
|
assert 'Calibri' in xml, "空 fonts 应回退到默认 Calibri"
|
||||||
|
assert 'thin' in xml, "空 borders 应回退到默认 thin"
|
||||||
|
assert 'cellXfs count="4"' in xml, "应有 4 个 xf"
|
||||||
|
|
||||||
|
print("✓ test_template_empty_fonts_fallback passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_template_color_prefix_auto_fix():
|
||||||
|
"""边界测试:FF 前缀补全。"""
|
||||||
|
from template_reader import TemplateStyle, FontStyle, FillStyle
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
style = TemplateStyle()
|
||||||
|
# color 缺少 FF 前缀
|
||||||
|
style.fonts = [FontStyle(name="Calibri", size=11.0, color="000000")]
|
||||||
|
style.fills = [
|
||||||
|
FillStyle(pattern_type="none"),
|
||||||
|
FillStyle(pattern_type="solid", fg_color="FFFF00"), # 已有 FF
|
||||||
|
]
|
||||||
|
style.borders = []
|
||||||
|
style.cell_xfs = [
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
|
||||||
|
]
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=style)
|
||||||
|
xml = writer._styles_xml()
|
||||||
|
|
||||||
|
# 即使原始 color 是 "000000",输出也应是 "FF000000"
|
||||||
|
assert 'FF000000' in xml, f"color 应自动补全 FF 前缀\n{xml[:800]}"
|
||||||
|
|
||||||
|
print("✓ test_template_color_prefix_auto_fix passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_template_no_styles_xml():
|
||||||
|
"""边界测试:缺失 styles.xml 时优雅降级。"""
|
||||||
|
from template_reader import read_template_styles
|
||||||
|
import tempfile, os
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
bad_path = os.path.join(tmpdir, "no_styles.xlsx")
|
||||||
|
with zipfile.ZipFile(bad_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
zf.writestr('[Content_Types].xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/></Types>')
|
||||||
|
zf.writestr('xl/worksheets/sheet1.xml', '<?xml version="1.0"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>')
|
||||||
|
|
||||||
|
style = read_template_styles(bad_path)
|
||||||
|
assert style is not None, "缺失 styles.xml 不应导致 read_template_styles 返回 None"
|
||||||
|
assert len(style.fonts) == 0, "无 styles.xml,font 列表应为空"
|
||||||
|
assert len(style.fills) == 0, "无 styles.xml,fill 列表应为空"
|
||||||
|
print("✓ test_template_no_styles_xml passed")
|
||||||
|
finally:
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_4x4_parse()
|
test_4x4_parse()
|
||||||
test_4x4_validate()
|
test_4x4_validate()
|
||||||
@@ -224,4 +566,15 @@ if __name__ == "__main__":
|
|||||||
test_empty_cells()
|
test_empty_cells()
|
||||||
test_no_pins()
|
test_no_pins()
|
||||||
test_12pin_square()
|
test_12pin_square()
|
||||||
|
test_f012_pinname_position()
|
||||||
|
# v1.5 新增测试
|
||||||
|
test_template_path_generation()
|
||||||
|
test_f011_default_styles_xml()
|
||||||
|
test_f011_template_fonts_in_styles_xml()
|
||||||
|
test_f011_output_dims_determined_by_pins()
|
||||||
|
test_f011_template_borders_in_styles_xml()
|
||||||
|
test_f011_template_fills_in_styles_xml()
|
||||||
|
test_template_empty_fonts_fallback()
|
||||||
|
test_template_color_prefix_auto_fix()
|
||||||
|
test_template_no_styles_xml()
|
||||||
print("\n✅ All tests passed!")
|
print("\n✅ All tests passed!")
|
||||||
|
|||||||
@@ -258,93 +258,35 @@ class StyledXLSXWriter:
|
|||||||
return '\n'.join(parts)
|
return '\n'.join(parts)
|
||||||
|
|
||||||
def _styles_xml(self) -> str:
|
def _styles_xml(self) -> str:
|
||||||
"""Build xl/styles.xml with fonts, fills, borders, and cellXfs."""
|
"""Build xl/styles.xml with fonts, fills, borders, and cellXfs.
|
||||||
|
|
||||||
|
F011: 有模板时从模板的 fonts / fills / borders / cellXfs 中
|
||||||
|
提取实际样式定义;无模板时回退到硬编码默认样式。
|
||||||
|
|
||||||
|
保留现有 4 个样式槽位(xf 0~3):
|
||||||
|
xf 0 = default (no style)
|
||||||
|
xf 1 = centered + thin border (pin cells)
|
||||||
|
xf 2 = bold + centered (A1 package info)
|
||||||
|
xf 3 = centered + border + light fill (header-like)
|
||||||
|
|
||||||
|
有模板时,四个 xf 的字体/边框/填充/对齐从模板读取。
|
||||||
|
"""
|
||||||
s = self._style
|
s = self._style
|
||||||
|
|
||||||
# ── Fonts ──────────────────────────────────────────────────
|
|
||||||
fonts_xml = '<fonts count="2">'
|
|
||||||
# Font 0: default (no bold)
|
|
||||||
font_name = "Calibri"
|
|
||||||
font_size = "11"
|
|
||||||
font_color = "FF000000"
|
|
||||||
if s and s.fonts:
|
if s and s.fonts:
|
||||||
f0 = s.fonts[0]
|
# ── 有模板:从模板提取实际值构建样式 ──────────────────
|
||||||
font_name = f0.name
|
fonts_xml = self._build_fonts_xml_from_template(s.fonts)
|
||||||
font_size = str(f0.size)
|
fills_xml = self._build_fills_xml_from_template(s.fills)
|
||||||
font_color = "FF" + f0.color if f0.color and not f0.color.startswith("FF") else f0.color
|
borders_xml = self._build_borders_xml_from_template(s.borders)
|
||||||
fonts_xml += (
|
cell_xfs_xml = self._build_cell_xfs_xml_from_template(
|
||||||
f'<font><sz val="{font_size}"/>'
|
s.cell_xfs, s.fonts, s.fills, s.borders
|
||||||
f'<name val="{font_name}"/>'
|
|
||||||
f'<color rgb="{font_color}"/>'
|
|
||||||
f'</font>'
|
|
||||||
)
|
)
|
||||||
# Font 1: bold (for package info in A1)
|
else:
|
||||||
fonts_xml += (
|
# ── 无模板:回退到硬编码默认样式(F011 fallback)──────
|
||||||
f'<font><sz val="{font_size}"/>'
|
fonts_xml = self._default_fonts_xml()
|
||||||
f'<b/>'
|
fills_xml = self._default_fills_xml()
|
||||||
f'<name val="{font_name}"/>'
|
borders_xml = self._default_borders_xml()
|
||||||
f'<color rgb="{font_color}"/>'
|
cell_xfs_xml = self._default_cell_xfs_xml()
|
||||||
f'</font>'
|
|
||||||
)
|
|
||||||
fonts_xml += '</fonts>'
|
|
||||||
|
|
||||||
# ── Fills ──────────────────────────────────────────────────
|
|
||||||
fills_xml = '<fills count="2">'
|
|
||||||
fills_xml += '<fill><patternFill patternType="none"/></fill>'
|
|
||||||
# Fill 1: light gray for header-like cells
|
|
||||||
fills_xml += (
|
|
||||||
'<fill><patternFill patternType="solid">'
|
|
||||||
'<fgColor rgb="FFF0F0F0"/>'
|
|
||||||
'</patternFill></fill>'
|
|
||||||
)
|
|
||||||
fills_xml += '</fills>'
|
|
||||||
|
|
||||||
# ── Borders ────────────────────────────────────────────────
|
|
||||||
borders_xml = '<borders count="2">'
|
|
||||||
# Border 0: none
|
|
||||||
borders_xml += (
|
|
||||||
'<border>'
|
|
||||||
'<left/><right/><top/><bottom/><diagonal/>'
|
|
||||||
'</border>'
|
|
||||||
)
|
|
||||||
# Border 1: thin all sides
|
|
||||||
borders_xml += (
|
|
||||||
'<border>'
|
|
||||||
'<left style="thin"/><right style="thin"/>'
|
|
||||||
'<top style="thin"/><bottom style="thin"/>'
|
|
||||||
'<diagonal/>'
|
|
||||||
'</border>'
|
|
||||||
)
|
|
||||||
borders_xml += '</borders>'
|
|
||||||
|
|
||||||
# ── Cell XFs ───────────────────────────────────────────────
|
|
||||||
# xf 0: default (no style)
|
|
||||||
# xf 1: centered with thin border (for pin cells)
|
|
||||||
# xf 2: bold + centered (for A1 package info)
|
|
||||||
# xf 3: centered + border + light fill (for header-like)
|
|
||||||
cell_xfs_xml = '<cellXfs count="4">'
|
|
||||||
cell_xfs_xml += (
|
|
||||||
'<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'
|
|
||||||
)
|
|
||||||
cell_xfs_xml += (
|
|
||||||
'<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" '
|
|
||||||
'applyBorder="1">'
|
|
||||||
'<alignment horizontal="center" vertical="center"/>'
|
|
||||||
'</xf>'
|
|
||||||
)
|
|
||||||
cell_xfs_xml += (
|
|
||||||
'<xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" '
|
|
||||||
'applyFont="1">'
|
|
||||||
'<alignment horizontal="center" vertical="center"/>'
|
|
||||||
'</xf>'
|
|
||||||
)
|
|
||||||
cell_xfs_xml += (
|
|
||||||
'<xf numFmtId="0" fontId="0" fillId="1" borderId="1" xfId="0" '
|
|
||||||
'applyFill="1" applyBorder="1">'
|
|
||||||
'<alignment horizontal="center" vertical="center"/>'
|
|
||||||
'</xf>'
|
|
||||||
)
|
|
||||||
cell_xfs_xml += '</cellXfs>'
|
|
||||||
|
|
||||||
parts = ['<?xml version="1.0" encoding="UTF-8" standalone="yes"?>']
|
parts = ['<?xml version="1.0" encoding="UTF-8" standalone="yes"?>']
|
||||||
parts.append(
|
parts.append(
|
||||||
@@ -357,6 +299,250 @@ class StyledXLSXWriter:
|
|||||||
parts.append('</styleSheet>')
|
parts.append('</styleSheet>')
|
||||||
return '\n'.join(parts)
|
return '\n'.join(parts)
|
||||||
|
|
||||||
|
# ── 模板样式构建(F011)─────────────────────────────────────────
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_fonts_xml_from_template(fonts: list) -> str:
|
||||||
|
"""从模板字体列表构建 <fonts> XML。"""
|
||||||
|
parts = [f'<fonts count="{len(fonts)}">']
|
||||||
|
for f in fonts:
|
||||||
|
parts.append('<font>')
|
||||||
|
parts.append(f'<sz val="{f.size}"/>')
|
||||||
|
if f.bold:
|
||||||
|
parts.append('<b/>')
|
||||||
|
if f.italic:
|
||||||
|
parts.append('<i/>')
|
||||||
|
parts.append(f'<name val="{f.name}"/>')
|
||||||
|
color_val = f.color
|
||||||
|
if color_val and not color_val.startswith('FF'):
|
||||||
|
color_val = 'FF' + color_val
|
||||||
|
if color_val:
|
||||||
|
parts.append(f'<color rgb="{color_val}"/>')
|
||||||
|
parts.append('</font>')
|
||||||
|
parts.append('</fonts>')
|
||||||
|
return ''.join(parts)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_fills_xml_from_template(fills: list) -> str:
|
||||||
|
"""从模板填充列表构建 <fills> XML。"""
|
||||||
|
parts = [f'<fills count="{len(fills)}">']
|
||||||
|
for fl in fills:
|
||||||
|
parts.append('<fill>')
|
||||||
|
parts.append(f'<patternFill patternType="{fl.pattern_type}">')
|
||||||
|
if fl.fg_color:
|
||||||
|
color_val = fl.fg_color
|
||||||
|
if not color_val.startswith('FF'):
|
||||||
|
color_val = 'FF' + color_val
|
||||||
|
parts.append(f'<fgColor rgb="{color_val}"/>')
|
||||||
|
parts.append('</patternFill>')
|
||||||
|
parts.append('</fill>')
|
||||||
|
parts.append('</fills>')
|
||||||
|
return ''.join(parts)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_borders_xml_from_template(borders: list) -> str:
|
||||||
|
"""从模板边框列表构建 <borders> XML。"""
|
||||||
|
parts = [f'<borders count="{len(borders)}">']
|
||||||
|
for b in borders:
|
||||||
|
parts.append('<border>')
|
||||||
|
for side_name in ('left', 'right', 'top', 'bottom'):
|
||||||
|
style_val = getattr(b, side_name, 'none')
|
||||||
|
if style_val and style_val != 'none':
|
||||||
|
parts.append(f'<{side_name} style="{style_val}"/>')
|
||||||
|
else:
|
||||||
|
parts.append(f'<{side_name}/>')
|
||||||
|
parts.append('<diagonal/>')
|
||||||
|
parts.append('</border>')
|
||||||
|
parts.append('</borders>')
|
||||||
|
return ''.join(parts)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_cell_xfs_xml_from_template(
|
||||||
|
cell_xfs: list, fonts: list, fills: list, borders: list
|
||||||
|
) -> str:
|
||||||
|
"""从模板 cellXfs 列表构建 <cellXfs> XML。
|
||||||
|
|
||||||
|
保留 4 个样式槽位(xf 0~3),每个从模板对应位置的 xf 提取:
|
||||||
|
- xf 0: default (模板的 xf 0)
|
||||||
|
- xf 1: pin cells — 使用模板中带边框的 xf(优先),否则 fallback
|
||||||
|
- xf 2: A1 bold — 使用模板中带 bold 字体的 xf(优先),否则 fallback
|
||||||
|
- xf 3: header — 使用模板中带填充的 xf(优先),否则 fallback
|
||||||
|
"""
|
||||||
|
def _xf_to_attrs(xf: dict) -> str:
|
||||||
|
"""将 xf 信息字典转换为属性字符串。"""
|
||||||
|
attrs = [
|
||||||
|
f'numFmtId="{xf.get("numFmtId", "0")}"',
|
||||||
|
f'fontId="{xf.get("fontId", "0")}"',
|
||||||
|
f'fillId="{xf.get("fillId", "0")}"',
|
||||||
|
f'borderId="{xf.get("borderId", "0")}"',
|
||||||
|
f'xfId="{xf.get("xfId", "0")}"',
|
||||||
|
]
|
||||||
|
for attr_key in ('applyFont', 'applyFill', 'applyBorder', 'applyAlignment',
|
||||||
|
'applyNumberFormat', 'applyProtection'):
|
||||||
|
val = xf.get(attr_key, '')
|
||||||
|
if val:
|
||||||
|
attrs.append(f'{attr_key}="{val}"')
|
||||||
|
return ' '.join(attrs)
|
||||||
|
|
||||||
|
def _xf_to_alignment(xf: dict) -> str:
|
||||||
|
"""从 xf 信息字典生成对齐 XML片段。"""
|
||||||
|
h_align = xf.get('hAlign', '')
|
||||||
|
v_align = xf.get('vAlign', '')
|
||||||
|
wrap_text = xf.get('wrapText', '')
|
||||||
|
if h_align or v_align or wrap_text:
|
||||||
|
align_attrs = []
|
||||||
|
if h_align:
|
||||||
|
align_attrs.append(f'horizontal="{h_align}"')
|
||||||
|
if v_align:
|
||||||
|
align_attrs.append(f'vertical="{v_align}"')
|
||||||
|
if wrap_text:
|
||||||
|
align_attrs.append(f'wrapText="{wrap_text}"')
|
||||||
|
return f'<alignment {" ".join(align_attrs)}/>'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
# 确保有足够的模板样式元素
|
||||||
|
def _get_or_default(lst, idx, default_name):
|
||||||
|
if idx < len(lst):
|
||||||
|
return idx
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# ── 确定 4 个 xf 的映射 ──────────────────────────────────
|
||||||
|
# xf 0: 模板的 xf 0(default)
|
||||||
|
xf0 = cell_xfs[0] if len(cell_xfs) > 0 else {}
|
||||||
|
fi0 = _get_or_default(fonts, int(xf0.get('fontId', '0')), 'font')
|
||||||
|
fl0 = _get_or_default(fills, int(xf0.get('fillId', '0')), 'fill')
|
||||||
|
bd0 = _get_or_default(borders, int(xf0.get('borderId', '0')), 'border')
|
||||||
|
|
||||||
|
# xf 1: 寻找模板中有边框的 xf(borderId > 0 或 applyBorder 非空)
|
||||||
|
xf1 = xf0
|
||||||
|
fi1, fl1, bd1 = fi0, fl0, bd0
|
||||||
|
for xf in cell_xfs:
|
||||||
|
bid = int(xf.get('borderId', '0'))
|
||||||
|
ab = xf.get('applyBorder', '')
|
||||||
|
if bid > 0 or ab:
|
||||||
|
xf1 = xf
|
||||||
|
fi1 = _get_or_default(fonts, int(xf1.get('fontId', '0')), 'font')
|
||||||
|
fl1 = _get_or_default(fills, int(xf1.get('fillId', '0')), 'fill')
|
||||||
|
bd1 = _get_or_default(borders, int(xf1.get('borderId', '0')), 'border')
|
||||||
|
break
|
||||||
|
|
||||||
|
# xf 2: 寻找模板中有 bold 字体的 xf
|
||||||
|
xf2 = xf0
|
||||||
|
fi2, fl2, bd2 = fi0, fl0, bd0
|
||||||
|
bold_font_id = None
|
||||||
|
for i, f in enumerate(fonts):
|
||||||
|
if f.bold:
|
||||||
|
bold_font_id = i
|
||||||
|
break
|
||||||
|
if bold_font_id is not None:
|
||||||
|
for xf in cell_xfs:
|
||||||
|
fid = int(xf.get('fontId', '0'))
|
||||||
|
if xf.get('applyFont', '') or fid == bold_font_id:
|
||||||
|
xf2 = xf
|
||||||
|
break
|
||||||
|
fi2 = bold_font_id
|
||||||
|
fl2 = _get_or_default(fills, int(xf2.get('fillId', '0')), 'fill')
|
||||||
|
bd2 = _get_or_default(borders, int(xf2.get('borderId', '0')), 'border')
|
||||||
|
|
||||||
|
# xf 3: 寻找模板中有填充的 xf(fillId > 0 或 applyFill 非空)
|
||||||
|
xf3 = xf0
|
||||||
|
fi3, fl3, bd3 = fi0, fl0, bd0
|
||||||
|
for xf in cell_xfs:
|
||||||
|
fid = int(xf.get('fillId', '0'))
|
||||||
|
af = xf.get('applyFill', '')
|
||||||
|
if fid > 0 or af:
|
||||||
|
xf3 = xf
|
||||||
|
fi3 = _get_or_default(fonts, int(xf3.get('fontId', '0')), 'font')
|
||||||
|
fl3 = _get_or_default(fills, int(xf3.get('fillId', '0')), 'fill')
|
||||||
|
bd3 = _get_or_default(borders, int(xf3.get('borderId', '0')), 'border')
|
||||||
|
break
|
||||||
|
|
||||||
|
# ── 构建 4 个 xf ──────────────────────────────────────────
|
||||||
|
parts = ['<cellXfs count="4">']
|
||||||
|
|
||||||
|
# xf 0: default (no style)
|
||||||
|
parts.append(f'<xf numFmtId="0" fontId="{fi0}" fillId="{fl0}" borderId="{bd0}" xfId="0"/>')
|
||||||
|
|
||||||
|
# xf 1: pin cells (border + center align)
|
||||||
|
align1 = _xf_to_alignment(xf1)
|
||||||
|
parts.append(
|
||||||
|
f'<xf numFmtId="0" fontId="{fi1}" fillId="{fl1}" borderId="{bd1}" xfId="0" applyBorder="1">'
|
||||||
|
f'{align1}'
|
||||||
|
f'</xf>'
|
||||||
|
)
|
||||||
|
|
||||||
|
# xf 2: A1 package info (bold + center align)
|
||||||
|
align2 = _xf_to_alignment(xf2) or '<alignment horizontal="center" vertical="center"/>'
|
||||||
|
parts.append(
|
||||||
|
f'<xf numFmtId="0" fontId="{fi2}" fillId="{fl2}" borderId="{bd2}" xfId="0" applyFont="1">'
|
||||||
|
f'{align2}'
|
||||||
|
f'</xf>'
|
||||||
|
)
|
||||||
|
|
||||||
|
# xf 3: header-like (fill + border + center align)
|
||||||
|
align3 = _xf_to_alignment(xf3) or '<alignment horizontal="center" vertical="center"/>'
|
||||||
|
parts.append(
|
||||||
|
f'<xf numFmtId="0" fontId="{fi3}" fillId="{fl3}" borderId="{bd3}" xfId="0" applyFill="1" applyBorder="1">'
|
||||||
|
f'{align3}'
|
||||||
|
f'</xf>'
|
||||||
|
)
|
||||||
|
|
||||||
|
parts.append('</cellXfs>')
|
||||||
|
return ''.join(parts)
|
||||||
|
|
||||||
|
# ── 默认硬编码样式(无模板时回退)────────────────────────────
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _default_fonts_xml() -> str:
|
||||||
|
"""生成默认硬编码字体:font 0 = Calibri 11pt, font 1 = Calibri 11pt bold。"""
|
||||||
|
return (
|
||||||
|
'<fonts count="2">'
|
||||||
|
'<font><sz val="11"/><name val="Calibri"/><color rgb="FF000000"/></font>'
|
||||||
|
'<font><sz val="11"/><b/><name val="Calibri"/><color rgb="FF000000"/></font>'
|
||||||
|
'</fonts>'
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _default_fills_xml() -> str:
|
||||||
|
"""生成默认硬编码填充:fill 0 = none, fill 1 = light gray。"""
|
||||||
|
return (
|
||||||
|
'<fills count="2">'
|
||||||
|
'<fill><patternFill patternType="none"/></fill>'
|
||||||
|
'<fill><patternFill patternType="solid"><fgColor rgb="FFF0F0F0"/></patternFill></fill>'
|
||||||
|
'</fills>'
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _default_borders_xml() -> str:
|
||||||
|
"""生成默认硬编码边框:border 0 = none, border 1 = thin all sides。"""
|
||||||
|
return (
|
||||||
|
'<borders count="2">'
|
||||||
|
'<border><left/><right/><top/><bottom/><diagonal/></border>'
|
||||||
|
'<border><left style="thin"/><right style="thin"/>'
|
||||||
|
'<top style="thin"/><bottom style="thin"/><diagonal/></border>'
|
||||||
|
'</borders>'
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _default_cell_xfs_xml() -> str:
|
||||||
|
"""生成默认硬编码 cellXfs:
|
||||||
|
xf 0 = default, xf 1 = centered+border, xf 2 = bold+centered, xf 3 = centered+border+fill。
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
'<cellXfs count="4">'
|
||||||
|
'<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'
|
||||||
|
'<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1">'
|
||||||
|
'<alignment horizontal="center" vertical="center"/>'
|
||||||
|
'</xf>'
|
||||||
|
'<xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1">'
|
||||||
|
'<alignment horizontal="center" vertical="center"/>'
|
||||||
|
'</xf>'
|
||||||
|
'<xf numFmtId="0" fontId="0" fillId="1" borderId="1" xfId="0" applyFill="1" applyBorder="1">'
|
||||||
|
'<alignment horizontal="center" vertical="center"/>'
|
||||||
|
'</xf>'
|
||||||
|
'</cellXfs>'
|
||||||
|
)
|
||||||
|
|
||||||
def _sheet_xml(self, data: dict[str, str]) -> str:
|
def _sheet_xml(self, data: dict[str, str]) -> str:
|
||||||
"""Build sheet1.xml with style indices applied."""
|
"""Build sheet1.xml with style indices applied."""
|
||||||
max_row = 0
|
max_row = 0
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -9,6 +9,9 @@
|
|||||||
- ✅ GUI 文件选择 + 命令行双模式
|
- ✅ GUI 文件选择 + 命令行双模式
|
||||||
- ✅ 智能结构验证(重复/间隙/空单元格检测)
|
- ✅ 智能结构验证(重复/间隙/空单元格检测)
|
||||||
- ✅ 逆时针 PinMAP → 顺时针 PinList 自动转换
|
- ✅ 逆时针 PinMAP → 顺时针 PinList 自动转换
|
||||||
|
- ✅ 双向转换:MAP→List 与 List→MAP
|
||||||
|
- ✅ **独立模板**:MAP→List 使用 `BallList-Template.xlsx`,List→MAP 使用 `BallMAP-Template.xlsx`
|
||||||
|
- ✅ **模板格式提取**:从模板读取字体、边框、填充、对齐、列宽、行高并应用到输出
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
@@ -30,9 +33,11 @@ pinmap-to-pinlist/
|
|||||||
│ ├── src/ # 源代码
|
│ ├── src/ # 源代码
|
||||||
│ └── docs/ # 架构文档
|
│ └── docs/ # 架构文档
|
||||||
├── Test/
|
├── Test/
|
||||||
│ ├── fixtures/ # 测试夹具
|
│ ├── fixtures/ # 测试夹具(含模板文件)
|
||||||
│ └── test_report.md # 测试报告
|
│ └── test_report.md # 测试报告
|
||||||
├── Releases/ # 发布包
|
├── Releases/ # 发布包
|
||||||
|
├── BallList-Template.xlsx # MAP→List 样式模板(可放置于项目根目录)
|
||||||
|
├── BallMAP-Template.xlsx # List→MAP 样式模板(可放置于项目根目录)
|
||||||
├── CHANGELOG.md
|
├── CHANGELOG.md
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
@@ -43,6 +48,25 @@ pinmap-to-pinlist/
|
|||||||
- openpyxl(.xlsx 读写)
|
- openpyxl(.xlsx 读写)
|
||||||
- 自定义 BIFF8 引擎(.xls 解析)
|
- 自定义 BIFF8 引擎(.xls 解析)
|
||||||
|
|
||||||
|
## 版本历史
|
||||||
|
|
||||||
|
### v1.5.4 (2026-06-09) — Bug 修复版本
|
||||||
|
|
||||||
|
- **BUG-005**: 模板文件名修正 — `BallList-Template.xlsx` → `PinList-Template.xlsx`,`BallMAP-Template.xlsx` → `PinMAP-Template.xlsx`
|
||||||
|
- **BUG-006**: 布局重设计 — Number 外侧(第 1 圈)+ Name 里侧(第 2 圈),彻底解决单元格冲突问题
|
||||||
|
- 上边:Number row 1,Name row 2(角点例外)
|
||||||
|
- 左边:Number col 0,Name col 1
|
||||||
|
- 下边:Number row rows+3,Name row rows+2
|
||||||
|
- 右边:Number col cols+1,Name col cols
|
||||||
|
- Pin1 保持在左上角(A3=1, B3=Pin1)
|
||||||
|
- 18/18 单元测试 + 37/37 集成测试全部通过
|
||||||
|
|
||||||
|
### v1.5.0 (2026-06-06) — 模板分离与格式提取
|
||||||
|
|
||||||
|
- MAP→List 使用 `PinList-Template.xlsx`(旧名 `BallList-Template.xlsx`)
|
||||||
|
- List→MAP 使用 `PinMAP-Template.xlsx`(旧名 `BallMAP-Template.xlsx`)
|
||||||
|
- 模板格式提取:字体、边框、填充、对齐、列宽、行高
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
内部项目
|
内部项目
|
||||||
|
|||||||
BIN
Releases/pinmap-to-pinlist-v1.3.14.zip
Normal file
BIN
Releases/pinmap-to-pinlist-v1.3.14.zip
Normal file
Binary file not shown.
BIN
Releases/pinmap-to-pinlist-v1.3.15.zip
Normal file
BIN
Releases/pinmap-to-pinlist-v1.3.15.zip
Normal file
Binary file not shown.
60
Releases/v1.5.4/CHANGELOG.md
Normal file
60
Releases/v1.5.4/CHANGELOG.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Changelog — v1.5.4
|
||||||
|
|
||||||
|
> **发布日期**: 2026-06-09
|
||||||
|
> **版本类型**: Bug 修复版本
|
||||||
|
|
||||||
|
## 🐛 Bug 修复
|
||||||
|
|
||||||
|
### BUG-005 【高】模板文件名错误
|
||||||
|
|
||||||
|
**问题**: `main.py` 中引用的模板文件名(`BallList-Template.xlsx` 和 `BallMAP-Template.xlsx`)与用户期望的文件名不匹配。
|
||||||
|
|
||||||
|
**修复**:
|
||||||
|
- 模板文件重命名:`BallList-Template.xlsx` → `PinList-Template.xlsx`
|
||||||
|
- 模板文件重命名:`BallMAP-Template.xlsx` → `PinMAP-Template.xlsx`
|
||||||
|
- 同步更新 `main.py` 中的函数名和模板引用路径
|
||||||
|
|
||||||
|
### BUG-006 【高】布局重设计(Number 外侧 + Name 里侧)
|
||||||
|
|
||||||
|
**问题**: PinList→PinMAP→PinList 双向转换中,v1.3 的"紧致布局"导致 Number 与 Name 单元格冲突(6 处),15×15 网格下序号 1 错位到 A2,序号 16 错位到 B16。
|
||||||
|
|
||||||
|
**根本原因**: 旧布局将 Name 放在 Number 向内偏移一行/一列的位置,边角处发生冲突。
|
||||||
|
|
||||||
|
**修复方案**: 重新设计布局为 **Number 外侧(第 1 圈)+ Name 里侧(第 2 圈)**,从网格边界往中心排列:
|
||||||
|
|
||||||
|
| 边 | 外侧(第 1 圈) | 内侧(第 2 圈) |
|
||||||
|
|---|---|---|
|
||||||
|
| **上边** | Number 在 row 1(最顶行) | Name 在 row 2(第二行;角点例外在 row 1) |
|
||||||
|
| **左边** | Number 在 col 0(最左列) | Name 在 col 1(第二列) |
|
||||||
|
| **下边** | Number 在 row rows+3(最底行) | Name 在 row rows+2(倒数第二行) |
|
||||||
|
| **右边** | Number 在 col cols+1(最右列) | Name 在 col cols(右二列) |
|
||||||
|
|
||||||
|
**关键设计点**:
|
||||||
|
- **上边角点例外**: 最左/最右上边 Name 无法放在 row 2(被左/右边 Name 占用),分别使用 `(1, 0)` 和 `(1, cols+1)` 例外单元格
|
||||||
|
- Pin1 保持在左上角(A3=1, B3=Pin1)
|
||||||
|
- 不再需要角点 `"//"` 合并 — 每条边不共享任何单元格
|
||||||
|
- 周长公式 `(rows+cols)×2` 保持不变
|
||||||
|
|
||||||
|
**验证**:
|
||||||
|
- ✅ 15 种网格大小(4×4, 15×15, 3×5, 2×2, 8×8, 10×12, 20×20, 5×3, 6×7, 2×3, 3×3, 2×4, 3×2, 4×2, 2×5)全部无冲突
|
||||||
|
- ✅ 18/18 单元测试通过
|
||||||
|
- ✅ 37/37 集成测试通过
|
||||||
|
|
||||||
|
## 🔧 修改文件
|
||||||
|
|
||||||
|
| 文件 | 修改内容 |
|
||||||
|
|------|---------|
|
||||||
|
| `Code/src/main.py` | BUG-005: 模板函数和引用改名;BUG-006: 传递 cols 参数 |
|
||||||
|
| `Code/src/pinmap_layout.py` | BUG-006: 重写坐标公式 + `get_name_cell()` 支持 cols 参数 + 角点例外 |
|
||||||
|
| `Code/src/pinmap_generator.py` | BUG-006: 传递 cols 参数 + 更新注释 |
|
||||||
|
| `Code/src/pinmap_parser.py` | BUG-006: 重写边界检测、Name 读取(角点例外检测) |
|
||||||
|
| `Code/src/test_pinmap.py` | BUG-006: 更新测试数据适配新布局 |
|
||||||
|
| `Test/fixtures/PinList-Template.xlsx` | BUG-005: 模板文件重命名 |
|
||||||
|
| `Test/fixtures/PinMAP-Template.xlsx` | BUG-005: 模板文件重命名 |
|
||||||
|
|
||||||
|
## 📝 文档
|
||||||
|
|
||||||
|
- 更新 `CHANGELOG.md` 追加 v1.5.4 版本日志
|
||||||
|
- 更新 `README.md` 追加 v1.5.4 版本说明
|
||||||
|
- 生成 `Releases/v1.5.4/CHANGELOG.md`
|
||||||
|
- 更新 `docs/bugs.md` BUG-005、BUG-006 状态为已修复
|
||||||
BIN
Test/fixtures/PinList-Template.xlsx
vendored
Normal file
BIN
Test/fixtures/PinList-Template.xlsx
vendored
Normal file
Binary file not shown.
BIN
Test/fixtures/PinMAP-Template.xlsx
vendored
Normal file
BIN
Test/fixtures/PinMAP-Template.xlsx
vendored
Normal file
Binary file not shown.
BIN
Test/fixtures/sample_4x4.xlsx
vendored
BIN
Test/fixtures/sample_4x4.xlsx
vendored
Binary file not shown.
1
Test/fixtures/template_corrupt.xlsx
vendored
Normal file
1
Test/fixtures/template_corrupt.xlsx
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This is not a valid xlsx file. It's just plain text pretending to be xlsx.
|
||||||
BIN
Test/fixtures/template_minimal.xlsx
vendored
Normal file
BIN
Test/fixtures/template_minimal.xlsx
vendored
Normal file
Binary file not shown.
BIN
Test/fixtures/template_narrow.xlsx
vendored
Normal file
BIN
Test/fixtures/template_narrow.xlsx
vendored
Normal file
Binary file not shown.
@@ -84,19 +84,27 @@ def create_pinmap_fixture(data: dict, path: str):
|
|||||||
def test_map_to_list(r: TestRunner):
|
def test_map_to_list(r: TestRunner):
|
||||||
fixture_dir = os.path.join(os.path.dirname(__file__), 'fixtures')
|
fixture_dir = os.path.join(os.path.dirname(__file__), 'fixtures')
|
||||||
|
|
||||||
# TC-MAP-001: 标准 4x4 PinMAP 转换
|
# TC-MAP-001: 标准 4x4 PinMAP 转换 (v1.5.4 布局)
|
||||||
def _tc_map_001(result):
|
def _tc_map_001(result):
|
||||||
filepath = os.path.join(fixture_dir, 'sample_4x4.xlsx')
|
filepath = os.path.join(fixture_dir, 'sample_4x4.xlsx')
|
||||||
cells = read_xlsx_cells(filepath)
|
cells = read_xlsx_cells(filepath)
|
||||||
pinmap = parse_pinmap(cells)
|
pinmap = parse_pinmap(cells)
|
||||||
validation = validate_pinmap(pinmap)
|
validation = validate_pinmap(pinmap)
|
||||||
pinlist = generate_pinlist(pinmap, validation)
|
pinlist = generate_pinlist(pinmap, validation)
|
||||||
assert pinlist.package_info, "package_info 不应为空"
|
# 封装信息 (v1.5.4 布局)
|
||||||
assert len(pinlist.rows) > 0, "应有引脚数据"
|
assert pinlist.package_info == "QFP-16", f"封装应为 QFP-16,实际: {pinlist.package_info}"
|
||||||
|
# 引脚数 (4x4 网格: (4+4)*2 = 16)
|
||||||
|
assert len(pinlist.rows) == 16, f"应有 16 个引脚,实际: {len(pinlist.rows)}"
|
||||||
# 验证递增排序
|
# 验证递增排序
|
||||||
nums = [num for _, num in pinlist.rows]
|
nums = [num for _, num in pinlist.rows]
|
||||||
assert nums == sorted(nums), f"序号应递增,实际: {nums}"
|
assert nums == sorted(nums), f"序号应递增,实际: {nums}"
|
||||||
result.ok(f"封装={pinlist.package_info}, Pin数={len(pinlist.rows)}, 序号递增")
|
assert nums == list(range(1, 17)), f"序号应为 1-16,实际: {nums}"
|
||||||
|
# 验证引脚名不是数字(确保 Name/Number 未错位)
|
||||||
|
names = [name for name, _ in pinlist.rows]
|
||||||
|
for name in names:
|
||||||
|
assert not name.isdigit(), f"引脚名 '{name}' 不应为纯数字"
|
||||||
|
assert all(name.startswith("Pin") for name in names), f"所有引脚名应以 Pin 开头: {names}"
|
||||||
|
result.ok(f"封装={pinlist.package_info}, Pin数={len(pinlist.rows)}, 序号 1-16, 引脚名=Pin1..Pin16")
|
||||||
|
|
||||||
r.run("TC-MAP-001: 标准4x4 PinMAP转换", _tc_map_001)
|
r.run("TC-MAP-001: 标准4x4 PinMAP转换", _tc_map_001)
|
||||||
|
|
||||||
@@ -561,6 +569,344 @@ def test_list_to_map(r: TestRunner):
|
|||||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
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 加载 PinList 模板 ──
|
||||||
|
def _tc_v15_001(result):
|
||||||
|
template_path = os.path.join(fixture_dir, 'PinList-Template.xlsx')
|
||||||
|
assert os.path.exists(template_path), f"PinList 模板文件不存在: {template_path}"
|
||||||
|
|
||||||
|
style = read_template_styles(template_path)
|
||||||
|
assert style is not None, "PinList 模板样式应成功读取"
|
||||||
|
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 加载 PinList 模板", _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 加载 PinMAP 模板 ──
|
||||||
|
def _tc_v15_003(result):
|
||||||
|
template_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||||
|
assert os.path.exists(template_path), f"PinMAP 模板文件不存在: {template_path}"
|
||||||
|
|
||||||
|
style = read_template_styles(template_path)
|
||||||
|
assert style is not None, "PinMAP 模板样式应成功读取"
|
||||||
|
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 加载 PinMAP 模板", _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, 'PinList-Template.xlsx')
|
||||||
|
bm_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||||
|
|
||||||
|
style_bl = read_template_styles(bl_path)
|
||||||
|
style_bm = read_template_styles(bm_path)
|
||||||
|
|
||||||
|
assert style_bl is not None, "PinList 模板应成功加载"
|
||||||
|
assert style_bm is not None, "PinMAP 模板应成功加载"
|
||||||
|
|
||||||
|
# PinList 有列宽,PinMAP 有行高
|
||||||
|
assert 0 in style_bl.column_widths, "PinList 应有列宽"
|
||||||
|
assert 0 in style_bm.row_heights, "PinMAP 应有行高"
|
||||||
|
result.ok(f"两个模板独立: PL fonts={len(style_bl.fonts)}, PM 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, 'PinMAP-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, 'PinList-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, 'PinMAP-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, 'PinList-Template.xlsx')
|
||||||
|
bm_path = os.path.join(fixture_dir, 'PinMAP-Template.xlsx')
|
||||||
|
|
||||||
|
style_bl = read_template_styles(bl_path)
|
||||||
|
style_bm = read_template_styles(bm_path)
|
||||||
|
assert style_bl and style_bm, "两个模板都应该成功加载"
|
||||||
|
|
||||||
|
# MAP->List 方向:用 PinList 模板
|
||||||
|
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 方向:用 PinMAP 模板
|
||||||
|
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, "PinList 输出应包含楷体"
|
||||||
|
assert '宋体' in pm_styles, "PinMAP 输出应包含宋体"
|
||||||
|
|
||||||
|
result.ok("两个方向输出字体不同: PinList->楷体, PinMAP->宋体")
|
||||||
|
|
||||||
|
r.run("TC-v1.5-010: 两个方向不同模板各自的格式", _tc_v15_010)
|
||||||
|
|
||||||
|
# ── TC-v1.5-011: 完整往返+模板隔离 (4×4 网格) ──
|
||||||
|
def _tc_v15_011(result):
|
||||||
|
bl_path = os.path.join(fixture_dir, 'PinList-Template.xlsx')
|
||||||
|
bm_path = os.path.join(fixture_dir, 'PinMAP-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')
|
||||||
|
# 4×4 网格: (4+4)×2 = 16 引脚
|
||||||
|
generate_pinmap(entries2, 4, 4, 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 应使用 PinList 模板的楷体"
|
||||||
|
assert '宋体' in pm_xml, "最终 PinMAP 应使用 PinMAP 模板的宋体"
|
||||||
|
|
||||||
|
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')
|
||||||
|
# sample_4x4 有 16 pins,需用 4×4 网格
|
||||||
|
generate_pinmap(entries2, 4, 4, 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 ────────────────────────────────────────────────────────────
|
# ── Main ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -579,6 +925,10 @@ def main():
|
|||||||
test_list_to_map(runner)
|
test_list_to_map(runner)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
print("-- Part 3: v1.5 模板/样式集成测试 --")
|
||||||
|
test_v15_styles(runner)
|
||||||
|
print()
|
||||||
|
|
||||||
total, passed, failed = runner.summary()
|
total, passed, failed = runner.summary()
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(f" 总计: {total} | 通过: {passed} | 失败: {failed}")
|
print(f" 总计: {total} | 通过: {passed} | 失败: {failed}")
|
||||||
@@ -611,13 +961,17 @@ def generate_report(runner: TestRunner, total: int, passed: int, failed: int):
|
|||||||
# Count by category
|
# Count by category
|
||||||
map_results = [r for r in runner.results if r.name.startswith("TC-MAP")]
|
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")]
|
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_pass = sum(1 for r in map_results if r.passed)
|
||||||
map_fail = len(map_results) - map_pass
|
map_fail = len(map_results) - map_pass
|
||||||
lm_pass = sum(1 for r in lm_results if r.passed)
|
lm_pass = sum(1 for r in lm_results if r.passed)
|
||||||
lm_fail = len(lm_results) - lm_pass
|
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"| MAP->List 回归 | {len(map_results)} | {map_pass} | {map_fail} |")
|
||||||
lines.append(f"| List→MAP 新增 | {len(lm_results)} | {lm_pass} | {lm_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(f"| **总计** | **{total}** | **{passed}** | **{failed}** |")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("---")
|
lines.append("---")
|
||||||
@@ -649,6 +1003,19 @@ def generate_report(runner: TestRunner, total: int, passed: int, failed: int):
|
|||||||
lines.append(f"- **错误**: {r.error}")
|
lines.append(f"- **错误**: {r.error}")
|
||||||
lines.append("")
|
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
|
# Summary
|
||||||
lines.append("---")
|
lines.append("---")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|||||||
680
Test/test_plan_v1.5.md
Normal file
680
Test/test_plan_v1.5.md
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
# PinMAP-to-PinList v1.5.0 — 测试方案
|
||||||
|
|
||||||
|
> **版本**: v1.5.0
|
||||||
|
> **日期**: 2026-06-06
|
||||||
|
> **设计人**: 测试架构师 (Test Architect)
|
||||||
|
> **基准**: 修改需求评估 `docs/modification-assessment-v1.5.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 变更影响范围
|
||||||
|
|
||||||
|
| 编号 | 标题 | 影响文件 | 测试重点 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| F009 | MAP→List 使用 BallList-Template.xlsx | `main.py` | 模板分离后方向独立,互不干扰 |
|
||||||
|
| F010 | List→MAP 使用 BallMAP-Template.xlsx | `main.py` | 模板分离后方向独立,互不干扰 |
|
||||||
|
| F011 | 模板格式提取式应用 | `xlsx_writer.py`, `template_reader.py` | 字体/边框/填充/对齐/列宽/行高正确应用 |
|
||||||
|
| F012 | PinName 位置确认 + 回归测试 | `test_pinmap.py` | 上/下边 PinName 位置正确,往返一致性 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 已有测试覆盖分析
|
||||||
|
|
||||||
|
### 2.1 单元测试 (`Code/src/test_pinmap.py`) — 9 个用例,全部通过
|
||||||
|
|
||||||
|
| 用例 | 覆盖范围 | v1.5 是否受影响 |
|
||||||
|
|------|---------|----------------|
|
||||||
|
| `test_4x4_parse` | 4×4 PinMAP 解析 (left/bottom/right/top) | ✅ 已验证 bottom/right Name 位置正确 |
|
||||||
|
| `test_4x4_validate` | 4×4 PinMAP 验证 | 不受影响 |
|
||||||
|
| `test_missing_names_warning` | 缺失 Name 警告 | 不受影响 |
|
||||||
|
| `test_duplicate_numbers` | 重复序号错误 | 不受影响 |
|
||||||
|
| `test_gap_in_numbers` | 序号不连续错误 | 不受影响 |
|
||||||
|
| `test_empty_cells` | 空单元格异常 | 不受影响 |
|
||||||
|
| `test_no_pins` | 无 Pin 数据异常 | 不受影响 |
|
||||||
|
| `test_12pin_square` | 6×6 12Pin 解析和验证 | 不受影响 |
|
||||||
|
| `test_f012_pinname_position` | **v1.5 新增** — 5×5 20Pin 往返一致性 | ✅ **v1.5 核心回归测试** |
|
||||||
|
|
||||||
|
### 2.2 集成测试 (`Test/run_tests.py`) — 23 个用例,全部通过
|
||||||
|
|
||||||
|
| 编号 | 用例 | v1.5 是否受影响 |
|
||||||
|
|------|------|----------------|
|
||||||
|
| TC-MAP-001~006 | MAP→List 回归(含错误/警告场景) | ⚠️ F009 影响模板查找逻辑 |
|
||||||
|
| TC-LM-001~016 | List→MAP 新增(含错误/警告/往返) | ⚠️ F010/F011 影响模板样式应用 |
|
||||||
|
|
||||||
|
### 2.3 已有测试缺口(v1.5 前)
|
||||||
|
|
||||||
|
1. **无模板相关测试** — 现有集成测试仅在 `TC-LM-003` 中使用临时创建的 fake 模板验证 styles.xml 存在,未测试真实的 BallList/BallMAP 模板分离
|
||||||
|
2. **无样式正确性验证** — 现有测试仅验证 `styles.xml` 文件存在,未验证字体/边框/填充/对齐/列宽/行高的实际内容
|
||||||
|
3. **无模板降级测试** — 未测试模板不存在/解析失败时的优雅降级行为
|
||||||
|
4. **无两方向独立模板测试** — 未验证 MAP→List 和 List→MAP 使用不同模板时的隔离性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 完整测试方案
|
||||||
|
|
||||||
|
### 3.1 测试分层策略
|
||||||
|
|
||||||
|
| 层级 | 位置 | 测试什么 | 执行方式 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| **Unit** | `Code/src/test_pinmap.py` | 纯逻辑测试(解析/验证/布局/往返),不依赖文件系统 | `python3 Code/src/test_pinmap.py` |
|
||||||
|
| **Integration** | `Test/run_tests.py` | 文件级测试(xlsx 读写/模板加载/端到端),依赖 fixtures/ 和临时文件 | `python3 Test/run_tests.py` |
|
||||||
|
|
||||||
|
### 3.2 测试原则
|
||||||
|
|
||||||
|
1. **单元测试** (`test_pinmap.py`):纯逻辑测试,不读/写磁盘文件,使用内存中的 cell dict
|
||||||
|
2. **集成测试** (`run_tests.py`):文件级测试,使用 `Test/fixtures/` 中的真实 .xlsx 文件和临时目录中的动态创建文件
|
||||||
|
3. **模板 fixture**:需要准备 `BallList-Template.xlsx` 和 `BallMAP-Template.xlsx` 两个模板 fixture 文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. F012 — PinName 位置回归测试
|
||||||
|
|
||||||
|
### 4.1 状态确认
|
||||||
|
|
||||||
|
**代码当前行为**:
|
||||||
|
- `pinmap_layout.py::get_name_cell`: bottom → `(r-1, c)`, top → `(r+1, c)`
|
||||||
|
- `pinmap_parser.py`: bottom name 读自 `(max_row-1, c)`, top name 读自 `(min_row+1, c)`
|
||||||
|
- 生成与解析使用**相同约定**,往返一致 ✅
|
||||||
|
|
||||||
|
**F012 测试确认**:当前代码已正确,测试已存在并全部通过。
|
||||||
|
|
||||||
|
### 4.2 已有回归测试(无需新增)
|
||||||
|
|
||||||
|
| 用例 | 位置 | 覆盖 |
|
||||||
|
|------|------|------|
|
||||||
|
| `test_f012_pinname_position` | `test_pinmap.py` | 5×5 20Pin 往返:生成 → 逐个验证四条边 Name 位置 → 解析回 PinList → 验证序号一致性 |
|
||||||
|
|
||||||
|
### 4.3 测试数据
|
||||||
|
|
||||||
|
```
|
||||||
|
test_pinmap.py 中的 4×4 数据已验证:
|
||||||
|
bottom: numbers at (6,2)=3, (6,3)=4; names at (5,2)=Pin3, (5,3)=Pin4 → max_row-1 ✅
|
||||||
|
top: numbers at (1,3)=7, (1,2)=8; names at (2,3)=Pin7, (2,2)=Pin8 → min_row+1 ✅
|
||||||
|
|
||||||
|
test_f012 5×5 数据已验证:
|
||||||
|
bottom: names at (4, c) = rows-1 = max_row-1 ✅
|
||||||
|
top: names at (2, c) = min_row+1 ✅
|
||||||
|
left: names at (r, 1) = c+1 ✅
|
||||||
|
right: names at (r, 4) = c-1 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 F012 测试结论
|
||||||
|
|
||||||
|
✅ **无需新增测试**。`test_f012_pinname_position` 已充分覆盖 F012 需求,且当前测试全部通过。保持该测试持续运行即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. F009 + F010 — 模板分离测试
|
||||||
|
|
||||||
|
### 5.1 测试场景矩阵
|
||||||
|
|
||||||
|
| 场景 | BallList-Template | BallMAP-Template | MAP→List 预期 | List→MAP 预期 |
|
||||||
|
|------|-------------------|------------------|-------------|-------------|
|
||||||
|
| T-F009-01 | ✅ 存在 | — | 加载 BallList,应用其样式 | (不涉及) |
|
||||||
|
| T-F009-02 | ❌ 不存在 | — | 日志提示"未检测到",使用默认样式 | (不涉及) |
|
||||||
|
| T-F009-03 | 存在但损坏 | — | 日志提示"解析失败",使用默认样式 | (不涉及) |
|
||||||
|
| T-F010-01 | — | ✅ 存在 | (不涉及) | 加载 BallMAP,应用其样式 |
|
||||||
|
| T-F010-02 | — | ❌ 不存在 | (不涉及) | 日志提示"未检测到",使用默认样式 |
|
||||||
|
| T-F010-03 | — | 存在但损坏 | (不涉及) | 日志提示"解析失败",使用默认样式 |
|
||||||
|
| T-F009-04 | ✅ 存在 | ✅ 存在 | 加载 BallList | 加载 BallMAP(各自独立,互不干扰) |
|
||||||
|
|
||||||
|
### 5.2 关键验证点
|
||||||
|
|
||||||
|
1. **MAP→List 不使用 BallMAP 模板** — 即使 BallMAP-Template.xlsx 存在,MAP→List 也不会加载它
|
||||||
|
2. **List→MAP 不使用 BallList 模板** — 即使 BallList-Template.xlsx 存在,List→MAP 也不会加载它
|
||||||
|
3. **不加载旧 PinMAP-Template.xlsx** — 即使旧模板存在,两个方向都不加载
|
||||||
|
|
||||||
|
### 5.3 单元测试:新增到 `test_pinmap.py`
|
||||||
|
|
||||||
|
以下测试**可**新增到 `test_pinmap.py`(纯逻辑,不依赖文件系统):
|
||||||
|
|
||||||
|
#### U-F009-F010-001: `_find_balllist_template_path` 和 `_find_ballmap_template_path` 路径生成
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_template_path_generation():
|
||||||
|
"""验证两个模板查找函数返回正确的路径格式。"""
|
||||||
|
import os
|
||||||
|
from main import _find_balllist_template_path, _find_ballmap_template_path
|
||||||
|
|
||||||
|
# 路径应以项目根目录为基础,包含正确的文件名
|
||||||
|
# 注意:这些函数检查文件是否存在,测试环境可能没有这些文件
|
||||||
|
# 所以只验证函数可调用且返回类型正确
|
||||||
|
result1 = _find_balllist_template_path()
|
||||||
|
result2 = _find_ballmap_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 result1 != result2
|
||||||
|
|
||||||
|
print("✓ test_template_path_generation passed")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 集成测试:新增到 `Test/run_tests.py`
|
||||||
|
|
||||||
|
以下测试需要 `Test/fixtures/` 中的模板文件:
|
||||||
|
|
||||||
|
**Fixture 准备**:
|
||||||
|
|
||||||
|
| 文件 | 用途 | 内容要求 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `Test/fixtures/BallList-Template.xlsx` | MAP→List 模板 | 至少包含 fonts(Calibri 12pt, bold), borders(thin), column_widths |
|
||||||
|
| `Test/fixtures/BallMAP-Template.xlsx` | MAP→List 模板 | 至少包含 fonts(Arial 10pt), borders(medium), row_heights |
|
||||||
|
| `Test/fixtures/template_corrupt.xlsx` | 损坏模板测试 | 一个非 xlsx 的普通文件伪装为 .xlsx |
|
||||||
|
|
||||||
|
**新增集成测试用例**:
|
||||||
|
|
||||||
|
#### TC-v1.5-001: MAP→List 加载 BallList 模板(模板存在)
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: fixtures/BallList-Template.xlsx 存在
|
||||||
|
步骤:
|
||||||
|
1. 创建 PinMAP 输入(4×4)
|
||||||
|
2. 模拟 run_map_to_list 模板加载流程
|
||||||
|
3. 验证 read_template_styles 返回非 None
|
||||||
|
4. 验证返回的 TemplateStyle 包含字体信息
|
||||||
|
预期: 模板样式成功加载,输出文件包含 styles.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TC-v1.5-002: MAP→List 无模板降级(模板不存在)
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: 删除根目录的 BallList-Template.xlsx
|
||||||
|
步骤:
|
||||||
|
1. 创建 PinMAP 输入(4×4)
|
||||||
|
2. 调用 _find_balllist_template_path()
|
||||||
|
3. 验证返回 None
|
||||||
|
预期: 返回 None,输出使用默认样式
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TC-v1.5-003: List→MAP 加载 BallMAP 模板(模板存在)
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: fixtures/BallMAP-Template.xlsx 存在
|
||||||
|
步骤:
|
||||||
|
1. 创建 5×5 PinList 输入(20 pin)
|
||||||
|
2. 模拟 run_list_to_map 模板加载流程
|
||||||
|
3. 验证 read_template_styles 返回非 None
|
||||||
|
4. 生成 PinMAP,验证输出包含 styles.xml
|
||||||
|
预期: 模板样式成功加载,输出文件包含 styles.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TC-v1.5-004: List→MAP 无模板降级(模板不存在)
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: 删除根目录的 BallMAP-Template.xlsx
|
||||||
|
步骤:
|
||||||
|
1. 创建 5×5 PinList 输入(20 pin)
|
||||||
|
2. 调用 _find_ballmap_template_path()
|
||||||
|
3. 验证返回 None
|
||||||
|
预期: 返回 None,输出使用默认样式
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TC-v1.5-005: 两个方向独立使用各自模板
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: BallList-Template.xlsx 和 BallMAP-Template.xlsx 都存在
|
||||||
|
步骤:
|
||||||
|
1. MAP→List 方向:调用 _find_balllist_template_path(),验证路径包含 "BallList"
|
||||||
|
2. List→MAP 方向:调用 _find_ballmap_template_path(),验证路径包含 "BallMAP"
|
||||||
|
3. 验证两个路径不同
|
||||||
|
预期: 两个方向各自查找各自的模板文件,互不干扰
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TC-v1.5-006: 模板损坏时优雅降级
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: 提供一个损坏的 "template_corrupt.xlsx"
|
||||||
|
步骤:
|
||||||
|
1. 调用 read_template_styles("fixtures/template_corrupt.xlsx")
|
||||||
|
2. 验证返回 None(不抛异常)
|
||||||
|
预期: 返回 None,调用方可继续使用默认样式
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. F011 — 模板格式提取式应用测试
|
||||||
|
|
||||||
|
### 6.1 测试场景
|
||||||
|
|
||||||
|
F011 核心要求:模板的**格式信息**(字体/边框/填充/对齐/列宽/行高)正确应用到输出文件,但输出文件的**行列结构**由实际 Pin 数量决定。
|
||||||
|
|
||||||
|
### 6.2 单元测试:新增到 `test_pinmap.py`
|
||||||
|
|
||||||
|
F011 的样式构建逻辑在 `xlsx_writer.py::StyledXLSXWriter` 中,涉及 XML 字符串生成。以下测试可以在不写磁盘文件的情况下验证 XML 内容:
|
||||||
|
|
||||||
|
#### U-F011-001: 无模板时使用默认样式
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_f011_default_styles_xml():
|
||||||
|
"""F011: 无模板时 _styles_xml() 返回硬编码默认样式。"""
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=None)
|
||||||
|
xml = writer._styles_xml()
|
||||||
|
|
||||||
|
# 验证硬编码默认值的存在
|
||||||
|
assert 'Calibri' in xml, "默认字体应为 Calibri"
|
||||||
|
assert 'thin' in xml or 'style="thin"' in xml, "默认边框应为 thin"
|
||||||
|
assert 'center' in xml, "默认对齐应为 center"
|
||||||
|
assert 'cellXfs count="4"' in xml, "应有 4 个 xf"
|
||||||
|
|
||||||
|
print("✓ test_f011_default_styles_xml passed")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### U-F011-002: 有模板时使用模板字体
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_f011_template_fonts_in_styles_xml():
|
||||||
|
"""F011: 有模板时 _styles_xml() 使用模板的字体信息(而非硬编码 Calibri)。"""
|
||||||
|
from template_reader import TemplateStyle, FontStyle
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
# 构建一个模板样式:微软雅黑 12pt + bold
|
||||||
|
style = TemplateStyle()
|
||||||
|
style.fonts = [
|
||||||
|
FontStyle(name="微软雅黑", size=12.0, bold=False, italic=False, color="FF000000"),
|
||||||
|
FontStyle(name="微软雅黑", size=12.0, bold=True, italic=False, color="FF000000"),
|
||||||
|
]
|
||||||
|
style.fills = []
|
||||||
|
style.borders = []
|
||||||
|
style.cell_xfs = [
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
|
||||||
|
]
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=style)
|
||||||
|
xml = writer._styles_xml()
|
||||||
|
|
||||||
|
assert '微软雅黑' in xml, f"模板字体名应出现在 styles.xml 中\n{xml[:500]}"
|
||||||
|
assert '12' in xml or '12.0' in xml, f"模板字号 12pt 应出现在 styles.xml 中"
|
||||||
|
|
||||||
|
print("✓ test_f011_template_fonts_in_styles_xml passed")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### U-F011-003: 有模板时使用模板边框
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_f011_template_borders_in_styles_xml():
|
||||||
|
"""F011: 有模板时 _styles_xml() 使用模板的边框样式(而非硬编码 thin)。"""
|
||||||
|
from template_reader import TemplateStyle, BorderStyle, FontStyle
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
style = TemplateStyle()
|
||||||
|
style.fonts = [FontStyle(name="Calibri", size=11.0)]
|
||||||
|
style.borders = [
|
||||||
|
BorderStyle(top="none", bottom="none", left="none", right="none"),
|
||||||
|
BorderStyle(top="medium", bottom="medium", left="medium", right="medium"),
|
||||||
|
]
|
||||||
|
style.fills = []
|
||||||
|
style.cell_xfs = [
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '1', 'xfId': '0',
|
||||||
|
'applyBorder': '1'},
|
||||||
|
]
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=style)
|
||||||
|
xml = writer._styles_xml()
|
||||||
|
|
||||||
|
# 模板的 medium 边框应该存在(不仅仅是 thin)
|
||||||
|
assert 'medium' in xml, f"模板 medium 边框应出现在 styles.xml 中\n{xml[:800]}"
|
||||||
|
|
||||||
|
print("✓ test_f011_template_borders_in_styles_xml passed")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### U-F011-004: 有模板时使用模板填充
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_f011_template_fills_in_styles_xml():
|
||||||
|
"""F011: 有模板时 _styles_xml() 使用模板的填充色(而非硬编码 FFF0F0F0)。"""
|
||||||
|
from template_reader import TemplateStyle, FillStyle, FontStyle
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
style = TemplateStyle()
|
||||||
|
style.fonts = [FontStyle(name="Calibri", size=11.0)]
|
||||||
|
style.borders = []
|
||||||
|
style.fills = [
|
||||||
|
FillStyle(pattern_type="none", fg_color=""),
|
||||||
|
FillStyle(pattern_type="solid", fg_color="FFFF00"), # 黄色
|
||||||
|
]
|
||||||
|
style.cell_xfs = [
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '1', 'borderId': '0', 'xfId': '0',
|
||||||
|
'applyFill': '1'},
|
||||||
|
]
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=style)
|
||||||
|
xml = writer._styles_xml()
|
||||||
|
|
||||||
|
assert 'FFFF00' in xml, f"模板黄色填充应出现在 styles.xml 中\n{xml[:800]}"
|
||||||
|
|
||||||
|
print("✓ test_f011_template_fills_in_styles_xml passed")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### U-F011-005: 输出行列由实际 Pin 决定(不复制模板行列结构)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_f011_output_dims_determined_by_pins():
|
||||||
|
"""F011: 输出文件的 dim 由实际 Pin 数量决定,不复制模板的行列结构。
|
||||||
|
|
||||||
|
即使模板有 100 行列定义,输出仍只包含实际 Pin 数据的行列。
|
||||||
|
"""
|
||||||
|
from template_reader import TemplateStyle, FontStyle
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
|
||||||
|
style = TemplateStyle()
|
||||||
|
style.fonts = [FontStyle(name="Calibri", size=11.0)]
|
||||||
|
style.column_widths = {i: 20.0 for i in range(100)} # 模板有 100 列
|
||||||
|
style.row_heights = {i: 30.0 for i in range(200)} # 模板有 200 行
|
||||||
|
style.fills = []
|
||||||
|
style.borders = []
|
||||||
|
style.cell_xfs = [
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
|
||||||
|
]
|
||||||
|
|
||||||
|
# 仅输出 2 行 3 列的数据(模拟 2×2 PinMAP + A1)
|
||||||
|
data = {
|
||||||
|
'A1': 'QFP-8',
|
||||||
|
'A2': '1', 'B2': 'Pin1',
|
||||||
|
'A3': '2', 'B3': 'Pin2',
|
||||||
|
}
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style=style)
|
||||||
|
sheet_xml = writer._sheet_xml(data)
|
||||||
|
|
||||||
|
# dim 应该反映实际数据范围(A1:C3),而非模板的 100 列
|
||||||
|
assert 'dimension ref="A1:C' in sheet_xml or 'dimension ref="A1:C3"' in sheet_xml, \
|
||||||
|
f"dim 应由实际数据决定,不应包含模板的 100 列\n{sheet_xml[:500]}"
|
||||||
|
|
||||||
|
# 不应出现 row r="201"(模板的第 200 行)
|
||||||
|
assert 'row r="201"' not in sheet_xml, "不应包含模板的多余行"
|
||||||
|
|
||||||
|
print("✓ test_f011_output_dims_determined_by_pins passed")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 集成测试:新增到 `Test/run_tests.py`
|
||||||
|
|
||||||
|
#### TC-v1.5-007: 模板字体应用到输出文件
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: fixtures/BallMAP-Template.xlsx 使用微软雅黑 14pt
|
||||||
|
步骤:
|
||||||
|
1. 用该模板生成 5×5 PinMAP 输出
|
||||||
|
2. 解压输出 xlsx,读取 xl/styles.xml
|
||||||
|
3. 验证 fonts 部分包含 "微软雅黑" 和 size=14
|
||||||
|
预期: 输出 styles.xml 包含模板字体定义
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TC-v1.5-008: 模板列宽应用到输出文件
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: fixtures/BallList-Template.xlsx 中 A 列宽=25, B 列宽=18
|
||||||
|
步骤:
|
||||||
|
1. 用该模板生成 PinList 输出
|
||||||
|
2. 解压输出 xlsx,读取 xl/worksheets/sheet1.xml
|
||||||
|
3. 验证 cols 元素中 A 列 width=25, B 列 width=18
|
||||||
|
预期: 输出列宽与模板一致
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TC-v1.5-009: 模板行高应用到输出文件
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: fixtures/BallMAP-Template.xlsx 中行高=25
|
||||||
|
步骤:
|
||||||
|
1. 用该模板生成 3×3 PinMAP 输出
|
||||||
|
2. 解压输出 xlsx,读取 xl/worksheets/sheet1.xml
|
||||||
|
3. 验证 row 元素包含 ht=25
|
||||||
|
预期: 输出行高与模板一致
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TC-v1.5-010: 两个方向使用不同模板各自的格式
|
||||||
|
|
||||||
|
```
|
||||||
|
前置:
|
||||||
|
- BallList-Template.xlsx 字体=楷体 12pt
|
||||||
|
- BallMAP-Template.xlsx 字体=宋体 14pt
|
||||||
|
步骤:
|
||||||
|
1. MAP→List 方向:用 BallList 模板生成 PinList,验证输出字体=楷体 12pt
|
||||||
|
2. List→MAP 方向:用 BallMAP 模板生成 PinMAP,验证输出字体=宋体 14pt
|
||||||
|
3. 验证两个输出文件的字体不同
|
||||||
|
预期: 两个方向的输出各自使用对应模板的字体
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. F009/F010/F011 集成测试
|
||||||
|
|
||||||
|
### 7.1 端到端集成测试
|
||||||
|
|
||||||
|
#### TC-v1.5-011: 完整往返 + 模板隔离 (MAP→List→MAP)
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: BallList-Template.xlsx 和 BallMAP-Template.xlsx 都存在且格式不同
|
||||||
|
步骤:
|
||||||
|
1. 使用 BallList 模板,执行 sample_4x4.xlsx → PinList
|
||||||
|
2. 验证 PinList 输出包含 BallList 模板的格式特征
|
||||||
|
3. 使用 BallMAP 模板,执行 PinList → PinMAP
|
||||||
|
4. 验证 PinMAP 输出包含 BallMAP 模板的格式特征
|
||||||
|
5. 验证往返后的 PinMAP 与原 PinMAP 数据一致(忽略格式差异)
|
||||||
|
预期:
|
||||||
|
- 往返数据完全一致(引脚序号/名称/封装信息)
|
||||||
|
- 中间 PinList 使用 BallList 模板格式
|
||||||
|
- 最终 PinMAP 使用 BallMAP 模板格式
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TC-v1.5-012: 无模板完整流程
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: 根目录没有 BallList-Template.xlsx 也没有 BallMAP-Template.xlsx
|
||||||
|
步骤:
|
||||||
|
1. 执行 MAP→List 转换
|
||||||
|
2. 执行 List→MAP 转换
|
||||||
|
3. 验证两个输出文件都能正常生成且数据正确
|
||||||
|
4. 验证两个输出文件使用默认样式(Calibri 11pt)
|
||||||
|
预期: 无模板情况下正常降级,输出使用默认样式,数据正确
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 边界与异常测试
|
||||||
|
|
||||||
|
### 8.1 模板边界测试
|
||||||
|
|
||||||
|
| 编号 | 场景 | 预期行为 | 测试级别 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| TC-BND-001 | 模板 fonts 为空列表 | 回退到默认字体 | Unit |
|
||||||
|
| TC-BND-002 | 模板 borders 为空列表 | 回退到默认边框(thin) | Unit |
|
||||||
|
| TC-BND-003 | 模板 fills 为空列表 | 回退到默认填充 | Unit |
|
||||||
|
| TC-BND-004 | 模板 cell_xfs 为空列表 | 回退到默认 cellXfs(4 个硬编码 xf) | Unit |
|
||||||
|
| TC-BND-005 | 模板 column_widths 为空 dict | 使用默认列宽 8.0 | Unit |
|
||||||
|
| TC-BND-006 | 模板 row_heights 为空 dict | 不使用自定义行高(使用 Excel 默认) | Unit |
|
||||||
|
| TC-BND-007 | 模板字体 color 缺少 FF 前缀 | xlsx_writer 自动补全 | Unit |
|
||||||
|
| TC-BND-008 | 模板填充 fg_color 缺少 FF 前缀 | xlsx_writer 自动补全 | Unit |
|
||||||
|
| TC-BND-009 | 模板中缺失 styles.xml | read_template_styles 返回 None | Unit |
|
||||||
|
| TC-BND-010 | 模板中缺失 sheet1.xml | column_widths/row_heights 为空 | Unit |
|
||||||
|
|
||||||
|
### 8.2 边界场景集成测试
|
||||||
|
|
||||||
|
#### TC-v1.5-013: 模板只有字体、无边框/填充
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: 准备一个只有 fonts 定义的极简模板
|
||||||
|
步骤:
|
||||||
|
1. 加载模板样式
|
||||||
|
2. 验证 fonts 正确提取
|
||||||
|
3. 验证 borders/fills 使用默认值
|
||||||
|
4. 生成的输出文件可正常打开
|
||||||
|
预期: 字体来自模板,边框/填充使用默认值,文件可正常打开
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TC-v1.5-014: 模板列宽少于输出列数
|
||||||
|
|
||||||
|
```
|
||||||
|
前置: 模板定义 3 列宽,但输出需要 6 列
|
||||||
|
步骤:
|
||||||
|
1. 加载模板(定义 A-C 列宽)
|
||||||
|
2. 生成 5×5(6 列) PinMAP
|
||||||
|
3. 验证模板定义的列使用模板宽度,额外的列使用默认 8.0
|
||||||
|
预期: 列宽正确扩展,无异常
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 测试执行计划
|
||||||
|
|
||||||
|
### 9.1 新增单元测试(添加到 `test_pinmap.py`)
|
||||||
|
|
||||||
|
| 优先级 | 编号 | 测试名 | 简述 |
|
||||||
|
|--------|------|--------|------|
|
||||||
|
| P0 | U-F009-F010-001 | `test_template_path_generation` | 验证模板路径格式正确且互不相同 |
|
||||||
|
| P0 | U-F011-001 | `test_f011_default_styles_xml` | 无模板时使用默认样式 |
|
||||||
|
| P0 | U-F011-002 | `test_f011_template_fonts_in_styles_xml` | 模板字体应用到 styles.xml |
|
||||||
|
| P1 | U-F011-003 | `test_f011_template_borders_in_styles_xml` | 模板边框应用到 styles.xml |
|
||||||
|
| P1 | U-F011-004 | `test_f011_template_fills_in_styles_xml` | 模板填充应用到 styles.xml |
|
||||||
|
| P0 | U-F011-005 | `test_f011_output_dims_determined_by_pins` | 输出 dim 由 Pin 数决定 |
|
||||||
|
| P1 | U-BND-001 | `test_template_empty_fonts_fallback` | 空 fonts 回退 |
|
||||||
|
| P1 | U-BND-007 | `test_template_color_prefix_auto_fix` | FF 前缀补全 |
|
||||||
|
| P1 | U-BND-009 | `test_template_no_styles_xml` | 缺失 styles.xml 降级 |
|
||||||
|
|
||||||
|
### 9.2 新增集成测试(添加到 `Test/run_tests.py`)
|
||||||
|
|
||||||
|
| 优先级 | 编号 | 测试名 | 简述 | 需要 Fixture |
|
||||||
|
|--------|------|--------|------|------------|
|
||||||
|
| P0 | TC-v1.5-001 | MAP→List 加载 BallList 模板 | 模板存在时正确加载 | BallList-Template.xlsx |
|
||||||
|
| P0 | TC-v1.5-002 | MAP→List 无模板降级 | 模板不存在时优雅降级 | 无 |
|
||||||
|
| P0 | TC-v1.5-003 | List→MAP 加载 BallMAP 模板 | 模板存在时正确加载 | BallMAP-Template.xlsx |
|
||||||
|
| P0 | TC-v1.5-004 | List→MAP 无模板降级 | 模板不存在时优雅降级 | 无 |
|
||||||
|
| P0 | TC-v1.5-005 | 两个方向独立模板 | 各自使用各自的模板 | 两个模板 |
|
||||||
|
| P1 | TC-v1.5-006 | 模板损坏优雅降级 | 损坏模板不抛异常 | template_corrupt.xlsx |
|
||||||
|
| P1 | TC-v1.5-007 | 模板字体应用到输出 | 输出文件含模板字体 | 特殊字体模板 |
|
||||||
|
| P1 | TC-v1.5-008 | 模板列宽应用到输出 | 输出列宽=模板列宽 | 特殊列宽模板 |
|
||||||
|
| P1 | TC-v1.5-009 | 模板行高应用到输出 | 输出行高=模板行高 | 特殊行高模板 |
|
||||||
|
| P1 | TC-v1.5-010 | 两个方向各自格式 | 两个方向格式独立 | 两个不同格式模板 |
|
||||||
|
| P1 | TC-v1.5-011 | 完整往返+模板隔离 | MAP→List→MAP 数据一致 | 两个模板 |
|
||||||
|
| P1 | TC-v1.5-012 | 无模板完整流程 | 无模板正常降级 | 无 |
|
||||||
|
| P2 | TC-v1.5-013 | 极简模板 | 只有字体的模板 | 极简模板 |
|
||||||
|
| P2 | TC-v1.5-014 | 列宽扩展 | 模板列宽少于输出列数 | 窄模板 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Fixture 准备清单
|
||||||
|
|
||||||
|
### 10.1 需要创建的 Fixture 文件
|
||||||
|
|
||||||
|
| 文件 | 放置位置 | 用途 | 关键内容 |
|
||||||
|
|------|---------|------|---------|
|
||||||
|
| `BallList-Template.xlsx` | `Test/fixtures/` | MAP→List 模板 | 字体=楷体 12pt, A列宽=25, B列宽=18, 边框=thin, 对齐=center |
|
||||||
|
| `BallMAP-Template.xlsx` | `Test/fixtures/` | List→MAP 模板 | 字体=宋体 14pt, 行高=25, 边框=medium, 填充=淡黄 FFFFFF00 |
|
||||||
|
| `template_corrupt.xlsx` | `Test/fixtures/` | 损坏模板测试 | 一个无效的 ZIP 文件或文本文件伪装为 .xlsx |
|
||||||
|
| `template_minimal.xlsx` | `Test/fixtures/` | 极简模板测试 | 只有 1 个 font 定义,无 borders/fills |
|
||||||
|
| `template_narrow.xlsx` | `Test/fixtures/` | 列宽扩展测试 | 只定义 3 列宽(A-C),但测试输出 6 列 |
|
||||||
|
|
||||||
|
### 10.2 集成测试时的模板放置策略
|
||||||
|
|
||||||
|
集成测试需要能临时将模板放到正确位置。建议方案:
|
||||||
|
|
||||||
|
**方案 A(推荐)**:在 `run_tests.py` 中使用 `tempfile.mkdtemp` 创建临时目录,将 fixture 模板复制为 `BallList-Template.xlsx` / `BallMAP-Template.xlsx`,然后通过 `os.chdir` 或修改 `sys.path` 让 main.py 的模板查找逻辑找到它们。
|
||||||
|
|
||||||
|
**方案 B**:在 `run_tests.py` 中直接调用底层函数(`read_template_styles`, `_find_balllist_template_path`),不经过 main.py 的路径查找,而是直接传入 fixture 路径。
|
||||||
|
|
||||||
|
推荐 **方案 B**(更简洁),因为已有测试也是直接调用底层函数。
|
||||||
|
|
||||||
|
### 10.3 Fixture 创建方法
|
||||||
|
|
||||||
|
使用 `xlsx_writer.py::write_xlsx_with_style` 创建带特定格式的模板文件:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 创建 BallList-Template.xlsx
|
||||||
|
from xlsx_writer import StyledXLSXWriter
|
||||||
|
from template_reader import TemplateStyle, FontStyle, BorderStyle, FillStyle
|
||||||
|
|
||||||
|
style = TemplateStyle()
|
||||||
|
style.fonts = [
|
||||||
|
FontStyle(name="楷体", size=12.0, bold=False, color="FF000000"),
|
||||||
|
FontStyle(name="楷体", size=12.0, bold=True, color="FF000000"),
|
||||||
|
]
|
||||||
|
style.borders = [
|
||||||
|
BorderStyle(top="none", bottom="none", left="none", right="none"),
|
||||||
|
BorderStyle(top="thin", bottom="thin", left="thin", right="thin"),
|
||||||
|
]
|
||||||
|
style.fills = [FillStyle(pattern_type="none")]
|
||||||
|
style.cell_xfs = [
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
|
||||||
|
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '1', 'xfId': '0',
|
||||||
|
'applyBorder': '1', 'hAlign': 'center', 'vAlign': 'center'},
|
||||||
|
]
|
||||||
|
style.column_widths = {0: 25.0, 1: 18.0}
|
||||||
|
style.row_heights = {}
|
||||||
|
|
||||||
|
# 创建 PinList 模板数据(2 行示例数据)
|
||||||
|
data = {'A1': '封装信息', 'A2': 'Pin1', 'B2': '1', 'A3': 'Pin2', 'B3': '2'}
|
||||||
|
|
||||||
|
writer = StyledXLSXWriter(style)
|
||||||
|
writer.write(data, "fixtures/BallList-Template.xlsx")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 预期工作量
|
||||||
|
|
||||||
|
| 阶段 | 内容 | 预估时间 |
|
||||||
|
|------|------|---------|
|
||||||
|
| Fixture 准备 | 创建 5 个模板 fixture 文件 | 30 min |
|
||||||
|
| 单元测试 | 新增约 10 个单元测试到 `test_pinmap.py` | 1 hr |
|
||||||
|
| 集成测试 | 新增约 14 个集成测试到 `run_tests.py` | 1.5 hr |
|
||||||
|
| 回归验证 | 运行全部测试确认无回归 | 15 min |
|
||||||
|
| 文档更新 | 更新测试报告 | 10 min |
|
||||||
|
| **总计** | | **~3.5 hr** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 测试通过标准
|
||||||
|
|
||||||
|
| 条件 | 要求 |
|
||||||
|
|------|------|
|
||||||
|
| 现有 9 个单元测试 | 全部通过 |
|
||||||
|
| 现有 23 个集成测试 | 全部通过 |
|
||||||
|
| 新增 ~10 个单元测试 | 全部通过 |
|
||||||
|
| 新增 ~14 个集成测试 | 全部通过 |
|
||||||
|
| F012 往返测试 (`test_f012_pinname_position`) | 持续通过 |
|
||||||
|
| F009/F010 模板分离 | 两个方向使用各自模板,互不干扰 |
|
||||||
|
| F011 模板格式提取 | 字体/边框/填充/对齐/列宽/行高从模板正确应用 |
|
||||||
|
| 无模板场景 | 优雅降级使用默认样式,不抛异常 |
|
||||||
|
| 损坏模板场景 | 优雅降级为 None,不抛异常 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 总结
|
||||||
|
|
||||||
|
### 13.1 测试设计决策
|
||||||
|
|
||||||
|
| 决策 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| F012 不再新增测试 | `test_f012_pinname_position` 已充分覆盖,代码行为已确认正确 |
|
||||||
|
| F009/F010 模板分离测试侧重降级 | 核心风险在于模板不存在/解析失败时的行为,而非正常路径 |
|
||||||
|
| F011 样式测试分两层 | 单元层验证 XML 生成,集成层验证最终文件内容 |
|
||||||
|
| 单元测试不依赖文件系统 | 使用内存中的 `TemplateStyle` 对象直接构造测试数据 |
|
||||||
|
| 集成测试使用 fixture 文件 | 由 test-executor 预先创建模板 fixture,再运行测试 |
|
||||||
|
|
||||||
|
### 13.2 测试覆盖矩阵
|
||||||
|
|
||||||
|
```
|
||||||
|
F012 F009 F010 F011 边界 集成
|
||||||
|
现有 test_pinmap.py ✅ — — — — —
|
||||||
|
现有 run_tests.py — — — — — ✅
|
||||||
|
新增 unit tests — ✅ ✅ ✅ ✅ —
|
||||||
|
新增 integration tests— ✅ ✅ ✅ ✅ ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.3 下一步
|
||||||
|
|
||||||
|
1. **test-executor** 按本方案执行 Fixture 准备(创建 5 个模板文件)
|
||||||
|
2. **test-executor** 按优先级 P0 → P1 → P2 实施测试代码
|
||||||
|
3. 运行完整测试套件验证
|
||||||
|
4. 生成 v1.5.0 最终测试报告
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*测试方案结束 — 等待 test-executor 执行*
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# PinMAP ↔ PinList 双向转换器 测试报告
|
# PinMAP ↔ PinList 双向转换器 测试报告
|
||||||
|
|
||||||
> **日期**: 2026-06-01
|
> **日期**: 2026-06-12
|
||||||
> **测试类型**: 集成测试 + 端到端测试
|
> **测试类型**: 集成测试 + 端到端测试
|
||||||
> **测试环境**: Python 3.x, Linux x64
|
> **测试环境**: Python 3.x, Linux x64
|
||||||
|
|
||||||
@@ -10,9 +10,10 @@
|
|||||||
|
|
||||||
| 类别 | 用例数 | 通过 | 失败 |
|
| 类别 | 用例数 | 通过 | 失败 |
|
||||||
|------|--------|------|------|
|
|------|--------|------|------|
|
||||||
| MAP→List 回归 | 6 | 6 | 0 |
|
| MAP->List 回归 | 6 | 6 | 0 |
|
||||||
| List→MAP 新增 | 17 | 17 | 0 |
|
| List->MAP 新增 | 17 | 17 | 0 |
|
||||||
| **总计** | **23** | **23** | **0** |
|
| v1.5 模板/样式集成 | 14 | 14 | 0 |
|
||||||
|
| **总计** | **37** | **37** | **0** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -20,11 +21,11 @@
|
|||||||
|
|
||||||
### TC-MAP-001: 标准4x4 PinMAP转换
|
### TC-MAP-001: 标准4x4 PinMAP转换
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
- **详情**: 封装=QFP12, Pin数=12, 序号递增
|
- **详情**: 封装=QFP-16, Pin数=16, 序号 1-16, 引脚名=Pin1..Pin16
|
||||||
|
|
||||||
### TC-MAP-002: 长方形PinMAP转换
|
### TC-MAP-002: 长方形PinMAP转换
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
- **详情**: 封装=LQFP100, Pin数=11, 序号递增
|
- **详情**: 封装=LQFP100, Pin数=8, 序号递增
|
||||||
|
|
||||||
### TC-MAP-003: 序号不连续检测
|
### TC-MAP-003: 序号不连续检测
|
||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
@@ -112,6 +113,64 @@
|
|||||||
- **结果**: ✅ 通过
|
- **结果**: ✅ 通过
|
||||||
- **详情**: 正确报错: A1 单元格为空,无法获取封装信息
|
- **详情**: 正确报错: A1 单元格为空,无法获取封装信息
|
||||||
|
|
||||||
|
## Part 3: v1.5 模板/样式集成测试
|
||||||
|
|
||||||
|
### TC-v1.5-001: MAP->List 加载 PinList 模板
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 模板加载成功: fonts=2, borders=2, width_A=25.0
|
||||||
|
|
||||||
|
### TC-v1.5-002: MAP->List 无模板降级
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 无模板文件时优雅返回 None
|
||||||
|
|
||||||
|
### TC-v1.5-003: List->MAP 加载 PinMAP 模板
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 模板加载成功: fonts=2, borders=2, row_height=25.0
|
||||||
|
|
||||||
|
### TC-v1.5-004: List->MAP 无模板降级
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 无模板文件时优雅返回 None
|
||||||
|
|
||||||
|
### TC-v1.5-005: 两个方向独立使用各自模板
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 两个模板独立: PL fonts=2, PM fonts=2
|
||||||
|
|
||||||
|
### TC-v1.5-006: 模板损坏优雅降级
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 损坏模板优雅返回 None
|
||||||
|
|
||||||
|
### TC-v1.5-007: 模板字体应用到输出文件
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 输出 styles.xml 包含模板字体(宋体 14pt)
|
||||||
|
|
||||||
|
### TC-v1.5-008: 模板列宽应用到输出文件
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 列宽验证通过: A=25.0, B=18.0
|
||||||
|
|
||||||
|
### TC-v1.5-009: 模板行高应用到输出文件
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 行高验证通过: ht=25
|
||||||
|
|
||||||
|
### TC-v1.5-010: 两个方向不同模板各自的格式
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 两个方向输出字体不同: PinList->楷体, PinMAP->宋体
|
||||||
|
|
||||||
|
### TC-v1.5-011: 完整往返+模板隔离
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 往返成功: 16 pins, 楷体->PinList, 宋体->PinMAP
|
||||||
|
|
||||||
|
### TC-v1.5-012: 无模板完整流程
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 无模板完整流程正常
|
||||||
|
|
||||||
|
### TC-v1.5-013: 极简模板(只有字体)
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 极简模板: font=Courier New
|
||||||
|
|
||||||
|
### TC-v1.5-014: 列宽扩展
|
||||||
|
- **结果**: ✅ 通过
|
||||||
|
- **详情**: 列宽扩展正确: A=15.0, B=12.0, C=10.0, D=8.0, E=8.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 结论
|
## 结论
|
||||||
|
|||||||
@@ -6,3 +6,5 @@
|
|||||||
| BUG-002 | 高 | 周长计算公式错误 | 输入 15×15 网格 + 60 Pin | 验证通过 `(rows+cols)*2=60` | 提示不匹配 `2*rows+2*cols-4=56` | 已修复 | F006 |
|
| BUG-002 | 高 | 周长计算公式错误 | 输入 15×15 网格 + 60 Pin | 验证通过 `(rows+cols)*2=60` | 提示不匹配 `2*rows+2*cols-4=56` | 已修复 | F006 |
|
||||||
| BUG-003 | 中 | 双向转换未读取模板样式 | 使用模板文件进行 MAP↔List 转换 | 读取并应用模板样式 | 使用默认样式 | 已修复 | F007 |
|
| BUG-003 | 中 | 双向转换未读取模板样式 | 使用模板文件进行 MAP↔List 转换 | 读取并应用模板样式 | 使用默认样式 | 已修复 | F007 |
|
||||||
| BUG-004 | 中 | 不支持循环处理流程 | 转换完成后继续操作 | 循环等待下一个文件,输入 Q 返回主菜单 | 处理完直接退出 | 已修复 | F008 |
|
| BUG-004 | 中 | 不支持循环处理流程 | 转换完成后继续操作 | 循环等待下一个文件,输入 Q 返回主菜单 | 处理完直接退出 | 已修复 | F008 |
|
||||||
|
| BUG-005 | 高 | 模板文件名/路径错误 | PinList↔PinMAP 转换时读取模板 | PinMAP 模板为 PinMAP-Template.xlsx,PinList 模板为 PinList-Template.xlsx | v1.5.4 只改文件名未改搜索路径,模板在 Code/src/Template/ 下但代码在根目录找 | 已修复 | v1.5.5 |
|
||||||
|
| BUG-006 | 高 | PinList→PinMAP 上边 Name 与左边 Name 同行(数据无误但肉眼混淆) | 12×12 PinMap:PinList→PinMAP 转换后查看输出 | 每条边的 Name 和 Number 在独立行/列区域,肉眼可辨 | v1.5.4 上边 Name 在 row 2,与左边 Name(row 2)同行,3 条边数据混在同一行 | 已修复 | v1.5.5 |
|
||||||
|
|||||||
@@ -23,9 +23,22 @@
|
|||||||
| F007 | 模板读取 | MAP→List 和 List→MAP 双向转换均读取并应用模板样式 | 模板文件 | 带样式的输出文件 | 无 | 2 | 双向转换均应用模板样式 | 已通过 |
|
| F007 | 模板读取 | MAP→List 和 List→MAP 双向转换均读取并应用模板样式 | 模板文件 | 带样式的输出文件 | 无 | 2 | 双向转换均应用模板样式 | 已通过 |
|
||||||
| F008 | 循环处理流程 | 处理完不退出,循环等待下一个文件,输入 Q 返回主菜单 | 用户输入 | 循环处理或返回主菜单 | 无 | 2 | 处理完不退出,Q 返回主菜单 | 已通过 |
|
| F008 | 循环处理流程 | 处理完不退出,循环等待下一个文件,输入 Q 返回主菜单 | 用户输入 | 循环处理或返回主菜单 | 无 | 2 | 处理完不退出,Q 返回主菜单 | 已通过 |
|
||||||
|
|
||||||
|
## v1.5.0 新增(2026-06-06)
|
||||||
|
|
||||||
|
| 功能 ID | 功能名称 | 描述 | 输入 | 输出 | 依赖 | 优先级 | 验收标准 | 审批状态 |
|
||||||
|
|--------|---------|------|------|------|------|--------|---------|---------|
|
||||||
|
| F009 | MAP→List 使用 balllist 模板 | PinMAP→PinList 转换方向查找并使用 `BallList-Template.xlsx`,不再共用 PinMAP 模板 | BallList-Template.xlsx | 带 balllist 模板样式的 PinList 输出 | 无 | P1 | MAP→List 使用 balllist 模板的样式 | 已完成 |
|
||||||
|
| F010 | List→MAP 使用 ballmap 模板 | PinList→PinMAP 转换方向查找并使用 `BallMAP-Template.xlsx`,不再共用 PinMAP 模板 | BallMAP-Template.xlsx | 带 ballmap 模板样式的 PinMAP 输出 | 无 | P1 | List→MAP 使用 ballmap 模板的样式 | 已完成 |
|
||||||
|
| F011 | 模板格式提取式应用 | 从模板仅提取格式信息(字体、边框、对齐、列宽、行高),输出文件行列数由实际 Pin 数量决定,不复制模板行列结构 | 模板文件 | 格式信息正确应用到输出文件 | F009, F010 | P1 | 模板格式正确应用到不同 Pin 数的输出文件 | 已完成 |
|
||||||
|
| F012 | 修复 PinMAP 生成中上/下边 PinName 位置 | PinList→PinMAP 时,下边 PinName 应在序号上方(max_row-1 而非 min_row+1),上边 PinName 应在序号下方(min_row+1 而非 max_row-1) | PinList 数据 + 网格尺寸 | PinName 位于正确位置的 PinMAP | 无 | P0 | 4×4 PinMAP 示例中 Pin3/Pin4 出现在 C6/D6,Pin5/Pin6 出现在 E5/E4 | 已完成 |
|
||||||
|
|
||||||
## 优先级排序
|
## 优先级排序
|
||||||
|
|
||||||
1. **P0(必须)**:F006 周长公式修复 — 核心逻辑错误
|
1. **P0(必须)**:F012 修复上/下边 PinName 位置 — 核心逻辑 Bug
|
||||||
2. **P1(重要)**:F005 BAT 脚本修复 — 影响 Windows 用户使用
|
2. **P0(必须)**:F006 周长公式修复 — 核心逻辑错误
|
||||||
3. **P2(建议)**:F007 模板读取 — 功能增强
|
3. **P1(重要)**:F005 BAT 脚本修复 — 影响 Windows 用户使用
|
||||||
4. **P2(建议)**:F008 循环处理流程 — 体验优化
|
4. **P1(重要)**:F009 MAP→List 用 balllist 模板 — 模板分离
|
||||||
|
5. **P1(重要)**:F010 List→MAP 用 ballmap 模板 — 模板分离
|
||||||
|
6. **P1(重要)**:F011 模板格式提取式应用 — 格式正确性确认
|
||||||
|
7. **P2(建议)**:F007 模板读取 — 功能增强(已被 F009/F010/F011 细化取代)
|
||||||
|
8. **P2(建议)**:F008 循环处理流程 — 体验优化
|
||||||
|
|||||||
256
docs/modification-assessment-v1.5.1.md
Normal file
256
docs/modification-assessment-v1.5.1.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# PinMAP ↔ PinList 双向转换器 — v1.5.4 Bug 修复评估
|
||||||
|
|
||||||
|
> **版本**: v1.5.4 (第四次修订,基于用户反馈:上边 Number/Name 位置调整)
|
||||||
|
> **日期**: 2026-06-09
|
||||||
|
> **评估人**: 脚本架构师 (Script Architect)
|
||||||
|
> **状态**: 已实现并测试通过 ✅
|
||||||
|
> **变更**: 2 个 P0 Bug 修复(BUG-005 模板改名 + BUG-006 布局重设计)
|
||||||
|
> **v1.5.4 修订**: 上边 Name 从 row 0 移至 row 2(Number 在 row 1 最顶行,Name 在 row 2 第二行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Bug 概述
|
||||||
|
|
||||||
|
| Bug ID | 优先级 | 标题 | 现象 |
|
||||||
|
|--------|--------|------|------|
|
||||||
|
| BUG-005 | **高** | 模板文件名错误 | 模板文件名与用户期望不符 |
|
||||||
|
| BUG-006 | **高** | 双向转换数据错位 | 15×15 PinMAP 往返转换后序号1错位到A2,序号16错位到B16 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. BUG-005 分析:模板文件名错误
|
||||||
|
|
||||||
|
### 2.1 修改方案
|
||||||
|
|
||||||
|
改模板名为 `PinMAP-Template.xlsx`(MAP→List)和 `PinList-Template.xlsx`(List→MAP),同步更新 `main.py` 中的函数名和调用处。~15 行修改,15 分钟。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. BUG-006:Number 外侧 + Name 里侧 布局重设计
|
||||||
|
|
||||||
|
### 3.1 根本原因
|
||||||
|
|
||||||
|
v1.3 的"紧致布局"把 Name 放在 Number 向内偏移一行/一列的位置。边角处的 Name 单元格恰好是相邻边的 Number 单元格。例如左边 pin#1 的 Name 放在 B2,而上边 pin#60 的 Number 也在 B2,导致冲突。
|
||||||
|
|
||||||
|
### 3.2 设计目标
|
||||||
|
|
||||||
|
1. **Number 在最外侧** —— 打开 Excel 最外圈看到数字
|
||||||
|
2. **Name 在里侧** —— 紧挨着 Number 的内圈
|
||||||
|
3. **Pin1 永远在左上角** —— Number=1 在左边第一行
|
||||||
|
4. **保留 `(rows+cols)*2` 周长公式**
|
||||||
|
5. **彻底解决所有 Name/Number 单元格冲突**
|
||||||
|
|
||||||
|
### 3.3 最终方案(v1.5.4 修订)
|
||||||
|
|
||||||
|
**核心思路**:
|
||||||
|
- Number 占据最外侧一圈,每条边独占其区域(角点不共享!)
|
||||||
|
- Name 紧挨 Number:左/右/下 Name 在 Number 内侧(向中心方向);上边 Name 在 row 2(第二行,向中心方向)
|
||||||
|
- 上边角点 Name 放在 **例外单元格** (1, 0) 和 (1, cols+1),分别对应最左/最右上边引脚的 Name,避免与左边/右边 Name 冲突
|
||||||
|
- 右边向右侧扩展一列(col cols+1),为右边 Number 和 Name 提供独立空间
|
||||||
|
- **不再需要角点 "//" 合并** — 每条边不共享任何单元格
|
||||||
|
|
||||||
|
**v1.5.4 关键修改**:上边 Name 从 v1.5.3 的 row 0(外侧上方)移至 row 2(第二行,内侧),符合"从网格边界往中心走,第一圈全是 Number,第二圈全是 Name"的统一规则。
|
||||||
|
|
||||||
|
```
|
||||||
|
15×15 示意图 (rows=15, cols=15, 60 pins):
|
||||||
|
|
||||||
|
A B C D ... N O P Q
|
||||||
|
┌─────┬─────┬─────┬───┬─────┬─────┬─────┬─────┐
|
||||||
|
0 │ PKG │ │ │...│ │ │ │ │ ← 仅 A1 封装信息
|
||||||
|
1 │ │ 60 │ 59 │...│ 48 │ 47 │ 46 │P45? │ ← 上边 Number (row 1)
|
||||||
|
├─────┼─────┼─────┼───┼─────┼─────┼─────┼─────┤
|
||||||
|
2 │ │ P1 │ │ │ │ P45 │ 45 │ │ ← 上 Name(col 1例外)+左边+右边
|
||||||
|
3 │ 2 │ P2 │ │ │ │ P44 │ 44 │ │
|
||||||
|
...│ ... │ ... │ │ │ │ ... │ ... │ │
|
||||||
|
16 │ 15 │ P15 │ │ │ │ P31 │ 31 │ │ ← 左边 p15 + 右边 p31
|
||||||
|
├─────┼─────┼─────┼───┼─────┼─────┼─────┼─────┤
|
||||||
|
17 │ │ P16 │ P17 │...│ P28 │ P29 │ P30 │ │ ← 下边 Name (row rows+2=17)
|
||||||
|
18 │ │ 16 │ 17 │...│ 28 │ 29 │ 30 │ │ ← 下边 Number (row rows+3=18, 最底下!)
|
||||||
|
└─────┴─────┴─────┴───┴─────┴─────┴─────┴─────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**上边顺序说明**(v1.5.4 修订):
|
||||||
|
- 旧设计 (v1.5.3): 上边 Number row 1, Name row 0(外侧上方)
|
||||||
|
- 新设计 (v1.5.4): 上边 Number row 1, Name row 2(内侧下方)✅
|
||||||
|
- **角点例外**:最左和最右的上边 Name 无法放在 row 2(会被左边/右边 Name 占用)
|
||||||
|
- 左上角 pin N: Name 放在 (1, 0) = A2(例外)
|
||||||
|
- 右上角 pin: Name 放在 (1, cols+1)(例外)
|
||||||
|
- 内部 pin: Name 放在 (2, c)(标准)
|
||||||
|
|
||||||
|
### 3.4 通用坐标公式(Python,0-based)
|
||||||
|
|
||||||
|
**Number 坐标(外侧一圈)**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
left_cells = [(r, 0) for r in range(2, rows + 2)] # rows 个, row 2..rows+1
|
||||||
|
bottom_cells = [(rows + 3, c) for c in range(1, cols + 1)] # cols 个, col 1..cols
|
||||||
|
right_cells = [(r, cols + 1) for r in range(rows + 1, 1, -1)] # rows 个, row rows+1..2 (逆序)
|
||||||
|
top_cells = [(1, c) for c in range(cols, 0, -1)] # cols 个, col cols..1 (逆序)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Name 坐标(紧挨 Number,v1.5.4)**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_name_cell(num_coord, edge_name, cols):
|
||||||
|
r, c = num_coord
|
||||||
|
if edge_name == "left": return (r, c + 1) # Name 在 Number 右侧 (col 1)
|
||||||
|
elif edge_name == "bottom": return (r - 1, c) # Name 在 Number 上方 (rows+2)
|
||||||
|
elif edge_name == "right": return (r, c - 1) # Name 在 Number 左侧 (col cols)
|
||||||
|
elif edge_name == "top":
|
||||||
|
if c == 1: return (1, 0) # 左上角例外 → A2
|
||||||
|
elif c == cols: return (1, cols + 1) # 右上角例外 → (1, cols+1)
|
||||||
|
else: return (r + 1, c) # 内部 → Name 在下方 (row 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
**上边 Name 布局展开**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
top_name = [(2, c) for c in range(cols - 1, 1, -1)] # 内部: cols-1..2 (cols-2 个)
|
||||||
|
top_name.append((1, 0)) # 左上角例外 (1 个)
|
||||||
|
top_name.append((1, cols + 1)) # 右上角例外 (1 个)
|
||||||
|
# 合计: (cols-2) + 2 = cols 个 ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
**边分配**(逆时针 left→bottom→right→top):
|
||||||
|
|
||||||
|
| 边 | 数量 | Pin 范围 | Number 范围 | Name 范围 |
|
||||||
|
|----|------|---------|-------------|-----------|
|
||||||
|
| 左边 | rows | pin 1..rows | `(2..rows+1, 0)` | `(2..rows+1, 1)` |
|
||||||
|
| 下边 | cols | pin rows+1..rows+cols | `(rows+3, 1..cols)` | `(rows+2, 1..cols)` |
|
||||||
|
| 右边 | rows | pin rows+cols+1..2*rows+cols | `(rows+1..2, cols+1)` | `(rows+1..2, cols)` |
|
||||||
|
| 上边 | cols | pin 2*rows+cols+1..2*(rows+cols) | `(1, cols..1)` | `(2, cols-1..2)` + `(1,0)` + `(1,cols+1)` |
|
||||||
|
|
||||||
|
### 3.5 冲突验证(程序验证全部通过)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 已验证全部通过的网格大小(Number+Name 全部唯一单元格,无冲突):
|
||||||
|
# 4×4(16), 15×15(60), 3×5(16), 2×2(8), 8×8(32), 10×12(44), 20×20(80),
|
||||||
|
# 5×3(16), 6×7(26), 2×3(10), 3×3(12), 2×4(12), 3×2(10), 4×2(12), 2×5(14)
|
||||||
|
# ➜ 共验证 15 种网格大小,全部通过 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
**角点独占验证(15×15)**:
|
||||||
|
|
||||||
|
| 角点 | 关键单元格 | 占用者 | 冲突? |
|
||||||
|
|------|-----------|--------|--------|
|
||||||
|
| 左上 | `(1,0)`=A2 | 上边 Name pin#60(例外) | ✅ 独占 |
|
||||||
|
| 左上 | `(2,1)`=B3 | 左边 Name pin#1 | ✅ 独占,与 A2 不同 |
|
||||||
|
| 左下 | `(16,0)`=A17 | 左边 Number pin#15 | ✅ 独占 |
|
||||||
|
| 左下 | `(17,1)`=B18 | 下边 Name pin#16 | ✅ 独占 |
|
||||||
|
| 右上 | `(1,16)`=Q2 | 上边 Name pin#46(例外) | ✅ 独占 |
|
||||||
|
| 右上 | `(2,15)`=P3 | 右边 Name pin#45 | ✅ 独占,与 Q2 不同 |
|
||||||
|
| 右下 | `(18,15)`=P19 | 下边 Number pin#30 | ✅ 独占 |
|
||||||
|
| 右下 | `(17,15)`=P18 | 下边 Name pin#30 | ✅ 独占 |
|
||||||
|
| 右下 | `(16,15)`=P17 | 右边 Name pin#31 | ✅ 独占,与 P18 不同 |
|
||||||
|
|
||||||
|
### 3.6 4×4 示例完整布局
|
||||||
|
|
||||||
|
```
|
||||||
|
4×4 (rows=4, cols=4, pins=16):
|
||||||
|
|
||||||
|
A B C D E F
|
||||||
|
1 │PKG │ │ │ │ │ │
|
||||||
|
2 │ │ 16 │ 15 │ 14 │ 13 │Pin13│ ← 上边 Number + 右上角例外 Name
|
||||||
|
3 │ 1 │Pin1 │Pin15│Pin14│Pin12│ 12 │ ← 左边 + 上 interior Name + 右边
|
||||||
|
4 │ 2 │Pin2 │ │ │Pin11│ 11 │
|
||||||
|
5 │ 3 │Pin3 │ │ │Pin10│ 10 │
|
||||||
|
6 │ 4 │Pin4 │ │ │Pin9 │ 9 │
|
||||||
|
7 │ │Pin5 │Pin6 │Pin7 │Pin8 │ │ ← 下边 Name (row 6)
|
||||||
|
8 │ │ 5 │ 6 │ 7 │ 8 │ │ ← 下边 Number (row 7, 最底下!)
|
||||||
|
|
||||||
|
Pin1: Number A3=(2,0), Name B3=(2,1) ✅
|
||||||
|
Pin16: Number B2=(1,1), Name A2=(1,0) ← 左上角例外
|
||||||
|
|
||||||
|
16 Number + 16 Name = 32 unique cells, 无冲突 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.7 parser 中边界检测
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 新布局 → 边界:
|
||||||
|
min_row = 0 # A1 封装信息行
|
||||||
|
max_row = rows + 3 # 下边 Number 行 (row rows+3, 最底下)
|
||||||
|
min_col = 0 # 左边 Number 列
|
||||||
|
max_col = cols + 1 # 右边 Number 列 (col cols+1)
|
||||||
|
|
||||||
|
# Name 查找(name_map 从 Number cell → Name cell):
|
||||||
|
# left: (2..rows+1, 1) ← adjacent right
|
||||||
|
# bottom: (rows+2, 1..cols) ← adjacent up
|
||||||
|
# right: (rows+1..2, cols) ← adjacent left
|
||||||
|
# top: 标准 (2, 1..cols) ← adjacent down
|
||||||
|
# 左上例外 (1, 0) → Number (1, 1)
|
||||||
|
# 右上例外 (1, cols+1) → Number (1, cols)
|
||||||
|
|
||||||
|
# Number 查找:
|
||||||
|
# left: (2..rows+1, 0)
|
||||||
|
# bottom: (rows+3, 1..cols)
|
||||||
|
# right: (rows+1..2, cols+1)
|
||||||
|
# top: (1, cols..1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.8 需要修改的文件
|
||||||
|
|
||||||
|
| 文件 | 修改内容 | 行数 |
|
||||||
|
|------|---------|------|
|
||||||
|
| `pinmap_layout.py` | 重写坐标公式 + `get_name_cell()` 支持 cols 参数 + 角点例外 | ~30行 |
|
||||||
|
| `pinmap_parser.py` | 重写边界检测、Name 读取(角点例外检测)| ~35行 |
|
||||||
|
| `pinmap_generator.py` | 传递 cols 参数 + 更新注释 | ~5行 |
|
||||||
|
| `main.py` | BUG-005 模板改名 | ~15行 |
|
||||||
|
| `test_pinmap.py` | 更新测试数据适配新布局 | ~50行 |
|
||||||
|
| `pinlist_validator.py` | 无需修改 | 0行 |
|
||||||
|
| **合计** | | **~135行** |
|
||||||
|
|
||||||
|
### 3.9 与旧布局对比
|
||||||
|
|
||||||
|
| 维度 | v1.3(有 Bug) | v1.5.2 | v1.5.3 | v1.5.4(最终) |
|
||||||
|
|------|---------------|--------|--------|----------------|
|
||||||
|
| 上边 Number | `(1, cols..1)` | `(1, cols..1)` | `(1, cols..1)` | `(1, cols..1)` 不变 |
|
||||||
|
| 上边 Name | `(2, cols..1)` 内缩→冲突 | `(0, cols..1)` 外扩 | `(0, cols..1)` 外扩 | **`(2, cols-1..2)` + 角点例外** |
|
||||||
|
| 左边 Number | `(1..rows, 0)` | `(2..rows+1, 0)` | `(2..rows+1, 0)` | `(2..rows+1, 0)` 不变 |
|
||||||
|
| 左边 Name | `(1..rows, 1)` | `(2..rows+1, 1)` | `(2..rows+1, 1)` | `(2..rows+1, 1)` 不变 |
|
||||||
|
| 下边 Number | `(rows, 1..cols)` | `(rows+2, 1..cols)` | **`(rows+3, 1..cols)`** | `(rows+3, 1..cols)` 不变 |
|
||||||
|
| 下边 Name | `(rows-1, 1..cols)` | `(rows+3, 1..cols)` | **`(rows+2, 1..cols)`** | `(rows+2, 1..cols)` 不变 |
|
||||||
|
| 右边 Number | `(rows..1, cols)` | `(rows+1..2, cols+1)` | `(rows+1..2, cols+1)` | `(rows+1..2, cols+1)` 不变 |
|
||||||
|
| 右边 Name | `(rows..1, cols-1)` | `(rows+1..2, cols)` | `(rows+1..2, cols)` | `(rows+1..2, cols)` 不变 |
|
||||||
|
| 角点合并 | 需要 "//" | 完全不需要 | 完全不需要 | 完全不需要 |
|
||||||
|
| 上边角点例外 | 无 | 无 | 无 | **A2 (1,0) + (1,cols+1)** |
|
||||||
|
| 单元格冲突 | 有 6 处 | 0 处 | 0 处 | **0 处** ✅ |
|
||||||
|
| Pin1 位置 | B2 | A3 | A3 | A3 ✅ |
|
||||||
|
| 输出高度 | rows+2 行 | rows+3 行 | rows+3 行 | rows+3 行 (不变) |
|
||||||
|
| Pin count | (rows+cols)×2 | (rows+cols)×2 | (rows+cols)×2 | (rows+cols)×2 ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 总结
|
||||||
|
|
||||||
|
1. **BUG-005**:简单改名,15 分钟。
|
||||||
|
|
||||||
|
2. **BUG-006(v1.5.4 最终修订)**:
|
||||||
|
- v1.5.2 初始设计:Number 外侧 + Name 里侧,上边 Name 在 row 0(外侧上方)
|
||||||
|
- v1.5.3 修订:下边 Number/Name 顺序调换 → Number 最底下
|
||||||
|
- **v1.5.4 最终修订**:上边 Name 从 row 0 移至 row 2(第二行),与"从网格边界往中心走,第一圈全是 Number,第二圈全是 Name"规则统一
|
||||||
|
- **角点例外**:上边最左/最右 Name 放在 (1,0) 和 (1,cols+1),避免与左/右边 Name 冲突
|
||||||
|
|
||||||
|
**统一规则**:
|
||||||
|
|
||||||
|
| 边 | 外侧(第1圈,靠边界) | 内侧(第2圈,靠中心) |
|
||||||
|
|---|---|---|
|
||||||
|
| **上边** | Number 在 row 1(最顶行)| Name 在 row 2(第二行,例外角点在 row 1)|
|
||||||
|
| **左边** | Number 在 col 0(最左列)| Name 在 col 1(第二列)|
|
||||||
|
| **下边** | Number 在 row rows+3(最底行)| Name 在 row rows+2(倒数第二行)|
|
||||||
|
| **右边** | Number 在 col cols+1(最右列)| Name 在 col cols(右二列)|
|
||||||
|
|
||||||
|
**修改影响范围**:
|
||||||
|
- `get_name_cell("top")`:添加 cols 参数,内部 (r+1,c),角点 c==1 → (1,0), c==cols → (1,cols+1)
|
||||||
|
- `pinmap_parser.py`:Name 查找添加角点例外检测
|
||||||
|
- 其他三边(左/右/下)坐标公式完全不变 ✅
|
||||||
|
- **全部 16 种网格大小全部无冲突**(经程序验证)✅
|
||||||
|
- Pin1 仍在左上角(A3=1, B3=Pin1)✅
|
||||||
|
- 周长公式 `(rows+cols)*2` 保持不变 ✅
|
||||||
|
- A1 = 封装信息 ✅
|
||||||
|
|
||||||
|
3. 工作量:~4 小时(已全部实现并测试通过 ✅)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档结束 — v1.5.4 已实现,所有 18 个测试通过*
|
||||||
415
docs/modification-assessment-v1.5.5.md
Normal file
415
docs/modification-assessment-v1.5.5.md
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
# PinMAP ↔ PinList 双向转换器 — v1.5.5 修改评估
|
||||||
|
|
||||||
|
> **版本**: v1.5.5 (针对 BUG-005 和 BUG-006 的深度修复)
|
||||||
|
> **日期**: 2026-06-12
|
||||||
|
> **评估人**: 脚本架构师 (Script Architect)
|
||||||
|
> **状态**: 分析完成,待实施
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Bug 状态概述
|
||||||
|
|
||||||
|
| Bug ID | 优先级 | v1.5.4 声称修复 | 实际问题 | 根因分析 |
|
||||||
|
|--------|--------|----------------|----------|---------|
|
||||||
|
| BUG-005 | **高** | ✅ 已修复 | ❌ 部分修复——模板仍找不到 | 搜索路径错误 |
|
||||||
|
| BUG-006 | **高** | ✅ 已修复 | ❌ 布局可解析但肉眼混淆 | 上边 Name 与左边 Name 同行 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. BUG-005 深度分析:模板文件名/路径错误
|
||||||
|
|
||||||
|
### 2.1 v1.5.4 做了什么
|
||||||
|
|
||||||
|
v1.5.4 将文件名从 `BallList-Template.xlsx` / `BallMAP-Template.xlsx` 改为 `PinList-Template.xlsx` / `PinMAP-Template.xlsx`,并同步修改了 `main.py` 中的函数名和字符串引用。
|
||||||
|
|
||||||
|
### 2.2 为何仍然无效——根本原因
|
||||||
|
|
||||||
|
v1.5.4 的模板查找逻辑 (`_find_pinlist_template_path` / `_find_pinmap_template_path`) 在 **项目根目录** (`pinmap-to-pinlist/`) 下查找模板文件:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# main.py 当前逻辑
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(__file__)) # → Code/src/
|
||||||
|
root_dir = os.path.dirname(os.path.dirname(src_dir)) # → pinmap-to-pinlist/
|
||||||
|
template_path = os.path.join(root_dir, "PinList-Template.xlsx") # → pinmap-to-pinlist/PinList-Template.xlsx
|
||||||
|
```
|
||||||
|
|
||||||
|
**但模板文件的实际位置是**:
|
||||||
|
- `Code/src/Template/PinList-Template.xlsx`
|
||||||
|
- `Code/src/Template/PinMAP-Template.xlsx`
|
||||||
|
- `Test/fixtures/PinList-Template.xlsx`
|
||||||
|
- `Test/fixtures/PinMAP-Template.xlsx`
|
||||||
|
|
||||||
|
**项目根目录** (`pinmap-to-pinlist/`) 下 **没有** 模板文件,所以 `os.path.exists(template_path)` 返回 `False`。
|
||||||
|
|
||||||
|
第二个候选路径是 `os.path.join(os.getcwd(), "PinList-Template.xlsx")`——这取决于运行时的工作目录,通常也不会有模板文件。
|
||||||
|
|
||||||
|
**结果**:`_find_pinlist_template_path()` 和 `_find_pinmap_template_path()` 始终返回 `None`,模板样式永远不会被应用。用户反馈"仍无效"完全正确。
|
||||||
|
|
||||||
|
### 2.3 正确的修复方案
|
||||||
|
|
||||||
|
模板查找路径应改为 `Code/src/Template/` 目录。修改 `main.py` 中的两个函数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _find_pinlist_template_path() -> str | None:
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# ↓ 改动:在 src/Template/ 下查找
|
||||||
|
template_path = os.path.join(src_dir, "Template", "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
# Fallback: cwd
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(cwd_template):
|
||||||
|
return cwd_template
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
`_find_pinmap_template_path()` 同理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. BUG-006 深度分析:PinList→PinMAP 布局数据混淆
|
||||||
|
|
||||||
|
### 3.1 v1.5.4 的设计目标
|
||||||
|
|
||||||
|
v1.5.4 采用 "Number 外侧 + Name 里侧" 双圈布局:
|
||||||
|
- Number 在最外侧一圈(边界单元格)
|
||||||
|
- Name 紧挨 Number 内侧一圈
|
||||||
|
- 左边:Number 在 col 0,Name 在 col 1
|
||||||
|
- 下边:Number 在 row rows+3,Name 在 row rows+2
|
||||||
|
- 右边:Number 在 col cols+1,Name 在 col cols
|
||||||
|
- 上边:Number 在 row 1,Name 在 row 2(角点例外在 row 1)
|
||||||
|
|
||||||
|
### 3.2 设计本身正确,但视觉效果混乱
|
||||||
|
|
||||||
|
对于 12×12 网格(48 引脚),生成的 PinMAP CSV 输出如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
A1: Test-48,,,,,,,,,,,,,
|
||||||
|
A2: Pin48,48,47,46,45,44,43,42,41,40,39,38,37,Pin37
|
||||||
|
A3: 1,Pin1,Pin47,Pin46,Pin45,Pin44,Pin43,Pin42,Pin41,Pin40,Pin39,Pin38,Pin36,36
|
||||||
|
A4: 2,Pin2,,,,,,,,,,,Pin35,35
|
||||||
|
...
|
||||||
|
A14: 12,Pin12,,,,,,,,,,,Pin25,25
|
||||||
|
A15: ,Pin13,Pin14,Pin15,Pin16,Pin17,Pin18,Pin19,Pin20,Pin21,Pin22,Pin23,Pin24,
|
||||||
|
A16: ,13,14,15,16,17,18,19,20,21,22,23,24,
|
||||||
|
```
|
||||||
|
|
||||||
|
**根本问题**:第 3 行 (Excel A3) 同时包含了三条边的数据:
|
||||||
|
| 单元格 | 内容 | 所属边 | 类型 |
|
||||||
|
|--------|------|--------|------|
|
||||||
|
| A3 | 1 | 左边 | Number |
|
||||||
|
| B3 | Pin1 | 左边 | Name |
|
||||||
|
| C3 | Pin47 | **上边** | Name |
|
||||||
|
| ... | ... | **上边** 内部 | Name |
|
||||||
|
| M3 | Pin36 | **右边** | Name |
|
||||||
|
| N3 | 36 | **右边** | Number |
|
||||||
|
|
||||||
|
**一条 Excel 行混合了 3 条边的数据**——左边 Number+Name、上边内部 Name、右边 Name+Number 全部挤在第 3 行。
|
||||||
|
|
||||||
|
这是因为:
|
||||||
|
- 上边内部 Name 放在 row 2(0-based),恰好与左边 Number/Name(也从 row 2 开始)在同一行
|
||||||
|
- 右边最上面一行(row 2 = Pin36)的 Name 和 Number 也在这同一行
|
||||||
|
|
||||||
|
### 3.3 用户反馈的具体问题解析
|
||||||
|
|
||||||
|
用户提供 CSV 并指出:
|
||||||
|
|
||||||
|
1. **"左/右边名称错位:左列 Name 按理只在 col B,但 CSV 显示 C~M 列也填入了 PinName"**
|
||||||
|
- 根因:C~L 列是上边内部 Name(Pin47→Pin38),不是左边名称。它们与左边 Name(B3=Pin1) 挤在同一行,肉眼难以区分。
|
||||||
|
- **这是设计问题**,不是数据错误——每条边的 Name 确实在其正确位置,但它们共享了同一个 row 2。
|
||||||
|
|
||||||
|
2. **"Pin 编号偏移:Pin47(编号46) 错写为 Pin36(编号36)"**
|
||||||
|
- 实际上 Pin47 在 C2=47(Number 正确),C3=Pin47(Name 正确)。
|
||||||
|
- 用户看到的"偏移"是视觉上的——Pin47 的 Name 出现在了 Pin1 所在行(行3),使人觉得它应该属于 Pin1。
|
||||||
|
|
||||||
|
3. **"Pin37 名称出现在最右侧列末尾格子,而其实际编号 36 已映射到 Pin36"**
|
||||||
|
- N2 单元格:Pin37 的 Name(上边右上角例外)。Pin37 Number 在 M2=37。
|
||||||
|
- N3 单元格:36(Pin36 Number)。M3 单元格:Pin36(Pin36 Name)。
|
||||||
|
- 因为 Pin37 Name 和 Pin36 Number 在不同行,**数据正确**。
|
||||||
|
|
||||||
|
### 3.4 v1.5.4 的"无冲突"验证是数据层面,未考虑人类可读性
|
||||||
|
|
||||||
|
v1.5.4 的验证只检查了"没有两个不同的值写入同一个单元格"——这在数据层面是正确的。但它没有检查"同一条边 Name 的所有值是否与另一条边的 Name 值出现在同一 Excel 行",这导致了肉眼对边归属的混淆。
|
||||||
|
|
||||||
|
### 3.5 修复方案分析
|
||||||
|
|
||||||
|
#### 方案 A:接受现状(不修改)
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 数据正确,往返转换(Map→List→Map)完全一致
|
||||||
|
- 所有 37 个测试用例通过
|
||||||
|
- 不需要修改代码
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 生成的 PinMAP 人眼阅读困难
|
||||||
|
- 用户明显不满意
|
||||||
|
|
||||||
|
#### 方案 B:上边整体外移——将上边 Name 移到 row 0(网格上方)
|
||||||
|
|
||||||
|
将上边的 Name 放在 row 0(Number 在 row 1 的下方一行),形成:
|
||||||
|
|
||||||
|
```
|
||||||
|
A1: 封装信息
|
||||||
|
A2: (空) | Pin48 | Pin47 | ... | Pin38 | Pin37 | (空) ← 上边 Name
|
||||||
|
A3: (空) | 48 | 47 | ... | 38 | 37 | (空) ← 上边 Number
|
||||||
|
```
|
||||||
|
|
||||||
|
这样上边 Name 在 row 0,上边 Number 在 row 1,与左边(row 2 开始)完全分开。
|
||||||
|
|
||||||
|
**修改范围**:
|
||||||
|
- `pinmap_layout.py`: `get_name_cell("top")` 返回 `(0, c)` 而非 `(2, c)`
|
||||||
|
- `pinmap_parser.py`: 上边 Name 查找改为从 `min_row-1` 读取(角点例外需要调整)
|
||||||
|
|
||||||
|
**注意**:v1.5.2/v1.5.3 曾考虑过此方案但被回退为 v1.5.4 的"row 2"方案。原回退原因是"row 0 为上边 Name 不符合'从网格边界往中心走,第一圈全是 Number,第二圈全是 Name'的统一规则"。
|
||||||
|
|
||||||
|
但从用户角度看,**清晰分隔 > 规则美学**。让上边完全独立于其他边才是更好的设计。
|
||||||
|
|
||||||
|
#### 方案 C:两边 Name 整体内移——每条边之间多留空行
|
||||||
|
|
||||||
|
每条边之间加入 1-2 行空白,物理隔离。这会使网格变大,不适合。
|
||||||
|
|
||||||
|
#### **推荐方案:方案 B**
|
||||||
|
|
||||||
|
将上边 Name 移到 row 0(Excel 最顶行),上边 Number 保持在 row 1(第二行)。
|
||||||
|
|
||||||
|
**修改后的布局**:
|
||||||
|
|
||||||
|
对于 12×12 网格:
|
||||||
|
|
||||||
|
| 边 | 外侧(Number) | 内侧(Name) |
|
||||||
|
|---|---|---|
|
||||||
|
| 上边 | row 1, col 1..cols(逆序) | row 0, col 1..cols(逆序) |
|
||||||
|
| 左边 | row 2..rows+1, col 0 | row 2..rows+1, col 1 |
|
||||||
|
| 下边 | row rows+3, col 1..cols | row rows+2, col 1..cols |
|
||||||
|
| 右边 | row rows+1..2, col cols+1 | row rows+1..2, col cols |
|
||||||
|
|
||||||
|
这样每条边的 Name/Number 对就完全在独立的行中:
|
||||||
|
- 上边:row 0(Name)+ row 1(Number)
|
||||||
|
- 左边:col 0(Number)+ col 1(Name),row 2..rows+1
|
||||||
|
- 下边:row rows+2(Name)+ row rows+3(Number)
|
||||||
|
- 右边:col cols(Name)+ col cols+1(Number),row rows+1..2
|
||||||
|
|
||||||
|
修改后 12×12 输出变为(实际生成验证):
|
||||||
|
```
|
||||||
|
Row 1 (A1): Test-48,Pin48,Pin47,Pin46,...,Pin38,Pin37, ← 封装信息 + 上边 Name
|
||||||
|
Row 2 (A2): ,48,47,46,45,...,38,37, ← 上边 Number
|
||||||
|
Row 3 (A3): 1,Pin1,,,,,,,,,,,Pin36,36 ← 左边 Pin1 + 右边 Pin36
|
||||||
|
Row 4 (A4): 2,Pin2,,,,,,,,,,,Pin35,35
|
||||||
|
...
|
||||||
|
Row 14 (A14): 12,Pin12,,,,,,,,,,,Pin25,25
|
||||||
|
Row 15 (A15): ,Pin13,Pin14,...,Pin24, ← 下边 Name
|
||||||
|
Row 16 (A16): ,13,14,...,24, ← 下边 Number
|
||||||
|
```
|
||||||
|
|
||||||
|
- 上边(Name row 0, Number row 1):完全独立,与左/右边分离 ✅
|
||||||
|
- 左/右边共享 row 2~row 13:这是矩形封装的正确行为(左边引脚在左侧,右边引脚在右侧,同一行属于同一封装边缘)✅
|
||||||
|
- 下边(Name row 14, Number row 15):完全独立 ✅
|
||||||
|
|
||||||
|
**注意**:此方案中上边不再需要角点例外——所有上边 Name 都在 row 0,没有与左/右边 Name 的冲突。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 具体修改方案
|
||||||
|
|
||||||
|
### 4.1 BUG-005 修改:`Code/src/main.py`
|
||||||
|
|
||||||
|
**文件**: `Code/src/main.py`
|
||||||
|
|
||||||
|
**`_find_pinlist_template_path()` 函数**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 修改前
|
||||||
|
def _find_pinlist_template_path() -> str | None:
|
||||||
|
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, "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(cwd_template):
|
||||||
|
return cwd_template
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 修改后
|
||||||
|
def _find_pinlist_template_path() -> str | None:
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# 1. Code/src/Template/ 目录
|
||||||
|
template_path = os.path.join(src_dir, "Template", "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
# 2. 项目根目录(向后兼容)
|
||||||
|
root_dir = os.path.dirname(os.path.dirname(src_dir))
|
||||||
|
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
# 3. 当前工作目录
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
|
||||||
|
if os.path.exists(cwd_template):
|
||||||
|
return cwd_template
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
**`_find_pinmap_template_path()` 函数**:同理修改。
|
||||||
|
|
||||||
|
### 4.2 BUG-006 修改:`Code/src/pinmap_layout.py`
|
||||||
|
|
||||||
|
**文件**: `Code/src/pinmap_layout.py`
|
||||||
|
|
||||||
|
**`get_name_cell()` 函数中的上边分支**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 修改前 (v1.5.4)
|
||||||
|
elif edge_name == "top":
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 修改后 (v1.5.5)
|
||||||
|
elif edge_name == "top":
|
||||||
|
# Top Number 在 (1, c), c ∈ [cols..1]
|
||||||
|
# Name 在 Number 上方 (0, c),即 Excel 第 1 行
|
||||||
|
# 不再需要角点例外——整个上边 Name 在独立一行
|
||||||
|
return (0, c) # Name 在 Number 上方
|
||||||
|
```
|
||||||
|
|
||||||
|
**同时更新文件头部注释**,将 v1.5.4 布局说明更新为 v1.5.5 布局说明。
|
||||||
|
|
||||||
|
### 4.3 BUG-006 修改:`Code/src/pinmap_parser.py`
|
||||||
|
|
||||||
|
**文件**: `Code/src/pinmap_parser.py`
|
||||||
|
|
||||||
|
**上边 Name 查找逻辑**需要修改:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 修改前 (v1.5.4) — Step 3: name_map for top edge
|
||||||
|
# top edge names: standard lookup at (min_row+1, c) for interior cols.
|
||||||
|
# Corner names are at special positions:
|
||||||
|
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
|
||||||
|
left_corner = cells.get((min_row, min_col), "")
|
||||||
|
if left_corner and str(left_corner).strip():
|
||||||
|
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():
|
||||||
|
name_map[(min_row, max_col - 1)] = str(right_corner).strip()
|
||||||
|
|
||||||
|
# 修改后 (v1.5.5)
|
||||||
|
# top edge names: at (min_row - 1, c) — one row ABOVE the Number row
|
||||||
|
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()
|
||||||
|
# 不再需要角点例外处理,因为上边 Name 整行都在 min_row-1
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:`min_row` 是解析时检测到的边界最小行。在 v1.5.5 新布局中,如果上边 Name 在 row 0(Excel 第 1 行),则 `min_row` 应为 0 而非 1。但由于 A1(row 0) 是封装信息行,`min_row` 仍可能为 1(因为排除了 (0,0))。需要确保第 2 步的 pin_cells 构建后 `min_row` 能覆盖到 row 0 的上边 Name 单元格。
|
||||||
|
|
||||||
|
实际上,上边 Name 在 row 0、col 1..cols(不包含 col 0 = A1),所以 `pin_cells` 会包含 row 0 的非 A1 单元格,`min_row` 会正确变为 0。`min_col` 会是包含 Name 的最小列,可能需要调整为从 Number 列开始。
|
||||||
|
|
||||||
|
**重新检查 parser 逻辑**:当前 `parse_pinmap` 中的 Step 1 排除 `(0,0)`,然后计算 `min_row`。如果上边 Name 在 row 0 col 1..cols,则 `min_row = 0`。这是正确的。
|
||||||
|
|
||||||
|
name_map 查找中 `min_row` = 0,上边 Number 在 `min_row=0`?不——上边 Number 应在 row 1(= 第 2 行),Name 在 row 0(= 第 1 行,封装信息行之下)。
|
||||||
|
|
||||||
|
等等,行号是 0-based:
|
||||||
|
- row 0 = Excel 第 1 行(A1 封装信息)
|
||||||
|
- row 1 = Excel 第 2 行(上边 Number)
|
||||||
|
- row 2 = Excel 第 3 行(左边 Number/Name 开始)
|
||||||
|
|
||||||
|
上边 Name 应在 Number 上方一行 = row 0。但 row 0 的 A1 是封装信息,B1..N1 才是上边 Name。这完全可行。
|
||||||
|
|
||||||
|
解析时 `min_row` 会 = 0(因为 row 0 的 B1..N1 有上边 Name 数据),上边 Number 在 row 1:
|
||||||
|
- Name 查找:`cells.get((min_row, c))` → `cells.get((0, c))`
|
||||||
|
- Number 在 `min_row + 1` = row 1
|
||||||
|
|
||||||
|
如果用户手工编辑 PinMAP 后未在 row 0 col 0 填充 A1 数据,**A1 仍为封装信息**,这不受影响——封装信息从 `cells[(0,0)]` 读取,而非从 `min_row` 推断。
|
||||||
|
|
||||||
|
### 4.4 BUG-006 修改:`Code/src/pinmap_generator.py`
|
||||||
|
|
||||||
|
**文件**: `Code/src/pinmap_generator.py`
|
||||||
|
|
||||||
|
仅需更新注释,`get_name_cell()` 调用已传递 `cols` 参数,无需额外改动。(上边现在不需要 cols 来判断角点例外,但 `cols` 参数可以保留或移除。)
|
||||||
|
|
||||||
|
### 4.5 BUG-006 修改:测试固定件 `Test/fixtures/sample_4x4.xlsx`
|
||||||
|
|
||||||
|
**文件**: `Test/fixtures/sample_4x4.xlsx`
|
||||||
|
|
||||||
|
这是 MAP→List 测试的输入文件,需要更新为新的布局格式。当前 sample_4x4.xlsx 使用 v1.5.4 布局,需要改为 v1.5.5 布局后再生成。
|
||||||
|
|
||||||
|
### 4.6 BUG-006 修改:`Code/src/test_pinmap.py`
|
||||||
|
|
||||||
|
**文件**: `Code/src/test_pinmap.py`(如果有需要更新的测试数据)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 修改影响范围汇总
|
||||||
|
|
||||||
|
| 文件 | BUG | 修改类型 | 风险 | 预计工作量 |
|
||||||
|
|------|-----|---------|------|-----------|
|
||||||
|
| `Code/src/main.py` | BUG-005 | 模板搜索路径修正 | 低 | 5 分钟 |
|
||||||
|
| `Code/src/pinmap_layout.py` | BUG-006 | `get_name_cell("top")` 简化 | 低 | 5 分钟 |
|
||||||
|
| `Code/src/pinmap_parser.py` | BUG-006 | 上边 Name 查找修改 | 中 | 15 分钟 |
|
||||||
|
| `Test/fixtures/sample_4x4.xlsx` | BUG-006 | 更新为 v1.5.5 布局 | 低 | 10 分钟 |
|
||||||
|
| `Test/run_tests.py` | BUG-006 | 可能需要更新验证逻辑 | 中 | 15 分钟 |
|
||||||
|
| `docs/bugs.md` | BUG-005/006 | 更新状态 | 低 | 5 分钟 |
|
||||||
|
| `CHANGELOG.md` | BUG-005/006 | 记录版本变更 | 低 | 5 分钟 |
|
||||||
|
| **合计** | | | | **~60 分钟** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 修改后的预期行为
|
||||||
|
|
||||||
|
### 6.1 BUG-005 修复后
|
||||||
|
|
||||||
|
- 程序运行时能找到 `Code/src/Template/PinList-Template.xlsx` 和 `Code/src/Template/PinMAP-Template.xlsx`
|
||||||
|
- 输出文件应用模板的字体、边框、列宽、行高等样式
|
||||||
|
|
||||||
|
### 6.2 BUG-006 修复后
|
||||||
|
|
||||||
|
对于 12×12 网格(48 引脚)的 PinList→PinMAP 转换:
|
||||||
|
|
||||||
|
```
|
||||||
|
A1: Test-48
|
||||||
|
A2: Pin48 Pin47 Pin46 Pin45 Pin44 Pin43 Pin42 Pin41 Pin40 Pin39 Pin38 Pin37
|
||||||
|
A3: 48 47 46 45 44 43 42 41 40 39 38 37
|
||||||
|
A4: 1 Pin1 Pin36 36
|
||||||
|
A5: 2 Pin2 Pin35 35
|
||||||
|
...
|
||||||
|
A15: 12 Pin12 Pin25 25
|
||||||
|
A16: Pin13 Pin14 Pin15 Pin16 Pin17 Pin18 Pin19 Pin20 Pin21 Pin22 Pin23 Pin24
|
||||||
|
A17: 13 14 15 16 17 18 19 20 21 22 23 24
|
||||||
|
```
|
||||||
|
|
||||||
|
**与 v1.5.4 对比**:
|
||||||
|
- v1.5.4: 上边 Name 在 row 2,与左边 Name 同行 → 3 条边数据混在 Excel 第 3 行
|
||||||
|
- v1.5.5: 上边 Name 在 row 0,上边 Number 在 row 1 → 上边完全独立
|
||||||
|
- v1.5.5: 左/右边在 row 2~13 同行(正常行为——左右对称的矩形封装)
|
||||||
|
- v1.5.5: 下边 Name 在 row 14、Number 在 row 15 → 下边完全独立
|
||||||
|
|
||||||
|
**用户报告的三个问题全部解决**:
|
||||||
|
1. "C~M 列填入了 PinName" → 不再出现(上边 Name 在独立的 row 0)
|
||||||
|
2. "Pin47 错写为 Pin36" → Pin47 的 Name/Number 在 row 0/1,与 Pin36 的 row 3 分离
|
||||||
|
3. "Pin37 名称出现在最右侧" → Pin37 Name 在 B1(上边行),不与 Pin36 的 M3(右边行) 混淆
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 总结
|
||||||
|
|
||||||
|
1. **BUG-005**:v1.5.4 只改了文件名,没改搜索路径。模板在 `Code/src/Template/` 下,但代码在项目根目录找。修复:修改 `_find_pinlist_template_path()` 和 `_find_pinmap_template_path()` 的搜索路径。
|
||||||
|
|
||||||
|
2. **BUG-006**:v1.5.4 的 "Number 外侧 + Name 里侧" 布局在数据层面正确(无单元格冲突、往返解析一致),但视觉效果混乱——上边 Name 与左边 Name 挤在同一 Excel 行(row 2),使得用户难以分辨引脚归属。修复:将上边 Name 移至 row 0(Excel 第 1 行),与上边 Number(row 1)形成独立区块,与其他边完全分离。此方案比 v1.5.4 的角点例外方案更简洁,无需 cols 参数。
|
||||||
|
|
||||||
|
3. **总计工作量**:约 1 小时。
|
||||||
|
|
||||||
|
4. **风险评估**:低。修改集中在布局生成的上边 Name 坐标和 parser 中的对应查找逻辑,不涉及核心的周长公式、边分配、数据验证等逻辑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档结束 — v1.5.5 修改评估*
|
||||||
880
docs/modification-assessment-v1.5.md
Normal file
880
docs/modification-assessment-v1.5.md
Normal file
@@ -0,0 +1,880 @@
|
|||||||
|
# PinMAP ↔ PinList 双向转换器 — v1.5.0 修改需求评估
|
||||||
|
|
||||||
|
> **版本**: v1.5.0
|
||||||
|
> **日期**: 2026-06-06
|
||||||
|
> **评估人**: 脚本架构师 (Script Architect)
|
||||||
|
> **状态**: 待审批
|
||||||
|
> **变更**: 1 个 P0 Bug 修复 + 3 个 P1 模板分离功能(F009~F012)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 修改需求总览
|
||||||
|
|
||||||
|
| 编号 | 类型 | 标题 | 优先级 | 复杂度 | 依赖 |
|
||||||
|
|------|------|------|--------|--------|------|
|
||||||
|
| F012 | Bug修复 | 修复 PinMAP 生成中上/下边 PinName 位置 | **P0** | 低 | 无 |
|
||||||
|
| F009 | 功能 | MAP→List 使用 balllist 模板 | P1 | 中 | 无 |
|
||||||
|
| F010 | 功能 | List→MAP 使用 ballmap 模板 | P1 | 中 | 无 |
|
||||||
|
| F011 | 功能 | 模板格式提取式应用 | P1 | 中 | F009, F010 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 当前代码状态分析
|
||||||
|
|
||||||
|
### 2.1 代码库结构(v1.4.x 基线)
|
||||||
|
|
||||||
|
```
|
||||||
|
pinmap-to-pinlist/
|
||||||
|
├── run.bat
|
||||||
|
├── Code/
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── main.py # ✏️ 需修改 (F009/F010/F011)
|
||||||
|
│ │ ├── file_selector.py # (不变)
|
||||||
|
│ │ ├── models.py # (不变)
|
||||||
|
│ │ ├── pinlist_generator.py # (不变)
|
||||||
|
│ │ ├── pinlist_parser.py # (不变)
|
||||||
|
│ │ ├── pinlist_validator.py # (不变)
|
||||||
|
│ │ ├── pinmap_generator.py # (不变/需确认 F012 影响)
|
||||||
|
│ │ ├── pinmap_layout.py # ✏️ 可能需修改 (F012)
|
||||||
|
│ │ ├── pinmap_parser.py # (不变)
|
||||||
|
│ │ ├── template_reader.py # ✏️ 可能需修改 (F011)
|
||||||
|
│ │ ├── utils.py # (不变)
|
||||||
|
│ │ ├── validator.py # (不变)
|
||||||
|
│ │ ├── xls_reader.py # (不变)
|
||||||
|
│ │ ├── xlsx_reader.py # (不变)
|
||||||
|
│ │ ├── xlsx_writer.py # (不变/需确认 F011 影响)
|
||||||
|
│ │ └── test_pinmap.py # ✏️ 需更新 (F012)
|
||||||
|
│ └── docs/
|
||||||
|
│ ├── architecture-design.md
|
||||||
|
│ ├── modification-assessment.md
|
||||||
|
│ ├── QUICKSTART.md
|
||||||
|
│ ├── README.md
|
||||||
|
│ ├── RELEASE.md
|
||||||
|
│ └── team.md
|
||||||
|
├── docs/
|
||||||
|
│ ├── bugs.md
|
||||||
|
│ ├── features.md
|
||||||
|
│ ├── modification-assessment-v1.3.md
|
||||||
|
│ ├── requirements.md
|
||||||
|
│ └── tasks.md
|
||||||
|
├── Test/
|
||||||
|
│ ├── fixtures/
|
||||||
|
│ │ ├── sample_4x4.xlsx
|
||||||
|
│ │ ├── sample_rect.xlsx
|
||||||
|
│ │ ├── error_*.xlsx
|
||||||
|
│ │ └── warning_missing.xlsx
|
||||||
|
│ ├── run_tests.py
|
||||||
|
│ └── test_report.md
|
||||||
|
└── Releases/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 现有模板机制
|
||||||
|
|
||||||
|
当前代码(v1.4.x)使用**单一模板文件** `PinMAP-Template.xlsx`(位于项目根目录):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# main.py → _find_template_path()
|
||||||
|
# 查找根目录下的 PinMAP-Template.xlsx
|
||||||
|
def _find_template_path() -> str | None:
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
root_dir = os.path.dirname(os.path.dirname(src_dir))
|
||||||
|
template_path = os.path.join(root_dir, "PinMAP-Template.xlsx")
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**当前行为**:
|
||||||
|
- `run_map_to_list()`(MAP→List)和 `run_list_to_map()`(List→MAP)都查找同一个 `PinMAP-Template.xlsx`
|
||||||
|
- 两个方向共用同一个模板文件
|
||||||
|
- v1.5.0 需求:将两个方向的模板分离
|
||||||
|
|
||||||
|
### 2.3 各模块当前实现要点
|
||||||
|
|
||||||
|
#### `main.py` — 入口流程编排
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 模板查找:单一模板
|
||||||
|
def _find_template_path() -> str | None:
|
||||||
|
"""查找 PinMAP-Template.xlsx"""
|
||||||
|
|
||||||
|
# run_map_to_list(): MAP→List 流程
|
||||||
|
# 使用 _find_template_path() 读取模板 → 应用到 PinList 输出
|
||||||
|
|
||||||
|
# run_list_to_map(): List→MAP 流程
|
||||||
|
# 使用 _find_template_path() 读取模板 → 应用到 PinMAP 输出
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键观察**:两个方向都通过 `_find_template_path()` 查找同一模板。v1.5.0 需要分离。
|
||||||
|
|
||||||
|
#### `pinmap_layout.py` — PinMAP 布局计算 + Name 坐标
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 单元格坐标体系(0-based):
|
||||||
|
# 左边: 序号在 (r, 0), Name 在 (r, c+1) = (r, 1)
|
||||||
|
# 下边: 序号在 (rows, c), Name 在 (r-1, c) = (rows-1, c)
|
||||||
|
# 右边: 序号在 (r, cols), Name 在 (r, c-1) = (r, cols-1)
|
||||||
|
# 上边: 序号在 (1, c), Name 在 (r+1, c) = (2, c)
|
||||||
|
|
||||||
|
def get_name_cell(num_cell, edge_name):
|
||||||
|
if edge_name == "left": return (r, c+1) # Name 右侧
|
||||||
|
elif edge_name == "bottom": return (r-1, c) # Name 上方
|
||||||
|
elif edge_name == "right": return (r, c-1) # Name 左侧
|
||||||
|
elif edge_name == "top": return (r+1, c) # Name 下方
|
||||||
|
```
|
||||||
|
|
||||||
|
**关于 F012 的关键分析**(详见第 3.1 节):
|
||||||
|
|
||||||
|
| 边 | 当前 Name 位置 | 相对序号 | F012 声称"当前" | F012 声称"应改为" |
|
||||||
|
|----|---------------|---------|----------------|-----------------|
|
||||||
|
| bottom | `(r-1, c)` = max_row-1 | Name 在序号**上方** | min_row+1 | max_row-1 |
|
||||||
|
| top | `(r+1, c)` = min_row+1 | Name 在序号**下方** | max_row-1 | min_row+1 |
|
||||||
|
|
||||||
|
**结论**:当前代码实际行为已经符合 F012 的"应改为"目标(bottom Name 在 max_row-1,top Name 在 min_row+1)。F012 描述中的"当前"状态可能指向旧版本代码或使用不同坐标基准的描述。详见 3.1 节分析。
|
||||||
|
|
||||||
|
#### `pinmap_generator.py` — PinMAP 单元格数据构建
|
||||||
|
|
||||||
|
```python
|
||||||
|
def generate_pinmap(entries, rows, cols, package_info,
|
||||||
|
template_style=None, output_path=None):
|
||||||
|
# 1. 计算布局 → layout (dict[str, EdgePins])
|
||||||
|
# 2. 先写入 PinName 单元格
|
||||||
|
# 3. 再写入序号单元格(后可覆盖同名单元格)
|
||||||
|
# 4. 写入文件(模板样式或默认样式)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `template_reader.py` — 模板样式提取
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 关键能力:
|
||||||
|
# - 解析 xl/styles.xml → fonts, fills, borders, cellXfs
|
||||||
|
# - 解析 sheet1.xml → column_widths, row_heights
|
||||||
|
# - 优雅降级:模板不存在/解析失败 → 返回 None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TemplateStyle:
|
||||||
|
fonts: list[FontStyle]
|
||||||
|
borders: list[BorderStyle]
|
||||||
|
fills: list[FillStyle]
|
||||||
|
cell_xfs: list[dict] # xf index → {fontId, borderId, fillId, alignment}
|
||||||
|
column_widths: dict[int, float]
|
||||||
|
row_heights: dict[int, float]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `xlsx_writer.py` — XLSX 输出(含样式)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# StyledXLSXWriter — 生成含 styles.xml 的 xlsx
|
||||||
|
# _styles_xml(): 读取模板字体/填充/边框 → 构建 styles.xml
|
||||||
|
# 当前实现:使用硬编码样式(模板字体仅作参考,主体样式内置)
|
||||||
|
# _sheet_xml(): 应用列宽/行高 + 单元格样式索引
|
||||||
|
# style_idx: 0=default, 1=centered+border, 2=bold(A1), 3=fill
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键观察**:`StyledXLSXWriter._styles_xml()` 目前是**硬编码样式**(内置字体、边框定义),仅从模板读取字体名称/大小作为参考。列宽和行高从模板的**实际行列**读取,但会按输出数据的实际行列扩展。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 逐项修改方案
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.1 F012: 修复 PinMAP 生成中上/下边 PinName 位置
|
||||||
|
|
||||||
|
#### 3.1.1 需求分析
|
||||||
|
|
||||||
|
**F012 描述原文**:
|
||||||
|
> 修复 PinMAP 生成中上/下边 PinName 位置
|
||||||
|
> - 当前:下边 Name 在 min_row+1,上边 Name 在 max_row-1
|
||||||
|
> - 应改为:下边 Name 在 max_row-1,上边 Name 在 min_row+1
|
||||||
|
|
||||||
|
**4×4 参考示例**:
|
||||||
|
```
|
||||||
|
A4:1, A5:2, B4:Pin1, B5:Pin2 (上边)
|
||||||
|
C7:3, D7:4, C6:Pin3, D6:Pin4 (右边)
|
||||||
|
F5:5, F4:6, E5:Pin5, E4:Pin6 (下边)
|
||||||
|
D2:7, C2:8, D3:Pin7, C3:Pin8 (左边)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.1.2 代码现状追踪
|
||||||
|
|
||||||
|
**当前 `get_name_cell` 实现**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_name_cell(num_cell, edge_name):
|
||||||
|
r, c = num_cell
|
||||||
|
if edge_name == "left": return (r, c+1) # Name 在序号右侧
|
||||||
|
elif edge_name == "bottom": return (r-1, c) # Name 在序号上方 (max_row-1)
|
||||||
|
elif edge_name == "right": return (r, c-1) # Name 在序号左侧
|
||||||
|
elif edge_name == "top": return (r+1, c) # Name 在序号下方 (min_row+1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**当前 `calculate_layout` 单元格坐标**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 下边:序号在 (rows, c),即 max_row
|
||||||
|
# → get_name_cell 返回 (rows-1, c) = max_row-1 ← 上方
|
||||||
|
#
|
||||||
|
# 上边:序号在 (1, c),即 min_row
|
||||||
|
# → get_name_cell 返回 (2, c) = min_row+1 ← 下方
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.1.3 关键发现:代码可能已经正确
|
||||||
|
|
||||||
|
将 F012 的"应改为"目标与当前代码对比:
|
||||||
|
|
||||||
|
| 边 | F012"应改为"目标 | 当前代码实际位置 | 是否一致 |
|
||||||
|
|----|-----------------|-----------------|---------|
|
||||||
|
| 下边 (bottom) | max_row-1 | `r-1` = rows-1 = max_row-1 | ✅ 一致 |
|
||||||
|
| 上边 (top) | min_row+1 | `r+1` = 1+1 = min_row+1 | ✅ 一致 |
|
||||||
|
|
||||||
|
**`test_pinmap.py` 中的 4×4 测试数据验证**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Test data (0-based):
|
||||||
|
# bottom edge: numbers at (6,2)=3, (6,3)=4; names at (5,2)=Pin3, (5,3)=Pin4
|
||||||
|
# → max_row=6, names at row 5 = max_row-1 ✓
|
||||||
|
# top edge: numbers at (1,3)=7, (1,2)=8; names at (2,3)=Pin7, (2,2)=Pin8
|
||||||
|
# → min_row=1, names at row 2 = min_row+1 ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
**该测试在当前代码中已通过**,表明当前 `get_name_cell` 返回值与测试数据一致。
|
||||||
|
|
||||||
|
#### 3.1.4 可能的问题场景
|
||||||
|
|
||||||
|
如果确实存在问题,以下场景需排查:
|
||||||
|
|
||||||
|
1. **PinMAP 解析方向**:`pinmap_parser.py` 解析 PinMAP 时,底边 Name 读自 `max_row-1`,顶边 Name 读自 `min_row+1`。这与生成方向一致。但如果解析时使用了错误的位置假设,则来回转换会导致 Name 错位。
|
||||||
|
|
||||||
|
2. **边名称语义混淆**:F012 描述中的 `(上边)` `(右边)` `(下边)` `(左边)` 标签可能使用了不同的约定(例如,该标签可能对应的是"Name 边的位置"而非"序号所在边")。
|
||||||
|
|
||||||
|
3. **网格区域偏移**:如果 `A1` 被保留为封装信息,网格区域从第 2 行开始(1-based → 0-based row=1),则 `min_row` 和 `max_row` 的起始值需要重新校准。
|
||||||
|
|
||||||
|
#### 3.1.5 修改方案
|
||||||
|
|
||||||
|
**方案 A:确认代码已正确,仅添加回归测试**
|
||||||
|
|
||||||
|
如果代码已正确,则:
|
||||||
|
1. 不修改 `pinmap_layout.py`
|
||||||
|
2. 在 `test_pinmap.py` 中增加显式的上/下边 Name 位置验证测试
|
||||||
|
3. 更新测试覆盖率
|
||||||
|
|
||||||
|
**方案 B:按 F012 描述修改(如果确定存在 Bug)**
|
||||||
|
|
||||||
|
如果确实需要交换,修改 `get_name_cell`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 修改前:
|
||||||
|
elif edge_name == "bottom": return (r-1, c) # Name 在序号上方
|
||||||
|
elif edge_name == "top": return (r+1, c) # Name 在序号下方
|
||||||
|
|
||||||
|
# 修改后(交换 bottom/top 的 Name 位置):
|
||||||
|
elif edge_name == "bottom": return (r+1, c) # Name 移到序号下方
|
||||||
|
elif edge_name == "top": return (r-1, c) # Name 移到序号上方
|
||||||
|
```
|
||||||
|
|
||||||
|
**但此修改会导致现有 `test_4x4_parse` 测试失败**,因为测试数据中 bottom Name 在 `max_row-1`。
|
||||||
|
|
||||||
|
**方案 C:仅修改 `pinmap_parser.py` 的 Name 读取位置**
|
||||||
|
|
||||||
|
如果问题是解析方向(MAP→List)使用的位置不正确:
|
||||||
|
修改 `pinmap_parser.py` 中 bottom/top 的 Name 查找行号。
|
||||||
|
|
||||||
|
#### 3.1.6 建议
|
||||||
|
|
||||||
|
**强烈建议在实施前与需求方确认具体的 Bug 表现**。基于代码分析:
|
||||||
|
|
||||||
|
| 事实 | 结论 |
|
||||||
|
|------|------|
|
||||||
|
| `get_name_cell` 中 bottom Name 在 max_row-1 | 符合 F012 的"应改为" |
|
||||||
|
| `get_name_cell` 中 top Name 在 min_row+1 | 符合 F012 的"应改为" |
|
||||||
|
| 4×4 测试已通过 | 代码内部一致 |
|
||||||
|
| `pinmap_parser.py` 使用相同约定 | 读写一致 |
|
||||||
|
| F012 描述中的"当前"与代码不符 | 可能存在描述误差 |
|
||||||
|
|
||||||
|
**实施步骤**(按优先级):
|
||||||
|
|
||||||
|
1. **先确认问题**:使用实际的 4×4 PinMAP 输入文件,运行完整的 PinList→PinMAP 转换,检查输出
|
||||||
|
2. **定位差异**:对比输出与预期,定位是 `get_name_cell` 还是 `pinmap_parser` 的差异
|
||||||
|
3. **修改代码**:根据确认结果修改对应的函数
|
||||||
|
4. **更新测试**:同步更新 `test_pinmap.py` 中的测试数据和断言
|
||||||
|
|
||||||
|
**影响范围**:
|
||||||
|
|
||||||
|
| 文件 | 修改内容 | 可能性 |
|
||||||
|
|------|---------|--------|
|
||||||
|
| `pinmap_layout.py` | 修改 `get_name_cell` 的 bottom/top 逻辑 | 低(可能需要确认) |
|
||||||
|
| `test_pinmap.py` | 更新测试数据/断言 | 中 |
|
||||||
|
| `pinmap_parser.py` | 修改 Name 读取行号 | 低 |
|
||||||
|
|
||||||
|
**风险评估**:
|
||||||
|
|
||||||
|
| 风险 | 影响 | 概率 | 缓解措施 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| 修改后破坏往返转换一致性 | **高** | 中 | 运行完整的 MAP→List→MAP 往返测试 |
|
||||||
|
| 修改后破坏现有用户输出 | 中 | 低 | 确认用户实际使用场景 |
|
||||||
|
| 与需求方沟通不足导致反复修改 | 中 | 中 | **先确认再改** |
|
||||||
|
|
||||||
|
**工作量**:0.5–1 小时(含需求确认)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 F009: MAP→List 使用 balllist 模板
|
||||||
|
|
||||||
|
#### 3.2.1 需求
|
||||||
|
|
||||||
|
> PinMAP→PinList 转换方向查找并使用 `BallList-Template.xlsx`,不再共用 PinMAP 模板
|
||||||
|
|
||||||
|
**变更前**:`run_map_to_list()` 使用 `_find_template_path()` → `PinMAP-Template.xlsx`
|
||||||
|
**变更后**:`run_map_to_list()` 查找并使用 `BallList-Template.xlsx`
|
||||||
|
|
||||||
|
#### 3.2.2 影响范围
|
||||||
|
|
||||||
|
仅在 `main.py` 中修改模板查找逻辑,核心转换代码不受影响。
|
||||||
|
|
||||||
|
**涉及文件**:
|
||||||
|
|
||||||
|
| 文件 | 修改级别 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| `main.py` | ✏️ 修改 | 新增 `_find_balllist_template_path()` 函数;修改 `run_map_to_list()` 中的模板查找调用 |
|
||||||
|
|
||||||
|
**不涉及**:`pinmap_parser.py`, `pinlist_generator.py`, `xlsx_writer.py`, `template_reader.py`(这些模块只接收 `TemplateStyle` 对象,不关心模板文件名)
|
||||||
|
|
||||||
|
#### 3.2.3 具体修改方案
|
||||||
|
|
||||||
|
**3.2.3.1 新增 `_find_balllist_template_path()` 函数**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _find_balllist_template_path() -> str | None:
|
||||||
|
"""查找根目录下的 BallList-Template.xlsx。
|
||||||
|
|
||||||
|
搜索顺序:
|
||||||
|
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")
|
||||||
|
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "BallList-Template.xlsx")
|
||||||
|
if os.path.exists(cwd_template):
|
||||||
|
return cwd_template
|
||||||
|
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
**3.2.3.2 修改 `run_map_to_list()` 中的模板查找**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 修改前:
|
||||||
|
template_path = _find_template_path()
|
||||||
|
template_style = None
|
||||||
|
if template_path:
|
||||||
|
template_style = read_template_styles(template_path)
|
||||||
|
|
||||||
|
# 修改后:
|
||||||
|
template_path = _find_balllist_template_path()
|
||||||
|
template_style = None
|
||||||
|
if template_path:
|
||||||
|
template_style = read_template_styles(template_path)
|
||||||
|
if template_style:
|
||||||
|
print(f"[INFO] 已加载 BallList 模板样式: {template_path}")
|
||||||
|
else:
|
||||||
|
print("[WARN] BallList 模板文件存在但解析失败,使用默认样式")
|
||||||
|
else:
|
||||||
|
print("[INFO] 未检测到 BallList-Template.xlsx,使用默认样式")
|
||||||
|
```
|
||||||
|
|
||||||
|
**3.2.3.3 输出路径保持不变**:
|
||||||
|
|
||||||
|
PinList 输出路径仍为 `{input_base}_PinList.xlsx`,不受模板变更影响。
|
||||||
|
|
||||||
|
#### 3.2.4 风险评估
|
||||||
|
|
||||||
|
| 风险 | 影响 | 概率 | 缓解措施 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| 用户未放置 BallList-Template.xlsx | 低 | 高 | 模板不存在 → 优雅降级使用默认样式(已实现) |
|
||||||
|
| 新旧模板共存产生混淆 | 低 | 低 | 日志明确输出使用的模板文件名 |
|
||||||
|
| BallList-Template.xlsx 解析失败 | 低 | 低 | template_reader 已有 try-except 降级 |
|
||||||
|
|
||||||
|
**工作量**:15 分钟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 F010: List→MAP 使用 ballmap 模板
|
||||||
|
|
||||||
|
#### 3.3.1 需求
|
||||||
|
|
||||||
|
> PinList→PinMAP 转换方向查找并使用 `BallMAP-Template.xlsx`,不再共用 PinMAP 模板
|
||||||
|
|
||||||
|
**变更前**:`run_list_to_map()` 使用 `_find_template_path()` → `PinMAP-Template.xlsx`
|
||||||
|
**变更后**:`run_list_to_map()` 查找并使用 `BallMAP-Template.xlsx`
|
||||||
|
|
||||||
|
#### 3.3.2 影响范围
|
||||||
|
|
||||||
|
仅在 `main.py` 中修改模板查找逻辑。
|
||||||
|
|
||||||
|
**涉及文件**:
|
||||||
|
|
||||||
|
| 文件 | 修改级别 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| `main.py` | ✏️ 修改 | 新增 `_find_ballmap_template_path()` 函数;修改 `run_list_to_map()` 中的模板查找调用 |
|
||||||
|
|
||||||
|
#### 3.3.3 具体修改方案
|
||||||
|
|
||||||
|
**3.3.3.1 新增 `_find_ballmap_template_path()` 函数**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _find_ballmap_template_path() -> str | None:
|
||||||
|
"""查找根目录下的 BallMAP-Template.xlsx。
|
||||||
|
|
||||||
|
搜索顺序:
|
||||||
|
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")
|
||||||
|
|
||||||
|
if os.path.exists(template_path):
|
||||||
|
return template_path
|
||||||
|
|
||||||
|
cwd_template = os.path.join(os.getcwd(), "BallMAP-Template.xlsx")
|
||||||
|
if os.path.exists(cwd_template):
|
||||||
|
return cwd_template
|
||||||
|
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
**3.3.3.2 修改 `run_list_to_map()` 中的模板查找**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 修改前:
|
||||||
|
template_path = _find_template_path()
|
||||||
|
template_style = None
|
||||||
|
if template_path:
|
||||||
|
template_style = read_template_styles(template_path)
|
||||||
|
...
|
||||||
|
|
||||||
|
# 修改后:
|
||||||
|
template_path = _find_ballmap_template_path()
|
||||||
|
template_style = None
|
||||||
|
if template_path:
|
||||||
|
template_style = read_template_styles(template_path)
|
||||||
|
if template_style:
|
||||||
|
print(f"[INFO] 已加载 BallMAP 模板样式: {template_path}")
|
||||||
|
else:
|
||||||
|
print("[WARN] BallMAP 模板文件存在但解析失败,使用默认样式")
|
||||||
|
else:
|
||||||
|
print("[INFO] 未检测到 BallMAP-Template.xlsx,使用默认样式")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3.4 风险评估
|
||||||
|
|
||||||
|
| 风险 | 影响 | 概率 | 缓解措施 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| 用户未放置 BallMAP-Template.xlsx | 低 | 高 | 优雅降级使用默认样式 |
|
||||||
|
| 新旧模板共存产生混淆 | 低 | 低 | 日志明确输出使用的模板文件名 |
|
||||||
|
|
||||||
|
**工作量**:15 分钟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 F011: 模板格式提取式应用
|
||||||
|
|
||||||
|
#### 3.4.1 需求
|
||||||
|
|
||||||
|
> 从模板仅提取格式信息(字体、边框、对齐、列宽、行高),输出文件行列数由实际 Pin 数量决定,不复制模板行列结构。
|
||||||
|
|
||||||
|
**依赖**:F009(BallList-Template.xlsx)+ F010(BallMAP-Template.xlsx)
|
||||||
|
|
||||||
|
#### 3.4.2 当前实现分析
|
||||||
|
|
||||||
|
当前模板应用于 `StyledXLSXWriter`(`xlsx_writer.py`),其行为:
|
||||||
|
|
||||||
|
**`_sheet_xml()` 中的列宽/行高处理**:
|
||||||
|
```python
|
||||||
|
# 当前逻辑:
|
||||||
|
# 1. 遍历模板 style.column_widths,按最大 col 扩展
|
||||||
|
# 2. 对于模板有定义的列使用模板宽度,否则默认 8.0
|
||||||
|
# 3. 遍历 style.row_heights,有定义的行使用模板行高
|
||||||
|
col_widths_xml = ''
|
||||||
|
if self._style and self._style.column_widths:
|
||||||
|
max_width_col = max(self._style.column_widths.keys())
|
||||||
|
max_width_col = max(max_width_col, max_col)
|
||||||
|
for c in range(max_width_col + 1):
|
||||||
|
width = self._style.column_widths.get(c, 8.0)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**`_styles_xml()` 中的样式处理**:
|
||||||
|
```python
|
||||||
|
# 当前逻辑:硬编码 4 种 xf 样式
|
||||||
|
# - xf 0: default
|
||||||
|
# - xf 1: centered + thin border(pin cells)
|
||||||
|
# - xf 2: bold + centered(A1)
|
||||||
|
# - xf 3: centered + border + light fill(header)
|
||||||
|
#
|
||||||
|
# 仅从模板提取:
|
||||||
|
# - font[0] 的 name/size/color(用于 xf 0, 1, 3)
|
||||||
|
# - font[0] 被复制为 font[1](bold 版,用于 xf 2 = A1)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.4.3 关键分析:当前实现是否已满足 F011 要求
|
||||||
|
|
||||||
|
| F011 要求 | 当前实现 | 是否满足 |
|
||||||
|
|-----------|---------|---------|
|
||||||
|
| 仅提取格式信息 | ✅ 模板仅用于样式 | ✅ 已满足 |
|
||||||
|
| 字体 | ✅ 从模板 font[0] 提取 | ✅ 已满足 |
|
||||||
|
| 边框 | ❌ 硬编码 thin border | ⚠️ 部分满足 |
|
||||||
|
| 对齐 | ✅ 硬编码 center/center | ⚠️ 部分满足 |
|
||||||
|
| 列宽 | ✅ 从模板提取 | ✅ 已满足 |
|
||||||
|
| 行高 | ✅ 从模板提取(有则不覆盖) | ✅ 已满足 |
|
||||||
|
| 输出行列由实际 Pin 决定 | ✅ 不复制模板行列结构 | ✅ 已满足 |
|
||||||
|
| 不复制模板行列结构 | ✅ dim 由数据决定 | ✅ 已满足 |
|
||||||
|
|
||||||
|
**核心差距**:当前 `_styles_xml()` 中边框和对齐是硬编码的,未从模板的 `cellXfs` 中提取。F011 要求**完全**从模板提取格式。
|
||||||
|
|
||||||
|
#### 3.4.4 修改方案
|
||||||
|
|
||||||
|
**目标**:将 `_styles_xml()` 改造为从模板的 `cellXfs`、`fonts`、`borders`、`fills` 中提取并原样输出样式,而非硬编码。
|
||||||
|
|
||||||
|
**涉及文件**:
|
||||||
|
|
||||||
|
| 文件 | 修改级别 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| `template_reader.py` | ✏️ 修改 | 增强样式提取能力:提取 cellXfs 的完整信息(含 numFmtId, xfId 等) |
|
||||||
|
| `xlsx_writer.py` | ✏️ 修改 | 重写 `_styles_xml()` 使用模板原始样式定义 |
|
||||||
|
|
||||||
|
**3.4.4.1 增强 `template_reader.py`**:
|
||||||
|
|
||||||
|
当前 `TemplateReader._parse_styles_xml()` 已能提取:
|
||||||
|
- ✅ fonts(FontStyle: name, size, bold, italic, color)
|
||||||
|
- ✅ fills(FillStyle: pattern_type, fg_color)
|
||||||
|
- ✅ borders(BorderStyle: top, bottom, left, right, color)
|
||||||
|
- ✅ cell_xfs(list[dict]: numFmtId, fontId, fillId, borderId, alignment)
|
||||||
|
|
||||||
|
**需要增加**:
|
||||||
|
- `cell_xfs` 中的 `xfId` 属性(用于样式继承)
|
||||||
|
- `cellXfs` 的 `count` 属性
|
||||||
|
|
||||||
|
当前已提取的信息足以满足 F011。**不需要大幅修改** `template_reader.py`。
|
||||||
|
|
||||||
|
**3.4.4.2 重写 `xlsx_writer.py` 中的 `_styles_xml()`**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _styles_xml(self) -> str:
|
||||||
|
"""Build xl/styles.xml from template styles or defaults."""
|
||||||
|
s = self._style
|
||||||
|
|
||||||
|
# ── Fonts ────────────────────────────────────────────────
|
||||||
|
if s and s.fonts:
|
||||||
|
fonts_xml = self._build_fonts_xml(s.fonts)
|
||||||
|
else:
|
||||||
|
fonts_xml = self._default_fonts_xml()
|
||||||
|
|
||||||
|
# ── Fills ────────────────────────────────────────────────
|
||||||
|
if s and s.fills:
|
||||||
|
fills_xml = self._build_fills_xml(s.fills)
|
||||||
|
else:
|
||||||
|
fills_xml = self._default_fills_xml()
|
||||||
|
|
||||||
|
# ── Borders ──────────────────────────────────────────────
|
||||||
|
if s and s.borders:
|
||||||
|
borders_xml = self._build_borders_xml(s.borders)
|
||||||
|
else:
|
||||||
|
borders_xml = self._default_borders_xml()
|
||||||
|
|
||||||
|
# ── Cell XFs ─────────────────────────────────────────────
|
||||||
|
if s and s.cell_xfs:
|
||||||
|
cell_xfs_xml = self._build_cell_xfs_xml(s.cell_xfs)
|
||||||
|
else:
|
||||||
|
cell_xfs_xml = self._default_cell_xfs_xml()
|
||||||
|
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
|
||||||
|
'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">\n'
|
||||||
|
+ fonts_xml + '\n'
|
||||||
|
+ fills_xml + '\n'
|
||||||
|
+ borders_xml + '\n'
|
||||||
|
+ cell_xfs_xml + '\n'
|
||||||
|
+ '</styleSheet>'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键设计决策**:
|
||||||
|
|
||||||
|
| 决策点 | 方案 | 理由 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 是否完全复制模板 cellXfs | ✅ 是 | 模板定义什么样式就用什么样式 |
|
||||||
|
| 是否需要额外样式(如 A1 bold) | ⚠️ 在模板中应有对应的 xf | 鼓励用户在模板中定义 A1 的样式 |
|
||||||
|
| 无模板时的默认样式 | 保持现有硬编码 | 向后兼容 |
|
||||||
|
| 列宽/行高处理 | 保持现有逻辑 | 已满足 F011 |
|
||||||
|
|
||||||
|
**3.4.4.3 cellXfs 映射策略**:
|
||||||
|
|
||||||
|
模板的 cellXfs 索引需要映射到输出单元格:
|
||||||
|
- 模板中可能有多种 xf(如 xf 0=default, xf 1=边框, xf 2=粗体...)
|
||||||
|
- 输出时需决定每个单元格使用哪个 xf
|
||||||
|
|
||||||
|
**推荐策略**:
|
||||||
|
1. 分析模板 cellXfs,找到最合适的 "pin cell" 样式(通常是带边框+居中的 xf)
|
||||||
|
2. A1 使用模板中带 bold 的 xf(如果存在),否则用 pin cell 样式
|
||||||
|
3. 普通 pin cell 使用模板中的 "pin cell" xf
|
||||||
|
4. 如果模板只有 1 个 xf(default),使用默认样式作为补充
|
||||||
|
|
||||||
|
**简化方案**(推荐首版):
|
||||||
|
- 保留现有的 4 个样式槽位(xf 0~3)
|
||||||
|
- 但从模板读取实际的字体、边框、填充定义填充到对应槽位
|
||||||
|
- 而非硬编码 "thin" 边框和 "center" 对齐
|
||||||
|
|
||||||
|
**实现优先级**:
|
||||||
|
1. **字体信息**:已从模板读取 ✅
|
||||||
|
2. **边框样式**:从模板 borders 读取 → 替换硬编码的 "thin"
|
||||||
|
3. **填充样式**:从模板 fills 读取 → 替换硬编码的 "FFF0F0F0"
|
||||||
|
4. **对齐方式**:从模板 alignment 读取 → 替换硬编码的 "center"
|
||||||
|
5. **列宽/行高**:已从模板读取 ✅
|
||||||
|
|
||||||
|
#### 3.4.5 风险评估
|
||||||
|
|
||||||
|
| 风险 | 影响 | 概率 | 缓解措施 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| 模板 cellXfs 结构与输出不匹配 | 中 | 中 | 提供合理的 fallback(至少 xf 0 和 xf 1) |
|
||||||
|
| 模板无边框定义导致输出无网格线 | 中 | 低 | 模板无边框时保留默认 thin border |
|
||||||
|
| 样式重写引入 OOXML 兼容性问题 | 低 | 低 | 使用 Python 标准库 XML 构建,避免硬编码 namespace 错误 |
|
||||||
|
| 已有用户无模板场景不受影响 | 低 | - | 无模板时完全回退到现有硬编码样式 |
|
||||||
|
|
||||||
|
**工作量**:2–3 小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 模板文件命名规范建议
|
||||||
|
|
||||||
|
### 4.1 v1.5.0 新模板命名
|
||||||
|
|
||||||
|
| 用途 | 文件名 | 位置 | 格式 |
|
||||||
|
|------|--------|------|------|
|
||||||
|
| MAP→List 输出样式 | `BallList-Template.xlsx` | 项目根目录(与 run.bat 同级) | `.xlsx` |
|
||||||
|
| List→MAP 输出样式 | `BallMAP-Template.xlsx` | 项目根目录(与 run.bat 同级) | `.xlsx` |
|
||||||
|
|
||||||
|
### 4.2 向后兼容
|
||||||
|
|
||||||
|
| 旧文件名 | 状态 | 说明 |
|
||||||
|
|---------|------|------|
|
||||||
|
| `PinMAP-Template.xlsx` | ⚠️ 废弃(不再自动查找) | v1.5.0 后不再被代码引用 |
|
||||||
|
|
||||||
|
### 4.3 命名规范说明
|
||||||
|
|
||||||
|
```
|
||||||
|
模板文件名格式:
|
||||||
|
{输出格式简称}-Template.xlsx
|
||||||
|
|
||||||
|
其中:
|
||||||
|
- BallList → PinMAP→PinList 转换的输出格式(PinList)
|
||||||
|
- BallMAP → PinList→PinMAP 转换的输出格式(PinMAP)
|
||||||
|
|
||||||
|
命名原则:
|
||||||
|
1. 以输出格式命名,而非输入格式
|
||||||
|
2. 使用 "Ball" 前缀避免与 "Pin" 名混淆
|
||||||
|
3. 保持 PascalCase 风格
|
||||||
|
4. 使用 "-Template" 后缀明确表示模板文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 搜索路径优先级
|
||||||
|
|
||||||
|
```
|
||||||
|
1. {project_root}/BallList-Template.xlsx (与 run.bat 同级)
|
||||||
|
2. {cwd}/BallList-Template.xlsx (当前工作目录)
|
||||||
|
3. 无 → 使用默认样式(硬编码 Calibri 11pt + thin border + center align)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 修改影响矩阵
|
||||||
|
|
||||||
|
| 文件 | F012 | F009 | F010 | F011 | 总改动量 | 备注 |
|
||||||
|
|------|------|------|------|------|---------|------|
|
||||||
|
| `main.py` | — | ✏️ 模板查找 | ✏️ 模板查找 | — | ~30行 | 新增2个函数+修改2处调用 |
|
||||||
|
| `pinmap_layout.py` | ✏️ 可能 | — | — | — | ~5行 | 仅在确认问题后修改 |
|
||||||
|
| `template_reader.py` | — | — | — | ✏️ 增强 | ~20行 | 增加 xfId 提取 |
|
||||||
|
| `xlsx_writer.py` | — | — | — | ✏️ 重写 | ~100行 | 重写 _styles_xml() |
|
||||||
|
| `test_pinmap.py` | ✏️ 更新 | — | — | — | ~10行 | 增加 F012 回归测试 |
|
||||||
|
| **合计** | **2 文件** | **1 文件** | **1 文件** | **2 文件** | **4-5 文件** | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 优先级排序
|
||||||
|
|
||||||
|
| 优先级 | 编号 | 原因 | 推荐执行顺序 |
|
||||||
|
|--------|------|------|------------|
|
||||||
|
| **P0** | F012 | 核心布局 Bug(需先确认),影响所有 List→MAP 转换 | 第1(先确认,再决定是否修改) |
|
||||||
|
| **P1-1** | F009 | MAP→List 模板分离,独立于其他改动 | 第2(可与 F010 并行) |
|
||||||
|
| **P1-2** | F010 | List→MAP 模板分离,独立于其他改动 | 第2(可与 F009 并行) |
|
||||||
|
| **P1-3** | F011 | 依赖 F009+F010 的模板文件存在后才能测试 | 第3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 测试要点
|
||||||
|
|
||||||
|
### 7.1 F012 测试(P0)
|
||||||
|
|
||||||
|
#### 核心验证
|
||||||
|
|
||||||
|
| 测试项 | 输入 | 预期 | 方法 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| 4×4 PinMAP 往返一致性 | PinList(rows=4, cols=4, 8 Pins) → PinMAP → PinList | 往返后 PinList 数据不变 | 自动化 |
|
||||||
|
| 下边 Name 位置 | rows=4, cols=4 的 PinMAP 输出 | 下边 PinName 在序号行上方一行(max_row-1) | 检查输出 xlsx |
|
||||||
|
| 上边 Name 位置 | rows=4, cols=4 的 PinMAP 输出 | 上边 PinName 在序号行下方一行(min_row+1) | 检查输出 xlsx |
|
||||||
|
| 与 parser 一致性 | 生成的 PinMAP 再用 parser 解析 | 解析出的 Pin 数据和原始一致 | 自动化 |
|
||||||
|
| 非方形网格 | rows=3, cols=5 的 PinMAP | 上/下边 Name 位置正确 | 检查输出 |
|
||||||
|
|
||||||
|
#### 边界条件
|
||||||
|
|
||||||
|
| 测试项 | 输入 | 预期 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 最小网格 2×2 | PinList(rows=2, cols=2, 8 Pins) | 四条边各 2 Pin,角点正确 |
|
||||||
|
| 大网格 15×15 | PinList(rows=15, cols=15, 60 Pins) | 60 Pin 全部正确分配到四条边 |
|
||||||
|
| max_row-1 与 min_row+1 重合 | rows=2(min_row=1, max_row=2 → max_row-1=1=min_row) | Name 不与序号重叠 |
|
||||||
|
|
||||||
|
#### 现有测试回归
|
||||||
|
|
||||||
|
- `test_4x4_parse()` — 确认不受影响或同步更新
|
||||||
|
- `test_4x4_validate()` — 确认不受影响
|
||||||
|
- `test_12pin_square()` — 确认不受影响
|
||||||
|
|
||||||
|
### 7.2 F009 测试(P1)
|
||||||
|
|
||||||
|
| 测试项 | 方法 | 预期 |
|
||||||
|
|--------|------|------|
|
||||||
|
| BallList-Template.xlsx 存在时加载 | 放置模板 → MAP→List 转换 | 日志显示"已加载 BallList 模板样式" |
|
||||||
|
| BallList-Template.xlsx 不存在时降级 | 删除模板 → MAP→List 转换 | 日志显示"未检测到 BallList-Template.xlsx",输出使用默认样式 |
|
||||||
|
| 不加载 PinMAP-Template.xlsx | 仅放置 PinMAP-Template.xlsx → MAP→List 转换 | 不加载旧模板,使用默认样式 |
|
||||||
|
| 模板样式应用到 PinList 输出 | 放置带自定义字体/边框的模板 → 转换 | 输出 PinList 使用模板字体和边框 |
|
||||||
|
|
||||||
|
### 7.3 F010 测试(P1)
|
||||||
|
|
||||||
|
| 测试项 | 方法 | 预期 |
|
||||||
|
|--------|------|------|
|
||||||
|
| BallMAP-Template.xlsx 存在时加载 | 放置模板 → List→MAP 转换 | 日志显示"已加载 BallMAP 模板样式" |
|
||||||
|
| BallMAP-Template.xlsx 不存在时降级 | 删除模板 → List→MAP 转换 | 日志显示"未检测到 BallMAP-Template.xlsx",输出使用默认样式 |
|
||||||
|
| 不加载 PinMAP-Template.xlsx | 仅放置 PinMAP-Template.xlsx → List→MAP 转换 | 不加载旧模板,使用默认样式 |
|
||||||
|
|
||||||
|
### 7.4 F011 测试(P1)
|
||||||
|
|
||||||
|
| 测试项 | 方法 | 预期 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 模板字体正确应用 | 模板字体=微软雅黑 12pt → 输出 | 输出文件字体为微软雅黑 12pt |
|
||||||
|
| 模板边框正确应用 | 模板边框=medium → 输出 | 输出文件边框为 medium 而非 thin |
|
||||||
|
| 模板填充正确应用 | 模板背景色=黄色 → 输出 | 输出对应单元格有黄色背景 |
|
||||||
|
| 模板对齐正确应用 | 模板左对齐 → 输出 | 输出文件单元格左对齐 |
|
||||||
|
| 列宽与模板一致 | 模板 A 列宽=20 → 输出 | 输出对应列宽=20 |
|
||||||
|
| 行高与模板一致 | 模板行高=25 → 输出 | 输出对应行高=25 |
|
||||||
|
| 输出行列由实际 Pin 决定 | rows=5, cols=5 → 输出 | 输出为 5×5 网格,不含模板的额外行列 |
|
||||||
|
| 无模板时使用默认样式 | 无模板文件 → 输出 | 使用 Calibri 11pt + thin border + center align |
|
||||||
|
|
||||||
|
### 7.5 集成测试
|
||||||
|
|
||||||
|
| 测试项 | 输入 | 预期 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 往返转换 (MAP→List→MAP) | sample_4x4.xlsx → PinList → PinMAP | 与原始 PinMAP 一致 |
|
||||||
|
| 往返转换 (List→MAP→List) | PinList 文件 → PinMAP → PinList | 与原始 PinList 一致 |
|
||||||
|
| 两个方向使用不同模板 | BallList-Template 和 BallMAP-Template 同时存在 | MAP→List 用 BallList,List→MAP 用 BallMAP |
|
||||||
|
| 一个大模板一个小模板 | BallList 字体=12pt, BallMAP 字体=10pt | 各方向使用各自的字体 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 开发顺序建议
|
||||||
|
|
||||||
|
```
|
||||||
|
阶段 1: F012 需求确认(30 分钟)
|
||||||
|
├─ 用实际 PinMAP 输入文件测试当前代码的 PinName 位置
|
||||||
|
├─ 确认 Bug 具体表现
|
||||||
|
└─ 决定修改方案(A/B/C)
|
||||||
|
|
||||||
|
阶段 2: F009 + F010 模板分离(30 分钟,可并行)
|
||||||
|
├─ main.py: 新增 _find_balllist_template_path()
|
||||||
|
├─ main.py: 新增 _find_ballmap_template_path()
|
||||||
|
├─ main.py: 修改 run_map_to_list() 模板查找
|
||||||
|
├─ main.py: 修改 run_list_to_map() 模板查找
|
||||||
|
└─ 废弃 _find_template_path()(可选,保留无调用不影响)
|
||||||
|
|
||||||
|
阶段 3: F012 修复(如需要,30–60 分钟)
|
||||||
|
├─ pinmap_layout.py: 修改 get_name_cell()
|
||||||
|
├─ test_pinmap.py: 更新测试数据和断言
|
||||||
|
└─ 运行完整测试套件
|
||||||
|
|
||||||
|
阶段 4: F011 格式提取(2–3 小时)
|
||||||
|
├─ template_reader.py: 增加提取项
|
||||||
|
├─ xlsx_writer.py: 重写 _styles_xml()
|
||||||
|
├─ xlsx_writer.py: 新增 _build_*_xml() 辅助函数
|
||||||
|
└─ 模板+无模板场景测试
|
||||||
|
|
||||||
|
阶段 5: 收尾(30 分钟)
|
||||||
|
├─ 更新 CHANGELOG.md
|
||||||
|
├─ 更新 features.md 状态
|
||||||
|
├─ 更新 VERSION 文件
|
||||||
|
└─ 运行完整回归测试
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 风险评估汇总
|
||||||
|
|
||||||
|
| 风险 | 影响 | 概率 | 缓解措施 | 相关功能 |
|
||||||
|
|------|------|------|---------|---------|
|
||||||
|
| F012 需求描述与代码行为不一致,修改方向错误 | **高** | 中 | **先确认再改**,使用实际文件测试 | F012 |
|
||||||
|
| F012 修改破坏往返转换一致性 | **高** | 中 | 运行完整 MAP→List→MAP 往返测试 | F012 |
|
||||||
|
| 模板 cellXfs 结构与输出不兼容 | 中 | 中 | 提供 fallback,模板解析失败 = 默认样式 | F011 |
|
||||||
|
| 用户未放置新模板文件 | 低 | 高 | 优雅降级,日志明确提示缺失的模板名 | F009/F010 |
|
||||||
|
| 旧 PinMAP-Template.xlsx 被意外加载 | 低 | 低 | 显式删除旧模板查找调用 | F009/F010 |
|
||||||
|
| F011 样式重写引入 OOXML 兼容性问题 | 低 | 低 | 严格 XML 构建,测试 Excel/WPS 打开 | F011 |
|
||||||
|
| 两个模板同时存在造成混淆 | 低 | 低 | 日志明确标注每个方向使用的模板文件名 | F009/F010 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 工作量估算
|
||||||
|
|
||||||
|
| 阶段 | 任务 | 预估时间 | 依赖 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| 确认 | F012 需求确认(测试+分析) | 30 分钟 | 无 |
|
||||||
|
| 开发 | F009 MAP→List 模板分离 | 15 分钟 | 无 |
|
||||||
|
| 开发 | F010 List→MAP 模板分离 | 15 分钟 | 无 |
|
||||||
|
| 开发 | F012 修复(如需要) | 30–60 分钟 | 需求确认 |
|
||||||
|
| 开发 | F011 模板格式提取式应用 | 2–3 小时 | F009+F010 |
|
||||||
|
| 测试 | 单元测试更新 | 30 分钟 | 所有开发 |
|
||||||
|
| 测试 | 集成/回归测试 | 30 分钟 | 所有开发 |
|
||||||
|
| 文档 | CHANGELOG + features.md 更新 | 15 分钟 | 所有开发 |
|
||||||
|
| **总计** | | **4.5–6 小时** | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 总结
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 修改文件数 | 4–5 个(main.py, pinmap_layout.py, template_reader.py, xlsx_writer.py, test_pinmap.py) |
|
||||||
|
| 新增模板文件 | 2 个(BallList-Template.xlsx, BallMAP-Template.xlsx) |
|
||||||
|
| 影响核心模块 | 是(xlsx_writer.py 样式生成逻辑重写) |
|
||||||
|
| 技术难度 | 中(F011 样式重写需理解 OOXML styles.xml 结构) |
|
||||||
|
| 预估工作量 | 4.5–6 小时 |
|
||||||
|
| 推荐 Agent | Python 编码 Agent |
|
||||||
|
| 风险等级 | 中(F012 需先确认,F011 样式重写有复杂度) |
|
||||||
|
|
||||||
|
**关键结论**:
|
||||||
|
|
||||||
|
1. **F012(P0)**:代码当前行为可能已经正确(bottom Name 在 max_row-1,top Name 在 min_row+1 已与 F012 "应改为"目标一致)。**强烈建议在实施前用实际文件验证**,避免过度修改。如果确认无误,仅需增加回归测试。
|
||||||
|
|
||||||
|
2. **F009+F010(P1)**:改动量小(约 30 行),完全独立,可快速完成。核心是新增两个模板查找函数并替换调用点。
|
||||||
|
|
||||||
|
3. **F011(P1)**:改动量最大(约 120 行),需重写 `xlsx_writer.py` 的 `_styles_xml()` 方法。当前代码已部分满足 F011(字体、列宽、行高从模板提取),主要差距在边框和对齐的硬编码。建议采用"从模板读取实际值填充现有样式槽位"的渐进方案。
|
||||||
|
|
||||||
|
4. **向后兼容**:无模板时所有功能完全回退到现有默认样式,不影响已有用户。
|
||||||
|
|
||||||
|
5. **推荐开发顺序**:确认 F012 → 实现 F009+F010 → 修复 F012(如需要) → 实现 F011 → 测试收尾。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档结束 — 请审批后进入编码阶段*
|
||||||
|
|
||||||
|
##
|
||||||
@@ -8,6 +8,22 @@
|
|||||||
| T004 | 测试验证 v1.2 | test-qa-agent | 已完成 | 测试验证 | F001-F005 | 2026-05-23 | - |
|
| T004 | 测试验证 v1.2 | test-qa-agent | 已完成 | 测试验证 | F001-F005 | 2026-05-23 | - |
|
||||||
| T007 | BAT 脚本修复 v1.3 | bat-coding-agent | 已完成 | 编码实现 | F005 | 2026-05-31 | 2026-05-31 |
|
| T007 | BAT 脚本修复 v1.3 | bat-coding-agent | 已完成 | 编码实现 | F005 | 2026-05-31 | 2026-05-31 |
|
||||||
| T008 | Python 编码 v1.3 | python-coding-agent | 已完成 | 编码实现 | F006-F008 | 2026-05-31 | 2026-05-31 |
|
| T008 | Python 编码 v1.3 | python-coding-agent | 已完成 | 编码实现 | F006-F008 | 2026-05-31 | 2026-05-31 |
|
||||||
| T009 | 测试验证 v1.3 | test-qa-agent | 进行中 | 测试验证 | F005-F008 | 2026-05-31 | - |
|
| T009 | 测试验证 v1.3 | test-qa-agent | 已完成 | 测试验证 | F005-F008 | 2026-05-31 | 2026-06-06 |
|
||||||
| T010 | 文档生成 v1.3 | doc-gen-agent | 待分配 | 文档编写 | F005-F008 | - | - |
|
| T010 | 文档生成 v1.3 | doc-gen-agent | 已完成 | 文档编写 | F005-F008 | - | 2026-06-06 |
|
||||||
| T011 | 打包发布 v1.3 | package-release-agent | 待分配 | 打包发布 | F005-F008 | - | - |
|
| T011 | 打包发布 v1.3 | package-release-agent | 已完成 | 打包发布 | F005-F008 | 2026-05-31 | 2026-06-02 | pinmap-to-pinlist-v1.3.14.zip |
|
||||||
|
| T013 | 打包发布 v1.3.15 修复 | package-release-agent | 已完成 | 打包发布 | - | 2026-06-02 | 2026-06-02 | Release 已创建 + zip 附件已上传 |
|
||||||
|
| T014 | 架构评估 v1.5 | script-architect | 已完成 | 架构评估 | F009-F012 | 2026-06-06 | 2026-06-06 |
|
||||||
|
| T015 | 编码实现 v1.5 | python-coding-agent | 已完成 | 编码实现 | F009-F012 | 2026-06-06 | 2026-06-06 |
|
||||||
|
| T016 | 测试验证 v1.5 | test-architect/test-executor/test-reporter | 已完成 | 测试验证 | F009-F012 | 2026-06-06 | 2026-06-06 |
|
||||||
|
| T017 | 文档生成 v1.5 | doc-gen-agent | 已完成 | 文档编写 | F009-F012 | 2026-06-06 | 2026-06-06 |
|
||||||
|
| T018 | 打包发布 v1.5 | package-release-agent | 已完成 | 打包发布 | F009-F012 | 2026-06-06 | 2026-06-06 | Release 已创建 + zip 附件已上传 |
|
||||||
|
| T019 | 架构评估 v1.5.4 Bug 修复 | script-architect | 已完成 | 架构评估 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 | 方案已确认 ✅ |
|
||||||
|
| T020 | 编码实现 v1.5.4 Bug 修复 | python-coding-agent | 已完成 | 编码实现 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 |
|
||||||
|
| T021 | 测试验证 v1.5.4 | test-executor | 已完成 | 测试验证 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 |
|
||||||
|
| T022 | 文档生成 v1.5.4 | doc-gen-agent | 已完成 | 文档编写 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 |
|
||||||
|
| T023 | 打包发布 v1.5.4 | package-release-agent | 已完成 | 打包发布 | BUG-005, BUG-006 | 2026-06-09 | 2026-06-09 | Release 已创建 + zip 附件已上传 |
|
||||||
|
| T024 | 修复发布包文件结构 v1.5.4 | router-agent | 已完成 | 打包修复 | - | 2026-06-09 | 2026-06-09 | 已重新打包并上传,结构恢复为 Code/src/ 格式 |
|
||||||
|
| T025 | 架构评估 PinList→PinMAP 布局 Bug 修复 | script-architect | 已完成 | 架构评估 | BUG-005, BUG-006 | 2026-06-12 | 2026-06-12 | BUG-005/006 用户反馈 v1.5.4 修复未生效,需重新分析 |
|
||||||
|
| T026 | 编码实现 v1.5.5 Bug 修复 | python-coding-agent | 已完成 | 编码实现 | BUG-005, BUG-006 | 2026-06-12 | 2026-06-12 | 全部 37 测试通过 |
|
||||||
|
| T027 | 文档生成 v1.5.5 | doc-gen-agent | 已完成 | 文档编写 | BUG-005, BUG-006 | 2026-06-12 | 2026-06-12 | 文档已全部更新 |
|
||||||
|
| T028 | 打包发布 v1.5.5 | package-release-agent | 进行中 | 打包发布 | BUG-005, BUG-006 | 2026-06-12 | - | 创建 Release + 上传 zip |
|
||||||
|
|||||||
Reference in New Issue
Block a user