← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~tomasgroth/openlp/presentation-load-speedup into lp:openlp

 

Tomas Groth has proposed merging lp:~tomasgroth/openlp/presentation-load-speedup into lp:openlp.

Commit message:
WIP!
Fundamental change to how service files are storing data.

Requested reviews:
  OpenLP Core (openlp-core)
Related bugs:
  Bug #1677037 in OpenLP: "Powerpoint is slow to load"
  https://bugs.launchpad.net/openlp/+bug/1677037

For more details, see:
https://code.launchpad.net/~tomasgroth/openlp/presentation-load-speedup/+merge/367933

WIP!
Fundamental change to how service files are storing data.
When we add a file to a service file, instead of adding it with its name,
we should rename it so its name reflects the sha256 checksum of the
filecontent. So is a file is called "church-presentation.ppt" it is saved
as "505911856c191c5a7c4a0dc4858a8627e21b7171a1d9189bd8bb39090eac982f.ppt",
and no path/folders will be included. The new filename will also be added
to the service-item data. Doing this will avoid any problems with paths,
and if any files has the same hash it will be because they are the same, so
no duplicate content.
Avoiding folders from included files also allow us to use folders for
something else, namely thumbnails (for presentations). So reusing the the
example above, we could create a folder in the servicefile named
"505911856c191c5a7c4a0dc4858a8627e21b7171a1d9189bd8bb39090eac982f" which
contains all the thumbnails (and slide notes and titles), which can then be
reused when loading the servicefile, thereby skipping the step of
recreating the thumbnails (and thereby fixing bug #1677037).
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~tomasgroth/openlp/presentation-load-speedup into lp:openlp.
=== modified file 'openlp/core/common/__init__.py'
--- openlp/core/common/__init__.py	2019-05-22 06:47:00 +0000
+++ openlp/core/common/__init__.py	2019-05-25 20:21:21 +0000
@@ -276,6 +276,21 @@
     return hash_value
 
 
+def sha256_file_hash(filename):
+    """
+    Returns the hashed output of sha256 on the file content using Python3 hashlib
+
+    :param filename: Name of the file to hash
+    :returns: str
+    """
+    log.debug('sha256_hash(filename="{filename}")'.format(filename=filename))
+    hash_obj = hashlib.sha256()
+    with open(filename, 'rb') as f:
+        for chunk in iter(lambda: f.read(65536), b''):
+            hash_obj.update(chunk)
+    return hash_obj.hexdigest()
+
+
 def qmd5_hash(salt=None, data=None):
     """
     Returns the hashed output of MD5Sum on salt, data

=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py	2019-05-22 06:47:00 +0000
+++ openlp/core/lib/serviceitem.py	2019-05-25 20:21:21 +0000
@@ -34,7 +34,7 @@
 from PyQt5 import QtGui
 
 from openlp.core.state import State
-from openlp.core.common import md5_hash
+from openlp.core.common import sha256_file_hash
 from openlp.core.common.applocation import AppLocation
 from openlp.core.common.i18n import translate
 from openlp.core.common.mixins import RegistryProperties
@@ -112,6 +112,7 @@
         self.timed_slide_interval = 0
         self.will_auto_start = False
         self.has_original_files = True
+        self.sha256_file_hash = None
         self._new_item()
         self.metadata = []
 
@@ -209,7 +210,7 @@
             self._create_slides()
         return self._display_slides
 
-    def add_from_image(self, path, title, background=None, thumbnail=None):
+    def add_from_image(self, path, title, background=None, thumbnail=None, file_hash=None):
         """
         Add an image slide to the service item.
 
@@ -226,6 +227,10 @@
             slide['thumbnail'] = thumbnail
         self.slides.append(slide)
         # self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border)
+        if file_hash:
+            self.sha256_file_hash = file_hash
+        else:
+            self.sha256_file_hash = sha256_file_hash(path)
         self._new_item()
 
     def add_from_text(self, text, verse_tag=None):
@@ -245,7 +250,7 @@
         self.slides.append({'title': title, 'text': text, 'verse': verse_tag})
         self._new_item()
 
-    def add_from_command(self, path, file_name, image, display_title=None, notes=None):
+    def add_from_command(self, path, file_name, image, display_title=None, notes=None, file_hash=None):
         """
         Add a slide from a command.
 
@@ -254,18 +259,22 @@
         :param image: The command of/for the slide.
         :param display_title: Title to show in gui/webinterface, optional.
         :param notes: Notes to show in the webinteface, optional.
+        :param file_hash: Sha256 hash checksum of the file.
         """
         self.service_item_type = ServiceItemType.Command
         # If the item should have a display title but this frame doesn't have one, we make one up
         if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title:
             display_title = translate('OpenLP.ServiceItem',
                                       '[slide {frame:d}]').format(frame=len(self.slides) + 1)
+        if file_hash:
+            self.sha256_file_hash = file_hash
+        else:
+            file_location = os.path.join(path, file_name)
+            self.sha256_file_hash = sha256_file_hash(file_location)
         # Update image path to match servicemanager location if file was loaded from service
         if image and not self.has_original_files and self.name == 'presentations':
-            file_location = os.path.join(path, file_name)
-            file_location_hash = md5_hash(file_location.encode('utf-8'))
-            image = os.path.join(AppLocation.get_section_data_path(self.name), 'thumbnails', file_location_hash,
-                                 ntpath.basename(image))  # TODO: Pathlib
+            image = os.path.join(str(AppLocation.get_section_data_path(self.name)), 'thumbnails',
+                                 self.sha256_file__hash, ntpath.basename(image))
         self.slides.append({'title': file_name, 'image': image, 'path': path, 'display_title': display_title,
                             'notes': notes, 'thumbnail': image})
         # if self.is_capable(ItemCapabilities.HasThumbnails):
@@ -300,7 +309,8 @@
             'theme_overwritten': self.theme_overwritten,
             'will_auto_start': self.will_auto_start,
             'processor': self.processor,
-            'metadata': self.metadata
+            'metadata': self.metadata,
+            'sha256_file_hash': self.sha256_file_hash
         }
         service_data = []
         if self.service_item_type == ServiceItemType.Text:
