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
+38 -29
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: 状态保持为初始的 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 # 可选: 输出处理后的上下文摘要
}
+3 -2
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