Merge lp:~raoul-snyman/openlp/pyro-impress into lp:openlp
- pyro-impress
- Merge into trunk
Status: | Superseded |
---|---|
Proposed branch: | lp:~raoul-snyman/openlp/pyro-impress |
Merge into: | lp:openlp |
Diff against target: |
2415 lines (+2199/-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 (+28/-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 | Pending | ||
Review via email: mp+367607@code.launchpad.net |
This proposal supersedes a proposal from 2019-05-18.
This proposal has been superseded by a proposal from 2019-05-22.
Commit message
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 : | # |
Linux tests failed, please see https:/
- 2732. By Raoul Snyman
-
Fix up the tests
- 2733. By Raoul Snyman
-
HEAD
- 2734. By Raoul Snyman
-
Fix license issues
Unmerged revisions
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2019-03-25 21:24:51 +0000 |
3 | +++ .bzrignore 2019-05-18 15:59:34 +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-04-13 13:00:22 +0000 |
94 | +++ openlp/core/common/path.py 2019-05-18 15:59:34 +0000 |
95 | @@ -187,6 +187,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-05-05 05:59:29 +0000 |
107 | +++ openlp/core/ui/media/mediacontroller.py 2019-05-18 15:59:34 +0000 |
108 | @@ -129,6 +129,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 | self._generate_extensions_lists() |
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-05-18 15:59:34 +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-2016 OpenLP Developers # |
129 | +# --------------------------------------------------------------------------- # |
130 | +# This program is free software; you can redistribute it and/or modify it # |
131 | +# under the terms of the GNU General Public License as published by the Free # |
132 | +# Software Foundation; version 2 of the License. # |
133 | +# # |
134 | +# This program is distributed in the hope that it will be useful, but WITHOUT # |
135 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # |
136 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # |
137 | +# more details. # |
138 | +# # |
139 | +# You should have received a copy of the GNU General Public License along # |
140 | +# with this program; if not, write to the Free Software Foundation, Inc., 59 # |
141 | +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # |
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-05-18 15:59:34 +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-2016 OpenLP Developers # |
565 | +# --------------------------------------------------------------------------- # |
566 | +# This program is free software; you can redistribute it and/or modify it # |
567 | +# under the terms of the GNU General Public License as published by the Free # |
568 | +# Software Foundation; version 2 of the License. # |
569 | +# # |
570 | +# This program is distributed in the hope that it will be useful, but WITHOUT # |
571 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # |
572 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # |
573 | +# more details. # |
574 | +# # |
575 | +# You should have received a copy of the GNU General Public License along # |
576 | +# with this program; if not, write to the Free Software Foundation, Inc., 59 # |
577 | +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # |
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-04-13 13:00:22 +0000 |
827 | +++ openlp/plugins/presentations/lib/presentationcontroller.py 2019-05-18 15:59:34 +0000 |
828 | @@ -408,7 +408,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 | @@ -428,6 +429,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-04-13 13:00:22 +0000 |
849 | +++ openlp/plugins/presentations/lib/presentationtab.py 2019-05-18 15:59:34 +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-05-18 15:59:34 +0000 |
867 | @@ -0,0 +1,28 @@ |
868 | +try: |
869 | + from openlp.core.common.path import Path |
870 | +except ImportError: |
871 | + from pathlib import Path |
872 | + |
873 | +from Pyro4.util import SerializerBase |
874 | + |
875 | + |
876 | +def path_class_to_dict(obj): |
877 | + """ |
878 | + Serialize a Path object for Pyro4 |
879 | + """ |
880 | + return { |
881 | + '__class__': 'Path', |
882 | + 'parts': obj.parts |
883 | + } |
884 | + |
885 | + |
886 | +def path_dict_to_class(classname, d): |
887 | + return Path(d['parts']) |
888 | + |
889 | + |
890 | +def register_classes(): |
891 | + """ |
892 | + Register the serializers |
893 | + """ |
894 | + SerializerBase.register_class_to_dict(Path, path_class_to_dict) |
895 | + SerializerBase.register_dict_to_class('Path', path_dict_to_class) |
896 | |
897 | === added directory 'openlp/plugins/presentations/lib/vendor' |
898 | === added file 'openlp/plugins/presentations/lib/vendor/do_not_delete.txt' |
899 | --- openlp/plugins/presentations/lib/vendor/do_not_delete.txt 1970-01-01 00:00:00 +0000 |
900 | +++ openlp/plugins/presentations/lib/vendor/do_not_delete.txt 2019-05-18 15:59:34 +0000 |
901 | @@ -0,0 +1,5 @@ |
902 | +Vendor Directory |
903 | +================ |
904 | + |
905 | +Do not delete this directory, it is used on Mac OS to place Pyro4 and serpent for use with Impress. |
906 | + |
907 | |
908 | === modified file 'openlp/plugins/presentations/presentationplugin.py' |
909 | --- openlp/plugins/presentations/presentationplugin.py 2019-04-13 13:00:22 +0000 |
910 | +++ openlp/plugins/presentations/presentationplugin.py 2019-05-18 15:59:34 +0000 |
911 | @@ -28,13 +28,13 @@ |
912 | |
913 | from PyQt5 import QtCore |
914 | |
915 | -from openlp.core.state import State |
916 | from openlp.core.api.http import register_endpoint |
917 | from openlp.core.common import extension_loader |
918 | from openlp.core.common.i18n import translate |
919 | from openlp.core.common.settings import Settings |
920 | from openlp.core.lib import build_icon |
921 | from openlp.core.lib.plugin import Plugin, StringContent |
922 | +from openlp.core.state import State |
923 | from openlp.core.ui.icons import UiIcons |
924 | from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint |
925 | from openlp.plugins.presentations.lib.presentationcontroller import PresentationController |
926 | @@ -45,18 +45,20 @@ |
927 | log = logging.getLogger(__name__) |
928 | |
929 | |
930 | -__default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked, |
931 | - 'presentations/enable_pdf_program': QtCore.Qt.Unchecked, |
932 | - 'presentations/pdf_program': None, |
933 | - 'presentations/Impress': QtCore.Qt.Checked, |
934 | - 'presentations/Powerpoint': QtCore.Qt.Checked, |
935 | - 'presentations/Pdf': QtCore.Qt.Checked, |
936 | - 'presentations/presentations files': [], |
937 | - 'presentations/thumbnail_scheme': '', |
938 | - 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked, |
939 | - 'presentations/powerpoint control window': QtCore.Qt.Unchecked, |
940 | - 'presentations/last directory': None |
941 | - } |
942 | +__default_settings__ = { |
943 | + 'presentations/override app': QtCore.Qt.Unchecked, |
944 | + 'presentations/enable_pdf_program': QtCore.Qt.Unchecked, |
945 | + 'presentations/pdf_program': None, |
946 | + 'presentations/maclo': QtCore.Qt.Checked, |
947 | + 'presentations/Impress': QtCore.Qt.Checked, |
948 | + 'presentations/Powerpoint': QtCore.Qt.Checked, |
949 | + 'presentations/Pdf': QtCore.Qt.Checked, |
950 | + 'presentations/presentations files': [], |
951 | + 'presentations/thumbnail_scheme': '', |
952 | + 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked, |
953 | + 'presentations/powerpoint control window': QtCore.Qt.Unchecked, |
954 | + 'presentations/last directory': None |
955 | +} |
956 | |
957 | |
958 | class PresentationPlugin(Plugin): |
959 | @@ -100,7 +102,7 @@ |
960 | try: |
961 | self.controllers[controller].start_process() |
962 | except Exception: |
963 | - log.warning('Failed to start controller process') |
964 | + log.exception('Failed to start controller process') |
965 | self.controllers[controller].available = False |
966 | self.media_item.build_file_mask_string() |
967 | |
968 | |
969 | === modified file 'scripts/check_dependencies.py' |
970 | --- scripts/check_dependencies.py 2019-05-05 08:13:10 +0000 |
971 | +++ scripts/check_dependencies.py 2019-05-18 15:59:34 +0000 |
972 | @@ -159,6 +159,8 @@ |
973 | w('OK') |
974 | except ImportError: |
975 | w('FAIL') |
976 | + except Exception: |
977 | + w('ERROR') |
978 | w(os.linesep) |
979 | |
980 | |
981 | |
982 | === modified file 'tests/functional/openlp_core/common/test_path.py' |
983 | --- tests/functional/openlp_core/common/test_path.py 2019-04-13 13:00:22 +0000 |
984 | +++ tests/functional/openlp_core/common/test_path.py 2019-05-18 15:59:34 +0000 |
985 | @@ -243,7 +243,18 @@ |
986 | # WHEN: Calling `path_to_str` with an invalid Type |
987 | # THEN: A TypeError should have been raised |
988 | with self.assertRaises(TypeError): |
989 | - path_to_str(str()) |
990 | + path_to_str(57) |
991 | + |
992 | + def test_path_to_str_wth_str(self): |
993 | + """ |
994 | + Test that `path_to_str` just returns a str when given a str |
995 | + """ |
996 | + # GIVEN: The `path_to_str` function |
997 | + # WHEN: Calling `path_to_str` with a str |
998 | + result = path_to_str('/usr/bin') |
999 | + |
1000 | + # THEN: The string should be returned |
1001 | + assert result == '/usr/bin' |
1002 | |
1003 | def test_path_to_str_none(self): |
1004 | """ |
1005 | |
1006 | === added file 'tests/functional/openlp_plugins/presentations/test_libreofficeserver.py' |
1007 | --- tests/functional/openlp_plugins/presentations/test_libreofficeserver.py 1970-01-01 00:00:00 +0000 |
1008 | +++ tests/functional/openlp_plugins/presentations/test_libreofficeserver.py 2019-05-18 15:59:34 +0000 |
1009 | @@ -0,0 +1,948 @@ |
1010 | +# -*- coding: utf-8 -*- |
1011 | +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 |
1012 | + |
1013 | +############################################################################### |
1014 | +# OpenLP - Open Source Lyrics Projection # |
1015 | +# --------------------------------------------------------------------------- # |
1016 | +# Copyright (c) 2008-2016 OpenLP Developers # |
1017 | +# --------------------------------------------------------------------------- # |
1018 | +# This program is free software; you can redistribute it and/or modify it # |
1019 | +# under the terms of the GNU General Public License as published by the Free # |
1020 | +# Software Foundation; version 2 of the License. # |
1021 | +# # |
1022 | +# This program is distributed in the hope that it will be useful, but WITHOUT # |
1023 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # |
1024 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # |
1025 | +# more details. # |
1026 | +# # |
1027 | +# You should have received a copy of the GNU General Public License along # |
1028 | +# with this program; if not, write to the Free Software Foundation, Inc., 59 # |
1029 | +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # |
1030 | +############################################################################### |
1031 | +""" |
1032 | +Functional tests to test the LibreOffice Pyro server |
1033 | +""" |
1034 | +from unittest.mock import MagicMock, patch, call |
1035 | + |
1036 | +from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType, main |
1037 | + |
1038 | + |
1039 | +def test_constructor(): |
1040 | + """ |
1041 | + Test the Constructor from the server |
1042 | + """ |
1043 | + # GIVEN: No server |
1044 | + # WHEN: The server object is created |
1045 | + server = LibreOfficeServer() |
1046 | + |
1047 | + # THEN: The server should have been set up correctly |
1048 | + assert server._control is None |
1049 | + # assert server._desktop is None |
1050 | + assert server._document is None |
1051 | + assert server._presentation is None |
1052 | + assert server._process is None |
1053 | + |
1054 | + |
1055 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.Popen') |
1056 | +def test_start_process(MockedPopen): |
1057 | + """ |
1058 | + Test that the correct command is issued to run LibreOffice |
1059 | + """ |
1060 | + # GIVEN: A LOServer |
1061 | + mocked_process = MagicMock() |
1062 | + MockedPopen.return_value = mocked_process |
1063 | + server = LibreOfficeServer() |
1064 | + |
1065 | + # WHEN: The start_process() method is run |
1066 | + server.start_process() |
1067 | + |
1068 | + # THEN: The correct command line should run and the process should have started |
1069 | + MockedPopen.assert_called_with([ |
1070 | + '/Applications/LibreOffice.app/Contents/MacOS/soffice', |
1071 | + '--nologo', |
1072 | + '--norestore', |
1073 | + '--minimized', |
1074 | + '--nodefault', |
1075 | + '--nofirststartwizard', |
1076 | + '--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager' |
1077 | + ]) |
1078 | + assert server._process is mocked_process |
1079 | + |
1080 | + |
1081 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1082 | +def test_desktop_already_has_desktop(mocked_uno): |
1083 | + """ |
1084 | + Test that setup_desktop() exits early when there's already a desktop |
1085 | + """ |
1086 | + # GIVEN: A LibreOfficeServer instance |
1087 | + server = LibreOfficeServer() |
1088 | + server._desktop = MagicMock() |
1089 | + |
1090 | + # WHEN: the desktop property is called |
1091 | + desktop = server.desktop |
1092 | + |
1093 | + # THEN: setup_desktop() exits early |
1094 | + assert desktop is server._desktop |
1095 | + assert server._manager is None |
1096 | + |
1097 | + |
1098 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1099 | +def test_desktop_exception(mocked_uno): |
1100 | + """ |
1101 | + Test that setting up the desktop works correctly when an exception occurs |
1102 | + """ |
1103 | + # GIVEN: A LibreOfficeServer instance |
1104 | + server = LibreOfficeServer() |
1105 | + mocked_context = MagicMock() |
1106 | + mocked_resolver = MagicMock() |
1107 | + mocked_uno_instance = MagicMock() |
1108 | + MockedServiceManager = MagicMock() |
1109 | + mocked_uno.getComponentContext.return_value = mocked_context |
1110 | + mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver |
1111 | + mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance] |
1112 | + mocked_uno_instance.ServiceManager = MockedServiceManager |
1113 | + MockedServiceManager.createInstanceWithContext.side_effect = Exception() |
1114 | + |
1115 | + # WHEN: the desktop property is called |
1116 | + server.desktop |
1117 | + |
1118 | + # THEN: A desktop object was created |
1119 | + mocked_uno.getComponentContext.assert_called_once_with() |
1120 | + mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with( |
1121 | + 'com.sun.star.bridge.UnoUrlResolver', mocked_context) |
1122 | + expected_calls = [ |
1123 | + call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'), |
1124 | + call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext') |
1125 | + ] |
1126 | + assert mocked_resolver.resolve.call_args_list == expected_calls |
1127 | + MockedServiceManager.createInstanceWithContext.assert_called_once_with( |
1128 | + 'com.sun.star.frame.Desktop', mocked_uno_instance) |
1129 | + assert server._manager is MockedServiceManager |
1130 | + assert server._desktop is None |
1131 | + |
1132 | + |
1133 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1134 | +def test_desktop(mocked_uno): |
1135 | + """ |
1136 | + Test that setting up the desktop works correctly |
1137 | + """ |
1138 | + # GIVEN: A LibreOfficeServer instance |
1139 | + server = LibreOfficeServer() |
1140 | + mocked_context = MagicMock() |
1141 | + mocked_resolver = MagicMock() |
1142 | + mocked_uno_instance = MagicMock() |
1143 | + MockedServiceManager = MagicMock() |
1144 | + mocked_desktop = MagicMock() |
1145 | + mocked_uno.getComponentContext.return_value = mocked_context |
1146 | + mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver |
1147 | + mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance] |
1148 | + mocked_uno_instance.ServiceManager = MockedServiceManager |
1149 | + MockedServiceManager.createInstanceWithContext.return_value = mocked_desktop |
1150 | + |
1151 | + # WHEN: the desktop property is called |
1152 | + server.desktop |
1153 | + |
1154 | + # THEN: A desktop object was created |
1155 | + mocked_uno.getComponentContext.assert_called_once_with() |
1156 | + mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with( |
1157 | + 'com.sun.star.bridge.UnoUrlResolver', mocked_context) |
1158 | + expected_calls = [ |
1159 | + call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'), |
1160 | + call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext') |
1161 | + ] |
1162 | + assert mocked_resolver.resolve.call_args_list == expected_calls |
1163 | + MockedServiceManager.createInstanceWithContext.assert_called_once_with( |
1164 | + 'com.sun.star.frame.Desktop', mocked_uno_instance) |
1165 | + assert server._manager is MockedServiceManager |
1166 | + assert server._desktop is mocked_desktop |
1167 | + |
1168 | + |
1169 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.PropertyValue') |
1170 | +def test_create_property(MockedPropertyValue): |
1171 | + """ |
1172 | + Test that the _create_property() method works correctly |
1173 | + """ |
1174 | + # GIVEN: A server amnd property to set |
1175 | + server = LibreOfficeServer() |
1176 | + name = 'Hidden' |
1177 | + value = True |
1178 | + |
1179 | + # WHEN: The _create_property() method is called |
1180 | + prop = server._create_property(name, value) |
1181 | + |
1182 | + # THEN: The property should have the correct attributes |
1183 | + assert prop.Name == name |
1184 | + assert prop.Value == value |
1185 | + |
1186 | + |
1187 | +def test_get_text_from_page_slide_text(): |
1188 | + """ |
1189 | + Test that the _get_text_from_page() method gives us nothing for slide text |
1190 | + """ |
1191 | + # GIVEN: A LibreOfficeServer object and some mocked objects |
1192 | + text_type = TextType.SlideText |
1193 | + slide_no = 1 |
1194 | + server = LibreOfficeServer() |
1195 | + server._document = MagicMock() |
1196 | + mocked_pages = MagicMock() |
1197 | + mocked_page = MagicMock() |
1198 | + mocked_shape = MagicMock() |
1199 | + server._document.getDrawPages.return_value = mocked_pages |
1200 | + mocked_pages.getCount.return_value = 1 |
1201 | + mocked_pages.getByIndex.return_value = mocked_page |
1202 | + mocked_page.getByIndex.return_value = mocked_shape |
1203 | + mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape' |
1204 | + mocked_shape.supportsService.return_value = True |
1205 | + mocked_shape.getString.return_value = 'Page Text' |
1206 | + |
1207 | + # WHEN: _get_text_from_page() is run for slide text |
1208 | + text = server._get_text_from_page(slide_no, text_type) |
1209 | + |
1210 | + # THE: The text is correct |
1211 | + assert text == 'Page Text\n' |
1212 | + |
1213 | + |
1214 | +def test_get_text_from_page_title(): |
1215 | + """ |
1216 | + Test that the _get_text_from_page() method gives us the text from the titles |
1217 | + """ |
1218 | + # GIVEN: A LibreOfficeServer object and some mocked objects |
1219 | + text_type = TextType.Title |
1220 | + slide_no = 1 |
1221 | + server = LibreOfficeServer() |
1222 | + server._document = MagicMock() |
1223 | + mocked_pages = MagicMock() |
1224 | + mocked_page = MagicMock() |
1225 | + mocked_shape = MagicMock() |
1226 | + server._document.getDrawPages.return_value = mocked_pages |
1227 | + mocked_pages.getCount.return_value = 1 |
1228 | + mocked_pages.getByIndex.return_value = mocked_page |
1229 | + mocked_page.getByIndex.return_value = mocked_shape |
1230 | + mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape' |
1231 | + mocked_shape.supportsService.return_value = True |
1232 | + mocked_shape.getString.return_value = 'Page Title' |
1233 | + |
1234 | + # WHEN: _get_text_from_page() is run for titles |
1235 | + text = server._get_text_from_page(slide_no, text_type) |
1236 | + |
1237 | + # THEN: The text should be correct |
1238 | + assert text == 'Page Title\n' |
1239 | + |
1240 | + |
1241 | +def test_get_text_from_page_notes(): |
1242 | + """ |
1243 | + Test that the _get_text_from_page() method gives us the text from the notes |
1244 | + """ |
1245 | + # GIVEN: A LibreOfficeServer object and some mocked objects |
1246 | + text_type = TextType.Notes |
1247 | + slide_no = 1 |
1248 | + server = LibreOfficeServer() |
1249 | + server._document = MagicMock() |
1250 | + mocked_pages = MagicMock() |
1251 | + mocked_page = MagicMock() |
1252 | + mocked_notes_page = MagicMock() |
1253 | + mocked_shape = MagicMock() |
1254 | + server._document.getDrawPages.return_value = mocked_pages |
1255 | + mocked_pages.getCount.return_value = 1 |
1256 | + mocked_pages.getByIndex.return_value = mocked_page |
1257 | + mocked_page.getNotesPage.return_value = mocked_notes_page |
1258 | + mocked_notes_page.getByIndex.return_value = mocked_shape |
1259 | + mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape' |
1260 | + mocked_shape.supportsService.return_value = True |
1261 | + mocked_shape.getString.return_value = 'Page Notes' |
1262 | + |
1263 | + # WHEN: _get_text_from_page() is run for titles |
1264 | + text = server._get_text_from_page(slide_no, text_type) |
1265 | + |
1266 | + # THEN: The text should be correct |
1267 | + assert text == 'Page Notes\n' |
1268 | + |
1269 | + |
1270 | +def test_shutdown_other_docs(): |
1271 | + """ |
1272 | + Test the shutdown method while other documents are open in LibreOffice |
1273 | + """ |
1274 | + def close_docs(): |
1275 | + server._docs = [] |
1276 | + |
1277 | + # GIVEN: An up an running LibreOfficeServer |
1278 | + server = LibreOfficeServer() |
1279 | + mocked_doc = MagicMock() |
1280 | + mocked_desktop = MagicMock() |
1281 | + mocked_docs = MagicMock() |
1282 | + mocked_list = MagicMock() |
1283 | + mocked_element_doc = MagicMock() |
1284 | + server._docs = [mocked_doc] |
1285 | + server._desktop = mocked_desktop |
1286 | + server._process = MagicMock() |
1287 | + mocked_doc.close_presentation.side_effect = close_docs |
1288 | + mocked_desktop.getComponents.return_value = mocked_docs |
1289 | + mocked_docs.hasElements.return_value = True |
1290 | + mocked_docs.createEnumeration.return_value = mocked_list |
1291 | + mocked_list.hasMoreElements.side_effect = [True, False] |
1292 | + mocked_list.nextElement.return_value = mocked_element_doc |
1293 | + mocked_element_doc.getImplementationName.side_effect = [ |
1294 | + 'org.openlp.Nothing', |
1295 | + 'com.sun.star.comp.framework.BackingComp' |
1296 | + ] |
1297 | + |
1298 | + # WHEN: shutdown() is called |
1299 | + server.shutdown() |
1300 | + |
1301 | + # THEN: The right methods are called and everything works |
1302 | + mocked_doc.close_presentation.assert_called_once_with() |
1303 | + mocked_desktop.getComponents.assert_called_once_with() |
1304 | + mocked_docs.hasElements.assert_called_once_with() |
1305 | + mocked_docs.createEnumeration.assert_called_once_with() |
1306 | + assert mocked_list.hasMoreElements.call_count == 2 |
1307 | + mocked_list.nextElement.assert_called_once_with() |
1308 | + mocked_element_doc.getImplementationName.assert_called_once_with() |
1309 | + assert mocked_desktop.terminate.call_count == 0 |
1310 | + assert server._process.kill.call_count == 0 |
1311 | + |
1312 | + |
1313 | +def test_shutdown(): |
1314 | + """ |
1315 | + Test the shutdown method |
1316 | + """ |
1317 | + def close_docs(): |
1318 | + server._docs = [] |
1319 | + |
1320 | + # GIVEN: An up an running LibreOfficeServer |
1321 | + server = LibreOfficeServer() |
1322 | + mocked_doc = MagicMock() |
1323 | + mocked_desktop = MagicMock() |
1324 | + mocked_docs = MagicMock() |
1325 | + mocked_list = MagicMock() |
1326 | + mocked_element_doc = MagicMock() |
1327 | + server._docs = [mocked_doc] |
1328 | + server._desktop = mocked_desktop |
1329 | + server._process = MagicMock() |
1330 | + mocked_doc.close_presentation.side_effect = close_docs |
1331 | + mocked_desktop.getComponents.return_value = mocked_docs |
1332 | + mocked_docs.hasElements.return_value = True |
1333 | + mocked_docs.createEnumeration.return_value = mocked_list |
1334 | + mocked_list.hasMoreElements.side_effect = [True, False] |
1335 | + mocked_list.nextElement.return_value = mocked_element_doc |
1336 | + mocked_element_doc.getImplementationName.return_value = 'com.sun.star.comp.framework.BackingComp' |
1337 | + |
1338 | + # WHEN: shutdown() is called |
1339 | + server.shutdown() |
1340 | + |
1341 | + # THEN: The right methods are called and everything works |
1342 | + mocked_doc.close_presentation.assert_called_once_with() |
1343 | + mocked_desktop.getComponents.assert_called_once_with() |
1344 | + mocked_docs.hasElements.assert_called_once_with() |
1345 | + mocked_docs.createEnumeration.assert_called_once_with() |
1346 | + assert mocked_list.hasMoreElements.call_count == 2 |
1347 | + mocked_list.nextElement.assert_called_once_with() |
1348 | + mocked_element_doc.getImplementationName.assert_called_once_with() |
1349 | + mocked_desktop.terminate.assert_called_once_with() |
1350 | + server._process.kill.assert_called_once_with() |
1351 | + |
1352 | + |
1353 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1354 | +def test_load_presentation_exception(mocked_uno): |
1355 | + """ |
1356 | + Test the load_presentation() method when an exception occurs |
1357 | + """ |
1358 | + # GIVEN: A LibreOfficeServer object |
1359 | + presentation_file = '/path/to/presentation.odp' |
1360 | + screen_number = 1 |
1361 | + server = LibreOfficeServer() |
1362 | + mocked_desktop = MagicMock() |
1363 | + mocked_uno.systemPathToFileUrl.side_effect = lambda x: x |
1364 | + server._desktop = mocked_desktop |
1365 | + mocked_desktop.loadComponentFromURL.side_effect = Exception() |
1366 | + |
1367 | + # WHEN: load_presentation() is called |
1368 | + with patch.object(server, '_create_property') as mocked_create_property: |
1369 | + mocked_create_property.side_effect = lambda x, y: {x: y} |
1370 | + result = server.load_presentation(presentation_file, screen_number) |
1371 | + |
1372 | + # THEN: A presentation is loaded |
1373 | + assert result is False |
1374 | + |
1375 | + |
1376 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1377 | +def test_load_presentation(mocked_uno): |
1378 | + """ |
1379 | + Test the load_presentation() method |
1380 | + """ |
1381 | + # GIVEN: A LibreOfficeServer object |
1382 | + presentation_file = '/path/to/presentation.odp' |
1383 | + screen_number = 1 |
1384 | + server = LibreOfficeServer() |
1385 | + mocked_desktop = MagicMock() |
1386 | + mocked_document = MagicMock() |
1387 | + mocked_presentation = MagicMock() |
1388 | + mocked_uno.systemPathToFileUrl.side_effect = lambda x: x |
1389 | + server._desktop = mocked_desktop |
1390 | + mocked_desktop.loadComponentFromURL.return_value = mocked_document |
1391 | + mocked_document.getPresentation.return_value = mocked_presentation |
1392 | + |
1393 | + # WHEN: load_presentation() is called |
1394 | + with patch.object(server, '_create_property') as mocked_create_property: |
1395 | + mocked_create_property.side_effect = lambda x, y: {x: y} |
1396 | + result = server.load_presentation(presentation_file, screen_number) |
1397 | + |
1398 | + # THEN: A presentation is loaded |
1399 | + assert result is True |
1400 | + mocked_uno.systemPathToFileUrl.assert_called_once_with(presentation_file) |
1401 | + mocked_create_property.assert_called_once_with('Hidden', True) |
1402 | + mocked_desktop.loadComponentFromURL.assert_called_once_with( |
1403 | + presentation_file, '_blank', 0, ({'Hidden': True},)) |
1404 | + assert server._document is mocked_document |
1405 | + mocked_document.getPresentation.assert_called_once_with() |
1406 | + assert server._presentation is mocked_presentation |
1407 | + assert server._presentation.Display == screen_number |
1408 | + assert server._control is None |
1409 | + |
1410 | + |
1411 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1412 | +def test_extract_thumbnails_no_pages(mocked_uno): |
1413 | + """ |
1414 | + Test the extract_thumbnails() method when there are no pages |
1415 | + """ |
1416 | + # GIVEN: A LibreOfficeServer instance |
1417 | + temp_folder = '/tmp' |
1418 | + server = LibreOfficeServer() |
1419 | + mocked_document = MagicMock() |
1420 | + server._document = mocked_document |
1421 | + mocked_uno.systemPathToFileUrl.side_effect = lambda x: x |
1422 | + mocked_document.getDrawPages.return_value = None |
1423 | + |
1424 | + # WHEN: The extract_thumbnails() method is called |
1425 | + with patch.object(server, '_create_property') as mocked_create_property: |
1426 | + mocked_create_property.side_effect = lambda x, y: {x: y} |
1427 | + thumbnails = server.extract_thumbnails(temp_folder) |
1428 | + |
1429 | + # THEN: Thumbnails have been extracted |
1430 | + mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder) |
1431 | + mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export') |
1432 | + mocked_document.getDrawPages.assert_called_once_with() |
1433 | + assert thumbnails == [] |
1434 | + |
1435 | + |
1436 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') |
1437 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.os') |
1438 | +def test_extract_thumbnails(mocked_os, mocked_uno): |
1439 | + """ |
1440 | + Test the extract_thumbnails() method |
1441 | + """ |
1442 | + # GIVEN: A LibreOfficeServer instance |
1443 | + temp_folder = '/tmp' |
1444 | + server = LibreOfficeServer() |
1445 | + mocked_document = MagicMock() |
1446 | + mocked_pages = MagicMock() |
1447 | + mocked_page_1 = MagicMock() |
1448 | + mocked_page_2 = MagicMock() |
1449 | + mocked_controller = MagicMock() |
1450 | + server._document = mocked_document |
1451 | + mocked_uno.systemPathToFileUrl.side_effect = lambda x: x |
1452 | + mocked_document.getDrawPages.return_value = mocked_pages |
1453 | + mocked_os.path.isdir.return_value = False |
1454 | + mocked_pages.getCount.return_value = 2 |
1455 | + mocked_pages.getByIndex.side_effect = [mocked_page_1, mocked_page_2] |
1456 | + mocked_document.getCurrentController.return_value = mocked_controller |
1457 | + mocked_os.path.join.side_effect = lambda *x: '/'.join(x) |
1458 | + |
1459 | + # WHEN: The extract_thumbnails() method is called |
1460 | + with patch.object(server, '_create_property') as mocked_create_property: |
1461 | + mocked_create_property.side_effect = lambda x, y: {x: y} |
1462 | + thumbnails = server.extract_thumbnails(temp_folder) |
1463 | + |
1464 | + # THEN: Thumbnails have been extracted |
1465 | + mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder) |
1466 | + mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export') |
1467 | + mocked_document.getDrawPages.assert_called_once_with() |
1468 | + mocked_pages.getCount.assert_called_once_with() |
1469 | + assert mocked_pages.getByIndex.call_args_list == [call(0), call(1)] |
1470 | + assert mocked_controller.setCurrentPage.call_args_list == \ |
1471 | + [call(mocked_page_1), call(mocked_page_2)] |
1472 | + assert mocked_document.storeToURL.call_args_list == \ |
1473 | + [call('/tmp/1.png', ({'FilterName': 'impress_png_Export'},)), |
1474 | + call('/tmp/2.png', ({'FilterName': 'impress_png_Export'},))] |
1475 | + assert thumbnails == ['/tmp/1.png', '/tmp/2.png'] |
1476 | + |
1477 | + |
1478 | +def test_get_titles_and_notes(): |
1479 | + """ |
1480 | + Test the get_titles_and_notes() method |
1481 | + """ |
1482 | + # GIVEN: A LibreOfficeServer object and a bunch of mocks |
1483 | + server = LibreOfficeServer() |
1484 | + mocked_document = MagicMock() |
1485 | + mocked_pages = MagicMock() |
1486 | + server._document = mocked_document |
1487 | + mocked_document.getDrawPages.return_value = mocked_pages |
1488 | + mocked_pages.getCount.return_value = 2 |
1489 | + |
1490 | + # WHEN: get_titles_and_notes() is called |
1491 | + with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page: |
1492 | + mocked_get_text_from_page.side_effect = [ |
1493 | + 'OpenLP on Mac OS X', |
1494 | + '', |
1495 | + '', |
1496 | + 'Installing is a drag-and-drop affair' |
1497 | + ] |
1498 | + titles, notes = server.get_titles_and_notes() |
1499 | + |
1500 | + # THEN: The right calls are made and the right stuff returned |
1501 | + mocked_document.getDrawPages.assert_called_once_with() |
1502 | + mocked_pages.getCount.assert_called_once_with() |
1503 | + assert mocked_get_text_from_page.call_count == 4 |
1504 | + expected_calls = [ |
1505 | + call(1, TextType.Title), call(1, TextType.Notes), |
1506 | + call(2, TextType.Title), call(2, TextType.Notes), |
1507 | + ] |
1508 | + assert mocked_get_text_from_page.call_args_list == expected_calls |
1509 | + assert titles == ['OpenLP on Mac OS X\n', '\n'], titles |
1510 | + assert notes == [' ', 'Installing is a drag-and-drop affair'], notes |
1511 | + |
1512 | + |
1513 | +def test_close_presentation(): |
1514 | + """ |
1515 | + Test that closing the presentation cleans things up correctly |
1516 | + """ |
1517 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1518 | + server = LibreOfficeServer() |
1519 | + mocked_document = MagicMock() |
1520 | + mocked_presentation = MagicMock() |
1521 | + server._document = mocked_document |
1522 | + server._presentation = mocked_presentation |
1523 | + |
1524 | + # WHEN: close_presentation() is called |
1525 | + server.close_presentation() |
1526 | + |
1527 | + # THEN: The presentation and document should be closed |
1528 | + mocked_presentation.end.assert_called_once_with() |
1529 | + mocked_document.dispose.assert_called_once_with() |
1530 | + assert server._document is None |
1531 | + assert server._presentation is None |
1532 | + |
1533 | + |
1534 | +def test_is_loaded_no_objects(): |
1535 | + """ |
1536 | + Test the is_loaded() method when there's no document or presentation |
1537 | + """ |
1538 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1539 | + server = LibreOfficeServer() |
1540 | + |
1541 | + # WHEN: The is_loaded() method is called |
1542 | + result = server.is_loaded() |
1543 | + |
1544 | + # THEN: The result should be false |
1545 | + assert result is False |
1546 | + |
1547 | + |
1548 | +def test_is_loaded_no_presentation(): |
1549 | + """ |
1550 | + Test the is_loaded() method when there's no presentation |
1551 | + """ |
1552 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1553 | + server = LibreOfficeServer() |
1554 | + mocked_document = MagicMock() |
1555 | + server._document = mocked_document |
1556 | + server._presentation = MagicMock() |
1557 | + mocked_document.getPresentation.return_value = None |
1558 | + |
1559 | + # WHEN: The is_loaded() method is called |
1560 | + result = server.is_loaded() |
1561 | + |
1562 | + # THEN: The result should be false |
1563 | + assert result is False |
1564 | + mocked_document.getPresentation.assert_called_once_with() |
1565 | + |
1566 | + |
1567 | +def test_is_loaded_exception(): |
1568 | + """ |
1569 | + Test the is_loaded() method when an exception is thrown |
1570 | + """ |
1571 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1572 | + server = LibreOfficeServer() |
1573 | + mocked_document = MagicMock() |
1574 | + server._document = mocked_document |
1575 | + server._presentation = MagicMock() |
1576 | + mocked_document.getPresentation.side_effect = Exception() |
1577 | + |
1578 | + # WHEN: The is_loaded() method is called |
1579 | + result = server.is_loaded() |
1580 | + |
1581 | + # THEN: The result should be false |
1582 | + assert result is False |
1583 | + mocked_document.getPresentation.assert_called_once_with() |
1584 | + |
1585 | + |
1586 | +def test_is_loaded(): |
1587 | + """ |
1588 | + Test the is_loaded() method |
1589 | + """ |
1590 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1591 | + server = LibreOfficeServer() |
1592 | + mocked_document = MagicMock() |
1593 | + mocked_presentation = MagicMock() |
1594 | + server._document = mocked_document |
1595 | + server._presentation = mocked_presentation |
1596 | + mocked_document.getPresentation.return_value = mocked_presentation |
1597 | + |
1598 | + # WHEN: The is_loaded() method is called |
1599 | + result = server.is_loaded() |
1600 | + |
1601 | + # THEN: The result should be false |
1602 | + assert result is True |
1603 | + mocked_document.getPresentation.assert_called_once_with() |
1604 | + |
1605 | + |
1606 | +def test_is_active_not_loaded(): |
1607 | + """ |
1608 | + Test is_active() when is_loaded() returns False |
1609 | + """ |
1610 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1611 | + server = LibreOfficeServer() |
1612 | + |
1613 | + # WHEN: is_active() is called with is_loaded() returns False |
1614 | + with patch.object(server, 'is_loaded') as mocked_is_loaded: |
1615 | + mocked_is_loaded.return_value = False |
1616 | + result = server.is_active() |
1617 | + |
1618 | + # THEN: It should have returned False |
1619 | + assert result is False |
1620 | + |
1621 | + |
1622 | +def test_is_active_no_control(): |
1623 | + """ |
1624 | + Test is_active() when is_loaded() returns True but there's no control |
1625 | + """ |
1626 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1627 | + server = LibreOfficeServer() |
1628 | + |
1629 | + # WHEN: is_active() is called with is_loaded() returns False |
1630 | + with patch.object(server, 'is_loaded') as mocked_is_loaded: |
1631 | + mocked_is_loaded.return_value = True |
1632 | + result = server.is_active() |
1633 | + |
1634 | + # THEN: The result should be False |
1635 | + assert result is False |
1636 | + mocked_is_loaded.assert_called_once_with() |
1637 | + |
1638 | + |
1639 | +def test_is_active(): |
1640 | + """ |
1641 | + Test is_active() |
1642 | + """ |
1643 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1644 | + server = LibreOfficeServer() |
1645 | + mocked_control = MagicMock() |
1646 | + server._control = mocked_control |
1647 | + mocked_control.isRunning.return_value = True |
1648 | + |
1649 | + # WHEN: is_active() is called with is_loaded() returns False |
1650 | + with patch.object(server, 'is_loaded') as mocked_is_loaded: |
1651 | + mocked_is_loaded.return_value = True |
1652 | + result = server.is_active() |
1653 | + |
1654 | + # THEN: The result should be False |
1655 | + assert result is True |
1656 | + mocked_is_loaded.assert_called_once_with() |
1657 | + mocked_control.isRunning.assert_called_once_with() |
1658 | + |
1659 | + |
1660 | +def test_unblank_screen(): |
1661 | + """ |
1662 | + Test the unblank_screen() method |
1663 | + """ |
1664 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1665 | + server = LibreOfficeServer() |
1666 | + mocked_control = MagicMock() |
1667 | + server._control = mocked_control |
1668 | + |
1669 | + # WHEN: unblank_screen() is run |
1670 | + server.unblank_screen() |
1671 | + |
1672 | + # THEN: The resume method should have been called |
1673 | + mocked_control.resume.assert_called_once_with() |
1674 | + |
1675 | + |
1676 | +def test_blank_screen(): |
1677 | + """ |
1678 | + Test the blank_screen() method |
1679 | + """ |
1680 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1681 | + server = LibreOfficeServer() |
1682 | + mocked_control = MagicMock() |
1683 | + server._control = mocked_control |
1684 | + |
1685 | + # WHEN: blank_screen() is run |
1686 | + server.blank_screen() |
1687 | + |
1688 | + # THEN: The resume method should have been called |
1689 | + mocked_control.blankScreen.assert_called_once_with(0) |
1690 | + |
1691 | + |
1692 | +def test_is_blank_no_control(): |
1693 | + """ |
1694 | + Test the is_blank() method when there's no control |
1695 | + """ |
1696 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1697 | + server = LibreOfficeServer() |
1698 | + |
1699 | + # WHEN: is_blank() is called |
1700 | + result = server.is_blank() |
1701 | + |
1702 | + # THEN: It should have returned False |
1703 | + assert result is False |
1704 | + |
1705 | + |
1706 | +def test_is_blank_control_is_running(): |
1707 | + """ |
1708 | + Test the is_blank() method when the control is running |
1709 | + """ |
1710 | + # GIVEN: A LibreOfficeServer instance and a bunch of mocks |
1711 | + server = LibreOfficeServer() |
1712 | + mocked_control = MagicMock() |
1713 | + server._control = mocked_control |
1714 | + mocked_control.isRunning.return_value = True |
1715 | + mocked_control.isPaused.return_value = True |
1716 | + |
1717 | + # WHEN: is_blank() is called |
1718 | + result = server.is_blank() |
1719 | + |
1720 | + # THEN: It should have returned False |
1721 | + assert result is True |
1722 | + mocked_control.isRunning.assert_called_once_with() |
1723 | + mocked_control.isPaused.assert_called_once_with() |
1724 | + |
1725 | + |
1726 | +def test_stop_presentation(): |
1727 | + """ |
1728 | + Test the stop_presentation() method |
1729 | + """ |
1730 | + # GIVEN: A LibreOfficeServer instance and a mocked presentation |
1731 | + server = LibreOfficeServer() |
1732 | + mocked_presentation = MagicMock() |
1733 | + mocked_control = MagicMock() |
1734 | + server._presentation = mocked_presentation |
1735 | + server._control = mocked_control |
1736 | + |
1737 | + # WHEN: stop_presentation() is called |
1738 | + server.stop_presentation() |
1739 | + |
1740 | + # THEN: The presentation is ended and the control is removed |
1741 | + mocked_presentation.end.assert_called_once_with() |
1742 | + assert server._control is None |
1743 | + |
1744 | + |
1745 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep') |
1746 | +def test_start_presentation_no_control(mocked_sleep): |
1747 | + """ |
1748 | + Test the start_presentation() method when there's no control |
1749 | + """ |
1750 | + # GIVEN: A LibreOfficeServer instance and some mocks |
1751 | + server = LibreOfficeServer() |
1752 | + mocked_control = MagicMock() |
1753 | + mocked_document = MagicMock() |
1754 | + mocked_presentation = MagicMock() |
1755 | + mocked_controller = MagicMock() |
1756 | + mocked_frame = MagicMock() |
1757 | + mocked_window = MagicMock() |
1758 | + server._document = mocked_document |
1759 | + server._presentation = mocked_presentation |
1760 | + mocked_document.getCurrentController.return_value = mocked_controller |
1761 | + mocked_controller.getFrame.return_value = mocked_frame |
1762 | + mocked_frame.getContainerWindow.return_value = mocked_window |
1763 | + mocked_presentation.getController.side_effect = [None, mocked_control] |
1764 | + |
1765 | + # WHEN: start_presentation() is called |
1766 | + server.start_presentation() |
1767 | + |
1768 | + # THEN: The slide number should be correct |
1769 | + mocked_document.getCurrentController.assert_called_once_with() |
1770 | + mocked_controller.getFrame.assert_called_once_with() |
1771 | + mocked_frame.getContainerWindow.assert_called_once_with() |
1772 | + mocked_presentation.start.assert_called_once_with() |
1773 | + assert mocked_presentation.getController.call_count == 2 |
1774 | + mocked_sleep.assert_called_once_with(0.1) |
1775 | + assert mocked_window.setVisible.call_args_list == [call(True), call(False)] |
1776 | + assert server._control is mocked_control |
1777 | + |
1778 | + |
1779 | +def test_start_presentation(): |
1780 | + """ |
1781 | + Test the start_presentation() method when there's a control |
1782 | + """ |
1783 | + # GIVEN: A LibreOfficeServer instance and some mocks |
1784 | + server = LibreOfficeServer() |
1785 | + mocked_control = MagicMock() |
1786 | + server._control = mocked_control |
1787 | + |
1788 | + # WHEN: start_presentation() is called |
1789 | + with patch.object(server, 'goto_slide') as mocked_goto_slide: |
1790 | + server.start_presentation() |
1791 | + |
1792 | + # THEN: The control should have been activated and the first slide selected |
1793 | + mocked_control.activate.assert_called_once_with() |
1794 | + mocked_goto_slide.assert_called_once_with(1) |
1795 | + |
1796 | + |
1797 | +def test_get_slide_number(): |
1798 | + """ |
1799 | + Test the get_slide_number() method |
1800 | + """ |
1801 | + # GIVEN: A LibreOfficeServer instance and some mocks |
1802 | + server = LibreOfficeServer() |
1803 | + mocked_control = MagicMock() |
1804 | + mocked_control.getCurrentSlideIndex.return_value = 3 |
1805 | + server._control = mocked_control |
1806 | + |
1807 | + # WHEN: get_slide_number() is called |
1808 | + result = server.get_slide_number() |
1809 | + |
1810 | + # THEN: The slide number should be correct |
1811 | + assert result == 4 |
1812 | + |
1813 | + |
1814 | +def test_get_slide_count(): |
1815 | + """ |
1816 | + Test the get_slide_count() method |
1817 | + """ |
1818 | + # GIVEN: A LibreOfficeServer instance and some mocks |
1819 | + server = LibreOfficeServer() |
1820 | + mocked_document = MagicMock() |
1821 | + mocked_pages = MagicMock() |
1822 | + server._document = mocked_document |
1823 | + mocked_document.getDrawPages.return_value = mocked_pages |
1824 | + mocked_pages.getCount.return_value = 2 |
1825 | + |
1826 | + # WHEN: get_slide_count() is called |
1827 | + result = server.get_slide_count() |
1828 | + |
1829 | + # THEN: The slide count should be correct |
1830 | + assert result == 2 |
1831 | + |
1832 | + |
1833 | +def test_goto_slide(): |
1834 | + """ |
1835 | + Test the goto_slide() method |
1836 | + """ |
1837 | + # GIVEN: A LibreOfficeServer instance and some mocks |
1838 | + server = LibreOfficeServer() |
1839 | + mocked_control = MagicMock() |
1840 | + server._control = mocked_control |
1841 | + |
1842 | + # WHEN: goto_slide() is called |
1843 | + server.goto_slide(1) |
1844 | + |
1845 | + # THEN: The slide number should be correct |
1846 | + mocked_control.gotoSlideIndex.assert_called_once_with(0) |
1847 | + |
1848 | + |
1849 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep') |
1850 | +def test_next_step_when_paused(mocked_sleep): |
1851 | + """ |
1852 | + Test the next_step() method when paused |
1853 | + """ |
1854 | + # GIVEN: A LibreOfficeServer instance and a mocked control |
1855 | + server = LibreOfficeServer() |
1856 | + mocked_control = MagicMock() |
1857 | + server._control = mocked_control |
1858 | + mocked_control.isPaused.side_effect = [False, True] |
1859 | + |
1860 | + # WHEN: next_step() is called |
1861 | + server.next_step() |
1862 | + |
1863 | + # THEN: The correct call should be made |
1864 | + mocked_control.gotoNextEffect.assert_called_once_with() |
1865 | + mocked_sleep.assert_called_once_with(0.1) |
1866 | + assert mocked_control.isPaused.call_count == 2 |
1867 | + mocked_control.gotoPreviousEffect.assert_called_once_with() |
1868 | + |
1869 | + |
1870 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep') |
1871 | +def test_next_step(mocked_sleep): |
1872 | + """ |
1873 | + Test the next_step() method when paused |
1874 | + """ |
1875 | + # GIVEN: A LibreOfficeServer instance and a mocked control |
1876 | + server = LibreOfficeServer() |
1877 | + mocked_control = MagicMock() |
1878 | + server._control = mocked_control |
1879 | + mocked_control.isPaused.side_effect = [True, True] |
1880 | + |
1881 | + # WHEN: next_step() is called |
1882 | + server.next_step() |
1883 | + |
1884 | + # THEN: The correct call should be made |
1885 | + mocked_control.gotoNextEffect.assert_called_once_with() |
1886 | + mocked_sleep.assert_called_once_with(0.1) |
1887 | + assert mocked_control.isPaused.call_count == 1 |
1888 | + assert mocked_control.gotoPreviousEffect.call_count == 0 |
1889 | + |
1890 | + |
1891 | +def test_previous_step(): |
1892 | + """ |
1893 | + Test the previous_step() method |
1894 | + """ |
1895 | + # GIVEN: A LibreOfficeServer instance and a mocked control |
1896 | + server = LibreOfficeServer() |
1897 | + mocked_control = MagicMock() |
1898 | + server._control = mocked_control |
1899 | + |
1900 | + # WHEN: previous_step() is called |
1901 | + server.previous_step() |
1902 | + |
1903 | + # THEN: The correct call should be made |
1904 | + mocked_control.gotoPreviousEffect.assert_called_once_with() |
1905 | + |
1906 | + |
1907 | +def test_get_slide_text(): |
1908 | + """ |
1909 | + Test the get_slide_text() method |
1910 | + """ |
1911 | + # GIVEN: A LibreOfficeServer instance |
1912 | + server = LibreOfficeServer() |
1913 | + |
1914 | + # WHEN: get_slide_text() is called for a particular slide |
1915 | + with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page: |
1916 | + mocked_get_text_from_page.return_value = 'OpenLP on Mac OS X' |
1917 | + result = server.get_slide_text(5) |
1918 | + |
1919 | + # THEN: The text should be returned |
1920 | + mocked_get_text_from_page.assert_called_once_with(5) |
1921 | + assert result == 'OpenLP on Mac OS X' |
1922 | + |
1923 | + |
1924 | +def test_get_slide_notes(): |
1925 | + """ |
1926 | + Test the get_slide_notes() method |
1927 | + """ |
1928 | + # GIVEN: A LibreOfficeServer instance |
1929 | + server = LibreOfficeServer() |
1930 | + |
1931 | + # WHEN: get_slide_notes() is called for a particular slide |
1932 | + with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page: |
1933 | + mocked_get_text_from_page.return_value = 'Installing is a drag-and-drop affair' |
1934 | + result = server.get_slide_notes(3) |
1935 | + |
1936 | + # THEN: The text should be returned |
1937 | + mocked_get_text_from_page.assert_called_once_with(3, TextType.Notes) |
1938 | + assert result == 'Installing is a drag-and-drop affair' |
1939 | + |
1940 | + |
1941 | +@patch('openlp.plugins.presentations.lib.libreofficeserver.Daemon') |
1942 | +def test_main(MockedDaemon): |
1943 | + """ |
1944 | + Test the main() function |
1945 | + """ |
1946 | + # GIVEN: Mocked out Pyro objects |
1947 | + mocked_daemon = MagicMock() |
1948 | + MockedDaemon.return_value = mocked_daemon |
1949 | + |
1950 | + # WHEN: main() is run |
1951 | + main() |
1952 | + |
1953 | + # THEN: The correct calls are made |
1954 | + MockedDaemon.assert_called_once_with(host='localhost', port=4310) |
1955 | + mocked_daemon.register.assert_called_once_with(LibreOfficeServer, 'openlp.libreofficeserver') |
1956 | + mocked_daemon.requestLoop.assert_called_once_with() |
1957 | + mocked_daemon.close.assert_called_once_with() |
1958 | |
1959 | === added file 'tests/functional/openlp_plugins/presentations/test_maclocontroller.py' |
1960 | --- tests/functional/openlp_plugins/presentations/test_maclocontroller.py 1970-01-01 00:00:00 +0000 |
1961 | +++ tests/functional/openlp_plugins/presentations/test_maclocontroller.py 2019-05-18 15:59:34 +0000 |
1962 | @@ -0,0 +1,453 @@ |
1963 | +# -*- coding: utf-8 -*- |
1964 | +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 |
1965 | + |
1966 | +############################################################################### |
1967 | +# OpenLP - Open Source Lyrics Projection # |
1968 | +# --------------------------------------------------------------------------- # |
1969 | +# Copyright (c) 2008-2016 OpenLP Developers # |
1970 | +# --------------------------------------------------------------------------- # |
1971 | +# This program is free software; you can redistribute it and/or modify it # |
1972 | +# under the terms of the GNU General Public License as published by the Free # |
1973 | +# Software Foundation; version 2 of the License. # |
1974 | +# # |
1975 | +# This program is distributed in the hope that it will be useful, but WITHOUT # |
1976 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # |
1977 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # |
1978 | +# more details. # |
1979 | +# # |
1980 | +# You should have received a copy of the GNU General Public License along # |
1981 | +# with this program; if not, write to the Free Software Foundation, Inc., 59 # |
1982 | +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # |
1983 | +############################################################################### |
1984 | +""" |
1985 | +Functional tests to test the Mac LibreOffice class and related methods. |
1986 | +""" |
1987 | +import shutil |
1988 | +from tempfile import mkdtemp |
1989 | +from unittest import TestCase |
1990 | +from unittest.mock import MagicMock, patch, call |
1991 | + |
1992 | +from openlp.core.common.settings import Settings |
1993 | +from openlp.core.common.path import Path |
1994 | +from openlp.plugins.presentations.lib.maclocontroller import MacLOController, MacLODocument |
1995 | +from openlp.plugins.presentations.presentationplugin import __default_settings__ |
1996 | + |
1997 | +from tests.helpers.testmixin import TestMixin |
1998 | +from tests.utils.constants import TEST_RESOURCES_PATH |
1999 | + |
2000 | + |
2001 | +class TestMacLOController(TestCase, TestMixin): |
2002 | + """ |
2003 | + Test the MacLOController Class |
2004 | + """ |
2005 | + |
2006 | + def setUp(self): |
2007 | + """ |
2008 | + Set up the patches and mocks need for all tests. |
2009 | + """ |
2010 | + self.setup_application() |
2011 | + self.build_settings() |
2012 | + self.mock_plugin = MagicMock() |
2013 | + self.temp_folder = mkdtemp() |
2014 | + self.mock_plugin.settings_section = self.temp_folder |
2015 | + |
2016 | + def tearDown(self): |
2017 | + """ |
2018 | + Stop the patches |
2019 | + """ |
2020 | + self.destroy_settings() |
2021 | + shutil.rmtree(self.temp_folder) |
2022 | + |
2023 | + @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') |
2024 | + def test_constructor(self, mocked_start_server): |
2025 | + """ |
2026 | + Test the Constructor from the MacLOController |
2027 | + """ |
2028 | + # GIVEN: No presentation controller |
2029 | + controller = None |
2030 | + |
2031 | + # WHEN: The presentation controller object is created |
2032 | + controller = MacLOController(plugin=self.mock_plugin) |
2033 | + |
2034 | + # THEN: The name of the presentation controller should be correct |
2035 | + assert controller.name == 'maclo', \ |
2036 | + 'The name of the presentation controller should be correct' |
2037 | + assert controller.display_name == 'Impress on macOS', \ |
2038 | + 'The display name of the presentation controller should be correct' |
2039 | + mocked_start_server.assert_called_once_with() |
2040 | + |
2041 | + @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') |
2042 | + @patch('openlp.plugins.presentations.lib.maclocontroller.Proxy') |
2043 | + def test_client(self, MockedProxy, mocked_start_server): |
2044 | + """ |
2045 | + Test the client property of the Controller |
2046 | + """ |
2047 | + # GIVEN: A controller without a client and a mocked out Pyro |
2048 | + controller = MacLOController(plugin=self.mock_plugin) |
2049 | + mocked_client = MagicMock() |
2050 | + MockedProxy.return_value = mocked_client |
2051 | + mocked_client._pyroConnection = None |
2052 | + |
2053 | + # WHEN: the client property is called the first time |
2054 | + client = controller.client |
2055 | + |
2056 | + # THEN: a client is created |
2057 | + assert client == mocked_client |
2058 | + MockedProxy.assert_called_once_with('PYRO:openlp.libreofficeserver@localhost:4310') |
2059 | + mocked_client._pyroReconnect.assert_called_once_with() |
2060 | + |
2061 | + @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') |
2062 | + def test_check_available(self, mocked_start_server): |
2063 | + """ |
2064 | + Test the check_available() method |
2065 | + """ |
2066 | + from openlp.plugins.presentations.lib.maclocontroller import macuno_available |
2067 | + |
2068 | + # GIVEN: A controller |
2069 | + controller = MacLOController(plugin=self.mock_plugin) |
2070 | + |
2071 | + # WHEN: check_available() is run |
2072 | + result = controller.check_available() |
2073 | + |
2074 | + # THEN: it should return false |
2075 | + assert result == macuno_available |
2076 | + |
2077 | + @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') |
2078 | + def test_start_process(self, mocked_start_server): |
2079 | + """ |
2080 | + Test the start_process() method |
2081 | + """ |
2082 | + # GIVEN: A controller and a client |
2083 | + controller = MacLOController(plugin=self.mock_plugin) |
2084 | + controller._client = MagicMock() |
2085 | + |
2086 | + # WHEN: start_process() is called |
2087 | + controller.start_process() |
2088 | + |
2089 | + # THEN: The client's start_process() should have been called |
2090 | + controller._client.start_process.assert_called_once_with() |
2091 | + |
2092 | + @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') |
2093 | + def test_kill(self, mocked_start_server): |
2094 | + """ |
2095 | + Test the kill() method |
2096 | + """ |
2097 | + # GIVEN: A controller and a client |
2098 | + controller = MacLOController(plugin=self.mock_plugin) |
2099 | + controller._client = MagicMock() |
2100 | + controller.server_process = MagicMock() |
2101 | + |
2102 | + # WHEN: start_process() is called |
2103 | + controller.kill() |
2104 | + |
2105 | + # THEN: The client's start_process() should have been called |
2106 | + controller._client.shutdown.assert_called_once_with() |
2107 | + controller.server_process.kill.assert_called_once_with() |
2108 | + |
2109 | + |
2110 | +class TestMacLODocument(TestCase): |
2111 | + """ |
2112 | + Test the MacLODocument Class |
2113 | + """ |
2114 | + def setUp(self): |
2115 | + mocked_plugin = MagicMock() |
2116 | + mocked_plugin.settings_section = 'presentations' |
2117 | + Settings().extend_default_settings(__default_settings__) |
2118 | + self.file_name = Path(TEST_RESOURCES_PATH) / 'presentations' / 'test.odp' |
2119 | + self.mocked_client = MagicMock() |
2120 | + with patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server'): |
2121 | + self.controller = MacLOController(mocked_plugin) |
2122 | + self.controller._client = self.mocked_client |
2123 | + self.document = MacLODocument(self.controller, self.file_name) |
2124 | + |
2125 | + @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList') |
2126 | + def test_load_presentation_cannot_load(self, MockedScreenList): |
2127 | + """ |
2128 | + Test the load_presentation() method when the server can't load the presentation |
2129 | + """ |
2130 | + # GIVEN: A document and a mocked client |
2131 | + mocked_screen_list = MagicMock() |
2132 | + MockedScreenList.return_value = mocked_screen_list |
2133 | + mocked_screen_list.current.number = 0 |
2134 | + self.mocked_client.load_presentation.return_value = False |
2135 | + |
2136 | + # WHEN: load_presentation() is called |
2137 | + result = self.document.load_presentation() |
2138 | + |
2139 | + # THEN: Stuff should work right |
2140 | + self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1) |
2141 | + assert result is False |
2142 | + |
2143 | + @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList') |
2144 | + def test_load_presentation(self, MockedScreenList): |
2145 | + """ |
2146 | + Test the load_presentation() method |
2147 | + """ |
2148 | + # GIVEN: A document and a mocked client |
2149 | + mocked_screen_list = MagicMock() |
2150 | + MockedScreenList.return_value = mocked_screen_list |
2151 | + mocked_screen_list.current.number = 0 |
2152 | + self.mocked_client.load_presentation.return_value = True |
2153 | + |
2154 | + # WHEN: load_presentation() is called |
2155 | + with patch.object(self.document, 'create_thumbnails') as mocked_create_thumbnails, \ |
2156 | + patch.object(self.document, 'create_titles_and_notes') as mocked_create_titles_and_notes: |
2157 | + result = self.document.load_presentation() |
2158 | + |
2159 | + # THEN: Stuff should work right |
2160 | + self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1) |
2161 | + mocked_create_thumbnails.assert_called_once_with() |
2162 | + mocked_create_titles_and_notes.assert_called_once_with() |
2163 | + assert result is True |
2164 | + |
2165 | + def test_create_thumbnails_already_exist(self): |
2166 | + """ |
2167 | + Test the create_thumbnails() method when thumbnails already exist |
2168 | + """ |
2169 | + # GIVEN: thumbnails that exist and a mocked client |
2170 | + self.document.check_thumbnails = MagicMock(return_value=True) |
2171 | + |
2172 | + # WHEN: create_thumbnails() is called |
2173 | + self.document.create_thumbnails() |
2174 | + |
2175 | + # THEN: The method should exit early |
2176 | + assert self.mocked_client.extract_thumbnails.call_count == 0 |
2177 | + |
2178 | + @patch('openlp.plugins.presentations.lib.maclocontroller.delete_file') |
2179 | + def test_create_thumbnails(self, mocked_delete_file): |
2180 | + """ |
2181 | + Test the create_thumbnails() method |
2182 | + """ |
2183 | + # GIVEN: thumbnails that don't exist and a mocked client |
2184 | + self.document.check_thumbnails = MagicMock(return_value=False) |
2185 | + self.mocked_client.extract_thumbnails.return_value = ['thumb1.png', 'thumb2.png'] |
2186 | + |
2187 | + # WHEN: create_thumbnails() is called |
2188 | + with patch.object(self.document, 'convert_thumbnail') as mocked_convert_thumbnail, \ |
2189 | + patch.object(self.document, 'get_temp_folder') as mocked_get_temp_folder: |
2190 | + mocked_get_temp_folder.return_value = 'temp' |
2191 | + self.document.create_thumbnails() |
2192 | + |
2193 | + # THEN: The method should complete successfully |
2194 | + self.mocked_client.extract_thumbnails.assert_called_once_with('temp') |
2195 | + assert mocked_convert_thumbnail.call_args_list == [ |
2196 | + call(Path('thumb1.png'), 1), call(Path('thumb2.png'), 2)] |
2197 | + assert mocked_delete_file.call_args_list == [call(Path('thumb1.png')), call(Path('thumb2.png'))] |
2198 | + |
2199 | + def test_create_titles_and_notes(self): |
2200 | + """ |
2201 | + Test create_titles_and_notes() method |
2202 | + """ |
2203 | + # GIVEN: mocked client and mocked save_titles_and_notes() method |
2204 | + self.mocked_client.get_titles_and_notes.return_value = ('OpenLP', 'This is a note') |
2205 | + |
2206 | + # WHEN: create_titles_and_notes() is called |
2207 | + with patch.object(self.document, 'save_titles_and_notes') as mocked_save_titles_and_notes: |
2208 | + self.document.create_titles_and_notes() |
2209 | + |
2210 | + # THEN save_titles_and_notes should have been called |
2211 | + self.mocked_client.get_titles_and_notes.assert_called_once_with() |
2212 | + mocked_save_titles_and_notes.assert_called_once_with('OpenLP', 'This is a note') |
2213 | + |
2214 | + def test_close_presentation(self): |
2215 | + """ |
2216 | + Test the close_presentation() method |
2217 | + """ |
2218 | + # GIVEN: A mocked client and mocked remove_doc() method |
2219 | + # WHEN: close_presentation() is called |
2220 | + with patch.object(self.controller, 'remove_doc') as mocked_remove_doc: |
2221 | + self.document.close_presentation() |
2222 | + |
2223 | + # THEN: The presentation should have been closed |
2224 | + self.mocked_client.close_presentation.assert_called_once_with() |
2225 | + mocked_remove_doc.assert_called_once_with(self.document) |
2226 | + |
2227 | + def test_is_loaded(self): |
2228 | + """ |
2229 | + Test the is_loaded() method |
2230 | + """ |
2231 | + # GIVEN: A mocked client |
2232 | + self.mocked_client.is_loaded.return_value = True |
2233 | + |
2234 | + # WHEN: is_loaded() is called |
2235 | + result = self.document.is_loaded() |
2236 | + |
2237 | + # THEN: Then the result should be correct |
2238 | + assert result is True |
2239 | + |
2240 | + def test_is_active(self): |
2241 | + """ |
2242 | + Test the is_active() method |
2243 | + """ |
2244 | + # GIVEN: A mocked client |
2245 | + self.mocked_client.is_active.return_value = True |
2246 | + |
2247 | + # WHEN: is_active() is called |
2248 | + result = self.document.is_active() |
2249 | + |
2250 | + # THEN: Then the result should be correct |
2251 | + assert result is True |
2252 | + |
2253 | + def test_unblank_screen(self): |
2254 | + """ |
2255 | + Test the unblank_screen() method |
2256 | + """ |
2257 | + # GIVEN: A mocked client |
2258 | + self.mocked_client.unblank_screen.return_value = True |
2259 | + |
2260 | + # WHEN: unblank_screen() is called |
2261 | + result = self.document.unblank_screen() |
2262 | + |
2263 | + # THEN: Then the result should be correct |
2264 | + self.mocked_client.unblank_screen.assert_called_once_with() |
2265 | + assert result is True |
2266 | + |
2267 | + def test_blank_screen(self): |
2268 | + """ |
2269 | + Test the blank_screen() method |
2270 | + """ |
2271 | + # GIVEN: A mocked client |
2272 | + self.mocked_client.blank_screen.return_value = True |
2273 | + |
2274 | + # WHEN: blank_screen() is called |
2275 | + self.document.blank_screen() |
2276 | + |
2277 | + # THEN: Then the result should be correct |
2278 | + self.mocked_client.blank_screen.assert_called_once_with() |
2279 | + |
2280 | + def test_is_blank(self): |
2281 | + """ |
2282 | + Test the is_blank() method |
2283 | + """ |
2284 | + # GIVEN: A mocked client |
2285 | + self.mocked_client.is_blank.return_value = True |
2286 | + |
2287 | + # WHEN: is_blank() is called |
2288 | + result = self.document.is_blank() |
2289 | + |
2290 | + # THEN: Then the result should be correct |
2291 | + assert result is True |
2292 | + |
2293 | + def test_stop_presentation(self): |
2294 | + """ |
2295 | + Test the stop_presentation() method |
2296 | + """ |
2297 | + # GIVEN: A mocked client |
2298 | + self.mocked_client.stop_presentation.return_value = True |
2299 | + |
2300 | + # WHEN: stop_presentation() is called |
2301 | + self.document.stop_presentation() |
2302 | + |
2303 | + # THEN: Then the result should be correct |
2304 | + self.mocked_client.stop_presentation.assert_called_once_with() |
2305 | + |
2306 | + @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList') |
2307 | + @patch('openlp.plugins.presentations.lib.maclocontroller.Registry') |
2308 | + def test_start_presentation(self, MockedRegistry, MockedScreenList): |
2309 | + """ |
2310 | + Test the start_presentation() method |
2311 | + """ |
2312 | + # GIVEN: a mocked client, and multiple screens |
2313 | + mocked_screen_list = MagicMock() |
2314 | + mocked_screen_list.__len__.return_value = 2 |
2315 | + mocked_registry = MagicMock() |
2316 | + mocked_main_window = MagicMock() |
2317 | + MockedScreenList.return_value = mocked_screen_list |
2318 | + MockedRegistry.return_value = mocked_registry |
2319 | + mocked_screen_list.screen_list = [0, 1] |
2320 | + mocked_registry.get.return_value = mocked_main_window |
2321 | + |
2322 | + # WHEN: start_presentation() is called |
2323 | + self.document.start_presentation() |
2324 | + |
2325 | + # THEN: The presentation should be started |
2326 | + self.mocked_client.start_presentation.assert_called_once_with() |
2327 | + mocked_registry.get.assert_called_once_with('main_window') |
2328 | + mocked_main_window.activateWindow.assert_called_once_with() |
2329 | + |
2330 | + def test_get_slide_number(self): |
2331 | + """ |
2332 | + Test the get_slide_number() method |
2333 | + """ |
2334 | + # GIVEN: A mocked client |
2335 | + self.mocked_client.get_slide_number.return_value = 5 |
2336 | + |
2337 | + # WHEN: get_slide_number() is called |
2338 | + result = self.document.get_slide_number() |
2339 | + |
2340 | + # THEN: Then the result should be correct |
2341 | + assert result == 5 |
2342 | + |
2343 | + def test_get_slide_count(self): |
2344 | + """ |
2345 | + Test the get_slide_count() method |
2346 | + """ |
2347 | + # GIVEN: A mocked client |
2348 | + self.mocked_client.get_slide_count.return_value = 8 |
2349 | + |
2350 | + # WHEN: get_slide_count() is called |
2351 | + result = self.document.get_slide_count() |
2352 | + |
2353 | + # THEN: Then the result should be correct |
2354 | + assert result == 8 |
2355 | + |
2356 | + def test_goto_slide(self): |
2357 | + """ |
2358 | + Test the goto_slide() method |
2359 | + """ |
2360 | + # GIVEN: A mocked client |
2361 | + # WHEN: goto_slide() is called |
2362 | + self.document.goto_slide(3) |
2363 | + |
2364 | + # THEN: Then the result should be correct |
2365 | + self.mocked_client.goto_slide.assert_called_once_with(3) |
2366 | + |
2367 | + def test_next_step(self): |
2368 | + """ |
2369 | + Test the next_step() method |
2370 | + """ |
2371 | + # GIVEN: A mocked client |
2372 | + # WHEN: next_step() is called |
2373 | + self.document.next_step() |
2374 | + |
2375 | + # THEN: Then the result should be correct |
2376 | + self.mocked_client.next_step.assert_called_once_with() |
2377 | + |
2378 | + def test_previous_step(self): |
2379 | + """ |
2380 | + Test the previous_step() method |
2381 | + """ |
2382 | + # GIVEN: A mocked client |
2383 | + # WHEN: previous_step() is called |
2384 | + self.document.previous_step() |
2385 | + |
2386 | + # THEN: Then the result should be correct |
2387 | + self.mocked_client.previous_step.assert_called_once_with() |
2388 | + |
2389 | + def test_get_slide_text(self): |
2390 | + """ |
2391 | + Test the get_slide_text() method |
2392 | + """ |
2393 | + # GIVEN: A mocked client |
2394 | + self.mocked_client.get_slide_text.return_value = 'Some slide text' |
2395 | + |
2396 | + # WHEN: get_slide_text() is called |
2397 | + result = self.document.get_slide_text(1) |
2398 | + |
2399 | + # THEN: Then the result should be correct |
2400 | + self.mocked_client.get_slide_text.assert_called_once_with(1) |
2401 | + assert result == 'Some slide text' |
2402 | + |
2403 | + def test_get_slide_notes(self): |
2404 | + """ |
2405 | + Test the get_slide_notes() method |
2406 | + """ |
2407 | + # GIVEN: A mocked client |
2408 | + self.mocked_client.get_slide_notes.return_value = 'This is a note' |
2409 | + |
2410 | + # WHEN: get_slide_notes() is called |
2411 | + result = self.document.get_slide_notes(2) |
2412 | + |
2413 | + # THEN: Then the result should be correct |
2414 | + self.mocked_client.get_slide_notes.assert_called_once_with(2) |
2415 | + 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.