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

1""" 

2Tests for ppt_utils.py using mocks for external dependencies 

3(python-pptx, LibreOffice via subprocess, and PyMuPDF/fitz). 

4""" 

5 

6import logging 

7import sys 

8import pytest 

9 

10from pathlib import Path 

11from unittest.mock import MagicMock, patch 

12 

13sys.path.append("wrapper") 

14 

15from ppt_utils import get_num_slides 

16from ppt_utils import pptx_to_images 

17 

18 

19# --------------------------------------------------------------------------- 

20# get_num_slides 

21# --------------------------------------------------------------------------- 

22 

23class TestGetNumSlides: 

24 """Tests for ppt_utils.get_num_slides().""" 

25 

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 

31 

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 

41 

42 with patch("ppt_utils.Presentation", return_value=presentation): 

43 count = get_num_slides("dummy.pptx", count_hidden=False) 

44 

45 assert count == 2 

46 

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 

56 

57 with patch("ppt_utils.Presentation", return_value=presentation): 

58 count = get_num_slides("dummy.pptx", count_hidden=True) 

59 

60 assert count == 3 

61 

62 def test_empty_presentation(self) -> None: 

63 """Empty presentation returns 0.""" 

64 presentation = MagicMock() 

65 presentation.slides = [] 

66 

67 with patch("ppt_utils.Presentation", return_value=presentation): 

68 count = get_num_slides("dummy.pptx") 

69 

70 assert count == 0 

71 

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 

80 

81 with patch("ppt_utils.Presentation", return_value=presentation): 

82 count = get_num_slides("dummy.pptx") 

83 

84 assert count == 0 

85 

86 def test_passes_path_to_presentation(self) -> None: 

87 """The PPTX path is forwarded to pptx.Presentation.""" 

88 presentation = MagicMock() 

89 presentation.slides = [] 

90 

91 with patch("ppt_utils.Presentation", return_value=presentation) as mock_pres: 

92 get_num_slides("/some/path/slides.pptx") 

93 

94 mock_pres.assert_called_once_with("/some/path/slides.pptx") 

95 

96 

97# --------------------------------------------------------------------------- 

98# pptx_to_images 

99# --------------------------------------------------------------------------- 

100 

101class TestPptxToImages: 

102 """Tests for ppt_utils.pptx_to_images().""" 

103 

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 

115 

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) 

124 

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) 

131 

132 doc.load_page = MagicMock(side_effect=pages) 

133 return doc 

134 

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)) 

142 

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)) 

151 

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") 

156 

157 # Create a dummy PDF file so os.path.exists passes. 

158 (tmp_path / "slides.pdf").touch() 

159 

160 fitz_doc = self._make_fitz_doc(num_pages=num_pages) 

161 

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)) 

167 

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") 

171 

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") 

175 

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 

183 

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 

191 

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) 

196 

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 

204 

205 mock_logger.debug.assert_called() 

206 mock_logger.warning.assert_called() 

207 

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() 

213 

214 fitz_doc = self._make_fitz_doc(num_pages=num_pages) 

215 

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) 

221 

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) 

229 

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() 

235 

236 fitz_doc = self._make_fitz_doc(num_pages=num_pages) 

237 

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) 

243 

244 # DPI/72 = 144/72 = 2.0 scale factor 

245 mock_matrix.assert_called_once_with(2.0, 2.0)