mvp:stage
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -5,6 +5,7 @@ import logging
|
||||
import time
|
||||
from typing import List, Dict, Any, Callable, Optional, Union
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
|
||||
# Add Pydantic BaseModel for APIOperationSpec
|
||||
from pydantic import BaseModel
|
||||
@@ -336,20 +337,27 @@ class ExecutedStageStepResult:
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
vps_details = []
|
||||
if self.validation_points:
|
||||
for vp in self.validation_points:
|
||||
if vp.details and isinstance(vp.details, dict):
|
||||
# 尝试序列化 details,如果包含复杂对象
|
||||
if self.validation_points: # self.validation_points is List[Dict[str, Any]]
|
||||
for vp_dict in self.validation_points: # vp_dict is a Dict from ValidationResult.to_dict()
|
||||
# Access dictionary keys instead of object attributes
|
||||
vp_details_content = vp_dict.get('details')
|
||||
vp_passed = vp_dict.get('passed', False)
|
||||
vp_message = vp_dict.get('message', '')
|
||||
|
||||
processed_detail = {"passed": vp_passed, "message": vp_message}
|
||||
|
||||
if vp_details_content and isinstance(vp_details_content, dict):
|
||||
try:
|
||||
# 只取部分关键信息或确保可序列化
|
||||
serializable_details = {"passed": vp.passed, "message": vp.message}
|
||||
if "status_code" in vp.details: serializable_details["status_code"] = vp.details["status_code"]
|
||||
#只取部分关键信息或确保可序列化
|
||||
if "status_code" in vp_details_content:
|
||||
processed_detail["status_code"] = vp_details_content["status_code"]
|
||||
# 不直接序列化整个 response body 以免过大
|
||||
vps_details.append(serializable_details)
|
||||
# You might want to add other relevant serializable fields from vp_details_content
|
||||
except TypeError:
|
||||
vps_details.append({"passed": vp.passed, "message": f"{vp.message} (Details not serializable)"})
|
||||
else:
|
||||
vps_details.append({"passed": vp.passed, "message": vp.message})
|
||||
# Update message if details were not serializable, though this part might be less reachable now
|
||||
processed_detail["message"] = f"{vp_message} (Details not fully serializable)"
|
||||
|
||||
vps_details.append(processed_detail)
|
||||
|
||||
|
||||
return {
|
||||
@@ -358,7 +366,7 @@ class ExecutedStageStepResult:
|
||||
"lookup_key": self.lookup_key if isinstance(self.lookup_key, str) else str(self.lookup_key), # <--- 添加到输出 (确保字符串化)
|
||||
"resolved_endpoint": self.resolved_endpoint, # <--- 添加到输出
|
||||
"status": self.status.value,
|
||||
"message": self.message or "; ".join([vp.message for vp in self.validation_points if not vp.passed]),
|
||||
"message": self.message or "; ".join([vp_dict.get('message', '') for vp_dict in self.validation_points if not vp_dict.get('passed')]),
|
||||
"duration_seconds": f"{self.duration:.4f}",
|
||||
"timestamp": time.strftime('%Y-%m-%dT%H:%M:%S%z', time.localtime(self.timestamp)),
|
||||
"validation_points": vps_details,
|
||||
@@ -386,23 +394,24 @@ class ExecutedStageResult:
|
||||
description: Optional[str] = None): # <--- 添加 description 参数
|
||||
self.stage_id = stage_id
|
||||
self.stage_name = stage_name
|
||||
self.description = description # <--- 存储 description
|
||||
self.api_group_metadata = api_group_metadata or {}
|
||||
self.overall_status: ExecutedStageResult.Status = ExecutedStageResult.Status.PENDING # 默认为 PENDING
|
||||
self.executed_steps: List[ExecutedStageStepResult] = []
|
||||
self.start_time: float = time.time()
|
||||
self.end_time: Optional[float] = None
|
||||
self.description = description
|
||||
self.api_group_metadata = api_group_metadata
|
||||
self.overall_status: ExecutedStageResult.Status = ExecutedStageResult.Status.PENDING
|
||||
self.start_time: datetime = datetime.now()
|
||||
self.end_time: Optional[datetime] = None
|
||||
self.duration: float = 0.0
|
||||
self.message: str = "" # 整个阶段的总结性消息,例如跳过原因或关键失败点
|
||||
self.final_stage_context: Optional[Dict[str, Any]] = None # 最终的 stage_context 内容 (敏感数据需谨慎处理)
|
||||
self.message: Optional[str] = None
|
||||
self.executed_steps: List[ExecutedStageStepResult] = [] # 确保初始化为空列表
|
||||
self.final_context: Optional[Dict[str, Any]] = None
|
||||
# executed_steps_count 应该是一个属性,或者在 to_dict 中计算
|
||||
|
||||
def add_step_result(self, step_result: ExecutedStageStepResult):
|
||||
self.executed_steps.append(step_result)
|
||||
|
||||
def finalize_stage_result(self, final_context: Optional[Dict[str, Any]] = None):
|
||||
self.end_time = time.time()
|
||||
self.duration = self.end_time - self.start_time
|
||||
self.final_stage_context = final_context
|
||||
self.end_time = datetime.now()
|
||||
self.duration = (self.end_time - self.start_time).total_seconds()
|
||||
self.final_context = final_context
|
||||
|
||||
if not self.executed_steps and self.overall_status == ExecutedStageResult.Status.SKIPPED:
|
||||
# 如果没有执行任何步骤且状态是初始的 SKIPPED,则保持
|
||||
@@ -427,10 +436,10 @@ class ExecutedStageResult:
|
||||
# else: 状态保持为初始的 SKIPPED,message也应该在之前设置了
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
# 对 final_stage_context 进行处理,避免过大或敏感信息直接输出
|
||||
# 对 final_context 进行处理,避免过大或敏感信息直接输出
|
||||
processed_context = {}
|
||||
if self.final_stage_context:
|
||||
for k, v in self.final_stage_context.items():
|
||||
if self.final_context:
|
||||
for k, v in self.final_context.items():
|
||||
if isinstance(v, (str, bytes)) and len(v) > 200: # 截断长字符串
|
||||
processed_context[k] = str(v)[:200] + '...'
|
||||
elif isinstance(v, (dict, list)): # 对于字典和列表,只显示键或少量元素
|
||||
@@ -445,10 +454,10 @@ class ExecutedStageResult:
|
||||
"api_group_name": self.api_group_metadata.get("name", "N/A"),
|
||||
"overall_status": self.overall_status.value,
|
||||
"duration_seconds": f"{self.duration:.2f}",
|
||||
"start_time": time.strftime('%Y-%m-%dT%H:%M:%S%z', time.localtime(self.start_time)),
|
||||
"end_time": time.strftime('%Y-%m-%dT%H:%M:%S%z', time.localtime(self.end_time)) if self.end_time else None,
|
||||
"start_time": self.start_time.strftime('%Y-%m-%dT%H:%M:%S%z'),
|
||||
"end_time": self.end_time.strftime('%Y-%m-%dT%H:%M:%S%z') if self.end_time else None,
|
||||
"message": self.message,
|
||||
"executed_steps_count": len(self.executed_steps),
|
||||
"executed_steps": [step.to_dict() for step in self.executed_steps],
|
||||
# "final_stage_context_summary": processed_context # 可选: 输出处理后的上下文摘要
|
||||
"final_stage_context_summary": processed_context # 可选: 输出处理后的上下文摘要
|
||||
}
|
||||
@@ -2187,11 +2187,11 @@ class APITestOrchestrator:
|
||||
headers=final_headers,
|
||||
body=final_body # APIRequest 会通过 model_post_init 将 body 赋给 json_data
|
||||
)
|
||||
current_step_result.request_details = api_request.model_dump() # Use model_dump for Pydantic v2
|
||||
current_step_result.request_details = api_request.model_dump(mode='json') # Use model_dump for Pydantic v2
|
||||
|
||||
self.logger.info(f"Stage '{stage_instance.id}', Step '{step_name}': Executing API call {api_request.method} {api_request.url}") # Log the full URL
|
||||
api_response, api_call_detail = self.api_caller.call_api(api_request) # APICaller.call_api expects APIRequest
|
||||
current_step_result.api_call_details = api_call_detail.model_dump() # Use model_dump for Pydantic v2
|
||||
current_step_result.api_call_details = api_call_detail.model_dump(mode='json') # Use model_dump for Pydantic v2
|
||||
|
||||
self.logger.debug(f"Stage '{stage_instance.id}', Step '{step_name}': Validating response. Status: {api_response.status_code}")
|
||||
|
||||
@@ -2343,6 +2343,7 @@ class APITestOrchestrator:
|
||||
elif stage_result.message: stage_result.message += f" | after_stage hook failed: {e_asg}"
|
||||
else: stage_result.message = f"after_stage hook failed: {e_asg}"
|
||||
|
||||
stage_result.executed_steps = executed_steps_results # <--- 将局部步骤结果列表赋值给最终结果对象
|
||||
stage_result.finalize_stage_result(final_context=stage_context)
|
||||
self.logger.info(f"Stage '{stage_instance.id}' execution finished. API Group: '{api_group_name}', Final Status: {stage_result.overall_status.value}, Duration: {stage_result.duration:.2f}s") # Corrected duration_seconds to duration
|
||||
return stage_result
|
||||
|
||||
+2513
-2511
File diff suppressed because it is too large
Load Diff
+2038
-2066
File diff suppressed because it is too large
Load Diff
+460
-190
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user