pytest-dsl 0.4.0__py3-none-any.whl → 0.6.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 (36) hide show
  1. pytest_dsl/cli.py +28 -33
  2. pytest_dsl/core/auto_decorator.py +72 -53
  3. pytest_dsl/core/auto_directory.py +8 -5
  4. pytest_dsl/core/dsl_executor.py +211 -53
  5. pytest_dsl/core/http_request.py +272 -221
  6. pytest_dsl/core/lexer.py +14 -13
  7. pytest_dsl/core/parser.py +27 -8
  8. pytest_dsl/core/parsetab.py +71 -66
  9. pytest_dsl/core/plugin_discovery.py +1 -8
  10. pytest_dsl/core/yaml_loader.py +96 -19
  11. pytest_dsl/examples/assert/assertion_example.auto +1 -1
  12. pytest_dsl/examples/assert/boolean_test.auto +2 -2
  13. pytest_dsl/examples/assert/expression_test.auto +1 -1
  14. pytest_dsl/examples/custom/test_advanced_keywords.auto +2 -2
  15. pytest_dsl/examples/custom/test_custom_keywords.auto +2 -2
  16. pytest_dsl/examples/custom/test_default_values.auto +2 -2
  17. pytest_dsl/examples/http/file_reference_test.auto +1 -1
  18. pytest_dsl/examples/http/http_advanced.auto +1 -1
  19. pytest_dsl/examples/http/http_example.auto +1 -1
  20. pytest_dsl/examples/http/http_length_test.auto +1 -1
  21. pytest_dsl/examples/http/http_retry_assertions.auto +1 -1
  22. pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +2 -2
  23. pytest_dsl/examples/http/http_with_yaml.auto +1 -1
  24. pytest_dsl/examples/quickstart/api_basics.auto +1 -1
  25. pytest_dsl/examples/quickstart/assertions.auto +1 -1
  26. pytest_dsl/examples/quickstart/loops.auto +2 -2
  27. pytest_dsl/keywords/assertion_keywords.py +76 -62
  28. pytest_dsl/keywords/global_keywords.py +43 -4
  29. pytest_dsl/keywords/http_keywords.py +141 -139
  30. {pytest_dsl-0.4.0.dist-info → pytest_dsl-0.6.0.dist-info}/METADATA +266 -15
  31. pytest_dsl-0.6.0.dist-info/RECORD +68 -0
  32. {pytest_dsl-0.4.0.dist-info → pytest_dsl-0.6.0.dist-info}/WHEEL +1 -1
  33. {pytest_dsl-0.4.0.dist-info → pytest_dsl-0.6.0.dist-info}/entry_points.txt +1 -0
  34. pytest_dsl-0.4.0.dist-info/RECORD +0 -68
  35. {pytest_dsl-0.4.0.dist-info → pytest_dsl-0.6.0.dist-info}/licenses/LICENSE +0 -0
  36. {pytest_dsl-0.4.0.dist-info → pytest_dsl-0.6.0.dist-info}/top_level.txt +0 -0
@@ -27,7 +27,7 @@ class DSLExecutor:
27
27
  self.test_context.executor = self # 让 test_context 能够访问到 executor
28
28
  self.variable_replacer = VariableReplacer(self.variables, self.test_context)
29
29
  self.imported_files = set() # 跟踪已导入的文件,避免循环导入
30
-
30
+
31
31
  def set_current_data(self, data):
32
32
  """设置当前测试数据集"""
33
33
  if data:
@@ -35,30 +35,30 @@ class DSLExecutor:
35
35
  # 同时将数据添加到测试上下文
36
36
  for key, value in data.items():
37
37
  self.test_context.set(key, value)
38
-
38
+
39
39
  def _load_test_data(self, data_source):
40
40
  """加载测试数据
41
-
41
+
42
42
  :param data_source: 数据源配置,包含 file 和 format 字段
43
43
  :return: 包含测试数据的列表
44
44
  """
45
45
  if not data_source:
46
46
  return [{}] # 如果没有数据源,返回一个空的数据集
