pytest-dsl 0.1.1__tar.gz → 0.2.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.
Files changed (88) hide show
  1. {pytest_dsl-0.1.1/pytest_dsl.egg-info → pytest_dsl-0.2.0}/PKG-INFO +1 -1
  2. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pyproject.toml +1 -1
  3. pytest_dsl-0.2.0/pytest_dsl/core/__init__.py +7 -0
  4. pytest_dsl-0.2.0/pytest_dsl/core/custom_keyword_manager.py +213 -0
  5. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/dsl_executor.py +39 -2
  6. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/http_request.py +163 -54
  7. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/lexer.py +14 -1
  8. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/parser.py +45 -2
  9. pytest_dsl-0.2.0/pytest_dsl/core/parsetab.py +88 -0
  10. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/variable_utils.py +1 -1
  11. pytest_dsl-0.2.0/pytest_dsl/examples/custom/test_advanced_keywords.auto +31 -0
  12. pytest_dsl-0.2.0/pytest_dsl/examples/custom/test_custom_keywords.auto +37 -0
  13. pytest_dsl-0.2.0/pytest_dsl/examples/custom/test_default_values.auto +34 -0
  14. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/http_retry_assertions.auto +2 -2
  15. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +2 -2
  16. pytest_dsl-0.2.0/pytest_dsl/examples/test_custom_keyword.py +9 -0
  17. pytest_dsl-0.2.0/pytest_dsl/examples/test_http.py +29 -0
  18. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/keywords/http_keywords.py +290 -102
  19. pytest_dsl-0.2.0/pytest_dsl/parsetab.py +69 -0
  20. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0/pytest_dsl.egg-info}/PKG-INFO +1 -1
  21. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl.egg-info/SOURCES.txt +7 -11
  22. pytest_dsl-0.1.1/pytest_dsl/core/__init__.py +0 -0
  23. pytest_dsl-0.1.1/pytest_dsl/core/custom_auth_example.py +0 -425
  24. pytest_dsl-0.1.1/pytest_dsl/core/parsetab.py +0 -76
  25. pytest_dsl-0.1.1/pytest_dsl/examples/csrf_auth_provider.py +0 -232
  26. pytest_dsl-0.1.1/pytest_dsl/examples/http/csrf_auth_test.auto +0 -64
  27. pytest_dsl-0.1.1/pytest_dsl/examples/http/custom_auth_test.auto +0 -76
  28. pytest_dsl-0.1.1/pytest_dsl/examples/http_clients.yaml +0 -48
  29. pytest_dsl-0.1.1/pytest_dsl/examples/keyword_example.py +0 -70
  30. pytest_dsl-0.1.1/pytest_dsl/examples/test_http.py +0 -168
  31. pytest_dsl-0.1.1/tests/conftest.py +0 -23
  32. pytest_dsl-0.1.1/tests/test_advanced_list.auto +0 -24
  33. pytest_dsl-0.1.1/tests/test_core/test_dsl.py +0 -30
  34. pytest_dsl-0.1.1/tests/test_list_support.auto +0 -20
  35. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/LICENSE +0 -0
  36. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/MANIFEST.in +0 -0
  37. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/README.md +0 -0
  38. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/__init__.py +0 -0
  39. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/cli.py +0 -0
  40. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/conftest_adapter.py +0 -0
  41. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/auth_provider.py +0 -0
  42. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/auto_decorator.py +0 -0
  43. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/auto_directory.py +0 -0
  44. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/context.py +0 -0
  45. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/dsl_executor_utils.py +0 -0
  46. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/global_context.py +0 -0
  47. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/http_client.py +0 -0
  48. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/keyword_manager.py +0 -0
  49. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/plugin_discovery.py +0 -0
  50. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/utils.py +0 -0
  51. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/yaml_loader.py +0 -0
  52. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/core/yaml_vars.py +0 -0
  53. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/docs/custom_keywords.md +0 -0
  54. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/__init__.py +0 -0
  55. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/assert/assertion_example.auto +0 -0
  56. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/assert/boolean_test.auto +0 -0
  57. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/assert/expression_test.auto +0 -0
  58. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/__init__.py +0 -0
  59. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/builtin_auth_test.auto +0 -0
  60. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/file_reference_test.auto +0 -0
  61. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/http_advanced.auto +0 -0
  62. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/http_example.auto +0 -0
  63. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/http_length_test.auto +0 -0
  64. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/http_with_yaml.auto +0 -0
  65. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/new_retry_test.auto +0 -0
  66. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/retry_assertions_only.auto +0 -0
  67. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/retry_config_only.auto +0 -0
  68. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/retry_debug.auto +0 -0
  69. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/retry_with_fix.auto +0 -0
  70. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/simple_retry.auto +0 -0
  71. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/http/vars.yaml +0 -0
  72. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/quickstart/api_basics.auto +0 -0
  73. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/quickstart/assertions.auto +0 -0
  74. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/quickstart/loops.auto +0 -0
  75. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/test_assert.py +0 -0
  76. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/examples/test_quickstart.py +0 -0
  77. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/keywords/__init__.py +0 -0
  78. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/keywords/assertion_keywords.py +0 -0
  79. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/keywords/global_keywords.py +0 -0
  80. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/keywords/system_keywords.py +0 -0
  81. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/main_adapter.py +0 -0
  82. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl/plugin.py +0 -0
  83. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl.egg-info/dependency_links.txt +0 -0
  84. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl.egg-info/entry_points.txt +0 -0
  85. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl.egg-info/requires.txt +0 -0
  86. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/pytest_dsl.egg-info/top_level.txt +0 -0
  87. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/setup.cfg +0 -0
  88. {pytest_dsl-0.1.1 → pytest_dsl-0.2.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-dsl
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: A DSL testing framework based on pytest
5
5
  Author: Chen Shuanglin
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytest-dsl"
7
- version = "0.1.1"
7
+ version = "0.2.0"
8
8
  description = "A DSL testing framework based on pytest"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -0,0 +1,7 @@
1
+ # 导入关键模块确保它们被初始化
2
+ from pytest_dsl.core.keyword_manager import keyword_manager
3
+ from pytest_dsl.core.global_context import global_context
4
+ from pytest_dsl.core.yaml_vars import yaml_vars
5
+
6
+ # 导入自定义关键字管理器
7
+ from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
@@ -0,0 +1,213 @@
1
+ from typing import Dict, Any, List, Optional
2
+ import os
3
+ import pathlib
4
+ from pytest_dsl.core.lexer import get_lexer
5
+ from pytest_dsl.core.parser import get_parser, Node
6
+ from pytest_dsl.core.dsl_executor import DSLExecutor
7
+ from pytest_dsl.core.keyword_manager import keyword_manager
8
+ from pytest_dsl.core.context import TestContext
9
+
10
+
11
+ class CustomKeywordManager:
12
+ """自定义关键字管理器
13
+
14
+ 负责加载和注册自定义关键字
15
+ """
16
+
17
+ def __init__(self):
18
+ """初始化自定义关键字管理器"""
19
+ self.resource_cache = {} # 缓存已加载的资源文件
20
+ self.resource_paths = [] # 资源文件搜索路径
21
+
22
+ def add_resource_path(self, path: str) -> None:
23
+ """添加资源文件搜索路径
24
+
25
+ Args:
26
+ path: 资源文件路径
27
+ """
28
+ if path not in self.resource_paths:
29
+ self.resource_paths.append(path)
30
+
31
+ def load_resource_file(self, file_path: str) -> None:
32
+ """加载资源文件
33
+
34
+ Args:
35
+ file_path: 资源文件路径
36
+ """
37
+ # 规范化路径,解决路径叠加的问题
38
+ file_path = os.path.normpath(file_path)
39
+
40
+ # 如果已经缓存,则跳过
41
+ absolute_path = os.path.abspath(file_path)
42
+ if absolute_path in self.resource_cache:
43
+ return
44
+
45
+ # 读取文件内容
46
+ if not os.path.exists(file_path):
47
+ # 尝试在资源路径中查找
48
+ for resource_path in self.resource_paths:
49
+ full_path = os.path.join(resource_path, file_path)
50
+ if os.path.exists(full_path):
51
+ file_path = full_path
52
+ absolute_path = os.path.abspath(file_path)
53
+ break
54
+ else:
55
+ # 如果文件不存在,尝试在根项目目录中查找
56
+ # 一般情况下文件路径可能是相对于项目根目录的
57
+ project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
58
+ full_path = os.path.join(project_root, file_path)
59
+ if os.path.exists(full_path):
60
+ file_path = full_path
61
+ absolute_path = os.path.abspath(file_path)
62
+ else:
63
+ raise FileNotFoundError(f"资源文件不存在: {file_path}")
64
+
65
+ try:
66
+ with open(file_path, 'r', encoding='utf-8') as f:
67
+ content = f.read()
68
+
69
+ # 解析资源文件
70
+ lexer = get_lexer()
71
+ parser = get_parser()
72
+ ast = parser.parse(content, lexer=lexer)
73
+
74
+ # 标记为已加载
75
+ self.resource_cache[absolute_path] = True
76
+
77
+ # 处理导入指令
78
+ self._process_imports(ast, os.path.dirname(file_path))
79
+
80
+ # 注册关键字
81
+ self._register_keywords(ast, file_path)
82
+ except Exception as e:
83
+ print(f"资源文件 {file_path} 加载失败: {str(e)}")
84
+ raise
85
+
86
+ def _process_imports(self, ast: Node, base_dir: str) -> None:
87
+ """处理资源文件中的导入指令
88
+
89
+ Args:
90
+ ast: 抽象语法树
91
+ base_dir: 基础目录
92
+ """
93
+ if ast.type != 'Start' or not ast.children:
94
+ return
95
+
96
+ metadata_node = ast.children[0]
97
+ if metadata_node.type != 'Metadata':
98
+ return
99
+
100
+ for item in metadata_node.children:
101
+ if item.type == '@import':
102
+ imported_file = item.value
103
+ # 处理相对路径
104
+ if not os.path.isabs(imported_file):
105
+ imported_file = os.path.join(base_dir, imported_file)
106
+
107
+ # 规范化路径,避免路径叠加问题
108
+ imported_file = os.path.normpath(imported_file)
109
+
110
+ # 递归加载导入的资源文件
111
+ self.load_resource_file(imported_file)
112
+
113
+ def _register_keywords(self, ast: Node, file_path: str) -> None:
114
+ """从AST中注册关键字
115
+
116
+ Args:
117
+ ast: 抽象语法树
118
+ file_path: 文件路径
119
+ """
120
+ if ast.type != 'Start' or len(ast.children) < 2:
121
+ return
122
+
123
+ # 遍历语句节点
124
+ statements_node = ast.children[1]
125
+ if statements_node.type != 'Statements':
126
+ return
127
+
128
+ for node in statements_node.children:
129
+ if node.type == 'CustomKeyword':
130
+ self._register_custom_keyword(node, file_path)
131
+
132
+ def _register_custom_keyword(self, node: Node, file_path: str) -> None:
133
+ """注册自定义关键字
134
+
135
+ Args:
136
+ node: 关键字节点
137
+ file_path: 资源文件路径
138
+ """
139
+ # 提取关键字信息
140
+ keyword_name = node.value
141
+ params_node = node.children[0]
142
+ body_node = node.children[1]
143
+
144
+ # 构建参数列表
145
+ parameters = []
146
+ param_mapping = {}
147
+ param_defaults = {} # 存储参数默认值
148
+
149
+ for param in params_node if params_node else []:
150
+ param_name = param.value
151
+ param_default = None
152
+
153
+ # 检查是否有默认值
154
+ if param.children and param.children[0]:
155
+ param_default = param.children[0].value
156
+ param_defaults[param_name] = param_default # 保存默认值
157
+
158
+ # 添加参数定义
159
+ parameters.append({
160
+ 'name': param_name,
161
+ 'mapping': param_name, # 中文参数名和内部参数名相同
162
+ 'description': f'自定义关键字参数 {param_name}'
163
+ })
164
+
165
+ param_mapping[param_name] = param_name
166
+
167
+ # 注册自定义关键字到关键字管理器
168
+ @keyword_manager.register(keyword_name, parameters)
169
+ def custom_keyword_executor(**kwargs):
170
+ """自定义关键字执行器"""
171
+ # 创建一个新的DSL执行器
172
+ executor = DSLExecutor()
173
+
174
+ # 获取传递的上下文
175
+ context = kwargs.get('context')
176
+ if context:
177
+ executor.test_context = context
178
+
179
+ # 先应用默认值
180
+ for param_name, default_value in param_defaults.items():
181
+ executor.variables[param_name] = default_value
182
+ executor.test_context.set(param_name, default_value)
183
+
184
+ # 然后应用传入的参数值(覆盖默认值)
185
+ for param_name, param_mapping_name in param_mapping.items():
186
+ if param_mapping_name in kwargs:
187
+ # 确保参数值在标准变量和测试上下文中都可用
188
+ executor.variables[param_name] = kwargs[param_mapping_name]
189
+ executor.test_context.set(param_name, kwargs[param_mapping_name])
190
+
191
+ # 执行关键字体中的语句
192
+ result = None
193
+ try:
194
+ for stmt in body_node.children:
195
+ # 检查是否是return语句
196
+ if stmt.type == 'Return':
197
+ # 对表达式求值
198
+ result = executor.eval_expression(stmt.children[0])
199
+ break
200
+ else:
201
+ # 执行普通语句
202
+ executor.execute(stmt)
203
+ except Exception as e:
204
+ print(f"执行自定义关键字 {keyword_name} 时发生错误: {str(e)}")
205
+ raise
206
+
207
+ return result
208
+
209
+ print(f"已注册自定义关键字: {keyword_name} 来自文件: {file_path}")
210
+
211
+
212
+ # 创建全局自定义关键字管理器实例
213
+ custom_keyword_manager = CustomKeywordManager()
@@ -26,6 +26,7 @@ class DSLExecutor:
26
26
  self.test_context = TestContext()
27
27
  self.test_context.executor = self # 让 test_context 能够访问到 executor
28
28
  self.variable_replacer = VariableReplacer(self.variables, self.test_context)
29
+ self.imported_files = set() # 跟踪已导入的文件,避免循环导入
29
30
 
30
31
  def set_current_data(self, data):
31
32
  """设置当前测试数据集"""
@@ -101,7 +102,7 @@ class DSLExecutor:
101
102
  return self.eval_expression(value)
102
103
  elif isinstance(value, str):
103
104
  # 定义变量引用模式
104
- pattern = r'\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
105
+ pattern = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)\}'
105
106
  # 检查整个字符串是否完全匹配单一变量引用模式
