feat: VBA Automated Testing and Precise Error Reporting System
Implement a comprehensive VBA testing framework that provides precise line-by-line error reporting through code weaving technology. Core Features: - CodeWeaver: Injects line number labels and error handling into VBA code - LoggerInjector: Manages TestLogger module for capturing test results - TestRunner: Orchestrates Excel lifecycle and test execution - Command-line interface supporting single and batch testing Key Capabilities: - Captures exact line numbers where errors occur - Displays source code context for error locations - Non-invasive testing (original files unchanged) - Batch testing support with detailed summaries Components: - vba_test_runner.py: Main framework implementation - demo.xlsm: Demo Excel file with test procedures - create_demo.py: Script to generate demo files - check_vba_access.py: VBA access permission checker Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Excel temporary files
|
||||||
|
~$*
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
nul
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Test files (optional - comment out if you want to track them)
|
||||||
|
test_*.py
|
||||||
|
debug_*.py
|
||||||
|
|
||||||
|
# Demo file (optional - comment out if you want to track it)
|
||||||
|
# demo.xlsm
|
||||||
184
IMPLEMENTATION_SUMMARY.md
Normal file
184
IMPLEMENTATION_SUMMARY.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# VBA 自动化测试与精确报错系统 - 实现总结
|
||||||
|
|
||||||
|
## 项目完成状态
|
||||||
|
|
||||||
|
✅ **已完成** - 所有核心功能已实现并测试通过
|
||||||
|
|
||||||
|
## 已实现的功能
|
||||||
|
|
||||||
|
### 1. 代码编织器 (CodeWeaver)
|
||||||
|
- ✅ 解析 VBA 代码中的所有 Sub/Function 过程
|
||||||
|
- ✅ 智能注入行号标签(10, 20, 30...)
|
||||||
|
- ✅ 自动跳过声明区、注释、空行
|
||||||
|
- ✅ 注入错误处理逻辑(On Error GoTo)
|
||||||
|
- ✅ 生成 Source Map(行号到源代码的映射)
|
||||||
|
|
||||||
|
### 2. 日志模块注入器 (LoggerInjector)
|
||||||
|
- ✅ 自动注入 TestLogger 辅助模块
|
||||||
|
- ✅ 支持模块替换(删除旧模块,注入新模块)
|
||||||
|
- ✅ 记录测试成功/失败状态
|
||||||
|
- ✅ 捕获错误详情(过程名、行号、错误号、描述)
|
||||||
|
|
||||||
|
### 3. 测试执行器 (TestRunner)
|
||||||
|
- ✅ Excel 生命周期管理
|
||||||
|
- ✅ VBA 代码读取和热替换
|
||||||
|
- ✅ 宏执行和结果获取
|
||||||
|
- ✅ 不保存原始文件(非侵入式)
|
||||||
|
|
||||||
|
### 4. 命令行界面
|
||||||
|
- ✅ 支持单个或批量测试
|
||||||
|
- ✅ 清晰的测试结果输出
|
||||||
|
- ✅ 测试汇总统计
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
### 测试场景
|
||||||
|
|
||||||
|
✅ **TestSuccessfulProcedure** - 成功执行
|
||||||
|
```
|
||||||
|
[PASS] TestSuccessfulProcedure - Test Passed
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **TestErrorProcedure** - 除以零错误
|
||||||
|
```
|
||||||
|
[FAIL] TestErrorProcedure - Test Failed
|
||||||
|
Error Description: Division by zero
|
||||||
|
Error Line: 30
|
||||||
|
Source Code: result = x / y
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **TestTypeMismatch** - 类型不匹配错误
|
||||||
|
```
|
||||||
|
[FAIL] TestTypeMismatch - Test Failed
|
||||||
|
Error Description: Type mismatch
|
||||||
|
Error Line: 20
|
||||||
|
Source Code: y = x
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **TestSubscriptError** - 下标越界错误
|
||||||
|
```
|
||||||
|
[FAIL] TestSubscriptError - Test Failed
|
||||||
|
Error Description: Subscript out of range
|
||||||
|
Error Line: 40
|
||||||
|
Source Code: value = arr(5)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量测试结果
|
||||||
|
```
|
||||||
|
===== VBA Batch Test Results =====
|
||||||
|
|
||||||
|
[1/4] [FAIL] TestErrorProcedure - Test Failed
|
||||||
|
Error Description: Division by zero
|
||||||
|
Error Line: 30
|
||||||
|
Source Code: result = x / y
|
||||||
|
[2/4] [FAIL] TestTypeMismatch - Test Failed
|
||||||
|
Error Description: Type mismatch
|
||||||
|
Error Line: 20
|
||||||
|
Source Code: y = x
|
||||||
|
[3/4] [FAIL] TestSubscriptError - Test Failed
|
||||||
|
Error Description: Subscript out of range
|
||||||
|
Error Line: 40
|
||||||
|
Source Code: value = arr(5)
|
||||||
|
[4/4] [PASS] TestSuccessfulProcedure - Test Passed
|
||||||
|
|
||||||
|
===== Test Summary =====
|
||||||
|
Passed: 1/4
|
||||||
|
Failed: 3/4
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
| 文件名 | 状态 | 描述 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `vba_test_runner.py` | ✅ 完成 | 主脚本,包含所有类 |
|
||||||
|
| `demo.xlsm` | ✅ 完成 | 测试目标文件(不修改) |
|
||||||
|
| `create_demo.py` | ✅ 完成 | 创建演示文件 |
|
||||||
|
| `README.md` | ✅ 完成 | 项目文档 |
|
||||||
|
| `check_vba_access.py` | ✅ 完成 | 检查 VBA 访问权限 |
|
||||||
|
|
||||||
|
## 关键技术实现
|
||||||
|
|
||||||
|
### 行号标签机制
|
||||||
|
VBA 的 `Erl` 函数会返回最近执行的行号标签:
|
||||||
|
```vba
|
||||||
|
10 x = 10
|
||||||
|
20 y = 0
|
||||||
|
30 result = x / y ' Erl 将返回 30
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理模板
|
||||||
|
```vba
|
||||||
|
On Error GoTo Auto_Err_Handler_{proc_name}
|
||||||
|
|
||||||
|
... 原有代码 ...
|
||||||
|
|
||||||
|
Call TestLogger.LogSuccess()
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
Auto_Err_Handler_{proc_name}:
|
||||||
|
Call TestLogger.LogError("{proc_name}", Err.Number, Err.Description, Erl)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 非侵入式测试
|
||||||
|
- 使用 `wb.api.Close(False)` 不保存更改
|
||||||
|
- 原始 Excel 文件保持不变
|
||||||
|
- 代码编织只在内存中执行
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
```bash
|
||||||
|
pip install xlwings pywin32
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启用 VBA 项目访问
|
||||||
|
1. 打开 Excel
|
||||||
|
2. 文件 > 选项 > 信任中心
|
||||||
|
3. 信任中心设置 > 宏设置
|
||||||
|
4. 勾选"信任对 VBA 工程对象模型的访问"
|
||||||
|
|
||||||
|
### 3. 运行测试
|
||||||
|
```bash
|
||||||
|
# 测试单个过程
|
||||||
|
python vba_test_runner.py demo.xlsm Module1 TestSuccessfulProcedure
|
||||||
|
|
||||||
|
# 批量测试
|
||||||
|
python vba_test_runner.py demo.xlsm Module1 TestErrorProcedure TestTypeMismatch TestSubscriptError TestSuccessfulProcedure
|
||||||
|
```
|
||||||
|
|
||||||
|
## 已解决的问题
|
||||||
|
|
||||||
|
### 问题 1: VBA 模块命名限制
|
||||||
|
- **问题**: 模块名不能以下划线开头
|
||||||
|
- **解决**: 将 `_TestLogger` 改为 `TestLogger`
|
||||||
|
|
||||||
|
### 问题 2: Exit Sub 语法错误
|
||||||
|
- **问题**: 代码生成 `Sub` 而不是 `Exit Sub`
|
||||||
|
- **解决**: 修复条件判断逻辑
|
||||||
|
|
||||||
|
### 问题 3: Excel API 兼容性
|
||||||
|
- **问题**: `wb.close(SaveChanges=False)` 不支持
|
||||||
|
- **解决**: 使用 `wb.api.Close(False)`
|
||||||
|
|
||||||
|
### 问题 4: Unicode 编码
|
||||||
|
- **问题**: Windows 控制台不支持 Unicode 字符
|
||||||
|
- **解决**: 使用 ASCII 字符 `[PASS]` 和 `[FAIL]`
|
||||||
|
|
||||||
|
## 核心价值
|
||||||
|
|
||||||
|
1. **精确报错**: 从"发生意外"到"第30行:result = x / y"
|
||||||
|
2. **自动化测试**: 批量执行多个 VBA 宏
|
||||||
|
3. **非侵入式**: 不污染原始代码文件
|
||||||
|
4. **易于使用**: 简单的命令行界面
|
||||||
|
|
||||||
|
## 扩展方向
|
||||||
|
|
||||||
|
- 支持类模块和窗体模块
|
||||||
|
- 支持参数化测试
|
||||||
|
- 生成 HTML 测试报告
|
||||||
|
- 集成到 CI/CD 流程
|
||||||
|
- 支持远程 Excel 实例
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
该系统成功实现了 VBA 自动化测试与精确报错功能,通过代码编织技术解决了传统 VBA 调试的痛点。所有测试场景均已验证通过,系统稳定可用。
|
||||||
234
README.md
Normal file
234
README.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# VBA 自动化测试与精确报错系统
|
||||||
|
|
||||||
|
通过代码编织(Code Weaving)技术,实现 VBA 宏的精确到行的代码报错定位。
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
|
- **精确行号定位**: 捕获 VBA 错误的具体行号,不再只是"发生意外"
|
||||||
|
- **源代码映射**: 通过 Source Map 机制显示出错行的原始代码
|
||||||
|
- **自动化测试**: 批量执行 VBA 宏并收集结果
|
||||||
|
- **非侵入式**: 测试过程不修改原始 Excel 文件
|
||||||
|
- **详细报告**: 提供清晰的测试结果输出
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install xlwings pywin32
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 创建演示文件
|
||||||
|
|
||||||
|
首先创建一个包含测试代码的 Excel 文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python create_demo.py
|
||||||
|
```
|
||||||
|
|
||||||
|
这将创建 `demo.xlsm` 文件,包含以下测试过程:
|
||||||
|
- `TestErrorProcedure`: 除以零错误
|
||||||
|
- `TestTypeMismatch`: 类型不匹配错误
|
||||||
|
- `TestSubscriptError`: 下标越界错误
|
||||||
|
- `TestSuccessfulProcedure`: 成功执行的测试
|
||||||
|
|
||||||
|
### 2. 运行测试
|
||||||
|
|
||||||
|
测试单个过程:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python vba_test_runner.py demo.xlsm Module1 TestSuccessfulProcedure
|
||||||
|
```
|
||||||
|
|
||||||
|
批量测试多个过程:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python vba_test_runner.py demo.xlsm Module1 TestErrorProcedure TestTypeMismatch TestSubscriptError TestSuccessfulProcedure
|
||||||
|
```
|
||||||
|
|
||||||
|
## 输出示例
|
||||||
|
|
||||||
|
### 单个测试
|
||||||
|
|
||||||
|
**成功时**:
|
||||||
|
```
|
||||||
|
[PASS] TestSuccessfulProcedure - Test Passed
|
||||||
|
```
|
||||||
|
|
||||||
|
**失败时**:
|
||||||
|
```
|
||||||
|
[FAIL] TestErrorProcedure - Test Failed
|
||||||
|
Error Description: Division by zero
|
||||||
|
Error Line: 30
|
||||||
|
Source Code: result = x / y
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量测试
|
||||||
|
|
||||||
|
```
|
||||||
|
===== VBA Batch Test Results =====
|
||||||
|
|
||||||
|
[1/4] [FAIL] TestErrorProcedure - Test Failed
|
||||||
|
Error Description: Division by zero
|
||||||
|
Error Line: 30
|
||||||
|
Source Code: result = x / y
|
||||||
|
[2/4] [FAIL] TestTypeMismatch - Test Failed
|
||||||
|
Error Description: Type mismatch
|
||||||
|
Error Line: 20
|
||||||
|
Source Code: y = x
|
||||||
|
[3/4] [FAIL] TestSubscriptError - Test Failed
|
||||||
|
Error Description: Subscript out of range
|
||||||
|
Error Line: 40
|
||||||
|
Source Code: value = arr(5)
|
||||||
|
[4/4] [PASS] TestSuccessfulProcedure - Test Passed
|
||||||
|
|
||||||
|
===== Test Summary =====
|
||||||
|
Passed: 1/4
|
||||||
|
Failed: 3/4
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
### 代码编织(Code Weaving)
|
||||||
|
|
||||||
|
系统通过以下步骤实现精确报错:
|
||||||
|
|
||||||
|
1. **解析 VBA 代码**: 识别所有 Sub/Function 过程
|
||||||
|
2. **注入行号标签**: 在可执行代码前插入数字标签(10, 20, 30...)
|
||||||
|
3. **注入错误处理**: 添加 `On Error GoTo` 语句和错误处理块
|
||||||
|
4. **创建 Source Map**: 维护行号到源代码的映射
|
||||||
|
|
||||||
|
### VBA 行号机制
|
||||||
|
|
||||||
|
VBA 的 `Erl` 函数会返回最近执行的行号标签:
|
||||||
|
|
||||||
|
```vba
|
||||||
|
10 x = 10
|
||||||
|
20 y = 0
|
||||||
|
30 result = x / y ' Erl 将返回 30
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试流程
|
||||||
|
|
||||||
|
```
|
||||||
|
原始 VBA 代码
|
||||||
|
↓
|
||||||
|
代码编织器注入行号和错误处理
|
||||||
|
↓
|
||||||
|
注入 _TestLogger 辅助模块
|
||||||
|
↓
|
||||||
|
热替换目标模块代码
|
||||||
|
↓
|
||||||
|
执行 VBA 宏
|
||||||
|
↓
|
||||||
|
从 _TestLogger 读取结果
|
||||||
|
↓
|
||||||
|
格式化输出
|
||||||
|
```
|
||||||
|
|
||||||
|
## 架构设计
|
||||||
|
|
||||||
|
### 模块划分
|
||||||
|
|
||||||
|
```
|
||||||
|
vba_test_runner.py
|
||||||
|
├── CodeWeaver (代码编织器类)
|
||||||
|
│ ├── parse_procedures() - 解析 VBA 过程
|
||||||
|
│ ├── weave_procedure() - 编织单个过程
|
||||||
|
│ ├── _inject_line_numbers() - 注入行号标签
|
||||||
|
│ └── _inject_error_handler() - 注入错误处理
|
||||||
|
│
|
||||||
|
├── LoggerInjector (日志模块注入器类)
|
||||||
|
│ ├── inject_or_replace() - 注入或替换 Logger 模块
|
||||||
|
│ └── LOGGER_MODULE_CODE - Logger 模块的 VBA 代码
|
||||||
|
│
|
||||||
|
└── TestRunner (执行控制器类)
|
||||||
|
├── run_test() - 执行完整测试流程
|
||||||
|
├── _get_vba_code() - 读取 VBA 代码
|
||||||
|
├── _replace_module_code() - 热替换模块代码
|
||||||
|
├── _execute_macro() - 执行宏
|
||||||
|
└── _get_test_result() - 获取测试结果
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键技术点
|
||||||
|
|
||||||
|
### 行号标签规则
|
||||||
|
|
||||||
|
- 使用纯数字标签: `10`, `20`, `30`... (不带冒号)
|
||||||
|
- 只在可执行代码前注入
|
||||||
|
- 跳过声明区(Dim, Private 等)
|
||||||
|
- 跳过注释行和空行
|
||||||
|
- 跳过现有的 On Error 语句
|
||||||
|
|
||||||
|
### 错误处理模板
|
||||||
|
|
||||||
|
```vba
|
||||||
|
On Error GoTo Auto_Err_Handler_{proc_name}
|
||||||
|
|
||||||
|
... 原有代码 ...
|
||||||
|
|
||||||
|
Call _TestLogger.LogSuccess()
|
||||||
|
Exit Sub/Function
|
||||||
|
|
||||||
|
Auto_Err_Handler_{proc_name}:
|
||||||
|
Call _TestLogger.LogError("{proc_name}", Err.Number, Err.Description, Erl)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 热替换不保存
|
||||||
|
|
||||||
|
使用 `wb.Close(SaveChanges=False)` 确保注入的代码不会污染原始文件。
|
||||||
|
|
||||||
|
## 限制和注意事项
|
||||||
|
|
||||||
|
1. **信任访问 VBA 项目**: 需要在 Excel 信任中心启用"信任对 VBA 工程对象模型的访问"
|
||||||
|
- 路径: 文件 > 选项 > 信任中心 > 信任中心设置 > 宏设置 > 勾选"信任对 VBA 工程对象模型的访问"
|
||||||
|
|
||||||
|
2. **行号标签冲突**: 如果原始代码中已使用相同数值的行号标签,可能会产生冲突
|
||||||
|
|
||||||
|
3. **复杂过程**: 对于非常复杂的过程(包含大量 GoTo 语句),可能需要额外处理
|
||||||
|
|
||||||
|
4. **只支持标准模块**: 当前版本不支持类模块和窗体模块
|
||||||
|
|
||||||
|
## 扩展开发
|
||||||
|
|
||||||
|
### 添加新的测试过程
|
||||||
|
|
||||||
|
在 Excel 文件的 VBA 模块中添加你的测试过程:
|
||||||
|
|
||||||
|
```vba
|
||||||
|
Sub YourTestProcedure()
|
||||||
|
' 你的测试代码
|
||||||
|
End Sub
|
||||||
|
```
|
||||||
|
|
||||||
|
然后运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python vba_test_runner.py your_file.xlsm Module1 YourTestProcedure
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义 Logger 模块
|
||||||
|
|
||||||
|
修改 `LoggerInjector.LOGGER_MODULE_CODE` 可以自定义日志记录逻辑。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
**Q: 为什么测试后原始文件没有被修改?**
|
||||||
|
|
||||||
|
A: 系统使用热替换技术,在内存中修改代码,测试完成后使用 `Close(SaveChanges=False)` 不保存更改。
|
||||||
|
|
||||||
|
**Q: 如何测试类模块中的方法?**
|
||||||
|
|
||||||
|
A: 当前版本只支持标准模块。要测试类模块,可以创建一个包装的 Sub 在标准模块中调用类方法。
|
||||||
|
|
||||||
|
**Q: 可以捕获运行时警告吗?**
|
||||||
|
|
||||||
|
A: 当前版本只捕获错误。要捕获警告,需要修改 Logger 模块来处理 `InfoMessage` 事件。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request!
|
||||||
54
check_vba_access.py
Normal file
54
check_vba_access.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
检查 Excel VBA 项目访问设置
|
||||||
|
"""
|
||||||
|
|
||||||
|
import winreg
|
||||||
|
import os
|
||||||
|
|
||||||
|
def check_vba_project_access():
|
||||||
|
"""检查是否启用了对 VBA 工程对象模型的访问"""
|
||||||
|
|
||||||
|
# Excel 版本的注册表路径
|
||||||
|
excel_versions = [
|
||||||
|
r"Software\Microsoft\Office\16.0\Excel\Security",
|
||||||
|
r"Software\Microsoft\Office\15.0\Excel\Security",
|
||||||
|
r"Software\Microsoft\Office\14.0\Excel\Security",
|
||||||
|
]
|
||||||
|
|
||||||
|
print("Checking Excel VBA Project Access Settings...\n")
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for version_path in excel_versions:
|
||||||
|
try:
|
||||||
|
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, version_path)
|
||||||
|
access_value, _ = winreg.QueryValueEx(key, "AccessVBOM")
|
||||||
|
winreg.CloseKey(key)
|
||||||
|
|
||||||
|
if access_value == 1:
|
||||||
|
print(f"[OK] VBA Project Access is ENABLED in {version_path}")
|
||||||
|
found = True
|
||||||
|
else:
|
||||||
|
print(f"[WARN] VBA Project Access is DISABLED in {version_path}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to check {version_path}: {e}")
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
print("\n[INFO] VBA Project Access settings not found in registry.")
|
||||||
|
print("You may need to enable it manually in Excel.")
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("How to Enable VBA Project Access in Excel:")
|
||||||
|
print("="*60)
|
||||||
|
print("1. Open Excel")
|
||||||
|
print("2. Go to File > Options > Trust Center")
|
||||||
|
print("3. Click 'Trust Center Settings...'")
|
||||||
|
print("4. Go to 'Macro Settings'")
|
||||||
|
print("5. Check the box: 'Trust access to the VBA project object model'")
|
||||||
|
print("6. Click OK to save changes")
|
||||||
|
print("7. Restart Excel")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
check_vba_project_access()
|
||||||
83
create_demo.py
Normal file
83
create_demo.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"""
|
||||||
|
创建演示用的 Excel 文件,包含测试用的 VBA 代码
|
||||||
|
"""
|
||||||
|
|
||||||
|
import xlwings as xw
|
||||||
|
|
||||||
|
# VBA 测试代码
|
||||||
|
VBA_TEST_CODE = """Option Explicit
|
||||||
|
|
||||||
|
Sub TestErrorProcedure()
|
||||||
|
Dim x As Integer
|
||||||
|
Dim y As Integer
|
||||||
|
Dim result As Double
|
||||||
|
|
||||||
|
x = 10
|
||||||
|
y = 0
|
||||||
|
result = x / y ' 这里会触发除以零错误
|
||||||
|
|
||||||
|
Debug.Print "Result: " & result
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Sub TestTypeMismatch()
|
||||||
|
Dim x As String
|
||||||
|
Dim y As Integer
|
||||||
|
|
||||||
|
x = "Hello"
|
||||||
|
y = x ' 这里会触发类型不匹配错误
|
||||||
|
|
||||||
|
Debug.Print "Y: " & y
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Sub TestSubscriptError()
|
||||||
|
Dim arr(1 To 3) As Integer
|
||||||
|
Dim value As Integer
|
||||||
|
|
||||||
|
arr(1) = 10
|
||||||
|
arr(2) = 20
|
||||||
|
arr(3) = 30
|
||||||
|
value = arr(5) ' 这里会触发下标越界错误
|
||||||
|
|
||||||
|
Debug.Print "Value: " & value
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Sub TestSuccessfulProcedure()
|
||||||
|
Dim x As Integer
|
||||||
|
Dim y As Integer
|
||||||
|
Dim sum As Integer
|
||||||
|
|
||||||
|
x = 10
|
||||||
|
y = 20
|
||||||
|
sum = x + y
|
||||||
|
|
||||||
|
Debug.Print "Sum: " & sum
|
||||||
|
End Sub
|
||||||
|
"""
|
||||||
|
|
||||||
|
def create_demo_file():
|
||||||
|
"""创建演示用的 Excel 文件"""
|
||||||
|
app = xw.App(visible=True)
|
||||||
|
wb = app.books.add()
|
||||||
|
|
||||||
|
# 添加 VBA 模块
|
||||||
|
vba_project = wb.api.VBProject
|
||||||
|
module = vba_project.VBComponents.Add(1) # 1 = vbext_ct_StdModule
|
||||||
|
module.Name = "Module1"
|
||||||
|
module.CodeModule.AddFromString(VBA_TEST_CODE)
|
||||||
|
|
||||||
|
# 保存文件
|
||||||
|
file_path = r"D:\python\xlwings\demo.xlsm"
|
||||||
|
wb.save(file_path)
|
||||||
|
wb.close()
|
||||||
|
|
||||||
|
print(f"演示文件已创建: {file_path}")
|
||||||
|
print("\n包含的测试过程:")
|
||||||
|
print(" - TestErrorProcedure: 除以零错误")
|
||||||
|
print(" - TestTypeMismatch: 类型不匹配错误")
|
||||||
|
print(" - TestSubscriptError: 下标越界错误")
|
||||||
|
print(" - TestSuccessfulProcedure: 成功执行的测试")
|
||||||
|
|
||||||
|
app.quit()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_demo_file()
|
||||||
502
vba_test_runner.py
Normal file
502
vba_test_runner.py
Normal file
@@ -0,0 +1,502 @@
|
|||||||
|
"""
|
||||||
|
VBA 自动化测试与精确报错系统
|
||||||
|
通过代码编织技术实现 VBA 宏的精确行号报错定位
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
import xlwings as xw
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TestResult:
|
||||||
|
"""测试结果数据类"""
|
||||||
|
procedure_name: str
|
||||||
|
success: bool
|
||||||
|
error_number: int = 0
|
||||||
|
error_description: str = ""
|
||||||
|
error_line: int = 0
|
||||||
|
source_code: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class CodeWeaver:
|
||||||
|
"""代码编织器 - 为 VBA 代码注入行号标签和错误处理"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.line_counter = 10 # 行号标签从 10 开始,步长 10
|
||||||
|
|
||||||
|
def parse_procedures(self, code: str) -> Dict[str, Dict]:
|
||||||
|
"""
|
||||||
|
解析 VBA 代码中的所有 Sub/Function
|
||||||
|
|
||||||
|
返回: {
|
||||||
|
过程名: {
|
||||||
|
'start_line': int,
|
||||||
|
'end_line': int,
|
||||||
|
'type': 'Sub' or 'Function'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
procedures = {}
|
||||||
|
lines = code.split('\n')
|
||||||
|
|
||||||
|
# 正则匹配 Sub/Function 定义行
|
||||||
|
proc_pattern = re.compile(
|
||||||
|
r'^\s*(Public|Private|Friend)?\s*(Sub|Function)\s+(\w+)\s*\((.*?)\)(?:\s+As\s+(\w+))?',
|
||||||
|
re.IGNORECASE | re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
end_pattern = re.compile(r'^\s*End\s+(Sub|Function)', re.IGNORECASE)
|
||||||
|
|
||||||
|
current_proc = None
|
||||||
|
proc_start_line = 0
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
# 检查是否是过程定义
|
||||||
|
match = proc_pattern.match(line)
|
||||||
|
if match:
|
||||||
|
visibility, proc_type, proc_name = match.groups()[:3]
|
||||||
|
current_proc = proc_name
|
||||||
|
proc_start_line = i
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检查是否是过程结束
|
||||||
|
if current_proc:
|
||||||
|
end_match = end_pattern.match(line)
|
||||||
|
if end_match:
|
||||||
|
procedures[current_proc] = {
|
||||||
|
'start_line': proc_start_line,
|
||||||
|
'end_line': i,
|
||||||
|
'type': proc_type
|
||||||
|
}
|
||||||
|
current_proc = None
|
||||||
|
|
||||||
|
return procedures
|
||||||
|
|
||||||
|
def weave_procedure(self, code: str, proc_name: str) -> Tuple[str, Dict[int, str]]:
|
||||||
|
"""
|
||||||
|
为指定过程注入行号和错误处理
|
||||||
|
|
||||||
|
返回: (编织后的代码, {行号: 源代码})
|
||||||
|
"""
|
||||||
|
procedures = self.parse_procedures(code)
|
||||||
|
|
||||||
|
if proc_name not in procedures:
|
||||||
|
raise ValueError(f"Procedure '{proc_name}' not found in code")
|
||||||
|
|
||||||
|
proc_info = procedures[proc_name]
|
||||||
|
lines = code.split('\n')
|
||||||
|
|
||||||
|
# 提取过程代码(包括定义行和结束行)
|
||||||
|
proc_lines = lines[proc_info['start_line']:proc_info['end_line'] + 1]
|
||||||
|
|
||||||
|
# 注入行号
|
||||||
|
woven_lines, source_map = self._inject_line_numbers(proc_lines)
|
||||||
|
|
||||||
|
# 注入错误处理
|
||||||
|
woven_lines = self._inject_error_handler(woven_lines, proc_name)
|
||||||
|
|
||||||
|
# 替换原代码中的过程
|
||||||
|
result_lines = (
|
||||||
|
lines[:proc_info['start_line']] +
|
||||||
|
woven_lines +
|
||||||
|
lines[proc_info['end_line'] + 1:]
|
||||||
|
)
|
||||||
|
|
||||||
|
return '\n'.join(result_lines), source_map
|
||||||
|
|
||||||
|
def _inject_line_numbers(self, lines: List[str]) -> Tuple[List[str], Dict[int, str]]:
|
||||||
|
"""
|
||||||
|
注入行号标签(10, 20, 30...)
|
||||||
|
跳过:
|
||||||
|
- Dim 声明
|
||||||
|
- 注释行
|
||||||
|
- 空行
|
||||||
|
- 现有的 On Error 语句
|
||||||
|
- 过程定义和结束行
|
||||||
|
"""
|
||||||
|
result_lines = []
|
||||||
|
source_map = {}
|
||||||
|
self.line_counter = 10
|
||||||
|
|
||||||
|
# 检查是否是声明行(Dim, Private, Public 等)
|
||||||
|
is_declaration = re.compile(r'^\s*(Dim|Private|Public|Friend|Const|Type|Declare)\b', re.IGNORECASE)
|
||||||
|
is_comment = re.compile(r'^\s*\'')
|
||||||
|
is_empty = re.compile(r'^\s*$')
|
||||||
|
is_on_error = re.compile(r'^\s*On\s+Error', re.IGNORECASE)
|
||||||
|
is_proc_def = re.compile(r'^\s*(Public|Private|Friend)?\s*(Sub|Function)', re.IGNORECASE)
|
||||||
|
is_proc_end = re.compile(r'^\s*End\s+(Sub|Function)', re.IGNORECASE)
|
||||||
|
is_exit_stmt = re.compile(r'^\s*Exit\s+(Sub|Function)', re.IGNORECASE)
|
||||||
|
|
||||||
|
in_declaration_block = True
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
# 检查是否声明块结束(遇到第一个可执行语句)
|
||||||
|
if in_declaration_block and not is_declaration.match(line) and not is_comment.match(line) and not is_empty.match(line):
|
||||||
|
if not is_proc_def.match(line) and not is_proc_end.match(line):
|
||||||
|
in_declaration_block = False
|
||||||
|
|
||||||
|
# 判断是否需要注入行号
|
||||||
|
should_inject = (
|
||||||
|
not in_declaration_block and
|
||||||
|
not is_comment.match(line) and
|
||||||
|
not is_empty.match(line) and
|
||||||
|
not is_on_error.match(line) and
|
||||||
|
not is_proc_def.match(line) and
|
||||||
|
not is_proc_end.match(line) and
|
||||||
|
not is_exit_stmt.match(line)
|
||||||
|
)
|
||||||
|
|
||||||
|
if should_inject:
|
||||||
|
# 保存源代码映射
|
||||||
|
source_map[self.line_counter] = line.strip()
|
||||||
|
# 注入行号标签
|
||||||
|
result_lines.append(f"{self.line_counter}")
|
||||||
|
result_lines.append(line)
|
||||||
|
self.line_counter += 10
|
||||||
|
else:
|
||||||
|
result_lines.append(line)
|
||||||
|
|
||||||
|
return result_lines, source_map
|
||||||
|
|
||||||
|
def _inject_error_handler(self, lines: List[str], proc_name: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
在过程开头添加: On Error GoTo Auto_Err_Handler
|
||||||
|
删除任何现有的 On Error 语句
|
||||||
|
在过程结束前添加错误处理块
|
||||||
|
"""
|
||||||
|
result_lines = []
|
||||||
|
on_error_pattern = re.compile(r'^\s*On\s+Error.*$', re.IGNORECASE)
|
||||||
|
proc_end_pattern = re.compile(r'^\s*End\s+(Sub|Function)', re.IGNORECASE)
|
||||||
|
exit_pattern = re.compile(r'^\s*(Exit\s+(Sub|Function))', re.IGNORECASE)
|
||||||
|
|
||||||
|
# 确定过程类型
|
||||||
|
proc_type = "Sub"
|
||||||
|
for line in lines:
|
||||||
|
if re.match(r'^\s*(Public|Private|Friend)?\s*Function', line, re.IGNORECASE):
|
||||||
|
proc_type = "Function"
|
||||||
|
break
|
||||||
|
|
||||||
|
# 在过程定义后插入 On Error 语句
|
||||||
|
inserted_error_handler = False
|
||||||
|
error_handler_inserted = False
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
# 跳过现有的 On Error 语句
|
||||||
|
if on_error_pattern.match(line):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检查是否是过程结束
|
||||||
|
is_end = proc_end_pattern.match(line)
|
||||||
|
|
||||||
|
if is_end and not error_handler_inserted:
|
||||||
|
# 在 End Sub/Function 之前插入错误处理块
|
||||||
|
result_lines.append(f"")
|
||||||
|
result_lines.append(f" Call TestLogger.LogSuccess()")
|
||||||
|
if proc_type == "Sub":
|
||||||
|
result_lines.append(f" Exit Sub")
|
||||||
|
else:
|
||||||
|
result_lines.append(f" Exit Function")
|
||||||
|
result_lines.append(f"")
|
||||||
|
result_lines.append(f"Auto_Err_Handler_{proc_name}:")
|
||||||
|
result_lines.append(f" Call TestLogger.LogError(\"{proc_name}\", Err.Number, Err.Description, Erl)")
|
||||||
|
result_lines.append(line) # 添加 End Sub/Function
|
||||||
|
error_handler_inserted = True
|
||||||
|
elif is_end:
|
||||||
|
# 跳过后续的 End 语句(不应该有)
|
||||||
|
pass
|
||||||
|
elif exit_pattern.match(line):
|
||||||
|
# 跳过现有的 Exit 语句
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# 在第一个非声明、非空行后插入 On Error
|
||||||
|
if not inserted_error_handler and not re.match(r'^\s*(\'|Dim|Private|Public|Const|$)', line, re.IGNORECASE):
|
||||||
|
if not re.match(r'^\s*(Public|Private|Friend)?\s*(Sub|Function)', line, re.IGNORECASE):
|
||||||
|
result_lines.append(f" On Error GoTo Auto_Err_Handler_{proc_name}")
|
||||||
|
inserted_error_handler = True
|
||||||
|
|
||||||
|
result_lines.append(line)
|
||||||
|
|
||||||
|
# 如果没有找到 End,追加到末尾
|
||||||
|
if not error_handler_inserted:
|
||||||
|
result_lines.append(f"")
|
||||||
|
result_lines.append(f" Call TestLogger.LogSuccess()")
|
||||||
|
if proc_type == "Sub":
|
||||||
|
result_lines.append(f" Exit Sub")
|
||||||
|
else:
|
||||||
|
result_lines.append(f" Exit Function")
|
||||||
|
result_lines.append(f"")
|
||||||
|
result_lines.append(f"Auto_Err_Handler_{proc_name}:")
|
||||||
|
result_lines.append(f" Call TestLogger.LogError(\"{proc_name}\", Err.Number, Err.Description, Erl)")
|
||||||
|
|
||||||
|
return result_lines
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerInjector:
|
||||||
|
"""日志模块注入器 - 向 Excel 项目注入 _TestLogger 辅助模块"""
|
||||||
|
|
||||||
|
LOGGER_MODULE_CODE = """Option Explicit
|
||||||
|
|
||||||
|
' Module-level variables to store test result
|
||||||
|
Private m_TestStatus As String
|
||||||
|
Private m_ErrProcedure As String
|
||||||
|
Private m_ErrNumber As Long
|
||||||
|
Private m_ErrDescription As String
|
||||||
|
Private m_ErrLine As Long
|
||||||
|
|
||||||
|
Sub LogError(ByVal procName As String, ByVal errNum As Long, _
|
||||||
|
ByVal errDesc As String, ByVal errLine As Long)
|
||||||
|
m_TestStatus = "ERROR"
|
||||||
|
m_ErrProcedure = procName
|
||||||
|
m_ErrNumber = errNum
|
||||||
|
m_ErrDescription = errDesc
|
||||||
|
m_ErrLine = errLine
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Sub LogSuccess()
|
||||||
|
m_TestStatus = "SUCCESS"
|
||||||
|
m_ErrProcedure = ""
|
||||||
|
m_ErrNumber = 0
|
||||||
|
m_ErrDescription = ""
|
||||||
|
m_ErrLine = 0
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Function GetResult() As String
|
||||||
|
If m_TestStatus = "SUCCESS" Then
|
||||||
|
GetResult = "SUCCESS"
|
||||||
|
Else
|
||||||
|
GetResult = "ERROR|" & m_ErrProcedure & "|" & m_ErrLine & "|" & _
|
||||||
|
m_ErrNumber & "|" & m_ErrDescription
|
||||||
|
End If
|
||||||
|
End Function
|
||||||
|
"""
|
||||||
|
|
||||||
|
def inject_or_replace(self, wb, module_name: str = "TestLogger") -> None:
|
||||||
|
"""
|
||||||
|
如果模块存在则删除,然后注入新的 Logger 模块
|
||||||
|
"""
|
||||||
|
# 获取 VBA 项目
|
||||||
|
vba_project = wb.api.VBProject
|
||||||
|
|
||||||
|
# 检查模块是否存在,存在则删除
|
||||||
|
for component in vba_project.VBComponents:
|
||||||
|
if component.Name == module_name:
|
||||||
|
vba_project.VBComponents.Remove(component)
|
||||||
|
break
|
||||||
|
|
||||||
|
# 注入新模块
|
||||||
|
new_module = vba_project.VBComponents.Add(1) # 1 = vbext_ct_StdModule
|
||||||
|
new_module.Name = module_name
|
||||||
|
new_module.CodeModule.AddFromString(self.LOGGER_MODULE_CODE)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunner:
|
||||||
|
"""测试执行器 - 管理 Excel 生命周期,执行测试,返回结果"""
|
||||||
|
|
||||||
|
def __init__(self, file_path: str, visible: bool = True):
|
||||||
|
"""
|
||||||
|
初始化测试执行器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Excel 文件路径
|
||||||
|
visible: 是否显示 Excel 窗口
|
||||||
|
"""
|
||||||
|
self.file_path = file_path
|
||||||
|
self.visible = visible
|
||||||
|
self.app = None
|
||||||
|
self.wb = None
|
||||||
|
self.code_weaver = CodeWeaver()
|
||||||
|
self.logger_injector = LoggerInjector()
|
||||||
|
self.source_map = {} # 行号 -> 源代码映射
|
||||||
|
|
||||||
|
def run_test(self, module_name: str, proc_name: str) -> TestResult:
|
||||||
|
"""
|
||||||
|
执行测试的完整流程:
|
||||||
|
1. 打开 Excel 文件
|
||||||
|
2. 读取原始 VBA 代码
|
||||||
|
3. 使用 CodeWeaver 编织代码
|
||||||
|
4. 注入/替换 Logger 模块
|
||||||
|
5. 热替换目标模块代码
|
||||||
|
6. 执行宏
|
||||||
|
7. 获取结果
|
||||||
|
8. 关闭工作簿(不保存)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1. 打开 Excel
|
||||||
|
self.app = xw.App(visible=self.visible)
|
||||||
|
self.wb = self.app.books.open(self.file_path)
|
||||||
|
|
||||||
|
# 2. 读取原始 VBA 代码
|
||||||
|
original_code = self._get_vba_code(module_name)
|
||||||
|
|
||||||
|
# 3. 编织代码
|
||||||
|
woven_code, self.source_map = self.code_weaver.weave_procedure(
|
||||||
|
original_code, proc_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. 注入 Logger 模块
|
||||||
|
self.logger_injector.inject_or_replace(self.wb)
|
||||||
|
|
||||||
|
# 5. 热替换目标模块代码
|
||||||
|
self._replace_module_code(module_name, woven_code)
|
||||||
|
|
||||||
|
# 6. 执行宏
|
||||||
|
self._execute_macro(f"{module_name}.{proc_name}")
|
||||||
|
|
||||||
|
# 7. 获取结果
|
||||||
|
result = self._get_test_result(proc_name)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return TestResult(
|
||||||
|
procedure_name=proc_name,
|
||||||
|
success=False,
|
||||||
|
error_number=0,
|
||||||
|
error_description=f"Test execution failed: {str(e)}",
|
||||||
|
error_line=0,
|
||||||
|
source_code=""
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# 8. 关闭工作簿(不保存)
|
||||||
|
self._cleanup()
|
||||||
|
|
||||||
|
def _get_vba_code(self, module_name: str) -> str:
|
||||||
|
"""从 Excel 读取指定模块的 VBA 代码"""
|
||||||
|
vba_project = self.wb.api.VBProject
|
||||||
|
|
||||||
|
for component in vba_project.VBComponents:
|
||||||
|
if component.Name == module_name:
|
||||||
|
code_module = component.CodeModule
|
||||||
|
line_count = code_module.CountOfLines
|
||||||
|
return code_module.Lines(1, line_count) if line_count > 0 else ""
|
||||||
|
|
||||||
|
raise ValueError(f"Module '{module_name}' not found in VBA project")
|
||||||
|
|
||||||
|
def _replace_module_code(self, module_name: str, new_code: str) -> None:
|
||||||
|
"""热替换模块代码"""
|
||||||
|
vba_project = self.wb.api.VBProject
|
||||||
|
|
||||||
|
for component in vba_project.VBComponents:
|
||||||
|
if component.Name == module_name:
|
||||||
|
code_module = component.CodeModule
|
||||||
|
# 删除所有现有代码
|
||||||
|
if code_module.CountOfLines > 0:
|
||||||
|
code_module.DeleteLines(1, code_module.CountOfLines)
|
||||||
|
# 添加新代码
|
||||||
|
code_module.AddFromString(new_code)
|
||||||
|
return
|
||||||
|
|
||||||
|
raise ValueError(f"Module '{module_name}' not found in VBA project")
|
||||||
|
|
||||||
|
def _execute_macro(self, proc_name: str) -> None:
|
||||||
|
"""执行 VBA 宏"""
|
||||||
|
try:
|
||||||
|
self.app.api.Run(proc_name)
|
||||||
|
except Exception as e:
|
||||||
|
# 错误会被 VBA 的错误处理捕获,这里不需要处理
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_test_result(self, proc_name: str) -> TestResult:
|
||||||
|
"""从 Logger 模块获取测试结果"""
|
||||||
|
try:
|
||||||
|
result_str = self.app.api.Run("TestLogger.GetResult")
|
||||||
|
|
||||||
|
if result_str == "SUCCESS":
|
||||||
|
return TestResult(
|
||||||
|
procedure_name=proc_name,
|
||||||
|
success=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 解析错误信息: ERROR|ProcedureName|Line|Number|Description
|
||||||
|
parts = result_str.split('|')
|
||||||
|
if len(parts) >= 5:
|
||||||
|
error_proc_name = parts[1]
|
||||||
|
error_line = int(parts[2])
|
||||||
|
error_number = int(parts[3])
|
||||||
|
error_desc = parts[4]
|
||||||
|
|
||||||
|
# 从 Source Map 获取源代码
|
||||||
|
source_code = self.source_map.get(error_line, "Source code not found")
|
||||||
|
|
||||||
|
return TestResult(
|
||||||
|
procedure_name=error_proc_name,
|
||||||
|
success=False,
|
||||||
|
error_number=error_number,
|
||||||
|
error_description=error_desc,
|
||||||
|
error_line=error_line,
|
||||||
|
source_code=source_code
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return TestResult(
|
||||||
|
procedure_name=proc_name,
|
||||||
|
success=False,
|
||||||
|
error_number=0,
|
||||||
|
error_description=f"Failed to get test result: {str(e)}",
|
||||||
|
error_line=0,
|
||||||
|
source_code=""
|
||||||
|
)
|
||||||
|
|
||||||
|
def _cleanup(self):
|
||||||
|
"""清理资源,关闭 Excel"""
|
||||||
|
try:
|
||||||
|
if self.wb:
|
||||||
|
self.wb.api.Close(False)
|
||||||
|
if self.app:
|
||||||
|
self.app.quit()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def print_test_result(result: TestResult, index: int = None, total: int = None):
|
||||||
|
"""打印单个测试结果"""
|
||||||
|
prefix = f"[{index}/{total}] " if index and total else ""
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
print(f"{prefix}[PASS] {result.procedure_name} - Test Passed")
|
||||||
|
else:
|
||||||
|
print(f"{prefix}[FAIL] {result.procedure_name} - Test Failed")
|
||||||
|
print(f" Error Description: {result.error_description}")
|
||||||
|
print(f" Error Line: {result.error_line}")
|
||||||
|
print(f" Source Code: {result.source_code}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_summary(results: List[TestResult]):
|
||||||
|
"""打印测试汇总"""
|
||||||
|
passed = sum(1 for r in results if r.success)
|
||||||
|
failed = sum(1 for r in results if not r.success)
|
||||||
|
total = len(results)
|
||||||
|
|
||||||
|
print("\n===== Test Summary =====")
|
||||||
|
print(f"Passed: {passed}/{total}")
|
||||||
|
print(f"Failed: {failed}/{total}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主入口函数"""
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("Usage: python vba_test_runner.py <excel_file> <module_name> <procedure1> [procedure2] ...")
|
||||||
|
print("Example: python vba_test_runner.py demo.xlsm Module1 TestErrorProcedure")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
excel_file = sys.argv[1]
|
||||||
|
module_name = sys.argv[2]
|
||||||
|
procedures = sys.argv[3:]
|
||||||
|
|
||||||
|
print(f"===== VBA Batch Test Results =====\n")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i, proc_name in enumerate(procedures, 1):
|
||||||
|
runner = TestRunner(excel_file, visible=False)
|
||||||
|
result = runner.run_test(module_name, proc_name)
|
||||||
|
results.append(result)
|
||||||
|
print_test_result(result, i, len(procedures))
|
||||||
|
|
||||||
|
print_summary(results)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user