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