Merge lp:~raoul-snyman/openlp/better-threading into lp:openlp

Proposed by Raoul Snyman
Status: Superseded
Proposed branch: lp:~raoul-snyman/openlp/better-threading
Merge into: lp:openlp
Diff against target: 3036 lines (+638/-970)
26 files modified
openlp/core/api/deploy.py (+4/-4)
openlp/core/api/http/server.py (+19/-20)
openlp/core/api/websockets.py (+72/-89)
openlp/core/app.py (+2/-5)
openlp/core/common/applocation.py (+1/-1)
openlp/core/common/httputils.py (+8/-7)
openlp/core/lib/imagemanager.py (+22/-13)
openlp/core/projectors/manager.py (+1/-2)
openlp/core/projectors/pjlink.py (+2/-2)
openlp/core/threading.py (+70/-14)
openlp/core/ui/firsttimeform.py (+36/-39)
openlp/core/ui/mainwindow.py (+44/-36)
openlp/core/ui/media/systemplayer.py (+14/-14)
openlp/core/version.py (+6/-7)
openlp/plugins/songs/forms/songselectform.py (+9/-31)
tests/functional/openlp_core/api/http/test_http.py (+19/-19)
tests/functional/openlp_core/api/test_websockets.py (+10/-10)
tests/functional/openlp_core/common/test_httputils.py (+2/-2)
tests/functional/openlp_core/lib/test_image_manager.py (+148/-53)
tests/functional/openlp_core/test_app.py (+18/-12)
tests/functional/openlp_core/test_threading.py (+89/-0)
tests/functional/openlp_core/ui/media/test_systemplayer.py (+0/-549)
tests/functional/openlp_core/ui/test_firsttimeform.py (+15/-15)
tests/functional/openlp_core/ui/test_mainwindow.py (+11/-10)
tests/functional/openlp_plugins/songs/test_songselect.py (+2/-2)
tests/interfaces/openlp_core/ui/test_mainwindow.py (+14/-14)
To merge this branch: bzr merge lp:~raoul-snyman/openlp/better-threading
Reviewer Review Type Date Requested Status
Phill Needs Fixing
Tim Bentley Needs Fixing
Review via email: mp+335801@code.launchpad.net

This proposal has been superseded by a proposal from 2018-01-07.

Description of the change

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 in MainWindow
- Tidied up closeEvent in MainWindow a bit

Bugs fixed:

- Logs have returned to the cache dir when XDG is around
- Flipped the --no-web-server flag (now False is off, not on)
- Fixed a call to reload_bibles()

Other things done:

- Removed the --style option (it never worked)
- Renamed "url_get_file() to download_file()
- Standardised a callback object for download_file()

Add this to your merge proposal:
--------------------------------------------------------------------------------
lp:~raoul-snyman/openlp/better-threading (revision 2803)
https://ci.openlp.io/job/Branch-01-Pull/2413/ [SUCCESS]
https://ci.openlp.io/job/Branch-02a-Linux-Tests/2314/ [SUCCESS]
https://ci.openlp.io/job/Branch-02b-macOS-Tests/109/ [SUCCESS]
https://ci.openlp.io/job/Branch-03a-Build-Source/32/ [SUCCESS]
https://ci.openlp.io/job/Branch-03b-Build-macOS/31/ [SUCCESS]
https://ci.openlp.io/job/Branch-04a-Code-Analysis/1494/ [SUCCESS]
https://ci.openlp.io/job/Branch-04b-Test-Coverage/1307/ [SUCCESS]
https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/258/ [FAILURE]
Stopping after failure

Failed builds:
 - Branch-05-AppVeyor-Tests #258: https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/258/console

To post a comment you must log in.
Revision history for this message
Tim Bentley (trb143) wrote :

Very nice.
3 Minor questions.

review: Needs Fixing
Revision history for this message
Phill (phill-ridout) wrote :

Just a few minor things

review: Needs Fixing
Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Replied to Tim's questions.

Revision history for this message
Raoul Snyman (raoul-snyman) :
Revision history for this message
Raoul Snyman (raoul-snyman) :
2804. By Raoul Snyman

Fix some issues highlighted by Tim and Phill, and added a file that was erroneously removed

2805. By Raoul Snyman

