kdtest-pw 2.0.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.
Files changed (57) hide show
  1. kdtest_pw/__init__.py +50 -0
  2. kdtest_pw/action/__init__.py +7 -0
  3. kdtest_pw/action/base_keyword.py +292 -0
  4. kdtest_pw/action/element_plus/__init__.py +23 -0
  5. kdtest_pw/action/element_plus/el_cascader.py +263 -0
  6. kdtest_pw/action/element_plus/el_datepicker.py +324 -0
  7. kdtest_pw/action/element_plus/el_dialog.py +317 -0
  8. kdtest_pw/action/element_plus/el_form.py +443 -0
  9. kdtest_pw/action/element_plus/el_menu.py +456 -0
  10. kdtest_pw/action/element_plus/el_select.py +268 -0
  11. kdtest_pw/action/element_plus/el_table.py +442 -0
  12. kdtest_pw/action/element_plus/el_tree.py +364 -0
  13. kdtest_pw/action/element_plus/el_upload.py +313 -0
  14. kdtest_pw/action/key_retrieval.py +311 -0
  15. kdtest_pw/action/page_action.py +1129 -0
  16. kdtest_pw/api/__init__.py +6 -0
  17. kdtest_pw/api/api_keyword.py +251 -0
  18. kdtest_pw/api/request_handler.py +232 -0
  19. kdtest_pw/cases/__init__.py +6 -0
  20. kdtest_pw/cases/case_collector.py +182 -0
  21. kdtest_pw/cases/case_executor.py +359 -0
  22. kdtest_pw/cases/read/__init__.py +6 -0
  23. kdtest_pw/cases/read/cell_handler.py +305 -0
  24. kdtest_pw/cases/read/excel_reader.py +223 -0
  25. kdtest_pw/cli/__init__.py +5 -0
  26. kdtest_pw/cli/run.py +318 -0
  27. kdtest_pw/common.py +106 -0
  28. kdtest_pw/core/__init__.py +7 -0
  29. kdtest_pw/core/browser_manager.py +196 -0
  30. kdtest_pw/core/config_loader.py +235 -0
  31. kdtest_pw/core/page_context.py +228 -0
  32. kdtest_pw/data/__init__.py +5 -0
  33. kdtest_pw/data/init_data.py +105 -0
  34. kdtest_pw/data/static/elementData.yaml +59 -0
  35. kdtest_pw/data/static/parameters.json +24 -0
  36. kdtest_pw/plugins/__init__.py +6 -0
  37. kdtest_pw/plugins/element_plus_plugin/__init__.py +5 -0
  38. kdtest_pw/plugins/element_plus_plugin/elementData/elementData.yaml +144 -0
  39. kdtest_pw/plugins/element_plus_plugin/element_plus_plugin.py +237 -0
  40. kdtest_pw/plugins/element_plus_plugin/my.ini +23 -0
  41. kdtest_pw/plugins/plugin_base.py +180 -0
  42. kdtest_pw/plugins/plugin_loader.py +260 -0
  43. kdtest_pw/product.py +5 -0
  44. kdtest_pw/reference.py +99 -0
  45. kdtest_pw/utils/__init__.py +13 -0
  46. kdtest_pw/utils/built_in_function.py +376 -0
  47. kdtest_pw/utils/decorator.py +211 -0
  48. kdtest_pw/utils/log/__init__.py +6 -0
  49. kdtest_pw/utils/log/html_report.py +336 -0
  50. kdtest_pw/utils/log/logger.py +123 -0
  51. kdtest_pw/utils/public_script.py +366 -0
  52. kdtest_pw-2.0.0.dist-info/METADATA +169 -0
  53. kdtest_pw-2.0.0.dist-info/RECORD +57 -0
  54. kdtest_pw-2.0.0.dist-info/WHEEL +5 -0
  55. kdtest_pw-2.0.0.dist-info/entry_points.txt +2 -0
  56. kdtest_pw-2.0.0.dist-info/licenses/LICENSE +21 -0
  57. kdtest_pw-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,376 @@
