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 zipfile import ZipFile
from openlp.core.common.applocation import AppLocation 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 from openlp.core.common.registry import Registry
@ -65,7 +65,7 @@ def download_and_check(callback=None):
sha256, version = download_sha256() sha256, version = download_sha256()
file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip') file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip')
callback.setRange(0, file_size) callback.setRange(0, file_size)
if url_get_file(callback, 'https://get.openlp.org/webclient/site.zip', if download_file(callback, 'https://get.openlp.org/webclient/site.zip',
AppLocation.get_section_data_path('remotes') / 'site.zip', AppLocation.get_section_data_path('remotes') / 'site.zip',
sha256=sha256): sha256=sha256):
deploy_zipfile(AppLocation.get_section_data_path('remotes'), 'site.zip') deploy_zipfile(AppLocation.get_section_data_path('remotes'), 'site.zip')

View File

@ -27,7 +27,7 @@ import logging
import time import time
from PyQt5 import QtCore, QtWidgets 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.deploy import download_and_check, download_sha256
from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint 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.path import create_paths
from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.threading import ThreadWorker, run_thread
log = logging.getLogger(__name__) 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. A special Qt thread class to allow the HTTP server to run at the same time as the UI.
""" """
def __init__(self): def start(self):
"""
Constructor for the thread class.
:param server: The http server class.
"""
super(HttpWorker, self).__init__()
def run(self):
""" """
Run the thread. Run the thread.
""" """
@ -68,12 +61,21 @@ class HttpWorker(QtCore.QObject):
port = Settings().value('api/port') port = Settings().value('api/port')
Registry().execute('get_website_version') Registry().execute('get_website_version')
try: try:
serve(application, host=address, port=port) self.server = create_server(application, host=address, port=port)
self.server.run()
except OSError: except OSError:
log.exception('An error occurred when serving the application.') log.exception('An error occurred when serving the application.')
self.quit.emit()
def stop(self): 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): class HttpServer(RegistryBase, RegistryProperties, LogMixin):
@ -85,12 +87,9 @@ class HttpServer(RegistryBase, RegistryProperties, LogMixin):
Initialise the http server, and start the http server Initialise the http server, and start the http server
""" """
super(HttpServer, self).__init__(parent) super(HttpServer, self).__init__(parent)
if Registry().get_flag('no_web_server'): if not Registry().get_flag('no_web_server'):
self.worker = HttpWorker() worker = HttpWorker()
self.thread = QtCore.QThread() run_thread(worker, 'http_server')
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.thread.start()
Registry().register_function('download_website', self.first_time) Registry().register_function('download_website', self.first_time)
Registry().register_function('get_website_version', self.website_version) Registry().register_function('get_website_version', self.website_version)
Registry().set_flag('website_version', '0.0') Registry().set_flag('website_version', '0.0')
@ -167,7 +166,7 @@ class DownloadProgressDialog(QtWidgets.QProgressDialog):
self.was_cancelled = False self.was_cancelled = False
self.previous_size = 0 self.previous_size = 0
def _download_progress(self, count, block_size): def update_progress(self, count, block_size):
""" """
Calculate and display the download progress. Calculate and display the download progress.
""" """

View File

@ -28,37 +28,88 @@ import json
import logging import logging
import time import time
import websockets from websockets import serve
from PyQt5 import QtCore
from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.threading import ThreadWorker, run_thread
log = logging.getLogger(__name__) 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. 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. Run the worker.
:param server: The http server class.
""" """
self.ws_server = server address = Settings().value('api/ip address')
super(WebSocketWorker, self).__init__() port = Settings().value('api/websocket port')
# Start the event loop
def run(self): self.event_loop = asyncio.new_event_loop()
""" asyncio.set_event_loop(self.event_loop)
Run the thread. # Create the websocker server
""" loop = 1
self.ws_server.start_server() 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): 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): class WebSocketServer(RegistryProperties, LogMixin):
@ -70,74 +121,6 @@ class WebSocketServer(RegistryProperties, LogMixin):
Initialise and start the WebSockets server Initialise and start the WebSockets server
""" """
super(WebSocketServer, self).__init__() super(WebSocketServer, self).__init__()
if Registry().get_flag('no_web_server'): if not Registry().get_flag('no_web_server'):
self.settings_section = 'api' worker = WebSocketWorker()
self.worker = WebSocketWorker(self) run_thread(worker, 'websocket_server')
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)

View File

@ -304,8 +304,7 @@ def parse_options(args=None):
'off a USB flash drive (not implemented).') 'off a USB flash drive (not implemented).')
parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true', parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',
help='Ignore the version file and pull the version directly from Bazaar') 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_true',
parser.add_argument('-w', '--no-web-server', dest='no_web_server', action='store_false',
help='Turn off the Web and Socket Server ') help='Turn off the Web and Socket Server ')
parser.add_argument('rargs', nargs='?', default=[]) parser.add_argument('rargs', nargs='?', default=[])
# Parse command line options and deal with them. Use args supplied pragmatically if possible. # 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) log.setLevel(logging.WARNING)
else: else:
log.setLevel(logging.INFO) 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. # Throw the rest of the arguments at Qt, just in case.
qt_args.extend(args.rargs) qt_args.extend(args.rargs)
# Bug #1018855: Set the WM_CLASS property in X11 # Bug #1018855: Set the WM_CLASS property in X11
@ -358,7 +355,7 @@ def main(args=None):
application.setOrganizationDomain('openlp.org') application.setOrganizationDomain('openlp.org')
application.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) application.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
application.setAttribute(QtCore.Qt.AA_DontCreateNativeWidgetSiblings, True) application.setAttribute(QtCore.Qt.AA_DontCreateNativeWidgetSiblings, True)
if args and args.portable: if args.portable:
application.setApplicationName('OpenLPPortable') application.setApplicationName('OpenLPPortable')
Settings.setDefaultFormat(Settings.IniFormat) Settings.setDefaultFormat(Settings.IniFormat)
# Get location OpenLPPortable.ini # Get location OpenLPPortable.ini

View File