@@ -317,6 +327,7 @@
                 service_data = [slide['title'] for slide in self.slides]
         elif self.service_item_type == ServiceItemType.Command:
             for slide in self.slides:
+                image_path = os.path.relpath(slide['image'], AppLocation().get_data_path())
                 service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path'],
                                      'display_title': slide['display_title'], 'notes': slide['notes']})
         return {'header': service_header, 'data': service_data}
@@ -356,6 +367,7 @@
         self.processor = header.get('processor', None)
         self.has_original_files = True
         self.metadata = header.get('item_meta_data', [])
+        self.sha256_file_hash = header.get('sha256_file_hash', None)
         if 'background_audio' in header and State().check_preconditions('media'):
             self.background_audio = []
             for file_path in header['background_audio']:
@@ -377,10 +389,11 @@
                 self.has_original_files = False
                 for text_image in service_item['serviceitem']['data']:
                     file_path = path / text_image
-                    self.add_from_image(file_path, text_image, background)
+                    self.add_from_image(file_path, text_image, background, file_hash=self.sha256_file_hash)
             else:
                 for text_image in service_item['serviceitem']['data']:
-                    self.add_from_image(text_image['path'], text_image['title'], background)
+                    self.add_from_image(text_image['path'], text_image['title'], background,
+                                        file_hash=self.sha256_file_hash)
         elif self.service_item_type == ServiceItemType.Command:
             for text_image in service_item['serviceitem']['data']:
                 if not self.title:
@@ -391,9 +404,11 @@
                 elif path:
                     self.has_original_files = False
                     self.add_from_command(path, text_image['title'], text_image['image'],
-                                          text_image.get('display_title', ''), text_image.get('notes', ''))
+                                          text_image.get('display_title', ''), text_image.get('notes', ''),
+                                          file_hash=self.sha256_file_hash)
                 else:
-                    self.add_from_command(Path(text_image['path']), text_image['title'], text_image['image'])
+                    self.add_from_command(Path(text_image['path']), text_image['title'], text_image['image'],
+                                          file_hash=self.sha256_file_hash)
         self._new_item()
 
     def get_display_title(self):
@@ -617,3 +632,12 @@
                         if file_suffix.lower() not in suffix_list:
                             self.is_valid = False
                             break
