增加宽容的schema验证
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@ from enum import Enum
|
||||
from typing import Any, Dict, Optional, List, Tuple, Type, Union
|
||||
import logging
|
||||
from .utils import schema_utils
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class TestSeverity(Enum):
|
||||
"""测试用例的严重程度"""
|
||||
@@ -114,7 +115,11 @@ class BaseAPITestCase:
|
||||
use_llm_for_query_params: bool = False
|
||||
use_llm_for_headers: bool = False
|
||||
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
|
||||
def __init__(self,
|
||||
endpoint_spec: Dict[str, Any],
|
||||
global_api_spec: Dict[str, Any],
|
||||
json_schema_validator: Optional[Any] = None,
|
||||
llm_service: Optional[Any] = None):
|
||||
"""
|
||||
初始化测试用例。
|
||||
Args:
|
||||
|
||||
@@ -42,6 +42,8 @@ except ImportError:
|
||||
LLMService = None
|
||||
logging.getLogger(__name__).info("LLMService 未找到,LLM 相关功能将不可用。")
|
||||
|
||||
from ddms_compliance_suite.utils.schema_provider import SchemaProvider
|
||||
|
||||
_dynamic_model_cache: Dict[str, Type[BaseModel]] = {}
|
||||
|
||||
class ExecutedTestCaseResult:
|
||||
@@ -510,6 +512,10 @@ class APITestOrchestrator:
|
||||
logging.info(f"strictness_level: {self.strictness_level}")
|
||||
elif strictness_level:
|
||||
logging.warning(f"提供了无效的严格等级 '{strictness_level}'。将使用默认行为。有效值: {', '.join([e.name for e in TestSeverity])}")
|
||||
|
||||
# 将这些属性的初始化移到此处,并设为None,避免在_execute_tests_from_parsed_spec之前被错误使用
|
||||
self.json_schema_validator: Optional[JSONSchemaValidator] = None
|
||||
self.schema_provider: Optional[SchemaProvider] = None
|
||||
|
||||
def get_api_call_details(self) -> List[APICallDetail]:
|
||||
"""Returns the collected list of API call details."""
|
||||
@@ -981,7 +987,7 @@ class APITestOrchestrator:
|
||||
endpoint_spec=endpoint_spec_dict,
|
||||
global_api_spec=global_spec_dict,
|
||||
json_schema_validator=self.json_validator,
|
||||
llm_service=self.llm_service # Pass the orchestrator's LLM service instance
|
||||
llm_service=self.llm_service
|
||||
)
|
||||
self.logger.info(f"开始执行测试用例 '{test_case_instance.id}' ({test_case_instance.name}) for endpoint '{endpoint_spec_dict.get('method', 'N/A')} {endpoint_spec_dict.get('path', 'N/A')}'")
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,38 @@
|
||||
from typing import Any, List
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def extract_data_for_validation(response_json: Any) -> List[Any]:
|
||||
"""
|
||||
从原始API响应JSON中智能提取需要被验证的核心业务数据列表。
|
||||
即使只有一个对象,也返回一个单元素的列表。
|
||||
|
||||
策略:
|
||||
1. 如果响应体是包含 'code' 和 'data' 的标准包装,则提取 'data' 的内容。
|
||||
2. 如果处理后的数据是列表,直接返回该列表。
|
||||
3. 如果处理后的数据是单个对象(字典),将其包装在单元素列表中返回。
|
||||
4. 如果数据为空或不适用,返回空列表。
|
||||
"""
|
||||
if not response_json:
|
||||
return []
|
||||
|
||||
data_to_process = response_json
|
||||
|
||||
# 策略 1: 解开标准包装
|
||||
if isinstance(response_json, dict) and 'code' in response_json and 'data' in response_json:
|
||||
logger.debug("检测到标准响应包装,提取 'data' 字段内容进行处理。")
|
||||
data_to_process = response_json['data']
|
||||
|
||||
# 策略 2: 统一返回列表
|
||||
if isinstance(data_to_process, list):
|
||||
logger.debug(f"数据本身为列表,包含 {len(data_to_process)} 个元素,直接返回。")
|
||||
return data_to_process
|
||||
|
||||
if isinstance(data_to_process, dict):
|
||||
logger.debug("数据为单个对象,将其包装在列表中返回。")
|
||||
return [data_to_process]
|
||||
|
||||
# 对于其他情况(如数据为None或非对象/列表类型),返回空列表
|
||||
logger.warning(f"待处理的数据既不是列表也不是对象,无法提取进行验证。数据: {str(data_to_process)[:100]}")
|
||||
return []
|
||||
@@ -0,0 +1,60 @@
|
||||
import logging
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class SchemaProvider:
|
||||
def __init__(self, global_api_spec: Dict[str, Any]):
|
||||
self.global_api_spec = global_api_spec
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def get_schema(self, endpoint_spec: Dict[str, Any], status_code: int) -> Optional[Dict]:
|
||||
"""
|
||||
获取端点在特定状态码下的响应Schema。
|
||||
|
||||
当前实现:从API规范中查找。
|
||||
未来可扩展:优先从动态映射中获取,如果失败或未配置,则回退到当前实现。
|
||||
"""
|
||||
# --- 预留的动态获取逻辑扩展点 ---
|
||||
# if self._use_dynamic_provider(endpoint_spec):
|
||||
# schema = self._fetch_dynamic_schema(endpoint_spec)
|
||||
# if schema:
|
||||
# return schema
|
||||
# ---------------------------------
|
||||
|
||||
return self._get_schema_from_spec(endpoint_spec, status_code)
|
||||
|
||||
def _get_schema_from_spec(self, endpoint_spec: Dict[str, Any], status_code: int) -> Optional[Dict]:
|
||||
"""
|
||||
(私有方法) 从API规范中提取Schema,这是当前版本的主要实现。
|
||||
"""
|
||||
self.logger.debug(f"尝试从API规范中为状态码 {status_code} 查找Schema。")
|
||||
|
||||
expected_schema = None
|
||||
|
||||
# 兼容 OpenAPI/Swagger 格式
|
||||
if 'responses' in endpoint_spec:
|
||||
responses = endpoint_spec['responses']
|
||||
# 优先匹配精确的状态码
|
||||
if str(status_code) in responses:
|
||||
response_def = responses[str(status_code)]
|
||||
if 'content' in response_def and 'application/json' in response_def['content']:
|
||||
expected_schema = response_def['content']['application/json'].get('schema')
|
||||
# 回退到 'default'
|
||||
elif 'default' in responses:
|
||||
response_def = responses['default']
|
||||
if 'content' in response_def and 'application/json' in response_def['content']:
|
||||
expected_schema = response_def['content']['application/json'].get('schema')
|
||||
|
||||
# 兼容 YAPI 格式 (简化)
|
||||
elif 'res_body_type' in endpoint_spec and endpoint_spec['res_body_type'] == 'json':
|
||||
if endpoint_spec.get('res_body_is_json_schema') and endpoint_spec.get('res_body'):
|
||||
try:
|
||||
expected_schema = json.loads(endpoint_spec['res_body'])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
self.logger.error(f"从YAPI res_body解析JSON schema失败。res_body: {endpoint_spec['res_body']}")
|
||||
return None # 解析失败
|
||||
|
||||
if not expected_schema:
|
||||
self.logger.info(f"在API规范中未找到针对状态码 {status_code} 的JSON schema。")
|
||||
|
||||
return expected_schema
|
||||
Reference in New Issue
Block a user