pytest-dsl 0.13.0__tar.gz → 0.14.0__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_dsl-0.13.0/pytest_dsl.egg-info → pytest_dsl-0.14.0}/PKG-INFO +1 -1
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pyproject.toml +4 -4
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/cli.py +34 -2
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/custom_keyword_manager.py +114 -14
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/dsl_executor.py +217 -28
- pytest_dsl-0.14.0/pytest_dsl/core/hook_manager.py +87 -0
- pytest_dsl-0.14.0/pytest_dsl/core/hookable_executor.py +134 -0
- pytest_dsl-0.14.0/pytest_dsl/core/hookable_keyword_manager.py +106 -0
- pytest_dsl-0.14.0/pytest_dsl/core/hookspecs.py +175 -0
- pytest_dsl-0.14.0/pytest_dsl/core/yaml_loader.py +239 -0
- pytest_dsl-0.14.0/pytest_dsl/core/yaml_vars.py +158 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0/pytest_dsl.egg-info}/PKG-INFO +1 -1
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl.egg-info/SOURCES.txt +11 -0
- pytest_dsl-0.14.0/tests/auth_config.yaml +280 -0
- pytest_dsl-0.14.0/tests/simple_config.yaml +64 -0
- pytest_dsl-0.14.0/tests/test_auth_mock_server.py +627 -0
- pytest_dsl-0.14.0/tests/test_auth_runner.py +337 -0
- pytest_dsl-0.14.0/tests/test_platform_hook_integration.py +981 -0
- pytest_dsl-0.14.0/tests/test_platform_hook_pytest.py +59 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_retry_runner.py +0 -1
- pytest_dsl-0.14.0/tests/test_simple_hook_demo.py +357 -0
- pytest_dsl-0.13.0/pytest_dsl/core/yaml_loader.py +0 -139
- pytest_dsl-0.13.0/pytest_dsl/core/yaml_vars.py +0 -75
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/LICENSE +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/MANIFEST.in +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/README.md +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/__init__.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/conftest_adapter.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/__init__.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/auth_provider.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/auto_decorator.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/auto_directory.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/context.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/dsl_executor_utils.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/global_context.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/http_client.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/http_request.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/keyword_manager.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/lexer.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/parser.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/parsetab.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/plugin_discovery.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/utils.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/variable_utils.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/docs/custom_keywords.md +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/__init__.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/assert/assertion_example.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/assert/boolean_test.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/assert/expression_test.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/custom/test_advanced_keywords.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/custom/test_custom_keywords.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/custom/test_default_values.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/__init__.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/builtin_auth_test.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/file_reference_test.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_advanced.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_example.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_length_test.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_retry_assertions.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_with_yaml.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/new_retry_test.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/retry_assertions_only.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/retry_config_only.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/retry_debug.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/retry_with_fix.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/simple_retry.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/vars.yaml +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/quickstart/api_basics.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/quickstart/assertions.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/quickstart/loops.auto +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/test_assert.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/test_custom_keyword.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/test_http.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/test_quickstart.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/keywords/__init__.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/keywords/assertion_keywords.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/keywords/global_keywords.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/keywords/http_keywords.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/keywords/system_keywords.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/main_adapter.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/plugin.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/remote/__init__.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/remote/hook_manager.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/remote/keyword_client.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/remote/keyword_server.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/remote/variable_bridge.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/templates/keywords_report.html +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl.egg-info/dependency_links.txt +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl.egg-info/entry_points.txt +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl.egg-info/requires.txt +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl.egg-info/top_level.txt +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/setup.cfg +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/setup.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/mock_config.yaml +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_end_to_end_seamless.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_enhanced_variable_access.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_http_assertions_extractors.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_mock_server.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_seamless_variable_sync.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_variable_sync.py +0 -0
- {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_variable_sync_demo.py +0 -0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "pytest-dsl"
|
7
|
-
version = "0.
|
7
|
+
version = "0.14.0"
|
8
8
|
description = "A DSL testing framework based on pytest"
|
9
9
|
readme = "README.md"
|
10
10
|
requires-python = ">=3.9"
|
@@ -49,6 +49,6 @@ pytest-dsl-list = "pytest_dsl.cli:main_list_keywords"
|
|
49
49
|
"Bug Tracker" = "https://github.com/felix-1991/pytest-dsl/issues"
|
50
50
|
|
51
51
|
|
52
|
-
[[tool.uv.index]]
|
53
|
-
name = "tuna"
|
54
|
-
url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
|
52
|
+
# [[tool.uv.index]]
|
53
|
+
# name = "tuna"
|
54
|
+
# url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
|
@@ -724,12 +724,17 @@ def list_keywords(output_format='json', name_filter=None,
|
|
724
724
|
|
725
725
|
def load_yaml_variables(args):
|
726
726
|
"""从命令行参数加载YAML变量"""
|
727
|
-
#
|
727
|
+
# 使用统一的加载函数,包含远程服务器自动连接功能和hook支持
|
728
728
|
try:
|
729
|
+
# 尝试从环境变量获取环境名称
|
730
|
+
environment = (os.environ.get('PYTEST_DSL_ENVIRONMENT') or
|
731
|
+
os.environ.get('ENVIRONMENT'))
|
732
|
+
|
729
733
|
load_yaml_variables_from_args(
|
730
734
|
yaml_files=args.yaml_vars,
|
731
735
|
yaml_vars_dir=args.yaml_vars_dir,
|
732
|
-
project_root=os.getcwd() # CLI模式下使用当前工作目录作为项目根目录
|
736
|
+
project_root=os.getcwd(), # CLI模式下使用当前工作目录作为项目根目录
|
737
|
+
environment=environment
|
733
738
|
)
|
734
739
|
except Exception as e:
|
735
740
|
print(f"加载YAML变量失败: {str(e)}")
|
@@ -774,6 +779,33 @@ def run_dsl_tests(args):
|
|
774
779
|
# 加载YAML变量(包括远程服务器自动连接)
|
775
780
|
load_yaml_variables(args)
|
776
781
|
|
782
|
+
# 支持hook机制的执行
|
783
|
+
from pytest_dsl.core.hookable_executor import hookable_executor
|
784
|
+
|
785
|
+
# 检查是否有hook提供的用例列表
|
786
|
+
hook_cases = hookable_executor.list_dsl_cases()
|
787
|
+
if hook_cases:
|
788
|
+
# 如果有hook提供的用例,优先执行这些用例
|
789
|
+
print(f"通过Hook发现 {len(hook_cases)} 个DSL用例")
|
790
|
+
failures = 0
|
791
|
+
for case in hook_cases:
|
792
|
+
case_id = case.get('id') or case.get('name', 'unknown')
|
793
|
+
try:
|
794
|
+
print(f"执行用例: {case.get('name', case_id)}")
|
795
|
+
hookable_executor.execute_dsl(str(case_id))
|
796
|
+
print(f"✓ 用例 {case.get('name', case_id)} 执行成功")
|
797
|
+
except Exception as e:
|
798
|
+
print(f"✗ 用例 {case.get('name', case_id)} 执行失败: {e}")
|
799
|
+
failures += 1
|
800
|
+
|
801
|
+
if failures > 0:
|
802
|
+
print(f"总计 {failures}/{len(hook_cases)} 个测试失败")
|
803
|
+
sys.exit(1)
|
804
|
+
else:
|
805
|
+
print(f"所有 {len(hook_cases)} 个测试成功完成")
|
806
|
+
return
|
807
|
+
|
808
|
+
# 如果没有hook用例,使用传统的文件执行方式
|
777
809
|
lexer = get_lexer()
|
778
810
|
parser = get_parser()
|
779
811
|
executor = DSLExecutor()
|
@@ -200,23 +200,37 @@ class CustomKeywordManager:
|
|
200
200
|
with open(file_path, 'r', encoding='utf-8') as f:
|
201
201
|
content = f.read()
|
202
202
|
|
203
|
-
#
|
204
|
-
lexer = get_lexer()
|
205
|
-
parser = get_parser()
|
206
|
-
ast = parser.parse(content, lexer=lexer)
|
207
|
-
|
208
|
-
# 标记为已加载
|
203
|
+
# 标记为已加载(在解析前标记,避免循环导入)
|
209
204
|
self.resource_cache[absolute_path] = True
|
210
205
|
|
211
|
-
#
|
212
|
-
self.
|
206
|
+
# 使用公共方法解析和处理资源文件内容
|
207
|
+
self._process_resource_file_content(content, file_path)
|
213
208
|
|
214
|
-
# 注册关键字
|
215
|
-
self._register_keywords(ast, file_path)
|
216
209
|
except Exception as e:
|
210
|
+
# 如果处理失败,移除缓存标记
|
211
|
+
self.resource_cache.pop(absolute_path, None)
|
217
212
|
print(f"资源文件 {file_path} 加载失败: {str(e)}")
|
218
213
|
raise
|
219
214
|
|
215
|
+
def _process_resource_file_content(self, content: str,
|
216
|
+
file_path: str) -> None:
|
217
|
+
"""处理资源文件内容
|
218
|
+
|
219
|
+
Args:
|
220
|
+
content: 文件内容
|
221
|
+
file_path: 文件路径
|
222
|
+
"""
|
223
|
+
# 解析资源文件
|
224
|
+
lexer = get_lexer()
|
225
|
+
parser = get_parser()
|
226
|
+
ast = parser.parse(content, lexer=lexer)
|
227
|
+
|
228
|
+
# 处理导入指令
|
229
|
+
self._process_imports(ast, os.path.dirname(file_path))
|
230
|
+
|
231
|
+
# 注册关键字
|
232
|
+
self._register_keywords_from_ast(ast, file_path)
|
233
|
+
|
220
234
|
def _process_imports(self, ast: Node, base_dir: str) -> None:
|
221
235
|
"""处理资源文件中的导入指令
|
222
236
|
|
@@ -244,12 +258,13 @@ class CustomKeywordManager:
|
|
244
258
|
# 递归加载导入的资源文件
|
245
259
|
self.load_resource_file(imported_file)
|
246
260
|
|
247
|
-
def
|
248
|
-
|
261
|
+
def _register_keywords_from_ast(self, ast: Node,
|
262
|
+
source_name: str) -> None:
|
263
|
+
"""从AST中注册关键字(重构后的版本)
|
249
264
|
|
250
265
|
Args:
|
251
266
|
ast: 抽象语法树
|
252
|
-
|
267
|
+
source_name: 来源名称
|
253
268
|
"""
|
254
269
|
if ast.type != 'Start' or len(ast.children) < 2:
|
255
270
|
return
|
@@ -261,7 +276,7 @@ class CustomKeywordManager:
|
|
261
276
|
|
262
277
|
for node in statements_node.children:
|
263
278
|
if node.type in ['CustomKeyword', 'Function']:
|
264
|
-
self._register_custom_keyword(node,
|
279
|
+
self._register_custom_keyword(node, source_name)
|
265
280
|
|
266
281
|
def _register_custom_keyword(self, node: Node, file_path: str) -> None:
|
267
282
|
"""注册自定义关键字
|
@@ -342,6 +357,91 @@ class CustomKeywordManager:
|
|
342
357
|
|
343
358
|
print(f"已注册自定义关键字: {keyword_name} 来自文件: {file_path}")
|
344
359
|
|
360
|
+
def register_keyword_from_dsl_content(self, dsl_content: str,
|
361
|
+
source_name: str = "DSL内容") -> list:
|
362
|
+
"""从DSL内容注册关键字(公共方法)
|
363
|
+
|
364
|
+
Args:
|
365
|
+
dsl_content: DSL文本内容
|
366
|
+
source_name: 来源名称,用于日志显示
|
367
|
+
|
368
|
+
Returns:
|
369
|
+
list: 注册成功的关键字名称列表
|
370
|
+
|
371
|
+
Raises:
|
372
|
+
Exception: 解析或注册失败时抛出异常
|
373
|
+
"""
|
374
|
+
try:
|
375
|
+
# 解析DSL内容
|
376
|
+
lexer = get_lexer()
|
377
|
+
parser = get_parser()
|
378
|
+
ast = parser.parse(dsl_content, lexer=lexer)
|
379
|
+
|
380
|
+
# 收集注册前的关键字列表
|
381
|
+
existing_keywords = (
|
382
|
+
set(keyword_manager._keywords.keys())
|
383
|
+
if hasattr(keyword_manager, '_keywords')
|
384
|
+
else set()
|
385
|
+
)
|
386
|
+
|
387
|
+
# 使用统一的注册方法
|
388
|
+
self._register_keywords_from_ast(ast, source_name)
|
389
|
+
|
390
|
+
# 计算新注册的关键字
|
391
|
+
new_keywords = (
|
392
|
+
set(keyword_manager._keywords.keys())
|
393
|
+
if hasattr(keyword_manager, '_keywords')
|
394
|
+
else set()
|
395
|
+
)
|
396
|
+
registered_keywords = list(new_keywords - existing_keywords)
|
397
|
+
|
398
|
+
if not registered_keywords:
|
399
|
+
raise ValueError("在DSL内容中未找到任何关键字定义")
|
400
|
+
|
401
|
+
return registered_keywords
|
402
|
+
|
403
|
+
except Exception as e:
|
404
|
+
print(f"从DSL内容注册关键字失败(来源:{source_name}): {e}")
|
405
|
+
raise
|
406
|
+
|
407
|
+
def register_specific_keyword_from_dsl_content(
|
408
|
+
self, keyword_name: str, dsl_content: str,
|
409
|
+
source_name: str = "DSL内容") -> bool:
|
410
|
+
"""从DSL内容注册指定的关键字(公共方法)
|
411
|
+
|
412
|
+
Args:
|
413
|
+
keyword_name: 要注册的关键字名称
|
414
|
+
dsl_content: DSL文本内容
|
415
|
+
source_name: 来源名称,用于日志显示
|
416
|
+
|
417
|
+
Returns:
|
418
|
+
bool: 是否注册成功
|
419
|
+
|
420
|
+
Raises:
|
421
|
+
Exception: 解析失败或未找到指定关键字时抛出异常
|
422
|
+
"""
|
423
|
+
try:
|
424
|
+
# 解析DSL内容
|
425
|
+
lexer = get_lexer()
|
426
|
+
parser = get_parser()
|
427
|
+
ast = parser.parse(dsl_content, lexer=lexer)
|
428
|
+
|
429
|
+
# 查找指定的关键字定义
|
430
|
+
if ast.type == 'Start' and len(ast.children) >= 2:
|
431
|
+
statements_node = ast.children[1]
|
432
|
+
if statements_node.type == 'Statements':
|
433
|
+
for node in statements_node.children:
|
434
|
+
if (node.type in ['CustomKeyword', 'Function'] and
|
435
|
+
node.value == keyword_name):
|
436
|
+
self._register_custom_keyword(node, source_name)
|
437
|
+
return True
|
438
|
+
|
439
|
+
raise ValueError(f"在DSL内容中未找到关键字定义: {keyword_name}")
|
440
|
+
|
441
|
+
except Exception as e:
|
442
|
+
print(f"从DSL内容注册关键字失败 {keyword_name}(来源:{source_name}): {e}")
|
443
|
+
raise
|
444
|
+
|
345
445
|
|
346
446
|
# 创建全局自定义关键字管理器实例
|
347
447
|
custom_keyword_manager = CustomKeywordManager()
|
@@ -2,14 +2,11 @@ import re
|
|
2
2
|
import allure
|
3
3
|
import csv
|
4
4
|
import os
|
5
|
-
import
|
6
|
-
from pytest_dsl.core.
|
7
|
-
from pytest_dsl.core.parser import get_parser, Node
|
5
|
+
from typing import Dict, Any
|
6
|
+
from pytest_dsl.core.parser import Node
|
8
7
|
from pytest_dsl.core.keyword_manager import keyword_manager
|
9
8
|
from pytest_dsl.core.global_context import global_context
|
10
9
|
from pytest_dsl.core.context import TestContext
|
11
|
-
import pytest_dsl.keywords
|
12
|
-
from pytest_dsl.core.yaml_vars import yaml_vars
|
13
10
|
from pytest_dsl.core.variable_utils import VariableReplacer
|
14
11
|
|
15
12
|
|
@@ -39,14 +36,25 @@ class DSLExecutor:
|
|
39
36
|
- PYTEST_DSL_KEEP_VARIABLES=0: (默认) 执行完成后清空变量,用于正常DSL执行
|
40
37
|
"""
|
41
38
|
|
42
|
-
def __init__(self):
|
43
|
-
"""初始化DSL执行器
|
39
|
+
def __init__(self, enable_hooks: bool = True):
|
40
|
+
"""初始化DSL执行器
|
41
|
+
|
42
|
+
Args:
|
43
|
+
enable_hooks: 是否启用hook机制,默认True
|
44
|
+
"""
|
44
45
|
self.variables = {}
|
45
46
|
self.test_context = TestContext()
|
46
47
|
self.test_context.executor = self # 让 test_context 能够访问到 executor
|
47
48
|
self.variable_replacer = VariableReplacer(
|
48
49
|
self.variables, self.test_context)
|
49
50
|
self.imported_files = set() # 跟踪已导入的文件,避免循环导入
|
51
|
+
|
52
|
+
# Hook相关配置
|
53
|
+
self.enable_hooks = enable_hooks
|
54
|
+
self.current_dsl_id = None # 当前执行的DSL标识符
|
55
|
+
|
56
|
+
if self.enable_hooks:
|
57
|
+
self._init_hooks()
|
50
58
|
|
51
59
|
def set_current_data(self, data):
|
52
60
|
"""设置当前测试数据集"""
|
@@ -141,7 +149,11 @@ class DSLExecutor:
|
|
141
149
|
return self.variable_replacer.local_variables[value]
|
142
150
|
|
143
151
|
# 定义扩展的变量引用模式,支持数组索引和字典键访问
|
144
|
-
pattern =
|
152
|
+
pattern = (
|
153
|
+
r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*'
|
154
|
+
r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)'
|
155
|
+
r'|(?:\[[^\]]+\]))*)\}'
|
156
|
+
)
|
145
157
|
# 检查整个字符串是否完全匹配单一变量引用模式
|
146
158
|
match = re.fullmatch(pattern, value)
|
147
159
|
if match:
|
@@ -198,13 +210,15 @@ class DSLExecutor:
|
|
198
210
|
operator = expr_node.value # 操作符: +, -, *, /, %
|
199
211
|
|
200
212
|
# 尝试类型转换 - 如果是字符串数字则转为数字
|
201
|
-
if isinstance(left_value, str) and
|
213
|
+
if (isinstance(left_value, str) and
|
214
|
+
str(left_value).replace('.', '', 1).isdigit()):
|
202
215
|
left_value = float(left_value)
|
203
216
|
# 如果是整数则转为整数
|
204
217
|
if left_value.is_integer():
|
205
218
|
left_value = int(left_value)
|
206
219
|
|
207
|
-
if isinstance(right_value, str) and
|
220
|
+
if (isinstance(right_value, str) and
|
221
|
+
str(right_value).replace('.', '', 1).isdigit()):
|
208
222
|
right_value = float(right_value)
|
209
223
|
# 如果是整数则转为整数
|
210
224
|
if right_value.is_integer():
|
@@ -220,9 +234,11 @@ class DSLExecutor:
|
|
220
234
|
return left_value - right_value
|
221
235
|
elif operator == '*':
|
222
236
|
# 如果其中一个是字符串,另一个是数字,则进行字符串重复
|
223
|
-
if isinstance(left_value, str) and
|
237
|
+
if (isinstance(left_value, str) and
|
238
|
+
isinstance(right_value, (int, float))):
|
224
239
|
return left_value * int(right_value)
|
225
|
-
elif isinstance(right_value, str) and
|
240
|
+
elif (isinstance(right_value, str) and
|
241
|
+
isinstance(left_value, (int, float))):
|
226
242
|
return right_value * int(left_value)
|
227
243
|
return left_value * right_value
|
228
244
|
elif operator == '/':
|
@@ -287,7 +303,8 @@ class DSLExecutor:
|
|
287
303
|
for stmt in statements_node.children:
|
288
304
|
if stmt.type == 'CustomKeyword':
|
289
305
|
# 导入自定义关键字管理器
|
290
|
-
from pytest_dsl.core.custom_keyword_manager import
|
306
|
+
from pytest_dsl.core.custom_keyword_manager import (
|
307
|
+
custom_keyword_manager)
|
291
308
|
# 注册自定义关键字
|
292
309
|
custom_keyword_manager._register_custom_keyword(
|
293
310
|
stmt, "current_file")
|
@@ -317,7 +334,7 @@ class DSLExecutor:
|
|
317
334
|
elif child.type == 'Teardown':
|
318
335
|
teardown_node = child
|
319
336
|
|
320
|
-
|
337
|
+
# 在_execute_test_iteration之前添加
|
321
338
|
self._handle_custom_keywords_in_file(node)
|
322
339
|
# 执行测试
|
323
340
|
self._execute_test_iteration(metadata, node, teardown_node)
|
@@ -339,8 +356,34 @@ class DSLExecutor:
|
|
339
356
|
|
340
357
|
def _auto_import_resources(self):
|
341
358
|
"""自动导入项目中的resources目录"""
|
359
|
+
# 首先尝试通过hook获取资源列表
|
360
|
+
if (self.enable_hooks and hasattr(self, 'hook_manager') and
|
361
|
+
self.hook_manager):
|
362
|
+
try:
|
363
|
+
cases = []
|
364
|
+
case_results = self.hook_manager.pm.hook.dsl_list_cases()
|
365
|
+
for result in case_results:
|
366
|
+
if result:
|
367
|
+
cases.extend(result)
|
368
|
+
|
369
|
+
# 如果hook返回了资源,导入它们
|
370
|
+
for case in cases:
|
371
|
+
case_id = case.get('id') or case.get('file_path', '')
|
372
|
+
if case_id and case_id not in self.imported_files:
|
373
|
+
try:
|
374
|
+
print(f"通过hook自动导入资源: {case_id}")
|
375
|
+
self._handle_import(case_id)
|
376
|
+
except Exception as e:
|
377
|
+
print(f"通过hook自动导入资源失败: {case_id}, 错误: {str(e)}")
|
378
|
+
continue
|
379
|
+
except Exception as e:
|
380
|
+
print(f"通过hook自动导入资源时出现警告: {str(e)}")
|
381
|
+
|
382
|
+
# 然后进行传统的文件系统自动导入
|
342
383
|
try:
|
343
|
-
from pytest_dsl.core.custom_keyword_manager import
|
384
|
+
from pytest_dsl.core.custom_keyword_manager import (
|
385
|
+
custom_keyword_manager
|
386
|
+
)
|
344
387
|
|
345
388
|
# 尝试从多个可能的项目根目录位置导入resources
|
346
389
|
possible_roots = [
|
@@ -355,14 +398,15 @@ class DSLExecutor:
|
|
355
398
|
pytest_root = pytest.config.rootdir
|
356
399
|
if pytest_root:
|
357
400
|
possible_roots.insert(0, str(pytest_root))
|
358
|
-
except:
|
401
|
+
except Exception:
|
359
402
|
pass
|
360
403
|
|
361
404
|
# 尝试每个可能的根目录
|
362
405
|
for project_root in possible_roots:
|
363
406
|
if project_root and os.path.exists(project_root):
|
364
407
|
resources_dir = os.path.join(project_root, "resources")
|
365
|
-
if os.path.exists(resources_dir) and
|
408
|
+
if (os.path.exists(resources_dir) and
|
409
|
+
os.path.isdir(resources_dir)):
|
366
410
|
custom_keyword_manager.auto_import_resources_directory(
|
367
411
|
project_root)
|
368
412
|
break
|
@@ -382,10 +426,34 @@ class DSLExecutor:
|
|
382
426
|
return
|
383
427
|
|
384
428
|
try:
|
385
|
-
#
|
386
|
-
|
387
|
-
|
388
|
-
|
429
|
+
# 尝试通过hook加载内容
|
430
|
+
content = None
|
431
|
+
if (self.enable_hooks and hasattr(self, 'hook_manager') and
|
432
|
+
self.hook_manager):
|
433
|
+
content_results = (
|
434
|
+
self.hook_manager.pm.hook.dsl_load_content(
|
435
|
+
dsl_id=file_path
|
436
|
+
)
|
437
|
+
)
|
438
|
+
for result in content_results:
|
439
|
+
if result is not None:
|
440
|
+
content = result
|
441
|
+
break
|
442
|
+
|
443
|
+
# 如果hook返回了内容,直接使用DSL解析方式处理
|
444
|
+
if content is not None:
|
445
|
+
ast = self._parse_dsl_content(content)
|
446
|
+
|
447
|
+
# 只处理自定义关键字,不执行测试流程
|
448
|
+
self._handle_custom_keywords_in_file(ast)
|
449
|
+
self.imported_files.add(file_path)
|
450
|
+
else:
|
451
|
+
# 使用传统方式导入文件
|
452
|
+
from pytest_dsl.core.custom_keyword_manager import (
|
453
|
+
custom_keyword_manager
|
454
|
+
)
|
455
|
+
custom_keyword_manager.load_resource_file(file_path)
|
456
|
+
self.imported_files.add(file_path)
|
389
457
|
except Exception as e:
|
390
458
|
print(f"导入资源文件失败: {file_path}, 错误: {str(e)}")
|
391
459
|
raise
|
@@ -485,7 +553,8 @@ class DSLExecutor:
|
|
485
553
|
if capture_var.startswith('g_'):
|
486
554
|
global_context.set_variable(capture_var, capture_value)
|
487
555
|
else:
|
488
|
-
self.variable_replacer.local_variables[
|
556
|
+
self.variable_replacer.local_variables[
|
557
|
+
capture_var] = capture_value
|
489
558
|
self.test_context.set(capture_var, capture_value)
|
490
559
|
|
491
560
|
# 将主要结果赋值给指定变量
|
@@ -504,7 +573,8 @@ class DSLExecutor:
|
|
504
573
|
)
|
505
574
|
else:
|
506
575
|
# 存储在本地变量字典和测试上下文中
|
507
|
-
self.variable_replacer.local_variables[
|
576
|
+
self.variable_replacer.local_variables[
|
577
|
+
var_name] = actual_result
|
508
578
|
self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
|
509
579
|
allure.attach(
|
510
580
|
f"变量: {var_name}\n值: {actual_result}",
|
@@ -566,7 +636,7 @@ class DSLExecutor:
|
|
566
636
|
# 由于KeywordManager中的wrapper已经添加了allure.step和日志,这里不再重复添加
|
567
637
|
result = keyword_manager.execute(keyword_name, **kwargs)
|
568
638
|
return result
|
569
|
-
except Exception
|
639
|
+
except Exception:
|
570
640
|
# 异常会在KeywordManager的wrapper中记录,这里只需要向上抛出
|
571
641
|
raise
|
572
642
|
|
@@ -741,7 +811,8 @@ class DSLExecutor:
|
|
741
811
|
if capture_var.startswith('g_'):
|
742
812
|
global_context.set_variable(capture_var, capture_value)
|
743
813
|
else:
|
744
|
-
self.variable_replacer.local_variables[
|
814
|
+
self.variable_replacer.local_variables[
|
815
|
+
capture_var] = capture_value
|
745
816
|
self.test_context.set(capture_var, capture_value)
|
746
817
|
|
747
818
|
# 将主要结果赋值给指定变量
|
@@ -760,7 +831,8 @@ class DSLExecutor:
|
|
760
831
|
)
|
761
832
|
else:
|
762
833
|
# 存储在本地变量字典和测试上下文中
|
763
|
-
self.variable_replacer.local_variables[
|
834
|
+
self.variable_replacer.local_variables[
|
835
|
+
var_name] = actual_result
|
764
836
|
self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
|
765
837
|
allure.attach(
|
766
838
|
f"变量: {var_name}\n值: {actual_result}",
|
@@ -768,7 +840,7 @@ class DSLExecutor:
|
|
768
840
|
attachment_type=allure.attachment_type.TEXT
|
769
841
|
)
|
770
842
|
else:
|
771
|
-
raise Exception(
|
843
|
+
raise Exception("远程关键字没有返回结果")
|
772
844
|
|
773
845
|
def execute(self, node):
|
774
846
|
"""执行AST节点"""
|
@@ -786,7 +858,8 @@ class DSLExecutor:
|
|
786
858
|
'CustomKeyword': lambda _: None, # 添加对CustomKeyword节点的处理,只需注册不需执行
|
787
859
|
'RemoteImport': self._handle_remote_import,
|
788
860
|
'RemoteKeywordCall': self._execute_remote_keyword_call,
|
789
|
-
'AssignmentRemoteKeywordCall':
|
861
|
+
'AssignmentRemoteKeywordCall': (
|
862
|
+
self._handle_assignment_remote_keyword_call),
|
790
863
|
'Break': self._handle_break,
|
791
864
|
'Continue': self._handle_continue
|
792
865
|
}
|
@@ -796,6 +869,122 @@ class DSLExecutor:
|
|
796
869
|
return handler(node)
|
797
870
|
raise Exception(f"未知的节点类型: {node.type}")
|
798
871
|
|
872
|
+
def __repr__(self):
|
873
|
+
"""返回DSL执行器的字符串表示"""
|
874
|
+
return (f"DSLExecutor(variables={len(self.variables)}, "
|
875
|
+
f"hooks_enabled={self.enable_hooks})")
|
876
|
+
|
877
|
+
def _init_hooks(self):
|
878
|
+
"""初始化hook机制"""
|
879
|
+
try:
|
880
|
+
from .hook_manager import hook_manager
|
881
|
+
hook_manager.initialize()
|
882
|
+
# 调用hook注册自定义关键字
|
883
|
+
hook_manager.pm.hook.dsl_register_custom_keywords()
|
884
|
+
self.hook_manager = hook_manager
|
885
|
+
except ImportError:
|
886
|
+
# 如果没有安装pluggy,禁用hook
|
887
|
+
self.enable_hooks = False
|
888
|
+
self.hook_manager = None
|
889
|
+
|
890
|
+
def execute_from_content(self, content: str, dsl_id: str = None,
|
891
|
+
context: Dict[str, Any] = None) -> Any:
|
892
|
+
"""从内容执行DSL,支持hook扩展
|
893
|
+
|
894
|
+
Args:
|
895
|
+
content: DSL内容,如果为空字符串将尝试通过hook加载
|
896
|
+
dsl_id: DSL标识符(可选)
|
897
|
+
context: 执行上下文(可选)
|
898
|
+
|
899
|
+
Returns:
|
900
|
+
执行结果
|
901
|
+
"""
|
902
|
+
self.current_dsl_id = dsl_id
|
903
|
+
|
904
|
+
# 如果content为空且有dsl_id,尝试通过hook加载内容
|
905
|
+
if (not content and dsl_id and self.enable_hooks and
|
906
|
+
hasattr(self, 'hook_manager') and self.hook_manager):
|
907
|
+
content_results = self.hook_manager.pm.hook.dsl_load_content(
|
908
|
+
dsl_id=dsl_id)
|
909
|
+
for result in content_results:
|
910
|
+
if result is not None:
|
911
|
+
content = result
|
912
|
+
break
|
913
|
+
|
914
|
+
if not content:
|
915
|
+
raise ValueError(f"无法获取DSL内容: {dsl_id}")
|
916
|
+
|
917
|
+
# 应用执行上下文
|
918
|
+
if context:
|
919
|
+
self.variables.update(context)
|
920
|
+
for key, value in context.items():
|
921
|
+
self.test_context.set(key, value)
|
922
|
+
self.variable_replacer = VariableReplacer(
|
923
|
+
self.variables, self.test_context
|
924
|
+
)
|
925
|
+
|
926
|
+
# 执行前hook
|
927
|
+
if self.enable_hooks and self.hook_manager:
|
928
|
+
self.hook_manager.pm.hook.dsl_before_execution(
|
929
|
+
dsl_id=dsl_id, context=context or {}
|
930
|
+
)
|
931
|
+
|
932
|
+
result = None
|
933
|
+
exception = None
|
934
|
+
|
935
|
+
try:
|
936
|
+
# 解析并执行
|
937
|
+
ast = self._parse_dsl_content(content)
|
938
|
+
result = self.execute(ast)
|
939
|
+
|
940
|
+
except Exception as e:
|
941
|
+
exception = e
|
942
|
+
# 执行后hook(在异常情况下)
|
943
|
+
if self.enable_hooks and self.hook_manager:
|
944
|
+
try:
|
945
|
+
self.hook_manager.pm.hook.dsl_after_execution(
|
946
|
+
dsl_id=dsl_id,
|
947
|
+
context=context or {},
|
948
|
+
result=result,
|
949
|
+
exception=exception
|
950
|
+
)
|
951
|
+
except Exception as hook_error:
|
952
|
+
print(f"Hook执行失败: {hook_error}")
|
953
|
+
raise
|
954
|
+
else:
|
955
|
+
# 执行后hook(在成功情况下)
|
956
|
+
if self.enable_hooks and self.hook_manager:
|
957
|
+
try:
|
958
|
+
self.hook_manager.pm.hook.dsl_after_execution(
|
959
|
+
dsl_id=dsl_id,
|
960
|
+
context=context or {},
|
961
|
+
result=result,
|
962
|
+
exception=None
|
963
|
+
)
|
964
|
+
except Exception as hook_error:
|
965
|
+
print(f"Hook执行失败: {hook_error}")
|
966
|
+
|
967
|
+
return result
|
968
|
+
|
969
|
+
def _parse_dsl_content(self, content: str) -> Node:
|
970
|
+
"""解析DSL内容为AST(公共方法)
|
971
|
+
|
972
|
+
Args:
|
973
|
+
content: DSL文本内容
|
974
|
+
|
975
|
+
Returns:
|
976
|
+
Node: 解析后的AST根节点
|
977
|
+
|
978
|
+
Raises:
|
979
|
+
Exception: 解析失败时抛出异常
|
980
|
+
"""
|
981
|
+
from pytest_dsl.core.lexer import get_lexer
|
982
|
+
from pytest_dsl.core.parser import get_parser
|
983
|
+
|
984
|
+
lexer = get_lexer()
|
985
|
+
parser = get_parser()
|
986
|
+
return parser.parse(content, lexer=lexer)
|
987
|
+
|
799
988
|
|
800
989
|
def read_file(filename):
|
801
990
|
"""读取 DSL 文件内容"""
|