@ -157,7 +157,7 @@ def _get_os_dir_path(dir_type):
return directory return directory
return Path('/usr', 'share', 'openlp') return Path('/usr', 'share', 'openlp')
if XDG_BASE_AVAILABLE: 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') return Path(BaseDirectory.xdg_data_home, 'openlp')
elif dir_type == AppLocation.CacheDir: elif dir_type == AppLocation.CacheDir:
return Path(BaseDirectory.xdg_cache_home, 'openlp') return Path(BaseDirectory.xdg_cache_home, 'openlp')

View File

@ -20,7 +20,7 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # 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 hashlib
import logging import logging
@ -104,7 +104,7 @@ def get_web_page(url, headers=None, update_openlp=False, proxies=None):
if retries >= CONNECTION_RETRIES: if retries >= CONNECTION_RETRIES:
raise ConnectionError('Unable to connect to {url}, see log for details'.format(url=url)) raise ConnectionError('Unable to connect to {url}, see log for details'.format(url=url))
retries += 1 retries += 1
except: except: # noqa
# Don't know what's happening, so reraise the original # Don't know what's happening, so reraise the original
log.exception('Unknown error when trying to connect to {url}'.format(url=url)) log.exception('Unknown error when trying to connect to {url}'.format(url=url))
raise raise
@ -136,12 +136,12 @@ def get_url_file_size(url):
continue 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 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. 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 url: URL to download
:param file_path: Destination file :param file_path: Destination file
:param sha256: The check sum value to be checked against the download value :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() hasher = hashlib.sha256()
# Download until finished or canceled. # Download until finished or canceled.
for chunk in response.iter_content(chunk_size=block_size): 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 break
saved_file.write(chunk) saved_file.write(chunk)
if sha256: if sha256:
hasher.update(chunk) hasher.update(chunk)
block_count += 1 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() response.close()
if sha256 and hasher.hexdigest() != sha256: if sha256 and hasher.hexdigest() != sha256:
log.error('sha256 sums did not match for file %s, got %s, expected %s', file_path, hasher.hexdigest(), 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 retries += 1
time.sleep(0.1) time.sleep(0.1)
continue 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() file_path.unlink()
return True 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.common.settings import Settings
from openlp.core.display.screens import ScreenList from openlp.core.display.screens import ScreenList
from openlp.core.lib import resize_image, image_to_byte from openlp.core.lib import resize_image, image_to_byte
from openlp.core.threading import ThreadWorker, run_thread
log = logging.getLogger(__name__) 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. byte stream in background.
""" """
def __init__(self, manager): def __init__(self, manager):
@ -51,14 +52,21 @@ class ImageThread(QtCore.QThread):
``manager`` ``manager``
The image manager. The image manager.
""" """
super(ImageThread, self).__init__(None) super().__init__()
self.image_manager = manager self.image_manager = manager
def run(self): def start(self):
""" """
Run the thread. Start the worker
""" """
self.image_manager.process() self.image_manager.process()
self.quit.emit()
def stop(self):
"""
Stop the worker
"""
self.image_manager.stop_manager = True
class Priority(object): class Priority(object):
@ -130,7 +138,7 @@ class Image(object):
class PriorityQueue(queue.PriorityQueue): 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`` 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` 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.width = current_screen['size'].width()
self.height = current_screen['size'].height() self.height = current_screen['size'].height()
self._cache = {} self._cache = {}
self.image_thread = ImageThread(self)
self._conversion_queue = PriorityQueue() self._conversion_queue = PriorityQueue()
self.stop_manager = False self.stop_manager = False
Registry().register_function('images_regenerate', self.process_updates) 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 Flush the queue to updated any data to update
""" """
# We want only one thread. try:
if not self.image_thread.isRunning(): worker = ImageWorker(self)
self.image_thread.start() 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): 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: if image.path == path and image.timestamp != os.stat(path).st_mtime:
image.timestamp = os.stat(path).st_mtime image.timestamp = os.stat(path).st_mtime
self._reset_image(image) self._reset_image(image)
# We want only one thread. self.process_updates()
if not self.image_thread.isRunning():
self.image_thread.start()
def process(self): def process(self):
""" """

View File

@ -308,8 +308,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
self.settings_section = 'projector' self.settings_section = 'projector'
self.projectordb = projectordb self.projectordb = projectordb
self.projector_list = [] self.projector_list = []
self.pjlink_udp = PJLinkUDP() self.pjlink_udp = PJLinkUDP(self.projector_list)
self.pjlink_udp.projector_list = self.projector_list
self.source_select_form = None self.source_select_form = None
def bootstrap_initialise(self): def bootstrap_initialise(self):

View File

@ -89,11 +89,11 @@ class PJLinkUDP(QtNetwork.QUdpSocket):
'SRCH' # Class 2 (reply is ACKN) 'SRCH' # Class 2 (reply is ACKN)
] ]
def __init__(self, port=PJLINK_PORT): def __init__(self, projector_list, port=PJLINK_PORT):
""" """
Initialize socket Initialize socket
""" """
self.projector_list = projector_list
self.port = port 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 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. 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 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 str thread_name: The name of the thread, used to keep track of the thread.
:param bool auto_start: Automatically start the thread. Defaults to True. :param bool can_start: Start the thread. Defaults to True.
""" """
# Set up attribute names if not thread_name:
thread_name = 'thread' raise ValueError('A thread_name is required when calling the "run_thread" function')
worker_name = 'worker' main_window = Registry().get('main_window')
if prefix: if thread_name in main_window.threads:
thread_name = '_'.join([prefix, thread_name]) raise KeyError('A thread with the name "{}" has already been created, please use another'.format(thread_name))
worker_name = '_'.join([prefix, worker_name])
# Create the thread and add the thread and the worker to the parent # Create the thread and add the thread and the worker to the parent
thread = QtCore.QThread() thread = QtCore.QThread()
setattr(parent, thread_name, thread) main_window.threads[thread_name] = {
setattr(parent, worker_name, worker) 'thread': thread,
'worker': worker
}
# Move the worker into the thread's context # Move the worker into the thread's context
worker.moveToThread(thread) worker.moveToThread(thread)
# Connect slots and signals # 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(thread.quit)
worker.quit.connect(worker.deleteLater) worker.quit.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater) thread.finished.connect(thread.deleteLater)
if auto_start: thread.finished.connect(make_remove_thread(thread_name))
if can_start:
thread.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. This module contains the first time wizard.
""" """
import logging import logging
import os
import socket
import time import time
import urllib.error import urllib.error
import urllib.parse 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 import clean_button_text, trace_error_handler
from openlp.core.common.applocation import AppLocation 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.i18n import translate
from openlp.core.common.mixins import RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.path import Path, create_paths 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.common.settings import Settings
from openlp.core.lib import PluginStatus, build_icon from openlp.core.lib import PluginStatus, build_icon
from openlp.core.lib.ui import critical_error_message_box 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__) log = logging.getLogger(__name__)
class ThemeScreenshotWorker(QtCore.QObject): class ThemeScreenshotWorker(ThreadWorker):
""" """
This thread downloads a theme's screenshot This thread downloads a theme's screenshot
""" """
screenshot_downloaded = QtCore.pyqtSignal(str, str, str) screenshot_downloaded = QtCore.pyqtSignal(str, str, str)
finished = QtCore.pyqtSignal()
def __init__(self, themes_url, title, filename, sha256, screenshot): def __init__(self, themes_url, title, filename, sha256, screenshot):
""" """
Set up the worker object Set up the worker object
""" """
self.was_download_cancelled = False self.was_cancelled = False
self.themes_url = themes_url self.themes_url = themes_url
self.title = title self.title = title
self.filename = filename self.filename = filename
self.sha256 = sha256 self.sha256 = sha256
self.screenshot = screenshot self.screenshot = screenshot
socket.setdefaulttimeout(CONNECTION_TIMEOUT) super().__init__()
super(ThemeScreenshotWorker, self).__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 return
try: try:
urllib.request.urlretrieve('{host}{name}'.format(host=self.themes_url, name=self.screenshot), download_path = Path(gettempdir()) / 'openlp' / self.screenshot
os.path.join(gettempdir(), 'openlp', self.screenshot)) is_success = download_file(self, '{host}{name}'.format(host=self.themes_url, name=self.screenshot),
# Signal that the screenshot has been downloaded download_path)
self.screenshot_downloaded.emit(self.title, self.filename, self.sha256) if is_success and not self.was_cancelled:
except: # 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') log.exception('Unable to download screenshot')
finally: finally:
self.finished.emit() self.quit.emit()
@QtCore.pyqtSlot(bool) @QtCore.pyqtSlot(bool)
def set_download_canceled(self, toggle): def set_download_canceled(self, toggle):
@ -145,12 +144,13 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
return FirstTimePage.Progress return FirstTimePage.Progress
elif self.currentId() == FirstTimePage.Themes: elif self.currentId() == FirstTimePage.Themes:
self.application.set_busy_cursor() 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) time.sleep(0.1)
self.application.process_events() self.application.process_events()
# Build the screenshot icons, as this can not be done in the thread. # Build the screenshot icons, as this can not be done in the thread.
self._build_theme_screenshots() self._build_theme_screenshots()
self.application.set_normal_cursor() self.application.set_normal_cursor()
self.theme_screenshot_threads = []
return FirstTimePage.Defaults return FirstTimePage.Defaults
else: else:
return self.get_next_page_id() return self.get_next_page_id()
@ -171,7 +171,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.screens = screens self.screens = screens
self.was_cancelled = False self.was_cancelled = False
self.theme_screenshot_threads = [] self.theme_screenshot_threads = []
self.theme_screenshot_workers = []
self.has_run_wizard = False self.has_run_wizard = False
def _download_index(self): 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='') sha256 = self.config.get('theme_{theme}'.format(theme=theme), 'sha256', fallback='')
screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot') screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot')
worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot) worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot)
self.theme_screenshot_workers.append(worker)
worker.screenshot_downloaded.connect(self.on_screenshot_downloaded) worker.screenshot_downloaded.connect(self.on_screenshot_downloaded)
thread = QtCore.QThread(self) thread_name = 'theme_screenshot_{title}'.format(title=title)
self.theme_screenshot_threads.append(thread) run_thread(worker, thread_name)
thread.started.connect(worker.run) self.theme_screenshot_threads.append(thread_name)
worker.finished.connect(thread.quit)
worker.moveToThread(thread)
thread.start()
self.application.process_events() self.application.process_events()
def set_defaults(self): def set_defaults(self):
@ -353,12 +348,14 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
Process the triggering of the cancel button. Process the triggering of the cancel button.
""" """
self.was_cancelled = True self.was_cancelled = True
if self.theme_screenshot_workers: if self.theme_screenshot_threads:
for worker in self.theme_screenshot_workers: for thread_name in self.theme_screenshot_threads:
worker.set_download_canceled(True) worker = get_thread_worker(thread_name)
if worker:
worker.set_download_canceled(True)
# Was the thread created. # Was the thread created.
if self.theme_screenshot_threads: 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) time.sleep(0.1)
self.application.set_normal_cursor() 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._increment_progress_bar(self.downloading.format(name=filename), 0)
self.previous_size = 0 self.previous_size = 0
destination = songs_destination_path / str(filename) destination = songs_destination_path / str(filename)
if not url_get_file(self, '{path}{name}'.format(path=self.songs_url, name=filename), if not download_file(self, '{path}{name}'.format(path=self.songs_url, name=filename),
destination, sha256): destination, sha256):
missed_files.append('Song: {name}'.format(name=filename)) missed_files.append('Song: {name}'.format(name=filename))
# Download Bibles # Download Bibles
bibles_iterator = QtWidgets.QTreeWidgetItemIterator(self.bibles_tree_widget) 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) bible, sha256 = item.data(0, QtCore.Qt.UserRole)
self._increment_progress_bar(self.downloading.format(name=bible), 0) self._increment_progress_bar(self.downloading.format(name=bible), 0)
self.previous_size = 0 self.previous_size = 0
if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible), if not download_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible),
bibles_destination_path / bible, sha256): bibles_destination_path / bible, sha256):
missed_files.append('Bible: {name}'.format(name=bible)) missed_files.append('Bible: {name}'.format(name=bible))
bibles_iterator += 1 bibles_iterator += 1
# Download themes # Download themes
@ -584,8 +581,8 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
theme, sha256 = item.data(QtCore.Qt.UserRole) theme, sha256 = item.data(QtCore.Qt.UserRole)
self._increment_progress_bar(self.downloading.format(name=theme), 0) self._increment_progress_bar(self.downloading.format(name=theme), 0)
self.previous_size = 0 self.previous_size = 0
if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), if not download_file(self, '{path}{name}'.format(path=self.themes_url, name=theme),
themes_destination_path / theme, sha256): themes_destination_path / theme, sha256):
missed_files.append('Theme: {name}'.format(name=theme)) missed_files.append('Theme: {name}'.format(name=theme))
if missed_files: if missed_files:
file_list = '' file_list = ''

View File

@ -24,7 +24,6 @@ This is the main window, where all the action happens.
""" """
import logging import logging
import sys import sys
import time
from datetime import datetime from datetime import datetime
from distutils import dir_util from distutils import dir_util
from distutils.errors import DistutilsFileError from distutils.errors import DistutilsFileError
@ -478,8 +477,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
""" """
super(MainWindow, self).__init__() super(MainWindow, self).__init__()
Registry().register('main_window', self) Registry().register('main_window', self)
self.version_thread = None self.threads = {}
self.version_worker = None
self.clipboard = self.application.clipboard() self.clipboard = self.application.clipboard()
self.arguments = ''.join(self.application.args) self.arguments = ''.join(self.application.args)
# Set up settings sections for the main application (not for use by plugins). # 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() Settings().set_up_default_values()
self.about_form = AboutForm(self) self.about_form = AboutForm(self)
MediaController() MediaController()
websockets.WebSocketServer() self.ws_server = websockets.WebSocketServer()
server.HttpServer() self.http_server = server.HttpServer(self)
SettingsForm(self) SettingsForm(self)
self.formatting_tag_form = FormattingTagForm(self) self.formatting_tag_form = FormattingTagForm(self)
self.shortcut_form = ShortcutListForm(self) self.shortcut_form = ShortcutListForm(self)
@ -549,6 +547,41 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
# Reset the cursor # Reset the cursor
self.application.set_normal_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): def bootstrap_post_set_up(self):
""" """
process the bootstrap post setup request process the bootstrap post setup request
@ -695,7 +728,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
# Update the theme widget # Update the theme widget
self.theme_manager_contents.load_themes() self.theme_manager_contents.load_themes()
# Check if any Bibles downloaded. If there are, they will be processed. # 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() self.application.set_normal_cursor()
def is_display_blank(self): def is_display_blank(self):
@ -1000,39 +1033,14 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
if not self.application.is_event_loop_active: if not self.application.is_event_loop_active:
event.ignore() event.ignore()
return 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(): if self.service_manager_contents.is_modified():
ret = self.service_manager_contents.save_modified_service() ret = self.service_manager_contents.save_modified_service()
if ret == QtWidgets.QMessageBox.Save: if ret == QtWidgets.QMessageBox.Save:
if self.service_manager_contents.decide_save_method(): if self.service_manager_contents.decide_save_method():
self.clean_up()
event.accept() event.accept()
else: else:
event.ignore() event.ignore()
elif ret == QtWidgets.QMessageBox.Discard: elif ret == QtWidgets.QMessageBox.Discard:
self.clean_up()
event.accept() event.accept()
else: else:
event.ignore() event.ignore()
@ -1048,13 +1056,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
close_button.setText(translate('OpenLP.MainWindow', '&Exit OpenLP')) close_button.setText(translate('OpenLP.MainWindow', '&Exit OpenLP'))
msg_box.setDefaultButton(QtWidgets.QMessageBox.Close) msg_box.setDefaultButton(QtWidgets.QMessageBox.Close)
if msg_box.exec() == QtWidgets.QMessageBox.Close: if msg_box.exec() == QtWidgets.QMessageBox.Close:
self.clean_up()
event.accept() event.accept()
else: else:
event.ignore() event.ignore()
else: else:
self.clean_up()
event.accept() 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): 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**. :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 save_settings:
if Settings().value('advanced/save current plugin'): if Settings().value('advanced/save current plugin'):
Settings().setValue('advanced/current media plugin', self.media_tool_box.currentIndex()) 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.common.i18n import translate
from openlp.core.ui.media import MediaState from openlp.core.ui.media import MediaState
from openlp.core.ui.media.mediaplayer import MediaPlayer from openlp.core.ui.media.mediaplayer import MediaPlayer
from openlp.core.threading import ThreadWorker, run_thread, is_thread_finished
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -293,39 +294,38 @@ class SystemPlayer(MediaPlayer):
:param path: Path to file to be checked :param path: Path to file to be checked
:return: True if file can be played otherwise False :return: True if file can be played otherwise False
""" """
thread = QtCore.QThread()
check_media_worker = CheckMediaWorker(path) check_media_worker = CheckMediaWorker(path)
check_media_worker.setVolume(0) check_media_worker.setVolume(0)
check_media_worker.moveToThread(thread) run_thread(check_media_worker, 'check_media')
check_media_worker.finished.connect(thread.quit) while not is_thread_finished('check_media'):
thread.started.connect(check_media_worker.play)
thread.start()
while thread.isRunning():
self.application.processEvents() self.application.processEvents()
return check_media_worker.result return check_media_worker.result
class CheckMediaWorker(QtMultimedia.QMediaPlayer): class CheckMediaWorker(QtMultimedia.QMediaPlayer, ThreadWorker):
""" """
Class used to check if a media file is playable Class used to check if a media file is playable
""" """
finished = QtCore.pyqtSignal()
def __init__(self, path): def __init__(self, path):
super(CheckMediaWorker, self).__init__(None, QtMultimedia.QMediaPlayer.VideoSurface) 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.error.connect(functools.partial(self.signals, 'error'))
self.mediaStatusChanged.connect(functools.partial(self.signals, 'media')) self.mediaStatusChanged.connect(functools.partial(self.signals, 'media'))
self.setMedia(QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile(self.path)))
self.setMedia(QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile(path))) self.play()
def signals(self, origin, status): def signals(self, origin, status):
if origin == 'media' and status == self.BufferedMedia: if origin == 'media' and status == self.BufferedMedia:
self.result = True self.result = True
self.stop() self.stop()
self.finished.emit() self.quit.emit()
elif origin == 'error' and status != self.NoError: elif origin == 'error' and status != self.NoError:
self.result = False self.result = False
self.stop() 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.applocation import AppLocation
from openlp.core.common.settings import Settings 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__) log = logging.getLogger(__name__)
@ -44,14 +44,13 @@ CONNECTION_TIMEOUT = 30
CONNECTION_RETRIES = 2 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 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. doesn't affect the loading time of OpenLP.
""" """
new_version = QtCore.pyqtSignal(dict) new_version = QtCore.pyqtSignal(dict)
no_internet = QtCore.pyqtSignal() no_internet = QtCore.pyqtSignal()
quit = QtCore.pyqtSignal()
def __init__(self, last_check_date, current_version): 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')) 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 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') last_check_date = Settings().value('core/last version test')
if date.today().strftime('%Y-%m-%d') <= last_check_date: if date.today().strftime('%Y-%m-%d') <= last_check_date:
log.debug('Version check skipped, last checked today') log.debug('Version check skipped, last checked today')
return return
worker = VersionWorker(last_check_date, get_version()) 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) worker.quit.connect(update_check_date)
# TODO: Use this to figure out if there's an Internet connection? # TODO: Use this to figure out if there's an Internet connection?
# worker.no_internet.connect(parent.on_no_internet) # worker.no_internet.connect(parent.on_no_internet)
run_thread(parent, worker, 'version') run_thread(worker, 'version')
def get_version(): def get_version():

View File

@ -27,24 +27,23 @@ from time import sleep
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common import is_win
from openlp.core.common.i18n import translate 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.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.forms.songselectdialog import Ui_SongSelectDialog
from openlp.plugins.songs.lib.songselect import SongSelectImport from openlp.plugins.songs.lib.songselect import SongSelectImport
log = logging.getLogger(__name__) 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. Run the actual SongSelect search, and notify the GUI when we find each song.
""" """
show_info = QtCore.pyqtSignal(str, str) show_info = QtCore.pyqtSignal(str, str)
found_song = QtCore.pyqtSignal(dict) found_song = QtCore.pyqtSignal(dict)
finished = QtCore.pyqtSignal() finished = QtCore.pyqtSignal()
quit = QtCore.pyqtSignal()
def __init__(self, importer, search_text): def __init__(self, importer, search_text):
super().__init__() super().__init__()
@ -74,7 +73,7 @@ class SearchWorker(QtCore.QObject):
self.found_song.emit(song) 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. The :class:`SongSelectForm` class is the SongSelect dialog.
""" """
@ -90,8 +89,6 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
""" """
Initialise the SongSelectForm Initialise the SongSelectForm
""" """
self.thread = None
self.worker = None
self.song_count = 0 self.song_count = 0
self.song = None self.song = None
self.set_progress_visible(False) self.set_progress_visible(False)
@ -311,17 +308,11 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
search_history = self.search_combobox.getItems() search_history = self.search_combobox.getItems()
Settings().setValue(self.plugin.settings_section + '/songselect searches', '|'.join(search_history)) Settings().setValue(self.plugin.settings_section + '/songselect searches', '|'.join(search_history))
# Create thread and run search # Create thread and run search
self.thread = QtCore.QThread() worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText())
self.worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText()) worker.show_info.connect(self.on_search_show_info)
self.worker.moveToThread(self.thread) worker.found_song.connect(self.on_search_found_song)
self.thread.started.connect(self.worker.start) worker.finished.connect(self.on_search_finished)
self.worker.show_info.connect(self.on_search_show_info) run_thread(worker, 'songselect')
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()
def on_stop_button_clicked(self): def on_stop_button_clicked(self):
""" """
@ -408,16 +399,3 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
""" """
self.search_progress_bar.setVisible(is_visible) self.search_progress_bar.setVisible(is_visible)
self.stop_button.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()) Registry().register('service_list', MagicMock())
@patch('openlp.core.api.http.server.HttpWorker') @patch('openlp.core.api.http.server.HttpWorker')
@patch('openlp.core.api.http.server.QtCore.QThread') @patch('openlp.core.api.http.server.run_thread')
def test_server_start(self, mock_qthread, mock_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', 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):
""" """
Test the starting of the Waitress Server with the disable flag set off Test the starting of the Waitress Server with the disable flag set off
""" """
@ -68,5 +53,20 @@ class TestHttpServer(TestCase):
HttpServer() HttpServer()
# THEN: the api environment should have been created # THEN: the api environment should have been created
assert mock_qthread.call_count == 0, 'The qthread should not have have been called' assert mocked_run_thread.call_count == 1, 'The qthread should have been called once'
assert mock_thread.call_count == 0, 'The http thread should not have been called' 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() self.destroy_settings()
@patch('openlp.core.api.websockets.WebSocketWorker') @patch('openlp.core.api.websockets.WebSocketWorker')
@patch('openlp.core.api.websockets.QtCore.QThread') @patch('openlp.core.api.websockets.run_thread')
def test_serverstart(self, mock_qthread, mock_worker): def test_serverstart(self, mocked_run_thread, MockWebSocketWorker):
""" """
Test the starting of the WebSockets Server with the disabled flag set on Test the starting of the WebSockets Server with the disabled flag set on
""" """
# GIVEN: A new httpserver # GIVEN: A new httpserver
# WHEN: I start the server # WHEN: I start the server
Registry().set_flag('no_web_server', True) Registry().set_flag('no_web_server', False)
WebSocketServer() WebSocketServer()
# THEN: the api environment should have been created # THEN: the api environment should have been created
assert mock_qthread.call_count == 1, 'The qthread should have been called once' assert mocked_run_thread.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 MockWebSocketWorker.call_count == 1, 'The http thread should have been called once'
@patch('openlp.core.api.websockets.WebSocketWorker') @patch('openlp.core.api.websockets.WebSocketWorker')
@patch('openlp.core.api.websockets.QtCore.QThread') @patch('openlp.core.api.websockets.run_thread')
def test_serverstart_not_required(self, mock_qthread, mock_worker): def test_serverstart_not_required(self, mocked_run_thread, MockWebSocketWorker):
""" """
Test the starting of the WebSockets Server with the disabled flag set off Test the starting of the WebSockets Server with the disabled flag set off
""" """
# GIVEN: A new httpserver and the server is not required # GIVEN: A new httpserver and the server is not required
# WHEN: I start the server # WHEN: I start the server
Registry().set_flag('no_web_server', False) Registry().set_flag('no_web_server', True)
WebSocketServer() WebSocketServer()
# THEN: the api environment should have been created # THEN: the api environment should have been created
assert mock_qthread.call_count == 0, 'The qthread should not have been called' assert mocked_run_thread.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 MockWebSocketWorker.call_count == 0, 'The http thread should not have been called'
def test_main_poll(self): def test_main_poll(self):
""" """

View File

@ -27,7 +27,7 @@ import tempfile
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch 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 openlp.core.common.path import Path
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
@ -235,7 +235,7 @@ class TestHttpUtils(TestCase, TestMixin):
mocked_requests.get.side_effect = OSError mocked_requests.get.side_effect = OSError
# WHEN: Attempt to retrieve a file # 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 # 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 # 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 os
import time import time
from threading import Lock from threading import Lock
from unittest import TestCase from unittest import TestCase, skip
from unittest.mock import patch from unittest.mock import MagicMock, patch
from PyQt5 import QtGui from PyQt5 import QtGui
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
from openlp.core.display.screens import ScreenList 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.helpers.testmixin import TestMixin
from tests.utils.constants import RESOURCE_PATH from tests.utils.constants import RESOURCE_PATH
TEST_PATH = str(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): class TestImageManager(TestCase, TestMixin):
def setUp(self): 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 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.stop_manager = True
self.image_manager.image_thread.wait()
del self.app 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 Test the Image Manager setup basic functionality
""" """
@ -86,7 +179,8 @@ class TestImageManager(TestCase, TestMixin):
self.image_manager.get_image(TEST_PATH, 'church1.jpg') self.image_manager.get_image(TEST_PATH, 'church1.jpg')
assert context.exception is not '', 'KeyError exception should have been thrown for missing image' 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 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) 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' 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 Test the process_cache method
""" """
with patch('openlp.core.lib.imagemanager.resize_image') as mocked_resize_image, \ # GIVEN: Mocked functions
patch('openlp.core.lib.imagemanager.image_to_byte') as mocked_image_to_byte: mocked_resize_image.side_effect = self.mocked_resize_image
# GIVEN: Mocked functions mocked_image_to_byte.side_effect = self.mocked_image_to_byte
mocked_resize_image.side_effect = self.mocked_resize_image image1 = 'church.jpg'
mocked_image_to_byte.side_effect = self.mocked_image_to_byte image2 = 'church2.jpg'
image1 = 'church.jpg' image3 = 'church3.jpg'
image2 = 'church2.jpg' image4 = 'church4.jpg'
image3 = 'church3.jpg'
image4 = 'church4.jpg'
# WHEN: Add the images. Then get the lock (=queue can not be processed). # WHEN: Add the images. Then get the lock (=queue can not be processed).
self.lock.acquire() self.lock.acquire()
self.image_manager.add_image(TEST_PATH, image1, None) self.image_manager.add_image(TEST_PATH, image1, None)
self.image_manager.add_image(TEST_PATH, image2, 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 # 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). # is being processed (see mocked methods/functions).
# Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the # # Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the #
# priority is adjusted to Priority.Lowest). # 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(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'" assert self.get_image_priority(image2) == Priority.Normal, "image2's priority should be 'Priority.Normal'"
# WHEN: Add more images. # WHEN: Add more images.
self.image_manager.add_image(TEST_PATH, image3, None) self.image_manager.add_image(TEST_PATH, image3, None)
self.image_manager.add_image(TEST_PATH, image4, None) self.image_manager.add_image(TEST_PATH, image4, None)
# Allow the queue to process. # Allow the queue to process.
self.lock.release() self.lock.release()
# Request some "data". # Request some "data".
self.image_manager.get_image_bytes(TEST_PATH, image4) self.image_manager.get_image_bytes(TEST_PATH, image4)
self.image_manager.get_image(TEST_PATH, image3) self.image_manager.get_image(TEST_PATH, image3)
# Now the mocked methods/functions do not have to sleep anymore. # Now the mocked methods/functions do not have to sleep anymore.
self.sleep_time = 0 self.sleep_time = 0
# Wait for the queue to finish. # Wait for the queue to finish.
while not self.image_manager._conversion_queue.empty(): while not self.image_manager._conversion_queue.empty():
time.sleep(0.1)
# Because empty() is not reliable, wait a litte; just to make sure.
time.sleep(0.1) time.sleep(0.1)
# THEN: The images' priority reflect how they were processed. # Because empty() is not reliable, wait a litte; just to make sure.
assert self.image_manager._conversion_queue.qsize() == 0, "The queue should be empty." time.sleep(0.1)
assert self.get_image_priority(image1) == Priority.Lowest, \ # THEN: The images' priority reflect how they were processed.
"The image should have not been requested (=Lowest)" assert self.image_manager._conversion_queue.qsize() == 0, "The queue should be empty."
assert self.get_image_priority(image2) == Priority.Lowest, \ assert self.get_image_priority(image1) == Priority.Lowest, \
"The image should have not been requested (=Lowest)" "The image should have not been requested (=Lowest)"
assert self.get_image_priority(image3) == Priority.Low, \ assert self.get_image_priority(image2) == Priority.Lowest, \
"Only the QImage should have been requested (=Low)." "The image should have not been requested (=Lowest)"
assert self.get_image_priority(image4) == Priority.Urgent, \ assert self.get_image_priority(image3) == Priority.Low, \
"The image bytes should have been requested (=Urgent)." "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): def get_image_priority(self, image):
""" """

View File

@ -36,14 +36,15 @@ def test_parse_options_basic():
""" """
# GIVEN: a a set of system arguments. # GIVEN: a a set of system arguments.
sys.argv[1:] = [] sys.argv[1:] = []
# WHEN: We we parse them to expand to options # WHEN: We we parse them to expand to options
args = parse_options(None) args = parse_options()
# THEN: the following fields will have been extracted. # THEN: the following fields will have been extracted.
assert args.dev_version is False, 'The dev_version flag should be False' 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.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.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.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' 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. # GIVEN: a a set of system arguments.
sys.argv[1:] = ['-l debug'] sys.argv[1:] = ['-l debug']
# WHEN: We we parse them to expand to options # WHEN: We we parse them to expand to options
args = parse_options(None) args = parse_options()
# THEN: the following fields will have been extracted. # THEN: the following fields will have been extracted.
assert args.dev_version is False, 'The dev_version flag should be False' 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.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.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.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' 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. # GIVEN: a a set of system arguments.
sys.argv[1:] = ['--portable'] sys.argv[1:] = ['--portable']
# WHEN: We we parse them to expand to options # WHEN: We we parse them to expand to options
args = parse_options(None) args = parse_options()
# THEN: the following fields will have been extracted. # THEN: the following fields will have been extracted.
assert args.dev_version is False, 'The dev_version flag should be False' 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.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.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.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' 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. # GIVEN: a a set of system arguments.
sys.argv[1:] = ['-l debug', '-d'] sys.argv[1:] = ['-l debug', '-d']
# WHEN: We we parse them to expand to options # WHEN: We we parse them to expand to options
args = parse_options(None) args = parse_options()
# THEN: the following fields will have been extracted. # THEN: the following fields will have been extracted.
assert args.dev_version is True, 'The dev_version flag should be True' 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.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.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.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' 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. # GIVEN: a a set of system arguments.
sys.argv[1:] = ['dummy_temp'] sys.argv[1:] = ['dummy_temp']
# WHEN: We we parse them to expand to options # WHEN: We we parse them to expand to options
args = parse_options(None) args = parse_options()
# THEN: the following fields will have been extracted. # THEN: the following fields will have been extracted.
assert args.dev_version is False, 'The dev_version flag should be False' 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.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.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.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' 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. # GIVEN: a a set of system arguments.
sys.argv[1:] = ['-l debug', 'dummy_temp'] sys.argv[1:] = ['-l debug', 'dummy_temp']
# WHEN: We we parse them to expand to options # WHEN: We we parse them to expand to options
args = parse_options(None) args = parse_options()
# THEN: the following fields will have been extracted. # THEN: the following fields will have been extracted.
assert args.dev_version is False, 'The dev_version flag should be False' 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.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.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.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' 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 # WHEN: The load() method is run
with patch.object(player, 'check_media') as mocked_check_media, \ 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 mocked_check_media.return_value = False
result = player.load(mocked_display) result = player.load(mocked_display)
@ -461,8 +461,9 @@ class TestSystemPlayer(TestCase):
assert expected_info == result assert expected_info == result
@patch('openlp.core.ui.media.systemplayer.CheckMediaWorker') @patch('openlp.core.ui.media.systemplayer.CheckMediaWorker')
@patch('openlp.core.ui.media.systemplayer.QtCore.QThread') @patch('openlp.core.ui.media.systemplayer.run_thread')
def test_check_media(self, MockQThread, MockCheckMediaWorker): @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 Test the check_media() method of the SystemPlayer
""" """
@ -472,12 +473,8 @@ class TestSystemPlayer(TestCase):
Registry().create() Registry().create()
Registry().register('application', mocked_application) Registry().register('application', mocked_application)
player = SystemPlayer(self) player = SystemPlayer(self)
mocked_thread = MagicMock() mocked_is_thread_finished.side_effect = [False, True]
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 = MagicMock()
mocked_check_media_worker.play = 'play'
mocked_check_media_worker.result = True mocked_check_media_worker.result = True
MockCheckMediaWorker.return_value = mocked_check_media_worker MockCheckMediaWorker.return_value = mocked_check_media_worker
@ -485,14 +482,11 @@ class TestSystemPlayer(TestCase):
result = player.check_media(valid_file) result = player.check_media(valid_file)
# THEN: It should return True # THEN: It should return True
MockQThread.assert_called_once_with()
MockCheckMediaWorker.assert_called_once_with(valid_file) MockCheckMediaWorker.assert_called_once_with(valid_file)
mocked_check_media_worker.setVolume.assert_called_once_with(0) mocked_check_media_worker.setVolume.assert_called_once_with(0)
mocked_check_media_worker.moveToThread.assert_called_once_with(mocked_thread) mocked_run_thread.assert_called_once_with(mocked_check_media_worker, 'check_media')
mocked_check_media_worker.finished.connect.assert_called_once_with('quit') mocked_is_thread_finished.assert_called_with('check_media')
mocked_thread.started.connect.assert_called_once_with('play') assert mocked_is_thread_finished.call_count == 2, 'is_thread_finished() should have been called twice'
mocked_thread.start.assert_called_once_with()
assert 2 == mocked_thread.isRunning.call_count
mocked_application.processEvents.assert_called_once_with() mocked_application.processEvents.assert_called_once_with()
assert result is True assert result is True
@ -523,12 +517,12 @@ class TestCheckMediaWorker(TestCase):
# WHEN: signals() is called with media and BufferedMedia # WHEN: signals() is called with media and BufferedMedia
with patch.object(worker, 'stop') as mocked_stop, \ 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) worker.signals('media', worker.BufferedMedia)
# THEN: The worker should exit and the result should be True # THEN: The worker should exit and the result should be True
mocked_stop.assert_called_once_with() 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 assert worker.result is True
def test_signals_error(self): def test_signals_error(self):
@ -540,10 +534,10 @@ class TestCheckMediaWorker(TestCase):
# WHEN: signals() is called with error and BufferedMedia # WHEN: signals() is called with error and BufferedMedia
with patch.object(worker, 'stop') as mocked_stop, \ 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) worker.signals('error', None)
# THEN: The worker should exit and the result should be True # THEN: The worker should exit and the result should be True
mocked_stop.assert_called_once_with() 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 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.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.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_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' assert frw.has_run_wizard is False, 'has_run_wizard should be False'
def test_set_defaults(self): 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.count.assert_called_with()
mocked_display_combo_box.setCurrentIndex.assert_called_with(1) 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 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 # 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 = FirstTimeForm(None)
frw.initialize(MagicMock()) frw.initialize(MagicMock())
mocked_worker = MagicMock() frw.theme_screenshot_threads = ['test_thread']
mocked_thread = MagicMock() with patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor:
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:
# WHEN: on_cancel_button_clicked() is called # WHEN: on_cancel_button_clicked() is called
frw.on_cancel_button_clicked() frw.on_cancel_button_clicked()
# THEN: The right things should be called in the right order # 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' 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_worker.set_download_canceled.assert_called_with(True)
mocked_thread.isRunning.assert_called_with() mocked_is_thread_finished.assert_called_with('test_thread')
assert 2 == mocked_thread.isRunning.call_count, 'isRunning() should have been called twice' assert mocked_is_thread_finished.call_count == 2, 'isRunning() should have been called twice'
mocked_time.sleep.assert_called_with(0.1) mocked_time.sleep.assert_called_once_with(0.1)
assert 1 == mocked_time.sleep.call_count, 'sleep() should have only been called once' mocked_set_normal_cursor.assert_called_once_with()
mocked_set_normal_cursor.assert_called_with()
def test_broken_config(self): def test_broken_config(self):
""" """

View File

@ -60,9 +60,10 @@ class TestMainWindow(TestCase, TestMixin):
# Mock cursor busy/normal methods. # Mock cursor busy/normal methods.
self.app.set_busy_cursor = MagicMock() self.app.set_busy_cursor = MagicMock()
self.app.set_normal_cursor = MagicMock() self.app.set_normal_cursor = MagicMock()
self.app.process_events = MagicMock()
self.app.args = [] self.app.args = []
Registry().register('application', self.app) 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.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 = self.add_toolbar_action_patcher.start()
self.mocked_add_toolbar_action.side_effect = self._create_mock_action 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 Delete all the C++ objects and stop all the patchers
""" """
self.add_toolbar_action_patcher.stop()
del self.main_window del self.main_window
self.add_toolbar_action_patcher.stop()
def test_cmd_line_file(self): def test_cmd_line_file(self):
""" """
@ -92,20 +93,20 @@ class TestMainWindow(TestCase, TestMixin):
# THEN the service from the arguments is loaded # THEN the service from the arguments is loaded
mocked_load_file.assert_called_with(service) 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. Test that passing a non service file does nothing.
""" """
# GIVEN a non service file as an argument to openlp # GIVEN a non service file as an argument to openlp
service = os.path.join('openlp.py') service = os.path.join('openlp.py')
self.main_window.arguments = [service] self.main_window.arguments = [service]
with patch('openlp.core.ui.servicemanager.ServiceManager.load_file') as mocked_load_file:
# WHEN the argument is processed # WHEN the argument is processed
self.main_window.open_cmd_line_files("") self.main_window.open_cmd_line_files(service)
# THEN the file should not be opened # THEN the file should not be opened
assert mocked_load_file.called is False, 'load_file should not have been called' assert mocked_load_file.called is False, 'load_file should not have been called'
def test_main_window_title(self): def test_main_window_title(self):
""" """

View File

@ -765,9 +765,9 @@ class TestSongSelectForm(TestCase, TestMixin):
assert ssform.search_combobox.isEnabled() is True assert ssform.search_combobox.isEnabled() is True
@patch('openlp.plugins.songs.forms.songselectform.Settings') @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') @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. 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.set_normal_cursor = MagicMock()
self.app.args = [] self.app.args = []
Registry().register('application', self.app) 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. # Mock classes and methods used by mainwindow.
with patch('openlp.core.ui.mainwindow.SettingsForm') as mocked_settings_form, \ with patch('openlp.core.ui.mainwindow.SettingsForm'), \
patch('openlp.core.ui.mainwindow.ImageManager') as mocked_image_manager, \ patch('openlp.core.ui.mainwindow.ImageManager'), \
patch('openlp.core.ui.mainwindow.LiveController') as mocked_live_controller, \ patch('openlp.core.ui.mainwindow.LiveController'), \
patch('openlp.core.ui.mainwindow.PreviewController') as mocked_preview_controller, \ patch('openlp.core.ui.mainwindow.PreviewController'), \
patch('openlp.core.ui.mainwindow.OpenLPDockWidget') as mocked_dock_widget, \ patch('openlp.core.ui.mainwindow.OpenLPDockWidget'), \
patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox') as mocked_q_tool_box_class, \ patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox'), \
patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget') as mocked_add_dock_method, \ patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget'), \
patch('openlp.core.ui.mainwindow.ServiceManager') as mocked_service_manager, \ patch('openlp.core.ui.mainwindow.ServiceManager'), \
patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \ patch('openlp.core.ui.mainwindow.ThemeManager'), \
patch('openlp.core.ui.mainwindow.ProjectorManager') as mocked_projector_manager, \ patch('openlp.core.ui.mainwindow.ProjectorManager'), \
patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \ patch('openlp.core.ui.mainwindow.Renderer'), \
patch('openlp.core.ui.mainwindow.websockets.WebSocketServer') as mocked_websocketserver, \ patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \
patch('openlp.core.ui.mainwindow.server.HttpServer') as mocked_httpserver: patch('openlp.core.ui.mainwindow.server.HttpServer'):
self.main_window = MainWindow() self.main_window = MainWindow()
def tearDown(self): def tearDown(self):