step2.1
This commit is contained in:
parent
82e79b393a
commit
61b7befb1f
Binary file not shown.
@ -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,
|
||||
|
||||
@ -356,7 +356,6 @@ def main():
|
||||
if not summary:
|
||||
logger.error("测试执行失败")
|
||||
return 1
|
||||
|
||||
# 打印结果摘要
|
||||
summary.print_summary_to_console()
|
||||
|
||||
|
||||
11704
test_report.json
11704
test_report.json
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user