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