add:output api call details and curl
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,15 @@
|
||||
"""API Caller Module"""
|
||||
import requests
|
||||
from typing import Any, Dict, Optional, Union, List
|
||||
import json # Added for cURL body pretty printing
|
||||
from typing import Any, Dict, Optional, Union, List, Tuple # Added Tuple
|
||||
import shlex # Added for shell quoting
|
||||
import urllib.parse # Moved import to top level for reuse
|
||||
|
||||
# Attempt to import curlify, if not found, the function will raise an error or fallback
|
||||
try:
|
||||
import curlify
|
||||
except ImportError:
|
||||
curlify = None # So we can check its availability
|
||||
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
|
||||
@@ -29,6 +38,19 @@ class APIResponse(BaseModel):
|
||||
json_content: Optional[Any] = None # Parsed JSON content if applicable
|
||||
elapsed_time: float # in seconds
|
||||
|
||||
class APICallDetail(BaseModel):
|
||||
"""Model to store detailed information about a single API call for logging."""
|
||||
request_method: str
|
||||
request_url: str
|
||||
request_headers: Dict[str, str]
|
||||
request_params: Optional[Dict[str, Any]] = None
|
||||
request_body: Optional[Any] = None
|
||||
curl_command: str
|
||||
response_status_code: int
|
||||
response_headers: Dict[str, str]
|
||||
response_body: Optional[Any] = None # Could be str for non-JSON, or parsed JSON
|
||||
response_elapsed_time: float
|
||||
|
||||
class APICaller:
|
||||
"""
|
||||
Responsible for executing HTTP/S API calls to the DDMS services.
|
||||
@@ -37,8 +59,52 @@ class APICaller:
|
||||
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 {}
|
||||
# Session object can be initialized here if we want to reuse it across calls,
|
||||
# but for curlify, a temporary one in _generate_curl_command is fine.
|
||||
self._session_for_curlify = requests.Session()
|
||||
|
||||
def call_api(self, request_data: APIRequest) -> APIResponse:
|
||||
def _generate_curl_command(self, request_data: APIRequest, actual_headers: Dict[str, str]) -> str:
|
||||
"""Generates an equivalent cURL command using the curlify library."""
|
||||
if curlify is None:
|
||||
# Fallback or error message if curlify is not installed
|
||||
# For now, returning a simple message. Ideally, log this.
|
||||
print("ERROR: curlify library is not installed. Cannot generate cURL command.")
|
||||
return "curlify_not_installed"
|
||||
|
||||
# Construct the full URL with parameters for the Request object
|
||||
url_with_params = str(request_data.url)
|
||||
if request_data.params:
|
||||
query_string = urllib.parse.urlencode(request_data.params)
|
||||
url_with_params = f"{url_with_params}?{query_string}"
|
||||
|
||||
# Create a requests.Request object
|
||||
# Note: requests.Request takes 'data' for form data and 'json' for json body.
|
||||
# Our APIRequest has json_data (preferred) and data.
|
||||
req = requests.Request(
|
||||
method=request_data.method.upper(),
|
||||
url=url_with_params,
|
||||
headers=actual_headers, # actual_headers already includes defaults + request-specific
|
||||
data=request_data.data, # Pass form data if present
|
||||
json=request_data.json_data # Pass json data if present (requests handles one or the other)
|
||||
)
|
||||
|
||||
# Prepare the request using a session (needed by curlify)
|
||||
# Using the session from the APICaller instance
|
||||
prepared_request = self._session_for_curlify.prepare_request(req)
|
||||
|
||||
try:
|
||||
# Generate cURL command using curlify
|
||||
# Adding verify=False to match current requests.request(verify=False) behavior
|
||||
# compressed=True by default in curlify, which is usually fine (adds --compressed)
|
||||
curl_command_str = curlify.to_curl(prepared_request, verify=False)
|
||||
print(f"DEBUG: curlify generated command (raw): {curl_command_str}") # Debug print
|
||||
print(f"DEBUG: curlify generated command (repr): {repr(curl_command_str)}") # Added repr print
|
||||
return curl_command_str
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to generate cURL command with curlify: {e}")
|
||||
return f"curlify_generation_failed: {e}"
|
||||
|
||||
def call_api(self, request_data: APIRequest) -> Tuple[APIResponse, APICallDetail]: # Modified return type
|
||||
"""
|
||||
Makes an API call based on the provided request data.
|
||||
|
||||
@@ -46,15 +112,24 @@ class APICaller:
|
||||
request_data: An APIRequest Pydantic model instance.
|
||||
|
||||
Returns:
|
||||
An APIResponse Pydantic model instance.
|
||||
A tuple containing:
|
||||
- An APIResponse Pydantic model instance.
|
||||
- An APICallDetail Pydantic model instance.
|
||||
"""
|
||||
merged_headers = {**self.default_headers, **(request_data.headers or {})}
|
||||
timeout = request_data.timeout or self.default_timeout
|
||||
json_payload = request_data.json_data
|
||||
|
||||
# Generate cURL command before making the request
|
||||
curl_command = self._generate_curl_command(request_data, merged_headers)
|
||||
|
||||
request_body_for_log = None
|
||||
if json_payload is not None:
|
||||
request_body_for_log = json_payload
|
||||
elif request_data.data is not None:
|
||||
request_body_for_log = request_data.data # Store as is, might be dict or str
|
||||
|
||||
try:
|
||||
# 如果提供了 body,使用它作为 json 参数
|
||||
json_payload = request_data.json_data
|
||||
|
||||
response = requests.request(
|
||||
method=request_data.method.upper(),
|
||||
url=str(request_data.url),
|
||||
@@ -63,47 +138,112 @@ class APICaller:
|
||||
json=json_payload,
|
||||
data=request_data.data,
|
||||
timeout=timeout,
|
||||
verify=False
|
||||
verify=False # As per original code
|
||||
)
|
||||
|
||||
# 不立即引发异常,而是捕获状态码
|
||||
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 = response.status_code
|
||||
response_headers_dict = dict(response.headers)
|
||||
response_content_bytes = response.content
|
||||
response_elapsed_time = response.elapsed.total_seconds()
|
||||
|
||||
parsed_json_content = None
|
||||
response_body_for_log: Any = response_content_bytes.decode('utf-8', errors='replace') # Default to decoded string
|
||||
|
||||
try:
|
||||
if response_headers_dict.get('Content-Type', '').startswith('application/json'):
|
||||
parsed_json_content = response.json()
|
||||
response_body_for_log = parsed_json_content # If JSON, log the parsed JSON
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
pass # Keep response_body_for_log as decoded string
|
||||
|
||||
api_response = APIResponse(
|
||||
status_code=status_code,
|
||||
headers=dict(response.headers),
|
||||
content=response.content,
|
||||
json_content=json_content,
|
||||
elapsed_time=response.elapsed.total_seconds()
|
||||
headers=response_headers_dict,
|
||||
content=response_content_bytes,
|
||||
json_content=parsed_json_content,
|
||||
elapsed_time=response_elapsed_time
|
||||
)
|
||||
|
||||
api_call_detail = APICallDetail(
|
||||
request_method=request_data.method.upper(),
|
||||
request_url=str(request_data.url),
|
||||
request_headers=merged_headers,
|
||||
request_params=request_data.params,
|
||||
request_body=request_body_for_log,
|
||||
curl_command=curl_command,
|
||||
response_status_code=status_code,
|
||||
response_headers=response_headers_dict,
|
||||
response_body=response_body_for_log,
|
||||
response_elapsed_time=response_elapsed_time
|
||||
)
|
||||
return api_response, api_call_detail
|
||||
|
||||
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()
|
||||
# For HTTPError, response object should exist
|
||||
status_code = e.response.status_code if e.response else 500
|
||||
response_headers_dict = dict(e.response.headers) if e.response else {}
|
||||
response_content_bytes = e.response.content if e.response else str(e).encode()
|
||||
response_elapsed_time = e.response.elapsed.total_seconds() if e.response and hasattr(e.response, 'elapsed') else 0
|
||||
|
||||
response_body_for_log = response_content_bytes.decode('utf-8', errors='replace')
|
||||
# Try to parse JSON from error response if possible, for logging
|
||||
parsed_json_content_error = None
|
||||
if response_headers_dict.get('Content-Type', '').startswith('application/json'):
|
||||
try:
|
||||
parsed_json_content_error = json.loads(response_body_for_log) # use json.loads for string
|
||||
response_body_for_log = parsed_json_content_error
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
|
||||
api_response_err = APIResponse(
|
||||
status_code=status_code,
|
||||
headers=response_headers_dict,
|
||||
content=response_content_bytes,
|
||||
json_content=parsed_json_content_error, # Log parsed JSON if available
|
||||
elapsed_time=response_elapsed_time
|
||||
)
|
||||
api_call_detail_err = APICallDetail(
|
||||
request_method=request_data.method.upper(),
|
||||
request_url=str(request_data.url),
|
||||
request_headers=merged_headers,
|
||||
request_params=request_data.params,
|
||||
request_body=request_body_for_log, # request body for log
|
||||
curl_command=curl_command,
|
||||
response_status_code=status_code,
|
||||
response_headers=response_headers_dict,
|
||||
response_body=response_body_for_log, # Log decoded string or parsed JSON
|
||||
response_elapsed_time=response_elapsed_time
|
||||
)
|
||||
return api_response_err, api_call_detail_err
|
||||
|
||||
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(),
|
||||
# For other RequestExceptions, e.response might not exist or be partial
|
||||
status_code = getattr(e.response, 'status_code', 503) # 503 Service Unavailable seems fitting
|
||||
response_headers_dict = dict(getattr(e.response, 'headers', {}))
|
||||
response_content_str = str(e)
|
||||
response_content_bytes = response_content_str.encode()
|
||||
|
||||
api_response_exc = APIResponse(
|
||||
status_code=status_code,
|
||||
headers=response_headers_dict,
|
||||
content=response_content_bytes,
|
||||
json_content=None,
|
||||
elapsed_time=0
|
||||
)
|
||||
api_call_detail_exc = APICallDetail(
|
||||
request_method=request_data.method.upper(),
|
||||
request_url=str(request_data.url),
|
||||
request_headers=merged_headers,
|
||||
request_params=request_data.params,
|
||||
request_body=request_body_for_log, # request body for log
|
||||
curl_command=curl_command,
|
||||
response_status_code=status_code,
|
||||
response_headers=response_headers_dict,
|
||||
response_body=response_content_str, # Log the error string
|
||||
response_elapsed_time=0
|
||||
)
|
||||
return api_response_exc, api_call_detail_exc
|
||||
|
||||
# Example Usage (can be moved to tests or main application logic)
|
||||
if __name__ == '__main__':
|
||||
@@ -115,13 +255,15 @@ if __name__ == '__main__':
|
||||
url=HttpUrl("https://jsonplaceholder.typicode.com/todos/1"),
|
||||
headers={"X-Request-ID": "12345"}
|
||||
)
|
||||
response = caller.call_api(get_req_data)
|
||||
response, detail = caller.call_api(get_req_data) # Unpack two values now
|
||||
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("GET Call Detail:")
|
||||
print(detail.model_dump_json(indent=2)) # Use model_dump_json for pretty print
|
||||
|
||||
print("\n")
|
||||
|
||||
@@ -132,13 +274,15 @@ if __name__ == '__main__':
|
||||
json_data={"title": "foo", "body": "bar", "userId": 1},
|
||||
headers={"Content-Type": "application/json; charset=UTF-8"}
|
||||
)
|
||||
response = caller.call_api(post_req_data)
|
||||
response, detail = 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")
|
||||
print("POST Call Detail (json_data):")
|
||||
print(detail.model_dump_json(indent=2))
|
||||
|
||||
# Example POST request with body (alias for json_data)
|
||||
post_req_data_with_body = APIRequest(
|
||||
@@ -147,13 +291,15 @@ if __name__ == '__main__':
|
||||
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)
|
||||
response, detail = 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")
|
||||
print("POST Call Detail (body):")
|
||||
print(detail.model_dump_json(indent=2))
|
||||
|
||||
# Example list body request (demonstrating array body support)
|
||||
array_req_data = APIRequest(
|
||||
@@ -162,20 +308,24 @@ if __name__ == '__main__':
|
||||
body=["test_string", "another_value"],
|
||||
headers={"Content-Type": "application/json; charset=UTF-8"}
|
||||
)
|
||||
response = caller.call_api(array_req_data)
|
||||
response, detail = 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")
|
||||
print("POST Call Detail (array body):")
|
||||
print(detail.model_dump_json(indent=2))
|
||||
|
||||
# Example Error request (non-existent domain)
|
||||
error_req_data = APIRequest(
|
||||
method="GET",
|
||||
url=HttpUrl("https://nonexistentdomain.invalid"),
|
||||
)
|
||||
response = caller.call_api(error_req_data)
|
||||
response, detail = 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")
|
||||
print(f"Time taken: {response.elapsed_time:.4f}s")
|
||||
print("Error GET Call Detail:")
|
||||
print(detail.model_dump_json(indent=2))
|
||||
@@ -7,35 +7,34 @@
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
import re # 添加 re 模块导入
|
||||
import os # Added os for path operations
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Union, Tuple, Type, ForwardRef
|
||||
from enum import Enum
|
||||
import datetime
|
||||
import datetime as dt
|
||||
from uuid import UUID
|
||||
from dataclasses import asdict as dataclass_asdict, is_dataclass # New import
|
||||
from dataclasses import asdict as dataclass_asdict, is_dataclass
|
||||
import copy
|
||||
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
from pydantic import BaseModel, Field, create_model, HttpUrl # Added HttpUrl for Literal type hint if needed
|
||||
from pydantic.networks import EmailStr
|
||||
from pydantic.types import Literal # Explicitly import Literal
|
||||
|
||||
from .input_parser.parser import InputParser, YAPIEndpoint, SwaggerEndpoint, ParsedYAPISpec, ParsedSwaggerSpec
|
||||
from .api_caller.caller import APICaller, APIRequest, APIResponse
|
||||
from .api_caller.caller import APICaller, APIRequest, APIResponse, APICallDetail # Ensure APICallDetail is imported
|
||||
from .json_schema_validator.validator import JSONSchemaValidator
|
||||
from .test_framework_core import ValidationResult, TestSeverity, APIRequestContext, APIResponseContext, BaseAPITestCase
|
||||
from .test_case_registry import TestCaseRegistry
|
||||
# 尝试导入 utils.schema_utils
|
||||
from .utils import schema_utils
|
||||
from .utils.common_utils import format_url_with_path_params # 新增导入
|
||||
from .utils.common_utils import format_url_with_path_params
|
||||
|
||||
# 尝试导入 LLMService,如果失败则允许,因为 LLM 功能是可选的
|
||||
try:
|
||||
from .llm_utils.llm_service import LLMService
|
||||
except ImportError:
|
||||
LLMService = None
|
||||
logging.getLogger(__name__).info("LLMService 未找到,LLM 相关功能将不可用。")
|
||||
|
||||
# Cache for dynamically created Pydantic models to avoid redefinition issues
|
||||
_dynamic_model_cache: Dict[str, Type[BaseModel]] = {}
|
||||
|
||||
class ExecutedTestCaseResult:
|
||||
@@ -321,8 +320,9 @@ class TestSummary:
|
||||
print(f" - 验证点: {vp.message}")
|
||||
|
||||
class APITestOrchestrator:
|
||||
"""API测试编排器"""
|
||||
|
||||
"""
|
||||
测试编排器,负责加载API定义、发现和执行测试用例、生成报告等。
|
||||
"""
|
||||
def __init__(self, base_url: str,
|
||||
custom_test_cases_dir: Optional[str] = None,
|
||||
llm_api_key: Optional[str] = None,
|
||||
@@ -331,77 +331,53 @@ class APITestOrchestrator:
|
||||
use_llm_for_request_body: bool = False,
|
||||
use_llm_for_path_params: bool = False,
|
||||
use_llm_for_query_params: bool = False,
|
||||
use_llm_for_headers: bool = False
|
||||
use_llm_for_headers: bool = False,
|
||||
output_dir: Optional[str] = None # output_dir is now optional and not used for saving API call details internally
|
||||
):
|
||||
"""
|
||||
初始化API测试编排器
|
||||
|
||||
Args:
|
||||
base_url: API基础URL
|
||||
custom_test_cases_dir: 存放自定义 APITestCase 的目录路径。如果为 None,则不加载自定义测试用例。
|
||||
llm_api_key: 大模型服务的API Key。
|
||||
llm_base_url: 大模型服务的兼容OpenAI的基础URL。
|
||||
llm_model_name: 要使用的具体模型名称。
|
||||
use_llm_for_request_body: 是否全局启用LLM生成请求体。
|
||||
use_llm_for_path_params: 是否全局启用LLM生成路径参数。
|
||||
use_llm_for_query_params: 是否全局启用LLM生成查询参数。
|
||||
use_llm_for_headers: 是否全局启用LLM生成头部参数。
|
||||
"""
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 初始化组件
|
||||
self.parser = InputParser()
|
||||
self.api_caller = APICaller()
|
||||
self.validator = JSONSchemaValidator() # JSON Schema 验证器,可能会被测试用例内部使用
|
||||
|
||||
self.test_case_registry: Optional[TestCaseRegistry] = None
|
||||
if custom_test_cases_dir:
|
||||
self.logger.info(f"初始化 TestCaseRegistry,扫描目录: {custom_test_cases_dir}")
|
||||
self.schema_validator = JSONSchemaValidator()
|
||||
self.test_case_registry = TestCaseRegistry(custom_test_cases_dir)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
# self.output_dir is kept if other parts of the orchestrator might use it,
|
||||
# but it's no longer used by the removed _save_api_call_details
|
||||
self.output_dir_param = output_dir
|
||||
|
||||
self.api_call_details_log: List[APICallDetail] = []
|
||||
|
||||
# LLM Service Initialization
|
||||
self.llm_service: Optional[LLMService] = None
|
||||
if LLMService and llm_api_key:
|
||||
try:
|
||||
self.test_case_registry = TestCaseRegistry(test_cases_dir=custom_test_cases_dir)
|
||||
self.logger.info(f"TestCaseRegistry 初始化完成,发现 {len(self.test_case_registry.get_all_test_case_classes())} 个测试用例类。")
|
||||
self.llm_service = LLMService(api_key=llm_api_key, base_url=llm_base_url, model_name=llm_model_name)
|
||||
self.logger.info(f"LLMService initialized successfully with model: {llm_model_name or 'default'}.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"初始化 TestCaseRegistry 失败: {e}", exc_info=True)
|
||||
else:
|
||||
self.logger.info("未提供 custom_test_cases_dir,不加载自定义 APITestCase。")
|
||||
self.logger.error(f"LLMService initialization failed: {e}. LLM features will be disabled.", exc_info=True)
|
||||
self.llm_service = None
|
||||
elif LLMService and not llm_api_key:
|
||||
self.logger.info("LLMService is available, but LLM API key was not provided. LLM features will be disabled.")
|
||||
|
||||
# LLM 全局配置开关
|
||||
self.use_llm_for_request_body = use_llm_for_request_body
|
||||
self.use_llm_for_path_params = use_llm_for_path_params
|
||||
self.use_llm_for_query_params = use_llm_for_query_params
|
||||
self.use_llm_for_headers = use_llm_for_headers
|
||||
|
||||
self.llm_service: Optional[LLMService] = None
|
||||
if LLMService is None:
|
||||
self.logger.warning("LLMService 类未能导入,LLM 相关功能将完全禁用。")
|
||||
# 强制所有LLM使用为False,并确保服务实例为None
|
||||
self.llm_endpoint_params_cache: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
if (self.use_llm_for_request_body or \
|
||||
self.use_llm_for_path_params or \
|
||||
self.use_llm_for_query_params or \
|
||||
self.use_llm_for_headers) and not self.llm_service:
|
||||
self.logger.warning("LLM-based data generation was enabled, but LLMService is not available or failed to initialize. Disabling all LLM features.")
|
||||
self.use_llm_for_request_body = False
|
||||
self.use_llm_for_path_params = False
|
||||
self.use_llm_for_query_params = False
|
||||
self.use_llm_for_headers = False
|
||||
elif llm_api_key and llm_base_url and llm_model_name: # 直接检查配置是否完整
|
||||
try:
|
||||
self.llm_service = LLMService(
|
||||
api_key=llm_api_key,
|
||||
base_url=llm_base_url,
|
||||
model_name=llm_model_name
|
||||
)
|
||||
self.logger.info(f"LLMService 已成功初始化,模型: {llm_model_name}。")
|
||||
except ValueError as ve:
|
||||
self.logger.error(f"LLMService 初始化失败 (参数错误): {ve}。LLM相关功能将不可用。")
|
||||
self.llm_service = None # 确保初始化失败时服务为None
|
||||
except Exception as e:
|
||||
self.logger.error(f"LLMService 初始化时发生未知错误: {e}。LLM相关功能将不可用。", exc_info=True)
|
||||
self.llm_service = None # 确保初始化失败时服务为None
|
||||
else:
|
||||
# 如果LLMService类存在,但配置不完整
|
||||
if LLMService:
|
||||
self.logger.warning("LLMService 类已找到,但未提供完整的LLM配置 (api_key, base_url, model_name)。LLM相关功能将不可用。")
|
||||
# self.llm_service 默认就是 None,无需额外操作
|
||||
|
||||
# 新增:端点级别的LLM生成参数缓存
|
||||
self.llm_endpoint_params_cache: Dict[str, Dict[str, Any]] = {}
|
||||
def get_api_call_details(self) -> List[APICallDetail]:
|
||||
"""Returns the collected list of API call details."""
|
||||
return self.api_call_details_log
|
||||
|
||||
def _should_use_llm_for_param_type(
|
||||
self,
|
||||
@@ -868,7 +844,7 @@ class APITestOrchestrator:
|
||||
test_case_instance = test_case_class(
|
||||
endpoint_spec=endpoint_spec_dict,
|
||||
global_api_spec=global_spec_dict,
|
||||
json_schema_validator=self.validator,
|
||||
json_schema_validator=self.schema_validator,
|
||||
llm_service=self.llm_service # Pass the orchestrator's LLM service instance
|
||||
)
|
||||
self.logger.info(f"开始执行测试用例 '{test_case_instance.id}' ({test_case_instance.name}) for endpoint '{endpoint_spec_dict.get('method', 'N/A')} {endpoint_spec_dict.get('path', 'N/A')}'")
|
||||
@@ -966,32 +942,34 @@ class APITestOrchestrator:
|
||||
)
|
||||
|
||||
response_call_start_time = time.time()
|
||||
api_response_obj = self.api_caller.call_api(api_request_obj)
|
||||
# api_response_obj = self.api_caller.call_api(api_request_obj)
|
||||
api_response, api_call_detail = self.api_caller.call_api(api_request_obj)
|
||||
self.api_call_details_log.append(api_call_detail) # 记录日志
|
||||
|
||||
response_call_elapsed_time = time.time() - response_call_start_time
|
||||
|
||||
actual_text_content: Optional[str] = None
|
||||
if hasattr(api_response_obj, 'text_content') and api_response_obj.text_content is not None:
|
||||
actual_text_content = api_response_obj.text_content
|
||||
elif api_response_obj.json_content is not None:
|
||||
if isinstance(api_response_obj.json_content, str): # Should not happen if json_content is parsed
|
||||
actual_text_content = api_response_obj.json_content
|
||||
# 使用解包后的 api_response:
|
||||
if hasattr(api_response, 'text_content') and api_response.text_content is not None:
|
||||
actual_text_content = api_response.text_content
|
||||
elif api_response.json_content is not None: # <--- 使用 api_response
|
||||
if isinstance(api_response.json_content, str):
|
||||
actual_text_content = api_response.json_content
|
||||
else:
|
||||
try:
|
||||
actual_text_content = json.dumps(api_response_obj.json_content, ensure_ascii=False)
|
||||
except TypeError: # If json_content is not serializable (e.g. bytes)
|
||||
actual_text_content = str(api_response_obj.json_content)
|
||||
|
||||
actual_text_content = json.dumps(api_response.json_content, ensure_ascii=False)
|
||||
except TypeError:
|
||||
actual_text_content = str(api_response.json_content)
|
||||
|
||||
api_response_context = APIResponseContext(
|
||||
status_code=api_response_obj.status_code,
|
||||
headers=api_response_obj.headers,
|
||||
json_content=api_response_obj.json_content,
|
||||
status_code=api_response.status_code, # <--- 使用 api_response
|
||||
headers=api_response.headers, # <--- 使用 api_response
|
||||
json_content=api_response.json_content, # <--- 使用 api_response
|
||||
text_content=actual_text_content,
|
||||
elapsed_time=response_call_elapsed_time,
|
||||
original_response= getattr(api_response_obj, 'raw_response', None), # Pass raw if available
|
||||
original_response= getattr(api_response, 'raw_response', None), # <--- 使用 api_response
|
||||
request_context=api_request_context
|
||||
)
|
||||
|
||||
validation_results.extend(test_case_instance.validate_response(api_response_context, api_request_context))
|
||||
validation_results.extend(test_case_instance.check_performance(api_response_context, api_request_context))
|
||||
|
||||
@@ -1424,12 +1402,15 @@ class APITestOrchestrator:
|
||||
self.logger.error(f"从 run_tests_from_yapi 重新初始化 TestCaseRegistry 失败: {e}", exc_info=True)
|
||||
|
||||
self.logger.info(f"从YAPI文件加载API定义: {yapi_file_path}")
|
||||
parsed_yapi = self.parser.parse_yapi_spec(yapi_file_path)
|
||||
self.api_call_details_log = [] # Reset for new run
|
||||
|
||||
parsed_yapi = self.parser.parse_yapi_spec(yapi_file_path) # Corrected: self.parser
|
||||
summary = TestSummary()
|
||||
|
||||
if not parsed_yapi:
|
||||
self.logger.error(f"解析YAPI文件失败: {yapi_file_path}")
|
||||
summary.finalize_summary()
|
||||
# No longer calls _save_api_call_details here
|
||||
return summary
|
||||
|
||||
endpoints_to_test = parsed_yapi.endpoints
|
||||
@@ -1453,6 +1434,8 @@ class APITestOrchestrator:
|
||||
summary.add_endpoint_result(result)
|
||||
|
||||
summary.finalize_summary()
|
||||
# No longer calls _save_api_call_details here
|
||||
summary.print_summary_to_console() # Keep console print
|
||||
return summary
|
||||
|
||||
def run_tests_from_swagger(self, swagger_file_path: str,
|
||||
@@ -1468,12 +1451,15 @@ class APITestOrchestrator:
|
||||
self.logger.error(f"从 run_tests_from_swagger 重新初始化 TestCaseRegistry 失败: {e}", exc_info=True)
|
||||
|
||||
self.logger.info(f"从Swagger文件加载API定义: {swagger_file_path}")
|
||||
parsed_swagger = self.parser.parse_swagger_spec(swagger_file_path)
|
||||
self.api_call_details_log = [] # Reset for new run
|
||||
|
||||
parsed_swagger = self.parser.parse_swagger_spec(swagger_file_path) # Corrected: self.parser
|
||||
summary = TestSummary()
|
||||
|
||||
if not parsed_swagger:
|
||||
self.logger.error(f"解析Swagger文件失败: {swagger_file_path}")
|
||||
summary.finalize_summary()
|
||||
# No longer calls _save_api_call_details here
|
||||
return summary
|
||||
|
||||
endpoints_to_test = parsed_swagger.endpoints
|
||||
@@ -1481,7 +1467,7 @@ class APITestOrchestrator:
|
||||
endpoints_to_test = [ep for ep in endpoints_to_test if any(tag in ep.tags for tag in tags)]
|
||||
|
||||
summary.set_total_endpoints_defined(len(endpoints_to_test))
|
||||
|
||||
|
||||
total_applicable_tcs = 0
|
||||
if self.test_case_registry:
|
||||
for endpoint_spec in endpoints_to_test:
|
||||
@@ -1497,6 +1483,8 @@ class APITestOrchestrator:
|
||||
summary.add_endpoint_result(result)
|
||||
|
||||
summary.finalize_summary()
|
||||
# No longer calls _save_api_call_details here
|
||||
summary.print_summary_to_console() # Keep console print
|
||||
return summary
|
||||
|
||||
def _generate_data_from_schema(self, schema: Dict[str, Any],
|
||||
|
||||
Reference in New Issue
Block a user