pytest-api-framework-alpha 0.1.4__tar.gz → 0.1.6__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 (29) hide show
  1. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/PKG-INFO +1 -3
  2. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/base_class.py +22 -15
  3. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/conftest.py +57 -30
  4. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/extract.py +11 -6
  5. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/http_client.py +15 -10
  6. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/startapp.py +7 -4
  7. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/common.py +19 -5
  8. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/validate.py +43 -19
  9. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/pytest_api_framework_alpha.egg-info/PKG-INFO +1 -3
  10. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/pytest_api_framework_alpha.egg-info/requires.txt +0 -2
  11. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/setup.py +1 -3
  12. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/__init__.py +0 -0
  13. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/allure_report.py +0 -0
  14. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/db/__init__.py +0 -0
  15. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/db/mysql_db.py +0 -0
  16. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/db/redis_db.py +0 -0
  17. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/exit_code.py +0 -0
  18. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/global_attribute.py +0 -0
  19. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/render_data.py +0 -0
  20. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/report.py +0 -0
  21. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/__init__.py +0 -0
  22. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/encrypt.py +0 -0
  23. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/log_util.py +0 -0
  24. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/teams_util.py +0 -0
  25. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/yaml_util.py +0 -0
  26. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/pytest_api_framework_alpha.egg-info/SOURCES.txt +0 -0
  27. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/pytest_api_framework_alpha.egg-info/dependency_links.txt +0 -0
  28. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/pytest_api_framework_alpha.egg-info/top_level.txt +0 -0
  29. {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-api-framework-alpha
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Author: alpha
5
5
  Author-email:
6
6
  Requires-Python: >=3.6
@@ -22,8 +22,6 @@ Requires-Dist: redis==3.5.3
22
22
  Requires-Dist: requests==2.25.1
23
23
  Requires-Dist: requests-toolbelt==1.0.0
24
24
  Requires-Dist: retry==0.9.2
25
- Requires-Dist: pytest-rerunfailures==11.1
26
- Requires-Dist: pytest-timeout==2.2.0
27
25
  Requires-Dist: dill==0.3.8
28
26
  Dynamic: author
29
27
  Dynamic: requires-dist
@@ -1,4 +1,5 @@
1
1
  import traceback
2
+ import importlib
2
3
  from urllib.parse import urlparse, urlunparse, urljoin
3
4
 
4
5
  import pytest
@@ -9,17 +10,20 @@ from framework.db.mysql_db import MysqlDB
9
10
  from framework.db.redis_db import RedisDB
10
11
  from framework.utils.log_util import logger
11
12
  from framework.http_client import ResponseUtil
12
- from framework.global_attribute import CONFIG, GlobalAttribute
13
+ from framework.utils.common import snake_to_pascal
14
+ from framework.global_attribute import CONFIG, GlobalAttribute, _FRAMEWORK_CONTEXT
15
+ from config.settings import UNAUTHORIZED_CODE
16
+ module = importlib.import_module("test_case.conftest")
13
17
 
14
18
 
15
19
  class BaseTestCase(object):
16
- context: GlobalAttribute = None
17
- config: GlobalAttribute = None
18
20
  http = None
19
21
  data: Box = None
20
22
  scenario: Box = None
21
23
  belong_app = None
22
24
  response: ResponseUtil = None
25
+ context: GlobalAttribute = None
26
+ config: GlobalAttribute = None
23
27
 
24
28
  def request(self, app=None, *, account, data, **kwargs):
25
29
  try:
@@ -28,6 +32,9 @@ class BaseTestCase(object):
28
32
  domain = self.context.get(app).get("domain")
29
33
  data.request.url = self.replace_domain(data.request.url, domain)
30
34
  self.response = getattr(app_http, account).request(data=data, kwargs=kwargs)
35
+ if self.response.status_code in UNAUTHORIZED_CODE:
36
+ _http = _FRAMEWORK_CONTEXT.get(key="_http")
37
+ setattr(_http, app, getattr(module, f"{snake_to_pascal(app)}Login")(app))
31
38
  return self.response
32
39
  except AttributeError as e:
33
40
  logger.error(f"app {app} or account {account} no exist: {e}")
@@ -37,27 +44,27 @@ class BaseTestCase(object):
37
44
 
38
45
  def post(self, app, account, url, data=None, json=None, **kwargs):
39
46
  domain = self.context.get(app).get("domain")
40
- url = urljoin(domain, url)
41
- self.response = getattr(getattr(self.http, app), account).post(app, url, data=data, json=json, **kwargs)
42
- return self.response
47
+ request = {"url": urljoin(domain, url), "data": data, "json": json}
48
+ request.update({"method": "post", "headers": {}, **kwargs})
49
+ return self.request(app=app, account=account, data=Box({"request": request}))
43
50
 
44
51
  def get(self, app, account, url, params=None, **kwargs):
45
52
  domain = self.context.get(app).get("domain")
46
- url = urljoin(domain, url)
47
- self.response = getattr(getattr(self.http, app), account).get(app, url, params=params, **kwargs)
48
- return self.response
53
+ request = {"url": urljoin(domain, url), "params": params}
54
+ request.update({"method": "get", "headers": {}, **kwargs})
55
+ return self.request(app=app, account=account, data=Box({"request": request}))
49
56
 
50
57
  def put(self, app, account, url, data=None, **kwargs):
51
58
  domain = self.context.get(app).get("domain")
52
- url = urljoin(domain, url)
53
- self.response = getattr(getattr(self.http, app), account).put(app, url, data=data, **kwargs)
54
- return self.response
59
+ request = {"url": urljoin(domain, url), "data": data}
60
+ request.update({"method": "put", "headers": {}, **kwargs})
61
+ return self.request(app=app, account=account, data=Box({"request": request}))
55
62
 
56
63
  def delete(self, app, account, url, **kwargs):
57
64
  domain = self.context.get(app).get("domain")
58
- url = urljoin(domain, url)
59
- self.response = getattr(getattr(self.http, app), account).delete(app, url, **kwargs)
60
- return self.response
65
+ request = {"url": urljoin(domain, url)}
66
+ request.update({"method": "delete", "headers": {}, **kwargs})
67
+ return self.request(app=app, account=account, data=Box({"request": request}))
61
68
 
62
69
  def mysql_conn(self, db, app=None):
63
70
  try:
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from itertools import chain
9
9
  from urllib.parse import urljoin
10
10
  from collections import OrderedDict
11
+ from datetime import datetime, timedelta
11
12
 
12
13
  import dill
13
14
  import retry
@@ -25,6 +26,7 @@ from framework.utils.common import snake_to_pascal, get_apps, convert_numbers_to
25
26
  from config.settings import DATA_DIR, CASES_DIR
26
27
 
27
28
  all_app = get_apps()
29
+ module = importlib.import_module("test_case.conftest")
28
30
 
29
31
 
30
32
  def find_data_path_by_case(app, case_file_name):
@@ -100,6 +102,8 @@ def pytest_generate_tests(metafunc):
100
102
  test_data = YamlUtil(data_path).load_yml()
101
103
  # 测试用例公共数据
102
104
  case_common = test_data.get("case_common")
105
+ if case_common.get("ignore"):
106
+ pytest.skip("Skipp the case because ignore")
103
107
  scenarios = case_common.get("scenarios")
104
108
  # 测试用例数据
105
109
  case_data = test_data.get(func_name)
@@ -155,25 +159,26 @@ def pytest_generate_tests(metafunc):
155
159
  if scenarios:
156
160
  ids = list()
157
161
  for index, item in enumerate(scenarios):
158
- if case_common.get("ignore"):
162
+ if item.get("scenario").get("ignore"):
163
+ continue
164
+ if func_name in item.get("scenario").get("exclude", list()):
159
165
  continue
160
166
  _mark = CONTEXT.get("mark")
161
- if _mark:
162
- flag = item.get("scenario").get("flag")
163
- if flag != _mark:
164
- continue
167
+ if _mark and item.get("scenario").get("flag") != _mark:
168
+ continue
165
169
  deep_copied_case_data = copy.deepcopy(case_data)
166
170
  try:
167
171
  deep_copied_case_data["_scenario"] = item.get("scenario")
168
172
  case_data_list.append(deep_copied_case_data)
169
173
  ids.append(case_data.get("title") + f"#{index + 1}")
170
174
  except KeyError as e:
171
- logger.error(f"parametrize参数化格式不正确:{e}")
175
+ logger.error(f"scenario参数化格式不正确:{e}")
172
176
  traceback.print_exc()
173
177
  pytest.exit(ExitCode.PARAMETRIZE_ATTRIBUTE_NOT_EXIT)
174
178
  metafunc.parametrize("data", case_data_list, ids=ids, scope="function")
175
179
  else:
176
180
  if not case_common.get("ignore"):
181
+ case_data["_scenario"] = {"data": {}}
177
182
  case_data_list = [case_data]
178
183
  # 进行参数化生成用例
179
184
  metafunc.parametrize("data", case_data_list, ids=[f'{case_data.get("title")}#1'], scope="function")
@@ -277,11 +282,13 @@ def pytest_runtest_call(item):
277
282
  for prev_item in reversed(item.session.items[:index]): # 只遍历当前 item 之前的
278
283
  if prev_item.parent.name == current_cls_name and re.search(pattern, prev_item.name).group(
279
284
  1) == current_turn.group(1): # 确保是同一个类
280
- status = getattr(prev_item, "status", None) # 访问 _status 属性
281
- if status == "skipped":
282
- pytest.skip(f"the test_setup method execution error")
285
+ status = getattr(prev_item, "status", None) # 访问 status 属性
286
+ skip_reason = getattr(prev_item, "skip_reason", None) # 访问 skip_reason 属性
287
+ if status == "skipped" and skip_reason in ["the previous method execution skipped",
288
+ "the previous method execution failed"]:
289
+ pytest.skip("the previous method execution skipped")
283
290
  elif status == "failed":
284
- pytest.skip(f"the previous method execution failed")
291
+ pytest.skip("the previous method execution failed")
285
292
 
286
293
  # 获取原始测试数据
287
294
  origin_data = item.funcargs.get("data")
@@ -291,25 +298,29 @@ def pytest_runtest_call(item):
291
298
  rendered_data = RenderData(origin_data).render()
292
299
  # 函数式测试用例添加参数data, belong_app
293
300
  http = item.funcargs.get("http")
294
- item.funcargs["data"] = Box(rendered_data)
295
- item.funcargs["scenario"] = Box(convert_numbers_to_decimal(rendered_data.get("_scenario").get("data")))
296
- item.funcargs["belong_app"] = origin_data.get("_belong_app")
297
- item.funcargs["config"] = CONFIG
298
- item.funcargs["context"] = CONTEXT
301
+ item.funcargs["data"] = item.instance.data = Box(rendered_data)
302
+ item.funcargs["scenario"] = item.instance.scenario = Box(
303
+ convert_numbers_to_decimal(rendered_data.get("_scenario").get("data")))
304
+ _belong_app = origin_data.get("_belong_app")
305
+ item.funcargs["belong_app"] = item.instance.belong_app = _belong_app
306
+ item.funcargs["config"] = item.instance.config = CONFIG
307
+ item.funcargs["context"] = item.instance.context = CONTEXT
299
308
  # 类式测试用例添加参数http,data, belong_app
300
309
  item.instance.http = http
301
- item.instance.data = Box(rendered_data)
302
- item.instance.scenario = Box(convert_numbers_to_decimal(rendered_data.get("_scenario").get("data")))
303
- item.instance.belong_app = origin_data.get("_belong_app")
304
- item.instance.context = CONTEXT
305
- item.instance.config = CONFIG
306
310
 
307
- # 获取测试函数体内容
308
- func_source = re.sub(r'(?<!["\'])#.*', '', dill.source.getsource(item.function))
309
- # 校验测试用例中是否有断言
310
- if "assert" not in func_source:
311
- logger.error(f"测试方法:{item.originalname}缺少断言")
312
- pytest.exit(ExitCode.MISSING_ASSERTIONS)
311
+ # # 获取测试函数体内容
312
+ # func_source = re.sub(r'(?<!["\'])#.*', '', dill.source.getsource(item.function))
313
+ # # 校验测试用例中是否有断言
314
+ # if "assert" not in func_source:
315
+ # logger.error(f"测试方法:{item.originalname}缺少断言")
316
+ # pytest.exit(ExitCode.MISSING_ASSERTIONS)
317
+
318
+ # 判断token是否过期,过期则重新登录
319
+ expire_time = _FRAMEWORK_CONTEXT.get(_belong_app)
320
+ if expire_time:
321
+ _http = _FRAMEWORK_CONTEXT.get("_http")
322
+ if datetime.now() >= expire_time:
323
+ setattr(_http, _belong_app, getattr(module, f"{snake_to_pascal(_belong_app)}Login")(_belong_app))
313
324
 
314
325
 
315
326
  def pytest_runtest_teardown(item):
@@ -330,6 +341,17 @@ def pytest_runtest_makereport(item, call):
330
341
  report = outcome.get_result()
331
342
  # 将测试结果存储到 item 对象的自定义属性 `_test_status`
332
343
  if report.when == "call": # 只记录测试执行阶段的状态,不包括 setup/teardown
344
+ longrepr = report.longrepr
345
+ if longrepr:
346
+ try:
347
+ if ":" in longrepr[2]:
348
+ key, reason = longrepr[2].split(":")
349
+ else:
350
+ key, reason = longrepr[2], ""
351
+ if key == "Skipped":
352
+ item.skip_reason = reason
353
+ except:
354
+ pass
333
355
  item.status = report.outcome # 'passed', 'failed', or 'skipped'
334
356
 
335
357
 
@@ -347,7 +369,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
347
369
  pass_rate = 0
348
370
  # 打印自定义统计信息
349
371
  terminalreporter.write("\n============ 执行结果统计 ============\n", blue=True, bold=True)
350
- terminalreporter.write(f"执行用例总数: {passed + failed + skipped}\n", bold=True)
372
+ terminalreporter.write(f"执行用例总数: {passed + failed}\n", bold=True)
351
373
  terminalreporter.write(f"通过用例数: {passed}\n", green=True, bold=True)
352
374
  terminalreporter.write(f"失败用例数: {failed}\n", red=True, bold=True)
353
375
  terminalreporter.write(f"跳过用例数: {skipped}\n", yellow=True, bold=True)
@@ -400,9 +422,9 @@ def context():
400
422
 
401
423
 
402
424
  @retry.retry(tries=3, delay=1)
403
- @pytest.fixture(scope="session", autouse=True)
425
+ @pytest.fixture(scope="function", autouse=True)
404
426
  def http():
405
- yield Http
427
+ yield _FRAMEWORK_CONTEXT.get("_http")
406
428
 
407
429
 
408
430
  class Http(object):
@@ -410,10 +432,15 @@ class Http(object):
410
432
 
411
433
 
412
434
  def login():
413
- module = importlib.import_module("conftest")
414
435
  try:
436
+ logger.info("登录账号".center(80, "*"))
415
437
  for app in all_app:
416
438
  setattr(Http, app, getattr(module, f"{snake_to_pascal(app)}Login")(app))
439
+ token_expiry = CONTEXT.get(app).get("token_expiry")
440
+ if token_expiry:
441
+ expire_time = datetime.now() + timedelta(seconds=token_expiry)
442
+ _FRAMEWORK_CONTEXT.set(key=app, value=expire_time)
443
+ logger.info("登录完成".center(80, "*"))
417
444
  return Http
418
445
  except Exception as e:
419
446
  logger.error(f"登录{app}异常:{e}")
@@ -4,7 +4,7 @@ from typing import List
4
4
 
5
5
  import allure
6
6
  import pytest
7
- from box import Box
7
+ from box import Box, BoxList
8
8
  from jsonpath import jsonpath
9
9
 
10
10
  from framework.exit_code import ExitCode
@@ -15,7 +15,12 @@ from framework.global_attribute import CONTEXT
15
15
 
16
16
  class Extract(object):
17
17
  def __init__(self, response, belong_app):
18
- self.response = response
18
+ if isinstance(response.json(), dict):
19
+ self.response = Box(response.json())
20
+ elif isinstance(response.json(), list):
21
+ self.response = BoxList(response.json())
22
+ else:
23
+ self.response = response
19
24
  self.belong_app = belong_app
20
25
  self.context = CONTEXT
21
26
 
@@ -48,13 +53,13 @@ class Extract(object):
48
53
  if key.startswith(tuple(f"{app}." for app in self.context.all_app)):
49
54
  self.belong_app, key = key.split(".", 1)
50
55
 
51
- extract_value = jsonpath(self.response.json(), expression)[0]
56
+ extract_value = jsonpath(self.response, expression)[0]
52
57
  with allure.step(f"后置提取变量: {key}: {extract_value}"):
53
58
  self.context.set(key, extract_value, self.belong_app)
54
59
  logger.info(f"后置提取变量: {key}: {extract_value}")
55
60
 
56
61
  except Exception as e:
57
- logger.error(f"jsonpath表达式错误或响应内容异常{e} 表达式: {expression};响应内容: {self.response.json()}")
62
+ logger.error(f"jsonpath表达式错误或响应内容异常{e} 表达式: {expression};响应内容: {self.response}")
58
63
  logger.error(f"后置提取变量{key}失败")
59
64
 
60
65
  def extract_by_regex(self, key, reg_expression):
@@ -76,9 +81,9 @@ class Extract(object):
76
81
  def extract_by_box(self, key, expression):
77
82
  if key.startswith(tuple(f"{app}." for app in self.context.all_app)):
78
83
  self.belong_app, key = key.split(".", 1)
79
- extract_value = self.get_nested_value(Box(self.response.json()), expression)
84
+ extract_value = self.get_nested_value(Box(self.response), expression)
80
85
  if not extract_value:
81
- logger.error(f"box表达式或响应内容异常 表达式: {expression}; 响应内容: {self.response.json()}")
86
+ logger.error(f"box表达式或响应内容异常 表达式: {expression}; 响应内容: {self.response}")
82
87
  logger.error(f"后置提取变量{key}失败")
83
88
  else:
84
89
  with allure.step(f"后置提取变量: {key}: {extract_value}"):
@@ -8,10 +8,13 @@ import json as built_json
8
8
  from box import Box, BoxList
9
9
  from jsonpath import jsonpath as jp
10
10
  from requests.models import Response
11
+ from requests_toolbelt import MultipartEncoder
12
+
11
13
  from framework.extract import Extract
14
+ from framework.validate import Validate
12
15
  from framework.utils.log_util import logger
13
16
  from framework.global_attribute import CONTEXT
14
- from requests_toolbelt import MultipartEncoder
17
+ from config.settings import CONSOLE_DETAILED_LOG
15
18
  from framework.utils.common import convert_numbers_to_decimal
16
19
 
17
20
 
@@ -111,7 +114,8 @@ class HttpClient(object):
111
114
  with allure.step(f"请求method: {request_obj.get('method')}"):
112
115
  logger.info(f"请求method: {request_obj.get('method')}")
113
116
  with allure.step(f"请求headers: {request_obj.get('headers')}"):
114
- logger.info(f"请求headers: {request_obj.get('headers')}")
117
+ if CONSOLE_DETAILED_LOG:
118
+ logger.info(f"请求headers: {request_obj.get('headers')}")
115
119
  if request_obj.get('params'):
116
120
  with allure.step(f"请求参数params: {request_obj.get('params')}"):
117
121
  logger.info(f"请求参数params: {request_obj.get('params')}")
@@ -123,18 +127,19 @@ class HttpClient(object):
123
127
  logger.info(f"请求参数json: {request_obj.get('json')}")
124
128
 
125
129
  with allure.step("响应结果"):
126
- with allure.step(f"响应code: {self.response.status_code}"):
127
- logger.info(f"响应code: {self.response.status_code}")
130
+ with allure.step(f"响应status code: {self.response.status_code}"):
131
+ logger.info(f"响应status code: {self.response.status_code}")
128
132
  with allure.step(f"响应headers: {self.response.headers}"):
129
- logger.info(f"响应headers: {self.response.headers}")
133
+ if CONSOLE_DETAILED_LOG:
134
+ logger.info(f"响应headers: {self.response.headers}")
130
135
  with allure.step(f"响应body: {self.response.json()}"):
131
136
  logger.info(f"响应body: {self.response.json()}")
132
137
 
133
- # # 断言
134
- # validates = params.get("validate")
135
- # if validates:
136
- # with allure.step("结果断言"):
137
- # Validate(self.response).valid(validates)
138
+ # 断言
139
+ validates = data.get("validate")
140
+ if validates:
141
+ with allure.step("结果断言"):
142
+ Validate(self.response).valid(validates)
138
143
 
139
144
  # 提取变量
140
145
  expressions = data.get("extract")
@@ -72,9 +72,12 @@ def create_test_data(app):
72
72
  {
73
73
  "scenario": {
74
74
  "data": {
75
- "tag_id": 4,
75
+ "tag_id": 1,
76
76
  "user_id": 1
77
- }
77
+ },
78
+ "exclude": [
79
+ "test_untitled"
80
+ ]
78
81
  }
79
82
  }
