mvp:stage

This commit is contained in:
gongwenxin 2025-06-05 15:31:22 +08:00
parent 7333cc8a2a
commit cf0df24530
7 changed files with 5052 additions and 4798 deletions

View File

@ -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: 状态保持为初始的 SKIPPEDmessage也应该在之前设置了
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 # 可选: 输出处理后的上下文摘要
}

View File

@ -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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff