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