pytest-api-framework-alpha 0.1.0__tar.gz → 0.1.2__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.
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/PKG-INFO +2 -1
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/base_class.py +15 -2
- pytest_api_framework_alpha-0.1.2/framework/conftest.py +422 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/db/mysql_db.py +2 -2
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/extract.py +6 -6
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/global_attribute.py +9 -2
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/http_client.py +10 -4
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/render_data.py +30 -1
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/report.py +1 -1
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/pytest_api_framework_alpha.egg-info/PKG-INFO +2 -1
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/pytest_api_framework_alpha.egg-info/requires.txt +1 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/setup.py +3 -2
- pytest_api_framework_alpha-0.1.0/framework/conftest.py +0 -523
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/__init__.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/allure_report.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/db/__init__.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/db/redis_db.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/exit_code.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/startapp.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/__init__.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/common.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/encrypt.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/log_util.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/teams_util.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/yaml_util.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/validate.py +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/pytest_api_framework_alpha.egg-info/SOURCES.txt +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/pytest_api_framework_alpha.egg-info/dependency_links.txt +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/pytest_api_framework_alpha.egg-info/top_level.txt +0 -0
- {pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/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.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Author: alpha
|
|
5
5
|
Author-email:
|
|
6
6
|
Requires-Python: >=3.6
|
|
@@ -24,6 +24,7 @@ Requires-Dist: requests-toolbelt==1.0.0
|
|
|
24
24
|
Requires-Dist: retry==0.9.2
|
|
25
25
|
Requires-Dist: pytest-rerunfailures==11.1
|
|
26
26
|
Requires-Dist: pytest-timeout==2.2.0
|
|
27
|
+
Requires-Dist: dill==0.3.8
|
|
27
28
|
Dynamic: author
|
|
28
29
|
Dynamic: requires-dist
|
|
29
30
|
Dynamic: requires-python
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/base_class.py
RENAMED
|
@@ -3,10 +3,10 @@ import traceback
|
|
|
3
3
|
import pytest
|
|
4
4
|
from box import Box
|
|
5
5
|
|
|
6
|
-
from framework.
|
|
6
|
+
from framework.exit_code import ExitCode
|
|
7
7
|
from framework.db.mysql_db import MysqlDB
|
|
8
8
|
from framework.db.redis_db import RedisDB
|
|
9
|
-
from framework.
|
|
9
|
+
from framework.utils.log_util import logger
|
|
10
10
|
from framework.http_client import ResponseUtil
|
|
11
11
|
from framework.global_attribute import CONFIG, GlobalAttribute
|
|
12
12
|
|
|
@@ -16,6 +16,7 @@ class BaseTestCase(object):
|
|
|
16
16
|
config: GlobalAttribute = None
|
|
17
17
|
http = None
|
|
18
18
|
data: Box = None
|
|
19
|
+
scenario: Box = None
|
|
19
20
|
belong_app = None
|
|
20
21
|
response: ResponseUtil = None
|
|
21
22
|
|
|
@@ -57,5 +58,17 @@ class BaseTestCase(object):
|
|
|
57
58
|
traceback.print_exc()
|
|
58
59
|
pytest.exit(ExitCode.LOAD_DATABASE_INFO_ERROR)
|
|
59
60
|
|
|
61
|
+
def context_set(self, app=None, *, key, value):
|
|
62
|
+
app = self.default_app(app)
|
|
63
|
+
self.context.set(app=app, key=key, value=value)
|
|
64
|
+
|
|
65
|
+
def context_get(self, app=None, *, key):
|
|
66
|
+
app = self.default_app(app)
|
|
67
|
+
return self.context.get(app=app, key=key)
|
|
68
|
+
|
|
69
|
+
def config_get(self, app=None, *, key):
|
|
70
|
+
app = self.default_app(app)
|
|
71
|
+
return self.config.get(app=app, key=key)
|
|
72
|
+
|
|
60
73
|
def default_app(self, app):
|
|
61
74
|
return app or self.belong_app
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
import copy
|
|
5
|
+
import importlib
|
|
6
|
+
import traceback
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from itertools import chain
|
|
9
|
+
from urllib.parse import urljoin
|
|
10
|
+
from collections import OrderedDict
|
|
11
|
+
|
|
12
|
+
import dill
|
|
13
|
+
import retry
|
|
14
|
+
import allure
|
|
15
|
+
import pytest
|
|
16
|
+
from box import Box
|
|
17
|
+
|
|
18
|
+
from framework.exit_code import ExitCode
|
|
19
|
+
from framework.utils.log_util import logger
|
|
20
|
+
from framework.render_data import RenderData
|
|
21
|
+
from framework.utils.yaml_util import YamlUtil
|
|
22
|
+
from framework.allure_report import generate_report
|
|
23
|
+
from framework.utils.common import snake_to_pascal, get_apps
|
|
24
|
+
from framework.global_attribute import CONTEXT, CONFIG, _FRAMEWORK_CONTEXT
|
|
25
|
+
from config.settings import DATA_DIR, CASES_DIR
|
|
26
|
+
|
|
27
|
+
all_app = get_apps()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def pytest_configure(config):
|
|
31
|
+
"""
|
|
32
|
+
初始化时被调用,可以用于设置全局状态或配置
|
|
33
|
+
:param config:
|
|
34
|
+
:return:
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
for app in all_app:
|
|
38
|
+
# 将所有app对应环境的基础测试数据加到全局
|
|
39
|
+
CONTEXT.set_from_yaml(f"config/{app}/context.yaml", CONTEXT.env, app)
|
|
40
|
+
# 将所有app对应环境的中间件配置加到全局
|
|
41
|
+
CONFIG.set_from_yaml(f"config/{app}/config.yaml", CONTEXT.env, app)
|
|
42
|
+
CONTEXT.set(key="all_app", value=all_app)
|
|
43
|
+
sys.path.append(CASES_DIR)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def pytest_addoption(parser):
|
|
47
|
+
parser.addini(name="ignore_error_and_continue", help="是否忽略失败case,继续执行")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def find_data_path_by_case(app, case_file_name):
|
|
51
|
+
"""
|
|
52
|
+
基于case文件名称查找与之对应的yml文件路径
|
|
53
|
+
:param app:
|
|
54
|
+
:param case_file_name:
|
|
55
|
+
:return:
|
|
56
|
+
"""
|
|
57
|
+
for file_path in Path(os.path.join(DATA_DIR, app)).rglob(f"{case_file_name}.y*"):
|
|
58
|
+
if file_path:
|
|
59
|
+
return file_path
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def __init_allure(params):
|
|
63
|
+
"""设置allure中case的 title, description, level"""
|
|
64
|
+
case_level_map = {
|
|
65
|
+
"p0": allure.severity_level.BLOCKER,
|
|
66
|
+
"p1": allure.severity_level.CRITICAL,
|
|
67
|
+
"p2": allure.severity_level.NORMAL,
|
|
68
|
+
"p3": allure.severity_level.MINOR,
|
|
69
|
+
"p4": allure.severity_level.TRIVIAL,
|
|
70
|
+
}
|
|
71
|
+
allure.dynamic.title(params.get("title"))
|
|
72
|
+
allure.dynamic.description(params.get("describe"))
|
|
73
|
+
allure.dynamic.severity(case_level_map.get(params.get("level")))
|
|
74
|
+
allure.dynamic.feature(params.get("module"))
|
|
75
|
+
allure.dynamic.story(params.get("describe"))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def pytest_generate_tests(metafunc):
|
|
79
|
+
"""
|
|
80
|
+
生成(多个)对测试函数的参数化调用
|
|
81
|
+
:param metafunc:
|
|
82
|
+
:return:
|
|
83
|
+
"""
|
|
84
|
+
# 获取当前待执行用例的文件名
|
|
85
|
+
module_name = metafunc.module.__name__.split('.')[-1]
|
|
86
|
+
func_file_path = metafunc.module.__file__
|
|
87
|
+
# 获取当前待执行用例的函数名
|
|
88
|
+
func_name = metafunc.function.__name__
|
|
89
|
+
if func_name in ["test_setup", "test_teardown"]:
|
|
90
|
+
return
|
|
91
|
+
# 获取测试用例所属app
|
|
92
|
+
belong_app = Path(func_file_path).relative_to(CASES_DIR).parts[0]
|
|
93
|
+
# 获取当前用例对应的测试数据路径
|
|
94
|
+
data_path = find_data_path_by_case(belong_app, module_name)
|
|
95
|
+
|
|
96
|
+
if not data_path:
|
|
97
|
+
logger.error(f"测试数据文件: {func_file_path} 不存在")
|
|
98
|
+
traceback.print_exc()
|
|
99
|
+
pytest.exit(ExitCode.CASE_YAML_NOT_EXIST)
|
|
100
|
+
test_data = YamlUtil(data_path).load_yml()
|
|
101
|
+
# 测试用例公共数据
|
|
102
|
+
case_common = test_data.get("case_common")
|
|
103
|
+
scenarios = case_common.get("scenarios")
|
|
104
|
+
# 测试用例数据
|
|
105
|
+
case_data = test_data.get(func_name)
|
|
106
|
+
if not case_data:
|
|
107
|
+
logger.error(f"测试方法: {func_name} 对应的测试数据不存在")
|
|
108
|
+
traceback.print_exc()
|
|
109
|
+
pytest.exit(ExitCode.CASE_DATA_NOT_EXIST)
|
|
110
|
+
if case_data.get("request") is None:
|
|
111
|
+
case_data["request"] = dict()
|
|
112
|
+
if case_data.get("request").get("headers") is None:
|
|
113
|
+
case_data["request"]["headers"] = dict()
|
|
114
|
+
|
|
115
|
+
# 合并测试数据
|
|
116
|
+
case_data.setdefault("module", case_common.get("module"))
|
|
117
|
+
case_data.setdefault("describe", case_common.get("describe"))
|
|
118
|
+
case_data["_belong_app"] = belong_app
|
|
119
|
+
|
|
120
|
+
domain = CONTEXT.get(key="domain", app=belong_app)
|
|
121
|
+
domain = domain if domain.startswith("http") else f"https://{domain}"
|
|
122
|
+
url = case_data.get("request").get("url")
|
|
123
|
+
method = case_data.get("request").get("method")
|
|
124
|
+
if not url:
|
|
125
|
+
if not case_common.get("url"):
|
|
126
|
+
logger.error(f"测试数据request中缺少必填字段: url", case_data)
|
|
127
|
+
pytest.exit(ExitCode.YAML_MISSING_FIELDS)
|
|
128
|
+
if case_common.get("url").strip().startswith("${"):
|
|
129
|
+
case_data["request"]["url"] = case_common.get("url")
|
|
130
|
+
else:
|
|
131
|
+
case_data["request"]["url"] = urljoin(domain, case_common.get("url"))
|
|
132
|
+
else:
|
|
133
|
+
if url.strip().startswith("${"):
|
|
134
|
+
case_data["request"]["url"] = url
|
|
135
|
+
else:
|
|
136
|
+
case_data["request"]["url"] = urljoin(domain, url)
|
|
137
|
+
|
|
138
|
+
if not method:
|
|
139
|
+
if not case_common.get("method"):
|
|
140
|
+
logger.error(f"测试数据request中缺少必填字段: method", case_data)
|
|
141
|
+
pytest.exit(ExitCode.YAML_MISSING_FIELDS)
|
|
142
|
+
case_data["request"]["method"] = case_common.get("method")
|
|
143
|
+
|
|
144
|
+
for key in ["title", "level"]:
|
|
145
|
+
if key not in case_data:
|
|
146
|
+
logger.error(f"测试数据{func_name}中缺少必填字段: {key}", case_data)
|
|
147
|
+
pytest.exit(ExitCode.YAML_MISSING_FIELDS)
|
|
148
|
+
|
|
149
|
+
if case_data.get("mark"):
|
|
150
|
+
metafunc.function.marks = [case_data.get("mark"), case_data.get("level")]
|
|
151
|
+
else:
|
|
152
|
+
metafunc.function.marks = [case_data.get("level")]
|
|
153
|
+
|
|
154
|
+
case_data_list = list()
|
|
155
|
+
if scenarios:
|
|
156
|
+
ids = list()
|
|
157
|
+
for index, item in enumerate(scenarios):
|
|
158
|
+
if case_common.get("ignore"):
|
|
159
|
+
continue
|
|
160
|
+
_mark = CONTEXT.get("mark")
|
|
161
|
+
if _mark:
|
|
162
|
+
flag = item.get("scenario").get("flag")
|
|
163
|
+
if flag != _mark:
|
|
164
|
+
continue
|
|
165
|
+
deep_copied_case_data = copy.deepcopy(case_data)
|
|
166
|
+
try:
|
|
167
|
+
deep_copied_case_data["_scenario"] = item.get("scenario")
|
|
168
|
+
case_data_list.append(deep_copied_case_data)
|
|
169
|
+
ids.append(case_data.get("title") + f"#{index + 1}")
|
|
170
|
+
except KeyError as e:
|
|
171
|
+
logger.error(f"parametrize参数化格式不正确:{e}")
|
|
172
|
+
traceback.print_exc()
|
|
173
|
+
pytest.exit(ExitCode.PARAMETRIZE_ATTRIBUTE_NOT_EXIT)
|
|
174
|
+
metafunc.parametrize("data", case_data_list, ids=ids, scope="function")
|
|
175
|
+
else:
|
|
176
|
+
if not case_common.get("ignore"):
|
|
177
|
+
case_data_list = [case_data]
|
|
178
|
+
# 进行参数化生成用例
|
|
179
|
+
metafunc.parametrize("data", case_data_list, ids=[f'{case_data.get("title")}#1'], scope="function")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def pytest_collection_modifyitems(items):
|
|
183
|
+
for item in items:
|
|
184
|
+
try:
|
|
185
|
+
marks = item.function.marks
|
|
186
|
+
for mark in marks:
|
|
187
|
+
if isinstance(mark, list):
|
|
188
|
+
for _ in mark:
|
|
189
|
+
item.add_marker(_)
|
|
190
|
+
else:
|
|
191
|
+
item.add_marker(mark)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def pytest_collection_finish(session):
|
|
197
|
+
"""获取最终排序后的 items 列表"""
|
|
198
|
+
# 过滤掉item名称是test_setup或test_teardown的
|
|
199
|
+
session.items = [item for item in session.items if item.name not in ["test_setup", "test_teardown"]]
|
|
200
|
+
|
|
201
|
+
# 1. 筛选出带井号 名称带'#' 的item,并记录原始索引
|
|
202
|
+
hash_items_with_index = [(index, item) for index, item in enumerate(session.items) if "#" in item.name]
|
|
203
|
+
|
|
204
|
+
# 2. 按照 'cls' 对带井号的元素进行分组
|
|
205
|
+
grouped_by_cls = {}
|
|
206
|
+
for index, item in hash_items_with_index:
|
|
207
|
+
cls = item.cls.__module__ + item.parent.name
|
|
208
|
+
if cls not in grouped_by_cls:
|
|
209
|
+
grouped_by_cls[cls] = []
|
|
210
|
+
grouped_by_cls[cls].append((index, item)) # 记录索引和元素
|
|
211
|
+
|
|
212
|
+
# 3. 对每个 cls 分组内的带井号的元素进行排序
|
|
213
|
+
for cls, group in grouped_by_cls.items():
|
|
214
|
+
group_values = [x[1] for x in group]
|
|
215
|
+
# 获取item#号后面的数字
|
|
216
|
+
pattern = r"#(\d+)]"
|
|
217
|
+
grouped_data = OrderedDict()
|
|
218
|
+
# 按照#号后面的数字进行排序并分组
|
|
219
|
+
for item in group_values:
|
|
220
|
+
index = re.search(pattern, item.name).group(1)
|
|
221
|
+
grouped_data.setdefault(index, []).append(item)
|
|
222
|
+
# 标记每个分组的第一个和最后一个
|
|
223
|
+
for group2 in grouped_data.values():
|
|
224
|
+
group2[0].funcargs["first"] = True
|
|
225
|
+
group2[-1].funcargs["last"] = True
|
|
226
|
+
|
|
227
|
+
group_values = list(chain.from_iterable(grouped_data.values()))
|
|
228
|
+
|
|
229
|
+
# 4. 将排序后的items放回原列表
|
|
230
|
+
for (original_index, _), val in zip(group, group_values):
|
|
231
|
+
session.items[original_index] = val # 将反转后的元素替换回原位置
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def pytest_runtestloop(session):
|
|
235
|
+
_FRAMEWORK_CONTEXT.set(key="_http", value=login())
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def pytest_runtest_setup(item):
|
|
239
|
+
if item.funcargs.get("first"):
|
|
240
|
+
test_object = item.instance
|
|
241
|
+
test_object.context = CONTEXT
|
|
242
|
+
test_object.config = CONFIG
|
|
243
|
+
test_object.http = _FRAMEWORK_CONTEXT.get(key="_http")
|
|
244
|
+
data = item.callspec.params.get("data")
|
|
245
|
+
test_object.data = Box(data)
|
|
246
|
+
test_object.scenario = Box(data.get("_scenario").get("data"))
|
|
247
|
+
test_object.belong_app = data.get("_belong_app")
|
|
248
|
+
test_setup = getattr(test_object, "test_setup", None)
|
|
249
|
+
if test_setup:
|
|
250
|
+
try:
|
|
251
|
+
test_setup()
|
|
252
|
+
item.funcargs["setup_success"] = True
|
|
253
|
+
except Exception as e:
|
|
254
|
+
item.funcargs["setup_success"] = False
|
|
255
|
+
traceback.print_exc()
|
|
256
|
+
logger.error(f"{item.location[0]} test_setup方法执行异常:{e}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def pytest_runtest_call(item):
|
|
260
|
+
"""
|
|
261
|
+
模版渲染,运行用例
|
|
262
|
+
:param item:
|
|
263
|
+
:return:
|
|
264
|
+
"""
|
|
265
|
+
ignore_error_and_continue = item.config.getini("ignore_error_and_continue")
|
|
266
|
+
if ignore_error_and_continue == "false":
|
|
267
|
+
# setup方法执行失败,则主动标记用例执行失败,不会执行用例
|
|
268
|
+
if item.funcargs.get("setup_success") is False:
|
|
269
|
+
pytest.skip(f"{item.nodeid} test_setup execute error")
|
|
270
|
+
# 判断上一个用例是否执行失败,如果上一个用例执行失败,则主动标记用例执行失败,不会执行用例(解决场景性用例,有一个失败则后续用例判为失败)
|
|
271
|
+
index = item.session.items.index(item)
|
|
272
|
+
current_cls_name = item.parent.name
|
|
273
|
+
# 向前遍历,找到属于同一个类的用例
|
|
274
|
+
pattern = r"#(\d+)]"
|
|
275
|
+
current_turn = re.search(pattern, item.name)
|
|
276
|
+
if current_turn:
|
|
277
|
+
for prev_item in reversed(item.session.items[:index]): # 只遍历当前 item 之前的
|
|
278
|
+
if prev_item.parent.name == current_cls_name and re.search(pattern, prev_item.name).group(
|
|
279
|
+
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")
|
|
283
|
+
elif status == "failed":
|
|
284
|
+
pytest.skip(f"the previous method execution failed")
|
|
285
|
+
|
|
286
|
+
# 获取原始测试数据
|
|
287
|
+
origin_data = item.funcargs.get("data")
|
|
288
|
+
__init_allure(origin_data)
|
|
289
|
+
logger.info(f"执行用例: {item.nodeid}")
|
|
290
|
+
# 对原始请求数据进行渲染替换
|
|
291
|
+
rendered_data = RenderData(origin_data).render()
|
|
292
|
+
# 函数式测试用例添加参数data, belong_app
|
|
293
|
+
http = item.funcargs.get("http")
|
|
294
|
+
item.funcargs["data"] = Box(rendered_data)
|
|
295
|
+
item.funcargs["scenario"] = Box(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
|
|
299
|
+
# 类式测试用例添加参数http,data, belong_app
|
|
300
|
+
item.instance.http = http
|
|
301
|
+
item.instance.data = Box(rendered_data)
|
|
302
|
+
item.instance.scenario = Box(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
|
+
|
|
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)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def pytest_runtest_teardown(item):
|
|
316
|
+
if item.funcargs.get("last") and getattr(item, "status", None) not in ["skipped", "failed"]:
|
|
317
|
+
test_object = item.instance
|
|
318
|
+
test_teardown = getattr(test_object, "test_teardown", None)
|
|
319
|
+
if test_teardown:
|
|
320
|
+
try:
|
|
321
|
+
test_teardown()
|
|
322
|
+
except Exception as e:
|
|
323
|
+
pytest.fail(f"the test_teardown method execution error: {e}")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
|
327
|
+
def pytest_runtest_makereport(item, call):
|
|
328
|
+
"""拦截 pytest 生成测试报告,移除特定用例的统计"""
|
|
329
|
+
outcome = yield
|
|
330
|
+
report = outcome.get_result()
|
|
331
|
+
# 将测试结果存储到 item 对象的自定义属性 `_test_status`
|
|
332
|
+
if report.when == "call": # 只记录测试执行阶段的状态,不包括 setup/teardown
|
|
333
|
+
item.status = report.outcome # 'passed', 'failed', or 'skipped'
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def pytest_terminal_summary(terminalreporter, exitstatus, config):
|
|
337
|
+
"""在 pytest 结束后修改统计数据或添加自定义报告"""
|
|
338
|
+
stats = terminalreporter.stats
|
|
339
|
+
# 统计各种测试结果
|
|
340
|
+
passed = len(stats.get("passed", []))
|
|
341
|
+
failed = len(stats.get("failed", []))
|
|
342
|
+
skipped = len(stats.get("skipped", []))
|
|
343
|
+
total = passed + failed + skipped
|
|
344
|
+
try:
|
|
345
|
+
pass_rate = round(passed / (total - skipped) * 100, 2)
|
|
346
|
+
except ZeroDivisionError:
|
|
347
|
+
pass_rate = 0
|
|
348
|
+
# 打印自定义统计信息
|
|
349
|
+
terminalreporter.write("\n============ 执行结果统计 ============\n", blue=True, bold=True)
|
|
350
|
+
terminalreporter.write(f"执行用例总数: {passed + failed + skipped}\n", bold=True)
|
|
351
|
+
terminalreporter.write(f"通过用例数: {passed}\n", green=True, bold=True)
|
|
352
|
+
terminalreporter.write(f"失败用例数: {failed}\n", red=True, bold=True)
|
|
353
|
+
terminalreporter.write(f"跳过用例数: {skipped}\n", yellow=True, bold=True)
|
|
354
|
+
terminalreporter.write(f"用例通过率: {pass_rate}%\n", green=True, bold=True)
|
|
355
|
+
terminalreporter.write("====================================\n", blue=True, bold=True)
|
|
356
|
+
# 生成allure测试报告
|
|
357
|
+
generate_report()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def pytest_exception_interact(node, call, report):
|
|
361
|
+
"""
|
|
362
|
+
用例执行抛出异常时,将异常记录到日志
|
|
363
|
+
:param node:
|
|
364
|
+
:param call:
|
|
365
|
+
:param report:
|
|
366
|
+
:return:
|
|
367
|
+
"""
|
|
368
|
+
if call.excinfo.type is AssertionError:
|
|
369
|
+
logger.error(f"{node.nodeid} failed: {call.excinfo.value}\n")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@pytest.fixture(autouse=True)
|
|
373
|
+
def response():
|
|
374
|
+
response = None
|
|
375
|
+
yield response
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@pytest.fixture(autouse=True)
|
|
379
|
+
def data():
|
|
380
|
+
data: dict = dict()
|
|
381
|
+
yield data
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@pytest.fixture(autouse=True)
|
|
385
|
+
def belong_app():
|
|
386
|
+
app = None
|
|
387
|
+
yield app
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@pytest.fixture(autouse=True)
|
|
391
|
+
def config():
|
|
392
|
+
config = None
|
|
393
|
+
yield config
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@pytest.fixture(autouse=True)
|
|
397
|
+
def context():
|
|
398
|
+
context = None
|
|
399
|
+
yield context
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@retry.retry(tries=3, delay=1)
|
|
403
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
404
|
+
def http():
|
|
405
|
+
yield Http
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class Http(object):
|
|
409
|
+
...
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def login():
|
|
413
|
+
module = importlib.import_module("conftest")
|
|
414
|
+
try:
|
|
415
|
+
for app in all_app:
|
|
416
|
+
setattr(Http, app, getattr(module, f"{snake_to_pascal(app)}Login")(app))
|
|
417
|
+
return Http
|
|
418
|
+
except Exception as e:
|
|
419
|
+
logger.error(f"登录{app}异常:{e}")
|
|
420
|
+
traceback.print_exc()
|
|
421
|
+
pytest.exit(ExitCode.LOGIN_ERROR)
|
|
422
|
+
return None
|
|
@@ -7,9 +7,9 @@ import pytest
|
|
|
7
7
|
from box import Box
|
|
8
8
|
from jsonpath import jsonpath
|
|
9
9
|
|
|
10
|
+
from framework.exit_code import ExitCode
|
|
10
11
|
from framework.utils.common import is_digit
|
|
11
12
|
from framework.utils.log_util import logger
|
|
12
|
-
from framework.exit_code import ExitCode
|
|
13
13
|
from framework.global_attribute import CONTEXT
|
|
14
14
|
|
|
15
15
|
|
|
@@ -55,7 +55,7 @@ class Extract(object):
|
|
|
55
55
|
|
|
56
56
|
except Exception as e:
|
|
57
57
|
logger.error(f"jsonpath表达式错误或响应内容异常{e} 表达式: {expression};响应内容: {self.response.json()}")
|
|
58
|
-
|
|
58
|
+
logger.error(f"后置提取变量{key}失败")
|
|
59
59
|
|
|
60
60
|
def extract_by_regex(self, key, reg_expression):
|
|
61
61
|
try:
|
|
@@ -69,9 +69,9 @@ class Extract(object):
|
|
|
69
69
|
self.context.set(key, extract_value, self.belong_app)
|
|
70
70
|
logger.info(f"后置提取变量: {key}: {extract_value}")
|
|
71
71
|
|
|
72
|
-
except Exception:
|
|
73
|
-
logger.error(f"
|
|
74
|
-
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"正则表达式或响应内容异常{e} 表达式: {reg_expression}; 响应内容: {self.response.text}")
|
|
74
|
+
logger.error(f"后置提取变量{key}失败")
|
|
75
75
|
|
|
76
76
|
def extract_by_box(self, key, expression):
|
|
77
77
|
if key.startswith(tuple(f"{app}." for app in self.context.all_app)):
|
|
@@ -79,7 +79,7 @@ class Extract(object):
|
|
|
79
79
|
extract_value = self.get_nested_value(Box(self.response.json()), expression)
|
|
80
80
|
if not extract_value:
|
|
81
81
|
logger.error(f"box表达式或响应内容异常 表达式: {expression}; 响应内容: {self.response.json()}")
|
|
82
|
-
|
|
82
|
+
logger.error(f"后置提取变量{key}失败")
|
|
83
83
|
else:
|
|
84
84
|
with allure.step(f"后置提取变量: {key}: {extract_value}"):
|
|
85
85
|
self.context.set(key, extract_value, self.belong_app)
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/global_attribute.py
RENAMED
|
@@ -5,10 +5,10 @@ import pytest
|
|
|
5
5
|
from box import Box
|
|
6
6
|
from box.exceptions import BoxError
|
|
7
7
|
|
|
8
|
-
from framework.utils.log_util import logger
|
|
9
|
-
from framework.utils.common import singleton
|
|
10
8
|
from config.settings import ROOT_DIR
|
|
11
9
|
from framework.exit_code import ExitCode
|
|
10
|
+
from framework.utils.log_util import logger
|
|
11
|
+
from framework.utils.common import singleton
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class GlobalAttribute(object):
|
|
@@ -94,7 +94,14 @@ class Config(GlobalAttribute):
|
|
|
94
94
|
...
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
@singleton
|
|
98
|
+
class FrameworkContext(GlobalAttribute):
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
|
|
97
102
|
# 创建管理变量的全局对象,用于存储临时变量
|
|
98
103
|
CONTEXT = Context()
|
|
99
104
|
# 创建配置内容管理的全局对象
|
|
100
105
|
CONFIG = Config()
|
|
106
|
+
|
|
107
|
+
_FRAMEWORK_CONTEXT = FrameworkContext()
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/http_client.py
RENAMED
|
@@ -4,8 +4,8 @@ from urllib.parse import urljoin
|
|
|
4
4
|
|
|
5
5
|
import allure
|
|
6
6
|
import requests
|
|
7
|
-
from box import Box
|
|
8
7
|
import json as built_json
|
|
8
|
+
from box import Box, BoxList
|
|
9
9
|
from jsonpath import jsonpath as jp
|
|
10
10
|
from requests.models import Response
|
|
11
11
|
from framework.extract import Extract
|
|
@@ -17,7 +17,12 @@ from requests_toolbelt import MultipartEncoder
|
|
|
17
17
|
class ResponseUtil(object):
|
|
18
18
|
def __init__(self, response: Response):
|
|
19
19
|
self.response = response
|
|
20
|
-
self.
|
|
20
|
+
if isinstance(self.response.json(), dict):
|
|
21
|
+
self.box = Box(self.response.json())
|
|
22
|
+
elif isinstance(self.response.json(), list):
|
|
23
|
+
self.box = BoxList(self.response.json())
|
|
24
|
+
else:
|
|
25
|
+
self.box = self.response.text
|
|
21
26
|
|
|
22
27
|
def __getattr__(self, name):
|
|
23
28
|
try:
|
|
@@ -94,7 +99,8 @@ class HttpClient(object):
|
|
|
94
99
|
if _is_multipart:
|
|
95
100
|
m = MultipartEncoder(fields=request_obj["data"])
|
|
96
101
|
request_obj["data"] = m
|
|
97
|
-
request_obj["headers"]
|
|
102
|
+
request_obj["headers"].update(self.headers)
|
|
103
|
+
request_obj["headers"]['Content-Type'] = m.content_type
|
|
98
104
|
else:
|
|
99
105
|
request_obj["headers"].update(self.headers)
|
|
100
106
|
self.response = ResponseUtil(requests.request(**request_obj))
|
|
@@ -105,7 +111,7 @@ class HttpClient(object):
|
|
|
105
111
|
logger.info(f"请求method: {request_obj.get('method')}")
|
|
106
112
|
with allure.step(f"请求headers: {request_obj.get('headers')}"):
|
|
107
113
|
logger.info(f"请求headers: {request_obj.get('headers')}")
|
|
108
|
-
if request_obj.get('
|
|
114
|
+
if request_obj.get('params'):
|
|
109
115
|
with allure.step(f"请求参数params: {request_obj.get('params')}"):
|
|
110
116
|
logger.info(f"请求参数params: {request_obj.get('params')}")
|
|
111
117
|
if request_obj.get('data'):
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/render_data.py
RENAMED
|
@@ -6,8 +6,8 @@ import traceback
|
|
|
6
6
|
import allure
|
|
7
7
|
import pytest
|
|
8
8
|
from faker import Faker
|
|
9
|
-
from framework.utils.log_util import logger
|
|
10
9
|
from framework.exit_code import ExitCode
|
|
10
|
+
from framework.utils.log_util import logger
|
|
11
11
|
from framework.global_attribute import CONTEXT
|
|
12
12
|
from config.settings import FAKER_LANGUAGE, DATA_DIR
|
|
13
13
|
|
|
@@ -31,6 +31,7 @@ class RenderData(object):
|
|
|
31
31
|
def __init__(self, data):
|
|
32
32
|
self.data = data
|
|
33
33
|
self.request = data.get("request")
|
|
34
|
+
self.scenario = data.get("_scenario").get("data")
|
|
34
35
|
self.context = CONTEXT
|
|
35
36
|
self.faker = SingletonFaker(locale=FAKER_LANGUAGE).faker
|
|
36
37
|
self._is_multipart = False
|
|
@@ -41,11 +42,39 @@ class RenderData(object):
|
|
|
41
42
|
:return:
|
|
42
43
|
"""
|
|
43
44
|
with allure.step("渲染数据"):
|
|
45
|
+
if self.scenario:
|
|
46
|
+
self.request = self.replace_variables_recursive(self.request, self.scenario)
|
|
44
47
|
self.replace_attribute(self.request)
|
|
45
48
|
self.data["request"] = self.request
|
|
46
49
|
self.data["_is_multipart"] = self._is_multipart
|
|
47
50
|
return self.data
|
|
48
51
|
|
|
52
|
+
def replace_variables_recursive(self, data, params):
|
|
53
|
+
"""
|
|
54
|
+
递归遍历 data 并用 params[key] 替换 ${key} 变量,保持数据类型
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
if isinstance(data, dict): # 处理字典
|
|
58
|
+
new_dict = {}
|
|
59
|
+
for key, value in data.items():
|
|
60
|
+
new_dict[key] = self.replace_variables_recursive(value, params) # 递归处理
|
|
61
|
+
return new_dict
|
|
62
|
+
|
|
63
|
+
elif isinstance(data, list): # 处理列表
|
|
64
|
+
return [self.replace_variables_recursive(item, params) for item in data]
|
|
65
|
+
|
|
66
|
+
elif isinstance(data, str) and data.startswith("${") and data.endswith("}"):
|
|
67
|
+
# 仅当字符串是完整的 "${key}" 格式时才替换
|
|
68
|
+
key = data[2:-1] # 提取 key 名称
|
|
69
|
+
if key in params.keys():
|
|
70
|
+
value = params[key]
|
|
71
|
+
logger.info(f"前置读取变量: {key}: {value}")
|
|
72
|
+
return value # 直接赋值(不存在则返回原值)
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
else: # 其他类型(int, float, bool, None)直接返回
|
|
76
|
+
return data
|
|
77
|
+
|
|
49
78
|
def replace_attribute(self, data):
|
|
50
79
|
pattern = re.compile(r"\$\{([\w.\[\]0-9]+(?:\(\w*(?:,\w*)*\))?)}")
|
|
51
80
|
file_path_pattern = re.compile(
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import argparse
|
|
3
|
-
from pathlib import Path
|
|
4
3
|
import datetime
|
|
5
4
|
import traceback
|
|
5
|
+
from pathlib import Path
|
|
6
6
|
from framework.db.mysql_db import MysqlDB
|
|
7
7
|
from config.settings import DATABASE_HOST, DATABASE_PASSWORD, DATABASE_DB, DATABASE_USERNAME, DATABASE_PORT
|
|
8
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-api-framework-alpha
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Author: alpha
|
|
5
5
|
Author-email:
|
|
6
6
|
Requires-Python: >=3.6
|
|
@@ -24,6 +24,7 @@ Requires-Dist: requests-toolbelt==1.0.0
|
|
|
24
24
|
Requires-Dist: retry==0.9.2
|
|
25
25
|
Requires-Dist: pytest-rerunfailures==11.1
|
|
26
26
|
Requires-Dist: pytest-timeout==2.2.0
|
|
27
|
+
Requires-Dist: dill==0.3.8
|
|
27
28
|
Dynamic: author
|
|
28
29
|
Dynamic: requires-dist
|
|
29
30
|
Dynamic: requires-python
|
|
@@ -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.
|
|
5
|
+
version="0.1.2",
|
|
6
6
|
packages=find_packages(),
|
|
7
7
|
author="alpha",
|
|
8
8
|
author_email="",
|
|
@@ -28,7 +28,8 @@ setup(
|
|
|
28
28
|
"requests-toolbelt==1.0.0",
|
|
29
29
|
"retry==0.9.2",
|
|
30
30
|
"pytest-rerunfailures==11.1",
|
|
31
|
-
"pytest-timeout==2.2.0"
|
|
31
|
+
"pytest-timeout==2.2.0",
|
|
32
|
+
"dill==0.3.8"
|
|
32
33
|
]
|
|
33
34
|
|
|
34
35
|
)
|
|
@@ -1,523 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
import sys
|
|
4
|
-
import copy
|
|
5
|
-
import inspect
|
|
6
|
-
import platform
|
|
7
|
-
import importlib
|
|
8
|
-
import traceback
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from urllib.parse import urljoin
|
|
11
|
-
|
|
12
|
-
import retry
|
|
13
|
-
import allure
|
|
14
|
-
import pytest
|
|
15
|
-
from box import Box
|
|
16
|
-
from itertools import groupby
|
|
17
|
-
from framework.utils.log_util import logger
|
|
18
|
-
from framework.utils.yaml_util import YamlUtil
|
|
19
|
-
from framework.utils.encrypt import RsaByPubKey
|
|
20
|
-
from framework.utils.teams_util import TeamsUtil
|
|
21
|
-
from framework.utils.common import generate_2fa_code, snake_to_pascal, get_apps
|
|
22
|
-
from framework.exit_code import ExitCode
|
|
23
|
-
from framework.http_client import HttpClient
|
|
24
|
-
from framework.render_data import RenderData
|
|
25
|
-
from framework.allure_report import generate_report
|
|
26
|
-
from framework.global_attribute import CONTEXT, CONFIG
|
|
27
|
-
from config.settings import DATA_DIR, CASES_DIR, ROOT_DIR
|
|
28
|
-
|
|
29
|
-
test_results = {"total": 0, "passed": 0, "failed": 0, "skipped": 0}
|
|
30
|
-
# 获取当前系统的文件分隔符
|
|
31
|
-
file_separator = os.sep
|
|
32
|
-
|
|
33
|
-
# 判断用例是否需要被跳过
|
|
34
|
-
should_skip = False
|
|
35
|
-
all_app = get_apps()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def pytest_configure(config):
|
|
39
|
-
"""
|
|
40
|
-
初始化时被调用,可以用于设置全局状态或配置
|
|
41
|
-
:param config:
|
|
42
|
-
:return:
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
for app in all_app:
|
|
46
|
-
# 将所有app对应环境的基础测试数据加到全局
|
|
47
|
-
CONTEXT.set_from_yaml(f"config/{app}/context.yaml", CONTEXT.env, app)
|
|
48
|
-
# 将所有app对应环境的中间件配置加到全局
|
|
49
|
-
CONFIG.set_from_yaml(f"config/{app}/config.yaml", CONTEXT.env, app)
|
|
50
|
-
CONTEXT.set(key="all_app", value=all_app)
|
|
51
|
-
CONTEXT.init_test_case_data_dict()
|
|
52
|
-
sys.path.append(CASES_DIR)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def find_data_path_by_case(app, case_file_name):
|
|
56
|
-
"""
|
|
57
|
-
基于case文件名称查找与之对应的yml文件路径
|
|
58
|
-
:param app:
|
|
59
|
-
:param case_file_name:
|
|
60
|
-
:return:
|
|
61
|
-
"""
|
|
62
|
-
for file_path in Path(os.path.join(DATA_DIR, app)).rglob(f"{case_file_name}.y*"):
|
|
63
|
-
if file_path:
|
|
64
|
-
return file_path
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def __init_allure(params):
|
|
68
|
-
"""设置allure中case的 title, description, level"""
|
|
69
|
-
case_level_map = {
|
|
70
|
-
"p0": allure.severity_level.BLOCKER,
|
|
71
|
-
"p1": allure.severity_level.CRITICAL,
|
|
72
|
-
"p2": allure.severity_level.NORMAL,
|
|
73
|
-
"p3": allure.severity_level.MINOR,
|
|
74
|
-
"p4": allure.severity_level.TRIVIAL,
|
|
75
|
-
}
|
|
76
|
-
allure.dynamic.title(params.get("title"))
|
|
77
|
-
allure.dynamic.description(params.get("describe"))
|
|
78
|
-
allure.dynamic.severity(case_level_map.get(params.get("level")))
|
|
79
|
-
allure.dynamic.feature(params.get("module"))
|
|
80
|
-
allure.dynamic.story(params.get("describe"))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def pytest_generate_tests(metafunc):
|
|
84
|
-
"""
|
|
85
|
-
生成(多个)对测试函数的参数化调用
|
|
86
|
-
:param metafunc:
|
|
87
|
-
:return:
|
|
88
|
-
"""
|
|
89
|
-
# 获取测试函数所属的模块
|
|
90
|
-
module = metafunc.module
|
|
91
|
-
# 获取模块的文件路径
|
|
92
|
-
class_path = os.path.abspath(module.__file__)
|
|
93
|
-
root_path = ROOT_DIR + file_separator
|
|
94
|
-
class_id = class_path.replace(root_path, "").replace(file_separator, '.')
|
|
95
|
-
# 获取测试函数名
|
|
96
|
-
function_name = metafunc.function.__name__
|
|
97
|
-
# 构建全路径
|
|
98
|
-
full_id = f"{class_id}::{function_name}"
|
|
99
|
-
|
|
100
|
-
# print(f"测试用例全路径: {full_id}")
|
|
101
|
-
# 获取当前待执行用例的文件名
|
|
102
|
-
module_name = metafunc.module.__name__.split('.')[-1]
|
|
103
|
-
# 获取当前待执行用例的函数名
|
|
104
|
-
func_name = metafunc.function.__name__
|
|
105
|
-
# 获取测试用例所属app
|
|
106
|
-
belong_app = Path(class_path).relative_to(CASES_DIR).parts[0]
|
|
107
|
-
# 获取当前用例对应的测试数据路径
|
|
108
|
-
data_path = find_data_path_by_case(belong_app, module_name)
|
|
109
|
-
|
|
110
|
-
if not data_path:
|
|
111
|
-
logger.error(f"未找到{metafunc.module.__file__}对应的测试数据文件")
|
|
112
|
-
traceback.print_exc()
|
|
113
|
-
pytest.exit(ExitCode.CASE_YAML_NOT_EXIST)
|
|
114
|
-
test_data = YamlUtil(data_path).load_yml()
|
|
115
|
-
# 测试用例公共数据
|
|
116
|
-
case_common = test_data.get("case_common")
|
|
117
|
-
# 标记用例是否跳过
|
|
118
|
-
metafunc.function.ignore = case_common.get("ignore")
|
|
119
|
-
|
|
120
|
-
scenario_list = case_common.get('scenarios')
|
|
121
|
-
|
|
122
|
-
test_case_datas = []
|
|
123
|
-
|
|
124
|
-
if scenario_list is None:
|
|
125
|
-
test_case_datas = [{'casedata': {}}]
|
|
126
|
-
else:
|
|
127
|
-
for scenario in scenario_list:
|
|
128
|
-
if hasattr(CONTEXT, "mark"):
|
|
129
|
-
if scenario.get('scenario') is not None and str(scenario.get('scenario').get('flag')) == CONTEXT.mark:
|
|
130
|
-
test_case_datas.append({'casedata': scenario.get('scenario').get('data')})
|
|
131
|
-
else:
|
|
132
|
-
if scenario.get('scenario') is not None and scenario.get('scenario').get('data') is not None:
|
|
133
|
-
test_case_datas.append({'casedata': scenario.get('scenario').get('data')})
|
|
134
|
-
|
|
135
|
-
if len(scenario_list) == 0:
|
|
136
|
-
test_case_datas = [{'casedata': {}}]
|
|
137
|
-
|
|
138
|
-
case_data_list = []
|
|
139
|
-
ids = []
|
|
140
|
-
case_suite_index = 0
|
|
141
|
-
|
|
142
|
-
test_case_data_dict = dict()
|
|
143
|
-
|
|
144
|
-
# 是否是@test_suite_setup标识的setup方法
|
|
145
|
-
is_test_setup = 'test_setup' in metafunc.definition.keywords
|
|
146
|
-
is_test_teardown = 'test_teardown' in metafunc.definition.keywords
|
|
147
|
-
if is_test_setup or is_test_teardown:
|
|
148
|
-
for test_case_data in test_case_datas:
|
|
149
|
-
case_suite_index += 1
|
|
150
|
-
test_case_data_dict[f'{full_id}#{case_suite_index}'] = test_case_data
|
|
151
|
-
case_data_list.append(test_case_data)
|
|
152
|
-
ids.append(f'{full_id}#{str(case_suite_index)}')
|
|
153
|
-
|
|
154
|
-
else:
|
|
155
|
-
|
|
156
|
-
for test_case_data in test_case_datas:
|
|
157
|
-
case_suite_index += 1
|
|
158
|
-
test_case_data_dict[f'{full_id}#{case_suite_index}'] = test_case_data
|
|
159
|
-
|
|
160
|
-
# 测试用例数据
|
|
161
|
-
case_data = test_data.get(func_name)
|
|
162
|
-
|
|
163
|
-
if not is_test_setup and not case_data:
|
|
164
|
-
logger.error(f"未找到用例{func_name}对应的数据")
|
|
165
|
-
pytest.exit(ExitCode.CASE_DATA_NOT_EXIST)
|
|
166
|
-
|
|
167
|
-
if case_data.get("request") is None:
|
|
168
|
-
case_data["request"] = dict()
|
|
169
|
-
if case_data.get("request").get("headers") is None:
|
|
170
|
-
case_data["request"]["headers"] = dict()
|
|
171
|
-
|
|
172
|
-
# 合并测试数据
|
|
173
|
-
case_data.setdefault("module", case_common.get("module"))
|
|
174
|
-
case_data.setdefault("describe", case_common.get("describe"))
|
|
175
|
-
case_data["_belong_app"] = belong_app
|
|
176
|
-
|
|
177
|
-
if case_data.get("request").get("url") is not None or case_common.get("url") is not None:
|
|
178
|
-
# 是一个带请求的case 反之 表示这条case不是一个带请求的case
|
|
179
|
-
domain = CONTEXT.get(key="domain", app=belong_app)
|
|
180
|
-
domain = domain if domain.startswith("http") else f"https://{domain}"
|
|
181
|
-
url = case_data.get("request").get("url")
|
|
182
|
-
if url.startswith('${'):
|
|
183
|
-
placeholder = url[2:len(url) - 1]
|
|
184
|
-
actual_url = CONTEXT.get(key=placeholder, app=belong_app)
|
|
185
|
-
if actual_url:
|
|
186
|
-
url = actual_url
|
|
187
|
-
method = case_data.get("request").get("method")
|
|
188
|
-
if not url:
|
|
189
|
-
if not case_common.get("url"):
|
|
190
|
-
logger.error(f"测试数据request中缺少必填字段: url", case_data)
|
|
191
|
-
pytest.exit(ExitCode.YAML_MISSING_FIELDS)
|
|
192
|
-
case_data["request"]["url"] = urljoin(domain, case_common.get("url"))
|
|
193
|
-
else:
|
|
194
|
-
case_data["request"]["url"] = urljoin(domain, url)
|
|
195
|
-
|
|
196
|
-
if not method:
|
|
197
|
-
if not case_common.get("method"):
|
|
198
|
-
logger.error(f"测试数据request中缺少必填字段: method", case_data)
|
|
199
|
-
pytest.exit(ExitCode.YAML_MISSING_FIELDS)
|
|
200
|
-
case_data["request"]["method"] = case_common.get("method")
|
|
201
|
-
|
|
202
|
-
for key in ["title"]:
|
|
203
|
-
if key not in case_data:
|
|
204
|
-
logger.error(f"测试数据{func_name}中缺少必填字段: {key}", case_data)
|
|
205
|
-
pytest.exit(ExitCode.YAML_MISSING_FIELDS)
|
|
206
|
-
|
|
207
|
-
# 给用例打order标记
|
|
208
|
-
if case_data.get("order", None):
|
|
209
|
-
metafunc.function.order = int(case_data.get("order"))
|
|
210
|
-
else:
|
|
211
|
-
metafunc.function.order = None
|
|
212
|
-
|
|
213
|
-
# 给用例打mark标记
|
|
214
|
-
if case_data.get("mark", None):
|
|
215
|
-
metafunc.function.marks = [case_data.get("mark"), case_data.get("level")]
|
|
216
|
-
else:
|
|
217
|
-
metafunc.function.marks = [case_data.get("level")]
|
|
218
|
-
|
|
219
|
-
case_data_list.append(case_data)
|
|
220
|
-
ids.append(f'{full_id}#{case_suite_index}')
|
|
221
|
-
|
|
222
|
-
CONTEXT.set(key=f'{full_id}#test_case_datas', value=test_case_data_dict)
|
|
223
|
-
|
|
224
|
-
metafunc.parametrize("data", case_data_list, ids=ids, scope='function')
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
@pytest.hookimpl
|
|
228
|
-
def pytest_runtest_setup(item):
|
|
229
|
-
allure.dynamic.sub_suite(item.allure_suite_mark)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def pytest_collection_modifyitems(items):
|
|
233
|
-
# 重新排序
|
|
234
|
-
new_items = sort(items)
|
|
235
|
-
# Demo: new_items = [items[0],items[2],items[1],items[3]]
|
|
236
|
-
items[:] = new_items
|
|
237
|
-
for item in items:
|
|
238
|
-
# 用例打标记
|
|
239
|
-
try:
|
|
240
|
-
marks = item.function.marks
|
|
241
|
-
for mark in marks:
|
|
242
|
-
item.add_marker(mark)
|
|
243
|
-
# 用例排序
|
|
244
|
-
order = item.function.order
|
|
245
|
-
if order:
|
|
246
|
-
item.add_marker(pytest.mark.order(order))
|
|
247
|
-
except Exception: # 忽略test_setup,test_teardown方法
|
|
248
|
-
pass
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def __get_group_key__(item):
|
|
252
|
-
return '::'.join(item.nodeid.split('::')[:2])
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def sort(case_items):
|
|
256
|
-
# 按测试类全路径分类,同一个类文件的用例归集到一起
|
|
257
|
-
# 使用 groupby 函数进行分组
|
|
258
|
-
item_group_list = [list(group) for _, group in groupby(case_items, key=__get_group_key__)]
|
|
259
|
-
|
|
260
|
-
all_item_list = []
|
|
261
|
-
clase_id = None
|
|
262
|
-
for items in item_group_list:
|
|
263
|
-
# 找出被test_setup标记的方法
|
|
264
|
-
custom_scope_setup_items = [item for item in items if 'test_setup' in item.keywords]
|
|
265
|
-
custom_scope_teardown_items = [item for item in items if 'test_teardown' in item.keywords]
|
|
266
|
-
# 未被test_setup/test_teardown 标记的test方法
|
|
267
|
-
non_custom_scope_items = [item for item in items if
|
|
268
|
-
'test_setup' not in item.keywords and 'test_teardown' not in item.keywords]
|
|
269
|
-
item_list = []
|
|
270
|
-
# 用例的组数
|
|
271
|
-
case_suite_num = 0
|
|
272
|
-
# 生成每个组当前的索引
|
|
273
|
-
ori_name_temp = None
|
|
274
|
-
ori_name_list = []
|
|
275
|
-
|
|
276
|
-
for item in non_custom_scope_items:
|
|
277
|
-
clase_id = item.cls.__name__
|
|
278
|
-
original_name = item.originalname
|
|
279
|
-
|
|
280
|
-
if ori_name_temp is None or ori_name_temp == original_name:
|
|
281
|
-
ori_name_temp = original_name
|
|
282
|
-
case_suite_num += 1
|
|
283
|
-
ori_name_list.append([original_name, item])
|
|
284
|
-
else:
|
|
285
|
-
break
|
|
286
|
-
|
|
287
|
-
# 根据组数 创建各组的数组 并插入第一个case
|
|
288
|
-
case_dict = dict()
|
|
289
|
-
for i in range(case_suite_num):
|
|
290
|
-
item = ori_name_list[i][1]
|
|
291
|
-
id = item.callspec.id
|
|
292
|
-
|
|
293
|
-
first_part = id.split('#', 1)[-1]
|
|
294
|
-
index = first_part.split(']')[0]
|
|
295
|
-
case_dict[index] = [item]
|
|
296
|
-
|
|
297
|
-
new_start_index = case_suite_num
|
|
298
|
-
# 以new_start_index为起点 重新遍历items
|
|
299
|
-
for i in range(new_start_index, len(non_custom_scope_items)):
|
|
300
|
-
item = non_custom_scope_items[i]
|
|
301
|
-
id = item.callspec.id
|
|
302
|
-
first_part = id.split('#', 1)[-1]
|
|
303
|
-
index = first_part.split(']')[0]
|
|
304
|
-
case_dict.get(index).append(item)
|
|
305
|
-
|
|
306
|
-
setup_dict = dict()
|
|
307
|
-
for item in custom_scope_setup_items:
|
|
308
|
-
id = item.callspec.id
|
|
309
|
-
first_part = id.split('#', 1)[-1]
|
|
310
|
-
index = first_part.split(']')[0]
|
|
311
|
-
setup_dict[index] = [item]
|
|
312
|
-
|
|
313
|
-
teardown_dict = dict()
|
|
314
|
-
for item in custom_scope_teardown_items:
|
|
315
|
-
id = item.callspec.id
|
|
316
|
-
first_part = id.split('#', 1)[-1]
|
|
317
|
-
index = first_part.split(']')[0]
|
|
318
|
-
teardown_dict[index] = [item]
|
|
319
|
-
|
|
320
|
-
index = 0
|
|
321
|
-
for id in case_dict:
|
|
322
|
-
index += 1
|
|
323
|
-
if setup_dict:
|
|
324
|
-
setup_item_list = setup_dict.get(id)
|
|
325
|
-
for item in setup_item_list:
|
|
326
|
-
allure_suite_mark = f'{clase_id}#{index}'
|
|
327
|
-
setattr(item, 'allure_suite_mark', allure_suite_mark)
|
|
328
|
-
|
|
329
|
-
item_list += setup_item_list
|
|
330
|
-
|
|
331
|
-
case_item_list = case_dict.get(id)
|
|
332
|
-
for item in case_item_list:
|
|
333
|
-
allure_suite_mark = f'{clase_id}#{index}'
|
|
334
|
-
setattr(item, 'allure_suite_mark', allure_suite_mark)
|
|
335
|
-
item_list += case_item_list
|
|
336
|
-
|
|
337
|
-
if teardown_dict:
|
|
338
|
-
teardown_item_list = teardown_dict.get(id)
|
|
339
|
-
for item in teardown_item_list:
|
|
340
|
-
allure_suite_mark = f'{clase_id}#{index}'
|
|
341
|
-
setattr(item, 'allure_suite_mark', allure_suite_mark)
|
|
342
|
-
item_list += teardown_item_list
|
|
343
|
-
|
|
344
|
-
all_item_list += item_list
|
|
345
|
-
|
|
346
|
-
return all_item_list
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
def pytest_runtest_call(item):
|
|
350
|
-
"""
|
|
351
|
-
模版渲染,运行用例
|
|
352
|
-
:param item:
|
|
353
|
-
:return:
|
|
354
|
-
"""
|
|
355
|
-
# 是否是test—setup 或 test-teardown方法
|
|
356
|
-
is_around_function = item.get_closest_marker("test_setup") or item.get_closest_marker("test_teardown")
|
|
357
|
-
if not is_around_function:
|
|
358
|
-
if item.function.ignore:
|
|
359
|
-
# 如果是普通方法 而且由于之前步骤出错则skiped
|
|
360
|
-
pytest.skip('skiped')
|
|
361
|
-
|
|
362
|
-
id = item.callspec.id
|
|
363
|
-
|
|
364
|
-
# 支持本地调试的标记
|
|
365
|
-
CONTEXT.set(key='run#index', value=id.split('::')[1])
|
|
366
|
-
|
|
367
|
-
# 获取参数化数据 放入上下文中
|
|
368
|
-
full_id = id.split('#')[0]
|
|
369
|
-
test_case_datas = CONTEXT.get(key=f'{full_id}#test_case_datas')
|
|
370
|
-
CONTEXT.set_from_dict(test_case_datas.get(id))
|
|
371
|
-
|
|
372
|
-
# 获取原始测试数据
|
|
373
|
-
origin_data = item.funcargs.get("data")
|
|
374
|
-
# 深拷贝这份数据
|
|
375
|
-
deep_copied_origin_data = copy.deepcopy(origin_data)
|
|
376
|
-
item.funcargs["origin_data"] = Box(origin_data)
|
|
377
|
-
|
|
378
|
-
__init_allure(origin_data)
|
|
379
|
-
logger.info(f"执行用例: {item.nodeid}")
|
|
380
|
-
# 对原始请求数据进行渲染替换
|
|
381
|
-
rendered_data = RenderData(deep_copied_origin_data).render()
|
|
382
|
-
# 测试用例函数添加参数data
|
|
383
|
-
http = item.funcargs.get("http")
|
|
384
|
-
item.funcargs["data"] = Box(rendered_data)
|
|
385
|
-
item.funcargs["belong_app"] = origin_data.get("_belong_app")
|
|
386
|
-
item.funcargs["config"] = CONFIG
|
|
387
|
-
item.funcargs["context"] = CONTEXT
|
|
388
|
-
if item.cls:
|
|
389
|
-
item.cls.http = http
|
|
390
|
-
item.cls.data = Box(rendered_data)
|
|
391
|
-
item.cls.origin_data = Box(origin_data)
|
|
392
|
-
item.cls.belong_app = origin_data.get("_belong_app")
|
|
393
|
-
item.cls.context = CONTEXT
|
|
394
|
-
item.cls.config = config
|
|
395
|
-
|
|
396
|
-
# 获取测试函数体内容
|
|
397
|
-
func_source = re.sub(r'(?<!["\'])#.*', '', inspect.getsource(item.function))
|
|
398
|
-
# 校验测试用例中是否有断言
|
|
399
|
-
if 'test_setup' not in item.keywords and 'test_teardown' not in item.keywords and "assert" not in func_source:
|
|
400
|
-
logger.error(f"测试方法:{item.originalname}缺少断言")
|
|
401
|
-
pytest.exit(ExitCode.MISSING_ASSERTIONS)
|
|
402
|
-
|
|
403
|
-
# # 检查函数体内容是否包含语句self.request,如果没有则自动发送请求
|
|
404
|
-
# if "self.request" not in func_source:
|
|
405
|
-
# # 测试用例函数添加参数response
|
|
406
|
-
# response = client.request(rendered_data)
|
|
407
|
-
# item.funcargs["response"] = response
|
|
408
|
-
# if item.cls:
|
|
409
|
-
# item.cls.response = response
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def pytest_runtest_logreport(report):
|
|
413
|
-
"""收集测试结果"""
|
|
414
|
-
if report.when == "call": # 确保是测试调用阶段(忽略setup和teardown)
|
|
415
|
-
test_results["total"] += 1
|
|
416
|
-
if report.passed:
|
|
417
|
-
test_results["passed"] += 1
|
|
418
|
-
elif report.failed:
|
|
419
|
-
test_results["failed"] += 1
|
|
420
|
-
elif report.skipped:
|
|
421
|
-
test_results["skipped"] += 1
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
|
425
|
-
def pytest_runtest_makereport(item, call):
|
|
426
|
-
# 获取测试报告
|
|
427
|
-
outcome = yield
|
|
428
|
-
report = outcome.get_result()
|
|
429
|
-
|
|
430
|
-
# 如果测试失败并且是执行阶段
|
|
431
|
-
if report.when == "call" and report.failed:
|
|
432
|
-
# 获取断言失败的消息
|
|
433
|
-
logger.error(f"断言失败: {report.longrepr.reprcrash}")
|
|
434
|
-
# 用于控制失败步骤后是否继续的标志
|
|
435
|
-
failStepContinuationFlag = item.callspec.params.get('data').get('failStepContinuationFlag')
|
|
436
|
-
|
|
437
|
-
global should_skip
|
|
438
|
-
if failStepContinuationFlag is None:
|
|
439
|
-
should_skip = True
|
|
440
|
-
else:
|
|
441
|
-
if failStepContinuationFlag == True:
|
|
442
|
-
should_skip = False
|
|
443
|
-
else:
|
|
444
|
-
should_skip = True
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
def pytest_terminal_summary(terminalreporter, exitstatus, config):
|
|
448
|
-
"""在 pytest 结束后修改统计数据或添加自定义报告"""
|
|
449
|
-
stats = terminalreporter.stats
|
|
450
|
-
# 统计各种测试结果
|
|
451
|
-
passed = len(stats.get("passed", []))
|
|
452
|
-
failed = len(stats.get("failed", []))
|
|
453
|
-
skipped = len(stats.get("skipped", []))
|
|
454
|
-
total = passed + failed + skipped
|
|
455
|
-
try:
|
|
456
|
-
pass_rate = round(passed / (total - skipped) * 100, 2)
|
|
457
|
-
except ZeroDivisionError:
|
|
458
|
-
pass_rate = 0
|
|
459
|
-
# 打印自定义统计信息
|
|
460
|
-
terminalreporter.write("\n============ 执行结果统计 ============\n", blue=True, bold=True)
|
|
461
|
-
terminalreporter.write(f"执行用例总数: {passed + failed + skipped}\n", bold=True)
|
|
462
|
-
terminalreporter.write(f"通过用例数: {passed}\n", green=True, bold=True)
|
|
463
|
-
terminalreporter.write(f"失败用例数: {failed}\n", red=True, bold=True)
|
|
464
|
-
terminalreporter.write(f"跳过用例数: {skipped}\n", yellow=True, bold=True)
|
|
465
|
-
terminalreporter.write(f"用例通过率: {pass_rate}%\n", green=True, bold=True)
|
|
466
|
-
terminalreporter.write("====================================\n", blue=True, bold=True)
|
|
467
|
-
# 生成allure测试报告
|
|
468
|
-
generate_report()
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
@pytest.fixture(autouse=True)
|
|
472
|
-
def response():
|
|
473
|
-
response = None
|
|
474
|
-
yield response
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
@pytest.fixture(autouse=True)
|
|
478
|
-
def data():
|
|
479
|
-
data: dict = dict()
|
|
480
|
-
yield data
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
@pytest.fixture(autouse=True)
|
|
484
|
-
def origin_data():
|
|
485
|
-
origin_data: dict = dict()
|
|
486
|
-
yield origin_data
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
@pytest.fixture(autouse=True)
|
|
490
|
-
def belong_app():
|
|
491
|
-
app = None
|
|
492
|
-
yield app
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
@pytest.fixture(autouse=True)
|
|
496
|
-
def config():
|
|
497
|
-
config = None
|
|
498
|
-
yield config
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
@pytest.fixture(autouse=True)
|
|
502
|
-
def context():
|
|
503
|
-
context = None
|
|
504
|
-
yield context
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
class Http(object):
|
|
508
|
-
pass
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
@retry.retry(tries=3, delay=1)
|
|
512
|
-
@pytest.fixture(scope="session", autouse=True)
|
|
513
|
-
def http():
|
|
514
|
-
module = importlib.import_module("conftest")
|
|
515
|
-
try:
|
|
516
|
-
for app in all_app:
|
|
517
|
-
setattr(Http, app, getattr(module, f"{snake_to_pascal(app)}Login")(app))
|
|
518
|
-
return Http
|
|
519
|
-
except Exception as e:
|
|
520
|
-
logger.error(f"登录{app}异常:{e}")
|
|
521
|
-
traceback.print_exc()
|
|
522
|
-
pytest.exit(ExitCode.LOGIN_ERROR)
|
|
523
|
-
return None
|
|
File without changes
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/allure_report.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/db/__init__.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/db/redis_db.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/exit_code.py
RENAMED
|
File without changes
|
|
File without changes
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/__init__.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/common.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/encrypt.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/log_util.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/teams_util.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.0 → pytest_api_framework_alpha-0.1.2}/framework/utils/yaml_util.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|