fix:stage

This commit is contained in:
gongwenxin 2025-08-08 00:43:22 +08:00
parent 7ddb530353
commit 8df41527d6
4 changed files with 270 additions and 35 deletions

View File

@ -173,7 +173,7 @@ class DmsCrudScenarioStage(BaseAPIStage):
name = "DMS Full CRUD Scenario" name = "DMS Full CRUD Scenario"
description = "Performs a full Create -> Read -> Update -> Read -> Delete -> List workflow for a single DMS business object." description = "Performs a full Create -> Read -> Update -> Read -> Delete -> List workflow for a single DMS business object."
tags = ["dms", "crud", "scenario"] tags = ["dms", "crud", "scenario"]
continue_on_failure = False continue_on_failure = True # 🔧 修改为True让Stage在失败时继续执行其他场景
# DMS Stage专用配置自动启用LLM智能数据生成 # DMS Stage专用配置自动启用LLM智能数据生成
enable_llm_data_generation = True # 如果LLM服务可用自动使用LLM生成测试数据 enable_llm_data_generation = True # 如果LLM服务可用自动使用LLM生成测试数据
@ -183,20 +183,24 @@ class DmsCrudScenarioStage(BaseAPIStage):
# scenarios will be populated by is_applicable_to_api_group # scenarios will be populated by is_applicable_to_api_group
self.scenarios: List[Dict[str, Endpoint]] = [] self.scenarios: List[Dict[str, Endpoint]] = []
self.current_scenario_index = -1 self.current_scenario_index = -1
# 新增:指定要处理的场景索引(用于多实例模式)
self.target_scenario_index: Optional[int] = kwargs.get('target_scenario_index', None)
def is_applicable_to_api_group(self, api_group_name: Optional[str], global_api_spec: ParsedAPISpec) -> bool: @staticmethod
def discover_crud_scenarios(parsed_spec: ParsedAPISpec) -> List[Dict[str, Any]]:
""" """
Checks if the group of APIs contains at least one full CRUD set for a DMS object. 静态方法发现所有完整的DMS CRUD场景
A full set consists of create, read, update, delete, and list operations. 返回场景信息列表每个场景包含resource_name和endpoints
""" """
endpoints_in_group = self.apis_in_group if not isinstance(parsed_spec, ParsedAPISpec):
return []
# We only care about DMS endpoints for this stage # 只处理DMS端点
dms_endpoints = [ep for ep in endpoints_in_group if isinstance(ep, DMSEndpoint)] dms_endpoints = [ep for ep in parsed_spec.endpoints if isinstance(ep, DMSEndpoint)]
if not dms_endpoints: if not dms_endpoints:
return False return []
# Group endpoints by base resource name from operation_id # 按资源名称分组端点
grouped_ops = defaultdict(dict) grouped_ops = defaultdict(dict)
for ep in dms_endpoints: for ep in dms_endpoints:
if not ep.operation_id: if not ep.operation_id:
@ -209,28 +213,76 @@ class DmsCrudScenarioStage(BaseAPIStage):
op_type, resource_name = parts op_type, resource_name = parts
grouped_ops[resource_name][op_type] = ep grouped_ops[resource_name][op_type] = ep
# Find complete scenarios # 找到完整的CRUD场景
required_ops = {'create', 'read', 'update', 'delete', 'list'} required_ops = {'create', 'read', 'update', 'delete', 'list'}
scenarios = []
for resource_name, ops in grouped_ops.items(): for resource_name, ops in grouped_ops.items():
if required_ops.issubset(ops.keys()): if required_ops.issubset(ops.keys()):
self.scenarios.append(ops) scenarios.append({
self.logger.info(f"Found complete CRUD scenario for DMS resource: '{resource_name}'") 'resource_name': resource_name,
'endpoints': ops,
'virtual_group_name': f"dms_crud_{resource_name}"
})
return scenarios
def is_applicable_to_api_group(self, api_group_name: Optional[str], global_api_spec: ParsedAPISpec) -> bool:
"""
检查此Stage是否适用于给定的API分组
支持两种模式
1. 传统模式处理所有发现的CRUD场景
2. 单场景模式只处理指定索引的场景通过virtual_group_name匹配
"""
# 使用静态方法发现所有场景
all_scenarios = self.discover_crud_scenarios(global_api_spec)
if not all_scenarios:
return False
# 如果指定了虚拟分组名称,只处理匹配的场景
if api_group_name and api_group_name.startswith('dms_crud_'):
# 单场景模式:只处理匹配的场景
target_scenario = None
for scenario in all_scenarios:
if scenario['virtual_group_name'] == api_group_name:
target_scenario = scenario
break
if target_scenario:
self.scenarios = [target_scenario['endpoints']]
self.logger.info(f"DMS Stage (单场景模式) 匹配到场景: {target_scenario['resource_name']}")
return True
else:
self.logger.info(f"DMS Stage (单场景模式) 未找到匹配的场景: {api_group_name}")
return False
else:
# 传统模式:处理所有场景(向后兼容)
self.scenarios = [scenario['endpoints'] for scenario in all_scenarios]
self.logger.info(f"DMS Stage (传统模式) 发现 {len(self.scenarios)} 个完整的CRUD场景")
return len(self.scenarios) > 0 return len(self.scenarios) > 0
def before_stage(self, stage_context: dict, global_api_spec: ParsedAPISpec, api_group_name: str | None): def before_stage(self, stage_context: dict, global_api_spec: ParsedAPISpec, api_group_name: str | None):
""" """
Set up the context for the next scenario to run. 为要运行的场景设置上下文
This will be called by the orchestrator. We will use it to prepare for a single scenario execution. 在单场景模式下只处理一个场景
""" """
self.current_scenario_index += 1 # 设置当前场景索引
if self.current_scenario_index >= len(self.scenarios): self.current_scenario_index = 0
# Should not happen if orchestrator works as expected (one stage instance per scenario) if len(self.scenarios) == 0:
# but as a safeguard. raise Exception("No CRUD scenarios found to run.")
raise Exception("No more scenarios to run.")
current_scenario = self.scenarios[self.current_scenario_index] current_scenario = self.scenarios[self.current_scenario_index]
self.logger.info(f"Setting up before_stage for scenario: {list(current_scenario.keys())}")
# 获取资源名称用于日志
resource_name = "unknown"
if current_scenario:
first_op = next(iter(current_scenario.values()))
if hasattr(first_op, 'operation_id') and first_op.operation_id:
parts = first_op.operation_id.split('_', 1)
if len(parts) == 2:
resource_name = parts[1]
self.logger.info(f"🎯 DMS Stage设置场景上下文: {resource_name} (分组: {api_group_name})")
self.logger.info(f"📋 场景包含操作: {list(current_scenario.keys())}")
# Get the 'create' endpoint to determine the primary key # Get the 'create' endpoint to determine the primary key
create_op: DMSEndpoint = current_scenario.get('create') create_op: DMSEndpoint = current_scenario.get('create')

