适配业务

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
+465 -42
View File
@@ -40,43 +40,111 @@ def validate_response_is_true(response_ctx: APIResponseContext, stage_ctx: dict)
return ValidationResult(passed=False, message=f"Expected response data to be true, but it was '{response_data.get('data')}'.")
def validate_resource_details(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
"""Validates the details of a resource against the context."""
"""验证资源详情,支持READ(对象)和LIST(数组)两种响应格式"""
pk_name = stage_ctx.get("pk_name")
pk_value = stage_ctx.get("pk_value")
payload = stage_ctx.get("current_payload")
use_list_for_read = stage_ctx.get("use_list_for_read", False)
response_data = _get_value_by_path(response_ctx.json_content, "data")
if not isinstance(response_data, dict):
return ValidationResult(passed=False, message=f"Response 'data' field is not a JSON object. Got: {response_data}")
# Check if all fields from the payload exist in the response and match
if use_list_for_read:
# LIST响应:data是数组,需要在数组中找到匹配的资源
if not isinstance(response_data, list):
return ValidationResult(passed=False, message=f"LIST响应的'data'字段应该是数组。实际: {type(response_data)}")
# 在数组中查找匹配所有主键的资源
identity_id_list = stage_ctx.get("identity_id_list", [pk_name])
matching_resource = None
for item in response_data:
if isinstance(item, dict):
# 检查是否所有主键都匹配
all_match = True
for pk_field in identity_id_list:
if pk_field in payload and item.get(pk_field) != payload[pk_field]:
all_match = False
break
if all_match:
matching_resource = item
break
if not matching_resource:
return ValidationResult(passed=False, message=f"在LIST响应中未找到匹配的资源,主键: {identity_id_list}")
# 验证找到的资源
resource_to_validate = matching_resource
else:
# READ响应:data是对象
if not isinstance(response_data, dict):
return ValidationResult(passed=False, message=f"READ响应的'data'字段应该是对象。实际: {type(response_data)}")
resource_to_validate = response_data
# 验证资源字段
for key, expected_value in payload.items():
if key not in response_data:
return ValidationResult(passed=False, message=f"Field '{key}' from payload not found in response.")
if response_data[key] != expected_value:
return ValidationResult(passed=False, message=f"Field '{key}' mismatch. Expected '{expected_value}', got '{response_data[key]}'.")
if key not in resource_to_validate:
return ValidationResult(passed=False, message=f"字段'{key}'在响应中不存在。")
if resource_to_validate[key] != expected_value:
return ValidationResult(passed=False, message=f"字段'{key}'不匹配。期望: '{expected_value}', 实际: '{resource_to_validate[key]}'")
return ValidationResult(passed=True, message="Resource details successfully validated against payload.")
operation_type = "LIST查询" if use_list_for_read else "READ查询"
return ValidationResult(passed=True, message=f"资源详情验证成功({operation_type})。")
def validate_resource_details_after_update(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
"""Validates the details of a resource against the *update* payload from the context."""
"""验证更新后的资源详情,支持READ(对象)和LIST(数组)两种响应格式"""
pk_name = stage_ctx.get("pk_name")
pk_value = stage_ctx.get("pk_value")
payload = stage_ctx.get("update_payload") # Use the specific update payload
payload = stage_ctx.get("update_payload") # 使用更新负载
use_list_for_read = stage_ctx.get("use_list_for_read", False)
response_data = _get_value_by_path(response_ctx.json_content, "data")
if not isinstance(response_data, dict):
return ValidationResult(passed=False, message=f"Response 'data' field is not a JSON object. Got: {response_data}")
# Check if all fields from the payload exist in the response and match
if use_list_for_read:
# LIST响应:data是数组,需要在数组中找到匹配的资源
if not isinstance(response_data, list):
return ValidationResult(passed=False, message=f"LIST响应的'data'字段应该是数组。实际: {type(response_data)}")
# 在数组中查找匹配所有主键的资源
identity_id_list = stage_ctx.get("identity_id_list", [pk_name])
matching_resource = None
for item in response_data:
if isinstance(item, dict):
# 检查是否所有主键都匹配
all_match = True
for pk_field in identity_id_list:
if pk_field in payload and item.get(pk_field) != payload[pk_field]:
all_match = False
break
if all_match:
matching_resource = item
break
if not matching_resource:
return ValidationResult(passed=False, message=f"在LIST响应中未找到匹配的更新资源,主键: {identity_id_list}")
# 验证找到的资源
resource_to_validate = matching_resource
else:
# READ响应:data是对象
if not isinstance(response_data, dict):
return ValidationResult(passed=False, message=f"READ响应的'data'字段应该是对象。实际: {type(response_data)}")
resource_to_validate = response_data
# 验证更新后的资源字段
for key, expected_value in payload.items():
if key not in response_data:
return ValidationResult(passed=False, message=f"Field '{key}' from update_payload not found in response.")
if response_data[key] != expected_value:
return ValidationResult(passed=False, message=f"Field '{key}' mismatch. Expected '{expected_value}' from update_payload, got '{response_data[key]}'.")
if key not in resource_to_validate:
return ValidationResult(passed=False, message=f"更新字段'{key}'在响应中不存在。")
if resource_to_validate[key] != expected_value:
return ValidationResult(passed=False, message=f"更新字段'{key}'不匹配。期望: '{expected_value}', 实际: '{resource_to_validate[key]}'")
return ValidationResult(passed=True, message="Resource details successfully validated against update_payload.")
operation_type = "LIST查询" if use_list_for_read else "READ查询"
return ValidationResult(passed=True, message=f"更新后资源详情验证成功({operation_type})。")
def validate_resource_is_deleted(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
@@ -174,7 +242,21 @@ class DmsCrudScenarioStage(BaseAPIStage):
delete_op = current_scenario['delete']
pk_name = next(iter(delete_op.request_body['content']['application/json']['schema']['properties']['data']['items']['properties']))
# 获取完整的主键列表
identity_id_list = getattr(create_op, 'identity_id_list', [])
if not identity_id_list:
identity_id_list = [pk_name] if pk_name else []
# 为主要主键生成值
pk_value = str(uuid.uuid4())
# 为所有主键生成值
all_pk_values = {}
for pk_field in identity_id_list:
if pk_field == pk_name:
all_pk_values[pk_field] = pk_value
else:
all_pk_values[pk_field] = self._generate_default_key_value(pk_field, {"type": "string"})
# 使用测试框架的数据生成器生成完整有效的请求负载
# from ddms_compliance_suite.utils.schema_utils import DataGenerator
@@ -186,23 +268,76 @@ class DmsCrudScenarioStage(BaseAPIStage):
if 'application/json' in content and 'schema' in content['application/json']:
create_schema = content['application/json']['schema']
# 生成创建请求负载
data_generator = DataGenerator(logger_param=self.logger)
# 生成创建请求负载 - 优先使用LLM智能生成
create_payload = all_pk_values.copy() # 包含所有主键的基础负载
if create_schema:
# 生成基于模式的数据
generated_data = data_generator.generate_data_from_schema(create_schema)
# 确保主键字段存在且被正确设置
if isinstance(generated_data, dict) and 'data' in generated_data and isinstance(generated_data['data'], list) and len(generated_data['data']) > 0:
generated_data['data'][0][pk_name] = pk_value
create_payload = generated_data['data'][0]
# 尝试使用LLM智能生成数据(如果可用)
if self.llm_service:
self.logger.info(f"使用LLM为CRUD Stage生成智能测试数据,端点: {create_op.path}")
# 构建针对业务规则的提示
business_rules_prompt = self._build_business_rules_prompt(create_schema, pk_name, pk_value)
try:
llm_generated_data = self.llm_service.generate_data_from_schema(
create_schema,
prompt_instruction=business_rules_prompt,
max_tokens=1024,
temperature=0.1
)
if llm_generated_data and isinstance(llm_generated_data, dict):
# 处理LLM生成的数据结构
if 'data' in llm_generated_data and isinstance(llm_generated_data['data'], list) and len(llm_generated_data['data']) > 0:
create_payload = llm_generated_data['data'][0]
elif 'data' in llm_generated_data and isinstance(llm_generated_data['data'], dict):
create_payload = llm_generated_data['data']
else:
create_payload = llm_generated_data
# 确保所有主键字段正确设置
for pk_field, pk_val in all_pk_values.items():
create_payload[pk_field] = pk_val
self.logger.info(f"LLM成功生成智能测试数据: {create_payload}")
else:
self.logger.warning("LLM生成的数据格式不符合预期,回退到传统数据生成")
raise ValueError("LLM数据格式无效")
except Exception as e:
self.logger.warning(f"LLM数据生成失败: {e},回退到传统数据生成")
# 回退到传统数据生成
data_generator = DataGenerator(logger_param=self.logger)
generated_data = data_generator.generate_data_from_schema(create_schema, context_name="create_payload", llm_service=self.llm_service)
if isinstance(generated_data, dict) and 'data' in generated_data and isinstance(generated_data['data'], list) and len(generated_data['data']) > 0:
# 设置所有主键字段
for pk_field, pk_val in all_pk_values.items():
generated_data['data'][0][pk_field] = pk_val
create_payload = generated_data['data'][0]
elif isinstance(generated_data, dict):
# 设置所有主键字段
for pk_field, pk_val in all_pk_values.items():
generated_data[pk_field] = pk_val
create_payload = generated_data
else:
# 如果生成的数据结构不符合预期,使用基本负载
self.logger.warning("Generated data structure was not as expected. Falling back to a minimal payload.")
create_payload = { pk_name: pk_value }
# 使用传统数据生成器(但仍然传递LLM服务以便在内部尝试使用)
self.logger.info("LLM服务不可用,使用传统数据生成器")
data_generator = DataGenerator(logger_param=self.logger)
generated_data = data_generator.generate_data_from_schema(create_schema, context_name="create_payload", llm_service=None)
if isinstance(generated_data, dict) and 'data' in generated_data and isinstance(generated_data['data'], list) and len(generated_data['data']) > 0:
# 设置所有主键字段
for pk_field, pk_val in all_pk_values.items():
generated_data['data'][0][pk_field] = pk_val
create_payload = generated_data['data'][0]
elif isinstance(generated_data, dict):
# 设置所有主键字段
for pk_field, pk_val in all_pk_values.items():
generated_data[pk_field] = pk_val
create_payload = generated_data
else:
self.logger.warning("Generated data structure was not as expected. Falling back to a minimal payload.")
else:
# 如果没有模式,使用基本负载
self.logger.warning("No create schema found. Falling back to a minimal payload.")
create_payload = { pk_name: pk_value }
# 更新负载基于创建负载,但修改描述字段
update_payload = copy.deepcopy(create_payload)
@@ -216,13 +351,43 @@ class DmsCrudScenarioStage(BaseAPIStage):
stage_context["scenario_endpoints"] = current_scenario
# Pre-build the delete body to avoid key-templating issues later
# Per user request, the delete body should be an array of PK values
stage_context["delete_request_body"] = {"data": [pk_value]}
# 构建删除请求体,支持多主键的对象列表
delete_request_body = self._build_delete_request_body(current_scenario, pk_name, pk_value, create_payload)
stage_context["delete_request_body"] = delete_request_body
# 为查询步骤准备参数(单主键用READ,多主键用LIST)
if len(identity_id_list) > 1:
# 多主键:使用LIST操作,准备查询过滤条件
list_filter_payload = self._build_list_filter_payload(identity_id_list, all_pk_values)
stage_context["use_list_for_read"] = True
stage_context["list_filter_payload"] = list_filter_payload
self.logger.info(f"多主键场景,使用LIST操作代替READ,过滤条件: {list_filter_payload}")
else:
# 单主键:使用READ操作
read_path_params = {"id": pk_value}
stage_context["use_list_for_read"] = False
stage_context["read_path_params"] = read_path_params
self.logger.info(f"单主键场景,使用READ操作,路径参数: {read_path_params}")
def get_api_spec_for_operation(self, lookup_key: str, *args, **kwargs) -> Optional[Endpoint]:
"""
Resolves a lookup key like "CREATE" to the actual endpoint for the current scenario.
"""
# 处理动态的VERIFY_READ操作
if lookup_key == "VERIFY_READ":
# 从stage_context中获取是否使用LIST代替READ
stage_context = kwargs.get('stage_context', {})
use_list_for_read = stage_context.get('use_list_for_read', False)
if use_list_for_read:
# 多主键场景:使用LIST操作
lookup_key = "LIST"
self.logger.info("多主键场景:VERIFY_READ使用LIST操作")
else:
# 单主键场景:使用READ操作
lookup_key = "READ"
self.logger.info("单主键场景:VERIFY_READ使用READ操作")
op_map = {
"CREATE": "create", "READ": "read", "UPDATE": "update",
"DELETE": "delete", "LIST": "list"
@@ -230,7 +395,7 @@ class DmsCrudScenarioStage(BaseAPIStage):
op_type = op_map.get(lookup_key)
if not op_type:
return None
scenario = self.scenarios[self.current_scenario_index]
return scenario.get(op_type)
@@ -246,10 +411,11 @@ class DmsCrudScenarioStage(BaseAPIStage):
outputs_to_context={}
),
StageStepDefinition(
name="Step 2: Read Resource to Verify Creation",
endpoint_spec_lookup_key="READ",
name="Step 2: Verify Resource Creation",
endpoint_spec_lookup_key="VERIFY_READ", # 动态选择READ或LIST
request_overrides={
"path_params": {"id": "{{stage_context.pk_value}}"}
"path_params": "{{stage_context.read_path_params}}",
"request_body": "{{stage_context.list_filter_payload}}"
},
response_assertions=[validate_resource_details]
),
@@ -262,10 +428,11 @@ class DmsCrudScenarioStage(BaseAPIStage):
response_assertions=[validate_response_is_true]
),
StageStepDefinition(
name="Step 4: Read Resource to Verify Update",
endpoint_spec_lookup_key="READ",
name="Step 4: Verify Resource Update",
endpoint_spec_lookup_key="VERIFY_READ", # 动态选择READ或LIST
request_overrides={
"path_params": {"id": "{{stage_context.pk_value}}"}
"path_params": "{{stage_context.read_path_params}}",
"request_body": "{{stage_context.list_filter_payload}}"
},
response_assertions=[validate_resource_details_after_update]
),
@@ -300,4 +467,260 @@ class DmsCrudScenarioStage(BaseAPIStage):
# get resource name from first op
op_id = next(iter(scenario.values())).operation_id
resource_name = op_id.split('_', 1)[1]
stage_result.description += f" (Scenario for: {resource_name})"
stage_result.description += f" (Scenario for: {resource_name})"
return stage_result
def _build_business_rules_prompt(self, schema: Dict[str, Any], pk_name: str, pk_value: str) -> str:
"""构建包含业务规则的LLM提示"""
# 分析schema中的业务规则
business_rules = []
def analyze_properties(properties: Dict[str, Any], path: str = ""):
"""递归分析属性中的业务规则"""
for prop_name, prop_schema in properties.items():
current_path = f"{path}.{prop_name}" if path else prop_name
# 检查枚举值
if 'enum' in prop_schema:
enum_values = prop_schema['enum']
business_rules.append(f"字段 '{prop_name}' 只能取值: {enum_values}")
# 检查特殊字段的业务规则
if prop_name == 'bsflag':
business_rules.append(f"字段 'bsflag' 是删除标识,只能是 1(正常数据)或 -5(废弃数据)")
# 检查日期字段
if prop_schema.get('type') == 'date' or prop_schema.get('format') == 'date':
business_rules.append(f"字段 '{prop_name}' 是日期字段,需要使用合理的日期值")
# 检查必需字段
if prop_name in schema.get('required', []):
business_rules.append(f"字段 '{prop_name}' 是必需字段,不能为空")
# 检查字符串长度限制
if prop_schema.get('type') == 'string':
if 'maxLength' in prop_schema:
business_rules.append(f"字段 '{prop_name}' 最大长度为 {prop_schema['maxLength']}")
if 'minLength' in prop_schema:
business_rules.append(f"字段 '{prop_name}' 最小长度为 {prop_schema['minLength']}")
# 检查数值范围
if prop_schema.get('type') in ['number', 'integer']:
if 'minimum' in prop_schema:
business_rules.append(f"字段 '{prop_name}' 最小值为 {prop_schema['minimum']}")
if 'maximum' in prop_schema:
business_rules.append(f"字段 '{prop_name}' 最大值为 {prop_schema['maximum']}")
# 递归处理嵌套对象
if prop_schema.get('type') == 'object' and 'properties' in prop_schema:
analyze_properties(prop_schema['properties'], current_path)
# 处理数组中的对象
if prop_schema.get('type') == 'array' and 'items' in prop_schema:
items_schema = prop_schema['items']
if items_schema.get('type') == 'object' and 'properties' in items_schema:
analyze_properties(items_schema['properties'], f"{current_path}[]")
# 分析根级属性
if 'properties' in schema:
analyze_properties(schema['properties'])
# 处理数组类型的schema
if schema.get('type') == 'array' and 'items' in schema:
items_schema = schema['items']
if 'properties' in items_schema:
analyze_properties(items_schema['properties'])
# 构建提示文本
prompt = f"""请为DMS数据管理系统生成符合业务规则的测试数据。
主键信息:
- 主键字段: {pk_name}
- 主键值: {pk_value}
业务规则约束:
"""
if business_rules:
for i, rule in enumerate(business_rules, 1):
prompt += f"{i}. {rule}\n"
else:
prompt += "- 无特殊业务规则约束\n"
prompt += """
数据生成要求:
1. 严格遵守上述业务规则约束
2. 生成真实、合理的测试数据
3. 日期字段使用当前日期或合理的历史日期
4. 字符串字段使用有意义的中文内容
5. 数值字段使用合理的数值范围
6. 确保所有必需字段都有值
请生成一个完整的JSON对象,包含所有必要的字段和合理的测试数据。"""
return prompt
def _build_read_path_params(self, identity_id_list: List[str], all_pk_values: Dict[str, str]) -> Dict[str, str]:
"""构建READ步骤的路径参数"""
if not identity_id_list or len(identity_id_list) <= 1:
# 单主键:使用传统的id参数
primary_pk_value = next(iter(all_pk_values.values())) if all_pk_values else ""
return {"id": primary_pk_value}
else:
# 多主键:使用所有主键作为路径参数
path_params = {}
for pk_field in identity_id_list:
if pk_field in all_pk_values:
path_params[pk_field] = all_pk_values[pk_field]
else:
# 如果缺少某个主键值,生成默认值
path_params[pk_field] = self._generate_default_key_value(pk_field, {"type": "string"})
self.logger.warning(f"READ路径参数缺少主键 {pk_field},使用默认值: {path_params[pk_field]}")
self.logger.info(f"构建多主键READ路径参数: {path_params}")
return path_params
def _build_list_filter_payload(self, identity_id_list: List[str], all_pk_values: Dict[str, str]) -> Dict[str, Any]:
"""构建LIST操作的过滤条件,用于多主键场景的查询,使用简化的单条件模式"""
# 选择第一个可用的主键作为过滤条件(简化模式)
filter_key = None
filter_value = None
for pk_field in identity_id_list:
if pk_field in all_pk_values:
filter_key = pk_field
filter_value = all_pk_values[pk_field]
break
# 构建LIST请求体,使用固定的简化模式
if filter_key and filter_value:
list_payload = {
"isSearchCount": True,
"query": {
"fields": [],
"filter": {
"logic": "AND",
"realValue": [],
"subFilter": [
{
"key": filter_key,
"logic": "AND",
"realValue": [filter_value],
"subFilter": [],
"symbol": "="
}
]
}
}
}
self.logger.info(f"构建LIST过滤条件,使用主键 {filter_key}={filter_value}")
else:
# 没有可用的过滤条件,返回基本查询
list_payload = {
"isSearchCount": True,
"query": {
"fields": [],
"filter": {
"logic": "AND",
"realValue": [],
"subFilter": []
}
}
}
self.logger.warning("没有可用的主键过滤条件,将返回所有数据")
return list_payload
def _build_delete_request_body(self, scenario: Dict[str, Any], pk_name: str, pk_value: str, create_payload: Dict[str, Any]) -> Dict[str, Any]:
"""构建删除请求体,根据identityId列表长度决定格式"""
delete_op = scenario.get('delete')
if not delete_op or not isinstance(delete_op, DMSEndpoint):
# 回退到简单的主键值列表
self.logger.warning("无法获取删除操作信息,使用简单主键值列表")
return {"data": [pk_value]}
# 获取identityId列表
identity_id_list = getattr(delete_op, 'identity_id_list', [])
if not identity_id_list:
self.logger.warning("删除操作没有identityId信息,使用简单主键值列表")
return {"data": [pk_value]}
# 根据identityId列表长度判断删除格式
if len(identity_id_list) > 1:
# 多主键:使用对象列表
self.logger.info(f"检测到多主键删除操作,主键字段: {identity_id_list}")
return self._build_multi_key_delete_body(identity_id_list, pk_name, pk_value, create_payload)
else:
# 单主键:使用字符串列表
self.logger.info(f"检测到单主键删除操作,主键字段: {identity_id_list[0]}")
return {"data": [pk_value]}
def _build_multi_key_delete_body(self, identity_id_list: List[str], primary_pk_name: str, primary_pk_value: str, create_payload: Dict[str, Any]) -> Dict[str, Any]:
"""构建多主键的删除请求体"""
# 构建删除对象,包含所有主键字段
delete_object = {}
# 设置所有主键字段
for pk_field in identity_id_list:
if pk_field == primary_pk_name:
# 主要主键使用传入的值
delete_object[pk_field] = primary_pk_value
elif pk_field in create_payload:
# 从创建负载中提取其他主键
delete_object[pk_field] = create_payload[pk_field]
self.logger.debug(f"从创建负载中提取主键字段: {pk_field} = {create_payload[pk_field]}")
else:
# 为缺失的主键生成默认值
default_value = self._generate_default_key_value(pk_field, {"type": "string"})
delete_object[pk_field] = default_value
self.logger.debug(f"为删除对象生成默认主键值: {pk_field} = {default_value}")
# 支持批量删除:生成多个删除对象
delete_objects = [delete_object]
# 可以添加第二个对象用于测试批量删除
if len(identity_id_list) > 1:
second_object = delete_object.copy()
# 修改非主要主键的值来创建第二个对象
for pk_field in identity_id_list:
if pk_field != primary_pk_name and isinstance(second_object[pk_field], str):
original_value = second_object[pk_field]
if original_value.endswith('1'):
second_object[pk_field] = original_value[:-1] + '2'
else:
second_object[pk_field] = original_value + '_2'
delete_objects.append(second_object)
self.logger.info(f"生成批量删除对象,共{len(delete_objects)}个,主键字段: {identity_id_list}")
return {
"version": "1.0.0",
"data": delete_objects
}
def _generate_default_key_value(self, field_name: str, field_schema: Dict[str, Any]) -> str:
"""为主键字段生成默认值"""
field_type = field_schema.get('type', 'string')
if field_type == 'string':
# 根据字段名生成语义化的值
if 'project' in field_name.lower():
return f"项目{uuid.uuid4().hex[:4]}"
elif 'survey' in field_name.lower():
return f"工区{uuid.uuid4().hex[:4]}"
elif 'site' in field_name.lower():
return f"站点{uuid.uuid4().hex[:4]}"
else:
return f"{field_name}_{uuid.uuid4().hex[:8]}"
elif field_type in ['number', 'integer']:
return 1
else:
return f"default_{field_name}"