Coverage for tests/test_ppt_utils.py: 100%
132 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
1"""
2Tests for ppt_utils.py using mocks for external dependencies
3(python-pptx, LibreOffice via subprocess, and PyMuPDF/fitz).
4"""
6import logging
7import sys
8import pytest
10from pathlib import Path
11from unittest.mock import MagicMock, patch
13sys.path.append("wrapper")
15from ppt_utils import get_num_slides
16from ppt_utils import pptx_to_images
19# ---------------------------------------------------------------------------
20# get_num_slides
21# ---------------------------------------------------------------------------
23class TestGetNumSlides:
24 """Tests for ppt_utils.get_num_slides()."""
26 def _make_slide(self, show: str | None = None) -> MagicMock:
27 """Create a mock slide object."""
28 slide = MagicMock()
29 slide._element.get.return_value = show
30 return slide
32 def test_counts_visible_slides(self) -> None:
33 """Only visible slides are counted when count_hidden=False."""
34 slides = [
35 self._make_slide(show=None), # visible
36 self._make_slide(show=None), # visible
37 self._make_slide(show="0"), # hidden
38 ]
39 presentation = MagicMock()
40 presentation.slides = slides
42 with patch("ppt_utils.Presentation", return_value=presentation):
43 count = get_num_slides("dummy.pptx", count_hidden=False)
45 assert count == 2
47 def test_counts_all_slides_with_count_hidden_true(self) -> None:
48 """All slides (including hidden) are counted when count_hidden=True."""
49 slides = [
50 self._make_slide(show=None), # visible
51 self._make_slide(show="0"), # hidden
52 self._make_slide(show="0"), # hidden
53 ]
54 presentation = MagicMock()
55 presentation.slides = slides
57 with patch("ppt_utils.Presentation", return_value=presentation):
58 count = get_num_slides("dummy.pptx", count_hidden=True)
60 assert count == 3
62 def test_empty_presentation(self) -> None:
63 """Empty presentation returns 0."""
64 presentation = MagicMock()
65 presentation.slides = []
67 with patch("ppt_utils.Presentation", return_value=presentation):
68 count = get_num_slides("dummy.pptx")
70 assert count == 0
72 def test_all_hidden_returns_zero_by_default(self) -> None:
73 """All-hidden slides return 0 when count_hidden=False."""
74 slides = [
75 self._make_slide(show="0"),
76 self._make_slide(show="0"),
77 ]
78 presentation = MagicMock()
79 presentation.slides = slides
81 with patch("ppt_utils.Presentation", return_value=presentation):
82 count = get_num_slides("dummy.pptx")
84 assert count == 0
86 def test_passes_path_to_presentation(self) -> None:
87 """The PPTX path is forwarded to pptx.Presentation."""
88 presentation = MagicMock()
89 presentation.slides = []
91 with patch("ppt_utils.Presentation", return_value=presentation) as mock_pres:
92 get_num_slides("/some/path/slides.pptx")
94 mock_pres.assert_called_once_with("/some/path/slides.pptx")
97# ---------------------------------------------------------------------------
98# pptx_to_images
99# ---------------------------------------------------------------------------
101class TestPptxToImages:
102 """Tests for ppt_utils.pptx_to_images()."""
104 def _make_subprocess_result(
105 self,
106 returncode: int = 0,
107 stdout: str = "",
108 stderr: str = "",
109 ) -> MagicMock:
110 result = MagicMock()
111 result.returncode = returncode
112 result.stdout = stdout
113 result.stderr = stderr
114 return result
116 def _make_fitz_doc(self, num_pages: int = 2) -> MagicMock:
117 """Create a minimal mock fitz.Document."""
118 doc = MagicMock()
119 doc.__len__.return_value = num_pages
120 rect = MagicMock()
121 rect.width = 1280.0
122 rect.height = 800.0
123 doc.__getitem__.return_value = MagicMock(rect=rect)
125 pages = []
126 for _ in range(num_pages):
127 page = MagicMock()
128 pix = MagicMock()
129 page.get_pixmap.return_value = pix
130 pages.append(page)
132 doc.load_page = MagicMock(side_effect=pages)
133 return doc
135 def test_raises_on_libreoffice_failure(self, tmp_path: Path) -> None:
136 """RuntimeError is raised when LibreOffice returns non-zero exit code."""
137 pptx_path = str(tmp_path / "slides.pptx")
138 with patch("ppt_utils.subprocess.run",
139 return_value=self._make_subprocess_result(returncode=1)):
140 with pytest.raises(RuntimeError, match="Failed to generate images from PPTX"):
141 pptx_to_images(pptx_path, str(tmp_path))
143 def test_raises_when_pdf_not_found(self, tmp_path: Path) -> None:
144 """FileNotFoundError is raised when the expected PDF is not produced."""
145 pptx_path = str(tmp_path / "slides.pptx")
146 # LibreOffice succeeds but the PDF does not exist on disk.
147 with patch("ppt_utils.subprocess.run",
148 return_value=self._make_subprocess_result(returncode=0)):
149 with pytest.raises(FileNotFoundError, match="Expected PDF file not found"):
150 pptx_to_images(pptx_path, str(tmp_path))
152 def test_returns_image_paths_for_each_page(self, tmp_path: Path) -> None:
153 """Returns one image path per PDF page."""
154 num_pages = 3
155 pptx_path = str(tmp_path / "slides.pptx")
157 # Create a dummy PDF file so os.path.exists passes.
158 (tmp_path / "slides.pdf").touch()
160 fitz_doc = self._make_fitz_doc(num_pages=num_pages)
162 with patch("ppt_utils.subprocess.run",
163 return_value=self._make_subprocess_result(returncode=0)), \
164 patch("ppt_utils.fitz.open", return_value=fitz_doc), \
165 patch("ppt_utils.fitz.Matrix", return_value=MagicMock()):
166 image_paths = pptx_to_images(pptx_path, str(tmp_path))
168 assert len(image_paths) == num_pages
169 for i, path in enumerate(image_paths, start=1):
170 assert path.endswith(f"slide_{i:03d}.png")
172 def test_libreoffice_command_uses_correct_args(self, tmp_path: Path) -> None:
173 """The LibreOffice command includes --headless, --convert-to pdf, and the file path."""
174 pptx_path = str(tmp_path / "slides.pptx")
176 with patch("ppt_utils.subprocess.run",
177 return_value=self._make_subprocess_result(returncode=0)) as mock_run:
178 # Will raise FileNotFoundError (PDF not created) – that's fine here.
179 try:
180 pptx_to_images(pptx_path, str(tmp_path))
181 except FileNotFoundError:
182 pass
184 call_args = mock_run.call_args
185 cmd = call_args[0][0]
186 assert "libreoffice" in cmd
187 assert "--headless" in cmd
188 assert "--convert-to" in cmd
189 assert "pdf" in cmd
190 assert pptx_path in cmd
192 def test_logger_receives_stdout_and_stderr(self, tmp_path: Path) -> None:
193 """When a logger is passed, LibreOffice stdout and stderr are forwarded."""
194 pptx_path = str(tmp_path / "slides.pptx")
195 mock_logger = MagicMock(spec=logging.Logger)
197 with patch("ppt_utils.subprocess.run",
198 return_value=self._make_subprocess_result(
199 returncode=1, stdout="out text", stderr="err text")):
200 try:
201 pptx_to_images(pptx_path, str(tmp_path), logger=mock_logger)
202 except RuntimeError:
203 pass
205 mock_logger.debug.assert_called()
206 mock_logger.warning.assert_called()
208 def test_custom_width_and_height_applied(self, tmp_path: Path) -> None:
209 """Custom width/height are used to build the fitz.Matrix scale factors."""
210 num_pages = 1
211 pptx_path = str(tmp_path / "deck.pptx")
212 (tmp_path / "deck.pdf").touch()
214 fitz_doc = self._make_fitz_doc(num_pages=num_pages)
216 with patch("ppt_utils.subprocess.run",
217 return_value=self._make_subprocess_result(returncode=0)), \
218 patch("ppt_utils.fitz.open", return_value=fitz_doc), \
219 patch("ppt_utils.fitz.Matrix") as mock_matrix:
220 pptx_to_images(pptx_path, str(tmp_path), width=640, height=400)
222 # Matrix should have been called with the scale factors derived from
223 # the requested dimensions divided by the doc page dimensions.
224 mock_matrix.assert_called_once()
225 args = mock_matrix.call_args[0]
226 assert len(args) == 2 # (matrix_width, matrix_height)
227 assert args[0] == pytest.approx(640 / 1280.0)
228 assert args[1] == pytest.approx(400 / 800.0)
230 def test_no_width_height_uses_dpi(self, tmp_path: Path) -> None:
231 """When width=None and height=None, a DPI-based fitz.Matrix is used."""
232 num_pages = 1
233 pptx_path = str(tmp_path / "deck.pptx")
234 (tmp_path / "deck.pdf").touch()
236 fitz_doc = self._make_fitz_doc(num_pages=num_pages)
238 with patch("ppt_utils.subprocess.run",
239 return_value=self._make_subprocess_result(returncode=0)), \
240 patch("ppt_utils.fitz.open", return_value=fitz_doc), \
241 patch("ppt_utils.fitz.Matrix") as mock_matrix:
242 pptx_to_images(pptx_path, str(tmp_path), width=None, height=None, dpi=144)
244 # DPI/72 = 144/72 = 2.0 scale factor
245 mock_matrix.assert_called_once_with(2.0, 2.0)