mm-qa-mcp 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- minimax_qa_mcp/__init__.py +14 -0
- minimax_qa_mcp/conf/__init__.py +6 -0
- minimax_qa_mcp/conf/conf.ini +86 -0
- minimax_qa_mcp/server.py +184 -0
- minimax_qa_mcp/src/__init__.py +0 -0
- minimax_qa_mcp/src/demo_langchain/__init__.py +6 -0
- minimax_qa_mcp/src/demo_langchain/langchain_demo.py +1 -0
- minimax_qa_mcp/src/gateway_case/__init__.py +0 -0
- minimax_qa_mcp/src/gateway_case/get_case.py +608 -0
- minimax_qa_mcp/src/generator_case/__init__.py +6 -0
- minimax_qa_mcp/src/generator_case/generator_case.py +1187 -0
- minimax_qa_mcp/src/generator_case/generator_case_langchain.py +1078 -0
- minimax_qa_mcp/src/get_weaviate_info/__init__.py +6 -0
- minimax_qa_mcp/src/get_weaviate_info/get_weaviate_info.py +298 -0
- minimax_qa_mcp/src/grafana/__init__.py +0 -0
- minimax_qa_mcp/src/grafana/service.py +104 -0
- minimax_qa_mcp/src/query_segments/__init__.py +6 -0
- minimax_qa_mcp/src/query_segments/query_segments.py +2848 -0
- minimax_qa_mcp/src/xmind2markdown/__init__.py +6 -0
- minimax_qa_mcp/src/xmind2markdown/xmind_to_markdown.py +976 -0
- minimax_qa_mcp/utils/__init__.py +0 -0
- minimax_qa_mcp/utils/logger.py +38 -0
- minimax_qa_mcp/utils/utils.py +246 -0
- mm_qa_mcp-0.2.0.dist-info/METADATA +167 -0
- mm_qa_mcp-0.2.0.dist-info/RECORD +28 -0
- mm_qa_mcp-0.2.0.dist-info/WHEEL +5 -0
- mm_qa_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- mm_qa_mcp-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1078 @@
|
|
1
|
+
"""
|
2
|
+
coding:utf-8
|
3
|
+
@Software: PyCharm
|
4
|
+
@Time: 2025/4/11 14:03
|
5
|
+
@Author: xingyun
|
6
|
+
@Desc: 使用LangChain框架实现的case自动生成工具
|
7
|
+
"""
|
8
|
+
import os
|
9
|
+
import json
|
10
|
+
import re
|
11
|
+
import requests
|
12
|
+
import subprocess
|
13
|
+
import sys
|
14
|
+
import shutil
|
15
|
+
from pathlib import Path
|
16
|
+
from typing import Dict
|
17
|
+
from datetime import datetime, timedelta, timezone
|
18
|
+
|
19
|
+
from langchain.tools import tool
|
20
|
+
from langchain.prompts import PromptTemplate
|
21
|
+
from langchain.chains import LLMChain
|
22
|
+
from langchain_core.runnables import Runnable
|
23
|
+
from langchain.schema.runnable import RunnableMap, RunnableLambda
|
24
|
+
|
25
|
+
# 确保导入项目相关模块
|
26
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
27
|
+
root_dir = os.path.dirname(os.path.dirname(current_dir))
|
28
|
+
sys.path.append(root_dir)
|
29
|
+
|
30
|
+
from minimax_qa_mcp.src.grafana.service import GetFromGrafana
|
31
|
+
from minimax_qa_mcp.src.query_segments.query_segments import main as query_segments_main
|
32
|
+
from minimax_qa_mcp.utils.utils import Utils
|
33
|
+
from minimax_qa_mcp.utils.logger import logger
|
34
|
+
|
35
|
+
|
36
|
+
class ModuleClient:
|
37
|
+
"""模型调用客户端"""
|
38
|
+
|
39
|
+
def __init__(self):
|
40
|
+
# 从配置文件读取API URL,如果不存在则使用默认值
|
41
|
+
try:
|
42
|
+
self.api_url = Utils.get_conf("generator_case_conf", "module_api_url")
|
43
|
+
logger.info(f"从配置读取模型API地址: {self.api_url}")
|
44
|
+
except KeyError:
|
45
|
+
# 默认API URL
|
46
|
+
self.api_url = "http://swing-babel-ali-prod.xaminim.com/swing/api/get_module_result"
|
47
|
+
logger.info(f"未找到API地址配置,使用默认地址: {self.api_url}")
|
48
|
+
|
49
|
+
def call_model(self, params):
|
50
|
+
"""调用模型API
|
51
|
+
|
52
|
+
Args:
|
53
|
+
params: 模型输入参数
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
模型返回的结果
|
57
|
+
"""
|
58
|
+
# 使用更简单的字符串替换处理,减少转义层级
|
59
|
+
clean_params = params.replace('\\"', "'") # 替换嵌套双引号为单引号
|
60
|
+
clean_params = clean_params.replace("\n", " ").strip()
|
61
|
+
|
62
|
+
payload = {
|
63
|
+
"scene": "qa_agent",
|
64
|
+
"params": {
|
65
|
+
"user_content": clean_params
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
# 打印发送到API的请求内容
|
70
|
+
logger.info(f"发送请求到API,payload: {json.dumps(payload, ensure_ascii=False)}")
|
71
|
+
|
72
|
+
# 添加verify=False参数,禁用SSL证书验证
|
73
|
+
response = requests.post(self.api_url, json=payload, headers={'Content-Type': 'application/json'}, verify=False)
|
74
|
+
|
75
|
+
logger.info(f"API响应状态码: {response.status_code}")
|
76
|
+
|
77
|
+
# 打印API响应的完整内容
|
78
|
+
logger.info(f"API响应内容: {response.text}")
|
79
|
+
|
80
|
+
if response.status_code == 200:
|
81
|
+
# 尝试解析JSON响应
|
82
|
+
try:
|
83
|
+
resp_json = response.json()
|
84
|
+
logger.info(f"解析后的JSON: {json.dumps(resp_json, ensure_ascii=False)}")
|
85
|
+
|
86
|
+
if 'response' in resp_json:
|
87
|
+
# 解析二层JSON
|
88
|
+
try:
|
89
|
+
model_response = json.loads(resp_json['response'])
|
90
|
+
logger.info(f"解析二层JSON: {json.dumps(model_response, ensure_ascii=False)}")
|
91
|
+
|
92
|
+
# 从content中提取文本
|
93
|
+
if 'content' in model_response and isinstance(model_response['content'], list):
|
94
|
+
text_content = ""
|
95
|
+
for item in model_response['content']:
|
96
|
+
if item.get('type') == 'text':
|
97
|
+
text_content += item.get('text', '')
|
98
|
+
logger.info(f"提取的文本内容: {text_content}")
|
99
|
+
return text_content
|
100
|
+
return str(model_response)
|
101
|
+
except Exception as e:
|
102
|
+
logger.error(f"解析二层JSON失败: {e}")
|
103
|
+
return resp_json['response']
|
104
|
+
return response.text
|
105
|
+
except Exception as e:
|
106
|
+
logger.error(f"解析JSON失败: {e}")
|
107
|
+
return response.text
|
108
|
+
|
109
|
+
return response.text
|
110
|
+
|
111
|
+
|
112
|
+
class CustomRunnable(Runnable):
|
113
|
+
"""自定义Runnable接口,用于langchain调用"""
|
114
|
+
|
115
|
+
def __init__(self):
|
116
|
+
self.client = ModuleClient()
|
117
|
+
|
118
|
+
def invoke(self, input, config=None):
|
119
|
+
"""调用模型API
|
120
|
+
|
121
|
+
Args:
|
122
|
+
input: 可以是字符串或字典
|
123
|
+
config: 配置参数
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
生成的文本
|
127
|
+
"""
|
128
|
+
if isinstance(input, dict):
|
129
|
+
# 如果输入是字典,尝试构建提示词
|
130
|
+
prompt = input.get("text", "")
|
131
|
+
for key, value in input.items():
|
132
|
+
if key != "text" and isinstance(value, str):
|
133
|
+
prompt = prompt.replace(f"{{{key}}}", value)
|
134
|
+
elif isinstance(input, str):
|
135
|
+
prompt = input
|
136
|
+
else:
|
137
|
+
prompt = str(input)
|
138
|
+
|
139
|
+
# 打印模型输入
|
140
|
+
logger.info(f"调用模型API,输入:{prompt}")
|
141
|
+
|
142
|
+
# 调用模型
|
143
|
+
response = self.client.call_model(prompt)
|
144
|
+
|
145
|
+
# 打印模型输出
|
146
|
+
logger.info(f"调用模型API,输出:{response}")
|
147
|
+
|
148
|
+
# 直接返回解析后的响应
|
149
|
+
return response
|
150
|
+
|
151
|
+
|
152
|
+
class LinkCaseTools:
|
153
|
+
"""提供链路测试用例生成所需的工具函数集合"""
|
154
|
+
|
155
|
+
def __init__(self, business: str, is_need_save: bool = False):
|
156
|
+
"""初始化工具类
|
157
|
+
|
158
|
+
Args:
|
159
|
+
business: 业务名称,如xingye、hailuo等
|
160
|
+
is_need_save: 是否需要保存生成的测试用例
|
161
|
+
"""
|
162
|
+
self.business = business
|
163
|
+
self.is_need_save = is_need_save
|
164
|
+
self.workspace_dir = Path(os.getcwd())
|
165
|
+
self.link_case_conf_path = Utils.get_link_conf_abspath()
|
166
|
+
self.result_dir = self.workspace_dir / "case_results"
|
167
|
+
self.result_dir.mkdir(exist_ok=True)
|
168
|
+
self.repo_dir = self.workspace_dir / "git_repos" / business
|
169
|
+
|
170
|
+
# 设置时间范围
|
171
|
+
now = datetime.now(timezone(timedelta(hours=8)))
|
172
|
+
self.to_time = now.isoformat()
|
173
|
+
self.from_time = (now - timedelta(hours=3)).isoformat()
|
174
|
+
|
175
|
+
# 设置场景映射
|
176
|
+
self.scene_mapping = {
|
177
|
+
"xingye": "xingye_prod",
|
178
|
+
"hailuo": "hailuo_video_cn_prod",
|
179
|
+
"kaiping": "talkie_prod"
|
180
|
+
}
|
181
|
+
self.scene = self.scene_mapping.get(business, "xingye_prod")
|
182
|
+
logger.info(f"初始化LinkCaseTools: business={business}")
|
183
|
+
|
184
|
+
@tool
|
185
|
+
def read_link_api_relations(self, api_name: str) -> Dict:
|
186
|
+
"""读取本地的link_api.txt文件,获取指定API的依赖关系
|
187
|
+
|
188
|
+
Args:
|
189
|
+
api_name: API名称
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
dict: API依赖关系,格式为 {"pre_api": [...], "post_api": [...]}
|
193
|
+
"""
|
194
|
+
logger.info(f"正在读取API依赖关系: {api_name}")
|
195
|
+
link_api_path = Path(self.link_case_conf_path)
|
196
|
+
if not link_api_path.exists():
|
197
|
+
logger.warning(f"link_api.txt文件不存在: {link_api_path}")
|
198
|
+
return {"pre_api": [], "post_api": []}
|
199
|
+
|
200
|
+
try:
|
201
|
+
api_deps = {}
|
202
|
+
with open(link_api_path, "r", encoding="utf-8") as f:
|
203
|
+
for line_num, line in enumerate(f, 1):
|
204
|
+
line = line.strip()
|
205
|
+
if not line or line.startswith("#"):
|
206
|
+
continue
|
207
|
+
|
208
|
+
parts = line.split("|")
|
209
|
+
if len(parts) >= 3:
|
210
|
+
api_part = parts[0].strip()
|
211
|
+
api = api_part.replace("[api]", "").strip()
|
212
|
+
|
213
|
+
pre_api_part = parts[1].strip()
|
214
|
+
pre_api_part = pre_api_part.replace("[pre_api]", "").strip()
|
215
|
+
pre_apis = [pre_api.strip() for pre_api in pre_api_part.split(",") if pre_api.strip()]
|
216
|
+
|
217
|
+
post_api_part = parts[2].strip()
|
218
|
+
post_api_part = post_api_part.replace("[post_api]", "").strip()
|
219
|
+
post_apis = [post_api.strip() for post_api in post_api_part.split(",") if post_api.strip()]
|
220
|
+
|
221
|
+
api_deps[api] = {"pre_api": pre_apis, "post_api": post_apis}
|
222
|
+
|
223
|
+
# 返回指定API的依赖关系或空字典
|
224
|
+
api_relation = api_deps.get(api_name, {"pre_api": [], "post_api": []})
|
225
|
+
logger.info(f"获取到API '{api_name}' 的依赖关系: {api_relation}")
|
226
|
+
return api_relation
|
227
|
+
|
228
|
+
except Exception as e:
|
229
|
+
logger.error(f"读取link_api.txt文件失败: {e}")
|
230
|
+
return {"pre_api": [], "post_api": []}
|
231
|
+
|
232
|
+
@tool
|
233
|
+
def get_grafana_data(self, api_name: str) -> Dict:
|
234
|
+
"""从Grafana获取API的请求和响应数据
|
235
|
+
|
236
|
+
Args:
|
237
|
+
api_name: API名称
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
dict: API的请求和响应数据
|
241
|
+
"""
|
242
|
+
logger.info(f"从Grafana获取API数据: {api_name}, scene={self.scene}")
|
243
|
+
try:
|
244
|
+
grafana_client = GetFromGrafana(
|
245
|
+
scene=self.scene,
|
246
|
+
from_time=self.from_time,
|
247
|
+
to_time=self.to_time
|
248
|
+
)
|
249
|
+
api_data = grafana_client.post_grafana(msgs=[api_name])
|
250
|
+
return {"raw_data": api_data}
|
251
|
+
except Exception as e:
|
252
|
+
logger.error(f"从Grafana获取API数据失败: {e}")
|
253
|
+
return {"raw_data": None}
|
254
|
+
|
255
|
+
@tool
|
256
|
+
def get_code_repo(self) -> Dict:
|
257
|
+
"""获取业务对应的代码仓库,并获取link case的demo
|
258
|
+
|
259
|
+
Returns:
|
260
|
+
dict: 代码仓库信息和demo case
|
261
|
+
"""
|
262
|
+
# 从配置文件获取git仓库地址
|
263
|
+
git_url = Utils.get_conf("generator_case_conf", f"{self.business}_git_url")
|
264
|
+
logger.info(f"获取代码仓库: {self.business}, git_url={git_url}")
|
265
|
+
|
266
|
+
# 从配置文件获取分支信息
|
267
|
+
branch = "main" # 默认使用main分支
|
268
|
+
try:
|
269
|
+
branch = Utils.get_conf("generator_case_conf", f"{self.business}_branch")
|
270
|
+
except KeyError:
|
271
|
+
logger.info(f"未找到分支配置,将使用默认分支: {branch}")
|
272
|
+
|
273
|
+
# 从配置文件获取demo case路径和pre demo路径
|
274
|
+
demo_case_path_key = f"{self.business}_link_case_demo_path"
|
275
|
+
pre_demo_path_key = f"{self.business}_pre_demo_case_path"
|
276
|
+
|
277
|
+
try:
|
278
|
+
demo_case_path = Utils.get_conf("generator_case_conf", demo_case_path_key)
|
279
|
+
logger.info(f"获取demo case路径: {demo_case_path}")
|
280
|
+
except KeyError:
|
281
|
+
logger.error(f"未找到demo case路径配置: {demo_case_path_key}")
|
282
|
+
raise ValueError(f"未指定{self.business}业务的demo case路径,请在conf.ini中配置{demo_case_path_key}")
|
283
|
+
|
284
|
+
try:
|
285
|
+
pre_demo_path = Utils.get_conf("generator_case_conf", pre_demo_path_key)
|
286
|
+
logger.info(f"获取pre demo路径: {pre_demo_path}")
|
287
|
+
except KeyError:
|
288
|
+
logger.warning(f"未找到pre demo路径配置: {pre_demo_path_key},将使用空内容")
|
289
|
+
pre_demo_path = None
|
290
|
+
|
291
|
+
# 处理代码仓库
|
292
|
+
if self.repo_dir.exists():
|
293
|
+
logger.info(f"删除已存在的仓库目录: {self.repo_dir}")
|
294
|
+
shutil.rmtree(str(self.repo_dir))
|
295
|
+
|
296
|
+
self.repo_dir.parent.mkdir(exist_ok=True)
|
297
|
+
self.repo_dir.mkdir(exist_ok=True)
|
298
|
+
|
299
|
+
demo_cases = {}
|
300
|
+
pre_demos = {}
|
301
|
+
if not git_url:
|
302
|
+
error_msg = f"未找到 {self.business} 的git仓库地址配置"
|
303
|
+
logger.error(error_msg)
|
304
|
+
raise ValueError(error_msg)
|
305
|
+
|
306
|
+
try:
|
307
|
+
# 克隆仓库
|
308
|
+
logger.info(f"克隆代码仓库: {git_url} -> {self.repo_dir}")
|
309
|
+
subprocess.run(
|
310
|
+
["git", "clone", git_url, str(self.repo_dir)],
|
311
|
+
check=True,
|
312
|
+
stdout=subprocess.PIPE,
|
313
|
+
stderr=subprocess.PIPE
|
314
|
+
)
|
315
|
+
|
316
|
+
# 如果不是默认分支,需要切换分支
|
317
|
+
if branch != "main" and branch != "master":
|
318
|
+
logger.info(f"切换到分支: {branch}")
|
319
|
+
subprocess.run(
|
320
|
+
["git", "checkout", branch],
|
321
|
+
cwd=str(self.repo_dir),
|
322
|
+
check=True,
|
323
|
+
stdout=subprocess.PIPE,
|
324
|
+
stderr=subprocess.PIPE
|
325
|
+
)
|
326
|
+
|
327
|
+
# 构建demo case的完整路径
|
328
|
+
full_demo_path = self.repo_dir / demo_case_path
|
329
|
+
logger.info(f"查找指定的demo case: {full_demo_path}")
|
330
|
+
|
331
|
+
# 读取指定的demo case文件
|
332
|
+
if os.path.isfile(full_demo_path):
|
333
|
+
with open(full_demo_path, "r", encoding="utf-8") as f:
|
334
|
+
content = f.read()
|
335
|
+
demo_cases[os.path.basename(full_demo_path)] = content
|
336
|
+
logger.info(f"找到demo case文件: {full_demo_path}")
|
337
|
+
else:
|
338
|
+
logger.warning(f"指定的demo case路径不存在或不是文件: {full_demo_path}")
|
339
|
+
# 直接抛出异常,不再尝试创建默认demo文件
|
340
|
+
error_msg = f"指定的demo case路径 '{demo_case_path}' 不存在或不是文件,请检查配置"
|
341
|
+
logger.error(error_msg)
|
342
|
+
raise FileNotFoundError(error_msg)
|
343
|
+
|
344
|
+
# 如果有pre demo path,也读取它
|
345
|
+
if pre_demo_path:
|
346
|
+
full_pre_demo_path = self.repo_dir / pre_demo_path
|
347
|
+
logger.info(f"查找指定的pre demo: {full_pre_demo_path}")
|
348
|
+
|
349
|
+
if os.path.isfile(full_pre_demo_path):
|
350
|
+
with open(full_pre_demo_path, "r", encoding="utf-8") as f:
|
351
|
+
content = f.read()
|
352
|
+
pre_demos[os.path.basename(full_pre_demo_path)] = content
|
353
|
+
logger.info(f"找到pre demo文件: {full_pre_demo_path}")
|
354
|
+
else:
|
355
|
+
logger.warning(f"指定的pre demo路径不存在或不是文件: {full_pre_demo_path}")
|
356
|
+
|
357
|
+
# 获取完demo后删除git仓库
|
358
|
+
logger.info(f"已读取必要的demo文件,删除git仓库: {self.repo_dir}")
|
359
|
+
shutil.rmtree(str(self.repo_dir))
|
360
|
+
|
361
|
+
# 检查父目录是否为空,如果为空也删除
|
362
|
+
parent_dir = self.repo_dir.parent
|
363
|
+
if parent_dir.exists() and not any(parent_dir.iterdir()):
|
364
|
+
logger.info(f"父目录为空,一并删除: {parent_dir}")
|
365
|
+
shutil.rmtree(str(parent_dir))
|
366
|
+
|
367
|
+
return {
|
368
|
+
"repo_info": f"{self.business} repository at {git_url}, branch: {branch}",
|
369
|
+
"demo_cases": demo_cases,
|
370
|
+
"pre_demos": pre_demos
|
371
|
+
}
|
372
|
+
|
373
|
+
except Exception as e:
|
374
|
+
# 如果发生异常,尝试清理已创建的目录
|
375
|
+
if self.repo_dir.exists():
|
376
|
+
logger.info(f"由于错误,删除仓库目录: {self.repo_dir}")
|
377
|
+
shutil.rmtree(str(self.repo_dir))
|
378
|
+
|
379
|
+
# 检查父目录是否为空,如果为空也删除
|
380
|
+
parent_dir = self.repo_dir.parent
|
381
|
+
if parent_dir.exists() and not any(parent_dir.iterdir()):
|
382
|
+
logger.info(f"父目录为空,一并删除: {parent_dir}")
|
383
|
+
shutil.rmtree(str(parent_dir))
|
384
|
+
|
385
|
+
# 直接抛出异常,不再尝试创建本地备用文件
|
386
|
+
error_msg = f"获取代码仓库失败: {str(e)},请检查git配置和网络连接"
|
387
|
+
logger.error(error_msg)
|
388
|
+
raise RuntimeError(error_msg)
|
389
|
+
|
390
|
+
@tool
|
391
|
+
def query_code_segments(self, api_name: str) -> Dict:
|
392
|
+
"""调用query_code_segments,获取API相关的出入参数以及业务逻辑
|
393
|
+
|
394
|
+
Args:
|
395
|
+
api_name: API名称
|
396
|
+
|
397
|
+
Returns:
|
398
|
+
dict: API相关的代码段信息
|
399
|
+
"""
|
400
|
+
# 确保API名称格式一致
|
401
|
+
if not api_name.startswith('/'):
|
402
|
+
api_name = f"/{api_name}"
|
403
|
+
|
404
|
+
logger.info(f"获取API代码段: {api_name}")
|
405
|
+
|
406
|
+
try:
|
407
|
+
# 直接调用main函数
|
408
|
+
code_segments = query_segments_main(
|
409
|
+
input_content=api_name,
|
410
|
+
input_type="API",
|
411
|
+
limit=10,
|
412
|
+
exact=False,
|
413
|
+
depth=1,
|
414
|
+
direction="both",
|
415
|
+
advanced=False,
|
416
|
+
output=None
|
417
|
+
)
|
418
|
+
|
419
|
+
return {"raw_segments": code_segments}
|
420
|
+
|
421
|
+
except Exception as e:
|
422
|
+
logger.error(f"获取API代码段失败: {e}")
|
423
|
+
return {"raw_segments": None}
|
424
|
+
|
425
|
+
@tool
|
426
|
+
def save_case_to_file(self, case_code: str, api_name: str, case_type: str = "link") -> str:
|
427
|
+
"""将生成的case保存到本地
|
428
|
+
|
429
|
+
Args:
|
430
|
+
case_code: 生成的测试用例代码
|
431
|
+
api_name: API名称
|
432
|
+
case_type: 测试用例类型,默认为link
|
433
|
+
|
434
|
+
Returns:
|
435
|
+
str: 保存的文件路径
|
436
|
+
"""
|
437
|
+
if not self.is_need_save:
|
438
|
+
logger.info("不需要保存测试用例")
|
439
|
+
return ""
|
440
|
+
|
441
|
+
# 格式化输出的代码内容
|
442
|
+
case_code = self._format_output_file(case_code)
|
443
|
+
|
444
|
+
case_type_dir = self.result_dir / case_type
|
445
|
+
case_type_dir.mkdir(exist_ok=True)
|
446
|
+
|
447
|
+
# 生成文件名
|
448
|
+
file_name = f"test_{api_name.replace('.', '_').replace('/', '_')}_{self.business}.py"
|
449
|
+
file_path = case_type_dir / file_name
|
450
|
+
|
451
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
452
|
+
f.write(case_code)
|
453
|
+
|
454
|
+
logger.info(f"测试用例已保存到: {file_path}")
|
455
|
+
return str(file_path)
|
456
|
+
|
457
|
+
def _format_output_file(self, code: str) -> str:
|
458
|
+
"""格式化输出的文件内容,修复常见格式问题
|
459
|
+
|
460
|
+
Args:
|
461
|
+
code: 代码内容
|
462
|
+
|
463
|
+
Returns:
|
464
|
+
str: 修复后的代码
|
465
|
+
"""
|
466
|
+
logger.info("开始格式化生成的代码文件")
|
467
|
+
|
468
|
+
# 将字符串包含的原始转义序列解码为Python字符串
|
469
|
+
try:
|
470
|
+
code = bytes(code, 'utf-8').decode('unicode_escape')
|
471
|
+
except Exception as e:
|
472
|
+
logger.warning(f"转义字符解码失败,将使用备用方法: {e}")
|
473
|
+
|
474
|
+
# 备用方法:逐个替换常见的转义序列
|
475
|
+
# 修复开头的转义问题
|
476
|
+
if code.startswith('\\n'):
|
477
|
+
code = code[2:]
|
478
|
+
|
479
|
+
# 修复引号的转义问题
|
480
|
+
code = code.replace("\\'\\'\\'", "'''") # 三引号
|
481
|
+
code = code.replace("\\'", "'") # 单引号
|
482
|
+
code = code.replace('\\"', '"') # 双引号
|
483
|
+
|
484
|
+
# 修复换行符的转义问题
|
485
|
+
code = code.replace("\\n", "\n")
|
486
|
+
|
487
|
+
# 修复制表符的转义问题
|
488
|
+
code = code.replace("\\t", "\t")
|
489
|
+
|
490
|
+
# 修复反斜杠的转义问题
|
491
|
+
code = code.replace("\\\\", "\\")
|
492
|
+
|
493
|
+
# 修复其他常见转义字符
|
494
|
+
for escape_char in ['\\r', '\\f', '\\v', '\\a', '\\b']:
|
495
|
+
char_value = eval(f'"{escape_char}"')
|
496
|
+
code = code.replace(escape_char, char_value)
|
497
|
+
|
498
|
+
logger.info("完成格式化生成的代码文件")
|
499
|
+
return code
|
500
|
+
|
501
|
+
|
502
|
+
# 设置提示词模板,包含更多上下文信息
|
503
|
+
prompt_template = """
|
504
|
+
你是一个资深的Python测试用例工程师,请根据以下信息,为API {target_api} 生成一个场景化的链路测试用例:
|
505
|
+
|
506
|
+
## 业务基本信息
|
507
|
+
- 业务名称: {business}
|
508
|
+
- API名称: {target_api}
|
509
|
+
|
510
|
+
## API依赖关系
|
511
|
+
{api_relations}
|
512
|
+
|
513
|
+
## 前置API数据
|
514
|
+
{pre_api_data}
|
515
|
+
|
516
|
+
## 后置API数据
|
517
|
+
{post_api_data}
|
518
|
+
|
519
|
+
## 代码仓库信息
|
520
|
+
{repo_info}
|
521
|
+
|
522
|
+
## API代码分析
|
523
|
+
{code_segments}
|
524
|
+
|
525
|
+
## 参考的Demo Case格式
|
526
|
+
```python
|
527
|
+
{demo_case}
|
528
|
+
```
|
529
|
+
|
530
|
+
请按照上面的demo case格式,为API {target_api} 生成一个完整的Python测试用例。
|
531
|
+
请确保生成的测试用例结构、导入方式、类和方法定义、测试步骤等都与demo case保持一致,只修改具体的测试内容和API调用。
|
532
|
+
保持相同的代码风格、错误处理方式和断言格式。
|
533
|
+
|
534
|
+
请遵循以下Python代码格式规范:
|
535
|
+
1. 使用四个空格进行缩进,不要使用制表符
|
536
|
+
2. 类定义后空两行,方法定义后空一行
|
537
|
+
3. 使用单引号表示字符串,除非字符串中包含单引号
|
538
|
+
4. 注释应该是完整的句子,并以句号结尾
|
539
|
+
5. 导入模块应该一行一个,并按照标准库、第三方库、本地库的顺序排列
|
540
|
+
6. 确保代码中没有过长的行(最好不超过100个字符)
|
541
|
+
7. 变量和方法使用小写字母和下划线命名法
|
542
|
+
|
543
|
+
如果demo case中有特定的导入、工具函数或基类,请在生成的代码中保留这些元素。
|
544
|
+
"""
|
545
|
+
|
546
|
+
|
547
|
+
class GeneratorCaseLangChain:
|
548
|
+
"""使用LangChain框架的测试用例生成器"""
|
549
|
+
|
550
|
+
def __init__(self, input_data, is_need_save: bool = True, case_type: str = "link"):
|
551
|
+
"""初始化测试用例生成器
|
552
|
+
|
553
|
+
Args:
|
554
|
+
input_data: JSON格式的输入数据,包含Business、API和Case信息
|
555
|
+
is_need_save: 是否需要保存生成的测试用例
|
556
|
+
case_type: 测试用例类型
|
557
|
+
"""
|
558
|
+
# 初始化基本属性
|
559
|
+
self.is_need_save = is_need_save
|
560
|
+
self.case_type = case_type
|
561
|
+
self.workspace_dir = Path(os.getcwd())
|
562
|
+
|
563
|
+
# 如果输入是字符串,尝试解析为JSON对象
|
564
|
+
if isinstance(input_data, str):
|
565
|
+
try:
|
566
|
+
self.input_data = json.loads(input_data)
|
567
|
+
except json.JSONDecodeError as e:
|
568
|
+
logger.error(f"输入数据不是有效的JSON格式: {e}")
|
569
|
+
raise ValueError(f"输入数据不是有效的JSON格式: {str(e)}")
|
570
|
+
else:
|
571
|
+
self.input_data = input_data
|
572
|
+
|
573
|
+
# 解析API和Case信息
|
574
|
+
self._parse_input_data()
|
575
|
+
|
576
|
+
# 初始化其他依赖于业务类型的属性
|
577
|
+
self.result_dir = self.workspace_dir / "case_results"
|
578
|
+
self.result_dir.mkdir(exist_ok=True)
|
579
|
+
self.repo_dir = self.workspace_dir / "git_repos" / self.business
|
580
|
+
|
581
|
+
# 设置默认的时间范围为3小时
|
582
|
+
now = datetime.now(timezone(timedelta(hours=8)))
|
583
|
+
self.to_time = now.isoformat()
|
584
|
+
self.from_time = (now - timedelta(hours=3)).isoformat()
|
585
|
+
|
586
|
+
# 根据业务名称确定场景
|
587
|
+
self.scene_mapping = {
|
588
|
+
"xingye": "xingye_prod",
|
589
|
+
"hailuo": "hailuo_video_cn_prod",
|
590
|
+
"kaiping": "talkie_prod"
|
591
|
+
}
|
592
|
+
self.scene = self.scene_mapping.get(self.business, "xingye_prod")
|
593
|
+
|
594
|
+
# 设置默认值
|
595
|
+
self.is_need_module = True
|
596
|
+
|
597
|
+
# 初始化工具类
|
598
|
+
self.tools = LinkCaseTools(business=self.business, is_need_save=is_need_save)
|
599
|
+
|
600
|
+
# 初始化模型
|
601
|
+
self.model = CustomRunnable()
|
602
|
+
|
603
|
+
# 创建提示词模板
|
604
|
+
self.prompt = PromptTemplate(
|
605
|
+
template=prompt_template,
|
606
|
+
input_variables=[
|
607
|
+
"target_api",
|
608
|
+
"business",
|
609
|
+
"api_relations",
|
610
|
+
"pre_api_data",
|
611
|
+
"post_api_data",
|
612
|
+
"repo_info",
|
613
|
+
"code_segments",
|
614
|
+
"demo_case"
|
615
|
+
]
|
616
|
+
)
|
617
|
+
|
618
|
+
# 创建测试用例生成链
|
619
|
+
self.chain = LLMChain(llm=self.model, prompt=self.prompt)
|
620
|
+
|
621
|
+
logger.info(f"初始化GeneratorCaseLangChain: api_name={self.api_name}, business={self.business}, case_type={case_type}")
|
622
|
+
|
623
|
+
def _parse_input_data(self):
|
624
|
+
"""解析输入的JSON数据,提取API和Case信息"""
|
625
|
+
# 检查输入数据结构是否符合预期
|
626
|
+
required_fields = ['Business', 'API', 'Case']
|
627
|
+
for field in required_fields:
|
628
|
+
if field not in self.input_data:
|
629
|
+
logger.error(f"输入数据格式错误,必须包含'{field}'字段")
|
630
|
+
raise ValueError(f"输入数据格式错误,必须包含'{field}'字段")
|
631
|
+
|
632
|
+
# 从JSON中提取业务类型
|
633
|
+
self.business = self.input_data.get('Business')
|
634
|
+
if not self.business:
|
635
|
+
logger.error("Business字段值为空")
|
636
|
+
raise ValueError("Business字段值为空,必须指定业务类型")
|
637
|
+
logger.info(f"从JSON中提取业务类型: {self.business}")
|
638
|
+
|
639
|
+
# 提取API信息
|
640
|
+
api_info = self.input_data['API']
|
641
|
+
if not api_info:
|
642
|
+
logger.error("API信息为空")
|
643
|
+
raise ValueError("API信息为空")
|
644
|
+
|
645
|
+
# 设置所有API列表
|
646
|
+
self.all_apis = list(api_info.keys())
|
647
|
+
if not self.all_apis:
|
648
|
+
logger.error("API列表为空")
|
649
|
+
raise ValueError("API列表为空,至少需要提供一个API")
|
650
|
+
|
651
|
+
# 所有API都是平等的,但仍需要一个api_name用于日志和文件名
|
652
|
+
self.api_name = self.all_apis[0] if self.all_apis else ""
|
653
|
+
self.api_descriptions = api_info
|
654
|
+
|
655
|
+
# 解析Case信息
|
656
|
+
case_info = self.input_data['Case']
|
657
|
+
if not case_info or not isinstance(case_info, list):
|
658
|
+
logger.error("Case信息为空或格式错误")
|
659
|
+
raise ValueError("Case信息为空或格式错误")
|
660
|
+
|
661
|
+
# 提取前置操作和测试场景
|
662
|
+
self.pre_apis = []
|
663
|
+
self.cases = []
|
664
|
+
self.detailed_pre_info = {}
|
665
|
+
self.detailed_case_info = {}
|
666
|
+
self.case_items = [] # 保存原始的case项
|
667
|
+
|
668
|
+
for case_item in case_info:
|
669
|
+
self.case_items.append(case_item) # 保存完整的case item
|
670
|
+
for case_name, case_data in case_item.items():
|
671
|
+
# 添加case名称
|
672
|
+
self.cases.append(case_name)
|
673
|
+
|
674
|
+
# 提取前置操作
|
675
|
+
if 'PRE' in case_data and isinstance(case_data['PRE'], dict):
|
676
|
+
for pre_name, pre_desc in case_data['PRE'].items():
|
677
|
+
if pre_name not in self.pre_apis:
|
678
|
+
self.pre_apis.append(pre_name)
|
679
|
+
# 保存详细前置信息
|
680
|
+
if pre_name not in self.detailed_pre_info:
|
681
|
+
self.detailed_pre_info[pre_name] = []
|
682
|
+
self.detailed_pre_info[pre_name].append(pre_desc)
|
683
|
+
|
684
|
+
# 提取测试步骤
|
685
|
+
if 'TEST' in case_data:
|
686
|
+
# 保存详细测试信息
|
687
|
+
self.detailed_case_info[case_name] = {
|
688
|
+
'steps': [],
|
689
|
+
'pre': list(case_data.get('PRE', {}).keys()),
|
690
|
+
'test': [case_data['TEST']]
|
691
|
+
}
|
692
|
+
|
693
|
+
# 设置API详细信息的格式
|
694
|
+
self.detailed_api_info = {}
|
695
|
+
for api_path, api_desc in self.api_descriptions.items():
|
696
|
+
# 提取API名称(可能作为分组)
|
697
|
+
api_name = api_path.split('/')[-1] if '/' in api_path else api_path
|
698
|
+
self.detailed_api_info[api_name] = [{
|
699
|
+
'path': api_path,
|
700
|
+
'description': api_desc
|
701
|
+
}]
|
702
|
+
|
703
|
+
logger.info(f"解析输入数据完成: API数量={len(self.all_apis)}, 前置操作={self.pre_apis}, Cases={self.cases}")
|
704
|
+
|
705
|
+
def _get_api_relations(self, api_name: str) -> Dict:
|
706
|
+
"""获取API依赖关系"""
|
707
|
+
return self.tools.read_link_api_relations(api_name)
|
708
|
+
|
709
|
+
def _process_pre_api_data(self, api_relations: Dict) -> Dict:
|
710
|
+
"""处理前置API数据"""
|
711
|
+
pre_api_data = {}
|
712
|
+
for pre_api in api_relations.get("pre_api", []):
|
713
|
+
pre_api_data[pre_api] = {
|
714
|
+
"grafana_info": self.tools.get_grafana_data(pre_api),
|
715
|
+
"code_segments": self.tools.query_code_segments(pre_api)
|
716
|
+
}
|
717
|
+
return pre_api_data
|
718
|
+
|
719
|
+
def _process_post_api_data(self, api_relations: Dict) -> Dict:
|
720
|
+
"""处理后置API数据"""
|
721
|
+
post_api_data = {}
|
722
|
+
for post_api in api_relations.get("post_api", []):
|
723
|
+
post_api_data[post_api] = {
|
724
|
+
"grafana_info": self.tools.get_grafana_data(post_api),
|
725
|
+
"code_segments": self.tools.query_code_segments(post_api)
|
726
|
+
}
|
727
|
+
return post_api_data
|
728
|
+
|
729
|
+
def _extract_demo_case(self, repo_info: Dict) -> str:
|
730
|
+
"""从仓库信息中提取demo case内容"""
|
731
|
+
demo_cases = repo_info.get("demo_cases", {})
|
732
|
+
if demo_cases:
|
733
|
+
return list(demo_cases.values())[0]
|
734
|
+
return ""
|
735
|
+
|
736
|
+
def _extract_python_code(self, response: str) -> str:
|
737
|
+
"""从模型响应中提取Python代码块"""
|
738
|
+
python_code_blocks = re.findall(r'```python\s*(.*?)\s*```', response, re.DOTALL)
|
739
|
+
if python_code_blocks:
|
740
|
+
return python_code_blocks[0]
|
741
|
+
return response
|
742
|
+
|
743
|
+
def _generate_case_with_model(self, api_info, api_data, repo_info, case_item, case_index):
|
744
|
+
"""为单个case生成测试用例
|
745
|
+
|
746
|
+
Args:
|
747
|
+
api_info: API信息,包含所有API的基本信息
|
748
|
+
api_data: 所有API的数据,包含Grafana信息和代码段信息
|
749
|
+
repo_info: 代码仓库信息,包含demo cases和pre demos
|
750
|
+
case_item: 当前case的信息
|
751
|
+
case_index: case的索引
|
752
|
+
|
753
|
+
Returns:
|
754
|
+
dict: 包含case和pre代码的字典
|
755
|
+
"""
|
756
|
+
case_name = list(case_item.keys())[0]
|
757
|
+
case_data = case_item[case_name]
|
758
|
+
|
759
|
+
logger.info(f"为case {case_name} 生成测试用例, type={self.case_type}")
|
760
|
+
|
761
|
+
# 获取demo案例作为参考
|
762
|
+
demo_cases = repo_info.get("demo_cases", {})
|
763
|
+
pre_demos = repo_info.get("pre_demos", {})
|
764
|
+
|
765
|
+
demo_case_content = ""
|
766
|
+
pre_demo_content = ""
|
767
|
+
|
768
|
+
if demo_cases:
|
769
|
+
# 使用第一个demo作为参考
|
770
|
+
demo_case_content = list(demo_cases.values())[0]
|
771
|
+
logger.info(f"找到demo case作为参考,长度: {len(demo_case_content)}")
|
772
|
+
else:
|
773
|
+
logger.warning("未找到demo case作为参考,将生成标准格式的测试用例")
|
774
|
+
|
775
|
+
if pre_demos:
|
776
|
+
# 使用第一个pre demo作为参考
|
777
|
+
pre_demo_content = list(pre_demos.values())[0]
|
778
|
+
logger.info(f"找到pre demo作为参考,长度: {len(pre_demo_content)}")
|
779
|
+
else:
|
780
|
+
logger.warning("未找到pre demo作为参考,将使用标准格式生成前置操作")
|
781
|
+
|
782
|
+
# 提取当前case的前置操作信息
|
783
|
+
pre_apis = list(case_data.get('PRE', {}).keys())
|
784
|
+
pre_details = {}
|
785
|
+
for pre_name, pre_desc in case_data.get('PRE', {}).items():
|
786
|
+
pre_details[pre_name] = pre_desc
|
787
|
+
|
788
|
+
# 提取当前case的测试步骤
|
789
|
+
test_steps = case_data.get('TEST', '')
|
790
|
+
|
791
|
+
# 根据JSON提取的信息构建场景化的模板
|
792
|
+
template = f"""你是一个资深的python代码工程师,请针对以下单个测试场景,生成两个独立的Python文件:一个是前置操作文件,一个是测试用例文件。
|
793
|
+
|
794
|
+
业务: {self.business}
|
795
|
+
所有API: {', '.join(api_info['all_apis'])}
|
796
|
+
|
797
|
+
当前需要实现的测试场景:
|
798
|
+
|
799
|
+
场景名称: {case_name}
|
800
|
+
前置操作: {json.dumps(pre_apis, ensure_ascii=False)}
|
801
|
+
前置操作详情: {json.dumps(pre_details, ensure_ascii=False)}
|
802
|
+
测试步骤: {test_steps}
|
803
|
+
|
804
|
+
API基本描述: {json.dumps(self.api_descriptions, ensure_ascii=False)}
|
805
|
+
所有API数据: {json.dumps(api_data, ensure_ascii=False)}
|
806
|
+
代码仓库信息: {json.dumps(repo_info, ensure_ascii=False)}
|
807
|
+
|
808
|
+
## 参考的demo case格式:
|
809
|
+
```python
|
810
|
+
{demo_case_content}
|
811
|
+
```
|
812
|
+
|
813
|
+
## 参考的pre demo格式:
|
814
|
+
```python
|
815
|
+
{pre_demo_content}
|
816
|
+
```
|
817
|
+
|
818
|
+
请生成以下两个文件:
|
819
|
+
|
820
|
+
1. 前置操作文件 (pre_file):
|
821
|
+
- 文件应该包含所有必要的前置操作函数实现,如daily_free(), vip_free()等
|
822
|
+
- 格式应参考pre demo
|
823
|
+
- 作为独立文件能被测试用例导入和使用
|
824
|
+
|
825
|
+
2. 测试用例文件 (case_file):
|
826
|
+
- 针对具体的测试场景"{case_name}"
|
827
|
+
- 实现该场景的完整测试流程
|
828
|
+
- 导入并使用前置操作文件中的函数
|
829
|
+
- 格式应参考demo case
|
830
|
+
|
831
|
+
请确保生成的两个文件互相配合,测试用例文件能够正确导入和使用前置操作文件中的功能。
|
832
|
+
前置操作方法应参考pre demo的格式实现,如果pre demo为空,则使用标准格式实现前置操作。
|
833
|
+
保持相同的代码风格、错误处理方式和断言格式。
|
834
|
+
|
835
|
+
请遵循以下Python代码格式规范:
|
836
|
+
1. 使用四个空格进行缩进,不要使用制表符
|
837
|
+
2. 类定义后空两行,方法定义后空一行
|
838
|
+
3. 使用单引号表示字符串,除非字符串中包含单引号
|
839
|
+
4. 注释应该是完整的句子,并以句号结尾
|
840
|
+
5. 导入模块应该一行一个,并按照标准库、第三方库、本地库的顺序排列
|
841
|
+
6. 确保代码中没有过长的行(最好不超过100个字符)
|
842
|
+
7. 变量和方法使用小写字母和下划线命名法
|
843
|
+
|
844
|
+
在你的回复中,请清晰地用markdown代码块分别标记这两个文件的内容,例如:
|
845
|
+
|
846
|
+
# 前置操作文件(pre_{case_name}.py):
|
847
|
+
```python
|
848
|
+
# 前置操作文件内容
|
849
|
+
```
|
850
|
+
|
851
|
+
# 测试用例文件(test_{case_name}.py):
|
852
|
+
```python
|
853
|
+
# 测试用例文件内容
|
854
|
+
```
|
855
|
+
"""
|
856
|
+
|
857
|
+
# 直接使用模型生成
|
858
|
+
raw_response = self.chain.run(
|
859
|
+
target_api=self.api_name,
|
860
|
+
business=self.business,
|
861
|
+
api_relations="",
|
862
|
+
pre_api_data="",
|
863
|
+
post_api_data="",
|
864
|
+
repo_info="",
|
865
|
+
code_segments="",
|
866
|
+
demo_case=template
|
867
|
+
)
|
868
|
+
logger.info(f"模型返回的响应: {raw_response}")
|
869
|
+
|
870
|
+
# 如果响应为空,返回空字符串
|
871
|
+
if not raw_response:
|
872
|
+
logger.warning("模型返回的响应为空")
|
873
|
+
return {"case_code": "", "pre_code": ""}
|
874
|
+
|
875
|
+
# 提取前置操作文件的Python代码块
|
876
|
+
pre_code_blocks = re.findall(r'# 前置操作文件.*?```python\s*(.*?)\s*```', raw_response, re.DOTALL)
|
877
|
+
# 提取测试用例文件的Python代码块
|
878
|
+
case_code_blocks = re.findall(r'# 测试用例文件.*?```python\s*(.*?)\s*```', raw_response, re.DOTALL)
|
879
|
+
|
880
|
+
# 如果没有找到明确标记的代码块,尝试提取所有Python代码块
|
881
|
+
if not pre_code_blocks or not case_code_blocks:
|
882
|
+
all_code_blocks = re.findall(r'```python\s*(.*?)\s*```', raw_response, re.DOTALL)
|
883
|
+
if len(all_code_blocks) >= 2:
|
884
|
+
pre_code_blocks = [all_code_blocks[0]]
|
885
|
+
case_code_blocks = [all_code_blocks[1]]
|
886
|
+
elif len(all_code_blocks) == 1:
|
887
|
+
# 只有一个代码块,假设是测试用例
|
888
|
+
case_code_blocks = [all_code_blocks[0]]
|
889
|
+
pre_code_blocks = ["# 未找到前置操作代码"]
|
890
|
+
|
891
|
+
pre_code = pre_code_blocks[0] if pre_code_blocks else "# 未找到前置操作代码"
|
892
|
+
case_code = case_code_blocks[0] if case_code_blocks else "# 未找到测试用例代码"
|
893
|
+
|
894
|
+
logger.info(f"为case {case_name} 生成的前置操作代码长度: {len(pre_code)}")
|
895
|
+
logger.info(f"为case {case_name} 生成的测试用例代码长度: {len(case_code)}")
|
896
|
+
|
897
|
+
return {
|
898
|
+
"pre_code": pre_code,
|
899
|
+
"case_code": case_code,
|
900
|
+
"case_name": case_name,
|
901
|
+
"case_index": case_index
|
902
|
+
}
|
903
|
+
|
904
|
+
def _save_cases_to_files(self, case_results):
|
905
|
+
"""将生成的cases和pres保存到本地
|
906
|
+
|
907
|
+
Args:
|
908
|
+
case_results: 包含case和pre代码的列表
|
909
|
+
|
910
|
+
Returns:
|
911
|
+
list: 保存的文件路径列表
|
912
|
+
"""
|
913
|
+
# 创建案例根目录
|
914
|
+
case_root_dir = self.result_dir / self.case_type / f"{self.api_name.replace('.', '_').replace('/', '_')}_{self.business}"
|
915
|
+
case_root_dir.mkdir(exist_ok=True, parents=True)
|
916
|
+
|
917
|
+
saved_paths = []
|
918
|
+
|
919
|
+
for result in case_results:
|
920
|
+
case_name = result.get("case_name", "unknown")
|
921
|
+
case_index = result.get("case_index", 0)
|
922
|
+
pre_code = result.get("pre_code", "")
|
923
|
+
case_code = result.get("case_code", "")
|
924
|
+
|
925
|
+
# 格式化输出的代码内容
|
926
|
+
pre_code = self.tools._format_output_file(pre_code)
|
927
|
+
case_code = self.tools._format_output_file(case_code)
|
928
|
+
|
929
|
+
# 为每个case创建单独的目录
|
930
|
+
case_dir = case_root_dir / f"{case_index:02d}_{case_name}"
|
931
|
+
case_dir.mkdir(exist_ok=True)
|
932
|
+
|
933
|
+
# 保存前置操作文件
|
934
|
+
pre_file_path = case_dir / f"pre_{case_name}.py"
|
935
|
+
with open(pre_file_path, "w", encoding="utf-8") as f:
|
936
|
+
f.write(pre_code)
|
937
|
+
logger.info(f"前置操作已保存到: {pre_file_path}")
|
938
|
+
saved_paths.append(str(pre_file_path))
|
939
|
+
|
940
|
+
# 保存测试用例文件
|
941
|
+
case_file_path = case_dir / f"test_{case_name}.py"
|
942
|
+
with open(case_file_path, "w", encoding="utf-8") as f:
|
943
|
+
f.write(case_code)
|
944
|
+
logger.info(f"测试用例已保存到: {case_file_path}")
|
945
|
+
saved_paths.append(str(case_file_path))
|
946
|
+
|
947
|
+
return saved_paths
|
948
|
+
|
949
|
+
def generator_case(self) -> Dict:
|
950
|
+
"""生成测试用例,完全基于JSON解析的内容
|
951
|
+
|
952
|
+
Returns:
|
953
|
+
dict: 生成结果
|
954
|
+
"""
|
955
|
+
try:
|
956
|
+
logger.info(f"开始生成测试用例: APIs={self.all_apis}, business={self.business}, type={self.case_type}")
|
957
|
+
|
958
|
+
# step1: 获取所有API的出入参数和业务逻辑
|
959
|
+
api_data = {}
|
960
|
+
# 平等处理所有API
|
961
|
+
for api in self.all_apis:
|
962
|
+
api_data[api] = {}
|
963
|
+
grafana_data = self.tools.get_grafana_data(api)
|
964
|
+
code_segments = self.tools.query_code_segments(api)
|
965
|
+
api_data[api]["grafana_info"] = grafana_data
|
966
|
+
api_data[api]["code_segments"] = code_segments
|
967
|
+
logger.info(f"获取到API数据: {api}")
|
968
|
+
|
969
|
+
# step2: 获取业务对应的代码仓库,并获取link case和pre的demo
|
970
|
+
repo_info = self.tools.get_code_repo()
|
971
|
+
logger.info(f"获取到业务对应的代码仓库信息: {repo_info}")
|
972
|
+
|
973
|
+
# 准备API信息,包含所有API
|
974
|
+
api_info = {
|
975
|
+
"all_apis": self.all_apis,
|
976
|
+
"api_descriptions": self.api_descriptions,
|
977
|
+
"business": self.business
|
978
|
+
}
|
979
|
+
|
980
|
+
# step3: 为每个case生成测试用例和前置操作
|
981
|
+
case_results = []
|
982
|
+
try:
|
983
|
+
for i, case_item in enumerate(self.case_items):
|
984
|
+
logger.info(f"处理第 {i+1}/{len(self.case_items)} 个case")
|
985
|
+
case_result = self._generate_case_with_model(
|
986
|
+
api_info,
|
987
|
+
api_data,
|
988
|
+
repo_info,
|
989
|
+
case_item,
|
990
|
+
i+1
|
991
|
+
)
|
992
|
+
case_results.append(case_result)
|
993
|
+
|
994
|
+
except Exception as e:
|
995
|
+
logger.error(f"生成测试用例失败: {e}")
|
996
|
+
# 继续处理已生成的结果
|
997
|
+
|
998
|
+
# step4: 将生成的cases和pres保存到对应的文件夹中
|
999
|
+
if self.is_need_save and case_results:
|
1000
|
+
saved_paths = self._save_cases_to_files(case_results)
|
1001
|
+
|
1002
|
+
return {
|
1003
|
+
"status": "success",
|
1004
|
+
"message": "测试用例生成成功",
|
1005
|
+
"saved_paths": saved_paths,
|
1006
|
+
"case_results": case_results,
|
1007
|
+
"api_name": self.api_name,
|
1008
|
+
"business": self.business,
|
1009
|
+
"pre_apis": self.pre_apis,
|
1010
|
+
"cases": self.cases
|
1011
|
+
}
|
1012
|
+
|
1013
|
+
if not case_results:
|
1014
|
+
return {
|
1015
|
+
"status": "error",
|
1016
|
+
"message": "没有生成任何测试用例"
|
1017
|
+
}
|
1018
|
+
|
1019
|
+
return {
|
1020
|
+
"status": "success",
|
1021
|
+
"message": "测试用例生成成功",
|
1022
|
+
"case_results": case_results,
|
1023
|
+
"api_name": self.api_name,
|
1024
|
+
"business": self.business,
|
1025
|
+
"pre_apis": self.pre_apis,
|
1026
|
+
"cases": self.cases
|
1027
|
+
}
|
1028
|
+
|
1029
|
+
except Exception as e:
|
1030
|
+
logger.error(f"生成测试用例失败: {e}")
|
1031
|
+
return {
|
1032
|
+
"status": "error",
|
1033
|
+
"message": f"生成测试用例失败: {str(e)}"
|
1034
|
+
}
|
1035
|
+
|
1036
|
+
|
1037
|
+
# 运行示例
|
1038
|
+
if __name__ == "__main__":
|
1039
|
+
# 使用JSON格式的输入
|
1040
|
+
test_input = {
|
1041
|
+
"Business": "xingye",
|
1042
|
+
"API": {
|
1043
|
+
"/weaver/api/v1/collection/card/get_all_direct_card": "- 直抽接口,抽卡并直接收下",
|
1044
|
+
"weaver/api/v1/collection/card/list/list_my_story_card_by_npc": "- 许愿池,我的卡牌",
|
1045
|
+
"/weaver/api/v1/collection/card/query_card_choice_history": "- 抽卡记录列表,我所有抽卡记录,按时间倒序排列"
|
1046
|
+
},
|
1047
|
+
"Case": [
|
1048
|
+
{
|
1049
|
+
"许愿池抽卡": {
|
1050
|
+
"PRE": {
|
1051
|
+
"每日免费": "def daily_free(), return uid,使用该uid,则享受每日免费权益",
|
1052
|
+
"月卡免费次数": "def vip_free(), return uid,使用该uid,则享受月卡免费权益",
|
1053
|
+
"星炉熔卡次数": "def reback_free(), return uid,使用该uid,则享受熔炉抽卡权益"
|
1054
|
+
},
|
1055
|
+
"TEST": "/weaver/api/v1/collection/card/get_all_direct_card -> weaver/api/v1/collection/card/list/list_my_story_card_by_npc"
|
1056
|
+
}
|
1057
|
+
},
|
1058
|
+
{
|
1059
|
+
"普通卡池-免费抽卡": {
|
1060
|
+
"PRE": {
|
1061
|
+
"每日免费": "def daily_free(), return uid,使用该uid,则享受每日免费权益",
|
1062
|
+
"月卡免费次数": "def vip_free(), return uid,使用该uid,则享受月卡免费权益",
|
1063
|
+
"星炉熔卡次数": "def reback_free(), return uid,使用该uid,则享受熔炉抽卡权益"
|
1064
|
+
},
|
1065
|
+
"TEST": "/weaver/api/v1/collection/card/get_all_direct_card -> weaver/api/v1/collection/card/list/list_my_story_card_by_npc"
|
1066
|
+
}
|
1067
|
+
}
|
1068
|
+
]
|
1069
|
+
}
|
1070
|
+
|
1071
|
+
generator = GeneratorCaseLangChain(test_input)
|
1072
|
+
result = generator.generator_case()
|
1073
|
+
logger.info(f"生成结果: {result['status']}")
|
1074
|
+
|
1075
|
+
if result["status"] == "success" and "saved_paths" in result:
|
1076
|
+
print("\n生成的测试用例文件路径:\n")
|
1077
|
+
for path in result["saved_paths"]:
|
1078
|
+
print(f"- {path}")
|