pytest-api-framework-alpha 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,185 @@
1
+ import os
2
+ import re
3
+ import importlib
4
+ import traceback
5
+
6
+ import allure
7
+ import pytest
8
+ from faker import Faker
9
+ from framework.utils.log_util import logger
10
+ from framework.exit_code import ExitCode
11
+ from framework.global_attribute import CONTEXT
12
+ from config.settings import FAKER_LANGUAGE, DATA_DIR
13
+
14
+
15
+ class SingletonFaker(object):
16
+ instance = None
17
+ init_flag = False
18
+
19
+ def __init__(self, locale):
20
+ if self.init_flag:
21
+ return
22
+ self.faker = Faker(locale)
23
+
24
+ def __new__(cls, *args, **kwargs):
25
+ if cls.instance is None:
26
+ cls.instance = super().__new__(cls)
27
+ return cls.instance
28
+
29
+
30
+ class RenderData(object):
31
+ def __init__(self, data):
32
+ self.data = data
33
+ self.request = data.get("request")
34
+ self.context = CONTEXT
35
+ self.faker = SingletonFaker(locale=FAKER_LANGUAGE).faker
36
+ self._is_multipart = False
37
+
38
+ def render(self):
39
+ """
40
+ 占位符赋值
41
+ :return:
42
+ """
43
+ with allure.step("渲染数据"):
44
+ self.replace_attribute(self.request)
45
+ self.data["request"] = self.request
46
+ self.data["_is_multipart"] = self._is_multipart
47
+ return self.data
48
+
49
+ def replace_attribute(self, data):
50
+ pattern = re.compile(r"\$\{([\w.\[\]0-9]+(?:\(\w*(?:,\w*)*\))?)}")
51
+ file_path_pattern = re.compile(
52
+ r'^(?:[^/\\]+\\)*[^/\\]+\.(txt|doc|docx|pdf|xls|xlsx|ppt|pptx|md|jpg|jpeg|png|gif|svg|webp|ico|mp4|avi|mov|wmv|flv|mkv|webm|mp3)$')
53
+
54
+ # 如果数据是字典类型,则遍历其键值对
55
+ if isinstance(data, dict):
56
+ for key, value in data.items():
57
+ # 递归遍历嵌套的字典或列表
58
+ if isinstance(value, (dict, list)):
59
+ self.replace_attribute(value)
60
+ # 如果是字符串类型并匹配正则表达式,则替换
61
+ elif isinstance(value, str):
62
+ if pattern.search(value):
63
+ data[key] = self.get_attribute(value[2:-1])
64
+ elif file_path_pattern.search(value):
65
+ data[key] = self.open_file_for_multipart(value)
66
+ self._is_multipart = True
67
+
68
+ # 如果数据是列表类型,则遍历其元素
69
+ elif isinstance(data, list):
70
+ for index, item in enumerate(data):
71
+ # 递归遍历嵌套的字典或列表
72
+ if isinstance(item, (dict, list)):
73
+ self.replace_attribute(item)
74
+ # 如果是字符串类型并匹配正则表达式,则替换
75
+ elif isinstance(item, str):
76
+ if pattern.search(item):
77
+ data[index] = self.get_attribute(item[2:-1])
78
+ elif file_path_pattern.search(item):
79
+ data[index] = self.open_file_for_multipart(item)
80
+ self._is_multipart = True
81
+
82
+ def get_attribute(self, keyword):
83
+ if not keyword.endswith(")"):
84
+ return self.get_attribute_variable(keyword)
85
+ # 如果关键字是函数
86
+ else:
87
+ pattern = re.compile(r'(?P<func_name>.+)\((?P<args>.*)\)')
88
+ match = re.match(pattern, keyword)
89
+ # 匹配方法名
90
+ func_name = match.group("func_name")
91
+ # 匹配方法位置参数
92
+ args = match.group("args")
93
+ if args:
94
+ # 将参数中字符串类型的数字转成数字类型
95
+ args = [eval(i) for i in args.replace(",", "").split(",")]
96
+ return self.get_func_variable(keyword, func_name, *args)
97
+ else:
98
+ return self.get_func_variable(keyword, func_name)
99
+
100
+ def get_attribute_variable(self, expression):
101
+ """
102
+ 去全局上下文中获取value并进行替换
103
+ :param expression:
104
+ :return:
105
+ """
106
+ if not expression.startswith(tuple(f"{app}." for app in self.context.all_app)):
107
+ belong_app = self.data.get('_belong_app')
108
+ new_expression = f"{belong_app}.{expression}"
109
+ else:
110
+ new_expression = expression
111
+ value = self.get_nested_value(self.context, expression) or self.get_nested_value(self.context, new_expression)
112
+
113
+ if not value:
114
+ logger.warning(f"未从CONTEXT中获取到对应变量{expression}")
115
+ # pytest.exit(ExitCode.GLOBAL_ATTRIBUTE_NOT_EXIST)
116
+
117
+ return None
118
+ with allure.step(f"{expression}: {value}"):
119
+ logger.info(f"前置读取变量: {expression}: {value}")
120
+ return value
121
+
122
+ def get_func_variable(self, keyword, func_name, *args):
123
+ """
124
+ 去utils>common.py中或faker对象中执行对应的方法并进行替换
125
+ :param keyword:
126
+ :param func_name:
127
+ :param args:
128
+ :return:
129
+ """
130
+ module = importlib.import_module("common")
131
+ try:
132
+ value = getattr(module, func_name)(*args)
133
+ except AttributeError:
134
+ value = getattr(self.faker, func_name, None)(*args)
135
+
136
+ if not value:
137
+ logger.error(f"common.py文件或faker对象中不存在函数:{keyword}")
138
+ traceback.print_exc()
139
+ pytest.exit(ExitCode.FUNCTION_NOT_EXIST)
140
+ else:
141
+ with allure.step(f"{keyword}: {value}"):
142
+ logger.info(f"前置读取函数: {keyword}: {value}")
143
+ return value
144
+
145
+ def get_file_obj(self, data, pattern):
146
+ # 如果 data 是字典,则遍历每一个键值对
147
+ if isinstance(data, dict):
148
+ for key, value in data.items():
149
+ # 递归调用处理值
150
+ data[key] = self.get_file_obj(value, pattern)
151
+
152
+ # 如果 data 是列表,则遍历每一个元素
153
+ elif isinstance(data, list):
154
+ for index in range(len(data)):
155
+ # 递归调用处理列表中的每一个元素
156
+ data[index] = self.get_file_obj(data[index], pattern)
157
+
158
+ # 如果 data 是字符串,则检查是否匹配正则表达式
159
+ elif isinstance(data, str):
160
+ if re.fullmatch(pattern, data):
161
+ # 如果匹配,则替换为文件流
162
+ self._is_multipart = True
163
+ return self.open_file_for_multipart(data)
164
+ # 返回处理后的数据
165
+ return data
166
+
167
+ @staticmethod
168
+ def get_nested_value(obj, attr_path):
169
+ """通过字符串路径(如 'a.b[0].c')获取嵌套属性值"""
170
+ # 使用正则表达式分解路径,支持属性和索引的组合
171
+ path_elements = re.findall(r'(\w+)|\[(\d+)]', attr_path)
172
+ try:
173
+ for attr, index in path_elements:
174
+ if attr: # 属性部分
175
+ obj = getattr(obj, attr)
176
+ if index: # 索引部分
177
+ obj = obj[int(index)]
178
+ return obj
179
+ except Exception:
180
+ return None
181
+
182
+ @staticmethod
183
+ def open_file_for_multipart(filepath):
184
+ f = open(os.path.join(DATA_DIR, "file", filepath), "rb")
185
+ return os.path.basename(f.name), f, "application/octet-stream"
framework/report.py ADDED
@@ -0,0 +1,102 @@
1
+ import json
2
+ import argparse
3
+ from pathlib import Path
4
+ import datetime
5
+ import traceback
6
+ from framework.db.mysql_db import MysqlDB
7
+ from config.settings import DATABASE_HOST, DATABASE_PASSWORD, DATABASE_DB, DATABASE_USERNAME, DATABASE_PORT
8
+
9
+
10
+ def timestamp_to_datetime_str(timestamp):
11
+ # 将时间戳转换为 datetime 对象
12
+ dt = datetime.datetime.fromtimestamp(timestamp)
13
+ # 将 datetime 对象格式化为指定格式的字符串
14
+ return dt.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
15
+
16
+
17
+ if __name__ == '__main__':
18
+ print('调用report脚本')
19
+ # 创建 ArgumentParser 对象
20
+ parser = argparse.ArgumentParser(description="演示如何使用 argparse 获取命令行参数")
21
+ # 添加位置参数
22
+ parser.add_argument('--task', help="输入任务的名字", required=True)
23
+ # 添加可选参数
24
+ parser.add_argument('--buildnum', type=int, help="输入构建次数", required=True)
25
+ parser.add_argument('--allure_result', type=str, help="allure_result路径", required=True)
26
+
27
+ # 解析命令行参数
28
+ args = parser.parse_args()
29
+ if not args.task:
30
+ exit(400)
31
+ print(f"task, {args.task}!")
32
+ taskName = args.task
33
+
34
+ if not args.buildnum:
35
+ exit(400)
36
+ print(f"buildnum, {args.buildnum}!")
37
+ buildNum = args.buildnum
38
+
39
+ if not args.allure_result:
40
+ exit(400)
41
+ print(f"allure_result_path, {args.allure_result}!")
42
+ allure_result_path = args.allure_result
43
+
44
+ # 初始化数据库连接
45
+ print('初始化数据库连接')
46
+ mysqlDB = MysqlDB(
47
+ host=DATABASE_HOST,
48
+ username=DATABASE_USERNAME,
49
+ password=DATABASE_PASSWORD,
50
+ port=DATABASE_PORT,
51
+ db=DATABASE_DB
52
+ )
53
+
54
+ current_time = datetime.datetime.now()
55
+ formatted_time = current_time.strftime('%Y-%m-%d %H:%M:%S')
56
+ task_id = mysqlDB.insert(
57
+ f"INSERT INTO tbl_task (name,last_run_time,buildnum) VALUES ('{taskName}','{formatted_time}','{buildNum}')")
58
+ print(f'{task_id}')
59
+
60
+ # 获取所有‘result.json‘结尾的文件
61
+ print('获取所有‘*result.json‘结尾的文件')
62
+ folder = Path(allure_result_path)
63
+ all = list(folder.rglob('*result.json'))
64
+ print(f'files count: {len(all)}')
65
+
66
+ for file_path in all:
67
+ try:
68
+ # 打开 JSON 文件
69
+ with open(file_path, 'r', encoding='utf-8') as file:
70
+ # 解析 JSON 文件内容
71
+ data = json.load(file)
72
+ # 示例:访问解析后的数据
73
+ if 'fullName' in data:
74
+ print(f"fullName: {data['fullName']}")
75
+ if 'testCaseId' in data:
76
+ print(f"testCaseId: {data['testCaseId']}")
77
+ if 'start' in data:
78
+ print(f"start: {data['start']}")
79
+ if 'stop' in data:
80
+ print(f"stop: {data['stop']}")
81
+ if 'uuid' in data:
82
+ print(f"uuid: {data['uuid']}")
83
+ if 'status' in data:
84
+ print(f"status: {data['status']}")
85
+
86
+ if 'name' in data:
87
+ print(f"name: {data['name']}")
88
+
89
+ if 'statusDetails' in data and data['statusDetails'] is not None:
90
+ mysqlDB.execute(
91
+ f"INSERT INTO tbl_test_case (test_case_id,status,name,starttime,stoptime,uuid,task_id,message) VALUES ('{data['testCaseId']}','{data['status']}','{data['name']}',{data['start']},{data['stop']},'{data['uuid']}',{task_id},'{str(data['statusDetails'])}')")
92
+ else:
93
+ mysqlDB.execute(
94
+ f"INSERT INTO tbl_test_case (test_case_id,status,name,starttime,stoptime,uuid,task_id) VALUES ('{data['testCaseId']}','{data['status']}','{data['name']}',{data['start']},{data['stop']},'{data['uuid']}',{task_id})")
95
+
96
+ except FileNotFoundError:
97
+ print("错误: 文件未找到,请检查文件路径是否正确。")
98
+ except json.JSONDecodeError:
99
+ print("错误: JSON 解析出错,请检查文件内容格式是否正确。")
100
+ except Exception as e:
101
+ print(f"发生未知错误: {e}")
102
+ traceback.print_exc()
framework/startapp.py ADDED
@@ -0,0 +1,126 @@
1
+ import os
2
+ import argparse
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+ from framework.utils.common import snake_to_pascal
7
+ from config.settings import CONFIG_DIR, CASES_DIR, DATA_DIR
8
+
9
+
10
+ def create_yaml(app):
11
+ """生成 YAML 文件"""
12
+ os.makedirs(os.path.join(CONFIG_DIR, app), exist_ok=True)
13
+ with open(os.path.join(CONFIG_DIR, app, "context.yaml"), 'w', encoding="utf-8") as f:
14
+ yaml.dump({
15
+ "dev": {
16
+ "domain": "dev.example.com",
17
+ "accounts": {
18
+ "admin": {
19
+ "username": "admin",
20
+ "password": "test"
21
+ }
22
+ }
23
+ }
24
+ }, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
25
+ with open(os.path.join(CONFIG_DIR, app, "config.yaml"), 'w', encoding="utf-8") as f:
26
+ yaml.dump({
27
+ "dev": {
28
+ "mysql": {
29
+ "host": None,
30
+ "username": None,
31
+ "password": None,
32
+ "port": 3306
33
+ },
34
+ "redis": {
35
+ "host": None,
36
+ "password": None,
37
+ "port": 6379
38
+ }
39
+ }
40
+ }, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
41
+
42
+
43
+ def create_test_case(app: str):
44
+ os.makedirs(os.path.join(CASES_DIR, app), exist_ok=True)
45
+ file_path = Path(os.path.join(CASES_DIR, app, "../__init__.py"))
46
+ file_path.touch(exist_ok=True)
47
+ content = f"""
48
+ from test_case import BaseTestCase
49
+
50
+
51
+ class TestUntitled(BaseTestCase):
52
+
53
+ def test_untitled(self):
54
+ # 发送请求
55
+ self.request(app="{app}", account="user", data=self.data)
56
+ # 断言
57
+ assert self.response.status_code == 200
58
+ assert len(self.response.jsonpath("$.data.balances")) > 0"""
59
+ with open(os.path.join(CASES_DIR, app, "test_untitled.py"), 'w', encoding="utf-8") as file:
60
+ file.write(content)
61
+
62
+
63
+ def create_test_data(app):
64
+ os.makedirs(os.path.join(DATA_DIR, app), exist_ok=True)
65
+
66
+ with open(os.path.join(DATA_DIR, app, "test_untitled.yaml"), 'w', encoding="utf-8") as f:
67
+ yaml.dump({
68
+ "case_common": {
69
+ "module": "功能模块名称",
70
+ "describe": "测试场景描述"
71
+ },
72
+ "test_untitled": {
73
+ "title": "登录成功",
74
+ "level": "p0",
75
+ "request": {
76
+ "url": "/login",
77
+ "method": "post",
78
+ "json": {
79
+ "name": "Jerry"
80
+ }
81
+ },
82
+ "extract": [
83
+ {
84
+ "id": "$.data.token"
85
+ }
86
+ ]
87
+ }
88
+ }, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
89
+
90
+
91
+ def create_app_session(app):
92
+ content = f"""
93
+
94
+ class {snake_to_pascal(app)}Login(Login):
95
+
96
+ def login(self, username, password, secret_key):
97
+ client = HttpClient()
98
+ # TODO 需要实现登录逻辑,将登录获取到的token添加到headers中
99
+ token = None
100
+ client.update_headers({{"token": token}})
101
+ return client"""
102
+ with open(os.path.join(CASES_DIR, "conftest.py"), "a", encoding="utf-8") as f:
103
+ f.write(content)
104
+
105
+
106
+ def process_command_line_args():
107
+ parser = argparse.ArgumentParser(description='startapp')
108
+ parser.add_argument('app', type=str, help='创建应用')
109
+ return parser.parse_args()
110
+
111
+
112
+ def main():
113
+ args = process_command_line_args()
114
+ app = args.app
115
+ if not os.path.exists(os.path.join(DATA_DIR, app)):
116
+ create_yaml(app)
117
+ create_test_case(app)
118
+ create_test_data(app)
119
+ create_app_session(app)
120
+ print(f"app {app}创建成功")
121
+ else:
122
+ print(f"app {app}已存在,未执行创建")
123
+
124
+
125
+ if __name__ == '__main__':
126
+ main()
File without changes
@@ -0,0 +1,211 @@
1
+ import re
2
+ import os
3
+ import time
4
+ import binascii
5
+ from datetime import datetime
6
+ from urllib.parse import unquote, quote, quote_plus, unquote_plus
7
+ import pyotp
8
+ import cn2an as c2a
9
+
10
+ from config.settings import CONFIG_DIR
11
+
12
+
13
+ def generate_2fa_code(secret_key):
14
+ """
15
+ 获取2fa code
16
+ :return:
17
+ """
18
+ current_time = int(time.time())
19
+ totp = pyotp.TOTP(secret_key)
20
+ google_code = totp.at(current_time)
21
+
22
+ return google_code
23
+
24
+
25
+ def is_digit(string):
26
+ """判断是否为数字字符串"""
27
+ digit_re = re.compile(r'^-?[0-9.]+$')
28
+ return digit_re.search(string)
29
+
30
+
31
+ def an2cn(integer):
32
+ """阿拉伯数字转中文数字"""
33
+ return c2a.an2cn(integer)
34
+
35
+
36
+ def cn2an(string):
37
+ """中文数字转阿拉伯数字"""
38
+ return c2a.cn2an(string)
39
+
40
+
41
+ def clean_symbol(text):
42
+ """
43
+ 清除字符串特殊符号
44
+ :param text:
45
+ :return:
46
+ """
47
+ return re.sub('[’!"#$`%&\':|*+~·,-./:;<=「」>@,。?★、…—?“”‘![\\]^_{}~]+', "", text)
48
+
49
+
50
+ def get_long_timestamp(int_type=False):
51
+ """
52
+ 获取毫秒级时间戳
53
+ :param int_type: 默认返回str类型
54
+ :return:
55
+ """
56
+
57
+ if int_type:
58
+ return int(time.time() * 1000)
59
+ return str(int(time.time() * 1000))
60
+
61
+
62
+ def get_short_timestamp(int_type=False):
63
+ """
64
+ 获取秒级时间戳
65
+ :param int_type: 默认返回str类型
66
+ :return:
67
+ """
68
+ if int_type:
69
+ return int(time.time())
70
+ return str(int(time.time()))
71
+
72
+
73
+ def get_current_datetime():
74
+ """
75
+ 获取当前日期和时间 2023-02-19 08:31:51
76
+ :return:
77
+ """
78
+ return time.strftime("%Y-%m-%d %X")
79
+
80
+
81
+ def get_current_date():
82
+ """
83
+ 获取当前日期 2023-02-19
84
+ :return:
85
+ """
86
+ return time.strftime("%Y-%m-%d")
87
+
88
+
89
+ def timestamp2datetime(timestamp):
90
+ """
91
+ 时间戳转日期时间
92
+ @param timestamp:
93
+ @return:
94
+ """
95
+ if isinstance(timestamp, int):
96
+ timestamp = str(timestamp)
97
+ timestamp = timestamp[:10]
98
+ return datetime.fromtimestamp(int(timestamp))
99
+
100
+
101
+ def timestamp2date(timestamp):
102
+ """
103
+ 时间戳转日期
104
+ @param timestamp:
105
+ @return:
106
+ """
107
+ if isinstance(timestamp, int):
108
+ timestamp = str(timestamp)
109
+ timestamp = timestamp[:10]
110
+ return time.strftime("%Y-%m-%d", time.localtime(int(timestamp)))
111
+
112
+
113
+ def valid_hex_format(s):
114
+ """
115
+ 判断是否为16进制字符串
116
+ :param s:
117
+ :return:
118
+ """
119
+ hex_re = re.compile(r"^[0-9a-fA-F]+$")
120
+ if hex_re.match(s):
121
+ return True
122
+ return False
123
+
124
+
125
+ def valid_b64_format(s):
126
+ """
127
+ 判断是否为base64字符串
128
+ :param s:
129
+ :return:
130
+ """
131
+ b64_re = re.compile(r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$")
132
+ if b64_re.match(s):
133
+ return True
134
+ return False
135
+
136
+
137
+ def hex_to_bytes(hex_str):
138
+ """
139
+ 16进制->字节
140
+ :param hex_str:
141
+ :return:
142
+ """
143
+ return binascii.a2b_hex(hex_str).strip()
144
+
145
+
146
+ def bytes_to_hex(byte):
147
+ """
148
+ 字节->16进制
149
+ :param byte:
150
+ :return:
151
+ """
152
+ return binascii.b2a_hex(byte)
153
+
154
+
155
+ def url_encode(string, use_quote_plus=False, encoding="utf-8"):
156
+ """
157
+ url编码
158
+ :param string:
159
+ :param use_quote_plus: 是否使用quote_plus编码
160
+ :param encoding: 编码格式
161
+ :return:
162
+ """
163
+ if use_quote_plus:
164
+ return quote_plus(string, encoding=encoding)
165
+ return quote(string, encoding=encoding)
166
+
167
+
168
+ def url_decode(string, use_unquote_plus=False, encoding="utf-8"):
169
+ """
170
+ url解码
171
+ :param string:
172
+ :param use_unquote_plus: 是否使用unquote_plus解码
173
+ :param encoding: 编码格式
174
+ :return:
175
+ """
176
+ if use_unquote_plus:
177
+ return unquote_plus(string, encoding=encoding)
178
+ return unquote(string, encoding=encoding)
179
+
180
+
181
+ def singleton(cls):
182
+ """
183
+ 单例模式装饰器
184
+ :param cls:
185
+ :return:
186
+ """
187
+ instances = {}
188
+
189
+ def get_instance(*args, **kwargs):
190
+ if cls not in instances:
191
+ instances[cls] = cls(*args, **kwargs)
192
+ return instances[cls]
193
+
194
+ return get_instance
195
+
196
+
197
+ def snake_to_pascal(name: str) -> str:
198
+ # 将字符串按照下划线分割,然后将每个单词的首字母大写,最后连接起来
199
+ return ''.join(word.capitalize() for word in name.split('_'))
200
+
201
+
202
+ def get_apps():
203
+ """
204
+ 获取所有app
205
+ """
206
+ return [name for name in os.listdir(CONFIG_DIR) if
207
+ os.path.isdir(os.path.join(CONFIG_DIR, name)) and not name.startswith(("__", "."))]
208
+
209
+
210
+ if __name__ == '__main__':
211
+ print(get_apps())