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: 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
Tomas Groth Needs Fixing
Tim Bentley Pending
Review via email: mp+367787@code.launchpad.net

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

This proposal has been superseded by a proposal from 2019-06-05.

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 :

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
Tomas Groth (tomasgroth) wrote :

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
lp:~raoul-snyman/openlp/pyro-impress updated
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
=== modified file '.bzrignore'
--- .bzrignore 2019-03-25 21:24:51 +0000
+++ .bzrignore 2019-06-05 04:57:51 +0000
@@ -1,57 +1,48 @@
1*.*~1*.*~
2*.~\?~
3\#*\#
4build
5.cache
6cover
7.coverage
8coverage
9.directory
10.vscode
11dist
12*.dll2*.dll
13documentation/build/doctrees
14documentation/build/html
15*.e4*3*.e4*
16*eric[1-9]project
17.git
18env
19# Git files
20.gitignore
21htmlcov
22.idea
23*.kate-swp4*.kate-swp
24*.kdev45*.kdev4
25.kdev4
26*.komodoproject6*.komodoproject
27.komodotools
28list
29*.log*7*.log*
30*.nja8*.nja
31openlp.cfg
32openlp/core/resources.py.old
33OpenLP.egg-info
34openlp.org 2.0.e4*
35openlp.pro
36openlp-test-projectordb.sqlite
37*.orig9*.orig
38output
39*.pyc10*.pyc
40__pycache__
41.pylint.d
42.pytest_cache
43*.qm11*.qm
44*.rej12*.rej
45# Rejected diff's
46resources/innosetup/Output
47resources/windows/warnOpenLP.txt
48*.ropeproject13*.ropeproject
49tags14*.~\?~
50output15*eric[1-9]project
16.cache
17.coverage
18.directory
19.git
20.gitignore
21.idea
22.kdev4
23.komodotools
24.pylint.d
25.pytest_cache
26.vscode
27OpenLP.egg-info
28\#*\#
29__pycache__
30build
31cover
32coverage
33dist
34env
51htmlcov35htmlcov
36list
52node_modules37node_modules
53openlp-test-projectordb.sqlite38openlp-test-projectordb.sqlite
39openlp.cfg
40openlp.pro
41openlp/core/resources.py.old
42openlp/plugins/presentations/lib/vendor/Pyro4
43openlp/plugins/presentations/lib/vendor/serpent.py
44output
54package-lock.json45package-lock.json
55.cache46tags
56test47test
57tests.kdev448tests.kdev4
5849
=== modified file 'openlp/core/common/path.py'
--- openlp/core/common/path.py 2019-05-22 06:47:00 +0000
+++ openlp/core/common/path.py 2019-06-05 04:57:51 +0000
@@ -78,6 +78,8 @@
78 :return: An empty string if :param:`path` is None, else a string representation of the :param:`path`78 :return: An empty string if :param:`path` is None, else a string representation of the :param:`path`
79 :rtype: str79 :rtype: str
80 """80 """
81 if isinstance(path, str):
82 return path
81 if not isinstance(path, Path) and path is not None:83 if not isinstance(path, Path) and path is not None:
82 raise TypeError('parameter \'path\' must be of type Path or NoneType')84 raise TypeError('parameter \'path\' must be of type Path or NoneType')
83 if path is None:85 if path is None:
8486
=== modified file 'openlp/core/ui/media/mediacontroller.py'
--- openlp/core/ui/media/mediacontroller.py 2019-06-01 06:59:45 +0000
+++ openlp/core/ui/media/mediacontroller.py 2019-06-05 04:57:51 +0000
@@ -104,6 +104,8 @@
104 State().update_pre_conditions('mediacontroller', True)104 State().update_pre_conditions('mediacontroller', True)
105 State().update_pre_conditions('media_live', True)105 State().update_pre_conditions('media_live', True)
106 else:106 else:
107 if hasattr(self.main_window, 'splash') and self.main_window.splash.isVisible():
108 self.main_window.splash.hide()
107 State().missing_text('media_live', translate('OpenLP.SlideController',109 State().missing_text('media_live', translate('OpenLP.SlideController',
108 'VLC or pymediainfo are missing, so you are unable to play any media'))110 'VLC or pymediainfo are missing, so you are unable to play any media'))
109 return True111 return True
110112
=== added file 'openlp/plugins/presentations/lib/libreofficeserver.py'
--- openlp/plugins/presentations/lib/libreofficeserver.py 1970-01-01 00:00:00 +0000
+++ openlp/plugins/presentations/lib/libreofficeserver.py 2019-06-05 04:57:51 +0000
@@ -0,0 +1,431 @@
1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
4##########################################################################
5# OpenLP - Open Source Lyrics Projection #
6# ---------------------------------------------------------------------- #
7# Copyright (c) 2008-2019 OpenLP Developers #
8# ---------------------------------------------------------------------- #
9# This program is free software: you can redistribute it and/or modify #
10# it under the terms of the GNU General Public License as published by #
11# the Free Software Foundation, either version 3 of the License, or #
12# (at your option) any later version. #
13# #
14# This program is distributed in the hope that it will be useful, #
15# but WITHOUT ANY WARRANTY; without even the implied warranty of #
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
17# GNU General Public License for more details. #
18# #
19# You should have received a copy of the GNU General Public License #
20# along with this program. If not, see <https://www.gnu.org/licenses/>. #
21##########################################################################
22"""
23This module runs a Pyro4 server using LibreOffice's version of Python
24
25Please Note: This intentionally uses os.path over pathlib because we don't know which version of Python is shipped with
26the version of LibreOffice on the user's computer.
27"""
28from subprocess import Popen
29import sys
30import os
31import logging
32import time
33
34
35if sys.platform.startswith('darwin'):
36 # Only make the log file on OS X when running as a server
37 logfile = os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'libreofficeserver.log')
38 print('Setting up log file: {logfile}'.format(logfile=logfile))
39 logging.basicConfig(filename=logfile, level=logging.INFO)
40
41
42# Add the current directory to sys.path so that we can load the serializers
43sys.path.append(os.path.join(os.path.dirname(__file__)))
44# Add the vendor directory to sys.path so that we can load Pyro4
45sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor'))
46
47from serializers import register_classes
48from Pyro4 import Daemon, expose
49
50try:
51 # Wrap these imports in a try so that we can run the tests on macOS
52 import uno
53 from com.sun.star.beans import PropertyValue
54 from com.sun.star.task import ErrorCodeIOException
55except ImportError:
56 # But they need to be defined for mocking
57 uno = None
58 PropertyValue = None
59 ErrorCodeIOException = Exception
60
61
62log = logging.getLogger(__name__)
63register_classes()
64
65
66class TextType(object):
67 """
68 Type Enumeration for Types of Text to request
69 """
70 Title = 0
71 SlideText = 1
72 Notes = 2
73
74
75class LibreOfficeException(Exception):
76 """
77 A specific exception for LO
78 """
79 pass
80
81
82@expose
83class LibreOfficeServer(object):
84 """
85 A Pyro4 server which controls LibreOffice
86 """
87 def __init__(self):
88 """
89 Set up the server
90 """
91 self._desktop = None
92 self._control = None
93 self._document = None
94 self._presentation = None
95 self._process = None
96 self._manager = None
97
98 def _create_property(self, name, value):
99 """
100 Create an OOo style property object which are passed into some Uno methods.
101 """
102 log.debug('create property')
103 property_object = PropertyValue()
104 property_object.Name = name
105 property_object.Value = value
106 return property_object
107
108 def _get_text_from_page(self, slide_no, text_type=TextType.SlideText):
109 """
110 Return any text extracted from the presentation page.
111
112 :param slide_no: The slide the notes are required for, starting at 1
113 :param notes: A boolean. If set the method searches the notes of the slide.
114 :param text_type: A TextType. Enumeration of the types of supported text.
115 """
116 text = ''
117 if TextType.Title <= text_type <= TextType.Notes:
118 pages = self._document.getDrawPages()
119 if 0 < slide_no <= pages.getCount():
120 page = pages.getByIndex(slide_no - 1)
121 if text_type == TextType.Notes:
122 page = page.getNotesPage()
123 for index in range(page.getCount()):
124 shape = page.getByIndex(index)
125 shape_type = shape.getShapeType()
126 if shape.supportsService('com.sun.star.drawing.Text'):
127 # if they requested title, make sure it is the title
128 if text_type != TextType.Title or shape_type == 'com.sun.star.presentation.TitleTextShape':
129 text += shape.getString() + '\n'
130 return text
131
132 def start_process(self):
133 """
134 Initialise Impress
135 """
136 uno_command = [
137 '/Applications/LibreOffice.app/Contents/MacOS/soffice',
138 '--nologo',
139 '--norestore',
140 '--minimized',
141 '--nodefault',
142 '--nofirststartwizard',
143 '--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager'
144 ]
145 self._process = Popen(uno_command)
146
147 @property
148 def desktop(self):
149 """
150 Set up an UNO desktop instance
151 """
152 if self._desktop is not None:
153 return self._desktop
154 uno_instance = None
155 context = uno.getComponentContext()
156 resolver = context.ServiceManager.createInstanceWithContext('com.sun.star.bridge.UnoUrlResolver', context)
157 loop = 0
158 while uno_instance is None and loop < 3:
159 try:
160 uno_instance = resolver.resolve('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext')
161 except Exception:
162 log.exception('Unable to find running instance, retrying...')
163 loop += 1
164 try:
165 self._manager = uno_instance.ServiceManager
166 log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop')
167 desktop = self._manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance)
168 if not desktop:
169 raise Exception('Failed to get UNO desktop')
170 self._desktop = desktop
171 return desktop
172 except Exception:
173 log.exception('Failed to get UNO desktop')
174 return None
175
176 def shutdown(self):
177 """
178 Shut down the server
179 """
180 can_kill = True
181 if hasattr(self, '_docs'):
182 while self._docs:
183 self._docs[0].close_presentation()
184 docs = self.desktop.getComponents()
185 count = 0
186 if docs.hasElements():
187 list_elements = docs.createEnumeration()
188 while list_elements.hasMoreElements():
189 doc = list_elements.nextElement()
190 if doc.getImplementationName() != 'com.sun.star.comp.framework.BackingComp':
191 count += 1
192 if count > 0:
193 log.debug('LibreOffice not terminated as docs are still open')
194 can_kill = False
195 else:
196 try:
197 self.desktop.terminate()
198 log.debug('LibreOffice killed')
199 except Exception:
200 log.exception('Failed to terminate LibreOffice')
201 if getattr(self, '_process') and can_kill:
202 self._process.kill()
203
204 def load_presentation(self, file_path, screen_number):
205 """
206 Load a presentation
207 """
208 self._file_path = file_path
209 url = uno.systemPathToFileUrl(file_path)
210 properties = (self._create_property('Hidden', True),)
211 self._document = None
212 loop_count = 0
213 while loop_count < 3:
214 try:
215 self._document = self.desktop.loadComponentFromURL(url, '_blank', 0, properties)
216 except Exception:
217 log.exception('Failed to load presentation {url}'.format(url=url))
218 if self._document:
219 break
220 time.sleep(0.5)
221 loop_count += 1
222 if loop_count == 3:
223 log.error('Looped too many times')
224 return False
225 self._presentation = self._document.getPresentation()
226 self._presentation.Display = screen_number
227 self._control = None
228 return True
229
230 def extract_thumbnails(self, temp_folder):
231 """
232 Create thumbnails for the presentation
233 """
234 thumbnails = []
235 thumb_dir_url = uno.systemPathToFileUrl(temp_folder)
236 properties = (self._create_property('FilterName', 'impress_png_Export'),)
237 pages = self._document.getDrawPages()
238 if not pages:
239 return []
240 if not os.path.isdir(temp_folder):
241 os.makedirs(temp_folder)
242 for index in range(pages.getCount()):
243 page = pages.getByIndex(index)
244 self._document.getCurrentController().setCurrentPage(page)
245 url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1))
246 path = os.path.join(temp_folder, str(index + 1) + '.png')
247 try:
248 self._document.storeToURL(url_path, properties)
249 thumbnails.append(path)
250 except ErrorCodeIOException as exception:
251 log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode))
252 except Exception:
253 log.exception('{path} - Unable to store openoffice preview'.format(path=path))
254 return thumbnails
255
256 def get_titles_and_notes(self):
257 """
258 Extract the titles and the notes from the slides.
259 """
260 titles = []
261 notes = []
262 pages = self._document.getDrawPages()
263 for slide_no in range(1, pages.getCount() + 1):
264 titles.append(self._get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n')
265 note = self._get_text_from_page(slide_no, TextType.Notes)
266 if len(note) == 0:
267 note = ' '
268 notes.append(note)
269 return titles, notes
270
271 def close_presentation(self):
272 """
273 Close presentation and clean up objects.
274 """
275 log.debug('close Presentation LibreOffice')
276 if self._document:
277 if self._presentation:
278 try:
279 self._presentation.end()
280 self._presentation = None
281 self._document.dispose()
282 except Exception:
283 log.exception("Closing presentation failed")
284 self._document = None
285
286 def is_loaded(self):
287 """
288 Returns true if a presentation is loaded.
289 """
290 log.debug('is loaded LibreOffice')
291 if self._presentation is None or self._document is None:
292 log.debug("is_loaded: no presentation or document")
293 return False
294 try:
295 if self._document.getPresentation() is None:
296 log.debug("getPresentation failed to find a presentation")
297 return False
298 except Exception:
299 log.exception("getPresentation failed to find a presentation")
300 return False
301 return True
302
303 def is_active(self):
304 """
305 Returns true if a presentation is active and running.
306 """
307 log.debug('is active LibreOffice')
308 if not self.is_loaded():
309 return False
310 return self._control.isRunning() if self._control else False
311
312 def unblank_screen(self):
313 """
314 Unblanks the screen.
315 """
316 log.debug('unblank screen LibreOffice')
317 return self._control.resume()
318
319 def blank_screen(self):
320 """
321 Blanks the screen.
322 """
323 log.debug('blank screen LibreOffice')
324 self._control.blankScreen(0)
325
326 def is_blank(self):
327 """
328 Returns true if screen is blank.
329 """
330 log.debug('is blank LibreOffice')
331 if self._control and self._control.isRunning():
332 return self._control.isPaused()
333 else:
334 return False
335
336 def stop_presentation(self):
337 """
338 Stop the presentation, remove from screen.
339 """
340 log.debug('stop presentation LibreOffice')
341 self._presentation.end()
342 self._control = None
343
344 def start_presentation(self):
345 """
346 Start the presentation from the beginning.
347 """
348 log.debug('start presentation LibreOffice')
349 if self._control is None or not self._control.isRunning():
350 window = self._document.getCurrentController().getFrame().getContainerWindow()
351 window.setVisible(True)
352 self._presentation.start()
353 self._control = self._presentation.getController()
354 # start() returns before the Component is ready. Try for 15 seconds.
355 sleep_count = 1
356 while not self._control and sleep_count < 150:
357 time.sleep(0.1)
358 sleep_count += 1
359 self._control = self._presentation.getController()
360 window.setVisible(False)
361 else:
362 self._control.activate()
363 self.goto_slide(1)
364
365 def get_slide_number(self):
366 """
367 Return the current slide number on the screen, from 1.
368 """
369 return self._control.getCurrentSlideIndex() + 1
370
371 def get_slide_count(self):
372 """
373 Return the total number of slides.
374 """
375 return self._document.getDrawPages().getCount()
376
377 def goto_slide(self, slide_no):
378 """
379 Go to a specific slide (from 1).
380
381 :param slide_no: The slide the text is required for, starting at 1
382 """
383 self._control.gotoSlideIndex(slide_no - 1)
384
385 def next_step(self):
386 """
387 Triggers the next effect of slide on the running presentation.
388 """
389 is_paused = self._control.isPaused()
390 self._control.gotoNextEffect()
391 time.sleep(0.1)
392 if not is_paused and self._control.isPaused():
393 self._control.gotoPreviousEffect()
394
395 def previous_step(self):
396 """
397 Triggers the previous slide on the running presentation.
398 """
399 self._control.gotoPreviousEffect()
400
401 def get_slide_text(self, slide_no):
402 """
403 Returns the text on the slide.
404
405 :param slide_no: The slide the text is required for, starting at 1
406 """
407 return self._get_text_from_page(slide_no)
408
409 def get_slide_notes(self, slide_no):
410 """
411 Returns the text in the slide notes.
412
413 :param slide_no: The slide the notes are required for, starting at 1
414 """
415 return self._get_text_from_page(slide_no, TextType.Notes)
416
417
418def main():
419 """
420 The main function which runs the server
421 """
422 daemon = Daemon(host='localhost', port=4310)
423 daemon.register(LibreOfficeServer, 'openlp.libreofficeserver')
424 try:
425 daemon.requestLoop()
426 finally:
427 daemon.close()
428
429
430if __name__ == '__main__':
431 main()
0432
=== added file 'openlp/plugins/presentations/lib/maclocontroller.py'
--- openlp/plugins/presentations/lib/maclocontroller.py 1970-01-01 00:00:00 +0000
+++ openlp/plugins/presentations/lib/maclocontroller.py 2019-06-05 04:57:51 +0000
@@ -0,0 +1,266 @@
1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
4##########################################################################
5# OpenLP - Open Source Lyrics Projection #
6# ---------------------------------------------------------------------- #
7# Copyright (c) 2008-2019 OpenLP Developers #
8# ---------------------------------------------------------------------- #
9# This program is free software: you can redistribute it and/or modify #
10# it under the terms of the GNU General Public License as published by #
11# the Free Software Foundation, either version 3 of the License, or #
12# (at your option) any later version. #
13# #
14# This program is distributed in the hope that it will be useful, #
15# but WITHOUT ANY WARRANTY; without even the implied warranty of #
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
17# GNU General Public License for more details. #
18# #
19# You should have received a copy of the GNU General Public License #
20# along with this program. If not, see <https://www.gnu.org/licenses/>. #
21##########################################################################
22
23import logging
24from subprocess import Popen
25
26from Pyro4 import Proxy
27
28from openlp.core.common import delete_file, is_macosx
29from openlp.core.common.applocation import AppLocation
30from openlp.core.common.path import Path
31from openlp.core.common.registry import Registry
32from openlp.core.display.screens import ScreenList
33from openlp.plugins.presentations.lib.serializers import register_classes
34from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
35
36
37LIBREOFFICE_PATH = Path('/Applications/LibreOffice.app')
38LIBREOFFICE_PYTHON = LIBREOFFICE_PATH / 'Contents' / 'Resources' / 'python'
39
40if is_macosx() and LIBREOFFICE_PATH.exists():
41 macuno_available = True
42else:
43 macuno_available = False
44
45
46log = logging.getLogger(__name__)
47register_classes()
48
49
50class MacLOController(PresentationController):
51 """
52 Class to control interactions with MacLO presentations on Mac OS X via Pyro4. It starts the Pyro4 nameserver,
53 starts the LibreOfficeServer, and then controls MacLO via Pyro4.
54 """
55 log.info('MacLOController loaded')
56
57 def __init__(self, plugin):
58 """
59 Initialise the class
60 """
61 log.debug('Initialising')
62 super(MacLOController, self).__init__(plugin, 'maclo', MacLODocument, 'Impress on macOS')
63 self.supports = ['odp']
64 self.also_supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm']
65 self.server_process = None
66 self._client = None
67 self._start_server()
68
69 def _start_server(self):
70 """
71 Start a LibreOfficeServer
72 """
73 libreoffice_python = Path('/Applications/LibreOffice.app/Contents/Resources/python')
74 libreoffice_server = AppLocation.get_directory(AppLocation.PluginsDir).joinpath('presentations', 'lib',
75 'libreofficeserver.py')
76 if libreoffice_python.exists():
77 self.server_process = Popen([str(libreoffice_python), str(libreoffice_server)])
78
79 @property
80 def client(self):
81 """
82 Set up a Pyro4 client so that we can talk to the LibreOfficeServer
83 """
84 if not self._client:
85 self._client = Proxy('PYRO:openlp.libreofficeserver@localhost:4310')
86 if not self._client._pyroConnection:
87 self._client._pyroReconnect()
88 return self._client
89
90 def check_available(self):
91 """
92 MacLO is able to run on this machine.
93 """
94 log.debug('check_available')
95 return macuno_available
96
97 def start_process(self):
98 """
99 Loads a running version of LibreOffice in the background. It is not displayed to the user but is available to
100 the UNO interface when required.
101 """
102 log.debug('Started automatically by the Pyro server')
103 self.client.start_process()
104
105 def kill(self):
106 """
107 Called at system exit to clean up any running presentations.
108 """
109 log.debug('Kill LibreOffice')
110 self.client.shutdown()
111 self.server_process.kill()
112
113
114class MacLODocument(PresentationDocument):
115 """
116 Class which holds information and controls a single presentation.
117 """
118
119 def __init__(self, controller, presentation):
120 """
121 Constructor, store information about the file and initialise.
122 """
123 log.debug('Init Presentation LibreOffice')
124 super(MacLODocument, self).__init__(controller, presentation)
125 self.client = controller.client
126
127 def load_presentation(self):
128 """
129 Tell the LibreOfficeServer to start the presentation.
130 """
131 log.debug('Load Presentation LibreOffice')
132 if not self.client.load_presentation(str(self.file_path), ScreenList().current.number + 1):
133 return False
134 self.create_thumbnails()
135 self.create_titles_and_notes()
136 return True
137
138 def create_thumbnails(self):
139 """
140 Create thumbnail images for presentation.
141 """
142 log.debug('create thumbnails LibreOffice')
143 if self.check_thumbnails():
144 return
145 temp_thumbnails = self.client.extract_thumbnails(str(self.get_temp_folder()))
146 for index, temp_thumb in enumerate(temp_thumbnails):
147 temp_thumb = Path(temp_thumb)
148 self.convert_thumbnail(temp_thumb, index + 1)
149 delete_file(temp_thumb)
150
151 def create_titles_and_notes(self):
152 """
153 Writes the list of titles (one per slide) to 'titles.txt' and the notes to 'slideNotes[x].txt'
154 in the thumbnails directory
155 """
156 titles, notes = self.client.get_titles_and_notes()
157 self.save_titles_and_notes(titles, notes)
158
159 def close_presentation(self):
160 """
161 Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
162 shutdown.
163 """
164 log.debug('close Presentation LibreOffice')
165 self.client.close_presentation()
166 self.controller.remove_doc(self)
167
168 def is_loaded(self):
169 """
170 Returns true if a presentation is loaded.
171 """
172 log.debug('is loaded LibreOffice')
173 return self.client.is_loaded()
174
175 def is_active(self):
176 """
177 Returns true if a presentation is active and running.
178 """
179 log.debug('is active LibreOffice')
180 return self.client.is_active()
181
182 def unblank_screen(self):
183 """
184 Unblanks the screen.
185 """
186 log.debug('unblank screen LibreOffice')
187 return self.client.unblank_screen()
188
189 def blank_screen(self):
190 """
191 Blanks the screen.
192 """
193 log.debug('blank screen LibreOffice')
194 self.client.blank_screen()
195
196 def is_blank(self):
197 """
198 Returns true if screen is blank.
199 """
200 log.debug('is blank LibreOffice')
201 return self.client.is_blank()
202
203 def stop_presentation(self):
204 """
205 Stop the presentation, remove from screen.
206 """
207 log.debug('stop presentation LibreOffice')
208 self.client.stop_presentation()
209
210 def start_presentation(self):
211 """
212 Start the presentation from the beginning.
213 """
214 log.debug('start presentation LibreOffice')
215 self.client.start_presentation()
216 # Make sure impress doesn't steal focus, unless we're on a single screen setup
217 if len(ScreenList()) > 1:
218 Registry().get('main_window').activateWindow()
219
220 def get_slide_number(self):
221 """
222 Return the current slide number on the screen, from 1.
223 """
224 return self.client.get_slide_number()
225
226 def get_slide_count(self):
227 """
228 Return the total number of slides.
229 """
230 return self.client.get_slide_count()
231
232 def goto_slide(self, slide_no):
233 """
234 Go to a specific slide (from 1).
235
236 :param slide_no: The slide the text is required for, starting at 1
237 """
238 self.client.goto_slide(slide_no)
239
240 def next_step(self):
241 """
242 Triggers the next effect of slide on the running presentation.
243 """
244 self.client.next_step()
245
246 def previous_step(self):
247 """
248 Triggers the previous slide on the running presentation.
249 """
250 self.client.previous_step()
251
252 def get_slide_text(self, slide_no):
253 """
254 Returns the text on the slide.
255
256 :param slide_no: The slide the text is required for, starting at 1
257 """
258 return self.client.get_slide_text(slide_no)
259
260 def get_slide_notes(self, slide_no):
261 """
262 Returns the text in the slide notes.
263
264 :param slide_no: The slide the notes are required for, starting at 1
265 """
266 return self.client.get_slide_notes(slide_no)
0267
=== modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
--- openlp/plugins/presentations/lib/presentationcontroller.py 2019-05-22 06:47:00 +0000
+++ openlp/plugins/presentations/lib/presentationcontroller.py 2019-06-05 04:57:51 +0000
@@ -410,7 +410,8 @@
410 """410 """
411 log.info('PresentationController loaded')411 log.info('PresentationController loaded')
412412
413 def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument):413 def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument,
414 display_name=None):
414 """415 """
415 This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins416 This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins
416417
@@ -430,6 +431,7 @@
430 self.docs = []431 self.docs = []
431 self.plugin = plugin432 self.plugin = plugin
432 self.name = name433 self.name = name
434 self.display_name = display_name if display_name is not None else name
433 self.document_class = document_class435 self.document_class = document_class
434 self.settings_section = self.plugin.settings_section436 self.settings_section = self.plugin.settings_section
435 self.available = None437 self.available = None
436438
=== modified file 'openlp/plugins/presentations/lib/presentationtab.py'
--- openlp/plugins/presentations/lib/presentationtab.py 2019-05-22 06:47:00 +0000
+++ openlp/plugins/presentations/lib/presentationtab.py 2019-06-05 04:57:51 +0000
@@ -127,10 +127,10 @@
127127
128 def set_controller_text(self, checkbox, controller):128 def set_controller_text(self, checkbox, controller):
129 if checkbox.isEnabled():129 if checkbox.isEnabled():
130 checkbox.setText(controller.name)130 checkbox.setText(controller.display_name)
131 else:131 else:
132 checkbox.setText(translate('PresentationPlugin.PresentationTab',132 checkbox.setText(translate('PresentationPlugin.PresentationTab',
133 '{name} (unavailable)').format(name=controller.name))133 '{name} (unavailable)').format(name=controller.display_name))
134134
135 def load(self):135 def load(self):
136 """136 """
137137
=== added file 'openlp/plugins/presentations/lib/serializers.py'
--- openlp/plugins/presentations/lib/serializers.py 1970-01-01 00:00:00 +0000
+++ openlp/plugins/presentations/lib/serializers.py 2019-06-05 04:57:51 +0000
@@ -0,0 +1,52 @@
1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
4##########################################################################
5# OpenLP - Open Source Lyrics Projection #
6# ---------------------------------------------------------------------- #
7# Copyright (c) 2008-2019 OpenLP Developers #
8# ---------------------------------------------------------------------- #
9# This program is free software: you can redistribute it and/or modify #
10# it under the terms of the GNU General Public License as published by #
11# the Free Software Foundation, either version 3 of the License, or #
12# (at your option) any later version. #
13# #
14# This program is distributed in the hope that it will be useful, #
15# but WITHOUT ANY WARRANTY; without even the implied warranty of #
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
17# GNU General Public License for more details. #
18# #
19# You should have received a copy of the GNU General Public License #
20# along with this program. If not, see <https://www.gnu.org/licenses/>. #
21##########################################################################
22"""
23This module contains some helpers for serializing Path objects in Pyro4
24"""
25try:
26 from openlp.core.common.path import Path
27except ImportError:
28 from pathlib import Path
29
30from Pyro4.util import SerializerBase
31
32
33def path_class_to_dict(obj):
34 """
35 Serialize a Path object for Pyro4
36 """
37 return {
38 '__class__': 'Path',
39 'parts': obj.parts
40 }
41
42
43def path_dict_to_class(classname, d):
44 return Path(d['parts'])
45
46
47def register_classes():
48 """
49 Register the serializers
50 """
51 SerializerBase.register_class_to_dict(Path, path_class_to_dict)
52 SerializerBase.register_dict_to_class('Path', path_dict_to_class)
053
=== added directory 'openlp/plugins/presentations/lib/vendor'
=== added file 'openlp/plugins/presentations/lib/vendor/do_not_delete.txt'
--- openlp/plugins/presentations/lib/vendor/do_not_delete.txt 1970-01-01 00:00:00 +0000
+++ openlp/plugins/presentations/lib/vendor/do_not_delete.txt 2019-06-05 04:57:51 +0000
@@ -0,0 +1,5 @@
1Vendor Directory
2================
3
4Do not delete this directory, it is used on Mac OS to place Pyro4 and serpent for use with Impress.
5
06
=== modified file 'openlp/plugins/presentations/presentationplugin.py'
--- openlp/plugins/presentations/presentationplugin.py 2019-04-13 13:00:22 +0000
+++ openlp/plugins/presentations/presentationplugin.py 2019-06-05 04:57:51 +0000
@@ -28,13 +28,13 @@
2828
29from PyQt5 import QtCore29from PyQt5 import QtCore
3030
31from openlp.core.state import State
32from openlp.core.api.http import register_endpoint31from openlp.core.api.http import register_endpoint
33from openlp.core.common import extension_loader32from openlp.core.common import extension_loader
34from openlp.core.common.i18n import translate33from openlp.core.common.i18n import translate
35from openlp.core.common.settings import Settings34from openlp.core.common.settings import Settings
36from openlp.core.lib import build_icon35from openlp.core.lib import build_icon
37from openlp.core.lib.plugin import Plugin, StringContent36from openlp.core.lib.plugin import Plugin, StringContent
37from openlp.core.state import State
38from openlp.core.ui.icons import UiIcons38from openlp.core.ui.icons import UiIcons
39from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint39from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint
40from openlp.plugins.presentations.lib.presentationcontroller import PresentationController40from openlp.plugins.presentations.lib.presentationcontroller import PresentationController
@@ -45,18 +45,20 @@
45log = logging.getLogger(__name__)45log = logging.getLogger(__name__)
4646
4747
48__default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked,48__default_settings__ = {
49 'presentations/enable_pdf_program': QtCore.Qt.Unchecked,49 'presentations/override app': QtCore.Qt.Unchecked,
50 'presentations/pdf_program': None,50 'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
51 'presentations/Impress': QtCore.Qt.Checked,51 'presentations/pdf_program': None,
52 'presentations/Powerpoint': QtCore.Qt.Checked,52 'presentations/maclo': QtCore.Qt.Checked,
53 'presentations/Pdf': QtCore.Qt.Checked,53 'presentations/Impress': QtCore.Qt.Checked,
54 'presentations/presentations files': [],54 'presentations/Powerpoint': QtCore.Qt.Checked,
55 'presentations/thumbnail_scheme': '',55 'presentations/Pdf': QtCore.Qt.Checked,
56 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked,56 'presentations/presentations files': [],
57 'presentations/powerpoint control window': QtCore.Qt.Unchecked,57 'presentations/thumbnail_scheme': '',
58 'presentations/last directory': None58 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked,
59 }59 'presentations/powerpoint control window': QtCore.Qt.Unchecked,
60 'presentations/last directory': None
61}
6062
6163
62class PresentationPlugin(Plugin):64class PresentationPlugin(Plugin):
@@ -100,7 +102,7 @@
100 try:102 try:
101 self.controllers[controller].start_process()103 self.controllers[controller].start_process()
102 except Exception:104 except Exception:
103 log.warning('Failed to start controller process')105 log.exception('Failed to start controller process')
104 self.controllers[controller].available = False106 self.controllers[controller].available = False
105 self.media_item.build_file_mask_string()107 self.media_item.build_file_mask_string()
106108
107109
=== modified file 'scripts/check_dependencies.py'
--- scripts/check_dependencies.py 2019-05-05 08:13:10 +0000
+++ scripts/check_dependencies.py 2019-06-05 04:57:51 +0000
@@ -159,6 +159,8 @@
159 w('OK')159 w('OK')
160 except ImportError:160 except ImportError:
161 w('FAIL')161 w('FAIL')
162 except Exception:
163 w('ERROR')
162 w(os.linesep)164 w(os.linesep)
163165
164166
165167
=== modified file 'tests/functional/openlp_core/common/test_path.py'
--- tests/functional/openlp_core/common/test_path.py 2019-05-26 10:30:37 +0000
+++ tests/functional/openlp_core/common/test_path.py 2019-06-05 04:57:51 +0000
@@ -110,7 +110,18 @@
110 # WHEN: Calling `path_to_str` with an invalid Type110 # WHEN: Calling `path_to_str` with an invalid Type
111 # THEN: A TypeError should have been raised111 # THEN: A TypeError should have been raised
112 with self.assertRaises(TypeError):112 with self.assertRaises(TypeError):
113 path_to_str(str())113 path_to_str(57)
114
115 def test_path_to_str_wth_str(self):
116 """
117 Test that `path_to_str` just returns a str when given a str
118 """
119 # GIVEN: The `path_to_str` function
120 # WHEN: Calling `path_to_str` with a str
121 result = path_to_str('/usr/bin')
122
123 # THEN: The string should be returned
124 assert result == '/usr/bin'
114125
115 def test_path_to_str_none(self):126 def test_path_to_str_none(self):
116 """127 """
117128
=== added file 'tests/functional/openlp_plugins/presentations/test_libreofficeserver.py'
--- tests/functional/openlp_plugins/presentations/test_libreofficeserver.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/presentations/test_libreofficeserver.py 2019-06-05 04:57:51 +0000
@@ -0,0 +1,948 @@
1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
4##########################################################################
5# OpenLP - Open Source Lyrics Projection #
6# ---------------------------------------------------------------------- #
7# Copyright (c) 2008-2019 OpenLP Developers #
8# ---------------------------------------------------------------------- #
9# This program is free software: you can redistribute it and/or modify #
10# it under the terms of the GNU General Public License as published by #
11# the Free Software Foundation, either version 3 of the License, or #
12# (at your option) any later version. #
13# #
14# This program is distributed in the hope that it will be useful, #
15# but WITHOUT ANY WARRANTY; without even the implied warranty of #
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
17# GNU General Public License for more details. #
18# #
19# You should have received a copy of the GNU General Public License #
20# along with this program. If not, see <https://www.gnu.org/licenses/>. #
21##########################################################################
22"""
23Functional tests to test the LibreOffice Pyro server
24"""
25from unittest.mock import MagicMock, patch, call
26
27from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType, main
28
29
30def test_constructor():
31 """
32 Test the Constructor from the server
33 """
34 # GIVEN: No server
35 # WHEN: The server object is created
36 server = LibreOfficeServer()
37
38 # THEN: The server should have been set up correctly
39 assert server._control is None
40 # assert server._desktop is None
41 assert server._document is None
42 assert server._presentation is None
43 assert server._process is None
44
45
46@patch('openlp.plugins.presentations.lib.libreofficeserver.Popen')
47def test_start_process(MockedPopen):
48 """
49 Test that the correct command is issued to run LibreOffice
50 """
51 # GIVEN: A LOServer
52 mocked_process = MagicMock()
53 MockedPopen.return_value = mocked_process
54 server = LibreOfficeServer()
55
56 # WHEN: The start_process() method is run
57 server.start_process()
58
59 # THEN: The correct command line should run and the process should have started
60 MockedPopen.assert_called_with([
61 '/Applications/LibreOffice.app/Contents/MacOS/soffice',
62 '--nologo',
63 '--norestore',
64 '--minimized',
65 '--nodefault',
66 '--nofirststartwizard',
67 '--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager'
68 ])
69 assert server._process is mocked_process
70
71
72@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
73def test_desktop_already_has_desktop(mocked_uno):
74 """
75 Test that setup_desktop() exits early when there's already a desktop
76 """
77 # GIVEN: A LibreOfficeServer instance
78 server = LibreOfficeServer()
79 server._desktop = MagicMock()
80
81 # WHEN: the desktop property is called
82 desktop = server.desktop
83
84 # THEN: setup_desktop() exits early
85 assert desktop is server._desktop
86 assert server._manager is None
87
88
89@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
90def test_desktop_exception(mocked_uno):
91 """
92 Test that setting up the desktop works correctly when an exception occurs
93 """
94 # GIVEN: A LibreOfficeServer instance
95 server = LibreOfficeServer()
96 mocked_context = MagicMock()
97 mocked_resolver = MagicMock()
98 mocked_uno_instance = MagicMock()
99 MockedServiceManager = MagicMock()
100 mocked_uno.getComponentContext.return_value = mocked_context
101 mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver
102 mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance]
103 mocked_uno_instance.ServiceManager = MockedServiceManager
104 MockedServiceManager.createInstanceWithContext.side_effect = Exception()
105
106 # WHEN: the desktop property is called
107 server.desktop
108
109 # THEN: A desktop object was created
110 mocked_uno.getComponentContext.assert_called_once_with()
111 mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with(
112 'com.sun.star.bridge.UnoUrlResolver', mocked_context)
113 expected_calls = [
114 call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'),
115 call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext')
116 ]
117 assert mocked_resolver.resolve.call_args_list == expected_calls
118 MockedServiceManager.createInstanceWithContext.assert_called_once_with(
119 'com.sun.star.frame.Desktop', mocked_uno_instance)
120 assert server._manager is MockedServiceManager
121 assert server._desktop is None
122
123
124@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
125def test_desktop(mocked_uno):
126 """
127 Test that setting up the desktop works correctly
128 """
129 # GIVEN: A LibreOfficeServer instance
130 server = LibreOfficeServer()
131 mocked_context = MagicMock()
132 mocked_resolver = MagicMock()
133 mocked_uno_instance = MagicMock()
134 MockedServiceManager = MagicMock()
135 mocked_desktop = MagicMock()
136 mocked_uno.getComponentContext.return_value = mocked_context
137 mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver
138 mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance]
139 mocked_uno_instance.ServiceManager = MockedServiceManager
140 MockedServiceManager.createInstanceWithContext.return_value = mocked_desktop
141
142 # WHEN: the desktop property is called
143 server.desktop
144
145 # THEN: A desktop object was created
146 mocked_uno.getComponentContext.assert_called_once_with()
147 mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with(
148 'com.sun.star.bridge.UnoUrlResolver', mocked_context)
149 expected_calls = [
150 call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'),
151 call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext')
152 ]
153 assert mocked_resolver.resolve.call_args_list == expected_calls
154 MockedServiceManager.createInstanceWithContext.assert_called_once_with(
155 'com.sun.star.frame.Desktop', mocked_uno_instance)
156 assert server._manager is MockedServiceManager
157 assert server._desktop is mocked_desktop
158
159
160@patch('openlp.plugins.presentations.lib.libreofficeserver.PropertyValue')
161def test_create_property(MockedPropertyValue):
162 """
163 Test that the _create_property() method works correctly
164 """
165 # GIVEN: A server amnd property to set
166 server = LibreOfficeServer()
167 name = 'Hidden'
168 value = True
169
170 # WHEN: The _create_property() method is called
171 prop = server._create_property(name, value)
172
173 # THEN: The property should have the correct attributes
174 assert prop.Name == name
175 assert prop.Value == value
176
177
178def test_get_text_from_page_slide_text():
179 """
180 Test that the _get_text_from_page() method gives us nothing for slide text
181 """
182 # GIVEN: A LibreOfficeServer object and some mocked objects
183 text_type = TextType.SlideText
184 slide_no = 1
185 server = LibreOfficeServer()
186 server._document = MagicMock()
187 mocked_pages = MagicMock()
188 mocked_page = MagicMock()
189 mocked_shape = MagicMock()
190 server._document.getDrawPages.return_value = mocked_pages
191 mocked_pages.getCount.return_value = 1
192 mocked_pages.getByIndex.return_value = mocked_page
193 mocked_page.getByIndex.return_value = mocked_shape
194 mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
195 mocked_shape.supportsService.return_value = True
196 mocked_shape.getString.return_value = 'Page Text'
197
198 # WHEN: _get_text_from_page() is run for slide text
199 text = server._get_text_from_page(slide_no, text_type)
200
201 # THE: The text is correct
202 assert text == 'Page Text\n'
203
204
205def test_get_text_from_page_title():
206 """
207 Test that the _get_text_from_page() method gives us the text from the titles
208 """
209 # GIVEN: A LibreOfficeServer object and some mocked objects
210 text_type = TextType.Title
211 slide_no = 1
212 server = LibreOfficeServer()
213 server._document = MagicMock()
214 mocked_pages = MagicMock()
215 mocked_page = MagicMock()
216 mocked_shape = MagicMock()
217 server._document.getDrawPages.return_value = mocked_pages
218 mocked_pages.getCount.return_value = 1
219 mocked_pages.getByIndex.return_value = mocked_page
220 mocked_page.getByIndex.return_value = mocked_shape
221 mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
222 mocked_shape.supportsService.return_value = True
223 mocked_shape.getString.return_value = 'Page Title'
224
225 # WHEN: _get_text_from_page() is run for titles
226 text = server._get_text_from_page(slide_no, text_type)
227
228 # THEN: The text should be correct
229 assert text == 'Page Title\n'
230
231
232def test_get_text_from_page_notes():
233 """
234 Test that the _get_text_from_page() method gives us the text from the notes
235 """
236 # GIVEN: A LibreOfficeServer object and some mocked objects
237 text_type = TextType.Notes
238 slide_no = 1
239 server = LibreOfficeServer()
240 server._document = MagicMock()
241 mocked_pages = MagicMock()
242 mocked_page = MagicMock()
243 mocked_notes_page = MagicMock()
244 mocked_shape = MagicMock()
245 server._document.getDrawPages.return_value = mocked_pages
246 mocked_pages.getCount.return_value = 1
247 mocked_pages.getByIndex.return_value = mocked_page
248 mocked_page.getNotesPage.return_value = mocked_notes_page
249 mocked_notes_page.getByIndex.return_value = mocked_shape
250 mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
251 mocked_shape.supportsService.return_value = True
252 mocked_shape.getString.return_value = 'Page Notes'
253
254 # WHEN: _get_text_from_page() is run for titles
255 text = server._get_text_from_page(slide_no, text_type)
256
257 # THEN: The text should be correct
258 assert text == 'Page Notes\n'
259
260
261def test_shutdown_other_docs():
262 """
263 Test the shutdown method while other documents are open in LibreOffice
264 """
265 def close_docs():
266 server._docs = []
267
268 # GIVEN: An up an running LibreOfficeServer
269 server = LibreOfficeServer()
270 mocked_doc = MagicMock()
271 mocked_desktop = MagicMock()
272 mocked_docs = MagicMock()
273 mocked_list = MagicMock()
274 mocked_element_doc = MagicMock()
275 server._docs = [mocked_doc]
276 server._desktop = mocked_desktop
277 server._process = MagicMock()
278 mocked_doc.close_presentation.side_effect = close_docs
279 mocked_desktop.getComponents.return_value = mocked_docs
280 mocked_docs.hasElements.return_value = True
281 mocked_docs.createEnumeration.return_value = mocked_list
282 mocked_list.hasMoreElements.side_effect = [True, False]
283 mocked_list.nextElement.return_value = mocked_element_doc
284 mocked_element_doc.getImplementationName.side_effect = [
285 'org.openlp.Nothing',
286 'com.sun.star.comp.framework.BackingComp'
287 ]
288
289 # WHEN: shutdown() is called
290 server.shutdown()
291
292 # THEN: The right methods are called and everything works
293 mocked_doc.close_presentation.assert_called_once_with()
294 mocked_desktop.getComponents.assert_called_once_with()
295 mocked_docs.hasElements.assert_called_once_with()
296 mocked_docs.createEnumeration.assert_called_once_with()
297 assert mocked_list.hasMoreElements.call_count == 2
298 mocked_list.nextElement.assert_called_once_with()
299 mocked_element_doc.getImplementationName.assert_called_once_with()
300 assert mocked_desktop.terminate.call_count == 0
301 assert server._process.kill.call_count == 0
302
303
304def test_shutdown():
305 """
306 Test the shutdown method
307 """
308 def close_docs():
309 server._docs = []
310
311 # GIVEN: An up an running LibreOfficeServer
312 server = LibreOfficeServer()
313 mocked_doc = MagicMock()
314 mocked_desktop = MagicMock()
315 mocked_docs = MagicMock()
316 mocked_list = MagicMock()
317 mocked_element_doc = MagicMock()
318 server._docs = [mocked_doc]
319 server._desktop = mocked_desktop
320 server._process = MagicMock()
321 mocked_doc.close_presentation.side_effect = close_docs
322 mocked_desktop.getComponents.return_value = mocked_docs
323 mocked_docs.hasElements.return_value = True
324 mocked_docs.createEnumeration.return_value = mocked_list
325 mocked_list.hasMoreElements.side_effect = [True, False]
326 mocked_list.nextElement.return_value = mocked_element_doc
327 mocked_element_doc.getImplementationName.return_value = 'com.sun.star.comp.framework.BackingComp'
328
329 # WHEN: shutdown() is called
330 server.shutdown()
331
332 # THEN: The right methods are called and everything works
333 mocked_doc.close_presentation.assert_called_once_with()
334 mocked_desktop.getComponents.assert_called_once_with()
335 mocked_docs.hasElements.assert_called_once_with()
336 mocked_docs.createEnumeration.assert_called_once_with()
337 assert mocked_list.hasMoreElements.call_count == 2
338 mocked_list.nextElement.assert_called_once_with()
339 mocked_element_doc.getImplementationName.assert_called_once_with()
340 mocked_desktop.terminate.assert_called_once_with()
341 server._process.kill.assert_called_once_with()
342
343
344@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
345def test_load_presentation_exception(mocked_uno):
346 """
347 Test the load_presentation() method when an exception occurs
348 """
349 # GIVEN: A LibreOfficeServer object
350 presentation_file = '/path/to/presentation.odp'
351 screen_number = 1
352 server = LibreOfficeServer()
353 mocked_desktop = MagicMock()
354 mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
355 server._desktop = mocked_desktop
356 mocked_desktop.loadComponentFromURL.side_effect = Exception()
357
358 # WHEN: load_presentation() is called
359 with patch.object(server, '_create_property') as mocked_create_property:
360 mocked_create_property.side_effect = lambda x, y: {x: y}
361 result = server.load_presentation(presentation_file, screen_number)
362
363 # THEN: A presentation is loaded
364 assert result is False
365
366
367@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
368def test_load_presentation(mocked_uno):
369 """
370 Test the load_presentation() method
371 """
372 # GIVEN: A LibreOfficeServer object
373 presentation_file = '/path/to/presentation.odp'
374 screen_number = 1
375 server = LibreOfficeServer()
376 mocked_desktop = MagicMock()
377 mocked_document = MagicMock()
378 mocked_presentation = MagicMock()
379 mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
380 server._desktop = mocked_desktop
381 mocked_desktop.loadComponentFromURL.return_value = mocked_document
382 mocked_document.getPresentation.return_value = mocked_presentation
383
384 # WHEN: load_presentation() is called
385 with patch.object(server, '_create_property') as mocked_create_property:
386 mocked_create_property.side_effect = lambda x, y: {x: y}
387 result = server.load_presentation(presentation_file, screen_number)
388
389 # THEN: A presentation is loaded
390 assert result is True
391 mocked_uno.systemPathToFileUrl.assert_called_once_with(presentation_file)
392 mocked_create_property.assert_called_once_with('Hidden', True)
393 mocked_desktop.loadComponentFromURL.assert_called_once_with(
394 presentation_file, '_blank', 0, ({'Hidden': True},))
395 assert server._document is mocked_document
396 mocked_document.getPresentation.assert_called_once_with()
397 assert server._presentation is mocked_presentation
398 assert server._presentation.Display == screen_number
399 assert server._control is None
400
401
402@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
403def test_extract_thumbnails_no_pages(mocked_uno):
404 """
405 Test the extract_thumbnails() method when there are no pages
406 """
407 # GIVEN: A LibreOfficeServer instance
408 temp_folder = '/tmp'
409 server = LibreOfficeServer()
410 mocked_document = MagicMock()
411 server._document = mocked_document
412 mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
413 mocked_document.getDrawPages.return_value = None
414
415 # WHEN: The extract_thumbnails() method is called
416 with patch.object(server, '_create_property') as mocked_create_property:
417 mocked_create_property.side_effect = lambda x, y: {x: y}
418 thumbnails = server.extract_thumbnails(temp_folder)
419
420 # THEN: Thumbnails have been extracted
421 mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder)
422 mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export')
423 mocked_document.getDrawPages.assert_called_once_with()
424 assert thumbnails == []
425
426
427@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
428@patch('openlp.plugins.presentations.lib.libreofficeserver.os')
429def test_extract_thumbnails(mocked_os, mocked_uno):
430 """
431 Test the extract_thumbnails() method
432 """
433 # GIVEN: A LibreOfficeServer instance
434 temp_folder = '/tmp'
435 server = LibreOfficeServer()
436 mocked_document = MagicMock()
437 mocked_pages = MagicMock()
438 mocked_page_1 = MagicMock()
439 mocked_page_2 = MagicMock()
440 mocked_controller = MagicMock()
441 server._document = mocked_document
442 mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
443 mocked_document.getDrawPages.return_value = mocked_pages
444 mocked_os.path.isdir.return_value = False
445 mocked_pages.getCount.return_value = 2
446 mocked_pages.getByIndex.side_effect = [mocked_page_1, mocked_page_2]
447 mocked_document.getCurrentController.return_value = mocked_controller
448 mocked_os.path.join.side_effect = lambda *x: '/'.join(x)
449
450 # WHEN: The extract_thumbnails() method is called
451 with patch.object(server, '_create_property') as mocked_create_property:
452 mocked_create_property.side_effect = lambda x, y: {x: y}
453 thumbnails = server.extract_thumbnails(temp_folder)
454
455 # THEN: Thumbnails have been extracted
456 mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder)
457 mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export')
458 mocked_document.getDrawPages.assert_called_once_with()
459 mocked_pages.getCount.assert_called_once_with()
460 assert mocked_pages.getByIndex.call_args_list == [call(0), call(1)]
461 assert mocked_controller.setCurrentPage.call_args_list == \
462 [call(mocked_page_1), call(mocked_page_2)]
463 assert mocked_document.storeToURL.call_args_list == \
464 [call('/tmp/1.png', ({'FilterName': 'impress_png_Export'},)),
465 call('/tmp/2.png', ({'FilterName': 'impress_png_Export'},))]
466 assert thumbnails == ['/tmp/1.png', '/tmp/2.png']
467
468
469def test_get_titles_and_notes():
470 """
471 Test the get_titles_and_notes() method
472 """
473 # GIVEN: A LibreOfficeServer object and a bunch of mocks
474 server = LibreOfficeServer()
475 mocked_document = MagicMock()
476 mocked_pages = MagicMock()
477 server._document = mocked_document
478 mocked_document.getDrawPages.return_value = mocked_pages
479 mocked_pages.getCount.return_value = 2
480
481 # WHEN: get_titles_and_notes() is called
482 with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page:
483 mocked_get_text_from_page.side_effect = [
484 'OpenLP on Mac OS X',
485 '',
486 '',
487 'Installing is a drag-and-drop affair'
488 ]
489 titles, notes = server.get_titles_and_notes()
490
491 # THEN: The right calls are made and the right stuff returned
492 mocked_document.getDrawPages.assert_called_once_with()
493 mocked_pages.getCount.assert_called_once_with()
494 assert mocked_get_text_from_page.call_count == 4
495 expected_calls = [
496 call(1, TextType.Title), call(1, TextType.Notes),
497 call(2, TextType.Title), call(2, TextType.Notes),
498 ]
499 assert mocked_get_text_from_page.call_args_list == expected_calls
500 assert titles == ['OpenLP on Mac OS X\n', '\n'], titles
501 assert notes == [' ', 'Installing is a drag-and-drop affair'], notes
502
503
504def test_close_presentation():
505 """
506 Test that closing the presentation cleans things up correctly
507 """
508 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
509 server = LibreOfficeServer()
510 mocked_document = MagicMock()
511 mocked_presentation = MagicMock()
512 server._document = mocked_document
513 server._presentation = mocked_presentation
514
515 # WHEN: close_presentation() is called
516 server.close_presentation()
517
518 # THEN: The presentation and document should be closed
519 mocked_presentation.end.assert_called_once_with()
520 mocked_document.dispose.assert_called_once_with()
521 assert server._document is None
522 assert server._presentation is None
523
524
525def test_is_loaded_no_objects():
526 """
527 Test the is_loaded() method when there's no document or presentation
528 """
529 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
530 server = LibreOfficeServer()
531
532 # WHEN: The is_loaded() method is called
533 result = server.is_loaded()
534
535 # THEN: The result should be false
536 assert result is False
537
538
539def test_is_loaded_no_presentation():
540 """
541 Test the is_loaded() method when there's no presentation
542 """
543 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
544 server = LibreOfficeServer()
545 mocked_document = MagicMock()
546 server._document = mocked_document
547 server._presentation = MagicMock()
548 mocked_document.getPresentation.return_value = None
549
550 # WHEN: The is_loaded() method is called
551 result = server.is_loaded()
552
553 # THEN: The result should be false
554 assert result is False
555 mocked_document.getPresentation.assert_called_once_with()
556
557
558def test_is_loaded_exception():
559 """
560 Test the is_loaded() method when an exception is thrown
561 """
562 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
563 server = LibreOfficeServer()
564 mocked_document = MagicMock()
565 server._document = mocked_document
566 server._presentation = MagicMock()
567 mocked_document.getPresentation.side_effect = Exception()
568
569 # WHEN: The is_loaded() method is called
570 result = server.is_loaded()
571
572 # THEN: The result should be false
573 assert result is False
574 mocked_document.getPresentation.assert_called_once_with()
575
576
577def test_is_loaded():
578 """
579 Test the is_loaded() method
580 """
581 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
582 server = LibreOfficeServer()
583 mocked_document = MagicMock()
584 mocked_presentation = MagicMock()
585 server._document = mocked_document
586 server._presentation = mocked_presentation
587 mocked_document.getPresentation.return_value = mocked_presentation
588
589 # WHEN: The is_loaded() method is called
590 result = server.is_loaded()
591
592 # THEN: The result should be false
593 assert result is True
594 mocked_document.getPresentation.assert_called_once_with()
595
596
597def test_is_active_not_loaded():
598 """
599 Test is_active() when is_loaded() returns False
600 """
601 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
602 server = LibreOfficeServer()
603
604 # WHEN: is_active() is called with is_loaded() returns False
605 with patch.object(server, 'is_loaded') as mocked_is_loaded:
606 mocked_is_loaded.return_value = False
607 result = server.is_active()
608
609 # THEN: It should have returned False
610 assert result is False
611
612
613def test_is_active_no_control():
614 """
615 Test is_active() when is_loaded() returns True but there's no control
616 """
617 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
618 server = LibreOfficeServer()
619
620 # WHEN: is_active() is called with is_loaded() returns False
621 with patch.object(server, 'is_loaded') as mocked_is_loaded:
622 mocked_is_loaded.return_value = True
623 result = server.is_active()
624
625 # THEN: The result should be False
626 assert result is False
627 mocked_is_loaded.assert_called_once_with()
628
629
630def test_is_active():
631 """
632 Test is_active()
633 """
634 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
635 server = LibreOfficeServer()
636 mocked_control = MagicMock()
637 server._control = mocked_control
638 mocked_control.isRunning.return_value = True
639
640 # WHEN: is_active() is called with is_loaded() returns False
641 with patch.object(server, 'is_loaded') as mocked_is_loaded:
642 mocked_is_loaded.return_value = True
643 result = server.is_active()
644
645 # THEN: The result should be False
646 assert result is True
647 mocked_is_loaded.assert_called_once_with()
648 mocked_control.isRunning.assert_called_once_with()
649
650
651def test_unblank_screen():
652 """
653 Test the unblank_screen() method
654 """
655 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
656 server = LibreOfficeServer()
657 mocked_control = MagicMock()
658 server._control = mocked_control
659
660 # WHEN: unblank_screen() is run
661 server.unblank_screen()
662
663 # THEN: The resume method should have been called
664 mocked_control.resume.assert_called_once_with()
665
666
667def test_blank_screen():
668 """
669 Test the blank_screen() method
670 """
671 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
672 server = LibreOfficeServer()
673 mocked_control = MagicMock()
674 server._control = mocked_control
675
676 # WHEN: blank_screen() is run
677 server.blank_screen()
678
679 # THEN: The resume method should have been called
680 mocked_control.blankScreen.assert_called_once_with(0)
681
682
683def test_is_blank_no_control():
684 """
685 Test the is_blank() method when there's no control
686 """
687 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
688 server = LibreOfficeServer()
689
690 # WHEN: is_blank() is called
691 result = server.is_blank()
692
693 # THEN: It should have returned False
694 assert result is False
695
696
697def test_is_blank_control_is_running():
698 """
699 Test the is_blank() method when the control is running
700 """
701 # GIVEN: A LibreOfficeServer instance and a bunch of mocks
702 server = LibreOfficeServer()
703 mocked_control = MagicMock()
704 server._control = mocked_control
705 mocked_control.isRunning.return_value = True
706 mocked_control.isPaused.return_value = True
707
708 # WHEN: is_blank() is called
709 result = server.is_blank()
710
711 # THEN: It should have returned False
712 assert result is True
713 mocked_control.isRunning.assert_called_once_with()
714 mocked_control.isPaused.assert_called_once_with()
715
716
717def test_stop_presentation():
718 """
719 Test the stop_presentation() method
720 """
721 # GIVEN: A LibreOfficeServer instance and a mocked presentation
722 server = LibreOfficeServer()
723 mocked_presentation = MagicMock()
724 mocked_control = MagicMock()
725 server._presentation = mocked_presentation
726 server._control = mocked_control
727
728 # WHEN: stop_presentation() is called
729 server.stop_presentation()
730
731 # THEN: The presentation is ended and the control is removed
732 mocked_presentation.end.assert_called_once_with()
733 assert server._control is None
734
735
736@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep')
737def test_start_presentation_no_control(mocked_sleep):
738 """
739 Test the start_presentation() method when there's no control
740 """
741 # GIVEN: A LibreOfficeServer instance and some mocks
742 server = LibreOfficeServer()
743 mocked_control = MagicMock()
744 mocked_document = MagicMock()
745 mocked_presentation = MagicMock()
746 mocked_controller = MagicMock()
747 mocked_frame = MagicMock()
748 mocked_window = MagicMock()
749 server._document = mocked_document
750 server._presentation = mocked_presentation
751 mocked_document.getCurrentController.return_value = mocked_controller
752 mocked_controller.getFrame.return_value = mocked_frame
753 mocked_frame.getContainerWindow.return_value = mocked_window
754 mocked_presentation.getController.side_effect = [None, mocked_control]
755
756 # WHEN: start_presentation() is called
757 server.start_presentation()
758
759 # THEN: The slide number should be correct
760 mocked_document.getCurrentController.assert_called_once_with()
761 mocked_controller.getFrame.assert_called_once_with()
762 mocked_frame.getContainerWindow.assert_called_once_with()
763 mocked_presentation.start.assert_called_once_with()
764 assert mocked_presentation.getController.call_count == 2
765 mocked_sleep.assert_called_once_with(0.1)
766 assert mocked_window.setVisible.call_args_list == [call(True), call(False)]
767 assert server._control is mocked_control
768
769
770def test_start_presentation():
771 """
772 Test the start_presentation() method when there's a control
773 """
774 # GIVEN: A LibreOfficeServer instance and some mocks
775 server = LibreOfficeServer()
776 mocked_control = MagicMock()
777 server._control = mocked_control
778
779 # WHEN: start_presentation() is called
780 with patch.object(server, 'goto_slide') as mocked_goto_slide:
781 server.start_presentation()
782
783 # THEN: The control should have been activated and the first slide selected
784 mocked_control.activate.assert_called_once_with()
785 mocked_goto_slide.assert_called_once_with(1)
786
787
788def test_get_slide_number():
789 """
790 Test the get_slide_number() method
791 """
792 # GIVEN: A LibreOfficeServer instance and some mocks
793 server = LibreOfficeServer()
794 mocked_control = MagicMock()
795 mocked_control.getCurrentSlideIndex.return_value = 3
796 server._control = mocked_control
797
798 # WHEN: get_slide_number() is called
799 result = server.get_slide_number()
800
801 # THEN: The slide number should be correct
802 assert result == 4
803
804
805def test_get_slide_count():
806 """
807 Test the get_slide_count() method
808 """
809 # GIVEN: A LibreOfficeServer instance and some mocks
810 server = LibreOfficeServer()
811 mocked_document = MagicMock()
812 mocked_pages = MagicMock()
813 server._document = mocked_document
814 mocked_document.getDrawPages.return_value = mocked_pages
815 mocked_pages.getCount.return_value = 2
816
817 # WHEN: get_slide_count() is called
818 result = server.get_slide_count()
819
820 # THEN: The slide count should be correct
821 assert result == 2
822
823
824def test_goto_slide():
825 """
826 Test the goto_slide() method
827 """
828 # GIVEN: A LibreOfficeServer instance and some mocks
829 server = LibreOfficeServer()
830 mocked_control = MagicMock()
831 server._control = mocked_control
832
833 # WHEN: goto_slide() is called
834 server.goto_slide(1)
835
836 # THEN: The slide number should be correct
837 mocked_control.gotoSlideIndex.assert_called_once_with(0)
838
839
840@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep')
841def test_next_step_when_paused(mocked_sleep):
842 """
843 Test the next_step() method when paused
844 """
845 # GIVEN: A LibreOfficeServer instance and a mocked control
846 server = LibreOfficeServer()
847 mocked_control = MagicMock()
848 server._control = mocked_control
849 mocked_control.isPaused.side_effect = [False, True]
850
851 # WHEN: next_step() is called
852 server.next_step()
853
854 # THEN: The correct call should be made
855 mocked_control.gotoNextEffect.assert_called_once_with()
856 mocked_sleep.assert_called_once_with(0.1)
857 assert mocked_control.isPaused.call_count == 2
858 mocked_control.gotoPreviousEffect.assert_called_once_with()
859
860
861@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep')
862def test_next_step(mocked_sleep):
863 """
864 Test the next_step() method when paused
865 """
866 # GIVEN: A LibreOfficeServer instance and a mocked control
867 server = LibreOfficeServer()
868 mocked_control = MagicMock()
869 server._control = mocked_control
870 mocked_control.isPaused.side_effect = [True, True]
871
872 # WHEN: next_step() is called
873 server.next_step()
874
875 # THEN: The correct call should be made
876 mocked_control.gotoNextEffect.assert_called_once_with()
877 mocked_sleep.assert_called_once_with(0.1)
878 assert mocked_control.isPaused.call_count == 1
879 assert mocked_control.gotoPreviousEffect.call_count == 0
880
881
882def test_previous_step():
883 """
884 Test the previous_step() method
885 """
886 # GIVEN: A LibreOfficeServer instance and a mocked control
887 server = LibreOfficeServer()
888 mocked_control = MagicMock()
889 server._control = mocked_control
890
891 # WHEN: previous_step() is called
892 server.previous_step()
893
894 # THEN: The correct call should be made
895 mocked_control.gotoPreviousEffect.assert_called_once_with()
896
897
898def test_get_slide_text():
899 """
900 Test the get_slide_text() method
901 """
902 # GIVEN: A LibreOfficeServer instance
903 server = LibreOfficeServer()
904
905 # WHEN: get_slide_text() is called for a particular slide
906 with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page:
907 mocked_get_text_from_page.return_value = 'OpenLP on Mac OS X'
908 result = server.get_slide_text(5)
909
910 # THEN: The text should be returned
911 mocked_get_text_from_page.assert_called_once_with(5)
912 assert result == 'OpenLP on Mac OS X'
913
914
915def test_get_slide_notes():
916 """
917 Test the get_slide_notes() method
918 """
919 # GIVEN: A LibreOfficeServer instance
920 server = LibreOfficeServer()
921
922 # WHEN: get_slide_notes() is called for a particular slide
923 with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page:
924 mocked_get_text_from_page.return_value = 'Installing is a drag-and-drop affair'
925 result = server.get_slide_notes(3)
926
927 # THEN: The text should be returned
928 mocked_get_text_from_page.assert_called_once_with(3, TextType.Notes)
929 assert result == 'Installing is a drag-and-drop affair'
930
931
932@patch('openlp.plugins.presentations.lib.libreofficeserver.Daemon')
933def test_main(MockedDaemon):
934 """
935 Test the main() function
936 """
937 # GIVEN: Mocked out Pyro objects
938 mocked_daemon = MagicMock()
939 MockedDaemon.return_value = mocked_daemon
940
941 # WHEN: main() is run
942 main()
943
944 # THEN: The correct calls are made
945 MockedDaemon.assert_called_once_with(host='localhost', port=4310)
946 mocked_daemon.register.assert_called_once_with(LibreOfficeServer, 'openlp.libreofficeserver')
947 mocked_daemon.requestLoop.assert_called_once_with()
948 mocked_daemon.close.assert_called_once_with()
0949
=== added file 'tests/functional/openlp_plugins/presentations/test_maclocontroller.py'
--- tests/functional/openlp_plugins/presentations/test_maclocontroller.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/presentations/test_maclocontroller.py 2019-06-05 04:57:51 +0000
@@ -0,0 +1,453 @@
1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
4##########################################################################
5# OpenLP - Open Source Lyrics Projection #
6# ---------------------------------------------------------------------- #
7# Copyright (c) 2008-2019 OpenLP Developers #
8# ---------------------------------------------------------------------- #
9# This program is free software: you can redistribute it and/or modify #
10# it under the terms of the GNU General Public License as published by #
11# the Free Software Foundation, either version 3 of the License, or #
12# (at your option) any later version. #
13# #
14# This program is distributed in the hope that it will be useful, #
15# but WITHOUT ANY WARRANTY; without even the implied warranty of #
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
17# GNU General Public License for more details. #
18# #
19# You should have received a copy of the GNU General Public License #
20# along with this program. If not, see <https://www.gnu.org/licenses/>. #
21##########################################################################
22"""
23Functional tests to test the Mac LibreOffice class and related methods.
24"""
25import shutil
26from tempfile import mkdtemp
27from unittest import TestCase
28from unittest.mock import MagicMock, patch, call
29
30from openlp.core.common.settings import Settings
31from openlp.core.common.path import Path
32from openlp.plugins.presentations.lib.maclocontroller import MacLOController, MacLODocument
33from openlp.plugins.presentations.presentationplugin import __default_settings__
34
35from tests.helpers.testmixin import TestMixin
36from tests.utils.constants import TEST_RESOURCES_PATH
37
38
39class TestMacLOController(TestCase, TestMixin):
40 """
41 Test the MacLOController Class
42 """
43
44 def setUp(self):
45 """
46 Set up the patches and mocks need for all tests.
47 """
48 self.setup_application()
49 self.build_settings()
50 self.mock_plugin = MagicMock()
51 self.temp_folder = mkdtemp()
52 self.mock_plugin.settings_section = self.temp_folder
53
54 def tearDown(self):
55 """
56 Stop the patches
57 """
58 self.destroy_settings()
59 shutil.rmtree(self.temp_folder)
60
61 @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
62 def test_constructor(self, mocked_start_server):
63 """
64 Test the Constructor from the MacLOController
65 """
66 # GIVEN: No presentation controller
67 controller = None
68
69 # WHEN: The presentation controller object is created
70 controller = MacLOController(plugin=self.mock_plugin)
71
72 # THEN: The name of the presentation controller should be correct
73 assert controller.name == 'maclo', \
74 'The name of the presentation controller should be correct'
75 assert controller.display_name == 'Impress on macOS', \
76 'The display name of the presentation controller should be correct'
77 mocked_start_server.assert_called_once_with()
78
79 @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
80 @patch('openlp.plugins.presentations.lib.maclocontroller.Proxy')
81 def test_client(self, MockedProxy, mocked_start_server):
82 """
83 Test the client property of the Controller
84 """
85 # GIVEN: A controller without a client and a mocked out Pyro
86 controller = MacLOController(plugin=self.mock_plugin)
87 mocked_client = MagicMock()
88 MockedProxy.return_value = mocked_client
89 mocked_client._pyroConnection = None
90
91 # WHEN: the client property is called the first time
92 client = controller.client
93
94 # THEN: a client is created
95 assert client == mocked_client
96 MockedProxy.assert_called_once_with('PYRO:openlp.libreofficeserver@localhost:4310')
97 mocked_client._pyroReconnect.assert_called_once_with()
98
99 @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
100 def test_check_available(self, mocked_start_server):
101 """
102 Test the check_available() method
103 """
104 from openlp.plugins.presentations.lib.maclocontroller import macuno_available
105
106 # GIVEN: A controller
107 controller = MacLOController(plugin=self.mock_plugin)
108
109 # WHEN: check_available() is run
110 result = controller.check_available()
111
112 # THEN: it should return false
113 assert result == macuno_available
114
115 @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
116 def test_start_process(self, mocked_start_server):
117 """
118 Test the start_process() method
119 """
120 # GIVEN: A controller and a client
121 controller = MacLOController(plugin=self.mock_plugin)
122 controller._client = MagicMock()
123
124 # WHEN: start_process() is called
125 controller.start_process()
126
127 # THEN: The client's start_process() should have been called
128 controller._client.start_process.assert_called_once_with()
129
130 @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
131 def test_kill(self, mocked_start_server):
132 """
133 Test the kill() method
134 """
135 # GIVEN: A controller and a client
136 controller = MacLOController(plugin=self.mock_plugin)
137 controller._client = MagicMock()
138 controller.server_process = MagicMock()
139
140 # WHEN: start_process() is called
141 controller.kill()
142
143 # THEN: The client's start_process() should have been called
144 controller._client.shutdown.assert_called_once_with()
145 controller.server_process.kill.assert_called_once_with()
146
147
148class TestMacLODocument(TestCase):
149 """
150 Test the MacLODocument Class
151 """
152 def setUp(self):
153 mocked_plugin = MagicMock()
154 mocked_plugin.settings_section = 'presentations'
155 Settings().extend_default_settings(__default_settings__)
156 self.file_name = Path(TEST_RESOURCES_PATH) / 'presentations' / 'test.odp'
157 self.mocked_client = MagicMock()
158 with patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server'):
159 self.controller = MacLOController(mocked_plugin)
160 self.controller._client = self.mocked_client
161 self.document = MacLODocument(self.controller, self.file_name)
162
163 @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList')
164 def test_load_presentation_cannot_load(self, MockedScreenList):
165 """
166 Test the load_presentation() method when the server can't load the presentation
167 """
168 # GIVEN: A document and a mocked client
169 mocked_screen_list = MagicMock()
170 MockedScreenList.return_value = mocked_screen_list
171 mocked_screen_list.current.number = 0
172 self.mocked_client.load_presentation.return_value = False
173
174 # WHEN: load_presentation() is called
175 result = self.document.load_presentation()
176
177 # THEN: Stuff should work right
178 self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1)
179 assert result is False
180
181 @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList')
182 def test_load_presentation(self, MockedScreenList):
183 """
184 Test the load_presentation() method
185 """
186 # GIVEN: A document and a mocked client
187 mocked_screen_list = MagicMock()
188 MockedScreenList.return_value = mocked_screen_list
189 mocked_screen_list.current.number = 0
190 self.mocked_client.load_presentation.return_value = True
191
192 # WHEN: load_presentation() is called
193 with patch.object(self.document, 'create_thumbnails') as mocked_create_thumbnails, \
194 patch.object(self.document, 'create_titles_and_notes') as mocked_create_titles_and_notes:
195 result = self.document.load_presentation()
196
197 # THEN: Stuff should work right
198 self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1)
199 mocked_create_thumbnails.assert_called_once_with()
200 mocked_create_titles_and_notes.assert_called_once_with()
201 assert result is True
202
203 def test_create_thumbnails_already_exist(self):
204 """
205 Test the create_thumbnails() method when thumbnails already exist
206 """
207 # GIVEN: thumbnails that exist and a mocked client
208 self.document.check_thumbnails = MagicMock(return_value=True)
209
210 # WHEN: create_thumbnails() is called
211 self.document.create_thumbnails()
212
213 # THEN: The method should exit early
214 assert self.mocked_client.extract_thumbnails.call_count == 0
215
216 @patch('openlp.plugins.presentations.lib.maclocontroller.delete_file')
217 def test_create_thumbnails(self, mocked_delete_file):
218 """
219 Test the create_thumbnails() method
220 """
221 # GIVEN: thumbnails that don't exist and a mocked client
222 self.document.check_thumbnails = MagicMock(return_value=False)
223 self.mocked_client.extract_thumbnails.return_value = ['thumb1.png', 'thumb2.png']
224
225 # WHEN: create_thumbnails() is called
226 with patch.object(self.document, 'convert_thumbnail') as mocked_convert_thumbnail, \
227 patch.object(self.document, 'get_temp_folder') as mocked_get_temp_folder:
228 mocked_get_temp_folder.return_value = 'temp'
229 self.document.create_thumbnails()
230
231 # THEN: The method should complete successfully
232 self.mocked_client.extract_thumbnails.assert_called_once_with('temp')
233 assert mocked_convert_thumbnail.call_args_list == [
234 call(Path('thumb1.png'), 1), call(Path('thumb2.png'), 2)]
235 assert mocked_delete_file.call_args_list == [call(Path('thumb1.png')), call(Path('thumb2.png'))]
236
237 def test_create_titles_and_notes(self):
238 """
239 Test create_titles_and_notes() method
240 """
241 # GIVEN: mocked client and mocked save_titles_and_notes() method
242 self.mocked_client.get_titles_and_notes.return_value = ('OpenLP', 'This is a note')
243
244 # WHEN: create_titles_and_notes() is called
245 with patch.object(self.document, 'save_titles_and_notes') as mocked_save_titles_and_notes:
246 self.document.create_titles_and_notes()
247
248 # THEN save_titles_and_notes should have been called
249 self.mocked_client.get_titles_and_notes.assert_called_once_with()
250 mocked_save_titles_and_notes.assert_called_once_with('OpenLP', 'This is a note')
251
252 def test_close_presentation(self):
253 """
254 Test the close_presentation() method
255 """
256 # GIVEN: A mocked client and mocked remove_doc() method
257 # WHEN: close_presentation() is called
258 with patch.object(self.controller, 'remove_doc') as mocked_remove_doc:
259 self.document.close_presentation()
260
261 # THEN: The presentation should have been closed
262 self.mocked_client.close_presentation.assert_called_once_with()
263 mocked_remove_doc.assert_called_once_with(self.document)
264
265 def test_is_loaded(self):
266 """
267 Test the is_loaded() method
268 """
269 # GIVEN: A mocked client
270 self.mocked_client.is_loaded.return_value = True
271
272 # WHEN: is_loaded() is called
273 result = self.document.is_loaded()
274
275 # THEN: Then the result should be correct
276 assert result is True
277
278 def test_is_active(self):
279 """
280 Test the is_active() method
281 """
282 # GIVEN: A mocked client
283 self.mocked_client.is_active.return_value = True
284
285 # WHEN: is_active() is called
286 result = self.document.is_active()
287
288 # THEN: Then the result should be correct
289 assert result is True
290
291 def test_unblank_screen(self):
292 """
293 Test the unblank_screen() method
294 """
295 # GIVEN: A mocked client
296 self.mocked_client.unblank_screen.return_value = True
297
298 # WHEN: unblank_screen() is called
299 result = self.document.unblank_screen()
300
301 # THEN: Then the result should be correct
302 self.mocked_client.unblank_screen.assert_called_once_with()
303 assert result is True
304
305 def test_blank_screen(self):
306 """
307 Test the blank_screen() method
308 """
309 # GIVEN: A mocked client
310 self.mocked_client.blank_screen.return_value = True
311
312 # WHEN: blank_screen() is called
313 self.document.blank_screen()
314
315 # THEN: Then the result should be correct
316 self.mocked_client.blank_screen.assert_called_once_with()
317
318 def test_is_blank(self):
319 """
320 Test the is_blank() method
321 """
322 # GIVEN: A mocked client
323 self.mocked_client.is_blank.return_value = True
324
325 # WHEN: is_blank() is called
326 result = self.document.is_blank()
327
328 # THEN: Then the result should be correct
329 assert result is True
330
331 def test_stop_presentation(self):
332 """
333 Test the stop_presentation() method
334 """
335 # GIVEN: A mocked client
336 self.mocked_client.stop_presentation.return_value = True
337
338 # WHEN: stop_presentation() is called
339 self.document.stop_presentation()
340
341 # THEN: Then the result should be correct
342 self.mocked_client.stop_presentation.assert_called_once_with()
343
344 @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList')
345 @patch('openlp.plugins.presentations.lib.maclocontroller.Registry')
346 def test_start_presentation(self, MockedRegistry, MockedScreenList):
347 """
348 Test the start_presentation() method
349 """
350 # GIVEN: a mocked client, and multiple screens
351 mocked_screen_list = MagicMock()
352 mocked_screen_list.__len__.return_value = 2
353 mocked_registry = MagicMock()
354 mocked_main_window = MagicMock()
355 MockedScreenList.return_value = mocked_screen_list
356 MockedRegistry.return_value = mocked_registry
357 mocked_screen_list.screen_list = [0, 1]
358 mocked_registry.get.return_value = mocked_main_window
359
360 # WHEN: start_presentation() is called
361 self.document.start_presentation()
362
363 # THEN: The presentation should be started
364 self.mocked_client.start_presentation.assert_called_once_with()
365 mocked_registry.get.assert_called_once_with('main_window')
366 mocked_main_window.activateWindow.assert_called_once_with()
367
368 def test_get_slide_number(self):
369 """
370 Test the get_slide_number() method
371 """
372 # GIVEN: A mocked client
373 self.mocked_client.get_slide_number.return_value = 5
374
375 # WHEN: get_slide_number() is called
376 result = self.document.get_slide_number()
377
378 # THEN: Then the result should be correct
379 assert result == 5
380
381 def test_get_slide_count(self):
382 """
383 Test the get_slide_count() method
384 """
385 # GIVEN: A mocked client
386 self.mocked_client.get_slide_count.return_value = 8
387
388 # WHEN: get_slide_count() is called
389 result = self.document.get_slide_count()
390
391 # THEN: Then the result should be correct
392 assert result == 8
393
394 def test_goto_slide(self):
395 """
396 Test the goto_slide() method
397 """
398 # GIVEN: A mocked client
399 # WHEN: goto_slide() is called
400 self.document.goto_slide(3)
401
402 # THEN: Then the result should be correct
403 self.mocked_client.goto_slide.assert_called_once_with(3)
404
405 def test_next_step(self):
406 """
407 Test the next_step() method
408 """
409 # GIVEN: A mocked client
410 # WHEN: next_step() is called
411 self.document.next_step()
412
413 # THEN: Then the result should be correct
414 self.mocked_client.next_step.assert_called_once_with()
415
416 def test_previous_step(self):
417 """
418 Test the previous_step() method
419 """
420 # GIVEN: A mocked client
421 # WHEN: previous_step() is called
422 self.document.previous_step()
423
424 # THEN: Then the result should be correct
425 self.mocked_client.previous_step.assert_called_once_with()
426
427 def test_get_slide_text(self):
428 """
429 Test the get_slide_text() method
430 """
431 # GIVEN: A mocked client
432 self.mocked_client.get_slide_text.return_value = 'Some slide text'
433
434 # WHEN: get_slide_text() is called
435 result = self.document.get_slide_text(1)
436
437 # THEN: Then the result should be correct
438 self.mocked_client.get_slide_text.assert_called_once_with(1)
439 assert result == 'Some slide text'
440
441 def test_get_slide_notes(self):
442 """
443 Test the get_slide_notes() method
444 """
445 # GIVEN: A mocked client
446 self.mocked_client.get_slide_notes.return_value = 'This is a note'
447
448 # WHEN: get_slide_notes() is called
449 result = self.document.get_slide_notes(2)
450
451 # THEN: Then the result should be correct
452 self.mocked_client.get_slide_notes.assert_called_once_with(2)
453 assert result == 'This is a note'