+
+    def get_thumbnail_path(self):
+        """
+        Returns the thumbnail folder. Should only be used for presentations/commands.
+        """
+        if self.is_command() and self._raw_frames:
+            return os.path.dirname(self._raw_frames[0]['image'])
+        else:
+            return None

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2019-05-24 18:50:51 +0000
+++ openlp/core/ui/servicemanager.py	2019-05-25 20:21:21 +0000
@@ -34,7 +34,7 @@
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
-from openlp.core.common import ThemeLevel, delete_file
+from openlp.core.common import ThemeLevel, delete_file, sha256_file_hash
 from openlp.core.common.actions import ActionList, CategoryOrder
 from openlp.core.common.applocation import AppLocation
 from openlp.core.common.i18n import UiStrings, format_time, translate
@@ -328,6 +328,7 @@
         self._service_path = None
         self.service_has_all_original_files = True
         self.list_double_clicked = False
+        self.servicefile_version = None
 
     def bootstrap_initialise(self):
         """
@@ -509,7 +510,8 @@
         service = []
         core = {
             'lite-service': self._save_lite,
-            'service-theme': self.service_theme
+            'service-theme': self.service_theme,
+            'openlp-servicefile-version': 3.0
         }
         service.append({'openlp_core': core})
         return service
@@ -527,16 +529,33 @@
             if item['service_item'].uses_file():
                 for frame in item['service_item'].get_frames():
                     path_from = item['service_item'].get_frame_path(frame=frame)
-                    if path_from in write_list or path_from in missing_list:
+                    path_from_path = Path(path_from)
+                    if item['service_item'].sha256_file_hash:
+                        sha256_file_name = item['service_item'].sha256_file_hash + os.path.splitext(path_from)[1]
+                    else:
+                        sha256_file_name = sha256_file_hash(path_from_path) + os.path.splitext(path_from)[1]
+                    path_from_tuple = (path_from_path, sha256_file_name)
+                    if path_from_tuple in write_list or path_from_path in missing_list:
                         continue
                     if not os.path.exists(path_from):
-                        missing_list.append(Path(path_from))
+                        missing_list.append(path_from_path)
                     else:
-                        write_list.append(Path(path_from))
+                        write_list.append(path_from_tuple)
+                # For presentations also store thumbnails and notes
+                if item['service_item'].is_command() and not item['service_item'].is_media():
+                    thumbnail_path = item['service_item'].get_thumbnail_path()
+                    # Run through everything in the thumbnail folder and add it
+                    thumbnail_path_parent = Path(thumbnail_path).parent
+                    for filename in os.listdir(thumbnail_path):
+                        filename_path = Path(thumbnail_path) / Path(filename)
+                        # Create a thumbnail path in the zip/service file
+                        service_path = filename_path.relative_to(thumbnail_path_parent)
+                        write_list.append((filename_path, service_path))
             for audio_path in item['service_item'].background_audio:
-                if audio_path in write_list:
+                audio_path_tuple = (audio_path, audio_path)
+                if audio_path_tuple in write_list:
                     continue
-                write_list.append(audio_path)
+                write_list.append(audio_path_tuple)
         return write_list, missing_list
 
     def save_file(self):
@@ -585,8 +604,8 @@
         service_content = json.dumps(service, cls=OpenLPJSONEncoder)
         service_content_size = len(bytes(service_content, encoding='utf-8'))
         total_size = service_content_size
-        for file_item in write_list:
-            total_size += file_item.stat().st_size
+        for local_file_item, zip_file_item in write_list:
+            total_size += local_file_item.stat().st_size
         self.log_debug('ServiceManager.save_file - ZIP contents size is %i bytes' % total_size)
         self.main_window.display_progress_bar(total_size)
         try:
@@ -596,9 +615,9 @@
                 zip_file.writestr('service_data.osj', service_content)
                 self.main_window.increment_progress_bar(service_content_size)
                 # Finally add all the listed media files.
-                for write_path in write_list:
-                    zip_file.write(write_path, write_path)
-                    self.main_window.increment_progress_bar(write_path.stat().st_size)
+                for local_file_item, zip_file_item in write_list:
+                    zip_file.write(str(local_file_item), str(zip_file_item))
+                    self.main_window.increment_progress_bar(local_file_item.stat().st_size)
                 with suppress(FileNotFoundError):
                     file_path.unlink()
                 os.link(temp_file.name, file_path)
@@ -687,11 +706,30 @@
         service_data = None
         self.application.set_busy_cursor()
         try:
-            with zipfile.ZipFile(file_path) as zip_file:
+            # TODO: figure out a way to use the presentation thumbnails in the service file
+            with zipfile.ZipFile(str(file_path)) as zip_file:
                 compressed_size = 0
                 for zip_info in zip_file.infolist():
                     compressed_size += zip_info.compress_size
                 self.main_window.display_progress_bar(compressed_size)
+                # First find the osj-file to find out how to handle the file
+                for zip_info in zip_file.infolist():
+                    # The json file has been called 'service_data.osj' since OpenLP 3.0
+                    if zip_info.filename == 'service_data.osj' or zip_info.filename.endswith('osj'):
+                        with zip_file.open(zip_info, 'r') as json_file:
+                            service_data = json_file.read()
+                        break
+                if service_data:
+                    items = json.loads(service_data, cls=OpenLPJSONDecoder)
+                else:
+                    raise ValidationError(msg='No service data found')
+                # Extract the service file version
+                for item in items:
+                    if 'openlp_core' in item:
+                        item = item['openlp_core']
+                        self.servicefile_version = item.get('openlp-servicefile-version', 2.0)
+                        break
+                print('service format version: %d' % self.servicefile_version)
                 for zip_info in zip_file.infolist():
                     self.log_debug('Extract file: {name}'.format(name=zip_info.filename))
                     # The json file has been called 'service_data.osj' since OpenLP 3.0
@@ -699,11 +737,13 @@
                         with zip_file.open(zip_info, 'r') as json_file:
                             service_data = json_file.read()
                     else:
-                        zip_info.filename = os.path.basename(zip_info.filename)
-                        zip_file.extract(zip_info, self.service_path)
+                        # Service files from earlier versions than 3.0 expects that all files are extracted
+                        # into the root of the service folder.
+                        if self.servicefile_version and self.servicefile_version < 3.0:
+                            zip_info.filename = os.path.basename(zip_info.filename.replace('/', os.path.sep))
+                        zip_file.extract(zip_info, str(self.service_path))
                     self.main_window.increment_progress_bar(zip_info.compress_size)
-            if service_data:
-                items = json.loads(service_data, cls=OpenLPJSONDecoder)
+                # Handle the content
                 self.new_file()
                 self.process_service_items(items)
                 self.set_file_name(file_path)
@@ -1247,11 +1287,12 @@
         """
         Empties the service_path of temporary files on system exit.
         """
-        for file_path in self.service_path.iterdir():
-            delete_file(file_path)
-        audio_path = self.service_path / 'audio'
-        if audio_path.exists():
-            shutil.rmtree(audio_path, True)
+        for file_name in os.listdir(self.service_path):
+            file_path = Path(self.service_path, file_name)
+            if os.path.isdir(file_path):
+                shutil.rmtree(file_path, True)
+            else:
+                delete_file(file_path)
 
     def on_theme_combo_box_selected(self, current_index):
         """