106
107
  match = re.fullmatch(pattern, value)
107
108
  if match:
@@ -133,6 +134,9 @@ class DSLExecutor:
133
134
  if child.type == 'Metadata':
134
135
  for item in child.children:
135
136
  metadata[item.type] = item.value
137
+ # 处理导入指令
138
+ if item.type == '@import':
139
+ self._handle_import(item.value)
136
140
  elif child.type == 'Teardown':
137
141
  teardown_node = child
138
142
 
@@ -154,6 +158,25 @@ class DSLExecutor:
154
158
  # 测试用例执行完成后清空上下文
155
159
  self.test_context.clear()
156
160
 
161
+ def _handle_import(self, file_path):
162
+ """处理导入指令
163
+
164
+ Args:
165
+ file_path: 资源文件路径
166
+ """
167
+ # 防止循环导入
168
+ if file_path in self.imported_files:
169
+ return
170
+
171
+ try:
172
+ # 导入自定义关键字文件
173
+ from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
174
+ custom_keyword_manager.load_resource_file(file_path)
175
+ self.imported_files.add(file_path)
176
+ except Exception as e:
177
+ print(f"导入资源文件失败: {file_path}, 错误: {str(e)}")
178
+ raise
179
+
157
180
  def _execute_test_iteration(self, metadata, node, teardown_node):
