api-engine-xin 0.0.20__tar.gz → 0.0.22__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 (21) hide show
  1. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/ApiEngine/BaseCase.py +40 -10
  2. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/ApiEngine/caseLog.py +5 -0
  3. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/ApiEngine/core.py +9 -3
  4. api_engine_xin-0.0.22/ApiEngine/testResult.py +86 -0
  5. {api_engine_xin-0.0.20/api_engine_xin.egg-info → api_engine_xin-0.0.22}/PKG-INFO +2 -15
  6. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22/api_engine_xin.egg-info}/PKG-INFO +3 -16
  7. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/setup.py +1 -1
  8. api_engine_xin-0.0.20/ApiEngine/testResult.py +0 -109
  9. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/ApiEngine/__init__.py +0 -0
  10. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/ApiEngine/dbClient.py +0 -0
  11. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/ApiEngine/global_func.py +0 -0
  12. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/LICENSE +0 -0
  13. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/README.md +0 -0
  14. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/api_engine_xin.egg-info/SOURCES.txt +0 -0
  15. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/api_engine_xin.egg-info/dependency_links.txt +0 -0
  16. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/api_engine_xin.egg-info/requires.txt +0 -0
  17. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/api_engine_xin.egg-info/top_level.txt +0 -0
  18. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/setup.cfg +0 -0
  19. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/tests/Tools.py +0 -0
  20. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/tests/__init__.py +0 -0
  21. {api_engine_xin-0.0.20 → api_engine_xin-0.0.22}/tests/runTest.py +0 -0
@@ -13,6 +13,8 @@ class BaseCase(CaseLogHandler):
13
13
  def __init__(self):
14
14
  self.session = requests.Session()
15
15
  self._db = None
16
+ self._extract_results = []
17
+ self._assert_results = []
16
18
 
17
19
  def __run_script(self, data):
18
20
  # 执行前后置脚本,可以在前后置脚本中共享数据
@@ -24,15 +26,17 @@ class BaseCase(CaseLogHandler):
24
26
  db = self._db
25
27
  # 1、读取前置脚本数据
26
28
  setup_scripts = data.get("setup_script")
27
- # 2、执行字符串中有效的python代码
28
- exec(setup_scripts)
29
+ # 2、执行字符串中有效的python代码(空值守卫)
30
+ if setup_scripts and isinstance(setup_scripts, str):
31
+ exec(setup_scripts)
29
32
 
30
33
  response = yield
31
34
 
32
35
  # 1、读取后置脚本数据
33
36
  teardown_scripts = data.get("teardown_script")
34
- # 2、执行字符串中有效的python代码
35
- exec(teardown_scripts)
37
+ # 2、执行字符串中有效的python代码(空值守卫)
38
+ if teardown_scripts and isinstance(teardown_scripts, str):
39
+ exec(teardown_scripts)
36
40
 
37
41
  yield
38
42
 
@@ -163,16 +167,17 @@ class BaseCase(CaseLogHandler):
163
167
  else:
164
168
  request_data["url"] = ENV.get("base_url") + data.get("interface").get("url")
165
169
  request_data["method"] = data.get("interface").get("method")
166
- # 2、处理请求头
167
- request_data["headers"] = ENV.get("headers")
168
- request_data["headers"].update(data.get("headers"))
170
+ # 2、处理请求头(使用副本,避免污染全局 ENV)
171
+ request_data["headers"] = dict(ENV.get("headers") or {})
172
+ request_data["headers"].update(data.get("headers") or {})
169
173
  # 3、处理请求参数
170
174
  request_data["params"] = data.get("request").get("params")
171
175
  content_type = request_data["headers"].get("Content-Type", "")
176
+ _req = data.get("request") or {}
172
177
  if "application/json" in content_type:
173
- request_data["json"] = data.get("request").get("json")
178
+ request_data["json"] = _req.get("json") or _req.get("data")
174
179
  if "application/x-www-form-urlencoded" in content_type or "multipart/form-data" in content_type:
175
- request_data["data"] = data.get("request").get("data")
180
+ request_data["data"] = _req.get("data") or _req.get("json")
176
181
  if "multipart/form-data" in content_type:
177
182
  # 注意:这里不直接把 files 放进需要做变量替换的数据里,避免 open 文件对象被 eval 破坏
178
183
  request_data["files"] = data.get("request").get("files")
@@ -221,6 +226,7 @@ class BaseCase(CaseLogHandler):
221
226
  from urllib.parse import urlencode
222
227
  query_string = urlencode(params)
223
228
  full_url = f"{self.url}?{query_string}"
229
+ self.url = full_url # 更新 self.url 为完整URL(含已替换的查询参数)
224
230
  self.info_log("请求地址:", full_url)
225
231
  self.info_log("请求方法:", self.method)
226
232
  self.info_log("请求头:", self.request_headers)
@@ -311,6 +317,12 @@ class BaseCase(CaseLogHandler):
311
317
  # 4、保存到环境变量(后续用 ${user_id} 引用)
312
318
  self.save_env_variable(var_name, extracted_value)
313
319
  self.info_log(f"数据提取成功:{var_name} = {extracted_value}")
320
+ # 5、记录提取结果
321
+ self._extract_results.append({
322
+ "var_name": var_name,
323
+ "extract_expr": extract_expr,
324
+ "value": extracted_value,
325
+ })
314
326
 
315
327
  def __assert_data(self, assertion, response):
316
328
  """
@@ -345,8 +357,11 @@ class BaseCase(CaseLogHandler):
345
357
  self.error_log("响应体非JSON格式,无法提取数据")
346
358
  # 3、通过 JSONPath 提取值
347
359
  extracted_value = self.json_extract(resp_json, extract_expr)
360
+ # 记录实际值(供 __execute_assertions 读取,即使断言失败也能获取)
361
+ self._last_actual = extracted_value
348
362
  # 4、断言
349
363
  self.assertion(assertion_type, assertion_content, extracted_value)
364
+ return extracted_value
350
365
 
351
366
  # 遍历所有assertions项,依次执行断言
352
367
  def __execute_assertions(self, data, response):
@@ -358,11 +373,16 @@ class BaseCase(CaseLogHandler):
358
373
  for idx, assertion in enumerate(data.get("assertions"), start=1):
359
374
  field = assertion.get("field", "未知")
360
375
  expected = assertion.get("expected", "未知")
376
+ assert_type = assertion.get("type", "eq")
377
+ passed = True
378
+ actual_value = None
361
379
  try:
362
380
  self.info_log(f" [{idx}/{total}] 执行断言: field={field}, expected={expected}")
363
- self.__assert_data(assertion, response)
381
+ actual_value = self.__assert_data(assertion, response)
364
382
  self.info_log(f" [{idx}/{total}] ✅ 断言通过: {field}")
365
383
  except AssertionError as e:
384
+ passed = False
385
+ actual_value = getattr(self, '_last_actual', None)
366
386
  self.error_log(f" [{idx}/{total}] ❌ 断言失败: {field} — {e}")
367
387
  assertion_errors.append({
368
388
  "index": idx,
@@ -371,6 +391,14 @@ class BaseCase(CaseLogHandler):
371
391
  "error_type": "ASSERTION_FAILED",
372
392
  "message": str(e)
373
393
  })
394
+ # 记录断言结果
395
+ self._assert_results.append({
396
+ "field": field,
397
+ "type": assert_type,
398
+ "expected": expected,
399
+ "actual": actual_value,
400
+ "passed": passed,
401
+ })
374
402
  # 所有断言执行完毕后输出汇总
375
403
  passed_count = total - len(assertion_errors)
376
404
  if assertion_errors:
@@ -477,6 +505,8 @@ class BaseCase(CaseLogHandler):
477
505
  """执行用例"""
478
506
  start_time = time.time()
479
507
  self._precondition_errors = [] # 记录前置错误(供结果查询)
508
+ self._extract_results = [] # 重置提取结果
509
+ self._assert_results = [] # 重置断言结果
480
510
  case_name = data.get('title')
481
511
  has_failure = False # ★ 新增:失败标志位
482
512
  try:
@@ -1,7 +1,12 @@
1
+ import time
2
+
3
+
1
4
  class CaseLogHandler:
2
5
  """用例日志处理类"""
3
6
  def save_log(self,msg,level):
4
7
  """保存日志"""
8
+ ts = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
9
+ msg = ts + " | " + msg
5
10
  # 1、判断当前实例是否有日志属性
6
11
  if not hasattr(self,"log_data"):
7
12
  setattr(self,"log_data",[])
@@ -32,7 +32,9 @@ class TestRunner:
32
32
  ENV.clear()
33
33
  ENV.update(self.env_data)
34
34
  # 将tools中的函数(用户自定义),通过exec执行(字符串中的python函数),加载到TestTools模块的命名空间中
35
- exec(ENV.get("global_func"), global_func.__dict__)
35
+ _gf = ENV.get("global_func")
36
+ if _gf and isinstance(_gf, str):
37
+ exec(_gf, global_func.__dict__)
36
38
  # 创建测试结果的记录器
37
39
  test_result = TestResult(all=len(testcases["cases"]),name=testcases["name"])
38
40
  # 运行测试用例
@@ -48,7 +50,9 @@ class TestRunner:
48
50
  ENV.clear()
49
51
  ENV.update(self.env_data)
50
52
  # 将tools中的函数(用户自定义),通过exec执行(字符串中的python函数),加载到TestTools模块的命名空间中
51
- exec(ENV.get("global_func"), global_func.__dict__)
53
+ _gf = ENV.get("global_func")
54
+ if _gf and isinstance(_gf, str):
55
+ exec(_gf, global_func.__dict__)
52
56
  # 创建测试结果的记录器
53
57
  test_result = TestResult(all=1)
54
58
  # log.info_log("执行测试用例:",testcases)
@@ -65,7 +69,9 @@ class TestRunner:
65
69
  ENV.clear()
66
70
  ENV.update(self.env_data)
67
71
  # 将tools中的函数(用户自定义),通过exec执行(字符串中的python函数),加载到TestTools模块的命名空间中
68
- exec(ENV.get("global_func"), global_func.__dict__)
72
+ _gf = ENV.get("global_func")
73
+ if _gf and isinstance(_gf, str):
74
+ exec(_gf, global_func.__dict__)
69
75
  # 新增检测日志
70
76
  log.info_log(f"gen_random_num 是否存在:{hasattr(global_func, 'gen_random_num')}")
71
77
  log.info_log(f"gen_random_num 是否可调用:{callable(getattr(global_func, 'gen_random_num', None))}")
@@ -0,0 +1,86 @@
1
+ from ApiEngine import log
2
+ from ApiEngine.BaseCase import BaseCase
3
+
4
+
5
+ class TestResult:
6
+ """测试结果类"""
7
+ def __init__(self, all, name="调试运行"):
8
+ """
9
+ :param all: 测试套件中的用例个数
10
+ :param name: 测试套件的名称
11
+ """
12
+ self.all = all
13
+ self.name = name
14
+ self.success = 0
15
+ self.fail = 0
16
+ self.error = 0
17
+ self.results = []
18
+
19
+ def _build_info(self, test: BaseCase, status: str) -> dict:
20
+ """构建用例结果信息(包含提取和断言结果)"""
21
+ return {
22
+ "name": getattr(test, "name", ""),
23
+ "url": getattr(test, "url", ""),
24
+ "method": getattr(test, "method", ""),
25
+ "request_headers": getattr(test, "request_headers", ""),
26
+ "request_body": getattr(test, "request_body", ""),
27
+ "response_code": getattr(test, "status_code", ""),
28
+ "response_headers": getattr(test, "response_headers", ""),
29
+ "response_body": getattr(test, "response_body", ""),
30
+ "status": status,
31
+ "log_data": getattr(test, "log_data", ""),
32
+ "run_time": getattr(test, "elapsed_ms", ""),
33
+ "extract_info": getattr(test, "_extract_results", []),
34
+ "assert_info": getattr(test, "_assert_results", []),
35
+ }
36
+
37
+ def add_success(self, test: BaseCase):
38
+ """
39
+ :param test: 用例对象
40
+ :return:
41
+ """
42
+ self.success += 1
43
+ self.results.append(self._build_info(test, "success"))
44
+
45
+ def add_fail(self, test: BaseCase):
46
+ """
47
+ :param test: 用例对象
48
+ :return:
49
+ """
50
+ self.fail += 1
51
+ self.results.append(self._build_info(test, "fail"))
52
+
53
+ def add_error(self, test: BaseCase, error):
54
+ """
55
+ :param test: 用例对象
56
+ :return:
57
+ """
58
+ self.error += 1
59
+ # 同时记录到全局日志和当前用例日志,确保返回结果中也能看到 ERROR 记录
60
+ log.error_log("用例执行错误,错误信息:", error)
61
+ try:
62
+ test.error_log("用例执行错误,错误信息:", error)
63
+ except Exception:
64
+ pass
65
+ self.results.append(self._build_info(test, "error"))
66
+
67
+ def get_result_info(self):
68
+ """
69
+ :return: 测试结果信息
70
+ """
71
+ if self.success == self.all:
72
+ state = "success"
73
+ elif self.fail > 0:
74
+ state = "fail"
75
+ else:
76
+ state = "error"
77
+ info = {
78
+ "name": self.name,
79
+ "all": self.all,
80
+ "success": self.success,
81
+ "fail": self.fail,
82
+ "error": self.error,
83
+ "cases": self.results,
84
+ "state": state
85
+ }
86
+ return info
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: api_engine_xin
3
- Version: 0.0.20
3
+ Version: 0.0.22
4
4
  Summary: 接口测试平台测试用例执行引擎
5
5
  Home-page: https://pypi.org/project/api_engine_xin/
6
6
  Author: Shawn
@@ -19,19 +19,6 @@ Classifier: Operating System :: OS Independent
19
19
  Requires-Python: >=3.6
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
- Requires-Dist: pymysql>=1.0.0
23
- Requires-Dist: requests>=2.26.0
24
- Dynamic: author
25
- Dynamic: author-email
26
- Dynamic: classifier
27
- Dynamic: description
28
- Dynamic: description-content-type
29
- Dynamic: home-page
30
- Dynamic: keywords
31
- Dynamic: license-file
32
- Dynamic: requires-dist
33
- Dynamic: requires-python
34
- Dynamic: summary
35
22
 
36
23
 
37
24
  # api_engine_xin
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
2
- Name: api_engine_xin
3
- Version: 0.0.20
1
+ Metadata-Version: 2.1
2
+ Name: api-engine-xin
3
+ Version: 0.0.22
4
4
  Summary: 接口测试平台测试用例执行引擎
5
5
  Home-page: https://pypi.org/project/api_engine_xin/
6
6
  Author: Shawn
@@ -19,19 +19,6 @@ Classifier: Operating System :: OS Independent
19
19
  Requires-Python: >=3.6
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
- Requires-Dist: pymysql>=1.0.0
23
- Requires-Dist: requests>=2.26.0
24
- Dynamic: author
25
- Dynamic: author-email
26
- Dynamic: classifier
27
- Dynamic: description
28
- Dynamic: description-content-type
29
- Dynamic: home-page
30
- Dynamic: keywords
31
- Dynamic: license-file
32
- Dynamic: requires-dist
33
- Dynamic: requires-python
34
- Dynamic: summary
35
22
 
36
23
 
37
24
  # api_engine_xin
@@ -12,7 +12,7 @@ with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as fh:
12
12
  # 核心配置
13
13
  setup(
14
14
  name="api_engine_xin", # ✅【必须改】pip install 这个名字!全网唯一,不能和PyPI上已有的包名重复
15
- version="0.0.20", # ✅【必须改】版本号,每次更新包都要升级版本(如0.0.2、0.1.0)
15
+ version="0.0.22", # ✅【必须改】版本号,每次更新包都要升级版本(如0.0.2、0.1.0)
16
16
  author="Shawn",# ✅【必须改】你的名字/昵称
17
17
  author_email="xiaoh0525@xiaoh.com",# ✅【必须改】你的注册PyPI的邮箱
18
18
  description="接口测试平台测试用例执行引擎", # ✅【必须改】一句话说明你的包是干嘛的
@@ -1,109 +0,0 @@
1
- from ApiEngine import log
2
- from ApiEngine.BaseCase import BaseCase
3
-
4
-
5
- class TestResult:
6
- """测试结果类"""
7
- def __init__(self, all, name="调试运行"):
8
- """
9
- :param all: 测试套件中的用例个数
10
- :param name: 测试套件的名称
11
- """
12
- self.all = all
13
- self.name = name
14
- self.success = 0
15
- self.fail = 0
16
- self.error = 0
17
- self.results = []
18
-
19
- def add_success(self, test: BaseCase):
20
- """
21
- :param test: 用例对象
22
- :return:
23
- """
24
- self.success += 1
25
- info = {
26
- "name": getattr(test, "name",""),
27
- "url": getattr(test, "url",""),
28
- "method": getattr(test, "method",""),
29
- "request_headers": getattr(test, "request_headers",""),
30
- "request_body": getattr(test, "request_body",""),
31
- "response_code": getattr(test, "status_code",""),
32
- "response_headers": getattr(test, "response_headers",""),
33
- "response_body": getattr(test, "response_body",""),
34
- "status": "success",
35
- "log_data": getattr(test, "log_data", ""),
36
- "run_time": getattr(test,"elapsed_ms","")
37
- }
38
- self.results.append(info)
39
-
40
- def add_fail(self, test: BaseCase):
41
- """
42
- :param test: 用例对象
43
- :return:
44
- """
45
- self.fail += 1
46
- info = {
47
- "name": getattr(test, "name",""),
48
- "url": getattr(test, "url",""),
49
- "method": getattr(test, "method",""),
50
- "request_headers": getattr(test, "request_headers",""),
51
- "request_body": getattr(test, "request_body",""),
52
- "response_code": getattr(test, "status_code",""),
53
- "response_headers": getattr(test, "response_headers",""),
54
- "response_body": getattr(test, "response_body",""),
55
- "status": "fail",
56
- "log_data": getattr(test, "log_data", ""),
57
- "run_time": getattr(test,"elapsed_ms","")
58
- }
59
- self.results.append(info)
60
-
61
- def add_error(self, test: BaseCase, error):
62
- """
63
- :param test: 用例对象
64
- :return:
65
- """
66
- self.error += 1
67
- # 同时记录到全局日志和当前用例日志,确保返回结果中也能看到 ERROR 记录
68
- log.error_log("用例执行错误,错误信息:", error)
69
- try:
70
- # BaseCase 继承了 CaseLogHandler,直接复用其 error_log 记录到用例的 log_data
71
- test.error_log("用例执行错误,错误信息:", error)
72
- except Exception:
73
- # 如果 test 上不存在 error_log(理论上不会发生),则忽略,不影响后续结果生成
74
- pass
75
- info = {
76
- "name": getattr(test, "name",""),
77
- "url": getattr(test, "url",""),
78
- "method": getattr(test, "method",""),
79
- "request_headers": getattr(test, "request_headers",""),
80
- "request_body": getattr(test, "request_body",""),
81
- "response_code": getattr(test, "status_code",""),
82
- "response_headers": getattr(test, "response_headers",""),
83
- "response_body": getattr(test, "response_body",""),
84
- "status": "error",
85
- "log_data": getattr(test, "log_data", ""),
86
- "run_time": getattr(test,"elapsed_ms","")
87
- }
88
- self.results.append(info)
89
-
90
- def get_result_info(self):
91
- """
92
- :return: 测试结果信息
93
- """
94
- if self.success == self.all:
95
- state = "success"
96
- elif self.fail > 0:
97
- state = "fail"
98
- else:
99
- state = "error"
100
- info = {
101
- "name": self.name,
102
- "all": self.all,
103
- "success": self.success,
104
- "fail": self.fail,
105
- "error": self.error,
106
- "cases": self.results,
107
- "state": state
108
- }
109
- return info
File without changes