Add presentations through LibreOffice on macOS.

bzr-revno: 2878
This commit is contained in:
Raoul Snyman 2019-06-10 22:01:02 -07:00
commit 596c484bd7
14 changed files with 2223 additions and 56 deletions

View File

@ -1,57 +1,48 @@
*.*~ *.*~
*.~\?~
\#*\#
build
.cache
cover
.coverage
coverage
.directory
.vscode
dist
*.dll *.dll
documentation/build/doctrees
documentation/build/html
*.e4* *.e4*
*eric[1-9]project
.git
env
# Git files
.gitignore
htmlcov
.idea
*.kate-swp *.kate-swp
*.kdev4 *.kdev4
.kdev4
*.komodoproject *.komodoproject
.komodotools
list
*.log* *.log*
*.nja *.nja
openlp.cfg
openlp/core/resources.py.old
OpenLP.egg-info
openlp.org 2.0.e4*
openlp.pro
openlp-test-projectordb.sqlite
*.orig *.orig
output
*.pyc *.pyc
__pycache__
.pylint.d
.pytest_cache
*.qm *.qm
*.rej *.rej
# Rejected diff's
resources/innosetup/Output
resources/windows/warnOpenLP.txt
*.ropeproject *.ropeproject
tags *.~\?~
output *eric[1-9]project
.cache
.coverage
.directory
.git
.gitignore
.idea
.kdev4
.komodotools
.pylint.d
.pytest_cache
.vscode
OpenLP.egg-info
\#*\#
__pycache__
build
cover
coverage
dist
env
htmlcov htmlcov
list
node_modules node_modules
openlp-test-projectordb.sqlite openlp-test-projectordb.sqlite
openlp.cfg
openlp.pro
openlp/core/resources.py.old
openlp/plugins/presentations/lib/vendor/Pyro4
openlp/plugins/presentations/lib/vendor/serpent.py
output
package-lock.json package-lock.json
.cache tags
test test
tests.kdev4 tests.kdev4

View File

@ -78,6 +78,8 @@ def path_to_str(path=None):
:return: An empty string if :param:`path` is None, else a string representation of the :param:`path` :return: An empty string if :param:`path` is None, else a string representation of the :param:`path`
:rtype: str :rtype: str
""" """
if isinstance(path, str):
return path
if not isinstance(path, Path) and path is not None: if not isinstance(path, Path) and path is not None:
raise TypeError('parameter \'path\' must be of type Path or NoneType') raise TypeError('parameter \'path\' must be of type Path or NoneType')
if path is None: if path is None:

View File

@ -104,6 +104,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
State().update_pre_conditions('mediacontroller', True) State().update_pre_conditions('mediacontroller', True)
State().update_pre_conditions('media_live', True) State().update_pre_conditions('media_live', True)
else: else:
if hasattr(self.main_window, 'splash') and self.main_window.splash.isVisible():
self.main_window.splash.hide()
State().missing_text('media_live', translate('OpenLP.SlideController', State().missing_text('media_live', translate('OpenLP.SlideController',
'VLC or pymediainfo are missing, so you are unable to play any media')) 'VLC or pymediainfo are missing, so you are unable to play any media'))
return True return True

View File

