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

Proposed by Raoul Snyman
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
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.

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 :

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

lp:~raoul-snyman/openlp/pyro-impress updated
2732. By Raoul Snyman

Fix up the tests

2733. By Raoul Snyman

HEAD

2734. By Raoul Snyman

Fix license issues

Unmerged revisions

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