api-engine-xin 0.0.28__tar.gz → 0.0.29__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.
- api_engine_xin-0.0.29/ApiEngine/core.py +195 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/engine/base_case.py +13 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/engine/precondition.py +1 -1
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/infra/__init__.py +0 -1
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/infra/suite_store.py +22 -6
- api_engine_xin-0.0.29/PKG-INFO +539 -0
- api_engine_xin-0.0.29/README.md +512 -0
- api_engine_xin-0.0.29/api_engine_xin.egg-info/PKG-INFO +539 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/api_engine_xin.egg-info/SOURCES.txt +1 -1
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/setup.py +1 -1
- api_engine_xin-0.0.28/ApiEngine/core.py +0 -486
- api_engine_xin-0.0.28/PKG-INFO +0 -672
- api_engine_xin-0.0.28/README.md +0 -645
- api_engine_xin-0.0.28/api_engine_xin.egg-info/PKG-INFO +0 -672
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/__init__.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/engine/__init__.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/engine/assertion.py +0 -0
- {api_engine_xin-0.0.28/ApiEngine/infra → api_engine_xin-0.0.29/ApiEngine/engine}/exceptions.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/engine/extractor.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/engine/replacer.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/engine/script_runner.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/http/__init__.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/http/client.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/http/files.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/infra/case_log.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/infra/db_client.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/infra/global_func.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/ApiEngine/infra/test_result.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/LICENSE +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/api_engine_xin.egg-info/dependency_links.txt +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/api_engine_xin.egg-info/requires.txt +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/api_engine_xin.egg-info/top_level.txt +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/setup.cfg +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/tests/Tools.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/tests/__init__.py +0 -0
- {api_engine_xin-0.0.28 → api_engine_xin-0.0.29}/tests/runTest.py +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from ApiEngine import log
|
|
5
|
+
from ApiEngine.infra import global_func
|
|
6
|
+
from ApiEngine.engine.exceptions import PreconditionChainError
|
|
7
|
+
from ApiEngine.infra.test_result import TestResult
|
|
8
|
+
from ApiEngine.infra.db_client import DBClient
|
|
9
|
+
from ApiEngine.infra.suite_store import RunRegistry, RunStore
|
|
10
|
+
from ApiEngine.engine.base_case import BaseCase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestRunner:
|
|
14
|
+
def __init__(self, env_data, run_id=None):
|
|
15
|
+
"""
|
|
16
|
+
:param env_data: 执行测试时的环境数据
|
|
17
|
+
:param run_id: 套件运行ID(传入时启用套件级变量共享)
|
|
18
|
+
"""
|
|
19
|
+
# 深拷贝环境数据,避免修改调用方原始数据
|
|
20
|
+
self.env_data = copy.deepcopy(env_data)
|
|
21
|
+
# debug_updates 保持原引用,变更对上层可见(仅无 run_id 的退化路径使用)
|
|
22
|
+
if "debug_updates" in env_data:
|
|
23
|
+
self.env_data["debug_updates"] = env_data["debug_updates"]
|
|
24
|
+
self.result = []
|
|
25
|
+
self._db = DBClient()
|
|
26
|
+
self._shared_env = {} # 实例级共享环境
|
|
27
|
+
self._run_id = run_id
|
|
28
|
+
self._suite_store: Optional[RunStore] = RunRegistry.get(run_id) if run_id else None
|
|
29
|
+
|
|
30
|
+
def execute_cases(self, testcases):
|
|
31
|
+
"""执行测试用例的方法"""
|
|
32
|
+
# 根据数据库的配置初始化数据库的连接
|
|
33
|
+
db_config = self.env_data.pop("db", None)
|
|
34
|
+
if db_config:
|
|
35
|
+
self._db.init_connect(db_config)
|
|
36
|
+
|
|
37
|
+
# 统一初始化共享环境
|
|
38
|
+
self._shared_env.clear()
|
|
39
|
+
self._shared_env.update(self.env_data)
|
|
40
|
+
|
|
41
|
+
# 如果有 run_id,将 _shared_env["envs"] 和 debug_updates 指向套件级存储
|
|
42
|
+
# 这样所有变量读写(包括 Replacer、用户脚本、前置用例提取)自动走 _suite_stores
|
|
43
|
+
if self._suite_store:
|
|
44
|
+
self._shared_env["envs"] = self._suite_store.envs
|
|
45
|
+
self._shared_env["debug_updates"] = self._suite_store.debug_updates
|
|
46
|
+
self._shared_env["_run_store"] = self._suite_store # 供 BaseCase 心跳 touch
|
|
47
|
+
|
|
48
|
+
self._load_global_func()
|
|
49
|
+
|
|
50
|
+
# 判断测试数据参数的类型
|
|
51
|
+
if isinstance(testcases, dict):
|
|
52
|
+
cases = testcases.get("cases")
|
|
53
|
+
if cases:
|
|
54
|
+
log.info_log("执行测试套件:", testcases["name"])
|
|
55
|
+
test_result = TestResult(all=len(testcases["cases"]), name=testcases["name"])
|
|
56
|
+
for case in testcases["cases"]:
|
|
57
|
+
log.info_log(case)
|
|
58
|
+
self.perform_case(case, test_result)
|
|
59
|
+
res = test_result.get_result_info()
|
|
60
|
+
self.result.append(res)
|
|
61
|
+
else:
|
|
62
|
+
log.info_log("调试单条接口用例:", testcases["title"])
|
|
63
|
+
test_result = TestResult(all=1)
|
|
64
|
+
self.perform_case(testcases, test_result)
|
|
65
|
+
res = test_result.get_result_info()
|
|
66
|
+
self.result = res["cases"][0]
|
|
67
|
+
elif isinstance(testcases, list):
|
|
68
|
+
results = []
|
|
69
|
+
for items in testcases:
|
|
70
|
+
log.info_log("执行测试套件:", items["name"])
|
|
71
|
+
test_result = TestResult(all=len(items["cases"]), name=items["name"])
|
|
72
|
+
for case in items["cases"]:
|
|
73
|
+
self.perform_case(case, test_result)
|
|
74
|
+
res = test_result.get_result_info()
|
|
75
|
+
results.append(res)
|
|
76
|
+
total_all = 0
|
|
77
|
+
total_success = 0
|
|
78
|
+
total_fail = 0
|
|
79
|
+
total_error = 0
|
|
80
|
+
for scence_result in results:
|
|
81
|
+
total_all += scence_result["all"]
|
|
82
|
+
total_success += scence_result["success"]
|
|
83
|
+
total_fail += scence_result["fail"]
|
|
84
|
+
total_error += scence_result["error"]
|
|
85
|
+
self.result = {
|
|
86
|
+
"results": results,
|
|
87
|
+
"all": total_all,
|
|
88
|
+
"success": total_success,
|
|
89
|
+
"fail": total_fail,
|
|
90
|
+
"error": total_error
|
|
91
|
+
}
|
|
92
|
+
else:
|
|
93
|
+
log.error_log("测试数据格式错误")
|
|
94
|
+
|
|
95
|
+
# 断开数据库连接
|
|
96
|
+
if db_config:
|
|
97
|
+
self._db.close_db_connect()
|
|
98
|
+
return self.result
|
|
99
|
+
|
|
100
|
+
def perform_case(self, case, test_result):
|
|
101
|
+
"""执行单条用例"""
|
|
102
|
+
# 心跳:每开始一条用例就刷新活跃时间,防止长套件被 cleanup 误清
|
|
103
|
+
if self._run_id:
|
|
104
|
+
RunRegistry.touch(self._run_id)
|
|
105
|
+
# 每次创建新的 BaseCase 实例,传入共享环境引用
|
|
106
|
+
c = BaseCase(shared_env=self._shared_env)
|
|
107
|
+
c._db = self._db
|
|
108
|
+
try:
|
|
109
|
+
c.perform(case)
|
|
110
|
+
except PreconditionChainError:
|
|
111
|
+
test_result.add_fail(c)
|
|
112
|
+
except AssertionError:
|
|
113
|
+
test_result.add_fail(c)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
test_result.add_error(c, e)
|
|
116
|
+
else:
|
|
117
|
+
test_result.add_success(c)
|
|
118
|
+
|
|
119
|
+
def _load_global_func(self):
|
|
120
|
+
"""安全加载用户自定义函数,避免跨执行污染"""
|
|
121
|
+
_gf = self.env_data.get("global_func")
|
|
122
|
+
if not (_gf and isinstance(_gf, str)):
|
|
123
|
+
return
|
|
124
|
+
# 只保留 __name__ 等双下划线内置属性
|
|
125
|
+
builtins = {k: v for k, v in global_func.__dict__.items() if k.startswith('__')}
|
|
126
|
+
global_func.__dict__.clear()
|
|
127
|
+
global_func.__dict__.update(builtins)
|
|
128
|
+
exec(_gf, global_func.__dict__)
|
|
129
|
+
|
|
130
|
+
def get_env_snapshot(self) -> dict:
|
|
131
|
+
"""获取执行后的环境变量快照(供上层平台读取)
|
|
132
|
+
|
|
133
|
+
返回包含 envs、debug_updates 的字典,
|
|
134
|
+
用于上层平台同步临时变量和持久化全局变量变更。
|
|
135
|
+
|
|
136
|
+
debug_updates 中的约定:
|
|
137
|
+
- value 非 None → 新增/更新全局变量
|
|
138
|
+
- value 为 None → 删除全局变量
|
|
139
|
+
"""
|
|
140
|
+
return {
|
|
141
|
+
"envs": copy.deepcopy(dict(self._shared_env.get("envs") or {})),
|
|
142
|
+
"debug_updates": copy.deepcopy(self._shared_env.get("debug_updates") or {}),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# ==================== 套件级共享变量:生命周期管理 ====================
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def register_run(cls, run_id: str, initial_envs: dict) -> None:
|
|
149
|
+
"""套件开始前注册共享存储。
|
|
150
|
+
|
|
151
|
+
:param run_id: 唯一运行标识(建议格式: suite-{id}-{run_id}-{timestamp})
|
|
152
|
+
:param initial_envs: 初始环境变量(用户配置的基线值,含 DB 全局变量)
|
|
153
|
+
"""
|
|
154
|
+
RunRegistry.register(run_id, initial_envs)
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def unregister_run(cls, run_id: str) -> None:
|
|
158
|
+
"""套件结束后清理共享存储。务必在 finally 中调用。"""
|
|
159
|
+
RunRegistry.unregister(run_id)
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def get_debug_updates(cls, run_id: str) -> dict:
|
|
163
|
+
"""获取需要持久化到 DB 的全局变量变更队列。
|
|
164
|
+
|
|
165
|
+
返回 dict 副本:
|
|
166
|
+
- value 非 None → upsert
|
|
167
|
+
- value 为 None → delete
|
|
168
|
+
"""
|
|
169
|
+
store = RunRegistry.get(run_id)
|
|
170
|
+
if not store:
|
|
171
|
+
return {}
|
|
172
|
+
with store.lock:
|
|
173
|
+
return dict(store.debug_updates)
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def clear_debug_updates(cls, run_id: str) -> None:
|
|
177
|
+
"""重置变更队列(平台写 DB 后调用,避免重复写入)。"""
|
|
178
|
+
store = RunRegistry.get(run_id)
|
|
179
|
+
if store:
|
|
180
|
+
with store.lock:
|
|
181
|
+
store.debug_updates.clear()
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def get_run_globals(cls, run_id: str) -> dict:
|
|
185
|
+
"""获取当前套件累积的全部环境变量(含初始基线 + 运行时设置)。"""
|
|
186
|
+
store = RunRegistry.get(run_id)
|
|
187
|
+
if not store:
|
|
188
|
+
return {}
|
|
189
|
+
with store.lock:
|
|
190
|
+
return dict(store.envs)
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def cleanup_stale_runs(cls, max_age_seconds: float = 3600) -> None:
|
|
194
|
+
"""清理超时的残留 store(防止异常中断导致内存泄漏)。"""
|
|
195
|
+
RunRegistry.cleanup_stale(max_age_seconds)
|
|
@@ -255,8 +255,18 @@ class BaseCase(CaseLogHandler):
|
|
|
255
255
|
|
|
256
256
|
# ==================== 环境变量操作 ====================
|
|
257
257
|
|
|
258
|
+
def _touch_store(self):
|
|
259
|
+
"""心跳:刷新 RunStore 活跃时间,防止长运行被误清"""
|
|
260
|
+
try:
|
|
261
|
+
run_store = self._shared_env.get("_run_store")
|
|
262
|
+
if run_store:
|
|
263
|
+
run_store.touch()
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
|
|
258
267
|
def _save_env_variable(self, key, value):
|
|
259
268
|
"""保存临时环境变量"""
|
|
269
|
+
self._touch_store()
|
|
260
270
|
self.info_log(f"保存(临时)环境变量:{key} = {value}")
|
|
261
271
|
self.env[key] = value
|
|
262
272
|
try:
|
|
@@ -272,6 +282,7 @@ class BaseCase(CaseLogHandler):
|
|
|
272
282
|
|
|
273
283
|
def del_evn_variable(self, key):
|
|
274
284
|
"""删除测试运行环境变量"""
|
|
285
|
+
self._touch_store()
|
|
275
286
|
self.info_log(f"删除(临时)环境变量:{key}")
|
|
276
287
|
self.env.pop(key, None)
|
|
277
288
|
# 同时从套件级共享存储中删除,防止后续用例读到已删除的变量
|
|
@@ -284,6 +295,7 @@ class BaseCase(CaseLogHandler):
|
|
|
284
295
|
|
|
285
296
|
def save_global_variable(self, key, value):
|
|
286
297
|
"""保存测试运行环境的全局变量"""
|
|
298
|
+
self._touch_store()
|
|
287
299
|
self.info_log(f"保存全局变量:{key} = {value}")
|
|
288
300
|
envs = self._shared_env.get("envs")
|
|
289
301
|
if isinstance(envs, dict):
|
|
@@ -298,6 +310,7 @@ class BaseCase(CaseLogHandler):
|
|
|
298
310
|
|
|
299
311
|
def del_global_variable(self, key):
|
|
300
312
|
"""删除测试运行环境的全局变量"""
|
|
313
|
+
self._touch_store()
|
|
301
314
|
self.info_log(f"删除全局变量:{key}")
|
|
302
315
|
envs = self._shared_env.get("envs")
|
|
303
316
|
if isinstance(envs, dict) and key in envs:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from ApiEngine.
|
|
1
|
+
from ApiEngine.engine.exceptions import PreconditionChainError
|
|
2
2
|
from ApiEngine.http.client import HttpClient
|
|
3
3
|
from ApiEngine.engine.script_runner import ScriptRunner
|
|
4
4
|
from ApiEngine.engine.extractor import Extractor
|
|
@@ -23,17 +23,23 @@ from typing import Dict, Optional, Set
|
|
|
23
23
|
class RunStore:
|
|
24
24
|
"""单次套件运行的共享变量存储。
|
|
25
25
|
|
|
26
|
-
envs:
|
|
27
|
-
debug_updates:
|
|
28
|
-
temp_keys:
|
|
29
|
-
lock:
|
|
30
|
-
created_at:
|
|
26
|
+
envs: 套件级累积环境变量(含初始基线 + 运行中写入)
|
|
27
|
+
debug_updates: 需要持久化到 DB 的全局变量变更队列
|
|
28
|
+
temp_keys: 标记哪些 key 是 save_env_variable 写入的(区分 temp 与 global)
|
|
29
|
+
lock: 保护并发写操作
|
|
30
|
+
created_at: 注册时间戳(仅用于诊断日志)
|
|
31
|
+
last_active_at: 最后一次变量读写的时间戳(cleanup 依据)
|
|
31
32
|
"""
|
|
32
33
|
envs: dict = field(default_factory=dict)
|
|
33
34
|
debug_updates: dict = field(default_factory=dict)
|
|
34
35
|
temp_keys: Set[str] = field(default_factory=set)
|
|
35
36
|
lock: threading.Lock = field(default_factory=threading.Lock)
|
|
36
37
|
created_at: float = field(default_factory=time.time)
|
|
38
|
+
last_active_at: float = field(default_factory=time.time)
|
|
39
|
+
|
|
40
|
+
def touch(self) -> None:
|
|
41
|
+
"""刷新活跃时间,防止长运行套件被 cleanup_stale 误清。"""
|
|
42
|
+
self.last_active_at = time.time()
|
|
37
43
|
|
|
38
44
|
|
|
39
45
|
class RunRegistry:
|
|
@@ -64,10 +70,20 @@ class RunRegistry:
|
|
|
64
70
|
with cls._registry_lock:
|
|
65
71
|
cls._stores.pop(run_id, None)
|
|
66
72
|
|
|
73
|
+
@classmethod
|
|
74
|
+
def touch(cls, run_id: str) -> None:
|
|
75
|
+
"""刷新指定运行的活跃时间。变量读写时调用。"""
|
|
76
|
+
store = cls._stores.get(run_id)
|
|
77
|
+
if store:
|
|
78
|
+
store.touch()
|
|
79
|
+
|
|
67
80
|
@classmethod
|
|
68
81
|
def cleanup_stale(cls, max_age_seconds: float = 3600) -> int:
|
|
69
82
|
"""清理超时的残留 store(防止异常中断导致内存泄漏)。
|
|
70
83
|
|
|
84
|
+
依据 last_active_at(最后一次变量读写)而非 created_at,
|
|
85
|
+
因此长时间运行但仍在活跃的套件不会被误清。
|
|
86
|
+
|
|
71
87
|
:param max_age_seconds: 超时阈值(秒),默认1小时
|
|
72
88
|
:return: 清理数量
|
|
73
89
|
"""
|
|
@@ -75,7 +91,7 @@ class RunRegistry:
|
|
|
75
91
|
with cls._registry_lock:
|
|
76
92
|
stale = [
|
|
77
93
|
rid for rid, s in cls._stores.items()
|
|
78
|
-
if now - s.
|
|
94
|
+
if now - s.last_active_at > max_age_seconds
|
|
79
95
|
]
|
|
80
96
|
for rid in stale:
|
|
81
97
|
cls._stores.pop(rid, None)
|