@ -0,0 +1,431 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
This module runs a Pyro4 server using LibreOffice's version of Python
Please Note: This intentionally uses os.path over pathlib because we don't know which version of Python is shipped with
the version of LibreOffice on the user's computer.
"""
from subprocess import Popen
import sys
import os
import logging
import time
if sys.platform.startswith('darwin'):
# Only make the log file on OS X when running as a server
logfile = os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'libreofficeserver.log')
print('Setting up log file: {logfile}'.format(logfile=logfile))
logging.basicConfig(filename=logfile, level=logging.INFO)
# Add the current directory to sys.path so that we can load the serializers
sys.path.append(os.path.join(os.path.dirname(__file__)))
# Add the vendor directory to sys.path so that we can load Pyro4
sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor'))
from serializers import register_classes
from Pyro4 import Daemon, expose
try:
# Wrap these imports in a try so that we can run the tests on macOS
import uno
from com.sun.star.beans import PropertyValue
from com.sun.star.task import ErrorCodeIOException
except ImportError:
# But they need to be defined for mocking
uno = None
PropertyValue = None
ErrorCodeIOException = Exception
log = logging.getLogger(__name__)
register_classes()
class TextType(object):
"""
Type Enumeration for Types of Text to request
"""
Title = 0
SlideText = 1
Notes = 2
class LibreOfficeException(Exception):
"""
A specific exception for LO
"""
pass
@expose
class LibreOfficeServer(object):
"""
A Pyro4 server which controls LibreOffice
"""
def __init__(self):
"""
Set up the server
"""
self._desktop = None
self._control = None
self._document = None
self._presentation = None
self._process = None
self._manager = None
def _create_property(self, name, value):
"""
Create an OOo style property object which are passed into some Uno methods.
"""
log.debug('create property')
property_object = PropertyValue()
property_object.Name = name
property_object.Value = value
return property_object
def _get_text_from_page(self, slide_no, text_type=TextType.SlideText):
"""
Return any text extracted from the presentation page.
:param slide_no: The slide the notes are required for, starting at 1
:param notes: A boolean. If set the method searches the notes of the slide.
:param text_type: A TextType. Enumeration of the types of supported text.
"""
text = ''
if TextType.Title <= text_type <= TextType.Notes:
pages = self._document.getDrawPages()
if 0 < slide_no <= pages.getCount():
page = pages.getByIndex(slide_no - 1)
if text_type == TextType.Notes:
page = page.getNotesPage()
for index in range(page.getCount()):
shape = page.getByIndex(index)
shape_type = shape.getShapeType()
if shape.supportsService('com.sun.star.drawing.Text'):
# if they requested title, make sure it is the title
if text_type != TextType.Title or shape_type == 'com.sun.star.presentation.TitleTextShape':
text += shape.getString() + '\n'
return text
def start_process(self):
"""
Initialise Impress
"""
uno_command = [
'/Applications/LibreOffice.app/Contents/MacOS/soffice',
'--nologo',
'--norestore',
'--minimized',
'--nodefault',
'--nofirststartwizard',
'--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager'
]
self._process = Popen(uno_command)
@property
def desktop(self):
"""
Set up an UNO desktop instance
"""
if self._desktop is not None:
return self._desktop
uno_instance = None
context = uno.getComponentContext()
resolver = context.ServiceManager.createInstanceWithContext('com.sun.star.bridge.UnoUrlResolver', context)
loop = 0
while uno_instance is None and loop < 3:
try:
uno_instance = resolver.resolve('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext')
except Exception:
log.exception('Unable to find running instance, retrying...')
loop += 1
try:
self._manager = uno_instance.ServiceManager
log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop')
desktop = self._manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance)
if not desktop:
raise Exception('Failed to get UNO desktop')
self._desktop = desktop
return desktop
except Exception:
log.exception('Failed to get UNO desktop')
return None
def shutdown(self):
"""
Shut down the server
"""
can_kill = True
if hasattr(self, '_docs'):
while self._docs:
self._docs[0].close_presentation()
docs = self.desktop.getComponents()
count = 0
if docs.hasElements():
list_elements = docs.createEnumeration()
while list_elements.hasMoreElements():
doc = list_elements.nextElement()
if doc.getImplementationName() != 'com.sun.star.comp.framework.BackingComp':
count += 1
if count > 0:
log.debug('LibreOffice not terminated as docs are still open')
can_kill = False
else:
try:
self.desktop.terminate()
log.debug('LibreOffice killed')
except Exception:
log.exception('Failed to terminate LibreOffice')
if getattr(self, '_process') and can_kill:
self._process.kill()
def load_presentation(self, file_path, screen_number):
"""
Load a presentation
"""
self._file_path = file_path
url = uno.systemPathToFileUrl(file_path)
properties = (self._create_property('Hidden', True),)
self._document = None
loop_count = 0
while loop_count < 3:
try:
self._document = self.desktop.loadComponentFromURL(url, '_blank', 0, properties)
except Exception:
log.exception('Failed to load presentation {url}'.format(url=url))
if self._document:
break
time.sleep(0.5)
loop_count += 1
if loop_count == 3:
log.error('Looped too many times')
return False
self._presentation = self._document.getPresentation()
self._presentation.Display = screen_number
self._control = None
return True
def extract_thumbnails(self, temp_folder):
"""
Create thumbnails for the presentation
"""
thumbnails = []
thumb_dir_url = uno.systemPathToFileUrl(temp_folder)
properties = (self._create_property('FilterName', 'impress_png_Export'),)
pages = self._document.getDrawPages()
if not pages:
return []
if not os.path.isdir(temp_folder):
os.makedirs(temp_folder)
for index in range(pages.getCount()):
page = pages.getByIndex(index)
self._document.getCurrentController().setCurrentPage(page)
url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1))
path = os.path.join(temp_folder, str(index + 1) + '.png')
try:
self._document.storeToURL(url_path, properties)
thumbnails.append(path)
except ErrorCodeIOException as exception:
log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode))
except Exception:
log.exception('{path} - Unable to store openoffice preview'.format(path=path))
return thumbnails
def get_titles_and_notes(self):
"""
Extract the titles and the notes from the slides.
"""
titles = []
notes = []
pages = self._document.getDrawPages()
for slide_no in range(1, pages.getCount() + 1):
titles.append(self._get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n')
note = self._get_text_from_page(slide_no, TextType.Notes)
if len(note) == 0:
note = ' '
notes.append(note)
return titles, notes
def close_presentation(self):
"""
Close presentation and clean up objects.
"""
log.debug('close Presentation LibreOffice')
if self._document:
if self._presentation:
try:
self._presentation.end()
self._presentation = None
self._document.dispose()
except Exception:
log.exception("Closing presentation failed")
self._document = None
def is_loaded(self):
"""
Returns true if a presentation is loaded.
"""
log.debug('is loaded LibreOffice')
if self._presentation is None or self._document is None:
log.debug("is_loaded: no presentation or document")
return False
try:
if self._document.getPresentation() is None:
log.debug("getPresentation failed to find a presentation")
return False
except Exception:
log.exception("getPresentation failed to find a presentation")
return False
return True
def is_active(self):
"""
Returns true if a presentation is active and running.
"""
log.debug('is active LibreOffice')
if not self.is_loaded():
return False
return self._control.isRunning() if self._control else False
def unblank_screen(self):
"""
Unblanks the screen.
"""
log.debug('unblank screen LibreOffice')
return self._control.resume()
def blank_screen(self):
"""
Blanks the screen.
"""
log.debug('blank screen LibreOffice')
self._control.blankScreen(0)
def is_blank(self):
"""
Returns true if screen is blank.
"""
log.debug('is blank LibreOffice')
if self._control and self._control.isRunning():
return self._control.isPaused()
else:
return False
def stop_presentation(self):
"""
Stop the presentation, remove from screen.
"""
log.debug('stop presentation LibreOffice')
self._presentation.end()
self._control = None
def start_presentation(self):
"""
Start the presentation from the beginning.
"""
log.debug('start presentation LibreOffice')
if self._control is None or not self._control.isRunning():
window = self._document.getCurrentController().getFrame().getContainerWindow()
window.setVisible(True)
self._presentation.start()
self._control = self._presentation.getController()
# start() returns before the Component is ready. Try for 15 seconds.
sleep_count = 1
while not self._control and sleep_count < 150:
time.sleep(0.1)
sleep_count += 1
self._control = self._presentation.getController()
window.setVisible(False)
else:
self._control.activate()
self.goto_slide(1)
def get_slide_number(self):
"""
Return the current slide number on the screen, from 1.
"""
return self._control.getCurrentSlideIndex() + 1
def get_slide_count(self):
"""
Return the total number of slides.
"""
return self._document.getDrawPages().getCount()
def goto_slide(self, slide_no):
"""
Go to a specific slide (from 1).
:param slide_no: The slide the text is required for, starting at 1
"""
self._control.gotoSlideIndex(slide_no - 1)
def next_step(self):
"""
Triggers the next effect of slide on the running presentation.
"""
is_paused = self._control.isPaused()
self._control.gotoNextEffect()
time.sleep(0.1)
if not is_paused and self._control.isPaused():
self._control.gotoPreviousEffect()
def previous_step(self):
"""
Triggers the previous slide on the running presentation.
"""
self._control.gotoPreviousEffect()
def get_slide_text(self, slide_no):
"""
Returns the text on the slide.
:param slide_no: The slide the text is required for, starting at 1
"""
return self._get_text_from_page(slide_no)
def get_slide_notes(self, slide_no):
"""
Returns the text in the slide notes.
:param slide_no: The slide the notes are required for, starting at 1
"""
return self._get_text_from_page(slide_no, TextType.Notes)
def main():
"""
The main function which runs the server
"""
daemon = Daemon(host='localhost', port=4310)
daemon.register(LibreOfficeServer, 'openlp.libreofficeserver')
try:
daemon.requestLoop()
finally:
daemon.close()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,266 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
import logging
from subprocess import Popen
from Pyro4 import Proxy
from openlp.core.common import delete_file, is_macosx
from openlp.core.common.applocation import AppLocation
from openlp.core.common.path import Path
from openlp.core.common.registry import Registry
from openlp.core.display.screens import ScreenList
from openlp.plugins.presentations.lib.serializers import register_classes
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
LIBREOFFICE_PATH = Path('/Applications/LibreOffice.app')
LIBREOFFICE_PYTHON = LIBREOFFICE_PATH / 'Contents' / 'Resources' / 'python'
if is_macosx() and LIBREOFFICE_PATH.exists():
macuno_available = True
else:
macuno_available = False
log = logging.getLogger(__name__)
register_classes()
class MacLOController(PresentationController):
"""
Class to control interactions with MacLO presentations on Mac OS X via Pyro4. It starts the Pyro4 nameserver,
starts the LibreOfficeServer, and then controls MacLO via Pyro4.
"""
log.info('MacLOController loaded')
def __init__(self, plugin):
"""
Initialise the class
"""
log.debug('Initialising')
super(MacLOController, self).__init__(plugin, 'maclo', MacLODocument, 'Impress on macOS')
self.supports = ['odp']
self.also_supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm']
self.server_process = None
self._client = None
self._start_server()
def _start_server(self):
"""
Start a LibreOfficeServer
"""
libreoffice_python = Path('/Applications/LibreOffice.app/Contents/Resources/python')
libreoffice_server = AppLocation.get_directory(AppLocation.PluginsDir).joinpath('presentations', 'lib',
'libreofficeserver.py')
if libreoffice_python.exists():
self.server_process = Popen([str(libreoffice_python), str(libreoffice_server)])
@property
def client(self):
"""
Set up a Pyro4 client so that we can talk to the LibreOfficeServer
"""
if not self._client:
self._client = Proxy('PYRO:openlp.libreofficeserver@localhost:4310')
if not self._client._pyroConnection:
self._client._pyroReconnect()
return self._client
def check_available(self):
"""
MacLO is able to run on this machine.
"""
log.debug('check_available')
return macuno_available
def start_process(self):
"""
Loads a running version of LibreOffice in the background. It is not displayed to the user but is available to
the UNO interface when required.
"""
log.debug('Started automatically by the Pyro server')
self.client.start_process()
def kill(self):
"""
Called at system exit to clean up any running presentations.
"""
log.debug('Kill LibreOffice')
self.client.shutdown()
self.server_process.kill()
class MacLODocument(PresentationDocument):
"""
Class which holds information and controls a single presentation.
"""
def __init__(self, controller, presentation):
"""
Constructor, store information about the file and initialise.
"""
log.debug('Init Presentation LibreOffice')
super(MacLODocument, self).__init__(controller, presentation)
self.client = controller.client
def load_presentation(self):
"""
Tell the LibreOfficeServer to start the presentation.
"""
log.debug('Load Presentation LibreOffice')
if not self.client.load_presentation(str(self.file_path), ScreenList().current.number + 1):
return False
self.create_thumbnails()
self.create_titles_and_notes()
return True
def create_thumbnails(self):
"""
Create thumbnail images for presentation.
"""
log.debug('create thumbnails LibreOffice')
if self.check_thumbnails():
return
temp_thumbnails = self.client.extract_thumbnails(str(self.get_temp_folder()))
for index, temp_thumb in enumerate(temp_thumbnails):
temp_thumb = Path(temp_thumb)
self.convert_thumbnail(temp_thumb, index + 1)
delete_file(temp_thumb)
def create_titles_and_notes(self):
"""
Writes the list of titles (one per slide) to 'titles.txt' and the notes to 'slideNotes[x].txt'
in the thumbnails directory
"""
titles, notes = self.client.get_titles_and_notes()
self.save_titles_and_notes(titles, notes)
def close_presentation(self):
"""
Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
shutdown.
"""
log.debug('close Presentation LibreOffice')
self.client.close_presentation()
self.controller.remove_doc(self)
def is_loaded(self):
"""
Returns true if a presentation is loaded.
"""
log.debug('is loaded LibreOffice')
return self.client.is_loaded()
def is_active(self):
"""
Returns true if a presentation is active and running.
"""
log.debug('is active LibreOffice')
return self.client.is_active()
def unblank_screen(self):
"""
Unblanks the screen.
"""
log.debug('unblank screen LibreOffice')
return self.client.unblank_screen()
def blank_screen(self):
"""
Blanks the screen.
"""
log.debug('blank screen LibreOffice')
self.client.blank_screen()
def is_blank(self):
"""
Returns true if screen is blank.
"""
log.debug('is blank LibreOffice')
return self.client.is_blank()
def stop_presentation(self):
"""
Stop the presentation, remove from screen.
"""
log.debug('stop presentation LibreOffice')
self.client.stop_presentation()
def start_presentation(self):
"""
Start the presentation from the beginning.
"""
log.debug('start presentation LibreOffice')
self.client.start_presentation()
# Make sure impress doesn't steal focus, unless we're on a single screen setup
if len(ScreenList()) > 1:
Registry().get('main_window').activateWindow()
def get_slide_number(self):
"""
Return the current slide number on the screen, from 1.
"""
return self.client.get_slide_number()
def get_slide_count(self):
"""
Return the total number of slides.
"""
return self.client.get_slide_count()
def goto_slide(self, slide_no):
"""
Go to a specific slide (from 1).
:param slide_no: The slide the text is required for, starting at 1
"""
self.client.goto_slide(slide_no)
def next_step(self):
"""
Triggers the next effect of slide on the running presentation.
"""
self.client.next_step()
def previous_step(self):
"""
Triggers the previous slide on the running presentation.
"""
self.client.previous_step()
def get_slide_text(self, slide_no):
"""
Returns the text on the slide.
:param slide_no: The slide the text is required for, starting at 1
"""
return self.client.get_slide_text(slide_no)
def get_slide_notes(self, slide_no):
"""
Returns the text in the slide notes.
:param slide_no: The slide the notes are required for, starting at 1
"""
return self.client.get_slide_notes(slide_no)

View File

@ -412,7 +412,8 @@ class PresentationController(object):
""" """
log.info('PresentationController loaded') log.info('PresentationController loaded')
def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument): def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument,
display_name=None):
""" """
This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins
@ -432,6 +433,7 @@ class PresentationController(object):
self.docs = [] self.docs = []
self.plugin = plugin self.plugin = plugin
self.name = name self.name = name
self.display_name = display_name if display_name is not None else name
self.document_class = document_class self.document_class = document_class
self.settings_section = self.plugin.settings_section self.settings_section = self.plugin.settings_section
self.available = None self.available = None

View File

@ -127,10 +127,10 @@ class PresentationTab(SettingsTab):
def set_controller_text(self, checkbox, controller): def set_controller_text(self, checkbox, controller):
if checkbox.isEnabled(): if checkbox.isEnabled():
checkbox.setText(controller.name) checkbox.setText(controller.display_name)
else: else:
checkbox.setText(translate('PresentationPlugin.PresentationTab', checkbox.setText(translate('PresentationPlugin.PresentationTab',
'{name} (unavailable)').format(name=controller.name)) '{name} (unavailable)').format(name=controller.display_name))
def load(self): def load(self):
""" """

View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
This module contains some helpers for serializing Path objects in Pyro4
"""
try:
from openlp.core.common.path import Path
except ImportError:
from pathlib import Path
from Pyro4.util import SerializerBase
def path_class_to_dict(obj):
"""
Serialize a Path object for Pyro4
"""
return {
'__class__': 'Path',
'parts': obj.parts
}
def path_dict_to_class(classname, d):
return Path(d['parts'])
def register_classes():
"""
Register the serializers
"""
SerializerBase.register_class_to_dict(Path, path_class_to_dict)
SerializerBase.register_dict_to_class('Path', path_dict_to_class)