1
+ """内置函数工具类"""
2
+
3
+ import random
4
+ import string
5
+ import uuid
6
+ import time
7
+ import re
8
+ from datetime import datetime, timedelta
9
+ from typing import Any, Optional, List
10
+
11
+
12
+ class BuiltInFunction:
13
+ """内置函数工具类
14
+
15
+ 提供测试中常用的工具函数。
16
+ """
17
+
18
+ # ==================== 日期时间函数 ====================
19
+
20
+ @staticmethod
21
+ def today(fmt: str = '%Y-%m-%d', offset: int = 0) -> str:
22
+ """获取今天日期
23
+
24
+ Args:
25
+ fmt: 日期格式
26
+ offset: 天数偏移
27
+
28
+ Returns:
29
+ str: 格式化的日期
30
+ """
31
+ date = datetime.now() + timedelta(days=offset)
32
+ return date.strftime(fmt)
33
+
34
+ @staticmethod
35
+ def now(fmt: str = '%Y-%m-%d %H:%M:%S') -> str:
36
+ """获取当前时间
37
+
38
+ Args:
39
+ fmt: 时间格式
40
+
41
+ Returns:
42
+ str: 格式化的时间
43
+ """
44
+ return datetime.now().strftime(fmt)
45
+
46
+ @staticmethod
47
+ def timestamp(unit: str = 'ms') -> int:
48
+ """获取时间戳
49
+
50
+ Args:
51
+ unit: 单位 (s, ms)
52
+
53
+ Returns:
54
+ int: 时间戳
55
+ """
56
+ ts = time.time()
57
+ if unit == 'ms':
58
+ return int(ts * 1000)
59
+ return int(ts)
60
+
61
+ @staticmethod
62
+ def date_add(
63
+ date_str: str,
64
+ days: int = 0,
65
+ hours: int = 0,
66
+ minutes: int = 0,
67
+ fmt: str = '%Y-%m-%d'
68
+ ) -> str:
69
+ """日期加减
70
+
71
+ Args:
72
+ date_str: 日期字符串
73
+ days: 天数
74
+ hours: 小时
75
+ minutes: 分钟
76
+ fmt: 日期格式
77
+
78
+ Returns:
79
+ str: 计算后的日期
80
+ """
81
+ date = datetime.strptime(date_str, fmt)
82
+ date += timedelta(days=days, hours=hours, minutes=minutes)
83
+ return date.strftime(fmt)
84
+
85
+ @staticmethod
86
+ def date_diff(date1: str, date2: str, fmt: str = '%Y-%m-%d') -> int:
87
+ """计算日期差
88
+
89
+ Args:
90
+ date1: 日期1
91
+ date2: 日期2
92
+ fmt: 日期格式
93
+
94
+ Returns:
95
+ int: 相差天数
96
+ """
97
+ d1 = datetime.strptime(date1, fmt)
98
+ d2 = datetime.strptime(date2, fmt)
99
+ return (d1 - d2).days
100
+
101
+ # ==================== 随机数函数 ====================
102
+
103
+ @staticmethod
104
+ def random_int(min_val: int = 0, max_val: int = 100) -> int:
105
+ """生成随机整数
106
+
107
+ Args:
108
+ min_val: 最小值
109
+ max_val: 最大值
110
+
111
+ Returns:
112
+ int: 随机整数
113
+ """
114
+ return random.randint(min_val, max_val)
115
+
116
+ @staticmethod
117
+ def random_float(min_val: float = 0, max_val: float = 1.0, precision: int = 2) -> float:
118
+ """生成随机浮点数
119
+
120
+ Args:
121
+ min_val: 最小值
122
+ max_val: 最大值
123
+ precision: 小数位数
124
+
125
+ Returns:
126
+ float: 随机浮点数
127
+ """
128
+ return round(random.uniform(min_val, max_val), precision)
129
+
130
+ @staticmethod
131
+ def random_string(length: int = 8, chars: str = 'alphanumeric') -> str:
132
+ """生成随机字符串
133
+
134
+ Args:
135
+ length: 长度
136
+ chars: 字符类型
137
+
138
+ Returns:
139
+ str: 随机字符串
140
+ """
141
+ char_map = {
142
+ 'alpha': string.ascii_letters,
143
+ 'numeric': string.digits,
144
+ 'alphanumeric': string.ascii_letters + string.digits,
145
+ 'lower': string.ascii_lowercase,
146
+ 'upper': string.ascii_uppercase,
147
+ 'hex': string.hexdigits.lower(),
148
+ }
149
+ charset = char_map.get(chars, string.ascii_letters + string.digits)
150
+ return ''.join(random.choices(charset, k=length))
151
+
152
+ @staticmethod
153
+ def random_phone(prefix: str = '1') -> str:
154
+ """生成随机手机号
155
+
156
+ Args:
157
+ prefix: 号码前缀
158
+
159
+ Returns:
160
+ str: 随机手机号
161
+ """
162
+ prefixes = ['13', '14', '15', '16', '17', '18', '19']
163
+ phone_prefix = random.choice(prefixes) if prefix == '1' else prefix
164
+ return phone_prefix + ''.join([str(random.randint(0, 9)) for _ in range(9)])
165
+
166
+ @staticmethod
167
+ def random_email(domain: str = 'test.com') -> str:
168
+ """生成随机邮箱
169
+
170
+ Args:
171
+ domain: 邮箱域名
172
+
173
+ Returns:
174
+ str: 随机邮箱
175
+ """
176
+ username = BuiltInFunction.random_string(8, 'lower')
177
+ return f"{username}@{domain}"
178
+
179
+ @staticmethod
180
+ def random_name(gender: str = 'random') -> str:
181
+ """生成随机中文姓名
182
+
183
+ Args:
184
+ gender: 性别 (male, female, random)
185
+
186
+ Returns:
187
+ str: 随机姓名
188
+ """
189
+ surnames = ['张', '王', '李', '赵', '陈', '刘', '杨', '黄', '周', '吴']
190
+ male_names = ['伟', '强', '磊', '军', '洋', '勇', '杰', '涛', '明', '超']
191
+ female_names = ['芳', '娟', '敏', '静', '丽', '娜', '秀', '玲', '艳', '红']
192
+
193
+ surname = random.choice(surnames)
194
+
195
+ if gender == 'male':
196
+ name = random.choice(male_names)
197
+ elif gender == 'female':
198
+ name = random.choice(female_names)
199
+ else:
200
+ name = random.choice(male_names + female_names)
201
+
202
+ # 随机添加双字名
203
+ if random.random() > 0.5:
204
+ name += random.choice(male_names + female_names)
205
+
206
+ return surname + name
207
+
208
+ @staticmethod
209
+ def random_id_card() -> str:
210
+ """生成随机身份证号
211
+
212
+ Returns:
213
+ str: 随机身份证号 (模拟,非真实)
214
+ """
215
+ # 地区码
216
+ area_code = random.choice(['110101', '310101', '440101', '330101', '320101'])
217
+
218
+ # 出生日期
219
+ year = random.randint(1970, 2000)
220
+ month = random.randint(1, 12)
221
+ day = random.randint(1, 28)
222
+ birth = f'{year}{month:02d}{day:02d}'
223
+
224
+ # 顺序码
225
+ seq = f'{random.randint(0, 999):03d}'
226
+
227
+ # 校验码 (简化)
228
+ check = random.choice('0123456789X')
229
+
230
+ return area_code + birth + seq + check
231
+
232
+ @staticmethod
233
+ def uuid4() -> str:
234
+ """生成 UUID
235
+
236
+ Returns:
237
+ str: UUID 字符串
238
+ """
239
+ return str(uuid.uuid4())
240
+
241
+ @staticmethod
242
+ def uuid_short() -> str:
243
+ """生成短 UUID (去掉横线)
244
+
245
+ Returns:
246
+ str: 短 UUID
247
+ """
248
+ return uuid.uuid4().hex
249
+
250
+ # ==================== 字符串函数 ====================
251
+
252
+ @staticmethod
253
+ def substring(text: str, start: int, end: int = None) -> str:
254
+ """截取子字符串
255
+
256
+ Args:
257
+ text: 原字符串
258
+ start: 起始位置
259
+ end: 结束位置
260
+
261
+ Returns:
262
+ str: 子字符串
263
+ """
264
+ return text[start:end]
265
+
266
+ @staticmethod
267
+ def replace(text: str, old: str, new: str) -> str:
268
+ """替换字符串
269
+
270
+ Args:
271
+ text: 原字符串
272
+ old: 被替换的内容
273
+ new: 新内容
274
+
275
+ Returns:
276
+ str: 替换后的字符串
277
+ """
278
+ return text.replace(old, new)
279
+
280
+ @staticmethod
281
+ def regex_match(text: str, pattern: str) -> Optional[str]:
282
+ """正则匹配
283
+
284
+ Args:
285
+ text: 原字符串
286
+ pattern: 正则表达式
287
+
288
+ Returns:
289
+ str: 匹配的内容
290
+ """
291
+ match = re.search(pattern, text)
292
+ return match.group(0) if match else None
293
+
294
+ @staticmethod
295
+ def regex_find_all(text: str, pattern: str) -> List[str]:
296
+ """正则查找所有
297
+
298
+ Args:
299
+ text: 原字符串
300
+ pattern: 正则表达式
301
+
302
+ Returns:
303
+ List[str]: 所有匹配
304
+ """
305
+ return re.findall(pattern, text)
306
+
307
+ @staticmethod
308
+ def format_string(template: str, **kwargs) -> str:
309
+ """格式化字符串
310
+
311
+ Args:
312
+ template: 模板字符串
313
+ **kwargs: 替换参数
314
+
315
+ Returns:
316
+ str: 格式化后的字符串
317
+ """
318
+ return template.format(**kwargs)
319
+
320
+ # ==================== 数学函数 ====================
321
+
322
+ @staticmethod
323
+ def abs_value(num: float) -> float:
324
+ """绝对值"""
325
+ return abs(num)
326
+
327
+ @staticmethod
328
+ def round_value(num: float, precision: int = 0) -> float:
329
+ """四舍五入"""
330
+ return round(num, precision)
331
+
332
+ @staticmethod
333
+ def floor(num: float) -> int:
334
+ """向下取整"""
335
+ import math
336
+ return math.floor(num)
337
+
338
+ @staticmethod
339
+ def ceil(num: float) -> int:
340
+ """向上取整"""
341
+ import math
342
+ return math.ceil(num)
343
+
344
+ @staticmethod
345
+ def max_value(*args) -> Any:
346
+ """最大值"""
347
+ return max(args)
348
+
349
+ @staticmethod
350
+ def min_value(*args) -> Any:
351
+ """最小值"""
352
+ return min(args)
353
+
354
+ # ==================== 类型转换 ====================
355
+
356
+ @staticmethod
357
+ def to_int(value: Any) -> int:
358
+ """转换为整数"""
359
+ return int(value)
360
+
361
+ @staticmethod
362
+ def to_float(value: Any) -> float:
363
+ """转换为浮点数"""
364
+ return float(value)
365
+
366
+ @staticmethod
367
+ def to_string(value: Any) -> str:
368
+ """转换为字符串"""
369
+ return str(value)
370
+
371
+ @staticmethod
372
+ def to_bool(value: Any) -> bool:
373
+ """转换为布尔值"""
374
+ if isinstance(value, str):
375
+ return value.lower() in ('true', '1', 'yes', 'on')
376
+ return bool(value)
@@ -0,0 +1,211 @@
1
+ """装饰器工具"""
2
+
3
+ import functools
4
+ import time
5
+ from typing import Callable, Type, Tuple, Any, Optional
6
+
7
+ from ..reference import INFO
8
+
9
+
10
+ def retry(
11
+ max_attempts: int = 3,
12
+ delay: float = 1.0,
13
+ exceptions: Tuple[Type[Exception], ...] = (Exception,),
14
+ on_retry: Optional[Callable] = None
15
+ ):
16
+ """重试装饰器
17
+
18
+ Args:
19
+ max_attempts: 最大重试次数
20
+ delay: 重试间隔 (秒)
21
+ exceptions: 需要重试的异常类型
22
+ on_retry: 重试时的回调函数
23
+
24
+ Example:
25
+ @retry(max_attempts=3, delay=1.0)
26
+ def unstable_function():
27
+ ...
28
+ """
29
+ def decorator(func: Callable) -> Callable:
30
+ @functools.wraps(func)
31
+ def wrapper(*args, **kwargs) -> Any:
32
+ last_exception = None
33
+
34
+ for attempt in range(1, max_attempts + 1):
35
+ try:
36
+ return func(*args, **kwargs)
37
+ except exceptions as e:
38
+ last_exception = e
39
+ if attempt < max_attempts:
40
+ INFO(f"第 {attempt} 次尝试失败: {e}, {delay}s 后重试...", "WARNING")
41
+ if on_retry:
42
+ on_retry(attempt, e)
43
+ time.sleep(delay)
44
+ else:
45
+ INFO(f"达到最大重试次数 ({max_attempts}), 放弃", "ERROR")
46
+
47
+ raise last_exception
48
+
49
+ return wrapper
50
+ return decorator
51
+
52
+
53
+ def timeout(seconds: float, default: Any = None, raise_error: bool = True):
54
+ """超时装饰器 (使用线程实现)
55
+
56
+ Args:
57
+ seconds: 超时时间 (秒)
58
+ default: 超时时返回的默认值
59
+ raise_error: 超时时是否抛出异常
60
+
61
+ Example:
62
+ @timeout(10.0)
63
+ def slow_function():
64
+ ...
65
+ """
66
+ def decorator(func: Callable) -> Callable:
67
+ @functools.wraps(func)
68
+ def wrapper(*args, **kwargs) -> Any:
69
+ import threading
70
+ result = [default]
71
+ exception = [None]
72
+
73
+ def target():
74
+ try:
75
+ result[0] = func(*args, **kwargs)
76
+ except Exception as e:
77
+ exception[0] = e
78
+
79
+ thread = threading.Thread(target=target)
80
+ thread.daemon = True
81
+ thread.start()
82
+ thread.join(timeout=seconds)
83
+
84
+ if thread.is_alive():
85
+ if raise_error:
86
+ raise TimeoutError(f"函数 {func.__name__} 执行超时 ({seconds}s)")
87
+ INFO(f"函数 {func.__name__} 执行超时 ({seconds}s)", "WARNING")
88
+ return default
89
+
90
+ if exception[0]:
91
+ raise exception[0]
92
+
93
+ return result[0]
94
+
95
+ return wrapper
96
+ return decorator
97
+
98
+
99
+ def catch_exception(
100
+ exceptions: Tuple[Type[Exception], ...] = (Exception,),
101
+ default: Any = None,
102
+ log_error: bool = True
103
+ ):
104
+ """异常捕获装饰器
105
+
106
+ Args:
107
+ exceptions: 要捕获的异常类型
108
+ default: 异常时返回的默认值
109
+ log_error: 是否记录错误日志
110
+
111
+ Example:
112
+ @catch_exception(default=None)
113
+ def risky_function():
114
+ ...
115
+ """
116
+ def decorator(func: Callable) -> Callable:
117
+ @functools.wraps(func)
118
+ def wrapper(*args, **kwargs) -> Any:
119
+ try:
120
+ return func(*args, **kwargs)
121
+ except exceptions as e:
122
+ if log_error:
123
+ INFO(f"函数 {func.__name__} 发生异常: {e}", "WARNING")
124
+ return default
125
+
126
+ return wrapper
127
+ return decorator
128
+
129
+
130
+ def log_execution(log_args: bool = True, log_result: bool = True):
131
+ """执行日志装饰器
132
+
133
+ Args:
134
+ log_args: 是否记录参数
135
+ log_result: 是否记录返回值
136
+
137
+ Example:
138
+ @log_execution()
139
+ def important_function(a, b):
140
+ ...
141
+ """
142
+ def decorator(func: Callable) -> Callable:
143
+ @functools.wraps(func)
144
+ def wrapper(*args, **kwargs) -> Any:
145
+ func_name = func.__name__
146
+
147
+ if log_args:
148
+ args_repr = [repr(a) for a in args]
149
+ kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
150
+ signature = ", ".join(args_repr + kwargs_repr)
151
+ INFO(f"调用 {func_name}({signature})")
152
+ else:
153
+ INFO(f"调用 {func_name}()")
154
+
155
+ start_time = time.time()
156
+ result = func(*args, **kwargs)
157
+ elapsed = time.time() - start_time
158
+
159
+ if log_result:
160
+ INFO(f"{func_name} 返回: {result!r} (耗时: {elapsed:.3f}s)")
161
+ else:
162
+ INFO(f"{func_name} 完成 (耗时: {elapsed:.3f}s)")
163
+
164
+ return result
165
+
166
+ return wrapper
167
+ return decorator
168
+
169
+
170
+ def singleton(cls: Type) -> Type:
171
+ """单例装饰器
172
+
173
+ Example:
174
+ @singleton
175
+ class MyClass:
176
+ ...
177
+ """
178
+ instances = {}
179
+
180
+ @functools.wraps(cls)
181
+ def get_instance(*args, **kwargs):
182
+ if cls not in instances:
183
+ instances[cls] = cls(*args, **kwargs)
184
+ return instances[cls]
185
+
186
+ return get_instance
187
+
188
+
189
+ def deprecated(message: str = ""):
190
+ """弃用警告装饰器
191
+
192
+ Args:
193
+ message: 弃用说明
194
+
195
+ Example:
196
+ @deprecated("请使用 new_function 代替")
197
+ def old_function():
198
+ ...
199
+ """
200
+ def decorator(func: Callable) -> Callable:
201
+ @functools.wraps(func)
202
+ def wrapper(*args, **kwargs) -> Any:
203
+ import warnings
204
+ warn_msg = f"{func.__name__} 已弃用"
205
+ if message:
206
+ warn_msg += f": {message}"
207
+ warnings.warn(warn_msg, DeprecationWarning, stacklevel=2)
208
+ return func(*args, **kwargs)
209
+
210
+ return wrapper
211
+ return decorator
@@ -0,0 +1,6 @@
1
+ """日志模块"""
2
+
3
+ from .logger import Logger
4
+ from .html_report import HtmlReporter
5
+
6
+ __all__ = ['Logger', 'HtmlReporter']