api-engine-xin 0.0.23__tar.gz → 0.0.26__tar.gz

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.
Files changed (41) hide show
  1. api_engine_xin-0.0.26/ApiEngine/__init__.py +3 -0
  2. api_engine_xin-0.0.26/ApiEngine/core.py +422 -0
  3. api_engine_xin-0.0.26/ApiEngine/engine/__init__.py +1 -0
  4. api_engine_xin-0.0.26/ApiEngine/engine/assertion.py +108 -0
  5. api_engine_xin-0.0.26/ApiEngine/engine/base_case.py +339 -0
  6. api_engine_xin-0.0.26/ApiEngine/engine/extractor.py +46 -0
  7. api_engine_xin-0.0.26/ApiEngine/engine/precondition.py +239 -0
  8. api_engine_xin-0.0.26/ApiEngine/engine/replacer.py +113 -0
  9. api_engine_xin-0.0.26/ApiEngine/engine/script_runner.py +34 -0
  10. api_engine_xin-0.0.26/ApiEngine/http/__init__.py +1 -0
  11. api_engine_xin-0.0.26/ApiEngine/http/client.py +111 -0
  12. api_engine_xin-0.0.26/ApiEngine/http/files.py +60 -0
  13. api_engine_xin-0.0.26/ApiEngine/infra/__init__.py +3 -0
  14. api_engine_xin-0.0.26/ApiEngine/infra/case_log.py +52 -0
  15. api_engine_xin-0.0.26/ApiEngine/infra/db_client.py +99 -0
  16. api_engine_xin-0.0.26/ApiEngine/infra/exceptions.py +5 -0
  17. api_engine_xin-0.0.26/ApiEngine/infra/global_func.py +2 -0
  18. api_engine_xin-0.0.23/ApiEngine/testResult.py → api_engine_xin-0.0.26/ApiEngine/infra/test_result.py +4 -5
  19. {api_engine_xin-0.0.23 → api_engine_xin-0.0.26}/LICENSE +1 -1
  20. {api_engine_xin-0.0.23 → api_engine_xin-0.0.26}/PKG-INFO +5 -1
  21. {api_engine_xin-0.0.23 → api_engine_xin-0.0.26}/api_engine_xin.egg-info/PKG-INFO +5 -1
  22. api_engine_xin-0.0.26/api_engine_xin.egg-info/SOURCES.txt +29 -0
  23. api_engine_xin-0.0.26/api_engine_xin.egg-info/requires.txt +17 -0
  24. api_engine_xin-0.0.26/setup.py +45 -0
  25. api_engine_xin-0.0.26/tests/runTest.py +326 -0
  26. api_engine_xin-0.0.23/ApiEngine/BaseCase.py +0 -812
  27. api_engine_xin-0.0.23/ApiEngine/__init__.py +0 -3
  28. api_engine_xin-0.0.23/ApiEngine/caseLog.py +0 -52
  29. api_engine_xin-0.0.23/ApiEngine/core.py +0 -454
  30. api_engine_xin-0.0.23/ApiEngine/dbClient.py +0 -103
  31. api_engine_xin-0.0.23/ApiEngine/global_func.py +0 -0
  32. api_engine_xin-0.0.23/api_engine_xin.egg-info/SOURCES.txt +0 -18
  33. api_engine_xin-0.0.23/api_engine_xin.egg-info/requires.txt +0 -2
  34. api_engine_xin-0.0.23/setup.py +0 -38
  35. api_engine_xin-0.0.23/tests/runTest.py +0 -58
  36. {api_engine_xin-0.0.23 → api_engine_xin-0.0.26}/README.md +0 -0
  37. {api_engine_xin-0.0.23 → api_engine_xin-0.0.26}/api_engine_xin.egg-info/dependency_links.txt +0 -0
  38. {api_engine_xin-0.0.23 → api_engine_xin-0.0.26}/api_engine_xin.egg-info/top_level.txt +0 -0
  39. {api_engine_xin-0.0.23 → api_engine_xin-0.0.26}/setup.cfg +0 -0
  40. {api_engine_xin-0.0.23 → api_engine_xin-0.0.26}/tests/Tools.py +0 -0
  41. {api_engine_xin-0.0.23 → api_engine_xin-0.0.26}/tests/__init__.py +0 -0