View File

@ -0,0 +1,5 @@
Vendor Directory
================
Do not delete this directory, it is used on Mac OS to place Pyro4 and serpent for use with Impress.

View File

@ -28,13 +28,13 @@ import os
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.state import State
from openlp.core.api.http import register_endpoint from openlp.core.api.http import register_endpoint
from openlp.core.common import extension_loader from openlp.core.common import extension_loader
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.lib.plugin import Plugin, StringContent from openlp.core.lib.plugin import Plugin, StringContent
from openlp.core.state import State
from openlp.core.ui.icons import UiIcons from openlp.core.ui.icons import UiIcons
from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController from openlp.plugins.presentations.lib.presentationcontroller import PresentationController
@ -45,9 +45,11 @@ from openlp.plugins.presentations.lib.presentationtab import PresentationTab
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
__default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked, __default_settings__ = {
'presentations/override app': QtCore.Qt.Unchecked,
'presentations/enable_pdf_program': QtCore.Qt.Unchecked, 'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
'presentations/pdf_program': None, 'presentations/pdf_program': None,
'presentations/maclo': QtCore.Qt.Checked,
'presentations/Impress': QtCore.Qt.Checked, 'presentations/Impress': QtCore.Qt.Checked,
'presentations/Powerpoint': QtCore.Qt.Checked, 'presentations/Powerpoint': QtCore.Qt.Checked,
'presentations/Pdf': QtCore.Qt.Checked, 'presentations/Pdf': QtCore.Qt.Checked,
@ -56,7 +58,7 @@ __default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked,
'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked, 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked,
'presentations/powerpoint control window': QtCore.Qt.Unchecked, 'presentations/powerpoint control window': QtCore.Qt.Unchecked,
'presentations/last directory': None 'presentations/last directory': None
} }
class PresentationPlugin(Plugin): class PresentationPlugin(Plugin):
@ -100,7 +102,7 @@ class PresentationPlugin(Plugin):
try: try:
self.controllers[controller].start_process() self.controllers[controller].start_process()
except Exception: except Exception:
log.warning('Failed to start controller process') log.exception('Failed to start controller process')
self.controllers[controller].available = False self.controllers[controller].available = False
self.media_item.build_file_mask_string() self.media_item.build_file_mask_string()

View File

@ -160,6 +160,8 @@ def check_module(mod, text='', indent=' '):
w('OK') w('OK')
except ImportError: except ImportError:
w('FAIL') w('FAIL')
except Exception:
w('ERROR')
w(os.linesep) w(os.linesep)

View File

