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().

Changes related to thread API:

- WebSocket was refactored (mostly into the worker)
- HttpServer was refactored a bit
- CheckMediaWorker was refactored a bit
- Version check refactored
- SongSelect search refactored
- New _wait_for_threads() method...

bzr-revno: 2807
This commit is contained in:
Raoul Snyman 2018-01-07 12:27:26 -07:00
commit c4681e60e3
26 changed files with 639 additions and 428 deletions

View File

@ -25,7 +25,7 @@ Download and "install" the remote web client
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 @@ def download_and_check(callback=None):
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')

View File

@ -27,7 +27,7 @@ import logging
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.mixins import LogMixin, RegistryProperties
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 @@ class HttpWorker(QtCore.QObject):
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 @@ class HttpServer(RegistryBase, RegistryProperties, LogMixin):
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 @@ class DownloadProgressDialog(QtWidgets.QProgressDialog):
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.
"""

View File

@ -28,37 +28,88 @@ import json
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 every 0.2 seconds to get the latest position and send if it changed. This only gets triggered when the first
client connects.
:param request: request from client
:param path: determines the endpoints supported
"""
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):
def start(self):
"""
Constructor for the thread class.
:param server: The http server class.
Run the worker.
"""
self.ws_server = server
super(WebSocketWorker, self).__init__()
def run(self):
"""
Run the thread.
"""
self.ws_server.start_server()
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:
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 @@ class WebSocketServer(RegistryProperties, LogMixin):
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')

View File

@ -304,8 +304,7 @@ def parse_options(args=None):
'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 @@ def main(args=None):
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 @@ def main(args=None):
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

View File

@ -157,7 +157,7 @@ def _get_os_dir_path(dir_type):
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')

View File

@ -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 @@ def get_web_page(url, headers=None, update_openlp=False, proxies=None):
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 @@ def get_url_file_size(url):
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 @@ def url_get_file(callback, url, file_path, sha256=None):
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 @@ def url_get_file(callback, url, file_path, sha256=None):
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

View File

@ -35,13 +35,14 @@ from openlp.core.common.registry import Registry
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 @@ class ImageThread(QtCore.QThread):
``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 Image(object):
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 @@ class ImageManager(QtCore.QObject):
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 @@ class ImageManager(QtCore.QObject):
"""
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 @@ class ImageManager(QtCore.QObject):
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):
"""

View File

@ -308,8 +308,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
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):

View File

@ -89,11 +89,11 @@ class PJLinkUDP(QtNetwork.QUdpSocket):
'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

View File

@ -24,26 +24,41 @@ The :mod:`openlp.core.threading` module contains some common threading code
"""
from PyQt5 import QtCore
from openlp.core.common.registry import Registry
def run_thread(parent, worker, prefix='', auto_start=True):
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 @@ def run_thread(parent, worker, prefix='', auto_start=True):
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: 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: 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: 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

View File

@ -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 PyQt5 import QtCore, QtWidgets
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.registry import Registry
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):
def start(self):
"""
Overridden method to run the thread.
Run the worker
"""
if self.was_download_cancelled:
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 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
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 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
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 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
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 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
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 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
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 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
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 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
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 = ''

View File

@ -24,7 +24,6 @@ This is the main window, where all the action happens.
"""
import logging
import sys
import time
from datetime import datetime
from distutils import dir_util
from distutils.errors import DistutilsFileError
@ -478,8 +477,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
"""
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 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
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 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
# 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 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
# 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 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
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 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
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):
"""
@ -1062,9 +1073,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
: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)
if save_settings:
if Settings().value('advanced/save current plugin'):
Settings().setValue('advanced/current media plugin', self.media_tool_box.currentIndex())

View File

@ -31,6 +31,7 @@ from PyQt5 import QtCore, QtMultimedia, QtMultimediaWidgets
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 @@ class SystemPlayer(MediaPlayer):
: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.result = None
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()

View File

@ -35,7 +35,7 @@ from PyQt5 import QtCore
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_TIMEOUT = 30
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 @@ def update_check_date():
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():

View File

@ -27,24 +27,23 @@ from time import sleep
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 @@ class SearchWorker(QtCore.QObject):
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 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
"""
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 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
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 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
"""
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

View File

@ -42,23 +42,8 @@ class TestHttpServer(TestCase):
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):
"""
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', True)
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):
@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
"""
@ -68,5 +53,20 @@ class TestHttpServer(TestCase):
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 == 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
"""
# GIVEN: A new httpserver
# WHEN: I start the server
Registry().set_flag('no_web_server', True)
HttpServer()
# THEN: the api environment should have been created
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'

View File

@ -63,34 +63,34 @@ class TestWSServer(TestCase, TestMixin):
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):
"""

