From 2cb22df60cd38700471fa0296c9ec45d2fbbbe21 Mon Sep 17 00:00:00 2001 From: Misaka Date: Wed, 4 Feb 2026 22:44:49 +0800 Subject: [PATCH] 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 --all (full project testing) - Mode 2: python vba_test_runner.py ... (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 --- demo.xlsm | Bin 12078 -> 12427 bytes vba_test_runner.py | 547 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 502 insertions(+), 45 deletions(-) diff --git a/demo.xlsm b/demo.xlsm index bc17e32735aa731736af2c04f52617e94e00a4cd..687bd97fd2c0fa8343efe3d47204a92d4962b768 100644 GIT binary patch delta 6824 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 x&$wBEOQ!AH(#D}xOL$lqcmEs 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 E0eAL!v;Y7A diff --git a/vba_test_runner.py b/vba_test_runner.py index aee0ec1..5354367 100644 --- a/vba_test_runner.py +++ b/vba_test_runner.py @@ -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 [procedure2] ...") - print("Example: python vba_test_runner.py demo.xlsm Module1 TestErrorProcedure") + """主入口函数(支持两种模式:全项目测试和指定过程测试)""" + if len(sys.argv) < 2: + print("用法1: python vba_test_runner.py --all") + print("用法2: python vba_test_runner.py [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 --all + print("错误: 缺少Excel文件路径") + print("正确用法: python vba_test_runner.py --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 --all") + print("用法2: python vba_test_runner.py [procedure2] ...") + sys.exit(1) if __name__ == "__main__":