This commit is contained in:
gongwenxin 2025-05-19 17:27:57 +08:00
parent 82e79b393a
commit 61b7befb1f
4 changed files with 5989 additions and 5967 deletions

View File

@ -360,37 +360,27 @@ class APITestOrchestrator:
if hasattr(endpoint_spec, 'to_dict') and callable(endpoint_spec.to_dict):
endpoint_spec_dict = endpoint_spec.to_dict()
elif isinstance(endpoint_spec, (YAPIEndpoint, SwaggerEndpoint)):
# 手动从对象属性构建字典,确保包含 method 和 path
endpoint_spec_dict = {
"method": getattr(endpoint_spec, 'method', 'UNKNOWN_METHOD'),
"path": getattr(endpoint_spec, 'path', 'UNKNOWN_PATH'),
"title": getattr(endpoint_spec, 'title', ''),
"summary": getattr(endpoint_spec, 'summary', ''),
# ... 可以根据需要添加更多来自 endpoint_spec 对象的属性 ...
"_original_object_type": type(endpoint_spec).__name__ # 记录原始类型以供调试
"_original_object_type": type(endpoint_spec).__name__
}
# 对于YAPIEndpoint它有更多直接的属性我们也可以把它们全部转储
if isinstance(endpoint_spec, YAPIEndpoint):
# 简单地将所有可序列化的属性添加到字典中
for attr_name in dir(endpoint_spec):
if not attr_name.startswith('_') and not callable(getattr(endpoint_spec, attr_name)):
try:
json.dumps({attr_name: getattr(endpoint_spec, attr_name)}) # 测试是否可序列化
# Test serializability before adding
json.dumps({attr_name: getattr(endpoint_spec, attr_name)})
endpoint_spec_dict[attr_name] = getattr(endpoint_spec, attr_name)
except (TypeError, OverflowError):
pass # 不可序列化则跳过
pass
elif isinstance(endpoint_spec, SwaggerEndpoint):
# SwaggerEndpoint 可能有更复杂的嵌套结构,如 parameters, responses 等
# 如果需要,可以有选择地将它们也转换为字典或保留其结构
if hasattr(endpoint_spec, 'parameters'):
endpoint_spec_dict['parameters'] = endpoint_spec.parameters # 这本身应该是个列表或字典
if hasattr(endpoint_spec, 'request_body'):
endpoint_spec_dict['request_body'] = endpoint_spec.request_body
if hasattr(endpoint_spec, 'responses'):
endpoint_spec_dict['responses'] = endpoint_spec.responses
if hasattr(endpoint_spec, 'parameters'): endpoint_spec_dict['parameters'] = endpoint_spec.parameters
if hasattr(endpoint_spec, 'request_body'): endpoint_spec_dict['request_body'] = endpoint_spec.request_body
if hasattr(endpoint_spec, 'responses'): endpoint_spec_dict['responses'] = endpoint_spec.responses
else:
# 如果它已经是字典或者其他未知类型,就按原样使用 (或记录一个警告)
endpoint_spec_dict = endpoint_spec if isinstance(endpoint_spec, dict) else {}
if not endpoint_spec_dict:
self.logger.warning(f"endpoint_spec 无法转换为字典,实际类型: {type(endpoint_spec)}")
@ -403,6 +393,7 @@ class APITestOrchestrator:
if not global_api_spec_dict:
self.logger.warning(f"global_api_spec 无法转换为字典,实际类型: {type(global_api_spec)}")
try:
test_case_instance = test_case_class(
endpoint_spec=endpoint_spec_dict,
@ -410,48 +401,33 @@ class APITestOrchestrator:
)
test_case_instance.logger.info(f"开始执行测试用例 '{test_case_instance.id}' for endpoint '{endpoint_spec_dict.get('method')} {endpoint_spec_dict.get('path')}'")
# 1. 请求构建阶段 (由测试用例驱动)
# 1a. 生成基础请求参数 (可以由编排器提供一个默认实现,或依赖测试用例完全自定义)
# 这里简化处理假设APIRequest的构建是测试用例的职责或者编排器提供一个初始的
# 但测试用例的 generate_* 方法是主要的驱动者。
# 1. 请求构建阶段
initial_request_data = self._prepare_initial_request_data(endpoint_spec) # endpoint_spec 是原始对象
# TODO: 详细实现请求构建过程,调用 test_case_instance.generate_* 方法
# 一个更完整的实现会是:
# base_query_params = self._generate_default_query_params(endpoint_spec)
# final_query_params = test_case_instance.generate_query_params(base_query_params)
# ...以此类推对 headers 和 body ...
# 暂时简化,假设编排器先构建一个粗略的请求,然后测试用例再调整
# 这个 _build_api_request_for_test_case 需要适应新的上下文
# ---- 内部请求构建和预校验 ----
# 1.1. 准备一个基础的APIRequest (这部分可以复用或重构旧的 _build_api_request 部分逻辑)
# 假设我们有一个方法来创建基于端点规格的"原始"或"默认"请求数据
initial_request_data = self._prepare_initial_request_data(endpoint_spec)
# 1.2. 测试用例修改请求数据
current_q_params = test_case_instance.generate_query_params(initial_request_data['query_params'])
current_headers = test_case_instance.generate_headers(initial_request_data['headers'])
current_body = test_case_instance.generate_request_body(initial_request_data['body'])
# 1.3. 构建最终请求URL (路径参数替换等)
# 路径参数应该从 initial_request_data 中获取,因为 _prepare_initial_request_data 负责生成它们
current_path_params = initial_request_data['path_params']
# 构建最终请求URL使用 current_path_params 进行替换
final_url = self.base_url + endpoint_spec_dict.get('path', '')
# TODO: 处理路径参数替换, 从 initial_request_data 或 endpoint_spec 获取
# 例如: path_params = self._extract_path_params(endpoint_spec, ...)
# for p_name, p_val in path_params.items():
# final_url = final_url.replace(f"{{{p_name}}}", str(p_val))
for p_name, p_val in current_path_params.items():
placeholder = f"{{{p_name}}}"
if placeholder in final_url:
final_url = final_url.replace(placeholder, str(p_val))
else:
self.logger.warning(f"路径参数 '{p_name}' 在路径模板 '{endpoint_spec_dict.get('path')}' 中未找到占位符,但为其生成了值。")
# 1.4. 创建 APIRequestContext
# 需要确保 endpoint_spec 也传递给 APIRequestContext
api_request_context = APIRequestContext(
method=endpoint_spec_dict.get('method', 'GET').upper(), # 从字典获取
method=endpoint_spec_dict.get('method', 'GET').upper(),
url=final_url,
path_params=initial_request_data.get('path_params', {}),
path_params=current_path_params,
query_params=current_q_params,
headers=current_headers,
body=current_body,
endpoint_spec=endpoint_spec_dict # 传递字典
endpoint_spec=endpoint_spec_dict
)
# 1.5. 请求预校验
@ -459,16 +435,28 @@ class APITestOrchestrator:
validation_points.extend(test_case_instance.validate_request_headers(api_request_context.headers, api_request_context))
validation_points.extend(test_case_instance.validate_request_body(api_request_context.body, api_request_context))
# 如果预校验有失败且是CRITICAL/HIGH可以考虑提前终止 (简化:暂时不提前终止,全部记录)
pre_validation_failed_critically = any(
not vp.passed and test_case_instance.severity in [TestSeverity.CRITICAL, TestSeverity.HIGH]
for vp in validation_points
# 检查是否有严重预校验失败
critical_pre_validation_failure = False
failure_messages = []
for vp in validation_points:
if not vp.passed and test_case_instance.severity in [TestSeverity.CRITICAL, TestSeverity.HIGH]:
critical_pre_validation_failure = True
failure_messages.append(vp.message)
if critical_pre_validation_failure:
self.logger.warning(f"测试用例 '{test_case_instance.id}' 因请求预校验失败而中止 (严重级别: {test_case_instance.severity.value})。失败信息: {'; '.join(failure_messages)}")
tc_duration = time.time() - tc_start_time
return ExecutedTestCaseResult(
test_case_id=test_case_instance.id,
test_case_name=test_case_instance.name,
test_case_severity=test_case_instance.severity,
status=ExecutedTestCaseResult.Status.FAILED, # 预校验失败算作 FAILED
validation_points=validation_points,
message=f"请求预校验失败: {'; '.join(failure_messages)}",
duration=tc_duration
)
# if pre_validation_failed_critically :
# # ... 构造 ExecutedTestCaseResult 并返回 ... (状态 FAILED)
# ---- API 调用 ----
# 2. 实际API调用
api_request_obj = APIRequest(
method=api_request_context.method,
url=api_request_context.url,
@ -545,89 +533,124 @@ class APITestOrchestrator:
def _prepare_initial_request_data(self, endpoint_spec: Union[YAPIEndpoint, SwaggerEndpoint]) -> Dict[str, Any]:
"""
根据端点规格准备一个初始的请求数据结构
这可以基于旧的 _build_api_request 的部分逻辑但不实际执行规则或API调用
返回一个包含 'path_params', 'query_params', 'headers', 'body' 的字典
"""
# TODO: 实现此辅助方法,从 endpoint_spec 生成默认的请求参数、头、体。
# 例如,从 schema 生成一个最基础的 body设置默认的 Content-Type 等。
# 以下为非常简化的占位符实现:
self.logger.debug(f"Preparing initial request data for: {endpoint_spec.method} {endpoint_spec.path}")
path_params_spec = []
query_params_spec = []
headers_spec = []
body_schema = None
# path_params_spec: List[Dict] # 用于存储从Swagger等提取的路径参数定义
# query_params_spec: List[Dict]
# headers_spec: List[Dict]
# body_schema: Optional[Dict]
# 重置/初始化这些变量,以避免跨调用共享状态(如果 APITestOrchestrator 实例被重用)
path_params_spec_list: List[Dict[str, Any]] = []
query_params_spec_list: List[Dict[str, Any]] = []
headers_spec_list: List[Dict[str, Any]] = []
body_schema_dict: Optional[Dict[str, Any]] = None
path_str = getattr(endpoint_spec, 'path', '')
if isinstance(endpoint_spec, YAPIEndpoint):
# YAPI specific parsing
# Path params are part of the path string, e.g., /users/{id}
# Query params from req_query
query_params_spec = endpoint_spec.req_query or []
# Headers from req_headers
headers_spec = endpoint_spec.req_headers or []
# Body from req_body_other (if JSON)
query_params_spec_list = endpoint_spec.req_query or []
headers_spec_list = endpoint_spec.req_headers or []
# YAPI 的路径参数在 req_params 中,如果用户定义了的话
if endpoint_spec.req_params:
for p in endpoint_spec.req_params:
# YAPI的req_params可能混合了路径参数和查询参数这里只关心路径中实际存在的
# 需要从 path_str 中解析出占位符,然后匹配 req_params 中的定义
# 简化:我们假设 req_params 中的条目如果其 name 在路径占位符中,则是路径参数
# 更好的做法是 YAPI 解析器能明确区分它们
pass # 下面会统一处理路径参数
if endpoint_spec.req_body_type == 'json' and endpoint_spec.req_body_other:
try:
body_schema = json.loads(endpoint_spec.req_body_other) if isinstance(endpoint_spec.req_body_other, str) else endpoint_spec.req_body_other
body_schema_dict = json.loads(endpoint_spec.req_body_other) if isinstance(endpoint_spec.req_body_other, str) else endpoint_spec.req_body_other
except json.JSONDecodeError:
self.logger.warning(f"YAPI req_body_other for {endpoint_spec.path} is not valid JSON: {endpoint_spec.req_body_other}")
body_schema = None
self.logger.warning(f"YAPI req_body_other for {path_str} is not valid JSON: {endpoint_spec.req_body_other}")
elif isinstance(endpoint_spec, SwaggerEndpoint):
# Swagger specific parsing
if endpoint_spec.parameters:
for param_spec in endpoint_spec.parameters:
if param_spec.get('in') == 'path':
path_params_spec.append(param_spec)
elif param_spec.get('in') == 'query':
query_params_spec.append(param_spec)
elif param_spec.get('in') == 'header':
headers_spec.append(param_spec)
param_in = param_spec.get('in')
if param_in == 'path':
path_params_spec_list.append(param_spec)
elif param_in == 'query':
query_params_spec_list.append(param_spec)
elif param_in == 'header':
headers_spec_list.append(param_spec)
if endpoint_spec.request_body and 'content' in endpoint_spec.request_body:
json_content_spec = endpoint_spec.request_body['content'].get('application/json', {})
if 'schema' in json_content_spec:
body_schema = json_content_spec['schema']
body_schema_dict = json_content_spec['schema']
# 生成数据
path_params_data = {} # 例如: {"id": "default_id"} - 需要更智能的生成
if hasattr(endpoint_spec, 'path'):
# --- 生成路径参数数据 ---
path_params_data: Dict[str, Any] = {}
import re
param_names = re.findall(r'{([^}]+)}', endpoint_spec.path)
for name in param_names:
# 尝试从 path_params_spec (Swagger) 查找默认值或示例
# 简化:用占位符
path_params_data[name] = f"example_{name}"
# 从路径字符串中提取所有占位符名称,例如 /users/{id}/items/{itemId} -> ["id", "itemId"]
path_param_names_in_url = re.findall(r'{(.*?)}', path_str)
for p_name in path_param_names_in_url:
found_spec = None
# 尝试从 Swagger 的 path_params_spec_list 查找详细定义
for spec in path_params_spec_list:
if spec.get('name') == p_name:
found_spec = spec
break
# 尝试从 YAPI 的 req_params (如果之前有解析并填充到类似 path_params_spec_list 的结构)
# (当前YAPI的req_params未直接用于填充path_params_spec_list, 需要改进InputParser或此处逻辑)
# TODO: YAPI的req_params需要更可靠地映射到路径参数
query_params_data = {}
for q_param in query_params_spec: # YAPI: {'name': 'limit', 'value': '10'}, Swagger: {'name': 'limit', 'schema':{...}}
name = q_param.get('name')
if found_spec and isinstance(found_spec, dict):
# 如果找到参数的详细规格 (例如来自Swagger)
value = found_spec.get('example')
if value is None and found_spec.get('schema'):
value = self._generate_data_from_schema(found_spec['schema'])
path_params_data[p_name] = value if value is not None else f"example_{p_name}" # Fallback
else:
# 如果没有详细规格,生成一个通用占位符值
path_params_data[p_name] = f"example_{p_name}"
self.logger.debug(f"Path param '{p_name}' generated value: {path_params_data[p_name]}")
# --- 生成查询参数数据 ---
query_params_data: Dict[str, Any] = {}
for q_param_spec in query_params_spec_list:
name = q_param_spec.get('name')
if name:
# 优先使用示例或默认值然后是基于schema的生成
value = q_param.get('example', q_param.get('default'))
if value is None and q_param.get('schema'):
value = self._generate_data_from_schema(q_param['schema']) # 复用旧方法
elif value is None and 'value' in q_param : # YAPI style default/example in 'value'
value = q_param['value']
query_params_data[name] = value if value is not None else "example_query_value"
value = q_param_spec.get('example') # Swagger/OpenAPI style
if value is None and 'value' in q_param_spec: # YAPI style (value often holds example or default)
value = q_param_spec['value']
if value is None and q_param_spec.get('schema'): # Swagger/OpenAPI schema for param
value = self._generate_data_from_schema(q_param_spec['schema'])
elif value is None and q_param_spec.get('type'): # YAPI may define type directly
# Simplified schema generation for YAPI direct type if no 'value' field
value = self._generate_data_from_schema({'type': q_param_spec.get('type')})
query_params_data[name] = value if value is not None else f"example_query_{name}"
# --- 生成请求头数据 ---
headers_data: Dict[str, str] = {"Content-Type": "application/json", "Accept": "application/json"}
for h_param_spec in headers_spec_list:
name = h_param_spec.get('name')
if name and name.lower() not in ['content-type', 'accept']: # 不要覆盖基础的Content-Type/Accept除非明确
value = h_param_spec.get('example')
if value is None and 'value' in h_param_spec: # YAPI
value = h_param_spec['value']
if value is None and h_param_spec.get('schema'): # Swagger
value = self._generate_data_from_schema(h_param_spec['schema'])
elif value is None and h_param_spec.get('type'): # YAPI
value = self._generate_data_from_schema({'type': h_param_spec.get('type')})
headers_data = {"Content-Type": "application/json", "Accept": "application/json"} # 默认值
for h_param in headers_spec: # YAPI: {'name':'X-Token', 'value':'abc'}, Swagger: {'name':'X-Token', 'schema':{}}
name = h_param.get('name')
if name:
value = h_param.get('example', h_param.get('default'))
if value is None and h_param.get('schema'):
value = self._generate_data_from_schema(h_param['schema'])
elif value is None and 'value' in h_param: # YAPI style
value = h_param['value']
if value is not None:
headers_data[name] = str(value)
else:
headers_data[name] = f"example_header_{name}"
body_data = None
if body_schema:
body_data = self._generate_data_from_schema(body_schema) # 复用旧方法
# --- 生成请求体数据 ---
body_data: Optional[Any] = None
if body_schema_dict:
body_data = self._generate_data_from_schema(body_schema_dict)
return {
"path_params": path_params_data,

View File

@ -356,7 +356,6 @@ def main():
if not summary:
logger.error("测试执行失败")
return 1
# 打印结果摘要
summary.print_summary_to_console()

File diff suppressed because it is too large Load Diff