Fix the tests I now added back in

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'openlp/core/api/deploy.py'
--- openlp/core/api/deploy.py 2017-12-29 09:15:48 +0000
+++ openlp/core/api/deploy.py 2018-01-07 18:07:40 +0000
@@ -25,7 +25,7 @@
25from zipfile import ZipFile25from zipfile import ZipFile
2626
27from openlp.core.common.applocation import AppLocation27from openlp.core.common.applocation import AppLocation
28from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size28from openlp.core.common.httputils import download_file, get_web_page, get_url_file_size
29from openlp.core.common.registry import Registry29from openlp.core.common.registry import Registry
3030
3131
@@ -65,7 +65,7 @@
65 sha256, version = download_sha256()65 sha256, version = download_sha256()
66 file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip')66 file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip')
67 callback.setRange(0, file_size)67 callback.setRange(0, file_size)
68 if url_get_file(callback, 'https://get.openlp.org/webclient/site.zip',68 if download_file(callback, 'https://get.openlp.org/webclient/site.zip',
69 AppLocation.get_section_data_path('remotes') / 'site.zip',69 AppLocation.get_section_data_path('remotes') / 'site.zip',
70 sha256=sha256):70 sha256=sha256):
71 deploy_zipfile(AppLocation.get_section_data_path('remotes'), 'site.zip')71 deploy_zipfile(AppLocation.get_section_data_path('remotes'), 'site.zip')
7272
=== modified file 'openlp/core/api/http/server.py'
--- openlp/core/api/http/server.py 2017-12-29 09:15:48 +0000
+++ openlp/core/api/http/server.py 2018-01-07 18:07:40 +0000
@@ -27,7 +27,7 @@
27import time27import time
2828
29from PyQt5 import QtCore, QtWidgets29from PyQt5 import QtCore, QtWidgets
30from waitress import serve30from waitress.server import create_server
3131
32from openlp.core.api.deploy import download_and_check, download_sha25632from openlp.core.api.deploy import download_and_check, download_sha256
33from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint33from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint
@@ -44,23 +44,16 @@
44from openlp.core.common.path import create_paths44from openlp.core.common.path import create_paths
45from openlp.core.common.registry import Registry, RegistryBase45from openlp.core.common.registry import Registry, RegistryBase
46from openlp.core.common.settings import Settings46from openlp.core.common.settings import Settings
47from openlp.core.threading import ThreadWorker, run_thread
4748
48log = logging.getLogger(__name__)49log = logging.getLogger(__name__)
4950
5051
51class HttpWorker(QtCore.QObject):52class HttpWorker(ThreadWorker):
52 """53 """
53 A special Qt thread class to allow the HTTP server to run at the same time as the UI.54 A special Qt thread class to allow the HTTP server to run at the same time as the UI.
54 """55 """
55 def __init__(self):56 def start(self):
56 """
57 Constructor for the thread class.
58
59 :param server: The http server class.
60 """
61 super(HttpWorker, self).__init__()
62
63 def run(self):
64 """57 """
65 Run the thread.58 Run the thread.
66 """59 """
@@ -68,12 +61,21 @@
68 port = Settings().value('api/port')61 port = Settings().value('api/port')
69 Registry().execute('get_website_version')62 Registry().execute('get_website_version')
70 try:63 try:
71 serve(application, host=address, port=port)64 self.server = create_server(application, host=address, port=port)
65 self.server.run()
72 except OSError:66 except OSError:
73 log.exception('An error occurred when serving the application.')67 log.exception('An error occurred when serving the application.')
68 self.quit.emit()
7469
75 def stop(self):70 def stop(self):
76 pass71 """
72 A method to stop the worker
73 """
74 if hasattr(self, 'server'):
75 # Loop through all the channels and close them to stop the server
76 for channel in self.server._map.values():
77 if hasattr(channel, 'close'):
78 channel.close()
7779
7880
79class HttpServer(RegistryBase, RegistryProperties, LogMixin):81class HttpServer(RegistryBase, RegistryProperties, LogMixin):
@@ -85,12 +87,9 @@
85 Initialise the http server, and start the http server87 Initialise the http server, and start the http server
86 """88 """
87 super(HttpServer, self).__init__(parent)89 super(HttpServer, self).__init__(parent)
88 if Registry().get_flag('no_web_server'):90 if not Registry().get_flag('no_web_server'):
89 self.worker = HttpWorker()91 worker = HttpWorker()
90 self.thread = QtCore.QThread()92 run_thread(worker, 'http_server')
91 self.worker.moveToThread(self.thread)
92 self.thread.started.connect(self.worker.run)
93 self.thread.start()
94 Registry().register_function('download_website', self.first_time)93 Registry().register_function('download_website', self.first_time)
95 Registry().register_function('get_website_version', self.website_version)94 Registry().register_function('get_website_version', self.website_version)
96 Registry().set_flag('website_version', '0.0')95 Registry().set_flag('website_version', '0.0')
@@ -167,7 +166,7 @@
167 self.was_cancelled = False166 self.was_cancelled = False
168 self.previous_size = 0167 self.previous_size = 0
169168
170 def _download_progress(self, count, block_size):169 def update_progress(self, count, block_size):
171 """170 """
172 Calculate and display the download progress.171 Calculate and display the download progress.
173 """172 """
174173
=== modified file 'openlp/core/api/websockets.py'
--- openlp/core/api/websockets.py 2017-12-29 09:15:48 +0000
+++ openlp/core/api/websockets.py 2018-01-07 18:07:40 +0000
@@ -28,37 +28,88 @@
28import logging28import logging
29import time29import time
3030
31import websockets31from websockets import serve
32from PyQt5 import QtCore
3332
34from openlp.core.common.mixins import LogMixin, RegistryProperties33from openlp.core.common.mixins import LogMixin, RegistryProperties
35from openlp.core.common.registry import Registry34from openlp.core.common.registry import Registry
36from openlp.core.common.settings import Settings35from openlp.core.common.settings import Settings
36from openlp.core.threading import ThreadWorker, run_thread
3737
38log = logging.getLogger(__name__)38log = logging.getLogger(__name__)
3939
4040
41class WebSocketWorker(QtCore.QObject):41async def handle_websocket(request, path):
42 """
43 Handle web socket requests and return the poll information
44
45 Check every 0.2 seconds to get the latest position and send if it changed. This only gets triggered when the first
46 client connects.
47
48 :param request: request from client
49 :param path: determines the endpoints supported
50 """
51 log.debug('WebSocket handler registered with client')
52 previous_poll = None
53 previous_main_poll = None
54 poller = Registry().get('poller')
55 if path == '/state':
56 while True:
57 current_poll = poller.poll()
58 if current_poll != previous_poll:
59 await request.send(json.dumps(current_poll).encode())
60 previous_poll = current_poll
61 await asyncio.sleep(0.2)
62 elif path == '/live_changed':
63 while True:
64 main_poll = poller.main_poll()
65 if main_poll != previous_main_poll:
66 await request.send(main_poll)
67 previous_main_poll = main_poll
68 await asyncio.sleep(0.2)
69
70
71class WebSocketWorker(ThreadWorker, RegistryProperties, LogMixin):
42 """72 """
43 A special Qt thread class to allow the WebSockets server to run at the same time as the UI.73 A special Qt thread class to allow the WebSockets server to run at the same time as the UI.
44 """74 """
45 def __init__(self, server):75 def start(self):
46 """76 """
47 Constructor for the thread class.77 Run the worker.
4878 """
49 :param server: The http server class.79 address = Settings().value('api/ip address')
50 """80 port = Settings().value('api/websocket port')
51 self.ws_server = server81 # Start the event loop
52 super(WebSocketWorker, self).__init__()82 self.event_loop = asyncio.new_event_loop()
5383 asyncio.set_event_loop(self.event_loop)
54 def run(self):84 # Create the websocker server
55 """85 loop = 1
56 Run the thread.86 self.server = None
57 """87 while not self.server:
58 self.ws_server.start_server()88 try:
89 self.server = serve(handle_websocket, address, port)
90 log.debug('WebSocket server started on {addr}:{port}'.format(addr=address, port=port))
91 except Exception:
92 log.exception('Failed to start WebSocket server')
93 loop += 1
94 time.sleep(0.1)
95 if not self.server and loop > 3:
96 log.error('Unable to start WebSocket server {addr}:{port}, giving up'.format(addr=address, port=port))
97 if self.server:
98 # If the websocket server exists, start listening
99 self.event_loop.run_until_complete(self.server)
100 self.event_loop.run_forever()
101 self.quit.emit()
59102
60 def stop(self):103 def stop(self):
61 self.ws_server.stop = True104 """
105 Stop the websocket server
106 """
107 if hasattr(self.server, 'ws_server'):
108 self.server.ws_server.close()
109 elif hasattr(self.server, 'server'):
110 self.server.server.close()
111 self.event_loop.stop()
112 self.event_loop.close()
62113
63114
64class WebSocketServer(RegistryProperties, LogMixin):115class WebSocketServer(RegistryProperties, LogMixin):
@@ -70,74 +121,6 @@
70 Initialise and start the WebSockets server121 Initialise and start the WebSockets server
71 """122 """
72 super(WebSocketServer, self).__init__()123 super(WebSocketServer, self).__init__()
73 if Registry().get_flag('no_web_server'):124 if not Registry().get_flag('no_web_server'):
74 self.settings_section = 'api'125 worker = WebSocketWorker()
75 self.worker = WebSocketWorker(self)126 run_thread(worker, 'websocket_server')
76 self.thread = QtCore.QThread()
77 self.worker.moveToThread(self.thread)
78 self.thread.started.connect(self.worker.run)
79 self.thread.start()
80
81 def start_server(self):
82 """
83 Start the correct server and save the handler
84 """
85 address = Settings().value(self.settings_section + '/ip address')
86 port = Settings().value(self.settings_section + '/websocket port')
87 self.start_websocket_instance(address, port)
88 # If web socket server start listening
89 if hasattr(self, 'ws_server') and self.ws_server:
90 event_loop = asyncio.new_event_loop()
91 asyncio.set_event_loop(event_loop)
92 event_loop.run_until_complete(self.ws_server)
93 event_loop.run_forever()
94 else:
95 log.debug('Failed to start ws server on port {port}'.format(port=port))
96
97 def start_websocket_instance(self, address, port):
98 """
99 Start the server
100
101 :param address: The server address
102 :param port: The run port
103 """
104 loop = 1
105 while loop < 4:
106 try:
107 self.ws_server = websockets.serve(self.handle_websocket, address, port)
108 log.debug("Web Socket Server started for class {address} {port}".format(address=address, port=port))
109 break
110 except Exception as e:
111 log.error('Failed to start ws server {why}'.format(why=e))
112 loop += 1
113 time.sleep(0.1)
114
115 @staticmethod
116 async def handle_websocket(request, path):
117 """
118 Handle web socket requests and return the poll information.
119 Check ever 0.2 seconds to get the latest position and send if changed.
120 Only gets triggered when 1st client attaches
121
122 :param request: request from client
123 :param path: determines the endpoints supported
124 :return:
125 """
126 log.debug("web socket handler registered with client")
127 previous_poll = None
128 previous_main_poll = None
129 poller = Registry().get('poller')
130 if path == '/state':
131 while True:
132 current_poll = poller.poll()
133 if current_poll != previous_poll:
134 await request.send(json.dumps(current_poll).encode())
135 previous_poll = current_poll
136 await asyncio.sleep(0.2)
137 elif path == '/live_changed':
138 while True:
139 main_poll = poller.main_poll()
140 if main_poll != previous_main_poll:
141 await request.send(main_poll)
142 previous_main_poll = main_poll
143 await asyncio.sleep(0.2)
144127
=== modified file 'openlp/core/app.py'
--- openlp/core/app.py 2017-12-29 09:15:48 +0000
+++ openlp/core/app.py 2018-01-07 18:07:40 +0000
@@ -304,8 +304,7 @@
304 'off a USB flash drive (not implemented).')304 'off a USB flash drive (not implemented).')
305 parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',305 parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',
306 help='Ignore the version file and pull the version directly from Bazaar')306 help='Ignore the version file and pull the version directly from Bazaar')
307 parser.add_argument('-s', '--style', dest='style', help='Set the Qt5 style (passed directly to Qt5).')307 parser.add_argument('-w', '--no-web-server', dest='no_web_server', action='store_true',
308 parser.add_argument('-w', '--no-web-server', dest='no_web_server', action='store_false',
309 help='Turn off the Web and Socket Server ')308 help='Turn off the Web and Socket Server ')
310 parser.add_argument('rargs', nargs='?', default=[])309 parser.add_argument('rargs', nargs='?', default=[])
311 # Parse command line options and deal with them. Use args supplied pragmatically if possible.310 # Parse command line options and deal with them. Use args supplied pragmatically if possible.
@@ -343,8 +342,6 @@
343 log.setLevel(logging.WARNING)342 log.setLevel(logging.WARNING)
344 else:343 else:
345 log.setLevel(logging.INFO)344 log.setLevel(logging.INFO)
346 if args and args.style:
347 qt_args.extend(['-style', args.style])
348 # Throw the rest of the arguments at Qt, just in case.345 # Throw the rest of the arguments at Qt, just in case.
349 qt_args.extend(args.rargs)346 qt_args.extend(args.rargs)
350 # Bug #1018855: Set the WM_CLASS property in X11347 # Bug #1018855: Set the WM_CLASS property in X11
@@ -358,7 +355,7 @@
358 application.setOrganizationDomain('openlp.org')355 application.setOrganizationDomain('openlp.org')
359 application.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)356 application.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
360 application.setAttribute(QtCore.Qt.AA_DontCreateNativeWidgetSiblings, True)357 application.setAttribute(QtCore.Qt.AA_DontCreateNativeWidgetSiblings, True)
361 if args and args.portable:358 if args.portable:
362 application.setApplicationName('OpenLPPortable')359 application.setApplicationName('OpenLPPortable')
363 Settings.setDefaultFormat(Settings.IniFormat)360 Settings.setDefaultFormat(Settings.IniFormat)
364 # Get location OpenLPPortable.ini361 # Get location OpenLPPortable.ini
365362
=== modified file 'openlp/core/common/applocation.py'
--- openlp/core/common/applocation.py 2017-12-29 09:15:48 +0000
+++ openlp/core/common/applocation.py 2018-01-07 18:07:40 +0000
@@ -157,7 +157,7 @@
157 return directory157 return directory
158 return Path('/usr', 'share', 'openlp')158 return Path('/usr', 'share', 'openlp')
159 if XDG_BASE_AVAILABLE:159 if XDG_BASE_AVAILABLE:
160 if dir_type == AppLocation.DataDir or dir_type == AppLocation.CacheDir:160 if dir_type == AppLocation.DataDir:
161 return Path(BaseDirectory.xdg_data_home, 'openlp')161 return Path(BaseDirectory.xdg_data_home, 'openlp')
162 elif dir_type == AppLocation.CacheDir:162 elif dir_type == AppLocation.CacheDir:
163 return Path(BaseDirectory.xdg_cache_home, 'openlp')163 return Path(BaseDirectory.xdg_cache_home, 'openlp')
164164
=== modified file 'openlp/core/common/httputils.py'
--- openlp/core/common/httputils.py 2017-12-29 09:15:48 +0000
+++ openlp/core/common/httputils.py 2018-01-07 18:07:40 +0000
@@ -20,7 +20,7 @@
20# Temple Place, Suite 330, Boston, MA 02111-1307 USA #20# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
21###############################################################################21###############################################################################
22"""22"""
23The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP.23The :mod:`openlp.core.common.httputils` module provides the utility methods for downloading stuff.
24"""24"""
25import hashlib25import hashlib
26import logging26import logging
@@ -104,7 +104,7 @@
104 if retries >= CONNECTION_RETRIES:104 if retries >= CONNECTION_RETRIES:
105 raise ConnectionError('Unable to connect to {url}, see log for details'.format(url=url))105 raise ConnectionError('Unable to connect to {url}, see log for details'.format(url=url))
106 retries += 1106 retries += 1
107 except:107 except: # noqa
108 # Don't know what's happening, so reraise the original108 # Don't know what's happening, so reraise the original
109 log.exception('Unknown error when trying to connect to {url}'.format(url=url))109 log.exception('Unknown error when trying to connect to {url}'.format(url=url))
110 raise110 raise
@@ -136,12 +136,12 @@
136 continue136 continue
137137
138138
139def url_get_file(callback, url, file_path, sha256=None):139def download_file(update_object, url, file_path, sha256=None):
140 """"140 """"
141 Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any141 Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any
142 point. Returns False on download error.142 point. Returns False on download error.
143143
144 :param callback: the class which needs to be updated144 :param update_object: the object which needs to be updated
145 :param url: URL to download145 :param url: URL to download
146 :param file_path: Destination file146 :param file_path: Destination file
147 :param sha256: The check sum value to be checked against the download value147 :param sha256: The check sum value to be checked against the download value
@@ -158,13 +158,14 @@
158 hasher = hashlib.sha256()158 hasher = hashlib.sha256()
159 # Download until finished or canceled.159 # Download until finished or canceled.
160 for chunk in response.iter_content(chunk_size=block_size):160 for chunk in response.iter_content(chunk_size=block_size):
161 if callback.was_cancelled:161 if hasattr(update_object, 'was_cancelled') and update_object.was_cancelled:
162 break162 break
163 saved_file.write(chunk)163 saved_file.write(chunk)
164 if sha256:164 if sha256:
165 hasher.update(chunk)165 hasher.update(chunk)
166 block_count += 1166 block_count += 1
167 callback._download_progress(block_count, block_size)167 if hasattr(update_object, 'update_progress'):
168 update_object.update_progress(block_count, block_size)
168 response.close()169 response.close()
169 if sha256 and hasher.hexdigest() != sha256:170 if sha256 and hasher.hexdigest() != sha256:
170 log.error('sha256 sums did not match for file %s, got %s, expected %s', file_path, hasher.hexdigest(),171 log.error('sha256 sums did not match for file %s, got %s, expected %s', file_path, hasher.hexdigest(),
@@ -183,7 +184,7 @@
183 retries += 1184 retries += 1
184 time.sleep(0.1)185 time.sleep(0.1)
185 continue186 continue
186 if callback.was_cancelled and file_path.exists():187 if hasattr(update_object, 'was_cancelled') and update_object.was_cancelled and file_path.exists():
187 file_path.unlink()188 file_path.unlink()
188 return True189 return True
189190
190191
=== modified file 'openlp/core/lib/imagemanager.py'
--- openlp/core/lib/imagemanager.py 2017-12-29 09:15:48 +0000
+++ openlp/core/lib/imagemanager.py 2018-01-07 18:07:40 +0000
@@ -35,13 +35,14 @@
35from openlp.core.common.settings import Settings35from openlp.core.common.settings import Settings
36from openlp.core.display.screens import ScreenList36from openlp.core.display.screens import ScreenList
37from openlp.core.lib import resize_image, image_to_byte37from openlp.core.lib import resize_image, image_to_byte
38from openlp.core.threading import ThreadWorker, run_thread
3839
39log = logging.getLogger(__name__)40log = logging.getLogger(__name__)
4041
4142
42class ImageThread(QtCore.QThread):43class ImageWorker(ThreadWorker):
43 """44 """
44 A special Qt thread class to speed up the display of images. This is threaded so it loads the frames and generates45 A thread worker class to speed up the display of images. This is threaded so it loads the frames and generates
45 byte stream in background.46 byte stream in background.
46 """47 """
47 def __init__(self, manager):48 def __init__(self, manager):
@@ -51,14 +52,21 @@
51 ``manager``52 ``manager``
52 The image manager.53 The image manager.
53 """54 """
54 super(ImageThread, self).__init__(None)55 super().__init__()
55 self.image_manager = manager56 self.image_manager = manager
5657
57 def run(self):58 def start(self):
58 """59 """
59 Run the thread.60 Start the worker
60 """61 """
61 self.image_manager.process()62 self.image_manager.process()
63 self.quit.emit()
64
65 def stop(self):
66 """
67 Stop the worker
68 """
69 self.image_manager.stop_manager = True
6270
6371
64class Priority(object):72class Priority(object):
@@ -130,7 +138,7 @@
130138
131class PriorityQueue(queue.PriorityQueue):139class PriorityQueue(queue.PriorityQueue):
132 """140 """
133 Customised ``Queue.PriorityQueue``.141 Customised ``queue.PriorityQueue``.
134142
135 Each item in the queue must be a tuple with three values. The first value is the :class:`Image`'s ``priority``143 Each item in the queue must be a tuple with three values. The first value is the :class:`Image`'s ``priority``
136 attribute, the second value the :class:`Image`'s ``secondary_priority`` attribute. The last value the :class:`Image`144 attribute, the second value the :class:`Image`'s ``secondary_priority`` attribute. The last value the :class:`Image`
@@ -179,7 +187,6 @@
179 self.width = current_screen['size'].width()187 self.width = current_screen['size'].width()
180 self.height = current_screen['size'].height()188 self.height = current_screen['size'].height()
181 self._cache = {}189 self._cache = {}
182 self.image_thread = ImageThread(self)
183 self._conversion_queue = PriorityQueue()190 self._conversion_queue = PriorityQueue()
184 self.stop_manager = False191 self.stop_manager = False
185 Registry().register_function('images_regenerate', self.process_updates)192 Registry().register_function('images_regenerate', self.process_updates)
@@ -230,9 +237,13 @@
230 """237 """
231 Flush the queue to updated any data to update238 Flush the queue to updated any data to update
232 """239 """
233 # We want only one thread.240 try:
234 if not self.image_thread.isRunning():241 worker = ImageWorker(self)
235 self.image_thread.start()242 run_thread(worker, 'image_manager')
243 except KeyError:
244 # run_thread() will throw a KeyError if this thread already exists, so ignore it so that we don't
245 # try to start another thread when one is already running
246 pass
236247
237 def get_image(self, path, source, width=-1, height=-1):248 def get_image(self, path, source, width=-1, height=-1):
238 """249 """
@@ -305,9 +316,7 @@
305 if image.path == path and image.timestamp != os.stat(path).st_mtime:316 if image.path == path and image.timestamp != os.stat(path).st_mtime:
306 image.timestamp = os.stat(path).st_mtime317 image.timestamp = os.stat(path).st_mtime
307 self._reset_image(image)318 self._reset_image(image)
308 # We want only one thread.319 self.process_updates()
309 if not self.image_thread.isRunning():
310 self.image_thread.start()
311320
312 def process(self):321 def process(self):
313 """322 """
314323
=== modified file 'openlp/core/projectors/manager.py'
--- openlp/core/projectors/manager.py 2018-01-03 00:35:14 +0000
+++ openlp/core/projectors/manager.py 2018-01-07 18:07:40 +0000
@@ -308,8 +308,7 @@
308 self.settings_section = 'projector'308 self.settings_section = 'projector'
309 self.projectordb = projectordb309 self.projectordb = projectordb
310 self.projector_list = []310 self.projector_list = []
311 self.pjlink_udp = PJLinkUDP()311 self.pjlink_udp = PJLinkUDP(self.projector_list)
312 self.pjlink_udp.projector_list = self.projector_list
313 self.source_select_form = None312 self.source_select_form = None
314313
315 def bootstrap_initialise(self):314 def bootstrap_initialise(self):
316315
=== modified file 'openlp/core/projectors/pjlink.py'
--- openlp/core/projectors/pjlink.py 2018-01-03 00:35:14 +0000
+++ openlp/core/projectors/pjlink.py 2018-01-07 18:07:40 +0000
@@ -89,11 +89,11 @@
89 'SRCH' # Class 2 (reply is ACKN)89 'SRCH' # Class 2 (reply is ACKN)
90 ]90 ]
9191
92 def __init__(self, port=PJLINK_PORT):92 def __init__(self, projector_list, port=PJLINK_PORT):
93 """93 """
94 Initialize socket94 Initialize socket
95 """95 """
9696 self.projector_list = projector_list
97 self.port = port97 self.port = port
9898
9999
100100
=== modified file 'openlp/core/threading.py'
--- openlp/core/threading.py 2017-12-29 09:15:48 +0000
+++ openlp/core/threading.py 2018-01-07 18:07:40 +0000
@@ -24,26 +24,41 @@
24"""24"""
25from PyQt5 import QtCore25from PyQt5 import QtCore
2626
2727from openlp.core.common.registry import Registry
28def run_thread(parent, worker, prefix='', auto_start=True):28
29
30class ThreadWorker(QtCore.QObject):
31 """
32 The :class:`~openlp.core.threading.ThreadWorker` class provides a base class for all worker objects
33 """
34 quit = QtCore.pyqtSignal()
35
36 def start(self):
37 """
38 The start method is how the worker runs. Basically, put your code here.
39 """
40 raise NotImplementedError('Your base class needs to override this method and run self.quit.emit() at the end.')
41
42
43def run_thread(worker, thread_name, can_start=True):
29 """44 """
30 Create a thread and assign a worker to it. This removes a lot of boilerplate code from the codebase.45 Create a thread and assign a worker to it. This removes a lot of boilerplate code from the codebase.
3146
32 :param object parent: The parent object so that the thread and worker are not orphaned.
33 :param QObject worker: A QObject-based worker object which does the actual work.47 :param QObject worker: A QObject-based worker object which does the actual work.
34 :param str prefix: A prefix to be applied to the attribute names.48 :param str thread_name: The name of the thread, used to keep track of the thread.
35 :param bool auto_start: Automatically start the thread. Defaults to True.49 :param bool can_start: Start the thread. Defaults to True.
36 """50 """
37 # Set up attribute names51 if not thread_name:
38 thread_name = 'thread'52 raise ValueError('A thread_name is required when calling the "run_thread" function')
39 worker_name = 'worker'53 main_window = Registry().get('main_window')
40 if prefix:54 if thread_name in main_window.threads:
41 thread_name = '_'.join([prefix, thread_name])55 raise KeyError('A thread with the name "{}" has already been created, please use another'.format(thread_name))
42 worker_name = '_'.join([prefix, worker_name])
43 # Create the thread and add the thread and the worker to the parent56 # Create the thread and add the thread and the worker to the parent
44 thread = QtCore.QThread()57 thread = QtCore.QThread()
45 setattr(parent, thread_name, thread)58 main_window.threads[thread_name] = {
46 setattr(parent, worker_name, worker)59 'thread': thread,
60 'worker': worker
61 }
47 # Move the worker into the thread's context62 # Move the worker into the thread's context
48 worker.moveToThread(thread)63 worker.moveToThread(thread)
49 # Connect slots and signals64 # Connect slots and signals
@@ -51,5 +66,46 @@
51 worker.quit.connect(thread.quit)66 worker.quit.connect(thread.quit)
52 worker.quit.connect(worker.deleteLater)67 worker.quit.connect(worker.deleteLater)
53 thread.finished.connect(thread.deleteLater)68 thread.finished.connect(thread.deleteLater)
54 if auto_start:69 thread.finished.connect(make_remove_thread(thread_name))
70 if can_start:
55 thread.start()71 thread.start()
72
73
74def get_thread_worker(thread_name):
75 """
76 Get the worker by the thread name
77
78 :param str thread_name: The name of the thread
79 :returns: The worker for this thread name
80 """
81 return Registry().get('main_window').threads.get(thread_name)
82
83
84def is_thread_finished(thread_name):
85 """
86 Check if a thread is finished running.
87
88 :param str thread_name: The name of the thread
89 :returns: True if the thread is finished, False if it is still running
90 """
91 main_window = Registry().get('main_window')
92 return thread_name not in main_window.threads or main_window.threads[thread_name]['thread'].isFinished()
93
94
95def make_remove_thread(thread_name):
96 """
97 Create a function to remove the thread once the thread is finished.
98
99 :param str thread_name: The name of the thread which should be removed from the thread registry.
100 :returns: A function which will remove the thread from the thread registry.
101 """
102 def remove_thread():
103 """
104 Stop and remove a registered thread
105
106 :param str thread_name: The name of the thread to stop and remove
107 """
108 main_window = Registry().get('main_window')
109 if thread_name in main_window.threads:
110 del main_window.threads[thread_name]
111 return remove_thread
56112
=== modified file 'openlp/core/ui/firsttimeform.py'
--- openlp/core/ui/firsttimeform.py 2017-12-29 09:15:48 +0000
+++ openlp/core/ui/firsttimeform.py 2018-01-07 18:07:40 +0000
@@ -23,8 +23,6 @@
23This module contains the first time wizard.23This module contains the first time wizard.
24"""24"""
25import logging25import logging
26import os
27import socket
28import time26import time
29import urllib.error27import urllib.error
30import urllib.parse28import urllib.parse
@@ -36,7 +34,7 @@
3634
37from openlp.core.common import clean_button_text, trace_error_handler35from openlp.core.common import clean_button_text, trace_error_handler
38from openlp.core.common.applocation import AppLocation36from openlp.core.common.applocation import AppLocation
39from openlp.core.common.httputils import get_web_page, get_url_file_size, url_get_file, CONNECTION_TIMEOUT37from openlp.core.common.httputils import get_web_page, get_url_file_size, download_file
40from openlp.core.common.i18n import translate38from openlp.core.common.i18n import translate
41from openlp.core.common.mixins import RegistryProperties39from openlp.core.common.mixins import RegistryProperties
42from openlp.core.common.path import Path, create_paths40from openlp.core.common.path import Path, create_paths
@@ -44,46 +42,47 @@
44from openlp.core.common.settings import Settings42from openlp.core.common.settings import Settings
45from openlp.core.lib import PluginStatus, build_icon43from openlp.core.lib import PluginStatus, build_icon
46from openlp.core.lib.ui import critical_error_message_box44from openlp.core.lib.ui import critical_error_message_box
47from .firsttimewizard import UiFirstTimeWizard, FirstTimePage45from openlp.core.threading import ThreadWorker, run_thread, get_thread_worker, is_thread_finished
46from openlp.core.ui.firsttimewizard import UiFirstTimeWizard, FirstTimePage
4847
49log = logging.getLogger(__name__)48log = logging.getLogger(__name__)
5049
5150
52class ThemeScreenshotWorker(QtCore.QObject):51class ThemeScreenshotWorker(ThreadWorker):
53 """52 """
54 This thread downloads a theme's screenshot53 This thread downloads a theme's screenshot
55 """54 """
56 screenshot_downloaded = QtCore.pyqtSignal(str, str, str)55 screenshot_downloaded = QtCore.pyqtSignal(str, str, str)
57 finished = QtCore.pyqtSignal()
5856
59 def __init__(self, themes_url, title, filename, sha256, screenshot):57 def __init__(self, themes_url, title, filename, sha256, screenshot):
60 """58 """
61 Set up the worker object59 Set up the worker object
62 """60 """
63 self.was_download_cancelled = False61 self.was_cancelled = False
64 self.themes_url = themes_url62 self.themes_url = themes_url
65 self.title = title63 self.title = title
66 self.filename = filename64 self.filename = filename
67 self.sha256 = sha25665 self.sha256 = sha256
68 self.screenshot = screenshot66 self.screenshot = screenshot
69 socket.setdefaulttimeout(CONNECTION_TIMEOUT)67 super().__init__()
70 super(ThemeScreenshotWorker, self).__init__()
7168
72 def run(self):69 def start(self):
73 """70 """
74 Overridden method to run the thread.71 Run the worker
75 """72 """
76 if self.was_download_cancelled:73 if self.was_cancelled:
77 return74 return
78 try:75 try:
79 urllib.request.urlretrieve('{host}{name}'.format(host=self.themes_url, name=self.screenshot),76 download_path = Path(gettempdir()) / 'openlp' / self.screenshot
80 os.path.join(gettempdir(), 'openlp', self.screenshot))77 is_success = download_file(self, '{host}{name}'.format(host=self.themes_url, name=self.screenshot),
81 # Signal that the screenshot has been downloaded78 download_path)
82 self.screenshot_downloaded.emit(self.title, self.filename, self.sha256)79 if is_success and not self.was_cancelled:
83 except:80 # Signal that the screenshot has been downloaded
81 self.screenshot_downloaded.emit(self.title, self.filename, self.sha256)
82 except: # noqa
84 log.exception('Unable to download screenshot')83 log.exception('Unable to download screenshot')
85 finally:84 finally:
86 self.finished.emit()85 self.quit.emit()
8786
88 @QtCore.pyqtSlot(bool)87 @QtCore.pyqtSlot(bool)
89 def set_download_canceled(self, toggle):88 def set_download_canceled(self, toggle):
@@ -145,12 +144,13 @@
145 return FirstTimePage.Progress144 return FirstTimePage.Progress
146 elif self.currentId() == FirstTimePage.Themes:145 elif self.currentId() == FirstTimePage.Themes:
147 self.application.set_busy_cursor()146 self.application.set_busy_cursor()
148 while not all([thread.isFinished() for thread in self.theme_screenshot_threads]):147 while not all([is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]):
149 time.sleep(0.1)148 time.sleep(0.1)
150 self.application.process_events()149 self.application.process_events()
151 # Build the screenshot icons, as this can not be done in the thread.150 # Build the screenshot icons, as this can not be done in the thread.
152 self._build_theme_screenshots()151 self._build_theme_screenshots()
153 self.application.set_normal_cursor()152 self.application.set_normal_cursor()
153 self.theme_screenshot_threads = []
154 return FirstTimePage.Defaults154 return FirstTimePage.Defaults
155 else:155 else:
156 return self.get_next_page_id()156 return self.get_next_page_id()
@@ -171,7 +171,6 @@
171 self.screens = screens171 self.screens = screens
172 self.was_cancelled = False172 self.was_cancelled = False
173 self.theme_screenshot_threads = []173 self.theme_screenshot_threads = []
174 self.theme_screenshot_workers = []
175 self.has_run_wizard = False174 self.has_run_wizard = False
176175
177 def _download_index(self):176 def _download_index(self):
@@ -256,14 +255,10 @@
256 sha256 = self.config.get('theme_{theme}'.format(theme=theme), 'sha256', fallback='')255 sha256 = self.config.get('theme_{theme}'.format(theme=theme), 'sha256', fallback='')
257 screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot')256 screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot')
258 worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot)257 worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot)
259 self.theme_screenshot_workers.append(worker)
260 worker.screenshot_downloaded.connect(self.on_screenshot_downloaded)258 worker.screenshot_downloaded.connect(self.on_screenshot_downloaded)
261 thread = QtCore.QThread(self)259 thread_name = 'theme_screenshot_{title}'.format(title=title)
262 self.theme_screenshot_threads.append(thread)260 run_thread(worker, thread_name)
263 thread.started.connect(worker.run)261 self.theme_screenshot_threads.append(thread_name)
264 worker.finished.connect(thread.quit)
265 worker.moveToThread(thread)
266 thread.start()
267 self.application.process_events()262 self.application.process_events()
268263
269 def set_defaults(self):264 def set_defaults(self):
@@ -353,12 +348,14 @@
353 Process the triggering of the cancel button.348 Process the triggering of the cancel button.
354 """349 """
355 self.was_cancelled = True350 self.was_cancelled = True
356 if self.theme_screenshot_workers:351 if self.theme_screenshot_threads:
357 for worker in self.theme_screenshot_workers:352 for thread_name in self.theme_screenshot_threads:
358 worker.set_download_canceled(True)353 worker = get_thread_worker(thread_name)
354 if worker:
355 worker.set_download_canceled(True)
359 # Was the thread created.356 # Was the thread created.
360 if self.theme_screenshot_threads:357 if self.theme_screenshot_threads:
361 while any([thread.isRunning() for thread in self.theme_screenshot_threads]):358 while any([not is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]):
362 time.sleep(0.1)359 time.sleep(0.1)
363 self.application.set_normal_cursor()360 self.application.set_normal_cursor()
364361
@@ -562,8 +559,8 @@
562 self._increment_progress_bar(self.downloading.format(name=filename), 0)559 self._increment_progress_bar(self.downloading.format(name=filename), 0)
563 self.previous_size = 0560 self.previous_size = 0
564 destination = songs_destination_path / str(filename)561 destination = songs_destination_path / str(filename)
565 if not url_get_file(self, '{path}{name}'.format(path=self.songs_url, name=filename),562 if not download_file(self, '{path}{name}'.format(path=self.songs_url, name=filename),
566 destination, sha256):563 destination, sha256):
567 missed_files.append('Song: {name}'.format(name=filename))564 missed_files.append('Song: {name}'.format(name=filename))
568 # Download Bibles565 # Download Bibles
569 bibles_iterator = QtWidgets.QTreeWidgetItemIterator(self.bibles_tree_widget)566 bibles_iterator = QtWidgets.QTreeWidgetItemIterator(self.bibles_tree_widget)
@@ -573,8 +570,8 @@
573 bible, sha256 = item.data(0, QtCore.Qt.UserRole)570 bible, sha256 = item.data(0, QtCore.Qt.UserRole)
574 self._increment_progress_bar(self.downloading.format(name=bible), 0)571 self._increment_progress_bar(self.downloading.format(name=bible), 0)
575 self.previous_size = 0572 self.previous_size = 0
576 if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible),573 if not download_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible),
577 bibles_destination_path / bible, sha256):574 bibles_destination_path / bible, sha256):
578 missed_files.append('Bible: {name}'.format(name=bible))575 missed_files.append('Bible: {name}'.format(name=bible))
579 bibles_iterator += 1576 bibles_iterator += 1
580 # Download themes577 # Download themes
@@ -584,8 +581,8 @@
584 theme, sha256 = item.data(QtCore.Qt.UserRole)581 theme, sha256 = item.data(QtCore.Qt.UserRole)
585 self._increment_progress_bar(self.downloading.format(name=theme), 0)582 self._increment_progress_bar(self.downloading.format(name=theme), 0)
586 self.previous_size = 0583 self.previous_size = 0
587 if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme),584 if not download_file(self, '{path}{name}'.format(path=self.themes_url, name=theme),
588 themes_destination_path / theme, sha256):585 themes_destination_path / theme, sha256):
589 missed_files.append('Theme: {name}'.format(name=theme))586 missed_files.append('Theme: {name}'.format(name=theme))
590 if missed_files:587 if missed_files:
591 file_list = ''588 file_list = ''
592589
=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py 2017-12-29 09:15:48 +0000
+++ openlp/core/ui/mainwindow.py 2018-01-07 18:07:40 +0000
@@ -24,7 +24,6 @@
24"""24"""
25import logging25import logging
26import sys26import sys
27import time
28from datetime import datetime27from datetime import datetime
29from distutils import dir_util28from distutils import dir_util
30from distutils.errors import DistutilsFileError29from distutils.errors import DistutilsFileError
@@ -478,8 +477,7 @@
478 """477 """
479 super(MainWindow, self).__init__()478 super(MainWindow, self).__init__()
480 Registry().register('main_window', self)479 Registry().register('main_window', self)
481 self.version_thread = None480 self.threads = {}
482 self.version_worker = None
483 self.clipboard = self.application.clipboard()481 self.clipboard = self.application.clipboard()
484 self.arguments = ''.join(self.application.args)482 self.arguments = ''.join(self.application.args)
485 # Set up settings sections for the main application (not for use by plugins).483 # Set up settings sections for the main application (not for use by plugins).
@@ -501,8 +499,8 @@
501 Settings().set_up_default_values()499 Settings().set_up_default_values()
502 self.about_form = AboutForm(self)500 self.about_form = AboutForm(self)
503 MediaController()501 MediaController()
504 websockets.WebSocketServer()502 self.ws_server = websockets.WebSocketServer()
505 server.HttpServer()503 self.http_server = server.HttpServer(self)
506 SettingsForm(self)504 SettingsForm(self)
507 self.formatting_tag_form = FormattingTagForm(self)505 self.formatting_tag_form = FormattingTagForm(self)
508 self.shortcut_form = ShortcutListForm(self)506 self.shortcut_form = ShortcutListForm(self)
@@ -549,6 +547,41 @@
549 # Reset the cursor547 # Reset the cursor
550 self.application.set_normal_cursor()548 self.application.set_normal_cursor()
551549
550 def _wait_for_threads(self):
551 """
552 Wait for the threads
553 """
554 # Sometimes the threads haven't finished, let's wait for them
555 wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self)
556 wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
557 wait_dialog.setAutoClose(False)
558 wait_dialog.setCancelButton(None)
559 wait_dialog.show()
560 for thread_name in self.threads.keys():
561 log.debug('Waiting for thread %s', thread_name)
562 self.application.processEvents()
563 thread = self.threads[thread_name]['thread']
564 worker = self.threads[thread_name]['worker']
565 try:
566 if worker and hasattr(worker, 'stop'):
567 # If the worker has a stop method, run it
568 worker.stop()
569 if thread and thread.isRunning():
570 # If the thread is running, let's wait 5 seconds for it
571 retry = 0
572 while thread.isRunning() and retry < 50:
573 # Make the GUI responsive while we wait
574 self.application.processEvents()
575 thread.wait(100)
576 retry += 1
577 if thread.isRunning():
578 # If the thread is still running after 5 seconds, kill it
579 thread.terminate()
580 except RuntimeError:
581 # Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object
582 pass
583 wait_dialog.close()
584
552 def bootstrap_post_set_up(self):585 def bootstrap_post_set_up(self):
553 """586 """
554 process the bootstrap post setup request587 process the bootstrap post setup request
@@ -695,7 +728,7 @@
695 # Update the theme widget728 # Update the theme widget
696 self.theme_manager_contents.load_themes()729 self.theme_manager_contents.load_themes()
697 # Check if any Bibles downloaded. If there are, they will be processed.730 # Check if any Bibles downloaded. If there are, they will be processed.
698 Registry().execute('bibles_load_list', True)731 Registry().execute('bibles_load_list')
699 self.application.set_normal_cursor()732 self.application.set_normal_cursor()
700733
701 def is_display_blank(self):734 def is_display_blank(self):
@@ -1000,39 +1033,14 @@
1000 if not self.application.is_event_loop_active:1033 if not self.application.is_event_loop_active:
1001 event.ignore()1034 event.ignore()
1002 return1035 return
1003 # Sometimes the version thread hasn't finished, let's wait for it
1004 try:
1005 if self.version_thread and self.version_thread.isRunning():
1006 wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self)
1007 wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
1008 wait_dialog.setAutoClose(False)
1009 wait_dialog.setCancelButton(None)
1010 wait_dialog.show()
1011 retry = 0
1012 while self.version_thread.isRunning() and retry < 50:
1013 self.application.processEvents()
1014 self.version_thread.wait(100)
1015 retry += 1
1016 if self.version_thread.isRunning():
1017 self.version_thread.terminate()
1018 wait_dialog.close()
1019 except RuntimeError:
1020 # Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object
1021 pass
1022 # If we just did a settings import, close without saving changes.
1023 if self.settings_imported:
1024 self.clean_up(False)
1025 event.accept()
1026 if self.service_manager_contents.is_modified():1036 if self.service_manager_contents.is_modified():
1027 ret = self.service_manager_contents.save_modified_service()1037 ret = self.service_manager_contents.save_modified_service()
1028 if ret == QtWidgets.QMessageBox.Save:1038 if ret == QtWidgets.QMessageBox.Save:
1029 if self.service_manager_contents.decide_save_method():1039 if self.service_manager_contents.decide_save_method():
1030 self.clean_up()
1031 event.accept()1040 event.accept()
1032 else:1041 else:
1033 event.ignore()1042 event.ignore()
1034 elif ret == QtWidgets.QMessageBox.Discard:1043 elif ret == QtWidgets.QMessageBox.Discard:
1035 self.clean_up()
1036 event.accept()1044 event.accept()
1037 else:1045 else:
1038 event.ignore()1046 event.ignore()
@@ -1048,13 +1056,16 @@
1048 close_button.setText(translate('OpenLP.MainWindow', '&Exit OpenLP'))1056 close_button.setText(translate('OpenLP.MainWindow', '&Exit OpenLP'))
1049 msg_box.setDefaultButton(QtWidgets.QMessageBox.Close)1057 msg_box.setDefaultButton(QtWidgets.QMessageBox.Close)
1050 if msg_box.exec() == QtWidgets.QMessageBox.Close:1058 if msg_box.exec() == QtWidgets.QMessageBox.Close:
1051 self.clean_up()
1052 event.accept()1059 event.accept()
1053 else:1060 else:
1054 event.ignore()1061 event.ignore()
1055 else:1062 else:
1056 self.clean_up()
1057 event.accept()1063 event.accept()
1064 if event.isAccepted():
1065 # Wait for all the threads to complete
1066 self._wait_for_threads()
1067 # If we just did a settings import, close without saving changes.
1068 self.clean_up(save_settings=not self.settings_imported)
10581069
1059 def clean_up(self, save_settings=True):1070 def clean_up(self, save_settings=True):
1060 """1071 """
@@ -1062,9 +1073,6 @@
10621073
1063 :param save_settings: Switch to prevent saving settings. Defaults to **True**.1074 :param save_settings: Switch to prevent saving settings. Defaults to **True**.
1064 """1075 """
1065 self.image_manager.stop_manager = True
1066 while self.image_manager.image_thread.isRunning():
1067 time.sleep(0.1)
1068 if save_settings:1076 if save_settings:
1069 if Settings().value('advanced/save current plugin'):1077 if Settings().value('advanced/save current plugin'):
1070 Settings().setValue('advanced/current media plugin', self.media_tool_box.currentIndex())1078 Settings().setValue('advanced/current media plugin', self.media_tool_box.currentIndex())
10711079
=== modified file 'openlp/core/ui/media/systemplayer.py'
--- openlp/core/ui/media/systemplayer.py 2017-12-29 09:15:48 +0000
+++ openlp/core/ui/media/systemplayer.py 2018-01-07 18:07:40 +0000
@@ -31,6 +31,7 @@
31from openlp.core.common.i18n import translate31from openlp.core.common.i18n import translate
32from openlp.core.ui.media import MediaState32from openlp.core.ui.media import MediaState
33from openlp.core.ui.media.mediaplayer import MediaPlayer33from openlp.core.ui.media.mediaplayer import MediaPlayer
34from openlp.core.threading import ThreadWorker, run_thread, is_thread_finished
3435
35log = logging.getLogger(__name__)36log = logging.getLogger(__name__)
3637
@@ -293,39 +294,38 @@
293 :param path: Path to file to be checked294 :param path: Path to file to be checked
294 :return: True if file can be played otherwise False295 :return: True if file can be played otherwise False
295 """296 """
296 thread = QtCore.QThread()
297 check_media_worker = CheckMediaWorker(path)297 check_media_worker = CheckMediaWorker(path)
298 check_media_worker.setVolume(0)298 check_media_worker.setVolume(0)
299 check_media_worker.moveToThread(thread)299 run_thread(check_media_worker, 'check_media')
300 check_media_worker.finished.connect(thread.quit)300 while not is_thread_finished('check_media'):
301 thread.started.connect(check_media_worker.play)
302 thread.start()
303 while thread.isRunning():
304 self.application.processEvents()301 self.application.processEvents()
305 return check_media_worker.result302 return check_media_worker.result
306303
307304
308class CheckMediaWorker(QtMultimedia.QMediaPlayer):305class CheckMediaWorker(QtMultimedia.QMediaPlayer, ThreadWorker):
309 """306 """
310 Class used to check if a media file is playable307 Class used to check if a media file is playable
311 """308 """
312 finished = QtCore.pyqtSignal()
313
314 def __init__(self, path):309 def __init__(self, path):
315 super(CheckMediaWorker, self).__init__(None, QtMultimedia.QMediaPlayer.VideoSurface)310 super(CheckMediaWorker, self).__init__(None, QtMultimedia.QMediaPlayer.VideoSurface)
311 self.path = path
312
313 def start(self):
314 """
315 Start the thread worker
316 """
316 self.result = None317 self.result = None
317
318 self.error.connect(functools.partial(self.signals, 'error'))318 self.error.connect(functools.partial(self.signals, 'error'))
319 self.mediaStatusChanged.connect(functools.partial(self.signals, 'media'))319 self.mediaStatusChanged.connect(functools.partial(self.signals, 'media'))
320320 self.setMedia(QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile(self.path)))
321 self.setMedia(QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile(path)))321 self.play()
322322
323 def signals(self, origin, status):323 def signals(self, origin, status):
324 if origin == 'media' and status == self.BufferedMedia:324 if origin == 'media' and status == self.BufferedMedia:
325 self.result = True325 self.result = True
326 self.stop()326 self.stop()
327 self.finished.emit()327 self.quit.emit()
328 elif origin == 'error' and status != self.NoError:328 elif origin == 'error' and status != self.NoError:
329 self.result = False329 self.result = False
330 self.stop()330 self.stop()
331 self.finished.emit()331 self.quit.emit()
332332
=== modified file 'openlp/core/version.py'
--- openlp/core/version.py 2018-01-02 21:00:54 +0000
+++ openlp/core/version.py 2018-01-07 18:07:40 +0000
@@ -35,7 +35,7 @@
3535
36from openlp.core.common.applocation import AppLocation36from openlp.core.common.applocation import AppLocation
37from openlp.core.common.settings import Settings37from openlp.core.common.settings import Settings
38from openlp.core.threading import run_thread38from openlp.core.threading import ThreadWorker, run_thread
3939
40log = logging.getLogger(__name__)40log = logging.getLogger(__name__)
4141
@@ -44,14 +44,13 @@
44CONNECTION_RETRIES = 244CONNECTION_RETRIES = 2
4545
4646
47class VersionWorker(QtCore.QObject):47class VersionWorker(ThreadWorker):
48 """48 """
49 A worker class to fetch the version of OpenLP from the website. This is run from within a thread so that it49 A worker class to fetch the version of OpenLP from the website. This is run from within a thread so that it
50 doesn't affect the loading time of OpenLP.50 doesn't affect the loading time of OpenLP.
51 """51 """
52 new_version = QtCore.pyqtSignal(dict)52 new_version = QtCore.pyqtSignal(dict)
53 no_internet = QtCore.pyqtSignal()53 no_internet = QtCore.pyqtSignal()
54 quit = QtCore.pyqtSignal()
5554
56 def __init__(self, last_check_date, current_version):55 def __init__(self, last_check_date, current_version):
57 """56 """
@@ -110,22 +109,22 @@
110 Settings().setValue('core/last version test', date.today().strftime('%Y-%m-%d'))109 Settings().setValue('core/last version test', date.today().strftime('%Y-%m-%d'))
111110
112111
113def check_for_update(parent):112def check_for_update(main_window):
114 """113 """
115 Run a thread to download and check the version of OpenLP114 Run a thread to download and check the version of OpenLP
116115
117 :param MainWindow parent: The parent object for the thread. Usually the OpenLP main window.116 :param MainWindow main_window: The OpenLP main window.
118 """117 """
119 last_check_date = Settings().value('core/last version test')118 last_check_date = Settings().value('core/last version test')
120 if date.today().strftime('%Y-%m-%d') <= last_check_date:119 if date.today().strftime('%Y-%m-%d') <= last_check_date:
121 log.debug('Version check skipped, last checked today')120 log.debug('Version check skipped, last checked today')
122 return121 return
123 worker = VersionWorker(last_check_date, get_version())122 worker = VersionWorker(last_check_date, get_version())
124 worker.new_version.connect(parent.on_new_version)123 worker.new_version.connect(main_window.on_new_version)
125 worker.quit.connect(update_check_date)124 worker.quit.connect(update_check_date)
126 # TODO: Use this to figure out if there's an Internet connection?125 # TODO: Use this to figure out if there's an Internet connection?
127 # worker.no_internet.connect(parent.on_no_internet)126 # worker.no_internet.connect(parent.on_no_internet)
128 run_thread(parent, worker, 'version')127 run_thread(worker, 'version')
129128
130129
131def get_version():130def get_version():
132131
=== modified file 'openlp/plugins/songs/forms/songselectform.py'
--- openlp/plugins/songs/forms/songselectform.py 2017-12-29 09:15:48 +0000
+++ openlp/plugins/songs/forms/songselectform.py 2018-01-07 18:07:40 +0000
@@ -27,24 +27,23 @@
2727
28from PyQt5 import QtCore, QtWidgets28from PyQt5 import QtCore, QtWidgets
2929
30from openlp.core.common import is_win
31from openlp.core.common.i18n import translate30from openlp.core.common.i18n import translate
32from openlp.core.common.registry import Registry31from openlp.core.common.mixins import RegistryProperties
33from openlp.core.common.settings import Settings32from openlp.core.common.settings import Settings
33from openlp.core.threading import ThreadWorker, run_thread
34from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog34from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog
35from openlp.plugins.songs.lib.songselect import SongSelectImport35from openlp.plugins.songs.lib.songselect import SongSelectImport
3636
37log = logging.getLogger(__name__)37log = logging.getLogger(__name__)
3838
3939
40class SearchWorker(QtCore.QObject):40class SearchWorker(ThreadWorker):
41 """41 """
42 Run the actual SongSelect search, and notify the GUI when we find each song.42 Run the actual SongSelect search, and notify the GUI when we find each song.
43 """43 """
44 show_info = QtCore.pyqtSignal(str, str)44 show_info = QtCore.pyqtSignal(str, str)
45 found_song = QtCore.pyqtSignal(dict)45 found_song = QtCore.pyqtSignal(dict)
46 finished = QtCore.pyqtSignal()46 finished = QtCore.pyqtSignal()
47 quit = QtCore.pyqtSignal()
4847
49 def __init__(self, importer, search_text):48 def __init__(self, importer, search_text):
50 super().__init__()49 super().__init__()
@@ -74,7 +73,7 @@
74 self.found_song.emit(song)73 self.found_song.emit(song)
7574
7675
77class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):76class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties):
78 """77 """
79 The :class:`SongSelectForm` class is the SongSelect dialog.78 The :class:`SongSelectForm` class is the SongSelect dialog.
80 """79 """
@@ -90,8 +89,6 @@
90 """89 """
91 Initialise the SongSelectForm90 Initialise the SongSelectForm
92 """91 """
93 self.thread = None
94 self.worker = None
95 self.song_count = 092 self.song_count = 0
96 self.song = None93 self.song = None
97 self.set_progress_visible(False)94 self.set_progress_visible(False)
@@ -311,17 +308,11 @@
311 search_history = self.search_combobox.getItems()308 search_history = self.search_combobox.getItems()
312 Settings().setValue(self.plugin.settings_section + '/songselect searches', '|'.join(search_history))309 Settings().setValue(self.plugin.settings_section + '/songselect searches', '|'.join(search_history))
313 # Create thread and run search310 # Create thread and run search
314 self.thread = QtCore.QThread()311 worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText())
315 self.worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText())312 worker.show_info.connect(self.on_search_show_info)
316 self.worker.moveToThread(self.thread)313 worker.found_song.connect(self.on_search_found_song)
317 self.thread.started.connect(self.worker.start)314 worker.finished.connect(self.on_search_finished)
318 self.worker.show_info.connect(self.on_search_show_info)315 run_thread(worker, 'songselect')
319 self.worker.found_song.connect(self.on_search_found_song)
320 self.worker.finished.connect(self.on_search_finished)
321 self.worker.quit.connect(self.thread.quit)
322 self.worker.quit.connect(self.worker.deleteLater)
323 self.thread.finished.connect(self.thread.deleteLater)
324 self.thread.start()
325316
326 def on_stop_button_clicked(self):317 def on_stop_button_clicked(self):
327 """318 """
@@ -408,16 +399,3 @@
408 """399 """
409 self.search_progress_bar.setVisible(is_visible)400 self.search_progress_bar.setVisible(is_visible)
410 self.stop_button.setVisible(is_visible)401 self.stop_button.setVisible(is_visible)
411
412 @property
413 def application(self):
414 """
415 Adds the openlp to the class dynamically.
416 Windows needs to access the application in a dynamic manner.
417 """
418 if is_win():
419 return Registry().get('application')
420 else:
421 if not hasattr(self, '_application'):
422 self._application = Registry().get('application')
423 return self._application
424402
=== modified file 'tests/functional/openlp_core/api/http/test_http.py'
--- tests/functional/openlp_core/api/http/test_http.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/api/http/test_http.py 2018-01-07 18:07:40 +0000
@@ -42,8 +42,23 @@
42 Registry().register('service_list', MagicMock())42 Registry().register('service_list', MagicMock())
4343
44 @patch('openlp.core.api.http.server.HttpWorker')44 @patch('openlp.core.api.http.server.HttpWorker')
45 @patch('openlp.core.api.http.server.QtCore.QThread')45 @patch('openlp.core.api.http.server.run_thread')
46 def test_server_start(self, mock_qthread, mock_thread):46 def test_server_start(self, mocked_run_thread, MockHttpWorker):
47 """
48 Test the starting of the Waitress Server with the disable flag set off
49 """
50 # GIVEN: A new httpserver
51 # WHEN: I start the server
52 Registry().set_flag('no_web_server', False)
53 HttpServer()
54
55 # THEN: the api environment should have been created
56 assert mocked_run_thread.call_count == 1, 'The qthread should have been called once'
57 assert MockHttpWorker.call_count == 1, 'The http thread should have been called once'
58
59 @patch('openlp.core.api.http.server.HttpWorker')
60 @patch('openlp.core.api.http.server.run_thread')
61 def test_server_start_not_required(self, mocked_run_thread, MockHttpWorker):
47 """62 """
48 Test the starting of the Waitress Server with the disable flag set off63 Test the starting of the Waitress Server with the disable flag set off
49 """64 """
@@ -53,20 +68,5 @@
53 HttpServer()68 HttpServer()
5469
55 # THEN: the api environment should have been created70 # THEN: the api environment should have been created
56 assert mock_qthread.call_count == 1, 'The qthread should have been called once'71 assert mocked_run_thread.call_count == 0, 'The qthread should not have have been called'
57 assert mock_thread.call_count == 1, 'The http thread should have been called once'72 assert MockHttpWorker.call_count == 0, 'The http thread should not have been called'
58
59 @patch('openlp.core.api.http.server.HttpWorker')
60 @patch('openlp.core.api.http.server.QtCore.QThread')
61 def test_server_start_not_required(self, mock_qthread, mock_thread):
62 """
63 Test the starting of the Waitress Server with the disable flag set off
64 """
65 # GIVEN: A new httpserver
66 # WHEN: I start the server
67 Registry().set_flag('no_web_server', False)
68 HttpServer()
69
70 # THEN: the api environment should have been created
71 assert mock_qthread.call_count == 0, 'The qthread should not have have been called'
72 assert mock_thread.call_count == 0, 'The http thread should not have been called'
7373
=== modified file 'tests/functional/openlp_core/api/test_websockets.py'
--- tests/functional/openlp_core/api/test_websockets.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/api/test_websockets.py 2018-01-07 18:07:40 +0000
@@ -63,34 +63,34 @@
63 self.destroy_settings()63 self.destroy_settings()
6464
65 @patch('openlp.core.api.websockets.WebSocketWorker')65 @patch('openlp.core.api.websockets.WebSocketWorker')
66 @patch('openlp.core.api.websockets.QtCore.QThread')66 @patch('openlp.core.api.websockets.run_thread')
67 def test_serverstart(self, mock_qthread, mock_worker):67 def test_serverstart(self, mocked_run_thread, MockWebSocketWorker):
68 """68 """
69 Test the starting of the WebSockets Server with the disabled flag set on69 Test the starting of the WebSockets Server with the disabled flag set on
70 """70 """
71 # GIVEN: A new httpserver71 # GIVEN: A new httpserver
72 # WHEN: I start the server72 # WHEN: I start the server
73 Registry().set_flag('no_web_server', True)73 Registry().set_flag('no_web_server', False)
74 WebSocketServer()74 WebSocketServer()
7575
76 # THEN: the api environment should have been created76 # THEN: the api environment should have been created
77 assert mock_qthread.call_count == 1, 'The qthread should have been called once'77 assert mocked_run_thread.call_count == 1, 'The qthread should have been called once'
78 assert mock_worker.call_count == 1, 'The http thread should have been called once'78 assert MockWebSocketWorker.call_count == 1, 'The http thread should have been called once'
7979
80 @patch('openlp.core.api.websockets.WebSocketWorker')80 @patch('openlp.core.api.websockets.WebSocketWorker')
81 @patch('openlp.core.api.websockets.QtCore.QThread')81 @patch('openlp.core.api.websockets.run_thread')
82 def test_serverstart_not_required(self, mock_qthread, mock_worker):82 def test_serverstart_not_required(self, mocked_run_thread, MockWebSocketWorker):
83 """83 """
84 Test the starting of the WebSockets Server with the disabled flag set off84 Test the starting of the WebSockets Server with the disabled flag set off
85 """85 """
86 # GIVEN: A new httpserver and the server is not required86 # GIVEN: A new httpserver and the server is not required
87 # WHEN: I start the server87 # WHEN: I start the server
88 Registry().set_flag('no_web_server', False)88 Registry().set_flag('no_web_server', True)
89 WebSocketServer()89 WebSocketServer()
9090
91 # THEN: the api environment should have been created91 # THEN: the api environment should have been created
92 assert mock_qthread.call_count == 0, 'The qthread should not have been called'92 assert mocked_run_thread.call_count == 0, 'The qthread should not have been called'
93 assert mock_worker.call_count == 0, 'The http thread should not have been called'93 assert MockWebSocketWorker.call_count == 0, 'The http thread should not have been called'
9494
95 def test_main_poll(self):95 def test_main_poll(self):
96 """96 """
9797
=== modified file 'tests/functional/openlp_core/common/test_httputils.py'
--- tests/functional/openlp_core/common/test_httputils.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/common/test_httputils.py 2018-01-07 18:07:40 +0000
@@ -27,7 +27,7 @@
27from unittest import TestCase27from unittest import TestCase
28from unittest.mock import MagicMock, patch28from unittest.mock import MagicMock, patch
2929
30from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file30from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, download_file
31from openlp.core.common.path import Path31from openlp.core.common.path import Path
32from tests.helpers.testmixin import TestMixin32from tests.helpers.testmixin import TestMixin
3333
@@ -235,7 +235,7 @@
235 mocked_requests.get.side_effect = OSError235 mocked_requests.get.side_effect = OSError
236236
237 # WHEN: Attempt to retrieve a file237 # WHEN: Attempt to retrieve a file
238 url_get_file(MagicMock(), url='http://localhost/test', file_path=Path(self.tempfile))238 download_file(MagicMock(), url='http://localhost/test', file_path=Path(self.tempfile))
239239
240 # THEN: socket.timeout should have been caught240 # THEN: socket.timeout should have been caught
241 # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files241 # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files
242242
=== modified file 'tests/functional/openlp_core/lib/test_image_manager.py'
--- tests/functional/openlp_core/lib/test_image_manager.py 2017-12-28 08:22:55 +0000
+++ tests/functional/openlp_core/lib/test_image_manager.py 2018-01-07 18:07:40 +0000
@@ -25,20 +25,113 @@
25import os25import os
26import time26import time
27from threading import Lock27from threading import Lock
28from unittest import TestCase28from unittest import TestCase, skip
29from unittest.mock import patch29from unittest.mock import MagicMock, patch
3030
31from PyQt5 import QtGui31from PyQt5 import QtGui
3232
33from openlp.core.common.registry import Registry33from openlp.core.common.registry import Registry
34from openlp.core.display.screens import ScreenList34from openlp.core.display.screens import ScreenList
35from openlp.core.lib.imagemanager import ImageManager, Priority35from openlp.core.lib.imagemanager import ImageWorker, ImageManager, Priority, PriorityQueue
36from tests.helpers.testmixin import TestMixin36from tests.helpers.testmixin import TestMixin
37from tests.utils.constants import RESOURCE_PATH37from tests.utils.constants import RESOURCE_PATH
3838
39TEST_PATH = str(RESOURCE_PATH)39TEST_PATH = str(RESOURCE_PATH)
4040
4141
42class TestImageWorker(TestCase, TestMixin):
43 """
44 Test all the methods in the ImageWorker class
45 """
46 def test_init(self):
47 """
48 Test the constructor of the ImageWorker
49 """
50 # GIVEN: An ImageWorker class and a mocked ImageManager
51 mocked_image_manager = MagicMock()
52
53 # WHEN: Creating the ImageWorker
54 worker = ImageWorker(mocked_image_manager)
55
56 # THEN: The image_manager attribute should be set correctly
57 assert worker.image_manager is mocked_image_manager, \
58 'worker.image_manager should have been the mocked_image_manager'
59
60 @patch('openlp.core.lib.imagemanager.ThreadWorker.quit')
61 def test_start(self, mocked_quit):
62 """
63 Test that the start() method of the image worker calls the process method and then emits quit.
64 """
65 # GIVEN: A mocked image_manager and a new image worker
66 mocked_image_manager = MagicMock()
67 worker = ImageWorker(mocked_image_manager)
68
69 # WHEN: start() is called
70 worker.start()
71
72 # THEN: process() should have been called and quit should have been emitted
73 mocked_image_manager.process.assert_called_once_with()
74 mocked_quit.emit.assert_called_once_with()
75
76 def test_stop(self):
77 """
78 Test that the stop method does the right thing
79 """
80 # GIVEN: A mocked image_manager and a worker
81 mocked_image_manager = MagicMock()
82 worker = ImageWorker(mocked_image_manager)
83
84 # WHEN: The stop() method is called
85 worker.stop()
86
87 # THEN: The stop_manager attrivute should have been set to True
88 assert mocked_image_manager.stop_manager is True, 'mocked_image_manager.stop_manager should have been True'
89
90
91class TestPriorityQueue(TestCase, TestMixin):
92 """
93 Test the PriorityQueue class
94 """
95 @patch('openlp.core.lib.imagemanager.PriorityQueue.remove')
96 @patch('openlp.core.lib.imagemanager.PriorityQueue.put')
97 def test_modify_priority(self, mocked_put, mocked_remove):
98 """
99 Test the modify_priority() method of PriorityQueue
100 """
101 # GIVEN: An instance of a PriorityQueue and a mocked image
102 mocked_image = MagicMock()
103 mocked_image.priority = Priority.Normal
104 mocked_image.secondary_priority = Priority.Low
105 queue = PriorityQueue()
106
107 # WHEN: modify_priority is called with a mocked image and a new priority
108 queue.modify_priority(mocked_image, Priority.High)
109
110 # THEN: The remove() method should have been called, image priority updated and put() called
111 mocked_remove.assert_called_once_with(mocked_image)
112 assert mocked_image.priority == Priority.High, 'The priority should have been Priority.High'
113 mocked_put.assert_called_once_with((Priority.High, Priority.Low, mocked_image))
114
115 def test_remove(self):
116 """
117 Test the remove() method of PriorityQueue
118 """
119 # GIVEN: A PriorityQueue instance with a mocked image and queue
120 mocked_image = MagicMock()
121 mocked_image.priority = Priority.High
122 mocked_image.secondary_priority = Priority.Normal
123 queue = PriorityQueue()
124
125 # WHEN: An image is removed
126 with patch.object(queue, 'queue') as mocked_queue:
127 mocked_queue.__contains__.return_value = True
128 queue.remove(mocked_image)
129
130 # THEN: The mocked queue.remove() method should have been called
131 mocked_queue.remove.assert_called_once_with((Priority.High, Priority.Normal, mocked_image))
132
133
134@skip('Probably not going to use ImageManager in WebEngine/Reveal.js')
42class TestImageManager(TestCase, TestMixin):135class TestImageManager(TestCase, TestMixin):
43136
44 def setUp(self):137 def setUp(self):
@@ -57,10 +150,10 @@
57 Delete all the C++ objects at the end so that we don't have a segfault150 Delete all the C++ objects at the end so that we don't have a segfault
58 """151 """
59 self.image_manager.stop_manager = True152 self.image_manager.stop_manager = True
60 self.image_manager.image_thread.wait()
61 del self.app153 del self.app
62154
63 def test_basic_image_manager(self):155 @patch('openlp.core.lib.imagemanager.run_thread')
156 def test_basic_image_manager(self, mocked_run_thread):
64 """157 """
65 Test the Image Manager setup basic functionality158 Test the Image Manager setup basic functionality
66 """159 """
@@ -86,7 +179,8 @@
86 self.image_manager.get_image(TEST_PATH, 'church1.jpg')179 self.image_manager.get_image(TEST_PATH, 'church1.jpg')
87 assert context.exception is not '', 'KeyError exception should have been thrown for missing image'180 assert context.exception is not '', 'KeyError exception should have been thrown for missing image'
88181
89 def test_different_dimension_image(self):182 @patch('openlp.core.lib.imagemanager.run_thread')
183 def test_different_dimension_image(self, mocked_run_thread):
90 """184 """
91 Test the Image Manager with dimensions185 Test the Image Manager with dimensions
92 """186 """
@@ -118,57 +212,58 @@
118 self.image_manager.get_image(full_path, 'church.jpg', 120, 120)212 self.image_manager.get_image(full_path, 'church.jpg', 120, 120)
119 assert context.exception is not '', 'KeyError exception should have been thrown for missing dimension'213 assert context.exception is not '', 'KeyError exception should have been thrown for missing dimension'
120214
121 def test_process_cache(self):215 @patch('openlp.core.lib.imagemanager.resize_image')
216 @patch('openlp.core.lib.imagemanager.image_to_byte')
217 @patch('openlp.core.lib.imagemanager.run_thread')
218 def test_process_cache(self, mocked_run_thread, mocked_image_to_byte, mocked_resize_image):
122 """219 """
123 Test the process_cache method220 Test the process_cache method
124 """221 """
125 with patch('openlp.core.lib.imagemanager.resize_image') as mocked_resize_image, \222 # GIVEN: Mocked functions
126 patch('openlp.core.lib.imagemanager.image_to_byte') as mocked_image_to_byte:223 mocked_resize_image.side_effect = self.mocked_resize_image
127 # GIVEN: Mocked functions224 mocked_image_to_byte.side_effect = self.mocked_image_to_byte
128 mocked_resize_image.side_effect = self.mocked_resize_image225 image1 = 'church.jpg'
129 mocked_image_to_byte.side_effect = self.mocked_image_to_byte226 image2 = 'church2.jpg'
130 image1 = 'church.jpg'227 image3 = 'church3.jpg'
131 image2 = 'church2.jpg'228 image4 = 'church4.jpg'
132 image3 = 'church3.jpg'229
133 image4 = 'church4.jpg'230 # WHEN: Add the images. Then get the lock (=queue can not be processed).
134231 self.lock.acquire()
135 # WHEN: Add the images. Then get the lock (=queue can not be processed).232 self.image_manager.add_image(TEST_PATH, image1, None)
136 self.lock.acquire()233 self.image_manager.add_image(TEST_PATH, image2, None)
137 self.image_manager.add_image(TEST_PATH, image1, None)234
138 self.image_manager.add_image(TEST_PATH, image2, None)235 # THEN: All images have been added to the queue, and only the first image is not be in the list anymore, but
139236 # is being processed (see mocked methods/functions).
140 # THEN: All images have been added to the queue, and only the first image is not be in the list anymore, but237 # Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the #
141 # is being processed (see mocked methods/functions).238 # priority is adjusted to Priority.Lowest).
142 # Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the #239 assert self.get_image_priority(image1) == Priority.Normal, "image1's priority should be 'Priority.Normal'"
143 # priority is adjusted to Priority.Lowest).240 assert self.get_image_priority(image2) == Priority.Normal, "image2's priority should be 'Priority.Normal'"
144 assert self.get_image_priority(image1) == Priority.Normal, "image1's priority should be 'Priority.Normal'"241
145 assert self.get_image_priority(image2) == Priority.Normal, "image2's priority should be 'Priority.Normal'"242 # WHEN: Add more images.
146243 self.image_manager.add_image(TEST_PATH, image3, None)
147 # WHEN: Add more images.244 self.image_manager.add_image(TEST_PATH, image4, None)
148 self.image_manager.add_image(TEST_PATH, image3, None)245 # Allow the queue to process.
149 self.image_manager.add_image(TEST_PATH, image4, None)246 self.lock.release()
150 # Allow the queue to process.247 # Request some "data".
151 self.lock.release()248 self.image_manager.get_image_bytes(TEST_PATH, image4)
152 # Request some "data".249 self.image_manager.get_image(TEST_PATH, image3)
153 self.image_manager.get_image_bytes(TEST_PATH, image4)250 # Now the mocked methods/functions do not have to sleep anymore.
154 self.image_manager.get_image(TEST_PATH, image3)251 self.sleep_time = 0
155 # Now the mocked methods/functions do not have to sleep anymore.252 # Wait for the queue to finish.
156 self.sleep_time = 0253 while not self.image_manager._conversion_queue.empty():
157 # Wait for the queue to finish.
158 while not self.image_manager._conversion_queue.empty():
159 time.sleep(0.1)
160 # Because empty() is not reliable, wait a litte; just to make sure.
161 time.sleep(0.1)254 time.sleep(0.1)
162 # THEN: The images' priority reflect how they were processed.255 # Because empty() is not reliable, wait a litte; just to make sure.
163 assert self.image_manager._conversion_queue.qsize() == 0, "The queue should be empty."256 time.sleep(0.1)
164 assert self.get_image_priority(image1) == Priority.Lowest, \257 # THEN: The images' priority reflect how they were processed.
165 "The image should have not been requested (=Lowest)"258 assert self.image_manager._conversion_queue.qsize() == 0, "The queue should be empty."
166 assert self.get_image_priority(image2) == Priority.Lowest, \259 assert self.get_image_priority(image1) == Priority.Lowest, \
167 "The image should have not been requested (=Lowest)"260 "The image should have not been requested (=Lowest)"
168 assert self.get_image_priority(image3) == Priority.Low, \261 assert self.get_image_priority(image2) == Priority.Lowest, \
169 "Only the QImage should have been requested (=Low)."262 "The image should have not been requested (=Lowest)"
170 assert self.get_image_priority(image4) == Priority.Urgent, \263 assert self.get_image_priority(image3) == Priority.Low, \
171 "The image bytes should have been requested (=Urgent)."264 "Only the QImage should have been requested (=Low)."
265 assert self.get_image_priority(image4) == Priority.Urgent, \
266 "The image bytes should have been requested (=Urgent)."
172267
173 def get_image_priority(self, image):268 def get_image_priority(self, image):
174 """269 """
175270
=== modified file 'tests/functional/openlp_core/test_app.py'
--- tests/functional/openlp_core/test_app.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/test_app.py 2018-01-07 18:07:40 +0000
@@ -36,14 +36,15 @@
36 """36 """
37 # GIVEN: a a set of system arguments.37 # GIVEN: a a set of system arguments.
38 sys.argv[1:] = []38 sys.argv[1:] = []
39
39 # WHEN: We we parse them to expand to options40 # WHEN: We we parse them to expand to options
40 args = parse_options(None)41 args = parse_options()
42
41 # THEN: the following fields will have been extracted.43 # THEN: the following fields will have been extracted.
42 assert args.dev_version is False, 'The dev_version flag should be False'44 assert args.dev_version is False, 'The dev_version flag should be False'
43 assert args.loglevel == 'warning', 'The log level should be set to warning'45 assert args.loglevel == 'warning', 'The log level should be set to warning'
44 assert args.no_error_form is False, 'The no_error_form should be set to False'46 assert args.no_error_form is False, 'The no_error_form should be set to False'
45 assert args.portable is False, 'The portable flag should be set to false'47 assert args.portable is False, 'The portable flag should be set to false'
46 assert args.style is None, 'There are no style flags to be processed'
47 assert args.rargs == [], 'The service file should be blank'48 assert args.rargs == [], 'The service file should be blank'
4849
4950
@@ -53,14 +54,15 @@
53 """54 """
54 # GIVEN: a a set of system arguments.55 # GIVEN: a a set of system arguments.
55 sys.argv[1:] = ['-l debug']56 sys.argv[1:] = ['-l debug']
57
56 # WHEN: We we parse them to expand to options58 # WHEN: We we parse them to expand to options
57 args = parse_options(None)59 args = parse_options()
60
58 # THEN: the following fields will have been extracted.61 # THEN: the following fields will have been extracted.
59 assert args.dev_version is False, 'The dev_version flag should be False'62 assert args.dev_version is False, 'The dev_version flag should be False'
60 assert args.loglevel == ' debug', 'The log level should be set to debug'63 assert args.loglevel == ' debug', 'The log level should be set to debug'
61 assert args.no_error_form is False, 'The no_error_form should be set to False'64 assert args.no_error_form is False, 'The no_error_form should be set to False'
62 assert args.portable is False, 'The portable flag should be set to false'65 assert args.portable is False, 'The portable flag should be set to false'
63 assert args.style is None, 'There are no style flags to be processed'
64 assert args.rargs == [], 'The service file should be blank'66 assert args.rargs == [], 'The service file should be blank'
6567
6668
@@ -70,14 +72,15 @@
70 """72 """
71 # GIVEN: a a set of system arguments.73 # GIVEN: a a set of system arguments.
72 sys.argv[1:] = ['--portable']74 sys.argv[1:] = ['--portable']
75
73 # WHEN: We we parse them to expand to options76 # WHEN: We we parse them to expand to options
74 args = parse_options(None)77 args = parse_options()
78
75 # THEN: the following fields will have been extracted.79 # THEN: the following fields will have been extracted.
76 assert args.dev_version is False, 'The dev_version flag should be False'80 assert args.dev_version is False, 'The dev_version flag should be False'
77 assert args.loglevel == 'warning', 'The log level should be set to warning'81 assert args.loglevel == 'warning', 'The log level should be set to warning'
78 assert args.no_error_form is False, 'The no_error_form should be set to False'82 assert args.no_error_form is False, 'The no_error_form should be set to False'
79 assert args.portable is True, 'The portable flag should be set to true'83 assert args.portable is True, 'The portable flag should be set to true'
80 assert args.style is None, 'There are no style flags to be processed'
81 assert args.rargs == [], 'The service file should be blank'84 assert args.rargs == [], 'The service file should be blank'
8285
8386
@@ -87,14 +90,15 @@
87 """90 """
88 # GIVEN: a a set of system arguments.91 # GIVEN: a a set of system arguments.
89 sys.argv[1:] = ['-l debug', '-d']92 sys.argv[1:] = ['-l debug', '-d']
93
90 # WHEN: We we parse them to expand to options94 # WHEN: We we parse them to expand to options
91 args = parse_options(None)95 args = parse_options()
96
92 # THEN: the following fields will have been extracted.97 # THEN: the following fields will have been extracted.
93 assert args.dev_version is True, 'The dev_version flag should be True'98 assert args.dev_version is True, 'The dev_version flag should be True'
94 assert args.loglevel == ' debug', 'The log level should be set to debug'99 assert args.loglevel == ' debug', 'The log level should be set to debug'
95 assert args.no_error_form is False, 'The no_error_form should be set to False'100 assert args.no_error_form is False, 'The no_error_form should be set to False'
96 assert args.portable is False, 'The portable flag should be set to false'101 assert args.portable is False, 'The portable flag should be set to false'
97 assert args.style is None, 'There are no style flags to be processed'
98 assert args.rargs == [], 'The service file should be blank'102 assert args.rargs == [], 'The service file should be blank'
99103
100104
@@ -104,14 +108,15 @@
104 """108 """
105 # GIVEN: a a set of system arguments.109 # GIVEN: a a set of system arguments.
106 sys.argv[1:] = ['dummy_temp']110 sys.argv[1:] = ['dummy_temp']
111
107 # WHEN: We we parse them to expand to options112 # WHEN: We we parse them to expand to options
108 args = parse_options(None)113 args = parse_options()
114
109 # THEN: the following fields will have been extracted.115 # THEN: the following fields will have been extracted.
110 assert args.dev_version is False, 'The dev_version flag should be False'116 assert args.dev_version is False, 'The dev_version flag should be False'
111 assert args.loglevel == 'warning', 'The log level should be set to warning'117 assert args.loglevel == 'warning', 'The log level should be set to warning'
112 assert args.no_error_form is False, 'The no_error_form should be set to False'118 assert args.no_error_form is False, 'The no_error_form should be set to False'
113 assert args.portable is False, 'The portable flag should be set to false'119 assert args.portable is False, 'The portable flag should be set to false'
114 assert args.style is None, 'There are no style flags to be processed'
115 assert args.rargs == 'dummy_temp', 'The service file should not be blank'120 assert args.rargs == 'dummy_temp', 'The service file should not be blank'
116121
117122
@@ -121,14 +126,15 @@
121 """126 """
122 # GIVEN: a a set of system arguments.127 # GIVEN: a a set of system arguments.
123 sys.argv[1:] = ['-l debug', 'dummy_temp']128 sys.argv[1:] = ['-l debug', 'dummy_temp']
129
124 # WHEN: We we parse them to expand to options130 # WHEN: We we parse them to expand to options
125 args = parse_options(None)131 args = parse_options()
132
126 # THEN: the following fields will have been extracted.133 # THEN: the following fields will have been extracted.
127 assert args.dev_version is False, 'The dev_version flag should be False'134 assert args.dev_version is False, 'The dev_version flag should be False'
128 assert args.loglevel == ' debug', 'The log level should be set to debug'135 assert args.loglevel == ' debug', 'The log level should be set to debug'
129 assert args.no_error_form is False, 'The no_error_form should be set to False'136 assert args.no_error_form is False, 'The no_error_form should be set to False'
130 assert args.portable is False, 'The portable flag should be set to false'137 assert args.portable is False, 'The portable flag should be set to false'
131 assert args.style is None, 'There are no style flags to be processed'
132 assert args.rargs == 'dummy_temp', 'The service file should not be blank'138 assert args.rargs == 'dummy_temp', 'The service file should not be blank'
133139
134140
135141
=== added file 'tests/functional/openlp_core/test_threading.py'
--- tests/functional/openlp_core/test_threading.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core/test_threading.py 2018-01-07 18:07:40 +0000
@@ -0,0 +1,89 @@
1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
4###############################################################################
5# OpenLP - Open Source Lyrics Projection #
6# --------------------------------------------------------------------------- #
7# Copyright (c) 2008-2018 OpenLP Developers #
8# --------------------------------------------------------------------------- #
9# This program is free software; you can redistribute it and/or modify it #
10# under the terms of the GNU General Public License as published by the Free #
11# Software Foundation; version 2 of the License. #
12# #
13# This program is distributed in the hope that it will be useful, but WITHOUT #
14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
15# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
16# more details. #
17# #
18# You should have received a copy of the GNU General Public License along #
19# with this program; if not, write to the Free Software Foundation, Inc., 59 #
20# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
21###############################################################################
22"""
23Package to test the openlp.core.threading package.
24"""
25from unittest.mock import MagicMock, call, patch
26
27from openlp.core.version import run_thread
28
29
30def test_run_thread_no_name():
31 """
32 Test that trying to run a thread without a name results in an exception being thrown
33 """
34 # GIVEN: A fake worker
35 # WHEN: run_thread() is called without a name
36 try:
37 run_thread(MagicMock(), '')
38 assert False, 'A ValueError should have been thrown to prevent blank names'
39 except ValueError:
40 # THEN: A ValueError should have been thrown
41 assert True, 'A ValueError was correctly thrown'
42
43
44@patch('openlp.core.threading.Registry')
45def test_run_thread_exists(MockRegistry):
46 """
47 Test that trying to run a thread with a name that already exists will throw a KeyError
48 """
49 # GIVEN: A mocked registry with a main window object
50 mocked_main_window = MagicMock()
51 mocked_main_window.threads = {'test_thread': MagicMock()}
52 MockRegistry.return_value.get.return_value = mocked_main_window
53
54 # WHEN: run_thread() is called
55 try:
56 run_thread(MagicMock(), 'test_thread')
57 assert False, 'A KeyError should have been thrown to show that a thread with this name already exists'
58 except KeyError:
59 assert True, 'A KeyError was correctly thrown'
60
61
62@patch('openlp.core.threading.QtCore.QThread')
63@patch('openlp.core.threading.Registry')
64def test_run_thread(MockRegistry, MockQThread):
65 """
66 Test that running a thread works correctly
67 """
68 # GIVEN: A mocked registry with a main window object
69 mocked_main_window = MagicMock()
70 mocked_main_window.threads = {}
71 MockRegistry.return_value.get.return_value = mocked_main_window
72
73 # WHEN: run_thread() is called
74 run_thread(MagicMock(), 'test_thread')
75
76 # THEN: The thread should be in the threads list and the correct methods should have been called
77 assert len(mocked_main_window.threads.keys()) == 1, 'There should be 1 item in the list of threads'
78 assert list(mocked_main_window.threads.keys()) == ['test_thread'], 'The test_thread item should be in the list'
79 mocked_worker = mocked_main_window.threads['test_thread']['worker']
80 mocked_thread = mocked_main_window.threads['test_thread']['thread']
81 mocked_worker.moveToThread.assert_called_once_with(mocked_thread)
82 mocked_thread.started.connect.assert_called_once_with(mocked_worker.start)
83 expected_quit_calls = [call(mocked_thread.quit), call(mocked_worker.deleteLater)]
84 assert mocked_worker.quit.connect.call_args_list == expected_quit_calls, \
85 'The workers quit signal should be connected twice'
86 assert mocked_thread.finished.connect.call_args_list[0] == call(mocked_thread.deleteLater), \
87 'The threads finished signal should be connected to its deleteLater slot'
88 assert mocked_thread.finished.connect.call_count == 2, 'The signal should have been connected twice'
89 mocked_thread.start.assert_called_once_with()
090
=== added file 'tests/functional/openlp_core/ui/media/test_systemplayer.py'
--- tests/functional/openlp_core/ui/media/test_systemplayer.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core/ui/media/test_systemplayer.py 2018-01-07 18:07:40 +0000
@@ -0,0 +1,543 @@
1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
4###############################################################################
5# OpenLP - Open Source Lyrics Projection #
6# --------------------------------------------------------------------------- #
7# Copyright (c) 2008-2018 OpenLP Developers #
8# --------------------------------------------------------------------------- #
9# This program is free software; you can redistribute it and/or modify it #
10# under the terms of the GNU General Public License as published by the Free #
11# Software Foundation; version 2 of the License. #
12# #
13# This program is distributed in the hope that it will be useful, but WITHOUT #
14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
15# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
16# more details. #
17# #
18# You should have received a copy of the GNU General Public License along #
19# with this program; if not, write to the Free Software Foundation, Inc., 59 #
20# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
21###############################################################################
22"""
23Package to test the openlp.core.ui.media.systemplayer package.
24"""
25from unittest import TestCase
26from unittest.mock import MagicMock, call, patch
27
28from PyQt5 import QtCore, QtMultimedia
29
30from openlp.core.common.registry import Registry
31from openlp.core.ui.media import MediaState
32from openlp.core.ui.media.systemplayer import SystemPlayer, CheckMediaWorker, ADDITIONAL_EXT
33
34
35class TestSystemPlayer(TestCase):
36 """
37 Test the system media player
38 """
39 @patch('openlp.core.ui.media.systemplayer.mimetypes')
40 @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
41 def test_constructor(self, MockQMediaPlayer, mocked_mimetypes):
42 """
43 Test the SystemPlayer constructor
44 """
45 # GIVEN: The SystemPlayer class and a mockedQMediaPlayer
46 mocked_media_player = MagicMock()
47 mocked_media_player.supportedMimeTypes.return_value = [
48 'application/postscript',
49 'audio/aiff',
50 'audio/x-aiff',
51 'text/html',
52 'video/animaflex',
53 'video/x-ms-asf'
54 ]
55 mocked_mimetypes.guess_all_extensions.side_effect = [
56 ['.aiff'],
57 ['.aiff'],
58 ['.afl'],
59 ['.asf']
60 ]
61 MockQMediaPlayer.return_value = mocked_media_player
62
63 # WHEN: An object is created from it
64 player = SystemPlayer(self)
65
66 # THEN: The correct initial values should be set up
67 assert 'system' == player.name
68 assert 'System' == player.original_name
69 assert '&System' == player.display_name
70 assert self == player.parent
71 assert ADDITIONAL_EXT == player.additional_extensions
72 MockQMediaPlayer.assert_called_once_with(None, QtMultimedia.QMediaPlayer.VideoSurface)
73 mocked_mimetypes.init.assert_called_once_with()
74 mocked_media_player.service.assert_called_once_with()
75 mocked_media_player.supportedMimeTypes.assert_called_once_with()
76 assert ['*.aiff'] == player.audio_extensions_list
77 assert ['*.afl', '*.asf'] == player.video_extensions_list
78
79 @patch('openlp.core.ui.media.systemplayer.QtMultimediaWidgets.QVideoWidget')
80 @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
81 def test_setup(self, MockQMediaPlayer, MockQVideoWidget):
82 """
83 Test the setup() method of SystemPlayer
84 """
85 # GIVEN: A SystemPlayer instance and a mock display
86 player = SystemPlayer(self)
87 mocked_display = MagicMock()
88 mocked_display.size.return_value = [1, 2, 3, 4]
89 mocked_video_widget = MagicMock()
90 mocked_media_player = MagicMock()
91 MockQVideoWidget.return_value = mocked_video_widget
92 MockQMediaPlayer.return_value = mocked_media_player
93
94 # WHEN: setup() is run
95 player.setup(mocked_display)
96
97 # THEN: The player should have a display widget
98 MockQVideoWidget.assert_called_once_with(mocked_display)
99 assert mocked_video_widget == mocked_display.video_widget
100 mocked_display.size.assert_called_once_with()
101 mocked_video_widget.resize.assert_called_once_with([1, 2, 3, 4])
102 MockQMediaPlayer.assert_called_with(mocked_display)
103 assert mocked_media_player == mocked_display.media_player
104 mocked_media_player.setVideoOutput.assert_called_once_with(mocked_video_widget)
105 mocked_video_widget.raise_.assert_called_once_with()
106 mocked_video_widget.hide.assert_called_once_with()
107 assert player.has_own_widget is True
108
109 def test_disconnect_slots(self):
110 """
111 Test that we the disconnect slots method catches the TypeError
112 """
113 # GIVEN: A SystemPlayer class and a signal that throws a TypeError
114 player = SystemPlayer(self)
115 mocked_signal = MagicMock()
116 mocked_signal.disconnect.side_effect = \
117 TypeError('disconnect() failed between \'durationChanged\' and all its connections')
118
119 # WHEN: disconnect_slots() is called
120 player.disconnect_slots(mocked_signal)
121
122 # THEN: disconnect should have been called and the exception should have been ignored
123 mocked_signal.disconnect.assert_called_once_with()
124
125 def test_check_available(self):
126 """
127 Test the check_available() method on SystemPlayer
128 """
129 # GIVEN: A SystemPlayer instance
130 player = SystemPlayer(self)
131
132 # WHEN: check_available is run
133 result = player.check_available()
134
135 # THEN: it should be available
136 assert result is True
137
138 def test_load_valid_media(self):
139 """
140 Test the load() method of SystemPlayer with a valid media file
141 """
142 # GIVEN: A SystemPlayer instance and a mocked display
143 player = SystemPlayer(self)
144 mocked_display = MagicMock()
145 mocked_display.controller.media_info.volume = 1
146 mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
147
148 # WHEN: The load() method is run
149 with patch.object(player, 'check_media') as mocked_check_media, \
150 patch.object(player, 'volume') as mocked_volume:
151 mocked_check_media.return_value = True
152 result = player.load(mocked_display)
153
154 # THEN: the file is sent to the video widget
155 mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
156 mocked_check_media.assert_called_once_with('/path/to/file')
157 mocked_display.media_player.setMedia.assert_called_once_with(
158 QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile('/path/to/file')))
159 mocked_volume.assert_called_once_with(mocked_display, 1)
160 assert result is True
161
162 def test_load_invalid_media(self):
163 """
164 Test the load() method of SystemPlayer with an invalid media file
165 """
166 # GIVEN: A SystemPlayer instance and a mocked display
167 player = SystemPlayer(self)
168 mocked_display = MagicMock()
169 mocked_display.controller.media_info.volume = 1
170 mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
171
172 # WHEN: The load() method is run
173 with patch.object(player, 'check_media') as mocked_check_media, \
174 patch.object(player, 'volume'):
175 mocked_check_media.return_value = False
176 result = player.load(mocked_display)
177
178 # THEN: stuff
179 mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
180 mocked_check_media.assert_called_once_with('/path/to/file')
181 assert result is False
182
183 def test_resize(self):
184 """
185 Test the resize() method of the SystemPlayer
186 """
187 # GIVEN: A SystemPlayer instance and a mocked display
188 player = SystemPlayer(self)
189 mocked_display = MagicMock()
190 mocked_display.size.return_value = [1, 2, 3, 4]
191
192 # WHEN: The resize() method is called
193 player.resize(mocked_display)
194
195 # THEN: The player is resized
196 mocked_display.size.assert_called_once_with()
197 mocked_display.video_widget.resize.assert_called_once_with([1, 2, 3, 4])
198
199 @patch('openlp.core.ui.media.systemplayer.functools')
200 def test_play_is_live(self, mocked_functools):
201 """
202 Test the play() method of the SystemPlayer on the live display
203 """
204 # GIVEN: A SystemPlayer instance and a mocked display
205 mocked_functools.partial.return_value = 'function'
206 player = SystemPlayer(self)
207 mocked_display = MagicMock()
208 mocked_display.controller.is_live = True
209 mocked_display.controller.media_info.start_time = 1
210 mocked_display.controller.media_info.volume = 1
211
212 # WHEN: play() is called
213 with patch.object(player, 'get_live_state') as mocked_get_live_state, \
214 patch.object(player, 'seek') as mocked_seek, \
215 patch.object(player, 'volume') as mocked_volume, \
216 patch.object(player, 'set_state') as mocked_set_state, \
217 patch.object(player, 'disconnect_slots') as mocked_disconnect_slots:
218 mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
219 result = player.play(mocked_display)
220
221 # THEN: the media file is played
222 mocked_get_live_state.assert_called_once_with()
223 mocked_display.media_player.play.assert_called_once_with()
224 mocked_seek.assert_called_once_with(mocked_display, 1000)
225 mocked_volume.assert_called_once_with(mocked_display, 1)
226 mocked_disconnect_slots.assert_called_once_with(mocked_display.media_player.durationChanged)
227 mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
228 mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
229 mocked_display.video_widget.raise_.assert_called_once_with()
230 assert result is True
231
232 @patch('openlp.core.ui.media.systemplayer.functools')
233 def test_play_is_preview(self, mocked_functools):
234 """
235 Test the play() method of the SystemPlayer on the preview display
236 """
237 # GIVEN: A SystemPlayer instance and a mocked display
238 mocked_functools.partial.return_value = 'function'
239 player = SystemPlayer(self)
240 mocked_display = MagicMock()
241 mocked_display.controller.is_live = False
242 mocked_display.controller.media_info.start_time = 1
243 mocked_display.controller.media_info.volume = 1
244
245 # WHEN: play() is called
246 with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
247 patch.object(player, 'seek') as mocked_seek, \
248 patch.object(player, 'volume') as mocked_volume, \
249 patch.object(player, 'set_state') as mocked_set_state:
250 mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
251 result = player.play(mocked_display)
252
253 # THEN: the media file is played
254 mocked_get_preview_state.assert_called_once_with()
255 mocked_display.media_player.play.assert_called_once_with()
256 mocked_seek.assert_called_once_with(mocked_display, 1000)
257 mocked_volume.assert_called_once_with(mocked_display, 1)
258 mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
259 mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
260 mocked_display.video_widget.raise_.assert_called_once_with()
261 assert result is True
262
263 def test_pause_is_live(self):
264 """
265 Test the pause() method of the SystemPlayer on the live display
266 """
267 # GIVEN: A SystemPlayer instance
268 player = SystemPlayer(self)
269 mocked_display = MagicMock()
270 mocked_display.controller.is_live = True
271
272 # WHEN: The pause method is called
273 with patch.object(player, 'get_live_state') as mocked_get_live_state, \
274 patch.object(player, 'set_state') as mocked_set_state:
275 mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PausedState
276 player.pause(mocked_display)
277
278 # THEN: The video is paused
279 mocked_display.media_player.pause.assert_called_once_with()
280 mocked_get_live_state.assert_called_once_with()
281 mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
282
283 def test_pause_is_preview(self):
284 """
285 Test the pause() method of the SystemPlayer on the preview display
286 """
287 # GIVEN: A SystemPlayer instance
288 player = SystemPlayer(self)
289 mocked_display = MagicMock()
290 mocked_display.controller.is_live = False
291
292 # WHEN: The pause method is called
293 with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
294 patch.object(player, 'set_state') as mocked_set_state:
295 mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PausedState
296 player.pause(mocked_display)
297
298 # THEN: The video is paused
299 mocked_display.media_player.pause.assert_called_once_with()
300 mocked_get_preview_state.assert_called_once_with()
301 mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
302
303 def test_stop(self):
304 """
305 Test the stop() method of the SystemPlayer
306 """
307 # GIVEN: A SystemPlayer instance
308 player = SystemPlayer(self)
309 mocked_display = MagicMock()
310
311 # WHEN: The stop method is called
312 with patch.object(player, 'set_visible') as mocked_set_visible, \
313 patch.object(player, 'set_state') as mocked_set_state:
314 player.stop(mocked_display)
315
316 # THEN: The video is stopped
317 mocked_display.media_player.stop.assert_called_once_with()
318 mocked_set_visible.assert_called_once_with(mocked_display, False)
319 mocked_set_state.assert_called_once_with(MediaState.Stopped, mocked_display)
320
321 def test_volume(self):
322 """
323 Test the volume() method of the SystemPlayer
324 """
325 # GIVEN: A SystemPlayer instance
326 player = SystemPlayer(self)
327 mocked_display = MagicMock()
328 mocked_display.has_audio = True
329
330 # WHEN: The stop method is called
331 player.volume(mocked_display, 2)
332
333 # THEN: The video is stopped
334 mocked_display.media_player.setVolume.assert_called_once_with(2)
335
336 def test_seek(self):
337 """
338 Test the seek() method of the SystemPlayer
339 """
340 # GIVEN: A SystemPlayer instance
341 player = SystemPlayer(self)
342 mocked_display = MagicMock()
343
344 # WHEN: The stop method is called
345 player.seek(mocked_display, 2)
346
347 # THEN: The video is stopped
348 mocked_display.media_player.setPosition.assert_called_once_with(2)
349
350 def test_reset(self):
351 """
352 Test the reset() method of the SystemPlayer
353 """
354 # GIVEN: A SystemPlayer instance
355 player = SystemPlayer(self)
356 mocked_display = MagicMock()
357
358 # WHEN: reset() is called
359 with patch.object(player, 'set_state') as mocked_set_state, \
360 patch.object(player, 'set_visible') as mocked_set_visible:
361 player.reset(mocked_display)
362
363 # THEN: The media player is reset
364 mocked_display.media_player.stop()
365 mocked_display.media_player.setMedia.assert_called_once_with(QtMultimedia.QMediaContent())
366 mocked_set_visible.assert_called_once_with(mocked_display, False)
367 mocked_display.video_widget.setVisible.assert_called_once_with(False)
368 mocked_set_state.assert_called_once_with(MediaState.Off, mocked_display)
369
370 def test_set_visible(self):
371 """
372 Test the set_visible() method on the SystemPlayer
373 """
374 # GIVEN: A SystemPlayer instance and a mocked display
375 player = SystemPlayer(self)
376 player.has_own_widget = True
377 mocked_display = MagicMock()
378
379 # WHEN: set_visible() is called
380 player.set_visible(mocked_display, True)
381
382 # THEN: The widget should be visible
383 mocked_display.video_widget.setVisible.assert_called_once_with(True)
384
385 def test_set_duration(self):
386 """
387 Test the set_duration() method of the SystemPlayer
388 """
389 # GIVEN: a mocked controller
390 mocked_controller = MagicMock()
391 mocked_controller.media_info.length = 5
392
393 # WHEN: The set_duration() is called. NB: the 10 here is ignored by the code
394 SystemPlayer.set_duration(mocked_controller, 10)
395
396 # THEN: The maximum length of the slider should be set
397 mocked_controller.seek_slider.setMaximum.assert_called_once_with(5)
398
399 def test_update_ui(self):
400 """
401 Test the update_ui() method on the SystemPlayer
402 """
403 # GIVEN: A SystemPlayer instance
404 player = SystemPlayer(self)
405 player.state = [MediaState.Playing, MediaState.Playing]
406 mocked_display = MagicMock()
407 mocked_display.media_player.state.return_value = QtMultimedia.QMediaPlayer.PausedState
408 mocked_display.controller.media_info.end_time = 1
409 mocked_display.media_player.position.return_value = 2
410 mocked_display.controller.seek_slider.isSliderDown.return_value = False
411
412 # WHEN: update_ui() is called
413 with patch.object(player, 'stop') as mocked_stop, \
414 patch.object(player, 'set_visible') as mocked_set_visible:
415 player.update_ui(mocked_display)
416
417 # THEN: The UI is updated
418 expected_stop_calls = [call(mocked_display)]
419 expected_position_calls = [call(), call()]
420 expected_block_signals_calls = [call(True), call(False)]
421 mocked_display.media_player.state.assert_called_once_with()
422 assert 1 == mocked_stop.call_count
423 assert expected_stop_calls == mocked_stop.call_args_list
424 assert 2 == mocked_display.media_player.position.call_count
425 assert expected_position_calls == mocked_display.media_player.position.call_args_list
426 mocked_set_visible.assert_called_once_with(mocked_display, False)
427 mocked_display.controller.seek_slider.isSliderDown.assert_called_once_with()
428 assert expected_block_signals_calls == mocked_display.controller.seek_slider.blockSignals.call_args_list
429 mocked_display.controller.seek_slider.setSliderPosition.assert_called_once_with(2)
430
431 def test_get_media_display_css(self):
432 """
433 Test the get_media_display_css() method of the SystemPlayer
434 """
435 # GIVEN: A SystemPlayer instance
436 player = SystemPlayer(self)
437
438 # WHEN: get_media_display_css() is called
439 result = player.get_media_display_css()
440
441 # THEN: The css should be empty
442 assert '' == result
443
444 @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
445 def test_get_info(self, MockQMediaPlayer):
446 """
447 Test the get_info() method of the SystemPlayer
448 """
449 # GIVEN: A SystemPlayer instance
450 mocked_media_player = MagicMock()
451 mocked_media_player.supportedMimeTypes.return_value = []
452 MockQMediaPlayer.return_value = mocked_media_player
453 player = SystemPlayer(self)
454
455 # WHEN: get_info() is called
456 result = player.get_info()
457
458 # THEN: The info should be correct
459 expected_info = 'This media player uses your operating system to provide media capabilities.<br/> ' \
460 '<strong>Audio</strong><br/>[]<br/><strong>Video</strong><br/>[]<br/>'
461 assert expected_info == result
462
463 @patch('openlp.core.ui.media.systemplayer.CheckMediaWorker')
464 @patch('openlp.core.ui.media.systemplayer.run_thread')
465 @patch('openlp.core.ui.media.systemplayer.is_thread_finished')
466 def test_check_media(self, mocked_is_thread_finished, mocked_run_thread, MockCheckMediaWorker):
467 """
468 Test the check_media() method of the SystemPlayer
469 """
470 # GIVEN: A SystemPlayer instance and a mocked thread
471 valid_file = '/path/to/video.ogv'
472 mocked_application = MagicMock()
473 Registry().create()
474 Registry().register('application', mocked_application)
475 player = SystemPlayer(self)
476 mocked_is_thread_finished.side_effect = [False, True]
477 mocked_check_media_worker = MagicMock()
478 mocked_check_media_worker.result = True
479 MockCheckMediaWorker.return_value = mocked_check_media_worker
480
481 # WHEN: check_media() is called with a valid media file
482 result = player.check_media(valid_file)
483
484 # THEN: It should return True
485 MockCheckMediaWorker.assert_called_once_with(valid_file)
486 mocked_check_media_worker.setVolume.assert_called_once_with(0)
487 mocked_run_thread.assert_called_once_with(mocked_check_media_worker, 'check_media')
488 mocked_is_thread_finished.assert_called_with('check_media')
489 assert mocked_is_thread_finished.call_count == 2, 'is_thread_finished() should have been called twice'
490 mocked_application.processEvents.assert_called_once_with()
491 assert result is True
492
493
494class TestCheckMediaWorker(TestCase):
495 """
496 Test the CheckMediaWorker class
497 """
498 def test_constructor(self):
499 """
500 Test the constructor of the CheckMediaWorker class
501 """
502 # GIVEN: A file path
503 path = 'file.ogv'
504
505 # WHEN: The CheckMediaWorker object is instantiated
506 worker = CheckMediaWorker(path)
507
508 # THEN: The correct values should be set up
509 assert worker is not None
510
511 def test_signals_media(self):
512 """
513 Test the signals() signal of the CheckMediaWorker class with a "media" origin
514 """
515 # GIVEN: A CheckMediaWorker instance
516 worker = CheckMediaWorker('file.ogv')
517
518 # WHEN: signals() is called with media and BufferedMedia
519 with patch.object(worker, 'stop') as mocked_stop, \
520 patch.object(worker, 'quit') as mocked_quit:
521 worker.signals('media', worker.BufferedMedia)
522
523 # THEN: The worker should exit and the result should be True
524 mocked_stop.assert_called_once_with()
525 mocked_quit.emit.assert_called_once_with()
526 assert worker.result is True
527
528 def test_signals_error(self):
529 """
530 Test the signals() signal of the CheckMediaWorker class with a "error" origin
531 """
532 # GIVEN: A CheckMediaWorker instance
533 worker = CheckMediaWorker('file.ogv')
534
535 # WHEN: signals() is called with error and BufferedMedia
536 with patch.object(worker, 'stop') as mocked_stop, \
537 patch.object(worker, 'quit') as mocked_quit:
538 worker.signals('error', None)
539
540 # THEN: The worker should exit and the result should be True
541 mocked_stop.assert_called_once_with()
542 mocked_quit.emit.assert_called_once_with()
543 assert worker.result is False
0544
=== removed file 'tests/functional/openlp_core/ui/media/test_systemplayer.py'
--- tests/functional/openlp_core/ui/media/test_systemplayer.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/ui/media/test_systemplayer.py 1970-01-01 00:00:00 +0000
@@ -1,549 +0,0 @@
1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
4###############################################################################
5# OpenLP - Open Source Lyrics Projection #
6# --------------------------------------------------------------------------- #
7# Copyright (c) 2008-2018 OpenLP Developers #
8# --------------------------------------------------------------------------- #
9# This program is free software; you can redistribute it and/or modify it #
10# under the terms of the GNU General Public License as published by the Free #
11# Software Foundation; version 2 of the License. #
12# #
13# This program is distributed in the hope that it will be useful, but WITHOUT #
14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
15# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
16# more details. #
17# #
18# You should have received a copy of the GNU General Public License along #
19# with this program; if not, write to the Free Software Foundation, Inc., 59 #
20# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
21###############################################################################
22"""
23Package to test the openlp.core.ui.media.systemplayer package.
24"""
25from unittest import TestCase
26from unittest.mock import MagicMock, call, patch
27
28from PyQt5 import QtCore, QtMultimedia
29
30from openlp.core.common.registry import Registry
31from openlp.core.ui.media import MediaState
32from openlp.core.ui.media.systemplayer import SystemPlayer, CheckMediaWorker, ADDITIONAL_EXT
33
34
35class TestSystemPlayer(TestCase):
36 """
37 Test the system media player
38 """
39 @patch('openlp.core.ui.media.systemplayer.mimetypes')
40 @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
41 def test_constructor(self, MockQMediaPlayer, mocked_mimetypes):
42 """
43 Test the SystemPlayer constructor
44 """
45 # GIVEN: The SystemPlayer class and a mockedQMediaPlayer
46 mocked_media_player = MagicMock()
47 mocked_media_player.supportedMimeTypes.return_value = [
48 'application/postscript',
49 'audio/aiff',
50 'audio/x-aiff',
51 'text/html',
52 'video/animaflex',
53 'video/x-ms-asf'
54 ]
55 mocked_mimetypes.guess_all_extensions.side_effect = [
56 ['.aiff'],
57 ['.aiff'],
58 ['.afl'],
59 ['.asf']
60 ]
61 MockQMediaPlayer.return_value = mocked_media_player
62
63 # WHEN: An object is created from it
64 player = SystemPlayer(self)
65
66 # THEN: The correct initial values should be set up
67 assert 'system' == player.name
68 assert 'System' == player.original_name
69 assert '&System' == player.display_name
70 assert self == player.parent
71 assert ADDITIONAL_EXT == player.additional_extensions
72 MockQMediaPlayer.assert_called_once_with(None, QtMultimedia.QMediaPlayer.VideoSurface)
73 mocked_mimetypes.init.assert_called_once_with()
74 mocked_media_player.service.assert_called_once_with()
75 mocked_media_player.supportedMimeTypes.assert_called_once_with()
76 assert ['*.aiff'] == player.audio_extensions_list
77 assert ['*.afl', '*.asf'] == player.video_extensions_list
78
79 @patch('openlp.core.ui.media.systemplayer.QtMultimediaWidgets.QVideoWidget')
80 @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
81 def test_setup(self, MockQMediaPlayer, MockQVideoWidget):
82 """
83 Test the setup() method of SystemPlayer
84 """
85 # GIVEN: A SystemPlayer instance and a mock display
86 player = SystemPlayer(self)
87 mocked_display = MagicMock()
88 mocked_display.size.return_value = [1, 2, 3, 4]
89 mocked_video_widget = MagicMock()
90 mocked_media_player = MagicMock()
91 MockQVideoWidget.return_value = mocked_video_widget
92 MockQMediaPlayer.return_value = mocked_media_player
93
94 # WHEN: setup() is run
95 player.setup(mocked_display)
96
97 # THEN: The player should have a display widget
98 MockQVideoWidget.assert_called_once_with(mocked_display)
99 assert mocked_video_widget == mocked_display.video_widget
100 mocked_display.size.assert_called_once_with()
101 mocked_video_widget.resize.assert_called_once_with([1, 2, 3, 4])
102 MockQMediaPlayer.assert_called_with(mocked_display)
103 assert mocked_media_player == mocked_display.media_player
104 mocked_media_player.setVideoOutput.assert_called_once_with(mocked_video_widget)
105 mocked_video_widget.raise_.assert_called_once_with()
106 mocked_video_widget.hide.assert_called_once_with()
107 assert player.has_own_widget is True
108
109 def test_disconnect_slots(self):
110 """
111 Test that we the disconnect slots method catches the TypeError
112 """
113 # GIVEN: A SystemPlayer class and a signal that throws a TypeError
114 player = SystemPlayer(self)
115 mocked_signal = MagicMock()
116 mocked_signal.disconnect.side_effect = \
117 TypeError('disconnect() failed between \'durationChanged\' and all its connections')
118
119 # WHEN: disconnect_slots() is called
120 player.disconnect_slots(mocked_signal)
121
122 # THEN: disconnect should have been called and the exception should have been ignored
123 mocked_signal.disconnect.assert_called_once_with()
124
125 def test_check_available(self):
126 """
127 Test the check_available() method on SystemPlayer
128 """
129 # GIVEN: A SystemPlayer instance
130 player = SystemPlayer(self)
131
132 # WHEN: check_available is run
133 result = player.check_available()
134
135 # THEN: it should be available
136 assert result is True
137
138 def test_load_valid_media(self):
139 """
140 Test the load() method of SystemPlayer with a valid media file
141 """
142 # GIVEN: A SystemPlayer instance and a mocked display
143 player = SystemPlayer(self)
144 mocked_display = MagicMock()
145 mocked_display.controller.media_info.volume = 1
146 mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
147
148 # WHEN: The load() method is run
149 with patch.object(player, 'check_media') as mocked_check_media, \
150 patch.object(player, 'volume') as mocked_volume:
151 mocked_check_media.return_value = True
152 result = player.load(mocked_display)
153
154 # THEN: the file is sent to the video widget
155 mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
156 mocked_check_media.assert_called_once_with('/path/to/file')
157 mocked_display.media_player.setMedia.assert_called_once_with(
158 QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile('/path/to/file')))
159 mocked_volume.assert_called_once_with(mocked_display, 1)
160 assert result is True
161
162 def test_load_invalid_media(self):
163 """
164 Test the load() method of SystemPlayer with an invalid media file
165 """
166 # GIVEN: A SystemPlayer instance and a mocked display
167 player = SystemPlayer(self)
168 mocked_display = MagicMock()
169 mocked_display.controller.media_info.volume = 1
170 mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
171
172 # WHEN: The load() method is run
173 with patch.object(player, 'check_media') as mocked_check_media, \
174 patch.object(player, 'volume') as mocked_volume:
175 mocked_check_media.return_value = False
176 result = player.load(mocked_display)
177
178 # THEN: stuff
179 mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
180 mocked_check_media.assert_called_once_with('/path/to/file')
181 assert result is False
182
183 def test_resize(self):
184 """
185 Test the resize() method of the SystemPlayer
186 """
187 # GIVEN: A SystemPlayer instance and a mocked display
188 player = SystemPlayer(self)
189 mocked_display = MagicMock()
190 mocked_display.size.return_value = [1, 2, 3, 4]
191
192 # WHEN: The resize() method is called
193 player.resize(mocked_display)
194
195 # THEN: The player is resized
196 mocked_display.size.assert_called_once_with()
197 mocked_display.video_widget.resize.assert_called_once_with([1, 2, 3, 4])
198
199 @patch('openlp.core.ui.media.systemplayer.functools')
200 def test_play_is_live(self, mocked_functools):
201 """
202 Test the play() method of the SystemPlayer on the live display
203 """
204 # GIVEN: A SystemPlayer instance and a mocked display
205 mocked_functools.partial.return_value = 'function'
206 player = SystemPlayer(self)
207 mocked_display = MagicMock()
208 mocked_display.controller.is_live = True
209 mocked_display.controller.media_info.start_time = 1
210 mocked_display.controller.media_info.volume = 1
211
212 # WHEN: play() is called
213 with patch.object(player, 'get_live_state') as mocked_get_live_state, \
214 patch.object(player, 'seek') as mocked_seek, \
215 patch.object(player, 'volume') as mocked_volume, \
216 patch.object(player, 'set_state') as mocked_set_state, \
217 patch.object(player, 'disconnect_slots') as mocked_disconnect_slots:
218 mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
219 result = player.play(mocked_display)
220
221 # THEN: the media file is played
222 mocked_get_live_state.assert_called_once_with()
223 mocked_display.media_player.play.assert_called_once_with()
224 mocked_seek.assert_called_once_with(mocked_display, 1000)
225 mocked_volume.assert_called_once_with(mocked_display, 1)
226 mocked_disconnect_slots.assert_called_once_with(mocked_display.media_player.durationChanged)
227 mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
228 mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
229 mocked_display.video_widget.raise_.assert_called_once_with()
230 assert result is True
231
232 @patch('openlp.core.ui.media.systemplayer.functools')
233 def test_play_is_preview(self, mocked_functools):
234 """
235 Test the play() method of the SystemPlayer on the preview display
236 """
237 # GIVEN: A SystemPlayer instance and a mocked display
238 mocked_functools.partial.return_value = 'function'
239 player = SystemPlayer(self)
240 mocked_display = MagicMock()
241 mocked_display.controller.is_live = False
242 mocked_display.controller.media_info.start_time = 1
243 mocked_display.controller.media_info.volume = 1
244
245 # WHEN: play() is called
246 with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
247 patch.object(player, 'seek') as mocked_seek, \
248 patch.object(player, 'volume') as mocked_volume, \
249 patch.object(player, 'set_state') as mocked_set_state:
250 mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
251 result = player.play(mocked_display)
252
253 # THEN: the media file is played
254 mocked_get_preview_state.assert_called_once_with()
255 mocked_display.media_player.play.assert_called_once_with()
256 mocked_seek.assert_called_once_with(mocked_display, 1000)
257 mocked_volume.assert_called_once_with(mocked_display, 1)
258 mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
259 mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
260 mocked_display.video_widget.raise_.assert_called_once_with()
261 assert result is True
262
263 def test_pause_is_live(self):
264 """
265 Test the pause() method of the SystemPlayer on the live display
266 """
267 # GIVEN: A SystemPlayer instance
268 player = SystemPlayer(self)
269 mocked_display = MagicMock()
270 mocked_display.controller.is_live = True
271
272 # WHEN: The pause method is called
273 with patch.object(player, 'get_live_state') as mocked_get_live_state, \
274 patch.object(player, 'set_state') as mocked_set_state:
275 mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PausedState
276 player.pause(mocked_display)
277
278 # THEN: The video is paused
279 mocked_display.media_player.pause.assert_called_once_with()
280 mocked_get_live_state.assert_called_once_with()
281 mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
282
283 def test_pause_is_preview(self):
284 """
285 Test the pause() method of the SystemPlayer on the preview display
286 """
287 # GIVEN: A SystemPlayer instance
288 player = SystemPlayer(self)
289 mocked_display = MagicMock()
290 mocked_display.controller.is_live = False
291
292 # WHEN: The pause method is called
293 with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
294 patch.object(player, 'set_state') as mocked_set_state:
295 mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PausedState
296 player.pause(mocked_display)
297
298 # THEN: The video is paused
299 mocked_display.media_player.pause.assert_called_once_with()
300 mocked_get_preview_state.assert_called_once_with()
301 mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
302
303 def test_stop(self):
304 """
305 Test the stop() method of the SystemPlayer
306 """
307 # GIVEN: A SystemPlayer instance
308 player = SystemPlayer(self)
309 mocked_display = MagicMock()
310
311 # WHEN: The stop method is called
312 with patch.object(player, 'set_visible') as mocked_set_visible, \
313 patch.object(player, 'set_state') as mocked_set_state:
314 player.stop(mocked_display)
315
316 # THEN: The video is stopped
317 mocked_display.media_player.stop.assert_called_once_with()
318 mocked_set_visible.assert_called_once_with(mocked_display, False)
319 mocked_set_state.assert_called_once_with(MediaState.Stopped, mocked_display)
320
321 def test_volume(self):
322 """
323 Test the volume() method of the SystemPlayer
324 """
325 # GIVEN: A SystemPlayer instance
326 player = SystemPlayer(self)
327 mocked_display = MagicMock()
328 mocked_display.has_audio = True
329
330 # WHEN: The stop method is called
331 player.volume(mocked_display, 2)
332
333 # THEN: The video is stopped
334 mocked_display.media_player.setVolume.assert_called_once_with(2)
335
336 def test_seek(self):
337 """
338 Test the seek() method of the SystemPlayer
339 """
340 # GIVEN: A SystemPlayer instance
341 player = SystemPlayer(self)
342 mocked_display = MagicMock()
343
344 # WHEN: The stop method is called
345 player.seek(mocked_display, 2)
346
347 # THEN: The video is stopped
348 mocked_display.media_player.setPosition.assert_called_once_with(2)
349
350 def test_reset(self):
351 """
352 Test the reset() method of the SystemPlayer
353 """
354 # GIVEN: A SystemPlayer instance
355 player = SystemPlayer(self)
356 mocked_display = MagicMock()
357
358 # WHEN: reset() is called
359 with patch.object(player, 'set_state') as mocked_set_state, \
360 patch.object(player, 'set_visible') as mocked_set_visible:
361 player.reset(mocked_display)
362
363 # THEN: The media player is reset
364 mocked_display.media_player.stop()
365 mocked_display.media_player.setMedia.assert_called_once_with(QtMultimedia.QMediaContent())
366 mocked_set_visible.assert_called_once_with(mocked_display, False)
367 mocked_display.video_widget.setVisible.assert_called_once_with(False)
368 mocked_set_state.assert_called_once_with(MediaState.Off, mocked_display)
369
370 def test_set_visible(self):
371 """
372 Test the set_visible() method on the SystemPlayer
373 """
374 # GIVEN: A SystemPlayer instance and a mocked display
375 player = SystemPlayer(self)
376 player.has_own_widget = True
377 mocked_display = MagicMock()
378
379 # WHEN: set_visible() is called
380 player.set_visible(mocked_display, True)
381
382 # THEN: The widget should be visible
383 mocked_display.video_widget.setVisible.assert_called_once_with(True)
384
385 def test_set_duration(self):
386 """
387 Test the set_duration() method of the SystemPlayer
388 """
389 # GIVEN: a mocked controller
390 mocked_controller = MagicMock()
391 mocked_controller.media_info.length = 5
392
393 # WHEN: The set_duration() is called. NB: the 10 here is ignored by the code
394 SystemPlayer.set_duration(mocked_controller, 10)
395
396 # THEN: The maximum length of the slider should be set
397 mocked_controller.seek_slider.setMaximum.assert_called_once_with(5)
398
399 def test_update_ui(self):
400 """
401 Test the update_ui() method on the SystemPlayer
402 """
403 # GIVEN: A SystemPlayer instance
404 player = SystemPlayer(self)
405 player.state = [MediaState.Playing, MediaState.Playing]
406 mocked_display = MagicMock()
407 mocked_display.media_player.state.return_value = QtMultimedia.QMediaPlayer.PausedState
408 mocked_display.controller.media_info.end_time = 1
409 mocked_display.media_player.position.return_value = 2
410 mocked_display.controller.seek_slider.isSliderDown.return_value = False
411
412 # WHEN: update_ui() is called
413 with patch.object(player, 'stop') as mocked_stop, \
414 patch.object(player, 'set_visible') as mocked_set_visible:
415 player.update_ui(mocked_display)
416
417 # THEN: The UI is updated
418 expected_stop_calls = [call(mocked_display)]
419 expected_position_calls = [call(), call()]
420 expected_block_signals_calls = [call(True), call(False)]
421 mocked_display.media_player.state.assert_called_once_with()
422 assert 1 == mocked_stop.call_count
423 assert expected_stop_calls == mocked_stop.call_args_list
424 assert 2 == mocked_display.media_player.position.call_count
425 assert expected_position_calls == mocked_display.media_player.position.call_args_list
426 mocked_set_visible.assert_called_once_with(mocked_display, False)
427 mocked_display.controller.seek_slider.isSliderDown.assert_called_once_with()
428 assert expected_block_signals_calls == mocked_display.controller.seek_slider.blockSignals.call_args_list
429 mocked_display.controller.seek_slider.setSliderPosition.assert_called_once_with(2)
430
431 def test_get_media_display_css(self):
432 """
433 Test the get_media_display_css() method of the SystemPlayer
434 """
435 # GIVEN: A SystemPlayer instance
436 player = SystemPlayer(self)
437
438 # WHEN: get_media_display_css() is called
439 result = player.get_media_display_css()
440
441 # THEN: The css should be empty
442 assert '' == result
443
444 @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
445 def test_get_info(self, MockQMediaPlayer):
446 """
447 Test the get_info() method of the SystemPlayer
448 """
449 # GIVEN: A SystemPlayer instance
450 mocked_media_player = MagicMock()
451 mocked_media_player.supportedMimeTypes.return_value = []
452 MockQMediaPlayer.return_value = mocked_media_player
453 player = SystemPlayer(self)
454
455 # WHEN: get_info() is called
456 result = player.get_info()
457
458 # THEN: The info should be correct
459 expected_info = 'This media player uses your operating system to provide media capabilities.<br/> ' \
460 '<strong>Audio</strong><br/>[]<br/><strong>Video</strong><br/>[]<br/>'
461 assert expected_info == result
462
463 @patch('openlp.core.ui.media.systemplayer.CheckMediaWorker')
464 @patch('openlp.core.ui.media.systemplayer.QtCore.QThread')
465 def test_check_media(self, MockQThread, MockCheckMediaWorker):
466 """
467 Test the check_media() method of the SystemPlayer
468 """
469 # GIVEN: A SystemPlayer instance and a mocked thread
470 valid_file = '/path/to/video.ogv'
471 mocked_application = MagicMock()
472 Registry().create()
473 Registry().register('application', mocked_application)
474 player = SystemPlayer(self)
475 mocked_thread = MagicMock()
476 mocked_thread.isRunning.side_effect = [True, False]
477 mocked_thread.quit = 'quit' # actually supposed to be a slot, but it's all mocked out anyway
478 MockQThread.return_value = mocked_thread
479 mocked_check_media_worker = MagicMock()
480 mocked_check_media_worker.play = 'play'
481 mocked_check_media_worker.result = True
482 MockCheckMediaWorker.return_value = mocked_check_media_worker
483
484 # WHEN: check_media() is called with a valid media file
485 result = player.check_media(valid_file)
486
487 # THEN: It should return True
488 MockQThread.assert_called_once_with()
489 MockCheckMediaWorker.assert_called_once_with(valid_file)
490 mocked_check_media_worker.setVolume.assert_called_once_with(0)
491 mocked_check_media_worker.moveToThread.assert_called_once_with(mocked_thread)
492 mocked_check_media_worker.finished.connect.assert_called_once_with('quit')
493 mocked_thread.started.connect.assert_called_once_with('play')
494 mocked_thread.start.assert_called_once_with()
495 assert 2 == mocked_thread.isRunning.call_count
496 mocked_application.processEvents.assert_called_once_with()
497 assert result is True
498
499
500class TestCheckMediaWorker(TestCase):
501 """
502 Test the CheckMediaWorker class
503 """
504 def test_constructor(self):
505 """
506 Test the constructor of the CheckMediaWorker class
507 """
508 # GIVEN: A file path
509 path = 'file.ogv'
510
511 # WHEN: The CheckMediaWorker object is instantiated
512 worker = CheckMediaWorker(path)
513
514 # THEN: The correct values should be set up
515 assert worker is not None
516
517 def test_signals_media(self):
518 """
519 Test the signals() signal of the CheckMediaWorker class with a "media" origin
520 """
521 # GIVEN: A CheckMediaWorker instance
522 worker = CheckMediaWorker('file.ogv')
523
524 # WHEN: signals() is called with media and BufferedMedia
525 with patch.object(worker, 'stop') as mocked_stop, \
526 patch.object(worker, 'finished') as mocked_finished:
527 worker.signals('media', worker.BufferedMedia)
528
529 # THEN: The worker should exit and the result should be True
530 mocked_stop.assert_called_once_with()
531 mocked_finished.emit.assert_called_once_with()
532 assert worker.result is True
533
534 def test_signals_error(self):
535 """
536 Test the signals() signal of the CheckMediaWorker class with a "error" origin
537 """
538 # GIVEN: A CheckMediaWorker instance
539 worker = CheckMediaWorker('file.ogv')
540
541 # WHEN: signals() is called with error and BufferedMedia
542 with patch.object(worker, 'stop') as mocked_stop, \
543 patch.object(worker, 'finished') as mocked_finished:
544 worker.signals('error', None)
545
546 # THEN: The worker should exit and the result should be True
547 mocked_stop.assert_called_once_with()
548 mocked_finished.emit.assert_called_once_with()
549 assert worker.result is False
5500
=== modified file 'tests/functional/openlp_core/ui/test_firsttimeform.py'
--- tests/functional/openlp_core/ui/test_firsttimeform.py 2017-12-28 08:22:55 +0000
+++ tests/functional/openlp_core/ui/test_firsttimeform.py 2018-01-07 18:07:40 +0000
@@ -92,7 +92,6 @@
92 assert frw.web_access is True, 'The default value of self.web_access should be True'92 assert frw.web_access is True, 'The default value of self.web_access should be True'
93 assert frw.was_cancelled is False, 'The default value of self.was_cancelled should be False'93 assert frw.was_cancelled is False, 'The default value of self.was_cancelled should be False'
94 assert [] == frw.theme_screenshot_threads, 'The list of threads should be empty'94 assert [] == frw.theme_screenshot_threads, 'The list of threads should be empty'
95 assert [] == frw.theme_screenshot_workers, 'The list of workers should be empty'
96 assert frw.has_run_wizard is False, 'has_run_wizard should be False'95 assert frw.has_run_wizard is False, 'has_run_wizard should be False'
9796
98 def test_set_defaults(self):97 def test_set_defaults(self):
@@ -155,32 +154,33 @@
155 mocked_display_combo_box.count.assert_called_with()154 mocked_display_combo_box.count.assert_called_with()
156 mocked_display_combo_box.setCurrentIndex.assert_called_with(1)155 mocked_display_combo_box.setCurrentIndex.assert_called_with(1)
157156
158 def test_on_cancel_button_clicked(self):157 @patch('openlp.core.ui.firsttimeform.time')
158 @patch('openlp.core.ui.firsttimeform.get_thread_worker')
159 @patch('openlp.core.ui.firsttimeform.is_thread_finished')
160 def test_on_cancel_button_clicked(self, mocked_is_thread_finished, mocked_get_thread_worker, mocked_time):
159 """161 """
160 Test that the cancel button click slot shuts down the threads correctly162 Test that the cancel button click slot shuts down the threads correctly
161 """163 """
162 # GIVEN: A FRW, some mocked threads and workers (that isn't quite done) and other mocked stuff164 # GIVEN: A FRW, some mocked threads and workers (that isn't quite done) and other mocked stuff
163 frw = FirstTimeForm(None)
164 frw.initialize(MagicMock())
165 mocked_worker = MagicMock()165 mocked_worker = MagicMock()
166 mocked_thread = MagicMock()166 mocked_get_thread_worker.return_value = mocked_worker
167 mocked_thread.isRunning.side_effect = [True, False]167 mocked_is_thread_finished.side_effect = [False, True]
168 frw.theme_screenshot_workers.append(mocked_worker)168 frw = FirstTimeForm(None)
169 frw.theme_screenshot_threads.append(mocked_thread)169 frw.initialize(MagicMock())
170 with patch('openlp.core.ui.firsttimeform.time') as mocked_time, \170 frw.theme_screenshot_threads = ['test_thread']
171 patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor:171 with patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor:
172172
173 # WHEN: on_cancel_button_clicked() is called173 # WHEN: on_cancel_button_clicked() is called
174 frw.on_cancel_button_clicked()174 frw.on_cancel_button_clicked()
175175
176 # THEN: The right things should be called in the right order176 # THEN: The right things should be called in the right order
177 assert frw.was_cancelled is True, 'The was_cancelled property should have been set to True'177 assert frw.was_cancelled is True, 'The was_cancelled property should have been set to True'
178 mocked_get_thread_worker.assert_called_once_with('test_thread')
178 mocked_worker.set_download_canceled.assert_called_with(True)179 mocked_worker.set_download_canceled.assert_called_with(True)
179 mocked_thread.isRunning.assert_called_with()180 mocked_is_thread_finished.assert_called_with('test_thread')
180 assert 2 == mocked_thread.isRunning.call_count, 'isRunning() should have been called twice'181 assert mocked_is_thread_finished.call_count == 2, 'isRunning() should have been called twice'
181 mocked_time.sleep.assert_called_with(0.1)182 mocked_time.sleep.assert_called_once_with(0.1)
182 assert 1 == mocked_time.sleep.call_count, 'sleep() should have only been called once'183 mocked_set_normal_cursor.assert_called_once_with()
183 mocked_set_normal_cursor.assert_called_with()
184184
185 def test_broken_config(self):185 def test_broken_config(self):
186 """186 """
187187
=== modified file 'tests/functional/openlp_core/ui/test_mainwindow.py'
--- tests/functional/openlp_core/ui/test_mainwindow.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/ui/test_mainwindow.py 2018-01-07 18:07:40 +0000
@@ -60,9 +60,10 @@
60 # Mock cursor busy/normal methods.60 # Mock cursor busy/normal methods.
61 self.app.set_busy_cursor = MagicMock()61 self.app.set_busy_cursor = MagicMock()
62 self.app.set_normal_cursor = MagicMock()62 self.app.set_normal_cursor = MagicMock()
63 self.app.process_events = MagicMock()
63 self.app.args = []64 self.app.args = []
64 Registry().register('application', self.app)65 Registry().register('application', self.app)
65 Registry().set_flag('no_web_server', False)66 Registry().set_flag('no_web_server', True)
66 self.add_toolbar_action_patcher = patch('openlp.core.ui.mainwindow.create_action')67 self.add_toolbar_action_patcher = patch('openlp.core.ui.mainwindow.create_action')
67 self.mocked_add_toolbar_action = self.add_toolbar_action_patcher.start()68 self.mocked_add_toolbar_action = self.add_toolbar_action_patcher.start()
68 self.mocked_add_toolbar_action.side_effect = self._create_mock_action69 self.mocked_add_toolbar_action.side_effect = self._create_mock_action
@@ -74,8 +75,8 @@
74 """75 """
75 Delete all the C++ objects and stop all the patchers76 Delete all the C++ objects and stop all the patchers
76 """77 """
78 del self.main_window
77 self.add_toolbar_action_patcher.stop()79 self.add_toolbar_action_patcher.stop()
78 del self.main_window
7980
80 def test_cmd_line_file(self):81 def test_cmd_line_file(self):
81 """82 """
@@ -92,20 +93,20 @@
92 # THEN the service from the arguments is loaded93 # THEN the service from the arguments is loaded
93 mocked_load_file.assert_called_with(service)94 mocked_load_file.assert_called_with(service)
9495
95 def test_cmd_line_arg(self):96 @patch('openlp.core.ui.servicemanager.ServiceManager.load_file')
97 def test_cmd_line_arg(self, mocked_load_file):
96 """98 """
97 Test that passing a non service file does nothing.99 Test that passing a non service file does nothing.
98 """100 """
99 # GIVEN a non service file as an argument to openlp101 # GIVEN a non service file as an argument to openlp
100 service = os.path.join('openlp.py')102 service = os.path.join('openlp.py')
101 self.main_window.arguments = [service]103 self.main_window.arguments = [service]
102 with patch('openlp.core.ui.servicemanager.ServiceManager.load_file') as mocked_load_file:104
103105 # WHEN the argument is processed
104 # WHEN the argument is processed106 self.main_window.open_cmd_line_files(service)
105 self.main_window.open_cmd_line_files("")107
106108 # THEN the file should not be opened
107 # THEN the file should not be opened109 assert mocked_load_file.called is False, 'load_file should not have been called'
108 assert mocked_load_file.called is False, 'load_file should not have been called'
109110
110 def test_main_window_title(self):111 def test_main_window_title(self):
111 """112 """
112113
=== modified file 'tests/functional/openlp_plugins/songs/test_songselect.py'
--- tests/functional/openlp_plugins/songs/test_songselect.py 2017-12-29 10:19:33 +0000
+++ tests/functional/openlp_plugins/songs/test_songselect.py 2018-01-07 18:07:40 +0000
@@ -765,9 +765,9 @@
765 assert ssform.search_combobox.isEnabled() is True765 assert ssform.search_combobox.isEnabled() is True
766766
767 @patch('openlp.plugins.songs.forms.songselectform.Settings')767 @patch('openlp.plugins.songs.forms.songselectform.Settings')
768 @patch('openlp.plugins.songs.forms.songselectform.QtCore.QThread')768 @patch('openlp.plugins.songs.forms.songselectform.run_thread')
769 @patch('openlp.plugins.songs.forms.songselectform.SearchWorker')769 @patch('openlp.plugins.songs.forms.songselectform.SearchWorker')
770 def test_on_search_button_clicked(self, MockedSearchWorker, MockedQtThread, MockedSettings):770 def test_on_search_button_clicked(self, MockedSearchWorker, mocked_run_thread, MockedSettings):
771 """771 """
772 Test that search fields are disabled when search button is clicked.772 Test that search fields are disabled when search button is clicked.
773 """773 """
774774
=== modified file 'tests/interfaces/openlp_core/ui/test_mainwindow.py'
--- tests/interfaces/openlp_core/ui/test_mainwindow.py 2017-12-29 09:15:48 +0000
+++ tests/interfaces/openlp_core/ui/test_mainwindow.py 2018-01-07 18:07:40 +0000
@@ -44,21 +44,21 @@
44 self.app.set_normal_cursor = MagicMock()44 self.app.set_normal_cursor = MagicMock()
45 self.app.args = []45 self.app.args = []
46 Registry().register('application', self.app)46 Registry().register('application', self.app)
47 Registry().set_flag('no_web_server', False)47 Registry().set_flag('no_web_server', True)
48 # Mock classes and methods used by mainwindow.48 # Mock classes and methods used by mainwindow.
49 with patch('openlp.core.ui.mainwindow.SettingsForm') as mocked_settings_form, \49 with patch('openlp.core.ui.mainwindow.SettingsForm'), \
50 patch('openlp.core.ui.mainwindow.ImageManager') as mocked_image_manager, \50 patch('openlp.core.ui.mainwindow.ImageManager'), \
51 patch('openlp.core.ui.mainwindow.LiveController') as mocked_live_controller, \51 patch('openlp.core.ui.mainwindow.LiveController'), \
52 patch('openlp.core.ui.mainwindow.PreviewController') as mocked_preview_controller, \52 patch('openlp.core.ui.mainwindow.PreviewController'), \
53 patch('openlp.core.ui.mainwindow.OpenLPDockWidget') as mocked_dock_widget, \53 patch('openlp.core.ui.mainwindow.OpenLPDockWidget'), \
54 patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox') as mocked_q_tool_box_class, \54 patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox'), \
55 patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget') as mocked_add_dock_method, \55 patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget'), \
56 patch('openlp.core.ui.mainwindow.ServiceManager') as mocked_service_manager, \56 patch('openlp.core.ui.mainwindow.ServiceManager'), \
57 patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \57 patch('openlp.core.ui.mainwindow.ThemeManager'), \
58 patch('openlp.core.ui.mainwindow.ProjectorManager') as mocked_projector_manager, \58 patch('openlp.core.ui.mainwindow.ProjectorManager'), \
59 patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \59 patch('openlp.core.ui.mainwindow.Renderer'), \
60 patch('openlp.core.ui.mainwindow.websockets.WebSocketServer') as mocked_websocketserver, \60 patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \
61 patch('openlp.core.ui.mainwindow.server.HttpServer') as mocked_httpserver:61 patch('openlp.core.ui.mainwindow.server.HttpServer'):
62 self.main_window = MainWindow()62 self.main_window = MainWindow()
6363
64 def tearDown(self):64 def tearDown(self):