Compare commits

..

2 Commits

Author SHA1 Message Date
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
5 changed files with 503 additions and 183 deletions

2
.gitignore vendored
View File

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

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

@@ -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()
' 只有在没有错误时才设置为成功
' 这确保子过程的错误不会被父过程覆盖
If m_TestStatus <> "ERROR" Then
m_TestStatus = "SUCCESS" m_TestStatus = "SUCCESS"
m_ErrProcedure = "" End If
m_ErrNumber = 0 ' 只有在没有错误时才清空调用栈
m_ErrDescription = "" If m_TestStatus <> "ERROR" Then
m_ErrLine = 0 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,20 +902,44 @@ 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]
# 模式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)
try:
results = runner.run_all_tests()
print_summary(results)
finally:
runner._cleanup()
# 模式2指定过程测试保持向后兼容
elif len(sys.argv) >= 3:
module_name = sys.argv[2] module_name = sys.argv[2]
procedures = sys.argv[3:] procedures = sys.argv[3:]
print(f"===== VBA Batch Test Results =====\n") if not procedures:
print("错误: 必须指定至少一个过程名")
sys.exit(1)
print(f"===== VBA 批量测试结果 =====\n")
results = [] results = []
for i, proc_name in enumerate(procedures, 1): for i, proc_name in enumerate(procedures, 1):
runner = TestRunner(excel_file, visible=False) runner = TestRunner(excel_file, visible=False)
result = runner.run_test(module_name, proc_name) result = runner.run_test(module_name, proc_name)
@@ -497,6 +948,12 @@ def main():
print_summary(results) 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__":
main() main()