Compare commits

..

5 Commits

Author SHA1 Message Date
Misaka
d9e11ff7dd chore: reorganize documentation structure
- Move vba_test_runner_flowchart.md to docs/ directory
- Remove IMPLEMENTATION_SUMMARY.md (content consolidated in README.md)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 23:38:22 +08:00
Misaka
15e9fabf25 docs: update README.md with current project structure and features
- Add project status and completion indicator
- Add comprehensive project structure documentation
- Enhance installation instructions with virtual environment setup
- Add development tools section with VS Code debugging configuration
- Document --all flag for full project testing
- Add references to flowchart and implementation summary documentation
- Update FAQ with new questions about debugging and virtual environment
- Reflect current implementation features (call chain tracking, class module support, etc.)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 23:37:10 +08:00
Misaka
0502137ef7 docs: add CLAUDE.md for future Claude Code instances
Add comprehensive documentation for Claude Code including project overview, core architecture, development commands, and key implementation details. Includes virtual environment requirement for running test scripts.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 23:02:55 +08:00
Misaka
01690580ae chore: remove test scripts and demo files from version control
Remove check_vba_access.py and create_demo.py utility scripts. Stop tracking demo.xlsm and update .gitignore to exclude all .xlsm files.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 22:48:53 +08:00
Misaka
2cb22df60c feat: VBA Call Chain Tracking and Full Project Testing System
## Phase 1: Core Call Chain Tracking

### New Features
- CallStack class module: Manually maintain VBA call stack
- Enhanced TestLogger: Integrated call stack management
  - LogEntry: Record on procedure entry
  - LogExit: Pop on procedure exit
  - LogError: Capture complete call chain
- Weave all procedures in module to support call chain tracking

### Data Model Updates
- TestResult new fields: error_module, call_chain
- Result format: ERROR|Module.Proc|Line|Number|Desc|CallChain

### Key Fixes
- Fixed LogSuccess overwriting error state
  LogSuccess now only sets success when no error exists
- Fixed incomplete call chain due to weaving only single procedure
  Now weaves all procedures in the entire module

## Phase 2: Full Project Testing

### New Methods
- CodeWeaver.parse_modules: Parse all VBA modules
- CodeWeaver.weave_module_all_procedures: Weave all procedures in single module
- CodeWeaver.weave_all_modules: Weave all procedures in multiple modules
- TestRunner.discover_all_tests: Auto-discover all entry points
- TestRunner.run_all_tests: Batch execute all tests
- TestRunner._weave_all_modules_inplace: In-place weave all modules

### CLI Interface
- Mode 1: python vba_test_runner.py <file> --all (full project testing)
- Mode 2: python vba_test_runner.py <file> <module> <proc>... (specific testing)

## Test Verification

### Call Chain Tracking Example
TestSuccessfulProcedure -> TestErrorProcedure -> TestTypeMismatch
Output:
  Call Chain: Module1.TestSuccessfulProcedure -> Module1.TestErrorProcedure -> Module1.TestTypeMismatch
  Location: Module1.TestTypeMismatch:20
  Source: y = 20

### Statistics
- New code: ~500 lines
- New methods: 8
- Test scenarios: Multi-level nested call chain verification passed

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 22:44:49 +08:00
9 changed files with 1517 additions and 410 deletions

2
.gitignore vendored
View File

@@ -35,4 +35,4 @@ test_*.py
debug_*.py
# Demo file (optional - comment out if you want to track it)
# demo.xlsm
*.xlsm

141
CLAUDE.md Normal file
View 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)

View File

@@ -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
View File