80
83
  ]
@@ -86,8 +89,8 @@ def create_test_data(app):
86
89
  "url": "/login",
87
90
  "method": "post",
88
91
  "json": {
89
- "name": "Jerry",
90
- "user_id": "${user_id}"
92
+ "username": "Jerry",
93
+ "password": "${user_id}"
91
94
  }
92
95
  },
93
96
  "extract": [
@@ -7,9 +7,9 @@ from typing import Any
7
7
  from decimal import Decimal
8
8
  from datetime import datetime
9
9
  from urllib.parse import unquote, quote, quote_plus, unquote_plus
10
+
10
11
  import pyotp
11
12
  import cn2an as c2a
12
-
13
13
  from config.settings import CONFIG_DIR
14
14
 
15
15
 
@@ -210,18 +210,32 @@ def get_apps():
210
210
  os.path.isdir(os.path.join(CONFIG_DIR, name)) and not name.startswith(("__", "."))]
211
211
 
212
212
 
213
+ # 匹配合法数字字符串(可含千分位)
214
+ decimal_pattern = re.compile(r'^\d{1,3}(,\d{3})*(\.\d+)?$|^\d+(\.\d+)?$')
215
+
216
+
213
217
  def convert_numbers_to_decimal(obj: Any) -> Any:
214
218
  if isinstance(obj, str):
