Merge lp:~phill-ridout/openlp/bug1073931 into lp:openlp

Proposed by Phill
Status: Merged
Approved by: Tim Bentley
Approved revision: 2498
Merged at revision: 2500
Proposed branch: lp:~phill-ridout/openlp/bug1073931
Merge into: lp:openlp
Diff against target: 388 lines (+119/-38)
5 files modified
openlp/core/lib/db.py (+32/-10)
openlp/core/ui/firsttimeform.py (+36/-21)
openlp/plugins/bibles/lib/db.py (+3/-2)
openlp/plugins/bibles/lib/manager.py (+2/-0)
tests/functional/test_init.py (+46/-5)
To merge this branch: bzr merge lp:~phill-ridout/openlp/bug1073931
Reviewer Review Type Date Requested Status
Tim Bentley Approve
Raoul Snyman Approve
Review via email: mp+249720@code.launchpad.net

This proposal supersedes a proposal from 2015-02-08.

Description of the change

Fixes bug1073931 "Corrupted databases stop OpenLP from starting"
Checks if the database session is available before trying to use it.
Use a sha256 hash to verify downloaded files. See also: https://code.launchpad.net/~phill-ridout/openlp/sha256

Tests failed on the crosswalk tests. I have included my locally run test output.

Add this to your merge proposal:
--------------------------------
lp:~phill-ridout/openlp/bug1073931 (revision 2495)
[SUCCESS] http://ci.openlp.org/job/Branch-01-Pull/934/
[SUCCESS] http://ci.openlp.org/job/Branch-02-Functional-Tests/860/
[FAILURE] http://ci.openlp.org/job/Branch-03-Interface-Tests/805/
Stopping after failure

Process finished with exit code 0

E

Error
Traceback (most recent call last):
  File "/usr/lib/python3.4/unittest/case.py", line 58, in testPartExecutor
    yield
  File "/usr/lib/python3.4/unittest/case.py", line 577, in run
    testMethod()
  File "/home/phill/Projects/openlp/bug1073931/tests/interfaces/openlp_plugins/bibles/test_lib_http.py", line 102, in crosswalk_extract_books_test
    books = handler.get_books_from_http('niv')
  File "/home/phill/Projects/openlp/bug1073931/openlp/plugins/bibles/lib/http.py", line 413, in get_books_from_http
    content = content.find('ul', {'class': 'parent'})
AttributeError: 'NoneType' object has no attribute 'find'
-------------------- >> begin captured logging << --------------------
openlp.core.common.registry: INFO: Registry Initialising
openlp.plugins.bibles.lib.http: DEBUG: CWExtract.init("None")
openlp.plugins.bibles.lib.http: DEBUG: CWExtract.get_books_from_http("niv")
openlp.core.utils.__init__: DEBUG: Downloading URL = http://www.biblestudytools.com/niv/
openlp.core.utils.__init__: DEBUG: Downloaded URL = http://www.biblestudytools.com/niv/
openlp.core.utils.__init__: DEBUG: <http.client.HTTPResponse object at 0x7f66e37052b0>
--------------------- >> end captured logging << ---------------------

E

