refactor: solidify startup UX and engine-aware preprocessing
- Slim core deps: move ML stack to optional extras (paddle/tables/figures/scientific/full) - Lazy settings proxy with config search paths (env var, cwd, user dir) - New commands: init, demo, setup [basic|full], first-run guard on run/watch - Engine-aware preprocessing: Paddle gets original image (fixes dark mode 0.83->0.95) - Results table shows Skipped count; lazy run-dir creation - kg_ocr marked experimental with extra, Docker defaults with OCR_PIPELINE_CONFIG - 25/25 tests, ruff clean
This commit is contained in:
116
tests/test_startup.py
Normal file
116
tests/test_startup.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from ocr_pipeline import config as config_module
|
||||
from ocr_pipeline.cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
SRC_DIR = Path(__file__).parent.parent / "src"
|
||||
|
||||
|
||||
def _isolate_config(tmp_path: Path, monkeypatch) -> None:
|
||||
"""Pretend no config exists anywhere."""
|
||||
monkeypatch.delenv(config_module.CONFIG_ENV_VAR, raising=False)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setattr(config_module, "user_config_dir", lambda: tmp_path / "no-such-dir")
|
||||
|
||||
|
||||
def test_find_config_file_prefers_env_var(tmp_path: Path, monkeypatch) -> None:
|
||||
env_cfg = tmp_path / "env.yaml"
|
||||
env_cfg.write_text("ocr:\n engine: tesseract\n", encoding="utf-8")
|
||||
cwd_dir = tmp_path / "cwd"
|
||||
cwd_dir.mkdir()
|
||||
(cwd_dir / "config.yaml").write_text("{}", encoding="utf-8")
|
||||
monkeypatch.setenv(config_module.CONFIG_ENV_VAR, str(env_cfg))
|
||||
monkeypatch.chdir(cwd_dir)
|
||||
assert config_module.find_config_file() == env_cfg
|
||||
|
||||
|
||||
def test_find_config_file_uses_cwd(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.delenv(config_module.CONFIG_ENV_VAR, raising=False)
|
||||
(tmp_path / "config.yaml").write_text("{}", encoding="utf-8")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
assert config_module.find_config_file() == tmp_path / "config.yaml"
|
||||
|
||||
|
||||
def test_find_config_file_none_when_unconfigured(tmp_path: Path, monkeypatch) -> None:
|
||||
_isolate_config(tmp_path, monkeypatch)
|
||||
assert config_module.find_config_file() is None
|
||||
|
||||
|
||||
def test_import_does_not_read_cwd_config(tmp_path: Path) -> None:
|
||||
(tmp_path / "config.yaml").write_text("{invalid yaml: [", encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", "import ocr_pipeline.config; print('ok')"],
|
||||
cwd=tmp_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={"PYTHONPATH": str(SRC_DIR), "PATH": "/usr/bin:/bin"},
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "ok" in result.stdout
|
||||
|
||||
|
||||
def test_settings_proxy_delegates() -> None:
|
||||
from ocr_pipeline.config import settings
|
||||
|
||||
assert settings.ocr.engine in {"tesseract", "paddleocr", "auto"}
|
||||
assert repr(settings)
|
||||
|
||||
|
||||
def test_init_writes_local_config(tmp_path: Path, monkeypatch) -> None:
|
||||
_isolate_config(tmp_path, monkeypatch)
|
||||
result = runner.invoke(app, ["init", "--local"])
|
||||
assert result.exit_code == 0
|
||||
content = (tmp_path / "config.yaml").read_text(encoding="utf-8")
|
||||
assert "paths:" in content
|
||||
assert "tesseract" in content
|
||||
|
||||
|
||||
def test_init_refuses_to_overwrite(tmp_path: Path, monkeypatch) -> None:
|
||||
_isolate_config(tmp_path, monkeypatch)
|
||||
(tmp_path / "config.yaml").write_text("existing: true\n", encoding="utf-8")
|
||||
result = runner.invoke(app, ["init", "--local"])
|
||||
assert result.exit_code == 1
|
||||
assert (tmp_path / "config.yaml").read_text(encoding="utf-8") == "existing: true\n"
|
||||
|
||||
|
||||
def test_run_guard_without_config_or_input_dir(tmp_path: Path, monkeypatch) -> None:
|
||||
_isolate_config(tmp_path, monkeypatch)
|
||||
result = runner.invoke(app, ["run"])
|
||||
assert result.exit_code == 2
|
||||
assert "ocr-pipeline init" in result.output
|
||||
assert "ocr-pipeline demo" in result.output
|
||||
|
||||
|
||||
def test_watch_guard_without_config(tmp_path: Path, monkeypatch) -> None:
|
||||
_isolate_config(tmp_path, monkeypatch)
|
||||
result = runner.invoke(app, ["watch"])
|
||||
assert result.exit_code == 2
|
||||
|
||||
|
||||
def test_setup_dry_run_prints_steps_without_executing() -> None:
|
||||
result = runner.invoke(app, ["setup", "full", "--dry-run"])
|
||||
assert result.exit_code == 0
|
||||
assert "uv sync" in result.output or "pip install" in result.output
|
||||
|
||||
|
||||
def test_setup_rejects_unknown_target() -> None:
|
||||
result = runner.invoke(app, ["setup", "everything", "--dry-run"])
|
||||
assert result.exit_code == 2
|
||||
|
||||
|
||||
def test_demo_image_bundled_and_valid() -> None:
|
||||
from PIL import Image
|
||||
|
||||
from ocr_pipeline.demo import demo_image_path
|
||||
|
||||
with Image.open(demo_image_path()) as img:
|
||||
assert img.size[0] >= 800
|
||||
assert img.format == "PNG"
|
||||
Reference in New Issue
Block a user