iflow-mcp_galaxyxieyu_api-auto-test 0.1.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.
- atf/__init__.py +48 -0
- atf/assets/__init__.py +0 -0
- atf/assets/report.css +243 -0
- atf/auth.py +99 -0
- atf/case_generator.py +737 -0
- atf/conftest.py +65 -0
- atf/core/__init__.py +40 -0
- atf/core/assert_handler.py +336 -0
- atf/core/config_manager.py +111 -0
- atf/core/globals.py +52 -0
- atf/core/log_manager.py +52 -0
- atf/core/login_handler.py +60 -0
- atf/core/request_handler.py +189 -0
- atf/core/variable_resolver.py +212 -0
- atf/handlers/__init__.py +10 -0
- atf/handlers/notification_handler.py +101 -0
- atf/handlers/report_generator.py +160 -0
- atf/handlers/teardown_handler.py +106 -0
- atf/mcp/__init__.py +1 -0
- atf/mcp/executor.py +469 -0
- atf/mcp/models.py +532 -0
- atf/mcp/tools/__init__.py +1 -0
- atf/mcp/tools/health_tool.py +58 -0
- atf/mcp/tools/metrics_tools.py +132 -0
- atf/mcp/tools/runner_tools.py +380 -0
- atf/mcp/tools/testcase_tools.py +603 -0
- atf/mcp/tools/unittest_tools.py +189 -0
- atf/mcp/utils.py +376 -0
- atf/mcp_server.py +169 -0
- atf/runner.py +134 -0
- atf/unit_case_generator.py +337 -0
- atf/utils/__init__.py +2 -0
- atf/utils/helpers.py +155 -0
- iflow_mcp_galaxyxieyu_api_auto_test-0.1.0.dist-info/METADATA +409 -0
- iflow_mcp_galaxyxieyu_api_auto_test-0.1.0.dist-info/RECORD +37 -0
- iflow_mcp_galaxyxieyu_api_auto_test-0.1.0.dist-info/WHEEL +4 -0
- iflow_mcp_galaxyxieyu_api_auto_test-0.1.0.dist-info/entry_points.txt +2 -0
atf/mcp/executor.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test Executor
|
|
3
|
+
测试执行逻辑
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from atf.core.log_manager import log
|
|
18
|
+
from atf.mcp.models import (
|
|
19
|
+
AssertionResultModel,
|
|
20
|
+
TestResultModel,
|
|
21
|
+
)
|
|
22
|
+
from atf.mcp.utils import (
|
|
23
|
+
PYTEST_TIMEOUT,
|
|
24
|
+
MAX_ERROR_LENGTH,
|
|
25
|
+
MAX_HISTORY_SIZE,
|
|
26
|
+
get_roots,
|
|
27
|
+
resolve_yaml_path,
|
|
28
|
+
expected_py_path,
|
|
29
|
+
load_yaml_file,
|
|
30
|
+
parse_testcase_input,
|
|
31
|
+
parse_unittest_input,
|
|
32
|
+
detect_testcase_type,
|
|
33
|
+
truncate_text,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# 测试执行结果存储(内存中)
|
|
38
|
+
_test_execution_history: list[dict] = []
|
|
39
|
+
_history_lock = threading.Lock()
|
|
40
|
+
|
|
41
|
+
# api-auto-test 包的安装目录(用于获取依赖列表)
|
|
42
|
+
_ATF_ROOT = Path(__file__).parent.parent.parent.parent
|
|
43
|
+
_AUTO_INSTALL_DEPS = os.getenv("ATF_AUTO_INSTALL_DEPS", "1").lower() in {"1", "true", "yes"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_report_path(repo_root: Path) -> Path:
|
|
47
|
+
"""获取 HTML 报告路径"""
|
|
48
|
+
report_dir = repo_root / "tests" / "reports"
|
|
49
|
+
report_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
51
|
+
return report_dir / f"report_{timestamp}.html"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _check_python_has_dependencies(python_path: str, required_modules: list[str]) -> tuple[bool, list[str]]:
|
|
55
|
+
"""检查 Python 环境是否包含必要的依赖模块
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
python_path: Python 解释器路径
|
|
59
|
+
required_modules: 需要检查的模块列表
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
(是否全部存在, 缺失的模块列表)
|
|
63
|
+
"""
|
|
64
|
+
missing = []
|
|
65
|
+
for module in required_modules:
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
[python_path, "-c", f"import {module}"],
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
timeout=10,
|
|
71
|
+
)
|
|
72
|
+
if result.returncode != 0:
|
|
73
|
+
missing.append(module)
|
|
74
|
+
return len(missing) == 0, missing
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _load_required_modules_from_requirements() -> list[str]:
|
|
78
|
+
"""返回需要检测的模块列表(写死配置)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
list[str]: 需要检测的模块名列表
|
|
82
|
+
"""
|
|
83
|
+
# 直接定义需要检测的模块(运行测试脚本需要的基础依赖)
|
|
84
|
+
# 注意:urllib3<2.0 因为 macOS 系统 Python 使用 LibreSSL,不兼容 v2 的 OpenSSL 要求
|
|
85
|
+
return ["loguru", "yaml", "requests", "urllib3", "pytest", "mysql", "Crypto"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _install_missing_dependencies(python_path: str, missing_modules: list[str]) -> bool:
|
|
89
|
+
"""为指定的 Python 环境安装缺失的依赖
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
python_path: Python 解释器路径
|
|
93
|
+
missing_modules: 缺失的模块列表
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
bool: 安装是否成功
|
|
97
|
+
"""
|
|
98
|
+
if not missing_modules:
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
log.info(f"正在为 {python_path} 安装缺失依赖: {missing_modules}")
|
|
102
|
+
|
|
103
|
+
# 包名到模块名的映射
|
|
104
|
+
module_to_package = {
|
|
105
|
+
"atf": "-e /Volumes/DATABASE/code/api-auto-test",
|
|
106
|
+
"loguru": "loguru",
|
|
107
|
+
"yaml": "pyyaml",
|
|
108
|
+
"requests": "requests",
|
|
109
|
+
"urllib3": "urllib3<2", # urllib3 v2 需要 OpenSSL 1.1.1+,macOS 使用 LibreSSL 不兼容
|
|
110
|
+
"pytest": "pytest",
|
|
111
|
+
"allure_python_commons": "allure-python-commons",
|
|
112
|
+
"mysql": "mysql-connector-python",
|
|
113
|
+
"crypto": "pycryptodome",
|
|
114
|
+
"pytest_html": "pytest-html",
|
|
115
|
+
"dingtalkchatbot": "DingtalkChatbot",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# 映射模块名到包名
|
|
119
|
+
packages = []
|
|
120
|
+
for module in missing_modules:
|
|
121
|
+
if module in module_to_package:
|
|
122
|
+
packages.append(module_to_package[module])
|
|
123
|
+
else:
|
|
124
|
+
packages.append(module)
|
|
125
|
+
|
|
126
|
+
if not packages:
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
# 优先升级 pip 以支持现代安装方式
|
|
131
|
+
log.info("升级 pip 以支持现代安装...")
|
|
132
|
+
result = subprocess.run(
|
|
133
|
+
[python_path, "-m", "pip", "install", "--upgrade", "pip"],
|
|
134
|
+
capture_output=True,
|
|
135
|
+
text=True,
|
|
136
|
+
timeout=120,
|
|
137
|
+
)
|
|
138
|
+
if result.returncode != 0:
|
|
139
|
+
log.warning(f"pip 升级失败,继续尝试安装: {result.stderr}")
|
|
140
|
+
|
|
141
|
+
# 安装依赖
|
|
142
|
+
result = subprocess.run(
|
|
143
|
+
[python_path, "-m", "pip", "install"] + packages,
|
|
144
|
+
capture_output=True,
|
|
145
|
+
text=True,
|
|
146
|
+
timeout=180,
|
|
147
|
+
)
|
|
148
|
+
if result.returncode == 0:
|
|
149
|
+
log.info(f"✅ 依赖安装成功: {packages}")
|
|
150
|
+
return True
|
|
151
|
+
else:
|
|
152
|
+
log.warning(f"❌ 依赖安装失败: {result.stderr}")
|
|
153
|
+
return False
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
log.warning(f"安装依赖时出错: {exc}")
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_python_path(repo_root: Path) -> str:
|
|
160
|
+
"""
|
|
161
|
+
获取项目可用的 Python 解释器路径。
|
|
162
|
+
|
|
163
|
+
优先级顺序:
|
|
164
|
+
1. 检测项目 venv 依赖,缺失则自动安装到项目 venv
|
|
165
|
+
2. 使用项目自身的 venv
|
|
166
|
+
3. uv run (当项目包含 pyproject.toml 且 uv 可用时)
|
|
167
|
+
4. 系统 Python 解释器作为回退
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
repo_root: 项目根目录路径
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
str: Python 解释器路径
|
|
174
|
+
"""
|
|
175
|
+
# 从 api-auto-test/requirements.txt 读取需要检测的依赖
|
|
176
|
+
required_modules = _load_required_modules_from_requirements()
|
|
177
|
+
|
|
178
|
+
# 查找项目 venv
|
|
179
|
+
venv_paths = [
|
|
180
|
+
repo_root / ".venv" / "bin" / "python",
|
|
181
|
+
repo_root / "venv" / "bin" / "python",
|
|
182
|
+
]
|
|
183
|
+
conda_paths = [
|
|
184
|
+
repo_root / ".conda" / "bin" / "python",
|
|
185
|
+
repo_root / "conda" / "bin" / "python",
|
|
186
|
+
]
|
|
187
|
+
project_pythons = [p for p in venv_paths + conda_paths if p.exists() and os.access(p, os.X_OK)]
|
|
188
|
+
|
|
189
|
+
# api-auto-test 的 venv(包含 atf 包)
|
|
190
|
+
api_auto_test_venv = _ATF_ROOT / ".venv" / "bin" / "python"
|
|
191
|
+
|
|
192
|
+
# 优先尝试项目自身的 venv
|
|
193
|
+
for venv_python in project_pythons:
|
|
194
|
+
has_deps, missing = _check_python_has_dependencies(str(venv_python), required_modules)
|
|
195
|
+
if has_deps:
|
|
196
|
+
log.info(f"使用项目 venv: {venv_python}")
|
|
197
|
+
return str(venv_python)
|
|
198
|
+
else:
|
|
199
|
+
log.warning(f"项目 venv 缺少依赖: {missing},正在自动安装...")
|
|
200
|
+
if _AUTO_INSTALL_DEPS:
|
|
201
|
+
# 自动安装缺失的依赖到项目 venv
|
|
202
|
+
if _install_missing_dependencies(str(venv_python), missing):
|
|
203
|
+
# 再次验证
|
|
204
|
+
has_deps, _ = _check_python_has_dependencies(str(venv_python), required_modules)
|
|
205
|
+
if has_deps:
|
|
206
|
+
log.info(f"✅ 依赖安装成功,使用项目 venv: {venv_python}")
|
|
207
|
+
return str(venv_python)
|
|
208
|
+
else:
|
|
209
|
+
log.warning("已禁用自动依赖安装(ATF_AUTO_INSTALL_DEPS=0),将继续使用当前 venv")
|
|
210
|
+
|
|
211
|
+
# 如果安装失败,继续使用项目 venv(至少其他项目依赖可用)
|
|
212
|
+
log.warning(f"⚠️ 部分依赖安装失败,继续使用项目 venv: {venv_python}")
|
|
213
|
+
return str(venv_python)
|
|
214
|
+
|
|
215
|
+
# 检查 api-auto-test 的 venv 是否可用(当项目没有 venv 时)
|
|
216
|
+
if api_auto_test_venv.exists() and os.access(api_auto_test_venv, os.X_OK):
|
|
217
|
+
has_deps, _ = _check_python_has_dependencies(str(api_auto_test_venv), required_modules)
|
|
218
|
+
if has_deps:
|
|
219
|
+
log.info(f"项目无 venv,使用 api-auto-test venv: {api_auto_test_venv}")
|
|
220
|
+
return str(api_auto_test_venv)
|
|
221
|
+
|
|
222
|
+
# 优先使用 uv run (需要 pyproject.toml 且 uv 可用时)
|
|
223
|
+
if (repo_root / "pyproject.toml").exists():
|
|
224
|
+
uv_path = shutil.which("uv")
|
|
225
|
+
if uv_path:
|
|
226
|
+
log.info(f"使用 uv 运行测试")
|
|
227
|
+
return "uv"
|
|
228
|
+
log.warning("pyproject.toml 存在但 uv 未安装")
|
|
229
|
+
|
|
230
|
+
# 回退到系统 Python
|
|
231
|
+
log.warning(f"未找到项目 Python 解释器,回退到系统 Python: {sys.executable}")
|
|
232
|
+
return sys.executable
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def run_pytest(pytest_path: str, repo_root: Path, python_path: str | None = None) -> dict:
|
|
236
|
+
"""执行 pytest 并返回结果
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
pytest_path: pytest 文件路径
|
|
240
|
+
repo_root: 项目根目录
|
|
241
|
+
python_path: 可选的 Python 解释器路径,如果不指定则自动检测
|
|
242
|
+
"""
|
|
243
|
+
start_time = time.time()
|
|
244
|
+
result_data = {
|
|
245
|
+
"test_name": "",
|
|
246
|
+
"status": "error",
|
|
247
|
+
"duration": 0.0,
|
|
248
|
+
"assertions": [],
|
|
249
|
+
"error_message": None,
|
|
250
|
+
"traceback": None,
|
|
251
|
+
"report_path": None,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
# 如果指定了 Python 路径则使用它,否则自动检测
|
|
256
|
+
if python_path:
|
|
257
|
+
log.info(f"使用指定的 Python: {python_path}")
|
|
258
|
+
else:
|
|
259
|
+
python_path = get_python_path(repo_root)
|
|
260
|
+
|
|
261
|
+
# 检查项目 venv 是否包含 atf 模块
|
|
262
|
+
env = os.environ.copy()
|
|
263
|
+
if python_path != "uv":
|
|
264
|
+
has_atf, _ = _check_python_has_dependencies(python_path, ["atf"])
|
|
265
|
+
if not has_atf:
|
|
266
|
+
# 添加 api-auto-test 到 PYTHONPATH,让测试脚本能导入 atf 模块
|
|
267
|
+
env["PYTHONPATH"] = f"{_ATF_ROOT}:{env.get('PYTHONPATH', '')}"
|
|
268
|
+
log.info(f"项目 venv 缺少 atf 模块,通过 PYTHONPATH 添加 api-auto-test")
|
|
269
|
+
|
|
270
|
+
# 构建 pytest 命令(使用 pytest-html 生成报告,无需 Java)
|
|
271
|
+
report_path = _get_report_path(repo_root)
|
|
272
|
+
css_path = _ATF_ROOT / "atf" / "assets" / "report.css"
|
|
273
|
+
|
|
274
|
+
base_args = [pytest_path, "-v", "--tb=short", f"--html={report_path}", "--self-contained-html"]
|
|
275
|
+
if css_path.exists():
|
|
276
|
+
base_args.append(f"--css={css_path}")
|
|
277
|
+
|
|
278
|
+
if python_path == "uv":
|
|
279
|
+
cmd = ["uv", "run", "pytest"] + base_args
|
|
280
|
+
else:
|
|
281
|
+
cmd = [python_path, "-m", "pytest"] + base_args
|
|
282
|
+
|
|
283
|
+
log.info(f"执行测试命令: {' '.join(cmd)}")
|
|
284
|
+
|
|
285
|
+
process = subprocess.Popen(
|
|
286
|
+
cmd,
|
|
287
|
+
cwd=str(repo_root),
|
|
288
|
+
stdout=subprocess.PIPE,
|
|
289
|
+
stderr=subprocess.PIPE,
|
|
290
|
+
text=True,
|
|
291
|
+
env=env,
|
|
292
|
+
)
|
|
293
|
+
try:
|
|
294
|
+
stdout, stderr = process.communicate(timeout=PYTEST_TIMEOUT)
|
|
295
|
+
except subprocess.TimeoutExpired:
|
|
296
|
+
process.kill()
|
|
297
|
+
stdout, stderr = process.communicate()
|
|
298
|
+
result_data["error_message"] = "测试执行超时(超过5分钟)"
|
|
299
|
+
result_data["traceback"] = "进程被强制终止"
|
|
300
|
+
end_time = time.time()
|
|
301
|
+
result_data["duration"] = round(end_time - start_time, 2)
|
|
302
|
+
return result_data
|
|
303
|
+
finally:
|
|
304
|
+
# 确保进程已终止并清理资源
|
|
305
|
+
if process.returncode is None:
|
|
306
|
+
process.kill()
|
|
307
|
+
process.wait()
|
|
308
|
+
if process.stdout:
|
|
309
|
+
process.stdout.close()
|
|
310
|
+
if process.stderr:
|
|
311
|
+
process.stderr.close()
|
|
312
|
+
|
|
313
|
+
end_time = time.time()
|
|
314
|
+
duration = round(end_time - start_time, 2)
|
|
315
|
+
|
|
316
|
+
# 从路径提取测试名称
|
|
317
|
+
test_name = Path(pytest_path).stem.replace("test_", "")
|
|
318
|
+
|
|
319
|
+
result_data["test_name"] = test_name
|
|
320
|
+
result_data["duration"] = duration
|
|
321
|
+
|
|
322
|
+
# 解析测试结果
|
|
323
|
+
if process.returncode == 0:
|
|
324
|
+
result_data["status"] = "passed"
|
|
325
|
+
else:
|
|
326
|
+
result_data["status"] = "failed"
|
|
327
|
+
# 提取错误信息(使用常量截断)
|
|
328
|
+
result_data["error_message"] = truncate_text(stderr)
|
|
329
|
+
result_data["traceback"] = truncate_text(stdout)
|
|
330
|
+
|
|
331
|
+
# 尝试解析断言失败信息
|
|
332
|
+
if "FAILED" in stdout or "AssertionError" in stderr:
|
|
333
|
+
result_data["assertions"] = [
|
|
334
|
+
AssertionResultModel(
|
|
335
|
+
assertion_type="unknown",
|
|
336
|
+
passed=False,
|
|
337
|
+
message=f"测试失败,返回码: {process.returncode}"
|
|
338
|
+
).model_dump()
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
# 尝试从 stdout 提取统计信息
|
|
342
|
+
if "passed" in stdout.lower() or "failed" in stdout.lower():
|
|
343
|
+
# 简化处理:创建一个通用的断言结果
|
|
344
|
+
result_data["assertions"] = [
|
|
345
|
+
AssertionResultModel(
|
|
346
|
+
assertion_type="execution",
|
|
347
|
+
passed=process.returncode == 0,
|
|
348
|
+
message=stdout.strip()[-200:] if stdout else "执行完成"
|
|
349
|
+
).model_dump()
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
# 报告路径信息
|
|
353
|
+
if report_path.exists():
|
|
354
|
+
result_data["report_path"] = str(report_path)
|
|
355
|
+
log.info(f"HTML 报告已生成: {report_path}")
|
|
356
|
+
|
|
357
|
+
except Exception as exc:
|
|
358
|
+
result_data["error_message"] = str(exc)
|
|
359
|
+
log.error(f"执行 pytest 失败: {exc}")
|
|
360
|
+
|
|
361
|
+
return result_data
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def execute_single_test(yaml_path: str, repo_root: Path, python_path: str | None = None) -> TestResultModel:
|
|
365
|
+
"""执行单个测试用例并返回结果
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
yaml_path: YAML 文件路径
|
|
369
|
+
repo_root: 项目根目录
|
|
370
|
+
python_path: 可选的 Python 解释器路径
|
|
371
|
+
"""
|
|
372
|
+
try:
|
|
373
|
+
# 必须传递 workspace 参数,确保路径解析正确
|
|
374
|
+
workspace = str(repo_root)
|
|
375
|
+
yaml_full_path, yaml_relative_path, _ = resolve_yaml_path(yaml_path, workspace)
|
|
376
|
+
|
|
377
|
+
log.info(f"[execute_single_test] yaml_path={yaml_path}, workspace={workspace}")
|
|
378
|
+
log.info(f"[execute_single_test] yaml_full_path={yaml_full_path}")
|
|
379
|
+
|
|
380
|
+
data = load_yaml_file(yaml_full_path)
|
|
381
|
+
|
|
382
|
+
# 使用统一的类型检测函数
|
|
383
|
+
testcase_type = detect_testcase_type(data)
|
|
384
|
+
|
|
385
|
+
# 解析测试用例
|
|
386
|
+
if testcase_type == "unittest":
|
|
387
|
+
testcase_model = parse_unittest_input(data)
|
|
388
|
+
else:
|
|
389
|
+
testcase_model = parse_testcase_input(data)
|
|
390
|
+
test_name = testcase_model.name
|
|
391
|
+
|
|
392
|
+
py_full_path, _ = expected_py_path(yaml_full_path, test_name, workspace)
|
|
393
|
+
log.info(f"[execute_single_test] py_full_path={py_full_path}")
|
|
394
|
+
|
|
395
|
+
if not py_full_path.exists():
|
|
396
|
+
return TestResultModel(
|
|
397
|
+
test_name=test_name,
|
|
398
|
+
status="error",
|
|
399
|
+
duration=0.0,
|
|
400
|
+
assertions=[],
|
|
401
|
+
error_message="pytest 文件不存在,请先生成",
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# 执行测试(传入自定义 Python 路径)
|
|
405
|
+
result_data = run_pytest(str(py_full_path), repo_root, python_path)
|
|
406
|
+
|
|
407
|
+
return TestResultModel(
|
|
408
|
+
test_name=result_data["test_name"],
|
|
409
|
+
status=result_data["status"],
|
|
410
|
+
duration=result_data["duration"],
|
|
411
|
+
assertions=[
|
|
412
|
+
AssertionResultModel(**a) for a in result_data.get("assertions", [])
|
|
413
|
+
],
|
|
414
|
+
error_message=result_data.get("error_message"),
|
|
415
|
+
traceback=result_data.get("traceback"),
|
|
416
|
+
report_path=result_data.get("report_path"),
|
|
417
|
+
)
|
|
418
|
+
except Exception as exc:
|
|
419
|
+
log.error(f"执行单个测试失败: {exc}")
|
|
420
|
+
return TestResultModel(
|
|
421
|
+
test_name=Path(yaml_path).stem,
|
|
422
|
+
status="error",
|
|
423
|
+
duration=0.0,
|
|
424
|
+
assertions=[],
|
|
425
|
+
error_message=str(exc),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def save_to_history(
|
|
430
|
+
run_id: str,
|
|
431
|
+
total: int,
|
|
432
|
+
passed: int,
|
|
433
|
+
failed: int,
|
|
434
|
+
skipped: int,
|
|
435
|
+
duration: float,
|
|
436
|
+
test_names: list[str],
|
|
437
|
+
) -> None:
|
|
438
|
+
"""保存执行结果到历史记录"""
|
|
439
|
+
global _test_execution_history
|
|
440
|
+
with _history_lock:
|
|
441
|
+
_test_execution_history.append({
|
|
442
|
+
"run_id": run_id,
|
|
443
|
+
"timestamp": datetime.now().isoformat(),
|
|
444
|
+
"total": total,
|
|
445
|
+
"passed": passed,
|
|
446
|
+
"failed": failed,
|
|
447
|
+
"skipped": skipped,
|
|
448
|
+
"duration": duration,
|
|
449
|
+
"test_names": test_names,
|
|
450
|
+
})
|
|
451
|
+
# 使用常量添加容量限制,防止内存溢出
|
|
452
|
+
if len(_test_execution_history) > MAX_HISTORY_SIZE:
|
|
453
|
+
_test_execution_history = _test_execution_history[-MAX_HISTORY_SIZE:]
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def get_history(limit: int = 10) -> list[dict]:
|
|
457
|
+
"""获取历史记录"""
|
|
458
|
+
global _test_execution_history
|
|
459
|
+
with _history_lock:
|
|
460
|
+
return _test_execution_history[-limit:] if limit > 0 else _test_execution_history
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
__all__ = [
|
|
464
|
+
"get_python_path",
|
|
465
|
+
"run_pytest",
|
|
466
|
+
"execute_single_test",
|
|
467
|
+
"save_to_history",
|
|
468
|
+
"get_history",
|
|
469
|
+
]
|