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.
- kdtest_pw/__init__.py +50 -0
- kdtest_pw/action/__init__.py +7 -0
- kdtest_pw/action/base_keyword.py +292 -0
- kdtest_pw/action/element_plus/__init__.py +23 -0
- kdtest_pw/action/element_plus/el_cascader.py +263 -0
- kdtest_pw/action/element_plus/el_datepicker.py +324 -0
- kdtest_pw/action/element_plus/el_dialog.py +317 -0
- kdtest_pw/action/element_plus/el_form.py +443 -0
- kdtest_pw/action/element_plus/el_menu.py +456 -0
- kdtest_pw/action/element_plus/el_select.py +268 -0
- kdtest_pw/action/element_plus/el_table.py +442 -0
- kdtest_pw/action/element_plus/el_tree.py +364 -0
- kdtest_pw/action/element_plus/el_upload.py +313 -0
- kdtest_pw/action/key_retrieval.py +311 -0
- kdtest_pw/action/page_action.py +1129 -0
- kdtest_pw/api/__init__.py +6 -0
- kdtest_pw/api/api_keyword.py +251 -0
- kdtest_pw/api/request_handler.py +232 -0
- kdtest_pw/cases/__init__.py +6 -0
- kdtest_pw/cases/case_collector.py +182 -0
- kdtest_pw/cases/case_executor.py +359 -0
- kdtest_pw/cases/read/__init__.py +6 -0
- kdtest_pw/cases/read/cell_handler.py +305 -0
- kdtest_pw/cases/read/excel_reader.py +223 -0
- kdtest_pw/cli/__init__.py +5 -0
- kdtest_pw/cli/run.py +318 -0
- kdtest_pw/common.py +106 -0
- kdtest_pw/core/__init__.py +7 -0
- kdtest_pw/core/browser_manager.py +196 -0
- kdtest_pw/core/config_loader.py +235 -0
- kdtest_pw/core/page_context.py +228 -0
- kdtest_pw/data/__init__.py +5 -0
- kdtest_pw/data/init_data.py +105 -0
- kdtest_pw/data/static/elementData.yaml +59 -0
- kdtest_pw/data/static/parameters.json +24 -0
- kdtest_pw/plugins/__init__.py +6 -0
- kdtest_pw/plugins/element_plus_plugin/__init__.py +5 -0
- kdtest_pw/plugins/element_plus_plugin/elementData/elementData.yaml +144 -0
- kdtest_pw/plugins/element_plus_plugin/element_plus_plugin.py +237 -0
- kdtest_pw/plugins/element_plus_plugin/my.ini +23 -0
- kdtest_pw/plugins/plugin_base.py +180 -0
- kdtest_pw/plugins/plugin_loader.py +260 -0
- kdtest_pw/product.py +5 -0
- kdtest_pw/reference.py +99 -0
- kdtest_pw/utils/__init__.py +13 -0
- kdtest_pw/utils/built_in_function.py +376 -0
- kdtest_pw/utils/decorator.py +211 -0
- kdtest_pw/utils/log/__init__.py +6 -0
- kdtest_pw/utils/log/html_report.py +336 -0
- kdtest_pw/utils/log/logger.py +123 -0
- kdtest_pw/utils/public_script.py +366 -0
- kdtest_pw-2.0.0.dist-info/METADATA +169 -0
- kdtest_pw-2.0.0.dist-info/RECORD +57 -0
- kdtest_pw-2.0.0.dist-info/WHEEL +5 -0
- kdtest_pw-2.0.0.dist-info/entry_points.txt +2 -0
- kdtest_pw-2.0.0.dist-info/licenses/LICENSE +21 -0
- kdtest_pw-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""用例执行引擎"""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import Dict, Any, Optional, List, Callable
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .read.excel_reader import TestCase, TestStep
|
|
10
|
+
from .read.cell_handler import CellHandler
|
|
11
|
+
from ..action.key_retrieval import KeyRetrieval
|
|
12
|
+
from ..reference import (
|
|
13
|
+
GSTORE, PRIVATEDATA, CASESDATA, INFO,
|
|
14
|
+
set_element_value, clear_element_values
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StepResult:
|
|
19
|
+
"""步骤执行结果"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, step: TestStep):
|
|
22
|
+
self.step = step
|
|
23
|
+
self.status: str = 'pending' # pending, passed, failed, skipped
|
|
24
|
+
self.start_time: Optional[datetime] = None
|
|
25
|
+
self.end_time: Optional[datetime] = None
|
|
26
|
+
self.duration: float = 0
|
|
27
|
+
self.error: Optional[str] = None
|
|
28
|
+
self.screenshot: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def is_passed(self) -> bool:
|
|
32
|
+
return self.status == 'passed'
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def is_failed(self) -> bool:
|
|
36
|
+
return self.status == 'failed'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CaseResult:
|
|
40
|
+
"""用例执行结果"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, case: TestCase):
|
|
43
|
+
self.case = case
|
|
44
|
+
self.status: str = 'pending' # pending, passed, failed, skipped
|
|
45
|
+
self.start_time: Optional[datetime] = None
|
|
46
|
+
self.end_time: Optional[datetime] = None
|
|
47
|
+
self.duration: float = 0
|
|
48
|
+
self.step_results: List[StepResult] = []
|
|
49
|
+
self.error: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_passed(self) -> bool:
|
|
53
|
+
return self.status == 'passed'
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_failed(self) -> bool:
|
|
57
|
+
return self.status == 'failed'
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def passed_steps(self) -> int:
|
|
61
|
+
return sum(1 for s in self.step_results if s.is_passed)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def failed_steps(self) -> int:
|
|
65
|
+
return sum(1 for s in self.step_results if s.is_failed)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CaseExecutor:
|
|
69
|
+
"""用例执行引擎
|
|
70
|
+
|
|
71
|
+
执行测试用例,管理执行流程和结果收集。
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
key_retrieval: KeyRetrieval = None,
|
|
77
|
+
cell_handler: CellHandler = None,
|
|
78
|
+
retry_count: int = 0,
|
|
79
|
+
screenshot_on_fail: bool = True,
|
|
80
|
+
stop_on_fail: bool = False
|
|
81
|
+
):
|
|
82
|
+
"""初始化执行引擎
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
key_retrieval: 关键字分发器
|
|
86
|
+
cell_handler: 单元格处理器
|
|
87
|
+
retry_count: 失败重试次数
|
|
88
|
+
screenshot_on_fail: 失败时是否截图
|
|
89
|
+
stop_on_fail: 失败时是否停止
|
|
90
|
+
"""
|
|
91
|
+
self._key_retrieval = key_retrieval or KeyRetrieval()
|
|
92
|
+
self._cell_handler = cell_handler or CellHandler()
|
|
93
|
+
self._retry_count = retry_count
|
|
94
|
+
self._screenshot_on_fail = screenshot_on_fail
|
|
95
|
+
self._stop_on_fail = stop_on_fail
|
|
96
|
+
|
|
97
|
+
self._results: List[CaseResult] = []
|
|
98
|
+
self._current_case: Optional[TestCase] = None
|
|
99
|
+
self._current_result: Optional[CaseResult] = None
|
|
100
|
+
|
|
101
|
+
# 回调
|
|
102
|
+
self._on_case_start: Optional[Callable] = None
|
|
103
|
+
self._on_case_end: Optional[Callable] = None
|
|
104
|
+
self._on_step_start: Optional[Callable] = None
|
|
105
|
+
self._on_step_end: Optional[Callable] = None
|
|
106
|
+
|
|
107
|
+
def set_callbacks(
|
|
108
|
+
self,
|
|
109
|
+
on_case_start: Callable = None,
|
|
110
|
+
on_case_end: Callable = None,
|
|
111
|
+
on_step_start: Callable = None,
|
|
112
|
+
on_step_end: Callable = None
|
|
113
|
+
) -> None:
|
|
114
|
+
"""设置回调函数
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
on_case_start: 用例开始回调
|
|
118
|
+
on_case_end: 用例结束回调
|
|
119
|
+
on_step_start: 步骤开始回调
|
|
120
|
+
on_step_end: 步骤结束回调
|
|
121
|
+
"""
|
|
122
|
+
self._on_case_start = on_case_start
|
|
123
|
+
self._on_case_end = on_case_end
|
|
124
|
+
self._on_step_start = on_step_start
|
|
125
|
+
self._on_step_end = on_step_end
|
|
126
|
+
|
|
127
|
+
def execute_cases(self, cases: List[TestCase]) -> List[CaseResult]:
|
|
128
|
+
"""执行多个用例
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
cases: 用例列表
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List[CaseResult]: 执行结果列表
|
|
135
|
+
"""
|
|
136
|
+
self._results.clear()
|
|
137
|
+
CASESDATA['total'] = len(cases)
|
|
138
|
+
CASESDATA['passed'] = 0
|
|
139
|
+
CASESDATA['failed'] = 0
|
|
140
|
+
CASESDATA['skipped'] = 0
|
|
141
|
+
|
|
142
|
+
INFO(f"开始执行 {len(cases)} 个用例")
|
|
143
|
+
|
|
144
|
+
for case in cases:
|
|
145
|
+
result = self.execute_case(case)
|
|
146
|
+
self._results.append(result)
|
|
147
|
+
|
|
148
|
+
if result.is_passed:
|
|
149
|
+
CASESDATA['passed'] += 1
|
|
150
|
+
elif result.is_failed:
|
|
151
|
+
CASESDATA['failed'] += 1
|
|
152
|
+
if self._stop_on_fail:
|
|
153
|
+
INFO("用例失败,停止执行", "WARNING")
|
|
154
|
+
break
|
|
155
|
+
else:
|
|
156
|
+
CASESDATA['skipped'] += 1
|
|
157
|
+
|
|
158
|
+
INFO(f"执行完成: 通过 {CASESDATA['passed']}, 失败 {CASESDATA['failed']}, 跳过 {CASESDATA['skipped']}")
|
|
159
|
+
return self._results
|
|
160
|
+
|
|
161
|
+
def execute_case(self, case: TestCase) -> CaseResult:
|
|
162
|
+
"""执行单个用例
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
case: 测试用例
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
CaseResult: 执行结果
|
|
169
|
+
"""
|
|
170
|
+
self._current_case = case
|
|
171
|
+
self._current_result = CaseResult(case)
|
|
172
|
+
PRIVATEDATA['CURRENT_CASE'] = case.name
|
|
173
|
+
|
|
174
|
+
INFO(f"执行用例: [{case.id}] {case.name}")
|
|
175
|
+
|
|
176
|
+
# 回调
|
|
177
|
+
if self._on_case_start:
|
|
178
|
+
self._on_case_start(case)
|
|
179
|
+
|
|
180
|
+
self._current_result.start_time = datetime.now()
|
|
181
|
+
|
|
182
|
+
# 清除上一个用例的缓存
|
|
183
|
+
clear_element_values()
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
# 执行所有步骤
|
|
187
|
+
for step in case.steps:
|
|
188
|
+
step_result = self._execute_step_with_retry(step)
|
|
189
|
+
self._current_result.step_results.append(step_result)
|
|
190
|
+
|
|
191
|
+
if step_result.is_failed and self._stop_on_fail:
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
# 判断用例结果
|
|
195
|
+
if any(s.is_failed for s in self._current_result.step_results):
|
|
196
|
+
self._current_result.status = 'failed'
|
|
197
|
+
else:
|
|
198
|
+
self._current_result.status = 'passed'
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
self._current_result.status = 'failed'
|
|
202
|
+
self._current_result.error = str(e)
|
|
203
|
+
INFO(f"用例执行异常: {e}", "ERROR")
|
|
204
|
+
|
|
205
|
+
self._current_result.end_time = datetime.now()
|
|
206
|
+
self._current_result.duration = (
|
|
207
|
+
self._current_result.end_time - self._current_result.start_time
|
|
208
|
+
).total_seconds()
|
|
209
|
+
|
|
210
|
+
status_text = '通过' if self._current_result.is_passed else '失败'
|
|
211
|
+
INFO(f"用例完成: [{case.id}] {case.name} - {status_text} ({self._current_result.duration:.2f}s)")
|
|
212
|
+
|
|
213
|
+
# 回调
|
|
214
|
+
if self._on_case_end:
|
|
215
|
+
self._on_case_end(case, self._current_result)
|
|
216
|
+
|
|
217
|
+
return self._current_result
|
|
218
|
+
|
|
219
|
+
def _execute_step_with_retry(self, step: TestStep) -> StepResult:
|
|
220
|
+
"""执行步骤(带重试)
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
step: 测试步骤
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
StepResult: 步骤结果
|
|
227
|
+
"""
|
|
228
|
+
result = None
|
|
229
|
+
retry = 0
|
|
230
|
+
|
|
231
|
+
while retry <= self._retry_count:
|
|
232
|
+
result = self._execute_step(step)
|
|
233
|
+
|
|
234
|
+
if result.is_passed:
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
if retry < self._retry_count:
|
|
238
|
+
INFO(f"步骤失败,第 {retry + 1} 次重试...", "WARNING")
|
|
239
|
+
retry += 1
|
|
240
|
+
else:
|
|
241
|
+
break
|
|
242
|
+
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
def _execute_step(self, step: TestStep) -> StepResult:
|
|
246
|
+
"""执行单个步骤
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
step: 测试步骤
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
StepResult: 步骤结果
|
|
253
|
+
"""
|
|
254
|
+
result = StepResult(step)
|
|
255
|
+
|
|
256
|
+
# 回调
|
|
257
|
+
if self._on_step_start:
|
|
258
|
+
self._on_step_start(step)
|
|
259
|
+
|
|
260
|
+
result.start_time = datetime.now()
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
# 处理定位信息
|
|
264
|
+
locator_info = []
|
|
265
|
+
for loc in step.locator:
|
|
266
|
+
processed = self._cell_handler.process(loc)
|
|
267
|
+
if processed:
|
|
268
|
+
locator_info.append(processed)
|
|
269
|
+
|
|
270
|
+
# 处理操作值
|
|
271
|
+
value = self._cell_handler.process(step.value) if step.value else None
|
|
272
|
+
|
|
273
|
+
# 执行关键字
|
|
274
|
+
INFO(f" 步骤: {step.description or step.keyword}")
|
|
275
|
+
exec_result = self._key_retrieval.execute(
|
|
276
|
+
keyword=step.keyword,
|
|
277
|
+
locator_info=locator_info,
|
|
278
|
+
value=value
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# 处理断言关键字返回值
|
|
282
|
+
if step.keyword.endswith('_assert') or step.keyword.startswith('assert'):
|
|
283
|
+
if exec_result is False:
|
|
284
|
+
raise AssertionError(f"断言失败: {step.keyword}")
|
|
285
|
+
|
|
286
|
+
result.status = 'passed'
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
result.status = 'failed'
|
|
290
|
+
result.error = str(e)
|
|
291
|
+
INFO(f" 步骤失败: {e}", "ERROR")
|
|
292
|
+
|
|
293
|
+
# 失败截图
|
|
294
|
+
if self._screenshot_on_fail:
|
|
295
|
+
result.screenshot = self._take_screenshot(step)
|
|
296
|
+
|
|
297
|
+
result.end_time = datetime.now()
|
|
298
|
+
result.duration = (result.end_time - result.start_time).total_seconds()
|
|
299
|
+
|
|
300
|
+
# 回调
|
|
301
|
+
if self._on_step_end:
|
|
302
|
+
self._on_step_end(step, result)
|
|
303
|
+
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
def _take_screenshot(self, step: TestStep) -> Optional[str]:
|
|
307
|
+
"""失败截图
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
step: 测试步骤
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
str: 截图路径
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
page = GSTORE.get('page')
|
|
317
|
+
if not page:
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
# 生成截图文件名
|
|
321
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
322
|
+
case_name = self._current_case.name if self._current_case else 'unknown'
|
|
323
|
+
# 移除非法字符
|
|
324
|
+
case_name = ''.join(c for c in case_name if c.isalnum() or c in ('_', '-'))
|
|
325
|
+
|
|
326
|
+
screenshot_dir = Path('result/screenshot')
|
|
327
|
+
screenshot_dir.mkdir(parents=True, exist_ok=True)
|
|
328
|
+
|
|
329
|
+
screenshot_path = screenshot_dir / f"{case_name}_{step.row}_{timestamp}.png"
|
|
330
|
+
page.screenshot(path=str(screenshot_path))
|
|
331
|
+
|
|
332
|
+
INFO(f" 截图已保存: {screenshot_path}")
|
|
333
|
+
return str(screenshot_path)
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
INFO(f" 截图失败: {e}", "WARNING")
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def results(self) -> List[CaseResult]:
|
|
341
|
+
"""获取所有执行结果"""
|
|
342
|
+
return self._results.copy()
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def summary(self) -> Dict[str, Any]:
|
|
346
|
+
"""获取执行摘要"""
|
|
347
|
+
total = len(self._results)
|
|
348
|
+
passed = sum(1 for r in self._results if r.is_passed)
|
|
349
|
+
failed = sum(1 for r in self._results if r.is_failed)
|
|
350
|
+
duration = sum(r.duration for r in self._results)
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
'total': total,
|
|
354
|
+
'passed': passed,
|
|
355
|
+
'failed': failed,
|
|
356
|
+
'skipped': total - passed - failed,
|
|
357
|
+
'pass_rate': (passed / total * 100) if total > 0 else 0,
|
|
358
|
+
'duration': duration,
|
|
359
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""单元格值处理器"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Optional, Dict
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
|
|
7
|
+
from ...reference import PRIVATEDATA, GSDSTORE, get_element_value, INFO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CellHandler:
|
|
11
|
+
"""单元格值处理器
|
|
12
|
+
|
|
13
|
+
处理 Excel 单元格中的特殊语法,如变量引用、内置函数等。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# 变量引用模式: ${varName} 或 ${module.varName}
|
|
17
|
+
VAR_PATTERN = re.compile(r'\$\{([^}]+)\}')
|
|
18
|
+
|
|
19
|
+
# 内置函数模式: @func(args) 或 @func()
|
|
20
|
+
FUNC_PATTERN = re.compile(r'@(\w+)\(([^)]*)\)')
|
|
21
|
+
|
|
22
|
+
def __init__(self, custom_params: Dict[str, Any] = None):
|
|
23
|
+
"""初始化处理器
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
custom_params: 自定义参数字典
|
|
27
|
+
"""
|
|
28
|
+
self._custom_params = custom_params or {}
|
|
29
|
+
|
|
30
|
+
# 注册内置函数
|
|
31
|
+
self._functions = {
|
|
32
|
+
'today': self._func_today,
|
|
33
|
+
'now': self._func_now,
|
|
34
|
+
'date': self._func_date,
|
|
35
|
+
'random': self._func_random,
|
|
36
|
+
'random_str': self._func_random_str,
|
|
37
|
+
'random_phone': self._func_random_phone,
|
|
38
|
+
'random_email': self._func_random_email,
|
|
39
|
+
'uuid': self._func_uuid,
|
|
40
|
+
'timestamp': self._func_timestamp,
|
|
41
|
+
'env': self._func_env,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def process(self, value: Any) -> Any:
|
|
45
|
+
"""处理单元格值
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
value: 原始值
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
处理后的值
|
|
52
|
+
"""
|
|
53
|
+
if value is None:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
value_str = str(value)
|
|
57
|
+
|
|
58
|
+
# 处理变量引用
|
|
59
|
+
value_str = self._process_variables(value_str)
|
|
60
|
+
|
|
61
|
+
# 处理内置函数
|
|
62
|
+
value_str = self._process_functions(value_str)
|
|
63
|
+
|
|
64
|
+
return value_str
|
|
65
|
+
|
|
66
|
+
def _process_variables(self, value: str) -> str:
|
|
67
|
+
"""处理变量引用
|
|
68
|
+
|
|
69
|
+
支持格式:
|
|
70
|
+
- ${varName} - 从元素值缓存获取
|
|
71
|
+
- ${self.param} - 从自定义参数获取
|
|
72
|
+
- ${env.name} - 从环境变量获取
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
value: 原始值
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
处理后的值
|
|
79
|
+
"""
|
|
80
|
+
def replace_var(match):
|
|
81
|
+
var_path = match.group(1)
|
|
82
|
+
|
|
83
|
+
# 检查是否有命名空间
|
|
84
|
+
if '.' in var_path:
|
|
85
|
+
namespace, var_name = var_path.split('.', 1)
|
|
86
|
+
|
|
87
|
+
if namespace == 'self':
|
|
88
|
+
# 自定义参数
|
|
89
|
+
return str(self._custom_params.get(var_name, match.group(0)))
|
|
90
|
+
elif namespace == 'env':
|
|
91
|
+
# 环境变量
|
|
92
|
+
import os
|
|
93
|
+
return os.environ.get(var_name, match.group(0))
|
|
94
|
+
elif namespace == 'ele':
|
|
95
|
+
# 元素值缓存
|
|
96
|
+
return str(get_element_value(var_name, match.group(0)))
|
|
97
|
+
|
|
98
|
+
# 默认从元素值缓存获取
|
|
99
|
+
cached = get_element_value(var_path)
|
|
100
|
+
if cached is not None:
|
|
101
|
+
return str(cached)
|
|
102
|
+
|
|
103
|
+
# 从自定义参数获取
|
|
104
|
+
if var_path in self._custom_params:
|
|
105
|
+
return str(self._custom_params[var_path])
|
|
106
|
+
|
|
107
|
+
# 返回原始匹配
|
|
108
|
+
return match.group(0)
|
|
109
|
+
|
|
110
|
+
return self.VAR_PATTERN.sub(replace_var, value)
|
|
111
|
+
|
|
112
|
+
def _process_functions(self, value: str) -> str:
|
|
113
|
+
"""处理内置函数
|
|
114
|
+
|
|
115
|
+
支持格式:
|
|
116
|
+
- @today() - 今天日期
|
|
117
|
+
- @now() - 当前时间
|
|
118
|
+
- @random(min, max) - 随机数
|
|
119
|
+
- @uuid() - UUID
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
value: 原始值
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
处理后的值
|
|
126
|
+
"""
|
|
127
|
+
def replace_func(match):
|
|
128
|
+
func_name = match.group(1)
|
|
129
|
+
args_str = match.group(2).strip()
|
|
130
|
+
|
|
131
|
+
# 解析参数
|
|
132
|
+
args = []
|
|
133
|
+
if args_str:
|
|
134
|
+
args = [a.strip().strip('"\'') for a in args_str.split(',')]
|
|
135
|
+
|
|
136
|
+
# 调用函数
|
|
137
|
+
if func_name in self._functions:
|
|
138
|
+
try:
|
|
139
|
+
return str(self._functions[func_name](*args))
|
|
140
|
+
except Exception as e:
|
|
141
|
+
INFO(f"函数执行失败: @{func_name}({args_str}) - {e}", "WARNING")
|
|
142
|
+
return match.group(0)
|
|
143
|
+
|
|
144
|
+
return match.group(0)
|
|
145
|
+
|
|
146
|
+
return self.FUNC_PATTERN.sub(replace_func, value)
|
|
147
|
+
|
|
148
|
+
# ==================== 内置函数 ====================
|
|
149
|
+
|
|
150
|
+
def _func_today(self, fmt: str = '%Y-%m-%d', offset: str = '0') -> str:
|
|
151
|
+
"""获取今天日期
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
fmt: 日期格式
|
|
155
|
+
offset: 天数偏移
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
str: 格式化的日期
|
|
159
|
+
"""
|
|
160
|
+
date = datetime.now() + timedelta(days=int(offset))
|
|
161
|
+
return date.strftime(fmt)
|
|
162
|
+
|
|
163
|
+
def _func_now(self, fmt: str = '%Y-%m-%d %H:%M:%S') -> str:
|
|
164
|
+
"""获取当前时间
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
fmt: 时间格式
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
str: 格式化的时间
|
|
171
|
+
"""
|
|
172
|
+
return datetime.now().strftime(fmt)
|
|
173
|
+
|
|
174
|
+
def _func_date(self, fmt: str = '%Y-%m-%d', offset: str = '0', unit: str = 'days') -> str:
|
|
175
|
+
"""获取日期
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
fmt: 日期格式
|
|
179
|
+
offset: 偏移量
|
|
180
|
+
unit: 单位 (days, hours, minutes, seconds)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
str: 格式化的日期
|
|
184
|
+
"""
|
|
185
|
+
offset_int = int(offset)
|
|
186
|
+
kwargs = {unit: offset_int}
|
|
187
|
+
date = datetime.now() + timedelta(**kwargs)
|
|
188
|
+
return date.strftime(fmt)
|
|
189
|
+
|
|
190
|
+
def _func_random(self, min_val: str = '0', max_val: str = '100') -> int:
|
|
191
|
+
"""生成随机整数
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
min_val: 最小值
|
|
195
|
+
max_val: 最大值
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
int: 随机整数
|
|
199
|
+
"""
|
|
200
|
+
import random
|
|
201
|
+
return random.randint(int(min_val), int(max_val))
|
|
202
|
+
|
|
203
|
+
def _func_random_str(self, length: str = '8', chars: str = 'alphanumeric') -> str:
|
|
204
|
+
"""生成随机字符串
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
length: 长度
|
|
208
|
+
chars: 字符类型 (alpha, numeric, alphanumeric)
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
str: 随机字符串
|
|
212
|
+
"""
|
|
213
|
+
import random
|
|
214
|
+
import string
|
|
215
|
+
|
|
216
|
+
char_map = {
|
|
217
|
+
'alpha': string.ascii_letters,
|
|
218
|
+
'numeric': string.digits,
|
|
219
|
+
'alphanumeric': string.ascii_letters + string.digits,
|
|
220
|
+
'lower': string.ascii_lowercase,
|
|
221
|
+
'upper': string.ascii_uppercase,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
charset = char_map.get(chars, string.ascii_letters + string.digits)
|
|
225
|
+
return ''.join(random.choices(charset, k=int(length)))
|
|
226
|
+
|
|
227
|
+
def _func_random_phone(self, prefix: str = '1') -> str:
|
|
228
|
+
"""生成随机手机号
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
prefix: 号码前缀
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
str: 随机手机号
|
|
235
|
+
"""
|
|
236
|
+
import random
|
|
237
|
+
prefixes = ['13', '14', '15', '16', '17', '18', '19']
|
|
238
|
+
phone_prefix = random.choice(prefixes) if prefix == '1' else prefix
|
|
239
|
+
return phone_prefix + ''.join([str(random.randint(0, 9)) for _ in range(9)])
|
|
240
|
+
|
|
241
|
+
def _func_random_email(self, domain: str = 'test.com') -> str:
|
|
242
|
+
"""生成随机邮箱
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
domain: 邮箱域名
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
str: 随机邮箱
|
|
249
|
+
"""
|
|
250
|
+
username = self._func_random_str('8', 'lower')
|
|
251
|
+
return f"{username}@{domain}"
|
|
252
|
+
|
|
253
|
+
def _func_uuid(self) -> str:
|
|
254
|
+
"""生成 UUID
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
str: UUID 字符串
|
|
258
|
+
"""
|
|
259
|
+
import uuid
|
|
260
|
+
return str(uuid.uuid4())
|
|
261
|
+
|
|
262
|
+
def _func_timestamp(self, unit: str = 'ms') -> int:
|
|
263
|
+
"""获取时间戳
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
unit: 单位 (s, ms)
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
int: 时间戳
|
|
270
|
+
"""
|
|
271
|
+
import time
|
|
272
|
+
ts = time.time()
|
|
273
|
+
if unit == 'ms':
|
|
274
|
+
return int(ts * 1000)
|
|
275
|
+
return int(ts)
|
|
276
|
+
|
|
277
|
+
def _func_env(self, name: str, default: str = '') -> str:
|
|
278
|
+
"""获取环境变量
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
name: 环境变量名
|
|
282
|
+
default: 默认值
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
str: 环境变量值
|
|
286
|
+
"""
|
|
287
|
+
import os
|
|
288
|
+
return os.environ.get(name, default)
|
|
289
|
+
|
|
290
|
+
def register_function(self, name: str, func) -> None:
|
|
291
|
+
"""注册自定义函数
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
name: 函数名
|
|
295
|
+
func: 函数对象
|
|
296
|
+
"""
|
|
297
|
+
self._functions[name] = func
|
|
298
|
+
|
|
299
|
+
def set_custom_params(self, params: Dict[str, Any]) -> None:
|
|
300
|
+
"""设置自定义参数
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
params: 参数字典
|
|
304
|
+
"""
|
|
305
|
+
self._custom_params.update(params)
|