api-engine-xin 0.0.28__tar.gz → 0.0.30__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 (36) hide show
  1. api_engine_xin-0.0.30/ApiEngine/core.py +195 -0
  2. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/engine/base_case.py +13 -0
  3. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/engine/precondition.py +1 -1
  4. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/engine/script_runner.py +14 -3
  5. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/infra/__init__.py +0 -1
  6. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/infra/suite_store.py +22 -6
  7. api_engine_xin-0.0.30/PKG-INFO +539 -0
  8. api_engine_xin-0.0.30/README.md +512 -0
  9. api_engine_xin-0.0.30/api_engine_xin.egg-info/PKG-INFO +539 -0
  10. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/api_engine_xin.egg-info/SOURCES.txt +1 -1
  11. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/setup.py +1 -1
  12. api_engine_xin-0.0.28/ApiEngine/core.py +0 -486
  13. api_engine_xin-0.0.28/PKG-INFO +0 -672
  14. api_engine_xin-0.0.28/README.md +0 -645
  15. api_engine_xin-0.0.28/api_engine_xin.egg-info/PKG-INFO +0 -672
  16. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/__init__.py +0 -0
  17. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/engine/__init__.py +0 -0
  18. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/engine/assertion.py +0 -0
  19. {api_engine_xin-0.0.28/ApiEngine/infra → api_engine_xin-0.0.30/ApiEngine/engine}/exceptions.py +0 -0
  20. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/engine/extractor.py +0 -0
  21. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/engine/replacer.py +0 -0
  22. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/http/__init__.py +0 -0
  23. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/http/client.py +0 -0
  24. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/http/files.py +0 -0
  25. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/infra/case_log.py +0 -0
  26. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/infra/db_client.py +0 -0
  27. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/infra/global_func.py +0 -0
  28. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/ApiEngine/infra/test_result.py +0 -0
  29. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/LICENSE +0 -0
  30. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/api_engine_xin.egg-info/dependency_links.txt +0 -0
  31. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/api_engine_xin.egg-info/requires.txt +0 -0
  32. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/api_engine_xin.egg-info/top_level.txt +0 -0
  33. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/setup.cfg +0 -0
  34. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/tests/Tools.py +0 -0
  35. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/tests/__init__.py +0 -0
  36. {api_engine_xin-0.0.28 → api_engine_xin-0.0.30}/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.infra.exceptions import PreconditionChainError
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
@@ -19,16 +19,27 @@ class ScriptRunner:
19
19
  print = test_instance.print_log
20
20
  test_instance.env = {}
21
21
 
22
+ # 显式 globals 命名空间,确保 import 语句和嵌套作用域(生成器表达式/lambda)正常工作
23
+ _namespace = {
24
+ "__builtins__": __builtins__,
25
+ "test": test,
26
+ "ENV": ENV,
27
+ "global_var": global_var,
28
+ "print": print,
29
+ "global_func": global_func,
30
+ "db": db,
31
+ }
32
+
22
33
  # 执行前置脚本
23
34
  setup_scripts = data.get("setup_script")
24
35
  if setup_scripts and isinstance(setup_scripts, str):
25
- exec(setup_scripts)
36
+ exec(setup_scripts, _namespace)
26
37
 
27
38
  response = yield
28
39
 
29
- # 执行后置脚本
40
+ # 执行后置脚本(复用 _namespace,保留前置脚本中 import 的模块和定义的变量)
30
41
  teardown_scripts = data.get("teardown_script")
31
42
  if teardown_scripts and isinstance(teardown_scripts, str):
32
- exec(teardown_scripts)
43
+ exec(teardown_scripts, _namespace)
33
44
 
34
45
  yield
@@ -1,3 +1,2 @@
1
1
  # ApiEngine 基础设施层
2
2
  from ApiEngine.infra.case_log import CaseLogHandler
3
- from ApiEngine.infra.exceptions import PreconditionChainError
@@ -23,17 +23,23 @@ from typing import Dict, Optional, Set
23
23
  class RunStore:
24
24
  """单次套件运行的共享变量存储。
25
25
 
26
- envs: 套件级累积环境变量(含初始基线 + 运行中写入)
27
- debug_updates: 需要持久化到 DB 的全局变量变更队列
28
- temp_keys: 标记哪些 key 是 save_env_variable 写入的(区分 temp 与 global)
29
- lock: 保护并发写操作
30
- created_at: 注册时间戳(用于 TTL 清理)
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.created_at > max_age_seconds
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)