180 lines
6.9 KiB
Python
180 lines
6.9 KiB
Python
"""CHV 架构 9.5+ 提升轨测试(feat-chv-arch95plus,堆叠于 #252)。
|
||
|
||
只用标准库与 mock,绝不启动真实 VM;测试里只用 FAKE
|
||
占位密钥,绝无真实密钥。覆盖:长函数提取后的行为不变、
|
||
WarmPool facade 不调实验接口、#215 对齐接口形状、审计
|
||
失败计数不中断、只读字段 chv_vm_dead 的派生规则、
|
||
lockfile 校验脚本的通过/漂移/空清单。
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
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.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.sandbox import CloudHypervisorExecutor
|
||
from cloud.server import _task_to_dict
|
||
|
||
|
||
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 FakeExecutor:
|
||
def __init__(self):
|
||
self.acquire_called = False
|
||
self.prewarm_called = False
|
||
|
||
def shutdown_vm(self, vm):
|
||
return True
|
||
|
||
def _emit(self, *args, **kwargs):
|
||
pass
|
||
|
||
|
||
class Compat215Test(unittest.TestCase):
|
||
def test_resolve_inside_blocks_escape_and_symlink(self):
|
||
with tempfile.TemporaryDirectory() as d:
|
||
inside = os.path.join(d, "a.txt")
|
||
with open(inside, "w") as fh:
|
||
fh.write("x")
|
||
self.assertEqual(resolve_inside(d, "a.txt"), inside)
|
||
self.assertIsNone(resolve_inside(d, "../outside"))
|
||
self.assertIsNone(resolve_inside(d, "/etc/passwd"))
|
||
self.assertEqual(resolve_inside(d, ""), d)
|
||
|
||
def test_tmpfs_isolated_chv_and_unknown_fail_closed(self):
|
||
isolated, _ = tmpfs_isolated("cloud-hypervisor", "/tmp")
|
||
self.assertFalse(isolated)
|
||
isolated, _ = tmpfs_isolated("unknown-kind", "/tmp")
|
||
self.assertFalse(isolated)
|
||
|
||
def test_snapshot_url_uses_resolve_inside(self):
|
||
with tempfile.TemporaryDirectory() as d:
|
||
state = os.path.join(d, "state")
|
||
os.makedirs(state)
|
||
path = os.path.join(state, "s.raw")
|
||
with open(path, "w") as fh:
|
||
fh.write("x")
|
||
self.assertEqual(
|
||
validate_snapshot_url("file://" + path, state), path)
|
||
with self.assertRaises(Exception):
|
||
validate_snapshot_url(
|
||
"file:///etc/passwd", state)
|
||
|
||
def test_no_timeout_or_quota_map(self):
|
||
import cloud.chv.compat215 as m
|
||
self.assertFalse(hasattr(m, "resolve_module_timeout"))
|
||
self.assertFalse(hasattr(m, "decide"))
|
||
|
||
|
||
class PoolFacadeTest(unittest.TestCase):
|
||
def test_facade_only_reconciles_never_acquires(self):
|
||
# facade 内部构造 WarmPool:用假执行器 + 空 registry,
|
||
# 只要它不调 acquire/prewarm 就返回 [] 且不炸。
|
||
ex = FakeExecutor()
|
||
# WarmPool 构造需要 state_dir 属性;没有则 registry 为空
|
||
ex.state_dir = tempfile.mkdtemp()
|
||
ex.warm_pool_size = 0
|
||
ex.snapshot_url = None
|
||
result = reconcile_orphans_for_executor(ex)
|
||
self.assertEqual(result, [])
|
||
self.assertFalse(ex.acquire_called)
|
||
self.assertFalse(ex.prewarm_called)
|
||
|
||
|
||
class AuditCounterTest(unittest.TestCase):
|
||
def test_emit_failure_counts_and_does_not_raise(self):
|
||
ex = CloudHypervisorExecutor.__new__(CloudHypervisorExecutor)
|
||
ex.audit_callback = lambda *a, **k: (_ for _ in ()).throw(
|
||
RuntimeError("db down"))
|
||
ex.audit_failure_count = 0
|
||
# _emit 不抛异常(任务不因此中断),计数 +1
|
||
ex._emit("vm_boot", {"id": "t1"}, "vm=chv-t1")
|
||
self.assertEqual(ex.audit_failure_count, 1)
|
||
ex._emit("vm_boot", {"id": "t1"}, "vm=chv-t1")
|
||
self.assertEqual(ex.audit_failure_count, 2)
|
||
|
||
|
||
class ChvVmDeadFieldTest(unittest.TestCase):
|
||
def test_only_chv_with_vm_death_reason_returns_true(self):
|
||
row = _row(executor="cloud-hypervisor",
|
||
interrupted_reason="VM 中途死亡,任务未完成")
|
||
d = _task_to_dict(row)
|
||
self.assertTrue(d.get("chv_vm_dead"))
|
||
row2 = _row(executor="cloud-hypervisor",
|
||
interrupted_reason="quota")
|
||
self.assertNotIn("chv_vm_dead", _task_to_dict(row2))
|
||
row3 = _row(executor="docker",
|
||
interrupted_reason="VM 中途死亡,任务未完成")
|
||
self.assertNotIn("chv_vm_dead", _task_to_dict(row3))
|
||
row4 = _row(executor="cloud-hypervisor",
|
||
interrupted_reason=None)
|
||
self.assertNotIn("chv_vm_dead", _task_to_dict(row4))
|
||
|
||
|
||
class LockfileScriptTest(unittest.TestCase):
|
||
def _run(self, data, extra=None):
|
||
with tempfile.TemporaryDirectory() as d:
|
||
p = os.path.join(d, "BUILDINFO.json")
|
||
with open(p, "w") as fh:
|
||
json.dump(data, fh)
|
||
script = str(Path(__file__).resolve().parents[2]
|
||
/ "chv-guest" / "check_lockfile.py")
|
||
cmd = [sys.executable, script, p] + (extra or [])
|
||
return subprocess.run(cmd, capture_output=True, text=True)
|
||
|
||
def test_pass_and_empty_and_drift(self):
|
||
good = {"tag": "t", "versions": {"dpkg": ["a 1"],
|
||
"pip": ["b==2"]}}
|
||
self.assertEqual(self._run(good, ["--tag", "t"]).returncode, 0)
|
||
bad = {"tag": "t", "versions": {"dpkg": [], "pip": []}}
|
||
self.assertNotEqual(self._run(bad).returncode, 0)
|
||
with tempfile.TemporaryDirectory() as d:
|
||
frozen = os.path.join(d, "frozen.json")
|
||
with open(frozen, "w") as fh:
|
||
json.dump({"dpkg": ["missing 9"], "pip": []}, fh)
|
||
self.assertNotEqual(
|
||
self._run(good, ["--frozen", frozen]).returncode, 0)
|
||
|
||
|
||
class LongFunctionTest(unittest.TestCase):
|
||
def test_no_chv_function_over_50_lines(self):
|
||
import ast
|
||
base = Path(__file__).resolve().parents[2] / "cloud" / "chv"
|
||
over = []
|
||
for f in base.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, [])
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|