pytest-dsl 0.1.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/__init__.py +10 -0
- pytest_dsl/cli.py +44 -0
- pytest_dsl/conftest_adapter.py +4 -0
- pytest_dsl/core/__init__.py +0 -0
- pytest_dsl/core/auth_provider.py +409 -0
- pytest_dsl/core/auto_decorator.py +181 -0
- pytest_dsl/core/auto_directory.py +81 -0
- pytest_dsl/core/context.py +23 -0
- pytest_dsl/core/custom_auth_example.py +425 -0
- pytest_dsl/core/dsl_executor.py +329 -0
- pytest_dsl/core/dsl_executor_utils.py +84 -0
- pytest_dsl/core/global_context.py +103 -0
- pytest_dsl/core/http_client.py +411 -0
- pytest_dsl/core/http_request.py +810 -0
- pytest_dsl/core/keyword_manager.py +109 -0
- pytest_dsl/core/lexer.py +139 -0
- pytest_dsl/core/parser.py +197 -0
- pytest_dsl/core/parsetab.py +76 -0
- pytest_dsl/core/plugin_discovery.py +187 -0
- pytest_dsl/core/utils.py +146 -0
- pytest_dsl/core/variable_utils.py +267 -0
- pytest_dsl/core/yaml_loader.py +62 -0
- pytest_dsl/core/yaml_vars.py +75 -0
- pytest_dsl/docs/custom_keywords.md +140 -0
- pytest_dsl/examples/__init__.py +5 -0
- pytest_dsl/examples/assert/assertion_example.auto +44 -0
- pytest_dsl/examples/assert/boolean_test.auto +34 -0
- pytest_dsl/examples/assert/expression_test.auto +49 -0
- pytest_dsl/examples/http/__init__.py +3 -0
- pytest_dsl/examples/http/builtin_auth_test.auto +79 -0
- pytest_dsl/examples/http/csrf_auth_test.auto +64 -0
- pytest_dsl/examples/http/custom_auth_test.auto +76 -0
- pytest_dsl/examples/http/file_reference_test.auto +111 -0
- pytest_dsl/examples/http/http_advanced.auto +91 -0
- pytest_dsl/examples/http/http_example.auto +147 -0
- pytest_dsl/examples/http/http_length_test.auto +55 -0
- pytest_dsl/examples/http/http_retry_assertions.auto +91 -0
- pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +94 -0
- pytest_dsl/examples/http/http_with_yaml.auto +58 -0
- pytest_dsl/examples/http/new_retry_test.auto +22 -0
- pytest_dsl/examples/http/retry_assertions_only.auto +52 -0
- pytest_dsl/examples/http/retry_config_only.auto +49 -0
- pytest_dsl/examples/http/retry_debug.auto +22 -0
- pytest_dsl/examples/http/retry_with_fix.auto +21 -0
- pytest_dsl/examples/http/simple_retry.auto +20 -0
- pytest_dsl/examples/http/vars.yaml +55 -0
- pytest_dsl/examples/http_clients.yaml +48 -0
- pytest_dsl/examples/keyword_example.py +70 -0
- pytest_dsl/examples/test_assert.py +16 -0
- pytest_dsl/examples/test_http.py +168 -0
- pytest_dsl/keywords/__init__.py +10 -0
- pytest_dsl/keywords/assertion_keywords.py +610 -0
- pytest_dsl/keywords/global_keywords.py +51 -0
- pytest_dsl/keywords/http_keywords.py +430 -0
- pytest_dsl/keywords/system_keywords.py +17 -0
- pytest_dsl/main_adapter.py +7 -0
- pytest_dsl/plugin.py +44 -0
- pytest_dsl-0.1.0.dist-info/METADATA +537 -0
- pytest_dsl-0.1.0.dist-info/RECORD +63 -0
- pytest_dsl-0.1.0.dist-info/WHEEL +5 -0
- pytest_dsl-0.1.0.dist-info/entry_points.txt +5 -0
- pytest_dsl-0.1.0.dist-info/licenses/LICENSE +21 -0
- pytest_dsl-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,329 @@
|
|
1
|
+
import re
|
2
|
+
import allure
|
3
|
+
import csv
|
4
|
+
import os
|
5
|
+
import pytest
|
6
|
+
from pytest_dsl.core.lexer import get_lexer
|
7
|
+
from pytest_dsl.core.parser import get_parser, Node
|
8
|
+
from pytest_dsl.core.keyword_manager import keyword_manager
|
9
|
+
from pytest_dsl.core.global_context import global_context
|
10
|
+
from pytest_dsl.core.context import TestContext
|
11
|
+
import pytest_dsl.keywords
|
12
|
+
from pytest_dsl.core.yaml_vars import yaml_vars
|
13
|
+
from pytest_dsl.core.variable_utils import VariableReplacer
|
14
|
+
|
15
|
+
|
16
|
+
class DSLExecutor:
|
17
|
+
"""DSL执行器,负责执行解析后的AST
|
18
|
+
|
19
|
+
环境变量控制:
|
20
|
+
- PYTEST_DSL_KEEP_VARIABLES=1: 执行完成后保留变量,用于单元测试中检查变量值
|
21
|
+
- PYTEST_DSL_KEEP_VARIABLES=0: (默认) 执行完成后清空变量,用于正常DSL执行
|
22
|
+
"""
|
23
|
+
def __init__(self):
|
24
|
+
"""初始化DSL执行器"""
|
25
|
+
self.variables = {}
|
26
|
+
self.test_context = TestContext()
|
27
|
+
self.test_context.executor = self # 让 test_context 能够访问到 executor
|
28
|
+
self.variable_replacer = VariableReplacer(self.variables, self.test_context)
|
29
|
+
|
30
|
+
def set_current_data(self, data):
|
31
|
+
"""设置当前测试数据集"""
|
32
|
+
if data:
|
33
|
+
self.variables.update(data)
|
34
|
+
# 同时将数据添加到测试上下文
|
35
|
+
for key, value in data.items():
|
36
|
+
self.test_context.set(key, value)
|
37
|
+
|
38
|
+
def _load_test_data(self, data_source):
|
39
|
+
"""加载测试数据
|
40
|
+
|
41
|
+
:param data_source: 数据源配置,包含 file 和 format 字段
|
42
|
+
:return: 包含测试数据的列表
|
43
|
+
"""
|
44
|
+
if not data_source:
|
45
|
+
return [{}] # 如果没有数据源,返回一个空的数据集
|
46
|
+
|
47
|
+
file_path = data_source['file']
|
48
|
+
format_type = data_source['format']
|
49
|
+
|
50
|
+
if not os.path.exists(file_path):
|
51
|
+
raise Exception(f"数据文件不存在: {file_path}")
|
52
|
+
|
53
|
+
if format_type.lower() == 'csv':
|
54
|
+
return self._load_csv_data(file_path)
|
55
|
+
else:
|
56
|
+
raise Exception(f"不支持的数据格式: {format_type}")
|
57
|
+
|
58
|
+
def _load_csv_data(self, file_path):
|
59
|
+
"""加载CSV格式的测试数据
|
60
|
+
|
61
|
+
:param file_path: CSV文件路径
|
62
|
+
:return: 包含测试数据的列表
|
63
|
+
"""
|
64
|
+
data_sets = []
|
65
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
66
|
+
reader = csv.DictReader(f)
|
67
|
+
for row in reader:
|
68
|
+
data_sets.append(row)
|
69
|
+
return data_sets
|
70
|
+
|
71
|
+
def eval_expression(self, expr_node):
|
72
|
+
"""
|
73
|
+
对表达式节点进行求值,返回表达式的值。
|
74
|
+
|
75
|
+
:param expr_node: AST中的表达式节点
|
76
|
+
:return: 表达式求值后的结果
|
77
|
+
:raises Exception: 当遇到未定义变量或无法求值的类型时抛出异常
|
78
|
+
"""
|
79
|
+
if expr_node.type == 'Expression':
|
80
|
+
value = self._eval_expression_value(expr_node.value)
|
81
|
+
# 统一处理变量替换
|
82
|
+
return self.variable_replacer.replace_in_value(value)
|
83
|
+
elif expr_node.type == 'KeywordCall':
|
84
|
+
return self.execute(expr_node)
|
85
|
+
elif expr_node.type == 'ListExpr':
|
86
|
+
# 处理列表表达式
|
87
|
+
result = []
|
88
|
+
for item in expr_node.children:
|
89
|
+
item_value = self.eval_expression(item)
|
90
|
+
result.append(item_value)
|
91
|
+
return result
|
92
|
+
elif expr_node.type == 'BooleanExpr':
|
93
|
+
# 处理布尔值表达式
|
94
|
+
return expr_node.value
|
95
|
+
else:
|
96
|
+
raise Exception(f"无法求值的表达式类型: {expr_node.type}")
|
97
|
+
|
98
|
+
def _eval_expression_value(self, value):
|
99
|
+
"""处理表达式值的具体逻辑"""
|
100
|
+
if isinstance(value, Node):
|
101
|
+
return self.eval_expression(value)
|
102
|
+
elif isinstance(value, str):
|
103
|
+
# 定义变量引用模式
|
104
|
+
pattern = r'\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
|
105
|
+
# 检查整个字符串是否完全匹配单一变量引用模式
|
106
|
+
match = re.fullmatch(pattern, value)
|
107
|
+
if match:
|
108
|
+
var_name = match.group(1)
|
109
|
+
return self.variable_replacer.get_variable(var_name)
|
110
|
+
else:
|
111
|
+
# 如果不是单一变量,则替换字符串中的所有变量引用
|
112
|
+
return self.variable_replacer.replace_in_string(value)
|
113
|
+
return value
|
114
|
+
|
115
|
+
def _get_variable(self, var_name):
|
116
|
+
"""获取变量值,优先从本地变量获取,如果不存在则尝试从全局上下文获取"""
|
117
|
+
return self.variable_replacer.get_variable(var_name)
|
118
|
+
|
119
|
+
def _replace_variables_in_string(self, value):
|
120
|
+
"""替换字符串中的变量引用"""
|
121
|
+
return self.variable_replacer.replace_in_string(value)
|
122
|
+
|
123
|
+
def _handle_start(self, node):
|
124
|
+
"""处理开始节点"""
|
125
|
+
try:
|
126
|
+
# 清空上下文,确保每个测试用例都有一个新的上下文
|
127
|
+
self.test_context.clear()
|
128
|
+
metadata = {}
|
129
|
+
teardown_node = None
|
130
|
+
|
131
|
+
# 先处理元数据和找到teardown节点
|
132
|
+
for child in node.children:
|
133
|
+
if child.type == 'Metadata':
|
134
|
+
for item in child.children:
|
135
|
+
metadata[item.type] = item.value
|
136
|
+
elif child.type == 'Teardown':
|
137
|
+
teardown_node = child
|
138
|
+
|
139
|
+
# 执行测试
|
140
|
+
self._execute_test_iteration(metadata, node, teardown_node)
|
141
|
+
|
142
|
+
except Exception as e:
|
143
|
+
# 如果是断言错误,直接抛出
|
144
|
+
if isinstance(e, AssertionError):
|
145
|
+
raise
|
146
|
+
# 如果是语法错误,记录并抛出
|
147
|
+
if "语法错误" in str(e):
|
148
|
+
print(f"DSL语法错误: {str(e)}")
|
149
|
+
raise
|
150
|
+
# 其他错误,记录并抛出
|
151
|
+
print(f"测试执行错误: {str(e)}")
|
152
|
+
raise
|
153
|
+
finally:
|
154
|
+
# 测试用例执行完成后清空上下文
|
155
|
+
self.test_context.clear()
|
156
|
+
|
157
|
+
def _execute_test_iteration(self, metadata, node, teardown_node):
|
158
|
+
"""执行测试迭代"""
|
159
|
+
try:
|
160
|
+
# 设置 Allure 报告信息
|
161
|
+
if '@name' in metadata:
|
162
|
+
test_name = metadata['@name']
|
163
|
+
allure.dynamic.title(test_name)
|
164
|
+
if '@description' in metadata:
|
165
|
+
description = metadata['@description']
|
166
|
+
allure.dynamic.description(description)
|
167
|
+
if '@tags' in metadata:
|
168
|
+
for tag in metadata['@tags']:
|
169
|
+
allure.dynamic.tag(tag.value)
|
170
|
+
|
171
|
+
# 执行所有非teardown节点
|
172
|
+
for child in node.children:
|
173
|
+
if child.type != 'Teardown' and child.type != 'Metadata':
|
174
|
+
self.execute(child)
|
175
|
+
|
176
|
+
# 执行teardown
|
177
|
+
if teardown_node:
|
178
|
+
with allure.step("执行清理操作"):
|
179
|
+
try:
|
180
|
+
self.execute(teardown_node)
|
181
|
+
except Exception as e:
|
182
|
+
allure.attach(
|
183
|
+
f"清理失败: {str(e)}",
|
184
|
+
name="清理失败",
|
185
|
+
attachment_type=allure.attachment_type.TEXT
|
186
|
+
)
|
187
|
+
finally:
|
188
|
+
# 使用环境变量控制是否清空变量
|
189
|
+
# 当 PYTEST_DSL_KEEP_VARIABLES=1 时,保留变量(用于单元测试)
|
190
|
+
# 否则清空变量(用于正常DSL执行)
|
191
|
+
import os
|
192
|
+
keep_variables = os.environ.get('PYTEST_DSL_KEEP_VARIABLES', '0') == '1'
|
193
|
+
|
194
|
+
if not keep_variables:
|
195
|
+
self.variables.clear()
|
196
|
+
# 同时清空测试上下文
|
197
|
+
self.test_context.clear()
|
198
|
+
|
199
|
+
def _handle_statements(self, node):
|
200
|
+
"""处理语句列表"""
|
201
|
+
for stmt in node.children:
|
202
|
+
self.execute(stmt)
|
203
|
+
|
204
|
+
@allure.step("变量赋值")
|
205
|
+
def _handle_assignment(self, node):
|
206
|
+
"""处理赋值语句"""
|
207
|
+
var_name = node.value
|
208
|
+
expr_value = self.eval_expression(node.children[0])
|
209
|
+
|
210
|
+
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
211
|
+
if var_name.startswith('g_'):
|
212
|
+
global_context.set_variable(var_name, expr_value)
|
213
|
+
allure.attach(
|
214
|
+
f"全局变量: {var_name}\n值: {expr_value}",
|
215
|
+
name="全局变量赋值",
|
216
|
+
attachment_type=allure.attachment_type.TEXT
|
217
|
+
)
|
218
|
+
else:
|
219
|
+
# 存储在本地变量字典和测试上下文中
|
220
|
+
self.variable_replacer.local_variables[var_name] = expr_value
|
221
|
+
self.test_context.set(var_name, expr_value) # 同时添加到测试上下文
|
222
|
+
allure.attach(
|
223
|
+
f"变量: {var_name}\n值: {expr_value}",
|
224
|
+
name="赋值详情",
|
225
|
+
attachment_type=allure.attachment_type.TEXT
|
226
|
+
)
|
227
|
+
|
228
|
+
@allure.step("关键字调用赋值")
|
229
|
+
def _handle_assignment_keyword_call(self, node):
|
230
|
+
"""处理关键字调用赋值"""
|
231
|
+
var_name = node.value
|
232
|
+
keyword_call_node = node.children[0]
|
233
|
+
result = self.execute(keyword_call_node)
|
234
|
+
|
235
|
+
if result is not None:
|
236
|
+
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
237
|
+
if var_name.startswith('g_'):
|
238
|
+
global_context.set_variable(var_name, result)
|
239
|
+
allure.attach(
|
240
|
+
f"全局变量: {var_name}\n值: {result}",
|
241
|
+
name="全局变量赋值",
|
242
|
+
attachment_type=allure.attachment_type.TEXT
|
243
|
+
)
|
244
|
+
else:
|
245
|
+
# 存储在本地变量字典和测试上下文中
|
246
|
+
self.variable_replacer.local_variables[var_name] = result
|
247
|
+
self.test_context.set(var_name, result) # 同时添加到测试上下文
|
248
|
+
allure.attach(
|
249
|
+
f"变量: {var_name}\n值: {result}",
|
250
|
+
name="赋值详情",
|
251
|
+
attachment_type=allure.attachment_type.TEXT
|
252
|
+
)
|
253
|
+
else:
|
254
|
+
raise Exception(f"关键字 {keyword_call_node.value} 没有返回结果")
|
255
|
+
|
256
|
+
@allure.step("执行循环")
|
257
|
+
def _handle_for_loop(self, node):
|
258
|
+
"""处理for循环"""
|
259
|
+
var_name = node.value
|
260
|
+
start = self.eval_expression(node.children[0])
|
261
|
+
end = self.eval_expression(node.children[1])
|
262
|
+
|
263
|
+
for i in range(int(start), int(end)):
|
264
|
+
# 存储在本地变量字典和测试上下文中
|
265
|
+
self.variable_replacer.local_variables[var_name] = i
|
266
|
+
self.test_context.set(var_name, i) # 同时添加到测试上下文
|
267
|
+
with allure.step(f"循环轮次: {var_name} = {i}"):
|
268
|
+
self.execute(node.children[2])
|
269
|
+
|
270
|
+
def _execute_keyword_call(self, node):
|
271
|
+
"""执行关键字调用"""
|
272
|
+
keyword_name = node.value
|
273
|
+
keyword_info = keyword_manager.get_keyword_info(keyword_name)
|
274
|
+
if not keyword_info:
|
275
|
+
raise Exception(f"未注册的关键字: {keyword_name}")
|
276
|
+
|
277
|
+
kwargs = self._prepare_keyword_params(node, keyword_info)
|
278
|
+
|
279
|
+
try:
|
280
|
+
# 由于KeywordManager中的wrapper已经添加了allure.step和日志,这里不再重复添加
|
281
|
+
result = keyword_manager.execute(keyword_name, **kwargs)
|
282
|
+
return result
|
283
|
+
except Exception as e:
|
284
|
+
# 异常会在KeywordManager的wrapper中记录,这里只需要向上抛出
|
285
|
+
raise
|
286
|
+
|
287
|
+
def _prepare_keyword_params(self, node, keyword_info):
|
288
|
+
"""准备关键字调用参数"""
|
289
|
+
mapping = keyword_info.get('mapping', {})
|
290
|
+
kwargs = {'context': self.test_context} # 默认传入context参数
|
291
|
+
|
292
|
+
# 检查是否有参数列表
|
293
|
+
if node.children[0]:
|
294
|
+
for param in node.children[0]:
|
295
|
+
param_name = param.value
|
296
|
+
english_param_name = mapping.get(param_name, param_name)
|
297
|
+
# 对参数值进行变量替换
|
298
|
+
param_value = self.eval_expression(param.children[0])
|
299
|
+
kwargs[english_param_name] = param_value
|
300
|
+
|
301
|
+
return kwargs
|
302
|
+
|
303
|
+
@allure.step("执行清理操作")
|
304
|
+
def _handle_teardown(self, node):
|
305
|
+
"""处理清理操作"""
|
306
|
+
self.execute(node.children[0])
|
307
|
+
|
308
|
+
def execute(self, node):
|
309
|
+
"""执行AST节点"""
|
310
|
+
handlers = {
|
311
|
+
'Start': self._handle_start,
|
312
|
+
'Metadata': lambda _: None,
|
313
|
+
'Statements': self._handle_statements,
|
314
|
+
'Assignment': self._handle_assignment,
|
315
|
+
'AssignmentKeywordCall': self._handle_assignment_keyword_call,
|
316
|
+
'ForLoop': self._handle_for_loop,
|
317
|
+
'KeywordCall': self._execute_keyword_call,
|
318
|
+
'Teardown': self._handle_teardown
|
319
|
+
}
|
320
|
+
|
321
|
+
handler = handlers.get(node.type)
|
322
|
+
if handler:
|
323
|
+
return handler(node)
|
324
|
+
raise Exception(f"未知的节点类型: {node.type}")
|
325
|
+
|
326
|
+
def read_file(filename):
|
327
|
+
"""读取 DSL 文件内容"""
|
328
|
+
with open(filename, 'r', encoding='utf-8') as f:
|
329
|
+
return f.read()
|
@@ -0,0 +1,84 @@
|
|
1
|
+
"""DSL执行器工具模块
|
2
|
+
|
3
|
+
该模块提供DSL文件的读取和执行功能,作为conftest.py和DSL执行器之间的桥梁。
|
4
|
+
"""
|
5
|
+
|
6
|
+
from pathlib import Path
|
7
|
+
from pytest_dsl.core.dsl_executor import DSLExecutor
|
8
|
+
from pytest_dsl.core.lexer import get_lexer
|
9
|
+
from pytest_dsl.core.parser import get_parser
|
10
|
+
|
11
|
+
# 获取词法分析器和解析器实例
|
12
|
+
lexer = get_lexer()
|
13
|
+
parser = get_parser()
|
14
|
+
|
15
|
+
|
16
|
+
def read_file(filename):
|
17
|
+
"""读取DSL文件内容
|
18
|
+
|
19
|
+
Args:
|
20
|
+
filename: 文件路径
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
str: 文件内容
|
24
|
+
"""
|
25
|
+
with open(filename, 'r', encoding='utf-8') as f:
|
26
|
+
return f.read()
|
27
|
+
|
28
|
+
|
29
|
+
def execute_dsl_file(filename: str) -> None:
|
30
|
+
"""执行DSL文件
|
31
|
+
|
32
|
+
Args:
|
33
|
+
filename: DSL文件路径
|
34
|
+
"""
|
35
|
+
try:
|
36
|
+
# 读取文件内容
|
37
|
+
with open(filename, 'r', encoding='utf-8') as f:
|
38
|
+
content = f.read()
|
39
|
+
|
40
|
+
# 创建词法分析器和语法分析器
|
41
|
+
lexer = get_lexer()
|
42
|
+
parser = get_parser()
|
43
|
+
|
44
|
+
# 解析DSL文件
|
45
|
+
ast = parser.parse(content, lexer=lexer)
|
46
|
+
|
47
|
+
# 创建执行器并执行
|
48
|
+
executor = DSLExecutor()
|
49
|
+
executor.execute(ast)
|
50
|
+
|
51
|
+
except Exception as e:
|
52
|
+
# 如果是语法错误,记录并抛出
|
53
|
+
if "语法错误" in str(e):
|
54
|
+
print(f"DSL语法错误: {str(e)}")
|
55
|
+
raise
|
56
|
+
# 其他错误直接抛出
|
57
|
+
raise
|
58
|
+
|
59
|
+
|
60
|
+
def extract_metadata_from_ast(ast):
|
61
|
+
"""从AST中提取元数据
|
62
|
+
|
63
|
+
提取DSL文件中的元数据信息,如@data和@name标记。
|
64
|
+
|
65
|
+
Args:
|
66
|
+
ast: 解析后的抽象语法树
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
tuple: (data_source, test_title) 元组,如果不存在则为None
|
70
|
+
"""
|
71
|
+
data_source = None
|
72
|
+
test_title = None
|
73
|
+
|
74
|
+
for child in ast.children:
|
75
|
+
if child.type == 'Metadata':
|
76
|
+
for item in child.children:
|
77
|
+
if item.type == '@data':
|
78
|
+
data_source = item.value
|
79
|
+
elif item.type == '@name':
|
80
|
+
test_title = item.value
|
81
|
+
if data_source and test_title:
|
82
|
+
break
|
83
|
+
|
84
|
+
return data_source, test_title
|
@@ -0,0 +1,103 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
import tempfile
|
4
|
+
import allure
|
5
|
+
from typing import Dict, Any, Optional
|
6
|
+
from filelock import FileLock
|
7
|
+
from .yaml_vars import yaml_vars
|
8
|
+
|
9
|
+
|
10
|
+
class GlobalContext:
|
11
|
+
"""全局上下文管理器,支持多进程环境下的变量共享"""
|
12
|
+
|
13
|
+
def __init__(self):
|
14
|
+
# 使用临时目录存储全局变量
|
15
|
+
self._storage_dir = os.path.join(
|
16
|
+
tempfile.gettempdir(), "pytest_dsl_global_vars")
|
17
|
+
os.makedirs(self._storage_dir, exist_ok=True)
|
18
|
+
self._storage_file = os.path.join(
|
19
|
+
self._storage_dir, "global_vars.json")
|
20
|
+
self._lock_file = os.path.join(self._storage_dir, "global_vars.lock")
|
21
|
+
|
22
|
+
def set_variable(self, name: str, value: Any) -> None:
|
23
|
+
"""设置全局变量"""
|
24
|
+
with FileLock(self._lock_file):
|
25
|
+
variables = self._load_variables()
|
26
|
+
variables[name] = value
|
27
|
+
self._save_variables(variables)
|
28
|
+
|
29
|
+
allure.attach(
|
30
|
+
f"全局变量: {name}\n值: {value}",
|
31
|
+
name="全局变量设置",
|
32
|
+
attachment_type=allure.attachment_type.TEXT
|
33
|
+
)
|
34
|
+
|
35
|
+
def get_variable(self, name: str) -> Any:
|
36
|
+
"""获取全局变量,优先从YAML变量中获取"""
|
37
|
+
# 首先尝试从YAML变量中获取
|
38
|
+
yaml_value = yaml_vars.get_variable(name)
|
39
|
+
if yaml_value is not None:
|
40
|
+
return yaml_value
|
41
|
+
|
42
|
+
# 如果YAML中没有,则从全局变量存储中获取
|
43
|
+
with FileLock(self._lock_file):
|
44
|
+
variables = self._load_variables()
|
45
|
+
return variables.get(name)
|
46
|
+
|
47
|
+
def has_variable(self, name: str) -> bool:
|
48
|
+
"""检查全局变量是否存在(包括YAML变量)"""
|
49
|
+
# 首先检查YAML变量
|
50
|
+
if yaml_vars.get_variable(name) is not None:
|
51
|
+
return True
|
52
|
+
|
53
|
+
# 然后检查全局变量存储
|
54
|
+
with FileLock(self._lock_file):
|
55
|
+
variables = self._load_variables()
|
56
|
+
return name in variables
|
57
|
+
|
58
|
+
def delete_variable(self, name: str) -> None:
|
59
|
+
"""删除全局变量(仅删除存储的变量,不影响YAML变量)"""
|
60
|
+
with FileLock(self._lock_file):
|
61
|
+
variables = self._load_variables()
|
62
|
+
if name in variables:
|
63
|
+
del variables[name]
|
64
|
+
self._save_variables(variables)
|
65
|
+
|
66
|
+
allure.attach(
|
67
|
+
f"删除全局变量: {name}",
|
68
|
+
name="全局变量删除",
|
69
|
+
attachment_type=allure.attachment_type.TEXT
|
70
|
+
)
|
71
|
+
|
72
|
+
def clear_all(self) -> None:
|
73
|
+
"""清除所有全局变量(包括YAML变量)"""
|
74
|
+
with FileLock(self._lock_file):
|
75
|
+
self._save_variables({})
|
76
|
+
|
77
|
+
# 清除YAML变量
|
78
|
+
yaml_vars.clear()
|
79
|
+
|
80
|
+
allure.attach(
|
81
|
+
"清除所有全局变量",
|
82
|
+
name="全局变量清除",
|
83
|
+
attachment_type=allure.attachment_type.TEXT
|
84
|
+
)
|
85
|
+
|
86
|
+
def _load_variables(self) -> Dict[str, Any]:
|
87
|
+
"""从文件加载变量"""
|
88
|
+
if not os.path.exists(self._storage_file):
|
89
|
+
return {}
|
90
|
+
try:
|
91
|
+
with open(self._storage_file, 'r', encoding='utf-8') as f:
|
92
|
+
return json.load(f)
|
93
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
94
|
+
return {}
|
95
|
+
|
96
|
+
def _save_variables(self, variables: Dict[str, Any]) -> None:
|
97
|
+
"""保存变量到文件"""
|
98
|
+
with open(self._storage_file, 'w', encoding='utf-8') as f:
|
99
|
+
json.dump(variables, f, ensure_ascii=False, indent=2)
|
100
|
+
|
101
|
+
|
102
|
+
# 创建全局上下文管理器实例
|
103
|
+
global_context = GlobalContext()
|