215
- stripped = obj.replace(",", "")
216
- try:
217
- return Decimal(stripped)
218
- except Exception:
219
+ if decimal_pattern.match(obj):
220
+ int_part = obj.split('.')[0]
221
+ if len(int_part) > 10 and "," not in int_part:
222
+ return obj # 整数部分超过10位,不转换
223
+ try:
224
+ stripped = obj.replace(",", "")
225
+ return Decimal(stripped)
226
+ except Exception:
227
+ return obj
228
+ else:
219
229
  return obj
220
230
  elif isinstance(obj, dict):
221
231
  return {k: convert_numbers_to_decimal(v) for k, v in obj.items()}
222
232
  elif isinstance(obj, list):
223
233
  return [convert_numbers_to_decimal(item) for item in obj]
224
234
  elif isinstance(obj, (int, float)) and not isinstance(obj, bool):
235
+ # 处理原生数值类型
236
+ int_part = str(obj).split('.')[0]
237
+ if len(int_part) > 10:
238
+ return obj # 整数部分超过10位,不转换
225
239
  return Decimal(str(obj))
226
240
  else:
227
241
  return obj
@@ -1,14 +1,21 @@
1
1
  import re
2
2
 
3
3
  import allure
4
- from utils.log_util import logger
4
+ from box import Box, BoxList
5
5
  from jsonpath import jsonpath
