Files
codex-app-server/cloud/tests/test_chv_arch_maint_fe.py
T

481 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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):
# feat-chv-p1fix(P1-02):非法条目不再静默丢弃,
# 先发 invalid 审计并计数,再发 cleaned 审计。
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_registry_invalid_entries",
"warm_pool_reconcile"])
with open(path) as fh:
self.assertEqual(json.load(fh), {})
def test_invalid_only_returns_empty_and_drops_audited_entries(self):
# feat-chv-p1fix(P1-02):仅非法条目时返回 [] 且
# 不 shutdown;非法条目已审计计数,落盘时丢弃,
# 不再每次重启重复审计同一批非法条目。
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, [])
self.assertEqual(
ex.emits, ["warm_pool_registry_invalid_entries"])
with open(path) as fh:
self.assertEqual(json.load(fh), {})
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_failure_not_cleaned_and_retained(self):
# feat-chv-p1fix(P1-01):shutdown 失败的条目不进
# cleaned、不触发清库,落盘保留原始条目可重试;
# _emit 失败仍 best-effort,不炸清理流程。
with tempfile.TemporaryDirectory() as d:
path = _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), [])
with open(path) as fh:
self.assertEqual(
json.load(fh),
{"a": {"state": "warm", "vm": "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()