适配业务

This commit is contained in:
gongwenxin
2025-08-07 22:44:57 +08:00
parent 7846479a1b
commit 4212d47400
19 changed files with 2818 additions and 967 deletions
+69 -9
View File
@@ -387,7 +387,8 @@ class DMSEndpoint(BaseEndpoint):
raw_record: Optional[Dict[str, Any]] = None,
test_mode: str = 'standalone',
operation_id: Optional[str] = None,
model_pk_name: Optional[str] = None):
model_pk_name: Optional[str] = None,
identity_id_list: Optional[List[str]] = None):
super().__init__(method=method.upper(), path=path)
self.title = title
self.request_body = request_body
@@ -398,6 +399,7 @@ class DMSEndpoint(BaseEndpoint):
self.test_mode = test_mode
self.operation_id = operation_id or f"{self.method.lower()}_{self.category_name or 'dms'}_{title.replace(' ', '_')}"
self.model_pk_name = model_pk_name
self.identity_id_list = identity_id_list or []
def to_dict(self) -> Dict[str, Any]:
"""Converts the DMS endpoint data into a standardized OpenAPI-like dictionary."""
@@ -643,27 +645,85 @@ class InputParser:
# Create Endpoint (POST)
create_path = f"/api/dms/{dms_instance_code}/v1/{name}"
create_request_body_schema = {"type": "object", "properties": {"version": {"type": "string", "example": version}, "act": {"type": "integer", "example": 0}, "data": {"type": "array", "items": model}}, "required": ["data"]}
endpoints.append(DMSEndpoint(path=create_path, method='post', title=f"Create {name}", request_body={'content': {'application/json': {'schema': create_request_body_schema}}}, responses=success_response, test_mode='scenario_only', operation_id=f"create_{name}", category_name=category_name, raw_record=item, model_pk_name=pk_name))
endpoints.append(DMSEndpoint(path=create_path, method='post', title=f"Create {name}", request_body={'content': {'application/json': {'schema': create_request_body_schema}}}, responses=success_response, test_mode='scenario_only', operation_id=f"create_{name}", category_name=category_name, raw_record=item, model_pk_name=pk_name, identity_id_list=identity_id_list))
# List Endpoint (POST)
list_path = f"/api/dms/{dms_instance_code}/v1/{name}/{version}"
list_response_schema = {"type": "object", "properties": {"code": {"type": "integer"}, "message": {"type": "string"}, "data": {"type": "array", "items": model}}}
endpoints.append(DMSEndpoint(path=list_path, method='post', title=f"List {name}", request_body={'content': {'application/json': {'schema': {}}}}, responses={'200': {'description': 'Successful Operation', 'content': {'application/json': {'schema': list_response_schema}}}}, test_mode='standalone', operation_id=f"list_{name}", category_name=category_name, raw_record=item, model_pk_name=pk_name))
endpoints.append(DMSEndpoint(path=list_path, method='post', title=f"List {name}", request_body={'content': {'application/json': {'schema': {}}}}, responses={'200': {'description': 'Successful Operation', 'content': {'application/json': {'schema': list_response_schema}}}}, test_mode='standalone', operation_id=f"list_{name}", category_name=category_name, raw_record=item, model_pk_name=pk_name, identity_id_list=identity_id_list))
# Read Endpoint (GET)
read_path = f"/api/dms/{dms_instance_code}/v1/{name}/{version}/{{id}}"
if isinstance(identity_id_list, list) and len(identity_id_list) > 1:
# 多主键:使用复合路径参数
path_params = []
read_parameters = []
for pk_field in identity_id_list:
path_params.append(f"{{{pk_field}}}")
if pk_field in model['properties']:
pk_field_schema = model['properties'][pk_field]
else:
pk_field_schema = {"type": "string"}
read_parameters.append({'name': pk_field, 'in': 'path', 'required': True, 'description': f'The {pk_field} of the {name}', 'schema': pk_field_schema})
read_path = f"/api/dms/{dms_instance_code}/v1/{name}/{version}/" + "/".join(path_params)
self.logger.info(f"创建多主键读取端点 '{name}',路径参数: {identity_id_list}")
else:
# 单主键:使用单个id参数
read_path = f"/api/dms/{dms_instance_code}/v1/{name}/{version}/{{id}}"
read_parameters = [{'name': 'id', 'in': 'path', 'required': True, 'description': f'The ID of the {name}, maps to {pk_name}', 'schema': pk_schema}]
self.logger.info(f"创建单主键读取端点 '{name}',路径参数: id")
read_response_schema = {"type": "object", "properties": {"code": {"type": "integer"}, "message": {"type": "string"}, "data": model}}
read_parameters = [{'name': 'id', 'in': 'path', 'required': True, 'description': f'The ID of the {name}, maps to {pk_name}', 'schema': pk_schema}]
endpoints.append(DMSEndpoint(path=read_path, method='get', title=f"Read {name}", request_body=None, responses={'200': {'description': 'Successful Operation', 'content': {'application/json': {'schema': read_response_schema}}}}, parameters=read_parameters, test_mode='scenario_only', operation_id=f"read_{name}", category_name=category_name, raw_record=item, model_pk_name=pk_name))
endpoints.append(DMSEndpoint(path=read_path, method='get', title=f"Read {name}", request_body=None, responses={'200': {'description': 'Successful Operation', 'content': {'application/json': {'schema': read_response_schema}}}}, parameters=read_parameters, test_mode='scenario_only', operation_id=f"read_{name}", category_name=category_name, raw_record=item, model_pk_name=pk_name, identity_id_list=identity_id_list))
# Update Endpoint (PUT)
update_path = f"/api/dms/{dms_instance_code}/v1/{name}"
endpoints.append(DMSEndpoint(path=update_path, method='put', title=f"Update {name}", request_body={'content': {'application/json': {'schema': create_request_body_schema}}}, responses=success_response, test_mode='scenario_only', operation_id=f"update_{name}", category_name=category_name, raw_record=item, model_pk_name=pk_name))
endpoints.append(DMSEndpoint(path=update_path, method='put', title=f"Update {name}", request_body={'content': {'application/json': {'schema': create_request_body_schema}}}, responses=success_response, test_mode='scenario_only', operation_id=f"update_{name}", category_name=category_name, raw_record=item, model_pk_name=pk_name, identity_id_list=identity_id_list))
# Delete Endpoint (DELETE)
delete_path = f"/api/dms/{dms_instance_code}/v1/{name}"
delete_request_body_schema = {"type": "object", "properties": {"version": {"type": "string", "example": version}, "data": {"type": "array", "items": {"type": "object", "properties": { pk_name: pk_schema }, "required": [pk_name]}}}, "required": ["data"]}
endpoints.append(DMSEndpoint(path=delete_path, method='delete', title=f"Delete {name}", request_body={'content': {'application/json': {'schema': delete_request_body_schema}}}, responses=success_response, test_mode='scenario_only', operation_id=f"delete_{name}", category_name=category_name, raw_record=item, model_pk_name=pk_name))
# 根据identityId列表长度决定删除schema结构
if isinstance(identity_id_list, list) and len(identity_id_list) > 1:
# 多主键:使用对象数组
delete_items_properties = {}
delete_required_fields = []
for pk_field in identity_id_list:
if pk_field in model['properties']:
delete_items_properties[pk_field] = model['properties'][pk_field]
delete_required_fields.append(pk_field)
delete_request_body_schema = {
"type": "object",
"properties": {
"version": {"type": "string", "example": version},
"data": {
"type": "array",
"items": {
"type": "object",
"properties": delete_items_properties,
"required": delete_required_fields
}
}
},
"required": ["data"]
}
self.logger.info(f"创建多主键删除端点 '{name}',主键字段: {identity_id_list}")
else:
# 单主键:使用字符串数组
delete_request_body_schema = {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["data"]
}
self.logger.info(f"创建单主键删除端点 '{name}',主键字段: {pk_name}")
endpoints.append(DMSEndpoint(path=delete_path, method='delete', title=f"Delete {name}", request_body={'content': {'application/json': {'schema': delete_request_body_schema}}}, responses=success_response, test_mode='scenario_only', operation_id=f"delete_{name}", category_name=category_name, raw_record=item, model_pk_name=pk_name, identity_id_list=identity_id_list))
# The 'spec' for ParsedDMSSpec should represent the whole document.
# We can construct a dictionary holding all the raw data we fetched.
+13 -8
View File
@@ -425,9 +425,9 @@ class APITestOrchestrator:
MAX_RECURSION_DEPTH_PYDANTIC = 10 # 新增一个常量用于 Pydantic 模型创建的递归深度限制
def __init__(self, base_url: str,
custom_test_cases_dir: Optional[str] = None,
stages_dir: Optional[str] = None,
def __init__(self, base_url: str,
custom_test_cases_dir: Optional[str] = None,
stages_dir: Optional[str] = None,
llm_api_key: Optional[str] = None,
llm_base_url: Optional[str] = None,
llm_model_name: Optional[str] = None,
@@ -436,7 +436,8 @@ class APITestOrchestrator:
use_llm_for_query_params: bool = False,
use_llm_for_headers: bool = False,
output_dir: Optional[str] = None,
strictness_level: Optional[str] = None
strictness_level: Optional[str] = None,
ignore_ssl: bool = False
):
"""
初始化测试编排器。
@@ -454,12 +455,14 @@ class APITestOrchestrator:
use_llm_for_headers (bool): 是否使用LLM生成头部参数。
output_dir (Optional[str]): 测试报告和工件的输出目录。
strictness_level (Optional[str]): 测试的严格等级, 如 'CRITICAL', 'HIGH'
ignore_ssl (bool): 是否忽略SSL证书验证。
"""
self.logger = logging.getLogger(__name__)
self.base_url = base_url.rstrip('/')
self.api_caller = APICaller()
self.base_url = base_url.rstrip('/')
self.api_caller = APICaller()
self.test_case_registry = TestCaseRegistry(test_cases_dir=custom_test_cases_dir)
self.global_api_call_details: List[APICallDetail] = []
self.global_api_call_details: List[APICallDetail] = []
self.ignore_ssl = ignore_ssl
self.stages_dir = stages_dir
self.stage_registry: Optional[StageRegistry] = None
@@ -2655,7 +2658,9 @@ class APITestOrchestrator:
parser = InputParser()
self.logger.info("从DMS动态服务启动测试...")
parsed_spec = parser.parse_dms_spec(domain_mapping_path, base_url=self.base_url, ignore_ssl=ignore_ssl)
# 如果方法参数中没有传递ignore_ssl,使用实例的设置
actual_ignore_ssl = ignore_ssl if ignore_ssl else self.ignore_ssl
parsed_spec = parser.parse_dms_spec(domain_mapping_path, base_url=self.base_url, ignore_ssl=actual_ignore_ssl)
if not parsed_spec:
self.logger.error("无法从DMS服务解析API,测试终止。")
+93 -5
View File
@@ -20,15 +20,17 @@ class DataGenerator:
def generate_data_from_schema(self, schema: Dict[str, Any],
context_name: Optional[str] = None,
operation_id: Optional[str] = None) -> Any:
operation_id: Optional[str] = None,
llm_service=None) -> Any:
"""
Generates test data from a JSON Schema.
This method was extracted and generalized from APITestOrchestrator.
Args:
schema: The JSON schema to generate data from.
context_name: A name for the context (e.g., 'requestBody'), for logging.
operation_id: The operation ID, for logging.
llm_service: Optional LLM service for intelligent data generation.
Returns:
Generated data that conforms to the schema.
@@ -66,17 +68,28 @@ class DataGenerator:
# Handle both 'object' and 'Object' (case-insensitive)
if schema_type and schema_type.lower() == 'object':
# 尝试使用LLM智能生成(如果可用且schema包含描述信息)
if llm_service and self._should_use_llm_for_schema(schema):
try:
llm_data = self._generate_with_llm(schema, llm_service, context_name, operation_id)
if llm_data is not None:
self.logger.debug(f"{log_prefix}LLM successfully generated data for{context_log}")
return llm_data
except Exception as e:
self.logger.debug(f"{log_prefix}LLM generation failed for{context_log}: {e}, falling back to traditional generation")
# 传统生成方式
result = {}
properties = schema.get('properties', {})
self.logger.debug(f"{log_prefix}Generating object data for{context_log}. Properties: {list(properties.keys())}")
for prop_name, prop_schema in properties.items():
nested_context = f"{context_name}.{prop_name}" if context_name else prop_name
result[prop_name] = self.generate_data_from_schema(prop_schema, nested_context, operation_id)
result[prop_name] = self.generate_data_from_schema(prop_schema, nested_context, operation_id, llm_service)
additional_properties = schema.get('additionalProperties')
if isinstance(additional_properties, dict):
self.logger.debug(f"{log_prefix}Generating an example property for additionalProperties for{context_log}")
result['additionalProp1'] = self.generate_data_from_schema(additional_properties, f"{context_name}.additionalProp1", operation_id)
result['additionalProp1'] = self.generate_data_from_schema(additional_properties, f"{context_name}.additionalProp1", operation_id, llm_service)
return result
# Handle both 'array' and 'Array' (case-insensitive)
@@ -117,3 +130,78 @@ class DataGenerator:
self.logger.warning(f"{log_prefix}Unsupported schema type '{schema_type}' in {context_log}. Schema: {schema}")
return None
def _should_use_llm_for_schema(self, schema: Dict[str, Any]) -> bool:
"""判断是否应该使用LLM来生成数据"""
# 检查schema是否包含足够的描述信息来让LLM理解
properties = schema.get('properties', {})
# 如果有字段包含描述信息,就使用LLM
for prop_name, prop_schema in properties.items():
if isinstance(prop_schema, dict):
# 检查是否有描述信息
if prop_schema.get('description') or prop_schema.get('title'):
return True
# 检查是否有特殊的业务字段(如bsflag)
if prop_name in ['bsflag', 'dataSource', 'dataRegion', 'surveyType', 'siteType']:
return True
return False
def _generate_with_llm(self, schema: Dict[str, Any], llm_service, context_name: str, operation_id: str) -> Any:
"""使用LLM生成数据"""
# 构建包含字段描述的提示
prompt = self._build_llm_prompt(schema, context_name, operation_id)
# 调用LLM服务
if hasattr(llm_service, 'generate_data_from_schema'):
return llm_service.generate_data_from_schema(
schema,
prompt_instruction=prompt,
max_tokens=512,
temperature=0.1
)
else:
# 如果LLM服务没有专门的方法,返回None让其回退到传统生成
return None
def _build_llm_prompt(self, schema: Dict[str, Any], context_name: str, operation_id: str) -> str:
"""构建LLM提示,包含字段描述信息"""
properties = schema.get('properties', {})
prompt = f"""请为以下JSON Schema生成合理的测试数据。
操作上下文: {operation_id or 'unknown'}
数据上下文: {context_name or 'unknown'}
字段说明:
"""
for prop_name, prop_schema in properties.items():
if isinstance(prop_schema, dict):
prop_type = prop_schema.get('type', 'unknown')
title = prop_schema.get('title', '')
description = prop_schema.get('description', '')
prompt += f"- {prop_name} ({prop_type})"
if title:
prompt += f" - {title}"
if description:
prompt += f": {description}"
prompt += "\n"
prompt += """
请根据字段的描述信息生成合理的测试数据:
1. 严格遵守字段描述中的业务规则
2. 生成真实、有意义的测试数据
3. 对于有特定取值范围的字段,请选择合适的值
4. 日期字段使用合理的日期格式
5. 返回一个完整的JSON对象
请只返回JSON数据,不要包含其他说明文字。"""
return prompt