pytest-dsl 0.1.1__py3-none-any.whl → 0.3.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.
- pytest_dsl/cli.py +42 -6
- pytest_dsl/core/__init__.py +7 -0
- pytest_dsl/core/custom_keyword_manager.py +213 -0
- pytest_dsl/core/dsl_executor.py +171 -3
- pytest_dsl/core/http_request.py +163 -54
- pytest_dsl/core/lexer.py +36 -1
- pytest_dsl/core/parser.py +155 -13
- pytest_dsl/core/parsetab.py +82 -49
- pytest_dsl/core/variable_utils.py +1 -1
- pytest_dsl/examples/custom/test_advanced_keywords.auto +31 -0
- pytest_dsl/examples/custom/test_custom_keywords.auto +37 -0
- pytest_dsl/examples/custom/test_default_values.auto +34 -0
- pytest_dsl/examples/http/http_retry_assertions.auto +2 -2
- pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +2 -2
- pytest_dsl/examples/test_custom_keyword.py +9 -0
- pytest_dsl/examples/test_http.py +0 -139
- pytest_dsl/keywords/http_keywords.py +290 -102
- pytest_dsl/parsetab.py +69 -0
- pytest_dsl-0.3.0.dist-info/METADATA +448 -0
- {pytest_dsl-0.1.1.dist-info → pytest_dsl-0.3.0.dist-info}/RECORD +24 -24
- {pytest_dsl-0.1.1.dist-info → pytest_dsl-0.3.0.dist-info}/WHEEL +1 -1
- pytest_dsl/core/custom_auth_example.py +0 -425
- pytest_dsl/examples/csrf_auth_provider.py +0 -232
- pytest_dsl/examples/http/csrf_auth_test.auto +0 -64
- pytest_dsl/examples/http/custom_auth_test.auto +0 -76
- pytest_dsl/examples/http_clients.yaml +0 -48
- pytest_dsl/examples/keyword_example.py +0 -70
- pytest_dsl-0.1.1.dist-info/METADATA +0 -504
- {pytest_dsl-0.1.1.dist-info → pytest_dsl-0.3.0.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.1.1.dist-info → pytest_dsl-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.1.1.dist-info → pytest_dsl-0.3.0.dist-info}/top_level.txt +0 -0
pytest_dsl/core/http_request.py
CHANGED
@@ -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 =
|
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 =
|
426
|
+
compare_operator = assertion_type # 使用断言类型作为操作符
|
427
427
|
else:
|
428
428
|
expected_value = None
|
429
|
-
compare_operator =
|
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" #
|
452
|
+
compare_operator = "eq" # 默认使用相等比较
|
453
453
|
expected_value = assertion[3] # 预期长度值
|
454
454
|
else:
|
455
455
|
# 对于其他特殊断言,使用第四个元素作为期望值
|
456
|
-
compare_operator =
|
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
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
|
pytest_dsl/core/lexer.py
CHANGED
@@ -9,7 +9,10 @@ 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关键字支持
|
14
|
+
'else': 'ELSE', # 添加else关键字支持
|
15
|
+
'if': 'IF' # 添加if关键字支持
|
13
16
|
}
|
14
17
|
|
15
18
|
# token 名称列表
|
@@ -33,6 +36,18 @@ tokens = [
|
|
33
36
|
'DATE_KEYWORD',
|
34
37
|
'TEARDOWN_KEYWORD',
|
35
38
|
'DATA_KEYWORD', # Add new token for @data keyword
|
39
|
+
'KEYWORD_KEYWORD', # 添加@keyword关键字
|
40
|
+
'IMPORT_KEYWORD', # 添加@import关键字
|
41
|
+
'GT', # 大于 >
|
42
|
+
'LT', # 小于 <
|
43
|
+
'GE', # 大于等于 >=
|
44
|
+
'LE', # 小于等于 <=
|
45
|
+
'EQ', # 等于 ==
|
46
|
+
'NE', # 不等于 !=
|
47
|
+
'PLUS', # 加法 +
|
48
|
+
'MINUS', # 减法 -
|
49
|
+
'TIMES', # 乘法 *
|
50
|
+
'DIVIDE', # 除法 /
|
36
51
|
] + list(reserved.values())
|
37
52
|
|
38
53
|
# 正则表达式定义 token
|
@@ -43,6 +58,16 @@ t_RBRACKET = r'\]'
|
|
43
58
|
t_COLON = r':'
|
44
59
|
t_COMMA = r','
|
45
60
|
t_EQUALS = r'='
|
61
|
+
t_GT = r'>'
|
62
|
+
t_LT = r'<'
|
63
|
+
t_GE = r'>='
|
64
|
+
t_LE = r'<='
|
65
|
+
t_EQ = r'=='
|
66
|
+
t_NE = r'!='
|
67
|
+
t_PLUS = r'\+'
|
68
|
+
t_MINUS = r'-'
|
69
|
+
t_TIMES = r'\*'
|
70
|
+
t_DIVIDE = r'/'
|
46
71
|
|
47
72
|
# 增加PLACEHOLDER规则,匹配 ${变量名} 格式
|
48
73
|
t_PLACEHOLDER = r'\$\{[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*\}'
|
@@ -106,6 +131,16 @@ def t_DATA_KEYWORD(t):
|
|
106
131
|
return t
|
107
132
|
|
108
133
|
|
134
|
+
def t_KEYWORD_KEYWORD(t):
|
135
|
+
r'@keyword'
|
136
|
+
return t
|
137
|
+
|
138
|
+
|
139
|
+
def t_IMPORT_KEYWORD(t):
|
140
|
+
r'@import'
|
141
|
+
return t
|
142
|
+
|
143
|
+
|
109
144
|
def t_NUMBER(t):
|
110
145
|
r'\d+'
|
111
146
|
t.value = int(t.value)
|
pytest_dsl/core/parser.py
CHANGED
@@ -12,23 +12,44 @@ class Node:
|
|
12
12
|
# 定义优先级和结合性
|
13
13
|
precedence = (
|
14
14
|
('left', 'COMMA'),
|
15
|
+
('left', 'GT', 'LT', 'GE', 'LE', 'EQ', 'NE'), # 比较运算符优先级
|
16
|
+
('left', 'PLUS', 'MINUS'), # 加减运算符优先级
|
17
|
+
('left', 'TIMES', 'DIVIDE'), # 乘除运算符优先级
|
15
18
|
('right', 'EQUALS'),
|
16
19
|
)
|
17
20
|
|
18
21
|
|
19
22
|
def p_start(p):
|
20
23
|
'''start : metadata statements teardown
|
21
|
-
| metadata statements
|
24
|
+
| metadata statements
|
25
|
+
| statements teardown
|
26
|
+
| statements'''
|
22
27
|
|
23
28
|
if len(p) == 4:
|
24
29
|
p[0] = Node('Start', [p[1], p[2], p[3]])
|
30
|
+
elif len(p) == 3:
|
31
|
+
# 判断第二个元素是teardown还是statements
|
32
|
+
if p[2].type == 'Teardown':
|
33
|
+
p[0] = Node('Start', [Node('Metadata', []), p[1], p[2]])
|
34
|
+
else:
|
35
|
+
p[0] = Node('Start', [p[1], p[2]])
|
25
36
|
else:
|
26
|
-
|
37
|
+
# 没有metadata和teardown
|
38
|
+
p[0] = Node('Start', [Node('Metadata', []), p[1]])
|
27
39
|
|
28
40
|
|
29
41
|
def p_metadata(p):
|
30
|
-
'''metadata : metadata_items
|
31
|
-
|
42
|
+
'''metadata : metadata_items
|
43
|
+
| empty'''
|
44
|
+
if p[1]:
|
45
|
+
p[0] = Node('Metadata', p[1])
|
46
|
+
else:
|
47
|
+
p[0] = Node('Metadata', [])
|
48
|
+
|
49
|
+
|
50
|
+
def p_empty(p):
|
51
|
+
'''empty :'''
|
52
|
+
p[0] = None
|
32
53
|
|
33
54
|
|
34
55
|
def p_metadata_items(p):
|
@@ -46,13 +67,16 @@ def p_metadata_item(p):
|
|
46
67
|
| TAGS_KEYWORD COLON LBRACKET tags RBRACKET
|
47
68
|
| AUTHOR_KEYWORD COLON metadata_value
|
48
69
|
| DATE_KEYWORD COLON DATE
|
49
|
-
| DATA_KEYWORD COLON data_source
|
70
|
+
| DATA_KEYWORD COLON data_source
|
71
|
+
| IMPORT_KEYWORD COLON STRING'''
|
50
72
|
if p[1] == '@tags':
|
51
73
|
p[0] = Node(p[1], value=p[4])
|
52
74
|
elif p[1] == '@data':
|
53
75
|
# 对于数据驱动测试,将数据源信息存储在节点中
|
54
76
|
data_info = p[3] # 这是一个包含 file 和 format 的字典
|
55
77
|
p[0] = Node(p[1], value=data_info, children=None)
|
78
|
+
elif p[1] == '@import':
|
79
|
+
p[0] = Node(p[1], value=p[3])
|
56
80
|
else:
|
57
81
|
p[0] = Node(p[1], value=p[3])
|
58
82
|
|
@@ -90,7 +114,10 @@ def p_statements(p):
|
|
90
114
|
def p_statement(p):
|
91
115
|
'''statement : assignment
|
92
116
|
| keyword_call
|
93
|
-
| loop
|
117
|
+
| loop
|
118
|
+
| custom_keyword
|
119
|
+
| return_statement
|
120
|
+
| if_statement'''
|
94
121
|
p[0] = p[1]
|
95
122
|
|
96
123
|
|
@@ -104,13 +131,31 @@ def p_assignment(p):
|
|
104
131
|
|
105
132
|
|
106
133
|
def p_expression(p):
|
107
|
-
'''expression :
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
134
|
+
'''expression : expr_atom
|
135
|
+
| comparison_expr
|
136
|
+
| arithmetic_expr'''
|
137
|
+
# 如果是比较表达式或其他复合表达式,则已经是一个Node对象
|
138
|
+
if isinstance(p[1], Node):
|
139
|
+
p[0] = p[1]
|
140
|
+
else:
|
141
|
+
p[0] = Node('Expression', value=p[1])
|
142
|
+
|
143
|
+
|
144
|
+
def p_expr_atom(p):
|
145
|
+
'''expr_atom : NUMBER
|
146
|
+
| STRING
|
147
|
+
| PLACEHOLDER
|
148
|
+
| ID
|
149
|
+
| boolean_expr
|
150
|
+
| list_expr
|
151
|
+
| LPAREN expression RPAREN'''
|
152
|
+
if p[1] == '(':
|
153
|
+
# 处理括号表达式,直接返回括号内的表达式节点
|
154
|
+
p[0] = p[2]
|
155
|
+
elif isinstance(p[1], Node):
|
156
|
+
p[0] = p[1]
|
157
|
+
else:
|
158
|
+
p[0] = Node('Expression', value=p[1])
|
114
159
|
|
115
160
|
|
116
161
|
def p_boolean_expr(p):
|
@@ -185,6 +230,103 @@ def p_data_source(p):
|
|
185
230
|
p[0] = {'file': p[1], 'format': p[3]}
|
186
231
|
|
187
232
|
|
233
|
+
def p_custom_keyword(p):
|
234
|
+
'''custom_keyword : KEYWORD_KEYWORD ID LPAREN param_definitions RPAREN DO statements END'''
|
235
|
+
p[0] = Node('CustomKeyword', [p[4], p[7]], p[2])
|
236
|
+
|
237
|
+
|
238
|
+
def p_param_definitions(p):
|
239
|
+
'''param_definitions : param_def_list
|
240
|
+
| '''
|
241
|
+
if len(p) == 2:
|
242
|
+
p[0] = p[1]
|
243
|
+
else:
|
244
|
+
p[0] = []
|
245
|
+
|
246
|
+
|
247
|
+
def p_param_def_list(p):
|
248
|
+
'''param_def_list : param_def COMMA param_def_list
|
249
|
+
| param_def'''
|
250
|
+
if len(p) == 4:
|
251
|
+
p[0] = [p[1]] + p[3]
|
252
|
+
else:
|
253
|
+
p[0] = [p[1]]
|
254
|
+
|
255
|
+
|
256
|
+
def p_param_def(p):
|
257
|
+
'''param_def : ID EQUALS STRING
|
258
|
+
| ID EQUALS NUMBER
|
259
|
+
| ID'''
|
260
|
+
if len(p) == 4:
|
261
|
+
p[0] = Node('ParameterDef', [Node('Expression', value=p[3])], p[1])
|
262
|
+
else:
|
263
|
+
p[0] = Node('ParameterDef', [], p[1])
|
264
|
+
|
265
|
+
|
266
|
+
def p_return_statement(p):
|
267
|
+
'''return_statement : RETURN expression'''
|
268
|
+
p[0] = Node('Return', [p[2]])
|
269
|
+
|
270
|
+
|
271
|
+
def p_if_statement(p):
|
272
|
+
'''if_statement : IF expression DO statements END
|
273
|
+
| IF expression DO statements ELSE statements END'''
|
274
|
+
if len(p) == 6:
|
275
|
+
p[0] = Node('IfStatement', [p[2], p[4]], None)
|
276
|
+
else:
|
277
|
+
p[0] = Node('IfStatement', [p[2], p[4], p[6]], None)
|
278
|
+
|
279
|
+
|
280
|
+
def p_comparison_expr(p):
|
281
|
+
'''comparison_expr : expr_atom GT expr_atom
|
282
|
+
| expr_atom LT expr_atom
|
283
|
+
| expr_atom GE expr_atom
|
284
|
+
| expr_atom LE expr_atom
|
285
|
+
| expr_atom EQ expr_atom
|
286
|
+
| expr_atom NE expr_atom'''
|
287
|
+
|
288
|
+
# 根据规则索引判断使用的是哪个操作符
|
289
|
+
if p.slice[2].type == 'GT':
|
290
|
+
operator = '>'
|
291
|
+
elif p.slice[2].type == 'LT':
|
292
|
+
operator = '<'
|
293
|
+
elif p.slice[2].type == 'GE':
|
294
|
+
operator = '>='
|
295
|
+
elif p.slice[2].type == 'LE':
|
296
|
+
operator = '<='
|
297
|
+
elif p.slice[2].type == 'EQ':
|
298
|
+
operator = '=='
|
299
|
+
elif p.slice[2].type == 'NE':
|
300
|
+
operator = '!='
|
301
|
+
else:
|
302
|
+
print(f"警告: 无法识别的操作符类型 {p.slice[2].type}")
|
303
|
+
operator = None
|
304
|
+
|
305
|
+
p[0] = Node('ComparisonExpr', [p[1], p[3]], operator)
|
306
|
+
|
307
|
+
|
308
|
+
def p_arithmetic_expr(p):
|
309
|
+
'''arithmetic_expr : expression PLUS expression
|
310
|
+
| expression MINUS expression
|
311
|
+
| expression TIMES expression
|
312
|
+
| expression DIVIDE expression'''
|
313
|
+
|
314
|
+
# 根据规则索引判断使用的是哪个操作符
|
315
|
+
if p.slice[2].type == 'PLUS':
|
316
|
+
operator = '+'
|
317
|
+
elif p.slice[2].type == 'MINUS':
|
318
|
+
operator = '-'
|
319
|
+
elif p.slice[2].type == 'TIMES':
|
320
|
+
operator = '*'
|
321
|
+
elif p.slice[2].type == 'DIVIDE':
|
322
|
+
operator = '/'
|
323
|
+
else:
|
324
|
+
print(f"警告: 无法识别的操作符类型 {p.slice[2].type}")
|
325
|
+
operator = None
|
326
|
+
|
327
|
+
p[0] = Node('ArithmeticExpr', [p[1], p[3]], operator)
|
328
|
+
|
329
|
+
|
188
330
|
def p_error(p):
|
189
331
|
if p:
|
190
332
|
print(
|