compliance/ddms_compliance_suite/test_case_registry.py
2025-06-06 14:52:08 +08:00

244 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import sys
import importlib.util
import inspect
import logging
import re
from typing import List, Type, Optional, Dict
# 确保可以从 sibling 模块导入
from .test_framework_core import BaseAPITestCase
class TestCaseRegistry:
"""
负责发现、加载和管理所有自定义的APITestCase类。
"""
def __init__(self, test_cases_dir: Optional[str]):
"""
初始化 TestCaseRegistry。
Args:
test_cases_dir: 存放自定义测试用例 (.py 文件) 的目录路径。
"""
self.logger = logging.getLogger(__name__)
self.test_cases_dir = test_cases_dir
self._registry: Dict[str, Type[BaseAPITestCase]] = {}
self._test_case_classes: List[Type[BaseAPITestCase]] = []
self._discovery_errors: List[str] = []
if self.test_cases_dir:
self.discover_test_cases()
else:
self.logger.info("No custom test cases directory provided. Skipping test case discovery.")
def discover_test_cases(self):
"""
扫描指定目录及其所有子目录,动态导入模块,并注册所有继承自 BaseAPITestCase 的类。
"""
if not self.test_cases_dir:
self.logger.info("Test cases directory is not set. Skipping discovery.")
return
if not os.path.isdir(self.test_cases_dir):
self.logger.error(f"Custom test cases directory not found or is not a directory: {self.test_cases_dir}")
self._discovery_errors.append(f"Directory not found: {self.test_cases_dir}")
return
self.logger.info(f"Discovering custom test cases from: {self.test_cases_dir}")
sys.path.insert(0, self.test_cases_dir)
found_count = 0
# 使用 os.walk 进行递归扫描
for root_dir, _, files in os.walk(self.test_cases_dir):
for filename in files:
if filename.endswith(".py") and not filename.startswith("__"):
module_name = filename[:-3]
file_path = os.path.join(root_dir, filename)
try:
# 动态导入模块
spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
self.logger.debug(f"成功导入模块: {module_name}{file_path}")
# 在模块中查找 BaseAPITestCase 的子类
for name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and issubclass(obj, BaseAPITestCase) and obj is not BaseAPITestCase:
if not hasattr(obj, 'id') or not obj.id:
self.logger.error(f"测试用例类 '{obj.__name__}' 在文件 '{file_path}' 中缺少有效的 'id' 属性,已跳过注册。")
continue
if obj.id in self._registry:
self.logger.warning(f"发现重复的测试用例 ID: '{obj.id}' (来自类 '{obj.__name__}' in {file_path})。之前的定义将被覆盖。")
self._registry[obj.id] = obj
# 更新 _test_case_classes 列表如果已存在相同ID的类替换它否则添加。
# 这确保了排序时使用的是最新的类定义以防ID冲突。
existing_class_indices = [i for i, tc_class in enumerate(self._test_case_classes) if tc_class.id == obj.id]
if existing_class_indices:
for index in sorted(existing_class_indices, reverse=True): # 从后往前删除,避免索引问题
self.logger.debug(f"从 _test_case_classes 列表中移除旧的同ID ('{obj.id}') 测试用例类: {self._test_case_classes[index].__name__}")
del self._test_case_classes[index]
self._test_case_classes.append(obj)
found_count += 1
self.logger.info(f"已注册测试用例: '{obj.id}' ({getattr(obj, 'name', 'N/A')}) 来自类 '{obj.__name__}' (路径: {file_path})")
else:
self.logger.error(f"无法为文件 '{file_path}' 创建模块规范。")
except ImportError as e:
self.logger.error(f"导入模块 '{module_name}''{file_path}' 失败: {e}", exc_info=True)
except AttributeError as e:
self.logger.error(f"在模块 '{module_name}' ({file_path}) 中查找测试用例时出错: {e}", exc_info=True)
except Exception as e:
self.logger.error(f"处理文件 '{file_path}' 时发生未知错误: {e}", exc_info=True)
# 根据 execution_order 对收集到的测试用例类进行排序
try:
self._test_case_classes.sort(key=lambda tc_class: (getattr(tc_class, 'execution_order', 100), tc_class.__name__))
self.logger.info(f"已根据 execution_order (主要) 和类名 (次要) 对 {len(self._test_case_classes)} 个测试用例类进行了排序。")
except AttributeError as e_sort:
self.logger.error(f"对测试用例类进行排序时发生 AttributeError (可能部分类缺少 execution_order): {e_sort}", exc_info=True)
except Exception as e_sort_general:
self.logger.error(f"对测试用例类进行排序时发生未知错误: {e_sort_general}", exc_info=True)
self.logger.info(f"测试用例发现完成。总共注册了 {len(self._registry)} 个独特的测试用例 (基于ID)。发现并排序了 {len(self._test_case_classes)} 个测试用例类。")
def get_test_case_by_id(self, case_id: str) -> Optional[Type[BaseAPITestCase]]:
"""根据ID获取已注册的测试用例类。"""
return self._registry.get(case_id)
def get_all_test_case_classes(self) -> List[Type[BaseAPITestCase]]:
"""获取所有已注册的测试用例类列表。"""
return list(self._test_case_classes) # 返回副本
def get_applicable_test_cases(self, endpoint_method: str, endpoint_path: str) -> List[Type[BaseAPITestCase]]:
"""
根据API端点的方法和路径筛选出适用的测试用例类。
Args:
endpoint_method: API端点的方法 (例如 "GET", "POST")。
endpoint_path: API端点的路径 (例如 "/users/{id}")。
Returns:
一个包含适用测试用例类的列表。
"""
applicable_cases: List[Type[BaseAPITestCase]] = []
for tc_class in self._test_case_classes:
# 1. 检查 applicable_methods
if tc_class.applicable_methods is not None:
if endpoint_method.upper() not in [m.upper() for m in tc_class.applicable_methods]:
self.logger.debug(f"测试用例 '{tc_class.id}' 不适用于方法 '{endpoint_method}' (期望: {tc_class.applicable_methods}),已跳过。")
continue # 方法不匹配,跳过此测试用例
# 2. 检查 applicable_paths_regex
if tc_class.applicable_paths_regex is not None:
try:
if not re.match(tc_class.applicable_paths_regex, endpoint_path):
self.logger.debug(f"测试用例 '{tc_class.id}' 不适用于路径 '{endpoint_path}' (正则: '{tc_class.applicable_paths_regex}'),已跳过。")
continue # 路径正则不匹配,跳过此测试用例
except re.error as e:
self.logger.error(f"测试用例 '{tc_class.id}' 中的路径正则表达式 '{tc_class.applicable_paths_regex}' 无效: {e}。此测试用例将不匹配任何路径。")
continue
# 如果通过了所有检查,则认为适用
applicable_cases.append(tc_class)
self.logger.debug(f"测试用例 '{tc_class.id}' 适用于端点 '{endpoint_method} {endpoint_path}'")
return applicable_cases
# 示例用法 (用于测试此模块,实际使用时由编排器调用)
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# 假设项目结构如下:
# your_project_root/
# ddms_compliance_suite/
# test_framework_core.py
# test_case_registry.py
# custom_testcases/ <-- 测试用例存放目录
# example_checks.py
# another_set_of_checks.py
# 创建一个临时的 custom_testcases 目录和一些示例测试用例文件用于测试
current_dir = os.path.dirname(os.path.abspath(__file__))
custom_testcases_path = os.path.join(os.path.dirname(current_dir), "custom_testcases") # 假设custom_testcases在ddms_compliance_suite的父目录
if not os.path.exists(custom_testcases_path):
os.makedirs(custom_testcases_path)
logger.info(f"创建临时目录: {custom_testcases_path}")
# 示例测试用例文件1: example_checks.py
example_checks_content = """
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
class MyFirstTest(BaseAPITestCase):
id = "TC-EXAMPLE-001"
name = "我的第一个测试"
description = "一个简单的示例测试。"
severity = TestSeverity.INFO
tags = ["example"]
applicable_methods = ["GET"]
class MySecondTest(BaseAPITestCase):
id = "TC-EXAMPLE-002"
name = "我的第二个测试"
description = "另一个示例测试,适用于所有方法和特定路径。"
severity = TestSeverity.MEDIUM
tags = ["example", "path_specific"]
applicable_paths_regex = r"/api/users/.+"
"""
with open(os.path.join(custom_testcases_path, "example_checks.py"), "w", encoding="utf-8") as f:
f.write(example_checks_content)
logger.info(f"创建示例测试文件: {os.path.join(custom_testcases_path, 'example_checks.py')}")
# 示例测试用例文件2: specific_feature_tests.py (无适用性限制)
specific_tests_content = """
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
class FeatureXCheck(BaseAPITestCase):
id = "TC-FEATUREX-001"
name = "特性X的检查"
description = "验证特性X的相关功能。"
severity = TestSeverity.HIGH
tags = ["feature-x"]
"""
with open(os.path.join(custom_testcases_path, "specific_feature_tests.py"), "w", encoding="utf-8") as f:
f.write(specific_tests_content)
logger.info(f"创建示例测试文件: {os.path.join(custom_testcases_path, 'specific_feature_tests.py')}")
# 测试 TestCaseRegistry
registry = TestCaseRegistry(test_cases_dir=custom_testcases_path)
logger.info("\n--- 所有已注册的测试用例类 ---")
all_cases = registry.get_all_test_case_classes()
for tc_class in all_cases:
logger.info(f" ID: {tc_class.id}, Name: {tc_class.name}, Methods: {tc_class.applicable_methods}, Path Regex: {tc_class.applicable_paths_regex}")
logger.info("\n--- 测试适用性筛选 ---")
endpoint1_method = "GET"
endpoint1_path = "/api/users/123"
logger.info(f"筛选适用于 '{endpoint1_method} {endpoint1_path}':")
applicable1 = registry.get_applicable_test_cases(endpoint1_method, endpoint1_path)
for tc_class in applicable1:
logger.info(f" Applicable: {tc_class.id} ({tc_class.name})")
endpoint2_method = "POST"
endpoint2_path = "/api/orders"
logger.info(f"筛选适用于 '{endpoint2_method} {endpoint2_path}':")
applicable2 = registry.get_applicable_test_cases(endpoint2_method, endpoint2_path)
for tc_class in applicable2:
logger.info(f" Applicable: {tc_class.id} ({tc_class.name})")
endpoint3_method = "GET"
endpoint3_path = "/api/health"
logger.info(f"筛选适用于 '{endpoint3_method} {endpoint3_path}':")
applicable3 = registry.get_applicable_test_cases(endpoint3_method, endpoint3_path)
for tc_class in applicable3:
logger.info(f" Applicable: {tc_class.id} ({tc_class.name})")
# 清理临时文件和目录
# os.remove(os.path.join(custom_testcases_path, "example_checks.py"))
# os.remove(os.path.join(custom_testcases_path, "specific_feature_tests.py"))
# if not os.listdir(custom_testcases_path):
# os.rmdir(custom_testcases_path)
# logger.info("已清理临时文件和目录。")