Error
Traceback (most recent call last):
  File "/usr/lib/python3.4/unittest/case.py", line 58, in testPartExecutor
    yield
  File "/usr/lib/python3.4/unittest/case.py", line 577, in run
    testMethod()
  File "/home/phill/Projects/openlp/bug1073931/tests/interfaces/openlp_plugins/bibles/test_lib_http.py", line 115, in crosswalk_extract_verse_test
    results = handler.get_bible_chapter('niv', 'john', 3)
  File "/home/phill/Projects/openlp/bug1073931/openlp/plugins/bibles/lib/http.py", line 371, in get_bible_chapter
    send_error_message('parse')
  File "/home/phill/Projects/openlp/bug1073931/openlp/plugins/bibles/lib/http.py", line 649, in send_error_message
    translate('BiblesPlugin.HTTPBible', 'There was a problem extracting your verse selection. If this error '
  File "/home/phill/Projects/openlp/bug1073931/openlp/core/lib/ui.py", line 114, in critical_error_message_box
    return Registry().get('main_window').error_message(title if title else UiStrings().Error, message)
AttributeError: 'NoneType' object has no attribute 'error_message'
-------------------- >> begin captured logging << --------------------
openlp.core.common.registry: INFO: Registry Initialising
openlp.plugins.bibles.lib.http: DEBUG: CWExtract.init("None")
openlp.plugins.bibles.lib.http: DEBUG: CWExtract.get_bible_chapter("niv", "john", "3")
openlp.core.utils.__init__: DEBUG: Downloading URL = http://www.biblestudytools.com/niv/john/3.html
openlp.core.utils.__init__: DEBUG: Downloaded URL = http://www.biblestudytools.com/niv/john/3.html
openlp.core.utils.__init__: DEBUG: <http.client.HTTPResponse object at 0x7f66e33ecd68>
openlp.plugins.bibles.lib.http: ERROR: No verses found in the CrossWalk response.
--------------------- >> end captured logging << ---------------------

======================================================================
ERROR: Test Crosswalk retrieval of book list for NIV bible
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/phill/Projects/openlp/bug1073931/tests/interfaces/openlp_plugins/bibles/test_lib_http.py", line 102, in crosswalk_extract_books_test
    books = handler.get_books_from_http('niv')
  File "/home/phill/Projects/openlp/bug1073931/openlp/plugins/bibles/lib/http.py", line 413, in get_books_from_http
    content = content.find('ul', {'class': 'parent'})
nose.proxy.AttributeError: 'NoneType' object has no attribute 'find'
-------------------- >> begin captured logging << --------------------
openlp.core.common.registry: INFO: Registry Initialising
openlp.plugins.bibles.lib.http: DEBUG: CWExtract.init("None")
openlp.plugins.bibles.lib.http: DEBUG: CWExtract.get_books_from_http("niv")
openlp.core.utils.__init__: DEBUG: Downloading URL = http://www.biblestudytools.com/niv/
openlp.core.utils.__init__: DEBUG: Downloaded URL = http://www.biblestudytools.com/niv/
openlp.core.utils.__init__: DEBUG: <http.client.HTTPResponse object at 0x7f66e37052b0>
--------------------- >> end captured logging << ---------------------

======================================================================
ERROR: Test Crosswalk retrieval of verse list for NIV bible John 3
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/phill/Projects/openlp/bug1073931/tests/interfaces/openlp_plugins/bibles/test_lib_http.py", line 115, in crosswalk_extract_verse_test
    results = handler.get_bible_chapter('niv', 'john', 3)
  File "/home/phill/Projects/openlp/bug1073931/openlp/plugins/bibles/lib/http.py", line 371, in get_bible_chapter
    send_error_message('parse')
  File "/home/phill/Projects/openlp/bug1073931/openlp/plugins/bibles/lib/http.py", line 649, in send_error_message
    translate('BiblesPlugin.HTTPBible', 'There was a problem extracting your verse selection. If this error '
  File "/home/phill/Projects/openlp/bug1073931/openlp/core/lib/ui.py", line 114, in critical_error_message_box
    return Registry().get('main_window').error_message(title if title else UiStrings().Error, message)
nose.proxy.AttributeError: 'NoneType' object has no attribute 'error_message'
-------------------- >> begin captured logging << --------------------
openlp.core.common.registry: INFO: Registry Initialising
openlp.plugins.bibles.lib.http: DEBUG: CWExtract.init("None")
openlp.plugins.bibles.lib.http: DEBUG: CWExtract.get_bible_chapter("niv", "john", "3")
openlp.core.utils.__init__: DEBUG: Downloading URL = http://www.biblestudytools.com/niv/john/3.html
openlp.core.utils.__init__: DEBUG: Downloaded URL = http://www.biblestudytools.com/niv/john/3.html
openlp.core.utils.__init__: DEBUG: <http.client.HTTPResponse object at 0x7f66e33ecd68>
openlp.plugins.bibles.lib.http: ERROR: No verses found in the CrossWalk response.
--------------------- >> end captured logging << ---------------------

----------------------------------------------------------------------
Ran 576 tests in 26.153s

FAILED (SKIP=4, errors=2)

Process finished with exit code 1

To post a comment you must log in.
Revision history for this message
Tim Bentley (trb143) : Posted in a previous version of this proposal
review: Needs Fixing
Revision history for this message
Phill (phill-ridout) wrote : Posted in a previous version of this proposal

Tim, could you see my comments please.

Revision history for this message
Raoul Snyman (raoul-snyman) : Posted in a previous version of this proposal
review: Needs Fixing
Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

I would rather call the hash you read in something like "hash" or "file_hash". The "sha256" gets really confusing, especially to someone like me who has used the hashing functions a fair amount, and I think you're using the hashing function, but then it's not right, etc.

Other than that, this looks OK to me.

review: Approve
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
1=== modified file 'openlp/core/lib/db.py'
2--- openlp/core/lib/db.py 2015-01-19 08:34:29 +0000
3+++ openlp/core/lib/db.py 2015-02-13 20:34:52 +0000
4@@ -60,6 +60,35 @@
5 return session, metadata
6
7
8+def get_db_path(plugin_name, db_file_name=None):
9+ """
10+ Create a path to a database from the plugin name and database name
11+
12+ :param plugin_name: Name of plugin
13+ :param db_file_name: File name of database
14+ :return: The path to the database as type str
15+ """
16+ if db_file_name is None:
17+ return 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name)
18+ else:
19+ return 'sqlite:///%s/%s' % (AppLocation.get_section_data_path(plugin_name), db_file_name)
20+
21+
22+def handle_db_error(plugin_name, db_file_name):
23+ """
24+ Log and report to the user that a database cannot be loaded
25+
26+ :param plugin_name: Name of plugin
27+ :param db_file_name: File name of database
28+ :return: None
29+ """
30+ db_path = get_db_path(plugin_name, db_file_name)
31+ log.exception('Error loading database: %s', db_path)
32+ critical_error_message_box(translate('OpenLP.Manager', 'Database Error'),
33+ translate('OpenLP.Manager', 'OpenLP cannot load your database.\n\nDatabase: %s')
34+ % db_path)
35+
36+
37 def init_url(plugin_name, db_file_name=None):
38 """
39 Return the database URL.
40@@ -69,13 +98,9 @@
41 """
42 settings = Settings()
43 settings.beginGroup(plugin_name)
44- db_url = ''
45 db_type = settings.value('db type')
46 if db_type == 'sqlite':
47- if db_file_name is None:
48- db_url = 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name)
49- else:
50- db_url = 'sqlite:///%s/%s' % (AppLocation.get_section_data_path(plugin_name), db_file_name)
51+ db_url = get_db_path(plugin_name, db_file_name)
52 else:
53 db_url = '%s://%s:%s@%s/%s' % (db_type, urlquote(settings.value('db username')),
54 urlquote(settings.value('db password')),
55@@ -212,7 +237,7 @@
56 try:
57 db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
58 except (SQLAlchemyError, DBAPIError):
59- log.exception('Error loading database: %s', self.db_url)
60+ handle_db_error(plugin_name, db_file_name)
61 return
62 if db_ver > up_ver:
63 critical_error_message_box(
64@@ -225,10 +250,7 @@
65 try:
66 self.session = init_schema(self.db_url)
67 except (SQLAlchemyError, DBAPIError):
68- log.exception('Error loading database: %s', self.db_url)
69- critical_error_message_box(translate('OpenLP.Manager', 'Database Error'),
70- translate('OpenLP.Manager', 'OpenLP cannot load your database.\n\nDatabase: %s')
71- % self.db_url)
72+ handle_db_error(plugin_name, db_file_name)
73
74 def save_object(self, object_instance, commit=True):
75 """
76
77=== modified file 'openlp/core/ui/firsttimeform.py'
78--- openlp/core/ui/firsttimeform.py 2015-01-27 14:52:18 +0000
79+++ openlp/core/ui/firsttimeform.py 2015-02-13 20:34:52 +0000
80@@ -22,6 +22,7 @@
81 """
82 This module contains the first time wizard.
83 """
84+import hashlib
85 import logging
86 import os
87 import time
88@@ -47,10 +48,10 @@
89 """
90 This thread downloads a theme's screenshot
91 """
92- screenshot_downloaded = QtCore.pyqtSignal(str, str)
93+ screenshot_downloaded = QtCore.pyqtSignal(str, str, str)
94 finished = QtCore.pyqtSignal()
95
96- def __init__(self, themes_url, title, filename, screenshot):
97+ def __init__(self, themes_url, title, filename, sha256, screenshot):
98 """
99 Set up the worker object
100 """
101@@ -58,6 +59,7 @@
102 self.themes_url = themes_url
103 self.title = title
104 self.filename = filename
105+ self.sha256 = sha256
106 self.screenshot = screenshot
107 super(ThemeScreenshotWorker, self).__init__()
108
109@@ -71,7 +73,7 @@
110 urllib.request.urlretrieve('%s%s' % (self.themes_url, self.screenshot),
111 os.path.join(gettempdir(), 'openlp', self.screenshot))
112 # Signal that the screenshot has been downloaded
113- self.screenshot_downloaded.emit(self.title, self.filename)
114+ self.screenshot_downloaded.emit(self.title, self.filename, self.sha256)
115 except:
116 log.exception('Unable to download screenshot')
117 finally:
118@@ -221,8 +223,9 @@
119 self.application.process_events()
120 title = self.config.get('songs_%s' % song, 'title')
121 filename = self.config.get('songs_%s' % song, 'filename')
122+ sha256 = self.config.get('songs_%s' % song, 'sha256', fallback='')
123 item = QtGui.QListWidgetItem(title, self.songs_list_widget)
124- item.setData(QtCore.Qt.UserRole, filename)
125+ item.setData(QtCore.Qt.UserRole, (filename, sha256))
126 item.setCheckState(QtCore.Qt.Unchecked)
127 item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
128 bible_languages = self.config.get('bibles', 'languages')
129@@ -237,8 +240,9 @@
130 self.application.process_events()
131 title = self.config.get('bible_%s' % bible, 'title')
132 filename = self.config.get('bible_%s' % bible, 'filename')
133+ sha256 = self.config.get('bible_%s' % bible, 'sha256', fallback='')
134 item = QtGui.QTreeWidgetItem(lang_item, [title])
135- item.setData(0, QtCore.Qt.UserRole, filename)
136+ item.setData(0, QtCore.Qt.UserRole, (filename, sha256))
137 item.setCheckState(0, QtCore.Qt.Unchecked)
138 item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
139 self.bibles_tree_widget.expandAll()
140@@ -249,8 +253,9 @@
141 self.application.process_events()
142 title = self.config.get('theme_%s' % theme, 'title')
143 filename = self.config.get('theme_%s' % theme, 'filename')
144+ sha256 = self.config.get('theme_%s' % theme, 'sha256', fallback='')
145 screenshot = self.config.get('theme_%s' % theme, 'screenshot')
146- worker = ThemeScreenshotWorker(self.themes_url, title, filename, screenshot)
147+ worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot)
148 self.theme_screenshot_workers.append(worker)
149 worker.screenshot_downloaded.connect(self.on_screenshot_downloaded)
150 thread = QtCore.QThread(self)
151@@ -356,7 +361,7 @@
152 time.sleep(0.1)
153 self.application.set_normal_cursor()
154
155- def on_screenshot_downloaded(self, title, filename):
156+ def on_screenshot_downloaded(self, title, filename, sha256):
157 """
158 Add an item to the list when a theme has been downloaded
159
160@@ -364,7 +369,7 @@
161 :param filename: The filename of the theme
162 """
163 item = QtGui.QListWidgetItem(title, self.themes_list_widget)
164- item.setData(QtCore.Qt.UserRole, filename)
165+ item.setData(QtCore.Qt.UserRole, (filename, sha256))
166 item.setCheckState(QtCore.Qt.Unchecked)
167 item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
168
169@@ -385,7 +390,7 @@
170 self.was_cancelled = True
171 self.close()
172
173- def url_get_file(self, url, f_path):
174+ def url_get_file(self, url, f_path, sha256=None):
175 """"
176 Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any
177 point. Returns False on download error.
178@@ -400,16 +405,24 @@
179 try:
180 url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT)
181 filename = open(f_path, "wb")
182+ if sha256:
183+ hasher = hashlib.sha256()
184 # Download until finished or canceled.
185 while not self.was_cancelled:
186 data = url_file.read(block_size)
187 if not data:
188 break
189 filename.write(data)
190+ if sha256:
191+ hasher.update(data)
192 block_count += 1
193 self._download_progress(block_count, block_size)
194 filename.close()
195- except ConnectionError:
196+ if sha256 and hasher.hexdigest() != sha256:
197+ log.error('sha256 sums did not match for file: {}'.format(f_path))
198+ os.remove(f_path)
199+ return False
200+ except urllib.error.URLError:
201 trace_error_handler(log)
202 filename.close()
203 os.remove(f_path)
204@@ -449,7 +462,7 @@
205 site = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT)
206 meta = site.info()
207 return int(meta.get("Content-Length"))
208- except ConnectionException:
209+ except urllib.error.URLError:
210 if retries > CONNECTION_RETRIES:
211 raise
212 else:
213@@ -491,7 +504,7 @@
214 self.application.process_events()
215 item = self.songs_list_widget.item(i)
216 if item.checkState() == QtCore.Qt.Checked:
217- filename = item.data(QtCore.Qt.UserRole)
218+ filename, sha256 = item.data(QtCore.Qt.UserRole)
219 size = self._get_file_size('%s%s' % (self.songs_url, filename))
220 self.max_progress += size
221 # Loop through the Bibles list and increase for each selected item
222@@ -500,7 +513,7 @@
223 self.application.process_events()
224 item = iterator.value()
225 if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
226- filename = item.data(0, QtCore.Qt.UserRole)
227+ filename, sha256 = item.data(0, QtCore.Qt.UserRole)
228 size = self._get_file_size('%s%s' % (self.bibles_url, filename))
229 self.max_progress += size
230 iterator += 1
231@@ -509,10 +522,10 @@
232 self.application.process_events()
233 item = self.themes_list_widget.item(i)
234 if item.checkState() == QtCore.Qt.Checked:
235- filename = item.data(QtCore.Qt.UserRole)
236+ filename, sha256 = item.data(QtCore.Qt.UserRole)
237 size = self._get_file_size('%s%s' % (self.themes_url, filename))
238 self.max_progress += size
239- except ConnectionError:
240+ except urllib.error.URLError:
241 trace_error_handler(log)
242 critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'),
243 translate('OpenLP.FirstTimeWizard', 'There was a connection problem during '
244@@ -608,31 +621,33 @@
245 for i in range(self.songs_list_widget.count()):
246 item = self.songs_list_widget.item(i)
247 if item.checkState() == QtCore.Qt.Checked:
248- filename = item.data(QtCore.Qt.UserRole)
249+ filename, sha256 = item.data(QtCore.Qt.UserRole)
250 self._increment_progress_bar(self.downloading % filename, 0)
251 self.previous_size = 0
252 destination = os.path.join(songs_destination, str(filename))
253- if not self.url_get_file('%s%s' % (self.songs_url, filename), destination):
254+ if not self.url_get_file('%s%s' % (self.songs_url, filename), destination, sha256):
255 return False
256 # Download Bibles
257 bibles_iterator = QtGui.QTreeWidgetItemIterator(self.bibles_tree_widget)
258 while bibles_iterator.value():
259 item = bibles_iterator.value()
260 if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
261- bible = item.data(0, QtCore.Qt.UserRole)
262+ bible, sha256 = item.data(0, QtCore.Qt.UserRole)
263 self._increment_progress_bar(self.downloading % bible, 0)
264 self.previous_size = 0
265- if not self.url_get_file('%s%s' % (self.bibles_url, bible), os.path.join(bibles_destination, bible)):
266+ if not self.url_get_file('%s%s' % (self.bibles_url, bible), os.path.join(bibles_destination, bible),
267+ sha256):
268 return False
269 bibles_iterator += 1
270 # Download themes
271 for i in range(self.themes_list_widget.count()):
272 item = self.themes_list_widget.item(i)
273 if item.checkState() == QtCore.Qt.Checked:
274- theme = item.data(QtCore.Qt.UserRole)
275+ theme, sha256 = item.data(QtCore.Qt.UserRole)
276 self._increment_progress_bar(self.downloading % theme, 0)
277 self.previous_size = 0
278- if not self.url_get_file('%s%s' % (self.themes_url, theme), os.path.join(themes_destination, theme)):
279+ if not self.url_get_file('%s%s' % (self.themes_url, theme), os.path.join(themes_destination, theme),
280+ sha256):
281 return False
282 return True
283
284
285=== modified file 'openlp/plugins/bibles/lib/db.py'
286--- openlp/plugins/bibles/lib/db.py 2015-01-18 13:39:21 +0000
287+++ openlp/plugins/bibles/lib/db.py 2015-02-13 20:34:52 +0000
288@@ -131,6 +131,7 @@
289 log.info('BibleDB loaded')
290 QtCore.QObject.__init__(self)
291 self.bible_plugin = parent
292+ self.session = None
293 if 'path' not in kwargs:
294 raise KeyError('Missing keyword argument "path".')
295 if 'name' not in kwargs and 'file' not in kwargs:
296@@ -144,8 +145,8 @@
297 if 'file' in kwargs:
298 self.file = kwargs['file']
299 Manager.__init__(self, 'bibles', init_schema, self.file, upgrade)
300- if 'file' in kwargs:
301- self.get_name()
302+ if self.session and 'file' in kwargs:
303+ self.get_name()
304 if 'path' in kwargs:
305 self.path = kwargs['path']
306 self.wizard = None
307
308=== modified file 'openlp/plugins/bibles/lib/manager.py'
309--- openlp/plugins/bibles/lib/manager.py 2015-01-18 13:39:21 +0000
310+++ openlp/plugins/bibles/lib/manager.py 2015-02-13 20:34:52 +0000
311@@ -121,6 +121,8 @@
312 self.old_bible_databases = []
313 for filename in files:
314 bible = BibleDB(self.parent, path=self.path, file=filename)
315+ if not bible.session:
316+ continue
317 name = bible.get_name()
318 # Remove corrupted files.
319 if name is None:
320
321=== modified file 'tests/functional/test_init.py'
322--- tests/functional/test_init.py 2015-01-26 20:42:19 +0000
323+++ tests/functional/test_init.py 2015-02-13 20:34:52 +0000
324@@ -125,7 +125,7 @@
325 # WHEN: Calling parse_options
326 results = parse_options(options)
327
328- # THEN: A tuple should be returned with the parsed options and left over args
329+ # THEN: A tuple should be returned with the parsed options and left over options
330 self.assertEqual(results, (Values({'no_error_form': True, 'dev_version': True, 'portable': True,
331 'style': 'style', 'loglevel': 'debug'}), ['extra', 'qt', 'args']))
332
333@@ -136,10 +136,51 @@
334 # GIVEN: A list of valid options
335 options = ['openlp.py', '-e', '-l', 'debug', '-pd', '-s', 'style', 'extra', 'qt', 'args']
336
337- # WHEN: Passing in the options through sys.argv and calling parse_args with None
338+ # WHEN: Passing in the options through sys.argv and calling parse_options with None
339 with patch.object(sys, 'argv', options):
340 results = parse_options(None)
341
342- # THEN: parse_args should return a tuple of valid options and of left over options that OpenLP does not use
343- self.assertEqual(results, (Values({'no_error_form': True, 'dev_version': True, 'portable': True,
344- 'style': 'style', 'loglevel': 'debug'}), ['extra', 'qt', 'args']))
345+ # THEN: parse_options should return a tuple of valid options and of left over options that OpenLP does not use
346+ self.assertEqual(results, (Values({'no_error_form': True, 'dev_version': True, 'portable': True,
347+ 'style': 'style', 'loglevel': 'debug'}), ['extra', 'qt', 'args']))
348+
349+ def test_parse_options_valid_long_options(self):
350+ """
351+ Test that parse_options parses valid long options correctly
352+ """
353+ # GIVEN: A list of valid options
354+ options = ['--no-error-form', 'extra', '--log-level', 'debug', 'qt', '--portable', '--dev-version', 'args',
355+ '--style=style']
356+
357+ # WHEN: Calling parse_options
358+ results = parse_options(options)
359+
360+ # THEN: parse_options should return a tuple of valid options and of left over options that OpenLP does not use
361+ self.assertEqual(results, (Values({'no_error_form': True, 'dev_version': True, 'portable': True,
362+ 'style': 'style', 'loglevel': 'debug'}), ['extra', 'qt', 'args']))
363+
364+ def test_parse_options_help_option(self):
365+ """
366+ Test that parse_options raises an SystemExit exception when called with invalid options
367+ """
368+ # GIVEN: The --help option
369+ options = ['--help']
370+
371+ # WHEN: Calling parse_options
372+ # THEN: parse_options should raise an SystemExit exception with exception code 0
373+ with self.assertRaises(SystemExit) as raised_exception:
374+ parse_options(options)
375+ self.assertEqual(raised_exception.exception.code, 0)
376+
377+ def test_parse_options_invalid_option(self):
378+ """
379+ Test that parse_options raises an SystemExit exception when called with invalid options
380+ """
381+ # GIVEN: A list including invalid options
382+ options = ['-t']
383+
384+ # WHEN: Calling parse_options
385+ # THEN: parse_options should raise an SystemExit exception with exception code 2
386+ with self.assertRaises(SystemExit) as raised_exception:
387+ parse_options(options)
388+ self.assertEqual(raised_exception.exception.code, 2)