47
-
47
+
48
48
  file_path = data_source['file']
49
49
  format_type = data_source['format']
50
-
50
+
51
51
  if not os.path.exists(file_path):
52
52
  raise Exception(f"数据文件不存在: {file_path}")
53
-
53
+
54
54
  if format_type.lower() == 'csv':
55
55
  return self._load_csv_data(file_path)
56
56
  else:
57
57
  raise Exception(f"不支持的数据格式: {format_type}")
58
-
58
+
59
59
  def _load_csv_data(self, file_path):
60
60
  """加载CSV格式的测试数据
61
-
61
+
62
62
  :param file_path: CSV文件路径
63
63
  :return: 包含测试数据的列表
64
64
  """
@@ -68,11 +68,11 @@ class DSLExecutor:
68
68
  for row in reader:
69
69
  data_sets.append(row)
70
70
  return data_sets
71
-
71
+
72
72
  def eval_expression(self, expr_node):
73
73
  """
74
74
  对表达式节点进行求值,返回表达式的值。
75
-
75
+
76
76
  :param expr_node: AST中的表达式节点
77
77
  :return: 表达式求值后的结果
78
78
  :raises Exception: 当遇到未定义变量或无法求值的类型时抛出异常
@@ -101,7 +101,7 @@ class DSLExecutor:
101
101
  return self._eval_arithmetic_expr(expr_node)
102
102
  else:
103
103
  raise Exception(f"无法求值的表达式类型: {expr_node.type}")
104
-
104
+
105
105
  def _eval_expression_value(self, value):
106
106
  """处理表达式值的具体逻辑"""
107
107
  if isinstance(value, Node):
@@ -110,7 +110,7 @@ class DSLExecutor:
110
110
  # 如果是ID类型的变量名
111
111
  if value in self.variable_replacer.local_variables:
112
112
  return self.variable_replacer.local_variables[value]
113
-
113
+
114
114
  # 定义变量引用模式
115
115
  pattern = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)\}'
116
116
  # 检查整个字符串是否完全匹配单一变量引用模式
@@ -122,24 +122,24 @@ class DSLExecutor:
122
122
  # 如果不是单一变量,则替换字符串中的所有变量引用
123
123
  return self.variable_replacer.replace_in_string(value)
124
124
  return value
125
-
125
+
126
126
  def _eval_comparison_expr(self, expr_node):
127
127
  """
128
128
  对比较表达式进行求值
129
-
129
+
130
130
  :param expr_node: 比较表达式节点
131
131
  :return: 比较结果(布尔值)
132
132
  """
133
133
  left_value = self.eval_expression(expr_node.children[0])
134
134
  right_value = self.eval_expression(expr_node.children[1])
135
135
  operator = expr_node.value # 操作符: >, <, >=, <=, ==, !=
136
-
136
+
137
137
  # 尝试类型转换
138
138
  if isinstance(left_value, str) and str(left_value).isdigit():
139
139
  left_value = int(left_value)
140
140
  if isinstance(right_value, str) and str(right_value).isdigit():
141
141
  right_value = int(right_value)
142
-
142
+
143
143
  # 根据操作符执行相应的比较操作
144
144
  if operator == '>':
145
145
  return left_value > right_value
@@ -155,31 +155,31 @@ class DSLExecutor:
155
155
  return left_value != right_value
156
156
  else:
157
157
  raise Exception(f"未知的比较操作符: {operator}")
158
-
158
+
159
159
  def _eval_arithmetic_expr(self, expr_node):
160
160
  """
161
161
  对算术表达式进行求值
162
-
162
+
163
163
  :param expr_node: 算术表达式节点
164
164
  :return: 计算结果
