test(extract): reach extra-missing through the import probe

Both tests reached `extractor_extra_missing` through a `.docx`/`.xlsx`
filename, which works only while `_UNPARSED_OPTIONAL_EXTENSIONS` is non-empty.
Those types are about to gain a converter, which empties the set and makes the
membership branch unreachable -- the tests would have gone red for the right
reason at the worst moment, mid-series.

Repointed both at the import probe, the mechanism the pdf gate already uses and
the one path that stays reachable however many types gain parsers.

Measured negative control: without the probe the same call raises
`extractor_pdf_error`, so the probe is load-bearing and the test can still fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-02 14:00:24 +02:00
commit 5f524f3902
2 changed files with 26 additions and 9 deletions

View file

@ -12,6 +12,7 @@ import hashlib
import importlib.util import importlib.util
import json import json
import sqlite3 import sqlite3
import sys
import urllib.error import urllib.error
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -344,12 +345,14 @@ def test_extractor_unknown() -> None:
assert code_of(excinfo) == "extractor_unknown" assert code_of(excinfo) == "extractor_unknown"
def test_extractor_extra_missing() -> None: def test_extractor_extra_missing(monkeypatch: pytest.MonkeyPatch) -> None:
# `docx` rather than `pdf`: the extra now ships a pdf parser, so the type # The import probe rather than a suffix: `docx` proved this code reachable
# that still has none is what proves this code is reachable. The pdf # only while the extra shipped no parser for it. Once it gains a converter,
# import-probe path is covered in tests/test_extract.py. # `_UNPARSED_OPTIONAL_EXTENSIONS` empties and the membership branch becomes
# unreachable — the probe is the path that survives.
monkeypatch.setitem(sys.modules, "pdfplumber", None)
with pytest.raises(ExtractionError) as excinfo: with pytest.raises(ExtractionError) as excinfo:
extract_text("doc.docx", b"binary") extract_text("report.pdf", b"%PDF-1.4")
assert code_of(excinfo) == "extractor_extra_missing" assert code_of(excinfo) == "extractor_extra_missing"

View file

@ -89,13 +89,27 @@ def test_missing_extension_fails_fast() -> None:
assert excinfo.value.code == "extractor_unknown" assert excinfo.value.code == "extractor_unknown"
# --- fail-fast: [extract]-gated binary types the extra ships no parser for --- # --- fail-fast: an [extract]-gated type without the extra installed ---
@pytest.mark.parametrize("name", ["doc.docx", "sheet.xlsx"]) def test_optional_type_without_the_extra_fails_fast(
def test_optional_type_without_a_parser_fails_fast(name: str) -> None: monkeypatch: pytest.MonkeyPatch,
) -> None:
"""`extractor_extra_missing` reached through the import probe, not a suffix set.
This test used to reach the code through a `.docx` or `.xlsx` filename,
which sat in `_UNPARSED_OPTIONAL_EXTENSIONS` because the extra shipped no
parser for those types. That set is on its way to empty: once those types gain a converter,
a membership test can no longer raise this code at all, and a test pinned
to it would go red for the right reason at the worst moment.
The import probe is the durable path it is how the gate actually works
(`extract.py`'s `_extract_pdf`), and it stays reachable no matter how many
types gain parsers.
"""
monkeypatch.setitem(sys.modules, "pdfplumber", None)
with pytest.raises(ExtractionError) as excinfo: with pytest.raises(ExtractionError) as excinfo:
extract_text(name, b"binary") extract_text("report.pdf", b"%PDF-1.4")
assert excinfo.value.code == "extractor_extra_missing" assert excinfo.value.code == "extractor_extra_missing"
# The error names the extra so the operator knows the remedy — never a # The error names the extra so the operator knows the remedy — never a
# silent skip, never a bundled parser in core. # silent skip, never a bundled parser in core.