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
1=== modified file 'openlp/core/api/deploy.py'
2--- openlp/core/api/deploy.py 2017-12-29 09:15:48 +0000
3+++ openlp/core/api/deploy.py 2018-01-07 18:07:40 +0000
4@@ -25,7 +25,7 @@
5 from zipfile import ZipFile
6
7 from openlp.core.common.applocation import AppLocation
8-from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size
9+from openlp.core.common.httputils import download_file, get_web_page, get_url_file_size
10 from openlp.core.common.registry import Registry
11
12
13@@ -65,7 +65,7 @@
14 sha256, version = download_sha256()
15 file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip')
16 callback.setRange(0, file_size)
17- if url_get_file(callback, 'https://get.openlp.org/webclient/site.zip',
18- AppLocation.get_section_data_path('remotes') / 'site.zip',
19- sha256=sha256):
20+ if download_file(callback, 'https://get.openlp.org/webclient/site.zip',
21+ AppLocation.get_section_data_path('remotes') / 'site.zip',
22+ sha256=sha256):
23 deploy_zipfile(AppLocation.get_section_data_path('remotes'), 'site.zip')
24
25=== modified file 'openlp/core/api/http/server.py'
26--- openlp/core/api/http/server.py 2017-12-29 09:15:48 +0000
27+++ openlp/core/api/http/server.py 2018-01-07 18:07:40 +0000
28@@ -27,7 +27,7 @@
29 import time
30
31 from PyQt5 import QtCore, QtWidgets
32-from waitress import serve
33+from waitress.server import create_server
34
35 from openlp.core.api.deploy import download_and_check, download_sha256
36 from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint
37@@ -44,23 +44,16 @@
38 from openlp.core.common.path import create_paths
39 from openlp.core.common.registry import Registry, RegistryBase
40 from openlp.core.common.settings import Settings
41+from openlp.core.threading import ThreadWorker, run_thread
42
43 log = logging.getLogger(__name__)
44
45
46-class HttpWorker(QtCore.QObject):
47+class HttpWorker(ThreadWorker):
48 """
49 A special Qt thread class to allow the HTTP server to run at the same time as the UI.
50 """
51- def __init__(self):
52- """
53- Constructor for the thread class.
54-
55- :param server: The http server class.
56- """
57- super(HttpWorker, self).__init__()
58-
59- def run(self):
60+ def start(self):
61 """
62 Run the thread.
63 """
64@@ -68,12 +61,21 @@
65 port = Settings().value('api/port')
66 Registry().execute('get_website_version')
67 try:
68- serve(application, host=address, port=port)
69+ self.server = create_server(application, host=address, port=port)
70+ self.server.run()
71 except OSError:
72 log.exception('An error occurred when serving the application.')
73+ self.quit.emit()
74
75 def stop(self):
76- pass
77+ """
78+ A method to stop the worker
79+ """
80+ if hasattr(self, 'server'):
81+ # Loop through all the channels and close them to stop the server
82+ for channel in self.server._map.values():
83+ if hasattr(channel, 'close'):
84+ channel.close()
85
86
87 class HttpServer(RegistryBase, RegistryProperties, LogMixin):
88@@ -85,12 +87,9 @@
89 Initialise the http server, and start the http server
90 """
91 super(HttpServer, self).__init__(parent)
92- if Registry().get_flag('no_web_server'):
93- self.worker = HttpWorker()
94- self.thread = QtCore.QThread()
95- self.worker.moveToThread(self.thread)
96- self.thread.started.connect(self.worker.run)
97- self.thread.start()
98+ if not Registry().get_flag('no_web_server'):
99+ worker = HttpWorker()
100+ run_thread(worker, 'http_server')
101 Registry().register_function('download_website', self.first_time)
102 Registry().register_function('get_website_version', self.website_version)
103 Registry().set_flag('website_version', '0.0')
104@@ -167,7 +166,7 @@
105 self.was_cancelled = False
106 self.previous_size = 0
107
108- def _download_progress(self, count, block_size):
109+ def update_progress(self, count, block_size):
110 """
111 Calculate and display the download progress.
112 """
113
114=== modified file 'openlp/core/api/websockets.py'
115--- openlp/core/api/websockets.py 2017-12-29 09:15:48 +0000
116+++ openlp/core/api/websockets.py 2018-01-07 18:07:40 +0000
117@@ -28,37 +28,88 @@
118 import logging
119 import time
120
121-import websockets
122-from PyQt5 import QtCore
123+from websockets import serve
124
125 from openlp.core.common.mixins import LogMixin, RegistryProperties
126 from openlp.core.common.registry import Registry
127 from openlp.core.common.settings import Settings
128+from openlp.core.threading import ThreadWorker, run_thread
129
130 log = logging.getLogger(__name__)
131
132
133-class WebSocketWorker(QtCore.QObject):
134+async def handle_websocket(request, path):
135+ """
136+ Handle web socket requests and return the poll information
137+
138+ Check every 0.2 seconds to get the latest position and send if it changed. This only gets triggered when the first
139+ client connects.
140+
141+ :param request: request from client
142+ :param path: determines the endpoints supported
143+ """
144+ log.debug('WebSocket handler registered with client')
145+ previous_poll = None
146+ previous_main_poll = None
147+ poller = Registry().get('poller')
148+ if path == '/state':
149+ while True:
150+ current_poll = poller.poll()
151+ if current_poll != previous_poll:
152+ await request.send(json.dumps(current_poll).encode())
153+ previous_poll = current_poll
154+ await asyncio.sleep(0.2)
155+ elif path == '/live_changed':
156+ while True:
157+ main_poll = poller.main_poll()
158+ if main_poll != previous_main_poll:
159+ await request.send(main_poll)
160+ previous_main_poll = main_poll
161+ await asyncio.sleep(0.2)
162+
163+
164+class WebSocketWorker(ThreadWorker, RegistryProperties, LogMixin):
165 """
166 A special Qt thread class to allow the WebSockets server to run at the same time as the UI.
167 """
168- def __init__(self, server):
169- """
170- Constructor for the thread class.
171-
172- :param server: The http server class.
173- """
174- self.ws_server = server
175- super(WebSocketWorker, self).__init__()
176-
177- def run(self):
178- """
179- Run the thread.
180- """
181- self.ws_server.start_server()
182+ def start(self):
183+ """
184+ Run the worker.
185+ """
186+ address = Settings().value('api/ip address')
187+ port = Settings().value('api/websocket port')
188+ # Start the event loop
189+ self.event_loop = asyncio.new_event_loop()
190+ asyncio.set_event_loop(self.event_loop)
191+ # Create the websocker server
192+ loop = 1
193+ self.server = None
194+ while not self.server:
195+ try:
196+ self.server = serve(handle_websocket, address, port)
197+ log.debug('WebSocket server started on {addr}:{port}'.format(addr=address, port=port))
198+ except Exception:
199+ log.exception('Failed to start WebSocket server')
200+ loop += 1
201+ time.sleep(0.1)
202+ if not self.server and loop > 3:
203+ log.error('Unable to start WebSocket server {addr}:{port}, giving up'.format(addr=address, port=port))
204+ if self.server:
205+ # If the websocket server exists, start listening
206+ self.event_loop.run_until_complete(self.server)
207+ self.event_loop.run_forever()
208+ self.quit.emit()
209
210 def stop(self):
211- self.ws_server.stop = True
212+ """
213+ Stop the websocket server
214+ """
215+ if hasattr(self.server, 'ws_server'):
216+ self.server.ws_server.close()
217+ elif hasattr(self.server, 'server'):
218+ self.server.server.close()
219+ self.event_loop.stop()
220+ self.event_loop.close()
221
222
223 class WebSocketServer(RegistryProperties, LogMixin):
224@@ -70,74 +121,6 @@
225 Initialise and start the WebSockets server
226 """
227 super(WebSocketServer, self).__init__()
228- if Registry().get_flag('no_web_server'):
229- self.settings_section = 'api'
230- self.worker = WebSocketWorker(self)
231- self.thread = QtCore.QThread()
232- self.worker.moveToThread(self.thread)
233- self.thread.started.connect(self.worker.run)
234- self.thread.start()
235-
236- def start_server(self):
237- """
238- Start the correct server and save the handler
239- """
240- address = Settings().value(self.settings_section + '/ip address')
241- port = Settings().value(self.settings_section + '/websocket port')
242- self.start_websocket_instance(address, port)
243- # If web socket server start listening
244- if hasattr(self, 'ws_server') and self.ws_server:
245- event_loop = asyncio.new_event_loop()
246- asyncio.set_event_loop(event_loop)
247- event_loop.run_until_complete(self.ws_server)
248- event_loop.run_forever()
249- else:
250- log.debug('Failed to start ws server on port {port}'.format(port=port))
251-
252- def start_websocket_instance(self, address, port):
253- """
254- Start the server
255-
256- :param address: The server address
257- :param port: The run port
258- """
259- loop = 1
260- while loop < 4:
261- try:
262- self.ws_server = websockets.serve(self.handle_websocket, address, port)
263- log.debug("Web Socket Server started for class {address} {port}".format(address=address, port=port))
264- break
265- except Exception as e:
266- log.error('Failed to start ws server {why}'.format(why=e))
267- loop += 1
268- time.sleep(0.1)
269-
270- @staticmethod
271- async def handle_websocket(request, path):
272- """
273- Handle web socket requests and return the poll information.
274- Check ever 0.2 seconds to get the latest position and send if changed.
275- Only gets triggered when 1st client attaches
276-
277- :param request: request from client
278- :param path: determines the endpoints supported
279- :return:
280- """
281- log.debug("web socket handler registered with client")
282- previous_poll = None
283- previous_main_poll = None
284- poller = Registry().get('poller')
285- if path == '/state':
286- while True:
287- current_poll = poller.poll()
288- if current_poll != previous_poll:
289- await request.send(json.dumps(current_poll).encode())
290- previous_poll = current_poll
291- await asyncio.sleep(0.2)
292- elif path == '/live_changed':
293- while True:
294- main_poll = poller.main_poll()
295- if main_poll != previous_main_poll:
296- await request.send(main_poll)
297- previous_main_poll = main_poll
298- await asyncio.sleep(0.2)
299+ if not Registry().get_flag('no_web_server'):
300+ worker = WebSocketWorker()
301+ run_thread(worker, 'websocket_server')
302
303=== modified file 'openlp/core/app.py'
304--- openlp/core/app.py 2017-12-29 09:15:48 +0000
305+++ openlp/core/app.py 2018-01-07 18:07:40 +0000
306@@ -304,8 +304,7 @@
307 'off a USB flash drive (not implemented).')
308 parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',
309 help='Ignore the version file and pull the version directly from Bazaar')
310- parser.add_argument('-s', '--style', dest='style', help='Set the Qt5 style (passed directly to Qt5).')
311- parser.add_argument('-w', '--no-web-server', dest='no_web_server', action='store_false',
312+ parser.add_argument('-w', '--no-web-server', dest='no_web_server', action='store_true',
313 help='Turn off the Web and Socket Server ')
314 parser.add_argument('rargs', nargs='?', default=[])
315 # Parse command line options and deal with them. Use args supplied pragmatically if possible.
316@@ -343,8 +342,6 @@
317 log.setLevel(logging.WARNING)
318 else:
319 log.setLevel(logging.INFO)
320- if args and args.style:
321- qt_args.extend(['-style', args.style])
322 # Throw the rest of the arguments at Qt, just in case.
323 qt_args.extend(args.rargs)
324 # Bug #1018855: Set the WM_CLASS property in X11
325@@ -358,7 +355,7 @@
326 application.setOrganizationDomain('openlp.org')
327 application.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
328 application.setAttribute(QtCore.Qt.AA_DontCreateNativeWidgetSiblings, True)
329- if args and args.portable:
330+ if args.portable:
331 application.setApplicationName('OpenLPPortable')
332 Settings.setDefaultFormat(Settings.IniFormat)
333 # Get location OpenLPPortable.ini
334
335=== modified file 'openlp/core/common/applocation.py'
336--- openlp/core/common/applocation.py 2017-12-29 09:15:48 +0000
337+++ openlp/core/common/applocation.py 2018-01-07 18:07:40 +0000
338@@ -157,7 +157,7 @@
339 return directory
340 return Path('/usr', 'share', 'openlp')
341 if XDG_BASE_AVAILABLE:
342- if dir_type == AppLocation.DataDir or dir_type == AppLocation.CacheDir:
343+ if dir_type == AppLocation.DataDir:
344 return Path(BaseDirectory.xdg_data_home, 'openlp')
345 elif dir_type == AppLocation.CacheDir:
346 return Path(BaseDirectory.xdg_cache_home, 'openlp')
347
348=== modified file 'openlp/core/common/httputils.py'
349--- openlp/core/common/httputils.py 2017-12-29 09:15:48 +0000
350+++ openlp/core/common/httputils.py 2018-01-07 18:07:40 +0000
351@@ -20,7 +20,7 @@
352 # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
353 ###############################################################################
354 """
355-The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP.
356+The :mod:`openlp.core.common.httputils` module provides the utility methods for downloading stuff.
357 """
358 import hashlib
359 import logging
360@@ -104,7 +104,7 @@
361 if retries >= CONNECTION_RETRIES:
362 raise ConnectionError('Unable to connect to {url}, see log for details'.format(url=url))
363 retries += 1
364- except:
365+ except: # noqa
366 # Don't know what's happening, so reraise the original
367 log.exception('Unknown error when trying to connect to {url}'.format(url=url))
368 raise
369@@ -136,12 +136,12 @@
370 continue
371
372
373-def url_get_file(callback, url, file_path, sha256=None):
374+def download_file(update_object, url, file_path, sha256=None):
375 """"
376 Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any
377 point. Returns False on download error.
378
379- :param callback: the class which needs to be updated
380+ :param update_object: the object which needs to be updated
381 :param url: URL to download
382 :param file_path: Destination file
383 :param sha256: The check sum value to be checked against the download value
384@@ -158,13 +158,14 @@
385 hasher = hashlib.sha256()
386 # Download until finished or canceled.
387 for chunk in response.iter_content(chunk_size=block_size):
388- if callback.was_cancelled:
389+ if hasattr(update_object, 'was_cancelled') and update_object.was_cancelled:
390 break
391 saved_file.write(chunk)
392 if sha256:
393 hasher.update(chunk)
394 block_count += 1
395- callback._download_progress(block_count, block_size)
396+ if hasattr(update_object, 'update_progress'):
397+ update_object.update_progress(block_count, block_size)
398 response.close()
399 if sha256 and hasher.hexdigest() != sha256:
400 log.error('sha256 sums did not match for file %s, got %s, expected %s', file_path, hasher.hexdigest(),
401@@ -183,7 +184,7 @@
402 retries += 1
403 time.sleep(0.1)
404 continue
405- if callback.was_cancelled and file_path.exists():
406+ if hasattr(update_object, 'was_cancelled') and update_object.was_cancelled and file_path.exists():
407 file_path.unlink()
408 return True
409
410
411=== modified file 'openlp/core/lib/imagemanager.py'
412--- openlp/core/lib/imagemanager.py 2017-12-29 09:15:48 +0000
413+++ openlp/core/lib/imagemanager.py 2018-01-07 18:07:40 +0000
414@@ -35,13 +35,14 @@
415 from openlp.core.common.settings import Settings
416 from openlp.core.display.screens import ScreenList
417 from openlp.core.lib import resize_image, image_to_byte
418+from openlp.core.threading import ThreadWorker, run_thread
419
420 log = logging.getLogger(__name__)
421
422
423-class ImageThread(QtCore.QThread):
424+class ImageWorker(ThreadWorker):
425 """
426- A special Qt thread class to speed up the display of images. This is threaded so it loads the frames and generates
427+ A thread worker class to speed up the display of images. This is threaded so it loads the frames and generates
428 byte stream in background.
429 """
430 def __init__(self, manager):
431@@ -51,14 +52,21 @@
432 ``manager``
433 The image manager.
434 """
435- super(ImageThread, self).__init__(None)
436+ super().__init__()
437 self.image_manager = manager
438
439- def run(self):
440+ def start(self):
441 """
442- Run the thread.
443+ Start the worker
444 """
445 self.image_manager.process()
446+ self.quit.emit()
447+
448+ def stop(self):
449+ """
450+ Stop the worker
451+ """
452+ self.image_manager.stop_manager = True
453
454
455 class Priority(object):
456@@ -130,7 +138,7 @@
457
458 class PriorityQueue(queue.PriorityQueue):
459 """
460- Customised ``Queue.PriorityQueue``.
461+ Customised ``queue.PriorityQueue``.
462
463 Each item in the queue must be a tuple with three values. The first value is the :class:`Image`'s ``priority``
464 attribute, the second value the :class:`Image`'s ``secondary_priority`` attribute. The last value the :class:`Image`
465@@ -179,7 +187,6 @@
466 self.width = current_screen['size'].width()
467 self.height = current_screen['size'].height()
468 self._cache = {}
469- self.image_thread = ImageThread(self)
470 self._conversion_queue = PriorityQueue()
471 self.stop_manager = False
472 Registry().register_function('images_regenerate', self.process_updates)
473@@ -230,9 +237,13 @@
474 """
475 Flush the queue to updated any data to update
476 """
477- # We want only one thread.
478- if not self.image_thread.isRunning():
479- self.image_thread.start()
480+ try:
481+ worker = ImageWorker(self)
482+ run_thread(worker, 'image_manager')
483+ except KeyError:
484+ # run_thread() will throw a KeyError if this thread already exists, so ignore it so that we don't
485+ # try to start another thread when one is already running
486+ pass
487
488 def get_image(self, path, source, width=-1, height=-1):
489 """
490@@ -305,9 +316,7 @@
491 if image.path == path and image.timestamp != os.stat(path).st_mtime:
492 image.timestamp = os.stat(path).st_mtime
493 self._reset_image(image)
494- # We want only one thread.
495- if not self.image_thread.isRunning():
496- self.image_thread.start()
497+ self.process_updates()
498
499 def process(self):
500 """
501
502=== modified file 'openlp/core/projectors/manager.py'
503--- openlp/core/projectors/manager.py 2018-01-03 00:35:14 +0000
504+++ openlp/core/projectors/manager.py 2018-01-07 18:07:40 +0000
505@@ -308,8 +308,7 @@
506 self.settings_section = 'projector'
507 self.projectordb = projectordb
508 self.projector_list = []
509- self.pjlink_udp = PJLinkUDP()
510- self.pjlink_udp.projector_list = self.projector_list
511+ self.pjlink_udp = PJLinkUDP(self.projector_list)
512 self.source_select_form = None
513
514 def bootstrap_initialise(self):
515
516=== modified file 'openlp/core/projectors/pjlink.py'
517--- openlp/core/projectors/pjlink.py 2018-01-03 00:35:14 +0000
518+++ openlp/core/projectors/pjlink.py 2018-01-07 18:07:40 +0000
519@@ -89,11 +89,11 @@
520 'SRCH' # Class 2 (reply is ACKN)
521 ]
522
523- def __init__(self, port=PJLINK_PORT):
524+ def __init__(self, projector_list, port=PJLINK_PORT):
525 """
526 Initialize socket
527 """
528-
529+ self.projector_list = projector_list
530 self.port = port
531
532
533
534=== modified file 'openlp/core/threading.py'
535--- openlp/core/threading.py 2017-12-29 09:15:48 +0000
536+++ openlp/core/threading.py 2018-01-07 18:07:40 +0000
537@@ -24,26 +24,41 @@
538 """
539 from PyQt5 import QtCore
540
541-
542-def run_thread(parent, worker, prefix='', auto_start=True):
543+from openlp.core.common.registry import Registry
544+
545+
546+class ThreadWorker(QtCore.QObject):
547+ """
548+ The :class:`~openlp.core.threading.ThreadWorker` class provides a base class for all worker objects
549+ """
550+ quit = QtCore.pyqtSignal()
551+
552+ def start(self):
553+ """
554+ The start method is how the worker runs. Basically, put your code here.
555+ """
556+ raise NotImplementedError('Your base class needs to override this method and run self.quit.emit() at the end.')
557+
558+
559+def run_thread(worker, thread_name, can_start=True):
560 """
561 Create a thread and assign a worker to it. This removes a lot of boilerplate code from the codebase.
562
563- :param object parent: The parent object so that the thread and worker are not orphaned.
564 :param QObject worker: A QObject-based worker object which does the actual work.
565- :param str prefix: A prefix to be applied to the attribute names.
566- :param bool auto_start: Automatically start the thread. Defaults to True.
567+ :param str thread_name: The name of the thread, used to keep track of the thread.
568+ :param bool can_start: Start the thread. Defaults to True.
569 """
570- # Set up attribute names
571- thread_name = 'thread'
572- worker_name = 'worker'
573- if prefix:
574- thread_name = '_'.join([prefix, thread_name])
575- worker_name = '_'.join([prefix, worker_name])
576+ if not thread_name:
577+ raise ValueError('A thread_name is required when calling the "run_thread" function')
578+ main_window = Registry().get('main_window')
579+ if thread_name in main_window.threads:
580+ raise KeyError('A thread with the name "{}" has already been created, please use another'.format(thread_name))
581 # Create the thread and add the thread and the worker to the parent
582 thread = QtCore.QThread()
583- setattr(parent, thread_name, thread)
584- setattr(parent, worker_name, worker)
585+ main_window.threads[thread_name] = {
586+ 'thread': thread,
587+ 'worker': worker
588+ }
589 # Move the worker into the thread's context
590 worker.moveToThread(thread)
591 # Connect slots and signals
592@@ -51,5 +66,46 @@
593 worker.quit.connect(thread.quit)
594 worker.quit.connect(worker.deleteLater)
595 thread.finished.connect(thread.deleteLater)
596- if auto_start:
597+ thread.finished.connect(make_remove_thread(thread_name))
598+ if can_start:
599 thread.start()
600+
601+
602+def get_thread_worker(thread_name):
603+ """
604+ Get the worker by the thread name
605+
606+ :param str thread_name: The name of the thread
607+ :returns: The worker for this thread name
608+ """
609+ return Registry().get('main_window').threads.get(thread_name)
610+
611+
612+def is_thread_finished(thread_name):
613+ """
614+ Check if a thread is finished running.
615+
616+ :param str thread_name: The name of the thread
617+ :returns: True if the thread is finished, False if it is still running
618+ """
619+ main_window = Registry().get('main_window')
620+ return thread_name not in main_window.threads or main_window.threads[thread_name]['thread'].isFinished()
621+
622+
623+def make_remove_thread(thread_name):
624+ """
625+ Create a function to remove the thread once the thread is finished.
626+
627+ :param str thread_name: The name of the thread which should be removed from the thread registry.
628+ :returns: A function which will remove the thread from the thread registry.
629+ """
630+ def remove_thread():
631+ """
632+ Stop and remove a registered thread
633+
634+ :param str thread_name: The name of the thread to stop and remove
635+ """
636+ main_window = Registry().get('main_window')
637+ if thread_name in main_window.threads:
638+ del main_window.threads[thread_name]
639+ return remove_thread
640
641=== modified file 'openlp/core/ui/firsttimeform.py'
642--- openlp/core/ui/firsttimeform.py 2017-12-29 09:15:48 +0000
643+++ openlp/core/ui/firsttimeform.py 2018-01-07 18:07:40 +0000
644@@ -23,8 +23,6 @@
645 This module contains the first time wizard.
646 """
647 import logging
648-import os
649-import socket
650 import time
651 import urllib.error
652 import urllib.parse
653@@ -36,7 +34,7 @@
654
655 from openlp.core.common import clean_button_text, trace_error_handler
656 from openlp.core.common.applocation import AppLocation
657-from openlp.core.common.httputils import get_web_page, get_url_file_size, url_get_file, CONNECTION_TIMEOUT
658+from openlp.core.common.httputils import get_web_page, get_url_file_size, download_file
659 from openlp.core.common.i18n import translate
660 from openlp.core.common.mixins import RegistryProperties
661 from openlp.core.common.path import Path, create_paths
662@@ -44,46 +42,47 @@
663 from openlp.core.common.settings import Settings
664 from openlp.core.lib import PluginStatus, build_icon
665 from openlp.core.lib.ui import critical_error_message_box
666-from .firsttimewizard import UiFirstTimeWizard, FirstTimePage
667+from openlp.core.threading import ThreadWorker, run_thread, get_thread_worker, is_thread_finished
668+from openlp.core.ui.firsttimewizard import UiFirstTimeWizard, FirstTimePage
669
670 log = logging.getLogger(__name__)
671
672
673-class ThemeScreenshotWorker(QtCore.QObject):
674+class ThemeScreenshotWorker(ThreadWorker):
675 """
676 This thread downloads a theme's screenshot
677 """
678 screenshot_downloaded = QtCore.pyqtSignal(str, str, str)
679- finished = QtCore.pyqtSignal()
680
681 def __init__(self, themes_url, title, filename, sha256, screenshot):
682 """
683 Set up the worker object
684 """
685- self.was_download_cancelled = False
686+ self.was_cancelled = False
687 self.themes_url = themes_url
688 self.title = title
689 self.filename = filename
690 self.sha256 = sha256
691 self.screenshot = screenshot
692- socket.setdefaulttimeout(CONNECTION_TIMEOUT)
693- super(ThemeScreenshotWorker, self).__init__()
694+ super().__init__()
695
696- def run(self):
697- """
698- Overridden method to run the thread.
699- """
700- if self.was_download_cancelled:
701+ def start(self):
702+ """
703+ Run the worker
704+ """
705+ if self.was_cancelled:
706 return
707 try:
708- urllib.request.urlretrieve('{host}{name}'.format(host=self.themes_url, name=self.screenshot),
709- os.path.join(gettempdir(), 'openlp', self.screenshot))
710- # Signal that the screenshot has been downloaded
711- self.screenshot_downloaded.emit(self.title, self.filename, self.sha256)
712- except:
713+ download_path = Path(gettempdir()) / 'openlp' / self.screenshot
714+ is_success = download_file(self, '{host}{name}'.format(host=self.themes_url, name=self.screenshot),
715+ download_path)
716+ if is_success and not self.was_cancelled:
717+ # Signal that the screenshot has been downloaded
718+ self.screenshot_downloaded.emit(self.title, self.filename, self.sha256)
719+ except: # noqa
720 log.exception('Unable to download screenshot')
721 finally:
722- self.finished.emit()
723+ self.quit.emit()
724
725 @QtCore.pyqtSlot(bool)
726 def set_download_canceled(self, toggle):
727@@ -145,12 +144,13 @@
728 return FirstTimePage.Progress
729 elif self.currentId() == FirstTimePage.Themes:
730 self.application.set_busy_cursor()
731- while not all([thread.isFinished() for thread in self.theme_screenshot_threads]):
732+ while not all([is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]):
733 time.sleep(0.1)
734 self.application.process_events()
735 # Build the screenshot icons, as this can not be done in the thread.
736 self._build_theme_screenshots()
737 self.application.set_normal_cursor()
738+ self.theme_screenshot_threads = []
739 return FirstTimePage.Defaults
740 else:
741 return self.get_next_page_id()
742@@ -171,7 +171,6 @@
743 self.screens = screens
744 self.was_cancelled = False
745 self.theme_screenshot_threads = []
746- self.theme_screenshot_workers = []
747 self.has_run_wizard = False
748
749 def _download_index(self):
750@@ -256,14 +255,10 @@
751 sha256 = self.config.get('theme_{theme}'.format(theme=theme), 'sha256', fallback='')
752 screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot')
753 worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot)
754- self.theme_screenshot_workers.append(worker)
755 worker.screenshot_downloaded.connect(self.on_screenshot_downloaded)
756- thread = QtCore.QThread(self)
757- self.theme_screenshot_threads.append(thread)
758- thread.started.connect(worker.run)
759- worker.finished.connect(thread.quit)
760- worker.moveToThread(thread)
761- thread.start()
762+ thread_name = 'theme_screenshot_{title}'.format(title=title)
763+ run_thread(worker, thread_name)
764+ self.theme_screenshot_threads.append(thread_name)
765 self.application.process_events()
766
767 def set_defaults(self):
768@@ -353,12 +348,14 @@
769 Process the triggering of the cancel button.
770 """
771 self.was_cancelled = True
772- if self.theme_screenshot_workers:
773- for worker in self.theme_screenshot_workers:
774- worker.set_download_canceled(True)
775+ if self.theme_screenshot_threads:
776+ for thread_name in self.theme_screenshot_threads:
777+ worker = get_thread_worker(thread_name)
778+ if worker:
779+ worker.set_download_canceled(True)
780 # Was the thread created.
781 if self.theme_screenshot_threads:
782- while any([thread.isRunning() for thread in self.theme_screenshot_threads]):
783+ while any([not is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]):
784 time.sleep(0.1)
785 self.application.set_normal_cursor()
786
787@@ -562,8 +559,8 @@
788 self._increment_progress_bar(self.downloading.format(name=filename), 0)
789 self.previous_size = 0
790 destination = songs_destination_path / str(filename)
791- if not url_get_file(self, '{path}{name}'.format(path=self.songs_url, name=filename),
792- destination, sha256):
793+ if not download_file(self, '{path}{name}'.format(path=self.songs_url, name=filename),
794+ destination, sha256):
795 missed_files.append('Song: {name}'.format(name=filename))
796 # Download Bibles
797 bibles_iterator = QtWidgets.QTreeWidgetItemIterator(self.bibles_tree_widget)
798@@ -573,8 +570,8 @@
799 bible, sha256 = item.data(0, QtCore.Qt.UserRole)
800 self._increment_progress_bar(self.downloading.format(name=bible), 0)
801 self.previous_size = 0
802- if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible),
803- bibles_destination_path / bible, sha256):
804+ if not download_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible),
805+ bibles_destination_path / bible, sha256):
806 missed_files.append('Bible: {name}'.format(name=bible))
807 bibles_iterator += 1
808 # Download themes
809@@ -584,8 +581,8 @@
810 theme, sha256 = item.data(QtCore.Qt.UserRole)
811 self._increment_progress_bar(self.downloading.format(name=theme), 0)
812 self.previous_size = 0
813- if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme),
814- themes_destination_path / theme, sha256):
815+ if not download_file(self, '{path}{name}'.format(path=self.themes_url, name=theme),
816+ themes_destination_path / theme, sha256):
817 missed_files.append('Theme: {name}'.format(name=theme))
818 if missed_files:
819 file_list = ''
820
821=== modified file 'openlp/core/ui/mainwindow.py'
822--- openlp/core/ui/mainwindow.py 2017-12-29 09:15:48 +0000
823+++ openlp/core/ui/mainwindow.py 2018-01-07 18:07:40 +0000
824@@ -24,7 +24,6 @@
825 """
826 import logging
827 import sys
828-import time
829 from datetime import datetime
830 from distutils import dir_util
831 from distutils.errors import DistutilsFileError
832@@ -478,8 +477,7 @@
833 """
834 super(MainWindow, self).__init__()
835 Registry().register('main_window', self)
836- self.version_thread = None
837- self.version_worker = None
838+ self.threads = {}
839 self.clipboard = self.application.clipboard()
840 self.arguments = ''.join(self.application.args)
841 # Set up settings sections for the main application (not for use by plugins).
842@@ -501,8 +499,8 @@
843 Settings().set_up_default_values()
844 self.about_form = AboutForm(self)
845 MediaController()
846- websockets.WebSocketServer()
847- server.HttpServer()
848+ self.ws_server = websockets.WebSocketServer()
849+ self.http_server = server.HttpServer(self)
850 SettingsForm(self)
851 self.formatting_tag_form = FormattingTagForm(self)
852 self.shortcut_form = ShortcutListForm(self)
853@@ -549,6 +547,41 @@
854 # Reset the cursor
855 self.application.set_normal_cursor()
856
857+ def _wait_for_threads(self):
858+ """
859+ Wait for the threads
860+ """
861+ # Sometimes the threads haven't finished, let's wait for them
862+ wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self)
863+ wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
864+ wait_dialog.setAutoClose(False)
865+ wait_dialog.setCancelButton(None)
866+ wait_dialog.show()
867+ for thread_name in self.threads.keys():
868+ log.debug('Waiting for thread %s', thread_name)
869+ self.application.processEvents()
870+ thread = self.threads[thread_name]['thread']
871+ worker = self.threads[thread_name]['worker']
872+ try:
873+ if worker and hasattr(worker, 'stop'):
874+ # If the worker has a stop method, run it
875+ worker.stop()
876+ if thread and thread.isRunning():
877+ # If the thread is running, let's wait 5 seconds for it
878+ retry = 0
879+ while thread.isRunning() and retry < 50:
880+ # Make the GUI responsive while we wait
881+ self.application.processEvents()
882+ thread.wait(100)
883+ retry += 1
884+ if thread.isRunning():
885+ # If the thread is still running after 5 seconds, kill it
886+ thread.terminate()
887+ except RuntimeError:
888+ # Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object
889+ pass
890+ wait_dialog.close()
891+
892 def bootstrap_post_set_up(self):
893 """
894 process the bootstrap post setup request
895@@ -695,7 +728,7 @@
896 # Update the theme widget
897 self.theme_manager_contents.load_themes()
898 # Check if any Bibles downloaded. If there are, they will be processed.
899- Registry().execute('bibles_load_list', True)
900+ Registry().execute('bibles_load_list')
901 self.application.set_normal_cursor()
902
903 def is_display_blank(self):
904@@ -1000,39 +1033,14 @@
905 if not self.application.is_event_loop_active:
906 event.ignore()
907 return
908- # Sometimes the version thread hasn't finished, let's wait for it
909- try:
910- if self.version_thread and self.version_thread.isRunning():
911- wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self)
912- wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
913- wait_dialog.setAutoClose(False)
914- wait_dialog.setCancelButton(None)
915- wait_dialog.show()
916- retry = 0
917- while self.version_thread.isRunning() and retry < 50:
918- self.application.processEvents()
919- self.version_thread.wait(100)
920- retry += 1
921- if self.version_thread.isRunning():
922- self.version_thread.terminate()
923- wait_dialog.close()
924- except RuntimeError:
925- # Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object
926- pass
927- # If we just did a settings import, close without saving changes.
928- if self.settings_imported:
929- self.clean_up(False)
930- event.accept()
931 if self.service_manager_contents.is_modified():
932 ret = self.service_manager_contents.save_modified_service()
933 if ret == QtWidgets.QMessageBox.Save:
934 if self.service_manager_contents.decide_save_method():
935- self.clean_up()
936 event.accept()
937 else:
938 event.ignore()
939 elif ret == QtWidgets.QMessageBox.Discard:
940- self.clean_up()
941 event.accept()
942 else:
943 event.ignore()
944@@ -1048,13 +1056,16 @@
945 close_button.setText(translate('OpenLP.MainWindow', '&Exit OpenLP'))
946 msg_box.setDefaultButton(QtWidgets.QMessageBox.Close)
947 if msg_box.exec() == QtWidgets.QMessageBox.Close:
948- self.clean_up()
949 event.accept()
950 else:
951 event.ignore()
952 else:
953- self.clean_up()
954 event.accept()
955+ if event.isAccepted():
956+ # Wait for all the threads to complete
957+ self._wait_for_threads()
958+ # If we just did a settings import, close without saving changes.
959+ self.clean_up(save_settings=not self.settings_imported)
960
961 def clean_up(self, save_settings=True):
962 """
963@@ -1062,9 +1073,6 @@
964
965 :param save_settings: Switch to prevent saving settings. Defaults to **True**.
966 """
967- self.image_manager.stop_manager = True
968- while self.image_manager.image_thread.isRunning():
969- time.sleep(0.1)
970 if save_settings:
971 if Settings().value('advanced/save current plugin'):
972 Settings().setValue('advanced/current media plugin', self.media_tool_box.currentIndex())
973
974=== modified file 'openlp/core/ui/media/systemplayer.py'
975--- openlp/core/ui/media/systemplayer.py 2017-12-29 09:15:48 +0000
976+++ openlp/core/ui/media/systemplayer.py 2018-01-07 18:07:40 +0000
977@@ -31,6 +31,7 @@
978 from openlp.core.common.i18n import translate
979 from openlp.core.ui.media import MediaState
980 from openlp.core.ui.media.mediaplayer import MediaPlayer
981+from openlp.core.threading import ThreadWorker, run_thread, is_thread_finished
982
983 log = logging.getLogger(__name__)
984
985@@ -293,39 +294,38 @@
986 :param path: Path to file to be checked
987 :return: True if file can be played otherwise False
988 """
989- thread = QtCore.QThread()
990 check_media_worker = CheckMediaWorker(path)
991 check_media_worker.setVolume(0)
992- check_media_worker.moveToThread(thread)
993- check_media_worker.finished.connect(thread.quit)
994- thread.started.connect(check_media_worker.play)
995- thread.start()
996- while thread.isRunning():
997+ run_thread(check_media_worker, 'check_media')
998+ while not is_thread_finished('check_media'):
999 self.application.processEvents()
1000 return check_media_worker.result
1001
1002
1003-class CheckMediaWorker(QtMultimedia.QMediaPlayer):
1004+class CheckMediaWorker(QtMultimedia.QMediaPlayer, ThreadWorker):
1005 """
1006 Class used to check if a media file is playable
1007 """
1008- finished = QtCore.pyqtSignal()
1009-
1010 def __init__(self, path):
1011 super(CheckMediaWorker, self).__init__(None, QtMultimedia.QMediaPlayer.VideoSurface)
1012+ self.path = path
1013+
1014+ def start(self):
1015+ """
1016+ Start the thread worker
1017+ """
1018 self.result = None
1019-
1020 self.error.connect(functools.partial(self.signals, 'error'))
1021 self.mediaStatusChanged.connect(functools.partial(self.signals, 'media'))
1022-
1023- self.setMedia(QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile(path)))
1024+ self.setMedia(QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile(self.path)))
1025+ self.play()
1026
1027 def signals(self, origin, status):
1028 if origin == 'media' and status == self.BufferedMedia:
1029 self.result = True
1030 self.stop()
1031- self.finished.emit()
1032+ self.quit.emit()
1033 elif origin == 'error' and status != self.NoError:
1034 self.result = False
1035 self.stop()
1036- self.finished.emit()
1037+ self.quit.emit()
1038
1039=== modified file 'openlp/core/version.py'
1040--- openlp/core/version.py 2018-01-02 21:00:54 +0000
1041+++ openlp/core/version.py 2018-01-07 18:07:40 +0000
1042@@ -35,7 +35,7 @@
1043
1044 from openlp.core.common.applocation import AppLocation
1045 from openlp.core.common.settings import Settings
1046-from openlp.core.threading import run_thread
1047+from openlp.core.threading import ThreadWorker, run_thread
1048
1049 log = logging.getLogger(__name__)
1050
1051@@ -44,14 +44,13 @@
1052 CONNECTION_RETRIES = 2
1053
1054
1055-class VersionWorker(QtCore.QObject):
1056+class VersionWorker(ThreadWorker):
1057 """
1058 A worker class to fetch the version of OpenLP from the website. This is run from within a thread so that it
1059 doesn't affect the loading time of OpenLP.
1060 """
1061 new_version = QtCore.pyqtSignal(dict)
1062 no_internet = QtCore.pyqtSignal()
1063- quit = QtCore.pyqtSignal()
1064
1065 def __init__(self, last_check_date, current_version):
1066 """
1067@@ -110,22 +109,22 @@
1068 Settings().setValue('core/last version test', date.today().strftime('%Y-%m-%d'))
1069
1070
1071-def check_for_update(parent):
1072+def check_for_update(main_window):
1073 """
1074 Run a thread to download and check the version of OpenLP
1075
1076- :param MainWindow parent: The parent object for the thread. Usually the OpenLP main window.
1077+ :param MainWindow main_window: The OpenLP main window.
1078 """
1079 last_check_date = Settings().value('core/last version test')
1080 if date.today().strftime('%Y-%m-%d') <= last_check_date:
1081 log.debug('Version check skipped, last checked today')
1082 return
1083 worker = VersionWorker(last_check_date, get_version())
1084- worker.new_version.connect(parent.on_new_version)
1085+ worker.new_version.connect(main_window.on_new_version)
1086 worker.quit.connect(update_check_date)
1087 # TODO: Use this to figure out if there's an Internet connection?
1088 # worker.no_internet.connect(parent.on_no_internet)
1089- run_thread(parent, worker, 'version')
1090+ run_thread(worker, 'version')
1091
1092
1093 def get_version():
1094
1095=== modified file 'openlp/plugins/songs/forms/songselectform.py'
1096--- openlp/plugins/songs/forms/songselectform.py 2017-12-29 09:15:48 +0000
1097+++ openlp/plugins/songs/forms/songselectform.py 2018-01-07 18:07:40 +0000
1098@@ -27,24 +27,23 @@
1099
1100 from PyQt5 import QtCore, QtWidgets
1101
1102-from openlp.core.common import is_win
1103 from openlp.core.common.i18n import translate
1104-from openlp.core.common.registry import Registry
1105+from openlp.core.common.mixins import RegistryProperties
1106 from openlp.core.common.settings import Settings
1107+from openlp.core.threading import ThreadWorker, run_thread
1108 from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog
1109 from openlp.plugins.songs.lib.songselect import SongSelectImport
1110
1111 log = logging.getLogger(__name__)
1112
1113
1114-class SearchWorker(QtCore.QObject):
1115+class SearchWorker(ThreadWorker):
1116 """
1117 Run the actual SongSelect search, and notify the GUI when we find each song.
1118 """
1119 show_info = QtCore.pyqtSignal(str, str)
1120 found_song = QtCore.pyqtSignal(dict)
1121 finished = QtCore.pyqtSignal()
1122- quit = QtCore.pyqtSignal()
1123
1124 def __init__(self, importer, search_text):
1125 super().__init__()
1126@@ -74,7 +73,7 @@
1127 self.found_song.emit(song)
1128
1129
1130-class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
1131+class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties):
1132 """
1133 The :class:`SongSelectForm` class is the SongSelect dialog.
1134 """
1135@@ -90,8 +89,6 @@
1136 """
1137 Initialise the SongSelectForm
1138 """
1139- self.thread = None
1140- self.worker = None
1141 self.song_count = 0
1142 self.song = None
1143 self.set_progress_visible(False)
1144@@ -311,17 +308,11 @@
1145 search_history = self.search_combobox.getItems()
1146 Settings().setValue(self.plugin.settings_section + '/songselect searches', '|'.join(search_history))
1147 # Create thread and run search
1148- self.thread = QtCore.QThread()
1149- self.worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText())
1150- self.worker.moveToThread(self.thread)
1151- self.thread.started.connect(self.worker.start)
1152- self.worker.show_info.connect(self.on_search_show_info)
1153- self.worker.found_song.connect(self.on_search_found_song)
1154- self.worker.finished.connect(self.on_search_finished)
1155- self.worker.quit.connect(self.thread.quit)
1156- self.worker.quit.connect(self.worker.deleteLater)
1157- self.thread.finished.connect(self.thread.deleteLater)
1158- self.thread.start()
1159+ worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText())
1160+ worker.show_info.connect(self.on_search_show_info)
1161+ worker.found_song.connect(self.on_search_found_song)
1162+ worker.finished.connect(self.on_search_finished)
1163+ run_thread(worker, 'songselect')
1164
1165 def on_stop_button_clicked(self):
1166 """
1167@@ -408,16 +399,3 @@
1168 """
1169 self.search_progress_bar.setVisible(is_visible)
1170 self.stop_button.setVisible(is_visible)
1171-
1172- @property
1173- def application(self):
1174- """
1175- Adds the openlp to the class dynamically.
1176- Windows needs to access the application in a dynamic manner.
1177- """
1178- if is_win():
1179- return Registry().get('application')
1180- else:
1181- if not hasattr(self, '_application'):
1182- self._application = Registry().get('application')
1183- return self._application
1184
1185=== modified file 'tests/functional/openlp_core/api/http/test_http.py'
1186--- tests/functional/openlp_core/api/http/test_http.py 2017-12-29 09:15:48 +0000
1187+++ tests/functional/openlp_core/api/http/test_http.py 2018-01-07 18:07:40 +0000
1188@@ -42,8 +42,23 @@
1189 Registry().register('service_list', MagicMock())
1190
1191 @patch('openlp.core.api.http.server.HttpWorker')
1192- @patch('openlp.core.api.http.server.QtCore.QThread')
1193- def test_server_start(self, mock_qthread, mock_thread):
1194+ @patch('openlp.core.api.http.server.run_thread')
1195+ def test_server_start(self, mocked_run_thread, MockHttpWorker):
1196+ """
1197+ Test the starting of the Waitress Server with the disable flag set off
1198+ """
1199+ # GIVEN: A new httpserver
1200+ # WHEN: I start the server
1201+ Registry().set_flag('no_web_server', False)
1202+ HttpServer()
1203+
1204+ # THEN: the api environment should have been created
1205+ assert mocked_run_thread.call_count == 1, 'The qthread should have been called once'
1206+ assert MockHttpWorker.call_count == 1, 'The http thread should have been called once'
1207+
1208+ @patch('openlp.core.api.http.server.HttpWorker')
1209+ @patch('openlp.core.api.http.server.run_thread')
1210+ def test_server_start_not_required(self, mocked_run_thread, MockHttpWorker):
1211 """
1212 Test the starting of the Waitress Server with the disable flag set off
1213 """
1214@@ -53,20 +68,5 @@
1215 HttpServer()
1216
1217 # THEN: the api environment should have been created
1218- assert mock_qthread.call_count == 1, 'The qthread should have been called once'
1219- assert mock_thread.call_count == 1, 'The http thread should have been called once'
1220-
1221- @patch('openlp.core.api.http.server.HttpWorker')
1222- @patch('openlp.core.api.http.server.QtCore.QThread')
1223- def test_server_start_not_required(self, mock_qthread, mock_thread):
1224- """
1225- Test the starting of the Waitress Server with the disable flag set off
1226- """
1227- # GIVEN: A new httpserver
1228- # WHEN: I start the server
1229- Registry().set_flag('no_web_server', False)
1230- HttpServer()
1231-
1232- # THEN: the api environment should have been created
1233- assert mock_qthread.call_count == 0, 'The qthread should not have have been called'
1234- assert mock_thread.call_count == 0, 'The http thread should not have been called'
1235+ assert mocked_run_thread.call_count == 0, 'The qthread should not have have been called'
1236+ assert MockHttpWorker.call_count == 0, 'The http thread should not have been called'
1237
1238=== modified file 'tests/functional/openlp_core/api/test_websockets.py'
1239--- tests/functional/openlp_core/api/test_websockets.py 2017-12-29 09:15:48 +0000
1240+++ tests/functional/openlp_core/api/test_websockets.py 2018-01-07 18:07:40 +0000
1241@@ -63,34 +63,34 @@
1242 self.destroy_settings()
1243
1244 @patch('openlp.core.api.websockets.WebSocketWorker')
1245- @patch('openlp.core.api.websockets.QtCore.QThread')
1246- def test_serverstart(self, mock_qthread, mock_worker):
1247+ @patch('openlp.core.api.websockets.run_thread')
1248+ def test_serverstart(self, mocked_run_thread, MockWebSocketWorker):
1249 """
1250 Test the starting of the WebSockets Server with the disabled flag set on
1251 """
1252 # GIVEN: A new httpserver
1253 # WHEN: I start the server
1254- Registry().set_flag('no_web_server', True)
1255+ Registry().set_flag('no_web_server', False)
1256 WebSocketServer()
1257
1258 # THEN: the api environment should have been created
1259- assert mock_qthread.call_count == 1, 'The qthread should have been called once'
1260- assert mock_worker.call_count == 1, 'The http thread should have been called once'
1261+ assert mocked_run_thread.call_count == 1, 'The qthread should have been called once'
1262+ assert MockWebSocketWorker.call_count == 1, 'The http thread should have been called once'
1263
1264 @patch('openlp.core.api.websockets.WebSocketWorker')
1265- @patch('openlp.core.api.websockets.QtCore.QThread')
1266- def test_serverstart_not_required(self, mock_qthread, mock_worker):
1267+ @patch('openlp.core.api.websockets.run_thread')
1268+ def test_serverstart_not_required(self, mocked_run_thread, MockWebSocketWorker):
1269 """
1270 Test the starting of the WebSockets Server with the disabled flag set off
1271 """
1272 # GIVEN: A new httpserver and the server is not required
1273 # WHEN: I start the server
1274- Registry().set_flag('no_web_server', False)
1275+ Registry().set_flag('no_web_server', True)
1276 WebSocketServer()
1277
1278 # THEN: the api environment should have been created
1279- assert mock_qthread.call_count == 0, 'The qthread should not have been called'
1280- assert mock_worker.call_count == 0, 'The http thread should not have been called'
1281+ assert mocked_run_thread.call_count == 0, 'The qthread should not have been called'
1282+ assert MockWebSocketWorker.call_count == 0, 'The http thread should not have been called'
1283
1284 def test_main_poll(self):
1285 """
1286
1287=== modified file 'tests/functional/openlp_core/common/test_httputils.py'
1288--- tests/functional/openlp_core/common/test_httputils.py 2017-12-29 09:15:48 +0000
1289+++ tests/functional/openlp_core/common/test_httputils.py 2018-01-07 18:07:40 +0000
1290@@ -27,7 +27,7 @@
1291 from unittest import TestCase
1292 from unittest.mock import MagicMock, patch
1293
1294-from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file
1295+from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, download_file
1296 from openlp.core.common.path import Path
1297 from tests.helpers.testmixin import TestMixin
1298
1299@@ -235,7 +235,7 @@
1300 mocked_requests.get.side_effect = OSError
1301
1302 # WHEN: Attempt to retrieve a file
1303- url_get_file(MagicMock(), url='http://localhost/test', file_path=Path(self.tempfile))
1304+ download_file(MagicMock(), url='http://localhost/test', file_path=Path(self.tempfile))
1305
1306 # THEN: socket.timeout should have been caught
1307 # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files
1308
1309=== modified file 'tests/functional/openlp_core/lib/test_image_manager.py'
1310--- tests/functional/openlp_core/lib/test_image_manager.py 2017-12-28 08:22:55 +0000
1311+++ tests/functional/openlp_core/lib/test_image_manager.py 2018-01-07 18:07:40 +0000
1312@@ -25,20 +25,113 @@
1313 import os
1314 import time
1315 from threading import Lock
1316-from unittest import TestCase
1317-from unittest.mock import patch
1318+from unittest import TestCase, skip
1319+from unittest.mock import MagicMock, patch
1320
1321 from PyQt5 import QtGui
1322
1323 from openlp.core.common.registry import Registry
1324 from openlp.core.display.screens import ScreenList
1325-from openlp.core.lib.imagemanager import ImageManager, Priority
1326+from openlp.core.lib.imagemanager import ImageWorker, ImageManager, Priority, PriorityQueue
1327 from tests.helpers.testmixin import TestMixin
1328 from tests.utils.constants import RESOURCE_PATH
1329
1330 TEST_PATH = str(RESOURCE_PATH)
1331
1332
1333+class TestImageWorker(TestCase, TestMixin):
1334+ """
1335+ Test all the methods in the ImageWorker class
1336+ """
1337+ def test_init(self):
1338+ """
1339+ Test the constructor of the ImageWorker
1340+ """
1341+ # GIVEN: An ImageWorker class and a mocked ImageManager
1342+ mocked_image_manager = MagicMock()
1343+
1344+ # WHEN: Creating the ImageWorker
1345+ worker = ImageWorker(mocked_image_manager)
1346+
1347+ # THEN: The image_manager attribute should be set correctly
1348+ assert worker.image_manager is mocked_image_manager, \
1349+ 'worker.image_manager should have been the mocked_image_manager'
1350+
1351+ @patch('openlp.core.lib.imagemanager.ThreadWorker.quit')
1352+ def test_start(self, mocked_quit):
1353+ """
1354+ Test that the start() method of the image worker calls the process method and then emits quit.
1355+ """
1356+ # GIVEN: A mocked image_manager and a new image worker
1357+ mocked_image_manager = MagicMock()
1358+ worker = ImageWorker(mocked_image_manager)
1359+
1360+ # WHEN: start() is called
1361+ worker.start()
1362+
1363+ # THEN: process() should have been called and quit should have been emitted
1364+ mocked_image_manager.process.assert_called_once_with()
1365+ mocked_quit.emit.assert_called_once_with()
1366+
1367+ def test_stop(self):
1368+ """
1369+ Test that the stop method does the right thing
1370+ """
1371+ # GIVEN: A mocked image_manager and a worker
1372+ mocked_image_manager = MagicMock()
1373+ worker = ImageWorker(mocked_image_manager)
1374+
1375+ # WHEN: The stop() method is called
1376+ worker.stop()
1377+
1378+ # THEN: The stop_manager attrivute should have been set to True
1379+ assert mocked_image_manager.stop_manager is True, 'mocked_image_manager.stop_manager should have been True'
1380+
1381+
1382+class TestPriorityQueue(TestCase, TestMixin):
1383+ """
1384+ Test the PriorityQueue class
1385+ """
1386+ @patch('openlp.core.lib.imagemanager.PriorityQueue.remove')
1387+ @patch('openlp.core.lib.imagemanager.PriorityQueue.put')
1388+ def test_modify_priority(self, mocked_put, mocked_remove):
1389+ """
1390+ Test the modify_priority() method of PriorityQueue
1391+ """
1392+ # GIVEN: An instance of a PriorityQueue and a mocked image
1393+ mocked_image = MagicMock()
1394+ mocked_image.priority = Priority.Normal
1395+ mocked_image.secondary_priority = Priority.Low
1396+ queue = PriorityQueue()
1397+
1398+ # WHEN: modify_priority is called with a mocked image and a new priority
1399+ queue.modify_priority(mocked_image, Priority.High)
1400+
1401+ # THEN: The remove() method should have been called, image priority updated and put() called
1402+ mocked_remove.assert_called_once_with(mocked_image)
1403+ assert mocked_image.priority == Priority.High, 'The priority should have been Priority.High'
1404+ mocked_put.assert_called_once_with((Priority.High, Priority.Low, mocked_image))
1405+
1406+ def test_remove(self):
1407+ """
1408+ Test the remove() method of PriorityQueue
1409+ """
1410+ # GIVEN: A PriorityQueue instance with a mocked image and queue
1411+ mocked_image = MagicMock()
1412+ mocked_image.priority = Priority.High
1413+ mocked_image.secondary_priority = Priority.Normal
1414+ queue = PriorityQueue()
1415+
1416+ # WHEN: An image is removed
1417+ with patch.object(queue, 'queue') as mocked_queue:
1418+ mocked_queue.__contains__.return_value = True
1419+ queue.remove(mocked_image)
1420+
1421+ # THEN: The mocked queue.remove() method should have been called
1422+ mocked_queue.remove.assert_called_once_with((Priority.High, Priority.Normal, mocked_image))
1423+
1424+
1425+@skip('Probably not going to use ImageManager in WebEngine/Reveal.js')
1426 class TestImageManager(TestCase, TestMixin):
1427
1428 def setUp(self):
1429@@ -57,10 +150,10 @@
1430 Delete all the C++ objects at the end so that we don't have a segfault
1431 """
1432 self.image_manager.stop_manager = True
1433- self.image_manager.image_thread.wait()
1434 del self.app
1435
1436- def test_basic_image_manager(self):
1437+ @patch('openlp.core.lib.imagemanager.run_thread')
1438+ def test_basic_image_manager(self, mocked_run_thread):
1439 """
1440 Test the Image Manager setup basic functionality
1441 """
1442@@ -86,7 +179,8 @@
1443 self.image_manager.get_image(TEST_PATH, 'church1.jpg')
1444 assert context.exception is not '', 'KeyError exception should have been thrown for missing image'
1445
1446- def test_different_dimension_image(self):
1447+ @patch('openlp.core.lib.imagemanager.run_thread')
1448+ def test_different_dimension_image(self, mocked_run_thread):
1449 """
1450 Test the Image Manager with dimensions
1451 """
1452@@ -118,57 +212,58 @@
1453 self.image_manager.get_image(full_path, 'church.jpg', 120, 120)
1454 assert context.exception is not '', 'KeyError exception should have been thrown for missing dimension'
1455
1456- def test_process_cache(self):
1457+ @patch('openlp.core.lib.imagemanager.resize_image')
1458+ @patch('openlp.core.lib.imagemanager.image_to_byte')
1459+ @patch('openlp.core.lib.imagemanager.run_thread')
1460+ def test_process_cache(self, mocked_run_thread, mocked_image_to_byte, mocked_resize_image):
1461 """
1462 Test the process_cache method
1463 """
1464- with patch('openlp.core.lib.imagemanager.resize_image') as mocked_resize_image, \
1465- patch('openlp.core.lib.imagemanager.image_to_byte') as mocked_image_to_byte:
1466- # GIVEN: Mocked functions
1467- mocked_resize_image.side_effect = self.mocked_resize_image
1468- mocked_image_to_byte.side_effect = self.mocked_image_to_byte
1469- image1 = 'church.jpg'
1470- image2 = 'church2.jpg'
1471- image3 = 'church3.jpg'
1472- image4 = 'church4.jpg'
1473-
1474- # WHEN: Add the images. Then get the lock (=queue can not be processed).
1475- self.lock.acquire()
1476- self.image_manager.add_image(TEST_PATH, image1, None)
1477- self.image_manager.add_image(TEST_PATH, image2, None)
1478-
1479- # THEN: All images have been added to the queue, and only the first image is not be in the list anymore, but
1480- # is being processed (see mocked methods/functions).
1481- # Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the #
1482- # priority is adjusted to Priority.Lowest).
1483- assert self.get_image_priority(image1) == Priority.Normal, "image1's priority should be 'Priority.Normal'"
1484- assert self.get_image_priority(image2) == Priority.Normal, "image2's priority should be 'Priority.Normal'"
1485-
1486- # WHEN: Add more images.
1487- self.image_manager.add_image(TEST_PATH, image3, None)
1488- self.image_manager.add_image(TEST_PATH, image4, None)
1489- # Allow the queue to process.
1490- self.lock.release()
1491- # Request some "data".
1492- self.image_manager.get_image_bytes(TEST_PATH, image4)
1493- self.image_manager.get_image(TEST_PATH, image3)
1494- # Now the mocked methods/functions do not have to sleep anymore.
1495- self.sleep_time = 0
1496- # Wait for the queue to finish.
1497- while not self.image_manager._conversion_queue.empty():
1498- time.sleep(0.1)
1499- # Because empty() is not reliable, wait a litte; just to make sure.
1500+ # GIVEN: Mocked functions
1501+ mocked_resize_image.side_effect = self.mocked_resize_image
1502+ mocked_image_to_byte.side_effect = self.mocked_image_to_byte
1503+ image1 = 'church.jpg'
1504+ image2 = 'church2.jpg'
1505+ image3 = 'church3.jpg'
1506+ image4 = 'church4.jpg'
1507+
1508+ # WHEN: Add the images. Then get the lock (=queue can not be processed).
1509+ self.lock.acquire()
1510+ self.image_manager.add_image(TEST_PATH, image1, None)
1511+ self.image_manager.add_image(TEST_PATH, image2, None)
1512+
1513+ # THEN: All images have been added to the queue, and only the first image is not be in the list anymore, but
1514+ # is being processed (see mocked methods/functions).
1515+ # Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the #
1516+ # priority is adjusted to Priority.Lowest).
1517+ assert self.get_image_priority(image1) == Priority.Normal, "image1's priority should be 'Priority.Normal'"
1518+ assert self.get_image_priority(image2) == Priority.Normal, "image2's priority should be 'Priority.Normal'"
1519+
1520+ # WHEN: Add more images.
1521+ self.image_manager.add_image(TEST_PATH, image3, None)
1522+ self.image_manager.add_image(TEST_PATH, image4, None)
1523+ # Allow the queue to process.
1524+ self.lock.release()
1525+ # Request some "data".
1526+ self.image_manager.get_image_bytes(TEST_PATH, image4)
1527+ self.image_manager.get_image(TEST_PATH, image3)
1528+ # Now the mocked methods/functions do not have to sleep anymore.
1529+ self.sleep_time = 0
1530+ # Wait for the queue to finish.
1531+ while not self.image_manager._conversion_queue.empty():
1532 time.sleep(0.1)
1533- # THEN: The images' priority reflect how they were processed.
1534- assert self.image_manager._conversion_queue.qsize() == 0, "The queue should be empty."
1535- assert self.get_image_priority(image1) == Priority.Lowest, \
1536- "The image should have not been requested (=Lowest)"
1537- assert self.get_image_priority(image2) == Priority.Lowest, \
1538- "The image should have not been requested (=Lowest)"
1539- assert self.get_image_priority(image3) == Priority.Low, \
1540- "Only the QImage should have been requested (=Low)."
1541- assert self.get_image_priority(image4) == Priority.Urgent, \
1542- "The image bytes should have been requested (=Urgent)."
1543+ # Because empty() is not reliable, wait a litte; just to make sure.
1544+ time.sleep(0.1)
1545+ # THEN: The images' priority reflect how they were processed.
1546+ assert self.image_manager._conversion_queue.qsize() == 0, "The queue should be empty."
1547+ assert self.get_image_priority(image1) == Priority.Lowest, \
1548+ "The image should have not been requested (=Lowest)"
1549+ assert self.get_image_priority(image2) == Priority.Lowest, \
1550+ "The image should have not been requested (=Lowest)"
1551+ assert self.get_image_priority(image3) == Priority.Low, \
1552+ "Only the QImage should have been requested (=Low)."
1553+ assert self.get_image_priority(image4) == Priority.Urgent, \
1554+ "The image bytes should have been requested (=Urgent)."
1555
1556 def get_image_priority(self, image):
1557 """
1558
1559=== modified file 'tests/functional/openlp_core/test_app.py'
1560--- tests/functional/openlp_core/test_app.py 2017-12-29 09:15:48 +0000
1561+++ tests/functional/openlp_core/test_app.py 2018-01-07 18:07:40 +0000
1562@@ -36,14 +36,15 @@
1563 """
1564 # GIVEN: a a set of system arguments.
1565 sys.argv[1:] = []
1566+
1567 # WHEN: We we parse them to expand to options
1568- args = parse_options(None)
1569+ args = parse_options()
1570+
1571 # THEN: the following fields will have been extracted.
1572 assert args.dev_version is False, 'The dev_version flag should be False'
1573 assert args.loglevel == 'warning', 'The log level should be set to warning'
1574 assert args.no_error_form is False, 'The no_error_form should be set to False'
1575 assert args.portable is False, 'The portable flag should be set to false'
1576- assert args.style is None, 'There are no style flags to be processed'
1577 assert args.rargs == [], 'The service file should be blank'
1578
1579
1580@@ -53,14 +54,15 @@
1581 """
1582 # GIVEN: a a set of system arguments.
1583 sys.argv[1:] = ['-l debug']
1584+
1585 # WHEN: We we parse them to expand to options
1586- args = parse_options(None)
1587+ args = parse_options()
1588+
1589 # THEN: the following fields will have been extracted.
1590 assert args.dev_version is False, 'The dev_version flag should be False'
1591 assert args.loglevel == ' debug', 'The log level should be set to debug'
1592 assert args.no_error_form is False, 'The no_error_form should be set to False'
1593 assert args.portable is False, 'The portable flag should be set to false'
1594- assert args.style is None, 'There are no style flags to be processed'
1595 assert args.rargs == [], 'The service file should be blank'
1596
1597
1598@@ -70,14 +72,15 @@
1599 """
1600 # GIVEN: a a set of system arguments.
1601 sys.argv[1:] = ['--portable']
1602+
1603 # WHEN: We we parse them to expand to options
1604- args = parse_options(None)
1605+ args = parse_options()
1606+
1607 # THEN: the following fields will have been extracted.
1608 assert args.dev_version is False, 'The dev_version flag should be False'
1609 assert args.loglevel == 'warning', 'The log level should be set to warning'
1610 assert args.no_error_form is False, 'The no_error_form should be set to False'
1611 assert args.portable is True, 'The portable flag should be set to true'
1612- assert args.style is None, 'There are no style flags to be processed'
1613 assert args.rargs == [], 'The service file should be blank'
1614
1615
1616@@ -87,14 +90,15 @@
1617 """
1618 # GIVEN: a a set of system arguments.
1619 sys.argv[1:] = ['-l debug', '-d']
1620+
1621 # WHEN: We we parse them to expand to options
1622- args = parse_options(None)
1623+ args = parse_options()
1624+
1625 # THEN: the following fields will have been extracted.
1626 assert args.dev_version is True, 'The dev_version flag should be True'
1627 assert args.loglevel == ' debug', 'The log level should be set to debug'
1628 assert args.no_error_form is False, 'The no_error_form should be set to False'
1629 assert args.portable is False, 'The portable flag should be set to false'
1630- assert args.style is None, 'There are no style flags to be processed'
1631 assert args.rargs == [], 'The service file should be blank'
1632
1633
1634@@ -104,14 +108,15 @@
1635 """
1636 # GIVEN: a a set of system arguments.
1637 sys.argv[1:] = ['dummy_temp']
1638+
1639 # WHEN: We we parse them to expand to options
1640- args = parse_options(None)
1641+ args = parse_options()
1642+
1643 # THEN: the following fields will have been extracted.
1644 assert args.dev_version is False, 'The dev_version flag should be False'
1645 assert args.loglevel == 'warning', 'The log level should be set to warning'
1646 assert args.no_error_form is False, 'The no_error_form should be set to False'
1647 assert args.portable is False, 'The portable flag should be set to false'
1648- assert args.style is None, 'There are no style flags to be processed'
1649 assert args.rargs == 'dummy_temp', 'The service file should not be blank'
1650
1651
1652@@ -121,14 +126,15 @@
1653 """
1654 # GIVEN: a a set of system arguments.
1655 sys.argv[1:] = ['-l debug', 'dummy_temp']
1656+
1657 # WHEN: We we parse them to expand to options
1658- args = parse_options(None)
1659+ args = parse_options()
1660+
1661 # THEN: the following fields will have been extracted.
1662 assert args.dev_version is False, 'The dev_version flag should be False'
1663 assert args.loglevel == ' debug', 'The log level should be set to debug'
1664 assert args.no_error_form is False, 'The no_error_form should be set to False'
1665 assert args.portable is False, 'The portable flag should be set to false'
1666- assert args.style is None, 'There are no style flags to be processed'
1667 assert args.rargs == 'dummy_temp', 'The service file should not be blank'
1668
1669
1670
1671=== added file 'tests/functional/openlp_core/test_threading.py'
1672--- tests/functional/openlp_core/test_threading.py 1970-01-01 00:00:00 +0000
1673+++ tests/functional/openlp_core/test_threading.py 2018-01-07 18:07:40 +0000
1674@@ -0,0 +1,89 @@
1675+# -*- coding: utf-8 -*-
1676+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
1677+
1678+###############################################################################
1679+# OpenLP - Open Source Lyrics Projection #
1680+# --------------------------------------------------------------------------- #
1681+# Copyright (c) 2008-2018 OpenLP Developers #
1682+# --------------------------------------------------------------------------- #
1683+# This program is free software; you can redistribute it and/or modify it #
1684+# under the terms of the GNU General Public License as published by the Free #
1685+# Software Foundation; version 2 of the License. #
1686+# #
1687+# This program is distributed in the hope that it will be useful, but WITHOUT #
1688+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
1689+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
1690+# more details. #
1691+# #
1692+# You should have received a copy of the GNU General Public License along #
1693+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
1694+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1695+###############################################################################
1696+"""
1697+Package to test the openlp.core.threading package.
1698+"""
1699+from unittest.mock import MagicMock, call, patch
1700+
1701+from openlp.core.version import run_thread
1702+
1703+
1704+def test_run_thread_no_name():
1705+ """
1706+ Test that trying to run a thread without a name results in an exception being thrown
1707+ """
1708+ # GIVEN: A fake worker
1709+ # WHEN: run_thread() is called without a name
1710+ try:
1711+ run_thread(MagicMock(), '')
1712+ assert False, 'A ValueError should have been thrown to prevent blank names'
1713+ except ValueError:
1714+ # THEN: A ValueError should have been thrown
1715+ assert True, 'A ValueError was correctly thrown'
1716+
1717+
1718+@patch('openlp.core.threading.Registry')
1719+def test_run_thread_exists(MockRegistry):
1720+ """
1721+ Test that trying to run a thread with a name that already exists will throw a KeyError
1722+ """
1723+ # GIVEN: A mocked registry with a main window object
1724+ mocked_main_window = MagicMock()
1725+ mocked_main_window.threads = {'test_thread': MagicMock()}
1726+ MockRegistry.return_value.get.return_value = mocked_main_window
1727+
1728+ # WHEN: run_thread() is called
1729+ try:
1730+ run_thread(MagicMock(), 'test_thread')
1731+ assert False, 'A KeyError should have been thrown to show that a thread with this name already exists'
1732+ except KeyError:
1733+ assert True, 'A KeyError was correctly thrown'
1734+
1735+
1736+@patch('openlp.core.threading.QtCore.QThread')
1737+@patch('openlp.core.threading.Registry')
1738+def test_run_thread(MockRegistry, MockQThread):
1739+ """
1740+ Test that running a thread works correctly
1741+ """
1742+ # GIVEN: A mocked registry with a main window object
1743+ mocked_main_window = MagicMock()
1744+ mocked_main_window.threads = {}
1745+ MockRegistry.return_value.get.return_value = mocked_main_window
1746+
1747+ # WHEN: run_thread() is called
1748+ run_thread(MagicMock(), 'test_thread')
1749+
1750+ # THEN: The thread should be in the threads list and the correct methods should have been called
1751+ assert len(mocked_main_window.threads.keys()) == 1, 'There should be 1 item in the list of threads'
1752+ assert list(mocked_main_window.threads.keys()) == ['test_thread'], 'The test_thread item should be in the list'
1753+ mocked_worker = mocked_main_window.threads['test_thread']['worker']
1754+ mocked_thread = mocked_main_window.threads['test_thread']['thread']
1755+ mocked_worker.moveToThread.assert_called_once_with(mocked_thread)
1756+ mocked_thread.started.connect.assert_called_once_with(mocked_worker.start)
1757+ expected_quit_calls = [call(mocked_thread.quit), call(mocked_worker.deleteLater)]
1758+ assert mocked_worker.quit.connect.call_args_list == expected_quit_calls, \
1759+ 'The workers quit signal should be connected twice'
1760+ assert mocked_thread.finished.connect.call_args_list[0] == call(mocked_thread.deleteLater), \
1761+ 'The threads finished signal should be connected to its deleteLater slot'
1762+ assert mocked_thread.finished.connect.call_count == 2, 'The signal should have been connected twice'
1763+ mocked_thread.start.assert_called_once_with()
1764
1765=== added file 'tests/functional/openlp_core/ui/media/test_systemplayer.py'
1766--- tests/functional/openlp_core/ui/media/test_systemplayer.py 1970-01-01 00:00:00 +0000
1767+++ tests/functional/openlp_core/ui/media/test_systemplayer.py 2018-01-07 18:07:40 +0000
1768@@ -0,0 +1,543 @@
1769+# -*- coding: utf-8 -*-
1770+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
1771+
1772+###############################################################################
1773+# OpenLP - Open Source Lyrics Projection #
1774+# --------------------------------------------------------------------------- #
1775+# Copyright (c) 2008-2018 OpenLP Developers #
1776+# --------------------------------------------------------------------------- #
1777+# This program is free software; you can redistribute it and/or modify it #
1778+# under the terms of the GNU General Public License as published by the Free #
1779+# Software Foundation; version 2 of the License. #
1780+# #
1781+# This program is distributed in the hope that it will be useful, but WITHOUT #
1782+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
1783+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
1784+# more details. #
1785+# #
1786+# You should have received a copy of the GNU General Public License along #
1787+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
1788+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1789+###############################################################################
1790+"""
1791+Package to test the openlp.core.ui.media.systemplayer package.
1792+"""
1793+from unittest import TestCase
1794+from unittest.mock import MagicMock, call, patch
1795+
1796+from PyQt5 import QtCore, QtMultimedia
1797+
1798+from openlp.core.common.registry import Registry
1799+from openlp.core.ui.media import MediaState
1800+from openlp.core.ui.media.systemplayer import SystemPlayer, CheckMediaWorker, ADDITIONAL_EXT
1801+
1802+
1803+class TestSystemPlayer(TestCase):
1804+ """
1805+ Test the system media player
1806+ """
1807+ @patch('openlp.core.ui.media.systemplayer.mimetypes')
1808+ @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
1809+ def test_constructor(self, MockQMediaPlayer, mocked_mimetypes):
1810+ """
1811+ Test the SystemPlayer constructor
1812+ """
1813+ # GIVEN: The SystemPlayer class and a mockedQMediaPlayer
1814+ mocked_media_player = MagicMock()
1815+ mocked_media_player.supportedMimeTypes.return_value = [
1816+ 'application/postscript',
1817+ 'audio/aiff',
1818+ 'audio/x-aiff',
1819+ 'text/html',
1820+ 'video/animaflex',
1821+ 'video/x-ms-asf'
1822+ ]
1823+ mocked_mimetypes.guess_all_extensions.side_effect = [
1824+ ['.aiff'],
1825+ ['.aiff'],
1826+ ['.afl'],
1827+ ['.asf']
1828+ ]
1829+ MockQMediaPlayer.return_value = mocked_media_player
1830+
1831+ # WHEN: An object is created from it
1832+ player = SystemPlayer(self)
1833+
1834+ # THEN: The correct initial values should be set up
1835+ assert 'system' == player.name
1836+ assert 'System' == player.original_name
1837+ assert '&System' == player.display_name
1838+ assert self == player.parent
1839+ assert ADDITIONAL_EXT == player.additional_extensions
1840+ MockQMediaPlayer.assert_called_once_with(None, QtMultimedia.QMediaPlayer.VideoSurface)
1841+ mocked_mimetypes.init.assert_called_once_with()
1842+ mocked_media_player.service.assert_called_once_with()
1843+ mocked_media_player.supportedMimeTypes.assert_called_once_with()
1844+ assert ['*.aiff'] == player.audio_extensions_list
1845+ assert ['*.afl', '*.asf'] == player.video_extensions_list
1846+
1847+ @patch('openlp.core.ui.media.systemplayer.QtMultimediaWidgets.QVideoWidget')
1848+ @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
1849+ def test_setup(self, MockQMediaPlayer, MockQVideoWidget):
1850+ """
1851+ Test the setup() method of SystemPlayer
1852+ """
1853+ # GIVEN: A SystemPlayer instance and a mock display
1854+ player = SystemPlayer(self)
1855+ mocked_display = MagicMock()
1856+ mocked_display.size.return_value = [1, 2, 3, 4]
1857+ mocked_video_widget = MagicMock()
1858+ mocked_media_player = MagicMock()
1859+ MockQVideoWidget.return_value = mocked_video_widget
1860+ MockQMediaPlayer.return_value = mocked_media_player
1861+
1862+ # WHEN: setup() is run
1863+ player.setup(mocked_display)
1864+
1865+ # THEN: The player should have a display widget
1866+ MockQVideoWidget.assert_called_once_with(mocked_display)
1867+ assert mocked_video_widget == mocked_display.video_widget
1868+ mocked_display.size.assert_called_once_with()
1869+ mocked_video_widget.resize.assert_called_once_with([1, 2, 3, 4])
1870+ MockQMediaPlayer.assert_called_with(mocked_display)
1871+ assert mocked_media_player == mocked_display.media_player
1872+ mocked_media_player.setVideoOutput.assert_called_once_with(mocked_video_widget)
1873+ mocked_video_widget.raise_.assert_called_once_with()
1874+ mocked_video_widget.hide.assert_called_once_with()
1875+ assert player.has_own_widget is True
1876+
1877+ def test_disconnect_slots(self):
1878+ """
1879+ Test that we the disconnect slots method catches the TypeError
1880+ """
1881+ # GIVEN: A SystemPlayer class and a signal that throws a TypeError
1882+ player = SystemPlayer(self)
1883+ mocked_signal = MagicMock()
1884+ mocked_signal.disconnect.side_effect = \
1885+ TypeError('disconnect() failed between \'durationChanged\' and all its connections')
1886+
1887+ # WHEN: disconnect_slots() is called
1888+ player.disconnect_slots(mocked_signal)
1889+
1890+ # THEN: disconnect should have been called and the exception should have been ignored
1891+ mocked_signal.disconnect.assert_called_once_with()
1892+
1893+ def test_check_available(self):
1894+ """
1895+ Test the check_available() method on SystemPlayer
1896+ """
1897+ # GIVEN: A SystemPlayer instance
1898+ player = SystemPlayer(self)
1899+
1900+ # WHEN: check_available is run
1901+ result = player.check_available()
1902+
1903+ # THEN: it should be available
1904+ assert result is True
1905+
1906+ def test_load_valid_media(self):
1907+ """
1908+ Test the load() method of SystemPlayer with a valid media file
1909+ """
1910+ # GIVEN: A SystemPlayer instance and a mocked display
1911+ player = SystemPlayer(self)
1912+ mocked_display = MagicMock()
1913+ mocked_display.controller.media_info.volume = 1
1914+ mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
1915+
1916+ # WHEN: The load() method is run
1917+ with patch.object(player, 'check_media') as mocked_check_media, \
1918+ patch.object(player, 'volume') as mocked_volume:
1919+ mocked_check_media.return_value = True
1920+ result = player.load(mocked_display)
1921+
1922+ # THEN: the file is sent to the video widget
1923+ mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
1924+ mocked_check_media.assert_called_once_with('/path/to/file')
1925+ mocked_display.media_player.setMedia.assert_called_once_with(
1926+ QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile('/path/to/file')))
1927+ mocked_volume.assert_called_once_with(mocked_display, 1)
1928+ assert result is True
1929+
1930+ def test_load_invalid_media(self):
1931+ """
1932+ Test the load() method of SystemPlayer with an invalid media file
1933+ """
1934+ # GIVEN: A SystemPlayer instance and a mocked display
1935+ player = SystemPlayer(self)
1936+ mocked_display = MagicMock()
1937+ mocked_display.controller.media_info.volume = 1
1938+ mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
1939+
1940+ # WHEN: The load() method is run
1941+ with patch.object(player, 'check_media') as mocked_check_media, \
1942+ patch.object(player, 'volume'):
1943+ mocked_check_media.return_value = False
1944+ result = player.load(mocked_display)
1945+
1946+ # THEN: stuff
1947+ mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
1948+ mocked_check_media.assert_called_once_with('/path/to/file')
1949+ assert result is False
1950+
1951+ def test_resize(self):
1952+ """
1953+ Test the resize() method of the SystemPlayer
1954+ """
1955+ # GIVEN: A SystemPlayer instance and a mocked display
1956+ player = SystemPlayer(self)
1957+ mocked_display = MagicMock()
1958+ mocked_display.size.return_value = [1, 2, 3, 4]
1959+
1960+ # WHEN: The resize() method is called
1961+ player.resize(mocked_display)
1962+
1963+ # THEN: The player is resized
1964+ mocked_display.size.assert_called_once_with()
1965+ mocked_display.video_widget.resize.assert_called_once_with([1, 2, 3, 4])
1966+
1967+ @patch('openlp.core.ui.media.systemplayer.functools')
1968+ def test_play_is_live(self, mocked_functools):
1969+ """
1970+ Test the play() method of the SystemPlayer on the live display
1971+ """
1972+ # GIVEN: A SystemPlayer instance and a mocked display
1973+ mocked_functools.partial.return_value = 'function'
1974+ player = SystemPlayer(self)
1975+ mocked_display = MagicMock()
1976+ mocked_display.controller.is_live = True
1977+ mocked_display.controller.media_info.start_time = 1
1978+ mocked_display.controller.media_info.volume = 1
1979+
1980+ # WHEN: play() is called
1981+ with patch.object(player, 'get_live_state') as mocked_get_live_state, \
1982+ patch.object(player, 'seek') as mocked_seek, \
1983+ patch.object(player, 'volume') as mocked_volume, \
1984+ patch.object(player, 'set_state') as mocked_set_state, \
1985+ patch.object(player, 'disconnect_slots') as mocked_disconnect_slots:
1986+ mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
1987+ result = player.play(mocked_display)
1988+
1989+ # THEN: the media file is played
1990+ mocked_get_live_state.assert_called_once_with()
1991+ mocked_display.media_player.play.assert_called_once_with()
1992+ mocked_seek.assert_called_once_with(mocked_display, 1000)
1993+ mocked_volume.assert_called_once_with(mocked_display, 1)
1994+ mocked_disconnect_slots.assert_called_once_with(mocked_display.media_player.durationChanged)
1995+ mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
1996+ mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
1997+ mocked_display.video_widget.raise_.assert_called_once_with()
1998+ assert result is True
1999+
2000+ @patch('openlp.core.ui.media.systemplayer.functools')
2001+ def test_play_is_preview(self, mocked_functools):
2002+ """
2003+ Test the play() method of the SystemPlayer on the preview display
2004+ """
2005+ # GIVEN: A SystemPlayer instance and a mocked display
2006+ mocked_functools.partial.return_value = 'function'
2007+ player = SystemPlayer(self)
2008+ mocked_display = MagicMock()
2009+ mocked_display.controller.is_live = False
2010+ mocked_display.controller.media_info.start_time = 1
2011+ mocked_display.controller.media_info.volume = 1
2012+
2013+ # WHEN: play() is called
2014+ with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
2015+ patch.object(player, 'seek') as mocked_seek, \
2016+ patch.object(player, 'volume') as mocked_volume, \
2017+ patch.object(player, 'set_state') as mocked_set_state:
2018+ mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
2019+ result = player.play(mocked_display)
2020+
2021+ # THEN: the media file is played
2022+ mocked_get_preview_state.assert_called_once_with()
2023+ mocked_display.media_player.play.assert_called_once_with()
2024+ mocked_seek.assert_called_once_with(mocked_display, 1000)
2025+ mocked_volume.assert_called_once_with(mocked_display, 1)
2026+ mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
2027+ mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
2028+ mocked_display.video_widget.raise_.assert_called_once_with()
2029+ assert result is True
2030+
2031+ def test_pause_is_live(self):
2032+ """
2033+ Test the pause() method of the SystemPlayer on the live display
2034+ """
2035+ # GIVEN: A SystemPlayer instance
2036+ player = SystemPlayer(self)
2037+ mocked_display = MagicMock()
2038+ mocked_display.controller.is_live = True
2039+
2040+ # WHEN: The pause method is called
2041+ with patch.object(player, 'get_live_state') as mocked_get_live_state, \
2042+ patch.object(player, 'set_state') as mocked_set_state:
2043+ mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PausedState
2044+ player.pause(mocked_display)
2045+
2046+ # THEN: The video is paused
2047+ mocked_display.media_player.pause.assert_called_once_with()
2048+ mocked_get_live_state.assert_called_once_with()
2049+ mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
2050+
2051+ def test_pause_is_preview(self):
2052+ """
2053+ Test the pause() method of the SystemPlayer on the preview display
2054+ """
2055+ # GIVEN: A SystemPlayer instance
2056+ player = SystemPlayer(self)
2057+ mocked_display = MagicMock()
2058+ mocked_display.controller.is_live = False
2059+
2060+ # WHEN: The pause method is called
2061+ with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
2062+ patch.object(player, 'set_state') as mocked_set_state:
2063+ mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PausedState
2064+ player.pause(mocked_display)
2065+
2066+ # THEN: The video is paused
2067+ mocked_display.media_player.pause.assert_called_once_with()
2068+ mocked_get_preview_state.assert_called_once_with()
2069+ mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
2070+
2071+ def test_stop(self):
2072+ """
2073+ Test the stop() method of the SystemPlayer
2074+ """
2075+ # GIVEN: A SystemPlayer instance
2076+ player = SystemPlayer(self)
2077+ mocked_display = MagicMock()
2078+
2079+ # WHEN: The stop method is called
2080+ with patch.object(player, 'set_visible') as mocked_set_visible, \
2081+ patch.object(player, 'set_state') as mocked_set_state:
2082+ player.stop(mocked_display)
2083+
2084+ # THEN: The video is stopped
2085+ mocked_display.media_player.stop.assert_called_once_with()
2086+ mocked_set_visible.assert_called_once_with(mocked_display, False)
2087+ mocked_set_state.assert_called_once_with(MediaState.Stopped, mocked_display)
2088+
2089+ def test_volume(self):
2090+ """
2091+ Test the volume() method of the SystemPlayer
2092+ """
2093+ # GIVEN: A SystemPlayer instance
2094+ player = SystemPlayer(self)
2095+ mocked_display = MagicMock()
2096+ mocked_display.has_audio = True
2097+
2098+ # WHEN: The stop method is called
2099+ player.volume(mocked_display, 2)
2100+
2101+ # THEN: The video is stopped
2102+ mocked_display.media_player.setVolume.assert_called_once_with(2)
2103+
2104+ def test_seek(self):
2105+ """
2106+ Test the seek() method of the SystemPlayer
2107+ """
2108+ # GIVEN: A SystemPlayer instance
2109+ player = SystemPlayer(self)
2110+ mocked_display = MagicMock()
2111+
2112+ # WHEN: The stop method is called
2113+ player.seek(mocked_display, 2)
2114+
2115+ # THEN: The video is stopped
2116+ mocked_display.media_player.setPosition.assert_called_once_with(2)
2117+
2118+ def test_reset(self):
2119+ """
2120+ Test the reset() method of the SystemPlayer
2121+ """
2122+ # GIVEN: A SystemPlayer instance
2123+ player = SystemPlayer(self)
2124+ mocked_display = MagicMock()
2125+
2126+ # WHEN: reset() is called
2127+ with patch.object(player, 'set_state') as mocked_set_state, \
2128+ patch.object(player, 'set_visible') as mocked_set_visible:
2129+ player.reset(mocked_display)
2130+
2131+ # THEN: The media player is reset
2132+ mocked_display.media_player.stop()
2133+ mocked_display.media_player.setMedia.assert_called_once_with(QtMultimedia.QMediaContent())
2134+ mocked_set_visible.assert_called_once_with(mocked_display, False)
2135+ mocked_display.video_widget.setVisible.assert_called_once_with(False)
2136+ mocked_set_state.assert_called_once_with(MediaState.Off, mocked_display)
2137+
2138+ def test_set_visible(self):
2139+ """
2140+ Test the set_visible() method on the SystemPlayer
2141+ """
2142+ # GIVEN: A SystemPlayer instance and a mocked display
2143+ player = SystemPlayer(self)
2144+ player.has_own_widget = True
2145+ mocked_display = MagicMock()
2146+
2147+ # WHEN: set_visible() is called
2148+ player.set_visible(mocked_display, True)
2149+
2150+ # THEN: The widget should be visible
2151+ mocked_display.video_widget.setVisible.assert_called_once_with(True)
2152+
2153+ def test_set_duration(self):
2154+ """
2155+ Test the set_duration() method of the SystemPlayer
2156+ """
2157+ # GIVEN: a mocked controller
2158+ mocked_controller = MagicMock()
2159+ mocked_controller.media_info.length = 5
2160+
2161+ # WHEN: The set_duration() is called. NB: the 10 here is ignored by the code
2162+ SystemPlayer.set_duration(mocked_controller, 10)
2163+
2164+ # THEN: The maximum length of the slider should be set
2165+ mocked_controller.seek_slider.setMaximum.assert_called_once_with(5)
2166+
2167+ def test_update_ui(self):
2168+ """
2169+ Test the update_ui() method on the SystemPlayer
2170+ """
2171+ # GIVEN: A SystemPlayer instance
2172+ player = SystemPlayer(self)
2173+ player.state = [MediaState.Playing, MediaState.Playing]
2174+ mocked_display = MagicMock()
2175+ mocked_display.media_player.state.return_value = QtMultimedia.QMediaPlayer.PausedState
2176+ mocked_display.controller.media_info.end_time = 1
2177+ mocked_display.media_player.position.return_value = 2
2178+ mocked_display.controller.seek_slider.isSliderDown.return_value = False
2179+
2180+ # WHEN: update_ui() is called
2181+ with patch.object(player, 'stop') as mocked_stop, \
2182+ patch.object(player, 'set_visible') as mocked_set_visible:
2183+ player.update_ui(mocked_display)
2184+
2185+ # THEN: The UI is updated
2186+ expected_stop_calls = [call(mocked_display)]
2187+ expected_position_calls = [call(), call()]
2188+ expected_block_signals_calls = [call(True), call(False)]
2189+ mocked_display.media_player.state.assert_called_once_with()
2190+ assert 1 == mocked_stop.call_count
2191+ assert expected_stop_calls == mocked_stop.call_args_list
2192+ assert 2 == mocked_display.media_player.position.call_count
2193+ assert expected_position_calls == mocked_display.media_player.position.call_args_list
2194+ mocked_set_visible.assert_called_once_with(mocked_display, False)
2195+ mocked_display.controller.seek_slider.isSliderDown.assert_called_once_with()
2196+ assert expected_block_signals_calls == mocked_display.controller.seek_slider.blockSignals.call_args_list
2197+ mocked_display.controller.seek_slider.setSliderPosition.assert_called_once_with(2)
2198+
2199+ def test_get_media_display_css(self):
2200+ """
2201+ Test the get_media_display_css() method of the SystemPlayer
2202+ """
2203+ # GIVEN: A SystemPlayer instance
2204+ player = SystemPlayer(self)
2205+
2206+ # WHEN: get_media_display_css() is called
2207+ result = player.get_media_display_css()
2208+
2209+ # THEN: The css should be empty
2210+ assert '' == result
2211+
2212+ @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
2213+ def test_get_info(self, MockQMediaPlayer):
2214+ """
2215+ Test the get_info() method of the SystemPlayer
2216+ """
2217+ # GIVEN: A SystemPlayer instance
2218+ mocked_media_player = MagicMock()
2219+ mocked_media_player.supportedMimeTypes.return_value = []
2220+ MockQMediaPlayer.return_value = mocked_media_player
2221+ player = SystemPlayer(self)
2222+
2223+ # WHEN: get_info() is called
2224+ result = player.get_info()
2225+
2226+ # THEN: The info should be correct
2227+ expected_info = 'This media player uses your operating system to provide media capabilities.<br/> ' \
2228+ '<strong>Audio</strong><br/>[]<br/><strong>Video</strong><br/>[]<br/>'
2229+ assert expected_info == result
2230+
2231+ @patch('openlp.core.ui.media.systemplayer.CheckMediaWorker')
2232+ @patch('openlp.core.ui.media.systemplayer.run_thread')
2233+ @patch('openlp.core.ui.media.systemplayer.is_thread_finished')
2234+ def test_check_media(self, mocked_is_thread_finished, mocked_run_thread, MockCheckMediaWorker):
2235+ """
2236+ Test the check_media() method of the SystemPlayer
2237+ """
2238+ # GIVEN: A SystemPlayer instance and a mocked thread
2239+ valid_file = '/path/to/video.ogv'
2240+ mocked_application = MagicMock()
2241+ Registry().create()
2242+ Registry().register('application', mocked_application)
2243+ player = SystemPlayer(self)
2244+ mocked_is_thread_finished.side_effect = [False, True]
2245+ mocked_check_media_worker = MagicMock()
2246+ mocked_check_media_worker.result = True
2247+ MockCheckMediaWorker.return_value = mocked_check_media_worker
2248+
2249+ # WHEN: check_media() is called with a valid media file
2250+ result = player.check_media(valid_file)
2251+
2252+ # THEN: It should return True
2253+ MockCheckMediaWorker.assert_called_once_with(valid_file)
2254+ mocked_check_media_worker.setVolume.assert_called_once_with(0)
2255+ mocked_run_thread.assert_called_once_with(mocked_check_media_worker, 'check_media')
2256+ mocked_is_thread_finished.assert_called_with('check_media')
2257+ assert mocked_is_thread_finished.call_count == 2, 'is_thread_finished() should have been called twice'
2258+ mocked_application.processEvents.assert_called_once_with()
2259+ assert result is True
2260+
2261+
2262+class TestCheckMediaWorker(TestCase):
2263+ """
2264+ Test the CheckMediaWorker class
2265+ """
2266+ def test_constructor(self):
2267+ """
2268+ Test the constructor of the CheckMediaWorker class
2269+ """
2270+ # GIVEN: A file path
2271+ path = 'file.ogv'
2272+
2273+ # WHEN: The CheckMediaWorker object is instantiated
2274+ worker = CheckMediaWorker(path)
2275+
2276+ # THEN: The correct values should be set up
2277+ assert worker is not None
2278+
2279+ def test_signals_media(self):
2280+ """
2281+ Test the signals() signal of the CheckMediaWorker class with a "media" origin
2282+ """
2283+ # GIVEN: A CheckMediaWorker instance
2284+ worker = CheckMediaWorker('file.ogv')
2285+
2286+ # WHEN: signals() is called with media and BufferedMedia
2287+ with patch.object(worker, 'stop') as mocked_stop, \
2288+ patch.object(worker, 'quit') as mocked_quit:
2289+ worker.signals('media', worker.BufferedMedia)
2290+
2291+ # THEN: The worker should exit and the result should be True
2292+ mocked_stop.assert_called_once_with()
2293+ mocked_quit.emit.assert_called_once_with()
2294+ assert worker.result is True
2295+
2296+ def test_signals_error(self):
2297+ """
2298+ Test the signals() signal of the CheckMediaWorker class with a "error" origin
2299+ """
2300+ # GIVEN: A CheckMediaWorker instance
2301+ worker = CheckMediaWorker('file.ogv')
2302+
2303+ # WHEN: signals() is called with error and BufferedMedia
2304+ with patch.object(worker, 'stop') as mocked_stop, \
2305+ patch.object(worker, 'quit') as mocked_quit:
2306+ worker.signals('error', None)
2307+
2308+ # THEN: The worker should exit and the result should be True
2309+ mocked_stop.assert_called_once_with()
2310+ mocked_quit.emit.assert_called_once_with()
2311+ assert worker.result is False
2312
2313=== removed file 'tests/functional/openlp_core/ui/media/test_systemplayer.py'
2314--- tests/functional/openlp_core/ui/media/test_systemplayer.py 2017-12-29 09:15:48 +0000
2315+++ tests/functional/openlp_core/ui/media/test_systemplayer.py 1970-01-01 00:00:00 +0000
2316@@ -1,549 +0,0 @@
2317-# -*- coding: utf-8 -*-
2318-# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
2319-
2320-###############################################################################
2321-# OpenLP - Open Source Lyrics Projection #
2322-# --------------------------------------------------------------------------- #
2323-# Copyright (c) 2008-2018 OpenLP Developers #
2324-# --------------------------------------------------------------------------- #
2325-# This program is free software; you can redistribute it and/or modify it #
2326-# under the terms of the GNU General Public License as published by the Free #
2327-# Software Foundation; version 2 of the License. #
2328-# #
2329-# This program is distributed in the hope that it will be useful, but WITHOUT #
2330-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
2331-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
2332-# more details. #
2333-# #
2334-# You should have received a copy of the GNU General Public License along #
2335-# with this program; if not, write to the Free Software Foundation, Inc., 59 #
2336-# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
2337-###############################################################################
2338-"""
2339-Package to test the openlp.core.ui.media.systemplayer package.
2340-"""
2341-from unittest import TestCase
2342-from unittest.mock import MagicMock, call, patch
2343-
2344-from PyQt5 import QtCore, QtMultimedia
2345-
2346-from openlp.core.common.registry import Registry
2347-from openlp.core.ui.media import MediaState
2348-from openlp.core.ui.media.systemplayer import SystemPlayer, CheckMediaWorker, ADDITIONAL_EXT
2349-
2350-
2351-class TestSystemPlayer(TestCase):
2352- """
2353- Test the system media player
2354- """
2355- @patch('openlp.core.ui.media.systemplayer.mimetypes')
2356- @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
2357- def test_constructor(self, MockQMediaPlayer, mocked_mimetypes):
2358- """
2359- Test the SystemPlayer constructor
2360- """
2361- # GIVEN: The SystemPlayer class and a mockedQMediaPlayer
2362- mocked_media_player = MagicMock()
2363- mocked_media_player.supportedMimeTypes.return_value = [
2364- 'application/postscript',
2365- 'audio/aiff',
2366- 'audio/x-aiff',
2367- 'text/html',
2368- 'video/animaflex',
2369- 'video/x-ms-asf'
2370- ]
2371- mocked_mimetypes.guess_all_extensions.side_effect = [
2372- ['.aiff'],
2373- ['.aiff'],
2374- ['.afl'],
2375- ['.asf']
2376- ]
2377- MockQMediaPlayer.return_value = mocked_media_player
2378-
2379- # WHEN: An object is created from it
2380- player = SystemPlayer(self)
2381-
2382- # THEN: The correct initial values should be set up
2383- assert 'system' == player.name
2384- assert 'System' == player.original_name
2385- assert '&System' == player.display_name
2386- assert self == player.parent
2387- assert ADDITIONAL_EXT == player.additional_extensions
2388- MockQMediaPlayer.assert_called_once_with(None, QtMultimedia.QMediaPlayer.VideoSurface)
2389- mocked_mimetypes.init.assert_called_once_with()
2390- mocked_media_player.service.assert_called_once_with()
2391- mocked_media_player.supportedMimeTypes.assert_called_once_with()
2392- assert ['*.aiff'] == player.audio_extensions_list
2393- assert ['*.afl', '*.asf'] == player.video_extensions_list
2394-
2395- @patch('openlp.core.ui.media.systemplayer.QtMultimediaWidgets.QVideoWidget')
2396- @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
2397- def test_setup(self, MockQMediaPlayer, MockQVideoWidget):
2398- """
2399- Test the setup() method of SystemPlayer
2400- """
2401- # GIVEN: A SystemPlayer instance and a mock display
2402- player = SystemPlayer(self)
2403- mocked_display = MagicMock()
2404- mocked_display.size.return_value = [1, 2, 3, 4]
2405- mocked_video_widget = MagicMock()
2406- mocked_media_player = MagicMock()
2407- MockQVideoWidget.return_value = mocked_video_widget
2408- MockQMediaPlayer.return_value = mocked_media_player
2409-
2410- # WHEN: setup() is run
2411- player.setup(mocked_display)
2412-
2413- # THEN: The player should have a display widget
2414- MockQVideoWidget.assert_called_once_with(mocked_display)
2415- assert mocked_video_widget == mocked_display.video_widget
2416- mocked_display.size.assert_called_once_with()
2417- mocked_video_widget.resize.assert_called_once_with([1, 2, 3, 4])
2418- MockQMediaPlayer.assert_called_with(mocked_display)
2419- assert mocked_media_player == mocked_display.media_player
2420- mocked_media_player.setVideoOutput.assert_called_once_with(mocked_video_widget)
2421- mocked_video_widget.raise_.assert_called_once_with()
2422- mocked_video_widget.hide.assert_called_once_with()
2423- assert player.has_own_widget is True
2424-
2425- def test_disconnect_slots(self):
2426- """
2427- Test that we the disconnect slots method catches the TypeError
2428- """
2429- # GIVEN: A SystemPlayer class and a signal that throws a TypeError
2430- player = SystemPlayer(self)
2431- mocked_signal = MagicMock()
2432- mocked_signal.disconnect.side_effect = \
2433- TypeError('disconnect() failed between \'durationChanged\' and all its connections')
2434-
2435- # WHEN: disconnect_slots() is called
2436- player.disconnect_slots(mocked_signal)
2437-
2438- # THEN: disconnect should have been called and the exception should have been ignored
2439- mocked_signal.disconnect.assert_called_once_with()
2440-
2441- def test_check_available(self):
2442- """
2443- Test the check_available() method on SystemPlayer
2444- """
2445- # GIVEN: A SystemPlayer instance
2446- player = SystemPlayer(self)
2447-
2448- # WHEN: check_available is run
2449- result = player.check_available()
2450-
2451- # THEN: it should be available
2452- assert result is True
2453-
2454- def test_load_valid_media(self):
2455- """
2456- Test the load() method of SystemPlayer with a valid media file
2457- """
2458- # GIVEN: A SystemPlayer instance and a mocked display
2459- player = SystemPlayer(self)
2460- mocked_display = MagicMock()
2461- mocked_display.controller.media_info.volume = 1
2462- mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
2463-
2464- # WHEN: The load() method is run
2465- with patch.object(player, 'check_media') as mocked_check_media, \
2466- patch.object(player, 'volume') as mocked_volume:
2467- mocked_check_media.return_value = True
2468- result = player.load(mocked_display)
2469-
2470- # THEN: the file is sent to the video widget
2471- mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
2472- mocked_check_media.assert_called_once_with('/path/to/file')
2473- mocked_display.media_player.setMedia.assert_called_once_with(
2474- QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile('/path/to/file')))
2475- mocked_volume.assert_called_once_with(mocked_display, 1)
2476- assert result is True
2477-
2478- def test_load_invalid_media(self):
2479- """
2480- Test the load() method of SystemPlayer with an invalid media file
2481- """
2482- # GIVEN: A SystemPlayer instance and a mocked display
2483- player = SystemPlayer(self)
2484- mocked_display = MagicMock()
2485- mocked_display.controller.media_info.volume = 1
2486- mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
2487-
2488- # WHEN: The load() method is run
2489- with patch.object(player, 'check_media') as mocked_check_media, \
2490- patch.object(player, 'volume') as mocked_volume:
2491- mocked_check_media.return_value = False
2492- result = player.load(mocked_display)
2493-
2494- # THEN: stuff
2495- mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
2496- mocked_check_media.assert_called_once_with('/path/to/file')
2497- assert result is False
2498-
2499- def test_resize(self):
2500- """
2501- Test the resize() method of the SystemPlayer
2502- """
2503- # GIVEN: A SystemPlayer instance and a mocked display
2504- player = SystemPlayer(self)
2505- mocked_display = MagicMock()
2506- mocked_display.size.return_value = [1, 2, 3, 4]
2507-
2508- # WHEN: The resize() method is called
2509- player.resize(mocked_display)
2510-
2511- # THEN: The player is resized
2512- mocked_display.size.assert_called_once_with()
2513- mocked_display.video_widget.resize.assert_called_once_with([1, 2, 3, 4])
2514-
2515- @patch('openlp.core.ui.media.systemplayer.functools')
2516- def test_play_is_live(self, mocked_functools):
2517- """
2518- Test the play() method of the SystemPlayer on the live display
2519- """
2520- # GIVEN: A SystemPlayer instance and a mocked display
2521- mocked_functools.partial.return_value = 'function'
2522- player = SystemPlayer(self)
2523- mocked_display = MagicMock()
2524- mocked_display.controller.is_live = True
2525- mocked_display.controller.media_info.start_time = 1
2526- mocked_display.controller.media_info.volume = 1
2527-
2528- # WHEN: play() is called
2529- with patch.object(player, 'get_live_state') as mocked_get_live_state, \
2530- patch.object(player, 'seek') as mocked_seek, \
2531- patch.object(player, 'volume') as mocked_volume, \
2532- patch.object(player, 'set_state') as mocked_set_state, \
2533- patch.object(player, 'disconnect_slots') as mocked_disconnect_slots:
2534- mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
2535- result = player.play(mocked_display)
2536-
2537- # THEN: the media file is played
2538- mocked_get_live_state.assert_called_once_with()
2539- mocked_display.media_player.play.assert_called_once_with()
2540- mocked_seek.assert_called_once_with(mocked_display, 1000)
2541- mocked_volume.assert_called_once_with(mocked_display, 1)
2542- mocked_disconnect_slots.assert_called_once_with(mocked_display.media_player.durationChanged)
2543- mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
2544- mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
2545- mocked_display.video_widget.raise_.assert_called_once_with()
2546- assert result is True
2547-
2548- @patch('openlp.core.ui.media.systemplayer.functools')
2549- def test_play_is_preview(self, mocked_functools):
2550- """
2551- Test the play() method of the SystemPlayer on the preview display
2552- """
2553- # GIVEN: A SystemPlayer instance and a mocked display
2554- mocked_functools.partial.return_value = 'function'
2555- player = SystemPlayer(self)
2556- mocked_display = MagicMock()
2557- mocked_display.controller.is_live = False
2558- mocked_display.controller.media_info.start_time = 1
2559- mocked_display.controller.media_info.volume = 1
2560-
2561- # WHEN: play() is called
2562- with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
2563- patch.object(player, 'seek') as mocked_seek, \
2564- patch.object(player, 'volume') as mocked_volume, \
2565- patch.object(player, 'set_state') as mocked_set_state:
2566- mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
2567- result = player.play(mocked_display)
2568-
2569- # THEN: the media file is played
2570- mocked_get_preview_state.assert_called_once_with()
2571- mocked_display.media_player.play.assert_called_once_with()
2572- mocked_seek.assert_called_once_with(mocked_display, 1000)
2573- mocked_volume.assert_called_once_with(mocked_display, 1)
2574- mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
2575- mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
2576- mocked_display.video_widget.raise_.assert_called_once_with()
2577- assert result is True
2578-
2579- def test_pause_is_live(self):
2580- """
2581- Test the pause() method of the SystemPlayer on the live display
2582- """
2583- # GIVEN: A SystemPlayer instance
2584- player = SystemPlayer(self)
2585- mocked_display = MagicMock()
2586- mocked_display.controller.is_live = True
2587-
2588- # WHEN: The pause method is called
2589- with patch.object(player, 'get_live_state') as mocked_get_live_state, \
2590- patch.object(player, 'set_state') as mocked_set_state:
2591- mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PausedState
2592- player.pause(mocked_display)
2593-
2594- # THEN: The video is paused
2595- mocked_display.media_player.pause.assert_called_once_with()
2596- mocked_get_live_state.assert_called_once_with()
2597- mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
2598-
2599- def test_pause_is_preview(self):
2600- """
2601- Test the pause() method of the SystemPlayer on the preview display
2602- """
2603- # GIVEN: A SystemPlayer instance
2604- player = SystemPlayer(self)
2605- mocked_display = MagicMock()
2606- mocked_display.controller.is_live = False
2607-
2608- # WHEN: The pause method is called
2609- with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
2610- patch.object(player, 'set_state') as mocked_set_state:
2611- mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PausedState
2612- player.pause(mocked_display)
2613-
2614- # THEN: The video is paused
2615- mocked_display.media_player.pause.assert_called_once_with()
2616- mocked_get_preview_state.assert_called_once_with()
2617- mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
2618-
2619- def test_stop(self):
2620- """
2621- Test the stop() method of the SystemPlayer
2622- """
2623- # GIVEN: A SystemPlayer instance
2624- player = SystemPlayer(self)
2625- mocked_display = MagicMock()
2626-
2627- # WHEN: The stop method is called
2628- with patch.object(player, 'set_visible') as mocked_set_visible, \
2629- patch.object(player, 'set_state') as mocked_set_state:
2630- player.stop(mocked_display)
2631-
2632- # THEN: The video is stopped
2633- mocked_display.media_player.stop.assert_called_once_with()
2634- mocked_set_visible.assert_called_once_with(mocked_display, False)
2635- mocked_set_state.assert_called_once_with(MediaState.Stopped, mocked_display)
2636-
2637- def test_volume(self):
2638- """
2639- Test the volume() method of the SystemPlayer
2640- """
2641- # GIVEN: A SystemPlayer instance
2642- player = SystemPlayer(self)
2643- mocked_display = MagicMock()
2644- mocked_display.has_audio = True
2645-
2646- # WHEN: The stop method is called
2647- player.volume(mocked_display, 2)
2648-
2649- # THEN: The video is stopped
2650- mocked_display.media_player.setVolume.assert_called_once_with(2)
2651-
2652- def test_seek(self):
2653- """
2654- Test the seek() method of the SystemPlayer
2655- """
2656- # GIVEN: A SystemPlayer instance
2657- player = SystemPlayer(self)
2658- mocked_display = MagicMock()
2659-
2660- # WHEN: The stop method is called
2661- player.seek(mocked_display, 2)
2662-
2663- # THEN: The video is stopped
2664- mocked_display.media_player.setPosition.assert_called_once_with(2)
2665-
2666- def test_reset(self):
2667- """
2668- Test the reset() method of the SystemPlayer
2669- """
2670- # GIVEN: A SystemPlayer instance
2671- player = SystemPlayer(self)
2672- mocked_display = MagicMock()
2673-
2674- # WHEN: reset() is called
2675- with patch.object(player, 'set_state') as mocked_set_state, \
2676- patch.object(player, 'set_visible') as mocked_set_visible:
2677- player.reset(mocked_display)
2678-
2679- # THEN: The media player is reset
2680- mocked_display.media_player.stop()
2681- mocked_display.media_player.setMedia.assert_called_once_with(QtMultimedia.QMediaContent())
2682- mocked_set_visible.assert_called_once_with(mocked_display, False)
2683- mocked_display.video_widget.setVisible.assert_called_once_with(False)
2684- mocked_set_state.assert_called_once_with(MediaState.Off, mocked_display)
2685-
2686- def test_set_visible(self):
2687- """
2688- Test the set_visible() method on the SystemPlayer
2689- """
2690- # GIVEN: A SystemPlayer instance and a mocked display
2691- player = SystemPlayer(self)
2692- player.has_own_widget = True
2693- mocked_display = MagicMock()
2694-
2695- # WHEN: set_visible() is called
2696- player.set_visible(mocked_display, True)
2697-
2698- # THEN: The widget should be visible
2699- mocked_display.video_widget.setVisible.assert_called_once_with(True)
2700-
2701- def test_set_duration(self):
2702- """
2703- Test the set_duration() method of the SystemPlayer
2704- """
2705- # GIVEN: a mocked controller
2706- mocked_controller = MagicMock()
2707- mocked_controller.media_info.length = 5
2708-
2709- # WHEN: The set_duration() is called. NB: the 10 here is ignored by the code
2710- SystemPlayer.set_duration(mocked_controller, 10)
2711-
2712- # THEN: The maximum length of the slider should be set
2713- mocked_controller.seek_slider.setMaximum.assert_called_once_with(5)
2714-
2715- def test_update_ui(self):
2716- """
2717- Test the update_ui() method on the SystemPlayer
2718- """
2719- # GIVEN: A SystemPlayer instance
2720- player = SystemPlayer(self)
2721- player.state = [MediaState.Playing, MediaState.Playing]
2722- mocked_display = MagicMock()
2723- mocked_display.media_player.state.return_value = QtMultimedia.QMediaPlayer.PausedState
2724- mocked_display.controller.media_info.end_time = 1
2725- mocked_display.media_player.position.return_value = 2
2726- mocked_display.controller.seek_slider.isSliderDown.return_value = False
2727-
2728- # WHEN: update_ui() is called
2729- with patch.object(player, 'stop') as mocked_stop, \
2730- patch.object(player, 'set_visible') as mocked_set_visible:
2731- player.update_ui(mocked_display)
2732-
2733- # THEN: The UI is updated
2734- expected_stop_calls = [call(mocked_display)]
2735- expected_position_calls = [call(), call()]
2736- expected_block_signals_calls = [call(True), call(False)]
2737- mocked_display.media_player.state.assert_called_once_with()
2738- assert 1 == mocked_stop.call_count
2739- assert expected_stop_calls == mocked_stop.call_args_list
2740- assert 2 == mocked_display.media_player.position.call_count
2741- assert expected_position_calls == mocked_display.media_player.position.call_args_list
2742- mocked_set_visible.assert_called_once_with(mocked_display, False)
2743- mocked_display.controller.seek_slider.isSliderDown.assert_called_once_with()
2744- assert expected_block_signals_calls == mocked_display.controller.seek_slider.blockSignals.call_args_list
2745- mocked_display.controller.seek_slider.setSliderPosition.assert_called_once_with(2)
2746-
2747- def test_get_media_display_css(self):
2748- """
2749- Test the get_media_display_css() method of the SystemPlayer
2750- """
2751- # GIVEN: A SystemPlayer instance
2752- player = SystemPlayer(self)
2753-
2754- # WHEN: get_media_display_css() is called
2755- result = player.get_media_display_css()
2756-
2757- # THEN: The css should be empty
2758- assert '' == result
2759-
2760- @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
2761- def test_get_info(self, MockQMediaPlayer):
2762- """
2763- Test the get_info() method of the SystemPlayer
2764- """
2765- # GIVEN: A SystemPlayer instance
2766- mocked_media_player = MagicMock()
2767- mocked_media_player.supportedMimeTypes.return_value = []
2768- MockQMediaPlayer.return_value = mocked_media_player
2769- player = SystemPlayer(self)
2770-
2771- # WHEN: get_info() is called
2772- result = player.get_info()
2773-
2774- # THEN: The info should be correct
2775- expected_info = 'This media player uses your operating system to provide media capabilities.<br/> ' \
2776- '<strong>Audio</strong><br/>[]<br/><strong>Video</strong><br/>[]<br/>'
2777- assert expected_info == result
2778-
2779- @patch('openlp.core.ui.media.systemplayer.CheckMediaWorker')
2780- @patch('openlp.core.ui.media.systemplayer.QtCore.QThread')
2781- def test_check_media(self, MockQThread, MockCheckMediaWorker):
2782- """
2783- Test the check_media() method of the SystemPlayer
2784- """
2785- # GIVEN: A SystemPlayer instance and a mocked thread
2786- valid_file = '/path/to/video.ogv'
2787- mocked_application = MagicMock()
2788- Registry().create()
2789- Registry().register('application', mocked_application)
2790- player = SystemPlayer(self)
2791- mocked_thread = MagicMock()
2792- mocked_thread.isRunning.side_effect = [True, False]
2793- mocked_thread.quit = 'quit' # actually supposed to be a slot, but it's all mocked out anyway
2794- MockQThread.return_value = mocked_thread
2795- mocked_check_media_worker = MagicMock()
2796- mocked_check_media_worker.play = 'play'
2797- mocked_check_media_worker.result = True
2798- MockCheckMediaWorker.return_value = mocked_check_media_worker
2799-
2800- # WHEN: check_media() is called with a valid media file
2801- result = player.check_media(valid_file)
2802-
2803- # THEN: It should return True
2804- MockQThread.assert_called_once_with()
2805- MockCheckMediaWorker.assert_called_once_with(valid_file)
2806- mocked_check_media_worker.setVolume.assert_called_once_with(0)
2807- mocked_check_media_worker.moveToThread.assert_called_once_with(mocked_thread)
2808- mocked_check_media_worker.finished.connect.assert_called_once_with('quit')
2809- mocked_thread.started.connect.assert_called_once_with('play')
2810- mocked_thread.start.assert_called_once_with()
2811- assert 2 == mocked_thread.isRunning.call_count
2812- mocked_application.processEvents.assert_called_once_with()
2813- assert result is True
2814-
2815-
2816-class TestCheckMediaWorker(TestCase):
2817- """
2818- Test the CheckMediaWorker class
2819- """
2820- def test_constructor(self):
2821- """
2822- Test the constructor of the CheckMediaWorker class
2823- """
2824- # GIVEN: A file path
2825- path = 'file.ogv'
2826-
2827- # WHEN: The CheckMediaWorker object is instantiated
2828- worker = CheckMediaWorker(path)
2829-
2830- # THEN: The correct values should be set up
2831- assert worker is not None
2832-
2833- def test_signals_media(self):
2834- """
2835- Test the signals() signal of the CheckMediaWorker class with a "media" origin
2836- """
2837- # GIVEN: A CheckMediaWorker instance
2838- worker = CheckMediaWorker('file.ogv')
2839-
2840- # WHEN: signals() is called with media and BufferedMedia
2841- with patch.object(worker, 'stop') as mocked_stop, \
2842- patch.object(worker, 'finished') as mocked_finished:
2843- worker.signals('media', worker.BufferedMedia)
2844-
2845- # THEN: The worker should exit and the result should be True
2846- mocked_stop.assert_called_once_with()
2847- mocked_finished.emit.assert_called_once_with()
2848- assert worker.result is True
2849-
2850- def test_signals_error(self):
2851- """
2852- Test the signals() signal of the CheckMediaWorker class with a "error" origin
2853- """
2854- # GIVEN: A CheckMediaWorker instance
2855- worker = CheckMediaWorker('file.ogv')
2856-
2857- # WHEN: signals() is called with error and BufferedMedia
2858- with patch.object(worker, 'stop') as mocked_stop, \
2859- patch.object(worker, 'finished') as mocked_finished:
2860- worker.signals('error', None)
2861-
2862- # THEN: The worker should exit and the result should be True
2863- mocked_stop.assert_called_once_with()
2864- mocked_finished.emit.assert_called_once_with()
2865- assert worker.result is False
2866
2867=== modified file 'tests/functional/openlp_core/ui/test_firsttimeform.py'
2868--- tests/functional/openlp_core/ui/test_firsttimeform.py 2017-12-28 08:22:55 +0000
2869+++ tests/functional/openlp_core/ui/test_firsttimeform.py 2018-01-07 18:07:40 +0000
2870@@ -92,7 +92,6 @@
2871 assert frw.web_access is True, 'The default value of self.web_access should be True'
2872 assert frw.was_cancelled is False, 'The default value of self.was_cancelled should be False'
2873 assert [] == frw.theme_screenshot_threads, 'The list of threads should be empty'
2874- assert [] == frw.theme_screenshot_workers, 'The list of workers should be empty'
2875 assert frw.has_run_wizard is False, 'has_run_wizard should be False'
2876
2877 def test_set_defaults(self):
2878@@ -155,32 +154,33 @@
2879 mocked_display_combo_box.count.assert_called_with()
2880 mocked_display_combo_box.setCurrentIndex.assert_called_with(1)
2881
2882- def test_on_cancel_button_clicked(self):
2883+ @patch('openlp.core.ui.firsttimeform.time')
2884+ @patch('openlp.core.ui.firsttimeform.get_thread_worker')
2885+ @patch('openlp.core.ui.firsttimeform.is_thread_finished')
2886+ def test_on_cancel_button_clicked(self, mocked_is_thread_finished, mocked_get_thread_worker, mocked_time):
2887 """
2888 Test that the cancel button click slot shuts down the threads correctly
2889 """
2890 # GIVEN: A FRW, some mocked threads and workers (that isn't quite done) and other mocked stuff
2891- frw = FirstTimeForm(None)
2892- frw.initialize(MagicMock())
2893 mocked_worker = MagicMock()
2894- mocked_thread = MagicMock()
2895- mocked_thread.isRunning.side_effect = [True, False]
2896- frw.theme_screenshot_workers.append(mocked_worker)
2897- frw.theme_screenshot_threads.append(mocked_thread)
2898- with patch('openlp.core.ui.firsttimeform.time') as mocked_time, \
2899- patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor:
2900+ mocked_get_thread_worker.return_value = mocked_worker
2901+ mocked_is_thread_finished.side_effect = [False, True]
2902+ frw = FirstTimeForm(None)
2903+ frw.initialize(MagicMock())
2904+ frw.theme_screenshot_threads = ['test_thread']
2905+ with patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor:
2906
2907 # WHEN: on_cancel_button_clicked() is called
2908 frw.on_cancel_button_clicked()
2909
2910 # THEN: The right things should be called in the right order
2911 assert frw.was_cancelled is True, 'The was_cancelled property should have been set to True'
2912+ mocked_get_thread_worker.assert_called_once_with('test_thread')
2913 mocked_worker.set_download_canceled.assert_called_with(True)
2914- mocked_thread.isRunning.assert_called_with()
2915- assert 2 == mocked_thread.isRunning.call_count, 'isRunning() should have been called twice'
2916- mocked_time.sleep.assert_called_with(0.1)
2917- assert 1 == mocked_time.sleep.call_count, 'sleep() should have only been called once'
2918- mocked_set_normal_cursor.assert_called_with()
2919+ mocked_is_thread_finished.assert_called_with('test_thread')
2920+ assert mocked_is_thread_finished.call_count == 2, 'isRunning() should have been called twice'
2921+ mocked_time.sleep.assert_called_once_with(0.1)
2922+ mocked_set_normal_cursor.assert_called_once_with()
2923
2924 def test_broken_config(self):
2925 """
2926
2927=== modified file 'tests/functional/openlp_core/ui/test_mainwindow.py'
2928--- tests/functional/openlp_core/ui/test_mainwindow.py 2017-12-29 09:15:48 +0000
2929+++ tests/functional/openlp_core/ui/test_mainwindow.py 2018-01-07 18:07:40 +0000
2930@@ -60,9 +60,10 @@
2931 # Mock cursor busy/normal methods.
2932 self.app.set_busy_cursor = MagicMock()
2933 self.app.set_normal_cursor = MagicMock()
2934+ self.app.process_events = MagicMock()
2935 self.app.args = []
2936 Registry().register('application', self.app)
2937- Registry().set_flag('no_web_server', False)
2938+ Registry().set_flag('no_web_server', True)
2939 self.add_toolbar_action_patcher = patch('openlp.core.ui.mainwindow.create_action')
2940 self.mocked_add_toolbar_action = self.add_toolbar_action_patcher.start()
2941 self.mocked_add_toolbar_action.side_effect = self._create_mock_action
2942@@ -74,8 +75,8 @@
2943 """
2944 Delete all the C++ objects and stop all the patchers
2945 """
2946+ del self.main_window
2947 self.add_toolbar_action_patcher.stop()
2948- del self.main_window
2949
2950 def test_cmd_line_file(self):
2951 """
2952@@ -92,20 +93,20 @@
2953 # THEN the service from the arguments is loaded
2954 mocked_load_file.assert_called_with(service)
2955
2956- def test_cmd_line_arg(self):
2957+ @patch('openlp.core.ui.servicemanager.ServiceManager.load_file')
2958+ def test_cmd_line_arg(self, mocked_load_file):
2959 """
2960 Test that passing a non service file does nothing.
2961 """
2962 # GIVEN a non service file as an argument to openlp
2963 service = os.path.join('openlp.py')
2964 self.main_window.arguments = [service]
2965- with patch('openlp.core.ui.servicemanager.ServiceManager.load_file') as mocked_load_file:
2966-
2967- # WHEN the argument is processed
2968- self.main_window.open_cmd_line_files("")
2969-
2970- # THEN the file should not be opened
2971- assert mocked_load_file.called is False, 'load_file should not have been called'
2972+
2973+ # WHEN the argument is processed
2974+ self.main_window.open_cmd_line_files(service)
2975+
2976+ # THEN the file should not be opened
2977+ assert mocked_load_file.called is False, 'load_file should not have been called'
2978
2979 def test_main_window_title(self):
2980 """
2981
2982=== modified file 'tests/functional/openlp_plugins/songs/test_songselect.py'
2983--- tests/functional/openlp_plugins/songs/test_songselect.py 2017-12-29 10:19:33 +0000
2984+++ tests/functional/openlp_plugins/songs/test_songselect.py 2018-01-07 18:07:40 +0000
2985@@ -765,9 +765,9 @@
2986 assert ssform.search_combobox.isEnabled() is True
2987
2988 @patch('openlp.plugins.songs.forms.songselectform.Settings')
2989- @patch('openlp.plugins.songs.forms.songselectform.QtCore.QThread')
2990+ @patch('openlp.plugins.songs.forms.songselectform.run_thread')
2991 @patch('openlp.plugins.songs.forms.songselectform.SearchWorker')
2992- def test_on_search_button_clicked(self, MockedSearchWorker, MockedQtThread, MockedSettings):
2993+ def test_on_search_button_clicked(self, MockedSearchWorker, mocked_run_thread, MockedSettings):
2994 """
2995 Test that search fields are disabled when search button is clicked.
2996 """
2997
2998=== modified file 'tests/interfaces/openlp_core/ui/test_mainwindow.py'
2999--- tests/interfaces/openlp_core/ui/test_mainwindow.py 2017-12-29 09:15:48 +0000
3000+++ tests/interfaces/openlp_core/ui/test_mainwindow.py 2018-01-07 18:07:40 +0000
3001@@ -44,21 +44,21 @@
3002 self.app.set_normal_cursor = MagicMock()
3003 self.app.args = []
3004 Registry().register('application', self.app)
3005- Registry().set_flag('no_web_server', False)
3006+ Registry().set_flag('no_web_server', True)
3007 # Mock classes and methods used by mainwindow.
3008- with patch('openlp.core.ui.mainwindow.SettingsForm') as mocked_settings_form, \
3009- patch('openlp.core.ui.mainwindow.ImageManager') as mocked_image_manager, \
3010- patch('openlp.core.ui.mainwindow.LiveController') as mocked_live_controller, \
3011- patch('openlp.core.ui.mainwindow.PreviewController') as mocked_preview_controller, \
3012- patch('openlp.core.ui.mainwindow.OpenLPDockWidget') as mocked_dock_widget, \
3013- patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox') as mocked_q_tool_box_class, \
3014- patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget') as mocked_add_dock_method, \
3015- patch('openlp.core.ui.mainwindow.ServiceManager') as mocked_service_manager, \
3016- patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \
3017- patch('openlp.core.ui.mainwindow.ProjectorManager') as mocked_projector_manager, \
3018- patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \
3019- patch('openlp.core.ui.mainwindow.websockets.WebSocketServer') as mocked_websocketserver, \
3020- patch('openlp.core.ui.mainwindow.server.HttpServer') as mocked_httpserver:
3021+ with patch('openlp.core.ui.mainwindow.SettingsForm'), \
3022+ patch('openlp.core.ui.mainwindow.ImageManager'), \
3023+ patch('openlp.core.ui.mainwindow.LiveController'), \
3024+ patch('openlp.core.ui.mainwindow.PreviewController'), \
3025+ patch('openlp.core.ui.mainwindow.OpenLPDockWidget'), \
3026+ patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox'), \
3027+ patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget'), \
3028+ patch('openlp.core.ui.mainwindow.ServiceManager'), \
3029+ patch('openlp.core.ui.mainwindow.ThemeManager'), \
3030+ patch('openlp.core.ui.mainwindow.ProjectorManager'), \
3031+ patch('openlp.core.ui.mainwindow.Renderer'), \
3032+ patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \
3033+ patch('openlp.core.ui.mainwindow.server.HttpServer'):
3034 self.main_window = MainWindow()
3035
3036 def tearDown(self):