View File

@ -2457,6 +2457,16 @@ class APITestOrchestrator:
if not api_groups_to_iterate: if not api_groups_to_iterate:
self.logger.info("Swagger规范: 未找到已定义的标签,或标签列表为空。将阶段应用于整个规范 (api_group_name=None).") self.logger.info("Swagger规范: 未找到已定义的标签,或标签列表为空。将阶段应用于整个规范 (api_group_name=None).")
api_groups_to_iterate.append(None) api_groups_to_iterate.append(None)
elif hasattr(parsed_spec, 'spec_type') and parsed_spec.spec_type == 'dms':
# 🔧 DMS特殊处理为每个CRUD场景创建虚拟分组
from custom_stages.dms_crud_scenario_stage import DmsCrudScenarioStage
dms_scenarios = DmsCrudScenarioStage.discover_crud_scenarios(parsed_spec)
if dms_scenarios:
api_groups_to_iterate.extend([scenario['virtual_group_name'] for scenario in dms_scenarios])
self.logger.info(f"DMS规范: 发现 {len(dms_scenarios)} 个CRUD场景创建对应的虚拟分组: {[s['virtual_group_name'] for s in dms_scenarios]}")
else:
self.logger.info("DMS规范: 未发现完整的CRUD场景将阶段应用于整个规范 (api_group_name=None).")
api_groups_to_iterate.append(None)
else: else:
self.logger.warning(f"未知的解析规范类型: {type(parsed_spec)}。将阶段应用于整个规范 (api_group_name=None).") self.logger.warning(f"未知的解析规范类型: {type(parsed_spec)}。将阶段应用于整个规范 (api_group_name=None).")
api_groups_to_iterate.append(None) api_groups_to_iterate.append(None)
@ -2535,6 +2545,16 @@ class APITestOrchestrator:
if isinstance(api, SwaggerEndpoint) and hasattr(api, 'tags') and isinstance(api.tags, list) and current_api_group_name in api.tags if isinstance(api, SwaggerEndpoint) and hasattr(api, 'tags') and isinstance(api.tags, list) and current_api_group_name in api.tags
] ]
self.logger.debug(f"For Swagger group '{current_api_group_name}', selected {len(apis_for_current_group_objects)} endpoint objects.") self.logger.debug(f"For Swagger group '{current_api_group_name}', selected {len(apis_for_current_group_objects)} endpoint objects.")
elif hasattr(parsed_spec, 'spec_type') and parsed_spec.spec_type == 'dms' and current_api_group_name and current_api_group_name.startswith('dms_crud_'):
# 🔧 DMS虚拟分组处理为特定的CRUD场景提供所有DMS端点
resource_name = current_api_group_name.replace('dms_crud_', '')
current_group_metadata = {
'name': current_api_group_name,
'description': f'DMS CRUD场景: {resource_name}'
}
# 为DMS虚拟分组提供所有DMS端点Stage会自己过滤
apis_for_current_group_objects = list(parsed_spec.endpoints)
self.logger.debug(f"For DMS virtual group '{current_api_group_name}', provided {len(apis_for_current_group_objects)} DMS endpoint objects.")
else: else:
self.logger.warning(f"Unknown spec type ({type(parsed_spec)}) for group '{current_api_group_name}'.") self.logger.warning(f"Unknown spec type ({type(parsed_spec)}) for group '{current_api_group_name}'.")
current_group_metadata = {"name": current_api_group_name, "description": f"未知规范类型 ({type(parsed_spec)}) 的分组"} current_group_metadata = {"name": current_api_group_name, "description": f"未知规范类型 ({type(parsed_spec)}) 的分组"}