165
165
  """
166
166
  left_value = self.eval_expression(expr_node.children[0])
167
167
  right_value = self.eval_expression(expr_node.children[1])
168
168
  operator = expr_node.value # 操作符: +, -, *, /
169
-
169
+
170
170
  # 尝试类型转换 - 如果是字符串数字则转为数字
171
171
  if isinstance(left_value, str) and str(left_value).replace('.', '', 1).isdigit():
172
172
  left_value = float(left_value)
173
173
  # 如果是整数则转为整数
174
174
  if left_value.is_integer():
175
175
  left_value = int(left_value)
176
-
176
+
177
177
  if isinstance(right_value, str) and str(right_value).replace('.', '', 1).isdigit():
178
178
  right_value = float(right_value)
179
179
  # 如果是整数则转为整数
180
180
  if right_value.is_integer():
181
181
  right_value = int(right_value)
182
-
182
+
183
183
  # 进行相应的算术运算
184
184
  if operator == '+':
185
185
  # 对于字符串,+是连接操作
@@ -202,18 +202,48 @@ class DSLExecutor:
202
202
  return left_value / right_value
203
203
  else:
204
204
  raise Exception(f"未知的算术操作符: {operator}")
205
-
205
+
206
206
  def _get_variable(self, var_name):
207
207
  """获取变量值,优先从本地变量获取,如果不存在则尝试从全局上下文获取"""
208
208
  return self.variable_replacer.get_variable(var_name)
209
-
209
+
210
210
  def _replace_variables_in_string(self, value):
211
211
  """替换字符串中的变量引用"""
212
212
  return self.variable_replacer.replace_in_string(value)
213
-
213
+
214
+ def _handle_remote_import(self, node):
215
+ """处理远程关键字导入
216
+
217
+ Args:
218
+ node: RemoteImport节点
219
+ """
220
+ from pytest_dsl.remote.keyword_client import remote_keyword_manager
221
+
222
+ remote_info = node.value
223
+ url = self._replace_variables_in_string(remote_info['url'])
224
+ alias = remote_info['alias']
225
+
226
+ print(f"正在连接远程关键字服务器: {url}, 别名: {alias}")
227
+
228
+ # 注册远程服务器
229
+ success = remote_keyword_manager.register_remote_server(url, alias)
230
+
231
+ if not success:
232
+ print(f"无法连接到远程关键字服务器: {url}")
233
+ raise Exception(f"无法连接到远程关键字服务器: {url}")
234
+
235
+ print(f"已成功连接到远程关键字服务器: {url}, 别名: {alias}")
236
+
237
+ allure.attach(
238
+ f"已连接到远程关键字服务器: {url}\n"
239
+ f"别名: {alias}",
240
+ name="远程关键字导入",
241
+ attachment_type=allure.attachment_type.TEXT
242
+ )
243
+
214
244
  def _handle_custom_keywords_in_file(self, node):
215
245
  """处理文件中的自定义关键字定义
216
-
246
+
217
247
  Args:
218
248
  node: Start节点
219
249
  """
@@ -233,7 +263,7 @@ class DSLExecutor:
233
263
  self.test_context.clear()
234
264
  metadata = {}
235
265
  teardown_node = None
236
-
266
+
237
267
  # 先处理元数据和找到teardown节点
238
268
  for child in node.children:
239
269
  if child.type == 'Metadata':
@@ -242,14 +272,17 @@ class DSLExecutor:
242
272
  # 处理导入指令
243
273
  if item.type == '@import':
244
274
  self._handle_import(item.value)
275
+ # 处理远程关键字导入
276
+ elif item.type == 'RemoteImport':
277
+ self._handle_remote_import(item)
245
278
  elif child.type == 'Teardown':
246
279
  teardown_node = child
247
-
280
+
248
281
  # 在_execute_test_iteration之前添加
249
282
  self._handle_custom_keywords_in_file(node)
250
283
  # 执行测试
251
284
  self._execute_test_iteration(metadata, node, teardown_node)
252
-
285
+
253
286
  except Exception as e:
254
287
  # 如果是断言错误,直接抛出
255
288
  if isinstance(e, AssertionError):
@@ -267,14 +300,14 @@ class DSLExecutor:
267
300
 
268
301
  def _handle_import(self, file_path):
269
302
  """处理导入指令
