add doctor command, fix ISBN false positives, wire up CI
Some checks failed
tests / core (macos-latest, py3.11) (push) Has been cancelled
tests / core (macos-latest, py3.12) (push) Has been cancelled
tests / core (macos-latest, py3.13) (push) Has been cancelled
tests / core (ubuntu-latest, py3.11) (push) Has been cancelled
tests / core (ubuntu-latest, py3.12) (push) Has been cancelled
tests / core (ubuntu-latest, py3.13) (push) Has been cancelled
tests / doctor CLI smoke test (push) Has been cancelled
Some checks failed
tests / core (macos-latest, py3.11) (push) Has been cancelled
tests / core (macos-latest, py3.12) (push) Has been cancelled
tests / core (macos-latest, py3.13) (push) Has been cancelled
tests / core (ubuntu-latest, py3.11) (push) Has been cancelled
tests / core (ubuntu-latest, py3.12) (push) Has been cancelled
tests / core (ubuntu-latest, py3.13) (push) Has been cancelled
tests / doctor CLI smoke test (push) Has been cancelled
ISBN regex now requires an explicit prefix, so it stops matching years, URLs, job IDs, and dilution ratios. doctor checks the environment end to end: Python, Tesseract, PaddleOCR, the PyTorch backend, table/figure detectors, scispaCy, config, DB, output. CI runs on 2 OS x 3 Python versions plus a doctor smoke job. Also added a changelog
This commit is contained in:
66
tests/test_citations.py
Normal file
66
tests/test_citations.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from ocr_pipeline.citations.regex_extractor import Citation, RegexCitationExtractor
|
||||
|
||||
|
||||
def _isbns(text: str) -> list[str]:
|
||||
return [c.identifier for c in RegexCitationExtractor.extract(text) if c.type == "isbn"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "expected"),
|
||||
[
|
||||
("ISBN:978-0-306-40615-7", "ISBN:9780306406157"),
|
||||
("ISBN 978-0-306-40615-7", "ISBN:9780306406157"),
|
||||
("ISBN-13: 978-0-306-40615-7", "ISBN:9780306406157"),
|
||||
("ISBN:0-306-40615-2", "ISBN:0306406152"),
|
||||
("ISBN 0-306-40615-2", "ISBN:0306406152"),
|
||||
("ISBN-10: 0-306-40615-2", "ISBN:0306406152"),
|
||||
],
|
||||
)
|
||||
def test_recognises_real_isbns(text: str, expected: str) -> None:
|
||||
assert expected in _isbns(text)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text",
|
||||
[
|
||||
"2024-01-15",
|
||||
"Due in 17 days, 2024",
|
||||
"https://www.jobbnorge.no/jobs/300588",
|
||||
"https://euraxess.ec.europa.eu/jobs/432300",
|
||||
"Anti-BRCA1 (1:1000), Anti-beta-actin (1:5000)",
|
||||
"30 ug per lane; gel 10% SDS-PAGE",
|
||||
"Lane 1: WT control",
|
||||
"PMID: 12345678",
|
||||
"DOI: 10.1038/nature12345",
|
||||
],
|
||||
)
|
||||
def test_no_isbn_false_positives(text: str) -> None:
|
||||
assert _isbns(text) == []
|
||||
|
||||
|
||||
def test_isbn_stripped_to_digits() -> None:
|
||||
identifiers = _isbns("ISBN: 978-0-306-40615-7")
|
||||
assert identifiers == ["ISBN:9780306406157"]
|
||||
|
||||
|
||||
def test_doi_still_works() -> None:
|
||||
dois = [
|
||||
c for c in RegexCitationExtractor.extract("See DOI: 10.1038/nature12345") if c.type == "doi"
|
||||
]
|
||||
assert len(dois) == 1
|
||||
assert dois[0].identifier == "10.1038/nature12345"
|
||||
|
||||
|
||||
def test_pmid_still_works() -> None:
|
||||
pmids = [c for c in RegexCitationExtractor.extract("PMID: 12345678") if c.type == "pmid"]
|
||||
assert len(pmids) == 1
|
||||
assert pmids[0].identifier == "PMID:12345678"
|
||||
|
||||
|
||||
def test_citation_dataclass_fields() -> None:
|
||||
c = Citation(raw="x", type="doi", identifier="y", confidence=0.5)
|
||||
assert c.context == ""
|
||||
154
tests/test_doctor.py
Normal file
154
tests/test_doctor.py
Normal file
@@ -0,0 +1,154 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from ocr_pipeline.cli import app
|
||||
from ocr_pipeline.config import settings
|
||||
from ocr_pipeline.doctor import (
|
||||
check_config,
|
||||
check_db,
|
||||
check_output_dir,
|
||||
check_paddle,
|
||||
check_python,
|
||||
check_spacy,
|
||||
check_tables,
|
||||
check_tesseract,
|
||||
check_torch,
|
||||
exit_code_for,
|
||||
render,
|
||||
run_checks,
|
||||
)
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_check_python_ok() -> None:
|
||||
r = check_python()
|
||||
assert r.status == "ok"
|
||||
assert "Python" in r.name
|
||||
|
||||
|
||||
def test_check_tesseract_ok_when_on_path(monkeypatch) -> None:
|
||||
monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/tesseract")
|
||||
r = check_tesseract()
|
||||
assert r.status == "ok"
|
||||
assert "/usr/bin/tesseract" in r.detail
|
||||
|
||||
|
||||
def test_check_tesseract_fails_when_missing(monkeypatch) -> None:
|
||||
monkeypatch.setattr("shutil.which", lambda _: None)
|
||||
r = check_tesseract()
|
||||
assert r.status == "fail"
|
||||
assert r.hint is not None
|
||||
|
||||
|
||||
def test_check_paddle_warns_when_missing(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"ocr_pipeline.doctor._has_module", lambda name: False if name == "paddleocr" else True
|
||||
)
|
||||
r = check_paddle()
|
||||
assert r.status == "warn"
|
||||
assert "paddle" in r.hint.lower() if r.hint else True
|
||||
|
||||
|
||||
def test_check_torch_reports_backend(monkeypatch) -> None:
|
||||
monkeypatch.setattr("ocr_pipeline.doctor._has_module", lambda name: True)
|
||||
fake_torch = type(
|
||||
"T", (), {"cuda": type("C", (), {"is_available": staticmethod(lambda: False)})()}
|
||||
)
|
||||
monkeypatch.setattr("sys.modules", {"torch": fake_torch})
|
||||
r = check_torch()
|
||||
assert r.status == "ok"
|
||||
assert "backend=" in r.detail
|
||||
|
||||
|
||||
def test_check_tables_warns_when_missing(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"ocr_pipeline.doctor._has_module",
|
||||
lambda name: False if name in ("torch", "transformers") else True,
|
||||
)
|
||||
r = check_tables()
|
||||
assert r.status == "warn"
|
||||
|
||||
|
||||
def test_check_spacy_warns_when_model_missing(monkeypatch, tmp_path) -> None:
|
||||
monkeypatch.setattr("ocr_pipeline.doctor._has_module", lambda name: True)
|
||||
|
||||
class FakeSpacy:
|
||||
@staticmethod
|
||||
def load(_model):
|
||||
raise OSError("model not found")
|
||||
|
||||
monkeypatch.setitem(__import__("sys").modules, "spacy", FakeSpacy)
|
||||
r = check_spacy()
|
||||
assert r.status == "warn"
|
||||
assert "en_core_sci_lg" in (r.hint or "")
|
||||
|
||||
|
||||
def test_check_config_warns_when_no_file(monkeypatch, tmp_path) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setattr("ocr_pipeline.config.user_config_dir", lambda: tmp_path / "missing")
|
||||
r = check_config()
|
||||
assert r.status == "warn"
|
||||
assert "init" in (r.hint or "")
|
||||
|
||||
|
||||
def test_check_db_writes_temp_file(tmp_path, monkeypatch) -> None:
|
||||
db = tmp_path / "sub" / "test.db"
|
||||
monkeypatch.setattr(settings.watch, "db_path", str(db))
|
||||
r = check_db()
|
||||
assert r.status == "ok"
|
||||
assert db.exists()
|
||||
|
||||
|
||||
def test_check_output_dir_creates_path(tmp_path, monkeypatch) -> None:
|
||||
out = tmp_path / "deep" / "output"
|
||||
monkeypatch.setattr(settings.output, "base_directory", str(out))
|
||||
r = check_output_dir()
|
||||
assert r.status == "ok"
|
||||
assert out.is_dir()
|
||||
|
||||
|
||||
def test_exit_code_for() -> None:
|
||||
from ocr_pipeline.doctor import CheckResult
|
||||
|
||||
assert exit_code_for([]) == 0
|
||||
assert exit_code_for([CheckResult("a", "ok", "x")]) == 0
|
||||
assert exit_code_for([CheckResult("a", "warn", "x")]) == 0
|
||||
assert exit_code_for([CheckResult("a", "fail", "x")]) == 1
|
||||
|
||||
|
||||
def test_run_checks_returns_all_categories() -> None:
|
||||
results = run_checks()
|
||||
names = {r.name for r in results}
|
||||
assert {"Tesseract", "PaddleOCR", "Python", "Config"}.issubset(names)
|
||||
|
||||
|
||||
def test_render_writes_table(capsys) -> None:
|
||||
from rich.console import Console
|
||||
|
||||
from ocr_pipeline.doctor import CheckResult
|
||||
|
||||
console = Console(file=__import__("sys").stdout)
|
||||
render(
|
||||
[CheckResult("A", "ok", "fine"), CheckResult("B", "fail", "bad", hint="fix me")], console
|
||||
)
|
||||
out = capsys.readouterr().out
|
||||
assert "A" in out and "B" in out
|
||||
|
||||
|
||||
def test_doctor_cli_runs(monkeypatch) -> None:
|
||||
from ocr_pipeline.doctor import CheckResult
|
||||
|
||||
monkeypatch.setattr("ocr_pipeline.cli.run_checks", lambda: [CheckResult("X", "ok", "ok")])
|
||||
result = runner.invoke(app, ["doctor"])
|
||||
assert result.exit_code == 0
|
||||
assert "X" in result.output
|
||||
|
||||
|
||||
def test_doctor_cli_exit_code_on_fail(monkeypatch) -> None:
|
||||
from ocr_pipeline.doctor import CheckResult
|
||||
|
||||
monkeypatch.setattr("ocr_pipeline.cli.run_checks", lambda: [CheckResult("X", "fail", "bad")])
|
||||
result = runner.invoke(app, ["doctor"])
|
||||
assert result.exit_code == 1
|
||||
Reference in New Issue
Block a user