Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

245 lines
8.6 KiB
Python

"""Regression tests for office-document upload support (issue #4414).
ODT/DOC/DOCX/PPT/PPTX/XLS/XLSX were advertised as supported but failed at
runtime because their ``unstructured`` parser dependencies (python-docx,
python-pptx, openpyxl) were never installed. These tests assert that:
1. With the parser dependencies installed, the office extensions are
registered and text actually round-trips through the upload extraction
path (``extract_text_from_bytes``).
2. The registry is *honest*: a format is advertised only when its real
runtime dependency is importable, so a missing dependency hides the
format instead of accepting an upload that later fails silently.
"""
import importlib
import shutil
import tempfile
from pathlib import Path
import pytest
REGISTRY_MODULE = "local_deep_research.document_loaders.loader_registry"
def _reload(module_name):
return importlib.reload(importlib.import_module(module_name))
class TestOfficeExtensionsRegistered:
"""Office formats are advertised when their parser deps are present."""
def test_modern_office_formats_registered(self):
# Modern office formats work with pip-only parser deps (no system
# binaries), so they are registered whenever those deps are present.
pytest.importorskip("docx")
pytest.importorskip("pptx")
pytest.importorskip("openpyxl")
pytest.importorskip("msoffcrypto")
from local_deep_research.document_loaders.loader_registry import (
LOADER_REGISTRY,
)
for ext in (".docx", ".pptx", ".xlsx"):
assert ext in LOADER_REGISTRY, f"{ext} should be registered"
def test_legacy_binary_formats_gated_on_libreoffice(self):
# .doc/.ppt are OLE binary formats that unstructured converts via
# LibreOffice; they must be advertised only when soffice is present.
import local_deep_research.document_loaders.loader_registry as mod
for ext in (".doc", ".ppt"):
assert (ext in mod.LOADER_REGISTRY) == (
mod.HAS_LIBREOFFICE
and (mod.HAS_DOCX_DEP if ext == ".doc" else mod.HAS_PPTX_DEP)
)
def test_odt_registered_when_docx_and_pandoc_present(self):
pytest.importorskip("docx")
pytest.importorskip("pypandoc")
from local_deep_research.document_loaders.loader_registry import (
LOADER_REGISTRY,
)
assert ".odt" in LOADER_REGISTRY
class TestOfficeExtractionRoundTrip:
"""Real files extract text through the public upload path."""
def test_docx_extraction(self):
pytest.importorskip("docx")
from docx import Document
from local_deep_research.document_loaders import (
extract_text_from_bytes,
)
marker = "Hello DOCX world, extraction works."
with tempfile.NamedTemporaryFile(suffix=".docx") as tmp:
doc = Document()
doc.add_paragraph(marker)
doc.save(tmp.name)
content = Path(tmp.name).read_bytes()
text = extract_text_from_bytes(content, ".docx", "sample.docx")
assert text is not None
assert marker in text
def test_xls_extraction(self):
# xlwt (a dev-only writer) builds the legacy fixture; the loader reads
# it with pandas + xlrd, bypassing the fragile unstructured path.
xlwt = pytest.importorskip("xlwt")
pytest.importorskip("xlrd")
from local_deep_research.document_loaders import (
extract_text_from_bytes,
)
wb = xlwt.Workbook()
ws = wb.add_sheet("Data")
for r, (a, b) in enumerate(
[("Animal", "Count"), ("Otter", "3"), ("Badger", "7")]
):
ws.write(r, 0, a)
ws.write(r, 1, b)
with tempfile.NamedTemporaryFile(suffix=".xls", delete=False) as tmp:
xls_path = tmp.name
try:
wb.save(xls_path)
content = Path(xls_path).read_bytes()
finally:
Path(xls_path).unlink(missing_ok=True)
text = extract_text_from_bytes(content, ".xls", "sample.xls")
assert text is not None
assert "Animal" in text and "Otter" in text and "Badger" in text
def test_odt_extraction(self):
pypandoc = pytest.importorskip("pypandoc")
pytest.importorskip("docx")
if not shutil.which("pandoc"):
try:
pypandoc.get_pandoc_version()
except OSError:
pytest.skip("pandoc binary not available")
from local_deep_research.document_loaders import (
extract_text_from_bytes,
)
marker = "Hello ODT world, extraction works."
with tempfile.NamedTemporaryFile(suffix=".odt", delete=False) as tmp:
odt_path = tmp.name
try:
# Generate a genuine ODT (with styles.xml etc.) via pandoc.
pypandoc.convert_text(
marker, "odt", format="md", outputfile=odt_path
)
content = Path(odt_path).read_bytes()
finally:
Path(odt_path).unlink(missing_ok=True)
text = extract_text_from_bytes(content, ".odt", "sample.odt")
assert text is not None
assert marker in text
class TestXLSLoaderEncryption:
"""Encrypted XLS files produce a clear error, not a generic traceback."""
def test_encrypted_xls_raises_value_error(self, monkeypatch):
pytest.importorskip("xlrd")
from local_deep_research.document_loaders.xls_loader import XLSLoader
def fake_read_excel(*args, **kwargs):
raise Exception("Workbook is encrypted")
monkeypatch.setattr("pandas.read_excel", fake_read_excel)
loader = XLSLoader("/tmp/fake.xls")
with pytest.raises(ValueError, match="encrypted"):
loader.load()
def test_corrupted_xls_raises_original_error(self, monkeypatch):
pytest.importorskip("xlrd")
from local_deep_research.document_loaders.xls_loader import XLSLoader
def fake_read_excel(*args, **kwargs):
raise Exception("Unsupported format")
monkeypatch.setattr("pandas.read_excel", fake_read_excel)
loader = XLSLoader("/tmp/fake.xls")
with pytest.raises(Exception, match="Unsupported format"):
loader.load()
class TestHonestDetection:
"""A missing runtime dependency hides the format from the registry.
The registry computes its capability flags at import time, so we reload
the module with ``importlib.util.find_spec`` patched to report specific
modules as absent. Patching ``_module_available`` directly would not
survive the reload (it redefines that function from source), so we patch
the lower-level ``find_spec`` it calls.
"""
@pytest.fixture
def restore_module(self):
# Restore the pre-test module dict instead of reloading: pytest
# finalizes this fixture before monkeypatch (LIFO, it is listed
# after monkeypatch in the test signatures), so a teardown reload
# would re-execute the registry with find_spec still patched and
# leak the blocked-dependency state to later tests.
module = importlib.import_module(REGISTRY_MODULE)
snapshot = dict(module.__dict__)
yield
module.__dict__.clear()
module.__dict__.update(snapshot)
@staticmethod
def _reload_without(monkeypatch, blocked):
import importlib.util as importlib_util
real_find_spec = importlib_util.find_spec
def fake_find_spec(name, *args, **kwargs):
if name in blocked:
return None
return real_find_spec(name, *args, **kwargs)
monkeypatch.setattr(importlib_util, "find_spec", fake_find_spec)
return _reload(REGISTRY_MODULE)
def test_missing_python_docx_hides_word_and_odt(
self, monkeypatch, restore_module
):
reloaded = self._reload_without(monkeypatch, {"docx"})
assert reloaded.HAS_DOCX_DEP is False
for ext in (".doc", ".docx", ".odt"):
assert ext not in reloaded.LOADER_REGISTRY
def test_missing_pptx_hides_powerpoint(self, monkeypatch, restore_module):
reloaded = self._reload_without(monkeypatch, {"pptx"})
assert reloaded.HAS_PPTX_DEP is False
for ext in (".ppt", ".pptx"):
assert ext not in reloaded.LOADER_REGISTRY
def test_missing_ocr_dep_hides_image_formats(
self, monkeypatch, restore_module
):
reloaded = self._reload_without(
monkeypatch, {"pytesseract", "unstructured.pytesseract"}
)
assert reloaded.HAS_OCR_DEP is False
for ext in (".png", ".jpg", ".jpeg", ".tiff", ".tif", ".bmp", ".heic"):
assert ext not in reloaded.LOADER_REGISTRY