Skip to content

Commit d9cae89

Browse files
authored
Replace local parser mocks with real conversion fixtures (#2586)
1 parent 2b61a33 commit d9cae89

13 files changed

Lines changed: 287 additions & 326 deletions

File tree

‎.github/workflows/tests.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ jobs:
2929
with:
3030
python-version: ${{ matrix.python-version }}
3131

32+
- name: Install media tools
33+
if: runner.os == 'Linux'
34+
run: |
35+
sudo apt-get update
36+
sudo apt-get install -y --no-install-recommends libimage-exiftool-perl ffmpeg
37+
exiftool -ver
38+
ffmpeg -version
39+
ffprobe -version
40+
3241
- name: Install Hatch
3342
run: pipx install hatch
3443
- name: Run tests

‎packages/markitdown-ocr/tests/test_pdf_converter.py‎

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import sys
1515
from pathlib import Path
1616
from typing import Any
17-
from unittest.mock import MagicMock, patch
1817

1918
import pytest
2019

@@ -207,28 +206,30 @@ def test_pdf_scanned_report(svc: MockOCRService) -> None:
207206
# ---------------------------------------------------------------------------
208207

209208

210-
def test_pdf_scanned_fallback_format(svc: MockOCRService) -> None:
211-
"""_ocr_full_pages emits *[Image OCR]...[End OCR]* for each page."""
209+
def test_pdf_scanned_fallback_format() -> None:
210+
"""Render a real PDF, replacing only the remote OCR service."""
211+
from PIL import Image
212+
212213
path = TEST_DATA_DIR / "pdf_image_start.pdf"
213-
if not path.exists():
214-
pytest.skip(f"Test file not found: {path}")
214+
seen = []
215+
216+
class RecordingOCRService:
217+
def extract_text(self, image_stream: Any) -> OCRResult:
218+
assert image_stream.tell() == 0
219+
with Image.open(image_stream) as image:
220+
assert image.format == "PNG"
221+
assert image.width > 0 and image.height > 0
222+
image.load()
223+
seen.append(image.size)
224+
return OCRResult(text=_MOCK_TEXT, backend_used="mock")
215225

216226
converter = PdfConverterWithOCR()
217-
with patch("pdfplumber.open") as mock_plumber:
218-
mock_pdf = MagicMock()
219-
mock_page = MagicMock()
220-
mock_page.page_number = 1
221-
mock_pdf.pages = [mock_page]
222-
mock_pdf.__enter__.return_value = mock_pdf
223-
mock_plumber.return_value = mock_pdf
224-
225-
with open(path, "rb") as f:
226-
md = converter._ocr_full_pages(io.BytesIO(f.read()), svc)
227-
228-
expected = "## Page 1\n\n\n" "*[Image OCR]\nMOCK_OCR_TEXT_12345\n[End OCR]*"
229-
assert (
230-
md == expected
231-
), f"_ocr_full_pages must produce:\n{expected!r}\nActual:\n{md!r}"
227+
markdown = converter._ocr_full_pages(
228+
io.BytesIO(path.read_bytes()), RecordingOCRService()
229+
)
230+
231+
assert len(seen) == 1
232+
assert markdown == "## Page 1\n\n\n" + _OCR_BLOCK
232233

233234

234235
# ---------------------------------------------------------------------------

‎packages/markitdown-ocr/tests/test_pptx_converter.py‎

Lines changed: 44 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@
1212
Slide text, notes, and layout come from the core PPTX converter.
1313
"""
1414

15+
import io
1516
import sys
1617
from pathlib import Path
1718
from typing import Any
1819

1920
import pytest
21+
from pptx import Presentation
22+
from pptx.chart.data import CategoryChartData
23+
from pptx.enum.chart import XL_CHART_TYPE
24+
from pptx.util import Inches
2025

2126
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
2227

@@ -138,54 +143,45 @@ def test_pptx_no_ocr_service_no_tags() -> None:
138143
assert "[End OCR]*" not in md
139144

140145

141-
def test_pptx_ocr_chart_no_title_text_frame() -> None:
142-
from unittest.mock import MagicMock
143-
from markitdown_ocr._pptx_converter_with_ocr import PptxConverterWithOCR
144-
145-
# python-pptx's ChartTitle.text_frame is destructive -- it creates a text
146-
# frame if one isn't already present, so it never returns None.
147-
# has_text_frame is the property that actually reflects presence/absence.
148-
mock_chart = MagicMock()
149-
mock_chart.has_title = True
150-
mock_chart.chart_title.has_text_frame = False
151-
152-
mock_category = MagicMock()
153-
mock_category.label = "Cat 1"
154-
mock_chart.plots = [MagicMock(categories=[mock_category])]
155-
156-
mock_series = MagicMock()
157-
mock_series.name = "Series 1"
158-
mock_series.values = [10.0]
159-
mock_chart.series = [mock_series]
160-
146+
def _chart_presentation(title: str | None) -> io.BytesIO:
147+
# Modify an in-memory copy of an existing deck; keep its fixture intact.
148+
presentation = Presentation(TEST_DATA_DIR / "pptx_image_middle.pptx")
149+
slide = presentation.slides.add_slide(presentation.slide_layouts[6])
150+
data = CategoryChartData()
151+
data.categories = ["Cat 1"]
152+
data.add_series("Series 1", [10.0])
153+
chart = slide.shapes.add_chart(
154+
XL_CHART_TYPE.COLUMN_CLUSTERED,
155+
Inches(1),
156+
Inches(1),
157+
Inches(5),
158+
Inches(3),
159+
data,
160+
).chart
161+
chart.has_title = True
162+
if title is not None:
163+
chart.chart_title.text_frame.text = title
164+
assert chart.chart_title.has_text_frame is (title is not None)
165+
stream = io.BytesIO()
166+
presentation.save(stream)
167+
stream.seek(0)
168+
return stream
169+
170+
171+
@pytest.mark.parametrize("title", [None, "Revenue"])
172+
def test_pptx_ocr_chart_title_text_frame(title: str | None) -> None:
173+
stream = _chart_presentation(title)
174+
# Reload the serialized package so the real parser reads title XML.
175+
chart = Presentation(stream).slides[-1].shapes[-1].chart
161176
converter = PptxConverterWithOCR()
162-
result = converter._convert_chart_to_markdown(mock_chart)
163-
164-
assert "### Chart" in result
177+
result = converter._convert_chart_to_markdown(chart)
178+
heading = "### Chart" if title is None else "### Chart: Revenue"
179+
assert result.strip().splitlines()[0] == heading
165180
assert "Cat 1" in result
166181
assert "Series 1" in result
167-
assert ":" not in result
168-
169-
170-
def test_pptx_ocr_chart_with_title_text_frame() -> None:
171-
from unittest.mock import MagicMock
172-
from markitdown_ocr._pptx_converter_with_ocr import PptxConverterWithOCR
173-
174-
mock_chart = MagicMock()
175-
mock_chart.has_title = True
176-
mock_chart.chart_title.has_text_frame = True
177-
mock_chart.chart_title.text_frame.text = "Revenue"
178-
179-
mock_category = MagicMock()
180-
mock_category.label = "Cat 1"
181-
mock_chart.plots = [MagicMock(categories=[mock_category])]
182-
183-
mock_series = MagicMock()
184-
mock_series.name = "Series 1"
185-
mock_series.values = [10.0]
186-
mock_chart.series = [mock_series]
187-
188-
converter = PptxConverterWithOCR()
189-
result = converter._convert_chart_to_markdown(mock_chart)
190-
191-
assert "### Chart: Revenue" in result
182+
assert "10.0" in result
183+
# Accessing a title without a text frame must not create one.
184+
assert chart.chart_title.has_text_frame is (title is not None)
185+
stream.seek(0)
186+
markdown = converter.convert(stream, StreamInfo(extension=".pptx")).markdown
187+
assert result.strip() in markdown

‎packages/markitdown/tests/README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,10 @@ The existing remote, credential, platform, and optional-dependency skip conditio
5252
apply. A consolidation should preserve test bodies, parametrization, fixtures,
5353
and skip conditions; compare collected cases as well as test results to catch
5454
accidentally dropped or overwritten tests.
55+
56+
The `test_files/pdf_cleanup_{plain,form,mixed}.pdf` fixtures contain copied pages
57+
from `test.pdf` and `REPAIR-2022-INV-001_multipage.pdf`. They exercise real PDF
58+
parsing, fallback, and cleanup without changing either source fixture. Rebuild
59+
them with `python tests/test_files/generate_pdf_cleanup_fixtures.py` in an
60+
environment with PyMuPDF installed; PyMuPDF is needed only to rebuild the files,
61+
not to run the core tests.

‎packages/markitdown/tests/_test_vectors.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,45 @@ class FileTestVector(object):
9797
],
9898
must_not_include=[],
9999
),
100+
FileTestVector(
101+
filename="pdf_cleanup_plain.pdf",
102+
mimetype="application/pdf",
103+
charset=None,
104+
url=None,
105+
must_include=[
106+
"While there is contemporaneous exploration of multi-agent approaches",
107+
],
108+
must_not_include=["ZAVA AUTO REPAIR", "| Customer Information |"],
109+
),
110+
FileTestVector(
111+
filename="pdf_cleanup_form.pdf",
112+
mimetype="application/pdf",
113+
charset=None,
114+
url=None,
115+
must_include=[
116+
"ZAVA AUTO REPAIR",
117+
"| Customer Information |",
118+
"Gabriel Diaz",
119+
"| GRAND TOTAL",
120+
"5,738",
121+
],
122+
must_not_include=["While there is contemporaneous exploration"],
123+
),
124+
FileTestVector(
125+
filename="pdf_cleanup_mixed.pdf",
126+
mimetype="application/pdf",
127+
charset=None,
128+
url=None,
129+
must_include=[
130+
"ZAVA AUTO REPAIR",
131+
"| Customer Information |",
132+
"Gabriel Diaz",
133+
"| GRAND TOTAL",
134+
"5,738",
135+
"While there is contemporaneous exploration of multi-agent approaches",
136+
],
137+
must_not_include=[],
138+
),
100139
FileTestVector(
101140
filename="test_blog.html",
102141
mimetype="text/html",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Rebuild copied PDF fixtures with PyMuPDF (already used by the OCR package).
2+
3+
Run from packages/markitdown with: python tests/test_files/generate_pdf_cleanup_fixtures.py
4+
The existing source PDFs are opened read-only; only pdf_cleanup_*.pdf is written.
5+
"""
6+
7+
from pathlib import Path
8+
9+
import pymupdf
10+
11+
12+
FIXTURES = Path(__file__).resolve().parent
13+
14+
15+
def main() -> None:
16+
with pymupdf.open(FIXTURES / "test.pdf") as plain, pymupdf.open(
17+
FIXTURES / "REPAIR-2022-INV-001_multipage.pdf"
18+
) as form:
19+
for name, pages in (
20+
("plain", [plain, plain, plain]),
21+
("form", [form, form, form]),
22+
("mixed", [form, plain, form]),
23+
):
24+
with pymupdf.open() as copied:
25+
for source in pages:
26+
copied.insert_pdf(source, from_page=0, to_page=0)
27+
copied.save(FIXTURES / f"pdf_cleanup_{name}.pdf", deflate=True)
28+
29+
30+
if __name__ == "__main__":
31+
main()
489 KB
Binary file not shown.
415 KB
Binary file not shown.
267 KB
Binary file not shown.

‎packages/markitdown/tests/test_image.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,11 @@ def test_image_without_llm_is_unchanged(
173173
skip_exiftool,
174174
reason="do not run if exiftool is not installed",
175175
)
176-
def test_markitdown_exiftool() -> None:
176+
def test_markitdown_exiftool(monkeypatch: pytest.MonkeyPatch) -> None:
177+
import speech_recognition as sr
178+
179+
# Decode audio locally, but avoid the remote speech service in metadata tests.
180+
monkeypatch.setattr(sr.Recognizer, "recognize_google", lambda *args, **kwargs: "")
177181
which_exiftool = shutil.which("exiftool")
178182
assert which_exiftool is not None
179183

@@ -185,7 +189,7 @@ def test_markitdown_exiftool() -> None:
185189
assert target in result.text_content
186190

187191
# Test setting the exiftool path through an environment variable
188-
os.environ["EXIFTOOL_PATH"] = which_exiftool
192+
monkeypatch.setenv("EXIFTOOL_PATH", which_exiftool)
189193
markitdown = MarkItDown()
190194
result = markitdown.convert(os.path.join(TEST_FILES_DIR, "test.jpg"))
191195
for key in JPG_TEST_EXIFTOOL:

0 commit comments

Comments
 (0)