适配业务
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,测试终止。")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user