update minio port
This commit is contained in:
41
backend/tests/reference_bundle_fixtures.py
Normal file
41
backend/tests/reference_bundle_fixtures.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Shared fixtures for minimal reference bundle trees in batch/subprocess tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def complete_result_tsv_body() -> str:
|
||||
return (
|
||||
"rank\tstart_sec\tend_sec\tproduct_id_top1\ttop1_name\ttop1_conf\n"
|
||||
"1\t0\t1\tP1\t耗材1\t1.0\n"
|
||||
"医生信息:测试医生 (id=123, conf=0.99)\n"
|
||||
)
|
||||
|
||||
|
||||
def write_minimal_reference_bundle(bundle: Path) -> None:
|
||||
bundle.mkdir(parents=True)
|
||||
(bundle / "main.py").write_text("# fake\n", encoding="utf-8")
|
||||
(bundle / "code").mkdir()
|
||||
(bundle / "code" / "repo_root.py").write_text("# fake\n", encoding="utf-8")
|
||||
(bundle / "configs").mkdir()
|
||||
(bundle / "configs" / "default_config.yaml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"io": {"video": "", "excel": "", "out": "", "whitelist_json": None},
|
||||
"weights": {},
|
||||
"runtime": {"work_dir": None, "keep_work_dir": False, "python": None},
|
||||
"device": {},
|
||||
"phase1": {},
|
||||
"phase2": {},
|
||||
"classification": {},
|
||||
"tear_merge": {},
|
||||
"output": {},
|
||||
},
|
||||
allow_unicode=True,
|
||||
sort_keys=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Tests for offline batch orchestration (app.algo_host)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@@ -12,73 +14,42 @@ import yaml
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.algorithm_runner import reference_bundle_runtime
|
||||
from app.algo_host import bundle as bundle_runtime
|
||||
from app.algo_host.batch_service import BatchAlgorithmService, BatchRunResult
|
||||
from app.algo_host.job_workspace import build_job_config
|
||||
from app.algo_host.result_adapter import (
|
||||
doctor_id_for_consumption_rows,
|
||||
is_reference_result_complete,
|
||||
parse_reference_doctor_info,
|
||||
parse_reference_tsv,
|
||||
)
|
||||
from app.algo_host.subprocess_runner import (
|
||||
build_batch_main_command,
|
||||
build_visualization_command,
|
||||
describe_batch_returncode,
|
||||
format_batch_failure,
|
||||
)
|
||||
from app.algo_host.transcode import (
|
||||
VISUALIZATION_MAX_WIDTH,
|
||||
batch_input_needs_normalize,
|
||||
browser_transcode_tmp_path,
|
||||
ensure_batch_pipeline_input_video,
|
||||
is_browser_compatible_mp4,
|
||||
transcode_visualization_for_browser,
|
||||
)
|
||||
from app.api import router as api_router
|
||||
from app.dependencies import get_surgery_pipeline
|
||||
from app.domain.consumption import SurgeryConsumptionStored
|
||||
from app.routers import recording_demo
|
||||
from app.schemas import SurgeryConsumptionDetail
|
||||
from app.services.video_batch_runner import (
|
||||
VideoBatchRunResult,
|
||||
VideoBatchRunner,
|
||||
_batch_input_needs_normalize,
|
||||
_is_browser_compatible_mp4,
|
||||
_transcode_visualization_for_browser,
|
||||
browser_transcode_tmp_path,
|
||||
build_reference_command,
|
||||
VISUALIZATION_MAX_WIDTH,
|
||||
build_reference_config,
|
||||
build_reference_visualization_command,
|
||||
describe_batch_returncode,
|
||||
doctor_id_for_consumption_rows,
|
||||
ensure_batch_pipeline_input_video,
|
||||
ensure_reference_actionformer_nms_patch,
|
||||
format_batch_failure,
|
||||
is_reference_result_complete,
|
||||
parse_reference_doctor_info,
|
||||
parse_reference_tsv,
|
||||
)
|
||||
from app.services.video_batch_cleanup import VISUALIZATION_FILENAME, visualization_output_path
|
||||
from tests.reference_bundle_fixtures import complete_result_tsv_body, write_minimal_reference_bundle
|
||||
|
||||
|
||||
def _complete_result_tsv_body() -> str:
|
||||
return (
|
||||
"rank\tstart_sec\tend_sec\tproduct_id_top1\ttop1_name\ttop1_conf\n"
|
||||
"1\t0\t1\tP1\t耗材1\t1.0\n"
|
||||
"医生信息:测试医生 (id=123, conf=0.99)\n"
|
||||
)
|
||||
|
||||
|
||||
def _write_minimal_reference_bundle(bundle: Path) -> None:
|
||||
bundle.mkdir(parents=True)
|
||||
(bundle / "main.py").write_text("# fake\n", encoding="utf-8")
|
||||
(bundle / "code").mkdir()
|
||||
(bundle / "code" / "repo_root.py").write_text("# fake\n", encoding="utf-8")
|
||||
(bundle / "configs").mkdir()
|
||||
(bundle / "configs" / "default_config.yaml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"io": {"video": "", "excel": "", "out": "", "whitelist_json": None},
|
||||
"weights": {},
|
||||
"runtime": {"work_dir": None, "keep_work_dir": False, "python": None},
|
||||
"device": {},
|
||||
"phase1": {},
|
||||
"phase2": {},
|
||||
"classification": {},
|
||||
"tear_merge": {},
|
||||
"output": {},
|
||||
},
|
||||
allow_unicode=True,
|
||||
sort_keys=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_build_reference_config_does_not_keep_work_dir(tmp_path: Path) -> None:
|
||||
def test_build_job_config_does_not_keep_work_dir(tmp_path: Path) -> None:
|
||||
bundle = tmp_path / "bundle"
|
||||
_write_minimal_reference_bundle(bundle)
|
||||
cfg = build_reference_config(
|
||||
write_minimal_reference_bundle(bundle)
|
||||
cfg = build_job_config(
|
||||
bundle_dir=bundle,
|
||||
video_path=tmp_path / "input.mp4",
|
||||
output_path=tmp_path / "out.tsv",
|
||||
@@ -91,7 +62,7 @@ def test_build_reference_config_does_not_keep_work_dir(tmp_path: Path) -> None:
|
||||
|
||||
def test_latest_visualization_path_uses_vis_directory(tmp_path: Path) -> None:
|
||||
root = tmp_path / "batch"
|
||||
runner = VideoBatchRunner(root_dir=root)
|
||||
runner = BatchAlgorithmService(root_dir=root)
|
||||
assert runner.latest_visualization_path("100001") is None
|
||||
|
||||
vis_path = visualization_output_path(root, "100001")
|
||||
@@ -102,7 +73,7 @@ def test_latest_visualization_path_uses_vis_directory(tmp_path: Path) -> None:
|
||||
|
||||
def test_is_reference_result_complete_requires_footer_and_rows(tmp_path: Path) -> None:
|
||||
complete = tmp_path / "complete.tsv"
|
||||
complete.write_text(_complete_result_tsv_body(), encoding="utf-8")
|
||||
complete.write_text(complete_result_tsv_body(), encoding="utf-8")
|
||||
partial = tmp_path / "partial.tsv"
|
||||
partial.write_text(
|
||||
"rank\tstart_sec\tend_sec\tproduct_id_top1\ttop1_name\ttop1_conf\n"
|
||||
@@ -225,11 +196,11 @@ def test_ensure_batch_pipeline_input_video_normalizes_non_h264(tmp_path: Path) -
|
||||
text=True,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert _batch_input_needs_normalize(source)
|
||||
assert batch_input_needs_normalize(source)
|
||||
ensure_batch_pipeline_input_video(source_path=source, dest_path=dest)
|
||||
assert dest.is_file()
|
||||
assert _is_browser_compatible_mp4(dest)
|
||||
assert not _batch_input_needs_normalize(dest)
|
||||
assert is_browser_compatible_mp4(dest)
|
||||
assert not batch_input_needs_normalize(dest)
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not installed")
|
||||
@@ -259,28 +230,27 @@ def test_transcode_visualization_for_browser_writes_h264_mp4(tmp_path: Path) ->
|
||||
text=True,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert _transcode_visualization_for_browser(source, output)
|
||||
assert transcode_visualization_for_browser(source, output)
|
||||
assert output.is_file()
|
||||
assert output.stat().st_size > 0
|
||||
assert not browser_transcode_tmp_path(output).exists()
|
||||
assert _is_browser_compatible_mp4(output)
|
||||
assert is_browser_compatible_mp4(output)
|
||||
|
||||
|
||||
def test_build_reference_visualization_command_uses_hand_model_and_result_tsv(
|
||||
def test_build_visualization_command_uses_hand_model_and_result_tsv(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
bundle = tmp_path / "bundle"
|
||||
_write_minimal_reference_bundle(bundle)
|
||||
write_minimal_reference_bundle(bundle)
|
||||
(bundle / "weights").mkdir()
|
||||
(bundle / "weights" / "hand_detect.pt").write_bytes(b"fake")
|
||||
(bundle / "visualize_result_video.py").write_text("# fake\n", encoding="utf-8")
|
||||
cfg_path = bundle / "configs" / "default_config.yaml"
|
||||
cfg = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
|
||||
cfg["weights"]["hand"] = "weights/hand_detect.pt"
|
||||
cfg["phase2"]["det_conf"] = 0.55
|
||||
cfg_path.write_text(yaml.safe_dump(cfg, allow_unicode=True, sort_keys=False), encoding="utf-8")
|
||||
|
||||
cmd = build_reference_visualization_command(
|
||||
cmd = build_visualization_command(
|
||||
bundle_dir=bundle,
|
||||
video_path=tmp_path / "input.mp4",
|
||||
result_path=tmp_path / "result.tsv",
|
||||
@@ -291,14 +261,13 @@ def test_build_reference_visualization_command_uses_hand_model_and_result_tsv(
|
||||
assert str(tmp_path / "result.tsv") in cmd
|
||||
assert "--hand-model" in cmd
|
||||
assert str(bundle / "weights" / "hand_detect.pt") in cmd
|
||||
assert "--det-conf" in cmd
|
||||
assert cmd[cmd.index("--det-conf") + 1] == "0.55"
|
||||
assert "--det-conf" not in cmd
|
||||
assert "--max-width" in cmd
|
||||
assert cmd[cmd.index("--max-width") + 1] == str(VISUALIZATION_MAX_WIDTH)
|
||||
|
||||
|
||||
def test_build_reference_command_uses_5_15_main_py(tmp_path: Path) -> None:
|
||||
cmd = build_reference_command(
|
||||
def test_build_batch_main_command_uses_5_15_main_py(tmp_path: Path) -> None:
|
||||
cmd = build_batch_main_command(
|
||||
bundle_dir=tmp_path / "algorithm_subprocesses" / "5.15",
|
||||
config_path=tmp_path / "config.yaml",
|
||||
)
|
||||
@@ -309,12 +278,12 @@ def test_build_reference_command_uses_5_15_main_py(tmp_path: Path) -> None:
|
||||
assert cmd[6:] == ["--config", str(tmp_path / "config.yaml")]
|
||||
|
||||
|
||||
def test_video_batch_runner_uses_reference_bundle_relative_env_override(
|
||||
def test_batch_service_respects_reference_bundle_relative_env(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
bundle = tmp_path / "algorithm_subprocesses" / "custom"
|
||||
_write_minimal_reference_bundle(bundle)
|
||||
write_minimal_reference_bundle(bundle)
|
||||
video = tmp_path / "case.mp4"
|
||||
video.write_bytes(b"same-video")
|
||||
calls: list[list[str]] = []
|
||||
@@ -329,14 +298,14 @@ def test_video_batch_runner_uses_reference_bundle_relative_env_override(
|
||||
config = yaml.safe_load(Path(cmd[cmd.index("--config") + 1]).read_text(encoding="utf-8"))
|
||||
output = Path(config["io"]["out"])
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(_complete_result_tsv_body(), encoding="utf-8")
|
||||
output.write_text(complete_result_tsv_body(), encoding="utf-8")
|
||||
return _Proc()
|
||||
|
||||
monkeypatch.setenv("REFERENCE_BUNDLE_RELATIVE", "algorithm_subprocesses/custom")
|
||||
monkeypatch.setattr(reference_bundle_runtime, "REPO_ROOT", tmp_path)
|
||||
monkeypatch.setattr("app.services.video_batch_runner.subprocess.run", fake_run)
|
||||
monkeypatch.setattr(bundle_runtime, "REPO_ROOT", tmp_path)
|
||||
monkeypatch.setattr("app.algo_host.subprocess_runner.subprocess.run", fake_run)
|
||||
|
||||
runner = VideoBatchRunner(root_dir=tmp_path / "batch")
|
||||
runner = BatchAlgorithmService(root_dir=tmp_path / "batch")
|
||||
result = runner.run(
|
||||
surgery_id="100001",
|
||||
uploaded_video_path=video,
|
||||
@@ -349,7 +318,7 @@ def test_video_batch_runner_uses_reference_bundle_relative_env_override(
|
||||
assert result.details[0].item_name == "耗材1"
|
||||
|
||||
|
||||
def test_video_batch_runner_reuses_cache_within_same_surgery(
|
||||
def test_batch_service_reuses_cache_on_repeat_run(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
@@ -423,16 +392,16 @@ def test_video_batch_runner_reuses_cache_within_same_surgery(
|
||||
config = yaml.safe_load(Path(cmd[cmd.index("--config") + 1]).read_text(encoding="utf-8"))
|
||||
output = Path(config["io"]["out"])
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(_complete_result_tsv_body(), encoding="utf-8")
|
||||
output.write_text(complete_result_tsv_body(), encoding="utf-8")
|
||||
return _Proc()
|
||||
|
||||
monkeypatch.setattr("app.services.video_batch_runner.subprocess.run", fake_run)
|
||||
monkeypatch.setattr("app.algo_host.subprocess_runner.subprocess.run", fake_run)
|
||||
monkeypatch.setattr(
|
||||
"app.services.video_batch_runner.VideoBatchRunner._generate_visualization",
|
||||
"app.algo_host.batch_service.BatchAlgorithmService._generate_visualization",
|
||||
lambda *_a, **_k: None,
|
||||
)
|
||||
|
||||
runner = VideoBatchRunner(bundle_dir=bundle, root_dir=tmp_path / "batch")
|
||||
runner = BatchAlgorithmService(bundle_dir=bundle, root_dir=tmp_path / "batch")
|
||||
first = runner.run(
|
||||
surgery_id="100001",
|
||||
uploaded_video_path=video,
|
||||
@@ -459,7 +428,7 @@ def test_video_batch_runner_reuses_cache_within_same_surgery(
|
||||
assert whitelist == {"allowed_names": ["耗材1"]}
|
||||
|
||||
|
||||
def test_video_batch_runner_does_not_share_cache_across_surgeries(
|
||||
def test_batch_service_shares_cache_across_surgeries_for_same_video(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
@@ -501,40 +470,27 @@ def test_video_batch_runner_does_not_share_cache_across_surgeries(
|
||||
config = yaml.safe_load(Path(cmd[cmd.index("--config") + 1]).read_text(encoding="utf-8"))
|
||||
output = Path(config["io"]["out"])
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(_complete_result_tsv_body(), encoding="utf-8")
|
||||
output.write_text(complete_result_tsv_body(), encoding="utf-8")
|
||||
return _Proc()
|
||||
|
||||
monkeypatch.setattr("app.services.video_batch_runner.subprocess.run", fake_run)
|
||||
monkeypatch.setattr("app.algo_host.subprocess_runner.subprocess.run", fake_run)
|
||||
monkeypatch.setattr(
|
||||
"app.services.video_batch_runner.VideoBatchRunner._generate_visualization",
|
||||
"app.algo_host.batch_service.BatchAlgorithmService._generate_visualization",
|
||||
lambda *_a, **_k: None,
|
||||
)
|
||||
|
||||
runner = VideoBatchRunner(bundle_dir=bundle, root_dir=tmp_path / "batch")
|
||||
runner = BatchAlgorithmService(bundle_dir=bundle, root_dir=tmp_path / "batch")
|
||||
first = runner.run(surgery_id="100001", uploaded_video_path=video, original_filename="case.mp4", candidate_consumables=[])
|
||||
second = runner.run(surgery_id="100002", uploaded_video_path=video, original_filename="case.mp4", candidate_consumables=[])
|
||||
|
||||
assert len(calls) == 2
|
||||
assert len(calls) == 1
|
||||
assert first.reused_cache is False
|
||||
assert second.reused_cache is False
|
||||
assert second.reused_cache is True
|
||||
assert first.video_sha256 == second.video_sha256
|
||||
assert "100001" in str(first.output_path)
|
||||
assert "100002" in str(second.output_path)
|
||||
|
||||
|
||||
def test_ensure_reference_actionformer_nms_patch_replaces_compiled_extension_import(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
bundle = tmp_path / "bundle"
|
||||
_write_minimal_reference_bundle(bundle)
|
||||
nms_path = bundle / "code" / "actionformer_release" / "libs" / "utils" / "nms.py"
|
||||
nms_path.parent.mkdir(parents=True)
|
||||
nms_path.write_text("from . import nms_1d_cpu\n", encoding="utf-8")
|
||||
|
||||
assert ensure_reference_actionformer_nms_patch(bundle) is True
|
||||
patched = nms_path.read_text(encoding="utf-8")
|
||||
assert "from . import nms_1d_cpu" not in patched
|
||||
assert "pure-PyTorch" in patched
|
||||
assert first.output_path == second.output_path
|
||||
assert "/cache/" in str(first.output_path)
|
||||
assert "100001" not in str(first.output_path)
|
||||
assert "100002" not in str(second.output_path)
|
||||
|
||||
|
||||
def test_batch_failure_message_keeps_stdout_stderr_and_decodes_245(tmp_path: Path) -> None:
|
||||
@@ -575,19 +531,19 @@ def test_demo_video_batch_endpoint_writes_queryable_result(
|
||||
def __init__(self) -> None:
|
||||
self.root_dir = root_dir
|
||||
|
||||
def run(self, **kwargs: Any) -> VideoBatchRunResult:
|
||||
def run(self, **kwargs: Any) -> BatchRunResult:
|
||||
assert kwargs["surgery_id"] == "100001"
|
||||
assert kwargs["uploaded_video_path"].is_file()
|
||||
assert kwargs["candidate_consumables"] == ["耗材1"]
|
||||
assert kwargs.get("include_visualization") is False
|
||||
cache_dir = root_dir / "cache" / "100001" / ("a" * 64) / "c1"
|
||||
cache_dir = root_dir / "cache" / ("a" * 64) / "c1"
|
||||
cache_input = cache_dir / "input" / "input.mp4"
|
||||
cache_input.parent.mkdir(parents=True)
|
||||
cache_input.write_bytes(b"pipeline-input")
|
||||
output_path = cache_dir / "output" / "result.tsv"
|
||||
output_path.parent.mkdir(parents=True)
|
||||
output_path.write_text(_complete_result_tsv_body(), encoding="utf-8")
|
||||
return VideoBatchRunResult(
|
||||
output_path.write_text(complete_result_tsv_body(), encoding="utf-8")
|
||||
return BatchRunResult(
|
||||
video_sha256="a" * 64,
|
||||
candidate_cache_key="c1",
|
||||
input_path=root_dir / "100001" / "input" / "saved.mp4",
|
||||
@@ -629,7 +585,7 @@ def test_demo_video_batch_endpoint_writes_queryable_result(
|
||||
for r in rows
|
||||
]
|
||||
|
||||
monkeypatch.setattr(recording_demo, "VideoBatchRunner", _FakeRunner)
|
||||
monkeypatch.setattr(recording_demo, "BatchAlgorithmService", _FakeRunner)
|
||||
pipeline = _FakePipeline()
|
||||
|
||||
app = FastAPI()
|
||||
@@ -648,7 +604,7 @@ def test_demo_video_batch_endpoint_writes_queryable_result(
|
||||
assert body["status"] == "accepted"
|
||||
assert body["visualization_url"] is None
|
||||
assert vis_calls == []
|
||||
assert not (root_dir / "cache" / "100001").exists()
|
||||
assert not (root_dir / "cache" / ("a" * 64)).exists()
|
||||
assert not (root_dir / "100001").exists()
|
||||
|
||||
got = client.get("/client/surgeries/100001/result")
|
||||
@@ -679,15 +635,15 @@ def test_demo_video_batch_endpoint_stages_vis_and_purges_cache_when_requested(
|
||||
def __init__(self) -> None:
|
||||
self.root_dir = root_dir
|
||||
|
||||
def run(self, **kwargs: Any) -> VideoBatchRunResult:
|
||||
cache_dir = root_dir / "cache" / "100001" / ("b" * 64) / "c1"
|
||||
def run(self, **kwargs: Any) -> BatchRunResult:
|
||||
cache_dir = root_dir / "cache" / ("b" * 64) / "c1"
|
||||
cache_input = cache_dir / "input" / "input.mp4"
|
||||
cache_input.parent.mkdir(parents=True)
|
||||
cache_input.write_bytes(b"pipeline-input")
|
||||
output_path = cache_dir / "output" / "result.tsv"
|
||||
output_path.parent.mkdir(parents=True)
|
||||
output_path.write_text(_complete_result_tsv_body(), encoding="utf-8")
|
||||
return VideoBatchRunResult(
|
||||
output_path.write_text(complete_result_tsv_body(), encoding="utf-8")
|
||||
return BatchRunResult(
|
||||
video_sha256="b" * 64,
|
||||
candidate_cache_key="c1",
|
||||
input_path=root_dir / "100001" / "input" / "saved.mp4",
|
||||
@@ -708,7 +664,7 @@ def test_demo_video_batch_endpoint_stages_vis_and_purges_cache_when_requested(
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(recording_demo, "VideoBatchRunner", _FakeRunner)
|
||||
monkeypatch.setattr(recording_demo, "BatchAlgorithmService", _FakeRunner)
|
||||
app = FastAPI()
|
||||
app.include_router(recording_demo.router)
|
||||
app.dependency_overrides[get_surgery_pipeline] = lambda: _FakePipeline()
|
||||
@@ -727,7 +683,7 @@ def test_demo_video_batch_endpoint_stages_vis_and_purges_cache_when_requested(
|
||||
body = res.json()
|
||||
assert body["visualization_url"] == "/internal/demo/offline-batch/100001/visualization"
|
||||
assert vis_calls == ["100001"]
|
||||
assert not (root_dir / "cache" / "100001").exists()
|
||||
assert not (root_dir / "cache" / ("b" * 64)).exists()
|
||||
pending_input = root_dir / "vis_pending" / "100001" / "input.mp4"
|
||||
pending_tsv = root_dir / "vis_pending" / "100001" / "result.tsv"
|
||||
assert pending_input.read_bytes() == b"pipeline-input"
|
||||
@@ -1,7 +1,7 @@
|
||||
"""FastAPI → 算法子进程调用链单元测试。
|
||||
|
||||
覆盖两条生产路径:
|
||||
1. ``POST /internal/demo/offline-batch`` → ``VideoBatchRunner`` → ``subprocess.run``(reference bundle ``main.py``)
|
||||
1. ``POST /internal/demo/offline-batch`` → ``BatchAlgorithmService`` → ``subprocess.run``(reference bundle ``main.py``)
|
||||
2. ``POST /client/surgeries/start`` → ``CameraSessionManager`` → ``asyncio.create_subprocess_exec``(``python -m app.algorithm_runner``)
|
||||
"""
|
||||
|
||||
@@ -24,8 +24,9 @@ from app.config import Settings
|
||||
from app.dependencies import build_container, get_surgery_pipeline, get_voice_terminal_hub
|
||||
from app.routers import recording_demo
|
||||
from app.services.video.session_manager import CameraSessionManager
|
||||
from app.services.video_batch_runner import VideoBatchRunner, build_reference_command
|
||||
from tests.test_video_batch_runner import _complete_result_tsv_body, _write_minimal_reference_bundle
|
||||
from app.algo_host.batch_service import BatchAlgorithmService
|
||||
from app.algo_host.subprocess_runner import build_batch_main_command
|
||||
from tests.reference_bundle_fixtures import complete_result_tsv_body, write_minimal_reference_bundle
|
||||
|
||||
|
||||
def _fake_reference_subprocess_run(captured: list[dict[str, Any]]):
|
||||
@@ -46,7 +47,7 @@ def _fake_reference_subprocess_run(captured: list[dict[str, Any]]):
|
||||
)
|
||||
output = Path(config["io"]["out"])
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(_complete_result_tsv_body(), encoding="utf-8")
|
||||
output.write_text(complete_result_tsv_body(), encoding="utf-8")
|
||||
return _Proc()
|
||||
|
||||
return _run
|
||||
@@ -55,7 +56,7 @@ def _fake_reference_subprocess_run(captured: list[dict[str, Any]]):
|
||||
@pytest.fixture
|
||||
def reference_bundle(tmp_path: Path) -> Path:
|
||||
bundle = tmp_path / "algorithm_subprocesses" / "5.15"
|
||||
_write_minimal_reference_bundle(bundle)
|
||||
write_minimal_reference_bundle(bundle)
|
||||
return bundle
|
||||
|
||||
|
||||
@@ -67,17 +68,13 @@ def test_video_batch_endpoint_invokes_reference_bundle_subprocess(
|
||||
) -> None:
|
||||
monkeypatch.setattr(recording_demo.settings, "demo_orchestrator_enabled", True)
|
||||
monkeypatch.setattr(
|
||||
"app.services.video_batch_runner.resolve_reference_bundle_dir",
|
||||
"app.algo_host.bundle.resolve_reference_bundle_dir",
|
||||
lambda _override=None: reference_bundle.resolve(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.video_batch_runner.ensure_reference_actionformer_nms_patch",
|
||||
lambda _bundle: True,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
recording_demo,
|
||||
"VideoBatchRunner",
|
||||
lambda: VideoBatchRunner(
|
||||
"BatchAlgorithmService",
|
||||
lambda: BatchAlgorithmService(
|
||||
bundle_dir=reference_bundle,
|
||||
root_dir=tmp_path / "video_batch",
|
||||
),
|
||||
@@ -85,7 +82,7 @@ def test_video_batch_endpoint_invokes_reference_bundle_subprocess(
|
||||
|
||||
captured: list[dict[str, Any]] = []
|
||||
monkeypatch.setattr(
|
||||
"app.services.video_batch_runner.subprocess.run",
|
||||
"app.algo_host.subprocess_runner.subprocess.run",
|
||||
_fake_reference_subprocess_run(captured),
|
||||
)
|
||||
|
||||
@@ -111,7 +108,7 @@ def test_video_batch_endpoint_invokes_reference_bundle_subprocess(
|
||||
cmd: list[str] = call["cmd"]
|
||||
kwargs: dict[str, Any] = call["kwargs"]
|
||||
|
||||
expected = build_reference_command(
|
||||
expected = build_batch_main_command(
|
||||
bundle_dir=reference_bundle,
|
||||
config_path=Path(cmd[cmd.index("--config") + 1]),
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ def test_purge_batch_artifacts_removes_cache_and_uploads(tmp_path: Path) -> None
|
||||
surgery_id = "100001"
|
||||
digest = "d" * 64
|
||||
candidate_key = "c1"
|
||||
cache_entry = root / "cache" / surgery_id / digest / candidate_key
|
||||
cache_entry = root / "cache" / digest / candidate_key
|
||||
(cache_entry / "input").mkdir(parents=True)
|
||||
(cache_entry / "input" / "input.mp4").write_bytes(b"x" * 100)
|
||||
(cache_entry / "output").mkdir(parents=True)
|
||||
|
||||
Reference in New Issue
Block a user