View File

@ -27,7 +27,7 @@ import tempfile
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 @@ class TestHttpUtils(TestCase, TestMixin):
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

View File

@ -25,20 +25,113 @@ Package to test the openlp.core.ui package.
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 @@ class TestImageManager(TestCase, TestMixin):
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 @@ class TestImageManager(TestCase, TestMixin):
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 @@ class TestImageManager(TestCase, TestMixin):
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'
# 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)
# 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'"
# 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.
# 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):
"""

View File

@ -36,14 +36,15 @@ def test_parse_options_basic():
"""
# 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 @@ def test_parse_options_debug():
"""
# 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 @@ def test_parse_options_debug_and_portable():
"""
# 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 @@ def test_parse_options_all_no_file():
"""
# 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 @@ def test_parse_options_file():
"""
# 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 @@ def test_parse_options_file_and_debug():
"""
# 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'

View File

@ -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-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.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()

View File

@ -171,7 +171,7 @@ class TestSystemPlayer(TestCase):
# WHEN: The load() method is run
with patch.object(player, 'check_media') as mocked_check_media, \
patch.object(player, 'volume') as mocked_volume:
patch.object(player, 'volume'):
mocked_check_media.return_value = False
result = player.load(mocked_display)
@ -461,8 +461,9 @@ class TestSystemPlayer(TestCase):
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):
@patch('openlp.core.ui.media.systemplayer.run_thread')
@patch('openlp.core.ui.media.systemplayer.is_thread_finished')
def test_check_media(self, mocked_is_thread_finished, mocked_run_thread, MockCheckMediaWorker):
"""
Test the check_media() method of the SystemPlayer
"""
@ -472,12 +473,8 @@ class TestSystemPlayer(TestCase):
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_is_thread_finished.side_effect = [False, True]
mocked_check_media_worker = MagicMock()
mocked_check_media_worker.play = 'play'
mocked_check_media_worker.result = True
MockCheckMediaWorker.return_value = mocked_check_media_worker
@ -485,14 +482,11 @@ class TestSystemPlayer(TestCase):
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_run_thread.assert_called_once_with(mocked_check_media_worker, 'check_media')
mocked_is_thread_finished.assert_called_with('check_media')
assert mocked_is_thread_finished.call_count == 2, 'is_thread_finished() should have been called twice'
mocked_application.processEvents.assert_called_once_with()
assert result is True
@ -523,12 +517,12 @@ class TestCheckMediaWorker(TestCase):
# WHEN: signals() is called with media and BufferedMedia
with patch.object(worker, 'stop') as mocked_stop, \
patch.object(worker, 'finished') as mocked_finished:
patch.object(worker, 'quit') as mocked_quit:
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()
mocked_quit.emit.assert_called_once_with()
assert worker.result is True
def test_signals_error(self):
@ -540,10 +534,10 @@ class TestCheckMediaWorker(TestCase):
# WHEN: signals() is called with error and BufferedMedia
with patch.object(worker, 'stop') as mocked_stop, \
patch.object(worker, 'finished') as mocked_finished:
patch.object(worker, 'quit') as mocked_quit:
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()
mocked_quit.emit.assert_called_once_with()
assert worker.result is False

View File

@ -92,7 +92,6 @@ class TestFirstTimeForm(TestCase, TestMixin):
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 @@ class TestFirstTimeForm(TestCase, TestMixin):
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
mocked_worker = MagicMock()
mocked_get_thread_worker.return_value = mocked_worker
mocked_is_thread_finished.side_effect = [False, True]
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:
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):
"""

View File

@ -60,9 +60,10 @@ class TestMainWindow(TestCase, TestMixin):
# 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 @@ class TestMainWindow(TestCase, TestMixin):
"""
Delete all the C++ objects and stop all the patchers
"""
self.add_toolbar_action_patcher.stop()
del self.main_window
self.add_toolbar_action_patcher.stop()
def test_cmd_line_file(self):
"""
@ -92,20 +93,20 @@ class TestMainWindow(TestCase, TestMixin):
# 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("")
# 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'
# 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):
"""

View File

@ -765,9 +765,9 @@ class TestSongSelectForm(TestCase, TestMixin):
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.
"""

View File

@ -44,21 +44,21 @@ class TestMainWindow(TestCase, TestMixin):
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):