Raoul Snyman has proposed merging lp:~raoul-snyman/openlp/better-threading into lp:openlp.
Requested reviews: OpenLP Core (openlp-core) For more details, see: https://code.launchpad.net/~raoul-snyman/openlp/better-threading/+merge/335801 Major overhaul of how threading in OpenLP works. Rather than messing around with threads yourself, you create a worker object descended from ThreadWorker, implement start() (and stop() if it's a long-running thread), and run it using run_thread(). Add this to your merge proposal: -------------------------------------------------------------------------------- lp:~raoul-snyman/openlp/better-threading (revision 2803) https://ci.openlp.io/job/Branch-01-Pull/2413/ [SUCCESS] https://ci.openlp.io/job/Branch-02a-Linux-Tests/2314/ [SUCCESS] https://ci.openlp.io/job/Branch-02b-macOS-Tests/109/ [SUCCESS] https://ci.openlp.io/job/Branch-03a-Build-Source/32/ [SUCCESS] https://ci.openlp.io/job/Branch-03b-Build-macOS/31/ [SUCCESS] https://ci.openlp.io/job/Branch-04a-Code-Analysis/1494/ [SUCCESS] https://ci.openlp.io/job/Branch-04b-Test-Coverage/1307/ [SUCCESS] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/258/ [FAILURE] Stopping after failure Failed builds: - Branch-05-AppVeyor-Tests #258: https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/258/console -- Your team OpenLP Core is requested to review the proposed merge of lp:~raoul-snyman/openlp/better-threading into lp:openlp.
=== modified file 'openlp/core/api/deploy.py' --- openlp/core/api/deploy.py 2017-12-29 09:15:48 +0000 +++ openlp/core/api/deploy.py 2018-01-07 05:37:57 +0000 @@ -25,7 +25,7 @@ from zipfile import ZipFile from openlp.core.common.applocation import AppLocation -from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size +from openlp.core.common.httputils import download_file, get_web_page, get_url_file_size from openlp.core.common.registry import Registry @@ -65,7 +65,7 @@ sha256, version = download_sha256() file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip') callback.setRange(0, file_size) - if url_get_file(callback, 'https://get.openlp.org/webclient/site.zip', - AppLocation.get_section_data_path('remotes') / 'site.zip', - sha256=sha256): + if download_file(callback, 'https://get.openlp.org/webclient/site.zip', + AppLocation.get_section_data_path('remotes') / 'site.zip', + sha256=sha256): deploy_zipfile(AppLocation.get_section_data_path('remotes'), 'site.zip') === modified file 'openlp/core/api/http/server.py' --- openlp/core/api/http/server.py 2017-12-29 09:15:48 +0000 +++ openlp/core/api/http/server.py 2018-01-07 05:37:57 +0000 @@ -27,7 +27,7 @@ import time from PyQt5 import QtCore, QtWidgets -from waitress import serve +from waitress.server import create_server from openlp.core.api.deploy import download_and_check, download_sha256 from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint @@ -44,23 +44,16 @@ from openlp.core.common.path import create_paths from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings +from openlp.core.threading import ThreadWorker, run_thread log = logging.getLogger(__name__) -class HttpWorker(QtCore.QObject): +class HttpWorker(ThreadWorker): """ A special Qt thread class to allow the HTTP server to run at the same time as the UI. """ - def __init__(self): - """ - Constructor for the thread class. - - :param server: The http server class. - """ - super(HttpWorker, self).__init__() - - def run(self): + def start(self): """ Run the thread. """ @@ -68,12 +61,21 @@ port = Settings().value('api/port') Registry().execute('get_website_version') try: - serve(application, host=address, port=port) + self.server = create_server(application, host=address, port=port) + self.server.run() except OSError: log.exception('An error occurred when serving the application.') + self.quit.emit() def stop(self): - pass + """ + A method to stop the worker + """ + if hasattr(self, 'server'): + # Loop through all the channels and close them to stop the server + for channel in self.server._map.values(): + if hasattr(channel, 'close'): + channel.close() class HttpServer(RegistryBase, RegistryProperties, LogMixin): @@ -85,12 +87,9 @@ Initialise the http server, and start the http server """ super(HttpServer, self).__init__(parent) - if Registry().get_flag('no_web_server'): - self.worker = HttpWorker() - self.thread = QtCore.QThread() - self.worker.moveToThread(self.thread) - self.thread.started.connect(self.worker.run) - self.thread.start() + if not Registry().get_flag('no_web_server'): + worker = HttpWorker() + run_thread(worker, 'http_server') Registry().register_function('download_website', self.first_time) Registry().register_function('get_website_version', self.website_version) Registry().set_flag('website_version', '0.0') @@ -167,7 +166,7 @@ self.was_cancelled = False self.previous_size = 0 - def _download_progress(self, count, block_size): + def update_progress(self, count, block_size): """ Calculate and display the download progress. """ === modified file 'openlp/core/api/websockets.py' --- openlp/core/api/websockets.py 2017-12-29 09:15:48 +0000 +++ openlp/core/api/websockets.py 2018-01-07 05:37:57 +0000 @@ -28,37 +28,88 @@ import logging import time -import websockets -from PyQt5 import QtCore +from websockets import serve from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings +from openlp.core.threading import ThreadWorker, run_thread log = logging.getLogger(__name__) -class WebSocketWorker(QtCore.QObject): +async def handle_websocket(request, path): + """ + Handle web socket requests and return the poll information. + Check ever 0.2 seconds to get the latest position and send if changed. + Only gets triggered when 1st client attaches + + :param request: request from client + :param path: determines the endpoints supported + :return: + """ + log.debug('WebSocket handler registered with client') + previous_poll = None + previous_main_poll = None + poller = Registry().get('poller') + if path == '/state': + while True: + current_poll = poller.poll() + if current_poll != previous_poll: + await request.send(json.dumps(current_poll).encode()) + previous_poll = current_poll + await asyncio.sleep(0.2) + elif path == '/live_changed': + while True: + main_poll = poller.main_poll() + if main_poll != previous_main_poll: + await request.send(main_poll) + previous_main_poll = main_poll + await asyncio.sleep(0.2) + + +class WebSocketWorker(ThreadWorker, RegistryProperties, LogMixin): """ A special Qt thread class to allow the WebSockets server to run at the same time as the UI. """ - def __init__(self, server): - """ - Constructor for the thread class. - - :param server: The http server class. - """ - self.ws_server = server - super(WebSocketWorker, self).__init__() - - def run(self): - """ - Run the thread. - """ - self.ws_server.start_server() + def start(self): + """ + Run the worker. + """ + address = Settings().value('api/ip address') + port = Settings().value('api/websocket port') + # Start the event loop + self.event_loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.event_loop) + # Create the websocker server + loop = 1 + self.server = None + while not self.server: + try: + self.server = serve(handle_websocket, address, port) + log.debug('WebSocket server started on {addr}:{port}'.format(addr=address, port=port)) + except Exception as e: + log.exception('Failed to start WebSocket server') + loop += 1 + time.sleep(0.1) + if not self.server and loop > 3: + log.error('Unable to start WebSocket server {addr}:{port}, giving up'.format(addr=address, port=port)) + if self.server: + # If the websocket server exists, start listening + self.event_loop.run_until_complete(self.server) + self.event_loop.run_forever() + self.quit.emit() def stop(self): - self.ws_server.stop = True + """ + Stop the websocket server + """ + if hasattr(self.server, 'ws_server'): + self.server.ws_server.close() + elif hasattr(self.server, 'server'): + self.server.server.close() + self.event_loop.stop() + self.event_loop.close() class WebSocketServer(RegistryProperties, LogMixin): @@ -70,74 +121,6 @@ Initialise and start the WebSockets server """ super(WebSocketServer, self).__init__() - if Registry().get_flag('no_web_server'): - self.settings_section = 'api' - self.worker = WebSocketWorker(self) - self.thread = QtCore.QThread() - self.worker.moveToThread(self.thread) - self.thread.started.connect(self.worker.run) - self.thread.start() - - def start_server(self): - """ - Start the correct server and save the handler - """ - address = Settings().value(self.settings_section + '/ip address') - port = Settings().value(self.settings_section + '/websocket port') - self.start_websocket_instance(address, port) - # If web socket server start listening - if hasattr(self, 'ws_server') and self.ws_server: - event_loop = asyncio.new_event_loop() - asyncio.set_event_loop(event_loop) - event_loop.run_until_complete(self.ws_server) - event_loop.run_forever() - else: - log.debug('Failed to start ws server on port {port}'.format(port=port)) - - def start_websocket_instance(self, address, port): - """ - Start the server - - :param address: The server address - :param port: The run port - """ - loop = 1 - while loop < 4: - try: - self.ws_server = websockets.serve(self.handle_websocket, address, port) - log.debug("Web Socket Server started for class {address} {port}".format(address=address, port=port)) - break - except Exception as e: - log.error('Failed to start ws server {why}'.format(why=e)) - loop += 1 - time.sleep(0.1) - - @staticmethod - async def handle_websocket(request, path): - """ - Handle web socket requests and return the poll information. - Check ever 0.2 seconds to get the latest position and send if changed. - Only gets triggered when 1st client attaches - - :param request: request from client - :param path: determines the endpoints supported - :return: - """ - log.debug("web socket handler registered with client") - previous_poll = None - previous_main_poll = None - poller = Registry().get('poller') - if path == '/state': - while True: - current_poll = poller.poll() - if current_poll != previous_poll: - await request.send(json.dumps(current_poll).encode()) - previous_poll = current_poll - await asyncio.sleep(0.2) - elif path == '/live_changed': - while True: - main_poll = poller.main_poll() - if main_poll != previous_main_poll: - await request.send(main_poll) - previous_main_poll = main_poll - await asyncio.sleep(0.2) + if not Registry().get_flag('no_web_server'): + worker = WebSocketWorker() + run_thread(worker, 'websocket_server') === modified file 'openlp/core/app.py' --- openlp/core/app.py 2017-12-29 09:15:48 +0000 +++ openlp/core/app.py 2018-01-07 05:37:57 +0000 @@ -304,8 +304,7 @@ 'off a USB flash drive (not implemented).') parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true', help='Ignore the version file and pull the version directly from Bazaar') - parser.add_argument('-s', '--style', dest='style', help='Set the Qt5 style (passed directly to Qt5).') - parser.add_argument('-w', '--no-web-server', dest='no_web_server', action='store_false', + parser.add_argument('-w', '--no-web-server', dest='no_web_server', action='store_true', help='Turn off the Web and Socket Server ') parser.add_argument('rargs', nargs='?', default=[]) # Parse command line options and deal with them. Use args supplied pragmatically if possible. @@ -343,8 +342,6 @@ log.setLevel(logging.WARNING) else: log.setLevel(logging.INFO) - if args and args.style: - qt_args.extend(['-style', args.style]) # Throw the rest of the arguments at Qt, just in case. qt_args.extend(args.rargs) # Bug #1018855: Set the WM_CLASS property in X11 @@ -358,7 +355,7 @@ application.setOrganizationDomain('openlp.org') application.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) application.setAttribute(QtCore.Qt.AA_DontCreateNativeWidgetSiblings, True) - if args and args.portable: + if args.portable: application.setApplicationName('OpenLPPortable') Settings.setDefaultFormat(Settings.IniFormat) # Get location OpenLPPortable.ini === modified file 'openlp/core/common/applocation.py' --- openlp/core/common/applocation.py 2017-12-29 09:15:48 +0000 +++ openlp/core/common/applocation.py 2018-01-07 05:37:57 +0000 @@ -157,7 +157,7 @@ return directory return Path('/usr', 'share', 'openlp') if XDG_BASE_AVAILABLE: - if dir_type == AppLocation.DataDir or dir_type == AppLocation.CacheDir: + if dir_type == AppLocation.DataDir: return Path(BaseDirectory.xdg_data_home, 'openlp') elif dir_type == AppLocation.CacheDir: return Path(BaseDirectory.xdg_cache_home, 'openlp') === modified file 'openlp/core/common/httputils.py' --- openlp/core/common/httputils.py 2017-12-29 09:15:48 +0000 +++ openlp/core/common/httputils.py 2018-01-07 05:37:57 +0000 @@ -20,7 +20,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. +The :mod:`openlp.core.common.httputils` module provides the utility methods for downloading stuff. """ import hashlib import logging @@ -104,7 +104,7 @@ if retries >= CONNECTION_RETRIES: raise ConnectionError('Unable to connect to {url}, see log for details'.format(url=url)) retries += 1 - except: + except: # noqa # Don't know what's happening, so reraise the original log.exception('Unknown error when trying to connect to {url}'.format(url=url)) raise @@ -136,12 +136,12 @@ continue -def url_get_file(callback, url, file_path, sha256=None): +def download_file(update_object, url, file_path, sha256=None): """" Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any point. Returns False on download error. - :param callback: the class which needs to be updated + :param update_object: the object which needs to be updated :param url: URL to download :param file_path: Destination file :param sha256: The check sum value to be checked against the download value @@ -158,13 +158,14 @@ hasher = hashlib.sha256() # Download until finished or canceled. for chunk in response.iter_content(chunk_size=block_size): - if callback.was_cancelled: + if hasattr(update_object, 'was_cancelled') and update_object.was_cancelled: break saved_file.write(chunk) if sha256: hasher.update(chunk) block_count += 1 - callback._download_progress(block_count, block_size) + if hasattr(update_object, 'update_progress'): + update_object.update_progress(block_count, block_size) response.close() if sha256 and hasher.hexdigest() != sha256: log.error('sha256 sums did not match for file %s, got %s, expected %s', file_path, hasher.hexdigest(), @@ -183,7 +184,7 @@ retries += 1 time.sleep(0.1) continue - if callback.was_cancelled and file_path.exists(): + if hasattr(update_object, 'was_cancelled') and update_object.was_cancelled and file_path.exists(): file_path.unlink() return True === modified file 'openlp/core/lib/imagemanager.py' --- openlp/core/lib/imagemanager.py 2017-12-29 09:15:48 +0000 +++ openlp/core/lib/imagemanager.py 2018-01-07 05:37:57 +0000 @@ -35,13 +35,14 @@ from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.lib import resize_image, image_to_byte +from openlp.core.threading import ThreadWorker, run_thread log = logging.getLogger(__name__) -class ImageThread(QtCore.QThread): +class ImageWorker(ThreadWorker): """ - A special Qt thread class to speed up the display of images. This is threaded so it loads the frames and generates + A thread worker class to speed up the display of images. This is threaded so it loads the frames and generates byte stream in background. """ def __init__(self, manager): @@ -51,14 +52,21 @@ ``manager`` The image manager. """ - super(ImageThread, self).__init__(None) + super().__init__() self.image_manager = manager - def run(self): + def start(self): """ - Run the thread. + Start the worker """ self.image_manager.process() + self.quit.emit() + + def stop(self): + """ + Stop the worker + """ + self.image_manager.stop_manager = True class Priority(object): @@ -130,7 +138,7 @@ class PriorityQueue(queue.PriorityQueue): """ - Customised ``Queue.PriorityQueue``. + Customised ``queue.PriorityQueue``. Each item in the queue must be a tuple with three values. The first value is the :class:`Image`'s ``priority`` attribute, the second value the :class:`Image`'s ``secondary_priority`` attribute. The last value the :class:`Image` @@ -179,7 +187,6 @@ self.width = current_screen['size'].width() self.height = current_screen['size'].height() self._cache = {} - self.image_thread = ImageThread(self) self._conversion_queue = PriorityQueue() self.stop_manager = False Registry().register_function('images_regenerate', self.process_updates) @@ -230,9 +237,13 @@ """ Flush the queue to updated any data to update """ - # We want only one thread. - if not self.image_thread.isRunning(): - self.image_thread.start() + try: + worker = ImageWorker(self) + run_thread(worker, 'image_manager') + except KeyError: + # run_thread() will throw a KeyError if this thread already exists, so ignore it so that we don't + # try to start another thread when one is already running + pass def get_image(self, path, source, width=-1, height=-1): """ @@ -305,9 +316,7 @@ if image.path == path and image.timestamp != os.stat(path).st_mtime: image.timestamp = os.stat(path).st_mtime self._reset_image(image) - # We want only one thread. - if not self.image_thread.isRunning(): - self.image_thread.start() + self.process_updates() def process(self): """ === modified file 'openlp/core/projectors/manager.py' --- openlp/core/projectors/manager.py 2018-01-03 00:35:14 +0000 +++ openlp/core/projectors/manager.py 2018-01-07 05:37:57 +0000 @@ -308,8 +308,7 @@ self.settings_section = 'projector' self.projectordb = projectordb self.projector_list = [] - self.pjlink_udp = PJLinkUDP() - self.pjlink_udp.projector_list = self.projector_list + self.pjlink_udp = PJLinkUDP(self.projector_list) self.source_select_form = None def bootstrap_initialise(self): === modified file 'openlp/core/projectors/pjlink.py' --- openlp/core/projectors/pjlink.py 2018-01-03 00:35:14 +0000 +++ openlp/core/projectors/pjlink.py 2018-01-07 05:37:57 +0000 @@ -89,11 +89,11 @@ 'SRCH' # Class 2 (reply is ACKN) ] - def __init__(self, port=PJLINK_PORT): + def __init__(self, projector_list, port=PJLINK_PORT): """ Initialize socket """ - + self.projector_list = projector_list self.port = port === modified file 'openlp/core/threading.py' --- openlp/core/threading.py 2017-12-29 09:15:48 +0000 +++ openlp/core/threading.py 2018-01-07 05:37:57 +0000 @@ -24,26 +24,41 @@ """ from PyQt5 import QtCore - -def run_thread(parent, worker, prefix='', auto_start=True): +from openlp.core.common.registry import Registry + + +class ThreadWorker(QtCore.QObject): + """ + The :class:`~openlp.core.threading.ThreadWorker` class provides a base class for all worker objects + """ + quit = QtCore.pyqtSignal() + + def start(self): + """ + The start method is how the worker runs. Basically, put your code here. + """ + raise NotImplementedError('Your base class needs to override this method and run self.quit.emit() at the end.') + + +def run_thread(worker, thread_name, can_start=True): """ Create a thread and assign a worker to it. This removes a lot of boilerplate code from the codebase. - :param object parent: The parent object so that the thread and worker are not orphaned. :param QObject worker: A QObject-based worker object which does the actual work. - :param str prefix: A prefix to be applied to the attribute names. - :param bool auto_start: Automatically start the thread. Defaults to True. + :param str thread_name: The name of the thread, used to keep track of the thread. + :param bool can_start: Start the thread. Defaults to True. """ - # Set up attribute names - thread_name = 'thread' - worker_name = 'worker' - if prefix: - thread_name = '_'.join([prefix, thread_name]) - worker_name = '_'.join([prefix, worker_name]) + if not thread_name: + raise ValueError('A thread_name is required when calling the "run_thread" function') + main_window = Registry().get('main_window') + if thread_name in main_window.threads: + raise KeyError('A thread with the name "{}" has already been created, please use another'.format(thread_name)) # Create the thread and add the thread and the worker to the parent thread = QtCore.QThread() - setattr(parent, thread_name, thread) - setattr(parent, worker_name, worker) + main_window.threads[thread_name] = { + 'thread': thread, + 'worker': worker + } # Move the worker into the thread's context worker.moveToThread(thread) # Connect slots and signals @@ -51,5 +66,46 @@ worker.quit.connect(thread.quit) worker.quit.connect(worker.deleteLater) thread.finished.connect(thread.deleteLater) - if auto_start: + thread.finished.connect(make_remove_thread(thread_name)) + if can_start: thread.start() + + +def get_thread_worker(thread_name): + """ + Get the worker by the thread name + + :param str thread_name: The name of the thread + :returns ThreadWorker: The worker for this thread name + """ + return Registry().get('main_window').threads.get(thread_name) + + +def is_thread_finished(thread_name): + """ + Check if a thread is finished running. + + :param str thread_name: The name of the thread + :returns bool: True if the thread is finished, False if it is still running + """ + main_window = Registry().get('main_window') + return thread_name not in main_window.threads or main_window.threads[thread_name]['thread'].isFinished() + + +def make_remove_thread(thread_name): + """ + Create a function to remove the thread once the thread is finished. + + :param str thread_name: The name of the thread which should be removed from the thread registry. + :returns function: A function which will remove the thread from the thread registry. + """ + def remove_thread(): + """ + Stop and remove a registered thread + + :param str thread_name: The name of the thread to stop and remove + """ + main_window = Registry().get('main_window') + if thread_name in main_window.threads: + del main_window.threads[thread_name] + return remove_thread === modified file 'openlp/core/ui/firsttimeform.py' --- openlp/core/ui/firsttimeform.py 2017-12-29 09:15:48 +0000 +++ openlp/core/ui/firsttimeform.py 2018-01-07 05:37:57 +0000 @@ -23,8 +23,6 @@ This module contains the first time wizard. """ import logging -import os -import socket import time import urllib.error import urllib.parse @@ -36,7 +34,7 @@ from openlp.core.common import clean_button_text, trace_error_handler from openlp.core.common.applocation import AppLocation -from openlp.core.common.httputils import get_web_page, get_url_file_size, url_get_file, CONNECTION_TIMEOUT +from openlp.core.common.httputils import get_web_page, get_url_file_size, download_file from openlp.core.common.i18n import translate from openlp.core.common.mixins import RegistryProperties from openlp.core.common.path import Path, create_paths @@ -44,46 +42,47 @@ from openlp.core.common.settings import Settings from openlp.core.lib import PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box -from .firsttimewizard import UiFirstTimeWizard, FirstTimePage +from openlp.core.threading import ThreadWorker, run_thread, get_thread_worker, is_thread_finished +from openlp.core.ui.firsttimewizard import UiFirstTimeWizard, FirstTimePage log = logging.getLogger(__name__) -class ThemeScreenshotWorker(QtCore.QObject): +class ThemeScreenshotWorker(ThreadWorker): """ This thread downloads a theme's screenshot """ screenshot_downloaded = QtCore.pyqtSignal(str, str, str) - finished = QtCore.pyqtSignal() def __init__(self, themes_url, title, filename, sha256, screenshot): """ Set up the worker object """ - self.was_download_cancelled = False + self.was_cancelled = False self.themes_url = themes_url self.title = title self.filename = filename self.sha256 = sha256 self.screenshot = screenshot - socket.setdefaulttimeout(CONNECTION_TIMEOUT) - super(ThemeScreenshotWorker, self).__init__() + super().__init__() - def run(self): - """ - Overridden method to run the thread. - """ - if self.was_download_cancelled: + def start(self): + """ + Run the worker + """ + if self.was_cancelled: return try: - urllib.request.urlretrieve('{host}{name}'.format(host=self.themes_url, name=self.screenshot), - os.path.join(gettempdir(), 'openlp', self.screenshot)) - # Signal that the screenshot has been downloaded - self.screenshot_downloaded.emit(self.title, self.filename, self.sha256) - except: + download_path = Path(gettempdir()) / 'openlp' / self.screenshot + is_success = download_file(self, '{host}{name}'.format(host=self.themes_url, name=self.screenshot), + download_path) + if is_success and not self.was_cancelled: + # Signal that the screenshot has been downloaded + self.screenshot_downloaded.emit(self.title, self.filename, self.sha256) + except: # noqa log.exception('Unable to download screenshot') finally: - self.finished.emit() + self.quit.emit() @QtCore.pyqtSlot(bool) def set_download_canceled(self, toggle): @@ -145,12 +144,13 @@ return FirstTimePage.Progress elif self.currentId() == FirstTimePage.Themes: self.application.set_busy_cursor() - while not all([thread.isFinished() for thread in self.theme_screenshot_threads]): + while not all([is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]): time.sleep(0.1) self.application.process_events() # Build the screenshot icons, as this can not be done in the thread. self._build_theme_screenshots() self.application.set_normal_cursor() + self.theme_screenshot_threads = [] return FirstTimePage.Defaults else: return self.get_next_page_id() @@ -171,7 +171,6 @@ self.screens = screens self.was_cancelled = False self.theme_screenshot_threads = [] - self.theme_screenshot_workers = [] self.has_run_wizard = False def _download_index(self): @@ -256,14 +255,10 @@ sha256 = self.config.get('theme_{theme}'.format(theme=theme), 'sha256', fallback='') screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot') worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot) - self.theme_screenshot_workers.append(worker) worker.screenshot_downloaded.connect(self.on_screenshot_downloaded) - thread = QtCore.QThread(self) - self.theme_screenshot_threads.append(thread) - thread.started.connect(worker.run) - worker.finished.connect(thread.quit) - worker.moveToThread(thread) - thread.start() + thread_name = 'theme_screenshot_{title}'.format(title=title) + run_thread(worker, thread_name) + self.theme_screenshot_threads.append(thread_name) self.application.process_events() def set_defaults(self): @@ -353,12 +348,14 @@ Process the triggering of the cancel button. """ self.was_cancelled = True - if self.theme_screenshot_workers: - for worker in self.theme_screenshot_workers: - worker.set_download_canceled(True) + if self.theme_screenshot_threads: + for thread_name in self.theme_screenshot_threads: + worker = get_thread_worker(thread_name) + if worker: + worker.set_download_canceled(True) # Was the thread created. if self.theme_screenshot_threads: - while any([thread.isRunning() for thread in self.theme_screenshot_threads]): + while any([not is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]): time.sleep(0.1) self.application.set_normal_cursor() @@ -562,8 +559,8 @@ self._increment_progress_bar(self.downloading.format(name=filename), 0) self.previous_size = 0 destination = songs_destination_path / str(filename) - if not url_get_file(self, '{path}{name}'.format(path=self.songs_url, name=filename), - destination, sha256): + if not download_file(self, '{path}{name}'.format(path=self.songs_url, name=filename), + destination, sha256): missed_files.append('Song: {name}'.format(name=filename)) # Download Bibles bibles_iterator = QtWidgets.QTreeWidgetItemIterator(self.bibles_tree_widget) @@ -573,8 +570,8 @@ bible, sha256 = item.data(0, QtCore.Qt.UserRole) self._increment_progress_bar(self.downloading.format(name=bible), 0) self.previous_size = 0 - if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible), - bibles_destination_path / bible, sha256): + if not download_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible), + bibles_destination_path / bible, sha256): missed_files.append('Bible: {name}'.format(name=bible)) bibles_iterator += 1 # Download themes @@ -584,8 +581,8 @@ theme, sha256 = item.data(QtCore.Qt.UserRole) self._increment_progress_bar(self.downloading.format(name=theme), 0) self.previous_size = 0 - if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), - themes_destination_path / theme, sha256): + if not download_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), + themes_destination_path / theme, sha256): missed_files.append('Theme: {name}'.format(name=theme)) if missed_files: file_list = '' === modified file 'openlp/core/ui/mainwindow.py' --- openlp/core/ui/mainwindow.py 2017-12-29 09:15:48 +0000 +++ openlp/core/ui/mainwindow.py 2018-01-07 05:37:57 +0000 @@ -24,7 +24,6 @@ """ import logging import sys -import time from datetime import datetime from distutils import dir_util from distutils.errors import DistutilsFileError @@ -478,8 +477,7 @@ """ super(MainWindow, self).__init__() Registry().register('main_window', self) - self.version_thread = None - self.version_worker = None + self.threads = {} self.clipboard = self.application.clipboard() self.arguments = ''.join(self.application.args) # Set up settings sections for the main application (not for use by plugins). @@ -501,8 +499,8 @@ Settings().set_up_default_values() self.about_form = AboutForm(self) MediaController() - websockets.WebSocketServer() - server.HttpServer() + self.ws_server = websockets.WebSocketServer() + self.http_server = server.HttpServer(self) SettingsForm(self) self.formatting_tag_form = FormattingTagForm(self) self.shortcut_form = ShortcutListForm(self) @@ -549,6 +547,41 @@ # Reset the cursor self.application.set_normal_cursor() + def _wait_for_threads(self): + """ + Wait for the threads + """ + # Sometimes the threads haven't finished, let's wait for them + wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self) + wait_dialog.setWindowModality(QtCore.Qt.WindowModal) + wait_dialog.setAutoClose(False) + wait_dialog.setCancelButton(None) + wait_dialog.show() + for thread_name in self.threads.keys(): + log.debug('Waiting for thread %s', thread_name) + self.application.processEvents() + thread = self.threads[thread_name]['thread'] + worker = self.threads[thread_name]['worker'] + try: + if worker and hasattr(worker, 'stop'): + # If the worker has a stop method, run it + worker.stop() + if thread and thread.isRunning(): + # If the thread is running, let's wait 5 seconds for it + retry = 0 + while thread.isRunning() and retry < 50: + # Make the GUI responsive while we wait + self.application.processEvents() + thread.wait(100) + retry += 1 + if thread.isRunning(): + # If the thread is still running after 5 seconds, kill it + thread.terminate() + except RuntimeError: + # Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object + pass + wait_dialog.close() + def bootstrap_post_set_up(self): """ process the bootstrap post setup request @@ -695,7 +728,7 @@ # Update the theme widget self.theme_manager_contents.load_themes() # Check if any Bibles downloaded. If there are, they will be processed. - Registry().execute('bibles_load_list', True) + Registry().execute('bibles_load_list') self.application.set_normal_cursor() def is_display_blank(self): @@ -1000,39 +1033,14 @@ if not self.application.is_event_loop_active: event.ignore() return - # Sometimes the version thread hasn't finished, let's wait for it - try: - if self.version_thread and self.version_thread.isRunning(): - wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self) - wait_dialog.setWindowModality(QtCore.Qt.WindowModal) - wait_dialog.setAutoClose(False) - wait_dialog.setCancelButton(None) - wait_dialog.show() - retry = 0 - while self.version_thread.isRunning() and retry < 50: - self.application.processEvents() - self.version_thread.wait(100) - retry += 1 - if self.version_thread.isRunning(): - self.version_thread.terminate() - wait_dialog.close() - except RuntimeError: - # Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object - pass - # If we just did a settings import, close without saving changes. - if self.settings_imported: - self.clean_up(False) - event.accept() if self.service_manager_contents.is_modified(): ret = self.service_manager_contents.save_modified_service() if ret == QtWidgets.QMessageBox.Save: if self.service_manager_contents.decide_save_method(): - self.clean_up() event.accept() else: event.ignore() elif ret == QtWidgets.QMessageBox.Discard: - self.clean_up() event.accept() else: event.ignore() @@ -1048,13 +1056,16 @@ close_button.setText(translate('OpenLP.MainWindow', '&Exit OpenLP')) msg_box.setDefaultButton(QtWidgets.QMessageBox.Close) if msg_box.exec() == QtWidgets.QMessageBox.Close: - self.clean_up() event.accept() else: event.ignore() else: - self.clean_up() event.accept() + if event.isAccepted(): + # Wait for all the threads to complete + self._wait_for_threads() + # If we just did a settings import, close without saving changes. + self.clean_up(save_settings=not self.settings_imported) def clean_up(self, save_settings=True): """ @@ -1063,8 +1074,8 @@ :param save_settings: Switch to prevent saving settings. Defaults to **True**. """ self.image_manager.stop_manager = True - while self.image_manager.image_thread.isRunning(): - time.sleep(0.1) + # while self.image_manager.image_thread.isRunning(): + # time.sleep(0.1) if save_settings: if Settings().value('advanced/save current plugin'): Settings().setValue('advanced/current media plugin', self.media_tool_box.currentIndex()) === modified file 'openlp/core/ui/media/systemplayer.py' --- openlp/core/ui/media/systemplayer.py 2017-12-29 09:15:48 +0000 +++ openlp/core/ui/media/systemplayer.py 2018-01-07 05:37:57 +0000 @@ -31,6 +31,7 @@ from openlp.core.common.i18n import translate from openlp.core.ui.media import MediaState from openlp.core.ui.media.mediaplayer import MediaPlayer +from openlp.core.threading import ThreadWorker, run_thread, is_thread_finished log = logging.getLogger(__name__) @@ -293,39 +294,38 @@ :param path: Path to file to be checked :return: True if file can be played otherwise False """ - thread = QtCore.QThread() check_media_worker = CheckMediaWorker(path) check_media_worker.setVolume(0) - check_media_worker.moveToThread(thread) - check_media_worker.finished.connect(thread.quit) - thread.started.connect(check_media_worker.play) - thread.start() - while thread.isRunning(): + run_thread(check_media_worker, 'check_media') + while not is_thread_finished('check_media'): self.application.processEvents() return check_media_worker.result -class CheckMediaWorker(QtMultimedia.QMediaPlayer): +class CheckMediaWorker(QtMultimedia.QMediaPlayer, ThreadWorker): """ Class used to check if a media file is playable """ - finished = QtCore.pyqtSignal() - def __init__(self, path): super(CheckMediaWorker, self).__init__(None, QtMultimedia.QMediaPlayer.VideoSurface) + self.path = path + + def start(self): + """ + Start the thread worker + """ self.result = None - self.error.connect(functools.partial(self.signals, 'error')) self.mediaStatusChanged.connect(functools.partial(self.signals, 'media')) - - self.setMedia(QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile(path))) + self.setMedia(QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile(self.path))) + self.play() def signals(self, origin, status): if origin == 'media' and status == self.BufferedMedia: self.result = True self.stop() - self.finished.emit() + self.quit.emit() elif origin == 'error' and status != self.NoError: self.result = False self.stop() - self.finished.emit() + self.quit.emit() === modified file 'openlp/core/version.py' --- openlp/core/version.py 2018-01-02 21:00:54 +0000 +++ openlp/core/version.py 2018-01-07 05:37:57 +0000 @@ -35,7 +35,7 @@ from openlp.core.common.applocation import AppLocation from openlp.core.common.settings import Settings -from openlp.core.threading import run_thread +from openlp.core.threading import ThreadWorker, run_thread log = logging.getLogger(__name__) @@ -44,14 +44,13 @@ CONNECTION_RETRIES = 2 -class VersionWorker(QtCore.QObject): +class VersionWorker(ThreadWorker): """ A worker class to fetch the version of OpenLP from the website. This is run from within a thread so that it doesn't affect the loading time of OpenLP. """ new_version = QtCore.pyqtSignal(dict) no_internet = QtCore.pyqtSignal() - quit = QtCore.pyqtSignal() def __init__(self, last_check_date, current_version): """ @@ -110,22 +109,22 @@ Settings().setValue('core/last version test', date.today().strftime('%Y-%m-%d')) -def check_for_update(parent): +def check_for_update(main_window): """ Run a thread to download and check the version of OpenLP - :param MainWindow parent: The parent object for the thread. Usually the OpenLP main window. + :param MainWindow main_window: The OpenLP main window. """ last_check_date = Settings().value('core/last version test') if date.today().strftime('%Y-%m-%d') <= last_check_date: log.debug('Version check skipped, last checked today') return worker = VersionWorker(last_check_date, get_version()) - worker.new_version.connect(parent.on_new_version) + worker.new_version.connect(main_window.on_new_version) worker.quit.connect(update_check_date) # TODO: Use this to figure out if there's an Internet connection? # worker.no_internet.connect(parent.on_no_internet) - run_thread(parent, worker, 'version') + run_thread(worker, 'version') def get_version(): === modified file 'openlp/plugins/songs/forms/songselectform.py' --- openlp/plugins/songs/forms/songselectform.py 2017-12-29 09:15:48 +0000 +++ openlp/plugins/songs/forms/songselectform.py 2018-01-07 05:37:57 +0000 @@ -27,24 +27,23 @@ from PyQt5 import QtCore, QtWidgets -from openlp.core.common import is_win from openlp.core.common.i18n import translate -from openlp.core.common.registry import Registry +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings +from openlp.core.threading import ThreadWorker, run_thread from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog from openlp.plugins.songs.lib.songselect import SongSelectImport log = logging.getLogger(__name__) -class SearchWorker(QtCore.QObject): +class SearchWorker(ThreadWorker): """ Run the actual SongSelect search, and notify the GUI when we find each song. """ show_info = QtCore.pyqtSignal(str, str) found_song = QtCore.pyqtSignal(dict) finished = QtCore.pyqtSignal() - quit = QtCore.pyqtSignal() def __init__(self, importer, search_text): super().__init__() @@ -74,7 +73,7 @@ self.found_song.emit(song) -class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog): +class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties): """ The :class:`SongSelectForm` class is the SongSelect dialog. """ @@ -90,8 +89,6 @@ """ Initialise the SongSelectForm """ - self.thread = None - self.worker = None self.song_count = 0 self.song = None self.set_progress_visible(False) @@ -311,17 +308,11 @@ search_history = self.search_combobox.getItems() Settings().setValue(self.plugin.settings_section + '/songselect searches', '|'.join(search_history)) # Create thread and run search - self.thread = QtCore.QThread() - self.worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText()) - self.worker.moveToThread(self.thread) - self.thread.started.connect(self.worker.start) - self.worker.show_info.connect(self.on_search_show_info) - self.worker.found_song.connect(self.on_search_found_song) - self.worker.finished.connect(self.on_search_finished) - self.worker.quit.connect(self.thread.quit) - self.worker.quit.connect(self.worker.deleteLater) - self.thread.finished.connect(self.thread.deleteLater) - self.thread.start() + worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText()) + worker.show_info.connect(self.on_search_show_info) + worker.found_song.connect(self.on_search_found_song) + worker.finished.connect(self.on_search_finished) + run_thread(worker, 'songselect') def on_stop_button_clicked(self): """ @@ -408,16 +399,3 @@ """ self.search_progress_bar.setVisible(is_visible) self.stop_button.setVisible(is_visible) - - @property - def application(self): - """ - Adds the openlp to the class dynamically. - Windows needs to access the application in a dynamic manner. - """ - if is_win(): - return Registry().get('application') - else: - if not hasattr(self, '_application'): - self._application = Registry().get('application') - return self._application === modified file 'tests/functional/openlp_core/api/http/test_http.py' --- tests/functional/openlp_core/api/http/test_http.py 2017-12-29 09:15:48 +0000 +++ tests/functional/openlp_core/api/http/test_http.py 2018-01-07 05:37:57 +0000 @@ -42,8 +42,23 @@ Registry().register('service_list', MagicMock()) @patch('openlp.core.api.http.server.HttpWorker') - @patch('openlp.core.api.http.server.QtCore.QThread') - def test_server_start(self, mock_qthread, mock_thread): + @patch('openlp.core.api.http.server.run_thread') + def test_server_start(self, mocked_run_thread, MockHttpWorker): + """ + Test the starting of the Waitress Server with the disable flag set off + """ + # GIVEN: A new httpserver + # WHEN: I start the server + Registry().set_flag('no_web_server', False) + HttpServer() + + # THEN: the api environment should have been created + assert mocked_run_thread.call_count == 1, 'The qthread should have been called once' + assert MockHttpWorker.call_count == 1, 'The http thread should have been called once' + + @patch('openlp.core.api.http.server.HttpWorker') + @patch('openlp.core.api.http.server.run_thread') + def test_server_start_not_required(self, mocked_run_thread, MockHttpWorker): """ Test the starting of the Waitress Server with the disable flag set off """ @@ -53,20 +68,5 @@ HttpServer() # THEN: the api environment should have been created - assert mock_qthread.call_count == 1, 'The qthread should have been called once' - assert mock_thread.call_count == 1, 'The http thread should have been called once' - - @patch('openlp.core.api.http.server.HttpWorker') - @patch('openlp.core.api.http.server.QtCore.QThread') - def test_server_start_not_required(self, mock_qthread, mock_thread): - """ - Test the starting of the Waitress Server with the disable flag set off - """ - # GIVEN: A new httpserver - # WHEN: I start the server - Registry().set_flag('no_web_server', False) - HttpServer() - - # THEN: the api environment should have been created - assert mock_qthread.call_count == 0, 'The qthread should not have have been called' - assert mock_thread.call_count == 0, 'The http thread should not have been called' + assert mocked_run_thread.call_count == 0, 'The qthread should not have have been called' + assert MockHttpWorker.call_count == 0, 'The http thread should not have been called' === modified file 'tests/functional/openlp_core/api/test_websockets.py' --- tests/functional/openlp_core/api/test_websockets.py 2017-12-29 09:15:48 +0000 +++ tests/functional/openlp_core/api/test_websockets.py 2018-01-07 05:37:57 +0000 @@ -63,34 +63,34 @@ self.destroy_settings() @patch('openlp.core.api.websockets.WebSocketWorker') - @patch('openlp.core.api.websockets.QtCore.QThread') - def test_serverstart(self, mock_qthread, mock_worker): + @patch('openlp.core.api.websockets.run_thread') + def test_serverstart(self, mocked_run_thread, MockWebSocketWorker): """ Test the starting of the WebSockets Server with the disabled flag set on """ # GIVEN: A new httpserver # WHEN: I start the server - Registry().set_flag('no_web_server', True) + Registry().set_flag('no_web_server', False) WebSocketServer() # THEN: the api environment should have been created - assert mock_qthread.call_count == 1, 'The qthread should have been called once' - assert mock_worker.call_count == 1, 'The http thread should have been called once' + assert mocked_run_thread.call_count == 1, 'The qthread should have been called once' + assert MockWebSocketWorker.call_count == 1, 'The http thread should have been called once' @patch('openlp.core.api.websockets.WebSocketWorker') - @patch('openlp.core.api.websockets.QtCore.QThread') - def test_serverstart_not_required(self, mock_qthread, mock_worker): + @patch('openlp.core.api.websockets.run_thread') + def test_serverstart_not_required(self, mocked_run_thread, MockWebSocketWorker): """ Test the starting of the WebSockets Server with the disabled flag set off """ # GIVEN: A new httpserver and the server is not required # WHEN: I start the server - Registry().set_flag('no_web_server', False) + Registry().set_flag('no_web_server', True) WebSocketServer() # THEN: the api environment should have been created - assert mock_qthread.call_count == 0, 'The qthread should not have been called' - assert mock_worker.call_count == 0, 'The http thread should not have been called' + assert mocked_run_thread.call_count == 0, 'The qthread should not have been called' + assert MockWebSocketWorker.call_count == 0, 'The http thread should not have been called' def test_main_poll(self): """ === modified file 'tests/functional/openlp_core/common/test_httputils.py' --- tests/functional/openlp_core/common/test_httputils.py 2017-12-29 09:15:48 +0000 +++ tests/functional/openlp_core/common/test_httputils.py 2018-01-07 05:37:57 +0000 @@ -27,7 +27,7 @@ from unittest import TestCase from unittest.mock import MagicMock, patch -from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file +from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, download_file from openlp.core.common.path import Path from tests.helpers.testmixin import TestMixin @@ -235,7 +235,7 @@ mocked_requests.get.side_effect = OSError # WHEN: Attempt to retrieve a file - url_get_file(MagicMock(), url='http://localhost/test', file_path=Path(self.tempfile)) + download_file(MagicMock(), url='http://localhost/test', file_path=Path(self.tempfile)) # THEN: socket.timeout should have been caught # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files === modified file 'tests/functional/openlp_core/lib/test_image_manager.py' --- tests/functional/openlp_core/lib/test_image_manager.py 2017-12-28 08:22:55 +0000 +++ tests/functional/openlp_core/lib/test_image_manager.py 2018-01-07 05:37:57 +0000 @@ -25,20 +25,113 @@ import os import time from threading import Lock -from unittest import TestCase -from unittest.mock import patch +from unittest import TestCase, skip +from unittest.mock import MagicMock, patch from PyQt5 import QtGui from openlp.core.common.registry import Registry from openlp.core.display.screens import ScreenList -from openlp.core.lib.imagemanager import ImageManager, Priority +from openlp.core.lib.imagemanager import ImageWorker, ImageManager, Priority, PriorityQueue from tests.helpers.testmixin import TestMixin from tests.utils.constants import RESOURCE_PATH TEST_PATH = str(RESOURCE_PATH) +class TestImageWorker(TestCase, TestMixin): + """ + Test all the methods in the ImageWorker class + """ + def test_init(self): + """ + Test the constructor of the ImageWorker + """ + # GIVEN: An ImageWorker class and a mocked ImageManager + mocked_image_manager = MagicMock() + + # WHEN: Creating the ImageWorker + worker = ImageWorker(mocked_image_manager) + + # THEN: The image_manager attribute should be set correctly + assert worker.image_manager is mocked_image_manager, \ + 'worker.image_manager should have been the mocked_image_manager' + + @patch('openlp.core.lib.imagemanager.ThreadWorker.quit') + def test_start(self, mocked_quit): + """ + Test that the start() method of the image worker calls the process method and then emits quit. + """ + # GIVEN: A mocked image_manager and a new image worker + mocked_image_manager = MagicMock() + worker = ImageWorker(mocked_image_manager) + + # WHEN: start() is called + worker.start() + + # THEN: process() should have been called and quit should have been emitted + mocked_image_manager.process.assert_called_once_with() + mocked_quit.emit.assert_called_once_with() + + def test_stop(self): + """ + Test that the stop method does the right thing + """ + # GIVEN: A mocked image_manager and a worker + mocked_image_manager = MagicMock() + worker = ImageWorker(mocked_image_manager) + + # WHEN: The stop() method is called + worker.stop() + + # THEN: The stop_manager attrivute should have been set to True + assert mocked_image_manager.stop_manager is True, 'mocked_image_manager.stop_manager should have been True' + + +class TestPriorityQueue(TestCase, TestMixin): + """ + Test the PriorityQueue class + """ + @patch('openlp.core.lib.imagemanager.PriorityQueue.remove') + @patch('openlp.core.lib.imagemanager.PriorityQueue.put') + def test_modify_priority(self, mocked_put, mocked_remove): + """ + Test the modify_priority() method of PriorityQueue + """ + # GIVEN: An instance of a PriorityQueue and a mocked image + mocked_image = MagicMock() + mocked_image.priority = Priority.Normal + mocked_image.secondary_priority = Priority.Low + queue = PriorityQueue() + + # WHEN: modify_priority is called with a mocked image and a new priority + queue.modify_priority(mocked_image, Priority.High) + + # THEN: The remove() method should have been called, image priority updated and put() called + mocked_remove.assert_called_once_with(mocked_image) + assert mocked_image.priority == Priority.High, 'The priority should have been Priority.High' + mocked_put.assert_called_once_with((Priority.High, Priority.Low, mocked_image)) + + def test_remove(self): + """ + Test the remove() method of PriorityQueue + """ + # GIVEN: A PriorityQueue instance with a mocked image and queue + mocked_image = MagicMock() + mocked_image.priority = Priority.High + mocked_image.secondary_priority = Priority.Normal + queue = PriorityQueue() + + # WHEN: An image is removed + with patch.object(queue, 'queue') as mocked_queue: + mocked_queue.__contains__.return_value = True + queue.remove(mocked_image) + + # THEN: The mocked queue.remove() method should have been called + mocked_queue.remove.assert_called_once_with((Priority.High, Priority.Normal, mocked_image)) + + +@skip('Probably not going to use ImageManager in WebEngine/Reveal.js') class TestImageManager(TestCase, TestMixin): def setUp(self): @@ -57,10 +150,10 @@ Delete all the C++ objects at the end so that we don't have a segfault """ self.image_manager.stop_manager = True - self.image_manager.image_thread.wait() del self.app - def test_basic_image_manager(self): + @patch('openlp.core.lib.imagemanager.run_thread') + def test_basic_image_manager(self, mocked_run_thread): """ Test the Image Manager setup basic functionality """ @@ -86,7 +179,8 @@ self.image_manager.get_image(TEST_PATH, 'church1.jpg') assert context.exception is not '', 'KeyError exception should have been thrown for missing image' - def test_different_dimension_image(self): + @patch('openlp.core.lib.imagemanager.run_thread') + def test_different_dimension_image(self, mocked_run_thread): """ Test the Image Manager with dimensions """ @@ -118,57 +212,58 @@ self.image_manager.get_image(full_path, 'church.jpg', 120, 120) assert context.exception is not '', 'KeyError exception should have been thrown for missing dimension' - def test_process_cache(self): + @patch('openlp.core.lib.imagemanager.resize_image') + @patch('openlp.core.lib.imagemanager.image_to_byte') + @patch('openlp.core.lib.imagemanager.run_thread') + def test_process_cache(self, mocked_run_thread, mocked_image_to_byte, mocked_resize_image): """ Test the process_cache method """ - with patch('openlp.core.lib.imagemanager.resize_image') as mocked_resize_image, \ - patch('openlp.core.lib.imagemanager.image_to_byte') as mocked_image_to_byte: - # GIVEN: Mocked functions - mocked_resize_image.side_effect = self.mocked_resize_image - mocked_image_to_byte.side_effect = self.mocked_image_to_byte - image1 = 'church.jpg' - image2 = 'church2.jpg' - image3 = 'church3.jpg' - image4 = 'church4.jpg' - - # WHEN: Add the images. Then get the lock (=queue can not be processed). - self.lock.acquire() - self.image_manager.add_image(TEST_PATH, image1, None) - self.image_manager.add_image(TEST_PATH, image2, None) - - # THEN: All images have been added to the queue, and only the first image is not be in the list anymore, but - # is being processed (see mocked methods/functions). - # Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the # - # priority is adjusted to Priority.Lowest). - assert self.get_image_priority(image1) == Priority.Normal, "image1's priority should be 'Priority.Normal'" - assert self.get_image_priority(image2) == Priority.Normal, "image2's priority should be 'Priority.Normal'" - - # WHEN: Add more images. - self.image_manager.add_image(TEST_PATH, image3, None) - self.image_manager.add_image(TEST_PATH, image4, None) - # Allow the queue to process. - self.lock.release() - # Request some "data". - self.image_manager.get_image_bytes(TEST_PATH, image4) - self.image_manager.get_image(TEST_PATH, image3) - # Now the mocked methods/functions do not have to sleep anymore. - self.sleep_time = 0 - # Wait for the queue to finish. - while not self.image_manager._conversion_queue.empty(): - time.sleep(0.1) - # Because empty() is not reliable, wait a litte; just to make sure. + # GIVEN: Mocked functions + mocked_resize_image.side_effect = self.mocked_resize_image + mocked_image_to_byte.side_effect = self.mocked_image_to_byte + image1 = 'church.jpg' + image2 = 'church2.jpg' + image3 = 'church3.jpg' + image4 = 'church4.jpg' + + # WHEN: Add the images. Then get the lock (=queue can not be processed). + self.lock.acquire() + self.image_manager.add_image(TEST_PATH, image1, None) + self.image_manager.add_image(TEST_PATH, image2, None) + + # THEN: All images have been added to the queue, and only the first image is not be in the list anymore, but + # is being processed (see mocked methods/functions). + # Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the # + # priority is adjusted to Priority.Lowest). + assert self.get_image_priority(image1) == Priority.Normal, "image1's priority should be 'Priority.Normal'" + assert self.get_image_priority(image2) == Priority.Normal, "image2's priority should be 'Priority.Normal'" + + # WHEN: Add more images. + self.image_manager.add_image(TEST_PATH, image3, None) + self.image_manager.add_image(TEST_PATH, image4, None) + # Allow the queue to process. + self.lock.release() + # Request some "data". + self.image_manager.get_image_bytes(TEST_PATH, image4) + self.image_manager.get_image(TEST_PATH, image3) + # Now the mocked methods/functions do not have to sleep anymore. + self.sleep_time = 0 + # Wait for the queue to finish. + while not self.image_manager._conversion_queue.empty(): time.sleep(0.1) - # THEN: The images' priority reflect how they were processed. - assert self.image_manager._conversion_queue.qsize() == 0, "The queue should be empty." - assert self.get_image_priority(image1) == Priority.Lowest, \ - "The image should have not been requested (=Lowest)" - assert self.get_image_priority(image2) == Priority.Lowest, \ - "The image should have not been requested (=Lowest)" - assert self.get_image_priority(image3) == Priority.Low, \ - "Only the QImage should have been requested (=Low)." - assert self.get_image_priority(image4) == Priority.Urgent, \ - "The image bytes should have been requested (=Urgent)." + # Because empty() is not reliable, wait a litte; just to make sure. + time.sleep(0.1) + # THEN: The images' priority reflect how they were processed. + assert self.image_manager._conversion_queue.qsize() == 0, "The queue should be empty." + assert self.get_image_priority(image1) == Priority.Lowest, \ + "The image should have not been requested (=Lowest)" + assert self.get_image_priority(image2) == Priority.Lowest, \ + "The image should have not been requested (=Lowest)" + assert self.get_image_priority(image3) == Priority.Low, \ + "Only the QImage should have been requested (=Low)." + assert self.get_image_priority(image4) == Priority.Urgent, \ + "The image bytes should have been requested (=Urgent)." def get_image_priority(self, image): """ === modified file 'tests/functional/openlp_core/test_app.py' --- tests/functional/openlp_core/test_app.py 2017-12-29 09:15:48 +0000 +++ tests/functional/openlp_core/test_app.py 2018-01-07 05:37:57 +0000 @@ -36,14 +36,15 @@ """ # GIVEN: a a set of system arguments. sys.argv[1:] = [] + # WHEN: We we parse them to expand to options - args = parse_options(None) + args = parse_options() + # THEN: the following fields will have been extracted. assert args.dev_version is False, 'The dev_version flag should be False' assert args.loglevel == 'warning', 'The log level should be set to warning' assert args.no_error_form is False, 'The no_error_form should be set to False' assert args.portable is False, 'The portable flag should be set to false' - assert args.style is None, 'There are no style flags to be processed' assert args.rargs == [], 'The service file should be blank' @@ -53,14 +54,15 @@ """ # GIVEN: a a set of system arguments. sys.argv[1:] = ['-l debug'] + # WHEN: We we parse them to expand to options - args = parse_options(None) + args = parse_options() + # THEN: the following fields will have been extracted. assert args.dev_version is False, 'The dev_version flag should be False' assert args.loglevel == ' debug', 'The log level should be set to debug' assert args.no_error_form is False, 'The no_error_form should be set to False' assert args.portable is False, 'The portable flag should be set to false' - assert args.style is None, 'There are no style flags to be processed' assert args.rargs == [], 'The service file should be blank' @@ -70,14 +72,15 @@ """ # GIVEN: a a set of system arguments. sys.argv[1:] = ['--portable'] + # WHEN: We we parse them to expand to options - args = parse_options(None) + args = parse_options() + # THEN: the following fields will have been extracted. assert args.dev_version is False, 'The dev_version flag should be False' assert args.loglevel == 'warning', 'The log level should be set to warning' assert args.no_error_form is False, 'The no_error_form should be set to False' assert args.portable is True, 'The portable flag should be set to true' - assert args.style is None, 'There are no style flags to be processed' assert args.rargs == [], 'The service file should be blank' @@ -87,14 +90,15 @@ """ # GIVEN: a a set of system arguments. sys.argv[1:] = ['-l debug', '-d'] + # WHEN: We we parse them to expand to options - args = parse_options(None) + args = parse_options() + # THEN: the following fields will have been extracted. assert args.dev_version is True, 'The dev_version flag should be True' assert args.loglevel == ' debug', 'The log level should be set to debug' assert args.no_error_form is False, 'The no_error_form should be set to False' assert args.portable is False, 'The portable flag should be set to false' - assert args.style is None, 'There are no style flags to be processed' assert args.rargs == [], 'The service file should be blank' @@ -104,14 +108,15 @@ """ # GIVEN: a a set of system arguments. sys.argv[1:] = ['dummy_temp'] + # WHEN: We we parse them to expand to options - args = parse_options(None) + args = parse_options() + # THEN: the following fields will have been extracted. assert args.dev_version is False, 'The dev_version flag should be False' assert args.loglevel == 'warning', 'The log level should be set to warning' assert args.no_error_form is False, 'The no_error_form should be set to False' assert args.portable is False, 'The portable flag should be set to false' - assert args.style is None, 'There are no style flags to be processed' assert args.rargs == 'dummy_temp', 'The service file should not be blank' @@ -121,14 +126,15 @@ """ # GIVEN: a a set of system arguments. sys.argv[1:] = ['-l debug', 'dummy_temp'] + # WHEN: We we parse them to expand to options - args = parse_options(None) + args = parse_options() + # THEN: the following fields will have been extracted. assert args.dev_version is False, 'The dev_version flag should be False' assert args.loglevel == ' debug', 'The log level should be set to debug' assert args.no_error_form is False, 'The no_error_form should be set to False' assert args.portable is False, 'The portable flag should be set to false' - assert args.style is None, 'There are no style flags to be processed' assert args.rargs == 'dummy_temp', 'The service file should not be blank' === added file 'tests/functional/openlp_core/test_threading.py' --- tests/functional/openlp_core/test_threading.py 1970-01-01 00:00:00 +0000 +++ tests/functional/openlp_core/test_threading.py 2018-01-07 05:37:57 +0000 @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 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 # +############################################################################### +""" +Package to test the openlp.core.threading package. +""" +from unittest.mock import MagicMock, call, patch + +from openlp.core.version import run_thread + + +def test_run_thread_no_name(): + """ + Test that trying to run a thread without a name results in an exception being thrown + """ + # GIVEN: A fake worker + # WHEN: run_thread() is called without a name + try: + run_thread(MagicMock(), '') + assert False, 'A ValueError should have been thrown to prevent blank names' + except ValueError: + # THEN: A ValueError should have been thrown + assert True, 'A ValueError was correctly thrown' + + +@patch('openlp.core.threading.Registry') +def test_run_thread_exists(MockRegistry): + """ + Test that trying to run a thread with a name that already exists will throw a KeyError + """ + # GIVEN: A mocked registry with a main window object + mocked_main_window = MagicMock() + mocked_main_window.threads = {'test_thread': MagicMock()} + MockRegistry.return_value.get.return_value = mocked_main_window + + # WHEN: run_thread() is called + try: + run_thread(MagicMock(), 'test_thread') + assert False, 'A KeyError should have been thrown to show that a thread with this name already exists' + except KeyError: + assert True, 'A KeyError was correctly thrown' + + +@patch('openlp.core.threading.QtCore.QThread') +@patch('openlp.core.threading.Registry') +def test_run_thread(MockRegistry, MockQThread): + """ + Test that running a thread works correctly + """ + # GIVEN: A mocked registry with a main window object + mocked_main_window = MagicMock() + mocked_main_window.threads = {} + MockRegistry.return_value.get.return_value = mocked_main_window + + # WHEN: run_thread() is called + run_thread(MagicMock(), 'test_thread') + + # THEN: The thread should be in the threads list and the correct methods should have been called + assert len(mocked_main_window.threads.keys()) == 1, 'There should be 1 item in the list of threads' + assert list(mocked_main_window.threads.keys()) == ['test_thread'], 'The test_thread item should be in the list' + mocked_worker = mocked_main_window.threads['test_thread']['worker'] + mocked_thread = mocked_main_window.threads['test_thread']['thread'] + mocked_worker.moveToThread.assert_called_once_with(mocked_thread) + mocked_thread.started.connect.assert_called_once_with(mocked_worker.start) + expected_quit_calls = [call(mocked_thread.quit), call(mocked_worker.deleteLater)] + assert mocked_worker.quit.connect.call_args_list == expected_quit_calls, \ + 'The workers quit signal should be connected twice' + assert mocked_thread.finished.connect.call_args_list[0] == call(mocked_thread.deleteLater), \ + 'The threads finished signal should be connected to its deleteLater slot' + assert mocked_thread.finished.connect.call_count == 2, 'The signal should have been connected twice' + mocked_thread.start.assert_called_once_with() === removed file 'tests/functional/openlp_core/ui/media/test_systemplayer.py' --- tests/functional/openlp_core/ui/media/test_systemplayer.py 2017-12-29 09:15:48 +0000 +++ tests/functional/openlp_core/ui/media/test_systemplayer.py 1970-01-01 00:00:00 +0000 @@ -1,549 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2018 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 # -############################################################################### -""" -Package to test the openlp.core.ui.media.systemplayer package. -""" -from unittest import TestCase -from unittest.mock import MagicMock, call, patch - -from PyQt5 import QtCore, QtMultimedia - -from openlp.core.common.registry import Registry -from openlp.core.ui.media import MediaState -from openlp.core.ui.media.systemplayer import SystemPlayer, CheckMediaWorker, ADDITIONAL_EXT - - -class TestSystemPlayer(TestCase): - """ - Test the system media player - """ - @patch('openlp.core.ui.media.systemplayer.mimetypes') - @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer') - def test_constructor(self, MockQMediaPlayer, mocked_mimetypes): - """ - Test the SystemPlayer constructor - """ - # GIVEN: The SystemPlayer class and a mockedQMediaPlayer - mocked_media_player = MagicMock() - mocked_media_player.supportedMimeTypes.return_value = [ - 'application/postscript', - 'audio/aiff', - 'audio/x-aiff', - 'text/html', - 'video/animaflex', - 'video/x-ms-asf' - ] - mocked_mimetypes.guess_all_extensions.side_effect = [ - ['.aiff'], - ['.aiff'], - ['.afl'], - ['.asf'] - ] - MockQMediaPlayer.return_value = mocked_media_player - - # WHEN: An object is created from it - player = SystemPlayer(self) - - # THEN: The correct initial values should be set up - assert 'system' == player.name - assert 'System' == player.original_name - assert '&System' == player.display_name - assert self == player.parent - assert ADDITIONAL_EXT == player.additional_extensions - MockQMediaPlayer.assert_called_once_with(None, QtMultimedia.QMediaPlayer.VideoSurface) - mocked_mimetypes.init.assert_called_once_with() - mocked_media_player.service.assert_called_once_with() - mocked_media_player.supportedMimeTypes.assert_called_once_with() - assert ['*.aiff'] == player.audio_extensions_list - assert ['*.afl', '*.asf'] == player.video_extensions_list - - @patch('openlp.core.ui.media.systemplayer.QtMultimediaWidgets.QVideoWidget') - @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer') - def test_setup(self, MockQMediaPlayer, MockQVideoWidget): - """ - Test the setup() method of SystemPlayer - """ - # GIVEN: A SystemPlayer instance and a mock display - player = SystemPlayer(self) - mocked_display = MagicMock() - mocked_display.size.return_value = [1, 2, 3, 4] - mocked_video_widget = MagicMock() - mocked_media_player = MagicMock() - MockQVideoWidget.return_value = mocked_video_widget - MockQMediaPlayer.return_value = mocked_media_player - - # WHEN: setup() is run - player.setup(mocked_display) - - # THEN: The player should have a display widget - MockQVideoWidget.assert_called_once_with(mocked_display) - assert mocked_video_widget == mocked_display.video_widget - mocked_display.size.assert_called_once_with() - mocked_video_widget.resize.assert_called_once_with([1, 2, 3, 4]) - MockQMediaPlayer.assert_called_with(mocked_display) - assert mocked_media_player == mocked_display.media_player - mocked_media_player.setVideoOutput.assert_called_once_with(mocked_video_widget) - mocked_video_widget.raise_.assert_called_once_with() - mocked_video_widget.hide.assert_called_once_with() - assert player.has_own_widget is True - - def test_disconnect_slots(self): - """ - Test that we the disconnect slots method catches the TypeError - """ - # GIVEN: A SystemPlayer class and a signal that throws a TypeError - player = SystemPlayer(self) - mocked_signal = MagicMock() - mocked_signal.disconnect.side_effect = \ - TypeError('disconnect() failed between \'durationChanged\' and all its connections') - - # WHEN: disconnect_slots() is called - player.disconnect_slots(mocked_signal) - - # THEN: disconnect should have been called and the exception should have been ignored - mocked_signal.disconnect.assert_called_once_with() - - def test_check_available(self): - """ - Test the check_available() method on SystemPlayer - """ - # GIVEN: A SystemPlayer instance - player = SystemPlayer(self) - - # WHEN: check_available is run - result = player.check_available() - - # THEN: it should be available - assert result is True - - def test_load_valid_media(self): - """ - Test the load() method of SystemPlayer with a valid media file - """ - # GIVEN: A SystemPlayer instance and a mocked display - player = SystemPlayer(self) - mocked_display = MagicMock() - mocked_display.controller.media_info.volume = 1 - mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file' - - # WHEN: The load() method is run - with patch.object(player, 'check_media') as mocked_check_media, \ - patch.object(player, 'volume') as mocked_volume: - mocked_check_media.return_value = True - result = player.load(mocked_display) - - # THEN: the file is sent to the video widget - mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with() - mocked_check_media.assert_called_once_with('/path/to/file') - mocked_display.media_player.setMedia.assert_called_once_with( - QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile('/path/to/file'))) - mocked_volume.assert_called_once_with(mocked_display, 1) - assert result is True - - def test_load_invalid_media(self): - """ - Test the load() method of SystemPlayer with an invalid media file - """ - # GIVEN: A SystemPlayer instance and a mocked display - player = SystemPlayer(self) - mocked_display = MagicMock() - mocked_display.controller.media_info.volume = 1 - mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file' - - # WHEN: The load() method is run - with patch.object(player, 'check_media') as mocked_check_media, \ - patch.object(player, 'volume') as mocked_volume: - mocked_check_media.return_value = False - result = player.load(mocked_display) - - # THEN: stuff - mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with() - mocked_check_media.assert_called_once_with('/path/to/file') - assert result is False - - def test_resize(self): - """ - Test the resize() method of the SystemPlayer - """ - # GIVEN: A SystemPlayer instance and a mocked display - player = SystemPlayer(self) - mocked_display = MagicMock() - mocked_display.size.return_value = [1, 2, 3, 4] - - # WHEN: The resize() method is called - player.resize(mocked_display) - - # THEN: The player is resized - mocked_display.size.assert_called_once_with() - mocked_display.video_widget.resize.assert_called_once_with([1, 2, 3, 4]) - - @patch('openlp.core.ui.media.systemplayer.functools') - def test_play_is_live(self, mocked_functools): - """ - Test the play() method of the SystemPlayer on the live display - """ - # GIVEN: A SystemPlayer instance and a mocked display - mocked_functools.partial.return_value = 'function' - player = SystemPlayer(self) - mocked_display = MagicMock() - mocked_display.controller.is_live = True - mocked_display.controller.media_info.start_time = 1 - mocked_display.controller.media_info.volume = 1 - - # WHEN: play() is called - with patch.object(player, 'get_live_state') as mocked_get_live_state, \ - patch.object(player, 'seek') as mocked_seek, \ - patch.object(player, 'volume') as mocked_volume, \ - patch.object(player, 'set_state') as mocked_set_state, \ - patch.object(player, 'disconnect_slots') as mocked_disconnect_slots: - mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PlayingState - result = player.play(mocked_display) - - # THEN: the media file is played - mocked_get_live_state.assert_called_once_with() - mocked_display.media_player.play.assert_called_once_with() - mocked_seek.assert_called_once_with(mocked_display, 1000) - mocked_volume.assert_called_once_with(mocked_display, 1) - mocked_disconnect_slots.assert_called_once_with(mocked_display.media_player.durationChanged) - mocked_display.media_player.durationChanged.connect.assert_called_once_with('function') - mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display) - mocked_display.video_widget.raise_.assert_called_once_with() - assert result is True - - @patch('openlp.core.ui.media.systemplayer.functools') - def test_play_is_preview(self, mocked_functools): - """ - Test the play() method of the SystemPlayer on the preview display - """ - # GIVEN: A SystemPlayer instance and a mocked display - mocked_functools.partial.return_value = 'function' - player = SystemPlayer(self) - mocked_display = MagicMock() - mocked_display.controller.is_live = False - mocked_display.controller.media_info.start_time = 1 - mocked_display.controller.media_info.volume = 1 - - # WHEN: play() is called - with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \ - patch.object(player, 'seek') as mocked_seek, \ - patch.object(player, 'volume') as mocked_volume, \ - patch.object(player, 'set_state') as mocked_set_state: - mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PlayingState - result = player.play(mocked_display) - - # THEN: the media file is played - mocked_get_preview_state.assert_called_once_with() - mocked_display.media_player.play.assert_called_once_with() - mocked_seek.assert_called_once_with(mocked_display, 1000) - mocked_volume.assert_called_once_with(mocked_display, 1) - mocked_display.media_player.durationChanged.connect.assert_called_once_with('function') - mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display) - mocked_display.video_widget.raise_.assert_called_once_with() - assert result is True - - def test_pause_is_live(self): - """ - Test the pause() method of the SystemPlayer on the live display - """ - # GIVEN: A SystemPlayer instance - player = SystemPlayer(self) - mocked_display = MagicMock() - mocked_display.controller.is_live = True - - # WHEN: The pause method is called - with patch.object(player, 'get_live_state') as mocked_get_live_state, \ - patch.object(player, 'set_state') as mocked_set_state: - mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PausedState - player.pause(mocked_display) - - # THEN: The video is paused - mocked_display.media_player.pause.assert_called_once_with() - mocked_get_live_state.assert_called_once_with() - mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display) - - def test_pause_is_preview(self): - """ - Test the pause() method of the SystemPlayer on the preview display - """ - # GIVEN: A SystemPlayer instance - player = SystemPlayer(self) - mocked_display = MagicMock() - mocked_display.controller.is_live = False - - # WHEN: The pause method is called - with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \ - patch.object(player, 'set_state') as mocked_set_state: - mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PausedState - player.pause(mocked_display) - - # THEN: The video is paused - mocked_display.media_player.pause.assert_called_once_with() - mocked_get_preview_state.assert_called_once_with() - mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display) - - def test_stop(self): - """ - Test the stop() method of the SystemPlayer - """ - # GIVEN: A SystemPlayer instance - player = SystemPlayer(self) - mocked_display = MagicMock() - - # WHEN: The stop method is called - with patch.object(player, 'set_visible') as mocked_set_visible, \ - patch.object(player, 'set_state') as mocked_set_state: - player.stop(mocked_display) - - # THEN: The video is stopped - mocked_display.media_player.stop.assert_called_once_with() - mocked_set_visible.assert_called_once_with(mocked_display, False) - mocked_set_state.assert_called_once_with(MediaState.Stopped, mocked_display) - - def test_volume(self): - """ - Test the volume() method of the SystemPlayer - """ - # GIVEN: A SystemPlayer instance - player = SystemPlayer(self) - mocked_display = MagicMock() - mocked_display.has_audio = True - - # WHEN: The stop method is called - player.volume(mocked_display, 2) - - # THEN: The video is stopped - mocked_display.media_player.setVolume.assert_called_once_with(2) - - def test_seek(self): - """ - Test the seek() method of the SystemPlayer - """ - # GIVEN: A SystemPlayer instance - player = SystemPlayer(self) - mocked_display = MagicMock() - - # WHEN: The stop method is called - player.seek(mocked_display, 2) - - # THEN: The video is stopped - mocked_display.media_player.setPosition.assert_called_once_with(2) - - def test_reset(self): - """ - Test the reset() method of the SystemPlayer - """ - # GIVEN: A SystemPlayer instance - player = SystemPlayer(self) - mocked_display = MagicMock() - - # WHEN: reset() is called - with patch.object(player, 'set_state') as mocked_set_state, \ - patch.object(player, 'set_visible') as mocked_set_visible: - player.reset(mocked_display) - - # THEN: The media player is reset - mocked_display.media_player.stop() - mocked_display.media_player.setMedia.assert_called_once_with(QtMultimedia.QMediaContent()) - mocked_set_visible.assert_called_once_with(mocked_display, False) - mocked_display.video_widget.setVisible.assert_called_once_with(False) - mocked_set_state.assert_called_once_with(MediaState.Off, mocked_display) - - def test_set_visible(self): - """ - Test the set_visible() method on the SystemPlayer - """ - # GIVEN: A SystemPlayer instance and a mocked display - player = SystemPlayer(self) - player.has_own_widget = True - mocked_display = MagicMock() - - # WHEN: set_visible() is called - player.set_visible(mocked_display, True) - - # THEN: The widget should be visible - mocked_display.video_widget.setVisible.assert_called_once_with(True) - - def test_set_duration(self): - """ - Test the set_duration() method of the SystemPlayer - """ - # GIVEN: a mocked controller - mocked_controller = MagicMock() - mocked_controller.media_info.length = 5 - - # WHEN: The set_duration() is called. NB: the 10 here is ignored by the code - SystemPlayer.set_duration(mocked_controller, 10) - - # THEN: The maximum length of the slider should be set - mocked_controller.seek_slider.setMaximum.assert_called_once_with(5) - - def test_update_ui(self): - """ - Test the update_ui() method on the SystemPlayer - """ - # GIVEN: A SystemPlayer instance - player = SystemPlayer(self) - player.state = [MediaState.Playing, MediaState.Playing] - mocked_display = MagicMock() - mocked_display.media_player.state.return_value = QtMultimedia.QMediaPlayer.PausedState - mocked_display.controller.media_info.end_time = 1 - mocked_display.media_player.position.return_value = 2 - mocked_display.controller.seek_slider.isSliderDown.return_value = False - - # WHEN: update_ui() is called - with patch.object(player, 'stop') as mocked_stop, \ - patch.object(player, 'set_visible') as mocked_set_visible: - player.update_ui(mocked_display) - - # THEN: The UI is updated - expected_stop_calls = [call(mocked_display)] - expected_position_calls = [call(), call()] - expected_block_signals_calls = [call(True), call(False)] - mocked_display.media_player.state.assert_called_once_with() - assert 1 == mocked_stop.call_count - assert expected_stop_calls == mocked_stop.call_args_list - assert 2 == mocked_display.media_player.position.call_count - assert expected_position_calls == mocked_display.media_player.position.call_args_list - mocked_set_visible.assert_called_once_with(mocked_display, False) - mocked_display.controller.seek_slider.isSliderDown.assert_called_once_with() - assert expected_block_signals_calls == mocked_display.controller.seek_slider.blockSignals.call_args_list - mocked_display.controller.seek_slider.setSliderPosition.assert_called_once_with(2) - - def test_get_media_display_css(self): - """ - Test the get_media_display_css() method of the SystemPlayer - """ - # GIVEN: A SystemPlayer instance - player = SystemPlayer(self) - - # WHEN: get_media_display_css() is called - result = player.get_media_display_css() - - # THEN: The css should be empty - assert '' == result - - @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer') - def test_get_info(self, MockQMediaPlayer): - """ - Test the get_info() method of the SystemPlayer - """ - # GIVEN: A SystemPlayer instance - mocked_media_player = MagicMock() - mocked_media_player.supportedMimeTypes.return_value = [] - MockQMediaPlayer.return_value = mocked_media_player - player = SystemPlayer(self) - - # WHEN: get_info() is called - result = player.get_info() - - # THEN: The info should be correct - expected_info = 'This media player uses your operating system to provide media capabilities.<br/> ' \ - '<strong>Audio</strong><br/>[]<br/><strong>Video</strong><br/>[]<br/>' - assert expected_info == result - - @patch('openlp.core.ui.media.systemplayer.CheckMediaWorker') - @patch('openlp.core.ui.media.systemplayer.QtCore.QThread') - def test_check_media(self, MockQThread, MockCheckMediaWorker): - """ - Test the check_media() method of the SystemPlayer - """ - # GIVEN: A SystemPlayer instance and a mocked thread - valid_file = '/path/to/video.ogv' - mocked_application = MagicMock() - Registry().create() - Registry().register('application', mocked_application) - player = SystemPlayer(self) - mocked_thread = MagicMock() - mocked_thread.isRunning.side_effect = [True, False] - mocked_thread.quit = 'quit' # actually supposed to be a slot, but it's all mocked out anyway - MockQThread.return_value = mocked_thread - mocked_check_media_worker = MagicMock() - mocked_check_media_worker.play = 'play' - mocked_check_media_worker.result = True - MockCheckMediaWorker.return_value = mocked_check_media_worker - - # WHEN: check_media() is called with a valid media file - result = player.check_media(valid_file) - - # THEN: It should return True - MockQThread.assert_called_once_with() - MockCheckMediaWorker.assert_called_once_with(valid_file) - mocked_check_media_worker.setVolume.assert_called_once_with(0) - mocked_check_media_worker.moveToThread.assert_called_once_with(mocked_thread) - mocked_check_media_worker.finished.connect.assert_called_once_with('quit') - mocked_thread.started.connect.assert_called_once_with('play') - mocked_thread.start.assert_called_once_with() - assert 2 == mocked_thread.isRunning.call_count - mocked_application.processEvents.assert_called_once_with() - assert result is True - - -class TestCheckMediaWorker(TestCase): - """ - Test the CheckMediaWorker class - """ - def test_constructor(self): - """ - Test the constructor of the CheckMediaWorker class - """ - # GIVEN: A file path - path = 'file.ogv' - - # WHEN: The CheckMediaWorker object is instantiated - worker = CheckMediaWorker(path) - - # THEN: The correct values should be set up - assert worker is not None - - def test_signals_media(self): - """ - Test the signals() signal of the CheckMediaWorker class with a "media" origin - """ - # GIVEN: A CheckMediaWorker instance - worker = CheckMediaWorker('file.ogv') - - # WHEN: signals() is called with media and BufferedMedia - with patch.object(worker, 'stop') as mocked_stop, \ - patch.object(worker, 'finished') as mocked_finished: - worker.signals('media', worker.BufferedMedia) - - # THEN: The worker should exit and the result should be True - mocked_stop.assert_called_once_with() - mocked_finished.emit.assert_called_once_with() - assert worker.result is True - - def test_signals_error(self): - """ - Test the signals() signal of the CheckMediaWorker class with a "error" origin - """ - # GIVEN: A CheckMediaWorker instance - worker = CheckMediaWorker('file.ogv') - - # WHEN: signals() is called with error and BufferedMedia - with patch.object(worker, 'stop') as mocked_stop, \ - patch.object(worker, 'finished') as mocked_finished: - worker.signals('error', None) - - # THEN: The worker should exit and the result should be True - mocked_stop.assert_called_once_with() - mocked_finished.emit.assert_called_once_with() - assert worker.result is False === modified file 'tests/functional/openlp_core/ui/test_firsttimeform.py' --- tests/functional/openlp_core/ui/test_firsttimeform.py 2017-12-28 08:22:55 +0000 +++ tests/functional/openlp_core/ui/test_firsttimeform.py 2018-01-07 05:37:57 +0000 @@ -92,7 +92,6 @@ assert frw.web_access is True, 'The default value of self.web_access should be True' assert frw.was_cancelled is False, 'The default value of self.was_cancelled should be False' assert [] == frw.theme_screenshot_threads, 'The list of threads should be empty' - assert [] == frw.theme_screenshot_workers, 'The list of workers should be empty' assert frw.has_run_wizard is False, 'has_run_wizard should be False' def test_set_defaults(self): @@ -155,32 +154,33 @@ mocked_display_combo_box.count.assert_called_with() mocked_display_combo_box.setCurrentIndex.assert_called_with(1) - def test_on_cancel_button_clicked(self): + @patch('openlp.core.ui.firsttimeform.time') + @patch('openlp.core.ui.firsttimeform.get_thread_worker') + @patch('openlp.core.ui.firsttimeform.is_thread_finished') + def test_on_cancel_button_clicked(self, mocked_is_thread_finished, mocked_get_thread_worker, mocked_time): """ Test that the cancel button click slot shuts down the threads correctly """ # GIVEN: A FRW, some mocked threads and workers (that isn't quite done) and other mocked stuff - frw = FirstTimeForm(None) - frw.initialize(MagicMock()) mocked_worker = MagicMock() - mocked_thread = MagicMock() - mocked_thread.isRunning.side_effect = [True, False] - frw.theme_screenshot_workers.append(mocked_worker) - frw.theme_screenshot_threads.append(mocked_thread) - with patch('openlp.core.ui.firsttimeform.time') as mocked_time, \ - patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor: + mocked_get_thread_worker.return_value = mocked_worker + mocked_is_thread_finished.side_effect = [False, True] + frw = FirstTimeForm(None) + frw.initialize(MagicMock()) + frw.theme_screenshot_threads = ['test_thread'] + with patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor: # WHEN: on_cancel_button_clicked() is called frw.on_cancel_button_clicked() # THEN: The right things should be called in the right order assert frw.was_cancelled is True, 'The was_cancelled property should have been set to True' + mocked_get_thread_worker.assert_called_once_with('test_thread') mocked_worker.set_download_canceled.assert_called_with(True) - mocked_thread.isRunning.assert_called_with() - assert 2 == mocked_thread.isRunning.call_count, 'isRunning() should have been called twice' - mocked_time.sleep.assert_called_with(0.1) - assert 1 == mocked_time.sleep.call_count, 'sleep() should have only been called once' - mocked_set_normal_cursor.assert_called_with() + mocked_is_thread_finished.assert_called_with('test_thread') + assert mocked_is_thread_finished.call_count == 2, 'isRunning() should have been called twice' + mocked_time.sleep.assert_called_once_with(0.1) + mocked_set_normal_cursor.assert_called_once_with() def test_broken_config(self): """ === modified file 'tests/functional/openlp_core/ui/test_mainwindow.py' --- tests/functional/openlp_core/ui/test_mainwindow.py 2017-12-29 09:15:48 +0000 +++ tests/functional/openlp_core/ui/test_mainwindow.py 2018-01-07 05:37:57 +0000 @@ -60,9 +60,10 @@ # Mock cursor busy/normal methods. self.app.set_busy_cursor = MagicMock() self.app.set_normal_cursor = MagicMock() + self.app.process_events = MagicMock() self.app.args = [] Registry().register('application', self.app) - Registry().set_flag('no_web_server', False) + Registry().set_flag('no_web_server', True) self.add_toolbar_action_patcher = patch('openlp.core.ui.mainwindow.create_action') self.mocked_add_toolbar_action = self.add_toolbar_action_patcher.start() self.mocked_add_toolbar_action.side_effect = self._create_mock_action @@ -74,8 +75,8 @@ """ Delete all the C++ objects and stop all the patchers """ + del self.main_window self.add_toolbar_action_patcher.stop() - del self.main_window def test_cmd_line_file(self): """ @@ -92,20 +93,20 @@ # THEN the service from the arguments is loaded mocked_load_file.assert_called_with(service) - def test_cmd_line_arg(self): + @patch('openlp.core.ui.servicemanager.ServiceManager.load_file') + def test_cmd_line_arg(self, mocked_load_file): """ Test that passing a non service file does nothing. """ # GIVEN a non service file as an argument to openlp service = os.path.join('openlp.py') self.main_window.arguments = [service] - with patch('openlp.core.ui.servicemanager.ServiceManager.load_file') as mocked_load_file: - - # WHEN the argument is processed - self.main_window.open_cmd_line_files("") - - # THEN the file should not be opened - assert mocked_load_file.called is False, 'load_file should not have been called' + + # WHEN the argument is processed + self.main_window.open_cmd_line_files(service) + + # THEN the file should not be opened + assert mocked_load_file.called is False, 'load_file should not have been called' def test_main_window_title(self): """ === modified file 'tests/functional/openlp_plugins/songs/test_songselect.py' --- tests/functional/openlp_plugins/songs/test_songselect.py 2017-12-29 10:19:33 +0000 +++ tests/functional/openlp_plugins/songs/test_songselect.py 2018-01-07 05:37:57 +0000 @@ -765,9 +765,9 @@ assert ssform.search_combobox.isEnabled() is True @patch('openlp.plugins.songs.forms.songselectform.Settings') - @patch('openlp.plugins.songs.forms.songselectform.QtCore.QThread') + @patch('openlp.plugins.songs.forms.songselectform.run_thread') @patch('openlp.plugins.songs.forms.songselectform.SearchWorker') - def test_on_search_button_clicked(self, MockedSearchWorker, MockedQtThread, MockedSettings): + def test_on_search_button_clicked(self, MockedSearchWorker, mocked_run_thread, MockedSettings): """ Test that search fields are disabled when search button is clicked. """ === modified file 'tests/interfaces/openlp_core/ui/test_mainwindow.py' --- tests/interfaces/openlp_core/ui/test_mainwindow.py 2017-12-29 09:15:48 +0000 +++ tests/interfaces/openlp_core/ui/test_mainwindow.py 2018-01-07 05:37:57 +0000 @@ -44,21 +44,21 @@ self.app.set_normal_cursor = MagicMock() self.app.args = [] Registry().register('application', self.app) - Registry().set_flag('no_web_server', False) + Registry().set_flag('no_web_server', True) # Mock classes and methods used by mainwindow. - with patch('openlp.core.ui.mainwindow.SettingsForm') as mocked_settings_form, \ - patch('openlp.core.ui.mainwindow.ImageManager') as mocked_image_manager, \ - patch('openlp.core.ui.mainwindow.LiveController') as mocked_live_controller, \ - patch('openlp.core.ui.mainwindow.PreviewController') as mocked_preview_controller, \ - patch('openlp.core.ui.mainwindow.OpenLPDockWidget') as mocked_dock_widget, \ - patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox') as mocked_q_tool_box_class, \ - patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget') as mocked_add_dock_method, \ - patch('openlp.core.ui.mainwindow.ServiceManager') as mocked_service_manager, \ - patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \ - patch('openlp.core.ui.mainwindow.ProjectorManager') as mocked_projector_manager, \ - patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \ - patch('openlp.core.ui.mainwindow.websockets.WebSocketServer') as mocked_websocketserver, \ - patch('openlp.core.ui.mainwindow.server.HttpServer') as mocked_httpserver: + with patch('openlp.core.ui.mainwindow.SettingsForm'), \ + patch('openlp.core.ui.mainwindow.ImageManager'), \ + patch('openlp.core.ui.mainwindow.LiveController'), \ + patch('openlp.core.ui.mainwindow.PreviewController'), \ + patch('openlp.core.ui.mainwindow.OpenLPDockWidget'), \ + patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox'), \ + patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget'), \ + patch('openlp.core.ui.mainwindow.ServiceManager'), \ + patch('openlp.core.ui.mainwindow.ThemeManager'), \ + patch('openlp.core.ui.mainwindow.ProjectorManager'), \ + patch('openlp.core.ui.mainwindow.Renderer'), \ + patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \ + patch('openlp.core.ui.mainwindow.server.HttpServer'): self.main_window = MainWindow() def tearDown(self):
_______________________________________________ Mailing list: https://launchpad.net/~openlp-core Post to : openlp-core@lists.launchpad.net Unsubscribe : https://launchpad.net/~openlp-core More help : https://help.launchpad.net/ListHelp