@ -110,7 +110,18 @@ class TestPath(TestCase):
# WHEN: Calling `path_to_str` with an invalid Type # WHEN: Calling `path_to_str` with an invalid Type
# THEN: A TypeError should have been raised # THEN: A TypeError should have been raised
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
path_to_str(str()) path_to_str(57)
def test_path_to_str_wth_str(self):
"""
Test that `path_to_str` just returns a str when given a str
"""
# GIVEN: The `path_to_str` function
# WHEN: Calling `path_to_str` with a str
result = path_to_str('/usr/bin')
# THEN: The string should be returned
assert result == '/usr/bin'
def test_path_to_str_none(self): def test_path_to_str_none(self):
""" """

View File

@ -0,0 +1,948 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
Functional tests to test the LibreOffice Pyro server
"""
from unittest.mock import MagicMock, patch, call
from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType, main
def test_constructor():
"""
Test the Constructor from the server
"""
# GIVEN: No server
# WHEN: The server object is created
server = LibreOfficeServer()
# THEN: The server should have been set up correctly
assert server._control is None
# assert server._desktop is None
assert server._document is None
assert server._presentation is None
assert server._process is None
@patch('openlp.plugins.presentations.lib.libreofficeserver.Popen')
def test_start_process(MockedPopen):
"""
Test that the correct command is issued to run LibreOffice
"""
# GIVEN: A LOServer
mocked_process = MagicMock()
MockedPopen.return_value = mocked_process
server = LibreOfficeServer()
# WHEN: The start_process() method is run
server.start_process()
# THEN: The correct command line should run and the process should have started
MockedPopen.assert_called_with([
'/Applications/LibreOffice.app/Contents/MacOS/soffice',
'--nologo',
'--norestore',
'--minimized',
'--nodefault',
'--nofirststartwizard',
'--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager'
])
assert server._process is mocked_process
@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
def test_desktop_already_has_desktop(mocked_uno):
"""
Test that setup_desktop() exits early when there's already a desktop
"""
# GIVEN: A LibreOfficeServer instance
server = LibreOfficeServer()
server._desktop = MagicMock()
# WHEN: the desktop property is called
desktop = server.desktop
# THEN: setup_desktop() exits early
assert desktop is server._desktop
assert server._manager is None
@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
def test_desktop_exception(mocked_uno):
"""
Test that setting up the desktop works correctly when an exception occurs
"""
# GIVEN: A LibreOfficeServer instance
server = LibreOfficeServer()
mocked_context = MagicMock()
mocked_resolver = MagicMock()
mocked_uno_instance = MagicMock()
MockedServiceManager = MagicMock()
mocked_uno.getComponentContext.return_value = mocked_context
mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver
mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance]
mocked_uno_instance.ServiceManager = MockedServiceManager
MockedServiceManager.createInstanceWithContext.side_effect = Exception()
# WHEN: the desktop property is called
server.desktop
# THEN: A desktop object was created
mocked_uno.getComponentContext.assert_called_once_with()
mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with(
'com.sun.star.bridge.UnoUrlResolver', mocked_context)
expected_calls = [
call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'),
call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext')
]
assert mocked_resolver.resolve.call_args_list == expected_calls
MockedServiceManager.createInstanceWithContext.assert_called_once_with(
'com.sun.star.frame.Desktop', mocked_uno_instance)
assert server._manager is MockedServiceManager
assert server._desktop is None
@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
def test_desktop(mocked_uno):
"""
Test that setting up the desktop works correctly
"""
# GIVEN: A LibreOfficeServer instance
server = LibreOfficeServer()
mocked_context = MagicMock()
mocked_resolver = MagicMock()
mocked_uno_instance = MagicMock()
MockedServiceManager = MagicMock()
mocked_desktop = MagicMock()
mocked_uno.getComponentContext.return_value = mocked_context
mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver
mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance]
mocked_uno_instance.ServiceManager = MockedServiceManager
MockedServiceManager.createInstanceWithContext.return_value = mocked_desktop
# WHEN: the desktop property is called
server.desktop
# THEN: A desktop object was created
mocked_uno.getComponentContext.assert_called_once_with()
mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with(
'com.sun.star.bridge.UnoUrlResolver', mocked_context)
expected_calls = [
call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'),
call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext')
]
assert mocked_resolver.resolve.call_args_list == expected_calls
MockedServiceManager.createInstanceWithContext.assert_called_once_with(
'com.sun.star.frame.Desktop', mocked_uno_instance)
assert server._manager is MockedServiceManager
assert server._desktop is mocked_desktop
@patch('openlp.plugins.presentations.lib.libreofficeserver.PropertyValue')
def test_create_property(MockedPropertyValue):
"""
Test that the _create_property() method works correctly
"""
# GIVEN: A server amnd property to set
server = LibreOfficeServer()
name = 'Hidden'
value = True
# WHEN: The _create_property() method is called
prop = server._create_property(name, value)
# THEN: The property should have the correct attributes
assert prop.Name == name
assert prop.Value == value
def test_get_text_from_page_slide_text():
"""
Test that the _get_text_from_page() method gives us nothing for slide text
"""
# GIVEN: A LibreOfficeServer object and some mocked objects
text_type = TextType.SlideText
slide_no = 1
server = LibreOfficeServer()
server._document = MagicMock()
mocked_pages = MagicMock()
mocked_page = MagicMock()
mocked_shape = MagicMock()
server._document.getDrawPages.return_value = mocked_pages
mocked_pages.getCount.return_value = 1
mocked_pages.getByIndex.return_value = mocked_page
mocked_page.getByIndex.return_value = mocked_shape
mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
mocked_shape.supportsService.return_value = True
mocked_shape.getString.return_value = 'Page Text'
# WHEN: _get_text_from_page() is run for slide text
text = server._get_text_from_page(slide_no, text_type)
# THE: The text is correct
assert text == 'Page Text\n'
def test_get_text_from_page_title():
"""
Test that the _get_text_from_page() method gives us the text from the titles
"""
# GIVEN: A LibreOfficeServer object and some mocked objects
text_type = TextType.Title
slide_no = 1
server = LibreOfficeServer()
server._document = MagicMock()
mocked_pages = MagicMock()
mocked_page = MagicMock()
mocked_shape = MagicMock()
server._document.getDrawPages.return_value = mocked_pages
mocked_pages.getCount.return_value = 1
mocked_pages.getByIndex.return_value = mocked_page
mocked_page.getByIndex.return_value = mocked_shape
mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
mocked_shape.supportsService.return_value = True
mocked_shape.getString.return_value = 'Page Title'
# WHEN: _get_text_from_page() is run for titles
text = server._get_text_from_page(slide_no, text_type)
# THEN: The text should be correct
assert text == 'Page Title\n'
def test_get_text_from_page_notes():
"""
Test that the _get_text_from_page() method gives us the text from the notes
"""
# GIVEN: A LibreOfficeServer object and some mocked objects
text_type = TextType.Notes
slide_no = 1
server = LibreOfficeServer()
server._document = MagicMock()
mocked_pages = MagicMock()
mocked_page = MagicMock()
mocked_notes_page = MagicMock()
mocked_shape = MagicMock()
server._document.getDrawPages.return_value = mocked_pages
mocked_pages.getCount.return_value = 1
mocked_pages.getByIndex.return_value = mocked_page
mocked_page.getNotesPage.return_value = mocked_notes_page
mocked_notes_page.getByIndex.return_value = mocked_shape
mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
mocked_shape.supportsService.return_value = True
mocked_shape.getString.return_value = 'Page Notes'
# WHEN: _get_text_from_page() is run for titles
text = server._get_text_from_page(slide_no, text_type)
# THEN: The text should be correct
assert text == 'Page Notes\n'
def test_shutdown_other_docs():
"""
Test the shutdown method while other documents are open in LibreOffice
"""
def close_docs():
server._docs = []
# GIVEN: An up an running LibreOfficeServer
server = LibreOfficeServer()
mocked_doc = MagicMock()
mocked_desktop = MagicMock()
mocked_docs = MagicMock()
mocked_list = MagicMock()
mocked_element_doc = MagicMock()
server._docs = [mocked_doc]
server._desktop = mocked_desktop
server._process = MagicMock()
mocked_doc.close_presentation.side_effect = close_docs
mocked_desktop.getComponents.return_value = mocked_docs
mocked_docs.hasElements.return_value = True
mocked_docs.createEnumeration.return_value = mocked_list
mocked_list.hasMoreElements.side_effect = [True, False]
mocked_list.nextElement.return_value = mocked_element_doc
mocked_element_doc.getImplementationName.side_effect = [
'org.openlp.Nothing',
'com.sun.star.comp.framework.BackingComp'
]
# WHEN: shutdown() is called
server.shutdown()
# THEN: The right methods are called and everything works
mocked_doc.close_presentation.assert_called_once_with()
mocked_desktop.getComponents.assert_called_once_with()
mocked_docs.hasElements.assert_called_once_with()
mocked_docs.createEnumeration.assert_called_once_with()
assert mocked_list.hasMoreElements.call_count == 2
mocked_list.nextElement.assert_called_once_with()
mocked_element_doc.getImplementationName.assert_called_once_with()
assert mocked_desktop.terminate.call_count == 0
assert server._process.kill.call_count == 0
def test_shutdown():
"""
Test the shutdown method
"""
def close_docs():
server._docs = []
# GIVEN: An up an running LibreOfficeServer
server = LibreOfficeServer()
mocked_doc = MagicMock()
mocked_desktop = MagicMock()
mocked_docs = MagicMock()
mocked_list = MagicMock()
mocked_element_doc = MagicMock()
server._docs = [mocked_doc]
server._desktop = mocked_desktop
server._process = MagicMock()
mocked_doc.close_presentation.side_effect = close_docs
mocked_desktop.getComponents.return_value = mocked_docs
mocked_docs.hasElements.return_value = True
mocked_docs.createEnumeration.return_value = mocked_list
mocked_list.hasMoreElements.side_effect = [True, False]
mocked_list.nextElement.return_value = mocked_element_doc
mocked_element_doc.getImplementationName.return_value = 'com.sun.star.comp.framework.BackingComp'
# WHEN: shutdown() is called
server.shutdown()
# THEN: The right methods are called and everything works
mocked_doc.close_presentation.assert_called_once_with()
mocked_desktop.getComponents.assert_called_once_with()
mocked_docs.hasElements.assert_called_once_with()
mocked_docs.createEnumeration.assert_called_once_with()
assert mocked_list.hasMoreElements.call_count == 2
mocked_list.nextElement.assert_called_once_with()
mocked_element_doc.getImplementationName.assert_called_once_with()
mocked_desktop.terminate.assert_called_once_with()
server._process.kill.assert_called_once_with()
@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
def test_load_presentation_exception(mocked_uno):
"""
Test the load_presentation() method when an exception occurs
"""
# GIVEN: A LibreOfficeServer object
presentation_file = '/path/to/presentation.odp'
screen_number = 1
server = LibreOfficeServer()
mocked_desktop = MagicMock()
mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
server._desktop = mocked_desktop
mocked_desktop.loadComponentFromURL.side_effect = Exception()
# WHEN: load_presentation() is called
with patch.object(server, '_create_property') as mocked_create_property:
mocked_create_property.side_effect = lambda x, y: {x: y}
result = server.load_presentation(presentation_file, screen_number)
# THEN: A presentation is loaded
assert result is False
@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
def test_load_presentation(mocked_uno):
"""
Test the load_presentation() method
"""
# GIVEN: A LibreOfficeServer object
presentation_file = '/path/to/presentation.odp'
screen_number = 1
server = LibreOfficeServer()
mocked_desktop = MagicMock()
mocked_document = MagicMock()
mocked_presentation = MagicMock()
mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
server._desktop = mocked_desktop
mocked_desktop.loadComponentFromURL.return_value = mocked_document
mocked_document.getPresentation.return_value = mocked_presentation
# WHEN: load_presentation() is called
with patch.object(server, '_create_property') as mocked_create_property:
mocked_create_property.side_effect = lambda x, y: {x: y}
result = server.load_presentation(presentation_file, screen_number)
# THEN: A presentation is loaded
assert result is True
mocked_uno.systemPathToFileUrl.assert_called_once_with(presentation_file)
mocked_create_property.assert_called_once_with('Hidden', True)
mocked_desktop.loadComponentFromURL.assert_called_once_with(
presentation_file, '_blank', 0, ({'Hidden': True},))
assert server._document is mocked_document
mocked_document.getPresentation.assert_called_once_with()
assert server._presentation is mocked_presentation
assert server._presentation.Display == screen_number
assert server._control is None
@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
def test_extract_thumbnails_no_pages(mocked_uno):
"""
Test the extract_thumbnails() method when there are no pages
"""
# GIVEN: A LibreOfficeServer instance
temp_folder = '/tmp'
server = LibreOfficeServer()
mocked_document = MagicMock()
server._document = mocked_document
mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
mocked_document.getDrawPages.return_value = None
# WHEN: The extract_thumbnails() method is called
with patch.object(server, '_create_property') as mocked_create_property:
mocked_create_property.side_effect = lambda x, y: {x: y}
thumbnails = server.extract_thumbnails(temp_folder)
# THEN: Thumbnails have been extracted
mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder)
mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export')
mocked_document.getDrawPages.assert_called_once_with()
assert thumbnails == []
@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
@patch('openlp.plugins.presentations.lib.libreofficeserver.os')
def test_extract_thumbnails(mocked_os, mocked_uno):
"""
Test the extract_thumbnails() method
"""
# GIVEN: A LibreOfficeServer instance
temp_folder = '/tmp'
server = LibreOfficeServer()
mocked_document = MagicMock()
mocked_pages = MagicMock()
mocked_page_1 = MagicMock()
mocked_page_2 = MagicMock()
mocked_controller = MagicMock()
server._document = mocked_document
mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
mocked_document.getDrawPages.return_value = mocked_pages
mocked_os.path.isdir.return_value = False
mocked_pages.getCount.return_value = 2
mocked_pages.getByIndex.side_effect = [mocked_page_1, mocked_page_2]
mocked_document.getCurrentController.return_value = mocked_controller
mocked_os.path.join.side_effect = lambda *x: '/'.join(x)
# WHEN: The extract_thumbnails() method is called
with patch.object(server, '_create_property') as mocked_create_property:
mocked_create_property.side_effect = lambda x, y: {x: y}
thumbnails = server.extract_thumbnails(temp_folder)
# THEN: Thumbnails have been extracted
mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder)
mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export')
mocked_document.getDrawPages.assert_called_once_with()
mocked_pages.getCount.assert_called_once_with()
assert mocked_pages.getByIndex.call_args_list == [call(0), call(1)]
assert mocked_controller.setCurrentPage.call_args_list == \
[call(mocked_page_1), call(mocked_page_2)]
assert mocked_document.storeToURL.call_args_list == \
[call('/tmp/1.png', ({'FilterName': 'impress_png_Export'},)),
call('/tmp/2.png', ({'FilterName': 'impress_png_Export'},))]
assert thumbnails == ['/tmp/1.png', '/tmp/2.png']
def test_get_titles_and_notes():
"""
Test the get_titles_and_notes() method
"""
# GIVEN: A LibreOfficeServer object and a bunch of mocks
server = LibreOfficeServer()
mocked_document = MagicMock()
mocked_pages = MagicMock()
server._document = mocked_document
mocked_document.getDrawPages.return_value = mocked_pages
mocked_pages.getCount.return_value = 2
# WHEN: get_titles_and_notes() is called
with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page:
mocked_get_text_from_page.side_effect = [
'OpenLP on Mac OS X',
'',
'',
'Installing is a drag-and-drop affair'
]
titles, notes = server.get_titles_and_notes()
# THEN: The right calls are made and the right stuff returned
mocked_document.getDrawPages.assert_called_once_with()
mocked_pages.getCount.assert_called_once_with()
assert mocked_get_text_from_page.call_count == 4
expected_calls = [
call(1, TextType.Title), call(1, TextType.Notes),
call(2, TextType.Title), call(2, TextType.Notes),
]
assert mocked_get_text_from_page.call_args_list == expected_calls
assert titles == ['OpenLP on Mac OS X\n', '\n'], titles
assert notes == [' ', 'Installing is a drag-and-drop affair'], notes
def test_close_presentation():
"""
Test that closing the presentation cleans things up correctly
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
mocked_document = MagicMock()
mocked_presentation = MagicMock()
server._document = mocked_document
server._presentation = mocked_presentation
# WHEN: close_presentation() is called
server.close_presentation()
# THEN: The presentation and document should be closed
mocked_presentation.end.assert_called_once_with()
mocked_document.dispose.assert_called_once_with()
assert server._document is None
assert server._presentation is None
def test_is_loaded_no_objects():
"""
Test the is_loaded() method when there's no document or presentation
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
# WHEN: The is_loaded() method is called
result = server.is_loaded()
# THEN: The result should be false
assert result is False
def test_is_loaded_no_presentation():
"""
Test the is_loaded() method when there's no presentation
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
mocked_document = MagicMock()
server._document = mocked_document
server._presentation = MagicMock()
mocked_document.getPresentation.return_value = None
# WHEN: The is_loaded() method is called
result = server.is_loaded()
# THEN: The result should be false
assert result is False
mocked_document.getPresentation.assert_called_once_with()
def test_is_loaded_exception():
"""
Test the is_loaded() method when an exception is thrown
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
mocked_document = MagicMock()
server._document = mocked_document
server._presentation = MagicMock()
mocked_document.getPresentation.side_effect = Exception()
# WHEN: The is_loaded() method is called
result = server.is_loaded()
# THEN: The result should be false
assert result is False
mocked_document.getPresentation.assert_called_once_with()
def test_is_loaded():
"""
Test the is_loaded() method
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
mocked_document = MagicMock()
mocked_presentation = MagicMock()
server._document = mocked_document
server._presentation = mocked_presentation
mocked_document.getPresentation.return_value = mocked_presentation
# WHEN: The is_loaded() method is called
result = server.is_loaded()
# THEN: The result should be false
assert result is True
mocked_document.getPresentation.assert_called_once_with()
def test_is_active_not_loaded():
"""
Test is_active() when is_loaded() returns False
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
# WHEN: is_active() is called with is_loaded() returns False
with patch.object(server, 'is_loaded') as mocked_is_loaded:
mocked_is_loaded.return_value = False
result = server.is_active()
# THEN: It should have returned False
assert result is False
def test_is_active_no_control():
"""
Test is_active() when is_loaded() returns True but there's no control
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
# WHEN: is_active() is called with is_loaded() returns False
with patch.object(server, 'is_loaded') as mocked_is_loaded:
mocked_is_loaded.return_value = True
result = server.is_active()
# THEN: The result should be False
assert result is False
mocked_is_loaded.assert_called_once_with()
def test_is_active():
"""
Test is_active()
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
mocked_control = MagicMock()
server._control = mocked_control
mocked_control.isRunning.return_value = True
# WHEN: is_active() is called with is_loaded() returns False
with patch.object(server, 'is_loaded') as mocked_is_loaded:
mocked_is_loaded.return_value = True
result = server.is_active()
# THEN: The result should be False
assert result is True
mocked_is_loaded.assert_called_once_with()
mocked_control.isRunning.assert_called_once_with()
def test_unblank_screen():
"""
Test the unblank_screen() method
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
mocked_control = MagicMock()
server._control = mocked_control
# WHEN: unblank_screen() is run
server.unblank_screen()
# THEN: The resume method should have been called
mocked_control.resume.assert_called_once_with()
def test_blank_screen():
"""
Test the blank_screen() method
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
mocked_control = MagicMock()
server._control = mocked_control
# WHEN: blank_screen() is run
server.blank_screen()
# THEN: The resume method should have been called
mocked_control.blankScreen.assert_called_once_with(0)
def test_is_blank_no_control():
"""
Test the is_blank() method when there's no control
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
# WHEN: is_blank() is called
result = server.is_blank()
# THEN: It should have returned False
assert result is False
def test_is_blank_control_is_running():
"""
Test the is_blank() method when the control is running
"""
# GIVEN: A LibreOfficeServer instance and a bunch of mocks
server = LibreOfficeServer()
mocked_control = MagicMock()
server._control = mocked_control
mocked_control.isRunning.return_value = True
mocked_control.isPaused.return_value = True
# WHEN: is_blank() is called
result = server.is_blank()
# THEN: It should have returned False
assert result is True
mocked_control.isRunning.assert_called_once_with()
mocked_control.isPaused.assert_called_once_with()
def test_stop_presentation():
"""
Test the stop_presentation() method
"""
# GIVEN: A LibreOfficeServer instance and a mocked presentation
server = LibreOfficeServer()
mocked_presentation = MagicMock()
mocked_control = MagicMock()
server._presentation = mocked_presentation
server._control = mocked_control
# WHEN: stop_presentation() is called
server.stop_presentation()
# THEN: The presentation is ended and the control is removed
mocked_presentation.end.assert_called_once_with()
assert server._control is None
@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep')
def test_start_presentation_no_control(mocked_sleep):
"""
Test the start_presentation() method when there's no control
"""
# GIVEN: A LibreOfficeServer instance and some mocks
server = LibreOfficeServer()
mocked_control = MagicMock()
mocked_document = MagicMock()
mocked_presentation = MagicMock()
mocked_controller = MagicMock()
mocked_frame = MagicMock()
mocked_window = MagicMock()
server._document = mocked_document
server._presentation = mocked_presentation
mocked_document.getCurrentController.return_value = mocked_controller
mocked_controller.getFrame.return_value = mocked_frame
mocked_frame.getContainerWindow.return_value = mocked_window
mocked_presentation.getController.side_effect = [None, mocked_control]
# WHEN: start_presentation() is called
server.start_presentation()
# THEN: The slide number should be correct
mocked_document.getCurrentController.assert_called_once_with()
mocked_controller.getFrame.assert_called_once_with()
mocked_frame.getContainerWindow.assert_called_once_with()
mocked_presentation.start.assert_called_once_with()
assert mocked_presentation.getController.call_count == 2
mocked_sleep.assert_called_once_with(0.1)
assert mocked_window.setVisible.call_args_list == [call(True), call(False)]
assert server._control is mocked_control
def test_start_presentation():
"""
Test the start_presentation() method when there's a control
"""
# GIVEN: A LibreOfficeServer instance and some mocks
server = LibreOfficeServer()
mocked_control = MagicMock()
server._control = mocked_control
# WHEN: start_presentation() is called
with patch.object(server, 'goto_slide') as mocked_goto_slide:
server.start_presentation()
# THEN: The control should have been activated and the first slide selected
mocked_control.activate.assert_called_once_with()
mocked_goto_slide.assert_called_once_with(1)
def test_get_slide_number():
"""
Test the get_slide_number() method
"""
# GIVEN: A LibreOfficeServer instance and some mocks
server = LibreOfficeServer()
mocked_control = MagicMock()
mocked_control.getCurrentSlideIndex.return_value = 3
server._control = mocked_control
# WHEN: get_slide_number() is called
result = server.get_slide_number()
# THEN: The slide number should be correct
assert result == 4
def test_get_slide_count():
"""
Test the get_slide_count() method
"""
# GIVEN: A LibreOfficeServer instance and some mocks
server = LibreOfficeServer()
mocked_document = MagicMock()
mocked_pages = MagicMock()
server._document = mocked_document
mocked_document.getDrawPages.return_value = mocked_pages
mocked_pages.getCount.return_value = 2
# WHEN: get_slide_count() is called
result = server.get_slide_count()
# THEN: The slide count should be correct
assert result == 2
def test_goto_slide():
"""
Test the goto_slide() method
"""
# GIVEN: A LibreOfficeServer instance and some mocks
server = LibreOfficeServer()
mocked_control = MagicMock()
server._control = mocked_control
# WHEN: goto_slide() is called
server.goto_slide(1)
# THEN: The slide number should be correct
mocked_control.gotoSlideIndex.assert_called_once_with(0)
@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep')
def test_next_step_when_paused(mocked_sleep):
"""
Test the next_step() method when paused
"""
# GIVEN: A LibreOfficeServer instance and a mocked control
server = LibreOfficeServer()
mocked_control = MagicMock()
server._control = mocked_control
mocked_control.isPaused.side_effect = [False, True]
# WHEN: next_step() is called
server.next_step()
# THEN: The correct call should be made
mocked_control.gotoNextEffect.assert_called_once_with()
mocked_sleep.assert_called_once_with(0.1)
assert mocked_control.isPaused.call_count == 2
mocked_control.gotoPreviousEffect.assert_called_once_with()
@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep')
def test_next_step(mocked_sleep):
"""
Test the next_step() method when paused
"""
# GIVEN: A LibreOfficeServer instance and a mocked control
server = LibreOfficeServer()
mocked_control = MagicMock()
server._control = mocked_control
mocked_control.isPaused.side_effect = [True, True]
# WHEN: next_step() is called
server.next_step()
# THEN: The correct call should be made
mocked_control.gotoNextEffect.assert_called_once_with()
mocked_sleep.assert_called_once_with(0.1)
assert mocked_control.isPaused.call_count == 1
assert mocked_control.gotoPreviousEffect.call_count == 0
def test_previous_step():
"""
Test the previous_step() method
"""
# GIVEN: A LibreOfficeServer instance and a mocked control
server = LibreOfficeServer()
mocked_control = MagicMock()
server._control = mocked_control
# WHEN: previous_step() is called
server.previous_step()
# THEN: The correct call should be made
mocked_control.gotoPreviousEffect.assert_called_once_with()
def test_get_slide_text():
"""
Test the get_slide_text() method
"""
# GIVEN: A LibreOfficeServer instance
server = LibreOfficeServer()
# WHEN: get_slide_text() is called for a particular slide
with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page:
mocked_get_text_from_page.return_value = 'OpenLP on Mac OS X'
result = server.get_slide_text(5)
# THEN: The text should be returned
mocked_get_text_from_page.assert_called_once_with(5)
assert result == 'OpenLP on Mac OS X'
def test_get_slide_notes():
"""
Test the get_slide_notes() method
"""
# GIVEN: A LibreOfficeServer instance
server = LibreOfficeServer()
# WHEN: get_slide_notes() is called for a particular slide
with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page:
mocked_get_text_from_page.return_value = 'Installing is a drag-and-drop affair'
result = server.get_slide_notes(3)
# THEN: The text should be returned
mocked_get_text_from_page.assert_called_once_with(3, TextType.Notes)
assert result == 'Installing is a drag-and-drop affair'
@patch('openlp.plugins.presentations.lib.libreofficeserver.Daemon')
def test_main(MockedDaemon):
"""
Test the main() function
"""
# GIVEN: Mocked out Pyro objects
mocked_daemon = MagicMock()
MockedDaemon.return_value = mocked_daemon
# WHEN: main() is run
main()
# THEN: The correct calls are made
MockedDaemon.assert_called_once_with(host='localhost', port=4310)
mocked_daemon.register.assert_called_once_with(LibreOfficeServer, 'openlp.libreofficeserver')
mocked_daemon.requestLoop.assert_called_once_with()
mocked_daemon.close.assert_called_once_with()

