Compare commits
5 Commits
7a86a88fc4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9e11ff7dd | ||
|
|
15e9fabf25 | ||
|
|
0502137ef7 | ||
|
|
01690580ae | ||
|
|
2cb22df60c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -35,4 +35,4 @@ test_*.py
|
|||||||
debug_*.py
|
debug_*.py
|
||||||
|
|
||||||
# Demo file (optional - comment out if you want to track it)
|
# Demo file (optional - comment out if you want to track it)
|
||||||
# demo.xlsm
|
*.xlsm
|
||||||
|
|||||||
141
CLAUDE.md
Normal file
141
CLAUDE.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a **VBA Automated Testing and Precise Error Reporting System** that enables automated testing of VBA macros in Excel files with exact line-level error reporting using code weaving techniques. The system solves the fundamental problem of debugging VBA macros by providing precise error locations and automated testing capabilities.
|
||||||
|
|
||||||
|
## Core Architecture
|
||||||
|
|
||||||
|
The system uses a **code weaving pattern** to instrument VBA code at runtime:
|
||||||
|
|
||||||
|
1. **CodeWeaver Class** (`vba_test_runner.py`): Parses VBA code, injects line number labels (10, 20, 30...) before executable code, and adds error handling with `On Error GoTo` statements. Generates source maps mapping line numbers to original source code.
|
||||||
|
|
||||||
|
2. **LoggerInjector Class**: Manages test logging infrastructure by injecting a `TestLogger` module and `CallStack` class into the VBA project for tracking execution flow and capturing results.
|
||||||
|
|
||||||
|
3. **TestRunner Class**: Coordinates the testing process - manages Excel application lifecycle, handles VBA code hot-swapping, executes macros, and ensures files remain unmodified (non-invasive).
|
||||||
|
|
||||||
|
4. **TestResult Data Class**: Structured container for procedure results including error details, source code mapping, error location, and call chain information.
|
||||||
|
|
||||||
|
### How Code Weaving Works
|
||||||
|
|
||||||
|
The system:
|
||||||
|
- Parses VBA code to identify all Sub/Function procedures
|
||||||
|
- Injects numeric line labels (10, 20, 30...) before executable code (skipping declarations, comments, empty lines)
|
||||||
|
- Adds `On Error GoTo` error handlers at procedure start
|
||||||
|
- Uses VBA's `Erl()` function to capture the last executed line number
|
||||||
|
- Maps line numbers back to original source code via Source Map
|
||||||
|
- Hot-swaps module code in Excel memory (never saves to disk)
|
||||||
|
|
||||||
|
### Error Detection Mechanism
|
||||||
|
|
||||||
|
- Leverages VBA's `Erl()` function which returns the last executed line number label
|
||||||
|
- Maintains a source map from injected line numbers to original source code
|
||||||
|
- Tracks call chains through LogEntry/LogExit in the TestLogger
|
||||||
|
- Displays exact error location with original source code context
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Virtual Environment Requirement
|
||||||
|
|
||||||
|
**IMPORTANT**: Always activate the virtual environment before running any test scripts. The project uses a virtual environment located at `.venv`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On Windows
|
||||||
|
.venv\Scripts\activate
|
||||||
|
|
||||||
|
# On Linux/Mac
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Never run `vba_test_runner.py` or other Python scripts directly from the system Python environment. Always ensure the virtual environment is active first.
|
||||||
|
|
||||||
|
### Install Dependencies
|
||||||
|
```bash
|
||||||
|
pip install xlwings pywin32
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
Test a single procedure:
|
||||||
|
```bash
|
||||||
|
python vba_test_runner.py demo.xlsm Module1 TestErrorProcedure
|
||||||
|
```
|
||||||
|
|
||||||
|
Test multiple procedures:
|
||||||
|
```bash
|
||||||
|
python vba_test_runner.py demo.xlsm Module1 TestErrorProcedure TestTypeMismatch TestSubscriptError
|
||||||
|
```
|
||||||
|
|
||||||
|
Full project testing (discovers and tests all procedures):
|
||||||
|
```bash
|
||||||
|
python vba_test_runner.py demo.xlsm --all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Excel Configuration Requirement
|
||||||
|
|
||||||
|
Before running tests, enable "Trust access to the VBA project object model" in Excel:
|
||||||
|
- Open Excel > File > Options > Trust Center > Trust Center Settings
|
||||||
|
- Go to Macro Settings
|
||||||
|
- Check: "Trust access to the VBA project object model"
|
||||||
|
- Restart Excel
|
||||||
|
|
||||||
|
## Key Implementation Details
|
||||||
|
|
||||||
|
### Line Number Label Rules
|
||||||
|
- Use pure numbers without colons: `10`, `20`, `30`...
|
||||||
|
- Only inject before executable code
|
||||||
|
- Skip declaration blocks (Dim, Private, etc.)
|
||||||
|
- Skip comments and empty lines
|
||||||
|
- Skip existing On Error statements
|
||||||
|
|
||||||
|
### Hot-Swap Non-Invasive Testing
|
||||||
|
All code modifications happen in memory. The system:
|
||||||
|
1. Reads original VBA code from Excel file
|
||||||
|
2. Applies code weaving transformations
|
||||||
|
3. Replaces module code in Excel's VBProject
|
||||||
|
4. Executes tests
|
||||||
|
5. Closes workbook with `SaveChanges=False`
|
||||||
|
|
||||||
|
Original Excel files are never modified.
|
||||||
|
|
||||||
|
### Procedure Filtering
|
||||||
|
- Automatically excludes Worksheet_ and Workbook_ event procedures
|
||||||
|
- Only processes standard modules and class modules (Type 1 and 2)
|
||||||
|
- Supports both single procedure and full project testing modes
|
||||||
|
|
||||||
|
### TestResult Structure
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class TestResult:
|
||||||
|
procedure_name: str
|
||||||
|
success: bool
|
||||||
|
error_number: int = 0
|
||||||
|
error_description: str = ""
|
||||||
|
error_line: int = 0
|
||||||
|
source_code: str = ""
|
||||||
|
error_module: str = "" # Module where error occurred
|
||||||
|
call_chain: str = "" # Full call stack
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main Entry Point
|
||||||
|
|
||||||
|
```python
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main() # Handles both single procedure and full project testing
|
||||||
|
```
|
||||||
|
|
||||||
|
Core methods:
|
||||||
|
- `TestRunner.run_test()`: Execute a single test procedure
|
||||||
|
- `TestRunner.run_all_tests()`: Discover and execute all testable procedures
|
||||||
|
- `CodeWeaver.weave_procedure()`: Weave a single procedure
|
||||||
|
- `CodeWeaver.weave_all_modules()`: Weave all modules in a project
|
||||||
|
- `CodeWeaver.parse_modules()`: Parse all modules in a VBA project
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Only supports standard modules and class modules (not form modules)
|
||||||
|
- Line number label conflicts possible if original code uses same numeric labels
|
||||||
|
- Complex procedures with many GoTo statements may require additional handling
|
||||||
|
- Requires Excel VBA project object model access (security setting)
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
# 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 调试的痛点。所有测试场景均已验证通过,系统稳定可用。
|
|
||||||
352
README.md
352
README.md
@@ -1,24 +1,72 @@
|
|||||||
# VBA 自动化测试与精确报错系统
|
# VBA 自动化测试与精确报错系统
|
||||||
|
|
||||||
通过代码编织(Code Weaving)技术,实现 VBA 宏的精确到行的代码报错定位。
|
通过代码编织(Code Weaving)技术,实现 VBA 宏的精确到行的代码报错定位,并支持完整的调用链追踪。
|
||||||
|
|
||||||
|
## 项目状态
|
||||||
|
|
||||||
|
✅ **已完成** - 所有核心功能已实现并测试通过
|
||||||
|
|
||||||
## 核心特性
|
## 核心特性
|
||||||
|
|
||||||
- **精确行号定位**: 捕获 VBA 错误的具体行号,不再只是"发生意外"
|
- **精确行号定位**: 捕获 VBA 错误的具体行号,不再只是"发生意外"
|
||||||
- **源代码映射**: 通过 Source Map 机制显示出错行的原始代码
|
- **源代码映射**: 通过 Source Map 机制显示出错行的原始代码
|
||||||
- **自动化测试**: 批量执行 VBA 宏并收集结果
|
- **调用链追踪**: 完整记录过程调用栈,追踪错误传播路径
|
||||||
|
- **全项目测试**: 自动发现并测试所有可测试的过程
|
||||||
|
- **模块级支持**: 支持标准模块和类模块的测试
|
||||||
- **非侵入式**: 测试过程不修改原始 Excel 文件
|
- **非侵入式**: 测试过程不修改原始 Excel 文件
|
||||||
- **详细报告**: 提供清晰的测试结果输出
|
- **详细报告**: 提供清晰的测试结果输出,包含错误位置、源代码和调用链
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
xlwings/
|
||||||
|
├── vba_test_runner.py # 主脚本 - 核心测试系统实现
|
||||||
|
├── demo.xlsm # 演示 Excel 文件(包含测试用例)
|
||||||
|
├── README.md # 项目文档(本文件)
|
||||||
|
├── vba_test_runner_flowchart.md # 详细流程图文档(Mermaid 图表)
|
||||||
|
├── IMPLEMENTATION_SUMMARY.md # 实现总结与技术细节
|
||||||
|
├── CLAUDE.md # Claude Code 开发指南
|
||||||
|
├── .gitignore # Git 忽略规则
|
||||||
|
├── .vscode/
|
||||||
|
│ └── launch.json # VS Code 调试配置
|
||||||
|
└── .venv/ # Python 虚拟环境(需自行创建)
|
||||||
|
```
|
||||||
|
|
||||||
## 安装依赖
|
## 安装依赖
|
||||||
|
|
||||||
|
### 1. 创建虚拟环境(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\activate
|
||||||
|
|
||||||
|
# Linux/Mac
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安装依赖包
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install xlwings pywin32
|
pip install xlwings pywin32
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 3. 配置 Excel 信任设置
|
||||||
|
|
||||||
|
在运行测试前,需要启用 VBA 项目对象模型访问:
|
||||||
|
|
||||||
|
1. 打开 Excel
|
||||||
|
2. 文件 > 选项 > 信任中心
|
||||||
|
3. 信任中心设置 > 宏设置
|
||||||
|
4. 勾选"信任对 VBA 工程对象模型的访问"
|
||||||
|
5. 重启 Excel
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 1. 创建演示文件
|
### 1. 使用演示文件测试
|
||||||
|
|
||||||
|
项目包含 `demo.xlsm` 演示文件,包含以下测试用例:
|
||||||
|
|
||||||
首先创建一个包含测试代码的 Excel 文件:
|
首先创建一个包含测试代码的 Excel 文件:
|
||||||
|
|
||||||
@@ -46,47 +94,78 @@ python vba_test_runner.py demo.xlsm Module1 TestSuccessfulProcedure
|
|||||||
python vba_test_runner.py demo.xlsm Module1 TestErrorProcedure TestTypeMismatch TestSubscriptError TestSuccessfulProcedure
|
python vba_test_runner.py demo.xlsm Module1 TestErrorProcedure TestTypeMismatch TestSubscriptError TestSuccessfulProcedure
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**全项目自动化测试**(自动发现并测试所有过程):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python vba_test_runner.py demo.xlsm --all
|
||||||
|
```
|
||||||
|
|
||||||
## 输出示例
|
## 输出示例
|
||||||
|
|
||||||
### 单个测试
|
### 单个测试
|
||||||
|
|
||||||
**成功时**:
|
**成功时**:
|
||||||
```
|
```
|
||||||
[PASS] TestSuccessfulProcedure - Test Passed
|
[PASS] TestSuccessfulProcedure
|
||||||
```
|
```
|
||||||
|
|
||||||
**失败时**:
|
**失败时**(包含调用链):
|
||||||
```
|
```
|
||||||
[FAIL] TestErrorProcedure - Test Failed
|
[FAIL] TestErrorProcedure
|
||||||
Error Description: Division by zero
|
Error: Division by zero
|
||||||
Error Line: 30
|
Location: Module1.TestErrorProcedure:30
|
||||||
Source Code: result = x / y
|
Call Chain: Module1.MainProc -> Module1.TestErrorProcedure
|
||||||
|
Source: result = x / y
|
||||||
```
|
```
|
||||||
|
|
||||||
### 批量测试
|
### 批量测试
|
||||||
|
|
||||||
```
|
```
|
||||||
===== VBA Batch Test Results =====
|
===== VBA 批量测试结果 =====
|
||||||
|
|
||||||
[1/4] [FAIL] TestErrorProcedure - Test Failed
|
[1/4] [FAIL] TestErrorProcedure
|
||||||
Error Description: Division by zero
|
Error: Division by zero
|
||||||
Error Line: 30
|
Location: Module1.TestErrorProcedure:30
|
||||||
Source Code: result = x / y
|
Source: result = x / y
|
||||||
[2/4] [FAIL] TestTypeMismatch - Test Failed
|
[2/4] [FAIL] TestTypeMismatch
|
||||||
Error Description: Type mismatch
|
Error: Type mismatch
|
||||||
Error Line: 20
|
Location: Module1.TestTypeMismatch:20
|
||||||
Source Code: y = x
|
Source: y = x
|
||||||
[3/4] [FAIL] TestSubscriptError - Test Failed
|
[3/4] [FAIL] TestSubscriptError
|
||||||
Error Description: Subscript out of range
|
Error: Subscript out of range
|
||||||
Error Line: 40
|
Location: Module1.TestSubscriptError:40
|
||||||
Source Code: value = arr(5)
|
Source: value = arr(5)
|
||||||
[4/4] [PASS] TestSuccessfulProcedure - Test Passed
|
[4/4] [PASS] TestSuccessfulProcedure
|
||||||
|
|
||||||
===== Test Summary =====
|
===== Test Summary =====
|
||||||
Passed: 1/4
|
Passed: 1/4
|
||||||
Failed: 3/4
|
Failed: 3/4
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 全项目测试(--all 模式)
|
||||||
|
|
||||||
|
```
|
||||||
|
===== VBA 全项目自动化测试 =====
|
||||||
|
|
||||||
|
发现 8 个可测试的入口点
|
||||||
|
总过程数: 12
|
||||||
|
|
||||||
|
[1/8] 测试 Module1.TestErrorProcedure
|
||||||
|
[FAIL] TestErrorProcedure
|
||||||
|
Error: Division by zero
|
||||||
|
Location: Module1.TestErrorProcedure:30
|
||||||
|
Call Chain: Module1.TestErrorProcedure
|
||||||
|
Source: result = x / y
|
||||||
|
|
||||||
|
[2/8] 测试 Module1.TestSuccessfulProcedure
|
||||||
|
[PASS] TestSuccessfulProcedure
|
||||||
|
...
|
||||||
|
|
||||||
|
===== Test Summary =====
|
||||||
|
Passed: 5/8
|
||||||
|
Failed: 3/8
|
||||||
|
```
|
||||||
|
|
||||||
## 工作原理
|
## 工作原理
|
||||||
|
|
||||||
### 代码编织(Code Weaving)
|
### 代码编织(Code Weaving)
|
||||||
@@ -95,8 +174,9 @@ Failed: 3/4
|
|||||||
|
|
||||||
1. **解析 VBA 代码**: 识别所有 Sub/Function 过程
|
1. **解析 VBA 代码**: 识别所有 Sub/Function 过程
|
||||||
2. **注入行号标签**: 在可执行代码前插入数字标签(10, 20, 30...)
|
2. **注入行号标签**: 在可执行代码前插入数字标签(10, 20, 30...)
|
||||||
3. **注入错误处理**: 添加 `On Error GoTo` 语句和错误处理块
|
3. **注入调用栈管理**: 添加 `LogEntry` 和 `LogExit` 调用
|
||||||
4. **创建 Source Map**: 维护行号到源代码的映射
|
4. **注入错误处理**: 添加 `On Error GoTo` 语句和错误处理块
|
||||||
|
5. **创建 Source Map**: 维护行号到源代码的映射
|
||||||
|
|
||||||
### VBA 行号机制
|
### VBA 行号机制
|
||||||
|
|
||||||
@@ -108,46 +188,100 @@ VBA 的 `Erl` 函数会返回最近执行的行号标签:
|
|||||||
30 result = x / y ' Erl 将返回 30
|
30 result = x / y ' Erl 将返回 30
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 调用链追踪
|
||||||
|
|
||||||
|
系统使用 `CallStack` 类模块追踪过程调用:
|
||||||
|
|
||||||
|
```vba
|
||||||
|
Sub LogEntry(procName, moduleName)
|
||||||
|
CallStack.Push procName, moduleName
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Sub LogExit()
|
||||||
|
CallStack.Pop
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Function GetCallChain() As String
|
||||||
|
GetCallChain = CallStack.GetCallChain() ' 返回 "Module1.Main -> Module1.Helper"
|
||||||
|
End Function
|
||||||
|
```
|
||||||
|
|
||||||
### 测试流程
|
### 测试流程
|
||||||
|
|
||||||
```
|
```
|
||||||
原始 VBA 代码
|
原始 VBA 代码
|
||||||
↓
|
↓
|
||||||
代码编织器注入行号和错误处理
|
代码编织器注入行号、调用栈管理和错误处理
|
||||||
↓
|
↓
|
||||||
注入 _TestLogger 辅助模块
|
注入 TestLogger 模块和 CallStack 类模块
|
||||||
↓
|
↓
|
||||||
热替换目标模块代码
|
热替换目标模块代码
|
||||||
↓
|
↓
|
||||||
执行 VBA 宏
|
执行 VBA 宏
|
||||||
↓
|
↓
|
||||||
从 _TestLogger 读取结果
|
从 TestLogger 读取结果(包含调用链)
|
||||||
↓
|
↓
|
||||||
格式化输出
|
格式化输出
|
||||||
```
|
```
|
||||||
|
|
||||||
## 架构设计
|
## 架构设计
|
||||||
|
|
||||||
### 模块划分
|
### 核心类
|
||||||
|
|
||||||
```
|
```
|
||||||
vba_test_runner.py
|
vba_test_runner.py
|
||||||
|
├── TestResult (测试结果数据类)
|
||||||
|
│ ├── procedure_name - 过程名称
|
||||||
|
│ ├── success - 测试是否成功
|
||||||
|
│ ├── error_number - 错误代码
|
||||||
|
│ ├── error_description - 错误描述
|
||||||
|
│ ├── error_line - 错误行号
|
||||||
|
│ ├── source_code - 源代码
|
||||||
|
│ ├── error_module - 错误发生的模块 (新增)
|
||||||
|
│ └── call_chain - 完整调用链 (新增)
|
||||||
|
│
|
||||||
├── CodeWeaver (代码编织器类)
|
├── CodeWeaver (代码编织器类)
|
||||||
│ ├── parse_procedures() - 解析 VBA 过程
|
│ ├── parse_modules() - 解析所有模块
|
||||||
│ ├── weave_procedure() - 编织单个过程
|
│ ├── parse_procedures() - 解析 VBA 过程
|
||||||
│ ├── _inject_line_numbers() - 注入行号标签
|
│ ├── weave_procedure() - 编织单个过程
|
||||||
│ └── _inject_error_handler() - 注入错误处理
|
│ ├── weave_procedure_with_callstack() - 编织过程(带调用栈)
|
||||||
|
│ ├── weave_module_all_procedures() - 编织模块的所有过程
|
||||||
|
│ ├── weave_all_modules() - 编织所有模块
|
||||||
|
│ ├── _inject_line_numbers() - 注入行号标签
|
||||||
|
│ ├── _inject_error_handler() - 注入错误处理
|
||||||
|
│ └── _inject_error_handler_with_callstack() - 注入错误处理(带调用栈)
|
||||||
│
|
│
|
||||||
├── LoggerInjector (日志模块注入器类)
|
├── LoggerInjector (日志模块注入器类)
|
||||||
│ ├── inject_or_replace() - 注入或替换 Logger 模块
|
│ ├── inject_or_replace() - 注入或替换 Logger 模块
|
||||||
│ └── LOGGER_MODULE_CODE - Logger 模块的 VBA 代码
|
│ ├── LOGGER_MODULE_CODE - TestLogger 模块的 VBA 代码
|
||||||
|
│ └── CALLSTACK_CLASS_CODE - CallStack 类模块的 VBA 代码
|
||||||
│
|
│
|
||||||
└── TestRunner (执行控制器类)
|
└── TestRunner (执行控制器类)
|
||||||
├── run_test() - 执行完整测试流程
|
├── run_test() - 执行单个测试
|
||||||
|
├── run_all_tests() - 执行全项目测试 (新增)
|
||||||
|
├── discover_all_tests() - 发现所有可测试过程 (新增)
|
||||||
├── _get_vba_code() - 读取 VBA 代码
|
├── _get_vba_code() - 读取 VBA 代码
|
||||||
├── _replace_module_code() - 热替换模块代码
|
├── _replace_module_code() - 热替换模块代码
|
||||||
├── _execute_macro() - 执行宏
|
├── _execute_macro() - 执行宏
|
||||||
└── _get_test_result() - 获取测试结果
|
├── _get_test_result() - 获取测试结果
|
||||||
|
├── _is_entry_point() - 判断是否为测试入口点 (新增)
|
||||||
|
├── _weave_all_modules_inplace() - 就地编织所有模块 (新增)
|
||||||
|
└── _run_single_test() - 执行单个测试(批量用) (新增)
|
||||||
|
```
|
||||||
|
|
||||||
|
### TestResult 结构
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class TestResult:
|
||||||
|
procedure_name: str # 过程名称
|
||||||
|
success: bool # 测试是否成功
|
||||||
|
error_number: int = 0 # 错误代码
|
||||||
|
error_description: str = "" # 错误描述
|
||||||
|
error_line: int = 0 # 错误行号
|
||||||
|
source_code: str = "" # 源代码
|
||||||
|
error_module: str = "" # 错误发生的模块
|
||||||
|
call_chain: str = "" # 完整调用链 (如: "Module1.A -> Module1.B")
|
||||||
```
|
```
|
||||||
|
|
||||||
## 关键技术点
|
## 关键技术点
|
||||||
@@ -160,18 +294,41 @@ vba_test_runner.py
|
|||||||
- 跳过注释行和空行
|
- 跳过注释行和空行
|
||||||
- 跳过现有的 On Error 语句
|
- 跳过现有的 On Error 语句
|
||||||
|
|
||||||
### 错误处理模板
|
### 错误处理模板(带调用栈)
|
||||||
|
|
||||||
```vba
|
```vba
|
||||||
|
Call TestLogger.LogEntry("{proc_name}", "{module_name}")
|
||||||
On Error GoTo Auto_Err_Handler_{proc_name}
|
On Error GoTo Auto_Err_Handler_{proc_name}
|
||||||
|
|
||||||
... 原有代码 ...
|
... 原有代码 ...
|
||||||
|
|
||||||
Call _TestLogger.LogSuccess()
|
Call TestLogger.LogExit()
|
||||||
|
Call TestLogger.LogSuccess()
|
||||||
Exit Sub/Function
|
Exit Sub/Function
|
||||||
|
|
||||||
Auto_Err_Handler_{proc_name}:
|
Auto_Err_Handler_{proc_name}:
|
||||||
Call _TestLogger.LogError("{proc_name}", Err.Number, Err.Description, Erl)
|
Call TestLogger.LogError("{proc_name}", "{module_name}", Err.Number, Err.Description, Erl)
|
||||||
|
Call TestLogger.LogExit()
|
||||||
|
```
|
||||||
|
|
||||||
|
### CallStack 类模块
|
||||||
|
|
||||||
|
```vba
|
||||||
|
' CallStack 类维护调用栈
|
||||||
|
Private m_Stack As Collection
|
||||||
|
|
||||||
|
Public Sub Push(procName, moduleName)
|
||||||
|
m_Stack.Add moduleName & "." & procName
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Public Sub Pop()
|
||||||
|
m_Stack.Remove m_Stack.Count
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Public Function GetCallChain() As String
|
||||||
|
' 返回 "Module1.A -> Module1.B -> Module2.C"
|
||||||
|
GetCallChain = Join(parts, " -> ")
|
||||||
|
End Function
|
||||||
```
|
```
|
||||||
|
|
||||||
### 热替换不保存
|
### 热替换不保存
|
||||||
@@ -187,7 +344,11 @@ Auto_Err_Handler_{proc_name}:
|
|||||||
|
|
||||||
3. **复杂过程**: 对于非常复杂的过程(包含大量 GoTo 语句),可能需要额外处理
|
3. **复杂过程**: 对于非常复杂的过程(包含大量 GoTo 语句),可能需要额外处理
|
||||||
|
|
||||||
4. **只支持标准模块**: 当前版本不支持类模块和窗体模块
|
4. **模块类型支持**: 支持标准模块(Type 1)和类模块(Type 2),不支持窗体模块(Type 3)
|
||||||
|
|
||||||
|
5. **过程过滤**: 全项目测试模式会自动排除以下过程:
|
||||||
|
- 以 `Worksheet_`、`Workbook_`、`Document_` 开头的事件过程
|
||||||
|
- Logger 相关过程(LogEntry、LogExit、LogError 等)
|
||||||
|
|
||||||
## 扩展开发
|
## 扩展开发
|
||||||
|
|
||||||
@@ -207,9 +368,89 @@ End Sub
|
|||||||
python vba_test_runner.py your_file.xlsm Module1 YourTestProcedure
|
python vba_test_runner.py your_file.xlsm Module1 YourTestProcedure
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 使用全项目测试
|
||||||
|
|
||||||
|
全项目测试模式会自动发现并测试所有过程:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python vba_test_runner.py your_file.xlsm --all
|
||||||
|
```
|
||||||
|
|
||||||
|
系统会:
|
||||||
|
1. 扫描所有标准模块和类模块
|
||||||
|
2. 识别所有 Sub/Function 过程
|
||||||
|
3. 过滤掉事件过程和内部方法
|
||||||
|
4. 为所有模块注入调用栈管理
|
||||||
|
5. 逐个执行测试并生成报告
|
||||||
|
|
||||||
### 自定义 Logger 模块
|
### 自定义 Logger 模块
|
||||||
|
|
||||||
修改 `LoggerInjector.LOGGER_MODULE_CODE` 可以自定义日志记录逻辑。
|
修改 `LoggerInjector.LOGGER_MODULE_CODE` 和 `CALLSTACK_CLASS_CODE` 可以自定义日志记录逻辑。
|
||||||
|
|
||||||
|
### 程序化使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
from vba_test_runner import TestRunner
|
||||||
|
|
||||||
|
# 单个测试
|
||||||
|
runner = TestRunner("demo.xlsm", visible=False)
|
||||||
|
result = runner.run_test("Module1", "TestErrorProcedure")
|
||||||
|
|
||||||
|
# 全项目测试
|
||||||
|
runner = TestRunner("demo.xlsm", visible=False)
|
||||||
|
results = runner.run_all_tests()
|
||||||
|
|
||||||
|
# 发现所有测试
|
||||||
|
tests = runner.discover_all_tests()
|
||||||
|
for test in tests:
|
||||||
|
print(f"{test['module']}.{test['procedure']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发工具
|
||||||
|
|
||||||
|
### VS Code 调试配置
|
||||||
|
|
||||||
|
项目包含 `.vscode/launch.json` 调试配置,可直接在 VS Code 中调试:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python: VBA Test Runner",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/vba_test_runner.py",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"args": ["demo.xlsm", "--all"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
1. 在 VS Code 中打开项目
|
||||||
|
2. 按 `F5` 或点击调试面板
|
||||||
|
3. 选择 "Python: VBA Test Runner" 配置
|
||||||
|
4. 可以在 `launch.json` 中修改 `args` 来测试不同的场景
|
||||||
|
|
||||||
|
### 查看流程图
|
||||||
|
|
||||||
|
详细的技术流程图请查看 [vba_test_runner_flowchart.md](vba_test_runner_flowchart.md),包含:
|
||||||
|
- 系统架构概览
|
||||||
|
- 单个测试执行流程
|
||||||
|
- 全项目测试流程
|
||||||
|
- 代码编织流程
|
||||||
|
- 错误处理与调用链追踪
|
||||||
|
- 类关系图
|
||||||
|
|
||||||
|
### 实现总结
|
||||||
|
|
||||||
|
查看 [IMPLEMENTATION_SUMMARY.md](IMPLEMENTATION_SUMMARY.md) 了解:
|
||||||
|
- 项目完成状态
|
||||||
|
- 已测试的验证场景
|
||||||
|
- 关键技术实现细节
|
||||||
|
- 已解决的问题
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|
||||||
@@ -219,12 +460,37 @@ A: 系统使用热替换技术,在内存中修改代码,测试完成后使
|
|||||||
|
|
||||||
**Q: 如何测试类模块中的方法?**
|
**Q: 如何测试类模块中的方法?**
|
||||||
|
|
||||||
A: 当前版本只支持标准模块。要测试类模块,可以创建一个包装的 Sub 在标准模块中调用类方法。
|
A: 系统支持类模块(Type 2)的测试。在全项目测试模式下,类模块中的公共方法会被自动发现和测试。
|
||||||
|
|
||||||
|
**Q: 调用链是如何追踪的?**
|
||||||
|
|
||||||
|
A: 系统在每个过程入口注入 `LogEntry` 调用,在出口注入 `LogExit` 调用。`CallStack` 类模块维护一个栈结构,记录所有正在执行的过程,错误发生时可以生成完整的调用链。
|
||||||
|
|
||||||
|
**Q: 全项目测试模式和手动指定过程有什么区别?**
|
||||||
|
|
||||||
|
A: 全项目测试模式(`--all`)会:
|
||||||
|
- 自动发现所有可测试的过程
|
||||||
|
- 一次性编织所有模块的代码
|
||||||
|
- 逐个执行测试并生成汇总报告
|
||||||
|
- 更适合大规模测试和回归测试
|
||||||
|
|
||||||
|
手动指定过程模式更适合:
|
||||||
|
- 调试单个过程
|
||||||
|
- 快速验证修复
|
||||||
|
- 选择性测试某些功能
|
||||||
|
|
||||||
|
**Q: 如何在 VS Code 中调试?**
|
||||||
|
|
||||||
|
A: 项目包含 `.vscode/launch.json` 配置文件。在 VS Code 中按 `F5` 即可启动调试,可以在配置中修改测试参数。
|
||||||
|
|
||||||
**Q: 可以捕获运行时警告吗?**
|
**Q: 可以捕获运行时警告吗?**
|
||||||
|
|
||||||
A: 当前版本只捕获错误。要捕获警告,需要修改 Logger 模块来处理 `InfoMessage` 事件。
|
A: 当前版本只捕获错误。要捕获警告,需要修改 Logger 模块来处理 `InfoMessage` 事件。
|
||||||
|
|
||||||
|
**Q: 虚拟环境是必须的吗?**
|
||||||
|
|
||||||
|
A: 强烈推荐使用虚拟环境来隔离项目依赖。项目包含 `.gitignore` 规则来忽略虚拟环境目录。
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
"""
|
|
||||||
检查 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()
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
"""
|
|
||||||
创建演示用的 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()
|
|
||||||
564
docs/vba_test_runner_flowchart.md
Normal file
564
docs/vba_test_runner_flowchart.md
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
# VBA 自动化测试系统 - 流程图文档
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
1. [系统架构概览](#系统架构概览)
|
||||||
|
2. [单个测试执行流程](#单个测试执行流程)
|
||||||
|
3. [全项目测试流程](#全项目测试流程)
|
||||||
|
4. [代码编织流程](#代码编织流程)
|
||||||
|
5. [错误处理与调用链追踪](#错误处理与调用链追踪)
|
||||||
|
6. [类关系图](#类关系图)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 系统架构概览
|
||||||
|
|
||||||
|
### 整体数据流
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
A[用户输入测试请求] --> B{测试模式}
|
||||||
|
B -->|单个过程| C[run_test]
|
||||||
|
B -->|全项目| D[run_all_tests]
|
||||||
|
|
||||||
|
C --> E[TestRunner 初始化]
|
||||||
|
D --> E
|
||||||
|
|
||||||
|
E --> F[CodeWeaver 解析 VBA 模块]
|
||||||
|
F --> G[注入行号标签和错误处理]
|
||||||
|
G --> H[LoggerInjector 注入日志模块]
|
||||||
|
H --> I[热替换模块代码]
|
||||||
|
I --> J[执行 VBA 宏]
|
||||||
|
J --> K[捕获执行结果]
|
||||||
|
K --> L[返回 TestResult]
|
||||||
|
|
||||||
|
L --> M{测试成功?}
|
||||||
|
M -->|是| N[显示成功信息]
|
||||||
|
M -->|否| O[显示详细错误信息]
|
||||||
|
O --> P[包含错误位置、源代码、调用链]
|
||||||
|
|
||||||
|
N --> Q[清理资源,关闭 Excel]
|
||||||
|
P --> Q
|
||||||
|
|
||||||
|
style A fill:#e1f5ff
|
||||||
|
style N fill:#c8e6c9
|
||||||
|
style O fill:#ffcdd2
|
||||||
|
style Q fill:#fff9c4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 单个测试执行流程
|
||||||
|
|
||||||
|
### TestRunner.run_test() 详细流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start([开始: run_test]) --> OpenExcel[1. 打开 Excel 文件<br/>xw.App.visible]
|
||||||
|
OpenExcel --> ReadCode[2. 读取原始 VBA 代码<br/>_get_vba_code]
|
||||||
|
|
||||||
|
ReadCode --> WeaveCode[3. 编织模块代码<br/>weave_module_all_procedures]
|
||||||
|
WeaveCode --> ParseProcs[3.1 解析所有过程<br/>parse_procedures]
|
||||||
|
ParseProcs --> InjectLineNum[3.2 注入行号标签<br/>_inject_line_numbers]
|
||||||
|
InjectLineNum --> InjectErrHandler[3.3 注入错误处理和调用栈<br/>_inject_error_handler_with_callstack]
|
||||||
|
|
||||||
|
InjectErrHandler --> InjectLogger[4. 注入 Logger 模块<br/>inject_or_replace]
|
||||||
|
InjectLogger --> ReplaceCode[5. 热替换模块代码<br/>_replace_module_code]
|
||||||
|
ReplaceCode --> Execute[6. 执行宏<br/>_execute_macro]
|
||||||
|
|
||||||
|
Execute --> GetResult[7. 获取测试结果<br/>_get_test_result]
|
||||||
|
GetResult --> CheckResult{结果字符串}
|
||||||
|
|
||||||
|
CheckResult -->|SUCCESS| CreateSuccess[创建成功 TestResult]
|
||||||
|
CheckResult -->|ERROR| ParseError[解析错误信息字符串<br/>格式: ERROR + 模块.过程 + 行号 + 编号 + 描述 + 调用链]
|
||||||
|
|
||||||
|
ParseError --> ExtractInfo[提取错误模块、过程、行号]
|
||||||
|
ExtractInfo --> LookupSource[从 Source Map 查找源代码<br/>source_map[line]]
|
||||||
|
LookupSource --> CreateError[创建失败 TestResult]
|
||||||
|
|
||||||
|
CreateSuccess --> Cleanup[8. 清理资源<br/>_cleanup]
|
||||||
|
CreateError --> Cleanup
|
||||||
|
Cleanup --> End([结束])
|
||||||
|
|
||||||
|
异常处理[Exception] --> CreateErrorResult[创建异常 TestResult]
|
||||||
|
CreateErrorResult --> Cleanup
|
||||||
|
|
||||||
|
style Start fill:#e1f5ff
|
||||||
|
style End fill:#e1f5ff
|
||||||
|
style CreateSuccess fill:#c8e6c9
|
||||||
|
style CreateError fill:#ffcdd2
|
||||||
|
style Cleanup fill:#fff9c4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 全项目测试流程
|
||||||
|
|
||||||
|
### TestRunner.run_all_tests() 批量测试流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start([开始: run_all_tests]) --> Discover[发现所有测试<br/>discover_all_tests]
|
||||||
|
|
||||||
|
Discover --> ParseModules[解析 VBA 项目<br/>parse_modules]
|
||||||
|
ParseModules --> IterateModules[遍历所有模块]
|
||||||
|
IterateModules --> IterateProcs[遍历每个过程]
|
||||||
|
IterateProcs --> CheckEntry{是否是入口点?<br/>_is_entry_point}
|
||||||
|
|
||||||
|
CheckEntry -->|排除事件/Logger过程| Filter[过滤掉]
|
||||||
|
CheckEntry -->|是| AddToList[添加到测试列表]
|
||||||
|
|
||||||
|
AddToList --> Filter
|
||||||
|
Filter --> MoreModules{更多模块?}
|
||||||
|
MoreModules -->|是| IterateModules
|
||||||
|
MoreModules -->|否| PrintStats[打印统计信息<br/>总过程数、可测试数]
|
||||||
|
|
||||||
|
PrintStats --> OpenExcel[打开 Excel 文件]
|
||||||
|
OpenExcel --> WeaveAll[一次性编织所有模块<br/>_weave_all_modules_inplace]
|
||||||
|
|
||||||
|
WeaveAll --> InjectLoggerModule[注入 TestLogger 和 CallStack]
|
||||||
|
InjectLoggerModule --> WeaveEach[编织每个模块]
|
||||||
|
WeaveEach --> ReplaceAll[热替换所有模块代码]
|
||||||
|
|
||||||
|
ReplaceAll --> RunTests[执行所有测试]
|
||||||
|
RunTests --> TestLoop[遍历入口点测试]
|
||||||
|
|
||||||
|
TestLoop --> InitLogger[初始化 TestLogger<br/>Initialize]
|
||||||
|
InitLogger --> RunSingle[执行单个测试<br/>_run_single_test]
|
||||||
|
|
||||||
|
RunSingle --> ExecuteMacro[执行宏]
|
||||||
|
ExecuteMacro --> GetSingleResult[获取结果]
|
||||||
|
GetSingleResult --> PrintResult[打印测试结果<br/>print_test_result]
|
||||||
|
|
||||||
|
PrintResult --> MoreTests{更多测试?}
|
||||||
|
MoreTests -->|是| TestLoop
|
||||||
|
MoreTests -->|否| PrintSummary[打印测试汇总<br/>print_summary]
|
||||||
|
|
||||||
|
PrintSummary --> Cleanup[清理资源]
|
||||||
|
Cleanup --> End([结束])
|
||||||
|
|
||||||
|
style Start fill:#e1f5ff
|
||||||
|
style End fill:#e1f5ff
|
||||||
|
style PrintStats fill:#fff9c4
|
||||||
|
style PrintSummary fill:#fff9c4
|
||||||
|
style Cleanup fill:#ffe0b2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码编织流程
|
||||||
|
|
||||||
|
### CodeWeaver 代码注入详细流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start([开始: weave_procedure_with_callstack]) --> Parse[解析过程<br/>parse_procedures]
|
||||||
|
|
||||||
|
Parse --> FindProc{找到过程?}
|
||||||
|
FindProc -->|否| Error[抛出 ValueError]
|
||||||
|
FindProc -->|是| ExtractLines[提取过程代码行<br/>start_line 到 end_line]
|
||||||
|
|
||||||
|
ExtractLines --> InjectLineNums[注入行号标签<br/>_inject_line_numbers]
|
||||||
|
|
||||||
|
InjectLineNums --> InitCounter[初始化计数器 = 10]
|
||||||
|
InitCounter --> LineLoop[遍历代码行]
|
||||||
|
|
||||||
|
LineLoop --> CheckLine{检查行类型}
|
||||||
|
CheckLine -->|声明行| SkipDec[跳过]
|
||||||
|
CheckLine -->|注释行| SkipComment[跳过]
|
||||||
|
CheckLine -->|空行| SkipEmpty[跳过]
|
||||||
|
CheckLine -->|On Error| SkipOnError[跳过]
|
||||||
|
CheckLine -->|过程定义/结束| SkipProc[跳过]
|
||||||
|
CheckLine -->|可执行代码| AddLabel[添加行号标签<br/>10, 20, 30...]
|
||||||
|
|
||||||
|
AddLabel --> SaveToMap[保存到 source_map<br/>行号 -> 源代码]
|
||||||
|
SaveToMap --> Increment[计数器 += 10]
|
||||||
|
|
||||||
|
SkipDec --> MoreLines{更多行?}
|
||||||
|
SkipComment --> MoreLines
|
||||||
|
SkipEmpty --> MoreLines
|
||||||
|
SkipOnError --> MoreLines
|
||||||
|
SkipProc --> MoreLines
|
||||||
|
Increment --> MoreLines
|
||||||
|
|
||||||
|
MoreLines -->|是| LineLoop
|
||||||
|
MoreLines -->|否| InjectHandler[注入错误处理<br/>_inject_error_handler_with_callstack]
|
||||||
|
|
||||||
|
InjectHandler --> DetermineType[确定过程类型<br/>Sub or Function]
|
||||||
|
DetermineType --> RemoveExisting[删除现有 On Error]
|
||||||
|
|
||||||
|
RemoveExisting --> InsertLogEntry[在第一个可执行语句后插入:<br/>TestLogger.LogEntry<br/>On Error GoTo Handler]
|
||||||
|
|
||||||
|
InsertLogEntry --> InsertExitBlock[在 End 前插入退出块:<br/>TestLogger.LogExit<br/>TestLogger.LogSuccess<br/>Exit Sub/Function]
|
||||||
|
|
||||||
|
InsertExitBlock --> InsertHandlerBlock[插入错误处理块:<br/>Auto_Err_Handler:<br/>TestLogger.LogError<br/>TestLogger.LogExit]
|
||||||
|
|
||||||
|
InsertHandlerBlock --> ReplaceProc[替换原过程]
|
||||||
|
ReplaceProc --> Return[返回编织后的代码<br/>和 source_map]
|
||||||
|
Return --> End([结束])
|
||||||
|
|
||||||
|
Error --> End
|
||||||
|
|
||||||
|
style Start fill:#e1f5ff
|
||||||
|
style End fill:#e1f5ff
|
||||||
|
style AddLabel fill:#c8e6c9
|
||||||
|
style InsertLogEntry fill:#b3e5fc
|
||||||
|
style InsertHandlerBlock fill:#ffccbc
|
||||||
|
```
|
||||||
|
|
||||||
|
### 行号标签注入规则
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[原始 VBA 代码] --> B{行类型判断}
|
||||||
|
|
||||||
|
B --> C[声明块<br/>Dim/Private/Public/Const]
|
||||||
|
B --> D[注释行<br/>' 开头]
|
||||||
|
B --> E[空行]
|
||||||
|
B --> F[On Error 语句]
|
||||||
|
B --> G[过程定义行<br/>Sub/Function]
|
||||||
|
B --> H[过程结束行<br/>End Sub/Function]
|
||||||
|
B --> I[退出语句<br/>Exit Sub/Function]
|
||||||
|
B --> J[可执行代码]
|
||||||
|
|
||||||
|
C --> Z[跳过,不注入]
|
||||||
|
D --> Z
|
||||||
|
E --> Z
|
||||||
|
F --> Z
|
||||||
|
G --> Z
|
||||||
|
H --> Z
|
||||||
|
I --> Z
|
||||||
|
|
||||||
|
J --> K[注入行号标签<br/>10, 20, 30...]
|
||||||
|
K --> L[记录到 source_map]
|
||||||
|
|
||||||
|
style C fill:#ffcdd2
|
||||||
|
style D fill:#ffcdd2
|
||||||
|
style E fill:#ffcdd2
|
||||||
|
style F fill:#ffcdd2
|
||||||
|
style G fill:#ffcdd2
|
||||||
|
style H fill:#ffcdd2
|
||||||
|
style I fill:#ffcdd2
|
||||||
|
style J fill:#c8e6c9
|
||||||
|
style Z fill:#ffe0b2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理与调用链追踪
|
||||||
|
|
||||||
|
### TestLogger 和 CallStack 工作流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Test as 测试流程
|
||||||
|
participant Logger as TestLogger
|
||||||
|
participant Stack as CallStack
|
||||||
|
participant VBA as VBA 过程
|
||||||
|
|
||||||
|
Test->>Logger: Initialize()
|
||||||
|
Logger->>Logger: m_TestStatus = "SUCCESS"
|
||||||
|
|
||||||
|
Test->>VBA: 调用过程 A
|
||||||
|
|
||||||
|
VBA->>Logger: LogEntry("A", "Module1")
|
||||||
|
Logger->>Stack: Push("A", "Module1")
|
||||||
|
Stack->>Stack: m_Stack.Add("Module1.A")
|
||||||
|
|
||||||
|
VBA->>VBA: 执行代码 (带行号标签)
|
||||||
|
|
||||||
|
alt 过程 A 调用过程 B
|
||||||
|
VBA->>Logger: LogEntry("B", "Module1")
|
||||||
|
Logger->>Stack: Push("B", "Module1")
|
||||||
|
Stack->>Stack: m_Stack.Add("Module1.B")
|
||||||
|
|
||||||
|
VBA->>VBA: 执行代码
|
||||||
|
Note over VBA: 20: x = 1 / 0 ❌ 除零错误
|
||||||
|
|
||||||
|
VBA->>Logger: LogError("B", "Module1", 11, "Division by zero", 20)
|
||||||
|
Logger->>Logger: m_TestStatus = "ERROR"
|
||||||
|
Logger->>Stack: GetCallChain()
|
||||||
|
Stack-->>Logger: "Module1.A -> Module1.B"
|
||||||
|
Logger->>Logger: 保存错误信息
|
||||||
|
|
||||||
|
VBA->>Logger: LogExit()
|
||||||
|
Logger->>Stack: Pop()
|
||||||
|
end
|
||||||
|
|
||||||
|
VBA->>Logger: LogExit()
|
||||||
|
Logger->>Stack: Pop()
|
||||||
|
|
||||||
|
Test->>Logger: GetResult()
|
||||||
|
Logger-->>Test: "ERROR + Module1.B + 20 + 11 + Division by zero + Module1.A -> Module1.B"
|
||||||
|
|
||||||
|
Test->>Test: 解析错误信息
|
||||||
|
Test->>Test: 从 source_map[20] 获取源代码
|
||||||
|
Test-->>User: 显示详细错误报告
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误信息传递流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[VBA 运行时错误] --> B[On Error GoTo 捕获]
|
||||||
|
B --> C[跳转到 Auto_Err_Handler]
|
||||||
|
C --> D[调用 TestLogger.LogError]
|
||||||
|
|
||||||
|
D --> E[设置 m_TestStatus = ERROR]
|
||||||
|
E --> F[保存错误信息:<br/>模块、过程、行号、编号、描述]
|
||||||
|
F --> G[从 CallStack 获取调用链]
|
||||||
|
|
||||||
|
G --> H[TestRunner 调用 GetResult]
|
||||||
|
H --> I{检查 m_TestStatus}
|
||||||
|
|
||||||
|
I -->|SUCCESS| J[返回 "SUCCESS"]
|
||||||
|
I -->|ERROR| K[构造错误字符串:<br/>ERROR + 模块.过程 + 行号 + 编号 + 描述 + 调用链]
|
||||||
|
|
||||||
|
K --> L[TestRunner 解析字符串]
|
||||||
|
L --> M[提取错误模块、过程、行号]
|
||||||
|
M --> N[从 source_map 查找源代码]
|
||||||
|
N --> O[构造 TestResult 对象]
|
||||||
|
O --> P[显示详细错误报告]
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
style J fill:#c8e6c9
|
||||||
|
style K fill:#ffccbc
|
||||||
|
style P fill:#fff9c4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 类关系图
|
||||||
|
|
||||||
|
### 系统类结构与依赖关系
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class TestResult {
|
||||||
|
+str procedure_name
|
||||||
|
+bool success
|
||||||
|
+int error_number
|
||||||
|
+str error_description
|
||||||
|
+int error_line
|
||||||
|
+str source_code
|
||||||
|
+str error_module
|
||||||
|
+str call_chain
|
||||||
|
}
|
||||||
|
|
||||||
|
class CodeWeaver {
|
||||||
|
-int line_counter
|
||||||
|
+parse_modules(vba_project) Dict
|
||||||
|
+parse_procedures(code) Dict
|
||||||
|
+weave_procedure_with_callstack(code, proc_name, module_name) Tuple
|
||||||
|
+weave_module_all_procedures(code, module_name) Tuple
|
||||||
|
+weave_all_modules(modules) Dict
|
||||||
|
-_inject_line_numbers(lines) Tuple
|
||||||
|
-_inject_error_handler_with_callstack(lines, proc_name, module_name) List
|
||||||
|
-_get_component_code(component) str
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoggerInjector {
|
||||||
|
+str CALLSTACK_CLASS_CODE
|
||||||
|
+str LOGGER_MODULE_CODE
|
||||||
|
+inject_or_replace(wb, module_name) None
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestRunner {
|
||||||
|
+str file_path
|
||||||
|
+bool visible
|
||||||
|
+CodeWeaver code_weaver
|
||||||
|
+LoggerInjector logger_injector
|
||||||
|
+Dict source_map
|
||||||
|
+run_test(module_name, proc_name) TestResult
|
||||||
|
+run_all_tests() List~TestResult~
|
||||||
|
+discover_all_tests() List~Dict~
|
||||||
|
-_get_vba_code(module_name) str
|
||||||
|
-_replace_module_code(module_name, new_code) None
|
||||||
|
-_execute_macro(proc_name) None
|
||||||
|
-_get_test_result(proc_name) TestResult
|
||||||
|
-_cleanup() None
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestLogger {
|
||||||
|
<<VBA Module>>
|
||||||
|
+LogEntry(procName, moduleName)
|
||||||
|
+LogExit()
|
||||||
|
+LogError(procName, moduleName, errNum, errDesc, errLine)
|
||||||
|
+LogSuccess()
|
||||||
|
+GetResult() String
|
||||||
|
+Initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CallStack {
|
||||||
|
<<VBA Class Module>>
|
||||||
|
-Collection m_Stack
|
||||||
|
+Push(procName, moduleName)
|
||||||
|
+Pop()
|
||||||
|
+GetCallChain() String
|
||||||
|
+Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
TestRunner --> CodeWeaver : 使用
|
||||||
|
TestRunner --> LoggerInjector : 使用
|
||||||
|
TestRunner --> TestResult : 创建
|
||||||
|
LoggerInjector --> TestLogger : 注入
|
||||||
|
LoggerInjector --> CallStack : 注入
|
||||||
|
TestLogger --> CallStack : 使用
|
||||||
|
CodeWeaver --> TestResult : 生成 source_map
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模块交互时序图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User as 用户
|
||||||
|
participant Main as main()
|
||||||
|
participant Runner as TestRunner
|
||||||
|
participant Weaver as CodeWeaver
|
||||||
|
participant Injector as LoggerInjector
|
||||||
|
participant Excel as Excel Application
|
||||||
|
|
||||||
|
User->>Main: 执行命令
|
||||||
|
Main->>Runner: 初始化(file_path, visible=False)
|
||||||
|
|
||||||
|
alt 单个测试模式
|
||||||
|
Main->>Runner: run_test(module, proc)
|
||||||
|
else 全项目测试模式
|
||||||
|
Main->>Runner: run_all_tests()
|
||||||
|
Runner->>Runner: discover_all_tests()
|
||||||
|
Runner->>Runner: _weave_all_modules_inplace()
|
||||||
|
end
|
||||||
|
|
||||||
|
Runner->>Excel: 打开工作簿
|
||||||
|
Runner->>Weaver: parse_modules(vba_project)
|
||||||
|
Weaver-->>Runner: 模块信息
|
||||||
|
|
||||||
|
Runner->>Weaver: weave_module_all_procedures(code, module)
|
||||||
|
Weaver->>Weaver: _inject_line_numbers()
|
||||||
|
Weaver->>Weaver: _inject_error_handler_with_callstack()
|
||||||
|
Weaver-->>Runner: woven_code, source_map
|
||||||
|
|
||||||
|
Runner->>Injector: inject_or_replace(wb)
|
||||||
|
Injector->>Excel: 删除旧的 TestLogger/CallStack
|
||||||
|
Injector->>Excel: 添加新的 CallStack 类模块
|
||||||
|
Injector->>Excel: 添加新的 TestLogger 模块
|
||||||
|
|
||||||
|
Runner->>Excel: _replace_module_code(module, woven_code)
|
||||||
|
Runner->>Excel: _execute_macro(module.proc)
|
||||||
|
Excel-->>Runner: 执行结果
|
||||||
|
|
||||||
|
Runner->>Excel: TestLogger.GetResult()
|
||||||
|
Excel-->>Runner: "SUCCESS" 或 "ERROR|..."
|
||||||
|
|
||||||
|
Runner-->>Main: TestResult
|
||||||
|
Main-->>User: 打印测试结果
|
||||||
|
|
||||||
|
Runner->>Excel: _cleanup()
|
||||||
|
Excel->>Excel: 关闭工作簿(不保存)
|
||||||
|
Excel->>Excel: 退出 Excel
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录:关键数据结构
|
||||||
|
|
||||||
|
### TestResult 数据类
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[TestResult 数据类] --> B[procedure_name: str<br/>被测试的过程名称]
|
||||||
|
A --> C[success: bool<br/>测试是否成功]
|
||||||
|
A --> D[error_number: int<br/>VBA 错误编号]
|
||||||
|
A --> E[error_description: str<br/>错误描述信息]
|
||||||
|
A --> F[error_line: int<br/>错误发生的行号标签]
|
||||||
|
A --> G[source_code: str<br/>错误行的原始源代码]
|
||||||
|
A --> H[error_module: str<br/>错误发生的模块名]
|
||||||
|
A --> I[call_chain: str<br/>完整调用链<br/>Module1.ProcA -> Module1.ProcB]
|
||||||
|
|
||||||
|
style A fill:#e1f5ff
|
||||||
|
style C fill:#c8e6c9
|
||||||
|
style F fill:#fff9c4
|
||||||
|
style G fill:#fff9c4
|
||||||
|
style I fill:#ffccbc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Map 映射关系
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[原始 VBA 代码] -->|CodeWeaver 处理| B[编织后的代码]
|
||||||
|
|
||||||
|
B --> C[行号标签 10<br/>x = 1]
|
||||||
|
B --> D[行号标签 20<br/>y = x / 0 ❌]
|
||||||
|
B --> E[行号标签 30<br/>z = y + 1]
|
||||||
|
|
||||||
|
C --> F[source_map[10] = 'x = 1']
|
||||||
|
D --> G[source_map[20] = 'y = x / 0']
|
||||||
|
E --> H[source_map[30] = 'z = y + 1']
|
||||||
|
|
||||||
|
G --> I[VBA 错误发生在行 20]
|
||||||
|
I --> J[通过 source_map[20]<br/>获取原始源代码]
|
||||||
|
J --> K[显示给用户:<br/>y = x / 0]
|
||||||
|
|
||||||
|
style A fill:#e1f5ff
|
||||||
|
style B fill:#c8e6c9
|
||||||
|
style G fill:#ffcdd2
|
||||||
|
style K fill:#fff9c4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用示例流程
|
||||||
|
|
||||||
|
### 命令行使用模式
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start([用户执行命令]) --> CheckArgs{参数检查}
|
||||||
|
|
||||||
|
CheckArgs -->|python vba_test_runner.py| ShowHelp1[显示用法1]
|
||||||
|
CheckArgs -->|python vba_test_runner.py file.xlsm --all| AllTest[全项目测试模式]
|
||||||
|
CheckArgs -->|python vba_test_runner.py file.xlsm Module Proc1 Proc2| SingleTest[单过程/多过程测试模式]
|
||||||
|
|
||||||
|
ShowHelp1 --> End1([结束])
|
||||||
|
|
||||||
|
AllTest --> InitRunner1[初始化 TestRunner<br/>visible=False]
|
||||||
|
InitRunner1 --> RunAll[执行 run_all_tests]
|
||||||
|
RunAll --> Discover[发现所有过程]
|
||||||
|
Discover --> WeaveAllModules[编织所有模块]
|
||||||
|
WeaveAllModules --> BatchExecute[批量执行测试]
|
||||||
|
BatchExecute --> ShowSummary[显示测试汇总]
|
||||||
|
ShowSummary --> Cleanup1[清理资源]
|
||||||
|
Cleanup1 --> End2([结束])
|
||||||
|
|
||||||
|
SingleTest --> InitRunner2[初始化 TestRunner<br/>visible=False]
|
||||||
|
InitRunner2 --> LoopProcs[循环遍历过程列表]
|
||||||
|
LoopProcs --> RunSingle[执行 run_test]
|
||||||
|
RunSingle --> ShowSingleResult[显示单个结果]
|
||||||
|
ShowSingleResult --> MoreProcs{更多过程?}
|
||||||
|
MoreProcs -->|是| LoopProcs
|
||||||
|
MoreProcs -->|否| ShowSummary2[显示测试汇总]
|
||||||
|
ShowSummary2 --> Cleanup2[清理资源]
|
||||||
|
Cleanup2 --> End3([结束])
|
||||||
|
|
||||||
|
style Start fill:#e1f5ff
|
||||||
|
style End1 fill:#e1f5ff
|
||||||
|
style End2 fill:#e1f5ff
|
||||||
|
style End3 fill:#e1f5ff
|
||||||
|
style AllTest fill:#c8e6c9
|
||||||
|
style SingleTest fill:#b3e5fc
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
该 VBA 自动化测试系统通过**代码编织技术**实现了:
|
||||||
|
|
||||||
|
1. **非侵入式测试**:在内存中修改代码,不保存到磁盘
|
||||||
|
2. **精确行号定位**:通过行号标签和 Source Map 实现源代码映射
|
||||||
|
3. **调用链追踪**:通过 CallStack 类追踪完整调用路径
|
||||||
|
4. **批量测试支持**:支持单过程和全项目两种测试模式
|
||||||
|
5. **详细错误报告**:包含错误位置、源代码、调用链等完整信息
|
||||||
|
|
||||||
|
系统的核心创新在于将编译器中的**代码编织技术**应用于 VBA 动态测试,通过在运行时注入检测代码来实现传统 IDE 无法提供的调试功能。
|
||||||
@@ -19,6 +19,8 @@ class TestResult:
|
|||||||
error_description: str = ""
|
error_description: str = ""
|
||||||
error_line: int = 0
|
error_line: int = 0
|
||||||
source_code: str = ""
|
source_code: str = ""
|
||||||
|
error_module: str = "" # 新增:错误发生的模块
|
||||||
|
call_chain: str = "" # 新增:完整调用链
|
||||||
|
|
||||||
|
|
||||||
class CodeWeaver:
|
class CodeWeaver:
|
||||||
@@ -27,6 +29,41 @@ class CodeWeaver:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.line_counter = 10 # 行号标签从 10 开始,步长 10
|
self.line_counter = 10 # 行号标签从 10 开始,步长 10
|
||||||
|
|
||||||
|
def parse_modules(self, vba_project) -> Dict[str, Dict]:
|
||||||
|
"""
|
||||||
|
解析VBA项目中的所有模块
|
||||||
|
|
||||||
|
返回: {
|
||||||
|
模块名: {
|
||||||
|
'type': 'StdModule' | 'ClassModule',
|
||||||
|
'code': str,
|
||||||
|
'procedures': {过程名: {...过程信息}},
|
||||||
|
'component': VBComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
modules = {}
|
||||||
|
for component in vba_project.VBComponents:
|
||||||
|
# 只处理标准模块和类模块
|
||||||
|
if component.Type in [1, 2]: # 1=StdModule, 2=ClassModule
|
||||||
|
module_name = component.Name
|
||||||
|
code = self._get_component_code(component)
|
||||||
|
procedures = self.parse_procedures(code)
|
||||||
|
|
||||||
|
modules[module_name] = {
|
||||||
|
'type': 'StdModule' if component.Type == 1 else 'ClassModule',
|
||||||
|
'code': code,
|
||||||
|
'procedures': procedures,
|
||||||
|
'component': component
|
||||||
|
}
|
||||||
|
return modules
|
||||||
|
|
||||||
|
def _get_component_code(self, component) -> str:
|
||||||
|
"""从VBComponent获取代码"""
|
||||||
|
code_module = component.CodeModule
|
||||||
|
line_count = code_module.CountOfLines
|
||||||
|
return code_module.Lines(1, line_count) if line_count > 0 else ""
|
||||||
|
|
||||||
def parse_procedures(self, code: str) -> Dict[str, Dict]:
|
def parse_procedures(self, code: str) -> Dict[str, Dict]:
|
||||||
"""
|
"""
|
||||||
解析 VBA 代码中的所有 Sub/Function
|
解析 VBA 代码中的所有 Sub/Function
|
||||||
@@ -75,6 +112,84 @@ class CodeWeaver:
|
|||||||
|
|
||||||
return procedures
|
return procedures
|
||||||
|
|
||||||
|
def weave_procedure_with_callstack(self, code: str, proc_name: str, module_name: str) -> Tuple[str, Dict[int, str]]:
|
||||||
|
"""
|
||||||
|
为指定过程注入调用栈管理、行号和错误处理(阶段1增强版)
|
||||||
|
|
||||||
|
返回: (编织后的代码, {行号: 源代码})
|
||||||
|
"""
|
||||||
|
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_with_callstack(woven_lines, proc_name, module_name)
|
||||||
|
|
||||||
|
# 替换原代码中的过程
|
||||||
|
result_lines = (
|
||||||
|
lines[:proc_info['start_line']] +
|
||||||
|
woven_lines +
|
||||||
|
lines[proc_info['end_line'] + 1:]
|
||||||
|
)
|
||||||
|
|
||||||
|
return '\n'.join(result_lines), source_map
|
||||||
|
|
||||||
|
def weave_module_all_procedures(self, code: str, module_name: str) -> Tuple[str, Dict[int, str]]:
|
||||||
|
"""
|
||||||
|
为单个模块中的所有过程注入调用栈管理和错误处理
|
||||||
|
|
||||||
|
这对于调用链追踪至关重要:当测试一个过程时,
|
||||||
|
该过程调用的其他过程也需要被编织,以便记录 LogEntry/LogExit
|
||||||
|
|
||||||
|
返回: (编织后的代码, {行号: 源代码})
|
||||||
|
"""
|
||||||
|
procedures = self.parse_procedures(code)
|
||||||
|
woven_code = code
|
||||||
|
source_map = {}
|
||||||
|
|
||||||
|
# 为模块中的每个过程注入代码
|
||||||
|
for proc_name in procedures.keys():
|
||||||
|
woven_code, proc_source_map = self.weave_procedure_with_callstack(
|
||||||
|
woven_code, proc_name, module_name
|
||||||
|
)
|
||||||
|
source_map.update(proc_source_map)
|
||||||
|
|
||||||
|
return woven_code, source_map
|
||||||
|
|
||||||
|
def weave_all_modules(self, modules: Dict[str, Dict]) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
为所有模块中的所有过程注入调用栈管理和错误处理
|
||||||
|
|
||||||
|
返回: {模块名: 编织后的代码}
|
||||||
|
"""
|
||||||
|
woven_modules = {}
|
||||||
|
|
||||||
|
for module_name, module_info in modules.items():
|
||||||
|
original_code = module_info['code']
|
||||||
|
woven_code = original_code
|
||||||
|
source_map = {}
|
||||||
|
|
||||||
|
# 为每个过程注入代码
|
||||||
|
for proc_name in module_info['procedures'].keys():
|
||||||
|
woven_code, proc_source_map = self.weave_procedure_with_callstack(
|
||||||
|
woven_code, proc_name, module_name
|
||||||
|
)
|
||||||
|
source_map.update(proc_source_map)
|
||||||
|
|
||||||
|
woven_modules[module_name] = woven_code
|
||||||
|
|
||||||
|
return woven_modules
|
||||||
|
|
||||||
def weave_procedure(self, code: str, proc_name: str) -> Tuple[str, Dict[int, str]]:
|
def weave_procedure(self, code: str, proc_name: str) -> Tuple[str, Dict[int, str]]:
|
||||||
"""
|
"""
|
||||||
为指定过程注入行号和错误处理
|
为指定过程注入行号和错误处理
|
||||||
@@ -161,6 +276,87 @@ class CodeWeaver:
|
|||||||
|
|
||||||
return result_lines, source_map
|
return result_lines, source_map
|
||||||
|
|
||||||
|
def _inject_error_handler_with_callstack(self, lines: List[str], proc_name: str, module_name: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
在过程开头添加: On Error GoTo Auto_Err_Handler
|
||||||
|
注入调用栈管理: LogEntry/LogExit
|
||||||
|
删除任何现有的 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
|
||||||
|
|
||||||
|
# 在过程定义后插入 LogEntry 和 On Error 语句
|
||||||
|
inserted_log_entry = False
|
||||||
|
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.LogExit()")
|
||||||
|
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}\", \"{module_name}\", Err.Number, Err.Description, Erl)")
|
||||||
|
result_lines.append(f" Call TestLogger.LogExit()")
|
||||||
|
result_lines.append(line) # 添加 End Sub/Function
|
||||||
|
error_handler_inserted = True
|
||||||
|
elif is_end:
|
||||||
|
# 跳过后续的 End 语句(不应该有)
|
||||||
|
pass
|
||||||
|
elif exit_pattern.match(line):
|
||||||
|
# 跳过现有的 Exit 语句,替换为 LogExit + Exit
|
||||||
|
result_lines.append(f" Call TestLogger.LogExit()")
|
||||||
|
result_lines.append(line)
|
||||||
|
else:
|
||||||
|
# 在第一个非声明、非空行后插入 LogEntry 和 On Error
|
||||||
|
if not inserted_log_entry 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" Call TestLogger.LogEntry(\"{proc_name}\", \"{module_name}\")")
|
||||||
|
result_lines.append(f" On Error GoTo Auto_Err_Handler_{proc_name}")
|
||||||
|
inserted_log_entry = True
|
||||||
|
inserted_error_handler = True
|
||||||
|
|
||||||
|
result_lines.append(line)
|
||||||
|
|
||||||
|
# 如果没有找到 End,追加到末尾
|
||||||
|
if not error_handler_inserted:
|
||||||
|
result_lines.append(f"")
|
||||||
|
result_lines.append(f" Call TestLogger.LogExit()")
|
||||||
|
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}\", \"{module_name}\", Err.Number, Err.Description, Erl)")
|
||||||
|
result_lines.append(f" Call TestLogger.LogExit()")
|
||||||
|
|
||||||
|
return result_lines
|
||||||
|
|
||||||
def _inject_error_handler(self, lines: List[str], proc_name: str) -> List[str]:
|
def _inject_error_handler(self, lines: List[str], proc_name: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
在过程开头添加: On Error GoTo Auto_Err_Handler
|
在过程开头添加: On Error GoTo Auto_Err_Handler
|
||||||
@@ -235,58 +431,130 @@ class CodeWeaver:
|
|||||||
|
|
||||||
|
|
||||||
class LoggerInjector:
|
class LoggerInjector:
|
||||||
"""日志模块注入器 - 向 Excel 项目注入 _TestLogger 辅助模块"""
|
"""日志模块注入器 - 向 Excel 项目注入 TestLogger 辅助模块和 CallStack 类模块"""
|
||||||
|
|
||||||
|
CALLSTACK_CLASS_CODE = """Option Explicit
|
||||||
|
|
||||||
|
Private m_Stack As Collection
|
||||||
|
|
||||||
|
Private Sub Class_Initialize()
|
||||||
|
Set m_Stack = New Collection
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Public Sub Push(ByVal procName As String, ByVal moduleName As String)
|
||||||
|
m_Stack.Add moduleName & "." & procName
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Public Sub Pop()
|
||||||
|
If m_Stack.Count > 0 Then m_Stack.Remove m_Stack.Count
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Public Function GetCallChain() As String
|
||||||
|
Dim i As Integer
|
||||||
|
Dim parts() As String
|
||||||
|
ReDim parts(1 To m_Stack.Count)
|
||||||
|
|
||||||
|
For i = 1 To m_Stack.Count
|
||||||
|
parts(i) = m_Stack(i)
|
||||||
|
Next i
|
||||||
|
|
||||||
|
GetCallChain = Join(parts, " -> ")
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Public Sub Clear()
|
||||||
|
Set m_Stack = New Collection
|
||||||
|
End Sub
|
||||||
|
"""
|
||||||
|
|
||||||
LOGGER_MODULE_CODE = """Option Explicit
|
LOGGER_MODULE_CODE = """Option Explicit
|
||||||
|
|
||||||
' Module-level variables to store test result
|
' Module-level variables to store test result
|
||||||
Private m_TestStatus As String
|
Private m_TestStatus As String
|
||||||
|
Private m_ErrModule As String
|
||||||
Private m_ErrProcedure As String
|
Private m_ErrProcedure As String
|
||||||
Private m_ErrNumber As Long
|
Private m_ErrNumber As Long
|
||||||
Private m_ErrDescription As String
|
Private m_ErrDescription As String
|
||||||
Private m_ErrLine As Long
|
Private m_ErrLine As Long
|
||||||
|
Private m_CallChain As String
|
||||||
|
Private m_CallStack As CallStack
|
||||||
|
|
||||||
Sub LogError(ByVal procName As String, ByVal errNum As Long, _
|
Sub LogEntry(ByVal procName As String, ByVal moduleName As String)
|
||||||
ByVal errDesc As String, ByVal errLine As Long)
|
If m_CallStack Is Nothing Then Set m_CallStack = New CallStack
|
||||||
|
m_CallStack.Push procName, moduleName
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Sub LogExit()
|
||||||
|
If Not m_CallStack Is Nothing Then m_CallStack.Pop
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Sub LogError(ByVal procName As String, ByVal moduleName As String, _
|
||||||
|
ByVal errNum As Long, ByVal errDesc As String, ByVal errLine As Long)
|
||||||
m_TestStatus = "ERROR"
|
m_TestStatus = "ERROR"
|
||||||
|
m_ErrModule = moduleName
|
||||||
m_ErrProcedure = procName
|
m_ErrProcedure = procName
|
||||||
m_ErrNumber = errNum
|
m_ErrNumber = errNum
|
||||||
m_ErrDescription = errDesc
|
m_ErrDescription = errDesc
|
||||||
m_ErrLine = errLine
|
m_ErrLine = errLine
|
||||||
|
If Not m_CallStack Is Nothing Then
|
||||||
|
m_CallChain = m_CallStack.GetCallChain()
|
||||||
|
Else
|
||||||
|
m_CallChain = ""
|
||||||
|
End If
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Sub LogSuccess()
|
Sub LogSuccess()
|
||||||
m_TestStatus = "SUCCESS"
|
' 只有在没有错误时才设置为成功
|
||||||
m_ErrProcedure = ""
|
' 这确保子过程的错误不会被父过程覆盖
|
||||||
m_ErrNumber = 0
|
If m_TestStatus <> "ERROR" Then
|
||||||
m_ErrDescription = ""
|
m_TestStatus = "SUCCESS"
|
||||||
m_ErrLine = 0
|
End If
|
||||||
|
' 只有在没有错误时才清空调用栈
|
||||||
|
If m_TestStatus <> "ERROR" Then
|
||||||
|
If Not m_CallStack Is Nothing Then m_CallStack.Clear
|
||||||
|
End If
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Function GetResult() As String
|
Function GetResult() As String
|
||||||
If m_TestStatus = "SUCCESS" Then
|
If m_TestStatus = "SUCCESS" Then
|
||||||
GetResult = "SUCCESS"
|
GetResult = "SUCCESS"
|
||||||
Else
|
Else
|
||||||
GetResult = "ERROR|" & m_ErrProcedure & "|" & m_ErrLine & "|" & _
|
GetResult = "ERROR|" & m_ErrModule & "." & m_ErrProcedure & "|" & _
|
||||||
m_ErrNumber & "|" & m_ErrDescription
|
m_ErrLine & "|" & m_ErrNumber & "|" & m_ErrDescription & "|" & _
|
||||||
|
m_CallChain
|
||||||
End If
|
End If
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
|
Sub Initialize()
|
||||||
|
m_TestStatus = "SUCCESS"
|
||||||
|
If Not m_CallStack Is Nothing Then m_CallStack.Clear
|
||||||
|
End Sub
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def inject_or_replace(self, wb, module_name: str = "TestLogger") -> None:
|
def inject_or_replace(self, wb, module_name: str = "TestLogger") -> None:
|
||||||
"""
|
"""
|
||||||
如果模块存在则删除,然后注入新的 Logger 模块
|
如果模块存在则删除,然后注入新的 Logger 模块和 CallStack 类模块
|
||||||
"""
|
"""
|
||||||
# 获取 VBA 项目
|
# 获取 VBA 项目
|
||||||
vba_project = wb.api.VBProject
|
vba_project = wb.api.VBProject
|
||||||
|
|
||||||
# 检查模块是否存在,存在则删除
|
# 检查并删除旧的 CallStack 类模块
|
||||||
|
for component in vba_project.VBComponents:
|
||||||
|
if component.Name == "CallStack":
|
||||||
|
vba_project.VBComponents.Remove(component)
|
||||||
|
break
|
||||||
|
|
||||||
|
# 注入 CallStack 类模块
|
||||||
|
callstack_module = vba_project.VBComponents.Add(2) # 2 = vbext_ct_ClassModule
|
||||||
|
callstack_module.Name = "CallStack"
|
||||||
|
callstack_module.CodeModule.AddFromString(self.CALLSTACK_CLASS_CODE)
|
||||||
|
|
||||||
|
# 检查并删除旧的 Logger 模块
|
||||||
for component in vba_project.VBComponents:
|
for component in vba_project.VBComponents:
|
||||||
if component.Name == module_name:
|
if component.Name == module_name:
|
||||||
vba_project.VBComponents.Remove(component)
|
vba_project.VBComponents.Remove(component)
|
||||||
break
|
break
|
||||||
|
|
||||||
# 注入新模块
|
# 注入新 Logger 模块
|
||||||
new_module = vba_project.VBComponents.Add(1) # 1 = vbext_ct_StdModule
|
new_module = vba_project.VBComponents.Add(1) # 1 = vbext_ct_StdModule
|
||||||
new_module.Name = module_name
|
new_module.Name = module_name
|
||||||
new_module.CodeModule.AddFromString(self.LOGGER_MODULE_CODE)
|
new_module.CodeModule.AddFromString(self.LOGGER_MODULE_CODE)
|
||||||
@@ -331,9 +599,10 @@ class TestRunner:
|
|||||||
# 2. 读取原始 VBA 代码
|
# 2. 读取原始 VBA 代码
|
||||||
original_code = self._get_vba_code(module_name)
|
original_code = self._get_vba_code(module_name)
|
||||||
|
|
||||||
# 3. 编织代码
|
# 3. 编织整个模块的所有过程(以支持调用链追踪)
|
||||||
woven_code, self.source_map = self.code_weaver.weave_procedure(
|
# 注意:为了追踪调用链,我们需要编织模块中的所有过程
|
||||||
original_code, proc_name
|
woven_code, self.source_map = self.code_weaver.weave_module_all_procedures(
|
||||||
|
original_code, module_name
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4. 注入 Logger 模块
|
# 4. 注入 Logger 模块
|
||||||
@@ -357,7 +626,9 @@ class TestRunner:
|
|||||||
error_number=0,
|
error_number=0,
|
||||||
error_description=f"Test execution failed: {str(e)}",
|
error_description=f"Test execution failed: {str(e)}",
|
||||||
error_line=0,
|
error_line=0,
|
||||||
source_code=""
|
source_code="",
|
||||||
|
error_module="",
|
||||||
|
call_chain=""
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
# 8. 关闭工作簿(不保存)
|
# 8. 关闭工作簿(不保存)
|
||||||
@@ -400,7 +671,7 @@ class TestRunner:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _get_test_result(self, proc_name: str) -> TestResult:
|
def _get_test_result(self, proc_name: str) -> TestResult:
|
||||||
"""从 Logger 模块获取测试结果"""
|
"""从 Logger 模块获取测试结果(增强版,支持调用链)"""
|
||||||
try:
|
try:
|
||||||
result_str = self.app.api.Run("TestLogger.GetResult")
|
result_str = self.app.api.Run("TestLogger.GetResult")
|
||||||
|
|
||||||
@@ -410,24 +681,33 @@ class TestRunner:
|
|||||||
success=True
|
success=True
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 解析错误信息: ERROR|ProcedureName|Line|Number|Description
|
# 解析错误信息: ERROR|Module.Proc|Line|Number|Desc|CallChain
|
||||||
parts = result_str.split('|')
|
parts = result_str.split('|')
|
||||||
if len(parts) >= 5:
|
if len(parts) >= 6:
|
||||||
error_proc_name = parts[1]
|
error_module_proc = parts[1]
|
||||||
error_line = int(parts[2])
|
error_line = int(parts[2])
|
||||||
error_number = int(parts[3])
|
error_number = int(parts[3])
|
||||||
error_desc = parts[4]
|
error_desc = parts[4]
|
||||||
|
call_chain = parts[5]
|
||||||
|
|
||||||
|
# 分离模块和过程名
|
||||||
|
error_module = ""
|
||||||
|
error_proc = error_module_proc
|
||||||
|
if '.' in error_module_proc:
|
||||||
|
error_module, error_proc = error_module_proc.split('.', 1)
|
||||||
|
|
||||||
# 从 Source Map 获取源代码
|
# 从 Source Map 获取源代码
|
||||||
source_code = self.source_map.get(error_line, "Source code not found")
|
source_code = self.source_map.get(error_line, "Source code not found")
|
||||||
|
|
||||||
return TestResult(
|
return TestResult(
|
||||||
procedure_name=error_proc_name,
|
procedure_name=error_proc,
|
||||||
success=False,
|
success=False,
|
||||||
error_number=error_number,
|
error_number=error_number,
|
||||||
error_description=error_desc,
|
error_description=error_desc,
|
||||||
error_line=error_line,
|
error_line=error_line,
|
||||||
source_code=source_code
|
source_code=source_code,
|
||||||
|
error_module=error_module,
|
||||||
|
call_chain=call_chain
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return TestResult(
|
return TestResult(
|
||||||
@@ -436,7 +716,9 @@ class TestRunner:
|
|||||||
error_number=0,
|
error_number=0,
|
||||||
error_description=f"Failed to get test result: {str(e)}",
|
error_description=f"Failed to get test result: {str(e)}",
|
||||||
error_line=0,
|
error_line=0,
|
||||||
source_code=""
|
source_code="",
|
||||||
|
error_module="",
|
||||||
|
call_chain=""
|
||||||
)
|
)
|
||||||
|
|
||||||
def _cleanup(self):
|
def _cleanup(self):
|
||||||
@@ -449,18 +731,163 @@ class TestRunner:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# ==================== Phase 2: 全项目测试方法 ====================
|
||||||
|
|
||||||
|
def discover_all_tests(self) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
发现所有可测试的过程
|
||||||
|
|
||||||
|
返回: [
|
||||||
|
{
|
||||||
|
'module': str,
|
||||||
|
'procedure': str,
|
||||||
|
'type': 'Sub' | 'Function',
|
||||||
|
'is_entry_point': bool
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
# 打开Excel(如果未打开)
|
||||||
|
if not self.wb:
|
||||||
|
self.app = xw.App(visible=self.visible)
|
||||||
|
self.wb = self.app.books.open(self.file_path)
|
||||||
|
|
||||||
|
vba_project = self.wb.api.VBProject
|
||||||
|
modules = self.code_weaver.parse_modules(vba_project)
|
||||||
|
|
||||||
|
all_tests = []
|
||||||
|
for module_name, module_info in modules.items():
|
||||||
|
for proc_name, proc_info in module_info['procedures'].items():
|
||||||
|
# 判断是否是入口点
|
||||||
|
is_entry_point = self._is_entry_point(module_name, proc_name)
|
||||||
|
|
||||||
|
all_tests.append({
|
||||||
|
'module': module_name,
|
||||||
|
'procedure': proc_name,
|
||||||
|
'type': proc_info['type'],
|
||||||
|
'is_entry_point': is_entry_point
|
||||||
|
})
|
||||||
|
|
||||||
|
return all_tests
|
||||||
|
|
||||||
|
def _is_entry_point(self, module_name: str, proc_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
判断过程是否可以作为测试入口点
|
||||||
|
|
||||||
|
规则:
|
||||||
|
1. 过程名不以"Worksheet_"、"Workbook_"开头(排除事件过程)
|
||||||
|
2. 过程名不以"Class_"开头(排除类模块内部方法,可选)
|
||||||
|
"""
|
||||||
|
# 排除事件过程
|
||||||
|
if proc_name.startswith(('Worksheet_', 'Workbook_', 'Document_', 'Class_')):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 排除Logger相关过程
|
||||||
|
if proc_name.startswith(('LogEntry', 'LogExit', 'LogError', 'LogSuccess', 'GetResult', 'Initialize')):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 简化实现:假设所有非事件过程都是入口点
|
||||||
|
return True
|
||||||
|
|
||||||
|
def run_all_tests(self) -> List[TestResult]:
|
||||||
|
"""
|
||||||
|
执行全项目测试
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
所有测试结果的列表
|
||||||
|
"""
|
||||||
|
# 发现所有测试
|
||||||
|
all_tests = self.discover_all_tests()
|
||||||
|
entry_tests = [t for t in all_tests if t['is_entry_point']]
|
||||||
|
|
||||||
|
print(f"发现 {len(entry_tests)} 个可测试的入口点")
|
||||||
|
print(f"总过程数: {len(all_tests)}")
|
||||||
|
|
||||||
|
# 打开Excel(如果未打开)
|
||||||
|
if not self.wb:
|
||||||
|
self.app = xw.App(visible=self.visible)
|
||||||
|
self.wb = self.app.books.open(self.file_path)
|
||||||
|
|
||||||
|
# 一次性编织所有模块
|
||||||
|
self._weave_all_modules_inplace()
|
||||||
|
|
||||||
|
# 执行所有测试
|
||||||
|
results = []
|
||||||
|
for i, test in enumerate(entry_tests, 1):
|
||||||
|
print(f"\n[{i}/{len(entry_tests)}] 测试 {test['module']}.{test['procedure']}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self._run_single_test(test['module'], test['procedure'])
|
||||||
|
results.append(result)
|
||||||
|
print_test_result(result)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [ERROR] 测试执行失败: {str(e)}")
|
||||||
|
results.append(TestResult(
|
||||||
|
procedure_name=test['procedure'],
|
||||||
|
success=False,
|
||||||
|
error_description=f"Test execution failed: {str(e)}"
|
||||||
|
))
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _weave_all_modules_inplace(self):
|
||||||
|
"""在Excel中直接编织所有模块"""
|
||||||
|
vba_project = self.wb.api.VBProject
|
||||||
|
modules = self.code_weaver.parse_modules(vba_project)
|
||||||
|
|
||||||
|
# 注入增强的Logger模块和CallStack类
|
||||||
|
self.logger_injector.inject_or_replace(self.wb)
|
||||||
|
|
||||||
|
# 编织所有模块
|
||||||
|
woven_modules = self.code_weaver.weave_all_modules(modules)
|
||||||
|
|
||||||
|
# 热替换所有模块代码
|
||||||
|
for module_name, woven_code in woven_modules.items():
|
||||||
|
try:
|
||||||
|
self._replace_module_code(module_name, woven_code)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" 警告: 无法编织模块 {module_name}: {str(e)}")
|
||||||
|
|
||||||
|
def _run_single_test(self, module_name: str, proc_name: str) -> TestResult:
|
||||||
|
"""执行单个测试(用于批量测试)"""
|
||||||
|
# 初始化Logger
|
||||||
|
try:
|
||||||
|
self.app.api.Run("TestLogger.Initialize")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 执行宏
|
||||||
|
try:
|
||||||
|
self._execute_macro(f"{module_name}.{proc_name}")
|
||||||
|
except:
|
||||||
|
pass # 错误会被VBA错误处理捕获
|
||||||
|
|
||||||
|
# 获取结果
|
||||||
|
return self._get_test_result(proc_name)
|
||||||
|
|
||||||
|
|
||||||
def print_test_result(result: TestResult, index: int = None, total: int = None):
|
def print_test_result(result: TestResult, index: int = None, total: int = None):
|
||||||
"""打印单个测试结果"""
|
"""打印单个测试结果(增强版,支持调用链)"""
|
||||||
prefix = f"[{index}/{total}] " if index and total else ""
|
prefix = f"[{index}/{total}] " if index and total else ""
|
||||||
|
|
||||||
if result.success:
|
if result.success:
|
||||||
print(f"{prefix}[PASS] {result.procedure_name} - Test Passed")
|
print(f"{prefix}[PASS] {result.procedure_name}")
|
||||||
else:
|
else:
|
||||||
print(f"{prefix}[FAIL] {result.procedure_name} - Test Failed")
|
print(f"{prefix}[FAIL] {result.procedure_name}")
|
||||||
print(f" Error Description: {result.error_description}")
|
print(f" Error: {result.error_description}")
|
||||||
print(f" Error Line: {result.error_line}")
|
|
||||||
print(f" Source Code: {result.source_code}")
|
# 显示位置信息
|
||||||
|
if result.error_module:
|
||||||
|
print(f" Location: {result.error_module}.{result.procedure_name}:{result.error_line}")
|
||||||
|
else:
|
||||||
|
print(f" Location: {result.procedure_name}:{result.error_line}")
|
||||||
|
|
||||||
|
# 显示调用链
|
||||||
|
if result.call_chain:
|
||||||
|
print(f" Call Chain: {result.call_chain}")
|
||||||
|
|
||||||
|
# 显示源代码
|
||||||
|
print(f" Source: {result.source_code}")
|
||||||
|
|
||||||
|
|
||||||
def print_summary(results: List[TestResult]):
|
def print_summary(results: List[TestResult]):
|
||||||
@@ -475,27 +902,57 @@ def print_summary(results: List[TestResult]):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""主入口函数"""
|
"""主入口函数(支持两种模式:全项目测试和指定过程测试)"""
|
||||||
if len(sys.argv) < 3:
|
if len(sys.argv) < 2:
|
||||||
print("Usage: python vba_test_runner.py <excel_file> <module_name> <procedure1> [procedure2] ...")
|
print("用法1: python vba_test_runner.py <excel_file> --all")
|
||||||
print("Example: python vba_test_runner.py demo.xlsm Module1 TestErrorProcedure")
|
print("用法2: python vba_test_runner.py <excel_file> <module_name> <procedure1> [procedure2] ...")
|
||||||
|
print("示例1: python vba_test_runner.py demo.xlsm --all")
|
||||||
|
print("示例2: python vba_test_runner.py demo.xlsm Module1 TestErrorProcedure")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
excel_file = sys.argv[1]
|
excel_file = sys.argv[1]
|
||||||
module_name = sys.argv[2]
|
|
||||||
procedures = sys.argv[3:]
|
|
||||||
|
|
||||||
print(f"===== VBA Batch Test Results =====\n")
|
# 模式1:全项目测试
|
||||||
|
if len(sys.argv) == 2 and sys.argv[1] == "--all":
|
||||||
results = []
|
# 这种情况应该是: python vba_test_runner.py --all
|
||||||
|
# 但实际上应该是: python vba_test_runner.py <file> --all
|
||||||
for i, proc_name in enumerate(procedures, 1):
|
print("错误: 缺少Excel文件路径")
|
||||||
|
print("正确用法: python vba_test_runner.py <excel_file> --all")
|
||||||
|
sys.exit(1)
|
||||||
|
elif len(sys.argv) >= 3 and sys.argv[2] == "--all":
|
||||||
|
print("===== VBA 全项目自动化测试 =====\n")
|
||||||
runner = TestRunner(excel_file, visible=False)
|
runner = TestRunner(excel_file, visible=False)
|
||||||
result = runner.run_test(module_name, proc_name)
|
try:
|
||||||
results.append(result)
|
results = runner.run_all_tests()
|
||||||
print_test_result(result, i, len(procedures))
|
print_summary(results)
|
||||||
|
finally:
|
||||||
|
runner._cleanup()
|
||||||
|
|
||||||
print_summary(results)
|
# 模式2:指定过程测试(保持向后兼容)
|
||||||
|
elif len(sys.argv) >= 3:
|
||||||
|
module_name = sys.argv[2]
|
||||||
|
procedures = sys.argv[3:]
|
||||||
|
|
||||||
|
if not procedures:
|
||||||
|
print("错误: 必须指定至少一个过程名")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"===== VBA 批量测试结果 =====\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)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("错误: 无效的参数")
|
||||||
|
print("用法1: python vba_test_runner.py <excel_file> --all")
|
||||||
|
print("用法2: python vba_test_runner.py <excel_file> <module_name> <procedure1> [procedure2] ...")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user