=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py	2019-05-22 06:47:00 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2019-05-25 20:21:21 +0000
@@ -356,7 +356,8 @@
                             note = ''
                             if notes and len(notes) >= i:
                                 note = notes[i - 1]
-                            service_item.add_from_command(str(path), file_name, str(thumbnail_path), title, note)
+                            service_item.add_from_command(str(path), file_name, str(thumbnail_path), title, note,
+                                                          doc.get_sha256_file_hash())
                             i += 1
                             thumbnail_path = doc.get_thumbnail_path(i, True)
                         doc.close_presentation()

=== modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
--- openlp/plugins/presentations/lib/presentationcontroller.py	2019-05-22 06:47:00 +0000
+++ openlp/plugins/presentations/lib/presentationcontroller.py	2019-05-25 20:21:21 +0000
@@ -25,12 +25,12 @@
 
 from PyQt5 import QtCore
 
-from openlp.core.common import md5_hash
+from openlp.core.common import md5_hash, sha256_file_hash
 from openlp.core.common.applocation import AppLocation
 from openlp.core.common.path import create_paths
 from openlp.core.common.registry import Registry
 from openlp.core.common.settings import Settings
-from openlp.core.lib import create_thumb, validate_thumb
+from openlp.core.lib import create_thumb
 
 
 log = logging.getLogger(__name__)