158
181
  """执行测试迭代"""
159
182
  try:
@@ -305,6 +328,19 @@ class DSLExecutor:
305
328
  """处理清理操作"""
306
329
  self.execute(node.children[0])
307
330
 
331
+ @allure.step("执行返回语句")
332
+ def _handle_return(self, node):
333
+ """处理return语句
334
+
335
+ Args:
336
+ node: Return节点
337
+
338
+ Returns:
339
+ 表达式求值结果
340
+ """
341
+ expr_node = node.children[0]
342
+ return self.eval_expression(expr_node)
343
+
308
344
  def execute(self, node):
309
345
  """执行AST节点"""
310
346
  handlers = {
@@ -315,7 +351,8 @@ class DSLExecutor:
315
351
  'AssignmentKeywordCall': self._handle_assignment_keyword_call,
316
352
  'ForLoop': self._handle_for_loop,
317
353
  'KeywordCall': self._execute_keyword_call,
318
- 'Teardown': self._handle_teardown
354
+ 'Teardown': self._handle_teardown,
355
+ 'Return': self._handle_return
319
356
  }
320
357
 
321
358
  handler = handlers.get(node.type)
@@ -13,7 +13,7 @@ from pytest_dsl.core.http_client import http_client_manager
13
13
 
14
14
  # 定义支持的比较操作符
