#!/usr/bin/env python3 import ast import inspect import sys import os # Add lib and tests to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib')) sys.path.insert(0, os.path.dirname(__file__)) from openttd import OpenTTDClient, OpenTTDAdminClient from openttd.protocol import OpenTTDProtocol, OpenTTDAdminProtocol # 1. Gather public functions dynamically at runtime using reflection markers classes = [OpenTTDClient, OpenTTDAdminClient, OpenTTDProtocol, OpenTTDAdminProtocol] public_funcs = {} # name -> has_params for cls in classes: # Class must be marked as public if not getattr(cls, '__is_public_api__', False): continue # Class constructor call (e.g. OpenTTDClient(host, ...)) sig_init = inspect.signature(cls.__init__) has_params_init = len([p for name, p in sig_init.parameters.items() if name != 'self']) > 0 public_funcs[cls.__name__] = has_params_init # Get all functions on the class (including inherited) for name, val in inspect.getmembers(cls): if name == '__init__': continue if not (inspect.isfunction(val) or inspect.ismethod(val) or inspect.iscoroutinefunction(val)): continue # Exclude private methods if name.startswith('_'): continue # Exclude protocol dispatch targets (receive_* except receive_packet) if name.startswith('receive_') and name != 'receive_packet': continue # Exclude lifecycle callbacks and manually decorated ones if getattr(val, '__exclude_call_check__', False): continue sig = inspect.signature(val) has_params = len([p for p_name, p in sig.parameters.items() if p_name != 'self']) > 0 public_funcs[name] = has_params # 2. Inspect E2E test module dynamically at runtime using reflection import tests.test_e2e as test_e2e calls = {name: [] for name in public_funcs} class E2ECallVisitor(ast.NodeVisitor): def visit_Call(self, node): method_name = None if isinstance(node.func, ast.Attribute): method_name = node.func.attr elif isinstance(node.func, ast.Name): method_name = node.func.id if method_name in public_funcs: # Serialize arguments to string to compare data args_str = [ast.unparse(a) for a in node.args] kwargs_str = [f"{kw.arg}={ast.unparse(kw.value)}" for kw in node.keywords] call_data = (tuple(args_str), tuple(sorted(kwargs_str))) calls[method_name].append(call_data) self.generic_visit(node) visitor = E2ECallVisitor() # Reflectively iterate through all test functions in tests.test_e2e for name, val in inspect.getmembers(test_e2e, predicate=inspect.isfunction): if name.startswith("test_e2e_"): # Retrieve function source dynamically via reflection source = inspect.getsource(val) func_tree = ast.parse(source) visitor.visit(func_tree) # 3. Perform assertion checks failed = False print("=== Public Function E2E Calls Verification (Reflection API Inspection) ===") for name, has_params in sorted(public_funcs.items()): func_calls = calls[name] num_calls = len(func_calls) if num_calls < 2: print(f"❌ {name}: Called {num_calls} time(s) (expected at least 2).") failed = True continue if has_params: unique_calls = set(func_calls) num_unique = len(unique_calls) if num_unique < 2: print(f"❌ {name}: Called {num_calls} times but with identical data: {unique_calls}") failed = True else: print(f"✅ {name}: Called {num_calls} times with {num_unique} different inputs.") else: print(f"✅ {name}: Called {num_calls} times (no parameters).") if failed: print("❌ Verification FAILED: Some public functions do not meet E2E call requirement.") sys.exit(1) else: print("✅ Verification PASSED: All public functions called at least 2 times with different data.") sys.exit(0)