@@ -98,6 +98,7 @@
         :rtype: None
         """
         self.controller = controller
+        self._sha256_file_hash = None
         self._setup(document_path)
 
     def _setup(self, document_path):
@@ -146,6 +147,12 @@
         #       get_temp_folder and PresentationPluginapp_startup is removed
         if Settings().value('presentations/thumbnail_scheme') == 'md5':
             folder = md5_hash(bytes(self.file_path))
+        elif Settings().value('presentations/thumbnail_scheme') == 'sha256file':
+            if self._sha256_file_hash:
+                folder = self._sha256_file_hash
+            else:
+                self._sha256_file_hash = sha256_file_hash(self.file_path)
+                folder = self._sha256_file_hash
         else:
             folder = self.file_path.name
         return Path(self.controller.thumbnail_folder, folder)
@@ -161,13 +168,20 @@
         #       get_thumbnail_folder and PresentationPluginapp_startup is removed
         if Settings().value('presentations/thumbnail_scheme') == 'md5':
             folder = md5_hash(bytes(self.file_path))
+        elif Settings().value('presentations/thumbnail_scheme') == 'sha256file':
+            if self._sha256_file_hash:
+                folder = self._sha256_file_hash
+            else:
+                self._sha256_file_hash = sha256_file_hash(self.file_path)
+                folder = self._sha256_file_hash
         else:
             folder = self.file_path.name
         return Path(self.controller.temp_folder, folder)
 
     def check_thumbnails(self):
         """
-        Check that the last thumbnail image exists and is valid and are more recent than the powerpoint file.
+        Check that the last thumbnail image exists and is valid. It is not checked if presentation file is newer than
+        thumbnail since the path is based on the file hash, so if it exists it is by definition up to date.
 
         :return: If the thumbnail is valid
         :rtype: bool
@@ -175,7 +189,7 @@
         last_image_path = self.get_thumbnail_path(self.get_slide_count(), True)
         if not (last_image_path and last_image_path.is_file()):
             return False
-        return validate_thumb(Path(self.file_path), Path(last_image_path))
+        return True
 
     def close_presentation(self):
         """
@@ -358,6 +372,17 @@
                 notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)
                 notes_path.write_text(note)
 
+    def get_sha256_file_hash(self):
+        """
+        Returns the sha256 file hash for the file.
+
+        :return: The sha256 file hash
+        :rtype: str
+        """
+        if not self._sha256_file_hash:
+            self._sha256_file_hash = sha256_file_hash(self.file_path)
+        return self._sha256_file_hash
+
 
 class PresentationController(object):
     """

=== modified file 'openlp/plugins/presentations/presentationplugin.py'
--- openlp/plugins/presentations/presentationplugin.py	2019-04-13 13:00:22 +0000
+++ openlp/plugins/presentations/presentationplugin.py	2019-05-25 20:21:21 +0000
@@ -156,7 +156,7 @@
         for path in presentation_paths:
             self.media_item.clean_up_thumbnails(path, clean_for_update=True)
         self.media_item.list_view.clear()
-        Settings().setValue('presentations/thumbnail_scheme', 'md5')
+        Settings().setValue('presentations/thumbnail_scheme', 'sha256file')
         self.media_item.validate_and_load(presentation_paths)
 
     @staticmethod

=== modified file 'tests/functional/openlp_core/lib/test_serviceitem.py'
--- tests/functional/openlp_core/lib/test_serviceitem.py	2019-05-22 06:47:00 +0000
+++ tests/functional/openlp_core/lib/test_serviceitem.py	2019-05-25 20:21:21 +0000
@@ -135,7 +135,8 @@
         assert 'Slide 2' == service_item.get_frame_title(1), '"Slide 2" has been returned as the title'
         assert '' == service_item.get_frame_title(2), 'Blank has been returned as the title of slide 3'
 
-    def test_service_item_load_image_from_service(self):
+    @patch('openlp.core.lib.serviceitem.sha256_file_hash')
+    def test_service_item_load_image_from_service(self, mocked_sha256_file_hash):
         """
         Test the Service Item - adding an image from a saved service
         """
@@ -174,9 +175,11 @@
         assert service_item.is_capable(ItemCapabilities.CanAppend) is True, \
             'This service item should be able to have new items added to it'
 
+    @patch('openlp.core.lib.serviceitem.sha256_file_hash')
     @patch('openlp.core.lib.serviceitem.os.path.exists')
     @patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path')
-    def test_service_item_load_image_from_local_service(self, mocked_get_section_data_path, mocked_exists):
+    def test_service_item_load_image_from_local_service(self, mocked_get_section_data_path, mocked_exists,
+                                                        mocked_sha256_file_hash):
         """
         Test the Service Item - adding an image from a saved local service
         """
@@ -229,7 +232,8 @@
         assert service_item.is_capable(ItemCapabilities.CanAppend) is True, \
             'This service item should be able to have new items added to it'
 
