feat(chv): 架构/可维护性/前端只读侧推进——生产实验分离、配置片段化、VM 只读展示
架构:WarmPool 从 cloud/chv/pool.py 移到 experimental_pool.py, 生产残留清理彻底分文件到 cloud/chv/orphans.py(facade 直接读 registry 文件清理,不构造池、不碰 prewarm/acquire/release); cloud/chv 与 cloud/sandbox 不再导出 WarmPool,旧路径一律 ImportError(CHV 未合并、无生产用户,兼容保留已无保留理由); worker 改走 orphans facade,不再持有 _warm_pool;#215 对齐从 接口形状推进到 REPLACEMENT_POINTS 替换清单(R1/R2 可机械替换、 R3 永不替换,#215 模块当前不存在由测试钉住)。 可维护性:build_chv_config 拆成 base/ssh/network/quota/disk/ timeout 六片段(组装只做 dict 合并,键冲突即报错;38 字段集 与取值不变,单项非法报错文案逐字一致);ADR-12 模块职责图 与禁 import 清单(AST 钉住);cloud/chv/ 公开 API 全量类型 标注、无 Any 逃逸,并修正三处旧标注错误;死代码扫一遍 (删实验池已无调用的 _quarantine 委托与未用 _LOGGER)。 前端(只读侧,不做控制按钮):chv.ts 新增纯逻辑谓词与文案 (执行器精确匹配、附件守卫、VM 状态文案、列表徽标);列表 行新增 VM 已报告死亡徽标;详情对全部 CHV 任务显示 VM 行 (已报告死亡/未报告死亡,不猜 live);WorkersSection 与成本 卡沿用已有数据,不新开池管理/成本页;服务端未新增第二个 生命周期字段(持久化事实只有三项,chv_vm_dead 已是完整派生), 只把派生抽成 _chv_vm_dead_from_row 并补 goal 回落形态测试。
This commit is contained in:
1 parent
42de20da68
commit
df8c64bd64
30 files changed
+1263
-331
No files matched your search
@@ -14,7 +14,7 @@ import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from cloud.chv.pool import WarmPool
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.sandbox import (
|
||||
ChvFailure,
|
||||
CloudHypervisorExecutor,
|
||||
@@ -23,7 +23,7 @@ try:
|
||||
)
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.chv.pool import WarmPool
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.sandbox import (
|
||||
ChvFailure,
|
||||
CloudHypervisorExecutor,
|
||||
|
||||
@@ -19,14 +19,14 @@ from pathlib import Path
|
||||
try:
|
||||
from cloud.chv.compat215 import resolve_inside, tmpfs_isolated
|
||||
from cloud.chv.hardening import validate_snapshot_url
|
||||
from cloud.chv.pool import reconcile_orphans_for_executor
|
||||
from cloud.chv.orphans import reconcile_orphans_for_executor
|
||||
from cloud.sandbox import CloudHypervisorExecutor
|
||||
from cloud.server import _task_to_dict
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.chv.compat215 import resolve_inside, tmpfs_isolated
|
||||
from cloud.chv.hardening import validate_snapshot_url
|
||||
from cloud.chv.pool import reconcile_orphans_for_executor
|
||||
from cloud.chv.orphans import reconcile_orphans_for_executor
|
||||
from cloud.sandbox import CloudHypervisorExecutor
|
||||
from cloud.server import _task_to_dict
|
||||
|
||||
@@ -92,10 +92,11 @@ class Compat215Test(unittest.TestCase):
|
||||
|
||||
class PoolFacadeTest(unittest.TestCase):
|
||||
def test_facade_only_reconciles_never_acquires(self):
|
||||
# facade 内部构造 WarmPool:用假执行器 + 空 registry,
|
||||
# 只要它不调 acquire/prewarm 就返回 [] 且不炸。
|
||||
# facade 直接读 registry 文件做清理(本轨不再构造
|
||||
# WarmPool):用假执行器 + 空 registry,返回 [] 不炸,
|
||||
# 且绝不调 acquire/prewarm(假执行器根本没有这两个方法)。
|
||||
ex = FakeExecutor()
|
||||
# WarmPool 构造需要 state_dir 属性;没有则 registry 为空
|
||||
# facade 需要 state_dir 属性定 registry 路径;没有则返回 []
|
||||
ex.state_dir = tempfile.mkdtemp()
|
||||
ex.warm_pool_size = 0
|
||||
ex.snapshot_url = None
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
"""CHV 架构/可维护性/前端(只读侧)推进轨测试。
|
||||
|
||||
本轨 feat-chv-arch-maint-fe,堆叠于 #254(feat-chv-
|
||||
arch95plus)。只用标准库与 mock,绝不启动真实 VM;
|
||||
测试里只用 FAKE 占位值,绝无真实密钥。覆盖:
|
||||
- WarmPool 破环性清理:旧 import 路径一律 ImportError,
|
||||
实验路径唯一,生产代码不 import 实验模块;
|
||||
- orphans facade:registry 语义(有效/非法/损坏/缺失/
|
||||
shutdown 抛错/_emit 抛错)与旧 WarmPool 逐项一致;
|
||||
- 配置片段:五片段键集互不相交、并集等于旧字段集、
|
||||
默认值与单项非法报错逐字一致、组装键冲突即报错;
|
||||
- ADR-12 模块依赖图:叶子模块只依赖标准库、禁 import
|
||||
清单、公开 API 全量类型标注且无 Any 逃逸;
|
||||
- #215 替换清单:每项 local 真实存在、#215 模块当前
|
||||
不存在、R3 永不替换;
|
||||
- 服务端只读字段:goal 回落形态(纯文本“VM 中途死亡”)
|
||||
同样派生 chv_vm_dead,非 VM 中断原因一律不派生。
|
||||
"""
|
||||
|
||||
import ast
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from cloud.chv import reconcile_orphans_for_executor
|
||||
from cloud.chv.compat215 import REPLACEMENT_POINTS
|
||||
from cloud.chv.compat215 import resolve_inside, tmpfs_isolated
|
||||
from cloud.chv.config import _assemble_chv_config
|
||||
from cloud.chv.config import _base_fragment
|
||||
from cloud.chv.config import _disk_fragment
|
||||
from cloud.chv.config import _network_fragment
|
||||
from cloud.chv.config import _quota_fragment
|
||||
from cloud.chv.config import _ssh_fragment
|
||||
from cloud.chv.config import _timeout_fragment
|
||||
from cloud.chv.config import build_chv_config
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.chv.orphans import _REGISTRY_FILENAME
|
||||
from cloud.chv.orphans import reconcile_orphans_for_executor as _facade
|
||||
from cloud.server import _chv_vm_dead_from_row, _task_to_dict
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.chv import reconcile_orphans_for_executor
|
||||
from cloud.chv.compat215 import REPLACEMENT_POINTS
|
||||
from cloud.chv.compat215 import resolve_inside, tmpfs_isolated
|
||||
from cloud.chv.config import _assemble_chv_config
|
||||
from cloud.chv.config import _base_fragment
|
||||
from cloud.chv.config import _disk_fragment
|
||||
from cloud.chv.config import _network_fragment
|
||||
from cloud.chv.config import _quota_fragment
|
||||
from cloud.chv.config import _ssh_fragment
|
||||
from cloud.chv.config import _timeout_fragment
|
||||
from cloud.chv.config import build_chv_config
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.chv.orphans import _REGISTRY_FILENAME
|
||||
from cloud.chv.orphans import reconcile_orphans_for_executor as _facade
|
||||
from cloud.server import _chv_vm_dead_from_row, _task_to_dict
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CHV = ROOT / "cloud" / "chv"
|
||||
|
||||
EXPECTED_FIELDS = {
|
||||
"binary", "kernel", "rootfs", "cpus", "memory", "state_dir",
|
||||
"kvm_path", "cpuinfo_path", "ch_remote", "model",
|
||||
"ssh_key", "ssh_user", "ssh_port", "host_key_mode",
|
||||
"guest_host_py", "cmdline",
|
||||
"net", "net_json", "ssh_host", "ssh_forward",
|
||||
"egress_proxy", "allow_direct_egress",
|
||||
"max_vms", "warm_pool_size",
|
||||
"disk_mode", "disk_gb", "reuse_experiment",
|
||||
"single_tenant_project", "snapshot_max_count",
|
||||
"snapshot_max_total_gb", "snapshot_retention_days",
|
||||
"snapshot_dir", "allow_remote_snapshot", "snapshot_url",
|
||||
"task_timeout", "api_timeout", "ready_timeout", "ready_interval",
|
||||
}
|
||||
|
||||
|
||||
class FakeProdExecutor:
|
||||
"""生产 facade 替身:只有 facade 允许碰的属性。
|
||||
|
||||
acquire/prewarm/release 一律不存在——facade 若敢碰,
|
||||
__getattr__ 直接抛错,测试即 fail。"""
|
||||
|
||||
def __init__(self, state_dir):
|
||||
self.state_dir = state_dir
|
||||
self.shutdowns = []
|
||||
self.emits = []
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in ("acquire", "prewarm", "release"):
|
||||
raise AssertionError("facade 不得碰实验接口:%s" % name)
|
||||
raise AttributeError(name)
|
||||
|
||||
def shutdown_vm(self, vm):
|
||||
self.shutdowns.append(vm)
|
||||
return True
|
||||
|
||||
def _emit(self, event, task, detail):
|
||||
self.emits.append(event)
|
||||
|
||||
|
||||
def _write_registry(state_dir, data):
|
||||
path = os.path.join(state_dir, _REGISTRY_FILENAME)
|
||||
if isinstance(data, str):
|
||||
with open(path, "w") as fh:
|
||||
fh.write(data)
|
||||
else:
|
||||
with open(path, "w") as fh:
|
||||
json.dump(data, fh)
|
||||
return path
|
||||
|
||||
|
||||
class BreakingCleanupTest(unittest.TestCase):
|
||||
def test_old_paths_raise_import_error(self):
|
||||
with self.assertRaises(ImportError):
|
||||
from cloud.chv import WarmPool # noqa: F401
|
||||
with self.assertRaises(ImportError):
|
||||
from cloud.sandbox import WarmPool # noqa: F401
|
||||
with self.assertRaises(ImportError):
|
||||
import cloud.chv.pool # noqa: F401
|
||||
|
||||
def test_new_paths_and_identity(self):
|
||||
self.assertEqual(WarmPool.__module__,
|
||||
"cloud.chv.experimental_pool")
|
||||
self.assertIs(reconcile_orphans_for_executor, _facade)
|
||||
import cloud.sandbox as sb
|
||||
self.assertIs(sb.reconcile_orphans_for_executor, _facade)
|
||||
self.assertFalse(WarmPool.wired_to_dispatch)
|
||||
|
||||
def test_production_sources_never_import_experimental(self):
|
||||
# 钉的是 import 语句,不是文档提及:executor/__init__
|
||||
# 的 docstring 本来就要写清实验模块去向,文本匹配会
|
||||
# 误杀,故用 AST 只看 Import/ImportFrom 节点。
|
||||
for rel in ("cloud/worker.py", "cloud/chv/executor.py",
|
||||
"cloud/chv/__init__.py", "cloud/sandbox.py",
|
||||
"cloud/chv/orphans.py"):
|
||||
tree = ast.parse((ROOT / rel).read_text())
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for a in node.names:
|
||||
self.assertNotIn(a.name, (
|
||||
"cloud.chv.pool",
|
||||
"cloud.chv.experimental_pool"), rel)
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
self.assertNotIn(node.module, (
|
||||
"cloud.chv.pool",
|
||||
"cloud.chv.experimental_pool"), rel)
|
||||
if node.module in ("cloud.chv", "cloud.sandbox"):
|
||||
names = [a.name for a in node.names]
|
||||
self.assertNotIn("WarmPool", names, rel)
|
||||
|
||||
|
||||
class OrphansTest(unittest.TestCase):
|
||||
def test_valid_warm_and_assigned_cleaned_invalid_dropped(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = _write_registry(d, {
|
||||
"a": {"state": "warm", "vm": "chv-a",
|
||||
"task_id": "a", "snapshot_url": None},
|
||||
"b": {"state": "assigned", "vm": "chv-b",
|
||||
"task_id": "b", "snapshot_url": None},
|
||||
"c": {"state": "warm", "task_id": "c"},
|
||||
"e": {"state": "running", "vm": "chv-e"},
|
||||
"f": "not-a-dict",
|
||||
})
|
||||
ex = FakeProdExecutor(d)
|
||||
self.assertEqual(
|
||||
reconcile_orphans_for_executor(ex),
|
||||
["chv-a", "chv-b"])
|
||||
self.assertEqual(ex.shutdowns, ["chv-a", "chv-b"])
|
||||
self.assertEqual(ex.emits, ["warm_pool_reconcile"])
|
||||
with open(path) as fh:
|
||||
self.assertEqual(json.load(fh), {})
|
||||
|
||||
def test_invalid_only_returns_empty_and_leaves_file(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = _write_registry(d, {
|
||||
"c": {"state": "warm", "task_id": "c"}})
|
||||
ex = FakeProdExecutor(d)
|
||||
self.assertEqual(reconcile_orphans_for_executor(ex), [])
|
||||
self.assertEqual(ex.shutdowns, [])
|
||||
with open(path) as fh:
|
||||
self.assertIn("task_id", fh.read())
|
||||
|
||||
def test_corrupt_json_quarantined(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = _write_registry(d, "{not json")
|
||||
ex = FakeProdExecutor(d)
|
||||
self.assertEqual(reconcile_orphans_for_executor(ex), [])
|
||||
self.assertFalse(os.path.exists(path))
|
||||
self.assertTrue(os.path.exists(path + ".corrupt"))
|
||||
|
||||
def test_nondict_top_level_quarantined(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = _write_registry(d, "[1, 2]")
|
||||
ex = FakeProdExecutor(d)
|
||||
self.assertEqual(reconcile_orphans_for_executor(ex), [])
|
||||
self.assertTrue(os.path.exists(path + ".corrupt"))
|
||||
|
||||
def test_missing_file_and_no_state_dir(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
ex = FakeProdExecutor(d)
|
||||
self.assertEqual(reconcile_orphans_for_executor(ex), [])
|
||||
|
||||
class NoDir:
|
||||
def shutdown_vm(self, vm):
|
||||
raise AssertionError("must not be called")
|
||||
|
||||
self.assertEqual(
|
||||
reconcile_orphans_for_executor(NoDir()), [])
|
||||
|
||||
def test_shutdown_and_emit_failures_do_not_break_cleanup(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
_write_registry(d, {
|
||||
"a": {"state": "warm", "vm": "chv-a"}})
|
||||
ex = FakeProdExecutor(d)
|
||||
|
||||
def boom(vm):
|
||||
raise RuntimeError("shutdown down")
|
||||
|
||||
ex.shutdown_vm = boom
|
||||
ex._emit = lambda *a, **k: (_ for _ in ()).throw(
|
||||
RuntimeError("audit down"))
|
||||
self.assertEqual(
|
||||
reconcile_orphans_for_executor(ex), ["chv-a"])
|
||||
|
||||
def test_experimental_pool_registry_roundtrip_unchanged(self):
|
||||
# 实验池自身的 registry 语义(载入即 orphan、reconcile
|
||||
# 清理)与上一轨一致,只是文件 IO 委托给 orphans。
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
ex = FakeProdExecutor(d)
|
||||
ex.warm_pool_size = 0
|
||||
ex.snapshot_url = None
|
||||
pool1 = WarmPool(ex)
|
||||
pool1._entries["chv-old"] = {
|
||||
"state": "warm", "vm": "chv-old",
|
||||
"task": {"id": "old"}, "snapshot_url": None}
|
||||
pool1._save_registry()
|
||||
pool2 = WarmPool(ex)
|
||||
self.assertEqual(pool2.orphan_count(), 1)
|
||||
self.assertEqual(pool2.warm_count(), 0)
|
||||
self.assertEqual(pool2.reconcile_orphans(), ["chv-old"])
|
||||
|
||||
|
||||
class ConfigFragmentsTest(unittest.TestCase):
|
||||
def test_fragment_keys_disjoint_and_union_is_field_set(self):
|
||||
frags = [_base_fragment({}, {}), _ssh_fragment({}, {}),
|
||||
_network_fragment({}, {}), _quota_fragment({}),
|
||||
_disk_fragment({}), _timeout_fragment({}, {})]
|
||||
seen = set()
|
||||
for frag in frags:
|
||||
self.assertFalse(seen & set(frag), "片段键重叠")
|
||||
seen |= set(frag)
|
||||
self.assertEqual(seen, EXPECTED_FIELDS)
|
||||
self.assertEqual(len(EXPECTED_FIELDS), 38)
|
||||
|
||||
def test_defaults_and_overrides(self):
|
||||
cfg = build_chv_config({}, {})
|
||||
self.assertEqual(cfg.binary, "cloud-hypervisor")
|
||||
self.assertEqual(cfg.ssh_port, 2222)
|
||||
self.assertEqual(cfg.ssh_user, "root")
|
||||
self.assertEqual(cfg.host_key_mode, "provisioned")
|
||||
self.assertEqual(cfg.max_vms, 4)
|
||||
self.assertEqual(cfg.disk_mode, "copy")
|
||||
self.assertEqual(cfg.disk_gb, 50.0)
|
||||
self.assertEqual(cfg.api_timeout, 30.0)
|
||||
self.assertEqual(cfg.ready_timeout, 120.0)
|
||||
self.assertEqual(cfg.ready_interval, 5.0)
|
||||
self.assertIsNone(cfg.task_timeout)
|
||||
self.assertIsNone(cfg.snapshot_url)
|
||||
cfg2 = build_chv_config(
|
||||
{"ssh_user": "u", "ssh_port": 2223},
|
||||
{"CHV_SSH_USER": "env-user", "CHV_MAX_VMS": "2",
|
||||
"CHV_EGRESS_PROXY": "http://p:1",
|
||||
"CHV_SNAPSHOT_URL": "file:///snap/base"})
|
||||
self.assertEqual(cfg2.ssh_user, "u")
|
||||
self.assertEqual(cfg2.ssh_port, 2223)
|
||||
self.assertEqual(cfg2.max_vms, 2)
|
||||
self.assertEqual(cfg2.egress_proxy, "http://p:1")
|
||||
self.assertEqual(cfg2.snapshot_url, "file:///snap/base")
|
||||
|
||||
def test_single_error_messages_unchanged(self):
|
||||
cases = [
|
||||
({"ssh_user": ""}, {}, "CHV_SSH_USER 不能为空字符串"),
|
||||
({}, {"CHV_SSH_PORT": "0"}, "CHV_SSH_PORT 必须在 1-65535 之间"),
|
||||
({}, {"CHV_SSH_PORT": "99999"},
|
||||
"CHV_SSH_PORT 必须在 1-65535 之间"),
|
||||
({}, {"CHV_DISK_MODE": "bad"},
|
||||
"CHV_DISK_MODE 只能是 copy 或 reuse"),
|
||||
({}, {"CHV_DISK_GB": "0"}, "CHV_DISK_GB 必须大于 0"),
|
||||
({}, {"CHV_SNAPSHOT_MAX_COUNT": "-1"},
|
||||
"CHV_SNAPSHOT_* 不能为负数"),
|
||||
({}, {"CHV_MAX_VMS": "x"}, "CHV_MAX_VMS 必须是整数"),
|
||||
({}, {"CHV_WARM_POOL_SIZE": "x"},
|
||||
"CHV_WARM_POOL_SIZE 必须是整数"),
|
||||
({}, {"CHV_API_TIMEOUT": "x"},
|
||||
"CHV_*_TIMEOUT/INTERVAL 必须是数字"),
|
||||
]
|
||||
for params, env, msg in cases:
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
build_chv_config(params, env)
|
||||
self.assertIn(msg, str(cm.exception))
|
||||
|
||||
def test_assemble_key_conflict_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
_assemble_chv_config([{"a": 1}, {"a": 2}])
|
||||
|
||||
|
||||
def _top_level_imports(path):
|
||||
tree = ast.parse(path.read_text())
|
||||
mods = set()
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Import):
|
||||
mods.update(a.name for a in node.names)
|
||||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||
mods.add(node.module)
|
||||
return mods
|
||||
|
||||
|
||||
class DependencyGraphTest(unittest.TestCase):
|
||||
def test_leaf_modules_only_import_stdlib(self):
|
||||
for name in ("orphans", "config", "compat215", "errors"):
|
||||
mods = _top_level_imports(CHV / (name + ".py"))
|
||||
for mod in mods:
|
||||
top = mod.split(".")[0]
|
||||
self.assertFalse(top.startswith("cloud"),
|
||||
"%s 不得 import %s" % (name, mod))
|
||||
|
||||
def test_executor_never_imports_pool_or_orphans(self):
|
||||
# 同上:只看 AST import 节点,模块头 docstring 的
|
||||
# 分工说明不算 import。
|
||||
tree = ast.parse((CHV / "executor.py").read_text())
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for a in node.names:
|
||||
self.assertNotIn(a.name, (
|
||||
"cloud.chv.pool",
|
||||
"cloud.chv.experimental_pool",
|
||||
"cloud.chv.orphans"))
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
self.assertNotIn(node.module, (
|
||||
"cloud.chv.pool",
|
||||
"cloud.chv.experimental_pool",
|
||||
"cloud.chv.orphans"))
|
||||
|
||||
def test_no_chv_function_over_50_lines(self):
|
||||
over = []
|
||||
for f in CHV.glob("*.py"):
|
||||
tree = ast.parse(f.read_text())
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
n = node.end_lineno - node.lineno + 1
|
||||
if n > 50:
|
||||
over.append("%s:%s:%d" % (f.name, node.name, n))
|
||||
self.assertEqual(over, [])
|
||||
|
||||
|
||||
class PublicApiTypesTest(unittest.TestCase):
|
||||
def test_public_api_fully_annotated_and_no_any(self):
|
||||
import re
|
||||
unannotated = []
|
||||
for f in CHV.glob("*.py"):
|
||||
text = f.read_text()
|
||||
for line in text.splitlines():
|
||||
self.assertIsNone(re.search(r"\bAny\b", line),
|
||||
"%s 含 Any 逃逸:%s" % (f.name, line))
|
||||
tree = ast.parse(text)
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.FunctionDef) \
|
||||
and not node.name.startswith("_"):
|
||||
bad = [a.arg for a in node.args.args
|
||||
if a.annotation is None]
|
||||
if bad or node.returns is None:
|
||||
unannotated.append("%s:%s" % (f.name, node.name))
|
||||
if isinstance(node, ast.ClassDef):
|
||||
for m in node.body:
|
||||
if isinstance(m, ast.FunctionDef) and (
|
||||
not m.name.startswith("_")
|
||||
or m.name == "__init__"):
|
||||
bad = [a.arg for a in m.args.args
|
||||
if a.arg != "self"
|
||||
and a.annotation is None]
|
||||
bad += [a.arg for a in m.args.kwonlyargs
|
||||
if a.annotation is None]
|
||||
if bad or m.returns is None:
|
||||
unannotated.append(
|
||||
"%s:%s.%s" % (f.name, node.name,
|
||||
m.name))
|
||||
self.assertEqual(unannotated, [])
|
||||
|
||||
def test_corrected_annotations_match_runtime(self):
|
||||
from cloud.chv.disk import record_snapshot
|
||||
from cloud.chv.hostkeys import host_key_paths
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
entries, removed = record_snapshot(
|
||||
d, "vm1", "https://example.invalid/snap", 20.0)
|
||||
self.assertIsInstance(entries, list)
|
||||
self.assertIsInstance(removed, list)
|
||||
paths = host_key_paths(d, "vm1")
|
||||
self.assertIsInstance(paths, dict)
|
||||
self.assertIn("private", paths)
|
||||
|
||||
|
||||
class ReplacementChecklistTest(unittest.TestCase):
|
||||
def test_points_resolve_and_215_modules_absent(self):
|
||||
by_id = {p["id"]: p for p in REPLACEMENT_POINTS}
|
||||
self.assertEqual(set(by_id), {"R1", "R2", "R3"})
|
||||
self.assertIsNotNone(resolve_inside)
|
||||
self.assertIsNotNone(tmpfs_isolated)
|
||||
hardening = (ROOT / "cloud" / "chv" / "hardening.py").read_text()
|
||||
self.assertIn("resolve_inside", hardening)
|
||||
for mod in ("cloud.gates", "cloud.sandbox_policy",
|
||||
"cloud.timeouts", "cloud.quota"):
|
||||
self.assertIsNone(importlib.util.find_spec(mod), mod)
|
||||
import cloud.chv.compat215 as m
|
||||
self.assertFalse(hasattr(m, "resolve_module_timeout"))
|
||||
self.assertFalse(hasattr(m, "decide"))
|
||||
self.assertIsNone(by_id["R3"]["replacement"])
|
||||
self.assertIsNone(by_id["R2"]["caller"])
|
||||
|
||||
def test_tmpfs_isolated_has_no_production_caller(self):
|
||||
hits = []
|
||||
for f in (ROOT / "cloud").rglob("*.py"):
|
||||
if "tests" in f.parts:
|
||||
continue
|
||||
if "tmpfs_isolated" in f.read_text():
|
||||
hits.append(str(f.relative_to(ROOT)))
|
||||
self.assertEqual(hits, ["cloud/chv/compat215.py"])
|
||||
|
||||
|
||||
def _row(**kw):
|
||||
base = {
|
||||
"id": "t1", "project_id": "p1", "prompt": "x",
|
||||
"status": "interrupted", "idempotency_key": None,
|
||||
"depends_on": "[]", "priority": 0, "created_by": "u",
|
||||
"created_at": 0, "updated_at": 0, "last_heartbeat": 0,
|
||||
}
|
||||
base.update(kw)
|
||||
return base
|
||||
|
||||
|
||||
class ServerLifecycleTest(unittest.TestCase):
|
||||
def test_goal_fallback_form_also_derives_dead(self):
|
||||
row = _row(executor="cloud-hypervisor",
|
||||
interrupted_reason="VM 中途死亡")
|
||||
self.assertTrue(_chv_vm_dead_from_row(row))
|
||||
self.assertTrue(_task_to_dict(row).get("chv_vm_dead"))
|
||||
|
||||
def test_non_vm_reasons_never_derive(self):
|
||||
for reason in ("quota", "worker_shutdown", None, ""):
|
||||
row = _row(executor="cloud-hypervisor",
|
||||
interrupted_reason=reason)
|
||||
self.assertIsNone(_chv_vm_dead_from_row(row))
|
||||
self.assertNotIn("chv_vm_dead", _task_to_dict(row))
|
||||
row = _row(executor="docker",
|
||||
interrupted_reason="VM 中途死亡,任务未完成")
|
||||
self.assertIsNone(_chv_vm_dead_from_row(row))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -23,7 +23,7 @@ try:
|
||||
from cloud.chv.api_client import _unix_socket_api
|
||||
from cloud.chv.disk import check_disk_quota, provision_task_disk
|
||||
from cloud.chv.hostkeys import build_seed_disk, seed_disk_path
|
||||
from cloud.chv.pool import WarmPool
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.sandbox import (
|
||||
ChvFailure,
|
||||
CloudHypervisorExecutor,
|
||||
@@ -34,7 +34,7 @@ except ImportError:
|
||||
from cloud.chv.api_client import _unix_socket_api
|
||||
from cloud.chv.disk import check_disk_quota, provision_task_disk
|
||||
from cloud.chv.hostkeys import build_seed_disk, seed_disk_path
|
||||
from cloud.chv.pool import WarmPool
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.sandbox import (
|
||||
ChvFailure,
|
||||
CloudHypervisorExecutor,
|
||||
|
||||
@@ -23,7 +23,7 @@ try:
|
||||
known_hosts_line,
|
||||
write_seed,
|
||||
)
|
||||
from cloud.chv.pool import WarmPool
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.sandbox import (
|
||||
ChvFailure,
|
||||
CloudHypervisorExecutor,
|
||||
@@ -39,7 +39,7 @@ except ImportError:
|
||||
known_hosts_line,
|
||||
write_seed,
|
||||
)
|
||||
from cloud.chv.pool import WarmPool
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.sandbox import (
|
||||
ChvFailure,
|
||||
CloudHypervisorExecutor,
|
||||
@@ -232,28 +232,38 @@ class ReconcileWiringTest(ChvGapCase):
|
||||
self.assertTrue(
|
||||
os.path.exists(os.path.join(self.state_dir, "warm-pool.json")))
|
||||
# Worker 构造即 startup reconcile:用假 shutdown 记录。
|
||||
# 本轨 worker 不再持有 _warm_pool(生产走 orphans
|
||||
# facade);实验池只用于本测试造 registry 种子。
|
||||
shutdowns = []
|
||||
orig_shutdown = ex.shutdown_vm
|
||||
ex.shutdown_vm = lambda name: shutdowns.append(name) or True
|
||||
worker = Worker(db_path, "/fake/host.py", executor=ex,
|
||||
env={"XCM_API_KEY": "test-placeholder"})
|
||||
self.assertIn("chv-old", shutdowns)
|
||||
self.assertEqual(worker._warm_pool.orphan_count(), 0)
|
||||
self.assertFalse(hasattr(worker, "_warm_pool"))
|
||||
with open(os.path.join(self.state_dir, "warm-pool.json")) as fh:
|
||||
self.assertEqual(json.load(fh), {})
|
||||
worker.close()
|
||||
# close 时再次 reconcile 不炸(池已空,返回 [] 即可)。
|
||||
self.assertEqual(worker._reconcile_warm_pool("shutdown"), [])
|
||||
|
||||
def test_reconcile_failure_does_not_break_close(self):
|
||||
from cloud.server import init_db
|
||||
import cloud.chv.orphans as orphans
|
||||
db_path = os.path.join(self.dir, "control.db")
|
||||
init_db(db_path)
|
||||
ex = self.make_exec()
|
||||
worker = Worker(db_path, "/fake/host.py", executor=ex,
|
||||
env={"XCM_API_KEY": "test-placeholder"})
|
||||
# 池的 reconcile 抛错时 close 仍不炸。
|
||||
worker._warm_pool.reconcile_orphans = lambda: (_ for _ in ()).throw(
|
||||
RuntimeError("boom"))
|
||||
worker.close() # 不抛即通过
|
||||
# facade 抛错时 close 仍不炸(worker 方法内每次现取
|
||||
# facade,猴子补丁模块属性即生效)。
|
||||
orig = orphans.reconcile_orphans_for_executor
|
||||
orphans.reconcile_orphans_for_executor = lambda ex: (
|
||||
(_ for _ in ()).throw(RuntimeError("boom")))
|
||||
try:
|
||||
worker.close() # 不抛即通过
|
||||
finally:
|
||||
orphans.reconcile_orphans_for_executor = orig
|
||||
|
||||
|
||||
# -- (3) SSH host key 预置 ---------------------------------------------
|
||||
|
||||
@@ -23,14 +23,15 @@ from pathlib import Path
|
||||
try:
|
||||
from cloud.chv import ChvFailure
|
||||
from cloud.chv import CloudHypervisorExecutor
|
||||
from cloud.chv import WarmPool
|
||||
from cloud.chv import reconcile_orphans_for_executor
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.chv.api_client import _unix_socket_api as _chv_api
|
||||
from cloud.chv.config import _parse_memory_bytes as _chv_mem
|
||||
from cloud.sandbox import ChvFailure as _sb_failure
|
||||
from cloud.sandbox import CloudHypervisorExecutor as _sb_exec
|
||||
from cloud.sandbox import DockerExecutor
|
||||
from cloud.sandbox import reconcile_orphans_for_executor as _sb_reconcile
|
||||
from cloud.sandbox import LocalExecutor
|
||||
from cloud.sandbox import WarmPool as _sb_pool
|
||||
from cloud.sandbox import _call_runner_with_timeout
|
||||
from cloud.sandbox import _parse_memory_bytes as _sb_mem
|
||||
from cloud.sandbox import _unix_socket_api as _sb_api
|
||||
@@ -38,14 +39,15 @@ except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.p
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.chv import ChvFailure
|
||||
from cloud.chv import CloudHypervisorExecutor
|
||||
from cloud.chv import WarmPool
|
||||
from cloud.chv import reconcile_orphans_for_executor
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.chv.api_client import _unix_socket_api as _chv_api
|
||||
from cloud.chv.config import _parse_memory_bytes as _chv_mem
|
||||
from cloud.sandbox import ChvFailure as _sb_failure
|
||||
from cloud.sandbox import CloudHypervisorExecutor as _sb_exec
|
||||
from cloud.sandbox import DockerExecutor
|
||||
from cloud.sandbox import reconcile_orphans_for_executor as _sb_reconcile
|
||||
from cloud.sandbox import LocalExecutor
|
||||
from cloud.sandbox import WarmPool as _sb_pool
|
||||
from cloud.sandbox import _call_runner_with_timeout
|
||||
from cloud.sandbox import _parse_memory_bytes as _sb_mem
|
||||
from cloud.sandbox import _unix_socket_api as _sb_api
|
||||
@@ -168,17 +170,27 @@ class ChvOptCase(unittest.TestCase):
|
||||
class SplitTest(unittest.TestCase):
|
||||
def test_new_and_old_import_paths_are_same_objects(self):
|
||||
self.assertIs(CloudHypervisorExecutor, _sb_exec)
|
||||
self.assertIs(WarmPool, _sb_pool)
|
||||
self.assertIs(ChvFailure, _sb_failure)
|
||||
self.assertIs(_chv_mem, _sb_mem)
|
||||
self.assertIs(_chv_api, _sb_api)
|
||||
self.assertIs(reconcile_orphans_for_executor, _sb_reconcile)
|
||||
|
||||
def test_classes_live_in_chv_modules(self):
|
||||
self.assertEqual(CloudHypervisorExecutor.__module__,
|
||||
"cloud.chv.executor")
|
||||
self.assertEqual(WarmPool.__module__, "cloud.chv.pool")
|
||||
self.assertEqual(WarmPool.__module__,
|
||||
"cloud.chv.experimental_pool")
|
||||
self.assertEqual(ChvFailure.__module__, "cloud.chv.errors")
|
||||
|
||||
def test_old_warm_pool_import_paths_are_gone(self):
|
||||
# 本轨破环性清理:旧路径一律 ImportError,实验路径可用。
|
||||
with self.assertRaises(ImportError):
|
||||
from cloud.chv import WarmPool # noqa: F401
|
||||
with self.assertRaises(ImportError):
|
||||
from cloud.sandbox import WarmPool # noqa: F401
|
||||
with self.assertRaises(ImportError):
|
||||
import cloud.chv.pool # noqa: F401
|
||||
|
||||
|
||||
# -- 去重复:goal 解析与 host 命令 ------------------------------------------
|
||||
class SharedGoalTest(unittest.TestCase):
|
||||
|
||||
@@ -38,7 +38,7 @@ try:
|
||||
dirty_marker_path,
|
||||
write_seed,
|
||||
)
|
||||
from cloud.chv.pool import WarmPool
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.chv.ssh_transport import (
|
||||
build_ssh_command,
|
||||
split_secret_env,
|
||||
@@ -67,7 +67,7 @@ except ImportError:
|
||||
dirty_marker_path,
|
||||
write_seed,
|
||||
)
|
||||
from cloud.chv.pool import WarmPool
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.chv.ssh_transport import (
|
||||
build_ssh_command,
|
||||
split_secret_env,
|
||||
|
||||
@@ -20,18 +20,18 @@ from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
try:
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.sandbox import (
|
||||
ChvFailure,
|
||||
CloudHypervisorExecutor,
|
||||
WarmPool,
|
||||
_parse_memory_bytes,
|
||||
)
|
||||
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.chv.experimental_pool import WarmPool
|
||||
from cloud.sandbox import (
|
||||
ChvFailure,
|
||||
CloudHypervisorExecutor,
|
||||
WarmPool,
|
||||
_parse_memory_bytes,
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user