6
- from utils.common import an2cn, is_digit
6
+
7
+ from framework.utils.log_util import logger
8
+ from framework.utils.common import an2cn, is_digit
7
9
 
8
10
 
9
11
  class Validate(object):
10
12
  def __init__(self, response):
11
- self.response = response
13
+ if isinstance(response.json(), dict):
14
+ self.response = Box(response.json())
15
+ elif isinstance(response.json(), list):
16
+ self.response = BoxList(response.json())
17
+ else:
18
+ self.response = response
12
19
 
13
20
  self.mapping = {
14
21
  "eq": "assert_equal",
@@ -43,13 +50,13 @@ class Validate(object):
43
50
  logger.info(
44
51
  f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言通过")
45
52
  else:
46
- raise Exception(f"不支持的断言方式: {key}")
53
+ logger.error(f"不支持的断言方式: {key}")
47
54
  except AssertionError as e:
48
55
  with allure.step(
49
56
  f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言失败。 失败原因: {e}"):
50
57
  logger.error(
51
58
  f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言失败。 失败原因: {e}")
52
- raise AssertionError(e)
59
+ logger.error(e)
53
60
 
54
61
  def parse_expectant_expression(self, expectant_expression):
55
62
 
@@ -61,17 +68,19 @@ class Validate(object):
61
68
  elif expectant_expression.startswith("/") and expectant_expression.endswith("/"):
62
69
  return self.exec_reg(expectant_expression[1: -1])
63
70
  else:
64
- # response对象取值
65
- return self.exec_response(expectant_expression)
71
+ # box句点表达式
72
+ return self.exec_box(expectant_expression)
73
+
66
74
  except Exception as e:
67
- with allure.step(f"不支持的变量提取方式{expectant_expression}: {e}"):
68
- logger.error(f"不支持的变量提取方式{expectant_expression}: {e}")
75
+ with allure.step(str(e)):
76
+ logger.error(str(e))
77
+ raise Exception(e)
69
78
 
70
79
  def exec_jsonpath(self, expression):
71
80
  try:
72
- return jsonpath(self.response.json(), expression)[0]
73
- except Exception:
74
- raise Exception(f"jsonpath表达式错误或非预期响应内容。表达式: {expression};响应内容: {self.response.json()}")
81
+ return jsonpath(self.response, expression)[0]
82
+ except Exception as e:
83
+ raise Exception(f"jsonpath表达式错误或非预期响应内容{e} 表达式: {expression};响应内容: {self.response}")
75
84
 
76
85
  def exec_reg(self, reg_expression):
77
86
  try:
@@ -80,14 +89,29 @@ class Validate(object):
80
89
  return eval(result)
81
90
  else:
82
91
  return result
83
- except AttributeError:
84
- raise Exception(f"正则表达式或非预期响应内容。表达式: {reg_expression}; 响应内容: {self.response.text}")
92
+ except AttributeError as e:
93
+ raise Exception(f"正则表达式或非预期响应内容{e} 表达式: {reg_expression}; 响应内容: {self.response.text}")
85
94
 
86
- def exec_response(self, attr):
87
- if attr in ["status_code", "url", "ok", "encoding"]:
88
- return getattr(self.response, attr)
89
- else:
90
- raise Exception(f"不支持提取的Response对象属性{attr}")
95
+ def exec_box(self, expression):
96
+ try:
97
+ return self.get_nested_value(Box(self.response), expression)
98
+ except Exception as e:
99
+ raise Exception(f"box表达式或响应内容异常{e} 表达式: {expression}; 响应内容: {self.response.text}")
100
+
101
+ @staticmethod
102
+ def get_nested_value(obj, attr_path):
103
+ """通过字符串路径(如 'a.b[0].c')获取嵌套属性值"""
104
+ # 使用正则表达式分解路径,支持属性和索引的组合
105
+ path_elements = re.findall(r'(\w+)|\[(\d+)]', attr_path)
106
+ try:
107
+ for attr, index in path_elements:
108
+ if attr: # 属性部分
109
+ obj = getattr(obj, attr)
110
+ if index: # 索引部分
111
+ obj = obj[int(index)]
112
+ return obj
113
+ except Exception:
114
+ return None
91
115
 
92
116
  def assert_equal(self, expectant_expression, practical_result):
93
117
  expectant_result = self.parse_expectant_expression(expectant_expression)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-api-framework-alpha
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Author: alpha
5
5
  Author-email:
6
6
  Requires-Python: >=3.6
@@ -22,8 +22,6 @@ Requires-Dist: redis==3.5.3
22
22
  Requires-Dist: requests==2.25.1
23
23
  Requires-Dist: requests-toolbelt==1.0.0
24
24
  Requires-Dist: retry==0.9.2
25
- Requires-Dist: pytest-rerunfailures==11.1
26
- Requires-Dist: pytest-timeout==2.2.0
27
25
  Requires-Dist: dill==0.3.8
28
26
  Dynamic: author
29
27
  Dynamic: requires-dist
@@ -16,6 +16,4 @@ redis==3.5.3
16
16
  requests==2.25.1
17
17
  requests-toolbelt==1.0.0
18
18
  retry==0.9.2
19
- pytest-rerunfailures==11.1
20
- pytest-timeout==2.2.0
21
19
  dill==0.3.8
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="pytest-api-framework-alpha", # 包名(必须唯一)
5
- version="0.1.4",
5
+ version="0.1.6",
6
6
  packages=find_packages(),
7
7
  author="alpha",
8
8
  author_email="",
@@ -27,8 +27,6 @@ setup(
27
27
  "requests==2.25.1",
28
28
  "requests-toolbelt==1.0.0",
29
29
  "retry==0.9.2",
30
- "pytest-rerunfailures==11.1",
31
- "pytest-timeout==2.2.0",
32
30
  "dill==0.3.8"
33
31
  ]
34
32
  )