-    def test_add_from_command_for_a_presentation(self):
+    @patch('openlp.core.lib.serviceitem.sha256_file_hash')
+    def test_add_from_command_for_a_presentation(self, mocked_sha256_file_hash):
         """
         Test the Service Item - adding a presentation
         """
@@ -249,7 +253,8 @@
         assert service_item.service_item_type == ServiceItemType.Command, 'It should be a Command'
         assert service_item.get_frames()[0] == frame, 'Frames should match'
 
-    def test_add_from_command_without_display_title_and_notes(self):
+    @patch('openlp.core.lib.serviceitem.sha256_file_hash')
+    def test_add_from_comamnd_without_display_title_and_notes(self, mocked_sha256_file_hash):
         """
         Test the Service Item - add from command, but not presentation
         """
@@ -267,13 +272,16 @@
         assert service_item.service_item_type == ServiceItemType.Command, 'It should be a Command'
         assert service_item.get_frames()[0] == frame, 'Frames should match'
 
-    @patch(u'openlp.core.lib.serviceitem.ServiceItem.image_manager')
+    @patch('openlp.core.lib.serviceitem.ServiceItem.image_manager')
     @patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path')
-    def test_add_from_command_for_a_presentation_thumb(self, mocked_get_section_data_path, mocked_image_manager):
+    @patch('openlp.core.lib.serviceitem.sha256_file_hash')
+    def test_add_from_command_for_a_presentation_thumb(self, mocked_sha256_file_hash, mocked_get_section_data_path,
+                                                       mocked_image_manager):
         """
         Test the Service Item - adding a presentation, updating the thumb path & adding the thumb to image_manager
         """
         # GIVEN: A service item, a mocked AppLocation and presentation data
+        mocked_sha256_file_hash.return_value = 'fake_file_hash'
         mocked_get_section_data_path.return_value = Path('mocked') / 'section' / 'path'
         service_item = ServiceItem(None)
         service_item.add_capability(ItemCapabilities.HasThumbnails)
@@ -283,8 +291,7 @@
         thumb = Path('tmp') / 'test' / 'thumb.png'
         display_title = 'DisplayTitle'
         notes = 'Note1\nNote2\n'
-        expected_thumb_path = Path('mocked') / 'section' / 'path' / 'thumbnails' / \
-            md5_hash(str(TEST_PATH / presentation_name).encode('utf8')) / 'thumb.png'
+        expected_thumb_path = Path('mocked') / 'section' / 'path' / 'thumbnails' / 'fake_file_hash' / 'thumb.png'
         frame = {'title': presentation_name, 'image': str(expected_thumb_path), 'path': str(TEST_PATH),
                  'display_title': display_title, 'notes': notes, 'thumbnail': str(expected_thumb_path)}
 
@@ -296,7 +303,8 @@
         assert service_item.get_frames()[0] == frame, 'Frames should match'
         # assert 1 == mocked_image_manager.add_image.call_count, 'image_manager should be used'
 
-    def test_service_item_load_optical_media_from_service(self):
+    @patch('openlp.core.lib.serviceitem.sha256_file_hash')
+    def test_service_item_load_optical_media_from_service(self, mocked_sha256_file_hash):
         """
         Test the Service Item - load an optical media item
         """

=== modified file 'tests/interfaces/openlp_core/widgets/test_views.py'
--- tests/interfaces/openlp_core/widgets/test_views.py	2019-04-13 13:00:22 +0000
+++ tests/interfaces/openlp_core/widgets/test_views.py	2019-05-25 20:21:21 +0000
@@ -79,7 +79,8 @@
         # THEN: The number of the current item should be -1.
         assert self.preview_widget.current_slide_number() == -1, 'The slide number should be -1.'
 
-    def test_replace_service_item(self):
+    @patch('openlp.core.lib.serviceitem.sha256_file_hash')
+    def test_replace_service_item(self, mocked_sha256_file_hash):
         """
         Test item counts and current number with a service item.
         """
@@ -94,7 +95,8 @@
         assert self.preview_widget.slide_count() == 2, 'The slide count should be 2.'
         assert self.preview_widget.current_slide_number() == 1, 'The current slide number should  be 1.'
 
-    def test_change_slide(self):
+    @patch('openlp.core.lib.serviceitem.sha256_file_hash')
+    def test_change_slide(self, mocked_sha256_file_hash):
         """
         Test the change_slide method.
         """


Follow ups