15
15
  COMPARISON_OPERATORS = {
16
- "eq", "neq", "lt", "lte", "gt", "gte", "in", "not_in"
16
+ "eq", "neq", "lt", "lte", "gt", "gte", "in", "not_in", "matches"
17
17
  }
18
18
 
19
19
  # 定义支持的断言类型
@@ -397,14 +397,14 @@ class HTTPRequest:
397
397
  if extractor_type in ["jsonpath", "body", "header"]:
398
398
  # 当提取器是jsonpath/body/header时,将contains当作断言类型处理
399
399
  assertion_type = assertion[1]
400
- compare_operator = "eq" # 默认操作符
400
+ compare_operator = assertion[1] # 匹配操作符和断言类型
401
401
  else:
402
402
  # 否则当作操作符处理
403
403
  assertion_type = "value"
404
404
  compare_operator = assertion[1]
405
405
  else:
406
406
  assertion_type = "value" # 标记为值比较
407
- compare_operator = assertion[1] # 比较操作符
407
+ compare_operator = assertion[1]
408
408
 
409
409
  expected_value = assertion[2] # 预期值
410
410
  else:
@@ -421,12 +421,12 @@ class HTTPRequest:
421
421
  # 特殊处理类型断言,它确实需要一个值但在这种格式中没有提供
