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/mcp/models.py ADDED
@@ -0,0 +1,532 @@
1
+ """
2
+ Pydantic Models for MCP Server
3
+ 所有数据模型定义,用于输入验证和响应格式化
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from typing import Any, Literal
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
12
+
13
+
14
+ def contains_chinese(text: str) -> bool:
15
+ """检测字符串是否包含中文字符"""
16
+ if not text:
17
+ return False
18
+ return bool(re.search(r'[\u4e00-\u9fff]', str(text)))
19
+
20
+
21
+ class AssertionModel(BaseModel):
22
+ """断言模型"""
23
+ model_config = ConfigDict(extra="forbid")
24
+
25
+ type: str
26
+ field: str | None = None
27
+ expected: Any | None = None
28
+ container: Any | None = None
29
+ query: str | None = None
30
+
31
+
32
+ class StepModel(BaseModel):
33
+ """测试步骤模型"""
34
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
35
+
36
+ id: str
37
+ path: str
38
+ method: str
39
+ headers: dict | None = None
40
+ data: Any | None = None
41
+ params: dict | None = None
42
+ files: dict | None = None
43
+ project: str | None = None
44
+ assert_: list[AssertionModel] | None = Field(default=None, alias="assert")
45
+
46
+ @model_validator(mode="after")
47
+ def validate_required(self) -> "StepModel":
48
+ if not self.id:
49
+ raise ValueError("steps.id 不能为空")
50
+ if not self.path:
51
+ raise ValueError("steps.path 不能为空")
52
+ if not self.method:
53
+ raise ValueError("steps.method 不能为空")
54
+ return self
55
+
56
+
57
+ class TeardownModel(BaseModel):
58
+ """清理步骤模型"""
59
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
60
+
61
+ id: str
62
+ operation_type: Literal["api", "db"]
63
+ path: str | None = None
64
+ method: str | None = None
65
+ headers: dict | None = None
66
+ data: Any | None = None
67
+ params: dict | None = None
68
+ files: dict | None = None
69
+ project: str | None = None
70
+ assert_: list[AssertionModel] | None = Field(default=None, alias="assert")
71
+ query: str | None = None
72
+
73
+ @model_validator(mode="after")
74
+ def validate_operation(self) -> "TeardownModel":
75
+ if self.operation_type == "api":
76
+ missing = []
77
+ if not self.path:
78
+ missing.append("path")
79
+ if not self.method:
80
+ missing.append("method")
81
+ if self.headers is None:
82
+ missing.append("headers")
83
+ if self.data is None:
84
+ missing.append("data")
85
+ if missing:
86
+ raise ValueError(f"teardowns.api 缺少必填字段: {', '.join(missing)}")
87
+ if self.operation_type == "db" and not self.query:
88
+ raise ValueError("teardowns.db 缺少必填字段: query")
89
+ return self
90
+
91
+
92
+ class AllureModel(BaseModel):
93
+ """Allure 报告配置模型"""
94
+ model_config = ConfigDict(extra="forbid")
95
+
96
+ epic: str | None = None
97
+ feature: str | None = None
98
+ story: str | None = None
99
+
100
+
101
+ # ==================== 单元测试模型 ====================
102
+
103
+
104
+ class MockModel(BaseModel):
105
+ """Mock 配置模型"""
106
+ model_config = ConfigDict(extra="forbid")
107
+
108
+ target: str # mock 目标路径,如 "app.services.user_service.UserRepository"
109
+ method: str | None = None # 方法名
110
+ return_value: Any | None = None # 返回值
111
+ side_effect: Any | None = None # 副作用(异常或可调用对象)
112
+
113
+
114
+ class UnitTestInputModel(BaseModel):
115
+ """单元测试输入参数模型"""
116
+ model_config = ConfigDict(extra="forbid")
117
+
118
+ args: list[Any] | None = None # 位置参数
119
+ kwargs: dict[str, Any] | None = None # 关键字参数
120
+
121
+
122
+ class UnitAssertionModel(BaseModel):
123
+ """单元测试断言模型"""
124
+ model_config = ConfigDict(extra="forbid")
125
+
126
+ type: str # equals, not_equals, contains, raises, called_once, called_with, etc.
127
+ field: str | None = None # JSONPath 字段路径
128
+ expected: Any | None = None # 期望值
129
+ mock: str | None = None # mock 名称(用于 mock 相关断言)
130
+ args: list[Any] | None = None # 期望的调用参数
131
+ kwargs: dict[str, Any] | None = None # 期望的关键字参数
132
+ exception: str | None = None # 期望的异常类型
133
+ message: str | None = None # 期望的异常消息
134
+
135
+
136
+ class UnitTestCaseModel(BaseModel):
137
+ """单个单元测试用例模型"""
138
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
139
+
140
+ id: str
141
+ description: str | None = None
142
+ mocks: list[MockModel] | None = None
143
+ inputs: UnitTestInputModel | None = None
144
+ assert_: list[UnitAssertionModel] | None = Field(default=None, alias="assert")
145
+
146
+ @model_validator(mode="after")
147
+ def validate_required(self) -> "UnitTestCaseModel":
148
+ if not self.id:
149
+ raise ValueError("unittest.cases.id 不能为空")
150
+ return self
151
+
152
+
153
+ class UnitTestTargetModel(BaseModel):
154
+ """被测目标模型"""
155
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
156
+
157
+ module: str # 被测模块路径
158
+ class_: str | None = Field(default=None, alias="class") # 被测类名
159
+ function: str | None = None # 被测函数名
160
+
161
+ @model_validator(mode="after")
162
+ def validate_target(self) -> "UnitTestTargetModel":
163
+ if not self.module:
164
+ raise ValueError("unittest.target.module 不能为空")
165
+ return self
166
+
167
+
168
+ class UnitTestFixtureModel(BaseModel):
169
+ """测试夹具模型"""
170
+ model_config = ConfigDict(extra="forbid")
171
+
172
+ type: str # patch, cleanup, setup_db, etc.
173
+ target: str | None = None
174
+ value: Any | None = None
175
+ action: str | None = None
176
+
177
+
178
+ class UnitTestFixturesModel(BaseModel):
179
+ """测试夹具集合模型"""
180
+ model_config = ConfigDict(extra="forbid")
181
+
182
+ setup: list[UnitTestFixtureModel] | None = None
183
+ teardown: list[UnitTestFixtureModel] | None = None
184
+
185
+
186
+ class UnitTestModel(BaseModel):
187
+ """单元测试模型"""
188
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
189
+
190
+ name: str
191
+ description: str | None = None
192
+ env_type: Literal["venv", "conda", "uv"] = "venv" # 运行环境的虚拟环境类型
193
+ target: UnitTestTargetModel
194
+ allure: AllureModel | None = None
195
+ cases: list[UnitTestCaseModel]
196
+ fixtures: UnitTestFixturesModel | None = None
197
+
198
+ @model_validator(mode="after")
199
+ def validate_required(self) -> "UnitTestModel":
200
+ if not self.name:
201
+ raise ValueError("unittest.name 不能为空")
202
+ if contains_chinese(self.name):
203
+ raise ValueError(
204
+ f"unittest.name 不能包含中文字符: '{self.name}'\n"
205
+ "请使用英文命名,例如: user_service_test, calculate_total_test"
206
+ )
207
+ if not self.cases:
208
+ raise ValueError("unittest.cases 不能为空")
209
+ return self
210
+
211
+
212
+ # ==================== 集成测试模型 ====================
213
+
214
+
215
+ class TestcaseModel(BaseModel):
216
+ """集成测试用例模型"""
217
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
218
+
219
+ name: str
220
+ description: str | None = None
221
+ host: str | None = None # 可选的测试服务地址
222
+ allure: AllureModel | None = None
223
+ steps: list[StepModel]
224
+ teardowns: list[TeardownModel] | None = None
225
+
226
+ @model_validator(mode="after")
227
+ def validate_required(self) -> "TestcaseModel":
228
+ if not self.name:
229
+ raise ValueError("testcase.name 不能为空")
230
+ if contains_chinese(self.name):
231
+ raise ValueError(
232
+ f"testcase.name 不能包含中文字符: '{self.name}'\n"
233
+ "请使用英文命名,例如: test_user_login, get_product_list"
234
+ )
235
+ if not self.steps:
236
+ raise ValueError("testcase.steps 不能为空")
237
+ return self
238
+
239
+
240
+ # ==================== 响应模型 ====================
241
+
242
+
243
+ class GenerateResponse(BaseModel):
244
+ """生成操作响应"""
245
+ model_config = ConfigDict(extra="forbid")
246
+
247
+ status: Literal["ok", "error"]
248
+ request_id: str | None = None
249
+ written_files: list[str]
250
+ # 名称转换信息
251
+ name_mapping: dict | None = None # {"original": "Health Check API", "safe": "health_check_api", "class": "HealthCheckApi"}
252
+ # 语法校验结果
253
+ syntax_valid: bool | None = None
254
+ syntax_errors: list[str] | None = None
255
+ # dry_run 模式预览
256
+ dry_run: bool = False
257
+ code_preview: str | None = None # dry_run 模式下的代码预览
258
+ # 错误信息
259
+ error_code: str | None = None
260
+ retryable: bool | None = None
261
+ error_message: str | None = None
262
+ error_details: dict | None = None # 更详细的错误信息,用于大模型理解
263
+
264
+
265
+ class HealthResponse(BaseModel):
266
+ """健康检查响应"""
267
+ model_config = ConfigDict(extra="forbid")
268
+
269
+ status: Literal["ok", "error"]
270
+ request_id: str | None = None
271
+ version: str
272
+ repo_root: str
273
+ tests_root: str
274
+ test_cases_root: str
275
+ error_code: str | None = None
276
+ retryable: bool | None = None
277
+ error_message: str | None = None
278
+ error_details: dict | None = None
279
+
280
+
281
+ class ListTestcasesResponse(BaseModel):
282
+ """列出测试用例响应"""
283
+ model_config = ConfigDict(extra="forbid")
284
+
285
+ status: Literal["ok", "error"]
286
+ request_id: str | None = None
287
+ testcases: list[str]
288
+ error_code: str | None = None
289
+ retryable: bool | None = None
290
+ error_message: str | None = None
291
+ error_details: dict | None = None
292
+
293
+
294
+ class ReadTestcaseResponse(BaseModel):
295
+ """读取测试用例响应"""
296
+ model_config = ConfigDict(extra="forbid")
297
+
298
+ status: Literal["ok", "error"]
299
+ request_id: str | None = None
300
+ yaml_path: str
301
+ mode: Literal["summary", "full"]
302
+ testcase: dict[str, Any] | None
303
+ error_code: str | None = None
304
+ retryable: bool | None = None
305
+ error_message: str | None = None
306
+ error_details: dict | None = None
307
+
308
+
309
+ class GetTestcaseResponse(BaseModel):
310
+ """获取测试用例响应(整合读取 + 校验)"""
311
+ model_config = ConfigDict(extra="forbid")
312
+
313
+ status: Literal["ok", "error"]
314
+ request_id: str | None = None
315
+ yaml_path: str
316
+ mode: Literal["summary", "full"]
317
+ testcase: dict[str, Any] | None # 测试用例内容
318
+ is_valid: bool # 是否通过校验
319
+ errors: list[str] # 校验错误列表(空表示通过)
320
+ error_code: str | None = None
321
+ retryable: bool | None = None
322
+ error_message: str | None = None
323
+ error_details: dict | None = None
324
+
325
+
326
+ class ValidateTestcaseResponse(BaseModel):
327
+ """校验测试用例响应(已废弃,请使用 get_testcase)"""
328
+ model_config = ConfigDict(extra="forbid")
329
+
330
+ status: Literal["ok", "error"]
331
+ request_id: str | None = None
332
+ errors: list[str]
333
+ error_code: str | None = None
334
+ retryable: bool | None = None
335
+ error_message: str | None = None
336
+ error_details: dict | None = None
337
+
338
+
339
+ class RegenerateResponse(BaseModel):
340
+ """重新生成响应"""
341
+ model_config = ConfigDict(extra="forbid")
342
+
343
+ status: Literal["ok", "error"]
344
+ request_id: str | None = None
345
+ written_files: list[str]
346
+ error_code: str | None = None
347
+ retryable: bool | None = None
348
+ error_message: str | None = None
349
+ error_details: dict | None = None
350
+
351
+
352
+ class DeleteTestcaseResponse(BaseModel):
353
+ """删除测试用例响应"""
354
+ model_config = ConfigDict(extra="forbid")
355
+
356
+ status: Literal["ok", "error"]
357
+ request_id: str | None = None
358
+ deleted_files: list[str]
359
+ error_code: str | None = None
360
+ retryable: bool | None = None
361
+ error_message: str | None = None
362
+ error_details: dict | None = None
363
+
364
+
365
+ # ==================== 测试执行结果模型 ====================
366
+
367
+
368
+ class AssertionResultModel(BaseModel):
369
+ """断言结果模型"""
370
+ model_config = ConfigDict(extra="forbid")
371
+
372
+ assertion_type: str # 断言类型: equals, contains, status_code, etc.
373
+ field: str | None = None # 字段路径
374
+ expected: Any | None = None # 期望值
375
+ actual: Any | None = None # 实际值
376
+ passed: bool # 是否通过
377
+ message: str | None = None # 失败时的消息
378
+
379
+
380
+ class TestResultModel(BaseModel):
381
+ """单个测试用例执行结果"""
382
+ model_config = ConfigDict(extra="forbid")
383
+
384
+ test_name: str # 测试名称
385
+ status: Literal["passed", "failed", "error", "skipped"] # 执行状态
386
+ duration: float # 执行时间(秒)
387
+ assertions: list[AssertionResultModel] # 断言结果列表
388
+ error_message: str | None = None # 错误信息
389
+ traceback: str | None = None # 错误堆栈
390
+ report_path: str | None = None # HTML 报告路径
391
+
392
+
393
+ class RunTestcaseResponse(BaseModel):
394
+ """单个测试用例执行响应"""
395
+ model_config = ConfigDict(extra="forbid")
396
+
397
+ status: Literal["ok", "error"]
398
+ request_id: str | None = None
399
+ test_name: str
400
+ yaml_path: str | None = None
401
+ py_path: str | None = None
402
+ result: TestResultModel | None = None
403
+ error_code: str | None = None
404
+ retryable: bool | None = None
405
+ error_message: str | None = None
406
+ error_details: dict | None = None
407
+
408
+
409
+ class BatchRunResponse(BaseModel):
410
+ """批量执行响应"""
411
+ model_config = ConfigDict(extra="forbid")
412
+
413
+ status: Literal["ok", "error"]
414
+ request_id: str | None = None
415
+ total: int # 总数
416
+ passed: int # 通过
417
+ failed: int # 失败
418
+ skipped: int # 跳过
419
+ duration: float # 总耗时(秒)
420
+ results: list[TestResultModel] # 每个测试用例的结果
421
+ error_code: str | None = None
422
+ retryable: bool | None = None
423
+ error_message: str | None = None
424
+ error_details: dict | None = None
425
+
426
+
427
+ class RunTestsResponse(BaseModel):
428
+ """统一测试执行响应(支持单用例和批量)"""
429
+ model_config = ConfigDict(extra="forbid")
430
+
431
+ status: Literal["ok", "error"]
432
+ request_id: str | None = None
433
+ mode: Literal["single", "batch"] # 执行模式
434
+ # 单用例模式字段
435
+ test_name: str | None = None
436
+ yaml_path: str | None = None
437
+ py_path: str | None = None
438
+ result: TestResultModel | None = None
439
+ # 批量模式字段
440
+ total: int | None = None
441
+ passed: int | None = None
442
+ failed: int | None = None
443
+ skipped: int | None = None
444
+ duration: float | None = None
445
+ results: list[TestResultModel] | None = None
446
+ has_failures: bool | None = None
447
+ # 错误信息
448
+ error_code: str | None = None
449
+ retryable: bool | None = None
450
+ error_message: str | None = None
451
+ error_details: dict | None = None
452
+
453
+
454
+ class TestResultHistoryModel(BaseModel):
455
+ """测试结果历史记录"""
456
+ model_config = ConfigDict(extra="forbid")
457
+
458
+ run_id: str # 运行ID
459
+ timestamp: str # 执行时间
460
+ total: int
461
+ passed: int
462
+ failed: int
463
+ skipped: int
464
+ duration: float
465
+ test_names: list[str] # 执行的测试用例列表
466
+
467
+
468
+ class GetTestResultsResponse(BaseModel):
469
+ """获取测试执行历史响应"""
470
+ model_config = ConfigDict(extra="forbid")
471
+
472
+ status: Literal["ok", "error"]
473
+ request_id: str | None = None
474
+ results: list[TestResultHistoryModel]
475
+ error_code: str | None = None
476
+ retryable: bool | None = None
477
+ error_message: str | None = None
478
+ error_details: dict | None = None
479
+
480
+
481
+ class McpMetricsResponse(BaseModel):
482
+ """MCP 工具调用指标响应"""
483
+ model_config = ConfigDict(extra="forbid")
484
+
485
+ status: Literal["ok", "error"]
486
+ request_id: str | None = None
487
+ total: int | None = None
488
+ success: int | None = None
489
+ error: int | None = None
490
+ success_rate: float | None = None
491
+ avg_latency_ms: float | None = None
492
+ p95_latency_ms: float | None = None
493
+ error_codes: dict[str, int] | None = None
494
+ window_minutes: int | None = None
495
+ error_code: str | None = None
496
+ retryable: bool | None = None
497
+ error_message: str | None = None
498
+ error_details: dict | None = None
499
+
500
+
501
+ __all__ = [
502
+ # Models
503
+ "AssertionModel",
504
+ "StepModel",
505
+ "TeardownModel",
506
+ "AllureModel",
507
+ "MockModel",
508
+ "UnitTestInputModel",
509
+ "UnitAssertionModel",
510
+ "UnitTestCaseModel",
511
+ "UnitTestTargetModel",
512
+ "UnitTestFixtureModel",
513
+ "UnitTestFixturesModel",
514
+ "UnitTestModel",
515
+ "TestcaseModel",
516
+ # Responses
517
+ "GenerateResponse",
518
+ "HealthResponse",
519
+ "ListTestcasesResponse",
520
+ "GetTestcaseResponse",
521
+ "ReadTestcaseResponse",
522
+ "ValidateTestcaseResponse",
523
+ "DeleteTestcaseResponse",
524
+ "AssertionResultModel",
525
+ "TestResultModel",
526
+ "RunTestcaseResponse",
527
+ "BatchRunResponse",
528
+ "RunTestsResponse",
529
+ "TestResultHistoryModel",
530
+ "GetTestResultsResponse",
531
+ "McpMetricsResponse",
532
+ ]
@@ -0,0 +1 @@
1
+ # MCP Tools Package
@@ -0,0 +1,58 @@
1
+ """
2
+ Health Check Tool
3
+ MCP 服务健康检查工具
4
+ """
5
+
6
+ import time
7
+
8
+ from mcp.server.fastmcp import FastMCP
9
+
10
+ from atf.mcp.models import HealthResponse
11
+ from atf.mcp.utils import (
12
+ REPO_ROOT,
13
+ TESTS_ROOT,
14
+ TEST_CASES_ROOT,
15
+ new_request_id,
16
+ log_tool_call,
17
+ )
18
+
19
+
20
+ MCP_VERSION = "0.1.0"
21
+
22
+
23
+ def register_health_tool(mcp: FastMCP) -> None:
24
+ """注册健康检查工具"""
25
+
26
+ @mcp.tool(
27
+ name="health_check",
28
+ title="MCP 服务健康检查",
29
+ description="返回服务版本与基础路径信息。",
30
+ )
31
+ def health_check() -> HealthResponse:
32
+ request_id = new_request_id()
33
+ start_time = time.perf_counter()
34
+ try:
35
+ response = HealthResponse(
36
+ status="ok",
37
+ request_id=request_id,
38
+ version=MCP_VERSION,
39
+ repo_root=str(REPO_ROOT),
40
+ tests_root=str(TESTS_ROOT),
41
+ test_cases_root=str(TEST_CASES_ROOT),
42
+ )
43
+ except Exception as exc:
44
+ response = HealthResponse(
45
+ status="error",
46
+ request_id=request_id,
47
+ version=MCP_VERSION,
48
+ repo_root=str(REPO_ROOT),
49
+ tests_root=str(TESTS_ROOT),
50
+ test_cases_root=str(TEST_CASES_ROOT),
51
+ error_code="MCP_HEALTH_ERROR",
52
+ retryable=False,
53
+ error_message=str(exc),
54
+ error_details={"error_type": "health_check_failed"},
55
+ )
56
+ latency_ms = int((time.perf_counter() - start_time) * 1000)
57
+ log_tool_call("health_check", request_id, response.status, latency_ms, response.error_code)
58
+ return response