@@ -0,0 +1,3 @@
1
+ from ApiEngine.infra.case_log import CaseLogHandler
2
+
3
+ log = CaseLogHandler()
@@ -0,0 +1,422 @@
1
+ import copy
2
+
3
+ from ApiEngine import log
4
+ from ApiEngine.infra import global_func
5
+ from ApiEngine.infra.exceptions import PreconditionChainError
6
+ from ApiEngine.infra.test_result import TestResult
7
+ from ApiEngine.infra.db_client import DBClient
8
+ from ApiEngine.engine.base_case import BaseCase
9
+
10
+
11
+ class TestRunner:
12
+ def __init__(self, env_data):
13
+ """
14
+ :param env_data: 执行测试时的环境数据
15
+ """
16
+ # 深拷贝环境数据,避免修改调用方原始数据
17
+ self.env_data = copy.deepcopy(env_data)
18
+ # debug_updates 保持原引用,变更对上层可见
19
+ if "debug_updates" in env_data:
20
+ self.env_data["debug_updates"] = env_data["debug_updates"]
21
+ self.result = []
22
+ self._db = DBClient()
23
+ self._shared_env = {} # 实例级共享环境
24
+
25
+ def execute_cases(self, testcases):
26
+ """执行测试用例的方法"""
27
+ # 根据数据库的配置初始化数据库的连接
28
+ db_config = self.env_data.pop("db", None)
29
+ if db_config:
30
+ self._db.init_connect(db_config)
31
+
32
+ # 统一初始化共享环境
33
+ self._shared_env.clear()
34
+ self._shared_env.update(self.env_data)
35
+ self._load_global_func()
36
+
37
+ # 判断测试数据参数的类型
38
+ if isinstance(testcases, dict):
39
+ cases = testcases.get("cases")
40
+ if cases:
41
+ log.info_log("执行测试套件:", testcases["name"])
42
+ test_result = TestResult(all=len(testcases["cases"]), name=testcases["name"])
43
+ for case in testcases["cases"]:
44
+ log.info_log(case)
45
+ self.perform_case(case, test_result)
46
+ res = test_result.get_result_info()
47
+ self.result.append(res)
48
+ else:
49
+ log.info_log("调试单条接口用例:", testcases["title"])
50
+ test_result = TestResult(all=1)
51
+ self.perform_case(testcases, test_result)
52
+ res = test_result.get_result_info()
53
+ self.result = res["cases"][0]
54
+ elif isinstance(testcases, list):
55
+ results = []
56
+ for items in testcases:
57
+ log.info_log("执行测试套件:", items["name"])
58
+ test_result = TestResult(all=len(items["cases"]), name=items["name"])
59
+ for case in items["cases"]:
60
+ self.perform_case(case, test_result)
61
+ res = test_result.get_result_info()
62
+ results.append(res)
63
+ total_all = 0
64
+ total_success = 0
65
+ total_fail = 0
66
+ total_error = 0
67
+ for scence_result in results:
68
+ total_all += scence_result["all"]
69
+ total_success += scence_result["success"]
70
+ total_fail += scence_result["fail"]
71
+ total_error += scence_result["error"]
72
+ self.result = {
73
+ "results": results,
74
+ "all": total_all,
75
+ "success": total_success,
76
+ "fail": total_fail,
77
+ "error": total_error
78
+ }
79
+ else:
80
+ log.error_log("测试数据格式错误")
81
+
82
+ # 断开数据库连接
83
+ if db_config:
84
+ self._db.close_db_connect()
85
+ return self.result
86
+
87
+ def perform_case(self, case, test_result):
88
+ """执行单条用例"""
89
+ # 每次创建新的 BaseCase 实例,传入共享环境引用
90
+ c = BaseCase(shared_env=self._shared_env)
91
+ c._db = self._db
92
+ try:
93
+ c.perform(case)
94
+ except PreconditionChainError:
95
+ test_result.add_fail(c)
96
+ except AssertionError:
97
+ test_result.add_fail(c)
98
+ except Exception as e:
99
+ test_result.add_error(c, e)
100
+ else:
101
+ test_result.add_success(c)
102
+
103
+ def _load_global_func(self):
104
+ """安全加载用户自定义函数,避免跨执行污染"""
105
+ _gf = self.env_data.get("global_func")
106
+ if not (_gf and isinstance(_gf, str)):
107
+ return
108
+ # 只保留 __name__ 等双下划线内置属性
109
+ builtins = {k: v for k, v in global_func.__dict__.items() if k.startswith('__')}
110
+ global_func.__dict__.clear()
111
+ global_func.__dict__.update(builtins)
112
+ exec(_gf, global_func.__dict__)
113
+
114
+ def get_env_snapshot(self) -> dict:
115
+ """获取执行后的环境变量快照(供上层平台读取)
116
+
117
+ 返回包含 envs、debug_updates 的字典,
118
+ 用于上层平台同步临时变量和持久化全局变量变更。
119
+
120
+ debug_updates 中的约定:
121
+ - value 非 None → 新增/更新全局变量
122
+ - value 为 None → 删除全局变量
123
+ """
124
+ return {
125
+ "envs": copy.deepcopy(dict(self._shared_env.get("envs") or {})),
126
+ "debug_updates": copy.deepcopy(self._shared_env.get("debug_updates") or {}),
127
+ }
128
+
129
+
130
+ if __name__ == '__main__':
131
+ import os
132
+
133
+ # 获取测试辅助文件的绝对路径
134
+ _test_dir = os.path.join(os.path.dirname(__file__), '..', 'tests')
135
+ _setup = open(os.path.join(_test_dir, 'setup_scripts.txt'), 'r', encoding='utf-8').read()
136
+ _teardown = open(os.path.join(_test_dir, 'teardown_scripts.txt'), 'r', encoding='utf-8').read()
137
+ _tools = open(os.path.join(_test_dir, 'Tools.py'), 'r', encoding='utf-8').read()
138
+
139
+ test_case = {
140
+ "title": "登录成功",
141
+ "interface": {
142
+ "url": "/member/public/login",
143
+ "method": "post"
144
+ },
145
+ "headers": {
146
+ "Content-Type": "application/x-www-form-urlencoded"
147
+ },
148
+ "request": {
149
+ "data": {
150
+ "keywords": "13012341231",
151
+ "password": "test123"
152
+ }
153
+ },
154
+ "setup_script": _setup,
155
+ "teardown_script": "",
156
+ "extract": [
157
+ {"var_name": "status", "extract_expr": "$.status"},
158
+ {"var_name": "description", "extract_expr": "$.description"}
159
+ ],
160
+ "assertions": [
161
+ {"type": "相等", "field": "$.status", "expected": 200},
162
+ {"type": "相等", "field": "$.description", "expected": "登录成功"}
163
+ ]
164
+ }
165
+
166
+ test_case2 = {
167
+ "title": "验证是否登录成功",
168
+ "interface": {
169
+ "url": "/member/public/islogin",
170
+ "method": "post"
171
+ },
172
+ "headers": {},
173
+ "request": {
174
+ "params": {
175
+ "m": "Home",
176
+ "c": "User",
177
+ "a": "do_login",
178
+ "t": "${status}"
179
+ },
180
+ },
181
+ "preconditions": [
182
+ {
183
+ "title": "P2P登录成功",
184
+ "interface": {
185
+ "url": "/member/public/login",
186
+ "method": "post"
187
+ },
188
+ "headers": {
189
+ "Content-Type": "application/x-www-form-urlencoded"
190
+ },
191
+ "request": {
192
+ "data": {
193
+ "keywords": "13012341231",
194
+ "password": "test123",
195
+ "description": "${description3}"
196
+ }
197
+ },
198
+ "setup_script": _setup,
199
+ "teardown_script": "",
200
+ "preconditions": [
201
+ {
202
+ "title": "验证当前用户没有登录",
203
+ "interface": {
204
+ "url": "/member/public/islogin",
205
+ "method": "post"
206
+ },
207
+ "headers": {},
208
+ "request": {},
209
+ "setup_script": _setup,
210
+ "teardown_script": "",
211
+ "extract": [
212
+ {"var_name": "status3", "extract_expr": "$.status"},
213
+ {"var_name": "description3", "extract_expr": "$.description"}
214
+ ],
215
+ "assertions": [
216
+ {"type": "相等", "field": "$.status", "expected": 200},
217
+ {"type": "相等", "field": "$.description", "expected": "您未登陆!"}
218
+ ]
219
+ }
220
+ ],
221
+ "extract": [
222
+ {"var_name": "status", "extract_expr": "$.status"},
223
+ {"var_name": "description", "extract_expr": "$.description"}
224
+ ],
225
+ "assertions": [
226
+ {"type": "相等", "field": "$.status", "expected": 200},
227
+ {"type": "相等", "field": "$.description", "expected": "登录成功"}
228
+ ]
229
+ }
230
+ ],
231
+ "setup_script": _setup,
232
+ "teardown_script": "",
233
+ "extract": [
234
+ {"var_name": "status2", "extract_expr": "$.status"},
235
+ {"var_name": "description2", "extract_expr": "$.description"}
236
+ ],
237
+ "assertions": [
238
+ {"type": "相等", "field": "$.status", "expected": "${status3}"},
239
+ {"type": "相等", "field": "$.description", "expected": "OK"}
240
+ ]
241
+ }
242
+
243
+ test_suite = {
244
+ "name": "测试套件1",
245
+ "cases": [
246
+ {
247
+ "title": "登录接口",
248
+ "interface": {
249
+ "url": "/member/public/login",
250
+ "method": "post"
251
+ },
252
+ "headers": {
253
+ "Content-Type": "application/x-www-form-urlencoded",
254
+ "Token": "${token}"
255
+ },
256
+ "request": {
257
+ "params": {},
258
+ "data": {"keywords": "13012349900", "password": "test123",
259
+ "user2": "${username}", "pwd2": "${password}",
260
+ "mobile": "${mobile}"},
261
+ "json": {"keywords": "13012349900", "password": "test123",
262
+ "user2": "${username}", "pwd2": "${password}",
263
+ "mobile": "${mobile}"}
264
+ },
265
+ "setup_script": _setup,
266
+ "teardown_script": _teardown
267
+ },
268
+ {
269
+ "title": "登录接口2",
270
+ "interface": {
271
+ "url": "/member/public/login",
272
+ "method": "post"
273
+ },
274
+ "headers": {
275
+ "Content-Type": "application/x-www-form-urlencoded",
276
+ "Token": "${token}"
277
+ },
278
+ "request": {
279
+ "params": {},
280
+ "data": {"keywords": "13012349900", "password": "test123",
281
+ "user2": "${username}", "pwd2": "${password}",
282
+ "mobile": "${mobile}"},
283
+ "json": {"keywords": "13012349900", "password": "test123",
284
+ "user2": "${username}", "pwd2": "${password}",
285
+ "mobile": "${mobile}"}
286
+ },
287
+ "setup_script": _setup,
288
+ "teardown_script": ""
289
+ }
290
+ ]
291
+ }
292
+
293
+ test_suites = [
294
+ {
295
+ "name": "测试套件1",
296
+ "cases": [
297
+ {
298
+ "title": "登录接口",
299
+ "interface": {
300
+ "url": "/member/public/login",
301
+ "method": "post"
302
+ },
303
+ "headers": {
304
+ "Content-Type": "application/x-www-form-urlencoded",
305
+ "Token": "${token}"
306
+ },
307
+ "request": {
308
+ "params": {},
309
+ "data": {"keywords": "13012349900", "password": "test123",
310
+ "user2": "${username}", "pwd2": "${password}",
311
+ "mobile": "${mobile}"},
312
+ "json": {"keywords": "13012349900", "password": "test123",
313
+ "user2": "${username}", "pwd2": "${password}",
314
+ "mobile": "${mobile}"}
315
+ },
316
+ "setup_script": _setup,
317
+ "teardown_script": _teardown
318
+ },
319
+ {
320
+ "title": "登录接口2",
321
+ "interface": {
322
+ "url": "/member/public/login",
323
+ "method": "post"
324
+ },
325
+ "headers": {
326
+ "Content-Type": "application/x-www-form-urlencoded",
327
+ "Token": "${token}"
328
+ },
329
+ "request": {
330
+ "params": {},
331
+ "data": {"keywords": "13012349900", "password": "test123",
332
+ "user2": "${username}", "pwd2": "${password}",
333
+ "mobile": "${mobile}"},
334
+ "json": {"keywords": "13012349900", "password": "test123",
335
+ "user2": "${username}", "pwd2": "${password}",
336
+ "mobile": "${mobile}"}
337
+ },
338
+ "setup_script": _setup,
339
+ "teardown_script": ""
340
+ }
341
+ ]
342
+ },
343
+ {
344
+ "name": "测试套件2",
345
+ "cases": [
346
+ {
347
+ "title": "登录接口3",
348
+ "interface": {
349
+ "url": "/member/public/login",
350
+ "method": "post"
351
+ },
352
+ "headers": {
353
+ "Content-Type": "application/x-www-form-urlencoded",
354
+ "Token": "${token}"
355
+ },
356
+ "request": {
357
+ "params": {},
358
+ "data": {"keywords": "13012349900", "password": "test123",
359
+ "user2": "${username}", "pwd2": "${password}",
360
+ "mobile": "${mobile}"},
361
+ "json": {"keywords": "13012349900", "password": "test123",
362
+ "user2": "${username}", "pwd2": "${password}",
363
+ "mobile": "${mobile}"}
364
+ },
365
+ "setup_script": _setup,
366
+ "teardown_script": _teardown
367
+ },
368
+ {
369
+ "title": "登录接口4",
370
+ "interface": {
371
+ "url": "/member/public/login",
372
+ "method": "post"
373
+ },
374
+ "headers": {
375
+ "Content-Type": "application/x-www-form-urlencoded",
376
+ "Token": "${token}"
377
+ },
378
+ "request": {
379
+ "params": {},
380
+ "data": {"keywords": "13012349900", "password": "test123",
381
+ "user2": "${username}", "pwd2": "${password}",
382
+ "mobile": "${mobile}"},
383
+ "json": {"keywords": "13012349900", "password": "test123",
384
+ "user2": "${username}", "pwd2": "${password}",
385
+ "mobile": "${mobile}"}
386
+ },
387
+ "setup_script": _setup,
388
+ "teardown_script": ""
389
+ }
390
+ ]
391
+ }
392
+ ]
393
+
394
+ # 全局环境数据
395
+ test_env_data = {
396
+ "base_url": "http://121.43.169.97:8081",
397
+ "headers": {
398
+ "Content-Type": "application/json"
399
+ },
400
+ "envs": {
401
+ "username": "rand_13012349900",
402
+ "password": "rand_test123",
403
+ "token": "12345678"
404
+ },
405
+ "global_func": _tools,
406
+ "db": [
407
+ {
408
+ "name": "P2P",
409
+ "type": "mysql",
410
+ "config": {
411
+ "host": "121.43.169.97",
412
+ "port": 3306,
413
+ "user": "student",
414
+ "password": "P2P_student_2023"
415
+ }
416
+ }
417
+ ]
418
+ }
419
+
420
+ runner = TestRunner(test_env_data)
421
+ result = runner.execute_cases(test_case2)
422
+ log.info_log("测试结果:", result)
@@ -0,0 +1 @@
1
+ # ApiEngine 用例执行引擎层
@@ -0,0 +1,108 @@
1
+ import re
2
+
3
+
4
+ class AssertionEngine:
5
+ """断言引擎:支持中英文关键字、智能类型归一化"""
6
+
7
+ METHOD_MAP = {
8
+ # 相等
9
+ "相等": lambda a, b: a == b,
10
+ "equals": lambda a, b: a == b,
11
+ "eq": lambda a, b: a == b,
12
+ "==": lambda a, b: a == b,
13
+ # 相等忽略大小写
14
+ "相等忽略大小写": lambda a, b: a.lower() == b.lower(),
15
+ "equals_ignore_case": lambda a, b: a.lower() == b.lower(),
16
+ "eq_ignore_case": lambda a, b: a.lower() == b.lower(),
17
+ # 不相等
18
+ "不相等": lambda a, b: a != b,
19
+ "not_equals": lambda a, b: a != b,
20
+ "ne": lambda a, b: a != b,
21
+ "!=": lambda a, b: a != b,
22
+ # 包含
23
+ "包含": lambda a, b: a in b,
24
+ "contains": lambda a, b: a in b,
25
+ "in": lambda a, b: a in b,
26
+ # 不包含
27
+ "不包含": lambda a, b: a not in b,
28
+ "not_contains": lambda a, b: a not in b,
29
+ "not_in": lambda a, b: a not in b,
30
+ # 大于
31
+ "大于": lambda a, b: a > b,
32
+ "greater_than": lambda a, b: a > b,
33
+ "gt": lambda a, b: a > b,
34
+ ">": lambda a, b: a > b,
35
+ # 小于
36
+ "小于": lambda a, b: a < b,
37
+ "less_than": lambda a, b: a < b,
38
+ "lt": lambda a, b: a < b,
39
+ "<": lambda a, b: a < b,
40
+ # 大于等于
41
+ "大于等于": lambda a, b: a >= b,
42
+ "greater_than_or_equals": lambda a, b: a >= b,
43
+ "ge": lambda a, b: a >= b,
44
+ ">=": lambda a, b: a >= b,
45
+ # 小于等于
46
+ "小于等于": lambda a, b: a <= b,
47
+ "less_than_or_equals": lambda a, b: a <= b,
48
+ "le": lambda a, b: a <= b,
49
+ "<=": lambda a, b: a <= b,
50
+ # 正则匹配
51
+ "正则匹配": lambda a, b: re.search(a, b),
52
+ "regex_match": lambda a, b: re.search(a, b),
53
+ "regex": lambda a, b: re.search(a, b),
54
+ "match": lambda a, b: re.search(a, b),
55
+ }
56
+
57
+ @staticmethod
58
+ def _normalize_for_compare(expect, actual):
59
+ """
60
+ 智能类型归一化,解决 str/int/float 类型不一致导致的断言失败
61
+ """
62
+ if type(expect) == type(actual):
63
+ return expect, actual
64
+
65
+ try:
66
+ if isinstance(actual, (int, float)) and isinstance(expect, str):
67
+ if '.' in str(expect):
68
+ return float(expect), actual
69
+ else:
70
+ return int(expect), actual
71
+ elif isinstance(expect, (int, float)) and isinstance(actual, str):
72
+ if '.' in str(actual):
73
+ return expect, float(actual)
74
+ else:
75
+ return expect, int(actual)
76
+ except (ValueError, TypeError):
77
+ pass
78
+
79
+ return expect, actual
80
+
81
+ @classmethod
82
+ def assert_value(cls, method, expect, actual, debug_log=None, info_log=None, error_log=None):
83
+ """
84
+ 执行断言
85
+ :param method: 断言比较方式
86
+ :param expect: 预期结果
87
+ :param actual: 实际结果
88
+ """
89
+ expect, actual = cls._normalize_for_compare(expect, actual)
90
+
91
+ assert_fun = cls.METHOD_MAP.get(method)
92
+ if assert_fun is None:
93
+ raise Exception("不支持的断言方法")
94
+ else:
95
+ if debug_log:
96
+ debug_log(f"断言比较方法是:{method}")
97
+ debug_log(f"预期结果是:{expect}")
98
+ debug_log(f"实际结果是:{actual}")
99
+ try:
100
+ assert assert_fun(expect, actual)
101
+ except AssertionError:
102
+ msg = f"断言失败,实际结果({actual}) 不满足({method}) 期望结果({expect})"
103
+ if error_log:
104
+ error_log(msg)
105
+ raise AssertionError(msg)
106
+ else:
107
+ if info_log:
108
+ info_log(f"断言成功,实际结果({actual}) 满足({method}) 期望结果({expect})")