feat: 完成 issue #8 ⑥ K8s/Helm 部署底座 + 昇腾适配层
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""测试引导:把 `core/inference-backend` 以包名 `inference_backend` 挂载到 sys.modules。
|
||||
|
||||
目录名 `inference-backend` 含连字符,无法直接以包名 import;挂载后模块内相对导入
|
||||
(`from inference_backend.base import ...`)在 unittest 发现机制下可正常解析。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, BACKEND_DIR)
|
||||
if "inference_backend" not in sys.modules:
|
||||
pkg = types.ModuleType("inference_backend")
|
||||
pkg.__path__ = [BACKEND_DIR]
|
||||
sys.modules["inference_backend"] = pkg
|
||||
@@ -0,0 +1,127 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""推理后端抽象层单元测试(EPIC #8 ⑥ 部署底座 / PRD 5.6)。
|
||||
|
||||
覆盖:
|
||||
1. 模板配置资产可解析、含 backend 选择键;
|
||||
2. 工厂按配置构建 GPU / NPU 后端(可插拔);
|
||||
3. 统一接口 load_model / infer / health / unload 行为;
|
||||
4. 切换后端仅改配置:同一业务调用面(接口方法)在两种后端下等价;
|
||||
5. 非法配置报错。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import _bootstrap # noqa: F401
|
||||
|
||||
from inference_backend.base import InferRequest, InferenceBackend # noqa: E402
|
||||
from inference_backend.factory import ( # noqa: E402
|
||||
BACKEND_REGISTRY,
|
||||
build_backend,
|
||||
default_config_path,
|
||||
load_backend_config,
|
||||
)
|
||||
|
||||
CONFIG_PATH = default_config_path()
|
||||
|
||||
|
||||
class TestTemplateConfig(unittest.TestCase):
|
||||
"""模板配置资产:可解析且包含后端选择配置点。"""
|
||||
|
||||
def test_template_parses_and_has_backend_key(self):
|
||||
cfg = load_backend_config(CONFIG_PATH)
|
||||
self.assertIn("backend", cfg)
|
||||
self.assertIn(cfg["backend"].lower(), ("gpu", "npu"))
|
||||
self.assertIn("inference", cfg)
|
||||
|
||||
def test_registry_covers_gpu_and_npu(self):
|
||||
self.assertEqual(sorted(BACKEND_REGISTRY), ["gpu", "npu"])
|
||||
|
||||
|
||||
class TestFactory(unittest.TestCase):
|
||||
"""工厂:按配置选择后端实现(可插拔适配层)。"""
|
||||
|
||||
def test_build_gpu_from_template(self):
|
||||
cfg = load_backend_config(CONFIG_PATH)
|
||||
backend = build_backend(cfg)
|
||||
self.assertIsInstance(backend, InferenceBackend)
|
||||
self.assertEqual(backend.backend_name, cfg["backend"].lower())
|
||||
self.assertEqual(backend.model, cfg["inference"]["model"])
|
||||
|
||||
def test_build_npu_by_config_switch(self):
|
||||
cfg = load_backend_config(CONFIG_PATH)
|
||||
cfg["backend"] = "npu"
|
||||
backend = build_backend(cfg)
|
||||
self.assertEqual(backend.backend_name, "npu")
|
||||
# 业务调用面不变(接口等价,不感知硬件)
|
||||
self.assertTrue(callable(backend.load_model))
|
||||
self.assertTrue(callable(backend.infer))
|
||||
self.assertTrue(callable(backend.health))
|
||||
self.assertTrue(callable(backend.unload))
|
||||
|
||||
def test_build_rejects_unknown_backend(self):
|
||||
with self.assertRaises(ValueError):
|
||||
build_backend({"backend": "tpu"})
|
||||
|
||||
def test_build_rejects_missing_backend(self):
|
||||
with self.assertRaises(ValueError):
|
||||
build_backend({"inference": {"model": "x"}})
|
||||
|
||||
|
||||
class TestBackendBehavior(unittest.TestCase):
|
||||
"""统一接口行为(dry-run:未配置 endpoint 时适配层就绪)。"""
|
||||
|
||||
def setUp(self):
|
||||
cfg = load_backend_config(CONFIG_PATH)
|
||||
cfg["inference"]["endpoint"] = "" # dry-run
|
||||
self.gpu = build_backend(cfg)
|
||||
cfg["backend"] = "npu"
|
||||
self.npu = build_backend(cfg)
|
||||
|
||||
def test_dry_run_load_health_unload(self):
|
||||
for backend in (self.gpu, self.npu):
|
||||
loaded = backend.load_model()
|
||||
self.assertEqual(loaded["status"], "ok")
|
||||
health = backend.health()
|
||||
self.assertEqual(health["status"], "ok")
|
||||
self.assertEqual(health["backend"], backend.backend_name)
|
||||
self.assertTrue(health["loaded"])
|
||||
unloaded = backend.unload()
|
||||
self.assertEqual(unloaded["status"], "ok")
|
||||
|
||||
def test_infer_extracts_text_from_openai_response(self):
|
||||
fake_body = {
|
||||
"choices": [{"message": {"content": "炉温偏高,建议降低加料比"}}],
|
||||
}
|
||||
for backend in (self.gpu, self.npu):
|
||||
with mock.patch.object(backend, "_post_json",
|
||||
return_value=fake_body) as post:
|
||||
result = backend.infer(InferRequest(prompt="请解释炉温报警"))
|
||||
post.assert_called_once_with(
|
||||
"/v1/chat/completions",
|
||||
mock.ANY,
|
||||
)
|
||||
self.assertEqual(result.text, "炉温偏高,建议降低加料比")
|
||||
self.assertEqual(result.backend, backend.backend_name)
|
||||
# mock 调用耗时可能为 0,仅断言字段类型与量级
|
||||
self.assertIsInstance(result.latency_ms, float)
|
||||
self.assertGreaterEqual(result.latency_ms, 0.0)
|
||||
|
||||
def test_infer_payload_carries_model(self):
|
||||
with mock.patch.object(self.gpu, "_post_json",
|
||||
return_value={"choices": []}) as post:
|
||||
self.gpu.infer(InferRequest(prompt="hi", max_tokens=64))
|
||||
_, payload = post.call_args[0]
|
||||
self.assertEqual(payload["model"], self.gpu.model)
|
||||
self.assertEqual(payload["max_tokens"], 64)
|
||||
|
||||
def test_health_dry_run_notes_reason(self):
|
||||
health = self.gpu.health()
|
||||
self.assertIn("reason", health)
|
||||
self.assertIn("dry-run", health["reason"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user