v0-json-schema

This commit is contained in:
gongwenxin
2025-05-16 15:18:02 +08:00
parent 32676a314f
commit 6d0b70fe11
260 changed files with 280278 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+180
View File
@@ -0,0 +1,180 @@
"""API Caller Module"""
import requests
from typing import Any, Dict, Optional, Union, List
from pydantic import BaseModel, Field, HttpUrl
# It's a good practice to define input/output models,
# even for internal components, using Pydantic.
class APIRequest(BaseModel):
method: str # GET, POST, PUT, DELETE, etc.
url: HttpUrl
headers: Optional[Dict[str, str]] = None
params: Optional[Dict[str, Any]] = None
json_data: Optional[Any] = None # For POST/PUT with JSON body (can be dict, list, string, etc.)
body: Optional[Any] = Field(default=None, description="Alias for json_data") # 添加别名,方便调用
data: Optional[Any] = None # For form data etc.
timeout: int = 30 # seconds
def model_post_init(self, __context):
"""初始化后处理,将 body 赋值给 json_data(如果提供了body"""
if self.body is not None and self.json_data is None:
self.json_data = self.body
class APIResponse(BaseModel):
status_code: int
headers: Dict[str, str]
content: bytes # Raw content
json_content: Optional[Any] = None # Parsed JSON content if applicable
elapsed_time: float # in seconds
class APICaller:
"""
Responsible for executing HTTP/S API calls to the DDMS services.
"""
def __init__(self, default_timeout: int = 30, default_headers: Optional[Dict[str, str]] = None):
self.default_timeout = default_timeout
self.default_headers = default_headers or {}
def call_api(self, request_data: APIRequest) -> APIResponse:
"""
Makes an API call based on the provided request data.
Args:
request_data: An APIRequest Pydantic model instance.
Returns:
An APIResponse Pydantic model instance.
"""
merged_headers = {**self.default_headers, **(request_data.headers or {})}
timeout = request_data.timeout or self.default_timeout
try:
# 如果提供了 body,使用它作为 json 参数
json_payload = request_data.json_data
response = requests.request(
method=request_data.method.upper(),
url=str(request_data.url),
headers=merged_headers,
params=request_data.params,
json=json_payload,
data=request_data.data,
timeout=timeout
)
# 不立即引发异常,而是捕获状态码
status_code = response.status_code
json_content = None
try:
if response.headers.get('Content-Type', '').startswith('application/json'):
json_content = response.json()
except requests.exceptions.JSONDecodeError:
# Not a JSON response or invalid JSON, that's fine for some cases.
pass
return APIResponse(
status_code=status_code,
headers=dict(response.headers),
content=response.content,
json_content=json_content,
elapsed_time=response.elapsed.total_seconds()
)
except requests.exceptions.HTTPError as e:
# 处理 HTTP 错误
print(f"API call to {request_data.url} failed: {e}")
return APIResponse(
status_code=e.response.status_code,
headers=dict(e.response.headers),
content=e.response.content,
json_content=None,
elapsed_time=e.response.elapsed.total_seconds()
)
except requests.exceptions.RequestException as e:
# 处理其他请求异常
print(f"API call to {request_data.url} failed: {e}")
return APIResponse(
status_code=getattr(e.response, 'status_code', 500),
headers=dict(getattr(e.response, 'headers', {})),
content=str(e).encode(),
json_content=None,
elapsed_time=0
)
# Example Usage (can be moved to tests or main application logic)
if __name__ == '__main__':
caller = APICaller(default_headers={"X-App-Name": "DDMSComplianceSuite"})
# Example GET request
get_req_data = APIRequest(
method="GET",
url=HttpUrl("https://jsonplaceholder.typicode.com/todos/1"),
headers={"X-Request-ID": "12345"}
)
response = caller.call_api(get_req_data)
print("GET Response:")
if response.json_content:
print(f"Status: {response.status_code}, Data: {response.json_content}")
else:
print(f"Status: {response.status_code}, Content: {response.content.decode()}")
print(f"Time taken: {response.elapsed_time:.4f}s")
print("\n")
# Example POST request with json_data
post_req_data = APIRequest(
method="POST",
url=HttpUrl("https://jsonplaceholder.typicode.com/posts"),
json_data={"title": "foo", "body": "bar", "userId": 1},
headers={"Content-Type": "application/json; charset=UTF-8"}
)
response = caller.call_api(post_req_data)
print("POST Response with json_data:")
if response.json_content:
print(f"Status: {response.status_code}, Data: {response.json_content}")
else:
print(f"Status: {response.status_code}, Content: {response.content.decode()}")
print(f"Time taken: {response.elapsed_time:.4f}s")
# Example POST request with body (alias for json_data)
post_req_data_with_body = APIRequest(
method="POST",
url=HttpUrl("https://jsonplaceholder.typicode.com/posts"),
body={"title": "using body", "body": "testing body alias", "userId": 2},
headers={"Content-Type": "application/json; charset=UTF-8"}
)
response = caller.call_api(post_req_data_with_body)
print("\nPOST Response with body:")
if response.json_content:
print(f"Status: {response.status_code}, Data: {response.json_content}")
else:
print(f"Status: {response.status_code}, Content: {response.content.decode()}")
print(f"Time taken: {response.elapsed_time:.4f}s")
# Example list body request (demonstrating array body support)
array_req_data = APIRequest(
method="POST",
url=HttpUrl("https://jsonplaceholder.typicode.com/posts"),
body=["test_string", "another_value"],
headers={"Content-Type": "application/json; charset=UTF-8"}
)
response = caller.call_api(array_req_data)
print("\nPOST Response with array body:")
if response.json_content:
print(f"Status: {response.status_code}, Data: {response.json_content}")
else:
print(f"Status: {response.status_code}, Content: {response.content.decode()}")
print(f"Time taken: {response.elapsed_time:.4f}s")
# Example Error request (non-existent domain)
error_req_data = APIRequest(
method="GET",
url=HttpUrl("https://nonexistentdomain.invalid"),
)
response = caller.call_api(error_req_data)
print("\nError GET Response:")
print(f"Status: {response.status_code}, Content: {response.content.decode()}")
print(f"Time taken: {response.elapsed_time:.4f}s")
@@ -0,0 +1 @@
@@ -0,0 +1,61 @@
"""Assertion Engine Module"""
from typing import Any, Dict, List
# from ..models.rule_models import BusinessAssertionTemplate # Assuming rule_models.py will exist
class AssertionEngine:
"""
Responsible for verifying test step results based on predefined rules.
This is a placeholder and will need significant development based on
how assertion rules are defined and evaluated (e.g., Python expressions, JSONPath, etc.).
"""
def __init__(self):
# Initialization, potentially loading common assertion helpers or context
pass
def evaluate_assertion(self, assertion_rule: Any, context_data: Dict[str, Any]) -> bool:
"""
Evaluates a single assertion rule against the given context data.
Args:
assertion_rule: The rule definition (e.g., a Pydantic model like BusinessAssertionTemplate).
The structure of this will depend on your rule design.
context_data: Data from the test execution context (e.g., API response, extracted variables).
Returns:
True if the assertion passes, False otherwise.
"""
# Placeholder logic - this needs to be implemented based on rule type
# Example: if rule is a python expression
# if assertion_rule.template_language == "python_expression":
# try:
# # Ensure the expression is safe to eval!
# # Consider using ast.literal_eval for simple cases or a safer evaluation library.
# # For complex expressions, a dedicated DSL or restricted environment is better.
# # The context_data would be made available to the expression.
# return bool(eval(assertion_rule.template_expression, {}, context_data))
# except Exception as e:
# print(f"Error evaluating Python expression assertion: {e}")
# return False
# Example: if rule is a simple equality check (defined differently)
# if "expected_value" in assertion_rule and "actual_value_path" in assertion_rule:
# actual_value = get_value_from_path(context_data, assertion_rule.actual_value_path) # Needs helper
# return actual_value == assertion_rule.expected_value
print(f"[AssertionEngine] Placeholder: Evaluating rule '{getattr(assertion_rule, "name", "Unnamed Rule")}'. Context: {context_data}")
# This is a very basic placeholder. Real implementation depends heavily on rule definition.
return True # Default to True for now
# Helper function example (would likely be more complex or use a library like jsonpath-ng)
# def get_value_from_path(data: Dict[str, Any], path: str) -> Any:
# """Retrieves a value from a nested dict using a simple dot-separated path."""
# keys = path.split('.')
# value = data
# for key in keys:
# if isinstance(value, dict) and key in value:
# value = value[key]
# else:
# return None # Or raise an error
# return value
+1
View File
@@ -0,0 +1 @@
+113
View File
@@ -0,0 +1,113 @@
"""Configuration Management Module"""
import yaml
from pydantic import ValidationError # BaseModel is not directly used here anymore for defining AppConfig
from typing import Optional, Any
from ..models.config_models import AppConfig # Import AppConfig from the new location
class ConfigurationManager:
"""
Loads and manages application configuration from a YAML file.
Uses Pydantic for validation.
"""
def __init__(self, config_path: str = "configs/config.yaml"):
self.config_path = config_path
self.config: Optional[AppConfig] = None
self._load_config()
def _load_config(self):
try:
with open(self.config_path, 'r') as f:
raw_config = yaml.safe_load(f)
if raw_config is None:
raw_config = {} # Allow empty config file, defaults from Pydantic will be used
self.config = AppConfig(**raw_config)
print(f"Configuration loaded successfully from {self.config_path}")
except FileNotFoundError:
print(f"Warning: Configuration file {self.config_path} not found. Using default settings.")
self.config = AppConfig() # Load with default values
except yaml.YAMLError as e:
print(f"Error parsing YAML configuration file {self.config_path}: {e}")
print("Falling back to default configuration.")
self.config = AppConfig()
except ValidationError as e:
print(f"Configuration validation error from {self.config_path}:\n{e}")
print("Falling back to default configuration due to validation errors.")
self.config = AppConfig()
except Exception as e:
print(f"An unexpected error occurred while loading configuration from {self.config_path}: {e}")
print("Falling back to default configuration.")
self.config = AppConfig()
def get_config(self) -> AppConfig:
"""Returns the loaded (and validated) application configuration."""
if self.config is None:
print("Error: Config not loaded. Attempting to load defaults.")
self.config = AppConfig()
return self.config
def get_module_config(self, module_name: str) -> Optional[Any]:
"""Returns configuration for a specific module."""
if self.config:
# Ensure the module_name is a valid attribute of AppConfig
if hasattr(self.config, module_name):
return getattr(self.config, module_name)
else:
print(f"Warning: Configuration for module '{module_name}' not found in AppConfig.")
return None
return None
# Example Usage (can be moved to tests or main application logic)
if __name__ == '__main__':
# Create a dummy configs directory if it doesn't exist for the example
import os
if not os.path.exists("configs"):
os.makedirs("configs")
dummy_config_file = "configs/dummy_config.yaml"
dummy_config_content = {
'app_name': 'My DDMS Checker',
'logging': {'level': 'DEBUG'},
'api_caller': {
'default_timeout': 60,
'default_headers': {'X-Custom-Header': 'TestValue'}
},
'rule_repository': {
'storage': {
'type': 'filesystem',
'path': './custom_rules'
},
'preload_rules': True
},
'json_schema_validator': { # Added for completeness of the example
'default_draft_version': 'draft7'
}
}
with open(dummy_config_file, 'w') as f_yaml:
yaml.dump(dummy_config_content, f_yaml)
manager = ConfigurationManager(config_path=dummy_config_file)
app_cfg = manager.get_config()
print(f"App Name: {app_cfg.app_name}")
print(f"Log Level: {app_cfg.logging.level}")
if app_cfg.api_caller:
print(f"API Caller Timeout: {app_cfg.api_caller.default_timeout}")
if app_cfg.rule_repository and app_cfg.rule_repository.storage:
print(f"Rule Storage Type: {app_cfg.rule_repository.storage.type}")
print(f"Rule Storage Path: {app_cfg.rule_repository.storage.path}")
if app_cfg.json_schema_validator:
print(f"JSON Schema Validator Draft: {app_cfg.json_schema_validator.default_draft_version}")
print("\nLoading with non-existent file (expect defaults):")
manager_default = ConfigurationManager(config_path="configs/non_existent_config.yaml")
app_cfg_default = manager_default.get_config()
print(f"App Name (Default): {app_cfg_default.app_name}")
if app_cfg_default.api_caller:
print(f"API Caller Timeout (Default): {app_cfg_default.api_caller.default_timeout}")
# Clean up the dummy file and directory if it was created
if os.path.exists(dummy_config_file):
os.remove(dummy_config_file)
if os.path.exists("configs") and not os.listdir("configs"):
os.rmdir("configs")
@@ -0,0 +1 @@
@@ -0,0 +1,355 @@
"""Input Parser Module"""
import json
import os
from typing import Any, Dict, Optional, List, Union
from pydantic import BaseModel # For defining the structure of parsed inputs
from dataclasses import dataclass, field
import logging
logger = logging.getLogger("InputParser")
class ParsedOpenAPISpec(BaseModel):
# Placeholder for OpenAPI spec details relevant to the compliance suite
spec: Dict[str, Any]
info: Dict[str, Any] # Swagger 'info' object with title, version, etc.
paths: Dict[str, Dict[str, Any]] # API paths and their operations
tags: Optional[List[Dict[str, str]]] = None # API tags
basePath: Optional[str] = None # Base path for all APIs
swagger_version: str # Swagger specification version
@dataclass
class YAPIEndpoint:
"""YAPI API端点信息"""
path: str
method: str
title: str = ""
description: str = ""
category_name: str = ""
req_params: List[Dict[str, Any]] = field(default_factory=list)
req_query: List[Dict[str, Any]] = field(default_factory=list)
req_headers: List[Dict[str, Any]] = field(default_factory=list)
req_body_type: str = ""
req_body_other: str = ""
res_body_type: str = ""
res_body: str = ""
@dataclass
class ParsedYAPISpec:
"""解析后的YAPI规范"""
endpoints: List[YAPIEndpoint]
categories: List[Dict[str, Any]]
total_count: int
@dataclass
class SwaggerEndpoint:
"""Swagger API端点信息"""
path: str
method: str
summary: str = ""
description: str = ""
operation_id: str = ""
tags: List[str] = field(default_factory=list)
parameters: List[Dict[str, Any]] = field(default_factory=list)
responses: Dict[str, Any] = field(default_factory=dict)
consumes: List[str] = field(default_factory=list)
produces: List[str] = field(default_factory=list)
request_body: Dict[str, Any] = field(default_factory=dict)
@dataclass
class ParsedSwaggerSpec:
"""解析后的Swagger规范"""
endpoints: List[SwaggerEndpoint]
info: Dict[str, Any]
swagger_version: str
host: str = ""
base_path: str = ""
schemes: List[str] = field(default_factory=list)
tags: List[Dict[str, Any]] = field(default_factory=list)
categories: List[Dict[str, Any]] = field(default_factory=list)
class ParsedBusinessLogic(BaseModel):
# Placeholder for parsed business logic flow
name: str
steps: list # List of steps, each could be another Pydantic model
class InputParser:
"""
Responsible for parsing DDMS supplier's input materials like API specs, etc.
"""
def __init__(self):
pass
def parse_openapi_spec(self, spec_path: str) -> Optional[ParsedOpenAPISpec]:
"""
Parses an OpenAPI specification from a file path.
Args:
spec_path: The file path of the OpenAPI specification.
Returns:
A ParsedOpenAPISpec object containing the parsed specification,
or None if parsing fails.
"""
try:
# Check if file exists
if not os.path.exists(spec_path):
print(f"Error: File not found: {spec_path}")
return None
# Read and parse JSON file
with open(spec_path, 'r', encoding='utf-8') as f:
swagger_data = json.load(f)
# Extract basic information
swagger_version = swagger_data.get('swagger', swagger_data.get('openapi', 'Unknown'))
info = swagger_data.get('info', {})
paths = swagger_data.get('paths', {})
tags = swagger_data.get('tags', [])
base_path = swagger_data.get('basePath', '')
# Create and return ParsedOpenAPISpec
return ParsedOpenAPISpec(
spec=swagger_data,
info=info,
paths=paths,
tags=tags,
basePath=base_path,
swagger_version=swagger_version
)
except FileNotFoundError:
print(f"File not found: {spec_path}")
return None
except json.JSONDecodeError as e:
print(f"Error parsing JSON from {spec_path}: {e}")
return None
except Exception as e:
print(f"Error parsing OpenAPI spec from {spec_path}: {e}")
return None
def parse_yapi_spec(self, file_path: str) -> Optional[ParsedYAPISpec]:
"""
解析YAPI规范文件
Args:
file_path: YAPI JSON文件路径
Returns:
Optional[ParsedYAPISpec]: 解析后的YAPI规范,如果解析失败则返回None
"""
if not os.path.isfile(file_path):
logger.error(f"文件不存在: {file_path}")
return None
try:
with open(file_path, 'r', encoding='utf-8') as f:
yapi_data = json.load(f)
if not isinstance(yapi_data, list):
logger.error(f"无效的YAPI文件格式: 顶层元素应该是数组")
return None
endpoints = []
categories = []
# 处理分类
for category_data in yapi_data:
if not isinstance(category_data, dict):
logger.warning(f"YAPI 分类条目格式不正确,应为字典类型,已跳过: {category_data}")
continue
category_name = category_data.get('name', '')
category_desc = category_data.get('desc', '')
# 添加到分类列表
categories.append({
'name': category_name,
'desc': category_desc
})
# 处理API接口
api_list = category_data.get('list', [])
if not isinstance(api_list, list):
logger.warning(f"分类 '{category_name}' 中的 API列表 (list) 格式不正确,应为数组类型,已跳过。")
continue
for api_item in api_list:
if not isinstance(api_item, dict):
logger.warning(f"分类 '{category_name}' 中的 API条目格式不正确,应为字典类型,已跳过: {api_item}")
continue
# 提取API信息
path = api_item.get('path', '')
if not path:
logger.info(f"分类 '{category_name}' 中的 API条目缺少 'path',使用空字符串。 API: {api_item.get('title', '未命名')}")
method = api_item.get('method', 'GET')
if api_item.get('method') is None: # 仅当原始数据中完全没有 method 字段时记录
logger.info(f"分类 '{category_name}' 中的 API条目 '{path}' 缺少 'method',使用默认值 'GET'")
title = api_item.get('title', '')
if not title:
logger.info(f"分类 '{category_name}' 中的 API条目 '{path}' ({method}) 缺少 'title',使用空字符串。")
description = api_item.get('desc', '')
# 提取请求参数
req_params = api_item.get('req_params', [])
req_query = api_item.get('req_query', [])
req_headers = api_item.get('req_headers', [])
# 提取请求体信息
req_body_type = api_item.get('req_body_type', '')
req_body_other = api_item.get('req_body_other', '')
# 提取响应体信息
res_body_type = api_item.get('res_body_type', '')
res_body = api_item.get('res_body', '')
# 创建端点对象
endpoint = YAPIEndpoint(
path=path,
method=method,
title=title,
description=description,
category_name=category_name,
req_params=req_params,
req_query=req_query,
req_headers=req_headers,
req_body_type=req_body_type,
req_body_other=req_body_other,
res_body_type=res_body_type,
res_body=res_body
)
endpoints.append(endpoint)
return ParsedYAPISpec(
endpoints=endpoints,
categories=categories,
total_count=len(endpoints)
)
except Exception as e:
logger.error(f"解析YAPI文件时出错: {str(e)}")
return None
def parse_swagger_spec(self, file_path: str) -> Optional[ParsedSwaggerSpec]:
"""
解析Swagger规范文件
Args:
file_path: Swagger JSON文件路径
Returns:
Optional[ParsedSwaggerSpec]: 解析后的Swagger规范,如果解析失败则返回None
"""
if not os.path.isfile(file_path):
logger.error(f"文件不存在: {file_path}")
return None
try:
with open(file_path, 'r', encoding='utf-8') as f:
swagger_data = json.load(f)
if not isinstance(swagger_data, dict):
logger.error(f"无效的Swagger文件格式: 顶层元素应该是对象")
return None
# 提取基本信息
swagger_version = swagger_data.get('swagger', swagger_data.get('openapi', ''))
info = swagger_data.get('info', {})
host = swagger_data.get('host', '')
base_path = swagger_data.get('basePath', '')
schemes = swagger_data.get('schemes', [])
tags = swagger_data.get('tags', [])
# 创建分类列表
categories = []
for tag in tags:
categories.append({
'name': tag.get('name', ''),
'desc': tag.get('description', '')
})
# 处理API路径
paths = swagger_data.get('paths', {})
endpoints = []
for path, path_item in paths.items():
if not isinstance(path_item, dict):
continue
# 处理每个HTTP方法 (GET, POST, PUT, DELETE等)
for method, operation in path_item.items():
if method in ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace']:
if not isinstance(operation, dict):
continue
# 提取操作信息
summary = operation.get('summary', '')
description = operation.get('description', '')
operation_id = operation.get('operationId', '')
operation_tags = operation.get('tags', [])
# 提取参数信息
parameters = operation.get('parameters', [])
# 提取响应信息
responses = operation.get('responses', {})
# 提取请求和响应的内容类型
consumes = operation.get('consumes', swagger_data.get('consumes', []))
produces = operation.get('produces', swagger_data.get('produces', []))
# 提取请求体信息 (OpenAPI 3.0 格式)
request_body = operation.get('requestBody', {})
# 创建端点对象
endpoint = SwaggerEndpoint(
path=path,
method=method.upper(),
summary=summary,
description=description,
operation_id=operation_id,
tags=operation_tags,
parameters=parameters,
responses=responses,
consumes=consumes,
produces=produces,
request_body=request_body
)
endpoints.append(endpoint)
# 创建返回对象
return ParsedSwaggerSpec(
endpoints=endpoints,
info=info,
swagger_version=swagger_version,
host=host,
base_path=base_path,
schemes=schemes,
tags=tags,
categories=categories
)
except Exception as e:
logger.error(f"解析Swagger文件时出错: {str(e)}")
return None
def parse_business_logic_flow(self, flow_description: str) -> Optional[ParsedBusinessLogic]:
"""
Parses a business logic flow description.
The format of this description is TBD and this parser would need to be built accordingly.
Args:
flow_description: The string content describing the business logic flow.
Returns:
A ParsedBusinessLogic object or None if parsing fails.
"""
print(f"[InputParser] Placeholder: Parsing business logic flow. Content: {flow_description[:100]}...")
# Placeholder: Actual parsing logic will depend on the defined format.
return ParsedBusinessLogic(name="Example Flow", steps=["Step 1 API call", "Step 2 Validate Response"])
# Add other parsers as needed (e.g., for data object definitions)
@@ -0,0 +1,3 @@
"""
JSON Schema Validator module for DDMS Compliance Suite.
"""
@@ -0,0 +1,146 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
JSON Schema validator implementation for DDMS Compliance Suite.
Provides validators to check if data objects conform to defined JSON Schemas.
"""
import json
import logging
from typing import Dict, List, Any, Optional
import jsonschema
from jsonschema import ValidationError
from ddms_compliance_suite.models.rule_models import JSONSchemaDefinition
logger = logging.getLogger(__name__)
class ValidationResult:
"""Validation result container"""
def __init__(self, is_valid: bool, errors: List[str] = None, warnings: List[str] = None):
"""
Initialize a validation result
Args:
is_valid: Whether the data is valid according to the schema
errors: List of error messages (if any)
warnings: List of warning messages (if any)
"""
self.is_valid = is_valid
self.errors = errors or []
self.warnings = warnings or []
def __str__(self) -> str:
"""String representation of validation result"""
status = "Valid" if self.is_valid else "Invalid"
result = f"Validation Result: {status}\n"
if self.errors:
result += f"Errors ({len(self.errors)}):\n"
for i, error in enumerate(self.errors, 1):
result += f" {i}. {error}\n"
if self.warnings:
result += f"Warnings ({len(self.warnings)}):\n"
for i, warning in enumerate(self.warnings, 1):
result += f" {i}. {warning}\n"
return result
class JSONSchemaValidator:
"""JSON Schema validator implementation"""
def __init__(self):
"""Initialize the JSON Schema validator"""
self.validator_cache = {}
def validate(self, data: Dict[str, Any], schema: Dict[str, Any]) -> ValidationResult:
"""
Validate data against a JSON schema
Args:
data: The data to validate
schema: The JSON schema to validate against
Returns:
ValidationResult: Result of the validation
"""
if not schema:
logger.error("Schema is empty or None")
return ValidationResult(False, ["Schema is not provided"])
if not data:
logger.error("Data is empty or None")
return ValidationResult(False, ["Data is not provided"])
try:
# Get or create validator
schema_str = json.dumps(schema, sort_keys=True)
if schema_str not in self.validator_cache:
self.validator_cache[schema_str] = jsonschema.Draft7Validator(schema)
validator = self.validator_cache[schema_str]
# Collect all validation errors
errors = list(validator.iter_errors(data))
if not errors:
return ValidationResult(True)
# Format error messages
error_messages = []
for error in errors:
path = ".".join(str(path_item) for path_item in error.path) if error.path else "root"
# Include the error type in the message to make it easier to identify
error_type = "unknown"
# Determine error type based on the validation error
if error.validator == 'required':
error_type = "required"
elif error.validator == 'pattern':
error_type = "pattern"
elif error.validator == 'enum':
error_type = "enum"
elif error.validator == 'type':
error_type = "type"
elif error.validator == 'format':
error_type = "format"
elif error.validator == 'minimum' or error.validator == 'maximum':
error_type = error.validator
elif error.validator == 'additionalProperties':
error_type = "additionalProperties"
else:
error_type = error.validator
error_messages.append(f"Error at {path}: {error_type} - {error.message}")
return ValidationResult(False, error_messages)
except Exception as e:
logger.exception("Schema validation failed with error")
return ValidationResult(False, [f"Validation error: {str(e)}"])
def validate_with_rule(self, data: Dict[str, Any], rule: JSONSchemaDefinition) -> ValidationResult:
"""
Validate data against a schema rule
Args:
data: The data to validate
rule: The JSON schema rule to validate against
Returns:
ValidationResult: Result of the validation
"""
if not rule:
logger.error("Rule is empty or None")
return ValidationResult(False, ["Rule is not provided"])
if not rule.schema_content:
logger.error(f"Rule {rule.id} does not have schema content")
return ValidationResult(False, [f"Rule {rule.id} does not have schema content"])
return self.validate(data, rule.schema_content)
+18
View File
@@ -0,0 +1,18 @@
"""
DMS Compliance Suite
This module is the main entry point or CLI for the application.
"""
def main():
print("Initializing DMS Compliance Suite...")
# TODO: Initialize ConfigurationManager
# TODO: Initialize LoggingService
# TODO: Parse arguments (if any)
# TODO: Initialize RuleRepository
# TODO: Initialize TestExecutor
# TODO: Start the validation process
print("DDMS Compliance Suite finished.")
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1,52 @@
"""Pydantic models for application configuration."""
from pydantic import BaseModel
from typing import Optional, Dict, Literal
# These models were previously in config/manager.py, moved here for better organization.
class APICallerConfig(BaseModel):
default_timeout: int = 30
default_headers: Optional[Dict[str, str]] = None
# Add other API caller specific configs: e.g., retry_attempts, backoff_factor
class JSONSchemaValidatorConfig(BaseModel):
default_draft_version: Optional[str] = None # e.g., "draft7", "draft2020-12"
# Add other schema validator specific configs
class RuleStorageConfig(BaseModel):
type: Literal["filesystem", "database", "in_memory"] = "filesystem"
path: Optional[str] = "./rules" # For filesystem adapter: path to rules directory
connection_string: Optional[str] = None # For database adapter
# Add other storage specific configs, e.g., for filesystem: file_pattern = "*.json"
class RuleRepositoryConfig(BaseModel):
storage: RuleStorageConfig = RuleStorageConfig()
default_version_strategy: Literal["latest_enabled", "latest_stable", "exact"] = "latest_enabled"
preload_rules: bool = False
# Add other rule repository specific configs
class LoggingConfig(BaseModel):
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# file_path: Optional[str] = None # Optional: if logging to a file
class TestExecutorConfig(BaseModel):
# Add configurations specific to the Test Executor
# For example, default behavior for failing a test step, etc.
stop_on_first_failure: bool = False
class ReportGeneratorConfig(BaseModel):
# Add configurations specific to the Report Generator
output_format: Literal["json", "html", "xml"] = "json"
output_path: str = "./reports"
class AppConfig(BaseModel):
app_name: str = "DDMS Compliance Suite"
logging: LoggingConfig = LoggingConfig()
api_caller: APICallerConfig = APICallerConfig()
json_schema_validator: JSONSchemaValidatorConfig = JSONSchemaValidatorConfig()
rule_repository: RuleRepositoryConfig = RuleRepositoryConfig()
test_executor: TestExecutorConfig = TestExecutorConfig()
report_generator: ReportGeneratorConfig = ReportGeneratorConfig()
# input_parser: ... # Config for input parser if needed
# assertion_engine: ... # Config for assertion engine if needed
+190
View File
@@ -0,0 +1,190 @@
"""Base Pydantic models for the application."""
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any, Union, Literal
from enum import Enum
class SeverityLevel(str, Enum):
ERROR = "error"
WARNING = "warning"
INFO = "info"
class RuleCategory(str, Enum):
JSON_SCHEMA = "JSONSchema"
API_LINTING = "APILinting"
BUSINESS_LOGIC = "BusinessLogic"
DATA_QUALITY = "DataQuality"
SECURITY = "Security"
PERFORMANCE = "Performance"
MAPPING = "Mapping"
PYTHON_CODE = "PythonCode" # Python代码规则类别
API_DESIGN = "APIDesign" # API设计规则类别
ERROR_HANDLING = "ErrorHandling" # 错误处理规则类别
COMPATIBILITY = "Compatibility" # 兼容性规则类别
GENERIC = "Generic"
class TargetType(str, Enum):
API_REQUEST = "APIRequest"
API_RESPONSE = "APIResponse"
DATA_OBJECT = "DataObject"
OPENAPI_SPECIFICATION = "OpenAPISpecification"
GENERIC_TARGET = "GenericTarget"
class RuleLifecycle(str, Enum):
"""规则生命周期枚举,定义规则在测试流程中的适用阶段"""
REQUEST_PREPARATION = "请求准备阶段"
RESPONSE_VALIDATION = "响应验证阶段"
POST_VALIDATION = "后处理阶段"
API_SPECIFICATION_VALIDATION = "API规范验证阶段" # 新增的生命周期
ANY_STAGE = "任意阶段"
class RuleScope(str, Enum):
"""规则作用域枚举,定义规则针对的具体对象"""
REQUEST_URL = "请求URL"
REQUEST_HEADERS = "请求头"
REQUEST_PARAMS = "请求参数"
REQUEST_BODY = "请求体"
RESPONSE_STATUS = "响应状态码"
RESPONSE_HEADERS = "响应头"
RESPONSE_BODY = "响应体"
RESPONSE_TIME = "响应时间"
SECURITY = "安全性"
PERFORMANCE = "性能"
ANY_SCOPE = "任意作用域"
class BaseRule(BaseModel):
"""通用规则属性 (Pydantic基类 BaseRule)"""
id: str # 规则唯一ID
name: str # 规则名称
description: Optional[str] = None # 规则详细描述
category: RuleCategory # 规则类别
version: str = "1.0.0" # 版本号
severity: SeverityLevel = SeverityLevel.INFO # 严重性
source: Optional[str] = None # 规则来源 (例如 "PlatformStandard-XYZ", "OWASP-ASVS")
is_enabled: bool = True # 是否启用
tags: Optional[List[str]] = None # 规则标签 (如 "critical", "wellbore")
target_type: Optional[TargetType] = None # 规则适用目标类型 (描述性元数据)
target_identifier: Optional[str] = None # 规则适用目标的具体标识 (描述性元数据)
author: Optional[str] = None # 规则作者
# 新增字段
lifecycle: Optional[RuleLifecycle] = Field(
default=RuleLifecycle.ANY_STAGE,
description="规则的推荐适用阶段或类型。主要用于识别 API_SPECIFICATION_VALIDATION (静态分析) 规则。"
"对于其他动态规则,此字段为元数据提示,实际执行阶段由 TestStep 定义。"
)
scope: Optional[RuleScope] = Field(
default=RuleScope.ANY_SCOPE,
description="描述规则逻辑主要关注的API交互部分 (例如,请求头、响应体)。"
"此字段为描述性元数据,用于规则分类和理解,不作为执行时的强制约束。"
)
code: Optional[str] = None # 规则验证代码
class JSONSchemaDefinition(BaseRule):
"""JSON Schema 定义 (用于API请求/响应体结构验证)"""
category: Literal[RuleCategory.JSON_SCHEMA] = RuleCategory.JSON_SCHEMA
schema_content: Dict[str, Any] # 实际的JSON Schema
class APILintingRuleset(BaseRule):
"""API 设计规范/Linting规则 (例如,基于Spectral的OpenAPI规范校验规则集)"""
category: Literal[RuleCategory.API_LINTING] = RuleCategory.API_LINTING
ruleset_format: str = "spectral" # 例如 "spectral", "custom_json_rules"
ruleset_content: Union[str, Dict[str, Any]] # 规则集内容或其引用(例如,文件路径或URL,或直接嵌入的规则字典)
class BusinessAssertionTemplate(BaseRule):
"""业务逻辑断言模板或具体规则"""
category: Literal[RuleCategory.BUSINESS_LOGIC] = RuleCategory.BUSINESS_LOGIC
template_language: str = "python_expression" # 如 "python_expression", "jsonpath_assert"
template_expression: str
expected_parameters: Optional[List[str]] = None # 执行断言模板时需要输入的参数列表
class DataQualityRule(BaseRule):
"""数据质量校验规则"""
category: Literal[RuleCategory.DATA_QUALITY] = RuleCategory.DATA_QUALITY
# Specific fields for data quality rules, e.g.:
field_name: Optional[str] = None
validation_type: str # e.g., "regex", "range", "custom_function"
validation_expression: str # regex pattern, range boundaries, function name/path
class PythonCodeRule(BaseRule):
"""使用 Python 代码定义的规则
此类规则允许以 Python 代码片段形式定义复杂的验证逻辑,提供了最大的灵活性。
代码将在受控环境中执行,以确保安全性。
"""
category: Literal[RuleCategory.PYTHON_CODE] = RuleCategory.PYTHON_CODE
# Python 代码内容或文件路径
code_file: Optional[str] = None
# 脚本入口函数名,默认为 validate
entry_function: str = "validate"
# 预期的入口函数参数
expected_parameters: Optional[List[str]] = None
# 是否允许导入外部模块 (出于安全考虑,默认为 False)
allow_imports: bool = False
# 允许导入的模块列表 (如果 allow_imports 为 True)
allowed_modules: Optional[List[str]] = None
# 超时设置(秒),防止无限循环等恶意代码
timeout: int = 5
# 代码依赖的其他规则ID
depends_on: Optional[List[str]] = None
# 新增规则类型
class PerformanceRule(BaseRule):
"""性能相关规则,如响应时间、吞吐量等"""
category: Literal[RuleCategory.PERFORMANCE] = RuleCategory.PERFORMANCE
lifecycle: Literal[RuleLifecycle.RESPONSE_VALIDATION] = RuleLifecycle.RESPONSE_VALIDATION
scope: Literal[RuleScope.RESPONSE_TIME] = RuleScope.RESPONSE_TIME
threshold: Union[float, int] # 性能阈值
metric: str # 性能指标名称
unit: str = "ms" # 默认单位:毫秒
class SecurityRule(BaseRule):
"""安全相关规则,如身份验证、授权、加密等"""
category: Literal[RuleCategory.SECURITY] = RuleCategory.SECURITY
check_type: str # 安全检查类型(如认证、授权、加密等)
expected_value: Optional[str] = None # 预期值
class RESTfulDesignRule(BaseRule):
"""RESTful API设计规则,如HTTP方法使用、URL设计等"""
category: Literal[RuleCategory.API_DESIGN] = RuleCategory.API_DESIGN
design_aspect: str # 设计方面(如URL、HTTP方法、参数等)
pattern: Optional[str] = None # 匹配模式(如正则表达式)
class ErrorHandlingRule(BaseRule):
"""错误处理规则,如错误码、错误消息等"""
category: Literal[RuleCategory.ERROR_HANDLING] = RuleCategory.ERROR_HANDLING
error_code: str # 错误码
expected_status: int # 预期HTTP状态码
expected_message: Optional[str] = None # 预期错误消息
# 更新联合类型以包含新规则类型
AnyRule = Union[
JSONSchemaDefinition,
APILintingRuleset,
BusinessAssertionTemplate,
DataQualityRule,
PythonCodeRule,
PerformanceRule,
SecurityRule,
RESTfulDesignRule,
ErrorHandlingRule,
BaseRule # 通用或未明确分类的规则
]
class RuleQuery(BaseModel):
"""规则查询条件对象 (Pydantic模型)"""
rule_id: Optional[str] = None
category: Optional[RuleCategory] = None
target_type: Optional[TargetType] = None
target_identifier: Optional[str] = None
version: Optional[str] = "latest" # 或特殊标识 ("latest", "stable", or specific version)
tags: Optional[List[str]] = None
is_enabled: Optional[bool] = True # 默认查询已启用的规则
# 新增字段
lifecycle: Optional[RuleLifecycle] = None
scope: Optional[RuleScope] = None
+144
View File
@@ -0,0 +1,144 @@
"""
Pydantic models for Test Cases, Test Steps, and Test Suites.
"""
from typing import Optional, List, Dict, Any, Union, Literal
from pydantic import BaseModel, Field, HttpUrl
from enum import Enum
from .rule_models import SeverityLevel # Assuming rule_models.py is in the same directory
from ..api_caller.caller import APIRequest # Adjust path as necessary
class TestDataGenerationStrategy(str, Enum):
"""Defines how test data for an API request should be generated."""
STATIC = "static" # Data is explicitly provided
FROM_SCHEMA = "from_schema" # Data is generated based on OpenAPI/JSON schema
FROM_PREVIOUS_STEP = "from_previous_step" # Data is extracted from a previous test step's output
AI_GENERATED = "ai_generated" # Data is generated by an AI model
class TestDataGenerationConfig(BaseModel):
"""Configuration for generating test data for an API request."""
strategy: TestDataGenerationStrategy = TestDataGenerationStrategy.STATIC
# For STATIC strategy:
static_data: Optional[Dict[str, Any]] = Field(default=None, description="Explicit data for the request (e.g., body, params).")
# For FROM_SCHEMA strategy:
schema_source: Optional[str] = Field(default=None, description="Identifier for the schema to be used (e.g., operationId, or path to schema file).")
# For FROM_PREVIOUS_STEP strategy:
source_step_id: Optional[str] = Field(default=None, description="The ID of the previous TestStep whose output will be used.")
extraction_rules: Optional[Dict[str, str]] = Field(default_None, description="Rules to extract data (e.g., JSONPath expressions like {'user_id': '$.body.id'}).")
# For AI_GENERATED strategy:
ai_prompt: Optional[str] = Field(default=None, description="Prompt to be used by an AI model to generate data.")
ai_model_config: Optional[Dict[str, Any]] = Field(default_None, description="Configuration for the AI model (e.g., temperature, max_tokens).")
class APIRequestContext(BaseModel):
"""
Defines how to construct the APIRequest for a TestStep, potentially using
data from previous steps or other dynamic sources.
"""
method: str = Field(description="HTTP method (GET, POST, PUT, DELETE, etc.).")
url_template: str = Field(description="URL template, can contain placeholders like {base_url} or {resource_id}.")
path_params_source: Optional[Dict[str, TestDataGenerationConfig]] = Field(default_None, description="How to generate/get path parameters.")
query_params_source: Optional[Dict[str, TestDataGenerationConfig]] = Field(default_None, description="How to generate/get query parameters.")
headers_source: Optional[Dict[str, TestDataGenerationConfig]] = Field(default_None, description="How to generate/get request headers. Default headers can be added by the orchestrator.")
body_source: Optional[TestDataGenerationConfig] = Field(default_None, description="How to generate/get the request body.")
# Stores the actual APIRequest object after context resolution
# This field will be populated by the TestOrchestrator before execution.
resolved_request: Optional[APIRequest] = Field(default=None, exclude=True)
class TestStep(BaseModel):
"""
Represents a single step within a TestCase, typically involving one API call
and a set of rules to be validated against its request/response.
"""
step_id: str = Field(description="Unique identifier for this test step within the TestCase.")
description: Optional[str] = Field(default=None, description="Description of what this test step does.")
api_request_context: APIRequestContext = Field(description="Context to build the API request for this step.")
# Rules to be applied at different phases of this step
# Applied after the request is prepared but before it's sent.
# Useful for validating the constructed request itself.
request_preparation_rule_ids: List[str] = Field(default_factory=list, description="Rules to apply to the APIRequest before sending.")
# Applied after the API response is received. This is the most common place for validation rules.
response_validation_rule_ids: List[str] = Field(default_factory=list, description="Rules to apply to the APIResponse.")
# Applied after response validation, يمكن استخدامه لتنظيف البيانات أو إجراءات ما بعد الاختبار
post_validation_rule_ids: List[str] = Field(default_factory=list, description="Rules to apply after response validation (e.g., cleanup).")
# Configuration for expected outcomes, more specific than just rule pass/fail
expected_status_code: Optional[int] = Field(default=None, description="Expected HTTP status code.")
# Allows defining assertions on the response body using something like JSONPath
# e.g., {"$.data.status": "completed", "$.errors": null}
response_body_assertions: Optional[Dict[str, Any]] = Field(default_None, description="Assertions to make on the response body (e.g., using JSONPath).")
# Execution control
skip: bool = Field(default=False, description="If True, this test step will be skipped.")
# Allows storing output from this step to be used by subsequent steps
# e.g., {"created_user_id": "$.response.body.id"}
outputs_to_extract: Optional[Dict[str, str]] = Field(default_None, description="JSONPath expressions to extract data from response to be used in later steps.")
class TestCaseExecutionMode(str, Enum):
SEQUENTIAL = "sequential" # Steps are executed one after another
PARALLEL = "parallel" # Steps (if independent) can be executed in parallel
class TestCase(BaseModel):
"""
Defines a test case, which consists of one or more test steps,
metadata, and configurations for execution.
"""
id: str = Field(description="Unique identifier for the test case.")
name: str = Field(description="Name of the test case.")
description: Optional[str] = Field(default=None, description="Detailed description of the test case.")
tags: List[str] = Field(default_factory=list, description="Tags for categorizing and filtering test cases.")
severity: SeverityLevel = Field(default=SeverityLevel.INFO, description="Severity of the test case if it fails.")
author: Optional[str] = Field(default=None, description="Author of the test case.")
creation_date: Optional[str] = Field(default=None, description="Date when the test case was created (ISO format).") # Consider using datetime
version: str = Field(default="1.0.0", description="Version of the test case.")
# Test Steps
steps: List[TestStep] = Field(description="A list of test steps to be executed for this test case.")
execution_mode: TestCaseExecutionMode = Field(default=TestCaseExecutionMode.SEQUENTIAL, description="How the steps in this test case should be executed.")
# Data that can be shared across steps within this test case.
# Steps can read from and write to this context.
shared_context: Dict[str, Any] = Field(default_factory=dict, description="Data context televisão across steps.")
# Overall expected outcome for the test case (can be high-level)
expected_overall_result: Optional[str] = Field(default=None, description="A high-level description of the expected outcome for the entire test case.")
# For iterative/load testing
execution_count: int = Field(default=1, ge=1, description="Number of times this test case should be executed.")
# If > 1, defines delay between iterations in seconds.
delay_between_iterations_sec: float = Field(default=0, ge=0)
class TestSuite(BaseModel):
"""
A collection of TestCases, possibly with a shared configuration or setup/teardown logic.
"""
id: str = Field(description="Unique identifier for the test suite.")
name: str = Field(description="Name of the test suite.")
description: Optional[str] = Field(default=None, description="Description of the test suite.")
tags: List[str] = Field(default_factory=list, description="Tags for categorizing and filtering test suites.")
# List of TestCase IDs or full TestCase objects. Using IDs allows for referencing.
# For simplicity in this initial design, let's assume full TestCase objects.
# Later, we could support referencing TestCase definitions stored elsewhere.
test_cases: List[TestCase] = Field(default_factory=list, description="List of test cases included in this suite.")
# Global parameters or configurations that can apply to all TestCases in the suite
# These could override individual TestCase settings or provide defaults.
global_parameters: Optional[Dict[str, Any]] = Field(default_None, description="Parameters applicable to all test cases in the suite.")
# Setup: API calls or actions to perform before running any TestCase in the suite.
# Similar structure to TestStep but without specific rule validations, more for setup.
setup_steps: Optional[List[TestStep]] = Field(default_None, description="Steps to execute before running test cases in the suite.")
# Teardown: API calls or actions to perform after all TestCases in the suite have run.
teardown_steps: Optional[List[TestStep]] = Field(default_None, description="Steps to execute after running test cases in the suite.")
# Execution control for the suite
# e.g., run all, run tagged, run failed from previous
execution_strategy: Optional[str] = Field(default=None, description="Strategy for executing test cases within the suite.")
@@ -0,0 +1,531 @@
"""规则执行引擎
该模块负责执行不同类型的规则,包括Python代码规则、断言规则等,
支持在API测试的不同生命周期阶段(请求准备、执行、响应验证等)执行规则。
"""
import logging
import importlib
import inspect
import time
import threading
from typing import Dict, List, Any, Optional, Union, Callable
from ..models.rule_models import (
AnyRule, BaseRule, RuleCategory, TargetType, RuleLifecycle, RuleScope,
PythonCodeRule, BusinessAssertionTemplate, PerformanceRule, SecurityRule,
RESTfulDesignRule, ErrorHandlingRule
)
from ..api_caller.caller import APIRequest, APIResponse
from ..rule_repository.repository import RuleRepository
class RuleExecutionError(Exception):
"""规则执行过程中发生的错误"""
pass
class RuleExecutionResult:
"""规则执行结果"""
def __init__(self,
rule: BaseRule,
is_valid: bool,
message: str = "",
details: Optional[Dict[str, Any]] = None,
error: Optional[Exception] = None):
"""
初始化规则执行结果
Args:
rule: 执行的规则
is_valid: 验证是否通过
message: 执行结果消息
details: 详细信息
error: 执行过程中发生的异常(如果有)
"""
self.rule = rule
self.rule_id = rule.id
self.rule_name = rule.name
self.rule_category = rule.category
self.is_valid = is_valid
self.message = message
self.details = details or {}
self.error = error
def __bool__(self):
"""允许直接使用结果对象作为布尔值,表示验证是否通过"""
return self.is_valid
def to_dict(self) -> Dict[str, Any]:
"""将结果转换为字典"""
return {
'rule_id': self.rule_id,
'rule_name': self.rule_name,
'rule_category': self.rule_category.value,
'is_valid': self.is_valid,
'message': self.message,
'details': self.details,
'error': str(self.error) if self.error else None
}
class RuleExecutor:
"""规则执行引擎"""
def __init__(self, rule_repository: RuleRepository):
"""
初始化规则执行引擎
Args:
rule_repository: 规则库实例
"""
self.rule_repository = rule_repository
self.logger = logging.getLogger(__name__)
def execute_rule(self, rule: BaseRule, context: Dict[str, Any]) -> RuleExecutionResult:
"""
执行单个规则
Args:
rule: 要执行的规则
context: 执行上下文,包含API请求、响应等信息
Returns:
执行结果
"""
if not rule.is_enabled:
return RuleExecutionResult(
rule=rule,
is_valid=True,
message=f"规则 {rule.id} 已禁用,跳过执行"
)
try:
# 根据规则类型选择适当的执行方法
if rule.category == RuleCategory.PYTHON_CODE or hasattr(rule, 'code') and rule.code:
return self._execute_python_code_rule(rule, context)
elif rule.category == RuleCategory.BUSINESS_LOGIC:
return self._execute_business_assertion_rule(rule, context)
elif rule.category == RuleCategory.PERFORMANCE:
return self._execute_performance_rule(rule, context)
elif rule.category == RuleCategory.SECURITY:
return self._execute_security_rule(rule, context)
elif rule.category == RuleCategory.API_DESIGN:
return self._execute_api_design_rule(rule, context)
elif rule.category == RuleCategory.ERROR_HANDLING:
return self._execute_error_handling_rule(rule, context)
else:
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"不支持的规则类型: {rule.category}"
)
except Exception as e:
self.logger.error(f"执行规则 {rule.id} 失败: {e}", exc_info=True)
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"规则执行失败: {e}",
error=e
)
def _execute_python_code_rule(self, rule: BaseRule, context: Dict[str, Any]) -> RuleExecutionResult:
"""执行Python代码规则"""
# 获取规则中的Python代码
code = getattr(rule, 'code', None)
if not code:
return RuleExecutionResult(
rule=rule,
is_valid=False,
message="规则未提供Python代码"
)
# 准备执行环境
namespace = {'__builtins__': __builtins__}
namespace.update(context)
try:
# 编译并执行代码
compiled_code = compile(code, f"<rule:{rule.id}>", 'exec')
exec(compiled_code, namespace)
# 查找并执行验证函数
entry_function = 'validate'
if hasattr(rule, 'entry_function') and rule.entry_function:
entry_function = rule.entry_function
if entry_function not in namespace:
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"找不到入口函数 '{entry_function}'"
)
validate_func = namespace[entry_function]
if not callable(validate_func):
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"'{entry_function}' 不是可调用的函数"
)
# 调用验证函数
timeout = getattr(rule, 'timeout', 5) # 默认5秒超时
result = self._execute_with_timeout(validate_func, (context,), {}, timeout)
# 处理执行结果
if isinstance(result, dict):
return RuleExecutionResult(
rule=rule,
is_valid=bool(result.get('is_valid', False)),
message=result.get('message', ''),
details=result.get('details', {})
)
else:
return RuleExecutionResult(
rule=rule,
is_valid=bool(result),
message=str(result) if result is not True else "验证通过"
)
except Exception as e:
self.logger.error(f"执行Python代码规则 {rule.id} 失败: {e}", exc_info=True)
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"执行代码失败: {e}",
error=e
)
def _execute_with_timeout(self, func: Callable, args: tuple, kwargs: Dict[str, Any], timeout: int) -> Any:
"""使用超时执行函数"""
result = [None]
exception = [None]
def target():
try:
result[0] = func(*args, **kwargs)
except Exception as e:
exception[0] = e
thread = threading.Thread(target=target)
thread.daemon = True
thread.start()
thread.join(timeout)
if thread.is_alive():
raise RuleExecutionError(f"规则执行超时(超过{timeout}秒)")
if exception[0]:
raise exception[0]
return result[0]
def _execute_business_assertion_rule(self, rule: BusinessAssertionTemplate, context: Dict[str, Any]) -> RuleExecutionResult:
"""执行业务断言规则"""
# 验证是否提供了所有必需的参数
if rule.expected_parameters:
missing_params = [p for p in rule.expected_parameters if p not in context]
if missing_params:
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"缺少必需的参数: {', '.join(missing_params)}"
)
if rule.template_language == "python_expression":
try:
# 使用eval执行Python表达式
result = eval(rule.template_expression, {'__builtins__': __builtins__}, context)
return RuleExecutionResult(
rule=rule,
is_valid=bool(result),
message="断言通过" if result else "断言失败"
)
except Exception as e:
self.logger.error(f"执行Python表达式断言失败: {e}", exc_info=True)
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"表达式执行失败: {e}",
error=e
)
else:
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"不支持的模板语言: {rule.template_language}"
)
def _execute_performance_rule(self, rule: PerformanceRule, context: Dict[str, Any]) -> RuleExecutionResult:
"""执行性能规则"""
response = context.get('api_response')
if not response or not isinstance(response, APIResponse):
return RuleExecutionResult(
rule=rule,
is_valid=False,
message="缺少有效的API响应对象"
)
# 获取响应时间(毫秒)
elapsed_time = response.elapsed_time * 1000 # 转换为毫秒
# 检查是否超过阈值
if elapsed_time > rule.threshold:
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"响应时间({elapsed_time:.2f}ms)超过阈值({rule.threshold}{rule.unit}",
details={
'actual_time': elapsed_time,
'threshold': rule.threshold,
'unit': rule.unit
}
)
return RuleExecutionResult(
rule=rule,
is_valid=True,
message=f"响应时间({elapsed_time:.2f}ms)在阈值范围内",
details={
'actual_time': elapsed_time,
'threshold': rule.threshold,
'unit': rule.unit
}
)
def _execute_security_rule(self, rule: SecurityRule, context: Dict[str, Any]) -> RuleExecutionResult:
"""执行安全规则"""
if rule.check_type == "transport_security":
request = context.get('api_request')
if not request or not isinstance(request, APIRequest):
return RuleExecutionResult(
rule=rule,
is_valid=False,
message="缺少有效的API请求对象"
)
url = str(request.url)
# 检查URL是否使用HTTPS
if not url.startswith('https://'):
return RuleExecutionResult(
rule=rule,
is_valid=False,
message="API请求必须使用HTTPS协议",
details={
'current_url': url,
'expected_protocol': 'https'
}
)
return RuleExecutionResult(
rule=rule,
is_valid=True,
message="API请求使用了HTTPS协议",
details={
'url': url
}
)
else:
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"不支持的安全检查类型: {rule.check_type}"
)
def _execute_api_design_rule(self, rule: RESTfulDesignRule, context: Dict[str, Any]) -> RuleExecutionResult:
"""执行API设计规则"""
import re
request = context.get('api_request')
if not request or not isinstance(request, APIRequest):
return RuleExecutionResult(
rule=rule,
is_valid=False,
message="缺少有效的API请求对象"
)
url = str(request.url)
# 解析URL,获取路径部分
from urllib.parse import urlparse
parsed_url = urlparse(url)
path = parsed_url.path
# 使用正则表达式验证路径
if rule.pattern and not re.match(rule.pattern, path):
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"API路径不符合{rule.design_aspect}规范",
details={
'current_path': path,
'expected_pattern': rule.pattern
}
)
return RuleExecutionResult(
rule=rule,
is_valid=True,
message=f"API路径符合{rule.design_aspect}规范",
details={
'path': path
}
)
def _execute_error_handling_rule(self, rule: ErrorHandlingRule, context: Dict[str, Any]) -> RuleExecutionResult:
"""执行错误处理规则"""
response = context.get('api_response')
if not response or not isinstance(response, APIResponse):
return RuleExecutionResult(
rule=rule,
is_valid=False,
message="缺少有效的API响应对象"
)
# 只验证4xx和5xx状态码
if response.status_code < 400:
return RuleExecutionResult(
rule=rule,
is_valid=True,
message="非错误响应,跳过验证",
details={
'status_code': response.status_code
}
)
# 验证状态码是否匹配
if rule.expected_status != -1 and response.status_code != rule.expected_status:
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"响应状态码({response.status_code})与预期({rule.expected_status})不符",
details={
'actual_status': response.status_code,
'expected_status': rule.expected_status
}
)
# 验证JSON响应
if not response.json_content:
return RuleExecutionResult(
rule=rule,
is_valid=False,
message="错误响应不是有效的JSON格式",
details={
'status_code': response.status_code,
'content_type': response.headers.get('Content-Type', '未知')
}
)
# 检查错误码
if rule.error_code != "*" and str(response.json_content.get('code', '')) != rule.error_code:
return RuleExecutionResult(
rule=rule,
is_valid=False,
message=f"错误码({response.json_content.get('code')})与预期({rule.error_code})不符",
details={
'actual_code': response.json_content.get('code'),
'expected_code': rule.error_code
}
)
# 验证错误消息
if rule.expected_message and rule.expected_message not in str(response.json_content.get('message', '')):
return RuleExecutionResult(
rule=rule,
is_valid=False,
message="错误消息与预期不符",
details={
'actual_message': response.json_content.get('message'),
'expected_message': rule.expected_message
}
)
return RuleExecutionResult(
rule=rule,
is_valid=True,
message="错误响应符合预期",
details={
'status_code': response.status_code,
'error_code': response.json_content.get('code'),
'error_message': response.json_content.get('message')
}
)
def execute_rules_for_lifecycle(self, lifecycle: RuleLifecycle, context: Dict[str, Any]) -> List[RuleExecutionResult]:
"""
执行特定生命周期阶段的所有规则
Args:
lifecycle: 生命周期阶段
context: 执行上下文
Returns:
执行结果列表
"""
# 获取适用于该生命周期阶段的所有规则
rules = self.rule_repository.get_rules_by_lifecycle(lifecycle)
# 执行规则
results = []
for rule in rules:
result = self.execute_rule(rule, context)
results.append(result)
return results
def execute_rules_for_target(self, target_type: TargetType, target_id: str, context: Dict[str, Any]) -> List[RuleExecutionResult]:
"""
执行特定目标的所有规则
Args:
target_type: 目标类型
target_id: 目标ID
context: 执行上下文
Returns:
执行结果列表
"""
# 获取适用于该目标的所有规则
rules = self.rule_repository.get_rules_for_target(target_type, target_id)
# 执行规则
results = []
for rule in rules:
result = self.execute_rule(rule, context)
results.append(result)
return results
def execute_specific_rules(self, rules: List[AnyRule], context: Dict[str, Any], lifecycle_phase: Optional[RuleLifecycle] = None) -> List[RuleExecutionResult]:
"""
执行一个明确指定的规则列表。
Args:
rules: 要执行的规则对象的列表。
context: 执行上下文,包含API请求、响应等信息。
lifecycle_phase: 可选的,名义上的生命周期阶段,可能用于上下文或某些规则的内部逻辑。
注意:此参数目前主要用于信息传递,核心执行逻辑在 execute_rule 中
并不直接依赖它来选择执行路径。
Returns:
一个包含每个规则执行结果的列表。
"""
results = []
if not rules:
self.logger.info("execute_specific_rules_called_with_no_rules")
return results
# 如果需要,可以将 lifecycle_phase 添加到 context 中传递给每个规则
# updated_context = context.copy()
# if lifecycle_phase:
# updated_context['current_lifecycle_phase'] = lifecycle_phase
for rule in rules:
# 使用现有的 execute_rule 方法执行单个规则
# result = self.execute_rule(rule, updated_context if lifecycle_phase else context)
result = self.execute_rule(rule, context) # 简化:暂时不修改context传递
results.append(result)
return results
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1,91 @@
"""Base adapter interface for rule storage."""
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any, Union
from ...models.rule_models import AnyRule, RuleQuery, BaseRule
class BaseRuleStorageAdapter(ABC):
"""Base class for rule storage adapters."""
@abstractmethod
def initialize(self) -> None:
"""初始化适配器(例如,连接到数据库,验证文件路径等)。"""
pass
@abstractmethod
def load_rule_by_id(self, rule_id: str, version: Optional[str] = None) -> Optional[AnyRule]:
"""
按ID加载单个规则。
Args:
rule_id: 规则的唯一标识符。
version: 可选的版本标识符。如果未提供,则使用配置的默认版本策略。
Returns:
找到的规则对象,或者如果未找到则返回None。
"""
pass
@abstractmethod
def query_rules(self, query: RuleQuery) -> List[AnyRule]:
"""
根据查询条件返回匹配的规则列表。
Args:
query: 规则查询条件。
Returns:
匹配规则的列表。
"""
pass
@abstractmethod
def save_rule(self, rule: BaseRule) -> bool:
"""
保存单个规则。
Args:
rule: 要保存的规则对象。
Returns:
操作是否成功。
"""
pass
@abstractmethod
def delete_rule(self, rule_id: str, version: Optional[str] = None) -> bool:
"""
删除单个规则。
Args:
rule_id: 规则的唯一标识符。
version: 可选的版本标识符。如果未提供,通常会删除所有版本。
Returns:
操作是否成功。
"""
pass
@abstractmethod
def list_all_rule_ids(self) -> List[str]:
"""
列出存储中的所有规则ID。
Returns:
规则ID的列表。
"""
pass
@abstractmethod
def get_rule_versions(self, rule_id: str) -> List[str]:
"""
获取特定规则ID的所有可用版本。
Args:
rule_id: 规则的唯一标识符。
Returns:
版本标识符的列表。
"""
pass
@@ -0,0 +1,324 @@
"""File system adapter for rule storage using JSON files."""
import os
import json
import glob
from typing import List, Dict, Optional, Any, Tuple, Type, Union, cast
import logging
from datetime import datetime
from .base_adapter import BaseRuleStorageAdapter
from ...models.rule_models import (
AnyRule, RuleQuery, BaseRule, RuleCategory, TargetType
)
from .rule_adapter_utils import parse_rule_data
class FilesystemAdapter(BaseRuleStorageAdapter):
"""
基于文件系统的规则存储适配器,使用JSON文件保存规则。
文件结构约定:
规则文件将按以下方式组织:
rules/
json_schemas/
rule_id1/
1.0.0.json
1.1.0.json
rule_id2/
1.0.0.json
business_logic/
rule_id3/
1.0.0.json
...
"""
def __init__(self, base_path: str = "./rules", file_pattern: str = "*.json"):
"""
初始化适配器。
Args:
base_path: 存储规则的基本目录路径。
file_pattern: 匹配规则文件的glob模式。
"""
self.base_path = os.path.abspath(base_path)
self.file_pattern = file_pattern
self.logger = logging.getLogger(__name__)
def initialize(self) -> None:
"""确保基本目录存在,并验证其可访问性。"""
if not os.path.exists(self.base_path):
try:
os.makedirs(self.base_path)
self.logger.info(f"Created rules directory at {self.base_path}")
except Exception as e:
self.logger.error(f"Failed to create rules directory at {self.base_path}: {e}")
raise ValueError(f"Rules directory {self.base_path} does not exist and could not be created")
if not os.access(self.base_path, os.R_OK | os.W_OK):
self.logger.error(f"Rules directory {self.base_path} is not readable and writable")
raise ValueError(f"Rules directory {self.base_path} is not readable and writable")
# 确保每个规则类别的子目录存在
for category in RuleCategory:
category_dir = self._get_category_dir(category)
if not os.path.exists(category_dir):
try:
os.makedirs(category_dir)
self.logger.debug(f"Created category directory at {category_dir}")
except Exception as e:
self.logger.warning(f"Failed to create category directory at {category_dir}: {e}")
def _get_category_dir(self, category: RuleCategory) -> str:
"""获取给定规则类别的目录路径。"""
return os.path.join(self.base_path, category.value.lower())
def _get_rule_dir(self, rule_id: str, category: Optional[RuleCategory] = None) -> str:
"""
获取给定规则ID的目录路径。
如果指定了类别,则直接使用该类别的目录;否则尝试在所有类别中查找。
"""
if category:
return os.path.join(self._get_category_dir(category), rule_id)
# 如果未指定类别,检查所有类别目录
for cat in RuleCategory:
rule_dir = os.path.join(self._get_category_dir(cat), rule_id)
if os.path.exists(rule_dir):
return rule_dir
# 如果未找到任何匹配项,默认返回通用类别目录
return os.path.join(self._get_category_dir(RuleCategory.GENERIC), rule_id)
def _get_rule_file_path(self, rule_id: str, version: str, category: Optional[RuleCategory] = None) -> str:
"""获取给定规则ID和版本的文件路径。"""
rule_dir = self._get_rule_dir(rule_id, category)
return os.path.join(rule_dir, f"{version}.json")
def _get_rule_from_file(self, file_path: str) -> Optional[AnyRule]:
"""从文件加载规则。"""
if not os.path.exists(file_path):
self.logger.debug(f"Rule file {file_path} does not exist")
return None
try:
with open(file_path, 'r', encoding='utf-8') as f:
raw_data = json.load(f)
# 使用工具函数解析规则数据
return parse_rule_data(raw_data)
except json.JSONDecodeError as e:
self.logger.error(f"Failed to parse JSON from {file_path}: {e}")
return None
except Exception as e:
self.logger.error(f"Error loading rule from {file_path}: {e}")
return None
def _save_rule_to_file(self, rule: BaseRule, file_path: str) -> bool:
"""将规则保存到文件。"""
try:
# 确保目录存在
dir_path = os.path.dirname(file_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
# 序列化规则为JSON并写入文件
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(rule.model_dump(), f, indent=2, ensure_ascii=False)
return True
except Exception as e:
self.logger.error(f"Failed to save rule to {file_path}: {e}")
return False
def _get_latest_version(self, rule_id: str, category: Optional[RuleCategory] = None) -> Optional[str]:
"""获取给定规则ID的最新版本。"""
versions = self.get_rule_versions(rule_id, category)
if not versions:
return None
# 简单地按字符串排序,假设版本格式是类似于"1.0.0"的语义化版本
# 如果需要更复杂的版本比较,可以使用packaging.version
versions.sort()
return versions[-1]
def get_rule_versions(self, rule_id: str, category: Optional[RuleCategory] = None) -> List[str]:
"""获取给定规则ID的所有版本。"""
rule_dir = self._get_rule_dir(rule_id, category)
if not os.path.exists(rule_dir):
return []
# 查找目录中所有匹配的JSON文件,并提取版本号(文件名)
pattern = os.path.join(rule_dir, self.file_pattern)
version_files = glob.glob(pattern)
versions = []
for vf in version_files:
version = os.path.splitext(os.path.basename(vf))[0] # 移除.json扩展名
versions.append(version)
return versions
def load_rule_by_id(self, rule_id: str, version: Optional[str] = None) -> Optional[AnyRule]:
"""
按ID加载单个规则。
Args:
rule_id: 规则的唯一标识符。
version: 可选的版本标识符。如果未提供,则加载最新版本。
Returns:
找到的规则对象,或者如果未找到则返回None。
"""
if not version:
version = self._get_latest_version(rule_id)
if not version:
self.logger.debug(f"No versions found for rule ID {rule_id}")
return None
file_path = self._get_rule_file_path(rule_id, version)
return self._get_rule_from_file(file_path)
def query_rules(self, query: RuleQuery) -> List[AnyRule]:
"""
根据查询条件查询规则。
Args:
query: 包含筛选条件的查询对象。
Returns:
匹配查询条件的规则列表。
"""
results = []
# 如果指定了规则ID,直接加载该规则
if query.rule_id:
rule = self.load_rule_by_id(query.rule_id, query.version if query.version != "latest" else None)
if rule and self._rule_matches_query(rule, query):
results.append(rule)
return results
# 否则,根据查询条件扫描规则文件
categories_to_search = [query.category] if query.category else list(RuleCategory)
for category in categories_to_search:
category_dir = self._get_category_dir(category)
if not os.path.exists(category_dir):
continue
# 获取该类别下的所有规则ID(子目录)
rule_dirs = [d for d in os.listdir(category_dir)
if os.path.isdir(os.path.join(category_dir, d))]
for rule_id in rule_dirs:
# 对于每个规则ID,加载指定版本或最新版本
if query.version and query.version != "latest":
file_path = self._get_rule_file_path(rule_id, query.version, category)
rule = self._get_rule_from_file(file_path)
if rule and self._rule_matches_query(rule, query):
results.append(rule)
else:
latest_version = self._get_latest_version(rule_id, category)
if latest_version:
file_path = self._get_rule_file_path(rule_id, latest_version, category)
rule = self._get_rule_from_file(file_path)
if rule and self._rule_matches_query(rule, query):
results.append(rule)
return results
def _rule_matches_query(self, rule: BaseRule, query: RuleQuery) -> bool:
"""检查规则是否匹配查询条件。"""
# 检查是否启用
if query.is_enabled is not None and rule.is_enabled != query.is_enabled:
return False
# 检查目标类型
if query.target_type and rule.target_type != query.target_type:
return False
# 检查目标标识符
if query.target_identifier and rule.target_identifier != query.target_identifier:
return False
# 检查标签
if query.tags:
if not rule.tags:
return False
# 检查是否所有查询标签都在规则标签中
if not all(tag in rule.tags for tag in query.tags):
return False
return True
def save_rule(self, rule: BaseRule) -> bool:
"""
保存规则到文件系统。
Args:
rule: 要保存的规则对象。
Returns:
操作是否成功。
"""
file_path = self._get_rule_file_path(rule.id, rule.version, rule.category)
return self._save_rule_to_file(rule, file_path)
def delete_rule(self, rule_id: str, version: Optional[str] = None) -> bool:
"""
删除规则。
Args:
rule_id: 规则的唯一标识符。
version: 如果提供,仅删除该版本;否则删除所有版本。
Returns:
操作是否成功。
"""
try:
if version:
# 删除特定版本
file_path = self._get_rule_file_path(rule_id, version)
if os.path.exists(file_path):
os.remove(file_path)
self.logger.info(f"Deleted rule file: {file_path}")
else:
self.logger.warning(f"Rule file not found for deletion: {file_path}")
return False
else:
# 删除所有版本(整个规则目录)
rule_dir = self._get_rule_dir(rule_id)
if os.path.exists(rule_dir):
# 递归删除目录及其内容
for root, dirs, files in os.walk(rule_dir, topdown=False):
for file in files:
os.remove(os.path.join(root, file))
for dir in dirs:
os.rmdir(os.path.join(root, dir))
os.rmdir(rule_dir)
self.logger.info(f"Deleted rule directory: {rule_dir}")
else:
self.logger.warning(f"Rule directory not found for deletion: {rule_dir}")
return False
return True
except Exception as e:
self.logger.error(f"Error deleting rule {rule_id} (version={version}): {e}")
return False
def list_all_rule_ids(self) -> List[str]:
"""列出所有规则ID。"""
all_rule_ids = set()
# 扫描所有类别目录
for category in RuleCategory:
category_dir = self._get_category_dir(category)
if not os.path.exists(category_dir):
continue
# 获取该类别下的所有规则ID(子目录)
rule_dirs = [d for d in os.listdir(category_dir)
if os.path.isdir(os.path.join(category_dir, d))]
all_rule_ids.update(rule_dirs)
return list(all_rule_ids)
@@ -0,0 +1,60 @@
"""规则适配器工具函数"""
import logging
from typing import Dict, Any, Optional, Type
from ...models.rule_models import (
AnyRule, BaseRule, RuleCategory,
JSONSchemaDefinition, APILintingRuleset, BusinessAssertionTemplate, DataQualityRule,
PythonCodeRule, PerformanceRule, SecurityRule, RESTfulDesignRule, ErrorHandlingRule
)
# 规则类别到Pydantic模型类的映射
RULE_CLASSES = {
RuleCategory.JSON_SCHEMA: JSONSchemaDefinition,
RuleCategory.API_LINTING: APILintingRuleset,
RuleCategory.BUSINESS_LOGIC: BusinessAssertionTemplate,
RuleCategory.DATA_QUALITY: DataQualityRule,
RuleCategory.PYTHON_CODE: PythonCodeRule,
RuleCategory.PERFORMANCE: PerformanceRule,
RuleCategory.SECURITY: SecurityRule,
RuleCategory.API_DESIGN: RESTfulDesignRule,
RuleCategory.ERROR_HANDLING: ErrorHandlingRule,
}
logger = logging.getLogger(__name__)
def get_rule_class_by_category(category: RuleCategory) -> Type[BaseRule]:
"""根据规则类别获取对应的规则类"""
return RULE_CLASSES.get(category, BaseRule)
def parse_rule_data(raw_data: Dict[str, Any]) -> Optional[AnyRule]:
"""
解析规则数据,返回对应类型的规则对象
Args:
raw_data: 从文件加载的原始规则数据
Returns:
规则对象,如果无法解析则返回None
"""
try:
# 确保数据包含类别信息
if 'category' not in raw_data:
logger.warning("Rule data missing 'category' field")
return None
# 尝试将字符串类别转换为枚举值
try:
category = RuleCategory(raw_data['category'])
except ValueError:
logger.warning(f"Rule data has invalid category: {raw_data['category']}")
return None
# 获取对应的Pydantic模型类
rule_class = get_rule_class_by_category(category)
# 使用Pydantic模型解析数据
return rule_class(**raw_data)
except Exception as e:
logger.error(f"Error parsing rule data: {e}")
return None
@@ -0,0 +1,314 @@
"""
Python代码规则执行器
负责安全地执行规则中包含的Python代码,确保代码在隔离的环境中运行,
并对可执行的操作进行限制,以防恶意代码。
"""
import ast
import logging
import importlib
import inspect
import time
import threading
import os
from typing import Dict, Any, Optional, List, Callable, Tuple
from concurrent.futures import ThreadPoolExecutor
from ..models.rule_models import PythonCodeRule
from ..models.rule_models import RuleCategory
logger = logging.getLogger(__name__)
class CodeExecutionError(Exception):
"""执行Python代码时发生的错误"""
pass
class TimeoutError(CodeExecutionError):
"""代码执行超时错误"""
pass
class ImportError(CodeExecutionError):
"""非法导入模块错误"""
pass
class ValidationResult:
"""Python代码验证的结果"""
def __init__(self,
is_valid: bool,
message: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
exception: Optional[Exception] = None):
self.is_valid = is_valid
self.message = message
self.details = details or {}
self.exception = exception
def __bool__(self):
return self.is_valid
class PythonRuleExecutor:
"""
Python代码规则执行器
负责安全地执行规则中的Python代码,并返回验证结果。
"""
def __init__(self, rules_base_path: str = "./rules"):
self.logger = logging.getLogger(__name__)
self.rules_base_path = os.path.abspath(rules_base_path)
def _load_code_from_file(self, rule: PythonCodeRule) -> str:
"""
从文件加载代码
Args:
rule: Python代码规则对象,包含code_file属性
Returns:
加载的代码内容
Raises:
CodeExecutionError: 如果加载代码失败
"""
if not rule.code_file:
raise CodeExecutionError("规则未指定code_file属性")
# 构建代码文件的绝对路径
# 如果规则ID和版本都存在,则在python_code目录下查找
if rule.id and rule.version:
# 代码文件路径格式: rules/python_code/{rule_id}/{version}.py
file_path = os.path.join(self.rules_base_path, "python_code", rule.id, f"{rule.version}.py")
else:
# 否则使用传入的相对路径
file_path = os.path.join(self.rules_base_path, rule.code_file)
# 检查文件是否存在
if not os.path.isfile(file_path):
raise CodeExecutionError(f"代码文件不存在: {file_path}")
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
raise CodeExecutionError(f"读取代码文件失败: {e}")
def analyze_code(self, code: str) -> List[str]:
"""
分析代码,检查其中使用的导入模块
Args:
code: 要分析的Python代码
Returns:
代码中导入的模块列表
Raises:
SyntaxError: 如果代码存在语法错误
"""
try:
tree = ast.parse(code)
except SyntaxError as e:
raise CodeExecutionError(f"代码语法错误: {e}")
imports = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for name in node.names:
imports.append(name.name)
elif isinstance(node, ast.ImportFrom):
imports.append(node.module)
return imports
def _execute_with_timeout(self,
func: Callable,
args: Tuple,
kwargs: Dict[str, Any],
timeout: int) -> Any:
"""
使用超时执行函数
Args:
func: 要执行的函数
args: 函数的位置参数
kwargs: 函数的关键字参数
timeout: 超时时间(秒)
Returns:
函数的返回值
Raises:
TimeoutError: 如果函数执行超过指定的超时时间
"""
result = [None]
exception = [None]
def target():
try:
result[0] = func(*args, **kwargs)
except Exception as e:
exception[0] = e
thread = threading.Thread(target=target)
thread.daemon = True
thread.start()
thread.join(timeout)
if thread.is_alive():
# 超时
raise TimeoutError(f"代码执行超时(超过{timeout}秒)")
if exception[0]:
raise exception[0]
return result[0]
def execute_rule(self, rule: PythonCodeRule, context: Dict[str, Any]) -> ValidationResult:
"""
执行Python代码规则
Args:
rule: Python代码规则定义
context: 验证上下文(包含规则执行所需的参数和数据)
Returns:
ValidationResult: 验证结果
"""
# 检查是否提供了所有必需的参数
if rule.expected_parameters:
missing_params = [p for p in rule.expected_parameters if p not in context]
if missing_params:
return ValidationResult(
is_valid=False,
message=f"缺少必需的参数: {', '.join(missing_params)}"
)
# 获取代码内容
try:
if rule.code:
code = rule.code
elif rule.code_file:
code = self._load_code_from_file(rule)
else:
return ValidationResult(
is_valid=False,
message="规则既没有code也没有code_file属性"
)
except CodeExecutionError as e:
return ValidationResult(
is_valid=False,
message=str(e),
exception=e
)
# 分析代码中的导入
try:
imports = self.analyze_code(code)
except CodeExecutionError as e:
return ValidationResult(
is_valid=False,
message=str(e),
exception=e
)
# 检查导入是否被允许
if imports and not rule.allow_imports:
return ValidationResult(
is_valid=False,
message=f"规则不允许导入模块,但代码尝试导入: {', '.join(imports)}"
)
# 如果允许导入,检查是否所有导入都在允许列表中
if imports and rule.allow_imports and rule.allowed_modules:
unauthorized_imports = [imp for imp in imports if imp not in rule.allowed_modules]
if unauthorized_imports:
return ValidationResult(
is_valid=False,
message=f"代码尝试导入未授权的模块: {', '.join(unauthorized_imports)}"
)
# 准备执行环境
# 创建一个隔离的命名空间
namespace = {'__builtins__': __builtins__}
# 添加上下文变量
namespace.update(context)
# 如果允许导入,预先导入允许的模块
if rule.allow_imports and imports:
for module_name in imports:
if not rule.allowed_modules or module_name in rule.allowed_modules:
try:
module = importlib.import_module(module_name)
namespace[module_name] = module
except Exception as e:
return ValidationResult(
is_valid=False,
message=f"导入模块 '{module_name}' 失败: {e}",
exception=e
)
# 执行代码
try:
# 编译代码
compiled_code = compile(code, f"<rule:{rule.id}>", 'exec')
# 执行代码
self._execute_with_timeout(
exec,
(compiled_code, namespace),
{},
rule.timeout
)
# 获取入口函数
if rule.entry_function not in namespace:
return ValidationResult(
is_valid=False,
message=f"代码未定义指定的入口函数 '{rule.entry_function}'"
)
entry_func = namespace[rule.entry_function]
if not callable(entry_func):
return ValidationResult(
is_valid=False,
message=f"'{rule.entry_function}' 不是一个可调用的函数"
)
# 调用入口函数
result = self._execute_with_timeout(
entry_func,
tuple(),
{},
rule.timeout
)
# 处理结果
if isinstance(result, dict):
# 如果函数返回一个字典,将它转换为ValidationResult
return ValidationResult(
is_valid=bool(result.get('is_valid', False)),
message=result.get('message'),
details=result.get('details', {})
)
else:
# 如果返回其他类型,将其解释为布尔值
return ValidationResult(
is_valid=bool(result),
message=str(result) if result is not True else "验证通过"
)
except TimeoutError as e:
return ValidationResult(
is_valid=False,
message=str(e),
exception=e
)
except Exception as e:
return ValidationResult(
is_valid=False,
message=f"执行代码时发生错误: {e}",
exception=e
)
@@ -0,0 +1,366 @@
"""规则库核心模块"""
import logging
from typing import Dict, List, Optional, Type, Union, Any
from ..models.rule_models import AnyRule, BaseRule, RuleQuery, RuleCategory, TargetType, RuleLifecycle, RuleScope
from ..models.config_models import RuleRepositoryConfig
from .adapters.base_adapter import BaseRuleStorageAdapter
from .adapters.filesystem_adapter import FilesystemAdapter
from .yaml_adapter import YAMLAdapter
# 未来可能添加的其他适配器
# from .adapters.db_adapter import DatabaseAdapter
# from .adapters.in_memory_adapter import InMemoryAdapter
class RuleRepository:
"""
规则库模块的核心类。
负责通过合适的存储适配器管理和提供规则。
"""
def __init__(self, config: RuleRepositoryConfig):
"""
初始化规则库。
Args:
config: 规则库配置
"""
self.config = config
self.logger = logging.getLogger(__name__)
# 创建适当的存储适配器
self.adapters = self._create_adapters()
# 用于在内存中缓存规则 (如果启用了preload_rules)
self.rule_cache: Dict[str, Dict[str, AnyRule]] = {} # {rule_id: {version: rule}}
# 初始化适配器
for adapter in self.adapters:
adapter.initialize()
# 如果配置了预加载规则,则加载所有规则到内存
if self.config.preload_rules:
self._preload_rules()
def _create_adapters(self) -> List[BaseRuleStorageAdapter]:
"""根据配置创建适当的存储适配器。"""
adapters = []
storage_type = self.config.storage.type.lower()
if storage_type == "filesystem":
# 添加JSON规则适配器
adapters.append(FilesystemAdapter(
base_path=self.config.storage.path or "./rules"
))
# 添加YAML规则适配器
adapters.append(YAMLAdapter(
base_path=self.config.storage.path or "./rules"
))
# 未来可能添加的其他适配器类型
# elif storage_type == "database":
# adapters.append(DatabaseAdapter(
# connection_string=self.config.storage.connection_string
# ))
# elif storage_type == "in_memory":
# adapters.append(InMemoryAdapter())
else:
raise ValueError(f"Unsupported rule storage type: {storage_type}")
return adapters
def _preload_rules(self) -> None:
"""预加载所有规则到内存缓存。"""
self.logger.info("Preloading rules from storage...")
all_rule_ids = set()
# 从所有适配器收集规则ID
for adapter in self.adapters:
rule_ids = adapter.list_all_rule_ids()
all_rule_ids.update(rule_ids)
loaded_count = 0
for rule_id in all_rule_ids:
versions_by_adapter = []
# 从所有适配器收集规则版本
for adapter in self.adapters:
versions = adapter.get_rule_versions(rule_id)
if versions:
versions_by_adapter.append((adapter, versions))
if not versions_by_adapter:
continue
if rule_id not in self.rule_cache:
self.rule_cache[rule_id] = {}
# 对于每个适配器的每个版本,尝试加载规则
for adapter, versions in versions_by_adapter:
for version in versions:
rule = adapter.load_rule_by_id(rule_id, version)
if rule:
self.rule_cache[rule_id][version] = rule
loaded_count += 1
self.logger.info(f"Preloaded {loaded_count} rules from {len(all_rule_ids)} rule IDs")
def get_rule(self, rule_id: str, version: Optional[str] = None) -> Optional[AnyRule]:
"""
获取指定ID和版本的规则。
Args:
rule_id: 规则ID
version: 规则版本(如果未指定,则使用配置的默认版本策略)
Returns:
规则对象,如果未找到则返回None
"""
# 优先从缓存中获取,如果启用了预加载
if self.config.preload_rules and rule_id in self.rule_cache:
if version and version in self.rule_cache[rule_id]:
return self.rule_cache[rule_id][version]
elif not version and self.rule_cache[rule_id]:
# 获取最新版本
latest_version = self._get_latest_version(list(self.rule_cache[rule_id].keys()))
return self.rule_cache[rule_id].get(latest_version)
# 从适配器加载
for adapter in self.adapters:
rule = adapter.load_rule_by_id(rule_id, version)
if rule:
return rule
return None
def _get_latest_version(self, versions: List[str]) -> str:
"""简单地按字符串排序获取最新版本。"""
if not versions:
return ""
versions.sort()
return versions[-1]
def query_rules(self, query: Optional[RuleQuery] = None) -> List[AnyRule]:
"""
根据查询条件查询规则。
Args:
query: 规则查询条件,如果为None则使用默认查询
Returns:
匹配规则的列表
"""
query = query or RuleQuery()
# 从所有适配器查询规则
results = []
for adapter in self.adapters:
adapter_results = adapter.query_rules(query)
if adapter_results:
results.extend(adapter_results)
# 去重(可能不同适配器返回相同ID和版本的规则)
deduplicated = {}
for rule in results:
key = f"{rule.id}:{rule.version}"
if key not in deduplicated:
deduplicated[key] = rule
return list(deduplicated.values())
def get_rules_by_tags(self, tags: List[str], match_all: bool = False) -> List[AnyRule]:
"""
根据标签查询规则。
Args:
tags: 要匹配的标签列表。
match_all: 如果为True,则规则必须包含所有指定的标签;
如果为False(默认),则规则包含任何一个指定标签即可匹配。
Returns:
匹配标签的规则列表。
"""
if not tags:
return [] # 如果没有提供标签,返回空列表
# 获取所有规则进行过滤。可以考虑优化,如果规则量非常大,
# 且适配器支持基于标签的查询,则直接调用适配器。
# 目前,我们先在查询所有规则后进行内存过滤。
all_rules = self.query_rules(RuleQuery(is_enabled=True)) # 通常只查询启用的规则
matched_rules = []
tag_set_query = set(tag.lower() for tag in tags) # 查询标签转换为小写集合以进行不区分大小写的比较
for rule in all_rules:
if not rule.tags: # 如果规则没有标签,则跳过
continue
rule_tags_set = set(t.lower() for t in rule.tags) # 规则的标签也转换为小写集合
if match_all:
# 需要匹配所有查询标签
if tag_set_query.issubset(rule_tags_set):
matched_rules.append(rule)
else:
# 只需要匹配任何一个查询标签
if not tag_set_query.isdisjoint(rule_tags_set): # 如果交集不为空
matched_rules.append(rule)
return matched_rules
def save_rule(self, rule: BaseRule) -> bool:
"""
保存规则到存储。
Args:
rule: 要保存的规则
Returns:
操作是否成功
"""
# 根据规则类别选择合适的适配器
adapter_to_use = self.adapters[0] # 默认使用第一个适配器
# 如果是YAML格式的规则,使用YAML适配器
if hasattr(rule, 'code') and rule.code:
for adapter in self.adapters:
if isinstance(adapter, YAMLAdapter):
adapter_to_use = adapter
break
result = adapter_to_use.save_rule(rule)
# 如果成功保存且启用了预加载,更新缓存
if result and self.config.preload_rules:
if rule.id not in self.rule_cache:
self.rule_cache[rule.id] = {}
self.rule_cache[rule.id][rule.version] = rule
return result
def delete_rule(self, rule_id: str, version: Optional[str] = None) -> bool:
"""
从存储中删除规则。
Args:
rule_id: 规则ID
version: 如果指定,仅删除该版本;否则删除所有版本
Returns:
操作是否成功
"""
# 从所有适配器删除规则
overall_result = True
for adapter in self.adapters:
try:
result = adapter.delete_rule(rule_id, version)
if not result:
overall_result = False
except Exception as e:
self.logger.error(f"Error deleting rule {rule_id} (version={version}) from adapter {adapter.__class__.__name__}: {e}")
overall_result = False
# 如果启用了预加载,更新缓存
if self.config.preload_rules:
if version and rule_id in self.rule_cache:
# 删除特定版本
if version in self.rule_cache[rule_id]:
del self.rule_cache[rule_id][version]
# 如果该规则没有更多版本,删除整个规则条目
if not self.rule_cache[rule_id]:
del self.rule_cache[rule_id]
elif rule_id in self.rule_cache:
# 删除所有版本
del self.rule_cache[rule_id]
return overall_result
def get_rules_for_target(self, target_type: TargetType, target_id: str) -> List[AnyRule]:
"""
获取适用于特定目标的规则。
这是一个便捷方法,用于当TestExecutor或JSONSchemaValidator需要找到适用于特定API操作或数据对象的规则。
Args:
target_type: 目标类型(如APIRequest, APIResponse, DataObject
target_id: 目标标识符(如API操作ID, 数据对象名称)
Returns:
适用于该目标的规则列表
"""
query = RuleQuery(
target_type=target_type,
target_identifier=target_id,
is_enabled=True
)
return self.query_rules(query)
def get_rules_by_lifecycle(self, lifecycle: RuleLifecycle, target_type: Optional[TargetType] = None) -> List[AnyRule]:
"""
获取适用于特定生命周期阶段的规则。
Args:
lifecycle: 规则适用的生命周期阶段
target_type: 可选的目标类型过滤
Returns:
适用于该生命周期阶段的规则列表
"""
query = RuleQuery(
lifecycle=lifecycle,
target_type=target_type,
is_enabled=True
)
return self.query_rules(query)
def get_rules_by_scope(self, scope: RuleScope, target_type: Optional[TargetType] = None) -> List[AnyRule]:
"""
获取适用于特定作用域的规则。
Args:
scope: 规则的作用域
target_type: 可选的目标类型过滤
Returns:
适用于该作用域的规则列表
"""
query = RuleQuery(
scope=scope,
target_type=target_type,
is_enabled=True
)
return self.query_rules(query)
def get_schema_for_target(self, target_type: TargetType, target_id: str) -> Optional[Dict[str, Any]]:
"""
获取适用于特定目标的JSON Schema。
这是一个便捷方法,用于当JSONSchemaValidator需要找到适用于特定API操作或数据对象的JSON Schema。
Args:
target_type: 目标类型(如APIRequest, APIResponse, DataObject
target_id: 目标标识符(如API操作ID, 数据对象名称)
Returns:
JSON Schema字典,如果未找到则返回None
"""
query = RuleQuery(
category=RuleCategory.JSON_SCHEMA,
target_type=target_type,
target_identifier=target_id,
is_enabled=True
)
schemas = self.query_rules(query)
if not schemas:
return None
# 如果有多个匹配的Schema规则,使用最新版本的
# 注意:这里可以根据需要实现更复杂的选择逻辑
schemas.sort(key=lambda x: x.version)
latest_schema = schemas[-1]
# 假设是JSONSchemaDefinition类型
if hasattr(latest_schema, 'schema_content'):
return latest_schema.schema_content
return None
@@ -0,0 +1,333 @@
"""YAML adapter for rule storage using YAML files."""
import os
import yaml
import glob
from typing import List, Dict, Optional, Any, Tuple, Type, Union, cast
import logging
from datetime import datetime
from .adapters.base_adapter import BaseRuleStorageAdapter
from ..models.rule_models import (
AnyRule, RuleQuery, BaseRule, RuleCategory, TargetType, RuleLifecycle, RuleScope
)
from .adapters.rule_adapter_utils import parse_rule_data
class YAMLAdapter(BaseRuleStorageAdapter):
"""
基于YAML文件的规则存储适配器,使用YAML文件保存规则。
文件结构约定:
规则文件将按以下方式组织:
rules/
yaml_rules/
json_schemas/
rule_id1/
1.0.0.yaml
1.1.0.yaml
rule_id2/
1.0.0.yaml
business_logic/
rule_id3/
1.0.0.yaml
...
"""
def __init__(self, base_path: str = "./rules", file_pattern: str = "*.yaml"):
"""
初始化适配器。
Args:
base_path: 存储规则的基本目录路径。
file_pattern: 匹配规则文件的glob模式。
"""
self.base_path = os.path.abspath(base_path)
self.file_pattern = file_pattern
self.logger = logging.getLogger(__name__)
self.yaml_dir = os.path.join(self.base_path, "yaml_rules")
def initialize(self) -> None:
"""确保基本目录存在,并验证其可访问性。"""
if not os.path.exists(self.yaml_dir):
try:
os.makedirs(self.yaml_dir)
self.logger.info(f"Created YAML rules directory at {self.yaml_dir}")
except Exception as e:
self.logger.error(f"Failed to create YAML rules directory at {self.yaml_dir}: {e}")
raise ValueError(f"YAML rules directory {self.yaml_dir} does not exist and could not be created")
if not os.access(self.yaml_dir, os.R_OK | os.W_OK):
self.logger.error(f"YAML rules directory {self.yaml_dir} is not readable and writable")
raise ValueError(f"YAML rules directory {self.yaml_dir} is not readable and writable")
# 确保每个规则类别的子目录存在
for category in RuleCategory:
category_dir = self._get_category_dir(category)
if not os.path.exists(category_dir):
try:
os.makedirs(category_dir)
self.logger.debug(f"Created category directory at {category_dir}")
except Exception as e:
self.logger.warning(f"Failed to create category directory at {category_dir}: {e}")
def _get_category_dir(self, category: RuleCategory) -> str:
"""获取给定规则类别的目录路径。"""
return os.path.join(self.yaml_dir, category.value.lower())
def _get_rule_dir(self, rule_id: str, category: Optional[RuleCategory] = None) -> str:
"""
获取给定规则ID的目录路径。
如果指定了类别,则直接使用该类别的目录;否则尝试在所有类别中查找。
"""
if category:
return os.path.join(self._get_category_dir(category), rule_id)
# 如果未指定类别,检查所有类别目录
for cat in RuleCategory:
rule_dir = os.path.join(self._get_category_dir(cat), rule_id)
if os.path.exists(rule_dir):
return rule_dir
# 如果未找到任何匹配项,默认返回通用类别目录
return os.path.join(self._get_category_dir(RuleCategory.GENERIC), rule_id)
def _get_rule_file_path(self, rule_id: str, version: str, category: Optional[RuleCategory] = None) -> str:
"""获取给定规则ID和版本的文件路径。"""
rule_dir = self._get_rule_dir(rule_id, category)
return os.path.join(rule_dir, f"{version}.yaml")
def _get_rule_from_file(self, file_path: str) -> Optional[AnyRule]:
"""从YAML文件加载规则。"""
if not os.path.exists(file_path):
self.logger.debug(f"Rule file {file_path} does not exist")
return None
try:
with open(file_path, 'r', encoding='utf-8') as f:
raw_data = yaml.safe_load(f)
# 使用工具函数解析规则数据
return parse_rule_data(raw_data)
except yaml.YAMLError as e:
self.logger.error(f"Failed to parse YAML from {file_path}: {e}")
return None
except Exception as e:
self.logger.error(f"Error loading rule from {file_path}: {e}")
return None
def _save_rule_to_file(self, rule: BaseRule, file_path: str) -> bool:
"""将规则保存到YAML文件。"""
try:
# 确保目录存在
dir_path = os.path.dirname(file_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
# 将规则序列化为YAML并写入文件
with open(file_path, 'w', encoding='utf-8') as f:
yaml.dump(rule.model_dump(), f, default_flow_style=False, sort_keys=False)
return True
except Exception as e:
self.logger.error(f"Failed to save rule to {file_path}: {e}")
return False
def _get_latest_version(self, rule_id: str, category: Optional[RuleCategory] = None) -> Optional[str]:
"""获取给定规则ID的最新版本。"""
versions = self.get_rule_versions(rule_id, category)
if not versions:
return None
# 简单地按字符串排序,假设版本格式是类似于"1.0.0"的语义化版本
versions.sort()
return versions[-1]
def get_rule_versions(self, rule_id: str, category: Optional[RuleCategory] = None) -> List[str]:
"""获取给定规则ID的所有版本。"""
rule_dir = self._get_rule_dir(rule_id, category)
if not os.path.exists(rule_dir):
return []
# 查找目录中所有匹配的YAML文件,并提取版本号(文件名)
pattern = os.path.join(rule_dir, self.file_pattern)
version_files = glob.glob(pattern)
versions = []
for vf in version_files:
version = os.path.splitext(os.path.basename(vf))[0] # 移除.yaml扩展名
versions.append(version)
return versions
def load_rule_by_id(self, rule_id: str, version: Optional[str] = None) -> Optional[AnyRule]:
"""
按ID加载单个规则。
Args:
rule_id: 规则的唯一标识符。
version: 可选的版本标识符。如果未提供,则加载最新版本。
Returns:
找到的规则对象,或者如果未找到则返回None。
"""
if not version:
version = self._get_latest_version(rule_id)
if not version:
self.logger.debug(f"No versions found for rule ID {rule_id}")
return None
file_path = self._get_rule_file_path(rule_id, version)
return self._get_rule_from_file(file_path)
def query_rules(self, query: RuleQuery) -> List[AnyRule]:
"""
根据查询条件查询规则。
Args:
query: 包含筛选条件的查询对象。
Returns:
匹配查询条件的规则列表。
"""
results = []
# 如果指定了规则ID,直接加载该规则
if query.rule_id:
rule = self.load_rule_by_id(query.rule_id, query.version if query.version != "latest" else None)
if rule and self._rule_matches_query(rule, query):
results.append(rule)
return results
# 否则,根据查询条件扫描规则文件
categories_to_search = [query.category] if query.category else list(RuleCategory)
for category in categories_to_search:
category_dir = self._get_category_dir(category)
if not os.path.exists(category_dir):
continue
# 获取该类别下的所有规则ID(子目录)
rule_dirs = [d for d in os.listdir(category_dir)
if os.path.isdir(os.path.join(category_dir, d))]
for rule_id in rule_dirs:
# 对于每个规则ID,加载指定版本或最新版本
if query.version and query.version != "latest":
file_path = self._get_rule_file_path(rule_id, query.version, category)
rule = self._get_rule_from_file(file_path)
if rule and self._rule_matches_query(rule, query):
results.append(rule)
else:
latest_version = self._get_latest_version(rule_id, category)
if latest_version:
file_path = self._get_rule_file_path(rule_id, latest_version, category)
rule = self._get_rule_from_file(file_path)
if rule and self._rule_matches_query(rule, query):
results.append(rule)
return results
def _rule_matches_query(self, rule: BaseRule, query: RuleQuery) -> bool:
"""检查规则是否匹配查询条件。"""
# 检查是否启用
if query.is_enabled is not None and rule.is_enabled != query.is_enabled:
return False
# 检查目标类型
if query.target_type and rule.target_type != query.target_type:
return False
# 检查目标标识符
if query.target_identifier and rule.target_identifier != query.target_identifier:
return False
# 检查标签
if query.tags:
if not rule.tags:
return False
# 检查是否所有查询标签都在规则标签中
if not all(tag in rule.tags for tag in query.tags):
return False
# 检查生命周期
if query.lifecycle and rule.lifecycle != query.lifecycle:
return False
# 检查作用域
if query.scope and rule.scope != query.scope:
return False
return True
def save_rule(self, rule: BaseRule) -> bool:
"""
保存规则到文件系统。
Args:
rule: 要保存的规则对象。
Returns:
操作是否成功。
"""
file_path = self._get_rule_file_path(rule.id, rule.version, rule.category)
return self._save_rule_to_file(rule, file_path)
def delete_rule(self, rule_id: str, version: Optional[str] = None) -> bool:
"""
删除规则。
Args:
rule_id: 规则的唯一标识符。
version: 如果提供,仅删除该版本;否则删除所有版本。
Returns:
操作是否成功。
"""
try:
if version:
# 删除特定版本
file_path = self._get_rule_file_path(rule_id, version)
if os.path.exists(file_path):
os.remove(file_path)
self.logger.info(f"Deleted rule file: {file_path}")
else:
self.logger.warning(f"Rule file not found for deletion: {file_path}")
return False
else:
# 删除所有版本(整个规则目录)
rule_dir = self._get_rule_dir(rule_id)
if os.path.exists(rule_dir):
# 递归删除目录及其内容
for root, dirs, files in os.walk(rule_dir, topdown=False):
for file in files:
os.remove(os.path.join(root, file))
for dir in dirs:
os.rmdir(os.path.join(root, dir))
os.rmdir(rule_dir)
self.logger.info(f"Deleted rule directory: {rule_dir}")
else:
self.logger.warning(f"Rule directory not found for deletion: {rule_dir}")
return False
return True
except Exception as e:
self.logger.error(f"Error deleting rule {rule_id} (version={version}): {e}")
return False
def list_all_rule_ids(self) -> List[str]:
"""列出所有规则ID。"""
all_rule_ids = set()
# 扫描所有类别目录
for category in RuleCategory:
category_dir = self._get_category_dir(category)
if not os.path.exists(category_dir):
continue
# 获取该类别下的所有规则ID(子目录)
rule_dirs = [d for d in os.listdir(category_dir)
if os.path.isdir(os.path.join(category_dir, d))]
all_rule_ids.update(rule_dirs)
return list(all_rule_ids)
+100
View File
@@ -0,0 +1,100 @@
# ddms_compliance_suite/test_loader/loader.py
from abc import ABC, abstractmethod
from pathlib import Path
from typing import List, Union, Dict, Any
import yaml # PyYAML
import json
from ..models.test_models import TestSuite # Ensure correct path
# from ..models.test_models import TestCase # If TestCase can be loaded standalone
class LoadError(Exception):
"""Custom exception for errors during test case loading."""
pass
class BaseTestCaseLoader(ABC):
@abstractmethod
def load_suites_from_file(self, file_path: Union[str, Path]) -> List[TestSuite]:
"""Loads a list of TestSuite objects from a single file."""
pass
def load_suites_from_directory(self, directory_path: Union[str, Path], recursive: bool = False, pattern: str = "*.yaml") -> List[TestSuite]:
"""Loads TestSuite objects from all matching files in a directory."""
suites: List[TestSuite] = []
p = Path(directory_path)
if not p.is_dir():
raise LoadError(f"Directory not found: {directory_path}")
file_paths = list(p.rglob(pattern)) if recursive else list(p.glob(pattern))
for file_path in file_paths:
if file_path.is_file():
try:
# Ensure that the loader instance calls its own method
# This part might need adjustment if called directly on BaseTestCaseLoader
# For now, assuming it's called from a concrete instance like YAMLTestCaseLoader
suites.extend(self.load_suites_from_file(file_path))
except LoadError as e:
# Consider using a logger for warnings/errors
print(f"Warning: Could not load test suites from {file_path}: {e}")
except Exception as e:
print(f"Warning: An unexpected error occurred loading {file_path}: {e}")
return suites
class YAMLTestCaseLoader(BaseTestCaseLoader):
def load_suites_from_file(self, file_path: Union[str, Path]) -> List[TestSuite]:
file_p = Path(file_path)
if not file_p.exists() or not file_p.is_file():
raise LoadError(f"Test case file not found or is not a file: {file_path}")
try:
with open(file_p, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
except yaml.YAMLError as e:
raise LoadError(f"Error parsing YAML file {file_p}: {e}")
except Exception as e:
raise LoadError(f"An unexpected error occurred while reading {file_p}: {e}")
if data is None: # Handle empty YAML file
return []
if not isinstance(data, dict) or "test_suites" not in data:
raise LoadError(f"Invalid format in {file_p}: Missing 'test_suites' top-level key or not a dictionary.")
suite_data_list = data.get("test_suites") # Use .get for safer access
if not isinstance(suite_data_list, list):
raise LoadError(f"Invalid format in {file_p}: 'test_suites' must be a list.")
loaded_suites: List[TestSuite] = []
for i, suite_data in enumerate(suite_data_list):
if not isinstance(suite_data, dict):
print(f"Warning: Suite data #{i+1} in {file_p} is not a dictionary, skipping.")
continue
try:
suite = TestSuite.model_validate(suite_data) # For Pydantic v2+
# For Pydantic v1, use: suite = TestSuite.parse_obj(suite_data)
loaded_suites.append(suite)
except Exception as e: # Catch Pydantic validation errors and others
# It's often better to log this and continue, or collect all errors
raise LoadError(f"Error validating test suite #{i+1} (ID: {suite_data.get('id', 'N/A')}) in {file_p}: {e}")
return loaded_suites
# Example Usage (conceptual):
# if __name__ == '__main__':
# yaml_loader = YAMLTestCaseLoader()
# try:
# # suites_from_single_file = yaml_loader.load_suites_from_file('path/to/your/single_test_suite_file.yaml')
# # for suite in suites_from_single_file:
# # print(f"Loaded Suite: {suite.name}")
# all_suites_in_dir = yaml_loader.load_suites_from_directory('path/to/test_suites_directory', recursive=True, pattern="*.test_suite.yaml")
# for suite in all_suites_in_dir:
# print(f"Loaded Suite from Dir: {suite.name}")
# for tc in suite.test_cases:
# print(f" - TestCase: {tc.name}")
# except LoadError as e:
# print(f"Loading Error: {e}")
+802
View File
@@ -0,0 +1,802 @@
"""
测试编排器模块
负责组合API解析器、API调用器、验证器和规则执行器,进行端到端的API测试
"""
import logging
import json
import time
from typing import Dict, List, Any, Optional, Union, Tuple
from enum import Enum
import datetime
from .input_parser.parser import InputParser, YAPIEndpoint, SwaggerEndpoint, ParsedYAPISpec, ParsedSwaggerSpec
from .api_caller.caller import APICaller, APIRequest, APIResponse
from .json_schema_validator.validator import JSONSchemaValidator
from .rule_repository.repository import RuleRepository
from .rule_executor.executor import RuleExecutor
from .models.rule_models import RuleQuery, TargetType, RuleCategory, RuleLifecycle, RuleScope
from .models.config_models import RuleRepositoryConfig, RuleStorageConfig
class TestResult:
"""测试结果类"""
class Status(str, Enum):
"""测试状态枚举"""
PASSED = "通过"
FAILED = "失败"
ERROR = "错误"
SKIPPED = "跳过"
def __init__(self,
endpoint_id: str,
endpoint_name: str,
status: Status,
message: str = "",
api_request: Optional[APIRequest] = None,
api_response: Optional[APIResponse] = None,
validation_details: Optional[Dict[str, Any]] = None,
elapsed_time: float = 0.0):
"""
初始化测试结果
Args:
endpoint_id: API端点ID(通常是方法+路径的组合)
endpoint_name: API端点名称
status: 测试状态
message: 测试结果消息
api_request: API请求对象
api_response: API响应对象
validation_details: 验证详情
elapsed_time: 执行耗时(秒)
"""
self.endpoint_id = endpoint_id
self.endpoint_name = endpoint_name
self.status = status
self.message = message
self.api_request = api_request
self.api_response = api_response
self.validation_details = validation_details or {}
self.elapsed_time = elapsed_time
self.timestamp = datetime.datetime.now()
def to_dict(self) -> Dict[str, Any]:
"""将测试结果转换为字典"""
result = {
"endpoint_id": self.endpoint_id,
"endpoint_name": self.endpoint_name,
"status": self.status,
"message": self.message,
"elapsed_time": self.elapsed_time,
"timestamp": self.timestamp.isoformat(),
}
if self.api_request:
result["api_request"] = {
"method": self.api_request.method,
"url": str(self.api_request.url),
"params": self.api_request.params,
"body": self.api_request.json_data
}
if self.api_response:
result["api_response"] = {
"status_code": self.api_response.status_code,
"content": self.api_response.json_content if self.api_response.json_content else str(self.api_response.content),
"elapsed_time": self.api_response.elapsed_time
}
if self.validation_details:
result["validation_details"] = self.validation_details
return result
class TestSummary:
"""测试结果摘要"""
def __init__(self):
"""初始化测试结果摘要"""
self.total = 0
self.passed = 0
self.failed = 0
self.error = 0
self.skipped = 0
self.start_time = datetime.datetime.now()
self.end_time: Optional[datetime.datetime] = None
self.results: List[TestResult] = []
def add_result(self, result: TestResult):
"""添加测试结果"""
self.total += 1
if result.status == TestResult.Status.PASSED:
self.passed += 1
elif result.status == TestResult.Status.FAILED:
self.failed += 1
elif result.status == TestResult.Status.ERROR:
self.error += 1
elif result.status == TestResult.Status.SKIPPED:
self.skipped += 1
self.results.append(result)
def finalize(self):
"""完成测试,记录结束时间"""
self.end_time = datetime.datetime.now()
@property
def duration(self) -> float:
"""测试持续时间(秒)"""
if not self.end_time:
return 0.0
return (self.end_time - self.start_time).total_seconds()
@property
def success_rate(self) -> float:
"""测试成功率"""
if self.total == 0:
return 0.0
return self.passed / self.total * 100
def to_dict(self) -> Dict[str, Any]:
"""将测试结果摘要转换为字典"""
return {
"total": self.total,
"passed": self.passed,
"failed": self.failed,
"error": self.error,
"skipped": self.skipped,
"success_rate": f"{self.success_rate:.2f}%",
"start_time": self.start_time.isoformat(),
"end_time": self.end_time.isoformat() if self.end_time else None,
"duration": f"{self.duration:.2f}",
"results": [result.to_dict() for result in self.results]
}
def to_json(self, pretty=True) -> str:
"""将测试结果摘要转换为JSON字符串"""
indent = 2 if pretty else None
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
def print_summary(self):
"""打印测试结果摘要"""
print(f"\n测试结果摘要:")
print(f"总测试数: {self.total}")
print(f"通过: {self.passed}")
print(f"失败: {self.failed}")
print(f"错误: {self.error}")
print(f"跳过: {self.skipped}")
print(f"成功率: {self.success_rate:.2f}%")
print(f"总耗时: {self.duration:.2f}")
class APITestOrchestrator:
"""API测试编排器"""
def __init__(self, base_url: str, rule_repo_path: str = "./rules"):
"""
初始化API测试编排器
Args:
base_url: API基础URL
rule_repo_path: 规则库路径
"""
self.base_url = base_url.rstrip('/')
self.logger = logging.getLogger(__name__)
# 初始化组件
self.parser = InputParser()
self.api_caller = APICaller()
self.validator = JSONSchemaValidator()
# 初始化规则库和规则执行器
rule_config = RuleRepositoryConfig(
storage=RuleStorageConfig(path=rule_repo_path)
)
self.rule_repo = RuleRepository(rule_config)
self.rule_executor = RuleExecutor(self.rule_repo)
def _build_api_request(self, endpoint: Union[YAPIEndpoint, SwaggerEndpoint]) -> Tuple[APIRequest, Dict[str, Any]]:
"""
构建API请求对象
Args:
endpoint: API端点对象
Returns:
Tuple[APIRequest, Dict[str, Any]]: API请求对象和测试数据
"""
# 获取端点信息
if hasattr(endpoint, 'method'):
method = endpoint.method
else:
method = "GET" # 默认方法
if hasattr(endpoint, 'path'):
path = endpoint.path
else:
path = "/" # 默认路径
# 替换路径中的参数占位符
path_params = {}
if "{" in path and "}" in path:
# 查找路径中的所有参数
import re
param_matches = re.findall(r'\{([^}]+)\}', path)
for param in param_matches:
# 生成一个随机值作为参数
path_params[param] = f"test_{param}"
# 查找请求参数
params = {}
headers = {"Content-Type": "application/json", "Accept": "application/json"}
body = None
# YAPI端点特有属性
if hasattr(endpoint, 'req_headers') and endpoint.req_headers:
for header in endpoint.req_headers:
if 'name' in header and 'value' in header:
headers[header['name']] = header['value']
if hasattr(endpoint, 'req_query') and endpoint.req_query:
for query in endpoint.req_query:
if 'name' in query:
params[query['name']] = query.get('value', '')
if hasattr(endpoint, 'req_body_type') and endpoint.req_body_type == 'json' and hasattr(endpoint, 'req_body_other'):
try:
# 如果req_body_other是JSON字符串,它可能包含请求体schema
req_body_schema = json.loads(endpoint.req_body_other) if isinstance(endpoint.req_body_other, str) else None
if req_body_schema and isinstance(req_body_schema, dict):
# 使用schema生成请求体
body = self._generate_data_from_schema(req_body_schema)
else:
# 如果不是有效的schema,使用它作为请求体示例
body = req_body_schema
except json.JSONDecodeError:
# 如果解析失败,使用一个默认的请求体
self.logger.warning(f"无法解析YAPI请求体JSON: {endpoint.req_body_other}")
body = {"test": "data"}
# Swagger端点特有属性
if hasattr(endpoint, 'parameters') and endpoint.parameters:
for param in endpoint.parameters:
param_in = param.get('in', '')
param_name = param.get('name', '')
param_schema = param.get('schema', {})
if not param_name:
continue
# 生成参数值,优先使用example
param_value = param.get('example', None)
if param_value is None and param_schema:
param_value = self._generate_data_from_schema(param_schema)
if param_value is None:
# 使用默认值
param_value = param.get('default', 'test_value')
if param_in == 'query':
params[param_name] = param_value
elif param_in == 'header':
headers[param_name] = str(param_value)
elif param_in == 'path' and param_name in path_params:
path_params[param_name] = str(param_value)
if hasattr(endpoint, 'request_body') and endpoint.request_body:
content = endpoint.request_body.get('content', {})
json_content = content.get('application/json', {})
if 'example' in json_content:
body = json_content['example']
elif 'schema' in json_content:
# 基于schema创建请求体
body = self._generate_data_from_schema(json_content['schema'])
# 构建完整URL(替换路径参数)
url = self.base_url + path
for param, value in path_params.items():
url = url.replace(f"{{{param}}}", str(value))
# 创建API请求
request = None
try:
request = APIRequest(
method=method,
url=url,
headers=headers,
params=params,
json_data=body
)
except Exception as e:
self.logger.error(f"创建API请求时发生错误: {e}")
raise e
# 执行请求准备阶段的规则
endpoint_id = f"{method.upper()} {path}"
# 创建规则执行上下文
context = {
'api_request': request,
'endpoint_id': endpoint_id,
'endpoint': endpoint,
'path_params': path_params,
'query_params': params,
'headers': headers,
'body': body
}
# 执行请求准备阶段的规则
rule_results = self.rule_executor.execute_rules_for_lifecycle(
lifecycle=RuleLifecycle.REQUEST_PREPARATION,
context=context
)
# 保存规则执行结果,以便在测试结果中使用
self.last_request_rule_results = rule_results
# 保存请求对象,以便在响应验证时使用
self.last_request = request
# 收集测试数据
test_data = {
"path_params": path_params,
"query_params": params,
"headers": headers,
"body": body,
"rule_results": [
{
"rule_id": result.rule.id,
"rule_name": result.rule.name,
"is_valid": result.is_valid,
"message": result.message
} for result in rule_results
]
}
return request, test_data
def _validate_response(self, response: APIResponse, endpoint: Union[YAPIEndpoint, SwaggerEndpoint]) -> Dict[str, Any]:
"""
验证API响应
Args:
response: API响应对象
endpoint: API端点对象
Returns:
Dict[str, Any]: 验证结果
"""
validation_results = {
"status_code": {
"is_valid": 200 <= response.status_code < 300,
"expected": "2XX",
"actual": response.status_code
},
"json_format": {
"is_valid": response.json_content is not None,
"message": "响应应为有效的JSON格式" if response.json_content is None else "响应是有效的JSON格式"
}
}
# 尝试从API定义中提取响应schema进行验证
schema = None
schema_source = "未知"
# 从YAPI定义中提取响应schema
if hasattr(endpoint, 'res_body') and endpoint.res_body and response.json_content:
try:
# YAPI中的res_body通常是JSON字符串格式的schema
if isinstance(endpoint.res_body, str) and endpoint.res_body.strip():
schema = json.loads(endpoint.res_body)
schema_source = "YAPI响应定义"
except json.JSONDecodeError:
self.logger.warning(f"无法解析YAPI响应schema: {endpoint.res_body}")
# 从Swagger定义中提取响应schema
elif hasattr(endpoint, 'responses') and endpoint.responses and response.json_content:
# Swagger中通常以状态码为key,包含schema定义
success_responses = endpoint.responses.get('200', {}) or endpoint.responses.get('201', {})
if not success_responses and any(str(k).startswith('2') for k in endpoint.responses.keys()):
# 尝试查找任何2xx响应
for k in endpoint.responses.keys():
if str(k).startswith('2'):
success_responses = endpoint.responses[k]
break
if success_responses:
schema_obj = None
if 'schema' in success_responses:
schema_obj = success_responses['schema']
elif 'content' in success_responses and 'application/json' in success_responses['content']:
schema_obj = success_responses['content']['application/json'].get('schema')
if schema_obj:
schema = schema_obj
schema_source = "Swagger响应定义"
# 使用提取的schema进行验证
if schema and response.json_content:
try:
result = self.validator.validate(response.json_content, schema)
validation_results["schema_validation"] = {
"source": schema_source,
"is_valid": result.is_valid,
"errors": result.errors if not result.is_valid else []
}
except Exception as e:
self.logger.error(f"验证响应时发生错误: {str(e)}")
validation_results["schema_validation"] = {
"source": schema_source,
"is_valid": False,
"errors": [f"验证过程中发生错误: {str(e)}"]
}
# 如果我们有JSON Schema规则,可以验证响应体
endpoint_id = ""
if hasattr(endpoint, 'path'):
path = endpoint.path
method = getattr(endpoint, 'method', "GET")
endpoint_id = f"{method.upper()} {path}"
schema_rules = self.rule_repo.get_rules_for_target(
target_type=TargetType.API_RESPONSE,
target_id=endpoint_id
)
if schema_rules:
# 使用找到的第一个规则
from .models.rule_models import JSONSchemaDefinition
for schema_rule in schema_rules:
if isinstance(schema_rule, JSONSchemaDefinition):
# 验证响应体
if response.json_content:
result = self.validator.validate_with_rule(response.json_content, schema_rule)
validation_results[f"rule_schema_validation_{schema_rule.id}"] = {
"source": f"规则库 ({schema_rule.id})",
"is_valid": result.is_valid,
"errors": result.errors if not result.is_valid else []
}
# 使用规则执行器验证规则
# 创建执行上下文
if hasattr(endpoint, 'path'):
api_request = None
if hasattr(self, 'last_request'):
api_request = self.last_request
context = {
'api_response': response,
'api_request': api_request,
'endpoint_id': endpoint_id,
'endpoint': endpoint
}
# 执行响应验证阶段的规则
rule_results = self.rule_executor.execute_rules_for_lifecycle(
lifecycle=RuleLifecycle.RESPONSE_VALIDATION,
context=context
)
# 将规则执行结果添加到验证结果中
for i, rule_result in enumerate(rule_results):
validation_results[f"rule_execution_{i}"] = {
"rule_id": rule_result.rule.id,
"rule_name": rule_result.rule.name,
"is_valid": rule_result.is_valid,
"message": rule_result.message,
"details": rule_result.details
}
# 基本验证: 检查返回码、响应时间等
validation_results["response_time"] = {
"value": response.elapsed_time,
"message": f"响应时间: {response.elapsed_time:.4f}"
}
return validation_results
def run_test_for_endpoint(self, endpoint: Union[YAPIEndpoint, SwaggerEndpoint]) -> TestResult:
"""
运行单个API端点的测试
Args:
endpoint: API端点对象
Returns:
TestResult: 测试结果
"""
# 获取端点信息
endpoint_id = f"{getattr(endpoint, 'method', 'GET')} {getattr(endpoint, 'path', '/')}"
endpoint_name = getattr(endpoint, 'title', '') or getattr(endpoint, 'summary', '') or endpoint_id
self.logger.info(f"测试端点: {endpoint_id} - {endpoint_name}")
try:
# 构建API请求
request, test_data = self._build_api_request(endpoint)
# 检查请求准备阶段的规则验证结果
request_rule_failures = []
for rule_result in test_data.get("rule_results", []):
if not rule_result.get("is_valid", True):
request_rule_failures.append(f"{rule_result.get('rule_name', '未知规则')}: {rule_result.get('message', '验证失败')}")
# 如果有关键性的请求验证失败,可以选择跳过API调用
if request_rule_failures and any("严重错误" in failure for failure in request_rule_failures):
return TestResult(
endpoint_id=endpoint_id,
endpoint_name=endpoint_name,
status=TestResult.Status.FAILED,
message=f"请求准备阶段验证失败: {'; '.join(request_rule_failures)}",
api_request=request,
api_response=None,
validation_details={"request_rule_failures": request_rule_failures},
elapsed_time=0.0
)
# 发送请求
start_time = time.time()
response = self.api_caller.call_api(request)
elapsed_time = time.time() - start_time
# 验证响应
validation_results = self._validate_response(response, endpoint)
# 执行请求后处理规则
context = {
'api_request': request,
'api_response': response,
'endpoint_id': endpoint_id,
'endpoint': endpoint,
'elapsed_time': elapsed_time
}
post_rule_results = self.rule_executor.execute_rules_for_lifecycle(
lifecycle=RuleLifecycle.POST_VALIDATION,
context=context
)
# 将后处理规则结果添加到验证结果中
for i, rule_result in enumerate(post_rule_results):
validation_results[f"post_rule_execution_{i}"] = {
"rule_id": rule_result.rule.id,
"rule_name": rule_result.rule.name,
"is_valid": rule_result.is_valid,
"message": rule_result.message,
"details": rule_result.details
}
# 判断测试是否通过
# 检查所有验证结果是否有失败的
rule_failures = []
validation_failures = []
for key, result in validation_results.items():
if isinstance(result, dict) and 'is_valid' in result and not result['is_valid']:
if key.startswith('rule_execution_') or key.startswith('post_rule_execution_'):
rule_name = result.get('rule_name', '未知规则')
rule_message = result.get('message', '验证失败')
rule_failures.append(f"{rule_name}: {rule_message}")
else:
validation_failures.append(result.get('message', f"{key}验证失败"))
# 合并请求规则失败和响应规则失败
all_rule_failures = request_rule_failures + rule_failures
# 决定测试结果状态
if not validation_failures and not all_rule_failures:
# 所有验证和规则都通过
result = TestResult(
endpoint_id=endpoint_id,
endpoint_name=endpoint_name,
status=TestResult.Status.PASSED,
message="API测试通过",
api_request=request,
api_response=response,
validation_details=validation_results,
elapsed_time=elapsed_time
)
elif not validation_failures and all_rule_failures:
# 基本验证通过,但规则验证失败
result = TestResult(
endpoint_id=endpoint_id,
endpoint_name=endpoint_name,
status=TestResult.Status.FAILED,
message=f"API规则验证失败: {'; '.join(all_rule_failures)}",
api_request=request,
api_response=response,
validation_details=validation_results,
elapsed_time=elapsed_time
)
self.logger.error(f"接口{endpoint_id} 规则验证失败: {'; '.join(all_rule_failures)}")
else:
# 基本验证失败
result = TestResult(
endpoint_id=endpoint_id,
endpoint_name=endpoint_name,
status=TestResult.Status.FAILED,
message=f"API测试失败: {'; '.join(validation_failures)}",
api_request=request,
api_response=response,
validation_details=validation_results,
elapsed_time=elapsed_time
)
self.logger.error(f"接口{endpoint_id} 测试失败: {'; '.join(validation_failures)}")
return result
except Exception as e:
self.logger.error(f"测试端点 {endpoint_id} 时发生错误: {str(e)}")
return TestResult(
endpoint_id=endpoint_id,
endpoint_name=endpoint_name,
status=TestResult.Status.ERROR,
message=f"测试执行错误: {str(e)}",
elapsed_time=0.0
)
def run_tests_from_yapi(self, yapi_file_path: str, categories: Optional[List[str]] = None) -> TestSummary:
"""
从YAPI定义文件运行API测试
Args:
yapi_file_path: YAPI定义文件路径
categories: 要测试的API分类列表(如果为None,则测试所有分类)
Returns:
TestSummary: 测试结果摘要
"""
# 解析YAPI文件
self.logger.info(f"从YAPI文件加载API定义: {yapi_file_path}")
parsed_yapi = self.parser.parse_yapi_spec(yapi_file_path)
if not parsed_yapi:
self.logger.error(f"解析YAPI文件失败: {yapi_file_path}")
# 创建一个空的测试摘要
summary = TestSummary()
summary.finalize()
return summary
# 筛选端点
endpoints = parsed_yapi.endpoints
if categories:
endpoints = [endpoint for endpoint in endpoints if endpoint.category_name in categories]
# 运行测试
summary = TestSummary()
for endpoint in endpoints:
result = self.run_test_for_endpoint(endpoint)
summary.add_result(result)
summary.finalize()
return summary
def run_tests_from_swagger(self, swagger_file_path: str, tags: Optional[List[str]] = None) -> TestSummary:
"""
从Swagger定义文件运行API测试
Args:
swagger_file_path: Swagger定义文件路径
tags: 要测试的API标签列表(如果为None,则测试所有标签)
Returns:
TestSummary: 测试结果摘要
"""
# 解析Swagger文件
self.logger.info(f"从Swagger文件加载API定义: {swagger_file_path}")
parsed_swagger = self.parser.parse_swagger_spec(swagger_file_path)
if not parsed_swagger:
self.logger.error(f"解析Swagger文件失败: {swagger_file_path}")
# 创建一个空的测试摘要
summary = TestSummary()
summary.finalize()
return summary
# 筛选端点
endpoints = parsed_swagger.endpoints
if tags:
endpoints = [endpoint for endpoint in endpoints if any(tag in endpoint.tags for tag in tags)]
# 运行测试
summary = TestSummary()
for endpoint in endpoints:
result = self.run_test_for_endpoint(endpoint)
summary.add_result(result)
summary.finalize()
return summary
def _generate_data_from_schema(self, schema: Dict[str, Any]) -> Any:
"""
根据JSON Schema生成测试数据
Args:
schema: JSON Schema
Returns:
生成的测试数据
"""
if not schema:
return None
schema_type = schema.get('type')
if schema_type == 'object':
result = {}
properties = schema.get('properties', {})
for prop_name, prop_schema in properties.items():
# 首先检查是否有example或default值
if 'example' in prop_schema:
result[prop_name] = prop_schema['example']
elif 'default' in prop_schema:
result[prop_name] = prop_schema['default']
else:
# 递归生成子属性的值
result[prop_name] = self._generate_data_from_schema(prop_schema)
return result
elif schema_type == 'array':
# 为数组生成一个样本项
items_schema = schema.get('items', {})
# 默认生成1个元素,对于测试来说通常足够
return [self._generate_data_from_schema(items_schema)]
elif schema_type == 'string':
# 处理不同的字符串格式
string_format = schema.get('format', '')
if string_format == 'date':
return '2023-01-01'
elif string_format == 'date-time':
return '2023-01-01T12:00:00Z'
elif string_format == 'email':
return 'test@example.com'
elif string_format == 'uuid':
return '00000000-0000-0000-0000-000000000000'
elif 'enum' in schema:
# 如果有枚举值,选择第一个
return schema['enum'][0] if schema['enum'] else 'enum_value'
elif 'pattern' in schema:
# 如果有正则表达式模式,返回一个简单的符合模式的字符串
# 注意:这里只是一个简单处理,不能处理所有正则表达式
return f"pattern_{schema['pattern']}_value"
else:
return 'test_string'
elif schema_type == 'number' or schema_type == 'integer':
# 处理数值类型
if 'minimum' in schema and 'maximum' in schema:
# 如果有最小值和最大值,取中间值
return (schema['minimum'] + schema['maximum']) / 2
elif 'minimum' in schema:
return schema['minimum']
elif 'maximum' in schema:
return schema['maximum']
elif schema_type == 'integer':
return 1
else:
return 1.0
elif schema_type == 'boolean':
return True
elif schema_type == 'null':
return None
# 如果是复杂类型或未知类型,返回一个默认值
return 'test_value'
# python run_api_tests.py --base-url http://127.0.0.1:4523/m1/6386850-6083489-default --yapi assets/doc/井筒API示例.json