View File

@ -86,8 +86,7 @@ def parse_args():
# 新增LLM 配置选项 # 新增LLM 配置选项
llm_group = parser.add_argument_group('LLM 配置选项 (可选)') llm_group = parser.add_argument_group('LLM 配置选项 (可选)')
llm_group.add_argument('--llm-api-key', llm_group.add_argument('--llm-api-key',
# default=os.environ.get("OPENAI_API_KEY"), # 尝试从环境变量获取 default=os.environ.get("OPENAI_API_KEY"), # 尝试从环境变量获取
default='sk-0213c70194624703a1d0d80e0f762b0e', # 尝试从环境变量获取
help='LLM服务的API密钥 (例如 OpenAI API Key)。默认从环境变量 OPENAI_API_KEY 读取。') help='LLM服务的API密钥 (例如 OpenAI API Key)。默认从环境变量 OPENAI_API_KEY 读取。')
llm_group.add_argument('--llm-base-url', llm_group.add_argument('--llm-base-url',
default="https://dashscope.aliyuncs.com/compatible-mode/v1", default="https://dashscope.aliyuncs.com/compatible-mode/v1",

164
test_dms_multi_stage.py Normal file
View File

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""
测试DMS多Stage实例功能的脚本
"""
import sys
import os
from collections import defaultdict
from typing import Dict, List, Any, Optional
# 简化的测试,不依赖完整的框架
class MockDMSEndpoint:
"""模拟的DMS端点"""
def __init__(self, method: str, path: str, operation_id: str):
self.method = method
self.path = path
self.operation_id = operation_id
class MockParsedDMSSpec:
"""模拟的DMS规范"""
def __init__(self, endpoints: List[MockDMSEndpoint]):
self.endpoints = endpoints
self.spec_type = 'dms'
def create_mock_dms_spec():
"""创建模拟的DMS规范包含3个完整的CRUD场景"""
# 模拟3个资源的CRUD端点
resources = ['cd_site', 'cd_well_event_log', 'op_pro_well_stim_daily']
endpoints = []
for resource in resources:
# 为每个资源创建5个CRUD操作
operations = ['create', 'read', 'update', 'delete', 'list']
for op in operations:
endpoint = MockDMSEndpoint(
method='POST' if op in ['create', 'list'] else 'GET' if op == 'read' else 'PUT' if op == 'update' else 'DELETE',
path=f'/api/dms/wb_cd/v1/{resource}',
operation_id=f'{op}_{resource}'
)
endpoints.append(endpoint)
return MockParsedDMSSpec(endpoints=endpoints)
def discover_crud_scenarios_mock(parsed_spec):
"""模拟场景发现逻辑"""
if not hasattr(parsed_spec, 'endpoints'):
return []
# 按资源名称分组端点
grouped_ops = defaultdict(dict)
for ep in parsed_spec.endpoints:
if not hasattr(ep, 'operation_id') or not ep.operation_id:
continue
parts = ep.operation_id.split('_', 1)
if len(parts) != 2:
continue
op_type, resource_name = parts
grouped_ops[resource_name][op_type] = ep
# 找到完整的CRUD场景
required_ops = {'create', 'read', 'update', 'delete', 'list'}
scenarios = []
for resource_name, ops in grouped_ops.items():
if required_ops.issubset(ops.keys()):
scenarios.append({
'resource_name': resource_name,
'endpoints': ops,
'virtual_group_name': f"dms_crud_{resource_name}"
})
return scenarios
def test_scenario_discovery():
"""测试场景发现功能"""
print("=== 测试DMS场景发现功能 ===")
mock_spec = create_mock_dms_spec()
scenarios = discover_crud_scenarios_mock(mock_spec)
print(f"发现的场景数量: {len(scenarios)}")
for i, scenario in enumerate(scenarios):
print(f"场景 {i+1}: {scenario['resource_name']} -> {scenario['virtual_group_name']}")
print(f" 包含操作: {list(scenario['endpoints'].keys())}")
assert len(scenarios) == 3, f"期望3个场景实际发现{len(scenarios)}"
expected_resources = ['cd_site', 'cd_well_event_log', 'op_pro_well_stim_daily']
actual_resources = [s['resource_name'] for s in scenarios]
assert set(actual_resources) == set(expected_resources), f"资源名称不匹配: {actual_resources}"
print("✅ 场景发现测试通过")
def test_virtual_group_logic():
"""测试虚拟分组逻辑"""
print("\n=== 测试虚拟分组逻辑 ===")
mock_spec = create_mock_dms_spec()
scenarios = discover_crud_scenarios_mock(mock_spec)
# 测试单场景匹配
target_group = 'dms_crud_cd_site'
matched_scenario = None
for scenario in scenarios:
if scenario['virtual_group_name'] == target_group:
matched_scenario = scenario
break
assert matched_scenario is not None, f"应该找到匹配的场景: {target_group}"
assert matched_scenario['resource_name'] == 'cd_site', "资源名称应该匹配"
expected_ops = {'create', 'read', 'update', 'delete', 'list'}
actual_ops = set(matched_scenario['endpoints'].keys())
assert actual_ops == expected_ops, f"场景操作不完整: {actual_ops}"
print(f"✅ 虚拟分组 '{target_group}' 匹配成功")
print(f" 资源名称: {matched_scenario['resource_name']}")
print(f" 包含操作: {list(matched_scenario['endpoints'].keys())}")
def test_orchestrator_logic():
"""测试orchestrator逻辑"""
print("\n=== 测试Orchestrator逻辑 ===")
mock_spec = create_mock_dms_spec()
scenarios = discover_crud_scenarios_mock(mock_spec)
# 模拟orchestrator的api_groups_to_iterate逻辑
api_groups_to_iterate = []
if hasattr(mock_spec, 'spec_type') and mock_spec.spec_type == 'dms':
if scenarios:
api_groups_to_iterate.extend([scenario['virtual_group_name'] for scenario in scenarios])
print(f"DMS规范: 发现 {len(scenarios)} 个CRUD场景创建对应的虚拟分组")
else:
api_groups_to_iterate.append(None)
print(f"生成的API分组列表: {api_groups_to_iterate}")
# 验证结果
expected_groups = [
'dms_crud_cd_site',
'dms_crud_cd_well_event_log',
'dms_crud_op_pro_well_stim_daily'
]
assert len(api_groups_to_iterate) == 3, f"应该生成3个分组实际生成{len(api_groups_to_iterate)}"
assert set(api_groups_to_iterate) == set(expected_groups), f"分组名称不匹配: {api_groups_to_iterate}"
print("✅ Orchestrator逻辑测试通过")
if __name__ == "__main__":
test_scenario_discovery()
test_virtual_group_logic()
test_orchestrator_logic()
print("\n🎉 所有DMS多Stage实例测试通过")
print("💡 现在orchestrator应该能为每个CRUD场景创建独立的Stage实例了")
print("\n📋 预期结果:")
print("- 10个model → 10个虚拟分组 → 10个Stage实例")
print("- 每个实例只处理一个CRUD场景")
print("- 一个实例失败不影响其他实例")
print("- 最终显示10个Stage执行结果")