Merge lp:~raoul-snyman/openlp/bug-1742910 into lp:openlp

Proposed by Raoul Snyman
Status: Merged
Merged at revision: 2808
Proposed branch: lp:~raoul-snyman/openlp/bug-1742910
Merge into: lp:openlp
Diff against target: 755 lines (+333/-194)
10 files modified
openlp/core/app.py (+1/-1)
openlp/core/common/path.py (+2/-2)
openlp/core/threading.py (+14/-10)
openlp/core/ui/mainwindow.py (+3/-4)
tests/functional/openlp_core/api/http/test_error.py (+37/-35)
tests/functional/openlp_core/api/test_deploy.py (+94/-15)
tests/functional/openlp_core/common/test_path.py (+14/-1)
tests/functional/openlp_core/lib/test_path.py (+0/-87)
tests/functional/openlp_core/lib/test_ui.py (+55/-28)
tests/functional/openlp_core/test_threading.py (+113/-11)
To merge this branch: bzr merge lp:~raoul-snyman/openlp/bug-1742910
Reviewer Review Type Date Requested Status
Tim Bentley Approve
Review via email: mp+336066@code.launchpad.net

This proposal supersedes a proposal from 2018-01-13.

Description of the change

Fix bug #1742910 by moving the threads to the application object instead of the main window object.

