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/case_generator.py ADDED
@@ -0,0 +1,737 @@
1
+ # @time: 2024-08-13
2
+ # @author: xiaoqq
3
+
4
+ import os
5
+ import re
6
+ import yaml
7
+ from atf.core.log_manager import log
8
+
9
+
10
+ def sanitize_name(name: str) -> str:
11
+ """
12
+ 将名称转换为安全的 Python 标识符
13
+ - 空格/连字符 -> 下划线
14
+ - 中文 -> 保留中文,使用下划线连接
15
+ - 特殊字符 -> 移除
16
+ """
17
+ # 替换空格和连字符为下划线
18
+ result = re.sub(r'[\s\-]+', '_', name)
19
+ # 保留中文,只移除其他非 ASCII 字符
20
+ result = re.sub(r'[^\w\u4e00-\u9fff]+', '', result)
21
+ # 移除非法字符(保留字母、数字、下划线、中文)
22
+ result = re.sub(r'[^\w]+', '', result)
23
+ # 移除开头的数字
24
+ result = re.sub(r'^[0-9]+', '', result)
25
+ # 移除连续下划线
26
+ result = re.sub(r'_+', '_', result)
27
+ # 移除首尾下划线
28
+ result = result.strip('_')
29
+ # 如果结果为空,使用默认名称
30
+ if not result:
31
+ result = 'unnamed_test'
32
+ return result.lower()
33
+
34
+
35
+ def to_class_name(name: str) -> str:
36
+ """
37
+ 将名称转换为 PascalCase 类名
38
+ "health check api" -> "HealthCheckApi"
39
+ "product_list_test" -> "ProductListTest"
40
+ """
41
+ safe_name = sanitize_name(name)
42
+ # 按下划线分割,每个部分首字母大写
43
+ parts = safe_name.split('_')
44
+ return ''.join(part.capitalize() for part in parts if part)
45
+
46
+
47
+ def check_python_syntax(code: str) -> tuple[bool, list[str]]:
48
+ """
49
+ 检查 Python 代码语法
50
+ 返回 (是否有效, 错误列表)
51
+ """
52
+ import ast
53
+ try:
54
+ ast.parse(code)
55
+ return True, []
56
+ except SyntaxError as e:
57
+ error_msg = f"Line {e.lineno}: {e.msg}"
58
+ if e.text:
59
+ error_msg += f" -> {e.text.strip()}"
60
+ return False, [error_msg]
61
+
62
+
63
+ class CaseGenerator:
64
+ """
65
+ 测试用例文件生成器
66
+ """
67
+
68
+ def generate_single(self, yaml_file: str, output_dir: str = None, base_dir: str = None, dry_run: bool = False) -> dict:
69
+ """
70
+ 生成单个测试用例,返回详细结果
71
+
72
+ Args:
73
+ yaml_file: YAML 文件路径
74
+ output_dir: 输出目录
75
+ base_dir: 基准目录
76
+ dry_run: 是否仅预览,不实际写入
77
+
78
+ Returns:
79
+ {
80
+ "success": bool,
81
+ "file_path": str,
82
+ "name_mapping": {"original": str, "safe": str, "class": str},
83
+ "syntax_valid": bool,
84
+ "syntax_errors": list[str],
85
+ "code_preview": str (dry_run 模式),
86
+ "error": str (失败时)
87
+ }
88
+ """
89
+ result = {
90
+ "success": False,
91
+ "file_path": None,
92
+ "name_mapping": None,
93
+ "syntax_valid": None,
94
+ "syntax_errors": None,
95
+ "code_preview": None,
96
+ "error": None
97
+ }
98
+
99
+ try:
100
+ # 计算基准目录
101
+ if base_dir is None:
102
+ base_dir = 'tests/cases'
103
+
104
+ # 计算相对路径(用于生成的代码中)
105
+ relative_yaml_path = os.path.relpath(yaml_file, base_dir)
106
+
107
+ # 加载 YAML
108
+ test_data_raw = self.load_test_data(yaml_file)
109
+ if not test_data_raw:
110
+ result["error"] = f"无法加载 YAML 文件: {yaml_file}"
111
+ return result
112
+
113
+ if not self.validate_test_data(test_data_raw):
114
+ result["error"] = "YAML 数据校验不通过"
115
+ return result
116
+
117
+ test_data = test_data_raw['testcase']
118
+ raw_name = test_data['name']
119
+
120
+ # 名称处理
121
+ safe_name = sanitize_name(raw_name)
122
+ class_name = to_class_name(raw_name)
123
+ result["name_mapping"] = {
124
+ "original": raw_name,
125
+ "safe": safe_name,
126
+ "class": class_name
127
+ }
128
+
129
+ # 生成代码,使用相对路径
130
+ code = self._generate_code(test_data, relative_yaml_path)
131
+
132
+ # 语法校验
133
+ syntax_valid, syntax_errors = check_python_syntax(code)
134
+ result["syntax_valid"] = syntax_valid
135
+ result["syntax_errors"] = syntax_errors
136
+
137
+ if not syntax_valid:
138
+ result["error"] = f"生成的代码存在语法错误: {syntax_errors}"
139
+ result["code_preview"] = code[:500] + "..." if len(code) > 500 else code
140
+ return result
141
+
142
+ # 计算输出路径
143
+ if base_dir is None:
144
+ base_dir = 'tests/cases'
145
+ relative_path = os.path.relpath(yaml_file, base_dir)
146
+ path_components = relative_path.split(os.sep)
147
+ if path_components:
148
+ path_components.pop()
149
+ directory_path = os.path.join(*path_components) if path_components else ""
150
+
151
+ file_name = f'test_{safe_name}.py'
152
+ if output_dir:
153
+ file_path = os.path.join(output_dir, directory_path, file_name)
154
+ else:
155
+ file_path = os.path.join('tests', 'scripts', directory_path, file_name)
156
+
157
+ result["file_path"] = file_path
158
+
159
+ if dry_run:
160
+ result["success"] = True
161
+ result["code_preview"] = code
162
+ return result
163
+
164
+ # 写入文件
165
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
166
+ with open(file_path, 'w', encoding='utf-8') as f:
167
+ f.write(code)
168
+
169
+ result["success"] = True
170
+ log.info(f"已生成测试用例文件: {file_path}")
171
+
172
+ except Exception as e:
173
+ result["error"] = str(e)
174
+ log.error(f"生成测试用例失败: {e}")
175
+
176
+ return result
177
+
178
+ def _generate_code(self, test_data: dict, relative_yaml_path: str) -> str:
179
+ """生成 Python 测试代码字符串,使用相对路径"""
180
+ import io
181
+
182
+ raw_name = test_data['name']
183
+ module_name = sanitize_name(raw_name)
184
+ module_class_name = to_class_name(raw_name)
185
+ description = test_data.get('description')
186
+ case_name = f"test_{module_name} ({description})" if description else f"test_{module_name}"
187
+
188
+ teardowns = test_data.get('teardowns')
189
+ validate_teardowns = self.validate_teardowns(teardowns)
190
+
191
+ # 计算 project_name(从相对路径中提取)
192
+ path_components = relative_yaml_path.split(os.sep)
193
+ if len(path_components) > 1:
194
+ project_name = path_components[0]
195
+ else:
196
+ project_name = "default"
197
+
198
+ allure_epic = test_data.get("allure", {}).get("epic", project_name)
199
+ allure_feature = test_data.get("allure", {}).get("feature")
200
+ allure_story = test_data.get("allure", {}).get("story", module_name)
201
+ testcase_host = test_data.get('host')
202
+
203
+ # 使用 StringIO 生成代码
204
+ f = io.StringIO()
205
+
206
+ f.write(f"# Auto-generated test module for {module_name}\n")
207
+ f.write(f"import os\n")
208
+ f.write(f"import re\n")
209
+ f.write(f"from atf.core.log_manager import log\n")
210
+ f.write(f"from atf.core.globals import Globals\n")
211
+ f.write(f"from atf.core.variable_resolver import VariableResolver\n")
212
+ f.write(f"from atf.core.request_handler import RequestHandler\n")
213
+ f.write(f"from atf.core.assert_handler import AssertHandler\n")
214
+ if validate_teardowns:
215
+ f.write(f"from atf.handlers.teardown_handler import TeardownHandler\n")
216
+ f.write(f"from atf.core.login_handler import LoginHandler\n")
217
+ f.write(f"import allure\n")
218
+ f.write(f"import yaml\n\n")
219
+
220
+ f.write(f"@allure.epic('{allure_epic}')\n")
221
+ if allure_feature:
222
+ f.write(f"@allure.feature('{allure_feature}')\n")
223
+ f.write(f"class Test{module_class_name}:\n")
224
+
225
+ f.write(f" @classmethod\n")
226
+ f.write(f" def setup_class(cls):\n")
227
+ f.write(f" log.info('========== 开始执行测试用例:{case_name} ==========')\n")
228
+ f.write(f" cls.test_case_data = cls.load_test_case_data()\n")
229
+
230
+ if validate_teardowns:
231
+ f.write(f" cls.login_handler = LoginHandler()\n")
232
+ f.write(f" cls.teardowns_dict = {{teardown['id']: teardown for teardown in cls.test_case_data['teardowns']}}\n")
233
+ f.write(f" for teardown in cls.test_case_data.get('teardowns', []):\n")
234
+ f.write(f" project = teardown.get('project')\n")
235
+ f.write(f" if project:\n")
236
+ f.write(f" cls.login_handler.check_and_login_project(project, Globals.get('env'))\n")
237
+
238
+ f.write(f" cls.steps_dict = {{step['id']: step for step in cls.test_case_data['steps']}}\n")
239
+ f.write(f" cls.session_vars = {{}}\n")
240
+ f.write(f" cls.global_vars = Globals.get_data()\n")
241
+
242
+ if testcase_host:
243
+ f.write(f" cls.testcase_host = '{testcase_host}'\n")
244
+
245
+ f.write(f" cls.VR = VariableResolver(global_vars=cls.global_vars, session_vars=cls.session_vars)\n")
246
+ f.write(f" log.info('Setup completed for Test{module_class_name}')\n\n")
247
+
248
+ f.write(f" @staticmethod\n")
249
+ f.write(f" def load_test_case_data():\n")
250
+ # 计算相对路径部分:tests/scripts/ -> tests/cases/
251
+ yaml_dir = os.path.dirname(relative_yaml_path)
252
+ yaml_basename = os.path.basename(relative_yaml_path)
253
+ if yaml_dir:
254
+ f.write(f" yaml_path = os.path.join(os.path.dirname(__file__), '..', 'cases', '{yaml_dir}', '{yaml_basename}')\n")
255
+ else:
256
+ f.write(f" yaml_path = os.path.join(os.path.dirname(__file__), '..', 'cases', '{yaml_basename}')\n")
257
+ f.write(f" with open(yaml_path, 'r', encoding='utf-8') as file:\n")
258
+ f.write(f" test_case_data = yaml.safe_load(file)['testcase']\n")
259
+ f.write(f" return test_case_data\n\n")
260
+
261
+ f.write(f" @allure.story('{allure_story}')\n")
262
+ f.write(f" def test_{module_name}(self):\n")
263
+ f.write(f" log.info('Starting test_{module_name}')\n")
264
+
265
+ for step in test_data['steps']:
266
+ step_id = step['id']
267
+ step_project = step.get("project")
268
+ f.write(f" # Step: {step_id}\n")
269
+ f.write(f" log.info(f'开始执行 step: {step_id}')\n")
270
+ f.write(f" {step_id} = self.steps_dict.get('{step_id}')\n")
271
+
272
+ if testcase_host:
273
+ f.write(f" step_host = self.testcase_host\n")
274
+ elif step_project:
275
+ f.write(f" project_config = self.global_vars.get('{step_project}')\n")
276
+ f.write(f" step_host = project_config['host'] if project_config else ''\n")
277
+ else:
278
+ f.write(f" project_config = self.global_vars.get('{project_name}')\n")
279
+ f.write(f" step_host = project_config['host'] if project_config else ''\n")
280
+
281
+ f.write(f" response = RequestHandler.send_request(\n")
282
+ f.write(f" method={step_id}['method'],\n")
283
+ f.write(f" url=step_host + self.VR.process_data({step_id}['path']),\n")
284
+ f.write(f" headers=self.VR.process_data({step_id}.get('headers')),\n")
285
+ f.write(f" data=self.VR.process_data({step_id}.get('data')),\n")
286
+ f.write(f" params=self.VR.process_data({step_id}.get('params')),\n")
287
+ f.write(f" files=self.VR.process_data({step_id}.get('files'))\n")
288
+ f.write(f" )\n")
289
+ f.write(f" log.info(f'{step_id} 请求结果为:{{response}}')\n")
290
+ f.write(f" self.session_vars['{step_id}'] = response\n")
291
+
292
+ if 'assert' in step:
293
+ if not testcase_host:
294
+ f.write(f" db_config = project_config.get('mysql')\n")
295
+ else:
296
+ f.write(f" db_config = None\n")
297
+ f.write(f" AssertHandler().handle_assertion(\n")
298
+ f.write(f" asserts=self.VR.process_data({step_id}['assert']),\n")
299
+ f.write(f" response=response,\n")
300
+ f.write(f" db_config=db_config\n")
301
+ f.write(f" )\n\n")
302
+
303
+ # teardown 处理
304
+ if validate_teardowns:
305
+ f.write(f" @classmethod\n")
306
+ f.write(f" def teardown_class(cls):\n")
307
+ f.write(f" log.info('Starting teardown for the Test{module_class_name}')\n")
308
+ for teardown_step in teardowns:
309
+ teardown_step_id = teardown_step['id']
310
+ teardown_step_project = teardown_step.get("project")
311
+ f.write(f" {teardown_step_id} = cls.teardowns_dict.get('{teardown_step_id}')\n")
312
+
313
+ if teardown_step_project:
314
+ f.write(f" project_config = cls.global_vars.get('{teardown_step_project}')\n")
315
+ else:
316
+ f.write(f" project_config = cls.global_vars.get('{project_name}')\n")
317
+
318
+ if teardown_step['operation_type'] == 'api':
319
+ f.write(f" response = RequestHandler.send_request(\n")
320
+ f.write(f" method={teardown_step_id}['method'],\n")
321
+ f.write(f" url=project_config['host'] + cls.VR.process_data({teardown_step_id}['path']),\n")
322
+ f.write(f" headers=cls.VR.process_data({teardown_step_id}.get('headers')),\n")
323
+ f.write(f" data=cls.VR.process_data({teardown_step_id}.get('data')),\n")
324
+ f.write(f" params=cls.VR.process_data({teardown_step_id}.get('params')),\n")
325
+ f.write(f" files=cls.VR.process_data({teardown_step_id}.get('files'))\n")
326
+ f.write(f" )\n")
327
+ f.write(f" log.info(f'{teardown_step_id} 请求结果为:{{response}}')\n")
328
+ f.write(f" cls.session_vars['{teardown_step_id}'] = response\n")
329
+
330
+ if 'assert' in teardown_step:
331
+ f.write(f" db_config = project_config.get('mysql')\n")
332
+ f.write(f" AssertHandler().handle_assertion(\n")
333
+ f.write(f" asserts=cls.VR.process_data({teardown_step_id}['assert']),\n")
334
+ f.write(f" response=response,\n")
335
+ f.write(f" db_config=db_config\n")
336
+ f.write(f" )\n\n")
337
+ elif teardown_step['operation_type'] == 'db':
338
+ f.write(f" db_config = project_config.get('mysql')\n")
339
+ f.write(f" TeardownHandler().handle_teardown(\n")
340
+ f.write(f" asserts=cls.VR.process_data({teardown_step_id}),\n")
341
+ f.write(f" db_config=db_config\n")
342
+ f.write(f" )\n\n")
343
+ f.write(f" pass\n")
344
+ else:
345
+ f.write(f" pass\n")
346
+
347
+ f.write(f" log.info('Teardown completed for Test{module_class_name}.')\n")
348
+
349
+ f.write(f"\n log.info(f\"Test case test_{module_name} completed.\")\n")
350
+
351
+ return f.getvalue()
352
+
353
+ def generate_test_cases(self, project_yaml_list=None, output_dir=None, base_dir=None):
354
+ """
355
+ 根据YAML文件生成测试用例并保存到指定目录
356
+ :param project_yaml_list: 列表形式,项目名称或YAML文件路径
357
+ :param output_dir: 测试用例文件生成目录,默认 'tests/scripts'
358
+ :param base_dir: 基准目录,用于计算相对路径,默认 'tests/cases'
359
+ """
360
+ # 如果没有传入project_yaml_list,默认遍历 tests/cases 目录下所有 YAML
361
+ if not project_yaml_list:
362
+ project_yaml_list = ["tests/cases/"]
363
+
364
+ # 基准目录,用于计算相对路径
365
+ if base_dir is None:
366
+ base_dir = 'tests/cases'
367
+
368
+ # 遍历传入的project_yaml_list
369
+ for item in project_yaml_list:
370
+ if os.path.isdir(item): # 如果是项目目录,如 tests/merchant
371
+ self._process_project_dir(item, output_dir, base_dir)
372
+ elif os.path.isfile(item) and item.endswith('.yaml'): # 如果是单个YAML文件
373
+ self._process_single_yaml(item, output_dir, base_dir)
374
+ else: # 如果是项目名称,如 merchant
375
+ project_dir = os.path.join("tests", "cases", item)
376
+ self._process_project_dir(project_dir, output_dir, base_dir)
377
+
378
+ log.info("Test automation framework execution completed")
379
+
380
+ def _process_project_dir(self, project_dir, output_dir, base_dir='tests/cases'):
381
+ """
382
+ 处理项目目录,遍历项目下所有YAML文件生成测试用例
383
+ :param project_dir: 项目目录路径
384
+ :param output_dir: 测试用例文件生成目录
385
+ :param base_dir: 基准目录,用于计算相对路径
386
+ """
387
+ for root, dirs, files in os.walk(project_dir):
388
+ for file in files:
389
+ if file.endswith('.yaml'):
390
+ yaml_file = os.path.join(root, file)
391
+ self._process_single_yaml(yaml_file, output_dir, base_dir)
392
+
393
+ def _process_single_yaml(self, yaml_file, output_dir, base_dir='tests/cases'):
394
+ """
395
+ 处理单个YAML文件,生成对应的测试用例文件
396
+ :param yaml_file: YAML文件路径
397
+ :param output_dir: 测试用例文件生成目录
398
+ :param base_dir: 基准目录,用于计算相对路径
399
+ """
400
+ log.info(f"[CaseGenerator] _process_single_yaml called:")
401
+ log.info(f"[CaseGenerator] yaml_file={yaml_file}")
402
+ log.info(f"[CaseGenerator] output_dir={output_dir}")
403
+ log.info(f"[CaseGenerator] base_dir={base_dir}")
404
+
405
+ # 读取YAML文件内容
406
+ _test_data = self.load_test_data(yaml_file)
407
+ validate_test_data = self.validate_test_data(_test_data)
408
+ if not validate_test_data:
409
+ log.warning(f"{yaml_file} 数据校验不通过,跳过生成测试用例。")
410
+ return
411
+ test_data = _test_data['testcase']
412
+ teardowns = test_data.get('teardowns')
413
+ validate_teardowns = self.validate_teardowns(teardowns)
414
+
415
+ # 计算 relative_path(YAML 相对于 base_dir 的路径)
416
+ relative_path = os.path.relpath(yaml_file, base_dir)
417
+ path_components = relative_path.split(os.sep)
418
+
419
+ # 分离目录和文件名
420
+ if len(path_components) > 1:
421
+ # YAML 在子目录中,如 backend/settings_api.yaml
422
+ relative_dir = os.path.join(*path_components[:-1]) # backend/
423
+ yaml_filename = path_components[-1] # settings_api.yaml
424
+ project_name = path_components[0] # 使用第一级目录名作为 project_name
425
+ else:
426
+ # YAML 在 base_dir 根目录
427
+ relative_dir = ""
428
+ yaml_filename = path_components[0] if path_components else os.path.basename(yaml_file)
429
+ project_name = "default" # 使用默认 project_name
430
+ # 移除最后一个组件(文件名)
431
+ if path_components:
432
+ path_components.pop() # 移除最后一个元素
433
+ directory_path = os.path.join(*path_components) if path_components else ""
434
+ directory_path = directory_path.rstrip(os.sep) # 确保路径不以斜杠结尾
435
+
436
+ log.info(f"[CaseGenerator] Path calculation:")
437
+ log.info(f"[CaseGenerator] relative_path={relative_path}")
438
+ log.info(f"[CaseGenerator] path_components={path_components}")
439
+ log.info(f"[CaseGenerator] project_name={project_name}")
440
+ log.info(f"[CaseGenerator] directory_path={directory_path}")
441
+
442
+ raw_name = test_data['name']
443
+ description = test_data.get('description')
444
+
445
+ # 安全处理名称:支持中文、空格等
446
+ module_name = sanitize_name(raw_name)
447
+ module_class_name = to_class_name(raw_name)
448
+
449
+ # 日志记录中的测试用例名称
450
+ case_name = f"test_{module_name} ({description})" if description is not None else f"test_{module_name}"
451
+ file_name = f'test_{module_name}.py'
452
+
453
+ log.info(f"[CaseGenerator] Name processing: '{raw_name}' -> module='{module_name}', class='{module_class_name}'")
454
+
455
+ # 生成文件路径
456
+ if output_dir:
457
+ file_path = os.path.join(output_dir, directory_path, file_name)
458
+ else:
459
+ file_path = os.path.join('tests', 'scripts', directory_path, file_name)
460
+
461
+ log.info(f"[CaseGenerator] File path generation:")
462
+ log.info(f"[CaseGenerator] file_name={file_name}")
463
+ log.info(f"[CaseGenerator] file_path={file_path}")
464
+ log.info(f"[CaseGenerator] dirname={os.path.dirname(file_path)}")
465
+
466
+ # 检查test_cases中对应的.py文件是否存在,存在则跳过生成
467
+ if os.path.exists(file_path):
468
+ log.info(f"测试用例文件已存在,跳过生成: {file_path}")
469
+ return
470
+
471
+ # 创建目录
472
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
473
+
474
+ allure_epic = test_data.get("allure", {}).get("epic", project_name)
475
+ allure_feature = test_data.get("allure", {}).get("feature")
476
+ allure_story = test_data.get("allure", {}).get("story", module_name)
477
+
478
+ with open(file_path, 'w', encoding='utf-8') as f:
479
+ f.write(f"# Auto-generated test module for {module_name}\n")
480
+ f.write(f"import os\n")
481
+ f.write(f"import re\n")
482
+ f.write(f"from atf.core.log_manager import log\n")
483
+ f.write(f"from atf.core.globals import Globals\n")
484
+ f.write(f"from atf.core.variable_resolver import VariableResolver\n")
485
+ f.write(f"from atf.core.request_handler import RequestHandler\n")
486
+ f.write(f"from atf.core.assert_handler import AssertHandler\n")
487
+ if validate_teardowns:
488
+ f.write(f"from atf.handlers.teardown_handler import TeardownHandler\n")
489
+ f.write(f"from atf.core.login_handler import LoginHandler\n")
490
+ f.write(f"import allure\n")
491
+ f.write(f"import yaml\n\n")
492
+
493
+ f.write(f"@allure.epic('{allure_epic}')\n")
494
+ if allure_feature:
495
+ f.write(f"@allure.feature('{allure_feature}')\n")
496
+ f.write(f"class Test{module_class_name}:\n")
497
+
498
+ f.write(f" @classmethod\n")
499
+ f.write(f" def setup_class(cls):\n")
500
+ f.write(f" log.info('========== 开始执行测试用例:{case_name} ==========')\n")
501
+ f.write(f" cls.test_case_data = cls.load_test_case_data()\n") # 获取测试数据
502
+ # 如果存在teardowns,则将步骤列表转换为字典, 在下面的测试方法中通过 id 查找步骤的信息
503
+ if validate_teardowns:
504
+ f.write(f" cls.login_handler = LoginHandler()\n")
505
+ f.write(f" cls.teardowns_dict = {{teardown['id']: teardown for teardown in cls.test_case_data['teardowns']}}\n")
506
+ f.write(f" for teardown in cls.test_case_data.get('teardowns', []):\n")
507
+ f.write(f" project = teardown.get('project')\n")
508
+ f.write(f" if project:\n")
509
+ f.write(f" cls.login_handler.check_and_login_project(project, Globals.get('env'))\n")
510
+
511
+ # 将步骤列表转换为字典, 在下面的测试方法中通过 id 查找步骤的信息
512
+ f.write(f" cls.steps_dict = {{step['id']: step for step in cls.test_case_data['steps']}}\n")
513
+
514
+ f.write(f" cls.session_vars = {{}}\n")
515
+ f.write(f" cls.global_vars = Globals.get_data()\n") # 获取全局变量
516
+ # f.write(f" cls.db_config = cls.global_vars.get('mysql')\n") # 获取数据库配置
517
+
518
+ # 处理 testcase 级别的 host 配置
519
+ testcase_host = test_data.get('host')
520
+ if testcase_host:
521
+ f.write(f" cls.testcase_host = '{testcase_host}'\n")
522
+
523
+ # 创建VariableResolver实例并保存在类变量中
524
+ f.write(f" cls.VR = VariableResolver(global_vars=cls.global_vars, session_vars=cls.session_vars)\n")
525
+
526
+ f.write(f" log.info('Setup completed for Test{module_class_name}')\n\n")
527
+
528
+ f.write(f" @staticmethod\n")
529
+ f.write(f" def load_test_case_data():\n")
530
+ if relative_dir:
531
+ f.write(f" yaml_path = os.path.join(os.path.dirname(__file__), '..', 'cases', '{relative_dir}', '{yaml_filename}')\n")
532
+ else:
533
+ f.write(f" yaml_path = os.path.join(os.path.dirname(__file__), '..', 'cases', '{yaml_filename}')\n")
534
+ f.write(f" with open(yaml_path, 'r', encoding='utf-8') as file:\n")
535
+ f.write(f" test_case_data = yaml.safe_load(file)['testcase']\n")
536
+ f.write(f" return test_case_data\n\n")
537
+
538
+ f.write(f" @allure.story('{allure_story}')\n")
539
+ f.write(f" def test_{module_name}(self):\n")
540
+ f.write(f" log.info('Starting test_{module_name}')\n")
541
+
542
+ for step in test_data['steps']:
543
+ step_id = step['id']
544
+ step_project = step.get("project") # 场景测试用例可能会请求不同项目的接口,需要在每个step中指定对应的project
545
+ f.write(f" # Step: {step_id}\n")
546
+ f.write(f" log.info(f'开始执行 step: {step_id}')\n")
547
+ f.write(f" {step_id} = self.steps_dict.get('{step_id}')\n")
548
+
549
+ # 如果 testcase 有 host 配置,使用它;否则使用 project 配置
550
+ if testcase_host:
551
+ f.write(f" step_host = self.testcase_host\n")
552
+ elif step_project:
553
+ f.write(f" project_config = self.global_vars.get('{step_project}')\n")
554
+ f.write(f" step_host = project_config['host'] if project_config else ''\n")
555
+ else:
556
+ f.write(f" project_config = self.global_vars.get('{project_name}')\n")
557
+ f.write(f" step_host = project_config['host'] if project_config else ''\n")
558
+
559
+ f.write(f" response = RequestHandler.send_request(\n")
560
+ f.write(f" method={step_id}['method'],\n")
561
+ f.write(f" url=step_host + self.VR.process_data({step_id}['path']),\n")
562
+ f.write(f" headers=self.VR.process_data({step_id}.get('headers')),\n")
563
+ f.write(f" data=self.VR.process_data({step_id}.get('data')),\n")
564
+ f.write(f" params=self.VR.process_data({step_id}.get('params')),\n")
565
+ f.write(f" files=self.VR.process_data({step_id}.get('files'))\n")
566
+ f.write(f" )\n")
567
+ f.write(f" log.info(f'{step_id} 请求结果为:{{response}}')\n")
568
+ f.write(f" self.session_vars['{step_id}'] = response\n")
569
+
570
+ if 'assert' in step:
571
+ # 只有在真正使用 project_config 时才获取 db_config
572
+ if not testcase_host:
573
+ f.write(f" db_config = project_config.get('mysql')\n")
574
+ else:
575
+ f.write(f" db_config = None\n")
576
+ f.write(f" AssertHandler().handle_assertion(\n")
577
+ f.write(f" asserts=self.VR.process_data({step_id}['assert']),\n")
578
+ f.write(f" response=response,\n")
579
+ f.write(f" db_config=db_config\n")
580
+ f.write(f" )\n\n")
581
+
582
+ # teardown处理
583
+ if validate_teardowns:
584
+ f.write(f" @classmethod\n")
585
+ f.write(f" def teardown_class(cls):\n")
586
+ f.write(f" log.info('Starting teardown for the Test{module_class_name}')\n")
587
+ for teardown_step in teardowns:
588
+ teardown_step_id = teardown_step['id']
589
+ teardown_step_project = teardown_step.get("project")
590
+ f.write(f" {teardown_step_id} = cls.teardowns_dict.get('{teardown_step_id}')\n")
591
+
592
+ if teardown_step_project:
593
+ f.write(f" project_config = cls.global_vars.get('{teardown_step_project}')\n")
594
+ else:
595
+ f.write(f" project_config = cls.global_vars.get('{project_name}')\n")
596
+
597
+ # 如果是请求接口操作
598
+ if teardown_step['operation_type'] == 'api':
599
+ f.write(f" response = RequestHandler.send_request(\n")
600
+ f.write(f" method={teardown_step_id}['method'],\n")
601
+ f.write(f" url=project_config['host'] + cls.VR.process_data({teardown_step_id}['path']),\n")
602
+ f.write(f" headers=cls.VR.process_data({teardown_step_id}.get('headers')),\n")
603
+ f.write(f" data=cls.VR.process_data({teardown_step_id}.get('data')),\n")
604
+ f.write(f" params=cls.VR.process_data({teardown_step_id}.get('params')),\n")
605
+ f.write(f" files=cls.VR.process_data({teardown_step_id}.get('files'))\n")
606
+ f.write(f" )\n")
607
+ f.write(f" log.info(f'{teardown_step_id} 请求结果为:{{response}}')\n")
608
+ f.write(f" cls.session_vars['{teardown_step_id}'] = response\n")
609
+
610
+ if 'assert' in teardown_step:
611
+ # if any(assertion['type'].startswith('mysql') for assertion in teardown_step['assert']):
612
+ # f.write(f" db_config = project_config.get('mysql')\n")
613
+ f.write(f" db_config = project_config.get('mysql')\n")
614
+ f.write(f" AssertHandler().handle_assertion(\n")
615
+ f.write(f" asserts=cls.VR.process_data({teardown_step_id}['assert']),\n")
616
+ f.write(f" response=response,\n")
617
+ f.write(f" db_config=db_config\n")
618
+ f.write(f" )\n\n")
619
+
620
+ # 如果是数据库操作,暂时未补充逻辑
621
+ elif teardown_step['operation_type'] == 'db':
622
+ f.write(f" db_config = project_config.get('mysql')\n")
623
+ f.write(f" TeardownHandler().handle_teardown(\n")
624
+ f.write(f" asserts=cls.VR.process_data({teardown_step_id}),\n")
625
+ f.write(f" db_config=db_config\n")
626
+ f.write(f" )\n\n")
627
+ f.write(f" pass\n")
628
+ else:
629
+ log.info(f"未知的 operation_type: {teardown_step['operation_type']}")
630
+ f.write(f" pass\n")
631
+
632
+ f.write(f" log.info('Teardown completed for Test{module_class_name}.')\n")
633
+
634
+ f.write(f"\n log.info(f\"Test case test_{module_name} completed.\")\n")
635
+
636
+ log.info(f"已生成测试用例文件: {file_path}")
637
+
638
+
639
+ @staticmethod
640
+ def load_test_data(test_data_file):
641
+ try:
642
+ with open(test_data_file, 'r', encoding='utf-8') as file:
643
+ test_data = yaml.safe_load(file)
644
+ return test_data
645
+ except FileNotFoundError:
646
+ log.error(f"未找到测试数据文件: {test_data_file}")
647
+ except yaml.YAMLError as e:
648
+ log.error(f"YAML配置文件解析错误: {e},{test_data_file} 跳过生成测试用例。")
649
+
650
+ @staticmethod
651
+ def validate_test_data(test_data):
652
+ """
653
+ 校验测试数据是否符合基本要求
654
+ :param test_data: 测试数据
655
+ :return:
656
+ """
657
+ if not test_data:
658
+ log.error("test_data 不能为空.")
659
+ return False
660
+
661
+ if not test_data.get('testcase'):
662
+ log.error("test_data 必须包含 'testcase' 键.")
663
+ return False
664
+
665
+ if not test_data['testcase'].get('name'):
666
+ log.error("'testcase' 下的 'name' 字段不能为空.")
667
+ return False
668
+
669
+ steps = test_data['testcase'].get('steps')
670
+ if not steps:
671
+ log.error("'testcase' 下的 'steps' 字段不能为空.")
672
+ return False
673
+
674
+ for step in steps:
675
+ if not all(key in step for key in ['id', 'path', 'method']):
676
+ log.error("每个步骤必须包含 'id', 'path', 和 'method' 字段.")
677
+ return False
678
+
679
+ if not step['id']:
680
+ log.error("步骤中的 'id' 字段不能为空.")
681
+ return False
682
+ if not step['path']:
683
+ log.error("步骤中的 'path' 字段不能为空.")
684
+ return False
685
+ if not step['method']:
686
+ log.error("步骤中的 'method' 字段不能为空.")
687
+ return False
688
+
689
+ return True
690
+
691
+ @staticmethod
692
+ def validate_teardowns(teardowns):
693
+ """
694
+ 验证 teardowns 数据是否符合要求
695
+ :param teardowns: teardowns 列表
696
+ :return: True 如果验证成功,否则 False
697
+ """
698
+ if not teardowns:
699
+ # log.warning("testcase 下的 'teardowns' 字段为空.")
700
+ return False
701
+
702
+ for teardown in teardowns:
703
+ if not all(key in teardown for key in ['id', 'operation_type']):
704
+ log.warning("teardown 必须包含 'id' 和 'operation_type' 字段.")
705
+ return False
706
+
707
+ if not teardown['id']:
708
+ log.warning("teardown 中的 'id' 字段为空.")
709
+ return False
710
+ if not teardown['operation_type']:
711
+ log.warning("teardown 中的 'operation_type' 字段为空.")
712
+ return False
713
+
714
+ if teardown['operation_type'] == 'api':
715
+ required_api_keys = ['path', 'method', 'headers', 'data']
716
+ if not all(key in teardown for key in required_api_keys):
717
+ log.warning("对于 API 类型的 teardown,必须包含 'path', 'method', 'headers', 'data' 字段.")
718
+ return False
719
+
720
+ if not teardown['path']:
721
+ log.warning("teardown 中的 'path' 字段为空.")
722
+ return False
723
+ if not teardown['method']:
724
+ log.warning("teardown 中的 'method' 字段为空.")
725
+ return False
726
+
727
+ elif teardown['operation_type'] == 'db':
728
+ if 'query' not in teardown or not teardown['query']:
729
+ log.warning("对于数据库类型的 teardown,'query' 字段不能为空.")
730
+ return False
731
+
732
+ return True
733
+
734
+
735
+ if __name__ == '__main__':
736
+ CG = CaseGenerator()
737
+ CG.generate_test_cases(project_yaml_list=["tests/cases/"])