Compare commits
2 Commits
7a86a88fc4
...
01690580ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01690580ae | ||
|
|
2cb22df60c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -35,4 +35,4 @@ test_*.py
|
||||
debug_*.py
|
||||
|
||||
# Demo file (optional - comment out if you want to track it)
|
||||
# demo.xlsm
|
||||
*.xlsm
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
"""
|
||||
检查 Excel VBA 项目访问设置
|
||||
"""
|
||||
|
||||
import winreg
|
||||
import os
|
||||
|
||||
def check_vba_project_access():
|
||||
"""检查是否启用了对 VBA 工程对象模型的访问"""
|
||||
|
||||
# Excel 版本的注册表路径
|
||||
excel_versions = [
|
||||
r"Software\Microsoft\Office\16.0\Excel\Security",
|
||||
r"Software\Microsoft\Office\15.0\Excel\Security",
|
||||
r"Software\Microsoft\Office\14.0\Excel\Security",
|
||||
]
|
||||
|
||||
print("Checking Excel VBA Project Access Settings...\n")
|
||||
|
||||
found = False
|
||||
for version_path in excel_versions:
|
||||
try:
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, version_path)
|
||||
access_value, _ = winreg.QueryValueEx(key, "AccessVBOM")
|
||||
winreg.CloseKey(key)
|
||||
|
||||
if access_value == 1:
|
||||
print(f"[OK] VBA Project Access is ENABLED in {version_path}")
|
||||
found = True
|
||||
else:
|
||||
print(f"[WARN] VBA Project Access is DISABLED in {version_path}")
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to check {version_path}: {e}")
|
||||
|
||||
if not found:
|
||||
print("\n[INFO] VBA Project Access settings not found in registry.")
|
||||
print("You may need to enable it manually in Excel.")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("How to Enable VBA Project Access in Excel:")
|
||||
print("="*60)
|
||||
print("1. Open Excel")
|
||||
print("2. Go to File > Options > Trust Center")
|
||||
print("3. Click 'Trust Center Settings...'")
|
||||
print("4. Go to 'Macro Settings'")
|
||||
print("5. Check the box: 'Trust access to the VBA project object model'")
|
||||
print("6. Click OK to save changes")
|
||||
print("7. Restart Excel")
|
||||
print("="*60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_vba_project_access()
|
||||
@@ -1,83 +0,0 @@
|
||||
"""
|
||||
创建演示用的 Excel 文件,包含测试用的 VBA 代码
|
||||
"""
|
||||
|
||||
import xlwings as xw
|
||||
|
||||
# VBA 测试代码
|
||||
VBA_TEST_CODE = """Option Explicit
|
||||
|
||||
Sub TestErrorProcedure()
|
||||
Dim x As Integer
|
||||
Dim y As Integer
|
||||
Dim result As Double
|
||||
|
||||
x = 10
|
||||
y = 0
|
||||
result = x / y ' 这里会触发除以零错误
|
||||
|
||||
Debug.Print "Result: " & result
|
||||
End Sub
|
||||
|
||||
Sub TestTypeMismatch()
|
||||
Dim x As String
|
||||
Dim y As Integer
|
||||
|
||||
x = "Hello"
|
||||
y = x ' 这里会触发类型不匹配错误
|
||||
|
||||
Debug.Print "Y: " & y
|
||||
End Sub
|
||||
|
||||
Sub TestSubscriptError()
|
||||
Dim arr(1 To 3) As Integer
|
||||
Dim value As Integer
|
||||
|
||||
arr(1) = 10
|
||||
arr(2) = 20
|
||||
arr(3) = 30
|
||||
value = arr(5) ' 这里会触发下标越界错误
|
||||
|
||||
Debug.Print "Value: " & value
|
||||
End Sub
|
||||
|
||||
Sub TestSuccessfulProcedure()
|
||||
Dim x As Integer
|
||||
Dim y As Integer
|
||||
Dim sum As Integer
|
||||
|
||||
x = 10
|
||||
y = 20
|
||||
sum = x + y
|
||||
|
||||
Debug.Print "Sum: " & sum
|
||||
End Sub
|
||||
"""
|
||||
|
||||
def create_demo_file():
|
||||
"""创建演示用的 Excel 文件"""
|
||||
app = xw.App(visible=True)
|
||||
wb = app.books.add()
|
||||
|
||||
# 添加 VBA 模块
|
||||
vba_project = wb.api.VBProject
|
||||
module = vba_project.VBComponents.Add(1) # 1 = vbext_ct_StdModule
|
||||
module.Name = "Module1"
|
||||
module.CodeModule.AddFromString(VBA_TEST_CODE)
|
||||
|
||||
# 保存文件
|
||||
file_path = r"D:\python\xlwings\demo.xlsm"
|
||||
wb.save(file_path)
|
||||
wb.close()
|
||||
|
||||
print(f"演示文件已创建: {file_path}")
|
||||
print("\n包含的测试过程:")
|
||||
print(" - TestErrorProcedure: 除以零错误")
|
||||
print(" - TestTypeMismatch: 类型不匹配错误")
|
||||
print(" - TestSubscriptError: 下标越界错误")
|
||||
print(" - TestSuccessfulProcedure: 成功执行的测试")
|
||||
|
||||
app.quit()
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_demo_file()
|
||||
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user