Merge lp:~raoul-snyman/openlp/pyro-impress into lp:openlp
- pyro-impress
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 2878 |
Proposed branch: | lp:~raoul-snyman/openlp/pyro-impress |
Merge into: | lp:openlp |
Diff against target: |
2439 lines (+2223/-56) 14 files modified
.bzrignore (+29/-38) openlp/core/common/path.py (+2/-0) openlp/core/ui/media/mediacontroller.py (+2/-0) openlp/plugins/presentations/lib/libreofficeserver.py (+431/-0) openlp/plugins/presentations/lib/maclocontroller.py (+266/-0) openlp/plugins/presentations/lib/presentationcontroller.py (+3/-1) openlp/plugins/presentations/lib/presentationtab.py (+2/-2) openlp/plugins/presentations/lib/serializers.py (+52/-0) openlp/plugins/presentations/lib/vendor/do_not_delete.txt (+5/-0) openlp/plugins/presentations/presentationplugin.py (+16/-14) scripts/check_dependencies.py (+2/-0) tests/functional/openlp_core/common/test_path.py (+12/-1) tests/functional/openlp_plugins/presentations/test_libreofficeserver.py (+948/-0) tests/functional/openlp_plugins/presentations/test_maclocontroller.py (+453/-0) |
To merge this branch: | bzr merge lp:~raoul-snyman/openlp/pyro-impress |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tim Bentley | Approve | ||
Tomas Groth | Pending | ||
Review via email: mp+368362@code.launchpad.net |
This proposal supersedes a proposal from 2019-05-22.
Commit message
Add presentations through LibreOffice on macOS.
Description of the change
Add presentations through LibreOffice on macOS. Comments and criticisms welcome.
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal | # |
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Linux tests failed, please see https:/
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal | # |
As commented all the license years are 2016 but that can be fixed with the gpl3 move!
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
This is still a WIP branch. There are some changes that need to be made after merging with trunk.
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Linux tests failed, please see https:/
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Linux tests failed, please see https:/
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Linux tests passed!
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Linting passed!
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
macOS tests passed!
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal | # |
Headers needs update - see inline.
I tried to test but the MacBook at hand was simply to slow to be of value....
Raoul Snyman (raoul-snyman) wrote : | # |
Linux tests passed!
Raoul Snyman (raoul-snyman) wrote : | # |
Linting passed!
Raoul Snyman (raoul-snyman) wrote : | # |
macOS tests passed!
Tim Bentley (trb143) wrote : | # |
Looks fine but no Mac so no idea if it runs!
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2019-03-25 21:24:51 +0000 |
3 | +++ .bzrignore 2019-06-05 05:00:52 +0000 |
4 | @@ -1,57 +1,48 @@ |
5 | *.*~ |
6 | -*.~\?~ |
7 | -\#*\# |
8 | -build |
9 | -.cache |
10 | -cover |
11 | -.coverage |
12 | -coverage |
13 | -.directory |
14 | -.vscode |
15 | -dist |
16 | *.dll |
17 | -documentation/build/doctrees |
18 | -documentation/build/html |
19 | *.e4* |
20 | -*eric[1-9]project |
21 | -.git |
22 | -env |
23 | -# Git files |
24 | -.gitignore |
25 | -htmlcov |
26 | -.idea |
27 | *.kate-swp |
28 | *.kdev4 |
29 | -.kdev4 |
30 | *.komodoproject |
31 | -.komodotools |
32 | -list |
33 | *.log* |
34 | *.nja |
35 | -openlp.cfg |
36 | -openlp/core/resources.py.old |
37 | -OpenLP.egg-info |
38 | -openlp.org 2.0.e4* |
39 | -openlp.pro |
40 | -openlp-test-projectordb.sqlite |
41 | *.orig |
42 | -output |
43 | *.pyc |
44 | -__pycache__ |
45 | -.pylint.d |
46 | -.pytest_cache |
47 | *.qm |
48 | *.rej |
49 | -# Rejected diff's |
50 | -resources/innosetup/Output |
51 | -resources/windows/warnOpenLP.txt |
52 | *.ropeproject |
53 | -tags |
54 | -output |
55 | +*.~\?~ |
56 | +*eric[1-9]project |
57 | +.cache |
58 | +.coverage |
59 | +.directory |
60 | +.git |
61 | +.gitignore |
62 | +.idea |
63 | +.kdev4 |
64 | +.komodotools |
65 | +.pylint.d |
66 | +.pytest_cache |
67 | +.vscode |
68 | +OpenLP.egg-info |
69 | +\#*\# |
70 | +__pycache__ |
71 | +build |
72 | +cover |
73 | +coverage |
74 | +dist |
75 | +env |
76 | htmlcov |
77 | +list |
78 | node_modules |
79 | openlp-test-projectordb.sqlite |
80 | +openlp.cfg |
81 | +openlp.pro |
82 | +openlp/core/resources.py.old |
83 | +openlp/plugins/presentations/lib/vendor/Pyro4 |
84 | +openlp/plugins/presentations/lib/vendor/serpent.py |
85 | +output |
86 | package-lock.json |
87 | -.cache |
88 | +tags |
89 | test |
90 | tests.kdev4 |
91 | |
92 | === modified file 'openlp/core/common/path.py' |
93 | --- openlp/core/common/path.py 2019-05-22 06:47:00 +0000 |
94 | +++ openlp/core/common/path.py 2019-06-05 05:00:52 +0000 |
95 | @@ -78,6 +78,8 @@ |
96 | :return: An empty string if :param:`path` is None, else a string representation of the :param:`path` |
97 | :rtype: str |
98 | """ |
99 | + if isinstance(path, str): |
100 | + return path |
101 | if not isinstance(path, Path) and path is not None: |
102 | raise TypeError('parameter \'path\' must be of type Path or NoneType') |
103 | if path is None: |
104 | |
105 | === modified file 'openlp/core/ui/media/mediacontroller.py' |
106 | --- openlp/core/ui/media/mediacontroller.py 2019-06-01 06:59:45 +0000 |
107 | +++ openlp/core/ui/media/mediacontroller.py 2019-06-05 05:00:52 +0000 |
108 | @@ -104,6 +104,8 @@ |
109 | State().update_pre_conditions('mediacontroller', True) |
110 | State().update_pre_conditions('media_live', True) |
111 | else: |
112 | + if hasattr(self.main_window, 'splash') and self.main_window.splash.isVisible(): |
113 | + self.main_window.splash.hide() |
114 | State().missing_text('media_live', translate('OpenLP.SlideController', |
115 | 'VLC or pymediainfo are missing, so you are unable to play any media')) |
116 | return True |
117 | |
118 | === added file 'openlp/plugins/presentations/lib/libreofficeserver.py' |
119 | --- openlp/plugins/presentations/lib/libreofficeserver.py 1970-01-01 00:00:00 +0000 |
120 | +++ openlp/plugins/presentations/lib/libreofficeserver.py 2019-06-05 05:00:52 +0000 |
121 | @@ -0,0 +1,431 @@ |
122 | +# -*- coding: utf-8 -*- |
123 | +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 |
124 | + |
125 | +########################################################################## |
126 | +# OpenLP - Open Source Lyrics Projection # |
127 | +# ---------------------------------------------------------------------- # |
128 | +# Copyright (c) 2008-2019 OpenLP Developers # |
129 | +# ---------------------------------------------------------------------- # |
130 | +# This program is free software: you can redistribute it and/or modify # |
131 | +# it under the terms of the GNU General Public License as published by # |
132 | +# the Free Software Foundation, either version 3 of the License, or # |
133 | +# (at your option) any later version. # |
134 | +# # |
135 | +# This program is distributed in the hope that it will be useful, # |
136 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of # |
137 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # |
138 | +# GNU General Public License for more details. # |
139 | +# # |
140 | +# You should have received a copy of the GNU General Public License # |
141 | +# along with this program. If not, see <https://www.gnu.org/licenses/>. # |
142 | +########################################################################## |
143 | +""" |
144 | +This module runs a Pyro4 server using LibreOffice's version of Python |
145 | + |
146 | +Please Note: This intentionally uses os.path over pathlib because we don't know which version of Python is shipped with |
147 | +the version of LibreOffice on the user's computer. |
148 | +""" |
149 | +from subprocess import Popen |
150 | +import sys |
151 | +import os |
152 | +import logging |
153 | +import time |
154 | + |
155 | + |
156 | +if sys.platform.startswith('darwin'): |
157 | + # Only make the log file on OS X when running as a server |
158 | + logfile = os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'libreofficeserver.log') |
159 | + print('Setting up log file: {logfile}'.format(logfile=logfile)) |
160 | + logging.basicConfig(filename=logfile, level=logging.INFO) |
161 | + |
162 | + |
163 | +# Add the current directory to sys.path so that we can load the serializers |
164 | +sys.path.append(os.path.join(os.path.dirname(__file__))) |
165 | +# Add the vendor directory to sys.path so that we can load Pyro4 |
166 | +sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor')) |
167 | + |
168 | +from serializers import register_classes |
169 | +from Pyro4 import Daemon, expose |
170 | + |
171 | +try: |
172 | + # Wrap these imports in a try so that we can run the tests on macOS |
173 | + import uno |
174 | + from com.sun.star.beans import PropertyValue |
175 | + from com.sun.star.task import ErrorCodeIOException |
176 | +except ImportError: |
177 | + # But they need to be defined for mocking |
178 | + uno = None |
179 | + PropertyValue = None |
180 | + ErrorCodeIOException = Exception |
181 | + |
182 | + |
183 | +log = logging.getLogger(__name__) |
184 | +register_classes() |
185 | + |
186 | + |
187 | +class TextType(object): |
188 | + """ |
189 | + Type Enumeration for Types of Text to request |
190 | + """ |
191 | + Title = 0 |
192 | + SlideText = 1 |
193 | + Notes = 2 |
194 | + |
195 | + |
196 | +class LibreOfficeException(Exception): |
197 | + """ |
198 | + A specific exception for LO |
199 | + """ |
200 | + pass |
201 | + |
202 | + |
203 | +@expose |
204 | +class LibreOfficeServer(object): |
205 | + """ |
206 | + A Pyro4 server which controls LibreOffice |
207 | + """ |
208 | + def __init__(self): |
209 | + """ |
210 | + Set up the server |
211 | + """ |
212 | + self._desktop = None |
213 | + self._control = None |
214 | + self._document = None |
215 | + self._presentation = None |
216 | + self._process = None |
217 | + self._manager = None |
218 | + |
219 | + def _create_property(self, name, value): |
220 | + """ |
221 | + Create an OOo style property object which are passed into some Uno methods. |
222 | + """ |
223 | + log.debug('create property') |
224 | + property_object = PropertyValue() |
225 | + property_object.Name = name |
226 | + property_object.Value = value |
227 | + return property_object |
228 | + |
229 | + def _get_text_from_page(self, slide_no, text_type=TextType.SlideText): |
230 | + """ |
231 | + Return any text extracted from the presentation page. |
232 | + |
233 | + :param slide_no: The slide the notes are required for, starting at 1 |
234 | + :param notes: A boolean. If set the method searches the notes of the slide. |
235 | + :param text_type: A TextType. Enumeration of the types of supported text. |
236 | + """ |
237 | + text = '' |
238 | + if TextType.Title <= text_type <= TextType.Notes: |
239 | + pages = self._document.getDrawPages() |
240 | + if 0 < slide_no <= pages.getCount(): |
241 | + page = pages.getByIndex(slide_no - 1) |
242 | + if text_type == TextType.Notes: |
243 | + page = page.getNotesPage() |
244 | + for index in range(page.getCount()): |
245 | + shape = page.getByIndex(index) |
246 | + shape_type = shape.getShapeType() |
247 | + if shape.supportsService('com.sun.star.drawing.Text'): |
248 | + # if they requested title, make sure it is the title |
249 | + if text_type != TextType.Title or shape_type == 'com.sun.star.presentation.TitleTextShape': |
250 | + text += shape.getString() + '\n' |
251 | + return text |
252 | + |
253 | + def start_process(self): |
254 | + """ |
255 | + Initialise Impress |
256 | + """ |
257 | + uno_command = [ |
258 | + '/Applications/LibreOffice.app/Contents/MacOS/soffice', |
259 | + '--nologo', |
260 | + '--norestore', |
261 | + '--minimized', |
262 | + '--nodefault', |
263 | + '--nofirststartwizard', |
264 | + '--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager' |
265 | + ] |
266 | + self._process = Popen(uno_command) |
267 | + |
268 | + @property |
269 | + def desktop(self): |
270 | + """ |
271 | + Set up an UNO desktop instance |
272 | + """ |
273 | + if self._desktop is not None: |
274 | + return self._desktop |
275 | + uno_instance = None |
276 | + context = uno.getComponentContext() |
277 | + resolver = context.ServiceManager.createInstanceWithContext('com.sun.star.bridge.UnoUrlResolver', context) |
278 | + loop = 0 |
279 | + while uno_instance is None and loop < 3: |
280 | + try: |
281 | + uno_instance = resolver.resolve('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext') |
282 | + except Exception: |
283 | + log.exception('Unable to find running instance, retrying...') |
284 | + loop += 1 |
285 | + try: |
286 | + self._manager = uno_instance.ServiceManager |
287 | + log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop') |
288 | + desktop = self._manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance) |
289 | + if not desktop: |
290 | + raise Exception('Failed to get UNO desktop') |
291 | + self._desktop = desktop |
292 | + return desktop |
293 | + except Exception: |
294 | + log.exception('Failed to get UNO desktop') |
295 | + return None |
296 | + |
297 | + def shutdown(self): |
298 | + """ |
299 | + Shut down the server |
300 | + """ |
301 | + can_kill = True |
302 | + if hasattr(self, '_docs'): |
303 | + while self._docs: |
304 | + self._docs[0].close_presentation() |
305 | + docs = self.desktop.getComponents() |
306 | + count = 0 |
307 | + if docs.hasElements(): |
308 | + list_elements = docs.createEnumeration() |
309 | + while list_elements.hasMoreElements(): |
310 | + doc = list_elements.nextElement() |
311 | + if doc.getImplementationName() != 'com.sun.star.comp.framework.BackingComp': |
312 | + count += 1 |
313 | + if count > 0: |
314 | + log.debug('LibreOffice not terminated as docs are still open') |
315 | + can_kill = False |
316 | + else: |
317 | + try: |
318 | + self.desktop.terminate() |
319 | + log.debug('LibreOffice killed') |
320 | + except Exception: |
321 | + log.exception('Failed to terminate LibreOffice') |
322 | + if getattr(self, '_process') and can_kill: |
323 | + self._process.kill() |
324 | + |
325 | + def load_presentation(self, file_path, screen_number): |
326 | + """ |
327 | + Load a presentation |
328 | + """ |
329 | + self._file_path = file_path |
330 | + url = uno.systemPathToFileUrl(file_path) |
331 | + properties = (self._create_property('Hidden', True),) |
332 | + self._document = None |
333 | + loop_count = 0 |
334 | + while loop_count < 3: |
335 | + try: |
336 | + self._document = self.desktop.loadComponentFromURL(url, '_blank', 0, properties) |
337 | + except Exception: |
338 | + log.exception('Failed to load presentation {url}'.format(url=url)) |
339 | + if self._document: |
340 | + break |
341 | + time.sleep(0.5) |
342 | + loop_count += 1 |
343 | + if loop_count == 3: |
344 | + log.error('Looped too many times') |
345 | + return False |
346 | + self._presentation = self._document.getPresentation() |
347 | + self._presentation.Display = screen_number |
348 | + self._control = None |
349 | + return True |
350 | + |
351 | + def extract_thumbnails(self, temp_folder): |
352 | + """ |
353 | + Create thumbnails for the presentation |
354 | + """ |
355 | + thumbnails = [] |
356 | + thumb_dir_url = uno.systemPathToFileUrl(temp_folder) |
357 | + properties = (self._create_property('FilterName', 'impress_png_Export'),) |
358 | + pages = self._document.getDrawPages() |
359 | + if not pages: |
360 | + return [] |
361 | + if not os.path.isdir(temp_folder): |
362 | + os.makedirs(temp_folder) |
363 | + for index in range(pages.getCount()): |
364 | + page = pages.getByIndex(index) |
365 | + self._document.getCurrentController().setCurrentPage(page) |
366 | + url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1)) |
367 | + path = os.path.join(temp_folder, str(index + 1) + '.png') |
368 | + try: |
369 | + self._document.storeToURL(url_path, properties) |
370 | + thumbnails.append(path) |
371 | + except ErrorCodeIOException as exception: |
372 | + log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) |
373 | + except Exception: |
374 | + log.exception('{path} - Unable to store openoffice preview'.format(path=path)) |
375 | + return thumbnails |
376 | + |
377 | + def get_titles_and_notes(self): |
378 | + """ |
379 | + Extract the titles and the notes from the slides. |
380 | + """ |
381 | + titles = [] |
382 | + notes = [] |
383 | + pages = self._document.getDrawPages() |
384 | + for slide_no in range(1, pages.getCount() + 1): |
385 | + titles.append(self._get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n') |
386 | + note = self._get_text_from_page(slide_no, TextType.Notes) |
387 | + if len(note) == 0: |
388 | + note = ' ' |
389 | + notes.append(note) |
390 | + return titles, notes |
391 | + |
392 | + def close_presentation(self): |
393 | + """ |
394 | + Close presentation and clean up objects. |
395 | + """ |
396 | + log.debug('close Presentation LibreOffice') |
397 | + if self._document: |
398 | + if self._presentation: |
399 | + try: |
400 | + self._presentation.end() |
401 | + self._presentation = None |
402 | + self._document.dispose() |
403 | + except Exception: |
404 | + log.exception("Closing presentation failed") |
405 | + self._document = None |
406 | + |
407 | + def is_loaded(self): |
408 | + """ |
409 | + Returns true if a presentation is loaded. |
410 | + """ |
411 | + log.debug('is loaded LibreOffice') |
412 | + if self._presentation is None or self._document is None: |
413 | + log.debug("is_loaded: no presentation or document") |
414 | + return False |
415 | + try: |
416 | + if self._document.getPresentation() is None: |
417 | + log.debug("getPresentation failed to find a presentation") |
418 | + return False |
419 | + except Exception: |
420 | + log.exception("getPresentation failed to find a presentation") |
421 | + return False |
422 | + return True |
423 | + |
424 | + def is_active(self): |
425 | + """ |
426 | + Returns true if a presentation is active and running. |
427 | + """ |
428 | + log.debug('is active LibreOffice') |
429 | + if not self.is_loaded(): |
430 | + return False |
431 | + return self._control.isRunning() if self._control else False |
432 | + |
433 | + def unblank_screen(self): |
434 | + """ |
435 | + Unblanks the screen. |
436 | + """ |
437 | + log.debug('unblank screen LibreOffice') |
438 | + return self._control.resume() |
439 | + |
440 | + def blank_screen(self): |
441 | + """ |
442 | + Blanks the screen. |
443 | + """ |
444 | + log.debug('blank screen LibreOffice') |
445 | + self._control.blankScreen(0) |
446 | + |
447 | + def is_blank(self): |
448 | + """ |
449 | + Returns true if screen is blank. |
450 | + """ |
451 | + log.debug('is blank LibreOffice') |
452 | + if self._control and self._control.isRunning(): |
453 | + return self._control.isPaused() |
454 | + else: |
455 | + return False |
456 | + |
457 | + def stop_presentation(self): |
458 | + """ |
459 | + Stop the presentation, remove from screen. |
460 | + """ |
461 | + log.debug('stop presentation LibreOffice') |
462 | + self._presentation.end() |
463 | + self._control = None |
464 | + |
465 | + def start_presentation(self): |
466 | + """ |
467 | + Start the presentation from the beginning. |
468 | + """ |
469 | + log.debug('start presentation LibreOffice') |
470 | + if self._control is None or not self._control.isRunning(): |
471 | + window = self._document.getCurrentController().getFrame().getContainerWindow() |
472 | + window.setVisible(True) |
473 | + self._presentation.start() |
474 | + self._control = self._presentation.getController() |
475 | + # start() returns before the Component is ready. Try for 15 seconds. |
476 | + sleep_count = 1 |
477 | + while not self._control and sleep_count < 150: |
478 | + time.sleep(0.1) |
479 | + sleep_count += 1 |
480 | + self._control = self._presentation.getController() |
481 | + window.setVisible(False) |
482 | + else: |
483 | + self._control.activate() |
484 | + self.goto_slide(1) |
485 | + |
486 | + def get_slide_number(self): |
487 | + """ |
488 | + Return the current slide number on the screen, from 1. |
489 | + """ |
490 | + return self._control.getCurrentSlideIndex() + 1 |
491 | + |
492 | + def get_slide_count(self): |
493 | + """ |
494 | + Return the total number of slides. |
495 | + """ |
496 | + return self._document.getDrawPages().getCount() |
497 | + |
498 | + def goto_slide(self, slide_no): |
499 | + """ |
500 | + Go to a specific slide (from 1). |
501 | + |
502 | + :param slide_no: The slide the text is required for, starting at 1 |
503 | + """ |
504 | + self._control.gotoSlideIndex(slide_no - 1) |
505 | + |
506 | + def next_step(self): |
507 | + """ |
508 | + Triggers the next effect of slide on the running presentation. |
509 | + """ |
510 | + is_paused = self._control.isPaused() |
511 | + self._control.gotoNextEffect() |
512 | + time.sleep(0.1) |
513 | + if not is_paused and self._control.isPaused(): |
514 | + self._control.gotoPreviousEffect() |
515 | + |
516 | + def previous_step(self): |
517 | + """ |
518 | + Triggers the previous slide on the running presentation. |
519 | + """ |
520 | + self._control.gotoPreviousEffect() |
521 | + |
522 | + def get_slide_text(self, slide_no): |
523 | + """ |
524 | + Returns the text on the slide. |
525 | + |
526 | + :param slide_no: The slide the text is required for, starting at 1 |
527 | + """ |
528 | + return self._get_text_from_page(slide_no) |
529 | + |
530 | + def get_slide_notes(self, slide_no): |
531 | + """ |
532 | + Returns the text in the slide notes. |
533 | + |
534 | + :param slide_no: The slide the notes are required for, starting at 1 |
535 | + """ |
536 | + return self._get_text_from_page(slide_no, TextType.Notes) |
537 | + |
538 | + |
539 | +def main(): |
540 | + """ |
541 | + The main function which runs the server |
542 | + """ |
543 | + daemon = Daemon(host='localhost', port=4310) |
544 | + daemon.register(LibreOfficeServer, 'openlp.libreofficeserver') |
545 | + try: |
546 | + daemon.requestLoop() |
547 | + finally: |
548 | + daemon.close() |
549 | + |
550 | + |
551 | +if __name__ == '__main__': |
552 | + main() |
553 | |
554 | === added file 'openlp/plugins/presentations/lib/maclocontroller.py' |
555 | --- openlp/plugins/presentations/lib/maclocontroller.py 1970-01-01 00:00:00 +0000 |
556 | +++ openlp/plugins/presentations/lib/maclocontroller.py 2019-06-05 05:00:52 +0000 |
557 | @@ -0,0 +1,266 @@ |
558 | +# -*- coding: utf-8 -*- |
559 | +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 |
560 | + |
561 | +########################################################################## |
562 | +# OpenLP - Open Source Lyrics Projection # |
563 | +# ---------------------------------------------------------------------- # |
564 | +# Copyright (c) 2008-2019 OpenLP Developers # |
565 | +# ---------------------------------------------------------------------- # |
566 | +# This program is free software: you can redistribute it and/or modify # |
567 | +# it under the terms of the GNU General Public License as published by # |
568 | +# the Free Software Foundation, either version 3 of the License, or # |
569 | +# (at your option) any later version. # |
570 | +# # |
571 | +# This program is distributed in the hope that it will be useful, # |
572 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of # |
573 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # |
574 | +# GNU General Public License for more details. # |
575 | +# # |
576 | +# You should have received a copy of the GNU General Public License # |
577 | +# along with this program. If not, see <https://www.gnu.org/licenses/>. # |
578 | +########################################################################## |
579 | + |
580 | +import logging |
581 | +from subprocess import Popen |
582 | + |
583 | +from Pyro4 import Proxy |
584 | + |
585 | +from openlp.core.common import delete_file, is_macosx |
586 | +from openlp.core.common.applocation import AppLocation |
587 | +from openlp.core.common.path import Path |
588 | +from openlp.core.common.registry import Registry |
589 | +from openlp.core.display.screens import ScreenList |
590 | +from openlp.plugins.presentations.lib.serializers import register_classes |
591 | +from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument |
592 | + |
593 | + |
594 | +LIBREOFFICE_PATH = Path('/Applications/LibreOffice.app') |
595 | +LIBREOFFICE_PYTHON = LIBREOFFICE_PATH / 'Contents' / 'Resources' / 'python' |
596 | + |
597 | +if is_macosx() and LIBREOFFICE_PATH.exists(): |
598 | + macuno_available = True |
599 | +else: |
600 | + macuno_available = False |
601 | + |
602 | + |
603 | +log = logging.getLogger(__name__) |
604 | +register_classes() |
605 | + |
606 | + |
607 | +class MacLOController(PresentationController): |
608 | + """ |
609 | + Class to control interactions with MacLO presentations on Mac OS X via Pyro4. It starts the Pyro4 nameserver, |
610 | + starts the LibreOfficeServer, and then controls MacLO via Pyro4. |
611 | + """ |
612 | + log.info('MacLOController loaded') |
613 | + |
614 | + def __init__(self, plugin): |
615 | + """ |
616 | + Initialise the class |
617 | + """ |
618 | + log.debug('Initialising') |
619 | + super(MacLOController, self).__init__(plugin, 'maclo', MacLODocument, 'Impress on macOS') |
620 | + self.supports = ['odp'] |
621 | + self.also_supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm'] |
622 | + self.server_process = None |
623 | + self._client = None |
624 | + self._start_server() |
625 | + |
626 | + def _start_server(self): |
627 | + """ |
628 | + Start a LibreOfficeServer |
629 | + """ |
630 | + libreoffice_python = Path('/Applications/LibreOffice.app/Contents/Resources/python') |
631 | + libreoffice_server = AppLocation.get_directory(AppLocation.PluginsDir).joinpath('presentations', 'lib', |
632 | + 'libreofficeserver.py') |
633 | + if libreoffice_python.exists(): |
634 | + self.server_process = Popen([str(libreoffice_python), str(libreoffice_server)]) |
635 | + |
636 | + @property |
637 | + def client(self): |
638 | + """ |
639 | + Set up a Pyro4 client so that we can talk to the LibreOfficeServer |
640 | + """ |
641 | + if not self._client: |
642 | + self._client = Proxy('PYRO:openlp.libreofficeserver@localhost:4310') |
643 | + if not self._client._pyroConnection: |
644 | + self._client._pyroReconnect() |
645 | + return self._client |
646 | + |
647 | + def check_available(self): |
648 | + """ |
649 | + MacLO is able to run on this machine. |
650 | + """ |
651 | + log.debug('check_available') |
652 | + return macuno_available |
653 | + |
654 | + def start_process(self): |
655 | + """ |
656 | + Loads a running version of LibreOffice in the background. It is not displayed to the user but is available to |
657 | + the UNO interface when required. |
658 | + """ |
659 | + log.debug('Started automatically by the Pyro server') |
660 | + self.client.start_process() |
661 | + |
662 | + def kill(self): |
663 | + """ |
664 | + Called at system exit to clean up any running presentations. |
665 | + """ |
666 | + log.debug('Kill LibreOffice') |
667 | + self.client.shutdown() |
668 | + self.server_process.kill() |
669 | + |
670 | + |
671 | +class MacLODocument(PresentationDocument): |
672 | + """ |
673 | + Class which holds information and controls a single presentation. |
674 | + """ |
675 | + |
676 | + def __init__(self, controller, presentation): |
677 | + """ |
678 | + Constructor, store information about the file and initialise. |
679 | + """ |
680 | + log.debug('Init Presentation LibreOffice') |
681 | + super(MacLODocument, self).__init__(controller, presentation) |
682 | + self.client = controller.client |
683 | + |
684 | + def load_presentation(self): |
685 | + """ |
686 | + Tell the LibreOfficeServer to start the presentation. |
687 | + """ |
688 | + log.debug('Load Presentation LibreOffice') |
689 | + if not self.client.load_presentation(str(self.file_path), ScreenList().current.number + 1): |
690 | + return False |
691 | + self.create_thumbnails() |
692 | + self.create_titles_and_notes() |
693 | + return True |
694 | + |
695 | + def create_thumbnails(self): |
696 | + """ |
697 | + Create thumbnail images for presentation. |
698 | + """ |
699 | + log.debug('create thumbnails LibreOffice') |
700 | + if self.check_thumbnails(): |
701 | + return |
702 | + temp_thumbnails = self.client.extract_thumbnails(str(self.get_temp_folder())) |
703 | + for index, temp_thumb in enumerate(temp_thumbnails): |
704 | + temp_thumb = Path(temp_thumb) |
705 | + self.convert_thumbnail(temp_thumb, index + 1) |
706 | + delete_file(temp_thumb) |
707 | + |
708 | + def create_titles_and_notes(self): |
709 | + """ |
710 | + Writes the list of titles (one per slide) to 'titles.txt' and the notes to 'slideNotes[x].txt' |
711 | + in the thumbnails directory |
712 | + """ |
713 | + titles, notes = self.client.get_titles_and_notes() |
714 | + self.save_titles_and_notes(titles, notes) |
715 | + |
716 | + def close_presentation(self): |
717 | + """ |
718 | + Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being |
719 | + shutdown. |
720 | + """ |
721 | + log.debug('close Presentation LibreOffice') |
722 | + self.client.close_presentation() |
723 | + self.controller.remove_doc(self) |
724 | + |
725 | + def is_loaded(self): |
726 | + """ |
727 | + Returns true if a presentation is loaded. |
728 | + """ |
729 | + log.debug('is loaded LibreOffice') |
730 | + return self.client.is_loaded() |
731 | + |
732 | + def is_active(self): |
733 | + """ |
734 | + Returns true if a presentation is active and running. |
735 | + """ |
736 | + log.debug('is active LibreOffice') |
737 | + return self.client.is_active() |
738 | + |
739 | + def unblank_screen(self): |
740 | + """ |
741 | + Unblanks the screen. |
742 | + """ |
743 | + log.debug('unblank screen LibreOffice') |
744 | + return self.client.unblank_screen() |
745 | + |
746 | + def blank_screen(self): |
747 | + """ |
748 | + Blanks the screen. |
749 | + """ |
750 | + log.debug('blank screen LibreOffice') |
751 | + self.client.blank_screen() |
752 | + |
753 | + def is_blank(self): |
754 | + """ |
755 | + Returns true if screen is blank. |
756 | + """ |
757 | + log.debug('is blank LibreOffice') |
758 | + return self.client.is_blank() |
759 | + |
760 | + def stop_presentation(self): |
761 | + """ |
762 | + Stop the presentation, remove from screen. |
763 | + """ |
764 | + log.debug('stop presentation LibreOffice') |
765 | + self.client.stop_presentation() |
766 | + |
767 | + def start_presentation(self): |
768 | + """ |
769 | + Start the presentation from the beginning. |
770 | + """ |
771 | + log.debug('start presentation LibreOffice') |
772 | + self.client.start_presentation() |
773 | + # Make sure impress doesn't steal focus, unless we're on a single screen setup |
774 | + if len(ScreenList()) > 1: |
775 | + Registry().get('main_window').activateWindow() |
776 | + |
777 | + def get_slide_number(self): |
778 | + """ |
779 | + Return the current slide number on the screen, from 1. |
780 | + """ |
781 | + return self.client.get_slide_number() |
782 | + |
783 | + def get_slide_count(self): |
784 | + """ |
785 | + Return the total number of slides. |
786 | + """ |
787 | + return self.client.get_slide_count() |
788 | + |
789 | + def goto_slide(self, slide_no): |
790 | + """ |
791 | + Go to a specific slide (from 1). |
792 | + |
793 | + :param slide_no: The slide the text is required for, starting at 1 |
794 | + """ |
795 | + self.client.goto_slide(slide_no) |
796 | + |
797 | + def next_step(self): |
798 | + """ |
799 | + Triggers the next effect of slide on the running presentation. |
800 | + """ |
801 | + self.client.next_step() |
802 | + |
803 | + def previous_step(self): |
804 | + """ |
805 | + Triggers the previous slide on the running presentation. |
806 | + """ |
807 | + self.client.previous_step() |
808 | + |
809 | + def get_slide_text(self, slide_no): |
810 | + """ |
811 | + Returns the text on the slide. |
812 | + |
813 | + :param slide_no: The slide the text is required for, starting at 1 |
814 | + """ |
815 | + return self.client.get_slide_text(slide_no) |
816 | + |
817 | + def get_slide_notes(self, slide_no): |
818 | + """ |
819 | + Returns the text in the slide notes. |
820 | + |
821 | + :param slide_no: The slide the notes are required for, starting at 1 |
822 | + """ |
823 | + return self.client.get_slide_notes(slide_no) |
824 | |
825 | === modified file 'openlp/plugins/presentations/lib/presentationcontroller.py' |
826 | --- openlp/plugins/presentations/lib/presentationcontroller.py 2019-05-22 06:47:00 +0000 |
827 | +++ openlp/plugins/presentations/lib/presentationcontroller.py 2019-06-05 05:00:52 +0000 |
828 | @@ -410,7 +410,8 @@ |
829 | """ |
830 | log.info('PresentationController loaded') |
831 | |
832 | - def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument): |
833 | + def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument, |
834 | + display_name=None): |
835 | """ |
836 | This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins |
837 | |
838 | @@ -430,6 +431,7 @@ |
839 | self.docs = [] |
840 | self.plugin = plugin |
841 | self.name = name |
842 | + self.display_name = display_name if display_name is not None else name |
843 | self.document_class = document_class |
844 | self.settings_section = self.plugin.settings_section |
845 | self.available = None |
846 | |
847 | === modified file 'openlp/plugins/presentations/lib/presentationtab.py' |
848 | --- openlp/plugins/presentations/lib/presentationtab.py 2019-05-22 06:47:00 +0000 |
849 | +++ openlp/plugins/presentations/lib/presentationtab.py 2019-06-05 05:00:52 +0000 |
850 | @@ -127,10 +127,10 @@ |
851 | |
852 | def set_controller_text(self, checkbox, controller): |
853 | if checkbox.isEnabled(): |
854 | - checkbox.setText(controller.name) |
855 | + checkbox.setText(controller.display_name) |
856 | else: |
857 | checkbox.setText(translate('PresentationPlugin.PresentationTab', |
858 | - '{name} (unavailable)').format(name=controller.name)) |
859 | + '{name} (unavailable)').format(name=controller.display_name)) |
860 | |
861 | def load(self): |
862 | """ |
863 | |
864 | === added file 'openlp/plugins/presentations/lib/serializers.py' |
865 | --- openlp/plugins/presentations/lib/serializers.py 1970-01-01 00:00:00 +0000 |
866 | +++ openlp/plugins/presentations/lib/serializers.py 2019-06-05 05:00:52 +0000 |
867 | @@ -0,0 +1,52 @@ |
868 | +# -*- coding: utf-8 -*- |
869 | +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 |
870 | + |
871 | +########################################################################## |
872 | +# OpenLP - Open Source Lyrics Projection # |
873 | +# ---------------------------------------------------------------------- # |
874 | +# Copyright (c) 2008-2019 OpenLP Developers # |
875 | +# ---------------------------------------------------------------------- # |
876 | +# This program is free software: you can redistribute it and/or modify # |
877 | +# it under the terms of the GNU General Public License as published by # |
878 | +# the Free Software Foundation, either version 3 of the License, or # |
879 | +# (at your option) any later version. # |
880 | +# # |
881 | +# This program is distributed in the hope that it will be useful, # |
882 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of # |
883 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # |
884 | +# GNU General Public License for more details. # |
885 | +# # |
886 | +# You should have received a copy of the GNU General Public License # |
887 | +# along with this program. If not, see <https://www.gnu.org/licenses/>. # |
888 | +########################################################################## |
889 | +""" |
890 | +This module contains some helpers for serializing Path objects in Pyro4 |
891 | +""" |
892 | +try: |
893 | + from openlp.core.common.path import Path |
894 | +except ImportError: |
895 | + from pathlib import Path |
896 | + |
897 | +from Pyro4.util import SerializerBase |
898 | + |
899 | + |
900 | +def path_class_to_dict(obj): |
901 | + """ |
902 | + Serialize a Path object for Pyro4 |
903 | + """ |
904 | + return { |
905 | + '__class__': 'Path', |
906 | + 'parts': obj.parts |
907 | + } |
908 | + |
909 | + |
910 | +def path_dict_to_class(classname, d): |
911 | + return Path(d['parts']) |
912 | + |
913 | + |
914 | +def register_classes(): |
915 | + """ |
916 | + Register the serializers |
917 | + """ |
918 | + SerializerBase.register_class_to_dict(Path, path_class_to_dict) |
919 | + SerializerBase.register_dict_to_class('Path', path_dict_to_class) |
920 | |
921 | === added directory 'openlp/plugins/presentations/lib/vendor' |
922 | === added file 'openlp/plugins/presentations/lib/vendor/do_not_delete.txt' |
923 | --- openlp/plugins/presentations/lib/vendor/do_not_delete.txt 1970-01-01 00:00:00 +0000 |
924 | +++ openlp/plugins/presentations/lib/vendor/do_not_delete.txt 2019-06-05 05:00:52 +0000 |
925 | @@ -0,0 +1,5 @@ |
926 | +Vendor Directory |
927 | +================ |
928 | + |
929 | +Do not delete this directory, it is used on Mac OS to place Pyro4 and serpent for use with Impress. |
930 | + |
931 | |
932 | === modified file 'openlp/plugins/presentations/presentationplugin.py' |
933 | --- openlp/plugins/presentations/presentationplugin.py 2019-04-13 13:00:22 +0000 |
934 | +++ openlp/plugins/presentations/presentationplugin.py 2019-06-05 05:00:52 +0000 |
935 | @@ -28,13 +28,13 @@ |
936 | |
937 | from PyQt5 import QtCore |
938 | |
939 | -from openlp.core.state import State |
940 | from openlp.core.api.http import register_endpoint |
941 | from openlp.core.common import extension_loader |
942 | from openlp.core.common.i18n import translate |
943 | from openlp.core.common.settings import Settings |
944 | from openlp.core.lib import build_icon |
945 | from openlp.core.lib.plugin import Plugin, StringContent |
946 | +from openlp.core.state import State |
947 | from openlp.core.ui.icons import UiIcons |
948 | from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint |
949 | from openlp.plugins.presentations.lib.presentationcontroller import PresentationController |
950 | @@ -45,18 +45,20 @@ |
951 | log = logging.getLogger(__name__) |
952 | |
953 | |
954 | -__default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked, |
955 | - 'presentations/enable_pdf_program': QtCore.Qt.Unchecked, |
956 | - 'presentations/pdf_program': None, |
957 | - 'presentations/Impress': QtCore.Qt.Checked, |
958 | - 'presentations/Powerpoint': QtCore.Qt.Checked, |
959 | - 'presentations/Pdf': QtCore.Qt.Checked, |
960 | - 'presentations/presentations files': [], |
961 | - 'presentations/thumbnail_scheme': '', |
962 | - 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked, |
963 | - 'presentations/powerpoint control window': QtCore.Qt.Unchecked, |
964 | - 'presentations/last directory': None |
965 | - } |
966 | +__default_settings__ = { |
967 | + 'presentations/override app': QtCore.Qt.Unchecked, |
968 | + 'presentations/enable_pdf_program': QtCore.Qt.Unchecked, |
969 | + 'presentations/pdf_program': None, |
970 | + 'presentations/maclo': QtCore.Qt.Checked, |
971 | + 'presentations/Impress': QtCore.Qt.Checked, |
972 | + 'presentations/Powerpoint': QtCore.Qt.Checked, |
973 | + 'presentations/Pdf': QtCore.Qt.Checked, |
974 | + 'presentations/presentations files': [], |
975 | + 'presentations/thumbnail_scheme': '', |
976 | + 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked, |
977 | + 'presentations/powerpoint control window': QtCore.Qt.Unchecked, |
978 | + 'presentations/last directory': None |
979 | +} |
980 | |
981 | |
982 | class PresentationPlugin(Plugin): |
983 | @@ -100,7 +102,7 @@ |
984 | try: |
985 | self.controllers[controller].start_process() |
986 | except Exception: |
987 | - log.warning('Failed to start controller process') |
988 | + log.exception('Failed to start controller process') |
989 | self.controllers[controller].available = False |
990 | self.media_item.build_file_mask_string() |
991 | |
992 | |
993 | === modified file 'scripts/check_dependencies.py' |
994 | --- scripts/check_dependencies.py 2019-05-05 08:13:10 +0000 |
995 | +++ scripts/check_dependencies.py 2019-06-05 05:00:52 +0000 |
996 | @@ -159,6 +159,8 @@ |
997 | w('OK') |
998 | except ImportError: |
999 | w('FAIL') |
1000 | + except Exception: |
1001 | + w('ERROR') |
1002 | w(os.linesep) |
1003 | |
1004 | |
1005 | |
1006 | === modified file 'tests/functional/openlp_core/common/test_path.py' |
1007 | --- tests/functional/openlp_core/common/test_path.py 2019-05-26 10:30:37 +0000 |
1008 | +++ tests/functional/openlp_core/common/test_path.py 2019-06-05 05:00:52 +0000 |
1009 | @@ -110,7 +110,18 @@ |
1010 | # WHEN: Calling `path_to_str` with an invalid Type |
1011 | # THEN: A TypeError should have been raised |
1012 | with self.assertRaises(TypeError): |
1013 | - path_to_str(str()) |
1014 | + path_to_str(57) |
1015 | + |
1016 | + def test_path_to_str_wth_str(self): |
1017 | + """ |
1018 | + Test that `path_to_str` just returns a str when given a str |
1019 | + """ |
1020 | + # GIVEN: The `path_to_str` function |
1021 | + # WHEN: Calling `path_to_str` with a str |
1022 | + result = path_to_str('/usr/bin') |
1023 | + |
1024 | + # THEN: The string should be returned |
1025 | + assert result == '/usr/bin' |
1026 | |
1027 | def test_path_to_str_none(self): |
1028 | """ |
1029 | |
1030 | === added file 'tests/functional/openlp_plugins/presentations/test_libreofficeserver.py' |
1031 | --- tests/functional/openlp_plugins/presentations/test_libreofficeserver.py 1970-01-01 00:00:00 +0000 |
1032 | +++ tests/functional/openlp_plugins/presentations/test_libreofficeserver.py 2019-06-05 05:00:52 +0000 |
1033 | @@ -0,0 +1,948 @@ |
1034 | +# -*- coding: utf-8 -*- |
1035 | +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 |
1036 | + |
1037 | +########################################################################## |
1038 | +# OpenLP - Open Source Lyrics Projection # |
1039 | +# ---------------------------------------------------------------------- # |
1040 | +# Copyright (c) 2008-2019 OpenLP Developers # |
1041 | +# ---------------------------------------------------------------------- # |
1042 | +# This program is free software: you can redistribute it and/or modify # |
1043 | +# it under the terms of the GNU General Public License as published by # |
1044 | +# the Free Software Foundation, either version 3 of the License, or # |
1045 | +# (at your option) any later version. # |
1046 | +# # |
1047 | +# This program is distributed in the hope that it will be useful, # |
1048 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of # |
1049 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # |
1050 | +# GNU General Public License for more details. # |
1051 | +# # |
1052 | +# You should have received a copy of the GNU General Public License # |
1053 | +# along with this program. If not, see <https://www.gnu.org/licenses/>. # |
1054 | +########################################################################## |
1055 | +""" |
1056 | +Functional tests to test the LibreOffice Pyro server |
1057 | +""" |
1058 | +from unittest.mock import MagicMock, patch, call |
1059 | + |
1060 | +from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType, main |
1061 | + |
1062 | + |
1063 | +def test_constructor(): |
1064 | + """ |
1065 | + Test the Constructor from the server |
1066 | + """ |
1067 | + # GIVEN: No server |
1068 | + # WHEN: The server object is created |
1069 | + server = LibreOfficeServer() |
1070 | + |
1071 | + # THEN: The server should have been set up correctly |
1072 | + assert server._control is None |
1073 | + # assert server._desktop is None |
1074 | + assert server._document is None |
1075 | + assert server._presentation is None |
1076 | + assert server._process is None |
1077 | + |
1078 | + |
1079 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.Popen') |
1080 | +def test_start_process(MockedPopen): |
1081 | + """ |
1082 | + Test that the correct command is issued to run LibreOffice |
1083 | + """ |
1084 | + # GIVEN: A LOServer |
1085 | + mocked_process = MagicMock() |
1086 | + MockedPopen.return_value = mocked_process |
1087 | + server = LibreOfficeServer() |
1088 | + |
1089 | + # WHEN: The start_process() method is run |
1090 | + server.start_process() |
1091 | + |
1092 | + # THEN: The correct command line should run and the process should have started |
1093 | + MockedPopen.assert_called_with([ |
1094 | + '/Applications/LibreOffice.app/Contents/MacOS/soffice', |
1095 | + '--nologo', |
1096 | + '--norestore', |
1097 | + '--minimized', |
1098 | + '--nodefault', |
1099 | + '--nofirststartwizard', |
1100 | + '--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager' |
1101 | + ]) |
1102 | + assert server._process is mocked_process |
1103 | + |
1104 | + |
1105 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1106 | +def test_desktop_already_has_desktop(mocked_uno): |
1107 | + """ |
1108 | + Test that setup_desktop() exits early when there's already a desktop |
1109 | + """ |
1110 | + # GIVEN: A LibreOfficeServer instance |
1111 | + server = LibreOfficeServer() |
1112 | + server._desktop = MagicMock() |
1113 | + |
1114 | + # WHEN: the desktop property is called |
1115 | + desktop = server.desktop |
1116 | + |
1117 | + # THEN: setup_desktop() exits early |
1118 | + assert desktop is server._desktop |
1119 | + assert server._manager is None |
1120 | + |
1121 | + |
1122 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1123 | +def test_desktop_exception(mocked_uno): |
1124 | + """ |
1125 | + Test that setting up the desktop works correctly when an exception occurs |
1126 | + """ |
1127 | + # GIVEN: A LibreOfficeServer instance |
1128 | + server = LibreOfficeServer() |
1129 | + mocked_context = MagicMock() |
1130 | + mocked_resolver = MagicMock() |
1131 | + mocked_uno_instance = MagicMock() |
1132 | + MockedServiceManager = MagicMock() |
1133 | + mocked_uno.getComponentContext.return_value = mocked_context |
1134 | + mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver |
1135 | + mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance] |
1136 | + mocked_uno_instance.ServiceManager = MockedServiceManager |
1137 | + MockedServiceManager.createInstanceWithContext.side_effect = Exception() |
1138 | + |
1139 | + # WHEN: the desktop property is called |
1140 | + server.desktop |
1141 | + |
1142 | + # THEN: A desktop object was created |
1143 | + mocked_uno.getComponentContext.assert_called_once_with() |
1144 | + mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with( |
1145 | + 'com.sun.star.bridge.UnoUrlResolver', mocked_context) |
1146 | + expected_calls = [ |
1147 | + call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'), |
1148 | + call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext') |
1149 | + ] |
1150 | + assert mocked_resolver.resolve.call_args_list == expected_calls |
1151 | + MockedServiceManager.createInstanceWithContext.assert_called_once_with( |
1152 | + 'com.sun.star.frame.Desktop', mocked_uno_instance) |
1153 | + assert server._manager is MockedServiceManager |
1154 | + assert server._desktop is None |
1155 | + |
1156 | + |
1157 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1158 | +def test_desktop(mocked_uno): |
1159 | + """ |
1160 | + Test that setting up the desktop works correctly |
1161 | + """ |
1162 | + # GIVEN: A LibreOfficeServer instance |
1163 | + server = LibreOfficeServer() |
1164 | + mocked_context = MagicMock() |
1165 | + mocked_resolver = MagicMock() |
1166 | + mocked_uno_instance = MagicMock() |
1167 | + MockedServiceManager = MagicMock() |
1168 | + mocked_desktop = MagicMock() |
1169 | + mocked_uno.getComponentContext.return_value = mocked_context |
1170 | + mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver |
1171 | + mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance] |
1172 | + mocked_uno_instance.ServiceManager = MockedServiceManager |
1173 | + MockedServiceManager.createInstanceWithContext.return_value = mocked_desktop |
1174 | + |
1175 | + # WHEN: the desktop property is called |
1176 | + server.desktop |
1177 | + |
1178 | + # THEN: A desktop object was created |
1179 | + mocked_uno.getComponentContext.assert_called_once_with() |
1180 | + mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with( |
1181 | + 'com.sun.star.bridge.UnoUrlResolver', mocked_context) |
1182 | + expected_calls = [ |
1183 | + call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'), |
1184 | + call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext') |
1185 | + ] |
1186 | + assert mocked_resolver.resolve.call_args_list == expected_calls |
1187 | + MockedServiceManager.createInstanceWithContext.assert_called_once_with( |
1188 | + 'com.sun.star.frame.Desktop', mocked_uno_instance) |
1189 | + assert server._manager is MockedServiceManager |
1190 | + assert server._desktop is mocked_desktop |
1191 | + |
1192 | + |
1193 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.PropertyValue') |
1194 | +def test_create_property(MockedPropertyValue): |
1195 | + """ |
1196 | + Test that the _create_property() method works correctly |
1197 | + """ |
1198 | + # GIVEN: A server amnd property to set |
1199 | + server = LibreOfficeServer() |
1200 | + name = 'Hidden' |
1201 | + value = True |
1202 | + |
1203 | + # WHEN: The _create_property() method is called |
1204 | + prop = server._create_property(name, value) |
1205 | + |
1206 | + # THEN: The property should have the correct attributes |
1207 | + assert prop.Name == name |
1208 | + assert prop.Value == value |
1209 | + |
1210 | + |
1211 | +def test_get_text_from_page_slide_text(): |
1212 | + """ |
1213 | + Test that the _get_text_from_page() method gives us nothing for slide text |
1214 | + """ |
1215 | + # GIVEN: A LibreOfficeServer object and some mocked objects |
1216 | + text_type = TextType.SlideText |
1217 | + slide_no = 1 |
1218 | + server = LibreOfficeServer() |
1219 | + server._document = MagicMock() |
1220 | + mocked_pages = MagicMock() |
1221 | + mocked_page = MagicMock() |
1222 | + mocked_shape = MagicMock() |
1223 | + server._document.getDrawPages.return_value = mocked_pages |
1224 | + mocked_pages.getCount.return_value = 1 |
1225 | + mocked_pages.getByIndex.return_value = mocked_page |
1226 | + mocked_page.getByIndex.return_value = mocked_shape |
1227 | + mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape' |
1228 | + mocked_shape.supportsService.return_value = True |
1229 | + mocked_shape.getString.return_value = 'Page Text' |
1230 | + |
1231 | + # WHEN: _get_text_from_page() is run for slide text |
1232 | + text = server._get_text_from_page(slide_no, text_type) |
1233 | + |
1234 | + # THE: The text is correct |
1235 | + assert text == 'Page Text\n' |
1236 | + |
1237 | + |
1238 | +def test_get_text_from_page_title(): |
1239 | + """ |
1240 | + Test that the _get_text_from_page() method gives us the text from the titles |
1241 | + """ |
1242 | + # GIVEN: A LibreOfficeServer object and some mocked objects |
1243 | + text_type = TextType.Title |
1244 | + slide_no = 1 |
1245 | + server = LibreOfficeServer() |
1246 | + server._document = MagicMock() |
1247 | + mocked_pages = MagicMock() |
1248 | + mocked_page = MagicMock() |
1249 | + mocked_shape = MagicMock() |
1250 | + server._document.getDrawPages.return_value = mocked_pages |
1251 | + mocked_pages.getCount.return_value = 1 |
1252 | + mocked_pages.getByIndex.return_value = mocked_page |
1253 | + mocked_page.getByIndex.return_value = mocked_shape |
1254 | + mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape' |
1255 | + mocked_shape.supportsService.return_value = True |
1256 | + mocked_shape.getString.return_value = 'Page Title' |
1257 | + |
1258 | + # WHEN: _get_text_from_page() is run for titles |
1259 | + text = server._get_text_from_page(slide_no, text_type) |
1260 | + |
1261 | + # THEN: The text should be correct |
1262 | + assert text == 'Page Title\n' |
1263 | + |
1264 | + |
1265 | +def test_get_text_from_page_notes(): |
1266 | + """ |
1267 | + Test that the _get_text_from_page() method gives us the text from the notes |
1268 | + """ |
1269 | + # GIVEN: A LibreOfficeServer object and some mocked objects |
1270 | + text_type = TextType.Notes |
1271 | + slide_no = 1 |
1272 | + server = LibreOfficeServer() |
1273 | + server._document = MagicMock() |
1274 | + mocked_pages = MagicMock() |
1275 | + mocked_page = MagicMock() |
1276 | + mocked_notes_page = MagicMock() |
1277 | + mocked_shape = MagicMock() |
1278 | + server._document.getDrawPages.return_value = mocked_pages |
1279 | + mocked_pages.getCount.return_value = 1 |
1280 | + mocked_pages.getByIndex.return_value = mocked_page |
1281 | + mocked_page.getNotesPage.return_value = mocked_notes_page |
1282 | + mocked_notes_page.getByIndex.return_value = mocked_shape |
1283 | + mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape' |
1284 | + mocked_shape.supportsService.return_value = True |
1285 | + mocked_shape.getString.return_value = 'Page Notes' |
1286 | + |
1287 | + # WHEN: _get_text_from_page() is run for titles |
1288 | + text = server._get_text_from_page(slide_no, text_type) |
1289 | + |
1290 | + # THEN: The text should be correct |
1291 | + assert text == 'Page Notes\n' |
1292 | + |
1293 | + |
1294 | +def test_shutdown_other_docs(): |
1295 | + """ |
1296 | + Test the shutdown method while other documents are open in LibreOffice |
1297 | + """ |
1298 | + def close_docs(): |
1299 | + server._docs = [] |
1300 | + |
1301 | + # GIVEN: An up an running LibreOfficeServer |
1302 | + server = LibreOfficeServer() |
1303 | + mocked_doc = MagicMock() |
1304 | + mocked_desktop = MagicMock() |
1305 | + mocked_docs = MagicMock() |
1306 | + mocked_list = MagicMock() |
1307 | + mocked_element_doc = MagicMock() |
1308 | + server._docs = [mocked_doc] |
1309 | + server._desktop = mocked_desktop |
1310 | + server._process = MagicMock() |
1311 | + mocked_doc.close_presentation.side_effect = close_docs |
1312 | + mocked_desktop.getComponents.return_value = mocked_docs |
1313 | + mocked_docs.hasElements.return_value = True |
1314 | + mocked_docs.createEnumeration.return_value = mocked_list |
1315 | + mocked_list.hasMoreElements.side_effect = [True, False] |
1316 | + mocked_list.nextElement.return_value = mocked_element_doc |
1317 | + mocked_element_doc.getImplementationName.side_effect = [ |
1318 | + 'org.openlp.Nothing', |
1319 | + 'com.sun.star.comp.framework.BackingComp' |
1320 | + ] |
1321 | + |
1322 | + # WHEN: shutdown() is called |
1323 | + server.shutdown() |
1324 | + |
1325 | + # THEN: The right methods are called and everything works |
1326 | + mocked_doc.close_presentation.assert_called_once_with() |
1327 | + mocked_desktop.getComponents.assert_called_once_with() |
1328 | + mocked_docs.hasElements.assert_called_once_with() |
1329 | + mocked_docs.createEnumeration.assert_called_once_with() |
1330 | + assert mocked_list.hasMoreElements.call_count == 2 |
1331 | + mocked_list.nextElement.assert_called_once_with() |
1332 | + mocked_element_doc.getImplementationName.assert_called_once_with() |
1333 | + assert mocked_desktop.terminate.call_count == 0 |
1334 | + assert server._process.kill.call_count == 0 |
1335 | + |
1336 | + |
1337 | +def test_shutdown(): |
1338 | + """ |
1339 | + Test the shutdown method |
1340 | + """ |
1341 | + def close_docs(): |
1342 | + server._docs = [] |
1343 | + |
1344 | + # GIVEN: An up an running LibreOfficeServer |
1345 | + server = LibreOfficeServer() |
1346 | + mocked_doc = MagicMock() |
1347 | + mocked_desktop = MagicMock() |
1348 | + mocked_docs = MagicMock() |
1349 | + mocked_list = MagicMock() |
1350 | + mocked_element_doc = MagicMock() |
1351 | + server._docs = [mocked_doc] |
1352 | + server._desktop = mocked_desktop |
1353 | + server._process = MagicMock() |
1354 | + mocked_doc.close_presentation.side_effect = close_docs |
1355 | + mocked_desktop.getComponents.return_value = mocked_docs |
1356 | + mocked_docs.hasElements.return_value = True |
1357 | + mocked_docs.createEnumeration.return_value = mocked_list |
1358 | + mocked_list.hasMoreElements.side_effect = [True, False] |
1359 | + mocked_list.nextElement.return_value = mocked_element_doc |
1360 | + mocked_element_doc.getImplementationName.return_value = 'com.sun.star.comp.framework.BackingComp' |
1361 | + |
1362 | + # WHEN: shutdown() is called |
1363 | + server.shutdown() |
1364 | + |
1365 | + # THEN: The right methods are called and everything works |
1366 | + mocked_doc.close_presentation.assert_called_once_with() |
1367 | + mocked_desktop.getComponents.assert_called_once_with() |
1368 | + mocked_docs.hasElements.assert_called_once_with() |
1369 | + mocked_docs.createEnumeration.assert_called_once_with() |
1370 | + assert mocked_list.hasMoreElements.call_count == 2 |
1371 | + mocked_list.nextElement.assert_called_once_with() |
1372 | + mocked_element_doc.getImplementationName.assert_called_once_with() |
1373 | + mocked_desktop.terminate.assert_called_once_with() |
1374 | + server._process.kill.assert_called_once_with() |
1375 | + |
1376 | + |
1377 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1378 | +def test_load_presentation_exception(mocked_uno): |
1379 | + """ |
1380 | + Test the load_presentation() method when an exception occurs |
1381 | + """ |
1382 | + # GIVEN: A LibreOfficeServer object |
1383 | + presentation_file = '/path/to/presentation.odp' |
1384 | + screen_number = 1 |
1385 | + server = LibreOfficeServer() |
1386 | + mocked_desktop = MagicMock() |
1387 | + mocked_uno.systemPathToFileUrl.side_effect = lambda x: x |
1388 | + server._desktop = mocked_desktop |
1389 | + mocked_desktop.loadComponentFromURL.side_effect = Exception() |
1390 | + |
1391 | + # WHEN: load_presentation() is called |
1392 | + with patch.object(server, '_create_property') as mocked_create_property: |
1393 | + mocked_create_property.side_effect = lambda x, y: {x: y} |
1394 | + result = server.load_presentation(presentation_file, screen_number) |
1395 | + |
1396 | + # THEN: A presentation is loaded |
1397 | + assert result is False |
1398 | + |
1399 | + |
1400 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1401 | +def test_load_presentation(mocked_uno): |
1402 | + """ |
1403 | + Test the load_presentation() method |
1404 | + """ |
1405 | + # GIVEN: A LibreOfficeServer object |
1406 | + presentation_file = '/path/to/presentation.odp' |
1407 | + screen_number = 1 |
1408 | + server = LibreOfficeServer() |
1409 | + mocked_desktop = MagicMock() |
1410 | + mocked_document = MagicMock() |
1411 | + mocked_presentation = MagicMock() |
1412 | + mocked_uno.systemPathToFileUrl.side_effect = lambda x: x |
1413 | + server._desktop = mocked_desktop |
1414 | + mocked_desktop.loadComponentFromURL.return_value = mocked_document |
1415 | + mocked_document.getPresentation.return_value = mocked_presentation |
1416 | + |
1417 | + # WHEN: load_presentation() is called |
1418 | + with patch.object(server, '_create_property') as mocked_create_property: |
1419 | + mocked_create_property.side_effect = lambda x, y: {x: y} |
1420 | + result = server.load_presentation(presentation_file, screen_number) |
1421 | + |
1422 | + # THEN: A presentation is loaded |
1423 | + assert result is True |
1424 | + mocked_uno.systemPathToFileUrl.assert_called_once_with(presentation_file) |
1425 | + mocked_create_property.assert_called_once_with('Hidden', True) |
1426 | + mocked_desktop.loadComponentFromURL.assert_called_once_with( |
1427 | + presentation_file, '_blank', 0, ({'Hidden': True},)) |
1428 | + assert server._document is mocked_document |
1429 | + mocked_document.getPresentation.assert_called_once_with() |
1430 | + assert server._presentation is mocked_presentation |
1431 | + assert server._presentation.Display == screen_number |
1432 | + assert server._control is None |
1433 | + |
1434 | + |
1435 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1436 | +def test_extract_thumbnails_no_pages(mocked_uno): |
1437 | + """ |
1438 | + Test the extract_thumbnails() method when there are no pages |
1439 | + """ |
1440 | + # GIVEN: A LibreOfficeServer instance |
1441 | + temp_folder = '/tmp' |
1442 | + server = LibreOfficeServer() |
1443 | + mocked_document = MagicMock() |
1444 | + server._document = mocked_document |
1445 | + mocked_uno.systemPathToFileUrl.side_effect = lambda x: x |
1446 | + mocked_document.getDrawPages.return_value = None |
1447 | + |
1448 | + # WHEN: The extract_thumbnails() method is called |
1449 | + with patch.object(server, '_create_property') as mocked_create_property: |
1450 | + mocked_create_property.side_effect = lambda x, y: {x: y} |
1451 | + thumbnails = server.extract_thumbnails(temp_folder) |
1452 | + |
1453 | + # THEN: Thumbnails have been extracted |
1454 | + mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder) |
1455 | + mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export') |
1456 | + mocked_document.getDrawPages.assert_called_once_with() |
1457 | + assert thumbnails == [] |
1458 | + |
1459 | + |
1460 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1461 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.os') |
1462 | +def test_extract_thumbnails(mocked_os, mocked_uno): |
1463 | + """ |
1464 | + Test the extract_thumbnails() method |
1465 | + """ |
1466 | + # GIVEN: A LibreOfficeServer instance |
1467 | + temp_folder = '/tmp' |
1468 | + server = LibreOfficeServer() |
1469 | + mocked_document = MagicMock() |
1470 | + mocked_pages = MagicMock() |
1471 | + mocked_page_1 = MagicMock() |
1472 | + mocked_page_2 = MagicMock() |
1473 | + mocked_controller = MagicMock() |
1474 | + server._document = mocked_document |
1475 | + mocked_uno.systemPathToFileUrl.side_effect = lambda x: x |
1476 | + mocked_document.getDrawPages.return_value = mocked_pages |
1477 | + mocked_os.path.isdir.return_value = False |
1478 | + mocked_pages.getCount.return_value = 2 |
1479 | + mocked_pages.getByIndex.side_effect = [mocked_page_1, mocked_page_2] |
1480 | + mocked_document.getCurrentController.return_value = mocked_controller |
1481 | + mocked_os.path.join.side_effect = lambda *x: '/'.join(x) |
1482 | + |
1483 | + # WHEN: The extract_thumbnails() method is called |
1484 | + with patch.object(server, '_create_property') as mocked_create_property: |
1485 | + mocked_create_property.side_effect = lambda x, y: {x: y} |
1486 | + thumbnails = server.extract_thumbnails(temp_folder) |
1487 | + |
1488 | + # THEN: Thumbnails have been extracted |
1489 | + mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder) |
1490 | + mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export') |
1491 | + mocked_document.getDrawPages.assert_called_once_with() |
1492 | + mocked_pages.getCount.assert_called_once_with() |
1493 | + assert mocked_pages.getByIndex.call_args_list == [call(0), call(1)] |
1494 | + assert mocked_controller.setCurrentPage.call_args_list == \ |
1495 | + [call(mocked_page_1), call(mocked_page_2)] |
1496 | + assert mocked_document.storeToURL.call_args_list == \ |
1497 | + [call('/tmp/1.png', ({'FilterName': 'impress_png_Export'},)), |
1498 | + call('/tmp/2.png', ({'FilterName': 'impress_png_Export'},))] |
1499 | + assert thumbnails == ['/tmp/1.png', '/tmp/2.png'] |
1500 | + |
1501 | + |
1502 | +def test_get_titles_and_notes(): |
1503 | + """ |
1504 | + Test the get_titles_and_notes() method |
1505 | + """ |
1506 | + # GIVEN: A LibreOfficeServer object and a bunch of mocks |
1507 | + server = LibreOfficeServer() |
1508 | + mocked_document = MagicMock() |
1509 | + mocked_pages = MagicMock() |
1510 | + server._document = mocked_document |
1511 | + mocked_document.getDrawPages.return_value = mocked_pages |
1512 | + mocked_pages.getCount.return_value = 2 |
1513 | + |
1514 | + # WHEN: get_titles_and_notes() is called |
1515 | + with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page: |
1516 | + mocked_get_text_from_page.side_effect = [ |
1517 | + 'OpenLP on Mac OS X', |
1518 | + '', |
1519 | + '', |
1520 | + 'Installing is a drag-and-drop affair' |
1521 | + ] |
1522 | + titles, notes = server.get_titles_and_notes() |
1523 | + |
1524 | + # THEN: The right calls are made and the right stuff returned |
1525 | + mocked_document.getDrawPages.assert_called_once_with() |
1526 | + mocked_pages.getCount.assert_called_once_with() |
1527 | + assert mocked_get_text_from_page.call_count == 4 |
1528 | + expected_calls = [ |
1529 | + call(1, TextType.Title), call(1, TextType.Notes), |
1530 | + call(2, TextType.Title), call(2, TextType.Notes), |
1531 | + ] |
1532 | + assert mocked_get_text_from_page.call_args_list == expected_calls |
1533 | + assert titles == ['OpenLP on Mac OS X\n', '\n'], titles |
1534 | + assert notes == [' ', 'Installing is a drag-and-drop affair'], notes |
1535 | + |
1536 | + |
1537 | +def test_close_presentation(): |
1538 | + """ |
1539 | + Test that closing the presentation cleans things up correctly |
1540 | + """ |
1541 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1542 | + server = LibreOfficeServer() |
1543 | + mocked_document = MagicMock() |
1544 | + mocked_presentation = MagicMock() |
1545 | + server._document = mocked_document |
1546 | + server._presentation = mocked_presentation |
1547 | + |
1548 | + # WHEN: close_presentation() is called |
1549 | + server.close_presentation() |
1550 | + |
1551 | + # THEN: The presentation and document should be closed |
1552 | + mocked_presentation.end.assert_called_once_with() |
1553 | + mocked_document.dispose.assert_called_once_with() |
1554 | + assert server._document is None |
1555 | + assert server._presentation is None |
1556 | + |
1557 | + |
1558 | +def test_is_loaded_no_objects(): |
1559 | + """ |
1560 | + Test the is_loaded() method when there's no document or presentation |
1561 | + """ |
1562 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1563 | + server = LibreOfficeServer() |
1564 | + |
1565 | + # WHEN: The is_loaded() method is called |
1566 | + result = server.is_loaded() |
1567 | + |
1568 | + # THEN: The result should be false |
1569 | + assert result is False |
1570 | + |
1571 | + |
1572 | +def test_is_loaded_no_presentation(): |
1573 | + """ |
1574 | + Test the is_loaded() method when there's no presentation |
1575 | + """ |
1576 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1577 | + server = LibreOfficeServer() |
1578 | + mocked_document = MagicMock() |
1579 | + server._document = mocked_document |
1580 | + server._presentation = MagicMock() |
1581 | + mocked_document.getPresentation.return_value = None |
1582 | + |
1583 | + # WHEN: The is_loaded() method is called |
1584 | + result = server.is_loaded() |
1585 | + |
1586 | + # THEN: The result should be false |
1587 | + assert result is False |
1588 | + mocked_document.getPresentation.assert_called_once_with() |
1589 | + |
1590 | + |
1591 | +def test_is_loaded_exception(): |
1592 | + """ |
1593 | + Test the is_loaded() method when an exception is thrown |
1594 | + """ |
1595 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1596 | + server = LibreOfficeServer() |
1597 | + mocked_document = MagicMock() |
1598 | + server._document = mocked_document |
1599 | + server._presentation = MagicMock() |
1600 | + mocked_document.getPresentation.side_effect = Exception() |
1601 | + |
1602 | + # WHEN: The is_loaded() method is called |
1603 | + result = server.is_loaded() |
1604 | + |
1605 | + # THEN: The result should be false |
1606 | + assert result is False |
1607 | + mocked_document.getPresentation.assert_called_once_with() |
1608 | + |
1609 | + |
1610 | +def test_is_loaded(): |
1611 | + """ |
1612 | + Test the is_loaded() method |
1613 | + """ |
1614 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1615 | + server = LibreOfficeServer() |
1616 | + mocked_document = MagicMock() |
1617 | + mocked_presentation = MagicMock() |
1618 | + server._document = mocked_document |
1619 | + server._presentation = mocked_presentation |
1620 | + mocked_document.getPresentation.return_value = mocked_presentation |
1621 | + |
1622 | + # WHEN: The is_loaded() method is called |
1623 | + result = server.is_loaded() |
1624 | + |
1625 | + # THEN: The result should be false |
1626 | + assert result is True |
1627 | + mocked_document.getPresentation.assert_called_once_with() |
1628 | + |
1629 | + |
1630 | +def test_is_active_not_loaded(): |
1631 | + """ |
1632 | + Test is_active() when is_loaded() returns False |
1633 | + """ |
1634 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1635 | + server = LibreOfficeServer() |
1636 | + |
1637 | + # WHEN: is_active() is called with is_loaded() returns False |
1638 | + with patch.object(server, 'is_loaded') as mocked_is_loaded: |
1639 | + mocked_is_loaded.return_value = False |
1640 | + result = server.is_active() |
1641 | + |
1642 | + # THEN: It should have returned False |
1643 | + assert result is False |
1644 | + |
1645 | + |
1646 | +def test_is_active_no_control(): |
1647 | + """ |
1648 | + Test is_active() when is_loaded() returns True but there's no control |
1649 | + """ |
1650 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1651 | + server = LibreOfficeServer() |
1652 | + |
1653 | + # WHEN: is_active() is called with is_loaded() returns False |
1654 | + with patch.object(server, 'is_loaded') as mocked_is_loaded: |
1655 | + mocked_is_loaded.return_value = True |
1656 | + result = server.is_active() |
1657 | + |
1658 | + # THEN: The result should be False |
1659 | + assert result is False |
1660 | + mocked_is_loaded.assert_called_once_with() |
1661 | + |
1662 | + |
1663 | +def test_is_active(): |
1664 | + """ |
1665 | + Test is_active() |
1666 | + """ |
1667 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1668 | + server = LibreOfficeServer() |
1669 | + mocked_control = MagicMock() |
1670 | + server._control = mocked_control |
1671 | + mocked_control.isRunning.return_value = True |
1672 | + |
1673 | + # WHEN: is_active() is called with is_loaded() returns False |
1674 | + with patch.object(server, 'is_loaded') as mocked_is_loaded: |
1675 | + mocked_is_loaded.return_value = True |
1676 | + result = server.is_active() |
1677 | + |
1678 | + # THEN: The result should be False |
1679 | + assert result is True |
1680 | + mocked_is_loaded.assert_called_once_with() |
1681 | + mocked_control.isRunning.assert_called_once_with() |
1682 | + |
1683 | + |
1684 | +def test_unblank_screen(): |
1685 | + """ |
1686 | + Test the unblank_screen() method |
1687 | + """ |
1688 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1689 | + server = LibreOfficeServer() |
1690 | + mocked_control = MagicMock() |
1691 | + server._control = mocked_control |
1692 | + |
1693 | + # WHEN: unblank_screen() is run |
1694 | + server.unblank_screen() |
1695 | + |
1696 | + # THEN: The resume method should have been called |
1697 | + mocked_control.resume.assert_called_once_with() |
1698 | + |
1699 | + |
1700 | +def test_blank_screen(): |
1701 | + """ |
1702 | + Test the blank_screen() method |
1703 | + """ |
1704 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1705 | + server = LibreOfficeServer() |
1706 | + mocked_control = MagicMock() |
1707 | + server._control = mocked_control |
1708 | + |
1709 | + # WHEN: blank_screen() is run |
1710 | + server.blank_screen() |
1711 | + |
1712 | + # THEN: The resume method should have been called |
1713 | + mocked_control.blankScreen.assert_called_once_with(0) |
1714 | + |
1715 | + |
1716 | +def test_is_blank_no_control(): |
1717 | + """ |
1718 | + Test the is_blank() method when there's no control |
1719 | + """ |
1720 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1721 | + server = LibreOfficeServer() |
1722 | + |
1723 | + # WHEN: is_blank() is called |
1724 | + result = server.is_blank() |
1725 | + |
1726 | + # THEN: It should have returned False |
1727 | + assert result is False |
1728 | + |
1729 | + |
1730 | +def test_is_blank_control_is_running(): |
1731 | + """ |
1732 | + Test the is_blank() method when the control is running |
1733 | + """ |
1734 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1735 | + server = LibreOfficeServer() |
1736 | + mocked_control = MagicMock() |
1737 | + server._control = mocked_control |
1738 | + mocked_control.isRunning.return_value = True |
1739 | + mocked_control.isPaused.return_value = True |
1740 | + |
1741 | + # WHEN: is_blank() is called |
1742 | + result = server.is_blank() |
1743 | + |
1744 | + # THEN: It should have returned False |
1745 | + assert result is True |
1746 | + mocked_control.isRunning.assert_called_once_with() |
1747 | + mocked_control.isPaused.assert_called_once_with() |
1748 | + |
1749 | + |
1750 | +def test_stop_presentation(): |
1751 | + """ |
1752 | + Test the stop_presentation() method |
1753 | + """ |
1754 | + # GIVEN: A LibreOfficeServer instance and a mocked presentation |
1755 | + server = LibreOfficeServer() |
1756 | + mocked_presentation = MagicMock() |
1757 | + mocked_control = MagicMock() |
1758 | + server._presentation = mocked_presentation |
1759 | + server._control = mocked_control |
1760 | + |
1761 | + # WHEN: stop_presentation() is called |
1762 | + server.stop_presentation() |
1763 | + |
1764 | + # THEN: The presentation is ended and the control is removed |
1765 | + mocked_presentation.end.assert_called_once_with() |
1766 | + assert server._control is None |
1767 | + |
1768 | + |
1769 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep') |
1770 | +def test_start_presentation_no_control(mocked_sleep): |
1771 | + """ |
1772 | + Test the start_presentation() method when there's no control |
1773 | + """ |
1774 | + # GIVEN: A LibreOfficeServer instance and some mocks |
1775 | + server = LibreOfficeServer() |
1776 | + mocked_control = MagicMock() |
1777 | + mocked_document = MagicMock() |
1778 | + mocked_presentation = MagicMock() |
1779 | + mocked_controller = MagicMock() |
1780 | + mocked_frame = MagicMock() |
1781 | + mocked_window = MagicMock() |
1782 | + server._document = mocked_document |
1783 | + server._presentation = mocked_presentation |
1784 | + mocked_document.getCurrentController.return_value = mocked_controller |
1785 | + mocked_controller.getFrame.return_value = mocked_frame |
1786 | + mocked_frame.getContainerWindow.return_value = mocked_window |
1787 | + mocked_presentation.getController.side_effect = [None, mocked_control] |
1788 | + |
1789 | + # WHEN: start_presentation() is called |
1790 | + server.start_presentation() |
1791 | + |
1792 | + # THEN: The slide number should be correct |
1793 | + mocked_document.getCurrentController.assert_called_once_with() |
1794 | + mocked_controller.getFrame.assert_called_once_with() |
1795 | + mocked_frame.getContainerWindow.assert_called_once_with() |
1796 | + mocked_presentation.start.assert_called_once_with() |
1797 | + assert mocked_presentation.getController.call_count == 2 |
1798 | + mocked_sleep.assert_called_once_with(0.1) |
1799 | + assert mocked_window.setVisible.call_args_list == [call(True), call(False)] |
1800 | + assert server._control is mocked_control |
1801 | + |
1802 | + |
1803 | +def test_start_presentation(): |
1804 | + """ |
1805 | + Test the start_presentation() method when there's a control |
1806 | + """ |
1807 | + # GIVEN: A LibreOfficeServer instance and some mocks |
1808 | + server = LibreOfficeServer() |
1809 | + mocked_control = MagicMock() |
1810 | + server._control = mocked_control |
1811 | + |
1812 | + # WHEN: start_presentation() is called |
1813 | + with patch.object(server, 'goto_slide') as mocked_goto_slide: |
1814 | + server.start_presentation() |
1815 | + |
1816 | + # THEN: The control should have been activated and the first slide selected |
1817 | + mocked_control.activate.assert_called_once_with() |
1818 | + mocked_goto_slide.assert_called_once_with(1) |
1819 | + |
1820 | + |
1821 | +def test_get_slide_number(): |
1822 | + """ |
1823 | + Test the get_slide_number() method |
1824 | + """ |
1825 | + # GIVEN: A LibreOfficeServer instance and some mocks |
1826 | + server = LibreOfficeServer() |
1827 | + mocked_control = MagicMock() |
1828 | + mocked_control.getCurrentSlideIndex.return_value = 3 |
1829 | + server._control = mocked_control |
1830 | + |
1831 | + # WHEN: get_slide_number() is called |
1832 | + result = server.get_slide_number() |
1833 | + |
1834 | + # THEN: The slide number should be correct |
1835 | + assert result == 4 |
1836 | + |
1837 | + |
1838 | +def test_get_slide_count(): |
1839 | + """ |
1840 | + Test the get_slide_count() method |
1841 | + """ |
1842 | + # GIVEN: A LibreOfficeServer instance and some mocks |
1843 | + server = LibreOfficeServer() |
1844 | + mocked_document = MagicMock() |
1845 | + mocked_pages = MagicMock() |
1846 | + server._document = mocked_document |
1847 | + mocked_document.getDrawPages.return_value = mocked_pages |
1848 | + mocked_pages.getCount.return_value = 2 |
1849 | + |
1850 | + # WHEN: get_slide_count() is called |
1851 | + result = server.get_slide_count() |
1852 | + |
1853 | + # THEN: The slide count should be correct |
1854 | + assert result == 2 |
1855 | + |
1856 | + |
1857 | +def test_goto_slide(): |
1858 | + """ |
1859 | + Test the goto_slide() method |
1860 | + """ |
1861 | + # GIVEN: A LibreOfficeServer instance and some mocks |
1862 | + server = LibreOfficeServer() |
1863 | + mocked_control = MagicMock() |
1864 | + server._control = mocked_control |
1865 | + |
1866 | + # WHEN: goto_slide() is called |
1867 | + server.goto_slide(1) |
1868 | + |
1869 | + # THEN: The slide number should be correct |
1870 | + mocked_control.gotoSlideIndex.assert_called_once_with(0) |
1871 | + |
1872 | + |
1873 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep') |
1874 | +def test_next_step_when_paused(mocked_sleep): |
1875 | + """ |
1876 | + Test the next_step() method when paused |
1877 | + """ |
1878 | + # GIVEN: A LibreOfficeServer instance and a mocked control |
1879 | + server = LibreOfficeServer() |
1880 | + mocked_control = MagicMock() |
1881 | + server._control = mocked_control |
1882 | + mocked_control.isPaused.side_effect = [False, True] |
1883 | + |
1884 | + # WHEN: next_step() is called |
1885 | + server.next_step() |
1886 | + |
1887 | + # THEN: The correct call should be made |
1888 | + mocked_control.gotoNextEffect.assert_called_once_with() |
1889 | + mocked_sleep.assert_called_once_with(0.1) |
1890 | + assert mocked_control.isPaused.call_count == 2 |
1891 | + mocked_control.gotoPreviousEffect.assert_called_once_with() |
1892 | + |
1893 | + |
1894 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep') |
1895 | +def test_next_step(mocked_sleep): |
1896 | + """ |
1897 | + Test the next_step() method when paused |
1898 | + """ |
1899 | + # GIVEN: A LibreOfficeServer instance and a mocked control |
1900 | + server = LibreOfficeServer() |
1901 | + mocked_control = MagicMock() |
1902 | + server._control = mocked_control |
1903 | + mocked_control.isPaused.side_effect = [True, True] |
1904 | + |
1905 | + # WHEN: next_step() is called |
1906 | + server.next_step() |
1907 | + |
1908 | + # THEN: The correct call should be made |
1909 | + mocked_control.gotoNextEffect.assert_called_once_with() |
1910 | + mocked_sleep.assert_called_once_with(0.1) |
1911 | + assert mocked_control.isPaused.call_count == 1 |
1912 | + assert mocked_control.gotoPreviousEffect.call_count == 0 |
1913 | + |
1914 | + |
1915 | +def test_previous_step(): |
1916 | + """ |
1917 | + Test the previous_step() method |
1918 | + """ |
1919 | + # GIVEN: A LibreOfficeServer instance and a mocked control |
1920 | + server = LibreOfficeServer() |
1921 | + mocked_control = MagicMock() |
1922 | + server._control = mocked_control |
1923 | + |
1924 | + # WHEN: previous_step() is called |
1925 | + server.previous_step() |
1926 | + |
1927 | + # THEN: The correct call should be made |
1928 | + mocked_control.gotoPreviousEffect.assert_called_once_with() |
1929 | + |
1930 | + |
1931 | +def test_get_slide_text(): |
1932 | + """ |
1933 | + Test the get_slide_text() method |
1934 | + """ |
1935 | + # GIVEN: A LibreOfficeServer instance |
1936 | + server = LibreOfficeServer() |
1937 | + |
1938 | + # WHEN: get_slide_text() is called for a particular slide |
1939 | + with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page: |
1940 | + mocked_get_text_from_page.return_value = 'OpenLP on Mac OS X' |
1941 | + result = server.get_slide_text(5) |
1942 | + |
1943 | + # THEN: The text should be returned |
1944 | + mocked_get_text_from_page.assert_called_once_with(5) |
1945 | + assert result == 'OpenLP on Mac OS X' |
1946 | + |
1947 | + |
1948 | +def test_get_slide_notes(): |
1949 | + """ |
1950 | + Test the get_slide_notes() method |
1951 | + """ |
1952 | + # GIVEN: A LibreOfficeServer instance |
1953 | + server = LibreOfficeServer() |
1954 | + |
1955 | + # WHEN: get_slide_notes() is called for a particular slide |
1956 | + with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page: |
1957 | + mocked_get_text_from_page.return_value = 'Installing is a drag-and-drop affair' |
1958 | + result = server.get_slide_notes(3) |
1959 | + |
1960 | + # THEN: The text should be returned |
1961 | + mocked_get_text_from_page.assert_called_once_with(3, TextType.Notes) |
1962 | + assert result == 'Installing is a drag-and-drop affair' |
1963 | + |
1964 | + |
1965 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.Daemon') |
1966 | +def test_main(MockedDaemon): |
1967 | + """ |
1968 | + Test the main() function |
1969 | + """ |
1970 | + # GIVEN: Mocked out Pyro objects |
1971 | + mocked_daemon = MagicMock() |
1972 | + MockedDaemon.return_value = mocked_daemon |
1973 | + |
1974 | + # WHEN: main() is run |
1975 | + main() |
1976 | + |
1977 | + # THEN: The correct calls are made |
1978 | + MockedDaemon.assert_called_once_with(host='localhost', port=4310) |
1979 | + mocked_daemon.register.assert_called_once_with(LibreOfficeServer, 'openlp.libreofficeserver') |
1980 | + mocked_daemon.requestLoop.assert_called_once_with() |
1981 | + mocked_daemon.close.assert_called_once_with() |
1982 | |
1983 | === added file 'tests/functional/openlp_plugins/presentations/test_maclocontroller.py' |
1984 | --- tests/functional/openlp_plugins/presentations/test_maclocontroller.py 1970-01-01 00:00:00 +0000 |
1985 | +++ tests/functional/openlp_plugins/presentations/test_maclocontroller.py 2019-06-05 05:00:52 +0000 |
1986 | @@ -0,0 +1,453 @@ |
1987 | +# -*- coding: utf-8 -*- |
1988 | +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 |
1989 | + |
1990 | +########################################################################## |
1991 | +# OpenLP - Open Source Lyrics Projection # |
1992 | +# ---------------------------------------------------------------------- # |
1993 | +# Copyright (c) 2008-2019 OpenLP Developers # |
1994 | +# ---------------------------------------------------------------------- # |
1995 | +# This program is free software: you can redistribute it and/or modify # |
1996 | +# it under the terms of the GNU General Public License as published by # |
1997 | +# the Free Software Foundation, either version 3 of the License, or # |
1998 | +# (at your option) any later version. # |
1999 | +# # |
2000 | +# This program is distributed in the hope that it will be useful, # |
2001 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of # |
2002 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # |
2003 | +# GNU General Public License for more details. # |
2004 | +# # |
2005 | +# You should have received a copy of the GNU General Public License # |
2006 | +# along with this program. If not, see <https://www.gnu.org/licenses/>. # |
2007 | +########################################################################## |
2008 | +""" |
2009 | +Functional tests to test the Mac LibreOffice class and related methods. |
2010 | +""" |
2011 | +import shutil |
2012 | +from tempfile import mkdtemp |
2013 | +from unittest import TestCase |
2014 | +from unittest.mock import MagicMock, patch, call |
2015 | + |
2016 | +from openlp.core.common.settings import Settings |
2017 | +from openlp.core.common.path import Path |
2018 | +from openlp.plugins.presentations.lib.maclocontroller import MacLOController, MacLODocument |
2019 | +from openlp.plugins.presentations.presentationplugin import __default_settings__ |
2020 | + |
2021 | +from tests.helpers.testmixin import TestMixin |
2022 | +from tests.utils.constants import TEST_RESOURCES_PATH |
2023 | + |
2024 | + |
2025 | +class TestMacLOController(TestCase, TestMixin): |
2026 | + """ |
2027 | + Test the MacLOController Class |
2028 | + """ |
2029 | + |
2030 | + def setUp(self): |
2031 | + """ |
2032 | + Set up the patches and mocks need for all tests. |
2033 | + """ |
2034 | + self.setup_application() |
2035 | + self.build_settings() |
2036 | + self.mock_plugin = MagicMock() |
2037 | + self.temp_folder = mkdtemp() |
2038 | + self.mock_plugin.settings_section = self.temp_folder |
2039 | + |
2040 | + def tearDown(self): |
2041 | + """ |
2042 | + Stop the patches |
2043 | + """ |
2044 | + self.destroy_settings() |
2045 | + shutil.rmtree(self.temp_folder) |
2046 | + |
2047 | + @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') |
2048 | + def test_constructor(self, mocked_start_server): |
2049 | + """ |
2050 | + Test the Constructor from the MacLOController |
2051 | + """ |
2052 | + # GIVEN: No presentation controller |
2053 | + controller = None |
2054 | + |
2055 | + # WHEN: The presentation controller object is created |
2056 | + controller = MacLOController(plugin=self.mock_plugin) |
2057 | + |
2058 | + # THEN: The name of the presentation controller should be correct |
2059 | + assert controller.name == 'maclo', \ |
2060 | + 'The name of the presentation controller should be correct' |
2061 | + assert controller.display_name == 'Impress on macOS', \ |
2062 | + 'The display name of the presentation controller should be correct' |
2063 | + mocked_start_server.assert_called_once_with() |
2064 | + |
2065 | + @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') |
2066 | + @patch('openlp.plugins.presentations.lib.maclocontroller.Proxy') |
2067 | + def test_client(self, MockedProxy, mocked_start_server): |
2068 | + """ |
2069 | + Test the client property of the Controller |
2070 | + """ |
2071 | + # GIVEN: A controller without a client and a mocked out Pyro |
2072 | + controller = MacLOController(plugin=self.mock_plugin) |
2073 | + mocked_client = MagicMock() |
2074 | + MockedProxy.return_value = mocked_client |
2075 | + mocked_client._pyroConnection = None |
2076 | + |
2077 | + # WHEN: the client property is called the first time |
2078 | + client = controller.client |
2079 | + |
2080 | + # THEN: a client is created |
2081 | + assert client == mocked_client |
2082 | + MockedProxy.assert_called_once_with('PYRO:openlp.libreofficeserver@localhost:4310') |
2083 | + mocked_client._pyroReconnect.assert_called_once_with() |
2084 | + |
2085 | + @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') |
2086 | + def test_check_available(self, mocked_start_server): |
2087 | + """ |
2088 | + Test the check_available() method |
2089 | + """ |
2090 | + from openlp.plugins.presentations.lib.maclocontroller import macuno_available |
2091 | + |
2092 | + # GIVEN: A controller |
2093 | + controller = MacLOController(plugin=self.mock_plugin) |
2094 | + |
2095 | + # WHEN: check_available() is run |
2096 | + result = controller.check_available() |
2097 | + |
2098 | + # THEN: it should return false |
2099 | + assert result == macuno_available |
2100 | + |
2101 | + @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') |
2102 | + def test_start_process(self, mocked_start_server): |
2103 | + """ |
2104 | + Test the start_process() method |
2105 | + """ |
2106 | + # GIVEN: A controller and a client |
2107 | + controller = MacLOController(plugin=self.mock_plugin) |
2108 | + controller._client = MagicMock() |
2109 | + |
2110 | + # WHEN: start_process() is called |
2111 | + controller.start_process() |
2112 | + |
2113 | + # THEN: The client's start_process() should have been called |
2114 | + controller._client.start_process.assert_called_once_with() |
2115 | + |
2116 | + @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') |
2117 | + def test_kill(self, mocked_start_server): |
2118 | + """ |
2119 | + Test the kill() method |
2120 | + """ |
2121 | + # GIVEN: A controller and a client |
2122 | + controller = MacLOController(plugin=self.mock_plugin) |
2123 | + controller._client = MagicMock() |
2124 | + controller.server_process = MagicMock() |
2125 | + |
2126 | + # WHEN: start_process() is called |
2127 | + controller.kill() |
2128 | + |
2129 | + # THEN: The client's start_process() should have been called |
2130 | + controller._client.shutdown.assert_called_once_with() |
2131 | + controller.server_process.kill.assert_called_once_with() |
2132 | + |
2133 | + |
2134 | +class TestMacLODocument(TestCase): |
2135 | + """ |
2136 | + Test the MacLODocument Class |
2137 | + """ |
2138 | + def setUp(self): |
2139 | + mocked_plugin = MagicMock() |
2140 | + mocked_plugin.settings_section = 'presentations' |
2141 | + Settings().extend_default_settings(__default_settings__) |
2142 | + self.file_name = Path(TEST_RESOURCES_PATH) / 'presentations' / 'test.odp' |
2143 | + self.mocked_client = MagicMock() |
2144 | + with patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server'): |
2145 | + self.controller = MacLOController(mocked_plugin) |
2146 | + self.controller._client = self.mocked_client |
2147 | + self.document = MacLODocument(self.controller, self.file_name) |
2148 | + |
2149 | + @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList') |
2150 | + def test_load_presentation_cannot_load(self, MockedScreenList): |
2151 | + """ |
2152 | + Test the load_presentation() method when the server can't load the presentation |
2153 | + """ |
2154 | + # GIVEN: A document and a mocked client |
2155 | + mocked_screen_list = MagicMock() |
2156 | + MockedScreenList.return_value = mocked_screen_list |
2157 | + mocked_screen_list.current.number = 0 |
2158 | + self.mocked_client.load_presentation.return_value = False |
2159 | + |
2160 | + # WHEN: load_presentation() is called |
2161 | + result = self.document.load_presentation() |
2162 | + |
2163 | + # THEN: Stuff should work right |
2164 | + self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1) |
2165 | + assert result is False |
2166 | + |
2167 | + @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList') |
2168 | + def test_load_presentation(self, MockedScreenList): |
2169 | + """ |
2170 | + Test the load_presentation() method |
2171 | + """ |
2172 | + # GIVEN: A document and a mocked client |
2173 | + mocked_screen_list = MagicMock() |
2174 | + MockedScreenList.return_value = mocked_screen_list |
2175 | + mocked_screen_list.current.number = 0 |
2176 | + self.mocked_client.load_presentation.return_value = True |
2177 | + |
2178 | + # WHEN: load_presentation() is called |
2179 | + with patch.object(self.document, 'create_thumbnails') as mocked_create_thumbnails, \ |
2180 | + patch.object(self.document, 'create_titles_and_notes') as mocked_create_titles_and_notes: |
2181 | + result = self.document.load_presentation() |
2182 | + |
2183 | + # THEN: Stuff should work right |
2184 | + self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1) |
2185 | + mocked_create_thumbnails.assert_called_once_with() |
2186 | + mocked_create_titles_and_notes.assert_called_once_with() |
2187 | + assert result is True |
2188 | + |
2189 | + def test_create_thumbnails_already_exist(self): |
2190 | + """ |
2191 | + Test the create_thumbnails() method when thumbnails already exist |
2192 | + """ |
2193 | + # GIVEN: thumbnails that exist and a mocked client |
2194 | + self.document.check_thumbnails = MagicMock(return_value=True) |
2195 | + |
2196 | + # WHEN: create_thumbnails() is called |
2197 | + self.document.create_thumbnails() |
2198 | + |
2199 | + # THEN: The method should exit early |
2200 | + assert self.mocked_client.extract_thumbnails.call_count == 0 |
2201 | + |
2202 | + @patch('openlp.plugins.presentations.lib.maclocontroller.delete_file') |
2203 | + def test_create_thumbnails(self, mocked_delete_file): |
2204 | + """ |
2205 | + Test the create_thumbnails() method |
2206 | + """ |
2207 | + # GIVEN: thumbnails that don't exist and a mocked client |
2208 | + self.document.check_thumbnails = MagicMock(return_value=False) |
2209 | + self.mocked_client.extract_thumbnails.return_value = ['thumb1.png', 'thumb2.png'] |
2210 | + |
2211 | + # WHEN: create_thumbnails() is called |
2212 | + with patch.object(self.document, 'convert_thumbnail') as mocked_convert_thumbnail, \ |
2213 | + patch.object(self.document, 'get_temp_folder') as mocked_get_temp_folder: |
2214 | + mocked_get_temp_folder.return_value = 'temp' |
2215 | + self.document.create_thumbnails() |
2216 | + |
2217 | + # THEN: The method should complete successfully |
2218 | + self.mocked_client.extract_thumbnails.assert_called_once_with('temp') |
2219 | + assert mocked_convert_thumbnail.call_args_list == [ |
2220 | + call(Path('thumb1.png'), 1), call(Path('thumb2.png'), 2)] |
2221 | + assert mocked_delete_file.call_args_list == [call(Path('thumb1.png')), call(Path('thumb2.png'))] |
2222 | + |
2223 | + def test_create_titles_and_notes(self): |
2224 | + """ |
2225 | + Test create_titles_and_notes() method |
2226 | + """ |
2227 | + # GIVEN: mocked client and mocked save_titles_and_notes() method |
2228 | + self.mocked_client.get_titles_and_notes.return_value = ('OpenLP', 'This is a note') |
2229 | + |
2230 | + # WHEN: create_titles_and_notes() is called |
2231 | + with patch.object(self.document, 'save_titles_and_notes') as mocked_save_titles_and_notes: |
2232 | + self.document.create_titles_and_notes() |
2233 | + |
2234 | + # THEN save_titles_and_notes should have been called |
2235 | + self.mocked_client.get_titles_and_notes.assert_called_once_with() |
2236 | + mocked_save_titles_and_notes.assert_called_once_with('OpenLP', 'This is a note') |
2237 | + |
2238 | + def test_close_presentation(self): |
2239 | + """ |
2240 | + Test the close_presentation() method |
2241 | + """ |
2242 | + # GIVEN: A mocked client and mocked remove_doc() method |
2243 | + # WHEN: close_presentation() is called |
2244 | + with patch.object(self.controller, 'remove_doc') as mocked_remove_doc: |
2245 | + self.document.close_presentation() |
2246 | + |
2247 | + # THEN: The presentation should have been closed |
2248 | + self.mocked_client.close_presentation.assert_called_once_with() |
2249 | + mocked_remove_doc.assert_called_once_with(self.document) |
2250 | + |
2251 | + def test_is_loaded(self): |
2252 | + """ |
2253 | + Test the is_loaded() method |
2254 | + """ |
2255 | + # GIVEN: A mocked client |
2256 | + self.mocked_client.is_loaded.return_value = True |
2257 | + |
2258 | + # WHEN: is_loaded() is called |
2259 | + result = self.document.is_loaded() |
2260 | + |
2261 | + # THEN: Then the result should be correct |
2262 | + assert result is True |
2263 | + |
2264 | + def test_is_active(self): |
2265 | + """ |
2266 | + Test the is_active() method |
2267 | + """ |
2268 | + # GIVEN: A mocked client |
2269 | + self.mocked_client.is_active.return_value = True |
2270 | + |
2271 | + # WHEN: is_active() is called |
2272 | + result = self.document.is_active() |
2273 | + |
2274 | + # THEN: Then the result should be correct |
2275 | + assert result is True |
2276 | + |
2277 | + def test_unblank_screen(self): |
2278 | + """ |
2279 | + Test the unblank_screen() method |
2280 | + """ |
2281 | + # GIVEN: A mocked client |
2282 | + self.mocked_client.unblank_screen.return_value = True |
2283 | + |
2284 | + # WHEN: unblank_screen() is called |
2285 | + result = self.document.unblank_screen() |
2286 | + |
2287 | + # THEN: Then the result should be correct |
2288 | + self.mocked_client.unblank_screen.assert_called_once_with() |
2289 | + assert result is True |
2290 | + |
2291 | + def test_blank_screen(self): |
2292 | + """ |
2293 | + Test the blank_screen() method |
2294 | + """ |
2295 | + # GIVEN: A mocked client |
2296 | + self.mocked_client.blank_screen.return_value = True |
2297 | + |
2298 | + # WHEN: blank_screen() is called |
2299 | + self.document.blank_screen() |
2300 | + |
2301 | + # THEN: Then the result should be correct |
2302 | + self.mocked_client.blank_screen.assert_called_once_with() |
2303 | + |
2304 | + def test_is_blank(self): |
2305 | + """ |
2306 | + Test the is_blank() method |
2307 | + """ |
2308 | + # GIVEN: A mocked client |
2309 | + self.mocked_client.is_blank.return_value = True |
2310 | + |
2311 | + # WHEN: is_blank() is called |
2312 | + result = self.document.is_blank() |
2313 | + |
2314 | + # THEN: Then the result should be correct |
2315 | + assert result is True |
2316 | + |
2317 | + def test_stop_presentation(self): |
2318 | + """ |
2319 | + Test the stop_presentation() method |
2320 | + """ |
2321 | + # GIVEN: A mocked client |
2322 | + self.mocked_client.stop_presentation.return_value = True |
2323 | + |
2324 | + # WHEN: stop_presentation() is called |
2325 | + self.document.stop_presentation() |
2326 | + |
2327 | + # THEN: Then the result should be correct |
2328 | + self.mocked_client.stop_presentation.assert_called_once_with() |
2329 | + |
2330 | + @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList') |
2331 | + @patch('openlp.plugins.presentations.lib.maclocontroller.Registry') |
2332 | + def test_start_presentation(self, MockedRegistry, MockedScreenList): |
2333 | + """ |
2334 | + Test the start_presentation() method |
2335 | + """ |
2336 | + # GIVEN: a mocked client, and multiple screens |
2337 | + mocked_screen_list = MagicMock() |
2338 | + mocked_screen_list.__len__.return_value = 2 |
2339 | + mocked_registry = MagicMock() |
2340 | + mocked_main_window = MagicMock() |
2341 | + MockedScreenList.return_value = mocked_screen_list |
2342 | + MockedRegistry.return_value = mocked_registry |
2343 | + mocked_screen_list.screen_list = [0, 1] |
2344 | + mocked_registry.get.return_value = mocked_main_window |
2345 | + |
2346 | + # WHEN: start_presentation() is called |
2347 | + self.document.start_presentation() |
2348 | + |
2349 | + # THEN: The presentation should be started |
2350 | + self.mocked_client.start_presentation.assert_called_once_with() |
2351 | + mocked_registry.get.assert_called_once_with('main_window') |
2352 | + mocked_main_window.activateWindow.assert_called_once_with() |
2353 | + |
2354 | + def test_get_slide_number(self): |
2355 | + """ |
2356 | + Test the get_slide_number() method |
2357 | + """ |
2358 | + # GIVEN: A mocked client |
2359 | + self.mocked_client.get_slide_number.return_value = 5 |
2360 | + |
2361 | + # WHEN: get_slide_number() is called |
2362 | + result = self.document.get_slide_number() |
2363 | + |
2364 | + # THEN: Then the result should be correct |
2365 | + assert result == 5 |
2366 | + |
2367 | + def test_get_slide_count(self): |
2368 | + """ |
2369 | + Test the get_slide_count() method |
2370 | + """ |
2371 | + # GIVEN: A mocked client |
2372 | + self.mocked_client.get_slide_count.return_value = 8 |
2373 | + |
2374 | + # WHEN: get_slide_count() is called |
2375 | + result = self.document.get_slide_count() |
2376 | + |
2377 | + # THEN: Then the result should be correct |
2378 | + assert result == 8 |
2379 | + |
2380 | + def test_goto_slide(self): |
2381 | + """ |
2382 | + Test the goto_slide() method |
2383 | + """ |
2384 | + # GIVEN: A mocked client |
2385 | + # WHEN: goto_slide() is called |
2386 | + self.document.goto_slide(3) |
2387 | + |
2388 | + # THEN: Then the result should be correct |
2389 | + self.mocked_client.goto_slide.assert_called_once_with(3) |
2390 | + |
2391 | + def test_next_step(self): |
2392 | + """ |
2393 | + Test the next_step() method |
2394 | + """ |
2395 | + # GIVEN: A mocked client |
2396 | + # WHEN: next_step() is called |
2397 | + self.document.next_step() |
2398 | + |
2399 | + # THEN: Then the result should be correct |
2400 | + self.mocked_client.next_step.assert_called_once_with() |
2401 | + |
2402 | + def test_previous_step(self): |
2403 | + """ |
2404 | + Test the previous_step() method |
2405 | + """ |
2406 | + # GIVEN: A mocked client |
2407 | + # WHEN: previous_step() is called |
2408 | + self.document.previous_step() |
2409 | + |
2410 | + # THEN: Then the result should be correct |
2411 | + self.mocked_client.previous_step.assert_called_once_with() |
2412 | + |
2413 | + def test_get_slide_text(self): |
2414 | + """ |
2415 | + Test the get_slide_text() method |
2416 | + """ |
2417 | + # GIVEN: A mocked client |
2418 | + self.mocked_client.get_slide_text.return_value = 'Some slide text' |
2419 | + |
2420 | + # WHEN: get_slide_text() is called |
2421 | + result = self.document.get_slide_text(1) |
2422 | + |
2423 | + # THEN: Then the result should be correct |
2424 | + self.mocked_client.get_slide_text.assert_called_once_with(1) |
2425 | + assert result == 'Some slide text' |
2426 | + |
2427 | + def test_get_slide_notes(self): |
2428 | + """ |
2429 | + Test the get_slide_notes() method |
2430 | + """ |
2431 | + # GIVEN: A mocked client |
2432 | + self.mocked_client.get_slide_notes.return_value = 'This is a note' |
2433 | + |
2434 | + # WHEN: get_slide_notes() is called |
2435 | + result = self.document.get_slide_notes(2) |
2436 | + |
2437 | + # THEN: Then the result should be correct |
2438 | + self.mocked_client.get_slide_notes.assert_called_once_with(2) |
2439 | + assert result == 'This is a note' |
Looks fine and does not show up on my machine (Linux).
Needs a respin as will have a conflict with previous merge.