← Back to team overview

openlp-core team mailing list archive

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

 

Phill has proposed merging lp:~phill-ridout/openlp/bug1073931 into lp:openlp.

Requested reviews:
  Tim Bentley (trb143)
Related bugs:
  Bug #1073931 in OpenLP: "Corrupted databases stop OpenLP from starting"
  https://bugs.launchpad.net/openlp/+bug/1073931

For more details, see:
https://code.launchpad.net/~phill-ridout/openlp/bug1073931/+merge/249015

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

-- 
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/lib/db.py'
--- openlp/core/lib/db.py	2015-01-19 08:34:29 +0000
+++ openlp/core/lib/db.py	2015-02-08 18:45:17 +0000
@@ -60,6 +60,35 @@
     return session, metadata
 
 
+def get_db_path(plugin_name, db_file_name=None):
+    """
+    Create a path to a database from the plugin name and database name
+
+    :param plugin_name: Name of plugin
+    :param db_file_name: File name of database
+    :return: The path to the database as type str
+    """
+    if db_file_name is None:
+        return 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name)
+    else:
+        return 'sqlite:///%s/%s' % (AppLocation.get_section_data_path(plugin_name), db_file_name)
+
+
+def handle_db_error(plugin_name, db_file_name):
+    """
+    Log and report to the user that a database cannot be loaded
+
+    :param plugin_name: Name of plugin
+    :param db_file_name: File name of database
+    :return: None
+    """
+    db_path = get_db_path(plugin_name, db_file_name)
+    log.exception('Error loading database: %s', db_path)
+    critical_error_message_box(translate('OpenLP.Manager', 'Database Error'),
+                               translate('OpenLP.Manager', 'OpenLP cannot load your database.\n\nDatabase: %s')
+                               % db_path)
+
+
 def init_url(plugin_name, db_file_name=None):
     """
     Return the database URL.
@@ -69,13 +98,9 @@
     """
     settings = Settings()
     settings.beginGroup(plugin_name)
-    db_url = ''
     db_type = settings.value('db type')
     if db_type == 'sqlite':
