Merge lp:~raoul-snyman/openlp/pyro-impress into lp:openlp

Proposed by Raoul Snyman
Status: Merged
Merged at revision: 2878
Proposed branch: lp:~raoul-snyman/openlp/pyro-impress
Merge into: lp:openlp
Diff against target: 2439 lines (+2223/-56)
14 files modified
.bzrignore (+29/-38)
openlp/core/common/path.py (+2/-0)
openlp/core/ui/media/mediacontroller.py (+2/-0)
openlp/plugins/presentations/lib/libreofficeserver.py (+431/-0)
openlp/plugins/presentations/lib/maclocontroller.py (+266/-0)
openlp/plugins/presentations/lib/presentationcontroller.py (+3/-1)
openlp/plugins/presentations/lib/presentationtab.py (+2/-2)
openlp/plugins/presentations/lib/serializers.py (+52/-0)
openlp/plugins/presentations/lib/vendor/do_not_delete.txt (+5/-0)
openlp/plugins/presentations/presentationplugin.py (+16/-14)
scripts/check_dependencies.py (+2/-0)
tests/functional/openlp_core/common/test_path.py (+12/-1)
tests/functional/openlp_plugins/presentations/test_libreofficeserver.py (+948/-0)
tests/functional/openlp_plugins/presentations/test_maclocontroller.py (+453/-0)
To merge this branch: bzr merge lp:~raoul-snyman/openlp/pyro-impress
Reviewer Review Type Date Requested Status
Tim Bentley Approve
Tomas Groth Pending
Review via email: mp+368362@code.launchpad.net

This proposal supersedes a proposal from 2019-05-22.

Commit message

Add presentations through LibreOffice on macOS.

Description of the change

Add presentations through LibreOffice on macOS. Comments and criticisms welcome.

To post a comment you must log in.
Revision history for this message
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal

Looks fine and does not show up on my machine (Linux).
Needs a respin as will have a conflict with previous merge.

review: Needs Fixing
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linux tests failed, please see https://ci.openlp.io/job/MP-02-Linux_Tests/55/ for more details

Revision history for this message
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!

review: Approve
Revision history for this message
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.

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linux tests failed, please see https://ci.openlp.io/job/MP-02-Linux_Tests/156/ for more details

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linux tests failed, please see https://ci.openlp.io/job/MP-02-Linux_Tests/157/ for more details

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linux tests passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linting passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

macOS tests passed!

Revision history for this message
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal

Headers needs update - see inline.
I tried to test but the MacBook at hand was simply to slow to be of value....

review: Needs Fixing
Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linux tests passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linting passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

macOS tests passed!

Revision history for this message
Tim Bentley (trb143) wrote :

Looks fine but no Mac so no idea if it runs!

review: Approve

Preview Diff

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