Add this to your merge proposal:
--------------------------------------------------------------------------------
lp:~raoul-snyman/openlp/bug-1742910 (revision 2810)
https://ci.openlp.io/job/Branch-01-Pull/2418/ [SUCCESS]
https://ci.openlp.io/job/Branch-02a-Linux-Tests/2319/ [SUCCESS]
https://ci.openlp.io/job/Branch-02b-macOS-Tests/114/ [SUCCESS]
https://ci.openlp.io/job/Branch-03a-Build-Source/36/ [SUCCESS]
https://ci.openlp.io/job/Branch-03b-Build-macOS/35/ [SUCCESS]
https://ci.openlp.io/job/Branch-04a-Code-Analysis/1498/ [SUCCESS]
https://ci.openlp.io/job/Branch-04b-Test-Coverage/1311/ [SUCCESS]
https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/262/ [FAILURE]
Stopping after failure

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

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'openlp/core/app.py'
--- openlp/core/app.py 2018-01-07 04:40:40 +0000
+++ openlp/core/app.py 2018-01-13 06:00:49 +0000
@@ -63,8 +63,8 @@
63 The core application class. This class inherits from Qt's QApplication63 The core application class. This class inherits from Qt's QApplication
64 class in order to provide the core of the application.64 class in order to provide the core of the application.
65 """65 """
66
67 args = []66 args = []
67 worker_threads = {}
6868
69 def exec(self):69 def exec(self):
70 """70 """
7171
=== modified file 'openlp/core/common/path.py'
--- openlp/core/common/path.py 2017-12-29 09:15:48 +0000
+++ openlp/core/common/path.py 2018-01-13 06:00:49 +0000
@@ -26,9 +26,9 @@
26from openlp.core.common import is_win26from openlp.core.common import is_win
2727
28if is_win():28if is_win():
29 from pathlib import WindowsPath as PathVariant29 from pathlib import WindowsPath as PathVariant # pragma: nocover
30else:30else:
31 from pathlib import PosixPath as PathVariant31 from pathlib import PosixPath as PathVariant # pragma: nocover
3232
33log = logging.getLogger(__name__)33log = logging.getLogger(__name__)
3434
3535
=== modified file 'openlp/core/threading.py'
--- openlp/core/threading.py 2018-01-07 17:50:29 +0000
+++ openlp/core/threading.py 2018-01-13 06:00:49 +0000
@@ -50,12 +50,12 @@
50 """50 """
51 if not thread_name:51 if not thread_name:
52 raise ValueError('A thread_name is required when calling the "run_thread" function')52 raise ValueError('A thread_name is required when calling the "run_thread" function')
53 main_window = Registry().get('main_window')53 application = Registry().get('application')
54 if thread_name in main_window.threads:54 if thread_name in application.worker_threads:
55 raise KeyError('A thread with the name "{}" has already been created, please use another'.format(thread_name))55 raise KeyError('A thread with the name "{}" has already been created, please use another'.format(thread_name))
56 # Create the thread and add the thread and the worker to the parent56 # Create the thread and add the thread and the worker to the parent
57 thread = QtCore.QThread()57 thread = QtCore.QThread()
58 main_window.threads[thread_name] = {58 application.worker_threads[thread_name] = {
59 'thread': thread,59 'thread': thread,
60 'worker': worker60 'worker': worker
61 }61 }
@@ -78,7 +78,10 @@
78 :param str thread_name: The name of the thread78 :param str thread_name: The name of the thread
79 :returns: The worker for this thread name79 :returns: The worker for this thread name
80 """80 """
81 return Registry().get('main_window').threads.get(thread_name)81 thread_info = Registry().get('application').worker_threads.get(thread_name)
82 if not thread_info:
83 raise KeyError('No thread named "{}" exists'.format(thread_name))
84 return thread_info.get('worker')
8285
8386
84def is_thread_finished(thread_name):87def is_thread_finished(thread_name):
@@ -88,8 +91,8 @@
88 :param str thread_name: The name of the thread91 :param str thread_name: The name of the thread
89 :returns: True if the thread is finished, False if it is still running92 :returns: True if the thread is finished, False if it is still running
90 """93 """
91 main_window = Registry().get('main_window')94 app = Registry().get('application')
92 return thread_name not in main_window.threads or main_window.threads[thread_name]['thread'].isFinished()95 return thread_name not in app.worker_threads or app.worker_threads[thread_name]['thread'].isFinished()
9396
9497
95def make_remove_thread(thread_name):98def make_remove_thread(thread_name):
@@ -99,13 +102,14 @@
99 :param str thread_name: The name of the thread which should be removed from the thread registry.102 :param str thread_name: The name of the thread which should be removed from the thread registry.
100 :returns: A function which will remove the thread from the thread registry.103 :returns: A function which will remove the thread from the thread registry.
101 """104 """
102 def remove_thread():105
106 def remove_thread(): # pragma: nocover
103 """107 """
104 Stop and remove a registered thread108 Stop and remove a registered thread
105109
106 :param str thread_name: The name of the thread to stop and remove110 :param str thread_name: The name of the thread to stop and remove
107 """111 """
108 main_window = Registry().get('main_window')112 application = Registry().get('application')
109 if thread_name in main_window.threads:113 if thread_name in application.worker_threads:
110 del main_window.threads[thread_name]114 del application.worker_threads[thread_name]
111 return remove_thread115 return remove_thread
112116
=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py 2018-01-07 17:50:29 +0000
+++ openlp/core/ui/mainwindow.py 2018-01-13 06:00:49 +0000
@@ -477,7 +477,6 @@
477 """477 """
478 super(MainWindow, self).__init__()478 super(MainWindow, self).__init__()
479 Registry().register('main_window', self)479 Registry().register('main_window', self)
480 self.threads = {}
481 self.clipboard = self.application.clipboard()480 self.clipboard = self.application.clipboard()
482 self.arguments = ''.join(self.application.args)481 self.arguments = ''.join(self.application.args)
483 # Set up settings sections for the main application (not for use by plugins).482 # Set up settings sections for the main application (not for use by plugins).
@@ -557,11 +556,11 @@
557 wait_dialog.setAutoClose(False)556 wait_dialog.setAutoClose(False)
558 wait_dialog.setCancelButton(None)557 wait_dialog.setCancelButton(None)
559 wait_dialog.show()558 wait_dialog.show()
560 for thread_name in self.threads.keys():559 for thread_name in self.application.worker_threads.keys():
561 log.debug('Waiting for thread %s', thread_name)560 log.debug('Waiting for thread %s', thread_name)
562 self.application.processEvents()561 self.application.processEvents()
563 thread = self.threads[thread_name]['thread']562 thread = self.application.worker_threads[thread_name]['thread']
564 worker = self.threads[thread_name]['worker']563 worker = self.application.worker_threads[thread_name]['worker']
565 try:564 try:
566 if worker and hasattr(worker, 'stop'):565 if worker and hasattr(worker, 'stop'):
567 # If the worker has a stop method, run it566 # If the worker has a stop method, run it
568567
=== modified file 'tests/functional/openlp_core/api/http/test_error.py'
--- tests/functional/openlp_core/api/http/test_error.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/api/http/test_error.py 2018-01-13 06:00:49 +0000
@@ -22,38 +22,40 @@
22"""22"""
23Functional tests to test the API Error Class.23Functional tests to test the API Error Class.
24"""24"""
2525from openlp.core.api.http.errors import HttpError, NotFound, ServerError
26from unittest import TestCase26
2727
28from openlp.core.api.http.errors import NotFound, ServerError28def test_http_error():
2929 """
3030 Test the HTTPError class
31class TestApiError(TestCase):31 """
32 """32 # GIVEN: An HTTPError class
33 A test suite to test out the Error in the API code33 # WHEN: An instance is created
34 """34 error = HttpError(400, 'Access Denied')
35 def test_not_found(self):35
36 """36 # THEN: The to_response() method should return the correct information
37 Test the Not Found error displays the correct information37 assert error.to_response() == ('Access Denied', 400), 'to_response() should have returned the correct info'
38 """38
39 # GIVEN:39
40 # WHEN: I raise an exception40def test_not_found():
41 with self.assertRaises(Exception) as context:41 """
42 raise NotFound()42 Test the Not Found error displays the correct information
4343 """
44 # THEN: we get an error and a status44 # GIVEN: A NotFound class
45 assert 'Not Found' == context.exception.message, 'A Not Found exception should be thrown'45 # WHEN: An instance is created
46 assert 404 == context.exception.status, 'A 404 status should be thrown'46 error = NotFound()
4747
48 def test_server_error(self):48 # THEN: The to_response() method should return the correct information
49 """49 assert error.to_response() == ('Not Found', 404), 'to_response() should have returned the correct info'
50 Test the server error displays the correct information50
51 """51
52 # GIVEN:52def test_server_error():
53 # WHEN: I raise an exception53 """
54 with self.assertRaises(Exception) as context:54 Test the server error displays the correct information
55 raise ServerError()55 """
5656 # GIVEN: A ServerError class
57 # THEN: we get an error and a status57 # WHEN: An instance of the class is created
58 assert'Server Error' == context.exception.message, 'A Not Found exception should be thrown'58 error = ServerError()
59 assert 500 == context.exception.status, 'A 500 status should be thrown'59
60 # THEN: The to_response() method should return the correct information
61 assert error.to_response() == ('Server Error', 500), 'to_response() should have returned the correct info'
6062
=== modified file 'tests/functional/openlp_core/api/test_deploy.py'
--- tests/functional/openlp_core/api/test_deploy.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/api/test_deploy.py 2018-01-13 06:00:49 +0000
@@ -21,11 +21,12 @@
21###############################################################################21###############################################################################
22from tempfile import mkdtemp22from tempfile import mkdtemp
23from unittest import TestCase23from unittest import TestCase
2424from unittest.mock import MagicMock, patch
25from openlp.core.api.deploy import deploy_zipfile25
26from openlp.core.common.path import Path, copyfile26from openlp.core.api.deploy import deploy_zipfile, download_sha256, download_and_check
2727from openlp.core.common.path import Path
28TEST_PATH = (Path(__file__).parent / '..' / '..' / '..' / 'resources').resolve()28
29CONFIG_FILE = '2c266badff1e3d140664c50fd1460a2b332b24d5ad8c267fa62e506b5eb6d894 deploy/site.zip\n2017_06_27'
2930
3031
31class TestRemoteDeploy(TestCase):32class TestRemoteDeploy(TestCase):
@@ -45,17 +46,95 @@
45 """46 """
46 self.app_root_path.rmtree()47 self.app_root_path.rmtree()
4748
48 def test_deploy_zipfile(self):49 @patch('openlp.core.api.deploy.ZipFile')
50 def test_deploy_zipfile(self, MockZipFile):
49 """51 """
50 Remote Deploy tests - test the dummy zip file is processed correctly52 Remote Deploy tests - test the dummy zip file is processed correctly
51 """53 """
52 # GIVEN: A new downloaded zip file54 # GIVEN: A new downloaded zip file
53 zip_path = TEST_PATH / 'remotes' / 'site.zip'55 mocked_zipfile = MagicMock()
54 app_root_path = self.app_root_path / 'site.zip'56 MockZipFile.return_value = mocked_zipfile
55 copyfile(zip_path, app_root_path)57 root_path = Path('/tmp/remotes')
5658
57 # WHEN: I process the zipfile59 # WHEN: deploy_zipfile() is called
58 deploy_zipfile(self.app_root_path, 'site.zip')60 deploy_zipfile(root_path, 'site.zip')
5961
60 # THEN: test if www directory has been created62 # THEN: the zip file should have been extracted to the right location
61 assert (self.app_root_path / 'www').is_dir(), 'We should have a www directory'63 MockZipFile.assert_called_once_with('/tmp/remotes/site.zip')
64 mocked_zipfile.extractall.assert_called_once_with('/tmp/remotes')
65
66 @patch('openlp.core.api.deploy.Registry')
67 @patch('openlp.core.api.deploy.get_web_page')
68 def test_download_sha256_connection_error(self, mocked_get_web_page, MockRegistry):
69 """
70 Test that if a ConnectionError occurs while downloading a sha256 False is returned
71 """
72 # GIVEN: A bunch of mocks
73 MockRegistry.return_value.get.return_value.applicationVersion.return_value = '1.0'
74 mocked_get_web_page.side_effect = ConnectionError()
75
76 # WHEN: download_sha256() is called
77 result = download_sha256()
78
79 # THEN: The result should be False
80 assert result is False, 'download_sha256() should return False when encountering ConnectionError'
81
82 @patch('openlp.core.api.deploy.Registry')
83 @patch('openlp.core.api.deploy.get_web_page')
84 def test_download_sha256_no_config(self, mocked_get_web_page, MockRegistry):
85 """
86 Test that if there's no config when downloading a sha256 None is returned
87 """
88 # GIVEN: A bunch of mocks
89 MockRegistry.return_value.get.return_value.applicationVersion.return_value = '1.0'
90 mocked_get_web_page.return_value = None
91
92 # WHEN: download_sha256() is called
93 result = download_sha256()
94
95 # THEN: The result should be Nonw
96 assert result is None, 'download_sha256() should return None when there is a problem downloading the page'
97
98 @patch('openlp.core.api.deploy.Registry')
99 @patch('openlp.core.api.deploy.get_web_page')
100 def test_download_sha256(self, mocked_get_web_page, MockRegistry):
101 """
102 Test that the sha256 and the version are returned
103 """
104 # GIVEN: A bunch of mocks
105 MockRegistry.return_value.get.return_value.applicationVersion.return_value = '1.0'
106 mocked_get_web_page.return_value = CONFIG_FILE
107
108 # WHEN: download_sha256() is called
109 result = download_sha256()
110
111 # THEN: The result should be Nonw
112 assert result == ('2c266badff1e3d140664c50fd1460a2b332b24d5ad8c267fa62e506b5eb6d894', '2017_06_27'), \
113 'download_sha256() should return a tuple of sha256 and version'
114
115 @patch('openlp.core.api.deploy.Registry')
116 @patch('openlp.core.api.deploy.download_sha256')
117 @patch('openlp.core.api.deploy.get_url_file_size')
118 @patch('openlp.core.api.deploy.download_file')
119 @patch('openlp.core.api.deploy.AppLocation.get_section_data_path')
120 @patch('openlp.core.api.deploy.deploy_zipfile')
121 def test_download_and_check(self, mocked_deploy_zipfile, mocked_get_data_path, mocked_download_file,
122 mocked_get_url_file_size, mocked_download_sha256, MockRegistry):
123 # GIVEN: A bunch of mocks
124 mocked_get_data_path.return_value = Path('/tmp/remotes')
125 mocked_download_file.return_value = True
126 mocked_get_url_file_size.return_value = 5
127 mocked_download_sha256.return_value = ('asdfgh', '0.1')
128 MockRegistry.return_value.get.return_value.applicationVersion.return_value = '1.0'
129 mocked_callback = MagicMock()
130
131 # WHEN: download_and_check() is called
132 download_and_check(mocked_callback)
133
134 # THEN: The correct things should have been done
135 mocked_download_sha256.assert_called_once_with()
136 mocked_get_url_file_size.assert_called_once_with('https://get.openlp.org/webclient/site.zip')
137 mocked_callback.setRange.assert_called_once_with(0, 5)
138 mocked_download_file.assert_called_once_with(mocked_callback, 'https://get.openlp.org/webclient/site.zip',
139 Path('/tmp/remotes/site.zip'), sha256='asdfgh')
140 mocked_deploy_zipfile.assert_called_once_with(Path('/tmp/remotes'), 'site.zip')
62141
=== modified file 'tests/functional/openlp_core/common/test_path.py'
--- tests/functional/openlp_core/common/test_path.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/common/test_path.py 2018-01-13 06:00:49 +0000
@@ -27,7 +27,7 @@
27from unittest.mock import ANY, MagicMock, patch27from unittest.mock import ANY, MagicMock, patch
2828
29from openlp.core.common.path import Path, copy, copyfile, copytree, create_paths, path_to_str, replace_params, \29from openlp.core.common.path import Path, copy, copyfile, copytree, create_paths, path_to_str, replace_params, \
30 str_to_path, which30 str_to_path, which, files_to_paths
3131
3232
33class TestShutil(TestCase):33class TestShutil(TestCase):
@@ -401,3 +401,16 @@
401 except:401 except:
402 # THEN: `create_paths` raises an exception402 # THEN: `create_paths` raises an exception
403 pass403 pass
404
405 def test_files_to_paths(self):
406 """
407 Test the files_to_paths() method
408 """
409 # GIVEN: A list of string filenames
410 test_files = ['/tmp/openlp/file1.txt', '/tmp/openlp/file2.txt']
411
412 # WHEN: files_to_paths is called
413 result = files_to_paths(test_files)
414
415 # THEN: The result should be a list of Paths
416 assert result == [Path('/tmp/openlp/file1.txt'), Path('/tmp/openlp/file2.txt')]
404417
=== removed file 'tests/functional/openlp_core/lib/test_path.py'
--- tests/functional/openlp_core/lib/test_path.py 2017-12-23 09:09:45 +0000
+++ tests/functional/openlp_core/lib/test_path.py 1970-01-01 00:00:00 +0000
@@ -1,87 +0,0 @@
1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
4###############################################################################
5# OpenLP - Open Source Lyrics Projection #
6# --------------------------------------------------------------------------- #
7# Copyright (c) 2008-2017 OpenLP Developers #
8# --------------------------------------------------------------------------- #
9# This program is free software; you can redistribute it and/or modify it #
10# under the terms of the GNU General Public License as published by the Free #
11# Software Foundation; version 2 of the License. #
12# #
13# This program is distributed in the hope that it will be useful, but WITHOUT #
14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
15# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
16# more details. #
17# #
18# You should have received a copy of the GNU General Public License along #
19# with this program; if not, write to the Free Software Foundation, Inc., 59 #
20# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
21###############################################################################
22"""
23Package to test the openlp.core.lib.path package.
24"""
25import os
26from unittest import TestCase
27
28from openlp.core.common.path import Path, path_to_str, str_to_path
29
30
31class TestPath(TestCase):
32 """
33 Tests for the :mod:`openlp.core.lib.path` module
34 """
35
36 def test_path_to_str_type_error(self):
37 """
38 Test that `path_to_str` raises a type error when called with an invalid type
39 """
40 # GIVEN: The `path_to_str` function
41 # WHEN: Calling `path_to_str` with an invalid Type
42 # THEN: A TypeError should have been raised
43 with self.assertRaises(TypeError):
44 path_to_str(str())
45
46 def test_path_to_str_none(self):
47 """
48 Test that `path_to_str` correctly converts the path parameter when passed with None
49 """
50 # GIVEN: The `path_to_str` function
51 # WHEN: Calling the `path_to_str` function with None
52 result = path_to_str(None)
53
54 # THEN: `path_to_str` should return an empty string
55 assert result == ''
56
57 def test_path_to_str_path_object(self):
58 """
59 Test that `path_to_str` correctly converts the path parameter when passed a Path object
60 """
61 # GIVEN: The `path_to_str` function
62 # WHEN: Calling the `path_to_str` function with a Path object
63 result = path_to_str(Path('test/path'))
64
65 # THEN: `path_to_str` should return a string representation of the Path object
66 assert result == os.path.join('test', 'path')
67
68 def test_str_to_path_type_error(self):
69 """
70 Test that `str_to_path` raises a type error when called with an invalid type
71 """
72 # GIVEN: The `str_to_path` function
73 # WHEN: Calling `str_to_path` with an invalid Type
74 # THEN: A TypeError should have been raised
75 with self.assertRaises(TypeError):
76 str_to_path(Path())
77
78 def test_str_to_path_empty_str(self):
79 """
80 Test that `str_to_path` correctly converts the string parameter when passed with and empty string
81 """
82 # GIVEN: The `str_to_path` function
83 # WHEN: Calling the `str_to_path` function with None
84 result = str_to_path('')
85
86 # THEN: `path_to_str` should return None
87 assert result is None
880
=== modified file 'tests/functional/openlp_core/lib/test_ui.py'
--- tests/functional/openlp_core/lib/test_ui.py 2017-12-17 20:19:19 +0000
+++ tests/functional/openlp_core/lib/test_ui.py 2018-01-13 06:00:49 +0000
@@ -23,14 +23,14 @@
23Package to test the openlp.core.lib.ui package.23Package to test the openlp.core.lib.ui package.
24"""24"""
25from unittest import TestCase25from unittest import TestCase
26from unittest.mock import MagicMock, patch26from unittest.mock import MagicMock, patch, call
2727
28from PyQt5 import QtCore, QtGui, QtWidgets28from PyQt5 import QtCore, QtGui, QtWidgets
2929
30from openlp.core.common.i18n import UiStrings, translate30from openlp.core.common.i18n import UiStrings, translate
31from openlp.core.lib.ui import add_welcome_page, create_button_box, create_horizontal_adjusting_combo_box, \31from openlp.core.lib.ui import add_welcome_page, create_button_box, create_horizontal_adjusting_combo_box, \
32 create_button, create_action, create_valign_selection_widgets, find_and_set_in_combo_box, create_widget_action, \32 create_button, create_action, create_valign_selection_widgets, find_and_set_in_combo_box, create_widget_action, \
33 set_case_insensitive_completer33 set_case_insensitive_completer, critical_error_message_box
3434
3535
36class TestUi(TestCase):36class TestUi(TestCase):
@@ -80,6 +80,34 @@
80 assert 1 == len(btnbox.buttons())80 assert 1 == len(btnbox.buttons())
81 assert QtWidgets.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0])81 assert QtWidgets.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0])
8282
83 @patch('openlp.core.lib.ui.Registry')
84 def test_critical_error_message_box(self, MockRegistry):
85 """
86 Test the critical_error_message_box() function
87 """
88 # GIVEN: A mocked Registry
89 # WHEN: critical_error_message_box() is called
90 critical_error_message_box('Error', 'This is an error')
91
92 # THEN: The error_message() method on the main window should be called
93 MockRegistry.return_value.get.return_value.error_message.assert_called_once_with('Error', 'This is an error')
94
95 @patch('openlp.core.lib.ui.QtWidgets.QMessageBox.critical')
96 def test_critical_error_question(self, mocked_critical):
97 """
98 Test the critical_error_message_box() function
99 """
100 # GIVEN: A mocked critical() method and some other mocks
101 mocked_parent = MagicMock()
102
103 # WHEN: critical_error_message_box() is called
104 critical_error_message_box(None, 'This is a question', mocked_parent, True)
105
106 # THEN: The error_message() method on the main window should be called
107 mocked_critical.assert_called_once_with(mocked_parent, 'Error', 'This is a question',
108 QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
109 QtWidgets.QMessageBox.No))
110
83 def test_create_horizontal_adjusting_combo_box(self):111 def test_create_horizontal_adjusting_combo_box(self):
84 """112 """
85 Test creating a horizontal adjusting combo box113 Test creating a horizontal adjusting combo box
@@ -92,65 +120,64 @@
92120
93 # THEN: We should get a ComboBox121 # THEN: We should get a ComboBox
94 assert isinstance(combo, QtWidgets.QComboBox)122 assert isinstance(combo, QtWidgets.QComboBox)
95 assert 'combo1' == combo.objectName()123 assert combo.objectName() == 'combo1'
96 assert QtWidgets.QComboBox.AdjustToMinimumContentsLength == combo.sizeAdjustPolicy()124 assert QtWidgets.QComboBox.AdjustToMinimumContentsLength == combo.sizeAdjustPolicy()
97125
98 def test_create_button(self):126 @patch('openlp.core.lib.ui.log')
127 def test_create_button(self, mocked_log):
99 """128 """
100 Test creating a button129 Test creating a button
101 """130 """
102 # GIVEN: A dialog131 # GIVEN: A dialog
103 dialog = QtWidgets.QDialog()132 dialog = QtWidgets.QDialog()
104133
105 # WHEN: We create the button
106 btn = create_button(dialog, 'my_btn')
107
108 # THEN: We should get a button with a name
109 assert isinstance(btn, QtWidgets.QPushButton)
110 assert 'my_btn' == btn.objectName()
111 assert btn.isEnabled() is True
112
113 # WHEN: We create a button with some attributes134 # WHEN: We create a button with some attributes
114 btn = create_button(dialog, 'my_btn', text='Hello', tooltip='How are you?', enabled=False)135 btn = create_button(dialog, 'my_btn', text='Hello', tooltip='How are you?', enabled=False, role='test', test=1)
115136
116 # THEN: We should get a button with those attributes137 # THEN: We should get a button with those attributes
117 assert isinstance(btn, QtWidgets.QPushButton)138 assert isinstance(btn, QtWidgets.QPushButton)
118 assert 'Hello' == btn.text()139 assert btn.objectName() == 'my_btn'
119 assert 'How are you?' == btn.toolTip()140 assert btn.text() == 'Hello'
141 assert btn.toolTip() == 'How are you?'
120 assert btn.isEnabled() is False142 assert btn.isEnabled() is False
143 assert mocked_log.warning.call_args_list == [call('The role "test" is not defined in create_push_button().'),
144 call('Parameter test was not consumed in create_button().')]
145
146 def test_create_tool_button(self):
147 """
148 Test creating a toolbutton
149 """
150 # GIVEN: A dialog
151 dialog = QtWidgets.QDialog()
121152
122 # WHEN: We create a toolbutton153 # WHEN: We create a toolbutton
123 btn = create_button(dialog, 'my_btn', btn_class='toolbutton')154 btn = create_button(dialog, 'my_btn', btn_class='toolbutton')
124155
125 # THEN: We should get a toolbutton156 # THEN: We should get a toolbutton
126 assert isinstance(btn, QtWidgets.QToolButton)157 assert isinstance(btn, QtWidgets.QToolButton)
127 assert 'my_btn' == btn.objectName()158 assert btn.objectName() == 'my_btn'
128 assert btn.isEnabled() is True159 assert btn.isEnabled() is True
129160
130 def test_create_action(self):161 @patch('openlp.core.lib.ui.log')
162 def test_create_action(self, mocked_log):
131 """163 """
132 Test creating an action164 Test creating an action
133 """165 """
134 # GIVEN: A dialog166 # GIVEN: A dialog
135 dialog = QtWidgets.QDialog()167 dialog = QtWidgets.QDialog()
136168
137 # WHEN: We create an action
138 action = create_action(dialog, 'my_action')
139
140 # THEN: We should get a QAction
141 assert isinstance(action, QtWidgets.QAction)
142 assert 'my_action' == action.objectName()
143
144 # WHEN: We create an action with some properties169 # WHEN: We create an action with some properties
145 action = create_action(dialog, 'my_action', text='my text', icon=':/wizards/wizard_firsttime.bmp',170 action = create_action(dialog, 'my_action', text='my text', icon=':/wizards/wizard_firsttime.bmp',
146 tooltip='my tooltip', statustip='my statustip')171 tooltip='my tooltip', statustip='my statustip', test=1)
147172
148 # THEN: These properties should be set173 # THEN: These properties should be set
149 assert isinstance(action, QtWidgets.QAction)174 assert isinstance(action, QtWidgets.QAction)
150 assert 'my text' == action.text()175 assert action.objectName() == 'my_action'
176 assert action.text() == 'my text'
151 assert isinstance(action.icon(), QtGui.QIcon)177 assert isinstance(action.icon(), QtGui.QIcon)
152 assert 'my tooltip' == action.toolTip()178 assert action.toolTip() == 'my tooltip'
153 assert 'my statustip' == action.statusTip()179 assert action.statusTip() == 'my statustip'
180 mocked_log.warning.assert_called_once_with('Parameter test was not consumed in create_action().')
154181
155 def test_create_action_on_mac_osx(self):182 def test_create_action_on_mac_osx(self):
156 """183 """
157184
=== modified file 'tests/functional/openlp_core/test_threading.py'
--- tests/functional/openlp_core/test_threading.py 2018-01-07 17:50:29 +0000
+++ tests/functional/openlp_core/test_threading.py 2018-01-13 06:00:49 +0000
@@ -22,9 +22,10 @@
22"""22"""
23Package to test the openlp.core.threading package.23Package to test the openlp.core.threading package.
24"""24"""
25from inspect import isfunction
25from unittest.mock import MagicMock, call, patch26from unittest.mock import MagicMock, call, patch
2627
27from openlp.core.version import run_thread28from openlp.core.threading import ThreadWorker, run_thread, get_thread_worker, is_thread_finished, make_remove_thread
2829
2930
30def test_run_thread_no_name():31def test_run_thread_no_name():
@@ -47,9 +48,9 @@
47 Test that trying to run a thread with a name that already exists will throw a KeyError48 Test that trying to run a thread with a name that already exists will throw a KeyError
48 """49 """
49 # GIVEN: A mocked registry with a main window object50 # GIVEN: A mocked registry with a main window object
50 mocked_main_window = MagicMock()51 mocked_application = MagicMock()
51 mocked_main_window.threads = {'test_thread': MagicMock()}52 mocked_application.worker_threads = {'test_thread': MagicMock()}
52 MockRegistry.return_value.get.return_value = mocked_main_window53 MockRegistry.return_value.get.return_value = mocked_application
5354
54 # WHEN: run_thread() is called55 # WHEN: run_thread() is called
55 try:56 try:
@@ -66,18 +67,19 @@
66 Test that running a thread works correctly67 Test that running a thread works correctly
67 """68 """
68 # GIVEN: A mocked registry with a main window object69 # GIVEN: A mocked registry with a main window object
69 mocked_main_window = MagicMock()70 mocked_application = MagicMock()
70 mocked_main_window.threads = {}71 mocked_application.worker_threads = {}
71 MockRegistry.return_value.get.return_value = mocked_main_window72 MockRegistry.return_value.get.return_value = mocked_application
7273
73 # WHEN: run_thread() is called74 # WHEN: run_thread() is called
74 run_thread(MagicMock(), 'test_thread')75 run_thread(MagicMock(), 'test_thread')
7576
76 # THEN: The thread should be in the threads list and the correct methods should have been called77 # THEN: The thread should be in the threads list and the correct methods should have been called
77 assert len(mocked_main_window.threads.keys()) == 1, 'There should be 1 item in the list of threads'78 assert len(mocked_application.worker_threads.keys()) == 1, 'There should be 1 item in the list of threads'
78 assert list(mocked_main_window.threads.keys()) == ['test_thread'], 'The test_thread item should be in the list'79 assert list(mocked_application.worker_threads.keys()) == ['test_thread'], \
79 mocked_worker = mocked_main_window.threads['test_thread']['worker']80 'The test_thread item should be in the list'
80 mocked_thread = mocked_main_window.threads['test_thread']['thread']81 mocked_worker = mocked_application.worker_threads['test_thread']['worker']
82 mocked_thread = mocked_application.worker_threads['test_thread']['thread']
81 mocked_worker.moveToThread.assert_called_once_with(mocked_thread)83 mocked_worker.moveToThread.assert_called_once_with(mocked_thread)
82 mocked_thread.started.connect.assert_called_once_with(mocked_worker.start)84 mocked_thread.started.connect.assert_called_once_with(mocked_worker.start)
83 expected_quit_calls = [call(mocked_thread.quit), call(mocked_worker.deleteLater)]85 expected_quit_calls = [call(mocked_thread.quit), call(mocked_worker.deleteLater)]
@@ -87,3 +89,103 @@
87 'The threads finished signal should be connected to its deleteLater slot'89 'The threads finished signal should be connected to its deleteLater slot'
88 assert mocked_thread.finished.connect.call_count == 2, 'The signal should have been connected twice'90 assert mocked_thread.finished.connect.call_count == 2, 'The signal should have been connected twice'
89 mocked_thread.start.assert_called_once_with()91 mocked_thread.start.assert_called_once_with()
92
93
94def test_thread_worker():
95 """
96 Test that creating a thread worker object and calling start throws and NotImplementedError
97 """
98 # GIVEN: A ThreadWorker class
99 worker = ThreadWorker()
100
101 try:
102 # WHEN: calling start()
103 worker.start()
104 assert False, 'A NotImplementedError should have been thrown'
105 except NotImplementedError:
106 # A NotImplementedError should be thrown
107 pass
108 except Exception:
109 assert False, 'A NotImplementedError should have been thrown'
110
111
112@patch('openlp.core.threading.Registry')
113def test_get_thread_worker(MockRegistry):
114 """
115 Test that calling the get_thread_worker() function returns the correct worker
116 """
117 # GIVEN: A mocked thread worker
118 mocked_worker = MagicMock()
119 MockRegistry.return_value.get.return_value.worker_threads = {'test_thread': {'worker': mocked_worker}}
120
121 # WHEN: get_thread_worker() is called
122 worker = get_thread_worker('test_thread')
123
124 # THEN: The mocked worker is returned
125 assert worker is mocked_worker, 'The mocked worker should have been returned'
126
127
128@patch('openlp.core.threading.Registry')
129def test_get_thread_worker_mising(MockRegistry):
130 """
131 Test that calling the get_thread_worker() function raises a KeyError if it does not exist
132 """
133 # GIVEN: A mocked thread worker
134 MockRegistry.return_value.get.return_value.worker_threads = {}
135
136 try:
137 # WHEN: get_thread_worker() is called
138 get_thread_worker('test_thread')
139 assert False, 'A KeyError should have been raised'
140 except KeyError:
141 # THEN: The mocked worker is returned
142 pass
143 except Exception:
144 assert False, 'A KeyError should have been raised'
145
146
147@patch('openlp.core.threading.Registry')
148def test_is_thread_finished(MockRegistry):
149 """
150 Test the is_thread_finished() function
151 """
152 # GIVEN: A mock thread and worker
153 mocked_thread = MagicMock()
154 mocked_thread.isFinished.return_value = False
155 MockRegistry.return_value.get.return_value.worker_threads = {'test': {'thread': mocked_thread}}
156
157 # WHEN: is_thread_finished() is called
158 result = is_thread_finished('test')
159
160 # THEN: The result should be correct
161 assert result is False, 'is_thread_finished should have returned False'
162
163
164@patch('openlp.core.threading.Registry')
165def test_is_thread_finished_missing(MockRegistry):
166 """
167 Test that calling the is_thread_finished() function returns True if the thread doesn't exist
168 """
169 # GIVEN: A mocked thread worker
170 MockRegistry.return_value.get.return_value.worker_threads = {}
171
172 # WHEN: get_thread_worker() is called
173 result = is_thread_finished('test_thread')
174
175 # THEN: The result should be correct
176 assert result is True, 'is_thread_finished should return True when a thread is missing'
177
178
179def test_make_remove_thread():
180 """
181 Test the make_remove_thread() function
182 """
183 # GIVEN: A thread name
184 thread_name = 'test_thread'
185
186 # WHEN: make_remove_thread() is called
187 rm_func = make_remove_thread(thread_name)
188
189 # THEN: The result should be a function
190 assert isfunction(rm_func), 'make_remove_thread should return a function'
191 assert rm_func.__name__ == 'remove_thread'