feat: 按算法生成模板并支持从 Git LFS 下载模型
- init 新增 yolo26/resnet/arcface/lprnet/face-recognition 模板(多模型人脸检测+识别) - 新增 embedding/text 输出类型与模板资源(COCO、字符表等) - 新增 algpkg download-models:从 Gitea LFS 下载 models/BM1688 并做 SHA256 校验 - 补充单元测试与 README
This commit is contained in:
@@ -7,12 +7,18 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import http.server
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from algpkg_sdk import (AlgoBuilder, AlgoError, AlgoPackage, AlgoPackageError, Manifest,
|
||||
@@ -318,6 +324,436 @@ def test_cli_init_build_inspect_check(tmp_path: Path) -> None:
|
||||
assert cli_main(["list", str(tmp_path)]) == 0
|
||||
|
||||
|
||||
def test_cli_init_yolo26_template(tmp_path: Path) -> None:
|
||||
"""`--algorithm yolo26` 生成可直接运行的检测包(不是 TODO 占位)。"""
|
||||
src = tmp_path / "yolo.demo"
|
||||
assert cli_main(["init", str(src), "--id", "yolo.demo", "--name", "YOLO26 示例",
|
||||
"--algorithm", "yolo26"]) == 0
|
||||
manifest = Manifest.parse(json.loads((src / "manifest.json").read_text()))
|
||||
assert manifest.id == "yolo.demo" and manifest.kind == "detector"
|
||||
assert manifest.default_model().name == "yolo26s_fp16_1b"
|
||||
assert manifest.default_model().file == "models/yolo26s_fp16_1b.bmodel"
|
||||
assert manifest.output["format"] == "N6"
|
||||
|
||||
code = (src / manifest.code_package / "__init__.py").read_text()
|
||||
compile(code, "alg_yolo_demo/__init__.py", "exec") # 语法可编译(不 import 设备库)
|
||||
assert "class Algorithm" in code and "_preprocess" in code and "_postprocess" in code
|
||||
assert "TODO" not in code
|
||||
|
||||
labels = [line for line in (src / "assets" / "labels.txt").read_text().splitlines()
|
||||
if line.strip()]
|
||||
assert len(labels) == 80 and labels[0] == "person" and labels[-1] == "toothbrush"
|
||||
|
||||
model = tmp_path / "fake.bmodel"
|
||||
model.write_bytes(b"FAKE-BMODEL" * 512)
|
||||
out = tmp_path / "yolo.demo-1.0.0.mbank"
|
||||
assert cli_main(["build", str(src), "--out", str(out),
|
||||
"--model", f"yolo26s_fp16_1b={model}", "--quiet"]) == 0
|
||||
assert cli_main(["verify", str(out), "--json"]) == 0
|
||||
|
||||
|
||||
def test_cli_init_yolo26_alias_and_unknown_template(tmp_path: Path) -> None:
|
||||
src = tmp_path / "alias.demo"
|
||||
assert cli_main(["init", str(src), "--id", "alias.demo", "--name", "别名",
|
||||
"--template", "yolov26"]) == 0
|
||||
manifest = Manifest.parse(json.loads((src / "manifest.json").read_text()))
|
||||
assert manifest.default_model().name == "yolo26s_fp16_1b"
|
||||
|
||||
bad = tmp_path / "bad.demo"
|
||||
assert cli_main(["init", str(bad), "--id", "bad.demo", "--name", "未知",
|
||||
"--algorithm", "nope"]) == 2
|
||||
assert not (bad / "manifest.json").exists()
|
||||
|
||||
|
||||
def test_cli_init_resnet_template(tmp_path: Path) -> None:
|
||||
"""`--algorithm resnet` 生成 ResNet 分类包(ImageNet 归一化 + softmax + 整帧框)。"""
|
||||
src = tmp_path / "resnet.demo"
|
||||
assert cli_main(["init", str(src), "--id", "resnet.demo", "--name", "ResNet 示例",
|
||||
"--algorithm", "resnet"]) == 0
|
||||
manifest = Manifest.parse(json.loads((src / "manifest.json").read_text()))
|
||||
assert manifest.id == "resnet.demo" and manifest.kind == "classifier"
|
||||
assert manifest.default_model().name == "resnet50_fp32_1b"
|
||||
assert manifest.default_model().file == "models/resnet50_fp32_1b.bmodel"
|
||||
assert manifest.input["mean"] == [0.485, 0.456, 0.406]
|
||||
assert manifest.schema_defaults()["conf"] == 0.1
|
||||
|
||||
code = (src / manifest.code_package / "__init__.py").read_text()
|
||||
compile(code, "alg_resnet_demo/__init__.py", "exec")
|
||||
assert "class Algorithm" in code and "_preprocess" in code
|
||||
assert "_postprocess" in code and "yuv2bgr" in code
|
||||
|
||||
labels = [line for line in (src / "assets" / "labels.txt").read_text().splitlines()
|
||||
if line.strip() and not line.startswith("#")]
|
||||
assert len(labels) == 1000 and labels[0] == "class_0" and labels[-1] == "class_999"
|
||||
|
||||
model = tmp_path / "fake.bmodel"
|
||||
model.write_bytes(b"FAKE-BMODEL" * 512)
|
||||
out = tmp_path / "resnet.demo-1.0.0.mbank"
|
||||
assert cli_main(["build", str(src), "--out", str(out),
|
||||
"--model", f"resnet50_fp32_1b={model}", "--quiet"]) == 0
|
||||
assert cli_main(["verify", str(out), "--json"]) == 0
|
||||
|
||||
|
||||
def test_resnet_template_infer_with_fake_sail(tmp_path: Path, monkeypatch) -> None:
|
||||
"""不依赖设备验证分类后处理:top-1 → 整帧框(score/cls)与阈值过滤。"""
|
||||
src = tmp_path / "resnet.infer"
|
||||
cli_main(["init", str(src), "--id", "resnet.infer", "--name", "推理示例",
|
||||
"--algorithm", "resnet"])
|
||||
code_path = src / "alg_resnet_infer" / "__init__.py"
|
||||
|
||||
# 生成代码只在方法体内使用 sail;空模块足以导入(不需要真实设备)
|
||||
monkeypatch.setitem(sys.modules, "sophon", types.ModuleType("sophon"))
|
||||
monkeypatch.setitem(sys.modules, "sophon.sail", types.ModuleType("sophon.sail"))
|
||||
spec = importlib.util.spec_from_file_location("alg_resnet_infer_under_test", code_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
class Frame:
|
||||
def width(self) -> int:
|
||||
return 640
|
||||
|
||||
def height(self) -> int:
|
||||
return 480
|
||||
|
||||
class Tensor:
|
||||
def asnumpy(self):
|
||||
return [[0.1, 3.0, 0.2]] # logits: top-1 = 类别 1
|
||||
|
||||
class Net:
|
||||
def process(self, *args, **kwargs) -> None:
|
||||
return None
|
||||
|
||||
class Bmcv:
|
||||
def bm_image_to_tensor(self, *args, **kwargs) -> None:
|
||||
return None
|
||||
|
||||
algo = module.Algorithm(ctx=None)
|
||||
algo._conf = 0.5
|
||||
algo._labels = ["cat", "dog", "bird"]
|
||||
algo._output_scale = 1.0
|
||||
algo._output_tensor = Tensor()
|
||||
algo._output_tensors = {0: algo._output_tensor}
|
||||
algo._input_tensor = object()
|
||||
algo._input_name, algo._graph = "input", "graph"
|
||||
algo._net = Net()
|
||||
algo._bmcv = Bmcv()
|
||||
algo._preprocess = lambda frame: object()
|
||||
|
||||
result = algo.infer(Frame())
|
||||
assert result.boxes.shape == (1, 6)
|
||||
assert (float(result.boxes[0, 0]), float(result.boxes[0, 2])) == (0.0, 639.0)
|
||||
assert int(result.boxes[0, 5]) == 1
|
||||
assert float(result.boxes[0, 4]) > 0.8
|
||||
assert result.counts == {"dog": 1} and result.value == 1.0
|
||||
assert result.meta["class_id"] == 1
|
||||
|
||||
algo._conf = 0.99 # 低于阈值 → 空结果,不上报告警
|
||||
assert algo.infer(Frame()).boxes.shape == (0, 6)
|
||||
|
||||
|
||||
def test_cli_init_arcface_template(tmp_path: Path) -> None:
|
||||
"""`--algorithm arcface` 生成 ArcFace 特征包(embedding 输出、无类别表)。"""
|
||||
src = tmp_path / "arcface.demo"
|
||||
assert cli_main(["init", str(src), "--id", "arcface.demo", "--name", "ArcFace 示例",
|
||||
"--algorithm", "arcface"]) == 0
|
||||
manifest = Manifest.parse(json.loads((src / "manifest.json").read_text()))
|
||||
assert manifest.id == "arcface.demo" and manifest.kind == "classifier"
|
||||
assert manifest.default_model().name == "arcface_resnet50_fp32_1b"
|
||||
assert manifest.default_model().file == "models/arcface_resnet50_fp32_1b.bmodel"
|
||||
assert manifest.output["type"] == "embedding"
|
||||
assert manifest.output["embedding_size"] == 512
|
||||
assert manifest.schema_defaults()["interval"] == 0.5
|
||||
|
||||
code = (src / manifest.code_package / "__init__.py").read_text()
|
||||
compile(code, "alg_arcface_demo/__init__.py", "exec")
|
||||
assert "class Algorithm" in code and "_preprocess" in code
|
||||
assert "meta" in code and "linalg.norm" in code
|
||||
|
||||
labels = [line for line in (src / "assets" / "labels.txt").read_text().splitlines()
|
||||
if line.strip() and not line.startswith("#")]
|
||||
assert labels == [] # 特征包不需要类别表
|
||||
|
||||
model = tmp_path / "fake.bmodel"
|
||||
model.write_bytes(b"FAKE-BMODEL" * 512)
|
||||
out = tmp_path / "arcface.demo-1.0.0.mbank"
|
||||
assert cli_main(["build", str(src), "--out", str(out),
|
||||
"--model", f"arcface_resnet50_fp32_1b={model}", "--quiet"]) == 0
|
||||
assert cli_main(["verify", str(out), "--json"]) == 0
|
||||
|
||||
|
||||
def test_arcface_template_infer_with_fake_sail(tmp_path: Path, monkeypatch) -> None:
|
||||
"""不依赖设备验证特征提取:L2 归一化、512 维 embedding、face 计数。"""
|
||||
src = tmp_path / "arcface.infer"
|
||||
cli_main(["init", str(src), "--id", "arcface.infer", "--name", "特征示例",
|
||||
"--algorithm", "arcface"])
|
||||
code_path = src / "alg_arcface_infer" / "__init__.py"
|
||||
|
||||
monkeypatch.setitem(sys.modules, "sophon", types.ModuleType("sophon"))
|
||||
monkeypatch.setitem(sys.modules, "sophon.sail", types.ModuleType("sophon.sail"))
|
||||
spec = importlib.util.spec_from_file_location("alg_arcface_infer_under_test", code_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
class Frame:
|
||||
def width(self) -> int:
|
||||
return 112
|
||||
|
||||
def height(self) -> int:
|
||||
return 112
|
||||
|
||||
class Tensor:
|
||||
def asnumpy(self):
|
||||
return [[3.0, 4.0] + [0.0] * 510] # 归一化后前两维 = 0.6, 0.8
|
||||
|
||||
class Net:
|
||||
def process(self, *args, **kwargs) -> None:
|
||||
return None
|
||||
|
||||
class Bmcv:
|
||||
def bm_image_to_tensor(self, *args, **kwargs) -> None:
|
||||
return None
|
||||
|
||||
algo = module.Algorithm(ctx=None)
|
||||
algo._output_tensor = Tensor()
|
||||
algo._input_tensor = object()
|
||||
algo._input_name, algo._input_shapes = "input", {}
|
||||
algo._output_name, algo._graph = "output", "graph"
|
||||
algo._net, algo._bmcv = Net(), Bmcv()
|
||||
algo._preprocess = lambda frame: object()
|
||||
|
||||
result = algo.infer(Frame())
|
||||
assert result.boxes is None
|
||||
assert result.counts == {"face": 1} and result.value == 1.0
|
||||
assert result.meta["dim"] == 512
|
||||
assert result.meta["embedding"][0] == pytest.approx(0.6)
|
||||
assert result.meta["embedding"][1] == pytest.approx(0.8)
|
||||
assert result.meta["norm"] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_cli_init_lprnet_template(tmp_path: Path) -> None:
|
||||
"""`--algorithm lprnet` 生成车牌识别包(text 输出 + 字符表)。"""
|
||||
src = tmp_path / "lprnet.demo"
|
||||
assert cli_main(["init", str(src), "--id", "lprnet.demo", "--name", "车牌示例",
|
||||
"--algorithm", "lprnet"]) == 0
|
||||
manifest = Manifest.parse(json.loads((src / "manifest.json").read_text()))
|
||||
assert manifest.id == "lprnet.demo" and manifest.kind == "classifier"
|
||||
assert manifest.default_model().name == "lprnet_fp32_1b"
|
||||
assert manifest.default_model().file == "models/lprnet_fp32_1b.bmodel"
|
||||
assert manifest.default_model().resolution == [94, 24]
|
||||
assert manifest.output["type"] == "text"
|
||||
assert manifest.output["chars_file"] == "assets/chars.txt"
|
||||
assert manifest.schema_defaults()["interval"] == 0.5
|
||||
|
||||
code = (src / manifest.code_package / "__init__.py").read_text()
|
||||
compile(code, "alg_lprnet_demo/__init__.py", "exec")
|
||||
assert "_preprocess" in code and "_postprocess" in code and "chars.txt" in code
|
||||
|
||||
chars = [line for line in (src / "assets" / "chars.txt").read_text().splitlines()
|
||||
if line.strip() and not line.startswith("#")]
|
||||
assert len(chars) == 68 and chars[0] == "京" and chars[-1] == "-"
|
||||
|
||||
model = tmp_path / "fake.bmodel"
|
||||
model.write_bytes(b"FAKE-BMODEL" * 512)
|
||||
out = tmp_path / "lprnet.demo-1.0.0.mbank"
|
||||
assert cli_main(["build", str(src), "--out", str(out),
|
||||
"--model", f"lprnet_fp32_1b={model}", "--quiet"]) == 0
|
||||
assert cli_main(["verify", str(out), "--json"]) == 0
|
||||
|
||||
|
||||
def test_lprnet_template_infer_with_fake_sail(tmp_path: Path, monkeypatch) -> None:
|
||||
"""不依赖设备验证 CTC 贪心解码:去重、去 blank、空结果。"""
|
||||
src = tmp_path / "lprnet.infer"
|
||||
cli_main(["init", str(src), "--id", "lprnet.infer", "--name", "车牌推理",
|
||||
"--algorithm", "lprnet"])
|
||||
code_path = src / "alg_lprnet_infer" / "__init__.py"
|
||||
|
||||
monkeypatch.setitem(sys.modules, "sophon", types.ModuleType("sophon"))
|
||||
monkeypatch.setitem(sys.modules, "sophon.sail", types.ModuleType("sophon.sail"))
|
||||
spec = importlib.util.spec_from_file_location("alg_lprnet_infer_under_test", code_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
class Frame:
|
||||
def width(self) -> int:
|
||||
return 94
|
||||
|
||||
def height(self) -> int:
|
||||
return 24
|
||||
|
||||
class Tensor:
|
||||
def __init__(self) -> None:
|
||||
self.logits = np.full((1, 4, 6), -10.0, dtype=np.float32)
|
||||
|
||||
def asnumpy(self):
|
||||
return self.logits
|
||||
|
||||
class Net:
|
||||
def process(self, *args, **kwargs) -> None:
|
||||
return None
|
||||
|
||||
class Bmcv:
|
||||
def bm_image_to_tensor(self, *args, **kwargs) -> None:
|
||||
return None
|
||||
|
||||
tensor = Tensor()
|
||||
algo = module.Algorithm(ctx=None)
|
||||
algo._chars = ["京", "A", "1", "-"]
|
||||
algo._blank = 3
|
||||
algo._output_tensor = tensor
|
||||
algo._input_tensor = object()
|
||||
algo._input_name, algo._input_shapes = "input", {}
|
||||
algo._output_name, algo._graph = "output", "graph"
|
||||
algo._net, algo._bmcv = Net(), Bmcv()
|
||||
algo._preprocess = lambda frame: object()
|
||||
|
||||
# 模型输出 [1, C=4, T=6]:0,0,3(blank),1,2,2 → CTC 去重后 "京A1"
|
||||
for t, cls in enumerate([0, 0, 3, 1, 2, 2]):
|
||||
tensor.logits[0, cls, t] = 10.0
|
||||
result = algo.infer(Frame())
|
||||
assert result.boxes is None
|
||||
assert result.meta["text"] == "京A1"
|
||||
assert result.meta["chars"] == [0, 1, 2]
|
||||
assert result.counts == {"plate": 1} and result.value == 1.0
|
||||
|
||||
tensor.logits = np.full((1, 4, 6), -10.0, dtype=np.float32) # 全是 blank
|
||||
tensor.logits[0, 3, :] = 10.0
|
||||
empty = algo.infer(Frame())
|
||||
assert empty.meta["text"] == "" and empty.meta["chars"] == []
|
||||
assert empty.counts == {} and empty.value == 0.0
|
||||
|
||||
|
||||
def test_cli_init_face_recognition_template(tmp_path: Path, monkeypatch) -> None:
|
||||
"""`--algorithm face-recognition` 生成双模型包(检测框 + 512 维 embedding)。"""
|
||||
src = tmp_path / "face.demo"
|
||||
assert cli_main(["init", str(src), "--id", "face.demo", "--name", "人脸示例",
|
||||
"--algorithm", "face-recognition"]) == 0
|
||||
manifest = Manifest.parse(json.loads((src / "manifest.json").read_text()))
|
||||
assert manifest.id == "face.demo" and manifest.kind == "detector"
|
||||
assert [m.name for m in manifest.models] == ["face_detector", "arcface"]
|
||||
assert manifest.default_model().name == "face_detector"
|
||||
assert manifest.model("arcface").file == "models/arcface_resnet50_fp32_1b.bmodel"
|
||||
assert manifest.output["embedding_size"] == 512
|
||||
assert manifest.schema_defaults()["nms"] == 0.3
|
||||
assert manifest.schema_defaults()["min_face"] == 16
|
||||
|
||||
code = (src / manifest.code_package / "__init__.py").read_text()
|
||||
compile(code, "alg_face_demo/__init__.py", "exec")
|
||||
assert "_priorbox" in code and "_detect" in code and "_embed" in code
|
||||
assert 'model_bytes("arcface")' in code
|
||||
|
||||
labels = [line for line in (src / "assets" / "labels.txt").read_text().splitlines()
|
||||
if line.strip() and not line.startswith("#")]
|
||||
assert labels == ["face"]
|
||||
|
||||
det = tmp_path / "det.bmodel"
|
||||
det.write_bytes(b"DET" * 2048)
|
||||
arc = tmp_path / "arc.bmodel"
|
||||
arc.write_bytes(b"ARC" * 2048)
|
||||
out = tmp_path / "face.demo-1.0.0.mbank"
|
||||
assert cli_main(["build", str(src), "--out", str(out),
|
||||
"--model", f"face_detector={det}",
|
||||
"--model", f"arcface={arc}", "--quiet"]) == 0
|
||||
assert cli_main(["verify", str(out), "--json"]) == 0
|
||||
|
||||
# 不依赖设备验证先验框/解码/NMS 的集成(官方 RetinaFace 口径)
|
||||
monkeypatch.setitem(sys.modules, "sophon", types.ModuleType("sophon"))
|
||||
monkeypatch.setitem(sys.modules, "sophon.sail", types.ModuleType("sophon.sail"))
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"alg_face_detect_under_test", src / "alg_face_demo" / "__init__.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
priors = module._priorbox(640, 640)
|
||||
assert priors.shape == (16800, 4)
|
||||
assert np.allclose(priors[0], [4 / 640, 4 / 640, 16 / 640, 16 / 640])
|
||||
|
||||
algo = module.Algorithm(ctx=None)
|
||||
algo._conf, algo._nms, algo._max_faces = 0.5, 0.3, 20
|
||||
algo._priors = priors
|
||||
algo._det_w = algo._det_h = 640
|
||||
loc = np.zeros((1, 16800, 4), dtype=np.float32)
|
||||
conf = np.zeros((1, 16800, 2), dtype=np.float32)
|
||||
conf[0, :, 1] = 0.01
|
||||
conf[0, 0, 1] = 0.9
|
||||
dets = algo._detect([loc, conf], 1.0, 640, 640)
|
||||
assert dets.shape == (1, 6)
|
||||
assert np.allclose(dets[0, :4], [0, 0, 12, 12])
|
||||
assert dets[0, 4] == pytest.approx(0.9) and dets[0, 5] == 0
|
||||
|
||||
|
||||
def test_model_download_specs_and_aliases() -> None:
|
||||
from algpkg_sdk import models as model_store
|
||||
|
||||
assert [m.key for m in model_store.resolve_models(["all"])] == list(model_store.MODEL_FILES)
|
||||
assert model_store.resolve_models(["yolov26"])[0].key == "yolo26"
|
||||
assert model_store.resolve_models(["lpr"])[0].key == "lprnet"
|
||||
assert model_store.lfs_batch_url("http://host/repo.git").endswith(
|
||||
"/repo.git/info/lfs/objects/batch")
|
||||
with pytest.raises(ValueError):
|
||||
model_store.resolve_models(["nope"])
|
||||
|
||||
|
||||
def test_cli_download_models_via_lfs(tmp_path: Path, monkeypatch, capsys) -> None:
|
||||
"""本地假 LFS 服务验证下载/校验/跳过(不依赖外网与 git-lfs)。"""
|
||||
from algpkg_sdk import models as model_store
|
||||
|
||||
content = b"FAKE-BMODEL-DOWNLOAD" * 4096
|
||||
oid = hashlib.sha256(content).hexdigest()
|
||||
spec = model_store.ModelFile("demo", "demo_model.bmodel", oid, len(content), "测试模型")
|
||||
monkeypatch.setattr(model_store, "MODEL_FILES", {"demo": spec})
|
||||
monkeypatch.setattr(model_store, "ALIASES", {"demo": "demo"})
|
||||
monkeypatch.setenv("NO_PROXY", "127.0.0.1,localhost")
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_POST(self) -> None: # LFS batch
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
payload = json.loads(self.rfile.read(length) or b"{}")
|
||||
port = self.server.server_port
|
||||
objects = [{
|
||||
"oid": item["oid"], "size": item["size"],
|
||||
"actions": {"download": {
|
||||
"href": f"http://127.0.0.1:{port}/objects/{item['oid']}"}},
|
||||
} for item in payload.get("objects", [])]
|
||||
body = json.dumps({"objects": objects}).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/vnd.git-lfs+json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None: # LFS object
|
||||
if self.path.startswith("/objects/"):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def log_message(self, *args) -> None:
|
||||
return None
|
||||
|
||||
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
dest = tmp_path / "models" / "BM1688"
|
||||
url = f"http://127.0.0.1:{server.server_port}/sophon-edge/algpkg-sdk.git"
|
||||
assert cli_main(["download-models", "--dest", str(dest),
|
||||
"--url", url, "--model", "demo"]) == 0
|
||||
assert (dest / "demo_model.bmodel").read_bytes() == content
|
||||
assert "下载 1" in capsys.readouterr().out
|
||||
|
||||
assert cli_main(["download-models", "--dest", str(dest),
|
||||
"--url", url, "--model", "demo"]) == 0
|
||||
assert "跳过 1" in capsys.readouterr().out
|
||||
assert (dest / "demo_model.bmodel").read_bytes() == content
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
|
||||
def test_cli_check_reports_static_results(tmp_path: Path) -> None:
|
||||
"""check 在无设备时也要给出"清单/结构 OK、试加载跳过"的诚实结论。"""
|
||||
src = tmp_path / "chk.demo"
|
||||
|
||||
Reference in New Issue
Block a user