e4dcfc49aa
Tests / Lint and Format (push) Waiting to run
Tests / Web Node Tests (push) Waiting to run
Tests / Import Check (Python 3.11) (push) Waiting to run
Tests / Import Check (Python 3.12) (push) Waiting to run
Tests / Import Check (Python 3.13) (push) Waiting to run
Tests / Import Check (Python 3.14) (push) Waiting to run
Tests / Python Tests (Python 3.11) (push) Blocked by required conditions
Tests / Python Tests (Python 3.12) (push) Blocked by required conditions
Tests / Python Tests (Python 3.13) (push) Blocked by required conditions
Tests / Python Tests (Python 3.14) (push) Blocked by required conditions
Tests / Test Summary (push) Blocked by required conditions
176 lines
5.8 KiB
Python
176 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import threading
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from deeptutor.services.parsing.engines.mineru import models as mineru_models
|
|
from deeptutor.services.parsing.engines.mineru.models import (
|
|
ModelDownloadManager,
|
|
model_env_overrides,
|
|
resolve_models_downloader,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Downloader resolution
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_resolver_prefers_configured_cli_sibling(tmp_path: Path) -> None:
|
|
bin_dir = tmp_path / "env" / "bin"
|
|
bin_dir.mkdir(parents=True)
|
|
cli = bin_dir / "mineru"
|
|
cli.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
cli.chmod(0o755)
|
|
downloader = bin_dir / "mineru-models-download"
|
|
downloader.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
downloader.chmod(0o755)
|
|
|
|
resolved = resolve_models_downloader(str(cli))
|
|
assert resolved == {"found": True, "path": str(downloader)}
|
|
|
|
|
|
def test_resolver_reports_missing_sibling_no_path_fallback(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Even if PATH has the downloader, a configured CLI without a sibling
|
|
# downloader is a miss — the configured env is authoritative.
|
|
monkeypatch.setattr(
|
|
mineru_models.shutil, "which", lambda cmd: "/usr/bin/mineru-models-download"
|
|
)
|
|
cli = tmp_path / "mineru"
|
|
cli.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
cli.chmod(0o755)
|
|
|
|
resolved = resolve_models_downloader(str(cli))
|
|
assert resolved["found"] is False
|
|
assert resolved["path"].endswith("mineru-models-download")
|
|
|
|
|
|
def test_resolver_falls_back_to_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
mineru_models.shutil, "which", lambda cmd: "/opt/bin/mineru-models-download"
|
|
)
|
|
assert resolve_models_downloader("") == {
|
|
"found": True,
|
|
"path": "/opt/bin/mineru-models-download",
|
|
}
|
|
|
|
monkeypatch.setattr(mineru_models.shutil, "which", lambda cmd: None)
|
|
assert resolve_models_downloader("")["found"] is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Env overrides
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_model_env_overrides_shapes() -> None:
|
|
assert model_env_overrides("huggingface") == {"MINERU_MODEL_SOURCE": "huggingface"}
|
|
assert model_env_overrides("huggingface", "https://hf-mirror.com/") == {
|
|
"MINERU_MODEL_SOURCE": "huggingface",
|
|
"HF_ENDPOINT": "https://hf-mirror.com",
|
|
}
|
|
# Endpoint is an HF-only concept; modelscope ignores it.
|
|
assert model_env_overrides("modelscope", "https://hf-mirror.com") == {
|
|
"MINERU_MODEL_SOURCE": "modelscope"
|
|
}
|
|
# Unknown source degrades to the default.
|
|
assert model_env_overrides("weird")["MINERU_MODEL_SOURCE"] == "huggingface"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Download job manager
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _FakeProc:
|
|
def __init__(self, lines, returncode: int = 0):
|
|
self.stdout = iter(lines)
|
|
self._returncode = returncode
|
|
self.terminated = False
|
|
|
|
def wait(self) -> int:
|
|
return -15 if self.terminated else self._returncode
|
|
|
|
def poll(self):
|
|
return None if not self.terminated else -15
|
|
|
|
def terminate(self) -> None:
|
|
self.terminated = True
|
|
|
|
|
|
def _wait_for_terminal(manager: ModelDownloadManager, timeout: float = 2.0) -> str:
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
state = manager.status()["state"]
|
|
if state != "running":
|
|
return state
|
|
time.sleep(0.01)
|
|
return manager.status()["state"]
|
|
|
|
|
|
def test_manager_happy_path_and_cursor(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
manager = ModelDownloadManager()
|
|
monkeypatch.setattr(
|
|
mineru_models.subprocess,
|
|
"Popen",
|
|
lambda *a, **k: _FakeProc(["downloading layout model...\n"]),
|
|
)
|
|
|
|
result = manager.start(
|
|
downloader="/x/mineru-models-download",
|
|
model_type="pipeline",
|
|
source="huggingface",
|
|
)
|
|
assert result["ok"] is True
|
|
assert _wait_for_terminal(manager) == "done"
|
|
|
|
status = manager.status(0)
|
|
assert status["lines"] == ["downloading layout model..."]
|
|
assert status["message"] == "Download finished."
|
|
# Cursor protocol: re-polling from next_cursor yields nothing new.
|
|
assert manager.status(status["next_cursor"])["lines"] == []
|
|
|
|
|
|
def test_manager_nonzero_exit_is_failed(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
manager = ModelDownloadManager()
|
|
monkeypatch.setattr(
|
|
mineru_models.subprocess,
|
|
"Popen",
|
|
lambda *a, **k: _FakeProc(["boom\n"], returncode=3),
|
|
)
|
|
assert manager.start(downloader="/x/dl", model_type="pipeline", source="huggingface")["ok"]
|
|
assert _wait_for_terminal(manager) == "failed"
|
|
assert "code 3" in manager.status()["message"]
|
|
|
|
|
|
def test_manager_rejects_concurrent_start_and_cancels(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
release = threading.Event()
|
|
|
|
def blocking_lines():
|
|
yield "starting\n"
|
|
release.wait(timeout=5)
|
|
|
|
proc = _FakeProc(blocking_lines())
|
|
monkeypatch.setattr(mineru_models.subprocess, "Popen", lambda *a, **k: proc)
|
|
|
|
manager = ModelDownloadManager()
|
|
assert manager.start(downloader="/x/dl", model_type="all", source="modelscope")["ok"]
|
|
# Busy: a second start is rejected while the first is running.
|
|
busy = manager.start(downloader="/x/dl", model_type="pipeline", source="huggingface")
|
|
assert busy["ok"] is False
|
|
|
|
cancelled = manager.cancel()
|
|
assert cancelled["ok"] is True
|
|
release.set()
|
|
assert _wait_for_terminal(manager) == "cancelled"
|
|
|
|
|
|
def test_manager_cancel_without_job() -> None:
|
|
assert ModelDownloadManager().cancel()["ok"] is False
|