270
-
303
+
271
304
  Args:
272
305
  file_path: 资源文件路径
273
306
  """
274
307
  # 防止循环导入
275
308
  if file_path in self.imported_files:
276
309
  return
277
-
310
+
278
311
  try:
279
312
  # 导入自定义关键字文件
280
313
  from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
@@ -297,12 +330,12 @@ class DSLExecutor:
297
330
  if '@tags' in metadata:
298
331
  for tag in metadata['@tags']:
299
332
  allure.dynamic.tag(tag.value)
300
-
333
+
301
334
  # 执行所有非teardown节点
302
335
  for child in node.children:
303
336
  if child.type != 'Teardown' and child.type != 'Metadata':
304
337
  self.execute(child)
305
-
338
+
306
339
  # 执行teardown
307
340
  if teardown_node:
308
341
  with allure.step("执行清理操作"):
@@ -320,7 +353,7 @@ class DSLExecutor:
320
353
  # 否则清空变量(用于正常DSL执行)
321
354
  import os
322
355
  keep_variables = os.environ.get('PYTEST_DSL_KEEP_VARIABLES', '0') == '1'
323
-
356
+
324
357
  if not keep_variables:
325
358
  self.variables.clear()
326
359
  # 同时清空测试上下文
@@ -336,7 +369,7 @@ class DSLExecutor:
336
369
  """处理赋值语句"""
337
370
  var_name = node.value
338
371
  expr_value = self.eval_expression(node.children[0])
339
-
372
+
340
373
  # 检查变量名是否以g_开头,如果是则设置为全局变量
341
374
  if var_name.startswith('g_'):
342
375
  global_context.set_variable(var_name, expr_value)
@@ -361,22 +394,42 @@ class DSLExecutor:
361
394
  var_name = node.value
362
395
  keyword_call_node = node.children[0]
363
396
  result = self.execute(keyword_call_node)
364
-
397
+
365
398
  if result is not None:
399
+ # 处理新的统一返回格式(支持远程关键字模式)
400
+ if isinstance(result, dict) and 'result' in result:
401
+ # 提取主要返回值
402
+ main_result = result['result']
403
+
404
+ # 处理captures字段中的变量
405
+ captures = result.get('captures', {})
406
+ for capture_var, capture_value in captures.items():
407
+ if capture_var.startswith('g_'):
408
+ global_context.set_variable(capture_var, capture_value)
409
+ else:
410
+ self.variable_replacer.local_variables[capture_var] = capture_value
411
+ self.test_context.set(capture_var, capture_value)
412
+
413
+ # 将主要结果赋值给指定变量
414
+ actual_result = main_result
415
+ else:
416
+ # 传统格式,直接使用结果
417
+ actual_result = result
418
+
366
419
  # 检查变量名是否以g_开头,如果是则设置为全局变量
367
420
  if var_name.startswith('g_'):
368
- global_context.set_variable(var_name, result)
421
+ global_context.set_variable(var_name, actual_result)
369
422
  allure.attach(
370
- f"全局变量: {var_name}\n值: {result}",
423
+ f"全局变量: {var_name}\n值: {actual_result}",
371
424
  name="全局变量赋值",
372
425
  attachment_type=allure.attachment_type.TEXT
373
426
  )
374
427
  else:
375
428
  # 存储在本地变量字典和测试上下文中
376
- self.variable_replacer.local_variables[var_name] = result
377
- self.test_context.set(var_name, result) # 同时添加到测试上下文
429
+ self.variable_replacer.local_variables[var_name] = actual_result
430
+ self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
378
431
  allure.attach(
379
- f"变量: {var_name}\n值: {result}",
432
+ f"变量: {var_name}\n值: {actual_result}",
380
433
  name="赋值详情",
381
434
  attachment_type=allure.attachment_type.TEXT
382
435
  )
@@ -389,7 +442,7 @@ class DSLExecutor:
389
442
  var_name = node.value
390
443
  start = self.eval_expression(node.children[0])
391
444
  end = self.eval_expression(node.children[1])
392
-
445
+
393
446
  for i in range(int(start), int(end)):
394
447
  # 存储在本地变量字典和测试上下文中
395
448
  self.variable_replacer.local_variables[var_name] = i
@@ -403,9 +456,9 @@ class DSLExecutor:
403
456
  keyword_info = keyword_manager.get_keyword_info(keyword_name)
404
457
  if not keyword_info:
405
458
  raise Exception(f"未注册的关键字: {keyword_name}")
406
-
459
+
407
460
  kwargs = self._prepare_keyword_params(node, keyword_info)
408
-
461
+
409
462
  try:
410
463
  # 由于KeywordManager中的wrapper已经添加了allure.step和日志,这里不再重复添加
411
464
  result = keyword_manager.execute(keyword_name, **kwargs)
@@ -418,7 +471,7 @@ class DSLExecutor:
418
471
  """准备关键字调用参数"""
419
472
  mapping = keyword_info.get('mapping', {})
420
473
  kwargs = {'context': self.test_context} # 默认传入context参数
421
-
474
+
422
475
  # 检查是否有参数列表
423
476
  if node.children[0]:
424
477
  for param in node.children[0]:
@@ -427,7 +480,7 @@ class DSLExecutor:
427
480
  # 对参数值进行变量替换
428
481
  param_value = self.eval_expression(param.children[0])
429
482
  kwargs[english_param_name] = param_value
430
-
483
+
431
484
  return kwargs
432
485
 
433
486
  @allure.step("执行清理操作")
@@ -438,10 +491,10 @@ class DSLExecutor:
438
491
  @allure.step("执行返回语句")
439
492
  def _handle_return(self, node):
440
493
  """处理return语句