-        if db_file_name is None:
-            db_url = 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name)
-        else:
-            db_url = 'sqlite:///%s/%s' % (AppLocation.get_section_data_path(plugin_name), db_file_name)
+        db_url = get_db_path(plugin_name, db_file_name)
     else:
         db_url = '%s://%s:%s@%s/%s' % (db_type, urlquote(settings.value('db username')),
                                        urlquote(settings.value('db password')),
@@ -212,7 +237,7 @@
             try:
                 db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
             except (SQLAlchemyError, DBAPIError):
-                log.exception('Error loading database: %s', self.db_url)
+                handle_db_error(plugin_name, db_file_name)
                 return
             if db_ver > up_ver:
                 critical_error_message_box(
@@ -225,10 +250,7 @@
         try:
             self.session = init_schema(self.db_url)
         except (SQLAlchemyError, DBAPIError):
-            log.exception('Error loading database: %s', self.db_url)
-            critical_error_message_box(translate('OpenLP.Manager', 'Database Error'),
-                                       translate('OpenLP.Manager', 'OpenLP cannot load your database.\n\nDatabase: %s')
-                                       % self.db_url)
+            handle_db_error(plugin_name, db_file_name)
 
     def save_object(self, object_instance, commit=True):
         """
@@ -362,6 +384,8 @@
         :param object_class: The type of objects to return.
         :param filter_clause: The filter governing selection of objects to return. Defaults to None.
         """
+        if not self.session:
+            return
         query = self.session.query(object_class)
         if filter_clause is not None:
             query = query.filter(filter_clause)

=== modified file 'openlp/core/ui/firsttimeform.py'
--- openlp/core/ui/firsttimeform.py	2015-01-20 21:38:34 +0000
+++ openlp/core/ui/firsttimeform.py	2015-02-08 18:45:17 +0000
@@ -22,6 +22,7 @@
 """
 This module contains the first time wizard.
 """
+import hashlib
 import logging
 import os
 import time
@@ -47,10 +48,10 @@
     """
     This thread downloads a theme's screenshot
     """
-    screenshot_downloaded = QtCore.pyqtSignal(str, str)
+    screenshot_downloaded = QtCore.pyqtSignal(str, str, str)
     finished = QtCore.pyqtSignal()
 
-    def __init__(self, themes_url, title, filename, screenshot):
+    def __init__(self, themes_url, title, filename, sha256, screenshot):
         """
         Set up the worker object
         """
@@ -58,6 +59,7 @@
         self.themes_url = themes_url
         self.title = title
         self.filename = filename
+        self.sha256 = sha256
         self.screenshot = screenshot
         super(ThemeScreenshotWorker, self).__init__()
 
@@ -71,7 +73,7 @@
             urllib.request.urlretrieve('%s%s' % (self.themes_url, self.screenshot),
                                        os.path.join(gettempdir(), 'openlp', self.screenshot))
             # Signal that the screenshot has been downloaded
-            self.screenshot_downloaded.emit(self.title, self.filename)
+            self.screenshot_downloaded.emit(self.title, self.filename, self.sha256)
         except:
             log.exception('Unable to download screenshot')
         finally:
@@ -221,8 +223,9 @@
                 self.application.process_events()
                 title = self.config.get('songs_%s' % song, 'title')
                 filename = self.config.get('songs_%s' % song, 'filename')
+                sha256 = self.config.get('songs_%s' % song, 'sha256', fallback='')
                 item = QtGui.QListWidgetItem(title, self.songs_list_widget)
-                item.setData(QtCore.Qt.UserRole, filename)
+                item.setData(QtCore.Qt.UserRole, (filename, sha256))
                 item.setCheckState(QtCore.Qt.Unchecked)
                 item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
             bible_languages = self.config.get('bibles', 'languages')
@@ -237,8 +240,9 @@
                     self.application.process_events()
                     title = self.config.get('bible_%s' % bible, 'title')
                     filename = self.config.get('bible_%s' % bible, 'filename')
+                    sha256 = self.config.get('bible_%s' % bible, 'sha256', fallback='')
                     item = QtGui.QTreeWidgetItem(lang_item, [title])
-                    item.setData(0, QtCore.Qt.UserRole, filename)
+                    item.setData(0, QtCore.Qt.UserRole, (filename, sha256))
                     item.setCheckState(0, QtCore.Qt.Unchecked)
                     item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
             self.bibles_tree_widget.expandAll()
@@ -249,8 +253,9 @@
                 self.application.process_events()
                 title = self.config.get('theme_%s' % theme, 'title')
                 filename = self.config.get('theme_%s' % theme, 'filename')
+                sha256 = self.config.get('theme_%s' % theme, 'sha256', fallback='')
                 screenshot = self.config.get('theme_%s' % theme, 'screenshot')
-                worker = ThemeScreenshotWorker(self.themes_url, title, filename, screenshot)
+                worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot)
                 self.theme_screenshot_workers.append(worker)
                 worker.screenshot_downloaded.connect(self.on_screenshot_downloaded)
                 thread = QtCore.QThread(self)
@@ -350,7 +355,7 @@
                 time.sleep(0.1)
         self.application.set_normal_cursor()
 
-    def on_screenshot_downloaded(self, title, filename):
+    def on_screenshot_downloaded(self, title, filename, sha256):
         """
         Add an item to the list when a theme has been downloaded
 
@@ -358,7 +363,7 @@
         :param filename: The filename of the theme
         """
         item = QtGui.QListWidgetItem(title, self.themes_list_widget)
-        item.setData(QtCore.Qt.UserRole, filename)
+        item.setData(QtCore.Qt.UserRole, (filename, sha256))
         item.setCheckState(QtCore.Qt.Unchecked)
         item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
 
@@ -372,7 +377,7 @@
         Settings().setValue('core/has run wizard', True)
         self.close()
 
-    def url_get_file(self, url, f_path):
+    def url_get_file(self, url, f_path, sha256=None):
         """"
         Download a file given a URL.  The file is retrieved in chunks, giving the ability to cancel the download at any
         point. Returns False on download error.
@@ -387,16 +392,24 @@
             try:
                 url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT)
                 filename = open(f_path, "wb")
+                if sha256:
+                    hasher = hashlib.sha256()
                 # Download until finished or canceled.
                 while not self.was_cancelled:
                     data = url_file.read(block_size)
                     if not data:
                         break
                     filename.write(data)
+                    if sha256:
+                        hasher.update(data)
                     block_count += 1
                     self._download_progress(block_count, block_size)
                 filename.close()
-            except ConnectionError:
+                if sha256 and hasher.hexdigest() != sha256:
+                    log.error('sha256 sums did not match for file: {}'.format(f_path))
+                    os.remove(f_path)
+                    return False
+            except urllib.error.URLError:
                 trace_error_handler(log)
                 filename.close()
                 os.remove(f_path)
@@ -436,7 +449,7 @@
                 site = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT)
                 meta = site.info()
                 return int(meta.get("Content-Length"))
-            except ConnectionException:
+            except urllib.error.URLError:
                 if retries > CONNECTION_RETRIES:
                     raise
                 else:
@@ -478,7 +491,7 @@
                 self.application.process_events()
                 item = self.songs_list_widget.item(i)
                 if item.checkState() == QtCore.Qt.Checked:
-                    filename = item.data(QtCore.Qt.UserRole)
+                    filename, sha256 = item.data(QtCore.Qt.UserRole)
                     size = self._get_file_size('%s%s' % (self.songs_url, filename))
                     self.max_progress += size
             # Loop through the Bibles list and increase for each selected item
@@ -487,7 +500,7 @@
                 self.application.process_events()
                 item = iterator.value()
                 if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
-                    filename = item.data(0, QtCore.Qt.UserRole)
+                    filename, sha256 = item.data(0, QtCore.Qt.UserRole)
                     size = self._get_file_size('%s%s' % (self.bibles_url, filename))
                     self.max_progress += size
                 iterator += 1
@@ -496,10 +509,10 @@
                 self.application.process_events()
                 item = self.themes_list_widget.item(i)
                 if item.checkState() == QtCore.Qt.Checked:
-                    filename = item.data(QtCore.Qt.UserRole)
+                    filename, sha256 = item.data(QtCore.Qt.UserRole)
                     size = self._get_file_size('%s%s' % (self.themes_url, filename))
                     self.max_progress += size
-        except ConnectionError:
+        except urllib.error.URLError:
             trace_error_handler(log)
             critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'),
                                        translate('OpenLP.FirstTimeWizard', 'There was a connection problem during '
@@ -595,31 +608,33 @@
         for i in range(self.songs_list_widget.count()):
             item = self.songs_list_widget.item(i)
             if item.checkState() == QtCore.Qt.Checked:
-                filename = item.data(QtCore.Qt.UserRole)
+                filename, sha256 = item.data(QtCore.Qt.UserRole)
                 self._increment_progress_bar(self.downloading % filename, 0)
                 self.previous_size = 0
                 destination = os.path.join(songs_destination, str(filename))
-                if not self.url_get_file('%s%s' % (self.songs_url, filename), destination):
+                if not self.url_get_file('%s%s' % (self.songs_url, filename), destination, sha256):
                     return False
         # Download Bibles
         bibles_iterator = QtGui.QTreeWidgetItemIterator(self.bibles_tree_widget)
         while bibles_iterator.value():
             item = bibles_iterator.value()
             if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
-                bible = item.data(0, QtCore.Qt.UserRole)
+                bible, sha256 = item.data(0, QtCore.Qt.UserRole)
                 self._increment_progress_bar(self.downloading % bible, 0)
                 self.previous_size = 0
-                if not self.url_get_file('%s%s' % (self.bibles_url, bible), os.path.join(bibles_destination, bible)):
+                if not self.url_get_file('%s%s' % (self.bibles_url, bible), os.path.join(bibles_destination, bible),
+                                         sha256):
                     return False
             bibles_iterator += 1
         # Download themes
         for i in range(self.themes_list_widget.count()):
             item = self.themes_list_widget.item(i)
             if item.checkState() == QtCore.Qt.Checked:
-                theme = item.data(QtCore.Qt.UserRole)
+                theme, sha256 = item.data(QtCore.Qt.UserRole)
                 self._increment_progress_bar(self.downloading % theme, 0)
                 self.previous_size = 0
-                if not self.url_get_file('%s%s' % (self.themes_url, theme), os.path.join(themes_destination, theme)):
+                if not self.url_get_file('%s%s' % (self.themes_url, theme), os.path.join(themes_destination, theme),
+                                         sha256):
                     return False
         return True
 

=== modified file 'openlp/plugins/bibles/lib/db.py'
--- openlp/plugins/bibles/lib/db.py	2015-01-18 13:39:21 +0000
+++ openlp/plugins/bibles/lib/db.py	2015-02-08 18:45:17 +0000
@@ -131,6 +131,7 @@
         log.info('BibleDB loaded')
         QtCore.QObject.__init__(self)
         self.bible_plugin = parent
+        self.session = None
         if 'path' not in kwargs:
             raise KeyError('Missing keyword argument "path".')
         if 'name' not in kwargs and 'file' not in kwargs:
@@ -144,8 +145,9 @@
         if 'file' in kwargs:
             self.file = kwargs['file']
         Manager.__init__(self, 'bibles', init_schema, self.file, upgrade)
-        if 'file' in kwargs:
-            self.get_name()
+        if self.session:
+            if 'file' in kwargs:
+                self.get_name()
         if 'path' in kwargs:
             self.path = kwargs['path']
         self.wizard = None

=== modified file 'openlp/plugins/bibles/lib/manager.py'
--- openlp/plugins/bibles/lib/manager.py	2015-01-18 13:39:21 +0000
+++ openlp/plugins/bibles/lib/manager.py	2015-02-08 18:45:17 +0000
@@ -121,6 +121,8 @@
         self.old_bible_databases = []
         for filename in files:
             bible = BibleDB(self.parent, path=self.path, file=filename)
+            if not bible.session:
+                continue
             name = bible.get_name()
             # Remove corrupted files.
             if name is None:

=== modified file 'tests/functional/test_init.py'
--- tests/functional/test_init.py	2015-01-26 20:42:19 +0000
+++ tests/functional/test_init.py	2015-02-08 18:45:17 +0000
@@ -125,7 +125,7 @@
         # WHEN: Calling parse_options
         results = parse_options(options)
 
-        # THEN: A tuple should be returned with the parsed options and left over args
+        # THEN: A tuple should be returned with the parsed options and left over options
         self.assertEqual(results, (Values({'no_error_form': True, 'dev_version': True, 'portable': True,
                                            'style': 'style', 'loglevel': 'debug'}), ['extra', 'qt', 'args']))
 
@@ -136,10 +136,51 @@
         # GIVEN: A list of valid options
         options = ['openlp.py', '-e', '-l', 'debug', '-pd', '-s', 'style', 'extra', 'qt', 'args']
 
-        # WHEN: Passing in the options through sys.argv and calling parse_args with None
+        # WHEN: Passing in the options through sys.argv and calling parse_options with None
         with patch.object(sys, 'argv', options):
             results = parse_options(None)
 
-        # THEN: parse_args should return a tuple of valid options and of left over options that OpenLP does not use
-        self.assertEqual(results, (Values({'no_error_form': True, 'dev_version': True, 'portable': True,
-                                           'style': 'style', 'loglevel': 'debug'}), ['extra', 'qt', 'args']))
+        # THEN: parse_options should return a tuple of valid options and of left over options that OpenLP does not use
+        self.assertEqual(results, (Values({'no_error_form': True, 'dev_version': True, 'portable': True,
+                                           'style': 'style', 'loglevel': 'debug'}), ['extra', 'qt', 'args']))
+
+    def test_parse_options_valid_long_options(self):
+        """
+        Test that parse_options parses valid long options correctly
+        """
+        # GIVEN: A list of valid options
+        options = ['--no-error-form', 'extra', '--log-level', 'debug', 'qt', '--portable', '--dev-version', 'args',
+                   '--style=style']
+
+        # WHEN: Calling parse_options
+        results = parse_options(options)
+
+        # THEN: parse_options should return a tuple of valid options and of left over options that OpenLP does not use
+        self.assertEqual(results, (Values({'no_error_form': True, 'dev_version': True, 'portable': True,
+                                           'style': 'style', 'loglevel': 'debug'}), ['extra', 'qt', 'args']))
+
+    def test_parse_options_help_option(self):
+        """
+        Test that parse_options raises an SystemExit exception when called with invalid options
+        """
+        # GIVEN: The --help option
+        options = ['--help']
+
+        # WHEN: Calling parse_options
+        # THEN: parse_options should raise an SystemExit exception with exception code 0
+        with self.assertRaises(SystemExit) as raised_exception:
+            parse_options(options)
+        self.assertEqual(raised_exception.exception.code, 0)
+
+    def test_parse_options_invalid_option(self):
+        """
+        Test that parse_options raises an SystemExit exception when called with invalid options
+        """
+        # GIVEN: A list including invalid options
+        options = ['-t']
+
+        # WHEN: Calling parse_options
+        # THEN: parse_options should raise an SystemExit exception with exception code 2
+        with self.assertRaises(SystemExit) as raised_exception:
+            parse_options(options)
+        self.assertEqual(raised_exception.exception.code, 2)


Follow ups