@@ -1,24 +1,72 @@
# VBA 自动化测试与精确报错系统
通过代码编织Code Weaving技术实现 VBA 宏的精确到行的代码报错定位。
通过代码编织Code Weaving技术实现 VBA 宏的精确到行的代码报错定位,并支持完整的调用链追踪
## 项目状态
**已完成** - 所有核心功能已实现并测试通过
## 核心特性
- **精确行号定位**: 捕获 VBA 错误的具体行号,不再只是"发生意外"
- **源代码映射**: 通过 Source Map 机制显示出错行的原始代码
- **自动化测试**: 批量执行 VBA 宏并收集结果
- **调用链追踪**: 完整记录过程调用栈,追踪错误传播路径
- **全项目测试**: 自动发现并测试所有可测试的过程
- **模块级支持**: 支持标准模块和类模块的测试
- **非侵入式**: 测试过程不修改原始 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
pip install xlwings pywin32
```
### 3. 配置 Excel 信任设置
在运行测试前,需要启用 VBA 项目对象模型访问:
1. 打开 Excel
2. 文件 > 选项 > 信任中心
3. 信任中心设置 > 宏设置
4. 勾选"信任对 VBA 工程对象模型的访问"
5. 重启 Excel
## 快速开始
### 1. 创建演示文件
### 1. 使用演示文件测试
项目包含 `demo.xlsm` 演示文件,包含以下测试用例:
首先创建一个包含测试代码的 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
```
**全项目自动化测试**(自动发现并测试所有过程):
```bash
python vba_test_runner.py demo.xlsm --all
```
## 输出示例
### 单个测试
**成功时**:
```
[PASS] TestSuccessfulProcedure - Test Passed
[PASS] TestSuccessfulProcedure
```
**失败时**:
**失败时**(包含调用链):
```
[FAIL] TestErrorProcedure - Test Failed
Error Description: Division by zero
Error Line: 30
Source Code: result = x / y
[FAIL] TestErrorProcedure
Error: Division by zero
Location: Module1.TestErrorProcedure:30
Call Chain: Module1.MainProc -> Module1.TestErrorProcedure
Source: result = x / y
```
### 批量测试
```
===== VBA Batch Test Results =====
===== VBA 批量测试结果 =====
[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
[1/4] [FAIL] TestErrorProcedure
Error: Division by zero
Location: Module1.TestErrorProcedure:30
Source: result = x / y
[2/4] [FAIL] TestTypeMismatch
Error: Type mismatch
Location: Module1.TestTypeMismatch:20
Source: y = x
[3/4] [FAIL] TestSubscriptError
Error: Subscript out of range
Location: Module1.TestSubscriptError:40
Source: value = arr(5)
[4/4] [PASS] TestSuccessfulProcedure
===== Test Summary =====
Passed: 1/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
@@ -95,8 +174,9 @@ Failed: 3/4
1. **解析 VBA 代码**: 识别所有 Sub/Function 过程
2. **注入行号标签**: 在可执行代码前插入数字标签10, 20, 30...
3. **注入错误处**: 添加 `On Error GoTo` 语句和错误处理块
4. **创建 Source Map**: 维护行号到源代码的映射
3. **注入调用栈管**: 添加 `LogEntry``LogExit` 调用
4. **注入错误处理**: 添加 `On Error GoTo` 语句和错误处理块
5. **创建 Source Map**: 维护行号到源代码的映射
### VBA 行号机制
@@ -108,46 +188,100 @@ VBA 的 `Erl` 函数会返回最近执行的行号标签:
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 代码
代码编织器注入行号和错误处理
代码编织器注入行号、调用栈管理和错误处理
注入 _TestLogger 辅助模块
注入 TestLogger 模块和 CallStack 类模块
热替换目标模块代码
执行 VBA 宏
_TestLogger 读取结果
从 TestLogger 读取结果(包含调用链)
格式化输出
```
## 架构设计
### 模块划分
### 核心类
```
vba_test_runner.py
├── TestResult (测试结果数据类)
│ ├── procedure_name - 过程名称
│ ├── success - 测试是否成功
│ ├── error_number - 错误代码
│ ├── error_description - 错误描述
│ ├── error_line - 错误行号
│ ├── source_code - 源代码
│ ├── error_module - 错误发生的模块 (新增)
│ └── call_chain - 完整调用链 (新增)
├── CodeWeaver (代码编织器类)
│ ├── parse_procedures() - 解析 VBA 过程
│ ├── weave_procedure() - 编织单个过程
│ ├── _inject_line_numbers() - 注入行号标签
── _inject_error_handler() - 注入错误处理
│ ├── parse_modules() - 解析所有模块
│ ├── parse_procedures() - 解析 VBA 过程
│ ├── weave_procedure() - 编织单个过程
── weave_procedure_with_callstack() - 编织过程(带调用栈)
│ ├── weave_module_all_procedures() - 编织模块的所有过程
│ ├── weave_all_modules() - 编织所有模块
│ ├── _inject_line_numbers() - 注入行号标签
│ ├── _inject_error_handler() - 注入错误处理
│ └── _inject_error_handler_with_callstack() - 注入错误处理(带调用栈)
├── LoggerInjector (日志模块注入器类)
│ ├── inject_or_replace() - 注入或替换 Logger 模块
── LOGGER_MODULE_CODE - Logger 模块的 VBA 代码
── LOGGER_MODULE_CODE - TestLogger 模块的 VBA 代码
│ └── CALLSTACK_CLASS_CODE - CallStack 类模块的 VBA 代码
└── TestRunner (执行控制器类)
├── run_test() - 执行完整测试流程
├── run_test() - 执行单个测试
├── run_all_tests() - 执行全项目测试 (新增)
├── discover_all_tests() - 发现所有可测试过程 (新增)
├── _get_vba_code() - 读取 VBA 代码
├── _replace_module_code() - 热替换模块代码
├── _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 语句
### 错误处理模板
### 错误处理模板(带调用栈)
```vba
Call TestLogger.LogEntry("{proc_name}", "{module_name}")
On Error GoTo Auto_Err_Handler_{proc_name}
... 原有代码 ...
Call _TestLogger.LogSuccess()
Call TestLogger.LogExit()
Call TestLogger.LogSuccess()
Exit Sub/Function
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 语句),可能需要额外处理
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
```
### 使用全项目测试
全项目测试模式会自动发现并测试所有过程:
```bash
python vba_test_runner.py your_file.xlsm --all
```
系统会:
1. 扫描所有标准模块和类模块
2. 识别所有 Sub/Function 过程
3. 过滤掉事件过程和内部方法
4. 为所有模块注入调用栈管理
5. 逐个执行测试并生成报告
### 自定义 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: 如何测试类模块中的方法?**
A: 当前版本只支持标准模块。要测试类模块,可以创建一个包装的 Sub 在标准模块中调用类方法
A: 系统支持类模块Type 2的测试。在全项目测试模式下类模块中的公共方法会被自动发现和测试
**Q: 调用链是如何追踪的?**
A: 系统在每个过程入口注入 `LogEntry` 调用,在出口注入 `LogExit` 调用。`CallStack` 类模块维护一个栈结构,记录所有正在执行的过程,错误发生时可以生成完整的调用链。
**Q: 全项目测试模式和手动指定过程有什么区别?**
A: 全项目测试模式(`--all`)会:
- 自动发现所有可测试的过程
- 一次性编织所有模块的代码
- 逐个执行测试并生成汇总报告
- 更适合大规模测试和回归测试
手动指定过程模式更适合:
- 调试单个过程
- 快速验证修复
- 选择性测试某些功能
**Q: 如何在 VS Code 中调试?**
A: 项目包含 `.vscode/launch.json` 配置文件。在 VS Code 中按 `F5` 即可启动调试,可以在配置中修改测试参数。
**Q: 可以捕获运行时警告吗?**
A: 当前版本只捕获错误。要捕获警告,需要修改 Logger 模块来处理 `InfoMessage` 事件。
**Q: 虚拟环境是必须的吗?**
A: 强烈推荐使用虚拟环境来隔离项目依赖。项目包含 `.gitignore` 规则来忽略虚拟环境目录。
## 许可证
MIT License

View File

@@ -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()

View File

@@ -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()

BIN
demo.xlsm

Binary file not shown.

View 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 无法提供的调试功能。

View File

@@ -19,6 +19,8 @@ class TestResult:
error_description: str = ""
error_line: int = 0
source_code: str = ""
error_module: str = "" # 新增:错误发生的模块
call_chain: str = "" # 新增:完整调用链
class CodeWeaver:
@@ -27,6 +29,41 @@ class CodeWeaver:
def __init__(self):
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]:
"""
解析 VBA 代码中的所有 Sub/Function
@@ -75,6 +112,84 @@ class CodeWeaver:
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]]:
"""
为指定过程注入行号和错误处理
@@ -161,6 +276,87 @@ class CodeWeaver:
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]:
"""
在过程开头添加: On Error GoTo Auto_Err_Handler
@@ -235,58 +431,130 @@ class CodeWeaver:
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
' Module-level variables to store test result
Private m_TestStatus As String
Private m_ErrModule As String
Private m_ErrProcedure As String
Private m_ErrNumber As Long
Private m_ErrDescription As String
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, _
ByVal errDesc As String, ByVal errLine As Long)
Sub LogEntry(ByVal procName As String, ByVal moduleName As String)
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_ErrModule = moduleName
m_ErrProcedure = procName
m_ErrNumber = errNum
m_ErrDescription = errDesc
m_ErrLine = errLine
If Not m_CallStack Is Nothing Then
m_CallChain = m_CallStack.GetCallChain()
Else
m_CallChain = ""
End If
End Sub
Sub LogSuccess()
m_TestStatus = "SUCCESS"
m_ErrProcedure = ""
m_ErrNumber = 0
m_ErrDescription = ""
m_ErrLine = 0
' 只有在没有错误时才设置为成功
' 这确保子过程的错误不会被父过程覆盖
If m_TestStatus <> "ERROR" Then
m_TestStatus = "SUCCESS"
End If
' 只有在没有错误时才清空调用栈
If m_TestStatus <> "ERROR" Then
If Not m_CallStack Is Nothing Then m_CallStack.Clear
End If
End Sub
Function GetResult() As String
If m_TestStatus = "SUCCESS" Then
GetResult = "SUCCESS"
Else
GetResult = "ERROR|" & m_ErrProcedure & "|" & m_ErrLine & "|" & _
m_ErrNumber & "|" & m_ErrDescription
GetResult = "ERROR|" & m_ErrModule & "." & m_ErrProcedure & "|" & _
m_ErrLine & "|" & m_ErrNumber & "|" & m_ErrDescription & "|" & _
m_CallChain
End If
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:
"""
如果模块存在则删除,然后注入新的 Logger 模块
如果模块存在则删除,然后注入新的 Logger 模块和 CallStack 类模块
"""
# 获取 VBA 项目
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:
if component.Name == module_name:
vba_project.VBComponents.Remove(component)
break
# 注入新模块
# 注入新 Logger 模块
new_module = vba_project.VBComponents.Add(1) # 1 = vbext_ct_StdModule
new_module.Name = module_name
new_module.CodeModule.AddFromString(self.LOGGER_MODULE_CODE)
@@ -331,9 +599,10 @@ class TestRunner:
# 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
# 3. 编织整个模块的所有过程(以支持调用链追踪)
# 注意:为了追踪调用链,我们需要编织模块中的所有过程
woven_code, self.source_map = self.code_weaver.weave_module_all_procedures(
original_code, module_name
)
# 4. 注入 Logger 模块
@@ -357,7 +626,9 @@ class TestRunner:
error_number=0,
error_description=f"Test execution failed: {str(e)}",
error_line=0,
source_code=""
source_code="",
error_module="",
call_chain=""
)
finally:
# 8. 关闭工作簿(不保存)
@@ -400,7 +671,7 @@ class TestRunner:
pass
def _get_test_result(self, proc_name: str) -> TestResult:
"""从 Logger 模块获取测试结果"""
"""从 Logger 模块获取测试结果(增强版,支持调用链)"""
try:
result_str = self.app.api.Run("TestLogger.GetResult")
@@ -410,24 +681,33 @@ class TestRunner:
success=True
)
else:
# 解析错误信息: ERROR|ProcedureName|Line|Number|Description
# 解析错误信息: ERROR|Module.Proc|Line|Number|Desc|CallChain
parts = result_str.split('|')
if len(parts) >= 5:
error_proc_name = parts[1]
if len(parts) >= 6:
error_module_proc = parts[1]
error_line = int(parts[2])
error_number = int(parts[3])
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_code = self.source_map.get(error_line, "Source code not found")
return TestResult(
procedure_name=error_proc_name,
procedure_name=error_proc,
success=False,
error_number=error_number,
error_description=error_desc,
error_line=error_line,
source_code=source_code
source_code=source_code,
error_module=error_module,
call_chain=call_chain
)
except Exception as e:
return TestResult(
@@ -436,7 +716,9 @@ class TestRunner:
error_number=0,
error_description=f"Failed to get test result: {str(e)}",
error_line=0,
source_code=""
source_code="",
error_module="",
call_chain=""
)
def _cleanup(self):
@@ -449,18 +731,163 @@ class TestRunner:
except:
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):
"""打印单个测试结果"""
"""打印单个测试结果(增强版,支持调用链)"""
prefix = f"[{index}/{total}] " if index and total else ""
if result.success:
print(f"{prefix}[PASS] {result.procedure_name} - Test Passed")
print(f"{prefix}[PASS] {result.procedure_name}")
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}")
print(f"{prefix}[FAIL] {result.procedure_name}")
print(f" Error: {result.error_description}")
# 显示位置信息
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]):
@@ -475,27 +902,57 @@ def print_summary(results: List[TestResult]):
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")
"""主入口函数(支持两种模式:全项目测试和指定过程测试)"""
if len(sys.argv) < 2:
print("用法1: python vba_test_runner.py <excel_file> --all")
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)
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):
# 模式1全项目测试
if len(sys.argv) == 2 and sys.argv[1] == "--all":
# 这种情况应该是: python vba_test_runner.py --all
# 但实际上应该是: python vba_test_runner.py <file> --all
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)
result = runner.run_test(module_name, proc_name)
results.append(result)
print_test_result(result, i, len(procedures))
try:
results = runner.run_all_tests()
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__":