View File

@ -0,0 +1,453 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
Functional tests to test the Mac LibreOffice class and related methods.
"""
import shutil
from tempfile import mkdtemp
from unittest import TestCase
from unittest.mock import MagicMock, patch, call
from openlp.core.common.settings import Settings
from openlp.core.common.path import Path
from openlp.plugins.presentations.lib.maclocontroller import MacLOController, MacLODocument
from openlp.plugins.presentations.presentationplugin import __default_settings__
from tests.helpers.testmixin import TestMixin
from tests.utils.constants import TEST_RESOURCES_PATH
class TestMacLOController(TestCase, TestMixin):
"""
Test the MacLOController Class
"""
def setUp(self):
"""
Set up the patches and mocks need for all tests.
"""
self.setup_application()
self.build_settings()
self.mock_plugin = MagicMock()
self.temp_folder = mkdtemp()
self.mock_plugin.settings_section = self.temp_folder
def tearDown(self):
"""
Stop the patches
"""
self.destroy_settings()
shutil.rmtree(self.temp_folder)
@patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
def test_constructor(self, mocked_start_server):
"""
Test the Constructor from the MacLOController
"""
# GIVEN: No presentation controller
controller = None
# WHEN: The presentation controller object is created
controller = MacLOController(plugin=self.mock_plugin)
# THEN: The name of the presentation controller should be correct
assert controller.name == 'maclo', \
'The name of the presentation controller should be correct'
assert controller.display_name == 'Impress on macOS', \
'The display name of the presentation controller should be correct'
mocked_start_server.assert_called_once_with()
@patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
@patch('openlp.plugins.presentations.lib.maclocontroller.Proxy')
def test_client(self, MockedProxy, mocked_start_server):
"""
Test the client property of the Controller
"""
# GIVEN: A controller without a client and a mocked out Pyro
controller = MacLOController(plugin=self.mock_plugin)
mocked_client = MagicMock()
MockedProxy.return_value = mocked_client
mocked_client._pyroConnection = None
# WHEN: the client property is called the first time
client = controller.client
# THEN: a client is created
assert client == mocked_client
MockedProxy.assert_called_once_with('PYRO:openlp.libreofficeserver@localhost:4310')
mocked_client._pyroReconnect.assert_called_once_with()
@patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
def test_check_available(self, mocked_start_server):
"""
Test the check_available() method
"""
from openlp.plugins.presentations.lib.maclocontroller import macuno_available
# GIVEN: A controller
controller = MacLOController(plugin=self.mock_plugin)
# WHEN: check_available() is run
result = controller.check_available()
# THEN: it should return false
assert result == macuno_available
@patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
def test_start_process(self, mocked_start_server):
"""
Test the start_process() method
"""
# GIVEN: A controller and a client
controller = MacLOController(plugin=self.mock_plugin)
controller._client = MagicMock()
# WHEN: start_process() is called
controller.start_process()
# THEN: The client's start_process() should have been called
controller._client.start_process.assert_called_once_with()
@patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
def test_kill(self, mocked_start_server):
"""
Test the kill() method
"""
# GIVEN: A controller and a client
controller = MacLOController(plugin=self.mock_plugin)
controller._client = MagicMock()
controller.server_process = MagicMock()
# WHEN: start_process() is called
controller.kill()
# THEN: The client's start_process() should have been called
controller._client.shutdown.assert_called_once_with()
controller.server_process.kill.assert_called_once_with()
class TestMacLODocument(TestCase):
"""
Test the MacLODocument Class
"""
def setUp(self):
mocked_plugin = MagicMock()
mocked_plugin.settings_section = 'presentations'
Settings().extend_default_settings(__default_settings__)
self.file_name = Path(TEST_RESOURCES_PATH) / 'presentations' / 'test.odp'
self.mocked_client = MagicMock()
with patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server'):
self.controller = MacLOController(mocked_plugin)
self.controller._client = self.mocked_client
self.document = MacLODocument(self.controller, self.file_name)
@patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList')
def test_load_presentation_cannot_load(self, MockedScreenList):
"""
Test the load_presentation() method when the server can't load the presentation
"""
# GIVEN: A document and a mocked client
mocked_screen_list = MagicMock()
MockedScreenList.return_value = mocked_screen_list
mocked_screen_list.current.number = 0
self.mocked_client.load_presentation.return_value = False
# WHEN: load_presentation() is called
result = self.document.load_presentation()
# THEN: Stuff should work right
self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1)
assert result is False
@patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList')
def test_load_presentation(self, MockedScreenList):
"""
Test the load_presentation() method
"""
# GIVEN: A document and a mocked client
mocked_screen_list = MagicMock()
MockedScreenList.return_value = mocked_screen_list
mocked_screen_list.current.number = 0
self.mocked_client.load_presentation.return_value = True
# WHEN: load_presentation() is called
with patch.object(self.document, 'create_thumbnails') as mocked_create_thumbnails, \
patch.object(self.document, 'create_titles_and_notes') as mocked_create_titles_and_notes:
result = self.document.load_presentation()
# THEN: Stuff should work right
self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1)
mocked_create_thumbnails.assert_called_once_with()
mocked_create_titles_and_notes.assert_called_once_with()
assert result is True
def test_create_thumbnails_already_exist(self):
"""
Test the create_thumbnails() method when thumbnails already exist
"""
# GIVEN: thumbnails that exist and a mocked client
self.document.check_thumbnails = MagicMock(return_value=True)
# WHEN: create_thumbnails() is called
self.document.create_thumbnails()
# THEN: The method should exit early
assert self.mocked_client.extract_thumbnails.call_count == 0
@patch('openlp.plugins.presentations.lib.maclocontroller.delete_file')
def test_create_thumbnails(self, mocked_delete_file):
"""
Test the create_thumbnails() method
"""
# GIVEN: thumbnails that don't exist and a mocked client
self.document.check_thumbnails = MagicMock(return_value=False)
self.mocked_client.extract_thumbnails.return_value = ['thumb1.png', 'thumb2.png']
# WHEN: create_thumbnails() is called
with patch.object(self.document, 'convert_thumbnail') as mocked_convert_thumbnail, \
patch.object(self.document, 'get_temp_folder') as mocked_get_temp_folder:
mocked_get_temp_folder.return_value = 'temp'
self.document.create_thumbnails()
# THEN: The method should complete successfully
self.mocked_client.extract_thumbnails.assert_called_once_with('temp')
assert mocked_convert_thumbnail.call_args_list == [
call(Path('thumb1.png'), 1), call(Path('thumb2.png'), 2)]
assert mocked_delete_file.call_args_list == [call(Path('thumb1.png')), call(Path('thumb2.png'))]
def test_create_titles_and_notes(self):
"""
Test create_titles_and_notes() method
"""
# GIVEN: mocked client and mocked save_titles_and_notes() method
self.mocked_client.get_titles_and_notes.return_value = ('OpenLP', 'This is a note')
# WHEN: create_titles_and_notes() is called
with patch.object(self.document, 'save_titles_and_notes') as mocked_save_titles_and_notes:
self.document.create_titles_and_notes()
# THEN save_titles_and_notes should have been called
self.mocked_client.get_titles_and_notes.assert_called_once_with()
mocked_save_titles_and_notes.assert_called_once_with('OpenLP', 'This is a note')
def test_close_presentation(self):
"""
Test the close_presentation() method
"""
# GIVEN: A mocked client and mocked remove_doc() method
# WHEN: close_presentation() is called
with patch.object(self.controller, 'remove_doc') as mocked_remove_doc:
self.document.close_presentation()
# THEN: The presentation should have been closed
self.mocked_client.close_presentation.assert_called_once_with()
mocked_remove_doc.assert_called_once_with(self.document)
def test_is_loaded(self):
"""
Test the is_loaded() method
"""
# GIVEN: A mocked client
self.mocked_client.is_loaded.return_value = True
# WHEN: is_loaded() is called
result = self.document.is_loaded()
# THEN: Then the result should be correct
assert result is True
def test_is_active(self):
"""
Test the is_active() method
"""
# GIVEN: A mocked client
self.mocked_client.is_active.return_value = True
# WHEN: is_active() is called
result = self.document.is_active()
# THEN: Then the result should be correct
assert result is True
def test_unblank_screen(self):
"""
Test the unblank_screen() method
"""
# GIVEN: A mocked client
self.mocked_client.unblank_screen.return_value = True
# WHEN: unblank_screen() is called
result = self.document.unblank_screen()
# THEN: Then the result should be correct
self.mocked_client.unblank_screen.assert_called_once_with()
assert result is True
def test_blank_screen(self):
"""
Test the blank_screen() method
"""
# GIVEN: A mocked client
self.mocked_client.blank_screen.return_value = True
# WHEN: blank_screen() is called
self.document.blank_screen()
# THEN: Then the result should be correct
self.mocked_client.blank_screen.assert_called_once_with()
def test_is_blank(self):
"""
Test the is_blank() method
"""
# GIVEN: A mocked client
self.mocked_client.is_blank.return_value = True
# WHEN: is_blank() is called
result = self.document.is_blank()
# THEN: Then the result should be correct
assert result is True
def test_stop_presentation(self):
"""
Test the stop_presentation() method
"""
# GIVEN: A mocked client
self.mocked_client.stop_presentation.return_value = True
# WHEN: stop_presentation() is called
self.document.stop_presentation()
# THEN: Then the result should be correct
self.mocked_client.stop_presentation.assert_called_once_with()
@patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList')
@patch('openlp.plugins.presentations.lib.maclocontroller.Registry')
def test_start_presentation(self, MockedRegistry, MockedScreenList):
"""
Test the start_presentation() method
"""
# GIVEN: a mocked client, and multiple screens
mocked_screen_list = MagicMock()
mocked_screen_list.__len__.return_value = 2
mocked_registry = MagicMock()
mocked_main_window = MagicMock()
MockedScreenList.return_value = mocked_screen_list
MockedRegistry.return_value = mocked_registry
mocked_screen_list.screen_list = [0, 1]
mocked_registry.get.return_value = mocked_main_window
# WHEN: start_presentation() is called
self.document.start_presentation()
# THEN: The presentation should be started
self.mocked_client.start_presentation.assert_called_once_with()
mocked_registry.get.assert_called_once_with('main_window')
mocked_main_window.activateWindow.assert_called_once_with()
def test_get_slide_number(self):
"""
Test the get_slide_number() method
"""
# GIVEN: A mocked client
self.mocked_client.get_slide_number.return_value = 5
# WHEN: get_slide_number() is called
result = self.document.get_slide_number()
# THEN: Then the result should be correct
assert result == 5
def test_get_slide_count(self):
"""
Test the get_slide_count() method
"""
# GIVEN: A mocked client
self.mocked_client.get_slide_count.return_value = 8
# WHEN: get_slide_count() is called
result = self.document.get_slide_count()
# THEN: Then the result should be correct
assert result == 8
def test_goto_slide(self):
"""
Test the goto_slide() method
"""
# GIVEN: A mocked client
# WHEN: goto_slide() is called
self.document.goto_slide(3)
# THEN: Then the result should be correct
self.mocked_client.goto_slide.assert_called_once_with(3)
def test_next_step(self):
"""
Test the next_step() method
"""
# GIVEN: A mocked client
# WHEN: next_step() is called
self.document.next_step()
# THEN: Then the result should be correct
self.mocked_client.next_step.assert_called_once_with()
def test_previous_step(self):
"""
Test the previous_step() method
"""
# GIVEN: A mocked client
# WHEN: previous_step() is called
self.document.previous_step()
# THEN: Then the result should be correct
self.mocked_client.previous_step.assert_called_once_with()
def test_get_slide_text(self):
"""
Test the get_slide_text() method
"""
# GIVEN: A mocked client
self.mocked_client.get_slide_text.return_value = 'Some slide text'
# WHEN: get_slide_text() is called
result = self.document.get_slide_text(1)
# THEN: Then the result should be correct
self.mocked_client.get_slide_text.assert_called_once_with(1)
assert result == 'Some slide text'
def test_get_slide_notes(self):
"""
Test the get_slide_notes() method
"""
# GIVEN: A mocked client
self.mocked_client.get_slide_notes.return_value = 'This is a note'
# WHEN: get_slide_notes() is called
result = self.document.get_slide_notes(2)
# THEN: Then the result should be correct
self.mocked_client.get_slide_notes.assert_called_once_with(2)
assert result == 'This is a note'