441
-
494
+
442
495
  Args:
443
496
  node: Return节点
444
-
497
+
445
498
  Returns:
446
499
  表达式求值结果
447
500
  """
@@ -451,12 +504,12 @@ class DSLExecutor:
451
504
  @allure.step("执行条件语句")
452
505
  def _handle_if_statement(self, node):
453
506
  """处理if-else语句
454
-
507
+
455
508
  Args:
456
509
  node: IfStatement节点,包含条件表达式、if分支和可选的else分支
457
510
  """
458
511
  condition = self.eval_expression(node.children[0])
459
-
512
+
460
513
  # 将条件转换为布尔值进行评估
461
514
  if condition:
462
515
  # 执行if分支
@@ -466,10 +519,112 @@ class DSLExecutor:
466
519
  # 如果存在else分支且条件为假,则执行else分支
467
520
  with allure.step("执行else分支"):
468
521
  return self.execute(node.children[2])
469
-
522
+
470
523
  # 如果条件为假且没有else分支,则不执行任何操作
471
524
  return None
472
525
 
526
+ def _execute_remote_keyword_call(self, node):
527
+ """执行远程关键字调用
528
+
529
+ Args:
530
+ node: RemoteKeywordCall节点
531
+
532
+ Returns:
533
+ 执行结果
534
+ """
535
+ from pytest_dsl.remote.keyword_client import remote_keyword_manager
536
+
537
+ call_info = node.value
538
+ alias = call_info['alias']
539
+ keyword_name = call_info['keyword']
540
+
541
+ # 准备参数
542
+ params = []
543
+ if node.children and node.children[0]:
544
+ params = node.children[0]
545
+
546
+ kwargs = {}
547
+ for param in params:
548
+ param_name = param.value
549
+ param_value = self.eval_expression(param.children[0])
550
+ kwargs[param_name] = param_value
551
+
552
+ # 添加测试上下文
553
+ kwargs['context'] = self.test_context
554
+
555
+ with allure.step(f"执行远程关键字: {alias}|{keyword_name}"):
556
+ try:
557
+ # 执行远程关键字
558
+ result = remote_keyword_manager.execute_remote_keyword(alias, keyword_name, **kwargs)
559
+ allure.attach(
560
+ f"远程关键字参数: {kwargs}\n"
561
+ f"远程关键字结果: {result}",
562
+ name="远程关键字执行详情",
563
+ attachment_type=allure.attachment_type.TEXT
564
+ )
565
+ return result
566
+ except Exception as e:
567
+ # 记录错误并重新抛出
568
+ allure.attach(
569
+ f"远程关键字执行失败: {str(e)}",
570
+ name="远程关键字错误",
571
+ attachment_type=allure.attachment_type.TEXT
572
+ )
573
+ raise
574
+
575
+ def _handle_assignment_remote_keyword_call(self, node):
576
+ """处理远程关键字调用赋值
577
+
578
+ Args:
579
+ node: AssignmentRemoteKeywordCall节点
580
+ """
581
+ var_name = node.value
582
+ remote_keyword_call_node = node.children[0]
583
+ result = self.execute(remote_keyword_call_node)
584
+
585
+ if result is not None:
586
+ # 注意:远程关键字客户端已经处理了新格式的返回值,
587
+ # 这里接收到的result应该已经是主要返回值,而不是完整的字典格式
588
+ # 但为了保险起见,我们仍然检查是否为新格式
589
+ if isinstance(result, dict) and 'result' in result:
590
+ # 如果仍然是新格式(可能是嵌套的远程调用),提取主要返回值
591
+ main_result = result['result']
592
+
593
+ # 处理captures字段中的变量
594
+ captures = result.get('captures', {})
595
+ for capture_var, capture_value in captures.items():
596
+ if capture_var.startswith('g_'):
597
+ global_context.set_variable(capture_var, capture_value)
598
+ else:
599
+ self.variable_replacer.local_variables[capture_var] = capture_value
600
+ self.test_context.set(capture_var, capture_value)
601
+
602
+ # 将主要结果赋值给指定变量
603
+ actual_result = main_result
604
+ else:
605
+ # 传统格式或已经处理过的格式,直接使用结果
606
+ actual_result = result
607
+
608
+ # 检查变量名是否以g_开头,如果是则设置为全局变量
609
+ if var_name.startswith('g_'):
610
+ global_context.set_variable(var_name, actual_result)
611
+ allure.attach(
612
+ f"全局变量: {var_name}\n值: {actual_result}",
613
+ name="全局变量赋值",
614
+ attachment_type=allure.attachment_type.TEXT
615
+ )
616
+ else:
617
+ # 存储在本地变量字典和测试上下文中
618
+ self.variable_replacer.local_variables[var_name] = actual_result
619
+ self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
620
+ allure.attach(
621
+ f"变量: {var_name}\n值: {actual_result}",
622
+ name="赋值详情",
623
+ attachment_type=allure.attachment_type.TEXT
624
+ )
625
+ else:
626
+ raise Exception(f"远程关键字没有返回结果")
627
+
473
628
  def execute(self, node):
474
629
  """执行AST节点"""
475
630
  handlers = {
@@ -483,9 +638,12 @@ class DSLExecutor:
483
638
  'Teardown': self._handle_teardown,
484
639
  'Return': self._handle_return,
485
640
  'IfStatement': self._handle_if_statement,
486
- 'CustomKeyword': lambda _: None # 添加对CustomKeyword节点的处理,只需注册不需执行
641
+ 'CustomKeyword': lambda _: None, # 添加对CustomKeyword节点的处理,只需注册不需执行
642
+ 'RemoteImport': self._handle_remote_import,
643
+ 'RemoteKeywordCall': self._execute_remote_keyword_call,
644
+ 'AssignmentRemoteKeywordCall': self._handle_assignment_remote_keyword_call
487
645
  }
488
-
646
+
489
647
  handler = handlers.get(node.type)
490
648
  if handler:
491
649
  return handler(node)
@@ -494,4 +652,4 @@ class DSLExecutor:
494
652
  def read_file(filename):
495
653
  """读取 DSL 文件内容"""
496
654
  with open(filename, 'r', encoding='utf-8') as f:
497
- return f.read()
655
+ return f.read()