461bf6fd40
CI / lint (3.11) (push) Blocked by required conditions
CI / lint (3.12) (push) Blocked by required conditions
CI / lint (3.13) (push) Blocked by required conditions
CI / shellcheck (push) Waiting to run
CI / shfmt (push) Waiting to run
CI / setup (3.11) (push) Waiting to run
CI / setup (3.12) (push) Waiting to run
CI / setup (3.13) (push) Waiting to run
CI / check-licenses (3.12) (push) Blocked by required conditions
CI / test_unit (3.11) (push) Blocked by required conditions
CI / test_unit (3.12) (push) Blocked by required conditions
CI / test_unit (3.13) (push) Blocked by required conditions
CI / test_unit_no_extras (3.11) (push) Blocked by required conditions
CI / test_unit_no_extras (3.12) (push) Blocked by required conditions
CI / test_json_to_html (3.12) (push) Blocked by required conditions
CI / test_unit_no_extras (3.13) (push) Blocked by required conditions
CI / test_unit_dependency_extras (csv, 3.11, --extra csv) (push) Blocked by required conditions
CI / test_unit_dependency_extras (csv, 3.12, --extra csv) (push) Blocked by required conditions
CI / test_unit_dependency_extras (csv, 3.13, --extra csv) (push) Blocked by required conditions
CI / test_unit_dependency_extras (docx, 3.11, --extra docx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (docx, 3.12, --extra docx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (docx, 3.13, --extra docx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (markdown, 3.11, --extra md) (push) Blocked by required conditions
CI / test_unit_dependency_extras (markdown, 3.12, --extra md) (push) Blocked by required conditions
CI / test_unit_dependency_extras (markdown, 3.13, --extra md) (push) Blocked by required conditions
CI / test_unit_dependency_extras (odt, 3.11, --extra odt) (push) Blocked by required conditions
CI / test_unit_dependency_extras (odt, 3.12, --extra odt) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pdf-image, 3.12, --extra pdf --extra image --extra paddleocr) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pdf-image, 3.13, --extra pdf --extra image --extra paddleocr) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pptx, 3.13, --extra pptx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (odt, 3.13, --extra odt) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pdf-image, 3.11, --extra pdf --extra image --extra paddleocr) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pptx, 3.11, --extra pptx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pptx, 3.12, --extra pptx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pypandoc, 3.11, --extra epub --extra org --extra rtf --extra rst) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pypandoc, 3.12, --extra epub --extra org --extra rtf --extra rst) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pypandoc, 3.13, --extra epub --extra org --extra rtf --extra rst) (push) Blocked by required conditions
CI / test_unit_dependency_extras (xlsx, 3.11, --extra xlsx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (xlsx, 3.12, --extra xlsx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (xlsx, 3.13, --extra xlsx) (push) Blocked by required conditions
CI / test_ingest_src (3.12) (push) Blocked by required conditions
CI / test_json_to_markdown (3.12) (push) Blocked by required conditions
CI / changelog (push) Waiting to run
CI / test_dockerfile (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Build And Push Docker Image / set-short-sha (push) Waiting to run
Build And Push Docker Image / build-images (linux/amd64, opensource-linux-8core) (push) Blocked by required conditions
Build And Push Docker Image / build-images (linux/arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Build And Push Docker Image / publish-images (push) Blocked by required conditions
Partition Benchmark / setup (push) Waiting to run
Partition Benchmark / Measure and compare partition() runtime (push) Blocked by required conditions
359 lines
11 KiB
Python
359 lines
11 KiB
Python
"""Tests for :mod:`unstructured.cli` and :mod:`unstructured.doctor`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
from unstructured.cli import _cmd_doctor, main
|
|
from unstructured.doctor import (
|
|
_libmagic_status,
|
|
_needs_pandoc_runtime,
|
|
_pip_hint_for_file_type,
|
|
_tool_status,
|
|
build_report,
|
|
environment_rows,
|
|
evaluate_file_type_capability,
|
|
evaluate_specifier,
|
|
file_path_to_capability,
|
|
format_table,
|
|
python_import_deps_ok,
|
|
resolve_specifier,
|
|
system_tool_rows,
|
|
)
|
|
from unstructured.file_utils.model import FileType
|
|
|
|
|
|
def test_resolve_specifier_pdf() -> None:
|
|
fts = resolve_specifier("pdf")
|
|
assert fts == [FileType.PDF]
|
|
|
|
|
|
def test_resolve_specifier_jpg_exact() -> None:
|
|
assert resolve_specifier("jpg") == [FileType.JPG]
|
|
|
|
|
|
def test_resolve_specifier_image_family() -> None:
|
|
fts = resolve_specifier("image")
|
|
assert len(fts) >= 1
|
|
assert all(ft.extra_name == "image" for ft in fts)
|
|
|
|
|
|
def test_resolve_specifier_audio_family() -> None:
|
|
fts = resolve_specifier("audio")
|
|
assert len(fts) >= 1
|
|
assert all(ft.extra_name == "audio" for ft in fts)
|
|
|
|
|
|
def test_resolve_specifier_email_partitioner_shortname() -> None:
|
|
assert FileType.EML in resolve_specifier("email")
|
|
|
|
|
|
def test_resolve_specifier_unknown() -> None:
|
|
with pytest.raises(ValueError, match="Unknown"):
|
|
resolve_specifier("not-a-real-type-xyz")
|
|
|
|
|
|
def test_resolve_specifier_empty() -> None:
|
|
with pytest.raises(ValueError, match="Empty"):
|
|
resolve_specifier(" ")
|
|
|
|
|
|
def test_evaluate_specifier_dedupes_image_family() -> None:
|
|
with mock.patch("unstructured.doctor.evaluate_file_type_capability") as ev:
|
|
ev.return_value = mock.Mock(ready=True, messages=())
|
|
r = evaluate_specifier("image")
|
|
assert r.ready is True
|
|
assert ev.call_count == 1
|
|
|
|
|
|
def test_evaluate_specifier_multiple_targets() -> None:
|
|
def _cap(ready: bool, *msgs: str) -> mock.Mock:
|
|
m = mock.Mock()
|
|
m.ready = ready
|
|
m.messages = tuple(msgs)
|
|
return m
|
|
|
|
with (
|
|
mock.patch(
|
|
"unstructured.doctor.resolve_specifier",
|
|
return_value=[FileType.PDF, FileType.CSV],
|
|
),
|
|
mock.patch(
|
|
"unstructured.doctor.evaluate_file_type_capability",
|
|
side_effect=[_cap(False, "a"), _cap(True)],
|
|
),
|
|
):
|
|
r = evaluate_specifier("x")
|
|
assert r.ready is False
|
|
assert "[PDF] a" in r.messages
|
|
|
|
|
|
def test_evaluate_specifier_dedupes_repeated_message_lines() -> None:
|
|
with (
|
|
mock.patch("unstructured.doctor.resolve_specifier", return_value=[FileType.PDF]),
|
|
mock.patch(
|
|
"unstructured.doctor.evaluate_file_type_capability",
|
|
return_value=mock.Mock(ready=False, messages=("dup", "dup")),
|
|
),
|
|
):
|
|
r = evaluate_specifier("pdf")
|
|
assert r.messages.count("[PDF] dup") == 1
|
|
|
|
|
|
def test_main_doctor_invocation() -> None:
|
|
with mock.patch("unstructured.cli._cmd_doctor", return_value=0) as m:
|
|
assert main(["doctor"]) == 0
|
|
m.assert_called_once_with([])
|
|
|
|
|
|
def test_main_no_argv_reads_sys_argv(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr("sys.argv", ["unstructured", "doctor", "--for", "txt"])
|
|
with mock.patch("unstructured.cli._cmd_doctor", return_value=0) as m:
|
|
assert main(None) == 0
|
|
m.assert_called_once_with(["--for", "txt"])
|
|
|
|
|
|
def test_main_unknown() -> None:
|
|
assert main(["nope"]) == 2
|
|
|
|
|
|
def test_main_help_returns_zero() -> None:
|
|
assert main(["--help"]) == 0
|
|
|
|
|
|
def test_cmd_doctor_for_missing_dep_exit_code() -> None:
|
|
with mock.patch(
|
|
"unstructured.doctor.evaluate_specifier",
|
|
return_value=mock.Mock(ready=False, messages=("missing",)),
|
|
):
|
|
assert _cmd_doctor(["--for", "pdf"]) == 1
|
|
|
|
|
|
def test_cmd_doctor_for_ok() -> None:
|
|
with mock.patch(
|
|
"unstructured.doctor.evaluate_specifier",
|
|
return_value=mock.Mock(ready=True, messages=()),
|
|
):
|
|
assert _cmd_doctor(["--for", "pdf"]) == 0
|
|
|
|
|
|
def test_cmd_doctor_for_prints_messages(capsys: pytest.CaptureFixture[str]) -> None:
|
|
with mock.patch(
|
|
"unstructured.doctor.evaluate_specifier",
|
|
return_value=mock.Mock(ready=True, messages=("note",)),
|
|
):
|
|
assert _cmd_doctor(["--for", "pdf"]) == 0
|
|
assert "note" in capsys.readouterr().out
|
|
|
|
|
|
def test_cmd_doctor_file_not_found(tmp_path: Path) -> None:
|
|
missing = tmp_path / "nope.pdf"
|
|
assert _cmd_doctor(["--file", str(missing)]) == 1
|
|
|
|
|
|
def test_cmd_doctor_file_txt_ok(tmp_path: Path) -> None:
|
|
pytest.importorskip("olefile")
|
|
f = tmp_path / "a.txt"
|
|
f.write_text("hello", encoding="utf-8")
|
|
assert _cmd_doctor(["--file", str(f)]) == 0
|
|
|
|
|
|
def test_cmd_doctor_file_prints_messages(
|
|
tmp_path: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
f = tmp_path / "b.txt"
|
|
f.write_text("x", encoding="utf-8")
|
|
with mock.patch(
|
|
"unstructured.doctor.file_path_to_capability",
|
|
return_value=mock.Mock(ready=True, messages=("hi",)),
|
|
):
|
|
assert _cmd_doctor(["--file", str(f)]) == 0
|
|
assert "hi" in capsys.readouterr().out
|
|
|
|
|
|
def test_cmd_doctor_for_and_file_mutually_exclusive_returns_2() -> None:
|
|
assert _cmd_doctor(["--for", "pdf", "--file", "any.pdf"]) == 2
|
|
|
|
|
|
def test_cmd_doctor_invalid_for_returns_2() -> None:
|
|
assert _cmd_doctor(["--for", "not-a-real-type-xyz"]) == 2
|
|
|
|
|
|
def test_cmd_doctor_default_prints_report(capsys: pytest.CaptureFixture[str]) -> None:
|
|
assert _cmd_doctor([]) == 0
|
|
out = capsys.readouterr().out
|
|
assert "Environment" in out
|
|
assert "Partitionable file types" in out
|
|
|
|
|
|
def test_evaluate_doc_requires_soffice_when_deps_ok() -> None:
|
|
with mock.patch("unstructured.doctor.python_import_deps_ok", return_value=(True, [])):
|
|
with mock.patch("unstructured.doctor._libreoffice_on_path", return_value=None):
|
|
r = evaluate_file_type_capability(FileType.DOC)
|
|
assert r.ready is False
|
|
assert "soffice" in "".join(r.messages).lower()
|
|
|
|
|
|
def test_evaluate_epub_requires_pandoc_binary_when_python_ok(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
with mock.patch("unstructured.doctor.python_import_deps_ok", return_value=(True, [])):
|
|
|
|
def _which(cmd: str) -> str | None:
|
|
if cmd == "pandoc":
|
|
return None
|
|
return "/fake/bin/" + cmd
|
|
|
|
monkeypatch.setattr("unstructured.doctor.shutil.which", _which)
|
|
r = evaluate_file_type_capability(FileType.EPUB)
|
|
assert r.ready is False
|
|
assert "Pandoc" in "".join(r.messages)
|
|
|
|
|
|
def test_evaluate_zip_not_partitionable() -> None:
|
|
r = evaluate_file_type_capability(FileType.ZIP)
|
|
assert r.ready is False
|
|
assert "not partitionable" in r.messages[0]
|
|
|
|
|
|
def test_evaluate_html_no_extra() -> None:
|
|
r = evaluate_file_type_capability(FileType.HTML)
|
|
assert r.ready is True
|
|
assert not r.messages
|
|
|
|
|
|
def test_evaluate_audio_not_ready_without_ffmpeg(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
import shutil
|
|
|
|
real_which = shutil.which
|
|
|
|
def _which(cmd: str) -> str | None:
|
|
if cmd == "ffmpeg":
|
|
return None
|
|
return real_which(cmd)
|
|
|
|
monkeypatch.setattr("unstructured.doctor.shutil.which", _which)
|
|
with mock.patch("unstructured.doctor._audio_extra_import_ok", return_value=True):
|
|
r = evaluate_file_type_capability(FileType.MP3)
|
|
assert r.ready is False
|
|
assert "ffmpeg" in " ".join(r.messages).lower()
|
|
|
|
|
|
def test_python_import_deps_ok_returns_missing() -> None:
|
|
ok, missing = python_import_deps_ok(FileType.PDF)
|
|
assert isinstance(ok, bool)
|
|
assert isinstance(missing, list)
|
|
|
|
|
|
def test_format_table_empty_rows() -> None:
|
|
out = format_table(("A", "B"), [])
|
|
assert "(no rows)" in out
|
|
|
|
|
|
def test_format_table_with_rows() -> None:
|
|
out = format_table(("Col",), [("val",)])
|
|
assert "Col" in out
|
|
assert "val" in out
|
|
|
|
|
|
def test_environment_rows() -> None:
|
|
rows = environment_rows()
|
|
labels = [r[0] for r in rows]
|
|
assert "Python" in labels
|
|
assert "unstructured" in labels
|
|
|
|
|
|
def test_system_tool_rows_shape() -> None:
|
|
rows = system_tool_rows()
|
|
assert len(rows) >= 5
|
|
assert all(len(r) == 3 for r in rows)
|
|
|
|
|
|
def test_tool_status_branches() -> None:
|
|
assert _tool_status("x", True)[0] == "ok"
|
|
assert _tool_status("x", False)[0] == "missing"
|
|
|
|
|
|
def test_pip_hint_for_file_type() -> None:
|
|
assert "pdf" in _pip_hint_for_file_type(FileType.PDF)
|
|
assert _pip_hint_for_file_type(FileType.HTML) == "pip install unstructured"
|
|
|
|
|
|
def test_needs_pandoc_runtime() -> None:
|
|
assert _needs_pandoc_runtime(FileType.EPUB) is True
|
|
assert _needs_pandoc_runtime(FileType.HTML) is False
|
|
|
|
|
|
def test_build_report_smoke() -> None:
|
|
report = build_report()
|
|
assert "Environment" in report
|
|
assert "System tools" in report
|
|
assert "Partitionable file types" in report
|
|
|
|
|
|
def test_file_path_to_capability_txt(tmp_path: Path) -> None:
|
|
pytest.importorskip("olefile")
|
|
f = tmp_path / "n.txt"
|
|
f.write_text("x", encoding="utf-8")
|
|
r = file_path_to_capability(f)
|
|
assert r.ready is True
|
|
|
|
|
|
def test_file_path_to_capability_zip(tmp_path: Path) -> None:
|
|
pytest.importorskip("olefile")
|
|
zp = tmp_path / "empty.zip"
|
|
with zipfile.ZipFile(zp, "w"):
|
|
pass
|
|
r = file_path_to_capability(zp)
|
|
assert r.ready is False
|
|
assert "not partitionable" in r.messages[0]
|
|
|
|
|
|
def test_file_path_to_capability_missing_path(tmp_path: Path) -> None:
|
|
missing = tmp_path / "does_not_exist.txt"
|
|
r = file_path_to_capability(missing)
|
|
assert r.ready is False
|
|
assert "Not a file" in r.messages[0]
|
|
|
|
|
|
def test_libmagic_status_ok_with_mocked_magic(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
fake = mock.MagicMock()
|
|
fake.from_buffer = mock.MagicMock(return_value="application/pdf")
|
|
monkeypatch.setitem(sys.modules, "magic", fake)
|
|
st, msg = _libmagic_status()
|
|
assert st == "ok"
|
|
assert "OK" in msg or "ok" in msg
|
|
|
|
|
|
def test_libmagic_status_warn_when_from_buffer_fails(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
fake = mock.MagicMock()
|
|
fake.from_buffer = mock.MagicMock(side_effect=RuntimeError("boom"))
|
|
monkeypatch.setitem(sys.modules, "magic", fake)
|
|
st, msg = _libmagic_status()
|
|
assert st == "warn"
|
|
assert "libmagic" in msg.lower()
|
|
|
|
|
|
def test_libmagic_status_import_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
import builtins
|
|
|
|
real_import = builtins.__import__
|
|
|
|
def _imp(name: str, *args, **kwargs):
|
|
if name == "magic":
|
|
raise ImportError("blocked for test")
|
|
return real_import(name, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(builtins, "__import__", _imp)
|
|
st, msg = _libmagic_status()
|
|
assert st == "warn"
|
|
assert "magic" in msg.lower() or "fallback" in msg.lower()
|