From faeef883155f27da91b2563b753eabc4cfb42564 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 1 Nov 2016 14:14:02 +0200 Subject: [PATCH 01/36] Add a directory to put the extra libraries, and ignore the libraries so that they don't get added to version control --- .bzrignore | 2 ++ openlp/plugins/presentations/lib/vendor/do_not_delete.txt | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 openlp/plugins/presentations/lib/vendor/do_not_delete.txt diff --git a/.bzrignore b/.bzrignore index 2c7f64680..6cd66e8c4 100644 --- a/.bzrignore +++ b/.bzrignore @@ -47,3 +47,5 @@ coverage tags output htmlcov +openlp/plugins/presentations/lib/vendor/Pyro4 +openlp/plugins/presentations/lib/vendor/serpent.py diff --git a/openlp/plugins/presentations/lib/vendor/do_not_delete.txt b/openlp/plugins/presentations/lib/vendor/do_not_delete.txt new file mode 100644 index 000000000..0c81c2425 --- /dev/null +++ b/openlp/plugins/presentations/lib/vendor/do_not_delete.txt @@ -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. + From c5709b97781a9db30c51c82a36d376f464ef4284 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 1 Nov 2016 20:01:12 +0200 Subject: [PATCH 02/36] Take a stab at writing a presentation controller to control LibreOffice on Mac via Pyro --- .../presentations/lib/libreofficeserver.py | 361 ++++++++++++++++++ .../presentations/lib/maclocontroller.py | 302 +++++++++++++++ 2 files changed, 663 insertions(+) create mode 100644 openlp/plugins/presentations/lib/libreofficeserver.py create mode 100644 openlp/plugins/presentations/lib/maclocontroller.py diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py new file mode 100644 index 000000000..6d2b5752c --- /dev/null +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -0,0 +1,361 @@ +""" +This module runs a Pyro4 server using LibreOffice's version of Python +""" +from subprocess import Popen +import sys +import os +import logging + +# 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 Pyro4 import Daemon, expose, locateNS +from com.sun.star.beans import PropertyValue +from com.sun.star.task import ErrorCodeIOException +import uno + +logging.basicConfig(filename=os.path.dirname(__file__) + '/libreofficeserver.log', level=logging.INFO) +log = logging.getLogger(__name__) + + +class TextType(object): + """ + Type Enumeration for Types of Text to request + """ + Title = 0 + SlideText = 1 + Notes = 2 + + +@expose +class LibreOfficeServer(object): + """ + A Pyro4 server which controls LibreOffice + """ + def __init__(self): + """ + Set up the LibreOffice server + """ + self._init_impress() + + def _init_impress(self) + """ + Initialise Impress + """ + uno_command = [ + '/Applications/LibreOffice.app/Contents/MacOS/soffice', + '--nologo', + '--norestore', + '--minimized', + '--nodefault', + '--nofirststartwizard', + '"--accept=pipe,name=openlp_pipe;urp;"' + ] + self._process = Popen(uno_command) + uno_instance = None + context = uno.getComponentContext() + resolver = context.ServiceManager.createInstanceWithContext('com.sun.star.bridge.UnoUrlResolver', context) + while uno_instance is None and loop < 3: + try: + uno_instance = resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') + except: + log.warning('Unable to find running instance ') + loop += 1 + try: + self._manager = uno_instance.ServiceManager + log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop') + self._desktop = self.manager.createInstanceWithContext("com.sun.star.frame.Desktop", uno_instance) + except: + log.warning('Failed to get UNO desktop') + + 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 has_desktop(self): + """ + Say if we have a desktop object + """ + return self._desktop is not None + + def shutdown(self): + """ + Shut down the server + """ + while self._docs: + self._docs[0].close_presentation() + if not self._desktop: + return + 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') + else: + try: + self._desktop.terminate() + log.debug('LibreOffice killed') + except: + log.warning('Failed to terminate LibreOffice') + + 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),) + try: + self._document = self._desktop.loadComponentFromURL(url, '_blank', 0, properties) + except: + log.warning('Failed to load presentation {url}'.format(url=url)) + return False + self._presentation = self._document.getPresentation() + self._presentation.Display = screen_number + self._control = None + self.create_thumbnails() + self.create_titles_and_notes() + return True + + def create_thumbnails(self, temp_folder): + """ + Create thumbnails for the presentation + """ + thumb_dir_url = uno.systemPathToFileUrl(temp_folder) + properties = (self._create_property('FilterName', 'impress_png_Export'),) + doc = self.document + pages = doc.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) + doc.getCurrentController().setCurrentPage(page) + url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1)) + path = os.path.join(self.get_temp_folder(), str(index + 1) + '.png') + try: + doc.storeToURL(url_path, properties) + self.convert_thumbnail(path, index + 1) + delete_file(path) + except ErrorCodeIOException as exception: + log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) + except: + log.exception('{path} - Unable to store openoffice preview'.format(path=path)) + + def get_title_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 = [] + 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. Triggered by new object being added to SlideController or OpenLP being + shutdown. + """ + log.debug('close Presentation LibreOffice') + if self._document: + if self._presentation: + try: + self._presentation.end() + self._presentation = None + self._document.dispose() + except: + log.warning("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: + log.warning("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() + ns = locateNS() + uri = daemon.register(LibreOfficeServer) + ns.register('openlp.libreofficeserver', uri) + try: + daemon.requestLoop() + finally: + daemon.close() + + +if __name__ == '__main__': + main() + diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py new file mode 100644 index 000000000..8083d379d --- /dev/null +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + +# OOo API documentation: +# http://api.openoffice.org/docs/common/ref/com/sun/star/presentation/XSlideShowController.html +# http://wiki.services.openoffice.org/wiki/Documentation/DevGuide/ProUNO/Basic +# /Getting_Information_about_UNO_Objects#Inspecting_interfaces_during_debugging +# http://docs.go-oo.org/sd/html/classsd_1_1SlideShow.html +# http://www.oooforum.org/forum/viewtopic.phtml?t=5252 +# http://wiki.services.openoffice.org/wiki/Documentation/DevGuide/Working_with_Presentations +# http://mail.python.org/pipermail/python-win32/2008-January/006676.html +# http://www.linuxjournal.com/content/starting-stopping-and-connecting-openoffice-python +# http://nxsy.org/comparing-documents-with-openoffice-and-python + +import logging +import os +import time +from subprocess import Popen +from multiprocessing import Process + +from openlp.core.common import is_mac, Registry, delete_file + +if is_mac() and os.path.exists('/Applications/LibreOffice.app'): + macuno_available = True +else: + macuno_available = False + +from PyQt5 import QtCore +from Pyro4 import Proxy +from Pyro4.naming import NameServerDaemon + +from openlp.core.lib import ScreenList +from .presentationcontroller import PresentationController, PresentationDocument, TextType + + +log = logging.getLogger(__name__) + + +def run_nameserver(): + """ + Run a Pyro4 nameserver + """ + ns_daemon = NameServerDaemon() + ns_daemon._pyroHmacKey = 'openlp-libreoffice' + try: + ns_daemon.requestLoop() + finally: + ns_daemon.close() + + +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) + self.supports = ['odp'] + self.also_supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm'] + self.server_process = None + self.nameserver_process = None + self.client = None + self._start_nameserver() + self._start_server() + self._setup_client() + + def _start_nameserver(self): + """ + Start the Pyro4 nameserver + """ + self.nameserver_process = Process(run_nameserver) + self.nameserver_process.start() + + def _start_server(self): + """ + Start a LibreOfficeServer + """ + libreoffice_python = '/Applications/LibreOffice.app/Contents/Resources/python' + libreoffice_server = os.path.join(os.path.dirname(__file__), 'libreofficeserver.py') + self.server_process = Popen([libreoffice_python, libreoffice_server]) + + def _setup_client(self): + """ + Set up a Pyro4 client so that we can talk to the LibreOfficeServer + """ + self.client = Proxy('PYRONAME:openlp.libreofficeserver') + + 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('start process Openoffice') + if is_win(): + self.manager = self.get_com_servicemanager() + self.manager._FlagAsMethod('Bridge_GetStruct') + self.manager._FlagAsMethod('Bridge_GetValueObject') + else: + # -headless + cmd = get_uno_command() + self.process = QtCore.QProcess() + self.process.startDetached(cmd) + + def kill(self): + """ + Called at system exit to clean up any running presentations. + """ + log.debug('Kill LibreOffice') + self.client.shutdown() + self.server_process.terminate() + self.nameserver_process.terminate() + + +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 + self.document = None + self.presentation = None + self.control = None + + def load_presentation(self): + """ + Called when a presentation is added to the SlideController. It builds the environment, starts communcations with + the background LibreOffice task started earlier. If LibreOffice is not present is is started. Once the environment + is available the presentation is loaded and started. + """ + log.debug('Load Presentation LibreOffice') + if not self.client.has_desktop(): + return False + if not self.client.load_presentation(self.file_path, ScreenList().current['number'] + 1): + return False + self.create_thumbnails() + self.client.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 + self.client.create_thumbnails(self.get_temp_folder()) + + 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().screen_list) > 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) + From c7750a5f1a0c6e4baa4f130445a494310321e14d Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Wed, 2 Nov 2016 00:41:10 +0200 Subject: [PATCH 03/36] We can load presentations via LibreOffice now. Next step: more bugfixing, tests, and sorting out one or two hard-coded paths --- openlp/plugins/media/lib/mediaitem.py | 3 +- .../presentations/lib/libreofficeserver.py | 101 ++++++++++-------- .../presentations/lib/maclocontroller.py | 69 ++++-------- .../presentations/presentationplugin.py | 30 +++--- 4 files changed, 95 insertions(+), 108 deletions(-) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index dc196fb59..00fccb657 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -150,7 +150,8 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): triggers=self.on_replace_click) if 'webkit' not in get_media_players()[0]: self.replace_action.setDisabled(True) - self.replace_action_context.setDisabled(True) + if hasattr(self, 'replace_action_context'): + self.replace_action_context.setDisabled(True) self.reset_action = self.toolbar.add_toolbar_action('reset_action', icon=':/system/system_close.png', visible=False, triggers=self.on_reset_click) self.media_widget = QtWidgets.QWidget(self) diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index 6d2b5752c..10504e29e 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -9,10 +9,10 @@ import logging # 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 Pyro4 import Daemon, expose, locateNS +import uno from com.sun.star.beans import PropertyValue from com.sun.star.task import ErrorCodeIOException -import uno +from Pyro4 import Daemon, expose, locateNS logging.basicConfig(filename=os.path.dirname(__file__) + '/libreofficeserver.log', level=logging.INFO) log = logging.getLogger(__name__) @@ -34,11 +34,15 @@ class LibreOfficeServer(object): """ def __init__(self): """ - Set up the LibreOffice server + Set up the server """ - self._init_impress() + self._desktop = None + self._document = None + self._control = None + self._presentation = None + self._process = None - def _init_impress(self) + def start_process(self): """ Initialise Impress """ @@ -49,23 +53,31 @@ class LibreOfficeServer(object): '--minimized', '--nodefault', '--nofirststartwizard', - '"--accept=pipe,name=openlp_pipe;urp;"' + '--accept=socket,port=2002;urp;' ] self._process = Popen(uno_command) + + def setup_desktop(self): + """ + Set up an UNO desktop instance + """ + if self.has_desktop(): + return 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_pipe;urp;StarOffice.ComponentContext') - except: + uno_instance = resolver.resolve('uno:socket,port=2002;urp;StarOffice.ComponentContext') + except Exception as e: log.warning('Unable to find running instance ') loop += 1 try: self._manager = uno_instance.ServiceManager log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop') - self._desktop = self.manager.createInstanceWithContext("com.sun.star.frame.Desktop", uno_instance) - except: + self._desktop = self._manager.createInstanceWithContext("com.sun.star.frame.Desktop", uno_instance) + except Exception as e: log.warning('Failed to get UNO desktop') def _create_property(self, name, value): @@ -106,32 +118,35 @@ class LibreOfficeServer(object): """ Say if we have a desktop object """ - return self._desktop is not None + return hasattr(self, '_desktop') and self._desktop is not None def shutdown(self): """ Shut down the server """ - while self._docs: - self._docs[0].close_presentation() - if not self._desktop: - return - 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') - else: - try: - self._desktop.terminate() - log.debug('LibreOffice killed') - except: - log.warning('Failed to terminate LibreOffice') + if hasattr(self, '_docs'): + while self._docs: + self._docs[0].close_presentation() + if self.has_desktop(): + 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') + else: + try: + self._desktop.terminate() + log.debug('LibreOffice killed') + except: + log.warning('Failed to terminate LibreOffice') + if getattr(self, '_process'): + self._process.kill() + def load_presentation(self, file_path, screen_number): """ @@ -148,35 +163,33 @@ class LibreOfficeServer(object): self._presentation = self._document.getPresentation() self._presentation.Display = screen_number self._control = None - self.create_thumbnails() - self.create_titles_and_notes() return True - def create_thumbnails(self, temp_folder): + 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'),) - doc = self.document - pages = doc.getDrawPages() + 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) - doc.getCurrentController().setCurrentPage(page) + self._document.getCurrentController().setCurrentPage(page) url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1)) - path = os.path.join(self.get_temp_folder(), str(index + 1) + '.png') + path = os.path.join(temp_folder, str(index + 1) + '.png') try: - doc.storeToURL(url_path, properties) - self.convert_thumbnail(path, index + 1) - delete_file(path) + self._document.storeToURL(url_path, properties) + thumbnails.append(path) except ErrorCodeIOException as exception: log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) except: log.exception('{path} - Unable to store openoffice preview'.format(path=path)) + return thumbnails def get_title_and_notes(self): """ @@ -346,10 +359,8 @@ def main(): """ The main function which runs the server """ - daemon = Daemon() - ns = locateNS() - uri = daemon.register(LibreOfficeServer) - ns.register('openlp.libreofficeserver', uri) + daemon = Daemon(host='localhost', port=4310) + uri = daemon.register(LibreOfficeServer, 'openlp.libreofficeserver') try: daemon.requestLoop() finally: diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index 8083d379d..85df2178f 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -35,18 +35,16 @@ import logging import os import time from subprocess import Popen -from multiprocessing import Process -from openlp.core.common import is_mac, Registry, delete_file +from openlp.core.common import is_macosx, Registry, delete_file -if is_mac() and os.path.exists('/Applications/LibreOffice.app'): +if is_macosx() and os.path.exists('/Applications/LibreOffice.app'): macuno_available = True else: macuno_available = False from PyQt5 import QtCore from Pyro4 import Proxy -from Pyro4.naming import NameServerDaemon from openlp.core.lib import ScreenList from .presentationcontroller import PresentationController, PresentationDocument, TextType @@ -55,18 +53,6 @@ from .presentationcontroller import PresentationController, PresentationDocument log = logging.getLogger(__name__) -def run_nameserver(): - """ - Run a Pyro4 nameserver - """ - ns_daemon = NameServerDaemon() - ns_daemon._pyroHmacKey = 'openlp-libreoffice' - try: - ns_daemon.requestLoop() - finally: - ns_daemon.close() - - class MacLOController(PresentationController): """ Class to control interactions with MacLO presentations on Mac OS X via Pyro4. It starts the Pyro4 nameserver, @@ -83,18 +69,8 @@ class MacLOController(PresentationController): self.supports = ['odp'] self.also_supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm'] self.server_process = None - self.nameserver_process = None - self.client = None - self._start_nameserver() + self._client = None self._start_server() - self._setup_client() - - def _start_nameserver(self): - """ - Start the Pyro4 nameserver - """ - self.nameserver_process = Process(run_nameserver) - self.nameserver_process.start() def _start_server(self): """ @@ -104,11 +80,16 @@ class MacLOController(PresentationController): libreoffice_server = os.path.join(os.path.dirname(__file__), 'libreofficeserver.py') self.server_process = Popen([libreoffice_python, libreoffice_server]) - def _setup_client(self): + @property + def client(self): """ Set up a Pyro4 client so that we can talk to the LibreOfficeServer """ - self.client = Proxy('PYRONAME:openlp.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): """ @@ -119,19 +100,11 @@ class MacLOController(PresentationController): 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. + 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('start process Openoffice') - if is_win(): - self.manager = self.get_com_servicemanager() - self.manager._FlagAsMethod('Bridge_GetStruct') - self.manager._FlagAsMethod('Bridge_GetValueObject') - else: - # -headless - cmd = get_uno_command() - self.process = QtCore.QProcess() - self.process.startDetached(cmd) + log.debug('Started automatically by the Pyro server') + self.client.start_process() def kill(self): """ @@ -139,8 +112,7 @@ class MacLOController(PresentationController): """ log.debug('Kill LibreOffice') self.client.shutdown() - self.server_process.terminate() - self.nameserver_process.terminate() + self.server_process.kill() class MacLODocument(PresentationDocument): @@ -155,9 +127,6 @@ class MacLODocument(PresentationDocument): log.debug('Init Presentation LibreOffice') super(MacLODocument, self).__init__(controller, presentation) self.client = controller.client - self.document = None - self.presentation = None - self.control = None def load_presentation(self): """ @@ -166,12 +135,13 @@ class MacLODocument(PresentationDocument): is available the presentation is loaded and started. """ log.debug('Load Presentation LibreOffice') + self.client.setup_desktop() if not self.client.has_desktop(): return False if not self.client.load_presentation(self.file_path, ScreenList().current['number'] + 1): return False self.create_thumbnails() - self.client.create_titles_and_notes() + self.create_titles_and_notes() return True def create_thumbnails(self): @@ -181,7 +151,10 @@ class MacLODocument(PresentationDocument): log.debug('create thumbnails LibreOffice') if self.check_thumbnails(): return - self.client.create_thumbnails(self.get_temp_folder()) + temp_thumbnails = self.client.extract_thumbnails(self.get_temp_folder()) + for index, temp_thumb in enumerate(temp_thumbnails): + self.convert_thumbnail(temp_thumb, index + 1) + delete_file(temp_thumb) def create_titles_and_notes(self): """ diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index ca0ecba82..505f0dbb7 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -36,18 +36,20 @@ from openlp.plugins.presentations.lib import PresentationController, Presentatio log = logging.getLogger(__name__) -__default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked, - 'presentations/enable_pdf_program': QtCore.Qt.Unchecked, - 'presentations/pdf_program': '', - 'presentations/Impress': QtCore.Qt.Checked, - 'presentations/Powerpoint': QtCore.Qt.Checked, - 'presentations/Powerpoint Viewer': QtCore.Qt.Checked, - 'presentations/Pdf': QtCore.Qt.Checked, - 'presentations/presentations files': [], - 'presentations/thumbnail_scheme': '', - 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked, - 'presentations/powerpoint control window': QtCore.Qt.Unchecked - } +__default_settings__ = { + 'presentations/override app': QtCore.Qt.Unchecked, + 'presentations/enable_pdf_program': QtCore.Qt.Unchecked, + 'presentations/pdf_program': '', + 'presentations/MacLO': QtCore.Qt.Checked, + 'presentations/Impress': QtCore.Qt.Checked, + 'presentations/Powerpoint': QtCore.Qt.Checked, + 'presentations/Powerpoint Viewer': QtCore.Qt.Checked, + 'presentations/Pdf': QtCore.Qt.Checked, + 'presentations/presentations files': [], + 'presentations/thumbnail_scheme': '', + 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked, + 'presentations/powerpoint control window': QtCore.Qt.Unchecked +} class PresentationPlugin(Plugin): @@ -87,7 +89,7 @@ class PresentationPlugin(Plugin): try: self.controllers[controller].start_process() except Exception: - log.warning('Failed to start controller process') + log.exception('Failed to start controller process') self.controllers[controller].available = False self.media_item.build_file_mask_string() @@ -132,7 +134,7 @@ class PresentationPlugin(Plugin): try: __import__(module_name, globals(), locals(), []) except ImportError: - log.warning('Failed to import {name} on path {path}'.format(name=module_name, path=path)) + log.exception('Failed to import {name} on path {path}'.format(name=module_name, path=path)) controller_classes = PresentationController.__subclasses__() for controller_class in controller_classes: controller = controller_class(self) From 66eb2a63d3ddbec9de2479594c4d39bf222aebef Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Wed, 2 Nov 2016 00:44:01 +0200 Subject: [PATCH 04/36] Change back to using named pipes --- openlp/plugins/presentations/lib/libreofficeserver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index 10504e29e..d313bc3b4 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -53,7 +53,7 @@ class LibreOfficeServer(object): '--minimized', '--nodefault', '--nofirststartwizard', - '--accept=socket,port=2002;urp;' + '--accept=pipe,name=openlp_pipe;urp;' ] self._process = Popen(uno_command) @@ -69,7 +69,7 @@ class LibreOfficeServer(object): loop = 0 while uno_instance is None and loop < 3: try: - uno_instance = resolver.resolve('uno:socket,port=2002;urp;StarOffice.ComponentContext') + uno_instance = resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') except Exception as e: log.warning('Unable to find running instance ') loop += 1 From 96246c991a50c063868ee85cf10607e97805061c Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Wed, 2 Nov 2016 19:17:43 +0200 Subject: [PATCH 05/36] Fix a few issues in the server, and an erroneous 'formta' call --- openlp/plugins/presentations/lib/libreofficeserver.py | 3 ++- openlp/plugins/presentations/lib/messagelistener.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index d313bc3b4..2482bc581 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -5,6 +5,7 @@ from subprocess import Popen import sys import os import logging +import time # Add the vendor directory to sys.path so that we can load Pyro4 sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor')) @@ -191,7 +192,7 @@ class LibreOfficeServer(object): log.exception('{path} - Unable to store openoffice preview'.format(path=path)) return thumbnails - def get_title_and_notes(self): + def get_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 diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index c12f1b0fb..37d192fdb 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -191,7 +191,7 @@ class Controller(object): """ Based on the handler passed at startup triggers the previous slide event. """ - log.debug('Live = {live}, previous'.formta(live=self.is_live)) + log.debug('Live = {live}, previous'.format(live=self.is_live)) if not self.doc: return if not self.is_live: From eb6945b6e9b87c9d4d6a7494c87e77bc6e67c4af Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Wed, 2 Nov 2016 23:46:00 +0200 Subject: [PATCH 06/36] Now with tests --- .../presentations/lib/libreofficeserver.py | 30 +- .../presentations/test_libreofficeserver.py | 273 ++++++++++++++++++ 2 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 tests/functional/openlp_plugins/presentations/test_libreofficeserver.py diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index 2482bc581..55359d8ca 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -10,12 +10,24 @@ import time # Add the vendor directory to sys.path so that we can load Pyro4 sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor')) -import uno -from com.sun.star.beans import PropertyValue -from com.sun.star.task import ErrorCodeIOException -from Pyro4 import Daemon, expose, locateNS +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: + # But they need to be defined for mocking + uno = None + PropertyValue = None + ErrorCodeIOException = Exception + +if sys.platform.startswith('darwin') and uno is not None: + # 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') + logging.basicConfig(filename=logfile, level=logging.INFO) -logging.basicConfig(filename=os.path.dirname(__file__) + '/libreofficeserver.log', level=logging.INFO) log = logging.getLogger(__name__) @@ -37,9 +49,9 @@ class LibreOfficeServer(object): """ Set up the server """ + self._control = None self._desktop = None self._document = None - self._control = None self._presentation = None self._process = None @@ -77,7 +89,7 @@ class LibreOfficeServer(object): try: self._manager = uno_instance.ServiceManager log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop') - self._desktop = self._manager.createInstanceWithContext("com.sun.star.frame.Desktop", uno_instance) + self._desktop = self._manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance) except Exception as e: log.warning('Failed to get UNO desktop') @@ -109,9 +121,9 @@ class LibreOfficeServer(object): for index in range(page.getCount()): shape = page.getByIndex(index) shape_type = shape.getShapeType() - if shape.supportsService("com.sun.star.drawing.Text"): + 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": + if text_type != TextType.Title or shape_type == 'com.sun.star.presentation.TitleTextShape': text += shape.getString() + '\n' return text diff --git a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py new file mode 100644 index 000000000..d8a6da46f --- /dev/null +++ b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Functional tests to test the LibreOffice Pyro server +""" +from unittest import TestCase + +from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType + +from tests.functional import MagicMock, patch, call + + +class TestLibreOfficeServer(TestCase): + """ + Test the LibreOfficeServer Class + """ + def test_constructor(self): + """ + 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 + self.assertIsNone(server._control) + self.assertIsNone(server._desktop) + self.assertIsNone(server._document) + self.assertIsNone(server._presentation) + self.assertIsNone(server._process) + + @patch('openlp.plugins.presentations.lib.libreofficeserver.Popen') + def test_start_process(self, 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_pipe;urp;' + ]) + self.assertEqual(mocked_process, server._process) + + @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') + def test_setup_desktop(self, 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: setup_desktop() is called + server.setup_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) + self.assertEqual( + [ + call('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext'), + call('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') + ], + mocked_resolver.resolve.call_args_list + ) + MockedServiceManager.createInstanceWithContext.assert_called_once_with( + 'com.sun.star.frame.Desktop', mocked_uno_instance) + self.assertEqual(MockedServiceManager, server._manager) + self.assertEqual(mocked_desktop, server._desktop) + + @patch('openlp.plugins.presentations.lib.libreofficeserver.PropertyValue') + def test_create_property(self, 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 + self.assertEqual(name, prop.Name) + self.assertEqual(value, prop.Value) + + def test_get_text_from_page_slide_text(self): + """ + 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 + self.assertEqual('Page Text\n', text) + + def test_get_text_from_page_title(self): + """ + 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 + self.assertEqual('Page Title\n', text) + + def test_get_text_from_page_notes(self): + """ + 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 + self.assertEqual('Page Notes\n', text) + + def test_has_desktop_no_desktop(self): + """ + Test the has_desktop() method when there's no desktop + """ + # GIVEN: A LibreOfficeServer object + server = LibreOfficeServer() + + # WHEN: has_desktop() is called + result = server.has_desktop() + + # THEN: The result should be False + self.assertFalse(result) + + def test_has_desktop(self): + """ + Test the has_desktop() method + """ + # GIVEN: A LibreOfficeServer object and a desktop + server = LibreOfficeServer() + server._desktop = MagicMock() + + # WHEN: has_desktop() is called + result = server.has_desktop() + + # THEN: The result should be True + self.assertTrue(result) + + def test_shutdown(self): + """ + Test the shutdown method + """ + # 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() + def close_docs(): + server._docs = [] + 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() + self.assertEqual(2, mocked_list.hasMoreElements.call_count) + 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() From 5e765252b68f0bcc03978f77116966cefd1fa41f Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 4 Nov 2016 00:45:06 +0200 Subject: [PATCH 07/36] More tests --- .../presentations/test_libreofficeserver.py | 107 +++++++++++++++--- 1 file changed, 91 insertions(+), 16 deletions(-) diff --git a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py index d8a6da46f..c4e3521e1 100644 --- a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py +++ b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py @@ -42,11 +42,11 @@ class TestLibreOfficeServer(TestCase): server = LibreOfficeServer() # THEN: The server should have been set up correctly - self.assertIsNone(server._control) - self.assertIsNone(server._desktop) - self.assertIsNone(server._document) - self.assertIsNone(server._presentation) - self.assertIsNone(server._process) + 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(self, MockedPopen): @@ -71,7 +71,7 @@ class TestLibreOfficeServer(TestCase): '--nofirststartwizard', '--accept=pipe,name=openlp_pipe;urp;' ]) - self.assertEqual(mocked_process, server._process) + assert server._process is mocked_process @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') def test_setup_desktop(self, mocked_uno): @@ -107,8 +107,8 @@ class TestLibreOfficeServer(TestCase): ) MockedServiceManager.createInstanceWithContext.assert_called_once_with( 'com.sun.star.frame.Desktop', mocked_uno_instance) - self.assertEqual(MockedServiceManager, server._manager) - self.assertEqual(mocked_desktop, server._desktop) + assert server._manager is MockedServiceManager + assert server._desktop is mocked_desktop @patch('openlp.plugins.presentations.lib.libreofficeserver.PropertyValue') def test_create_property(self, MockedPropertyValue): @@ -124,8 +124,8 @@ class TestLibreOfficeServer(TestCase): prop = server._create_property(name, value) # THEN: The property should have the correct attributes - self.assertEqual(name, prop.Name) - self.assertEqual(value, prop.Value) + assert prop.Name == name + assert prop.Value == value def test_get_text_from_page_slide_text(self): """ @@ -151,7 +151,7 @@ class TestLibreOfficeServer(TestCase): text = server._get_text_from_page(slide_no, text_type) # THE: The text is correct - self.assertEqual('Page Text\n', text) + assert text == 'Page Text\n' def test_get_text_from_page_title(self): """ @@ -177,7 +177,7 @@ class TestLibreOfficeServer(TestCase): text = server._get_text_from_page(slide_no, text_type) # THEN: The text should be correct - self.assertEqual('Page Title\n', text) + assert text == 'Page Title\n' def test_get_text_from_page_notes(self): """ @@ -205,7 +205,7 @@ class TestLibreOfficeServer(TestCase): text = server._get_text_from_page(slide_no, text_type) # THEN: The text should be correct - self.assertEqual('Page Notes\n', text) + assert text == 'Page Notes\n' def test_has_desktop_no_desktop(self): """ @@ -218,7 +218,7 @@ class TestLibreOfficeServer(TestCase): result = server.has_desktop() # THEN: The result should be False - self.assertFalse(result) + assert result is False def test_has_desktop(self): """ @@ -232,7 +232,7 @@ class TestLibreOfficeServer(TestCase): result = server.has_desktop() # THEN: The result should be True - self.assertTrue(result) + assert result is True def test_shutdown(self): """ @@ -266,8 +266,83 @@ class TestLibreOfficeServer(TestCase): mocked_desktop.getComponents.assert_called_once_with() mocked_docs.hasElements.assert_called_once_with() mocked_docs.createEnumeration.assert_called_once_with() - self.assertEqual(2, mocked_list.hasMoreElements.call_count) + 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(self, 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') + @patch('openlp.plugins.presentations.lib.libreofficeserver.os') + def test_extract_thumbnails(self, 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'] From b9cf077b36f0f58395801957ed9a2325c9eebd9d Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 4 Nov 2016 17:13:37 +0200 Subject: [PATCH 08/36] Remove the class, makes the tests run faster. --- .../presentations/test_libreofficeserver.py | 556 +++++++++--------- 1 file changed, 275 insertions(+), 281 deletions(-) diff --git a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py index c4e3521e1..b0ba05826 100644 --- a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py +++ b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py @@ -29,320 +29,314 @@ from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer from tests.functional import MagicMock, patch, call -class TestLibreOfficeServer(TestCase): +def test_constructor(): """ - Test the LibreOfficeServer Class + Test the Constructor from the server """ - def test_constructor(self): - """ - Test the Constructor from the server - """ - # GIVEN: No server - # WHEN: The server object is created - server = LibreOfficeServer() + # 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 + # 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(self, MockedPopen): - """ - Test that the correct command is issued to run LibreOffice - """ - # GIVEN: A LOServer - mocked_process = MagicMock() - MockedPopen.return_value = mocked_process - server = LibreOfficeServer() +@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() + # 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_pipe;urp;' - ]) - assert server._process is mocked_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_pipe;urp;' + ]) + assert server._process is mocked_process - @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') - def test_setup_desktop(self, 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 +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') +def test_setup_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: setup_desktop() is called - server.setup_desktop() + # WHEN: setup_desktop() is called + server.setup_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) - self.assertEqual( - [ - call('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext'), - call('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') - ], - mocked_resolver.resolve.call_args_list - ) - 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 + # 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_pipe;urp;StarOffice.ComponentContext'), + call('uno:pipe,name=openlp_pipe;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(self, MockedPropertyValue): - """ - Test that the _create_property() method works correctly - """ - # GIVEN: A server amnd property to set - server = LibreOfficeServer() - name = 'Hidden' - value = True +@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) + # 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 + # THEN: The property should have the correct attributes + assert prop.Name == name + assert prop.Value == value - def test_get_text_from_page_slide_text(self): - """ - 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' +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) + # 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' + # THE: The text is correct + assert text == 'Page Text\n' - def test_get_text_from_page_title(self): - """ - 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' +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) + # 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' + # THEN: The text should be correct + assert text == 'Page Title\n' - def test_get_text_from_page_notes(self): - """ - 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' +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) + # 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' + # THEN: The text should be correct + assert text == 'Page Notes\n' - def test_has_desktop_no_desktop(self): - """ - Test the has_desktop() method when there's no desktop - """ - # GIVEN: A LibreOfficeServer object - server = LibreOfficeServer() +def test_has_desktop_no_desktop(): + """ + Test the has_desktop() method when there's no desktop + """ + # GIVEN: A LibreOfficeServer object + server = LibreOfficeServer() - # WHEN: has_desktop() is called - result = server.has_desktop() + # WHEN: has_desktop() is called + result = server.has_desktop() - # THEN: The result should be False - assert result is False + # THEN: The result should be False + assert result is False - def test_has_desktop(self): - """ - Test the has_desktop() method - """ - # GIVEN: A LibreOfficeServer object and a desktop - server = LibreOfficeServer() - server._desktop = MagicMock() +def test_has_desktop(): + """ + Test the has_desktop() method + """ + # GIVEN: A LibreOfficeServer object and a desktop + server = LibreOfficeServer() + server._desktop = MagicMock() - # WHEN: has_desktop() is called - result = server.has_desktop() + # WHEN: has_desktop() is called + result = server.has_desktop() - # THEN: The result should be True - assert result is True + # THEN: The result should be True + assert result is True - def test_shutdown(self): - """ - Test the shutdown method - """ - # 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() - def close_docs(): - server._docs = [] - 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' +def test_shutdown(): + """ + Test the shutdown method + """ + # 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() + def close_docs(): + server._docs = [] + 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() + # 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() + # 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(self, 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 +@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) + # 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 + # 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') - @patch('openlp.plugins.presentations.lib.libreofficeserver.os') - def test_extract_thumbnails(self, 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) +@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) + # 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'] + # 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'] From 5fe84e41305ade6f32fe2dde6b392be0c8c6e68c Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sat, 5 Nov 2016 14:17:36 +0200 Subject: [PATCH 09/36] More tests --- .../presentations/lib/libreofficeserver.py | 96 ++-- .../presentations/test_libreofficeserver.py | 471 +++++++++++++++++- 2 files changed, 527 insertions(+), 40 deletions(-) diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index 55359d8ca..e2017a23b 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -1,3 +1,24 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### """ This module runs a Pyro4 server using LibreOffice's version of Python """ @@ -54,6 +75,41 @@ class LibreOfficeServer(object): 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): """ @@ -93,40 +149,6 @@ class LibreOfficeServer(object): except Exception as e: log.warning('Failed to get UNO desktop') - 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 has_desktop(self): """ Say if we have a desktop object @@ -206,8 +228,7 @@ class LibreOfficeServer(object): def get_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 + Extract the titles and the notes from the slides. """ titles = [] notes = [] @@ -222,8 +243,7 @@ class LibreOfficeServer(object): def close_presentation(self): """ - Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being - shutdown. + Close presentation and clean up objects. """ log.debug('close Presentation LibreOffice') if self._document: diff --git a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py index b0ba05826..2f59639dc 100644 --- a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py +++ b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py @@ -22,8 +22,6 @@ """ Functional tests to test the LibreOffice Pyro server """ -from unittest import TestCase - from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType from tests.functional import MagicMock, patch, call @@ -44,6 +42,7 @@ def test_constructor(): assert server._presentation is None assert server._process is None + @patch('openlp.plugins.presentations.lib.libreofficeserver.Popen') def test_start_process(MockedPopen): """ @@ -69,6 +68,23 @@ def test_start_process(MockedPopen): ]) assert server._process is mocked_process + +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') +def test_setup_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: setup_desktop() is called + server.setup_desktop() + + # THEN: setup_desktop() exits early + assert server._manager is None + + @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') def test_setup_desktop(mocked_uno): """ @@ -104,6 +120,7 @@ def test_setup_desktop(mocked_uno): assert server._manager is MockedServiceManager assert server._desktop is mocked_desktop + @patch('openlp.plugins.presentations.lib.libreofficeserver.PropertyValue') def test_create_property(MockedPropertyValue): """ @@ -121,6 +138,7 @@ def test_create_property(MockedPropertyValue): 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 @@ -147,6 +165,7 @@ def test_get_text_from_page_slide_text(): # 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 @@ -173,6 +192,7 @@ def test_get_text_from_page_title(): # 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 @@ -201,6 +221,7 @@ def test_get_text_from_page_notes(): # THEN: The text should be correct assert text == 'Page Notes\n' + def test_has_desktop_no_desktop(): """ Test the has_desktop() method when there's no desktop @@ -214,6 +235,7 @@ def test_has_desktop_no_desktop(): # THEN: The result should be False assert result is False + def test_has_desktop(): """ Test the has_desktop() method @@ -228,6 +250,7 @@ def test_has_desktop(): # THEN: The result should be True assert result is True + def test_shutdown(): """ Test the shutdown method @@ -266,6 +289,7 @@ def test_shutdown(): 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(mocked_uno): """ @@ -300,6 +324,7 @@ def test_load_presentation(mocked_uno): assert server._presentation.Display == screen_number assert server._control is None + @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') @patch('openlp.plugins.presentations.lib.libreofficeserver.os') def test_extract_thumbnails(mocked_os, mocked_uno): @@ -340,3 +365,445 @@ def test_extract_thumbnails(mocked_os, mocked_uno): [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(): + """ + 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 + result = server.is_loaded() + + # 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 + result = 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 + result = 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 + result = 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 + result = 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' From 016c28e7e4bde7068ea2d3b12441587ce0726252 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sat, 5 Nov 2016 23:37:06 +0200 Subject: [PATCH 10/36] Finish writing tests for LibreOfficeServer --- .../presentations/lib/libreofficeserver.py | 8 +- .../presentations/test_libreofficeserver.py | 171 +++++++++++++++++- 2 files changed, 174 insertions(+), 5 deletions(-) diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index e2017a23b..d498fe985 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -159,6 +159,7 @@ class LibreOfficeServer(object): """ Shut down the server """ + can_kill = True if hasattr(self, '_docs'): while self._docs: self._docs[0].close_presentation() @@ -173,13 +174,14 @@ class LibreOfficeServer(object): 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: log.warning('Failed to terminate LibreOffice') - if getattr(self, '_process'): + if getattr(self, '_process') and can_kill: self._process.kill() @@ -209,7 +211,7 @@ class LibreOfficeServer(object): properties = (self._create_property('FilterName', 'impress_png_Export'),) pages = self._document.getDrawPages() if not pages: - return + return [] if not os.path.isdir(temp_folder): os.makedirs(temp_folder) for index in range(pages.getCount()): @@ -393,7 +395,7 @@ def main(): The main function which runs the server """ daemon = Daemon(host='localhost', port=4310) - uri = daemon.register(LibreOfficeServer, 'openlp.libreofficeserver') + daemon.register(LibreOfficeServer, 'openlp.libreofficeserver') try: daemon.requestLoop() finally: diff --git a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py index 2f59639dc..b410b4547 100644 --- a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py +++ b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py @@ -22,7 +22,7 @@ """ Functional tests to test the LibreOffice Pyro server """ -from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType +from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType, main from tests.functional import MagicMock, patch, call @@ -85,6 +85,42 @@ def test_setup_desktop_already_has_desktop(mocked_uno): assert server._manager is None +@patch('openlp.plugins.presentations.lib.libreofficeserver.uno') +def test_setup_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_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.side_effect = Exception() + + # WHEN: setup_desktop() is called + server.setup_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_pipe;urp;StarOffice.ComponentContext'), + call('uno:pipe,name=openlp_pipe;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_setup_desktop(mocked_uno): """ @@ -251,6 +287,48 @@ def test_has_desktop(): assert result is True +def test_shutdown_other_docs(): + """ + Test the shutdown method while other documents are open in LibreOffice + """ + # 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() + def close_docs(): + server._docs = [] + 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 @@ -290,6 +368,29 @@ def test_shutdown(): 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): """ @@ -325,6 +426,32 @@ def test_load_presentation(mocked_uno): 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() + mocked_pages = 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): @@ -456,6 +583,25 @@ def test_is_loaded_no_presentation(): 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 @@ -484,7 +630,9 @@ def test_is_active_not_loaded(): server = LibreOfficeServer() # WHEN: is_active() is called with is_loaded() returns False - result = server.is_loaded() + 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 @@ -807,3 +955,22 @@ def test_get_slide_notes(): # 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() From 704d9118a59b2ffe5ea551b55d2ded34c64afcab Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Mon, 7 Nov 2016 22:53:12 +0200 Subject: [PATCH 11/36] More tests --- .../presentations/lib/maclocontroller.py | 13 +- .../presentations/presentationplugin.py | 2 +- .../presentations/test_maclocontroller.py | 480 ++++++++++++++++++ 3 files changed, 482 insertions(+), 13 deletions(-) create mode 100644 tests/functional/openlp_plugins/presentations/test_maclocontroller.py diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index 85df2178f..91b30d0c8 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -20,17 +20,6 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -# OOo API documentation: -# http://api.openoffice.org/docs/common/ref/com/sun/star/presentation/XSlideShowController.html -# http://wiki.services.openoffice.org/wiki/Documentation/DevGuide/ProUNO/Basic -# /Getting_Information_about_UNO_Objects#Inspecting_interfaces_during_debugging -# http://docs.go-oo.org/sd/html/classsd_1_1SlideShow.html -# http://www.oooforum.org/forum/viewtopic.phtml?t=5252 -# http://wiki.services.openoffice.org/wiki/Documentation/DevGuide/Working_with_Presentations -# http://mail.python.org/pipermail/python-win32/2008-January/006676.html -# http://www.linuxjournal.com/content/starting-stopping-and-connecting-openoffice-python -# http://nxsy.org/comparing-documents-with-openoffice-and-python - import logging import os import time @@ -65,7 +54,7 @@ class MacLOController(PresentationController): Initialise the class """ log.debug('Initialising') - super(MacLOController, self).__init__(plugin, 'MacLO', MacLODocument) + super(MacLOController, self).__init__(plugin, 'Impress on macOS', MacLODocument) self.supports = ['odp'] self.also_supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm'] self.server_process = None diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 505f0dbb7..8189b2b66 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -40,7 +40,7 @@ __default_settings__ = { 'presentations/override app': QtCore.Qt.Unchecked, 'presentations/enable_pdf_program': QtCore.Qt.Unchecked, 'presentations/pdf_program': '', - 'presentations/MacLO': QtCore.Qt.Checked, + 'presentations/Impress on macOS': QtCore.Qt.Checked, 'presentations/Impress': QtCore.Qt.Checked, 'presentations/Powerpoint': QtCore.Qt.Checked, 'presentations/Powerpoint Viewer': QtCore.Qt.Checked, diff --git a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py new file mode 100644 index 000000000..b4948f7a7 --- /dev/null +++ b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py @@ -0,0 +1,480 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Functional tests to test the Mac LibreOffice class and related methods. +""" +from unittest import TestCase +import os +import shutil +from tempfile import mkdtemp + +from openlp.core.common import Settings +from openlp.plugins.presentations.lib.maclocontroller import \ + MacLOController, MacLODocument, TextType +from openlp.plugins.presentations.presentationplugin import __default_settings__ + +from tests.functional import MagicMock, patch, call +from tests.utils.constants import TEST_RESOURCES_PATH +from tests.helpers.testmixin import TestMixin + + +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.os') + @patch('openlp.plugins.presentations.lib.maclocontroller.Popen') + def test_constructor(self, MockedPopen, mocked_os): + """ + Test the Constructor from the MacLOController + """ + # GIVEN: No presentation controller + controller = None + mocked_process = MagicMock() + mocked_os.path.join.side_effect = lambda *x: '/'.join(x) + mocked_os.path.dirname.return_value = '' + MockedPopen.return_value = mocked_process + + # 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 == 'Impress on macOS', \ + 'The name of the presentation controller should be correct' + MockedPopen.assert_called_once_with(['/Applications/LibreOffice.app/Contents/Resources/python', + '/libreofficeserver.py']) + assert controller.server_process == mocked_process + + @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 = os.path.join(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) + + def test_load_presentation_no_desktop(self): + """ + Test the load_presentation() method when there's no desktop yet + """ + # GIVEN: A document and a mocked client + self.mocked_client.has_desktop.return_value = False + + # WHEN: load_presentation() is called + result = self.document.load_presentation() + + # THEN: Stuff should work right + self.mocked_client.setup_desktop.assert_called_once_with() + self.mocked_client.has_desktop.assert_called_once_with() + assert result is False + + @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.has_desktop.return_value = True + 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.setup_desktop.assert_called_once_with() + self.mocked_client.has_desktop.assert_called_once_with() + self.mocked_client.load_presentation.assert_called_once_with(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.has_desktop.return_value = True + 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.setup_desktop.assert_called_once_with() + self.mocked_client.has_desktop.assert_called_once_with() + self.mocked_client.load_presentation.assert_called_once_with(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('thumb1.png', 1), call('thumb2.png', 2)] + assert mocked_delete_file.call_args_list == [call('thumb1.png'), call('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_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' From 52c92e434b932064d53cdfdf0d6a403f44ddeee4 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 8 Nov 2016 13:22:36 +0200 Subject: [PATCH 12/36] Only try to start the LibreOfficeServer if LibreOffice Python exists --- openlp/plugins/presentations/lib/maclocontroller.py | 3 ++- .../openlp_plugins/presentations/test_maclocontroller.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index 91b30d0c8..d35faa567 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -67,7 +67,8 @@ class MacLOController(PresentationController): """ libreoffice_python = '/Applications/LibreOffice.app/Contents/Resources/python' libreoffice_server = os.path.join(os.path.dirname(__file__), 'libreofficeserver.py') - self.server_process = Popen([libreoffice_python, libreoffice_server]) + if os.path.exists(libreoffice_python): + self.server_process = Popen([libreoffice_python, libreoffice_server]) @property def client(self): diff --git a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py index b4948f7a7..ae24658df 100644 --- a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py @@ -70,6 +70,7 @@ class TestMacLOController(TestCase, TestMixin): mocked_process = MagicMock() mocked_os.path.join.side_effect = lambda *x: '/'.join(x) mocked_os.path.dirname.return_value = '' + mocked_os.path.exists.return_value = True MockedPopen.return_value = mocked_process # WHEN: The presentation controller object is created From f76c9adc39634bb5596a5ebaca46b124436e2914 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 8 Nov 2016 13:52:08 +0200 Subject: [PATCH 13/36] Fix up some pep8 issues --- .../presentations/lib/libreofficeserver.py | 2 -- .../presentations/lib/maclocontroller.py | 5 +---- .../presentations/test_libreofficeserver.py | 18 ++++++++++-------- .../presentations/test_maclocontroller.py | 1 - 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index d498fe985..4241d9bb4 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -184,7 +184,6 @@ class LibreOfficeServer(object): if getattr(self, '_process') and can_kill: self._process.kill() - def load_presentation(self, file_path, screen_number): """ Load a presentation @@ -404,4 +403,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index d35faa567..487825677 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -120,9 +120,7 @@ class MacLODocument(PresentationDocument): def load_presentation(self): """ - Called when a presentation is added to the SlideController. It builds the environment, starts communcations with - the background LibreOffice task started earlier. If LibreOffice is not present is is started. Once the environment - is available the presentation is loaded and started. + Tell the LibreOfficeServer to start the presentation. """ log.debug('Load Presentation LibreOffice') self.client.setup_desktop() @@ -262,4 +260,3 @@ class MacLODocument(PresentationDocument): :param slide_no: The slide the notes are required for, starting at 1 """ return self.client.get_slide_notes(slide_no) - diff --git a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py index b410b4547..d350ee9e1 100644 --- a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py +++ b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py @@ -109,14 +109,14 @@ def test_setup_desktop_exception(mocked_uno): # 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) + 'com.sun.star.bridge.UnoUrlResolver', mocked_context) expected_calls = [ call('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext'), call('uno:pipe,name=openlp_pipe;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) + 'com.sun.star.frame.Desktop', mocked_uno_instance) assert server._manager is MockedServiceManager assert server._desktop is None @@ -145,14 +145,14 @@ def test_setup_desktop(mocked_uno): # 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) + 'com.sun.star.bridge.UnoUrlResolver', mocked_context) expected_calls = [ call('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext'), call('uno:pipe,name=openlp_pipe;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) + 'com.sun.star.frame.Desktop', mocked_uno_instance) assert server._manager is MockedServiceManager assert server._desktop is mocked_desktop @@ -291,6 +291,9 @@ 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() @@ -301,8 +304,6 @@ def test_shutdown_other_docs(): server._docs = [mocked_doc] server._desktop = mocked_desktop server._process = MagicMock() - def close_docs(): - server._docs = [] mocked_doc.close_presentation.side_effect = close_docs mocked_desktop.getComponents.return_value = mocked_docs mocked_docs.hasElements.return_value = True @@ -333,6 +334,9 @@ def test_shutdown(): """ Test the shutdown method """ + def close_docs(): + server._docs = [] + # GIVEN: An up an running LibreOfficeServer server = LibreOfficeServer() mocked_doc = MagicMock() @@ -343,8 +347,6 @@ def test_shutdown(): server._docs = [mocked_doc] server._desktop = mocked_desktop server._process = MagicMock() - def close_docs(): - server._docs = [] mocked_doc.close_presentation.side_effect = close_docs mocked_desktop.getComponents.return_value = mocked_docs mocked_docs.hasElements.return_value = True diff --git a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py index ae24658df..ac55d09b3 100644 --- a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py @@ -134,7 +134,6 @@ class TestMacLOController(TestCase, TestMixin): # 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): """ From 487ff4e8d34354c20f0886a31e91c8321d96c49f Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 8 Nov 2016 21:59:33 +0200 Subject: [PATCH 14/36] Separate the visible name of the controller from the internal name --- openlp/plugins/presentations/lib/maclocontroller.py | 2 +- openlp/plugins/presentations/lib/presentationcontroller.py | 4 +++- openlp/plugins/presentations/lib/presentationtab.py | 4 ++-- openlp/plugins/presentations/presentationplugin.py | 2 +- .../openlp_plugins/presentations/test_maclocontroller.py | 4 +++- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index 487825677..c60d642cd 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -54,7 +54,7 @@ class MacLOController(PresentationController): Initialise the class """ log.debug('Initialising') - super(MacLOController, self).__init__(plugin, 'Impress on macOS', MacLODocument) + 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 diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 7c26462fd..f389db4c9 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -392,7 +392,8 @@ class PresentationController(object): """ 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 @@ -412,6 +413,7 @@ class PresentationController(object): self.docs = [] self.plugin = plugin self.name = name + self.display_name = display_name if display_name is not None else name self.document_class = document_class self.settings_section = self.plugin.settings_section self.available = None diff --git a/openlp/plugins/presentations/lib/presentationtab.py b/openlp/plugins/presentations/lib/presentationtab.py index 6a0aab211..7e04ad51d 100644 --- a/openlp/plugins/presentations/lib/presentationtab.py +++ b/openlp/plugins/presentations/lib/presentationtab.py @@ -135,10 +135,10 @@ class PresentationTab(SettingsTab): def set_controller_text(self, checkbox, controller): if checkbox.isEnabled(): - checkbox.setText(controller.name) + checkbox.setText(controller.display_name) else: checkbox.setText(translate('PresentationPlugin.PresentationTab', - '{name} (unavailable)').format(name=controller.name)) + '{name} (unavailable)').format(name=controller.display_name)) def load(self): """ diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 8189b2b66..d985df36b 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -40,7 +40,7 @@ __default_settings__ = { 'presentations/override app': QtCore.Qt.Unchecked, 'presentations/enable_pdf_program': QtCore.Qt.Unchecked, 'presentations/pdf_program': '', - 'presentations/Impress on macOS': QtCore.Qt.Checked, + 'presentations/maclo': QtCore.Qt.Checked, 'presentations/Impress': QtCore.Qt.Checked, 'presentations/Powerpoint': QtCore.Qt.Checked, 'presentations/Powerpoint Viewer': QtCore.Qt.Checked, diff --git a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py index ac55d09b3..bece4dfe6 100644 --- a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py @@ -77,8 +77,10 @@ class TestMacLOController(TestCase, TestMixin): controller = MacLOController(plugin=self.mock_plugin) # THEN: The name of the presentation controller should be correct - assert controller.name == 'Impress on macOS', \ + 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' MockedPopen.assert_called_once_with(['/Applications/LibreOffice.app/Contents/Resources/python', '/libreofficeserver.py']) assert controller.server_process == mocked_process From 4e4ed4305bd848049fa8de9f63f6e1e061aef4ba Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Wed, 9 Nov 2016 22:36:48 +0200 Subject: [PATCH 15/36] Fix a PEP8 indentation issue --- openlp/plugins/presentations/lib/presentationcontroller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index f389db4c9..cad0403d8 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -393,7 +393,7 @@ class PresentationController(object): log.info('PresentationController loaded') def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument, - display_name=None): + display_name=None): """ This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins From ad06ef528500c44ad4bcc037b6cb7901f6ca7b8d Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Wed, 9 Nov 2016 22:58:07 +0200 Subject: [PATCH 16/36] Fix a PEP8 indentation issue --- openlp/plugins/presentations/lib/presentationcontroller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index cad0403d8..2bc4f68a8 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -393,7 +393,7 @@ class PresentationController(object): log.info('PresentationController loaded') def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument, - display_name=None): + display_name=None): """ This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins From 34520d6dbe18bd028f8938c994f01580f25a9609 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sat, 26 Nov 2016 00:01:15 +0200 Subject: [PATCH 17/36] Use AppLocation to figure out what directory the server is in --- openlp/plugins/presentations/lib/maclocontroller.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index c60d642cd..6403581ad 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -25,7 +25,7 @@ import os import time from subprocess import Popen -from openlp.core.common import is_macosx, Registry, delete_file +from openlp.core.common import AppLocation, Registry, delete_file, is_macosx if is_macosx() and os.path.exists('/Applications/LibreOffice.app'): macuno_available = True @@ -66,7 +66,8 @@ class MacLOController(PresentationController): Start a LibreOfficeServer """ libreoffice_python = '/Applications/LibreOffice.app/Contents/Resources/python' - libreoffice_server = os.path.join(os.path.dirname(__file__), 'libreofficeserver.py') + libreoffice_server = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), + 'lib', 'libreofficeserver.py') if os.path.exists(libreoffice_python): self.server_process = Popen([libreoffice_python, libreoffice_server]) From e6496dc91e0927808352b931cb8b5db70ffd3fc0 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sat, 26 Nov 2016 00:21:24 +0200 Subject: [PATCH 18/36] Use the right path --- openlp/plugins/presentations/lib/maclocontroller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index 6403581ad..6945db1db 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -67,7 +67,7 @@ class MacLOController(PresentationController): """ libreoffice_python = '/Applications/LibreOffice.app/Contents/Resources/python' libreoffice_server = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), - 'lib', 'libreofficeserver.py') + 'presentations', 'lib', 'libreofficeserver.py') if os.path.exists(libreoffice_python): self.server_process = Popen([libreoffice_python, libreoffice_server]) From 597ca4b0ecc1d1fe6b730fe902b5fe3f148254e6 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sat, 26 Nov 2016 01:44:09 +0200 Subject: [PATCH 19/36] Fix the test --- .../openlp_plugins/presentations/test_maclocontroller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py index bece4dfe6..11a74ce0f 100644 --- a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py @@ -59,15 +59,17 @@ class TestMacLOController(TestCase, TestMixin): self.destroy_settings() shutil.rmtree(self.temp_folder) + @patch('openlp.plugins.presentations.lib.maclocontroller.AppLocation.get_directory') @patch('openlp.plugins.presentations.lib.maclocontroller.os') @patch('openlp.plugins.presentations.lib.maclocontroller.Popen') - def test_constructor(self, MockedPopen, mocked_os): + def test_constructor(self, MockedPopen, mocked_os, mocked_get_directory): """ Test the Constructor from the MacLOController """ # GIVEN: No presentation controller controller = None mocked_process = MagicMock() + mocked_get_directory.return_value = 'plugins' mocked_os.path.join.side_effect = lambda *x: '/'.join(x) mocked_os.path.dirname.return_value = '' mocked_os.path.exists.return_value = True @@ -82,7 +84,7 @@ class TestMacLOController(TestCase, TestMixin): assert controller.display_name == 'Impress on macOS', \ 'The display name of the presentation controller should be correct' MockedPopen.assert_called_once_with(['/Applications/LibreOffice.app/Contents/Resources/python', - '/libreofficeserver.py']) + 'plugins/presentations/lib/libreofficeserver.py']) assert controller.server_process == mocked_process @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') From 5eb0f6233224718271c5a4e7beb10ccd5ee6d25b Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 8 Sep 2017 17:18:59 -0700 Subject: [PATCH 20/36] Try to make Pyro4 and LibreOffice talk to each other. Failing miserably --- openlp.py | 6 +- openlp/core/common/path.py | 2 + .../presentations/lib/libreofficeserver.py | 69 ++++++++++--------- .../presentations/lib/maclocontroller.py | 14 ++-- 4 files changed, 48 insertions(+), 43 deletions(-) diff --git a/openlp.py b/openlp.py index 7ede25519..2c3160c3e 100755 --- a/openlp.py +++ b/openlp.py @@ -20,13 +20,15 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - -import sys +import faulthandler import multiprocessing +import sys from openlp.core.common import is_win, is_macosx from openlp.core import main +faulthandler.enable() + if __name__ == '__main__': """ diff --git a/openlp/core/common/path.py b/openlp/core/common/path.py index 3c4dd93c9..240ecec6b 100644 --- a/openlp/core/common/path.py +++ b/openlp/core/common/path.py @@ -37,6 +37,8 @@ def path_to_str(path=None): :return: An empty string if :param:`path` is None, else a string representation of the :param:`path` :rtype: str """ + if isinstance(path, str): + return path if not isinstance(path, Path) and path is not None: raise TypeError('parameter \'path\' must be of type Path or NoneType') if path is None: diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index 4241d9bb4..6bfa8435d 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -61,6 +61,13 @@ class TextType(object): Notes = 2 +class LibreOfficeException(Exception): + """ + A specific exception for LO + """ + pass + + @expose class LibreOfficeServer(object): """ @@ -71,7 +78,6 @@ class LibreOfficeServer(object): Set up the server """ self._control = None - self._desktop = None self._document = None self._presentation = None self._process = None @@ -122,39 +128,35 @@ class LibreOfficeServer(object): '--minimized', '--nodefault', '--nofirststartwizard', - '--accept=pipe,name=openlp_pipe;urp;' + '--accept=socket,host=localhost,port=2002;urp;StarOffice.ServiceManager' ] self._process = Popen(uno_command) - def setup_desktop(self): + @property + def desktop(self): """ Set up an UNO desktop instance """ - if self.has_desktop(): - return 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_pipe;urp;StarOffice.ComponentContext') + uno_instance = resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') except Exception as e: log.warning('Unable to find running instance ') loop += 1 try: - self._manager = uno_instance.ServiceManager + manager = uno_instance.ServiceManager log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop') - self._desktop = self._manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance) + desktop = manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance) + if not desktop: + raise Exception('Failed to get UNO desktop') + return desktop except Exception as e: log.warning('Failed to get UNO desktop') - def has_desktop(self): - """ - Say if we have a desktop object - """ - return hasattr(self, '_desktop') and self._desktop is not None - def shutdown(self): """ Shut down the server @@ -163,24 +165,23 @@ class LibreOfficeServer(object): if hasattr(self, '_docs'): while self._docs: self._docs[0].close_presentation() - if self.has_desktop(): - 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: - log.warning('Failed to terminate LibreOffice') + 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: + log.warning('Failed to terminate LibreOffice') if getattr(self, '_process') and can_kill: self._process.kill() @@ -191,8 +192,10 @@ class LibreOfficeServer(object): self._file_path = file_path url = uno.systemPathToFileUrl(file_path) properties = (self._create_property('Hidden', True),) + retries = 0 + self._document = None try: - self._document = self._desktop.loadComponentFromURL(url, '_blank', 0, properties) + self._document = self.desktop.loadComponentFromURL(url, '_blank', 0, properties) except: log.warning('Failed to load presentation {url}'.format(url=url)) return False diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index 6945db1db..b5d936efc 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -23,6 +23,7 @@ import logging import os import time +from pathlib import Path from subprocess import Popen from openlp.core.common import AppLocation, Registry, delete_file, is_macosx @@ -65,11 +66,11 @@ class MacLOController(PresentationController): """ Start a LibreOfficeServer """ - libreoffice_python = '/Applications/LibreOffice.app/Contents/Resources/python' - libreoffice_server = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), - 'presentations', 'lib', 'libreofficeserver.py') - if os.path.exists(libreoffice_python): - self.server_process = Popen([libreoffice_python, libreoffice_server]) + 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): @@ -124,9 +125,6 @@ class MacLODocument(PresentationDocument): Tell the LibreOfficeServer to start the presentation. """ log.debug('Load Presentation LibreOffice') - self.client.setup_desktop() - if not self.client.has_desktop(): - return False if not self.client.load_presentation(self.file_path, ScreenList().current['number'] + 1): return False self.create_thumbnails() From 02e1263d2096b38f87e132061795b2724ceb045e Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 25 Oct 2018 19:17:28 -0700 Subject: [PATCH 21/36] Fix a merge issue --- openlp/plugins/presentations/presentationplugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index bdc6a85cd..5e44dae7c 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -52,7 +52,7 @@ __default_settings__ = { 'presentations/presentations files': [], 'presentations/thumbnail_scheme': '', '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 } From df3b3520ff6f9ddcae50c84b87b1b9f75d79cd85 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 26 Oct 2018 23:01:13 -0700 Subject: [PATCH 22/36] Fix up some tests and things --- .../presentations/lib/libreofficeserver.py | 3 +- .../presentations/lib/maclocontroller.py | 21 +++++++------ .../openlp_core/common/test_path.py | 15 +++++++-- .../presentations/test_libreofficeserver.py | 16 +++++----- .../presentations/test_maclocontroller.py | 31 ++++++------------- 5 files changed, 44 insertions(+), 42 deletions(-) diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index 6bfa8435d..245a04f8e 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -128,7 +128,8 @@ class LibreOfficeServer(object): '--minimized', '--nodefault', '--nofirststartwizard', - '--accept=socket,host=localhost,port=2002;urp;StarOffice.ServiceManager' + '--accept=pipe,name=openlp_pipe;urp;' + # '--accept=socket,host=localhost,port=2002;urp;StarOffice.ServiceManager' ] self._process = Popen(uno_command) diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index b5d936efc..3adc1ead1 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -21,23 +21,26 @@ ############################################################################### import logging -import os -import time -from pathlib import Path from subprocess import Popen -from openlp.core.common import AppLocation, Registry, delete_file, is_macosx +from Pyro4 import Proxy -if is_macosx() and os.path.exists('/Applications/LibreOffice.app'): +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 + +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 -from PyQt5 import QtCore -from Pyro4 import Proxy -from openlp.core.lib import ScreenList -from .presentationcontroller import PresentationController, PresentationDocument, TextType +from openlp.core.display.screens import ScreenList +from .presentationcontroller import PresentationController, PresentationDocument log = logging.getLogger(__name__) diff --git a/tests/functional/openlp_core/common/test_path.py b/tests/functional/openlp_core/common/test_path.py index 8581d9b49..7341ede38 100644 --- a/tests/functional/openlp_core/common/test_path.py +++ b/tests/functional/openlp_core/common/test_path.py @@ -245,7 +245,18 @@ class TestPath(TestCase): # WHEN: Calling `path_to_str` with an invalid Type # THEN: A TypeError should have been raised 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): """ @@ -397,7 +408,7 @@ class TestPath(TestCase): try: create_paths(mocked_path) assert False, 'create_paths should have thrown an exception' - except: + except Exception: # THEN: `create_paths` raises an exception pass diff --git a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py index d350ee9e1..cb98705f4 100644 --- a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py +++ b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py @@ -22,9 +22,9 @@ """ Functional tests to test the LibreOffice Pyro server """ -from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType, main +from unittest.mock import MagicMock, patch, call -from tests.functional import MagicMock, patch, call +from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType, main def test_constructor(): @@ -37,7 +37,7 @@ def test_constructor(): # THEN: The server should have been set up correctly assert server._control is None - assert server._desktop is None + # assert server._desktop is None assert server._document is None assert server._presentation is None assert server._process is None @@ -96,7 +96,6 @@ def test_setup_desktop_exception(mocked_uno): 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] @@ -437,7 +436,6 @@ def test_extract_thumbnails_no_pages(mocked_uno): temp_folder = '/tmp' server = LibreOfficeServer() mocked_document = MagicMock() - mocked_pages = MagicMock() server._document = mocked_document mocked_uno.systemPathToFileUrl.side_effect = lambda x: x mocked_document.getDrawPages.return_value = None @@ -861,7 +859,7 @@ def test_goto_slide(): server._control = mocked_control # WHEN: goto_slide() is called - result = server.goto_slide(1) + server.goto_slide(1) # THEN: The slide number should be correct mocked_control.gotoSlideIndex.assert_called_once_with(0) @@ -879,7 +877,7 @@ def test_next_step_when_paused(mocked_sleep): mocked_control.isPaused.side_effect = [False, True] # WHEN: next_step() is called - result = server.next_step() + server.next_step() # THEN: The correct call should be made mocked_control.gotoNextEffect.assert_called_once_with() @@ -900,7 +898,7 @@ def test_next_step(mocked_sleep): mocked_control.isPaused.side_effect = [True, True] # WHEN: next_step() is called - result = server.next_step() + server.next_step() # THEN: The correct call should be made mocked_control.gotoNextEffect.assert_called_once_with() @@ -919,7 +917,7 @@ def test_previous_step(): server._control = mocked_control # WHEN: previous_step() is called - result = server.previous_step() + server.previous_step() # THEN: The correct call should be made mocked_control.gotoPreviousEffect.assert_called_once_with() diff --git a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py index 11a74ce0f..e295dfd86 100644 --- a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py @@ -22,19 +22,18 @@ """ Functional tests to test the Mac LibreOffice class and related methods. """ -from unittest import TestCase -import os import shutil from tempfile import mkdtemp +from unittest import TestCase +from unittest.mock import MagicMock, patch, call -from openlp.core.common import Settings -from openlp.plugins.presentations.lib.maclocontroller import \ - MacLOController, MacLODocument, TextType +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.functional import MagicMock, patch, call -from tests.utils.constants import TEST_RESOURCES_PATH from tests.helpers.testmixin import TestMixin +from tests.utils.constants import TEST_RESOURCES_PATH class TestMacLOController(TestCase, TestMixin): @@ -59,21 +58,13 @@ class TestMacLOController(TestCase, TestMixin): self.destroy_settings() shutil.rmtree(self.temp_folder) - @patch('openlp.plugins.presentations.lib.maclocontroller.AppLocation.get_directory') - @patch('openlp.plugins.presentations.lib.maclocontroller.os') - @patch('openlp.plugins.presentations.lib.maclocontroller.Popen') - def test_constructor(self, MockedPopen, mocked_os, mocked_get_directory): + @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 - mocked_process = MagicMock() - mocked_get_directory.return_value = 'plugins' - mocked_os.path.join.side_effect = lambda *x: '/'.join(x) - mocked_os.path.dirname.return_value = '' - mocked_os.path.exists.return_value = True - MockedPopen.return_value = mocked_process # WHEN: The presentation controller object is created controller = MacLOController(plugin=self.mock_plugin) @@ -83,9 +74,7 @@ class TestMacLOController(TestCase, TestMixin): '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' - MockedPopen.assert_called_once_with(['/Applications/LibreOffice.app/Contents/Resources/python', - 'plugins/presentations/lib/libreofficeserver.py']) - assert controller.server_process == mocked_process + mocked_start_server.assert_called_once_with() @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') @patch('openlp.plugins.presentations.lib.maclocontroller.Proxy') @@ -164,7 +153,7 @@ class TestMacLODocument(TestCase): mocked_plugin = MagicMock() mocked_plugin.settings_section = 'presentations' Settings().extend_default_settings(__default_settings__) - self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.odp') + 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) From 62b0eedfc61bed5c2bf48c262ca124f213241729 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Wed, 30 Jan 2019 00:18:03 -0700 Subject: [PATCH 23/36] Get Pyro working again --- openlp/core/common/__init__.py | 4 +- openlp/core/ui/media/mediacontroller.py | 2 + openlp/core/ui/media/vlcplayer.py | 2 + .../presentations/lib/libreofficeserver.py | 70 ++++++++++++------- .../presentations/lib/maclocontroller.py | 24 ++++--- .../plugins/presentations/lib/serializers.py | 28 ++++++++ 6 files changed, 91 insertions(+), 39 deletions(-) create mode 100644 openlp/plugins/presentations/lib/serializers.py diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index a7215e394..5fbe56996 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -134,8 +134,8 @@ def extension_loader(glob_pattern, excluded_files=[]): importlib.import_module(module_name) except (ImportError, OSError): # On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X) - log.warning('Failed to import {module_name} on path {extension_path}' - .format(module_name=module_name, extension_path=extension_path)) + log.exception('Failed to import {module_name} on path {extension_path}' + .format(module_name=module_name, extension_path=extension_path)) def path_to_module(path): diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index a2c8bad0d..08e2aba58 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -170,6 +170,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): if get_vlc() and pymediainfo_available: State().update_pre_conditions("mediacontroller", True) else: + if hasattr(self.main_window, 'splash') and self.main_window.splash.isVisible(): + self.main_window.splash.hide() State().missing_text("mediacontroller", translate('OpenLP.SlideController', "VLC or pymediainfo are missing, so you are unable to play any media")) self._generate_extensions_lists() diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 417936d24..ffdeee9cc 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -159,6 +159,8 @@ class VlcPlayer(MediaPlayer): :return: """ vlc = get_vlc() + if not vlc: + return display.vlc_widget = QtWidgets.QFrame(display) display.vlc_widget.setFrameStyle(QtWidgets.QFrame.NoFrame) # creating a basic vlc instance diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index 6bfa8435d..30bba25c2 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -28,9 +28,19 @@ import os import logging import time + +if sys.platform.startswith('darwin'): + print('Setting up log file') + # 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') + logging.basicConfig(filename=logfile, level=logging.INFO) + + # Add the vendor directory to sys.path so that we can load Pyro4 +sys.path.append(os.path.join(os.path.dirname(__file__))) sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor')) +from serializers import register_classes from Pyro4 import Daemon, expose try: @@ -38,18 +48,15 @@ try: import uno from com.sun.star.beans import PropertyValue from com.sun.star.task import ErrorCodeIOException -except: +except ImportError as e: # But they need to be defined for mocking + print(e) uno = None PropertyValue = None ErrorCodeIOException = Exception -if sys.platform.startswith('darwin') and uno is not None: - # 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') - logging.basicConfig(filename=logfile, level=logging.INFO) - log = logging.getLogger(__name__) +register_classes() class TextType(object): @@ -77,6 +84,7 @@ class LibreOfficeServer(object): """ Set up the server """ + self._desktop = None self._control = None self._document = None self._presentation = None @@ -128,7 +136,7 @@ class LibreOfficeServer(object): '--minimized', '--nodefault', '--nofirststartwizard', - '--accept=socket,host=localhost,port=2002;urp;StarOffice.ServiceManager' + '--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager' ] self._process = Popen(uno_command) @@ -137,15 +145,17 @@ class LibreOfficeServer(object): """ 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:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') - except Exception as e: - log.warning('Unable to find running instance ') + 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: manager = uno_instance.ServiceManager @@ -153,9 +163,11 @@ class LibreOfficeServer(object): desktop = 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 as e: - log.warning('Failed to get UNO desktop') + except Exception: + log.exception('Failed to get UNO desktop') + return None def shutdown(self): """ @@ -180,8 +192,8 @@ class LibreOfficeServer(object): try: self.desktop.terminate() log.debug('LibreOffice killed') - except: - log.warning('Failed to terminate LibreOffice') + except Exception: + log.exception('Failed to terminate LibreOffice') if getattr(self, '_process') and can_kill: self._process.kill() @@ -189,15 +201,21 @@ class LibreOfficeServer(object): """ Load a presentation """ - self._file_path = file_path url = uno.systemPathToFileUrl(file_path) - properties = (self._create_property('Hidden', True),) - retries = 0 + properties = [self._create_property('Hidden', True)] self._document = None - try: - self._document = self.desktop.loadComponentFromURL(url, '_blank', 0, properties) - except: - log.warning('Failed to load presentation {url}'.format(url=url)) + 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 @@ -226,7 +244,7 @@ class LibreOfficeServer(object): thumbnails.append(path) except ErrorCodeIOException as exception: log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) - except: + except Exception: log.exception('{path} - Unable to store openoffice preview'.format(path=path)) return thumbnails @@ -256,8 +274,8 @@ class LibreOfficeServer(object): self._presentation.end() self._presentation = None self._document.dispose() - except: - log.warning("Closing presentation failed") + except Exception: + log.exception("Closing presentation failed") self._document = None def is_loaded(self): @@ -272,8 +290,8 @@ class LibreOfficeServer(object): if self._document.getPresentation() is None: log.debug("getPresentation failed to find a presentation") return False - except: - log.warning("getPresentation failed to find a presentation") + except Exception: + log.exception("getPresentation failed to find a presentation") return False return True diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index b5d936efc..233fe20dc 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -22,25 +22,26 @@ import logging import os -import time -from pathlib import Path from subprocess import Popen -from openlp.core.common import AppLocation, Registry, delete_file, is_macosx +from Pyro4 import Proxy + +from openlp.core.common import is_macosx, delete_file +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 if is_macosx() and os.path.exists('/Applications/LibreOffice.app'): macuno_available = True else: macuno_available = False -from PyQt5 import QtCore -from Pyro4 import Proxy - -from openlp.core.lib import ScreenList -from .presentationcontroller import PresentationController, PresentationDocument, TextType - log = logging.getLogger(__name__) +register_classes() class MacLOController(PresentationController): @@ -125,7 +126,7 @@ class MacLODocument(PresentationDocument): Tell the LibreOfficeServer to start the presentation. """ log.debug('Load Presentation LibreOffice') - if not self.client.load_presentation(self.file_path, ScreenList().current['number'] + 1): + if not self.client.load_presentation(str(self.file_path), ScreenList().current['number'] + 1): return False self.create_thumbnails() self.create_titles_and_notes() @@ -138,8 +139,9 @@ class MacLODocument(PresentationDocument): log.debug('create thumbnails LibreOffice') if self.check_thumbnails(): return - temp_thumbnails = self.client.extract_thumbnails(self.get_temp_folder()) + 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) diff --git a/openlp/plugins/presentations/lib/serializers.py b/openlp/plugins/presentations/lib/serializers.py new file mode 100644 index 000000000..4c57c7f93 --- /dev/null +++ b/openlp/plugins/presentations/lib/serializers.py @@ -0,0 +1,28 @@ +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) From 482b80aa8229de892ac80c4c4ab5b233112efc7b Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 17 May 2019 22:10:51 -0700 Subject: [PATCH 24/36] Print out the location of the log file --- openlp/plugins/presentations/lib/libreofficeserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index c54500e01..04902a588 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -30,9 +30,9 @@ import time if sys.platform.startswith('darwin'): - print('Setting up log file') # 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) From dad969bc0eaf7b172a0d68083cd777671db2d10d Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 17 May 2019 22:21:38 -0700 Subject: [PATCH 25/36] Fix some small issues from the move to a new ScreenList object --- openlp/plugins/presentations/lib/maclocontroller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index 0a0f90bb5..6ac009cd4 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -129,7 +129,7 @@ class MacLODocument(PresentationDocument): 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): + if not self.client.load_presentation(str(self.file_path), ScreenList().current.number + 1): return False self.create_thumbnails() self.create_titles_and_notes() @@ -214,7 +214,7 @@ class MacLODocument(PresentationDocument): 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().screen_list) > 1: + if len(ScreenList()) > 1: Registry().get('main_window').activateWindow() def get_slide_number(self): From f74e0c7dc815c6eaca8d0b887f35fcfb3525ca49 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 17 May 2019 23:04:20 -0700 Subject: [PATCH 26/36] Revert some changes from the HEAD merge; Add the lib directory to sys.path for running tests --- openlp/core/common/path.py | 2 +- openlp/core/ui/media/vlcplayer.py | 50 ++----------------- .../presentations/lib/libreofficeserver.py | 5 ++ run_openlp.py | 8 ++- scripts/check_dependencies.py | 5 +- 5 files changed, 21 insertions(+), 49 deletions(-) diff --git a/openlp/core/common/path.py b/openlp/core/common/path.py index eda6b9817..8305e0bdb 100644 --- a/openlp/core/common/path.py +++ b/openlp/core/common/path.py @@ -78,7 +78,7 @@ class Path(PathVariant): :param onerror: Handler function to handle any errors :rtype: None """ - shutil.rmtree(str(self), ignore_errors, onerror) + shutil.rmtree(self, ignore_errors, onerror) def replace_params(args, kwargs, params): diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index bd82ef14f..6ba27998b 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -28,7 +28,7 @@ import os import sys import threading from datetime import datetime -from distutils.version import LooseVersion +import vlc from PyQt5 import QtWidgets @@ -65,59 +65,19 @@ def get_vlc(): :return: The "vlc" module, or None """ - if 'openlp.core.ui.media.vendor.vlc' in sys.modules: + if 'vlc' in sys.modules: # If VLC has already been imported, no need to do all the stuff below again is_vlc_available = False try: - is_vlc_available = bool(sys.modules['openlp.core.ui.media.vendor.vlc'].get_default_instance()) + is_vlc_available = bool(sys.modules['vlc'].get_default_instance()) except Exception: pass if is_vlc_available: - return sys.modules['openlp.core.ui.media.vendor.vlc'] + return sys.modules['vlc'] else: return None - is_vlc_available = False - try: - if is_macosx(): - # Newer versions of VLC on OS X need this. See https://forum.videolan.org/viewtopic.php?t=124521 - os.environ['VLC_PLUGIN_PATH'] = '/Applications/VLC.app/Contents/MacOS/plugins' - # On Windows when frozen in PyInstaller, we need to blank SetDllDirectoryW to allow loading of the VLC dll. - # This is due to limitations (by design) in PyInstaller. SetDllDirectoryW original value is restored once - # VLC has been imported. - if is_win(): - buffer_size = 1024 - dll_directory = ctypes.create_unicode_buffer(buffer_size) - new_buffer_size = ctypes.windll.kernel32.GetDllDirectoryW(buffer_size, dll_directory) - dll_directory = ''.join(dll_directory[:new_buffer_size]).replace('\0', '') - log.debug('Original DllDirectory: %s' % dll_directory) - ctypes.windll.kernel32.SetDllDirectoryW(None) - from openlp.core.ui.media.vendor import vlc - if is_win(): - ctypes.windll.kernel32.SetDllDirectoryW(dll_directory) - is_vlc_available = bool(vlc.get_default_instance()) - except (ImportError, NameError, NotImplementedError): - pass - except OSError as e: - # this will get raised the first time - if is_win(): - if not isinstance(e, WindowsError) and e.winerror != 126: - raise - else: - pass - if is_vlc_available: - try: - VERSION = vlc.libvlc_get_version().decode('UTF-8') - except Exception: - VERSION = '0.0.0' - # LooseVersion does not work when a string contains letter and digits (e. g. 2.0.5 Twoflower). - # http://bugs.python.org/issue14894 - if LooseVersion(VERSION.split()[0]) < LooseVersion('1.1.0'): - is_vlc_available = False - log.debug('VLC could not be loaded, because the vlc version is too old: %s' % VERSION) - if is_vlc_available: - return vlc else: - return None + return vlc # On linux we need to initialise X threads, but not when running tests. diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index 04902a588..38981789f 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -21,6 +21,9 @@ ############################################################################### """ 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 @@ -36,6 +39,8 @@ if sys.platform.startswith('darwin'): 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')) diff --git a/run_openlp.py b/run_openlp.py index c5fff52de..880cabbe9 100755 --- a/run_openlp.py +++ b/run_openlp.py @@ -42,8 +42,12 @@ def set_up_fault_handling(): """ Set up the Python fault handler """ - create_paths(AppLocation.get_directory(AppLocation.CacheDir)) - faulthandler.enable((AppLocation.get_directory(AppLocation.CacheDir) / 'error.log').open('wb')) + # Create the cache directory if it doesn't exist, and enable the fault handler to log to an error log file + try: + create_paths(AppLocation.get_directory(AppLocation.CacheDir)) + faulthandler.enable((AppLocation.get_directory(AppLocation.CacheDir) / 'error.log').open('wb')) + except OSError: + log.exception('An exception occurred when enabling the fault handler') def start(): diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index ab176022f..4914f0931 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -89,7 +89,8 @@ MODULES = [ 'webob', 'requests', 'qtawesome', - 'pymediainfo' + 'pymediainfo', + 'vlc' ] @@ -158,6 +159,8 @@ def check_module(mod, text='', indent=' '): w('OK') except ImportError: w('FAIL') + except Exception: + w('ERROR') w(os.linesep) From 3d18478ac793914082a71476ed12e37d0810d66c Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sat, 18 May 2019 08:56:22 -0700 Subject: [PATCH 27/36] Fix up the tests --- .../presentations/lib/libreofficeserver.py | 4 +- .../openlp_core/ui/media/test_vlcplayer.py | 15 ----- .../presentations/test_libreofficeserver.py | 58 +++++-------------- .../presentations/test_maclocontroller.py | 34 +++-------- 4 files changed, 24 insertions(+), 87 deletions(-) diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index 38981789f..99a04f654 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -162,9 +162,9 @@ class LibreOfficeServer(object): log.exception('Unable to find running instance, retrying...') loop += 1 try: - manager = uno_instance.ServiceManager + self._manager = uno_instance.ServiceManager log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop') - desktop = manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance) + desktop = self._manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance) if not desktop: raise Exception('Failed to get UNO desktop') self._desktop = desktop diff --git a/tests/functional/openlp_core/ui/media/test_vlcplayer.py b/tests/functional/openlp_core/ui/media/test_vlcplayer.py index 08e17377b..d3de03758 100644 --- a/tests/functional/openlp_core/ui/media/test_vlcplayer.py +++ b/tests/functional/openlp_core/ui/media/test_vlcplayer.py @@ -65,21 +65,6 @@ class TestVLCPlayer(TestCase, TestMixin): # THEN: The extra environment variable should be there assert 'openlp.core.ui.media.vendor.vlc' not in sys.modules - @patch('openlp.core.ui.media.vlcplayer.is_macosx') - def test_fix_vlc_22_plugin_path(self, mocked_is_macosx): - """ - Test that on OS X we set the VLC plugin path to fix a bug in the VLC module - """ - # GIVEN: We're on OS X and we don't have the VLC plugin path set - mocked_is_macosx.return_value = True - - # WHEN: An checking if the player is available - get_vlc() - - # THEN: The extra environment variable should be there - assert 'VLC_PLUGIN_PATH' in os.environ, 'The plugin path should be in the environment variables' - assert '/Applications/VLC.app/Contents/MacOS/plugins' == os.environ['VLC_PLUGIN_PATH'] - @patch.dict(os.environ) @patch('openlp.core.ui.media.vlcplayer.is_macosx') def test_not_osx_fix_vlc_22_plugin_path(self, mocked_is_macosx): diff --git a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py index cb98705f4..af8548a52 100644 --- a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py +++ b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py @@ -64,13 +64,13 @@ def test_start_process(MockedPopen): '--minimized', '--nodefault', '--nofirststartwizard', - '--accept=pipe,name=openlp_pipe;urp;' + '--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager' ]) assert server._process is mocked_process @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') -def test_setup_desktop_already_has_desktop(mocked_uno): +def test_desktop_already_has_desktop(mocked_uno): """ Test that setup_desktop() exits early when there's already a desktop """ @@ -78,15 +78,16 @@ def test_setup_desktop_already_has_desktop(mocked_uno): server = LibreOfficeServer() server._desktop = MagicMock() - # WHEN: setup_desktop() is called - server.setup_desktop() + # 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_setup_desktop_exception(mocked_uno): +def test_desktop_exception(mocked_uno): """ Test that setting up the desktop works correctly when an exception occurs """ @@ -102,16 +103,16 @@ def test_setup_desktop_exception(mocked_uno): mocked_uno_instance.ServiceManager = MockedServiceManager MockedServiceManager.createInstanceWithContext.side_effect = Exception() - # WHEN: setup_desktop() is called - server.setup_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_pipe;urp;StarOffice.ComponentContext'), - call('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') + 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( @@ -121,7 +122,7 @@ def test_setup_desktop_exception(mocked_uno): @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') -def test_setup_desktop(mocked_uno): +def test_desktop(mocked_uno): """ Test that setting up the desktop works correctly """ @@ -138,16 +139,16 @@ def test_setup_desktop(mocked_uno): mocked_uno_instance.ServiceManager = MockedServiceManager MockedServiceManager.createInstanceWithContext.return_value = mocked_desktop - # WHEN: setup_desktop() is called - server.setup_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_pipe;urp;StarOffice.ComponentContext'), - call('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') + 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( @@ -257,35 +258,6 @@ def test_get_text_from_page_notes(): assert text == 'Page Notes\n' -def test_has_desktop_no_desktop(): - """ - Test the has_desktop() method when there's no desktop - """ - # GIVEN: A LibreOfficeServer object - server = LibreOfficeServer() - - # WHEN: has_desktop() is called - result = server.has_desktop() - - # THEN: The result should be False - assert result is False - - -def test_has_desktop(): - """ - Test the has_desktop() method - """ - # GIVEN: A LibreOfficeServer object and a desktop - server = LibreOfficeServer() - server._desktop = MagicMock() - - # WHEN: has_desktop() is called - result = server.has_desktop() - - # THEN: The result should be True - assert result is True - - def test_shutdown_other_docs(): """ Test the shutdown method while other documents are open in LibreOffice diff --git a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py index e295dfd86..846ff098f 100644 --- a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py @@ -160,21 +160,6 @@ class TestMacLODocument(TestCase): self.controller._client = self.mocked_client self.document = MacLODocument(self.controller, self.file_name) - def test_load_presentation_no_desktop(self): - """ - Test the load_presentation() method when there's no desktop yet - """ - # GIVEN: A document and a mocked client - self.mocked_client.has_desktop.return_value = False - - # WHEN: load_presentation() is called - result = self.document.load_presentation() - - # THEN: Stuff should work right - self.mocked_client.setup_desktop.assert_called_once_with() - self.mocked_client.has_desktop.assert_called_once_with() - assert result is False - @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList') def test_load_presentation_cannot_load(self, MockedScreenList): """ @@ -183,17 +168,14 @@ class TestMacLODocument(TestCase): # 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.has_desktop.return_value = True + 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.setup_desktop.assert_called_once_with() - self.mocked_client.has_desktop.assert_called_once_with() - self.mocked_client.load_presentation.assert_called_once_with(self.file_name, 1) + 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') @@ -204,8 +186,7 @@ class TestMacLODocument(TestCase): # 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.has_desktop.return_value = True + mocked_screen_list.current.number = 0 self.mocked_client.load_presentation.return_value = True # WHEN: load_presentation() is called @@ -214,9 +195,7 @@ class TestMacLODocument(TestCase): result = self.document.load_presentation() # THEN: Stuff should work right - self.mocked_client.setup_desktop.assert_called_once_with() - self.mocked_client.has_desktop.assert_called_once_with() - self.mocked_client.load_presentation.assert_called_once_with(self.file_name, 1) + 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 @@ -252,8 +231,8 @@ class TestMacLODocument(TestCase): # THEN: The method should complete successfully self.mocked_client.extract_thumbnails.assert_called_once_with('temp') assert mocked_convert_thumbnail.call_args_list == [ - call('thumb1.png', 1), call('thumb2.png', 2)] - assert mocked_delete_file.call_args_list == [call('thumb1.png'), call('thumb2.png')] + 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): """ @@ -370,6 +349,7 @@ class TestMacLODocument(TestCase): """ # 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 From 7558fb6e64d3e6488c036513057f3681a0aea53f Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 28 May 2019 22:37:50 +0200 Subject: [PATCH 28/36] Use a live webengine widget when previwing themes. --- openlp/core/display/render.py | 55 +++++++++++++++++++++------------- openlp/core/ui/themeform.py | 33 ++++++++++---------- openlp/core/ui/thememanager.py | 24 +++++++-------- openlp/core/ui/themewizard.py | 20 ++++++------- 4 files changed, 72 insertions(+), 60 deletions(-) diff --git a/openlp/core/display/render.py b/openlp/core/display/render.py index 05b7b97fc..f90bcebca 100644 --- a/openlp/core/display/render.py +++ b/openlp/core/display/render.py @@ -425,7 +425,7 @@ def get_start_tags(raw_text): return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags) -class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow): +class ThemePreviewRenderer(LogMixin, DisplayWindow): """ A virtual display used for rendering thumbnails and other offscreen tasks """ @@ -435,24 +435,6 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow): """ super().__init__(*args, **kwargs) self.force_page = False - for screen in ScreenList(): - if screen.is_display: - self.setGeometry(screen.display_geometry.x(), screen.display_geometry.y(), - screen.display_geometry.width(), screen.display_geometry.height()) - break - # If the display is not show'ed and hidden like this webegine will not render - self.show() - self.hide() - self.theme_height = 0 - self.theme_level = ThemeLevel.Global - - def set_theme_level(self, theme_level): - """ - Sets the theme level. - - :param theme_level: The theme level to be used. - """ - self.theme_level = theme_level def calculate_line_count(self): """ @@ -466,7 +448,7 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow): """ return self.run_javascript('Display.clearSlides();') - def generate_preview(self, theme_data, force_page=False): + def generate_preview(self, theme_data, force_page=False, generate_screenshot=True): """ Generate a preview of a theme. @@ -486,7 +468,8 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow): verses['verse'] = 'V1' self.load_verses([verses]) self.force_page = False - return self.save_screenshot() + if generate_screenshot: + return self.save_screenshot() self.force_page = False return None @@ -745,3 +728,33 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow): pixmap.save(fname, ext) else: return pixmap + + +class Renderer(RegistryBase, RegistryProperties, ThemePreviewRenderer): + """ + A virtual display used for rendering thumbnails and other offscreen tasks + """ + def __init__(self, *args, **kwargs): + """ + Constructor + """ + super().__init__(*args, **kwargs) + self.force_page = False + for screen in ScreenList(): + if screen.is_display: + self.setGeometry(screen.display_geometry.x(), screen.display_geometry.y(), + screen.display_geometry.width(), screen.display_geometry.height()) + break + # If the display is not show'ed and hidden like this webegine will not render + self.show() + self.hide() + self.theme_height = 0 + self.theme_level = ThemeLevel.Global + + def set_theme_level(self, theme_level): + """ + Sets the theme level. + + :param theme_level: The theme level to be used. + """ + self.theme_level = theme_level diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index 0dfb53aab..1e6dd54b1 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -174,16 +174,12 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): if not event: event = QtGui.QResizeEvent(self.size(), self.size()) QtWidgets.QWizard.resizeEvent(self, event) - if hasattr(self, 'preview_page') and self.currentPage() == self.preview_page: - frame_width = self.preview_box_label.lineWidth() - pixmap_width = self.preview_area.width() - 2 * frame_width - pixmap_height = self.preview_area.height() - 2 * frame_width - aspect_ratio = float(pixmap_width) / pixmap_height - if aspect_ratio < self.display_aspect_ratio: - pixmap_height = int(pixmap_width / self.display_aspect_ratio + 0.5) - else: - pixmap_width = int(pixmap_height * self.display_aspect_ratio + 0.5) - self.preview_box_label.setFixedSize(pixmap_width + 2 * frame_width, pixmap_height + 2 * frame_width) + try: + self.display_aspect_ratio = self.renderer.width() / self.renderer.height() + except ZeroDivisionError: + self.display_aspect_ratio = 1 + self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio) + self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width()) def validateCurrentPage(self): """ @@ -208,10 +204,16 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): self.setOption(QtWidgets.QWizard.HaveCustomButton1, enabled) if self.page(page_id) == self.preview_page: self.update_theme() - frame = self.theme_manager.generate_image(self.theme) - frame.setDevicePixelRatio(self.devicePixelRatio()) - self.preview_box_label.setPixmap(frame) - self.display_aspect_ratio = float(frame.width()) / frame.height() + self.preview_box.set_theme(self.theme) + self.preview_box.clear_slides() + self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width()) + try: + self.display_aspect_ratio = self.renderer.width() / self.renderer.height() + except ZeroDivisionError: + self.display_aspect_ratio = 1 + self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio) + self.preview_box.generate_preview(self.theme, False, False) + self.preview_box.show() self.resizeEvent() def on_custom_1_button_clicked(self, number): @@ -400,6 +402,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): Handle the display and state of the Preview page. """ self.setField('name', self.theme.theme_name) + self.preview_box.set_theme(self.theme) def on_background_combo_box_current_index_changed(self, index): """ @@ -560,5 +563,5 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): source_path = self.theme.background_filename if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name): return - self.theme_manager.save_theme(self.theme, source_path, destination_path) + self.theme_manager.save_theme(self.theme, source_path, destination_path, self.preview_box.save_screenshot()) return QtWidgets.QDialog.accept(self) diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index fd003ed2e..3c8aee1e6 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -639,24 +639,14 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R return False return True - def save_theme(self, theme, image_source_path, image_destination_path): - """ - Called by theme maintenance Dialog to save the theme and to trigger the reload of the theme list - - :param Theme theme: The theme data object. - :param Path image_source_path: Where the theme image is currently located. - :param Path image_destination_path: Where the Theme Image is to be saved to - :rtype: None - """ - self._write_theme(theme, image_source_path, image_destination_path) - - def _write_theme(self, theme, image_source_path=None, image_destination_path=None): + def save_theme(self, theme, image_source_path, image_destination_path, image=None): """ Writes the theme to the disk and handles the background image if necessary :param Theme theme: The theme data object. :param Path image_source_path: Where the theme image is currently located. :param Path image_destination_path: Where the Theme Image is to be saved to + :param image: The example image of the theme. Optionally. :rtype: None """ name = theme.theme_name @@ -676,7 +666,15 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R shutil.copyfile(image_source_path, image_destination_path) except OSError: self.log_exception('Failed to save theme image') - self.generate_and_save_image(name, theme) + if image: + sample_path_name = self.theme_path / '{file_name}.png'.format(file_name=name) + if sample_path_name.exists(): + sample_path_name.unlink() + image.save(str(sample_path_name), 'png') + thumb_path = self.thumb_path / '{name}.png'.format(name=name) + create_thumb(sample_path_name, thumb_path, False) + else: + self.generate_and_save_image(name, theme) def generate_and_save_image(self, theme_name, theme): """ diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index e6a7ac2c5..1414e3f46 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -31,7 +31,8 @@ from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets from openlp.core.ui.icons import UiIcons from openlp.core.widgets.buttons import ColorButton from openlp.core.widgets.edits import PathEdit - +from openlp.core.widgets.layouts import AspectRatioLayout +from openlp.core.display.render import ThemePreviewRenderer class Ui_ThemeWizard(object): """ @@ -363,16 +364,13 @@ class Ui_ThemeWizard(object): self.preview_layout.addLayout(self.theme_name_layout) self.preview_area = QtWidgets.QWidget(self.preview_page) self.preview_area.setObjectName('PreviewArea') - self.preview_area_layout = QtWidgets.QGridLayout(self.preview_area) - self.preview_area_layout.setContentsMargins(0, 0, 0, 0) - self.preview_area_layout.setColumnStretch(0, 1) - self.preview_area_layout.setRowStretch(0, 1) - self.preview_area_layout.setObjectName('preview_area_layout') - self.preview_box_label = QtWidgets.QLabel(self.preview_area) - self.preview_box_label.setFrameShape(QtWidgets.QFrame.Box) - self.preview_box_label.setScaledContents(True) - self.preview_box_label.setObjectName('preview_box_label') - self.preview_area_layout.addWidget(self.preview_box_label) + self.preview_area_layout = AspectRatioLayout(self.preview_area, 0.75) # Dummy ratio, will be update + self.preview_area_layout.margin = 8 + self.preview_area_layout.setSpacing(0) + self.preview_area_layout.setObjectName('preview_web_layout') + self.preview_box = ThemePreviewRenderer(self) + self.preview_box.setObjectName('preview_box') + self.preview_area_layout.addWidget(self.preview_box) self.preview_layout.addWidget(self.preview_area) theme_wizard.addPage(self.preview_page) self.retranslate_ui(theme_wizard) From 8b489de9541631fe08204311a72b9cccc7135b05 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 3 Jun 2019 22:11:19 +0200 Subject: [PATCH 29/36] Generate a real footer --- openlp/core/display/render.py | 35 +++++++++++++++++++++++++--- openlp/plugins/songs/lib/songstab.py | 2 +- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/openlp/core/display/render.py b/openlp/core/display/render.py index f90bcebca..b437d7aee 100644 --- a/openlp/core/display/render.py +++ b/openlp/core/display/render.py @@ -24,6 +24,7 @@ The :mod:`~openlp.display.render` module contains functions for rendering. """ import html import logging +import mako import math import os import re @@ -32,8 +33,10 @@ import time from PyQt5 import QtWidgets, QtGui from openlp.core.common import ThemeLevel +from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.registry import Registry, RegistryBase +from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.display.window import DisplayWindow from openlp.core.lib import ItemCapabilities @@ -58,8 +61,11 @@ VERSE = 'The Lord said to {r}Noah{/r}: \n' \ '{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \ 'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n' VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100))) -TITLE = 'Arky Arky (Unknown)' -FOOTER = ['Public Domain', 'CCLI 123456'] +TITLE = 'Arky Arky' +AUTHOR = 'John Doe' +FOOTER_COPYRIGHT = 'Public Domain' +CCLI_NO = '123456' + def remove_tags(text, can_remove_chords=False): @@ -448,6 +454,28 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): """ return self.run_javascript('Display.clearSlides();') + def generate_footer(self): + """ + """ + footer_template = Settings().value('songs/footer template') + # Keep this in sync with the list in songstab.py + vars = { + 'title': TITLE, + 'authors_none_label': translate('OpenLP.Ui', 'Written by'), + 'authors_words_label': translate('SongsPlugin.AuthorType', 'Words', 'Author who wrote the lyrics of a song'), + 'authors_words': AUTHOR, + 'copyright': FOOTER_COPYRIGHT, + 'ccli_license': Settings().value('core/ccli number'), + 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'), + 'ccli_number': CCLI_NO, + } + try: + footer_html = mako.template.Template(footer_template).render_unicode(**vars).replace('\n', '') + except mako.exceptions.SyntaxException: + log.error('Failed to render Song footer html:\n' + mako.exceptions.text_error_template().render()) + footer_html = 'Dummy footer text' + return footer_html + def generate_preview(self, theme_data, force_page=False, generate_screenshot=True): """ Generate a preview of a theme. @@ -466,6 +494,7 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): verses['title'] = TITLE verses['text'] = slides[0] verses['verse'] = 'V1' + verses['footer'] = self.generate_footer() self.load_verses([verses]) self.force_page = False if generate_screenshot: @@ -498,7 +527,7 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): if item and item.is_capable(ItemCapabilities.CanWordSplit): pages = self._paginate_slide_words(text.split('\n'), line_end) # Songs and Custom - elif item is None or item.is_capable(ItemCapabilities.CanSoftBreak): + elif item is None or (item and item.is_capable(ItemCapabilities.CanSoftBreak)): pages = [] if '[---]' in text: # Remove Overflow split if at start of the text diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index 34b83a8e5..0abd6edda 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -88,7 +88,7 @@ class SongsTab(SettingsTab): self.footer_group_box = QtWidgets.QGroupBox(self.left_column) self.footer_group_box.setObjectName('footer_group_box') self.footer_layout = QtWidgets.QVBoxLayout(self.footer_group_box) - self.footer_layout.setObjectName('chords_layout') + self.footer_layout.setObjectName('footer_layout') self.footer_info_label = QtWidgets.QLabel(self.footer_group_box) self.footer_layout.addWidget(self.footer_info_label) self.footer_placeholder_info = QtWidgets.QTextEdit(self.footer_group_box) From 7819a9eb0290cb83112dd010768a352233594e38 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 4 Jun 2019 21:57:26 -0700 Subject: [PATCH 30/36] Fix license issues --- .../presentations/lib/libreofficeserver.py | 36 +++++++++---------- .../presentations/lib/maclocontroller.py | 36 +++++++++---------- .../plugins/presentations/lib/serializers.py | 24 +++++++++++++ .../presentations/test_libreofficeserver.py | 36 +++++++++---------- .../presentations/test_maclocontroller.py | 36 +++++++++---------- 5 files changed, 96 insertions(+), 72 deletions(-) diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py index 99a04f654..b3c0cd254 100644 --- a/openlp/plugins/presentations/lib/libreofficeserver.py +++ b/openlp/plugins/presentations/lib/libreofficeserver.py @@ -1,24 +1,24 @@ # -*- coding: utf-8 -*- # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2016 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; version 2 of the License. # -# # -# 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, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### +########################################################################## +# 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 . # +########################################################################## """ This module runs a Pyro4 server using LibreOffice's version of Python diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py index 6ac009cd4..10ba08e5e 100644 --- a/openlp/plugins/presentations/lib/maclocontroller.py +++ b/openlp/plugins/presentations/lib/maclocontroller.py @@ -1,24 +1,24 @@ # -*- coding: utf-8 -*- # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2016 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; version 2 of the License. # -# # -# 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, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### +########################################################################## +# 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 . # +########################################################################## import logging from subprocess import Popen diff --git a/openlp/plugins/presentations/lib/serializers.py b/openlp/plugins/presentations/lib/serializers.py index 4c57c7f93..6e9a69cb0 100644 --- a/openlp/plugins/presentations/lib/serializers.py +++ b/openlp/plugins/presentations/lib/serializers.py @@ -1,3 +1,27 @@ +# -*- 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 . # +########################################################################## +""" +This module contains some helpers for serializing Path objects in Pyro4 +""" try: from openlp.core.common.path import Path except ImportError: diff --git a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py index af8548a52..d747317d9 100644 --- a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py +++ b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py @@ -1,24 +1,24 @@ # -*- coding: utf-8 -*- # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2016 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; version 2 of the License. # -# # -# 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, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### +########################################################################## +# 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 . # +########################################################################## """ Functional tests to test the LibreOffice Pyro server """ diff --git a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py index 846ff098f..e53a0b576 100644 --- a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py @@ -1,24 +1,24 @@ # -*- coding: utf-8 -*- # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2016 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; version 2 of the License. # -# # -# 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, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### +########################################################################## +# 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 . # +########################################################################## """ Functional tests to test the Mac LibreOffice class and related methods. """ From f27fded597e35333ce42eafc9c653b1144d93b40 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Thu, 6 Jun 2019 22:10:39 +0200 Subject: [PATCH 31/36] Fix line calculation for the theme preview. Try to make VLC loading more robust. --- openlp/core/display/render.py | 8 ++++---- openlp/core/ui/media/vlcplayer.py | 27 ++++++++++++++------------- openlp/core/ui/themeform.py | 10 ++++++---- openlp/core/ui/thememanager.py | 2 +- openlp/plugins/media/lib/mediaitem.py | 21 +++++++++++---------- 5 files changed, 36 insertions(+), 32 deletions(-) diff --git a/openlp/core/display/render.py b/openlp/core/display/render.py index b437d7aee..cc252494f 100644 --- a/openlp/core/display/render.py +++ b/openlp/core/display/render.py @@ -463,7 +463,7 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): 'title': TITLE, 'authors_none_label': translate('OpenLP.Ui', 'Written by'), 'authors_words_label': translate('SongsPlugin.AuthorType', 'Words', 'Author who wrote the lyrics of a song'), - 'authors_words': AUTHOR, + 'authors_words': [AUTHOR], 'copyright': FOOTER_COPYRIGHT, 'ccli_license': Settings().value('core/ccli number'), 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'), @@ -489,10 +489,10 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): if not self.force_page: self.set_theme(theme_data) self.theme_height = theme_data.font_main_height - slides = self.format_slide(render_tags(VERSE), None) + slides = self.format_slide(VERSE, None) verses = dict() verses['title'] = TITLE - verses['text'] = slides[0] + verses['text'] = render_tags(slides[0]) verses['verse'] = 'V1' verses['footer'] = self.generate_footer() self.load_verses([verses]) @@ -734,7 +734,7 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): :param text: The text to check. It may contain HTML tags. """ self.clear_slides() - self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");'.format(text=text), is_sync=True) + self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");'.format(text=text.replace('"', '\\"')), is_sync=True) does_text_fits = self.run_javascript('Display.doesContentFit();', is_sync=True) return does_text_fits diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 6ba27998b..96cc5b3ab 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -28,7 +28,6 @@ import os import sys import threading from datetime import datetime -import vlc from PyQt5 import QtWidgets @@ -65,25 +64,27 @@ def get_vlc(): :return: The "vlc" module, or None """ - if 'vlc' in sys.modules: - # If VLC has already been imported, no need to do all the stuff below again - is_vlc_available = False + # Import the VLC module if not already done + if 'vlc' not in sys.modules: try: - is_vlc_available = bool(sys.modules['vlc'].get_default_instance()) - except Exception: - pass - if is_vlc_available: - return sys.modules['vlc'] - else: + import vlc + except ImportError: return None - else: - return vlc + # Verify that VLC is also loadable + is_vlc_available = False + try: + is_vlc_available = bool(sys.modules['vlc'].get_default_instance()) + except Exception: + pass + if is_vlc_available: + return sys.modules['vlc'] + return None # On linux we need to initialise X threads, but not when running tests. # This needs to happen on module load and not in get_vlc(), otherwise it can cause crashes on some DE on some setups # (reported on Gnome3, Unity, Cinnamon, all GTK+ based) when using native filedialogs... -if is_linux() and 'nose' not in sys.argv[0] and get_vlc(): +if is_linux() and 'pytest' not in sys.argv[0] and get_vlc(): try: try: x11 = ctypes.cdll.LoadLibrary('libX11.so.6') diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index 1e6dd54b1..00f2548c7 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -178,8 +178,10 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): self.display_aspect_ratio = self.renderer.width() / self.renderer.height() except ZeroDivisionError: self.display_aspect_ratio = 1 - self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio) - self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width()) + # Make sure we don't resize before the widgets are actually created + if hasattr(self, 'preview_area_layout'): + self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio) + self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width()) def validateCurrentPage(self): """ @@ -212,9 +214,9 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): except ZeroDivisionError: self.display_aspect_ratio = 1 self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio) - self.preview_box.generate_preview(self.theme, False, False) - self.preview_box.show() self.resizeEvent() + self.preview_box.show() + self.preview_box.generate_preview(self.theme, False, False) def on_custom_1_button_clicked(self, number): """ diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 3c8aee1e6..986a38135 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -476,7 +476,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R if not theme_paths: theme = Theme() theme.theme_name = UiStrings().Default - self._write_theme(theme) + self.save_theme(theme) Settings().setValue(self.settings_section + '/global theme', theme.theme_name) self.application.set_normal_cursor() diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 658773b99..5b33b3836 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -259,35 +259,36 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): # TODO needs to be fixed as no idea why this fails # media.sort(key=lambda file_path: get_natural_key(file_path.name)) for track in media: - track_info = QtCore.QFileInfo(track) + track_str = str(track) + track_info = QtCore.QFileInfo(track_str) item_name = None - if track.startswith('optical:'): + if track_str.startswith('optical:'): # Handle optical based item - (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track) + (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track_str) item_name = QtWidgets.QListWidgetItem(clip_name) item_name.setIcon(UiIcons().optical) - item_name.setData(QtCore.Qt.UserRole, track) + item_name.setData(QtCore.Qt.UserRole, track_str) item_name.setToolTip('{name}@{start}-{end}'.format(name=file_name, start=format_milliseconds(start), end=format_milliseconds(end))) elif not os.path.exists(track): # File doesn't exist, mark as error. - file_name = os.path.split(str(track))[1] + file_name = os.path.split(track_str)[1] item_name = QtWidgets.QListWidgetItem(file_name) item_name.setIcon(UiIcons().error) - item_name.setData(QtCore.Qt.UserRole, track) - item_name.setToolTip(track) + item_name.setData(QtCore.Qt.UserRole, track_str) + item_name.setToolTip(track_str) elif track_info.isFile(): # Normal media file handling. - file_name = os.path.split(str(track))[1] + file_name = os.path.split(track_str)[1] item_name = QtWidgets.QListWidgetItem(file_name) search = file_name.split('.')[-1].lower() if '*.{text}'.format(text=search) in self.media_controller.audio_extensions_list: item_name.setIcon(UiIcons().audio) else: item_name.setIcon(UiIcons().video) - item_name.setData(QtCore.Qt.UserRole, track) - item_name.setToolTip(track) + item_name.setData(QtCore.Qt.UserRole, track_str) + item_name.setToolTip(track_str) if item_name: self.list_view.addItem(item_name) From 4074d110ffb7e854234e3758b7ca60f38419ca6a Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Fri, 7 Jun 2019 22:46:51 +0200 Subject: [PATCH 32/36] some path/str fixes --- openlp/plugins/media/lib/mediaitem.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 27ad92936..30962967a 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -173,7 +173,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): item = self.list_view.currentItem() if item is None: return False - filename = item.data(QtCore.Qt.UserRole) + filename = str(item.data(QtCore.Qt.UserRole)) # Special handling if the filename is a optical clip if filename.startswith('optical:'): (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename) @@ -267,7 +267,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track_str) item_name = QtWidgets.QListWidgetItem(clip_name) item_name.setIcon(UiIcons().optical) - item_name.setData(QtCore.Qt.UserRole, track_str) + item_name.setData(QtCore.Qt.UserRole, track) item_name.setToolTip('{name}@{start}-{end}'.format(name=file_name, start=format_milliseconds(start), end=format_milliseconds(end))) @@ -276,18 +276,18 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): file_name = os.path.split(track_str)[1] item_name = QtWidgets.QListWidgetItem(file_name) item_name.setIcon(UiIcons().error) - item_name.setData(QtCore.Qt.UserRole, track_str) + item_name.setData(QtCore.Qt.UserRole, track) item_name.setToolTip(track_str) elif track_info.isFile(): # Normal media file handling. file_name = os.path.split(track_str)[1] item_name = QtWidgets.QListWidgetItem(file_name) search = file_name.split('.')[-1].lower() - if '*.{text}'.format(text=search) in self.media_controller.audio_extensions_list: + if search in AUDIO_EXT: item_name.setIcon(UiIcons().audio) else: item_name.setIcon(UiIcons().video) - item_name.setData(QtCore.Qt.UserRole, track_str) + item_name.setData(QtCore.Qt.UserRole, track) item_name.setToolTip(track_str) if item_name: self.list_view.addItem(item_name) From ad9d1df172f60f66721331d067dd8aae822bd94c Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Fri, 7 Jun 2019 22:51:19 +0200 Subject: [PATCH 33/36] pep8 --- openlp/core/display/render.py | 7 ++++--- openlp/core/ui/themewizard.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openlp/core/display/render.py b/openlp/core/display/render.py index cc252494f..0002d26e1 100644 --- a/openlp/core/display/render.py +++ b/openlp/core/display/render.py @@ -67,7 +67,6 @@ FOOTER_COPYRIGHT = 'Public Domain' CCLI_NO = '123456' - def remove_tags(text, can_remove_chords=False): """ Remove Tags from text for display @@ -462,7 +461,8 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): vars = { 'title': TITLE, 'authors_none_label': translate('OpenLP.Ui', 'Written by'), - 'authors_words_label': translate('SongsPlugin.AuthorType', 'Words', 'Author who wrote the lyrics of a song'), + 'authors_words_label': translate('SongsPlugin.AuthorType', 'Words', + 'Author who wrote the lyrics of a song'), 'authors_words': [AUTHOR], 'copyright': FOOTER_COPYRIGHT, 'ccli_license': Settings().value('core/ccli number'), @@ -734,7 +734,8 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): :param text: The text to check. It may contain HTML tags. """ self.clear_slides() - self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");'.format(text=text.replace('"', '\\"')), is_sync=True) + self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");' + .format(text=text.replace('"', '\\"')), is_sync=True) does_text_fits = self.run_javascript('Display.doesContentFit();', is_sync=True) return does_text_fits diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index 1414e3f46..a60e0dc7f 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -34,6 +34,7 @@ from openlp.core.widgets.edits import PathEdit from openlp.core.widgets.layouts import AspectRatioLayout from openlp.core.display.render import ThemePreviewRenderer + class Ui_ThemeWizard(object): """ The Create/Edit theme wizard From 04d3efef9e182a028d8c6c465270a0ddf10429b5 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Fri, 7 Jun 2019 23:05:02 +0200 Subject: [PATCH 34/36] fix some broken tests --- openlp/core/ui/thememanager.py | 2 +- .../openlp_core/ui/test_thememanager.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 986a38135..46d067970 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -639,7 +639,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R return False return True - def save_theme(self, theme, image_source_path, image_destination_path, image=None): + def save_theme(self, theme, image_source_path=None, image_destination_path=None, image=None): """ Writes the theme to the disk and handles the background image if necessary diff --git a/tests/functional/openlp_core/ui/test_thememanager.py b/tests/functional/openlp_core/ui/test_thememanager.py index b4d50331b..3b015f238 100644 --- a/tests/functional/openlp_core/ui/test_thememanager.py +++ b/tests/functional/openlp_core/ui/test_thememanager.py @@ -83,7 +83,7 @@ class TestThemeManager(TestCase): @patch('openlp.core.ui.thememanager.shutil') @patch('openlp.core.ui.thememanager.create_paths') - def test_write_theme_same_image(self, mocked_create_paths, mocked_shutil): + def test_save_theme_same_image(self, mocked_create_paths, mocked_shutil): """ Test that we don't try to overwrite a theme background image with itself """ @@ -98,16 +98,16 @@ class TestThemeManager(TestCase): mocked_theme.extract_formatted_xml = MagicMock() mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode() - # WHEN: Calling _write_theme with path to the same image, but the path written slightly different + # WHEN: Calling save_theme with path to the same image, but the path written slightly different file_path_1 = RESOURCE_PATH / 'church.jpg' - theme_manager._write_theme(mocked_theme, file_path_1, file_path_1) + theme_manager.save_theme(mocked_theme, file_path_1, file_path_1) # THEN: The mocked_copyfile should not have been called assert mocked_shutil.copyfile.called is False, 'copyfile should not be called' @patch('openlp.core.ui.thememanager.shutil') @patch('openlp.core.ui.thememanager.create_paths') - def test_write_theme_diff_images(self, mocked_create_paths, mocked_shutil): + def test_save_theme_diff_images(self, mocked_create_paths, mocked_shutil): """ Test that we do overwrite a theme background image when a new is submitted """ @@ -121,15 +121,15 @@ class TestThemeManager(TestCase): mocked_theme.theme_name = 'themename' mocked_theme.filename = "filename" - # WHEN: Calling _write_theme with path to different images + # WHEN: Calling save_theme with path to different images file_path_1 = RESOURCE_PATH / 'church.jpg' file_path_2 = RESOURCE_PATH / 'church2.jpg' - theme_manager._write_theme(mocked_theme, file_path_1, file_path_2) + theme_manager.save_theme(mocked_theme, file_path_1, file_path_2) # THEN: The mocked_copyfile should not have been called assert mocked_shutil.copyfile.called is True, 'copyfile should be called' - def test_write_theme_special_char_name(self): + def test_save_theme_special_char_name(self): """ Test that we can save themes with special characters in the name """ @@ -142,8 +142,8 @@ class TestThemeManager(TestCase): mocked_theme.theme_name = 'theme 愛 name' mocked_theme.export_theme.return_value = "{}" - # WHEN: Calling _write_theme with a theme with a name with special characters in it - theme_manager._write_theme(mocked_theme) + # WHEN: Calling save_theme with a theme with a name with special characters in it + theme_manager.save_theme(mocked_theme) # THEN: It should have been created assert os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.json')) is True, \ From 0c7fe8d97c963f978804e0959bf20f1d01f68c19 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 9 Jun 2019 22:06:20 +0200 Subject: [PATCH 35/36] fix test --- tests/openlp_core/ui/test_themeform.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/openlp_core/ui/test_themeform.py b/tests/openlp_core/ui/test_themeform.py index 87a5f3bf6..02a7d0589 100644 --- a/tests/openlp_core/ui/test_themeform.py +++ b/tests/openlp_core/ui/test_themeform.py @@ -23,6 +23,7 @@ Interface tests to test the ThemeWizard class and related methods. """ from unittest import TestCase +from unittest.mock import patch from openlp.core.common.registry import Registry from openlp.core.ui.themeform import ThemeForm @@ -39,7 +40,8 @@ class TestThemeManager(TestCase, TestMixin): """ Registry.create() - def test_create_theme_wizard(self): + @patch('openlp.core.display.window.QtWidgets.QVBoxLayout') + def test_create_theme_wizard(self, mocked_qvboxlayout): """ Test creating a ThemeForm instance """ From 31f6779d3fc67187688c473595e19ca9b8bc140a Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 9 Jun 2019 22:22:43 +0200 Subject: [PATCH 36/36] fix pep8 --- openlp/core/display/render.py | 2 +- openlp/core/ui/media/vlcplayer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/core/display/render.py b/openlp/core/display/render.py index 0002d26e1..1097b5ac0 100644 --- a/openlp/core/display/render.py +++ b/openlp/core/display/render.py @@ -33,7 +33,7 @@ import time from PyQt5 import QtWidgets, QtGui from openlp.core.common import ThemeLevel -from openlp.core.common.i18n import UiStrings, translate +from openlp.core.common.i18n import translate from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 85f1cb799..244916d24 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -64,7 +64,7 @@ def get_vlc(): # Import the VLC module if not already done if 'vlc' not in sys.modules: try: - import vlc + import vlc # noqa module is not used directly, but is used via sys.modules['vlc'] except ImportError: return None # Verify that VLC is also loadable