422
422
  if assertion_type == "type":
423
423
  raise ValueError(f"断言类型 'type' 需要预期值(string/number/boolean/array/object/null),但未提供: {assertion}")
424
- # 如果是不需要值的断言类型,则允许这种格式
424
+ # 断言类型作为操作符
425
425
  expected_value = None
426
- compare_operator = "eq" # 默认比较操作符
426
+ compare_operator = assertion_type # 使用断言类型作为操作符
427
427
  else:
428
428
  expected_value = None
429
- compare_operator = "eq" # 默认比较操作符
429
+ compare_operator = assertion_type # 存在性断言的操作符就是断言类型本身
430
430
  elif len(assertion) == 4: # 带操作符的断言 ["jsonpath", "$.id", "eq", 1] 或特殊断言 ["jsonpath", "$.type", "type", "string"]
431
431
  extraction_path = assertion[1]
432
432
 
@@ -449,11 +449,11 @@ class HTTPRequest:
449
449
  # 对于长度断言,总是需要有比较操作符和期望值
450
450
  if len(assertion) < 4:
451
451
  raise ValueError(f"长度断言需要提供预期值: {assertion}")
452
- compare_operator = "eq" # 在这种4元素格式中,使用默认比较操作符eq
452
+ compare_operator = "eq" # 默认使用相等比较
453
453
  expected_value = assertion[3] # 预期长度值
454
454
  else:
455
455
  # 对于其他特殊断言,使用第四个元素作为期望值
456
- compare_operator = "eq" # 默认比较操作符
456
+ compare_operator = assertion_type # 使用断言类型作为操作符
457
457
  expected_value = assertion[3]
458
458
  else: # 5个参数,例如 ["jsonpath", "$", "length", "eq", 10]
459
459
  extraction_path = assertion[1]
@@ -647,15 +647,34 @@ class HTTPRequest:
647
647
 
648
648
  # 执行完所有断言后,如果有失败的断言,抛出异常
649
649
  if failed_assertions:
650
- if len(failed_assertions) == 1:
651
- # 只有一个断言失败时,直接使用该断言的错误消息
652
- raise AssertionError(failed_assertions[0][1])
653
- else:
654
- # 多个断言失败时,汇总所有错误
655
- error_summary = f"多个断言失败 ({len(failed_assertions)}/{len(process_asserts)}):\n"
656
- for idx, (assertion_idx, error_msg) in enumerate(failed_assertions, 1):
657
- error_summary += f"\n{idx}. 断言 #{assertion_idx+1}: {error_msg.split('断言失败')[0].strip()}"
658
- raise AssertionError(error_summary)
650
+ # 检查是否只收集失败而不抛出异常(由重试机制设置)
651
+ collect_only = self.config.get('_collect_failed_assertions_only', False)
652
+
653
+ if not collect_only:
654
+ if len(failed_assertions) == 1:
655
+ # 只有一个断言失败时,直接使用该断言的错误消息
656
+ raise AssertionError(failed_assertions[0][1])
657
+ else:
658
+ # 多个断言失败时,汇总所有错误
659
+ error_summary = f"多个断言失败 ({len(failed_assertions)}/{len(process_asserts)}):\n"
660
+ for idx, (assertion_idx, error_msg) in enumerate(failed_assertions, 1):
661
+ # 从错误消息中提取关键部分
662
+ if "[" in error_msg and "]" in error_msg:
663
+ # 尝试提取提取器类型
664
+ extractor_type = error_msg.split("[", 1)[1].split("]")[0] if "[" in error_msg else "未知"
665
+ else:
666
+ extractor_type = "未知"
667
+
668
+ # 生成简短的断言标题
669
+ assertion_title = f"断言 #{assertion_idx+1} [{extractor_type}]"
670
+
671
+ # 添加分隔线使错误更容易辨别
672
+ error_summary += f"\n{'-' * 30}\n{idx}. {assertion_title}:\n{'-' * 30}\n{error_msg}"
673
+
674
+ # 添加底部分隔线
675
+ error_summary += f"\n{'-' * 50}"
676
+
677
+ raise AssertionError(error_summary)
659
678
 
660
679
  # 返回断言结果和需要重试的断言
661
680
  return assert_results, failed_retryable_assertions
@@ -716,15 +735,24 @@ class HTTPRequest:
716
735
  elif extractor_type == "status":
717
736
  return self.response.status_code
718
737
  elif extractor_type == "body":
719
- return self.response.text
738
+ if isinstance(self.response.text, str):
739
+ return self.response.text
740
+ return str(self.response.text)
720
741
  elif extractor_type == "response_time":
721
742
  return self.response.elapsed.total_seconds() * 1000
722
743
  else:
723
744
  raise ValueError(f"不支持的提取器类型: {extractor_type}")
724
745
  except Exception as e:
746
+ # 记录提取错误
747
+ error_message = f"提取值失败({extractor_type}, {extraction_path}): {type(e).__name__}: {str(e)}"
748
+ allure.attach(
749
+ error_message,
750
+ name=f"提取错误: {extractor_type}",
751
+ attachment_type=allure.attachment_type.TEXT
752
+ )
725
753
  if default_value is not None:
726
754
  return default_value
727
- raise ValueError(f"提取值失败({extractor_type}, {extraction_path}): {str(e)}")
755
+ raise ValueError(error_message)
728
756
 
729
757
  def _extract_jsonpath(self, path: str, default_value: Any = None) -> Any:
730
758
  """使用JSONPath从JSON响应提取值
@@ -907,13 +935,87 @@ class HTTPRequest:
907
935
  attachment_type=allure.attachment_type.TEXT
908
936
  )
909
937
 
938
+ # 记录断言参数
939
+ allure.attach(
940
+ f"断言类型: {assertion_type}\n"
941
+ f"比较操作符: {operator}\n"
942
+ f"实际值: {actual_value} ({type(actual_value).__name__})\n"
943
+ f"期望值: {expected_value} ({type(expected_value).__name__ if expected_value is not None else 'None'})",
944
+ name="断言参数",
945
+ attachment_type=allure.attachment_type.TEXT
946
+ )
947
+
910
948
  # 基于断言类型执行断言
911
949
  if assertion_type == "value":
912
950
  # 直接使用操作符进行比较
913
951
  return self._compare_values(actual_value, expected_value, operator)
952
+ # 特殊断言类型 - 这些类型的操作符与断言类型匹配
953
+ elif assertion_type in ["contains", "not_contains", "startswith", "endswith", "matches", "schema"]:
954
+ # 直接使用断言类型作为方法处理,实现特殊断言逻辑
955
+ if assertion_type == "contains":
956
+ # contains断言总是检查actual是否包含expected
957
+ if isinstance(actual_value, str):
958
+ return str(expected_value) in actual_value
959
+ elif isinstance(actual_value, (list, tuple, dict)):
960
+ return expected_value in actual_value
961
+ return False
962
+ elif assertion_type == "not_contains":
963
+ # not_contains断言总是检查actual是否不包含expected
964
+ if isinstance(actual_value, str):
965
+ return str(expected_value) not in actual_value
966
+ elif isinstance(actual_value, (list, tuple, dict)):
967
+ return expected_value not in actual_value
968
+ return True
969
+ elif assertion_type == "startswith":
970
+ return isinstance(actual_value, str) and actual_value.startswith(str(expected_value))
971
+ elif assertion_type == "endswith":
972
+ return isinstance(actual_value, str) and actual_value.endswith(str(expected_value))
973
+ elif assertion_type == "matches":
974
+ if not isinstance(actual_value, str):
975
+ actual_value = str(actual_value) if actual_value is not None else ""
976
+ try:
977
+ import re
978
+ pattern = str(expected_value)
979
+ match_result = bool(re.search(pattern, actual_value))
980
+ # 记录匹配结果
981
+ allure.attach(
982
+ f"正则表达式匹配结果: {'成功' if match_result else '失败'}\n"
983
+ f"模式: {pattern}\n"
984
+ f"目标字符串: {actual_value}",
985
+ name="正则表达式匹配",
986
+ attachment_type=allure.attachment_type.TEXT
987
+ )
988
+ return match_result
989
+ except Exception as e:
990
+ # 记录正则表达式匹配错误
991
+ allure.attach(
992
+ f"正则表达式匹配失败: {type(e).__name__}: {str(e)}\n"
993
+ f"模式: {expected_value}\n"
994
+ f"目标字符串: {actual_value}",
995
+ name="正则表达式错误",
996
+ attachment_type=allure.attachment_type.TEXT
997
+ )
998
+ return False
999
+ elif assertion_type == "schema":
1000
+ try:
1001
+ from jsonschema import validate
1002
+ validate(instance=actual_value, schema=expected_value)
1003
+ return True
1004
+ except Exception as e:
1005
+ # 记录JSON Schema验证错误
1006
+ allure.attach(
1007
+ f"JSON Schema验证失败: {type(e).__name__}: {str(e)}\n"
1008
+ f"Schema: {expected_value}\n"
1009
+ f"实例: {actual_value}",
1010
+ name="Schema验证错误",
1011
+ attachment_type=allure.attachment_type.TEXT
1012
+ )
1013
+ return False
914
1014
  elif assertion_type == "length":
915
1015
  # 长度比较
916
- return self._compare_values(actual_value, expected_value, operator)
1016
+ # 使用实际的比较操作符进行比较,默认使用eq
1017
+ effective_operator = "eq" if operator == "length" else operator
1018
+ return self._compare_values(actual_value, expected_value, effective_operator)
917
1019
  elif assertion_type == "exists":
918
1020
  return actual_value is not None
919
1021
  elif assertion_type == "not_exists":
@@ -932,40 +1034,6 @@ class HTTPRequest:
932
1034
  elif expected_value == "null":
933
1035
  return actual_value is None
934
1036
  return False
935
- elif assertion_type == "contains":
936
- # contains断言总是检查actual是否包含expected
937
- if isinstance(actual_value, str):
938
- return str(expected_value) in actual_value
939
- elif isinstance(actual_value, (list, tuple, dict)):
940
- return expected_value in actual_value
941
- return False
942
- elif assertion_type == "not_contains":
943
- # not_contains断言总是检查actual是否不包含expected
944
- if isinstance(actual_value, str):
945
- return str(expected_value) not in actual_value
946
- elif isinstance(actual_value, (list, tuple, dict)):
947
- return expected_value not in actual_value
948
- return True
949
- elif assertion_type == "startswith":
950
- return isinstance(actual_value, str) and actual_value.startswith(str(expected_value))
951
- elif assertion_type == "endswith":
952
- return isinstance(actual_value, str) and actual_value.endswith(str(expected_value))
953
- elif assertion_type == "matches":
954
- if not isinstance(actual_value, str):
955
- return False
956
- try:
957
- import re
958
- pattern = str(expected_value)
959
- return bool(re.search(pattern, actual_value))
960
- except:
961
- return False
962
- elif assertion_type == "schema":
963
- try:
964
- from jsonschema import validate
965
- validate(instance=actual_value, schema=expected_value)
966
- return True
967
- except:
968
- return False
969
1037
  else:
970
1038
  raise ValueError(f"不支持的断言类型: {assertion_type}")
971
1039
 
@@ -998,6 +1066,47 @@ class HTTPRequest:
998
1066
  elif operator == "not_in":
999
1067
  # not_in操作符检查actual是否不在expected列表中
1000
1068
  return actual_value not in expected_value
1069
+ elif operator == "contains":
1070
+ # contains操作符检查actual是否包含expected
1071
+ if isinstance(actual_value, str):
1072
+ return str(expected_value) in actual_value
1073
+ elif isinstance(actual_value, (list, tuple, dict)):
1074
+ return expected_value in actual_value
1075
+ return False
1076
+ elif operator == "not_contains":
1077
+ # not_contains操作符检查actual是否不包含expected
1078
+ if isinstance(actual_value, str):
1079
+ return str(expected_value) not in actual_value
1080
+ elif isinstance(actual_value, (list, tuple, dict)):
1081
+ return expected_value not in actual_value
1082
+ return True
1083
+ elif operator == "matches":
1084
+ # matches操作符使用正则表达式进行匹配
1085
+ if not isinstance(actual_value, str):
1086
+ actual_value = str(actual_value) if actual_value is not None else ""
1087
+ try:
1088
+ import re
1089
+ pattern = str(expected_value)
1090
+ match_result = bool(re.search(pattern, actual_value))
1091
+ # 记录匹配结果
1092
+ allure.attach(
1093
+ f"正则表达式匹配结果: {'成功' if match_result else '失败'}\n"
1094
+ f"模式: {pattern}\n"
1095
+ f"目标字符串: {actual_value}",
1096
+ name="正则表达式匹配",
1097
+ attachment_type=allure.attachment_type.TEXT
1098
+ )
1099
+ return match_result
1100
+ except Exception as e:
1101
+ # 记录正则表达式匹配错误
1102
+ allure.attach(
1103
+ f"正则表达式匹配失败: {type(e).__name__}: {str(e)}\n"
1104
+ f"模式: {expected_value}\n"
1105
+ f"目标字符串: {actual_value}",
1106
+ name="正则表达式错误",
1107
+ attachment_type=allure.attachment_type.TEXT
1108
+ )
1109
+ return False
1001
1110
  else:
1002
1111
  raise ValueError(f"不支持的比较操作符: {operator}")
1003
1112
 
@@ -9,7 +9,8 @@ reserved = {
9
9
  'range': 'RANGE',
10
10
  'using': 'USING', # Add new keyword for data-driven testing
11
11
  'True': 'TRUE', # 添加布尔值支持
12
- 'False': 'FALSE' # 添加布尔值支持
12
+ 'False': 'FALSE', # 添加布尔值支持
13
+ 'return': 'RETURN' # 添加return关键字支持
13
14
  }
14
15
 
15
16
  # token 名称列表
@@ -33,6 +34,8 @@ tokens = [
33
34
  'DATE_KEYWORD',
34
35
  'TEARDOWN_KEYWORD',
35
36
  'DATA_KEYWORD', # Add new token for @data keyword
37
+ 'KEYWORD_KEYWORD', # 添加@keyword关键字
38
+ 'IMPORT_KEYWORD', # 添加@import关键字
36
39
  ] + list(reserved.values())
37
40
 
38
41
  # 正则表达式定义 token
@@ -106,6 +109,16 @@ def t_DATA_KEYWORD(t):
106
109
  return t
107
110
 
108
111
 
112
+ def t_KEYWORD_KEYWORD(t):
113
+ r'@keyword'
114
+ return t
115
+
116
+
117
+ def t_IMPORT_KEYWORD(t):
118
+ r'@import'
119
+ return t
120
+
121
+
109
122
  def t_NUMBER(t):
110
123
  r'\d+'
111
124
  t.value = int(t.value)