Merge branch 'master' into new-theme-editor [skip ci]

This commit is contained in:
Mateus Meyer Jiacomelli 2023-01-21 14:53:02 -03:00
commit 4dc0af824e
79 changed files with 20578 additions and 19700 deletions

View File

@ -13,11 +13,13 @@ environment:
CHOCO_VLC_ARG:
FORCE_PACKAGING: 0
FORCE_PACKAGING_MANUAL: 0
PYICU_PACK: PyICU-2.9-cp38-cp38-win_amd64.whl
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
PY_DIR: C:\\Python38
CHOCO_VLC_ARG: --forcex86
FORCE_PACKAGING: 0
FORCE_PACKAGING_MANUAL: 0
PYICU_PACK: PyICU-2.9-cp38-cp38-win32.whl
- APPVEYOR_BUILD_WORKER_IMAGE: macos-catalina
QT_QPA_PLATFORM: offscreen
FORCE_PACKAGING: 0
@ -36,9 +38,13 @@ install:
# Install Windows only dependencies
- cmd: python -m pip install pyodbc pypiwin32
- cmd: choco install vlc %CHOCO_VLC_ARG% --no-progress --limit-output
# Download and install pyicu for windows (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/)
- cmd: python -m pip install https://get.openlp.org/win-sdk/%PYICU_PACK%
# Mac only dependencies
- sh: python -m pip install Pyro4 'pyobjc-core<8.2' 'pyobjc-framework-Cocoa<8.2' py-applescript
- sh: brew install --cask vlc
- sh: brew install pkg-config icu4c
- sh: PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/icu4c/lib/pkgconfig" python -m pip install pyicu
- sh: python -m pip install Pyro4 'pyobjc-core<8.2' 'pyobjc-framework-Cocoa<8.2' py-applescript
build: off

View File

@ -1 +1 @@
3.0.0
3.0.1

View File

@ -22,7 +22,7 @@
from openlp.core.api.versions.v2.controller import controller_views
from openlp.core.api.versions.v2.core import core
from openlp.core.api.versions.v2.service import service_views
from openlp.core.api.versions.v2.plugins import plugins
from openlp.core.api.versions.v2.plugins import plugins, alert_1_views, alert_2_views
def register_blueprints(app):
@ -30,3 +30,5 @@ def register_blueprints(app):
app.register_blueprint(core, url_prefix='/api/v2/core/')
app.register_blueprint(service_views, url_prefix='/api/v2/service/')
app.register_blueprint(plugins, url_prefix='/api/v2/plugins/')
app.register_blueprint(alert_2_views, url_prefix='/api/v2/plugins/alerts')
app.register_blueprint(alert_1_views, url_prefix='/api/alert')

View File

@ -24,7 +24,7 @@ import logging
import re
from flask import abort, request, Blueprint, jsonify
from openlp.core.api.lib import login_required
from openlp.core.api.lib import login_required, extract_request, old_success_response, old_auth
from openlp.core.lib.plugin import PluginStatus
from openlp.core.common.registry import Registry
from openlp.plugins.songs.lib import transpose_lyrics
@ -33,6 +33,8 @@ log = logging.getLogger(__name__)
plugins = Blueprint('v2-plugins', __name__)
alert_1_views = Blueprint('v1-alert-plugin', __name__)
alert_2_views = Blueprint('v2-alert-plugin', __name__)
def search(plugin_name, text):
@ -172,3 +174,27 @@ def transpose(transpose_value):
chord_slides.append({'chords': verse_list[i + 1].strip(), 'verse': verse_list[i]})
return jsonify(chord_slides), 200
abort(400)
@alert_2_views.route('', methods=['POST'])
@login_required
def alert():
data = request.json
if not data:
abort(400)
alert = data.get('text', '')
if alert:
if Registry().get('plugin_manager').get_plugin_by_name('alerts').status == PluginStatus.Active:
Registry().get('alerts_manager').alerts_text.emit([alert])
return '', 204
abort(400)
@alert_1_views.route('')
@old_auth
def old_alert():
alert = extract_request(request.args.get('data', ''), 'text')
if alert:
if Registry().get('plugin_manager').get_plugin_by_name('alerts').status == PluginStatus.Active:
Registry().get('alerts_manager').alerts_text.emit([alert])
return old_success_response()

View File

@ -205,6 +205,8 @@ class WebSocketServer(RegistryBase, RegistryProperties, QtCore.QObject, LogMixin
"""
Closes the WebSocket server and detach associated signals
"""
if Registry().get_flag('no_web_server'):
return
try:
poller.poller_changed.disconnect(self.handle_poller_signal)
poller.unhook_signals()

View File

@ -100,6 +100,25 @@ def trace_error_handler(logger):
logger.error(log_string)
def path_to_module(path):
"""
Convert a path to a module name (i.e openlp.core.common)
:param pathlib.Path path: The path to convert to a module name.
:return: The module name.
:rtype: str
"""
module_path = path.with_suffix('')
return 'openlp.' + '.'.join(module_path.parts)
def import_openlp_module(module_name):
"""
Refactor module import out for testability. In Python 3.11, mock.patch and import_module do not play along nicely.
"""
importlib.import_module(module_name)
def extension_loader(glob_pattern, excluded_files=None):
"""
A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and
@ -119,25 +138,13 @@ def extension_loader(glob_pattern, excluded_files=None):
log.debug('Attempting to import %s', extension_path)
module_name = path_to_module(extension_path)
try:
importlib.import_module(module_name)
import_openlp_module(module_name)
except (ImportError, OSError):
# On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X)
log.exception('Failed to import {module_name} on path {extension_path}'
.format(module_name=module_name, extension_path=extension_path))
def path_to_module(path):
"""
Convert a path to a module name (i.e openlp.core.common)
:param pathlib.Path path: The path to convert to a module name.
:return: The module name.
:rtype: str
"""
module_path = path.with_suffix('')
return 'openlp.' + '.'.join(module_path.parts)
def get_frozen_path(frozen_option, non_frozen_option):
"""
Return a path based on the system status.

View File

@ -112,6 +112,15 @@ class ServiceItemType(IntEnum):
Image = 2
Command = 3
@staticmethod
def parse(value):
if value in [1, '1', 'Text', 'ServiceItemType.Text']:
return ServiceItemType.Text
elif value in [2, '2', 'Image', 'ServiceItemType.Image']:
return ServiceItemType.Image
elif value in [3, '3', 'Command', 'ServiceItemType.Command']:
return ServiceItemType.Command
@unique
class PluginStatus(IntEnum):

View File

@ -22,6 +22,7 @@
The :mod:`languages` module provides a list of language names with utility functions.
"""
import itertools
import locale
import logging
import re
from collections import namedtuple
@ -51,7 +52,8 @@ def translate(context, text, comment=None, qt_translate=QtCore.QCoreApplication.
Language = namedtuple('Language', ['id', 'name', 'code'])
COLLATOR = None
ICU_COLLATOR = None
DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+')
LANGUAGES = sorted([
Language(1, translate('common.languages', '(Afan) Oromo', 'Language code: om'), 'om'),
Language(2, translate('common.languages', 'Abkhazian', 'Language code: ab'), 'ab'),
@ -503,19 +505,25 @@ def format_time(text, local_time):
return re.sub(r'%[a-zA-Z]', match_formatting, text)
def get_locale_key(string, numeric=False):
def get_locale_key(string):
"""
Creates a key for case insensitive, locale aware string sorting.
:param string: The corresponding string.
"""
string = string.lower()
global COLLATOR
if COLLATOR is None:
language = LanguageManager.get_language()
COLLATOR = QtCore.QCollator(QtCore.QLocale(language))
COLLATOR.setNumericMode(numeric)
return COLLATOR.sortKey(string)
# ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases.
global ICU_COLLATOR
try:
if ICU_COLLATOR is None:
import icu
language = LanguageManager.get_language()
icu_locale = icu.Locale(language)
ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
return ICU_COLLATOR.getSortKey(string)
except Exception:
log.warning('ICU not found! Fallback to strxfrm')
return locale.strxfrm(string).encode()
def get_natural_key(string):
@ -525,7 +533,13 @@ def get_natural_key(string):
:param string: string to be sorted by
Returns a list of string compare keys and integers.
"""
return get_locale_key(string, True)
key = DIGITS_OR_NONDIGITS.findall(string)
key = [int(part) if part.isdigit() else get_locale_key(part) for part in key]
# Python 3 does not support comparison of different types anymore. So make sure, that we do not compare str
# and int.
if string and string[0].isdigit():
return [b''] + key
return key
def get_language(name):

View File

@ -30,7 +30,7 @@ from openlp.core.common.registry import Registry
DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed',
'preview_size_changed', 'resizeEvent', 'eventFilter', 'tick']
'preview_size_changed', 'resizeEvent', 'eventFilter', 'tick', 'resize', 'update_ui']
class LogMixin(object):

View File

@ -291,6 +291,8 @@ class Settings(QtCore.QSettings):
'media/vlc arguments': '',
'media/live volume': 50,
'media/preview volume': 0,
'media/live loop': False,
'media/preview loop': False,
'media/db type': 'sqlite',
'media/db username': '',
'media/db password': '',

View File

@ -474,7 +474,7 @@ var Display = {
img.setAttribute("style", "height: 100%; width: 100%");
section.appendChild(img);
Display._slides['0'] = 0;
Display.replaceSlides(parentSection);
Display.replaceSlides(section);
},
/**
* Set fullscreen image from base64 data
@ -932,7 +932,7 @@ var Display = {
/*
Disabling all transitions (except body) to allow the Webview to attain the
transparent state before it gets hidden by Qt.
*/
*/
document.body.classList.add('disable-transitions');
document.body.classList.add('is-desktop');
Display._slidesContainer.style.opacity = 0;

View File

@ -463,7 +463,7 @@ class ServiceItem(RegistryProperties):
header = service_item['serviceitem']['header']
self.title = header['title']
self.name = header['name']
self.service_item_type = header['type']
self.service_item_type = ServiceItemType.parse(header['type'])
self.theme = header['theme']
self.add_icon()
self.raw_footer = header['footer']
@ -891,7 +891,7 @@ class ServiceItem(RegistryProperties):
data_dict = {
'title': self.title,
'name': self.name,
'type': str(self.service_item_type),
'type': self.service_item_type,
'theme': self.theme,
'footer': self.raw_footer,
'audit': self.audit,

View File

@ -22,6 +22,7 @@
This is the main window, where all the action happens.
"""
import shutil
from contextlib import contextmanager
from datetime import datetime, date
from pathlib import Path
from tempfile import gettempdir
@ -529,20 +530,32 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
self.ws_server = WebSocketServer()
self.screen_updating_lock = Lock()
@contextmanager
def _show_wait_dialog(self, title, message):
"""
Show a wait dialog, wait for some tasks to complete, and then close it.
"""
try:
# Display a progress dialog with a message
wait_dialog = QtWidgets.QProgressDialog(message, '', 0, 0, self)
wait_dialog.setWindowTitle(title)
for window_flag in [QtCore.Qt.WindowContextHelpButtonHint]:
wait_dialog.setWindowFlag(window_flag, False)
wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
wait_dialog.setAutoClose(False)
wait_dialog.setCancelButton(None)
wait_dialog.show()
QtWidgets.QApplication.processEvents()
yield
finally:
# Finally close the message window
wait_dialog.close()
def _wait_for_threads(self):
"""
Wait for the threads
"""
# Sometimes the threads haven't finished, let's wait for them
wait_dialog = QtWidgets.QProgressDialog(translate('OpenLP.MainWindow', 'Waiting for some things to finish...'),
'', 0, 0, self)
wait_dialog.setWindowTitle(translate('OpenLP.MainWindow', 'Please Wait'))
for window_flag in [QtCore.Qt.WindowContextHelpButtonHint]:
wait_dialog.setWindowFlag(window_flag, False)
wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
wait_dialog.setAutoClose(False)
wait_dialog.setCancelButton(None)
wait_dialog.show()
thread_names = list(self.application.worker_threads.keys())
for thread_name in thread_names:
if thread_name not in self.application.worker_threads.keys():
@ -569,7 +582,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
except RuntimeError:
# Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object
pass
wait_dialog.close()
def bootstrap_post_set_up(self):
"""
@ -1085,10 +1097,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
else:
event.accept()
if event.isAccepted():
# Wait for all the threads to complete
self._wait_for_threads()
# If we just did a settings import, close without saving changes.
self.clean_up(save_settings=not self.settings_imported)
with self._show_wait_dialog(translate('OpenLP.MainWindow', 'Please Wait'),
translate('OpenLP.MainWindow', 'Waiting for some things to finish...')):
# Wait for all the threads to complete
self._wait_for_threads()
# If we just did a settings import, close without saving changes.
self.clean_up(save_settings=not self.settings_imported)
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.FileOpen:

View File

@ -23,6 +23,8 @@ The :mod:`~openlp.core.ui.media` module contains classes and objects for media p
"""
import logging
from openlp.core.common.registry import Registry
log = logging.getLogger(__name__ + '.__init__')
# Audio and video extensions copied from 'include/vlc_interface.h' from vlc 2.2.0 source
@ -83,9 +85,7 @@ class ItemMediaInfo(object):
This class hold the media related info
"""
file_info = None
volume = 100
is_background = False
is_looping_playback = False
length = 0
start_time = 0
end_time = 0
@ -97,6 +97,57 @@ class ItemMediaInfo(object):
media_type = MediaType()
def get_volume(controller) -> int:
"""
The volume needs to be retrieved
:param controller: the controller in use
:return: Are we looping
"""
if controller.is_live:
return Registry().get('settings').value('media/live volume')
else:
return Registry().get('settings').value('media/preview volume')
def save_volume(controller, volume: int) -> None:
"""
The volume needs to be saved
:param controller: the controller in use
:param volume: The volume to use and save
:return: Are we looping
"""
if controller.is_live:
return Registry().get('settings').setValue('media/live volume', volume)
else:
return Registry().get('settings').setValue('media/preview volume', volume)
def is_looping_playback(controller) -> bool:
"""
:param controller: the controller in use
:return: Are we looping
"""
if controller.is_live:
return Registry().get('settings').value('media/live loop')
else:
return Registry().get('settings').value('media/preview loop')
def toggle_looping_playback(controller) -> None:
"""
:param controller: the controller in use
:return: None
"""
if controller.is_live:
Registry().get('settings').setValue('media/live loop', not Registry().get('settings').value('media/live loop'))
else:
Registry().get('settings').setValue('media/preview loop',
not Registry().get('settings').value('media/preview loop'))
def parse_optical_path(input_string):
"""
Split the optical path info.

View File

@ -42,7 +42,8 @@ from openlp.core.lib.serviceitem import ItemCapabilities
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.state import State
from openlp.core.ui import DisplayControllerType, HideMode
from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, parse_stream_path
from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, parse_stream_path, \
get_volume, toggle_looping_playback, is_looping_playback, save_volume
from openlp.core.ui.media.remote import register_views
from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc
@ -236,10 +237,6 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.media_reset(controller)
controller.media_info = ItemMediaInfo()
controller.media_info.media_type = MediaType.Video
if controller.is_live:
controller.media_info.volume = self.settings.value('media/live volume')
else:
controller.media_info.volume = self.settings.value('media/preview volume')
# background will always loop video.
if service_item.is_capable(ItemCapabilities.HasBackgroundAudio):
controller.media_info.file_info = [file_path for (file_path, file_hash) in service_item.background_audio]
@ -255,7 +252,6 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
elif service_item.is_capable(ItemCapabilities.HasBackgroundVideo):
controller.media_info.file_info = [service_item.video_file_name]
service_item.media_length = self.media_length(service_item.video_file_name)
controller.media_info.is_looping_playback = True
controller.media_info.is_background = True
else:
controller.media_info.file_info = [service_item.get_frame_path()]
@ -457,13 +453,13 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
controller.seek_slider.blockSignals(False)
controller.volume_slider.blockSignals(False)
return False
self.media_volume(controller, controller.media_info.volume)
self.media_volume(controller, get_volume(controller))
if not start_hidden:
self._media_set_visibility(controller, True)
controller.mediabar.actions['playbackPlay'].setVisible(False)
controller.mediabar.actions['playbackPause'].setVisible(True)
controller.mediabar.actions['playbackStop'].setDisabled(False)
controller.mediabar.actions['playbackLoop'].setChecked(controller.media_info.is_looping_playback)
controller.mediabar.actions['playbackLoop'].setChecked(is_looping_playback(controller))
controller.mediabar.actions['playbackStop'].setVisible(not controller.media_info.is_background or
controller.media_info.media_type is MediaType.Audio)
controller.mediabar.actions['playbackLoop'].setVisible((not controller.media_info.is_background and
@ -501,7 +497,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
if controller.media_info.is_playing and controller.media_info.length > 0:
controller.media_info.timer += TICK_TIME
if controller.media_info.timer >= controller.media_info.start_time + controller.media_info.length:
if controller.media_info.is_looping_playback:
if is_looping_playback(controller):
start_again = True
else:
self.media_stop(controller)
@ -522,8 +518,11 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
end_seconds = controller.media_info.end_time // 1000
end_minutes = end_seconds // 60
end_seconds %= 60
controller.position_label.setText(' %02d:%02d / %02d:%02d' %
(minutes, seconds, end_minutes, end_seconds))
if end_minutes == 0 and end_seconds == 0:
controller.position_label.setText('')
else:
controller.position_label.setText(' %02d:%02d / %02d:%02d' %
(minutes, seconds, end_minutes, end_seconds))
def media_pause_msg(self, msg):
"""
@ -573,8 +572,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
:param controller: The controller that needs to be stopped
"""
controller.media_info.is_looping_playback = not controller.media_info.is_looping_playback
controller.mediabar.actions['playbackLoop'].setChecked(controller.media_info.is_looping_playback)
toggle_looping_playback(controller)
controller.mediabar.actions['playbackLoop'].setChecked(is_looping_playback(controller))
def media_stop_msg(self, msg):
"""
@ -638,11 +637,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
:param volume: The volume to be set
"""
self.log_debug(f'media_volume {volume}')
if controller.is_live:
self.settings.setValue('media/live volume', volume)
else:
self.settings.setValue('media/preview volume', volume)
controller.media_info.volume = volume
save_volume(controller, volume)
self.current_media_players[controller.controller_type].volume(controller, volume)
controller.volume_slider.setValue(volume)
@ -688,6 +683,9 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
else:
self._media_set_visibility(controller, False)
del self.current_media_players[controller.controller_type]
controller.mediabar.actions['playbackPlay'].setVisible(True)
controller.mediabar.actions['playbackStop'].setDisabled(True)
controller.mediabar.actions['playbackPause'].setVisible(False)
def media_hide_msg(self, msg):
"""

View File

@ -32,16 +32,15 @@ from time import sleep
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import LogMixin
from openlp.core.common.platform import is_linux, is_macosx, is_win
from openlp.core.display.screens import ScreenList
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui.media import MediaState, MediaType, VlCState
from openlp.core.ui.media import MediaState, MediaType, VlCState, get_volume
from openlp.core.ui.media.mediaplayer import MediaPlayer
log = logging.getLogger(__name__)
# Audio and video extensions copied from 'include/vlc_interface.h' from vlc 2.2.0 source
STATE_WAIT_TIME = 60
@ -85,7 +84,7 @@ if is_linux() and 'pytest' not in sys.argv[0] and get_vlc():
log.exception('Failed to run XInitThreads(), VLC might not work properly!')
class VlcPlayer(MediaPlayer):
class VlcPlayer(MediaPlayer, LogMixin):
"""
A specialised version of the MediaPlayer class, which provides a VLC display.
"""
@ -135,7 +134,7 @@ class VlcPlayer(MediaPlayer):
controller.vlc_instance = vlc.Instance(command_line_options)
if not controller.vlc_instance:
return
log.debug(f"VLC version: {vlc.libvlc_get_version()}")
self.log_debug(f"VLC version: {vlc.libvlc_get_version()}")
# creating an empty vlc media player
controller.vlc_media_player = controller.vlc_instance.media_player_new()
controller.vlc_widget.resize(controller.size())
@ -174,7 +173,7 @@ class VlcPlayer(MediaPlayer):
"""
if not controller.vlc_instance:
return False
log.debug('load video in VLC Controller')
self.log_debug('load video in VLC Controller')
path = None
if file and not controller.media_info.media_type == MediaType.Stream:
path = os.path.normcase(file)
@ -203,7 +202,7 @@ class VlcPlayer(MediaPlayer):
path = '/' + path
dvd_location = 'dvd://' + path + '#' + controller.media_info.title_track
controller.vlc_media = controller.vlc_instance.media_new_location(dvd_location)
log.debug(f"vlc dvd load: {dvd_location}")
self.log_debug(f"vlc dvd load: {dvd_location}")
controller.vlc_media.add_option(f"start-time={int(controller.media_info.start_time // 1000)}")
controller.vlc_media.add_option(f"stop-time={int(controller.media_info.end_time // 1000)}")
controller.vlc_media_player.set_media(controller.vlc_media)
@ -212,10 +211,11 @@ class VlcPlayer(MediaPlayer):
self.media_state_wait(controller, VlCState.Playing)
if controller.media_info.audio_track > 0:
res = controller.vlc_media_player.audio_set_track(controller.media_info.audio_track)
log.debug('vlc play, audio_track set: ' + str(controller.media_info.audio_track) + ' ' + str(res))
self.log_debug('vlc play, audio_track set: ' + str(controller.media_info.audio_track) + ' ' + str(res))
if controller.media_info.subtitle_track > 0:
res = controller.vlc_media_player.video_set_spu(controller.media_info.subtitle_track)
log.debug('vlc play, subtitle_track set: ' + str(controller.media_info.subtitle_track) + ' ' + str(res))
self.log_debug('vlc play, subtitle_track set: ' +
str(controller.media_info.subtitle_track) + ' ' + str(res))
elif controller.media_info.media_type == MediaType.Stream:
controller.vlc_media = controller.vlc_instance.media_new_location(file[0])
controller.vlc_media.add_options(file[1])
@ -229,7 +229,7 @@ class VlcPlayer(MediaPlayer):
controller.vlc_media.parse()
controller.seek_slider.setMinimum(controller.media_info.start_time)
controller.seek_slider.setMaximum(controller.media_info.end_time)
self.volume(controller, controller.media_info.volume)
self.volume(controller, get_volume(controller))
return True
def media_state_wait(self, controller, media_state):
@ -271,11 +271,11 @@ class VlcPlayer(MediaPlayer):
:param output_display: The display where the media is
:return:
"""
log.debug('vlc play, mediatype: ' + str(controller.media_info.media_type))
self.log_debug('vlc play, mediatype: ' + str(controller.media_info.media_type))
threading.Thread(target=controller.vlc_media_player.play).start()
if not self.media_state_wait(controller, VlCState.Playing):
return False
self.volume(controller, controller.media_info.volume)
self.volume(controller, get_volume(controller))
self.set_state(MediaState.Playing, controller)
return True

View File

@ -29,7 +29,6 @@ import zipfile
from contextlib import suppress
from datetime import datetime, timedelta
from pathlib import Path
from tempfile import NamedTemporaryFile
from PyQt5 import QtCore, QtGui, QtWidgets
@ -690,8 +689,8 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.log_debug('ServiceManager.save_file - ZIP contents size is %i bytes' % total_size)
self.main_window.display_progress_bar(1000)
try:
with NamedTemporaryFile(dir=str(file_path.parent), prefix='.') as temp_file, \
zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED) as zip_file:
tmp_file_path = str(file_path) + ".saving"
with zipfile.ZipFile(tmp_file_path, 'w', zipfile.ZIP_DEFLATED) as zip_file:
# First we add service contents..
zip_file.writestr('service_data.osj', service_content)
self.main_window.increment_progress_bar(service_content_size / total_size * 1000)
@ -701,11 +700,8 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.main_window.increment_progress_bar(local_file_item.stat().st_size / total_size * 1000)
with suppress(FileNotFoundError):
file_path.unlink()
# Try to link rather than copy to prevent writing another file
try:
os.link(temp_file.name, file_path)
except OSError:
shutil.copyfile(temp_file.name, file_path)
# Move rather than copy to prevent writing another file if possible
shutil.move(tmp_file_path, file_path)
self.settings.setValue('servicemanager/last directory', file_path.parent)
except (PermissionError, OSError) as error:
self.log_exception('Failed to save service to disk: {name}'.format(name=file_path))

View File

@ -971,6 +971,8 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
Registry().execute(
'{text}_start'.format(text=self.service_item.name.lower()),
[self.service_item, self.is_live, self._current_hide_mode, slide_no])
if self.service_item.is_capable(ItemCapabilities.ProvidesOwnTheme):
self._set_theme(self.service_item)
else:
self._set_theme(self.service_item)
self.info_label.setText(self.service_item.title)

View File

@ -29,7 +29,6 @@ from openlp.core.lib.plugin import Plugin, StringContent
from openlp.core.lib.theme import VerticalType
from openlp.core.lib.ui import create_action
from openlp.core.ui.icons import UiIcons
from openlp.plugins.alerts.remote import register_views
from openlp.plugins.alerts.forms.alertform import AlertForm
from openlp.plugins.alerts.lib.alertsmanager import AlertsManager
from openlp.plugins.alerts.lib.alertstab import AlertsTab
@ -139,7 +138,6 @@ class AlertsPlugin(Plugin):
self.tools_alert_item.setVisible(True)
action_list = ActionList.get_instance()
action_list.add_action(self.tools_alert_item, UiStrings().Tools)
register_views()
def finalise(self):
"""

View File

@ -1,59 +0,0 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2023 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
from flask import Blueprint, request, abort
from openlp.core.api import app
from openlp.core.api.lib import login_required, extract_request, old_success_response, old_auth
from openlp.core.common.registry import Registry
from openlp.core.lib.plugin import PluginStatus
v1_views = Blueprint('v1-alert-plugin', __name__)
v2_views = Blueprint('v2-alert-plugin', __name__)
@v2_views.route('', methods=['POST'])
@login_required
def alert():
data = request.json
if not data:
abort(400)
alert = data.get('text', '')
if alert:
if Registry().get('plugin_manager').get_plugin_by_name('alerts').status == PluginStatus.Active:
Registry().get('alerts_manager').alerts_text.emit([alert])
return '', 204
abort(400)
@v1_views.route('')
@old_auth
def old_alert():
alert = extract_request(request.args.get('data', ''), 'text')
if alert:
if Registry().get('plugin_manager').get_plugin_by_name('alerts').status == PluginStatus.Active:
Registry().get('alerts_manager').alerts_text.emit([alert])
return old_success_response()
def register_views():
app.register_blueprint(v2_views, url_prefix='/api/v2/plugins/alerts')
app.register_blueprint(v1_views, url_prefix='/api/alert')

View File

@ -1,116 +0,0 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2023 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
import logging
from flask import abort, request, Blueprint, jsonify
from openlp.core.api import app
from openlp.core.api.lib import login_required, extract_request, old_auth
from openlp.core.lib.plugin import PluginStatus
from openlp.core.common.registry import Registry
log = logging.getLogger(__name__)
v1_media = Blueprint('v1-media-plugin', __name__)
v2_media = Blueprint('v2-media-plugin', __name__)
def search(text):
plugin = Registry().get('plugin_manager').get_plugin_by_name('media')
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
results = plugin.media_item.search(text, False)
return results
return None
def live(id):
plugin = Registry().get('plugin_manager').get_plugin_by_name('media')
if plugin.status == PluginStatus.Active and plugin.media_item:
plugin.media_item.media_go_live.emit([id, True])
def add(id):
plugin = Registry().get('plugin_manager').get_plugin_by_name('media')
if plugin.status == PluginStatus.Active and plugin.media_item:
item_id = plugin.media_item.create_item_from_id(id)
plugin.media_item.media_add_to_service.emit([item_id, True])
@v2_media.route('/search')
@login_required
def search_view():
text = request.args.get('text', '')
result = search(text)
return jsonify(result)
@v2_media.route('/add', methods=['POST'])
@login_required
def add_view():
data = request.json
if not data:
abort(400)
id = data.get('id', -1)
add(id)
return '', 204
@v2_media.route('/live', methods=['POST'])
@login_required
def live_view():
data = request.json
if not data:
abort(400)
id = data.get('id', -1)
live(id)
return '', 204
# ----------------- DEPRECATED --------------
@v1_media.route('/search')
@old_auth
def old_search():
text = extract_request(request.args.get('data', ''), 'text')
return jsonify({'results': {'items': search(text)}})
@v1_media.route('/add')
@old_auth
def old_add():
id = extract_request(request.args.get('data', ''), 'id')
add(id)
return '', 204
@v1_media.route('/live')
@old_auth
def old_live():
id = extract_request(request.args.get('data', ''), 'id')
live(id)
return '', 204
# ---------------- END DEPRECATED ----------------
def register_views():
app.register_blueprint(v2_media, url_prefix='/api/v2/plugins/media/')
app.register_blueprint(v1_media, url_prefix='/api/media/')

View File

@ -24,7 +24,7 @@ presentations from a variety of document formats.
"""
import logging
import os
from pathlib import Path
from openlp.core.common import extension_loader, sha256_file_hash
from openlp.core.common.i18n import translate
@ -84,7 +84,9 @@ class PresentationPlugin(Plugin):
has_old_scheme = True
# Migrate each file
presentation_paths = self.settings.value('presentations/presentations files') or []
for path in presentation_paths:
for presentation_path in presentation_paths:
# Typecast to Path object
path = Path(presentation_path)
# check to see if the file exists before trying to process it.
if not path.exists():
continue

View File

@ -94,20 +94,18 @@ class WorshipCenterProImport(SongImport):
marker_end = verse.find('>')
marker = verse[marker_start + 1:marker_end]
# Identify the marker type
if 'REFRAIN' in marker or 'CHORUS' in marker:
if marker in ['REFRAIN', 'CHORUS']:
marker_type = 'c'
elif 'BRIDGE' in marker:
elif marker == 'BRIDGE':
marker_type = 'b'
elif 'PRECHORUS' in marker:
elif marker == 'PRECHORUS':
marker_type = 'p'
elif 'END' in marker:
elif marker == 'END':
marker_type = 'e'
elif 'INTRO' in marker:
elif marker == 'INTRO':
marker_type = 'i'
elif 'TAG' in marker:
elif marker == 'TAG':
marker_type = 'o'
else:
marker_type = 'v'
# Strip tags from text
verse = re.sub('<[^<]+?>', '', verse)
self.add_verse(verse.strip(), marker_type)

View File

@ -52,7 +52,7 @@ def report_song_list():
report_file_path.with_suffix('.csv')
Registry().get('application').set_busy_cursor()
try:
with report_file_path.open('wt') as export_file:
with report_file_path.open('wt', encoding='utf8') as export_file:
fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic')
writer = csv.DictWriter(export_file, fieldnames=fieldnames, quoting=csv.QUOTE_ALL)
headers = dict((n, n) for n in fieldnames)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -52,6 +52,7 @@ WIN32_MODULES = [
'win32com',
'win32ui',
'pywintypes',
'icu',
]
LINUX_MODULES = [

View File

@ -107,6 +107,7 @@ using a computer and a display/projector.""",
'lxml',
'Mako',
"pillow",
'PyICU',
'pymediainfo >= 2.2',
'pyobjc; platform_system=="Darwin"',
'pyobjc-framework-Cocoa; platform_system=="Darwin"',

View File

@ -63,7 +63,7 @@ def mocked_qapp():
@pytest.fixture
def registry():
def registry(autouse=True):
"""An instance of the Registry"""
yield Registry.create()
Registry._instances = {}
@ -78,14 +78,14 @@ def settings(qapp, registry):
# Needed on windows to make sure a Settings object is available during the tests
sets = Settings()
sets.setValue('themes/global theme', 'my_theme')
Registry().register('settings', sets)
Registry().register('settings_thread', sets)
Registry().register('application', qapp)
registry.register('settings', sets)
registry.register('settings_thread', sets)
registry.register('application', qapp)
qapp.settings = sets
yield sets
del sets
Registry().remove('settings')
Registry().remove('settings_thread')
registry.remove('settings')
registry.remove('settings_thread')
os.close(fd)
os.unlink(Settings().fileName())
@ -95,12 +95,12 @@ def mock_settings(qapp, registry):
"""A Mock Settings() instance"""
# Create and register a mock settings object to work with
mk_settings = MagicMock()
Registry().register('settings', mk_settings)
Registry().register('application', qapp)
Registry().register('settings_thread', mk_settings)
registry.register('settings', mk_settings)
registry.register('application', qapp)
registry.register('settings_thread', mk_settings)
yield mk_settings
Registry().remove('settings')
Registry().remove('settings_thread')
registry.remove('settings')
registry.remove('settings_thread')
del mk_settings

View File

@ -346,7 +346,7 @@ describe("Screen Visibility", function () {
done();
}, TRANSITION_TIMEOUT);
});
it("should trigger dispatchEvent when toBlack(eventName) is called with an event parameter", function (done) {
var testEventName = 'event_33';
displayWatcher.dispatchEvent = function(eventName) {
@ -959,10 +959,33 @@ describe("Display.setImageSlides", function () {
expect(Display._slides["1"]).toEqual(1);
expect($(".slides > section > section").length).toEqual(2);
expect($(".slides > section > section > img").length).toEqual(2);
expect($(".slides > section > section > img")[0].getAttribute("src")).toEqual("file:///openlp1.jpg")
expect($(".slides > section > section > img")[0].getAttribute("style")).toEqual("width: 100%; height: 100%; margin: 0; object-fit: contain;")
expect($(".slides > section > section > img")[1].getAttribute("src")).toEqual("file:///openlp2.jpg")
expect($(".slides > section > section > img")[1].getAttribute("style")).toEqual("width: 100%; height: 100%; margin: 0; object-fit: contain;")
expect($(".slides > section > section > img")[0].getAttribute("src")).toEqual("file:///openlp1.jpg");
expect($(".slides > section > section > img")[0].getAttribute("style")).toEqual("width: 100%; height: 100%; margin: 0; object-fit: contain;");
expect($(".slides > section > section > img")[1].getAttribute("src")).toEqual("file:///openlp2.jpg");
expect($(".slides > section > section > img")[1].getAttribute("style")).toEqual("width: 100%; height: 100%; margin: 0; object-fit: contain;");
expect(Reveal.sync).toHaveBeenCalledTimes(1);
});
});
describe("Display.setFullscreenImage", function () {
beforeEach(function() {
document.body.innerHTML = "";
var slides_container = _createDiv({"class": "slides"});
var footer_container = _createDiv({"class": "footer"});
Display._slidesContainer = slides_container;
Display._footerContainer = footer_container;
Display._slides = {};
});
it("should set a fullscreen image", function () {
var image = "file:///openlp1.jpg";
let bg_color = "#000";
spyOn(Reveal, "sync");
spyOn(Reveal, "slide");
Display.setFullscreenImage(bg_color, image);
expect($(".slides > section > img")[0].getAttribute("src")).toEqual("file:///openlp1.jpg");
expect(Reveal.sync).toHaveBeenCalledTimes(1);
});
});

View File

@ -35,15 +35,38 @@ from openlp.core.common.registry import Registry
ZERO_URL = '0.0.0.0'
@pytest.fixture(scope="module")
def mocked_get_installed_version():
setup_patcher = patch('openlp.core.api.tab.get_installed_version')
mocked_setup_patcher = setup_patcher.start()
mocked_setup_patcher.return_value = None
yield mocked_setup_patcher
setup_patcher.stop()
@pytest.fixture
def api_tab(settings):
Registry().set_flag('website_version', '00-00-0000')
Registry().set_flag('no_web_server', False)
def api_tab_instantiate(mocked_get_installed_version, settings):
forms = []
parent = QtWidgets.QMainWindow()
form = ApiTab(parent)
yield form
def _create_api_tab():
nonlocal forms, parent
Registry().set_flag('website_version', '00-00-0000')
Registry().set_flag('no_web_server', False)
form = ApiTab(parent)
forms.append(form)
return form
yield _create_api_tab
del parent
del form
for form in forms:
del form
mocked_get_installed_version.return_value = None
@pytest.fixture
def api_tab(api_tab_instantiate):
yield api_tab_instantiate()
def test_get_ip_address_default(api_tab):
@ -183,9 +206,12 @@ def test_available_version_property_set_none(api_tab):
assert api_tab.available_version_value.text() == '(unknown)'
def test_installed_version_property_get_none(api_tab):
def test_installed_version_property_get_none(mocked_get_installed_version, api_tab_instantiate, settings):
"""Test that the installed version property is None on init"""
# GIVEN: An uninitialised API tab
mocked_get_installed_version.return_value = None
settings.setValue('api/download_version', None)
api_tab = api_tab_instantiate()
# WHEN: the installed version is GET'ed
result = api_tab.installed_version

View File

@ -41,42 +41,6 @@ def worker(settings):
yield worker
@patch('openlp.core.api.websockets.WebSocketWorker')
@patch('openlp.core.api.websockets.run_thread')
def test_serverstart(mocked_run_thread, MockWebSocketWorker, registry):
"""
Test the starting of the WebSockets Server with the disabled flag set off
"""
# GIVEN: A new WebSocketServer
Registry().set_flag('no_web_server', False)
server = WebSocketServer()
# WHEN: I start the server
server.start()
# THEN: the api environment should have been created
assert mocked_run_thread.call_count == 1, 'The qthread should have been called once'
assert MockWebSocketWorker.call_count == 1, 'The http thread should have been called once'
@patch('openlp.core.api.websockets.WebSocketWorker')
@patch('openlp.core.api.websockets.run_thread')
def test_serverstart_not_required(mocked_run_thread, MockWebSocketWorker, registry):
"""
Test the starting of the WebSockets Server with the disabled flag set on
"""
# GIVEN: A new WebSocketServer and the server is not required
Registry().set_flag('no_web_server', True)
server = WebSocketServer()
# WHEN: I start the server
server.start()
# THEN: the api environment should have not been created
assert mocked_run_thread.call_count == 0, 'The qthread should not have been called'
assert MockWebSocketWorker.call_count == 0, 'The http thread should not have been called'
def test_poller_get_state(poller, settings):
"""
Test the get_state function returns the correct JSON
@ -107,50 +71,6 @@ def test_poller_get_state(poller, settings):
assert poll_json['results']['item'] == '23-34-45', 'The item return value should match 23-34-45'
@patch('openlp.core.api.websockets.serve')
@patch('openlp.core.api.websockets.asyncio')
@patch('openlp.core.api.websockets.log')
def test_worker_start(mocked_log, mocked_asyncio, mocked_serve, worker, settings):
"""
Test the start function of the worker
"""
# GIVEN: A mocked serve function and event loop
mocked_serve.return_value = 'server_thing'
event_loop = MagicMock()
mocked_asyncio.new_event_loop.return_value = event_loop
# WHEN: The start function is called
worker.start()
# THEN: No error occurs
mocked_serve.assert_called_once()
event_loop.run_until_complete.assert_called_once_with('server_thing')
event_loop.run_forever.assert_called_once_with()
mocked_log.exception.assert_not_called()
# Because run_forever is mocked, it doesn't stall the thread so close will be called immediately
event_loop.close.assert_called_once_with()
@patch('openlp.core.api.websockets.serve')
@patch('openlp.core.api.websockets.asyncio')
@patch('openlp.core.api.websockets.log')
def test_worker_start_fail(mocked_log, mocked_asyncio, mocked_serve, worker, settings):
"""
Test the start function of the worker handles a error nicely
"""
# GIVEN: A mocked serve function and event loop. run_until_complete returns a error
mocked_serve.return_value = 'server_thing'
event_loop = MagicMock()
mocked_asyncio.new_event_loop.return_value = event_loop
event_loop.run_until_complete.side_effect = Exception()
# WHEN: The start function is called
worker.start()
# THEN: An exception is logged but is handled and the event_loop is closed
mocked_serve.assert_called_once()
event_loop.run_until_complete.assert_called_once_with('server_thing')
event_loop.run_forever.assert_not_called()
mocked_log.exception.assert_called_once()
event_loop.close.assert_called_once_with()
def test_poller_event_attach(poller, settings):
"""
Test the event attach of WebSocketPoller
@ -206,6 +126,102 @@ def test_poller_get_state_is_never_none(poller):
assert state is not None, 'get_state() return should not be None'
@patch('openlp.core.api.websockets.serve')
@patch('openlp.core.api.websockets.asyncio')
@patch('openlp.core.api.websockets.log')
def test_websocket_worker_start(mocked_log, mocked_asyncio, mocked_serve, worker, settings):
"""
Test the start function of the worker
"""
# GIVEN: A mocked serve function and event loop
mocked_serve.return_value = 'server_thing'
event_loop = MagicMock()
mocked_asyncio.new_event_loop.return_value = event_loop
# WHEN: The start function is called
worker.start()
# THEN: No error occurs
mocked_serve.assert_called_once()
event_loop.run_until_complete.assert_called_once_with('server_thing')
event_loop.run_forever.assert_called_once_with()
mocked_log.exception.assert_not_called()
# Because run_forever is mocked, it doesn't stall the thread so close will be called immediately
event_loop.close.assert_called_once_with()
@patch('openlp.core.api.websockets.serve')
@patch('openlp.core.api.websockets.asyncio')
@patch('openlp.core.api.websockets.log')
def test_websocket_worker_start_fail(mocked_log, mocked_asyncio, mocked_serve, worker, settings):
"""
Test the start function of the worker handles a error nicely
"""
# GIVEN: A mocked serve function and event loop. run_until_complete returns a error
mocked_serve.return_value = 'server_thing'
event_loop = MagicMock()
mocked_asyncio.new_event_loop.return_value = event_loop
event_loop.run_until_complete.side_effect = Exception()
# WHEN: The start function is called
worker.start()
# THEN: An exception is logged but is handled and the event_loop is closed
mocked_serve.assert_called_once()
event_loop.run_until_complete.assert_called_once_with('server_thing')
event_loop.run_forever.assert_not_called()
mocked_log.exception.assert_called_once()
event_loop.close.assert_called_once_with()
def test_websocket_server_bootstrap_post_set_up(settings):
"""
Test that the bootstrap_post_set_up() method calls the start method
"""
# GIVEN: A WebSocketServer with the start() method mocked out
Registry().set_flag('no_web_server', False)
server = WebSocketServer()
server.start = MagicMock()
# WHEN: bootstrap_post_set_up() is called
server.bootstrap_post_set_up()
# THEN: start() should have been called
server.start.assert_called_once_with()
@patch('openlp.core.api.websockets.WebSocketWorker')
@patch('openlp.core.api.websockets.run_thread')
def test_websocket_server_start(mocked_run_thread, MockWebSocketWorker, registry):
"""
Test the starting of the WebSockets Server with the disabled flag set off
"""
# GIVEN: A new WebSocketServer
Registry().set_flag('no_web_server', False)
server = WebSocketServer()
# WHEN: I start the server
server.start()
# THEN: the api environment should have been created
assert mocked_run_thread.call_count == 1, 'The qthread should have been called once'
assert MockWebSocketWorker.call_count == 1, 'The http thread should have been called once'
@patch('openlp.core.api.websockets.WebSocketWorker')
@patch('openlp.core.api.websockets.run_thread')
def test_websocket_server_start_not_required(mocked_run_thread, MockWebSocketWorker, registry):
"""
Test the starting of the WebSockets Server with the disabled flag set on
"""
# GIVEN: A new WebSocketServer and the server is not required
Registry().set_flag('no_web_server', True)
server = WebSocketServer()
# WHEN: I start the server
server.start()
# THEN: the api environment should have not been created
assert mocked_run_thread.call_count == 0, 'The qthread should not have been called'
assert MockWebSocketWorker.call_count == 0, 'The http thread should not have been called'
@patch('openlp.core.api.websockets.poller')
@patch('openlp.core.api.websockets.run_thread')
def test_websocket_server_connects_to_poller(mock_run_thread, mock_poller, settings):
@ -245,3 +261,65 @@ def test_websocket_worker_register_connections(mock_run_thread, mock_add_state_t
# THEN: WebSocketWorker state notify function should be called
mock_add_state_to_queues.assert_called_once_with(mock_state)
@patch('openlp.core.api.websockets.poller')
@patch('openlp.core.api.websockets.log')
def test_websocket_server_try_poller_hook_signals(mocked_log, mock_poller, settings):
"""
Test if the websocket_server invokes poller.hook_signals
"""
# GIVEN: A mocked poller signal and a server
mock_poller.hook_signals.side_effect = Exception
Registry().set_flag('no_web_server', False)
server = WebSocketServer()
# WHEN: WebSocketServer is started
server.try_poller_hook_signals()
# THEN: poller_changed should be connected with WebSocketServer and correct handler
mock_poller.hook_signals.assert_called_once_with()
mocked_log.error.assert_called_once_with('Failed to hook poller signals!')
@patch('openlp.core.api.websockets.poller')
def test_websocket_server_close(mock_poller, settings):
"""
Test that the websocket_server close method works correctly
"""
# GIVEN: A mocked poller signal and a server
Registry().set_flag('no_web_server', False)
mock_poller.poller_changed = MagicMock()
mock_poller.poller_changed.connect = MagicMock()
server = WebSocketServer()
server.handle_poller_signal = MagicMock()
mock_worker = MagicMock()
server.worker = mock_worker
# WHEN: WebSocketServer is started
server.close()
# THEN: poller_changed should be connected with WebSocketServer and correct handler
mock_poller.poller_changed.disconnect.assert_called_once_with(server.handle_poller_signal)
mock_poller.unhook_signals.assert_called_once_with()
mock_worker.stop.assert_called_once_with()
@patch('openlp.core.api.websockets.poller')
def test_websocket_server_close_when_disabled(mock_poller, registry, settings):
"""
Test if the websocket_server close method correctly skips teardown when disabled
"""
# GIVEN: A mocked poller signal and a server
Registry().set_flag('no_web_server', True)
mock_poller.poller_changed = MagicMock()
mock_poller.poller_changed.connect = MagicMock()
server = WebSocketServer()
server.handle_poller_signal = MagicMock()
# WHEN: WebSocketServer is started
server.close()
# THEN: poller_changed should be connected with WebSocketServer and correct handler
assert mock_poller.poller_changed.disconnect.call_count == 0
assert mock_poller.unhook_signals.call_count == 0

View File

@ -141,6 +141,16 @@ def test_get_natural_key():
# THEN: We get a properly sorted list
assert sorted_list == ['1st item', 'item 3b', 'item 10a'], 'Numbers should be sorted naturally'
# GIVEN: The language is still English (a language, which sorts digits before letters)
mocked_get_language.return_value = 'en'
unsorted_list = ['1 songname', '100 songname', '2 songname']
# WHEN: We sort the list and use get_natural_key() to generate the sorting keys
sorted_list = sorted(unsorted_list, key=get_natural_key)
# THEN: We get a properly sorted list
assert sorted_list == ['1 songname', '2 songname', '100 songname'], 'Numbers should be sorted naturally'
def test_check_same_instance():
"""

View File

@ -77,7 +77,7 @@ def test_extension_loader_files_found():
Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file2.py'),
Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file3.py'),
Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file4.py')]), \
patch('openlp.core.common.importlib.import_module') as mocked_import_module:
patch('openlp.core.common.import_openlp_module') as mocked_import_module:
# WHEN: Calling `extension_loader` with a list of files to exclude
extension_loader('glob', ['file2.py', 'file3.py'])
@ -97,7 +97,7 @@ def test_extension_loader_import_error():
return_value=Path('/', 'app', 'dir', 'openlp')), \
patch.object(Path, 'glob', return_value=[
Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file1.py')]), \
patch('openlp.core.common.importlib.import_module', side_effect=ImportError()), \
patch('openlp.core.common.import_openlp_module', side_effect=ImportError()), \
patch('openlp.core.common.log') as mocked_logger:
# WHEN: Calling `extension_loader`
@ -116,7 +116,7 @@ def test_extension_loader_os_error():
return_value=Path('/', 'app', 'dir', 'openlp')), \
patch.object(Path, 'glob', return_value=[
Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file1.py')]), \
patch('openlp.core.common.importlib.import_module', side_effect=OSError()), \
patch('openlp.core.common.import_openlp_module', side_effect=OSError()), \
patch('openlp.core.common.log') as mocked_logger:
# WHEN: Calling `extension_loader`

View File

@ -307,7 +307,7 @@ def test_set_startup_screen_hide(display_window_env, mock_settings):
'Display.setStartupSplashScreen("orange", "");')
def test_after_loaded(display_window_env, mock_settings):
def test_after_loaded(display_window_env, mock_settings, registry):
"""
Test the correct steps are taken when the webview is loaded
"""
@ -335,7 +335,7 @@ def test_after_loaded(display_window_env, mock_settings):
display_window.set_startup_screen.assert_called_once()
def test_after_loaded_hide_mouse_not_display(display_window_env, mock_settings):
def test_after_loaded_hide_mouse_not_display(display_window_env, mock_settings, registry):
"""
Test the mouse is showing even if the `hide mouse` setting is set while is_display=false
"""
@ -361,7 +361,7 @@ def test_after_loaded_hide_mouse_not_display(display_window_env, mock_settings):
'});')
def test_after_loaded_callback(display_window_env, mock_settings):
def test_after_loaded_callback(display_window_env, mock_settings, registry):
"""
Test if the __ is loaded on after_loaded() method correctly
"""
@ -462,7 +462,7 @@ def test_fix_font_bold_not_windows(mocked_is_win, display_window_env, mock_setti
@patch('openlp.core.display.window.is_win')
def test_fix_font_foundry(mocked_is_win, display_window_env, mock_settings):
def test_fix_font_foundry(mocked_is_win, display_window_env, mock_settings, registry):
"""
Test that a font with a foundry name in it has the foundry removed
"""

View File

@ -104,7 +104,7 @@ def test_get_upgrade_op():
MockedOperations.assert_called_with(mocked_context)
def test_delete_database_without_db_file_name():
def test_delete_database_without_db_file_name(registry):
"""
Test that the ``delete_database`` function removes a database file, without the file name parameter
"""

View File

@ -273,7 +273,7 @@ def test_image_to_byte_base_64():
assert 'byte_array base64ified' == result, 'The result should be the return value of the mocked base64 method'
def test_create_thumb_with_size():
def test_create_thumb_with_size(registry):
"""
Test the create_thumb() function with a given size.
"""
@ -308,7 +308,7 @@ def test_create_thumb_with_size():
pass
def test_create_thumb_no_size():
def test_create_thumb_no_size(registry):
"""
Test the create_thumb() function with no size specified.
"""
@ -343,7 +343,7 @@ def test_create_thumb_no_size():
pass
def test_create_thumb_invalid_size():
def test_create_thumb_invalid_size(registry):
"""
Test the create_thumb() function with invalid size specified.
"""
@ -379,7 +379,7 @@ def test_create_thumb_invalid_size():
pass
def test_create_thumb_width_only():
def test_create_thumb_width_only(registry):
"""
Test the create_thumb() function with a size of only width specified.
"""
@ -415,7 +415,7 @@ def test_create_thumb_width_only():
pass
def test_create_thumb_height_only():
def test_create_thumb_height_only(registry):
"""
Test the create_thumb() function with a size of only height specified.
"""
@ -451,7 +451,7 @@ def test_create_thumb_height_only():
pass
def test_create_thumb_empty_img():
def test_create_thumb_empty_img(registry):
"""
Test the create_thumb() function with a size of only height specified.
"""
@ -502,7 +502,7 @@ def test_create_thumb_empty_img():
@patch('openlp.core.lib.QtGui.QImageReader')
@patch('openlp.core.lib.build_icon')
def test_create_thumb_path_fails(mocked_build_icon, MockQImageReader):
def test_create_thumb_path_fails(mocked_build_icon, MockQImageReader, registry):
"""
Test that build_icon() is run against the image_path when the thumbnail fails to be created
"""
@ -537,7 +537,7 @@ def test_check_item_selected_true():
assert result is True, 'The result should be True'
def test_check_item_selected_false():
def test_check_item_selected_false(registry):
"""
Test that the check_item_selected() function returns False when there are no selected indexes.
"""
@ -608,7 +608,7 @@ def test_validate_thumb_file_exists_and_older():
assert result is False, 'The result should be False'
def test_resize_thumb():
def test_resize_thumb(registry):
"""
Test the resize_thumb() function
"""
@ -630,7 +630,7 @@ def test_resize_thumb():
assert image.pixel(0, 0) == wanted_background_rgb, 'The background should be white.'
def test_resize_thumb_ignoring_aspect_ratio():
def test_resize_thumb_ignoring_aspect_ratio(registry):
"""
Test the resize_thumb() function ignoring aspect ratio
"""
@ -652,7 +652,7 @@ def test_resize_thumb_ignoring_aspect_ratio():
assert image.pixel(0, 0) == wanted_background_rgb, 'The background should be white.'
def test_resize_thumb_width_aspect_ratio():
def test_resize_thumb_width_aspect_ratio(registry):
"""
Test the resize_thumb() function using the image's width as the reference
"""
@ -670,7 +670,7 @@ def test_resize_thumb_width_aspect_ratio():
assert wanted_width == result_size.width(), 'The image should have the requested width.'
def test_resize_thumb_same_aspect_ratio():
def test_resize_thumb_same_aspect_ratio(registry):
"""
Test the resize_thumb() function when the image and the wanted aspect ratio are the same
"""

View File

@ -862,7 +862,7 @@ def test_to_dict_text_item(mocked_sha256_file_hash, state_media, settings, servi
],
'theme': None,
'title': 'Amazing Grace',
'type': 'ServiceItemType.Text',
'type': ServiceItemType.Text,
'data': {'authors': 'John Newton', 'title': 'amazing grace@'}
}
assert result == expected_dict
@ -905,7 +905,7 @@ def test_to_dict_image_item(state_media, settings, service_item_env):
],
'theme': -1,
'title': 'Images',
'type': 'ServiceItemType.Image',
'type': ServiceItemType.Image,
'data': {}
}
assert result == expected_dict
@ -961,7 +961,7 @@ def test_to_dict_presentation_item(mocked_image_uri, mocked_get_data_path, state
],
'theme': None,
'title': '',
'type': 'ServiceItemType.Command',
'type': ServiceItemType.Command,
'data': {}
}
assert result == expected_dict

View File

@ -22,14 +22,18 @@
Package to test the openlp.core.ui.media package.
"""
from pathlib import Path
from unittest import skipUnless
from unittest.mock import MagicMock, patch
import pytest
from openlp.core.state import State
from openlp.core.common.platform import is_linux, is_macosx
from openlp.core.common.registry import Registry
from openlp.core.ui import DisplayControllerType, HideMode
from openlp.core.ui.media.mediacontroller import MediaController
from openlp.core.ui.media import ItemMediaInfo, MediaState
from openlp.core.ui.media import ItemMediaInfo, MediaState, MediaType
from tests.utils.constants import RESOURCE_PATH
@ -46,6 +50,216 @@ def media_env(registry):
yield media_controller
@patch('openlp.core.ui.media.mediacontroller.register_views')
def test_setup(mocked_register_views, media_env):
"""
Test that the setup method is called correctly
"""
# GIVEN: A media controller, and function list
expected_functions_list = ['bootstrap_initialise', 'bootstrap_post_set_up', 'bootstrap_completion',
'playbackPlay', 'playbackPause', 'playbackStop', 'playbackLoop', 'seek_slider',
'volume_slider', 'media_hide', 'media_blank', 'media_unblank', 'songs_hide',
'songs_blank', 'songs_unblank']
# WHEN: Setup is called
media_env.setup()
# THEN: the functions should be defined along with the api blueprint
assert list(Registry().functions_list.keys()) == expected_functions_list, \
f'The function list should have been {(Registry().functions_list.keys())}'
mocked_register_views.called_once(), 'The media blueprint has not been registered'
def test_initialise_good(media_env, state_media):
"""
Test that the bootstrap initialise method is called correctly
"""
# GIVEN: a mocked setup
with patch.object(media_env.media_controller, u'setup') as mocked_setup:
# THEN: The underlying method is called
media_env.media_controller.bootstrap_initialise()
# THEN: The following should have happened
mocked_setup.called_once(), 'The setup function has been called'
def test_initialise_missing_vlc(media_env, state_media):
"""
Test that the bootstrap initialise method is called correctly with no VLC
"""
# GIVEN: a mocked setup and no VLC
with patch.object(media_env.media_controller, u'setup') as mocked_setup, \
patch('openlp.core.ui.media.mediacontroller.get_vlc', return_value=False):
# THEN: The underlying method is called
media_env.media_controller.bootstrap_initialise()
# THEN: The following should have happened
mocked_setup.called_once(), 'The setup function has been called'
text = State().get_text()
if not is_macosx():
assert text.find("python3-vlc") > 0, "VLC should not be missing"
@patch('openlp.core.ui.media.mediacontroller.pymediainfo_available', False)
def test_initialise_missing_pymedia(media_env, state_media):
"""
Test that the bootstrap initialise method is called correctly with no pymediainfo
"""
# GIVEN: a mocked setup and no pymedia
with patch.object(media_env.media_controller, u'setup') as mocked_setup:
# THEN: The underlying method is called
media_env.media_controller.bootstrap_initialise()
# THEN: The following should have happened
mocked_setup.called_once(), 'The setup function has been called'
text = State().get_text()
if not is_macosx():
assert text.find("python3-pymediainfo") > 0, "PyMedia should not be missing"
@skipUnless(is_linux(), "Linux only")
def test_initialise_missing_pymedia_fedora(media_env, state_media):
"""
Test that the bootstrap initialise method is called correctly with no VLC
"""
# GIVEN: a mocked setup and no VLC
with patch.object(media_env.media_controller, u'setup') as mocked_setup, \
patch('openlp.core.ui.media.mediacontroller.get_vlc', return_value=False), \
patch('openlp.core.ui.media.mediacontroller.is_linux', return_value=True):
# WHEN: The underlying method is called
media_env.media_controller.bootstrap_initialise()
# THEN: The following should have happened
mocked_setup.called_once(), 'The setup function has been called'
text = State().get_text()
assert text.find("python3-pymediainfo") == -1, "PyMedia should be missing"
assert text.find("python3-vlc") > 0, "VLC should not be missing"
assert text.find("rpmfusion") > 0, "RPMFusion should provide the modules"
@skipUnless(is_linux(), "Linux only")
def test_initialise_missing_pymedia_not_fedora(media_env, state_media):
"""
Test that the bootstrap initialise method is called correctly with no VLC
"""
# GIVEN: a mocked setup and no VLC
with patch.object(media_env.media_controller, u'setup') as mocked_setup, \
patch('openlp.core.ui.media.mediacontroller.get_vlc', return_value=False), \
patch('openlp.core.ui.media.mediacontroller.is_linux', return_value=False):
# WHEN: The underlying method is called
media_env.media_controller.bootstrap_initialise()
# THEN: The following should have happened
mocked_setup.called_once(), 'The setup function has been called'
text = State().get_text()
assert text.find("python3-pymediainfo") == -1, "PyMedia should be missing"
assert text.find("python3-vlc") > 0, "VLC should not be missing"
assert text.find("rpmfusion") == -1, "RPMFusion should not provide the modules"
def test_initialise_missing_pymedia_mac_os(media_env, state_media):
"""
Test that the bootstrap initialise method is called correctly with no VLC
"""
# GIVEN: a mocked setup and no VLC
with patch.object(media_env.media_controller, u'setup') as mocked_setup, \
patch('openlp.core.ui.media.mediacontroller.get_vlc', return_value=False), \
patch('openlp.core.ui.media.mediacontroller.is_macosx', return_value=True):
# WHEN: The underlying method is called
media_env.media_controller.bootstrap_initialise()
# THEN: The following should have happened
mocked_setup.called_once(), 'The setup function has been called'
text = State().get_text()
assert text.find("python3-pymediainfo") == -1, "PyMedia should be missing"
assert text.find("python3-vlc") == -1, "PyMedia should be missing"
assert text.find("videolan") > 0, "VideoLAN should provide the modules"
def test_post_set_up_good(media_env, state_media):
"""
Test the Bootstrap post set up assuming all functions are good
"""
# GIVEN: A working environment
media_env.vlc_live_media_stop = MagicMock()
media_env.vlc_preview_media_stop = MagicMock()
media_env.vlc_live_media_tick = MagicMock()
media_env.vlc_preview_media_tick = MagicMock()
State().add_service("mediacontroller", 0)
State().update_pre_conditions("mediacontroller", True)
# WHEN: I call the function
with patch.object(media_env.media_controller, u'setup_display') as mocked_display:
media_env.bootstrap_post_set_up()
# THEN: the environment is set up correctly
assert mocked_display.call_count == 2, "Should have been called twice"
text = State().get_text()
assert text.find("No Displays") == -1, "No Displays have been disable"
assert mocked_display.has_calls(None, False) # Live Controller
assert mocked_display.has_calls(None, True) # Preview Controller
def test_media_state_live(media_env, state_media):
"""
Test the Bootstrap post set up assuming all functions are good
"""
# GIVEN: A working environment
media_env.vlc_live_media_stop = MagicMock()
media_env.vlc_preview_media_stop = MagicMock()
media_env.vlc_preview_media_tick = MagicMock()
mocked_live_controller = MagicMock()
mocked_live_controller.is_live = True
mocked_live_controller.media_info.media_type = MediaType.Audio
Registry().register('live_controller', mocked_live_controller)
media_env.media_controller.vlc_player = MagicMock()
media_env.media_controller._display_controllers = MagicMock(return_value=mocked_live_controller)
# WHEN: I call the function
with patch.object(media_env.media_controller, u'setup_display') as mocked_display:
media_env.bootstrap_post_set_up()
# THEN: the environment is set up correctly
text = State().get_text()
assert text.find("No Displays") == -1, "No Displays have been disable"
assert mocked_display.has_calls(None, False) # Live Controller
assert mocked_display.has_calls(None, True) # Preview Controller
def test_post_set_up_no_controller(media_env, state_media):
"""
Test the Bootstrap post set up assuming all functions are good
"""
# GIVEN: A working environment
media_env.vlc_live_media_stop = MagicMock()
media_env.vlc_preview_media_stop = MagicMock()
media_env.vlc_live_media_tick = MagicMock()
media_env.vlc_preview_media_tick = MagicMock()
State().add_service("mediacontroller", 0)
State().update_pre_conditions("mediacontroller", False)
# WHEN: I call the function
with patch.object(media_env.media_controller, u'setup_display') as mocked_display:
media_env.bootstrap_post_set_up()
# THEN: the environment is set up correctly
assert mocked_display.call_count == 0, "Should have not have been called twice"
text = State().get_text()
assert text.find("No Displays") == -1, "No Displays have been disable"
def test_post_set_up_controller_exception(media_env, state_media):
"""
Test the Bootstrap post set up assuming all functions are good
"""
# GIVEN: A working environment
media_env.vlc_live_media_stop = MagicMock()
media_env.vlc_preview_media_stop = MagicMock()
media_env.vlc_live_media_tick = MagicMock()
media_env.vlc_preview_media_tick = MagicMock()
State().add_service("mediacontroller", 0)
State().update_pre_conditions("mediacontroller", True)
State().add_service("media_live", 0)
State().update_pre_conditions("media_live", True)
# WHEN: I call the function
with patch.object(media_env.media_controller, u'setup_display') as mocked_display:
mocked_display.side_effect = AttributeError()
try:
media_env.bootstrap_post_set_up()
except AttributeError:
pass
# THEN: the environment is set up correctly
text = State().get_text()
assert text.find("Displays") > 0, "Displays have been disable"
assert mocked_display.call_count == 2, "Should have been called twice"
def test_resize(media_env):
"""
Test that the resize method is called correctly
@ -86,7 +300,6 @@ def test_load_video(media_env, settings):
# The video should have autoplayed
# The controls should have been made visible
media_env.media_controller.media_reset.assert_called_once_with(mocked_slide_controller)
assert mocked_slide_controller.media_info.volume == 1
media_env.media_controller.media_play.assert_called_once_with(mocked_slide_controller, False)
media_env.media_controller.set_controls_visible.assert_called_once_with(mocked_slide_controller, True)

View File

@ -283,7 +283,7 @@ def test_check_not_available(mocked_get_vlc):
@patch('openlp.core.ui.media.vlcplayer.get_vlc')
@patch('openlp.core.ui.media.vlcplayer.os.path.normcase')
def test_load(mocked_normcase, mocked_get_vlc):
def test_load(mocked_normcase, mocked_get_vlc, settings):
"""
Test loading a video into VLC
"""
@ -294,7 +294,6 @@ def test_load(mocked_normcase, mocked_get_vlc):
mocked_get_vlc.return_value = mocked_vlc
mocked_display = MagicMock()
mocked_controller = MagicMock()
mocked_controller.media_info.volume = 100
mocked_controller.media_info.media_type = MediaType.Video
mocked_controller.media_info.file_info.absoluteFilePath.return_value = media_path
mocked_vlc_media = MagicMock()
@ -305,8 +304,7 @@ def test_load(mocked_normcase, mocked_get_vlc):
vlc_player = VlcPlayer(None)
# WHEN: A video is loaded into VLC
with patch.object(vlc_player, 'volume') as mocked_volume:
result = vlc_player.load(mocked_controller, mocked_display, media_path)
result = vlc_player.load(mocked_controller, mocked_display, media_path)
# THEN: The video should be loaded
mocked_normcase.assert_called_with(media_path)
@ -314,14 +312,13 @@ def test_load(mocked_normcase, mocked_get_vlc):
assert mocked_vlc_media == mocked_controller.vlc_media
mocked_controller.vlc_media_player.set_media.assert_called_with(mocked_vlc_media)
mocked_vlc_media.parse.assert_called_with()
mocked_volume.assert_called_with(mocked_controller, 100)
assert result is True
@patch('openlp.core.ui.media.vlcplayer.is_win')
@patch('openlp.core.ui.media.vlcplayer.get_vlc')
@patch('openlp.core.ui.media.vlcplayer.os.path.normcase')
def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win):
def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win, settings):
"""
Test loading an audio CD into VLC
"""
@ -333,7 +330,6 @@ def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win):
mocked_get_vlc.return_value = mocked_vlc
mocked_display = MagicMock()
mocked_controller = MagicMock()
mocked_controller.media_info.volume = 100
mocked_controller.media_info.media_type = MediaType.CD
mocked_controller.media_info.title_track = 1
mocked_vlc_media = MagicMock()
@ -351,8 +347,7 @@ def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win):
vlc_player = VlcPlayer(None)
# WHEN: An audio CD is loaded into VLC
with patch.object(vlc_player, 'volume') as mocked_volume, \
patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait:
with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait:
result = vlc_player.load(mocked_controller, mocked_display, media_path)
# THEN: The video should be loaded
@ -361,7 +356,6 @@ def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win):
assert mocked_vlc_media == mocked_controller.vlc_media
mocked_controller.vlc_media_player.set_media.assert_called_with(mocked_vlc_media)
mocked_vlc_media.parse.assert_called_with()
mocked_volume.assert_called_with(mocked_controller, 100)
mocked_media_state_wait.assert_called_with(mocked_controller, ANY)
mocked_controller.seek_slider.setMinimum.assert_called_with(20000)
mocked_controller.seek_slider.setMaximum.assert_called_with(30000)
@ -371,7 +365,7 @@ def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win):
@patch('openlp.core.ui.media.vlcplayer.is_win')
@patch('openlp.core.ui.media.vlcplayer.get_vlc')
@patch('openlp.core.ui.media.vlcplayer.os.path.normcase')
def test_load_audio_cd_on_windows(mocked_normcase, mocked_get_vlc, mocked_is_win):
def test_load_audio_cd_on_windows(mocked_normcase, mocked_get_vlc, mocked_is_win, settings):
"""
Test loading an audio CD into VLC on Windows
"""
@ -383,7 +377,6 @@ def test_load_audio_cd_on_windows(mocked_normcase, mocked_get_vlc, mocked_is_win
mocked_get_vlc.return_value = mocked_vlc
mocked_display = MagicMock()
mocked_controller = MagicMock()
mocked_controller.media_info.volume = 100
mocked_controller.media_info.media_type = MediaType.CD
mocked_controller.media_info.file_info.absoluteFilePath.return_value = media_path
mocked_controller.media_info.title_track = 1
@ -400,8 +393,7 @@ def test_load_audio_cd_on_windows(mocked_normcase, mocked_get_vlc, mocked_is_win
vlc_player = VlcPlayer(None)
# WHEN: An audio CD is loaded into VLC
with patch.object(vlc_player, 'volume') as mocked_volume, \
patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait:
with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait:
result = vlc_player.load(mocked_controller, mocked_display, media_path)
# THEN: The video should be loaded
@ -410,7 +402,6 @@ def test_load_audio_cd_on_windows(mocked_normcase, mocked_get_vlc, mocked_is_win
assert mocked_vlc_media == mocked_controller.vlc_media
mocked_controller.vlc_media_player.set_media.assert_called_with(mocked_vlc_media)
mocked_vlc_media.parse.assert_called_with()
mocked_volume.assert_called_with(mocked_controller, 100)
mocked_media_state_wait.assert_called_with(mocked_controller, ANY)
assert result is True
@ -449,7 +440,7 @@ def test_load_audio_cd_no_tracks(mocked_normcase, mocked_get_vlc, mocked_is_win)
vlc_player = VlcPlayer(None)
# WHEN: An audio CD is loaded into VLC
with patch.object(vlc_player, 'volume'), patch.object(vlc_player, 'media_state_wait'):
with patch.object(vlc_player, 'media_state_wait'):
result = vlc_player.load(mocked_controller, mocked_display, media_path)
# THEN: The video should be loaded
@ -465,7 +456,7 @@ def test_load_audio_cd_no_tracks(mocked_normcase, mocked_get_vlc, mocked_is_win)
@patch('openlp.core.ui.media.vlcplayer.is_win')
@patch('openlp.core.ui.media.vlcplayer.get_vlc')
@patch('openlp.core.ui.media.vlcplayer.os.path.normcase')
def test_load_dvd(mocked_normcase, mocked_get_vlc, mocked_is_win):
def test_load_dvd(mocked_normcase, mocked_get_vlc, mocked_is_win, settings):
"""
Test loading a DVD into VLC
"""
@ -477,14 +468,12 @@ def test_load_dvd(mocked_normcase, mocked_get_vlc, mocked_is_win):
mocked_get_vlc.return_value = mocked_vlc
mocked_display = MagicMock()
mocked_controller = MagicMock()
mocked_controller.media_info.volume = 100
mocked_controller.media_info.media_type = MediaType.DVD
mocked_controller.media_info.title_track = '2'
mocked_controller.media_info.audio_track = 2
mocked_controller.media_info.subtitle_track = 4
mocked_vlc_media = MagicMock()
mocked_media = MagicMock()
mocked_controller.media_info.volume = 100
mocked_controller.media_info.start_time = 20000
mocked_controller.media_info.end_time = 30000
mocked_controller.media_info.length = 10000
@ -493,8 +482,7 @@ def test_load_dvd(mocked_normcase, mocked_get_vlc, mocked_is_win):
vlc_player = VlcPlayer(None)
# WHEN: A DVD clip is loaded into VLC
with patch.object(vlc_player, 'volume') as mocked_volume, \
patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait:
with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait:
result = vlc_player.load(mocked_controller, mocked_display, media_path)
# THEN: The video should be loaded
@ -505,7 +493,6 @@ def test_load_dvd(mocked_normcase, mocked_get_vlc, mocked_is_win):
mocked_controller.vlc_media_player.audio_set_track.assert_called_with(2)
mocked_controller.vlc_media_player.video_set_spu.assert_called_with(4)
mocked_vlc_media.parse.assert_called_with()
mocked_volume.assert_called_with(mocked_controller, 100)
mocked_media_state_wait.assert_called_with(mocked_controller, ANY)
mocked_controller.seek_slider.setMinimum.assert_called_with(20000)
mocked_controller.seek_slider.setMaximum.assert_called_with(30000)
@ -606,7 +593,7 @@ def test_resize():
@patch('openlp.core.ui.media.vlcplayer.threading')
@patch('openlp.core.ui.media.vlcplayer.get_vlc')
def test_play(mocked_get_vlc, mocked_threading):
def test_play(mocked_get_vlc, mocked_threading, settings):
"""
Test the play() method
"""
@ -618,21 +605,17 @@ def test_play(mocked_get_vlc, mocked_threading):
mocked_display = MagicMock()
mocked_controller = MagicMock()
mocked_media = MagicMock()
mocked_controller.media_info.volume = 100
mocked_controller.vlc_media_player.get_media.return_value = mocked_media
vlc_player = VlcPlayer(None)
vlc_player.set_state(MediaState.Paused, mocked_controller)
# WHEN: play() is called
with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait, \
patch.object(vlc_player, 'volume') as mocked_volume:
with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait:
mocked_media_state_wait.return_value = True
result = vlc_player.play(mocked_controller, mocked_display)
# THEN: A bunch of things should happen to play the media
mocked_thread.start.assert_called_with()
mocked_volume.assert_called_with(mocked_controller, 100)
assert MediaState.Playing == vlc_player.get_live_state()
assert result is True, 'The value returned from play() should be True'

View File

@ -38,6 +38,7 @@ exceptionform.MIGRATE_VERSION = 'Migrate Test'
exceptionform.CHARDET_VERSION = 'CHARDET Test'
exceptionform.ENCHANT_VERSION = 'Enchant Test'
exceptionform.MAKO_VERSION = 'Mako Test'
exceptionform.ICU_VERSION = 'ICU Test'
exceptionform.VLC_VERSION = 'VLC Test'
MAIL_ITEM_TEXT = ('**OpenLP Bug Report**\nVersion: Trunk Test\n\n--- Details of the Exception. ---\n\n'

View File

@ -831,3 +831,119 @@ def test_update_recent_files_menu(mocked_create_action, mocked_add_actions, Mock
# THEN: There should be no errors
assert mocked_create_action.call_count == 2
@patch('openlp.core.ui.mainwindow.QtWidgets.QProgressDialog')
def test_show_wait_dialog(MockProcessDialog, main_window_reduced):
"""Test that the show wait dialog context manager works correctly"""
# GIVEN: A mocked out QProgressDialog and a minimal main window
mocked_wait_dialog = MagicMock()
MockProcessDialog.return_value = mocked_wait_dialog
# WHEN: Calling _show_wait_dialog()
with main_window_reduced._show_wait_dialog('Test', 'This is a test'):
pass
# THEN: The correct methods should have been called
MockProcessDialog.assert_called_once_with('This is a test', '', 0, 0, main_window_reduced)
mocked_wait_dialog.setWindowTitle.assert_called_once_with('Test')
mocked_wait_dialog.setWindowFlag.assert_called_once_with(QtCore.Qt.WindowContextHelpButtonHint, False)
mocked_wait_dialog.setWindowModality.assert_called_once_with(QtCore.Qt.WindowModal)
mocked_wait_dialog.setAutoClose.assert_called_once_with(False)
mocked_wait_dialog.setCancelButton.assert_called_once_with(None)
mocked_wait_dialog.show.assert_called_once_with()
mocked_wait_dialog.close.assert_called_once_with()
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
def test_wait_for_threads(MockApp, main_window_reduced):
"""Test that the wait_for_threads() method correctly stops the threads"""
# GIVEN: A mocked application, and a reduced main window
mocked_http_thread = MagicMock()
mocked_http_thread.isRunning.side_effect = [True, True, False, False]
mocked_http_worker = MagicMock()
mocked_http_worker.stop = MagicMock()
main_window_reduced.application.worker_threads = {
'http': {'thread': mocked_http_thread, 'worker': mocked_http_worker}
}
# WHEN: _wait_for_threads() is called
main_window_reduced._wait_for_threads()
# THEN: The correct methods should have been called
assert MockApp.processEvents.call_count == 2, 'processEvents() should have been called twice'
mocked_http_worker.stop.assert_called_once()
assert mocked_http_thread.isRunning.call_count == 4, 'isRunning() should have been called 4 times'
mocked_http_thread.wait.assert_called_once_with(100)
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
def test_wait_for_threads_no_threads(MockApp, main_window_reduced):
"""Test that the wait_for_threads() method exits early when there are no threads"""
# GIVEN: A mocked application, and a reduced main window
main_window_reduced.application.worker_threads = {}
# WHEN: _wait_for_threads() is called
main_window_reduced._wait_for_threads()
# THEN: The correct methods should have been called
assert MockApp.processEvents.call_count == 0, 'processEvents() should not have been called'
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
def test_wait_for_threads_disappearing_thread(MockApp, main_window_reduced):
"""Test that the wait_for_threads() method correctly ignores threads that resolve themselves"""
# GIVEN: A mocked application, and a reduced main window
main_window_reduced.application.worker_threads = MagicMock(**{'keys.side_effect': [['http'], []]})
# WHEN: _wait_for_threads() is called
main_window_reduced._wait_for_threads()
# THEN: The correct methods should have been called
assert MockApp.processEvents.call_count == 0, 'processEvents() should not have been called'
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
def test_wait_for_threads_stuck_thread(MockApp, main_window_reduced):
"""Test that the wait_for_threads() method correctly stops the threads"""
# GIVEN: A mocked application, and a reduced main window
mocked_http_thread = MagicMock()
mocked_http_thread.isRunning.return_value = True
mocked_http_worker = MagicMock()
mocked_http_worker.stop = MagicMock()
main_window_reduced.application.worker_threads = {
'http': {'thread': mocked_http_thread, 'worker': mocked_http_worker}
}
# WHEN: _wait_for_threads() is called
main_window_reduced._wait_for_threads()
# THEN: The correct methods should have been called
assert MockApp.processEvents.call_count == 51, 'processEvents() should have been called 51 times'
mocked_http_worker.stop.assert_called_once()
assert mocked_http_thread.isRunning.call_count == 53, 'isRunning() should have been called 53 times'
mocked_http_thread.wait.assert_called_with(100)
mocked_http_thread.terminate.assert_called_once()
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
def test_wait_for_threads_runtime_error(MockApp, main_window_reduced):
"""Test that the wait_for_threads() method handles a runtime error"""
# GIVEN: A mocked application, and a reduced main window
mocked_http_thread = MagicMock()
mocked_http_thread.isRunning.side_effect = RuntimeError
mocked_http_worker = MagicMock()
mocked_http_worker.stop = MagicMock()
main_window_reduced.application.worker_threads = {
'http': {'thread': mocked_http_thread, 'worker': mocked_http_worker}
}
# WHEN: _wait_for_threads() is called
main_window_reduced._wait_for_threads()
# THEN: The correct methods should have been called
assert MockApp.processEvents.call_count == 1, 'processEvents() should have been called once'
mocked_http_worker.stop.assert_called_once()
assert mocked_http_thread.isRunning.call_count == 1, 'isRunning() should have been called once'
mocked_http_thread.wait.assert_not_called()
mocked_http_thread.terminate.assert_not_called()

View File

@ -690,11 +690,8 @@ def test_single_click_timeout_double(mocked_make_live, mocked_make_preview, sett
@patch('openlp.core.ui.servicemanager.zipfile')
@patch('openlp.core.ui.servicemanager.ServiceManager.save_file_as')
@patch('openlp.core.ui.servicemanager.os')
@patch('openlp.core.ui.servicemanager.shutil')
@patch('openlp.core.ui.servicemanager.NamedTemporaryFile')
def test_save_file_raises_permission_error(mocked_temp_file, mocked_shutil, mocked_os, mocked_save_file_as,
mocked_zipfile, settings):
def test_save_file_raises_permission_error(mocked_shutil, mocked_save_file_as, mocked_zipfile, settings):
"""
Test that when a PermissionError is raised when trying to save a file, it is handled correctly
"""
@ -709,8 +706,7 @@ def test_save_file_raises_permission_error(mocked_temp_file, mocked_shutil, mock
service_manager.service_manager_list = MagicMock()
mocked_save_file_as.return_value = False
mocked_zipfile.ZipFile.return_value = MagicMock()
mocked_os.link.side_effect = PermissionError
mocked_shutil.copyfile.side_effect = PermissionError
mocked_shutil.move.side_effect = PermissionError
# WHEN: The service is saved and a PermissionError is raised
result = service_manager.save_file()
@ -725,9 +721,7 @@ def test_save_file_raises_permission_error(mocked_temp_file, mocked_shutil, mock
@patch('openlp.core.ui.servicemanager.os')
@patch('openlp.core.ui.servicemanager.shutil')
@patch('openlp.core.ui.servicemanager.len')
@patch('openlp.core.ui.servicemanager.NamedTemporaryFile')
def test_save_file_large_file(mocked_temp_file, mocked_len, mocked_shutil, mocked_os, mocked_save_file_as,
mocked_zipfile, registry):
def test_save_file_large_file(mocked_len, mocked_shutil, mocked_os, mocked_save_file_as, mocked_zipfile, registry):
"""
Test that when a file size size larger than a 32bit signed int is attempted to save, the progress bar
should be provided a value that fits in a 32bit int (because it's passed to C++ as a 32bit unsigned int)
@ -759,39 +753,6 @@ def test_save_file_large_file(mocked_temp_file, mocked_len, mocked_shutil, mocke
mocked_save_file_as.assert_not_called()
@patch('openlp.core.ui.servicemanager.zipfile')
@patch('openlp.core.ui.servicemanager.ServiceManager.save_file_as')
@patch('openlp.core.ui.servicemanager.os')
@patch('openlp.core.ui.servicemanager.shutil')
@patch('openlp.core.ui.servicemanager.NamedTemporaryFile')
def test_save_file_falls_back_to_shutil(mocked_temp_file, mocked_shutil, mocked_os, mocked_save_file_as, mocked_zipfile,
registry):
"""
Test that when a PermissionError is raised when trying to save a file, it is handled correctly
"""
# GIVEN: A service manager, a service to save
mocked_main_window = MagicMock()
Registry().register('main_window', mocked_main_window)
Registry().register('application', MagicMock())
Registry().register('settings', MagicMock())
service_manager = ServiceManager(None)
service_manager._service_path = MagicMock()
service_manager._save_lite = False
service_manager.service_items = []
service_manager.service_theme = 'Default'
service_manager.service_manager_list = MagicMock()
mocked_save_file_as.return_value = False
mocked_zipfile.ZipFile.return_value = MagicMock()
mocked_os.link.side_effect = OSError
# WHEN: The service is saved and a PermissionError is raised
result = service_manager.save_file()
# THEN: The result is true
assert result is True
mocked_shutil.copyfile.assert_called_once()
@patch('openlp.core.ui.servicemanager.ServiceManager.regenerate_service_items')
def test_theme_change_global(mocked_regenerate_service_items, settings):
"""

View File

@ -26,10 +26,11 @@ import datetime
from unittest.mock import MagicMock, patch, sentinel
from PyQt5 import QtCore, QtGui
from openlp.core.lib.serviceitem import ServiceItem
from openlp.core.state import State
from openlp.core.common.registry import Registry
from openlp.core.lib import ServiceItemAction
from openlp.core.lib import ItemCapabilities, ServiceItemAction
from openlp.core.ui import HideMode
from openlp.core.ui.slidecontroller import NON_TEXT_MENU, WIDE_MENU, NARROW_MENU, InfoLabel, LiveController, \
PreviewController, SlideController
@ -1141,6 +1142,54 @@ def test_process_item_is_reloading_wont_change_display_hide_mode(mocked_execute,
slide_controller.set_hide_mode.assert_not_called()
@patch.object(Registry, 'execute')
def test_process_item_provides_own_theme(mocked_execute, registry, state_media):
"""
Test that media theme is set when media item is flagged with ProvidesOwnTheme
"""
# GIVEN: A mocked presentation service item that provides it's own theme, a mocked Registry.execute
# and a slide controller with many mocks.
mocked_pres_item = MagicMock()
mocked_pres_item.name = 'mocked_presentation_item'
mocked_pres_item.is_command.return_value = True
mocked_pres_item.is_media.return_value = False
mocked_pres_item.requires_media.return_value = False
mocked_pres_item.is_image.return_value = False
mocked_pres_item.is_text.return_value = False
# Needed to perform the capability checks
mocked_pres_item.is_capable = lambda param: ServiceItem.is_capable(mocked_pres_item, param)
mocked_pres_item.from_service = False
mocked_pres_item.capabilities = [ItemCapabilities.ProvidesOwnTheme]
mocked_pres_item.get_frames.return_value = []
mocked_settings = MagicMock()
mocked_settings.value.return_value = True
mocked_main_window = MagicMock()
Registry().register('settings', mocked_settings)
Registry().register('main_window', mocked_main_window)
Registry().register('media_controller', MagicMock())
Registry().register('application', MagicMock())
slide_controller = SlideController(None)
slide_controller.service_item = mocked_pres_item
slide_controller.is_live = False
slide_controller.preview_widget = MagicMock()
slide_controller.preview_display = MagicMock()
slide_controller.enable_tool_bar = MagicMock()
slide_controller.slide_selected = MagicMock()
slide_controller.on_stop_loop = MagicMock()
slide_controller.info_label = MagicMock()
slide_controller._set_theme = MagicMock()
slide_controller.displays = [MagicMock()]
slide_controller.split = 0
slide_controller.type_prefix = 'test'
slide_controller._current_hide_mode = None
# WHEN: _process_item is called
slide_controller._process_item(mocked_pres_item, 0)
# THEN: _set_theme should be called once
slide_controller._set_theme.assert_called_once()
def test_live_stolen_focus_shortcuts(settings):
"""
Test that all the needed shortcuts are available in scenarios where Live has stolen focus.

View File

@ -32,8 +32,7 @@ from openlp.plugins.alerts.alertsplugin import AlertsPlugin
def plugin_env(mocked_manager, settings, state, registry):
"""An instance of the AlertsPlugin"""
mocked_manager.return_value = MagicMock()
with patch('openlp.plugins.alerts.alertsplugin.register_views'):
return AlertsPlugin(), settings
return AlertsPlugin(), settings
def test_plugin_about():
@ -77,12 +76,10 @@ def test_alerts_initialise(plugin_env):
plugin = plugin_env[0]
plugin.tools_alert_item = MagicMock()
# WHEN: I request the form
with patch('openlp.core.common.actions.ActionList') as mocked_actionlist, \
patch('openlp.plugins.alerts.alertsplugin.register_views') as mocked_register_views:
with patch('openlp.core.common.actions.ActionList') as mocked_actionlist:
plugin.initialise()
# THEN: the form is loaded
mocked_actionlist.instance.add_action.assert_called_once()
mocked_register_views.assert_called_once_with()
plugin.tools_alert_item.setVisible.assert_called_once_with(True)

View File

@ -21,7 +21,14 @@
"""
This module contains tests for the plugin class Presentation plugin.
"""
from pathlib import Path
from unittest.mock import MagicMock, patch
from openlp.plugins.presentations.presentationplugin import PresentationPlugin
from openlp.plugins.presentations.lib.presentationtab import PresentationTab
TEST_RESOURCES_PATH = Path(__file__) / '..' / '..' / '..' / 'resources'
def test_plugin_about():
@ -34,3 +41,33 @@ def test_plugin_about():
'programs. The choice of available presentation programs is '
'available to the user in a drop down box.'
)
def test_creaste_settings_tab(qapp, state, registry, settings):
"""Test creating the settings tab"""
# GIVEN: A Presentations plugin
presentations_plugin = PresentationPlugin()
# WHEN: create_settings_tab is run
presentations_plugin.create_settings_tab(None)
# THEN: A settings tab should have been created
assert isinstance(presentations_plugin.settings_tab, PresentationTab)
@patch('openlp.plugins.presentations.presentationplugin.Manager')
def test_initialise(MockedManager, state, registry, mock_settings):
"""Test that initialising the plugin works correctly"""
# GIVEN: Some initial values needed for intialisation and a presentations plugin
mock_settings.setValue.side_effect = [None, [str(TEST_RESOURCES_PATH / 'presentations' / 'test.ppt')]]
mocked_main_window = MagicMock()
registry.register('main_window', mocked_main_window)
presentations_plugin = PresentationPlugin()
presentations_plugin.media_item = MagicMock()
# WHEN: initialise() is called
presentations_plugin.initialise()
# THEN: Nothing should break, and everything should be called
mock_settings.setValue.assert_called_with('presentations/thumbnail_scheme', 'sha256file')
mock_settings.remove.assert_called_once_with('presentations/presentations files')

View File

@ -95,7 +95,9 @@ def test_report_song_list_error_reading(mock_file_dialog, mock_log, registry):
Test that report song list sends an exception if the selected file location is not writable
"""
# GIVEN: A mocked file that returns a os error on open
def raise_os_error(x):
def raise_os_error(mode, encoding):
assert mode == 'wt'
assert encoding == 'utf8'
raise OSError
mock_file = MagicMock()
mock_file.open.side_effect = raise_os_error

View File

@ -52,10 +52,11 @@ if CAN_RUN_TESTS:
"""
This class logs changes in the title instance variable
"""
_title_assignment_list = []
_title_assignment_list = None
def __init__(self, manager):
WorshipCenterProImport.__init__(self, manager, file_paths=[])
self._title_assignment_list = []
@property
def title(self):
@ -63,7 +64,10 @@ if CAN_RUN_TESTS:
@title.setter
def title(self, title):
self._title_assignment_list.append(title)
try:
self._title_assignment_list.append(title)
except AttributeError:
self._title_assignment_list = [title]
RECORDSET_TEST_DATA = [DBTestRecord(1, 'TITLE', 'Amazing Grace'),
@ -71,21 +75,27 @@ RECORDSET_TEST_DATA = [DBTestRecord(1, 'TITLE', 'Amazing Grace'),
DBTestRecord(1, 'CCLISONGID', '12345'),
DBTestRecord(1, 'COMMENTS', 'The original version'),
DBTestRecord(1, 'COPY', 'Public Domain'),
DBTestRecord(1, 'SUBJECT', 'Grace'),
DBTestRecord(
1, 'LYRICS',
'Amazing grace! How&crlf;sweet the sound&crlf;That saved a wretch like me!&crlf;'
'<INTRO>Amazing grace! How&crlf;sweet the sound&crlf;That saved a wretch like me!&crlf;'
'I once was lost,&crlf;but now am found;&crlf;Was blind, but now I see.&crlf;&crlf;'
'\'Twas grace that&crlf;taught my heart to fear,&crlf;And grace my fears relieved;&crlf;'
'<PRECHORUS>\'Twas grace that&crlf;taught my heart to fear,&crlf;'
'And grace my fears relieved;&crlf;'
'How precious did&crlf;that grace appear&crlf;The hour I first believed.&crlf;&crlf;'
'Through many dangers,&crlf;toils and snares,&crlf;I have already come;&crlf;'
'<CHORUS>Through many dangers,&crlf;toils and snares,&crlf;I have already come;&crlf;'
'\'Tis grace hath brought&crlf;me safe thus far,&crlf;'
'And grace will lead me home.&crlf;&crlf;The Lord has&crlf;promised good to me,&crlf;'
'And grace will lead me home.&crlf;&crlf;'
'<REFRAIN>The Lord has&crlf;promised good to me,&crlf;'
'His Word my hope secures;&crlf;He will my Shield&crlf;and Portion be,&crlf;'
'As long as life endures.&crlf;&crlf;Yea, when this flesh&crlf;and heart shall fail,&crlf;'
'As long as life endures.&crlf;&crlf;'
'<BRIDGE>Yea, when this flesh&crlf;and heart shall fail,&crlf;'
'And mortal life shall cease,&crlf;I shall possess,&crlf;within the veil,&crlf;'
'A life of joy and peace.&crlf;&crlf;The earth shall soon&crlf;dissolve like snow,&crlf;'
'A life of joy and peace.&crlf;&crlf;'
'<TAG>The earth shall soon&crlf;dissolve like snow,&crlf;'
'The sun forbear to shine;&crlf;But God, Who called&crlf;me here below,&crlf;'
'Shall be forever mine.&crlf;&crlf;When we\'ve been there&crlf;ten thousand years,&crlf;'
'Shall be forever mine.&crlf;&crlf;'
'<END>When we\'ve been there&crlf;ten thousand years,&crlf;'
'Bright shining as the sun,&crlf;We\'ve no less days to&crlf;sing God\'s praise&crlf;'
'Than when we\'d first begun.&crlf;&crlf;'),
DBTestRecord(2, 'TITLE', 'Beautiful Garden Of Prayer, The'),
@ -105,32 +115,32 @@ RECORDSET_TEST_DATA = [DBTestRecord(1, 'TITLE', 'Amazing Grace'),
SONG_TEST_DATA = [{'title': 'Amazing Grace',
'verses': [
('Amazing grace! How\nsweet the sound\nThat saved a wretch like me!\nI once was lost,\n'
'but now am found;\nWas blind, but now I see.'),
'but now am found;\nWas blind, but now I see.', 'i'),
('\'Twas grace that\ntaught my heart to fear,\nAnd grace my fears relieved;\nHow precious did\n'
'that grace appear\nThe hour I first believed.'),
'that grace appear\nThe hour I first believed.', 'p'),
('Through many dangers,\ntoils and snares,\nI have already come;\n\'Tis grace hath brought\n'
'me safe thus far,\nAnd grace will lead me home.'),
'me safe thus far,\nAnd grace will lead me home.', 'c'),
('The Lord has\npromised good to me,\nHis Word my hope secures;\n'
'He will my Shield\nand Portion be,\nAs long as life endures.'),
'He will my Shield\nand Portion be,\nAs long as life endures.', 'c'),
('Yea, when this flesh\nand heart shall fail,\nAnd mortal life shall cease,\nI shall possess,\n'
'within the veil,\nA life of joy and peace.'),
'within the veil,\nA life of joy and peace.', 'b'),
('The earth shall soon\ndissolve like snow,\nThe sun forbear to shine;\nBut God, Who called\n'
'me here below,\nShall be forever mine.'),
'me here below,\nShall be forever mine.', 'o'),
('When we\'ve been there\nten thousand years,\nBright shining as the sun,\n'
'We\'ve no less days to\nsing God\'s praise\nThan when we\'d first begun.')],
'We\'ve no less days to\nsing God\'s praise\nThan when we\'d first begun.', 'e')],
'author': 'John Newton',
'comments': 'The original version',
'copyright': 'Public Domain'},
{'title': 'Beautiful Garden Of Prayer, The',
'verses': [
('There\'s a garden where\nJesus is waiting,\nThere\'s a place that\nis wondrously fair,\n'
'For it glows with the\nlight of His presence.\n\'Tis the beautiful\ngarden of prayer.'),
'For it glows with the\nlight of His presence.\n\'Tis the beautiful\ngarden of prayer.', 'v'),
('Oh, the beautiful garden,\nthe garden of prayer!\nOh, the beautiful\ngarden of prayer!\n'
'There my Savior awaits,\nand He opens the gates\nTo the beautiful\ngarden of prayer.'),
'There my Savior awaits,\nand He opens the gates\nTo the beautiful\ngarden of prayer.', 'v'),
('There\'s a garden where\nJesus is waiting,\nAnd I go with my\nburden and care,\n'
'Just to learn from His\nlips words of comfort\nIn the beautiful\ngarden of prayer.'),
'Just to learn from His\nlips words of comfort\nIn the beautiful\ngarden of prayer.', 'v'),
('There\'s a garden where\nJesus is waiting,\nAnd He bids you to come,\nmeet Him there;\n'
'Just to bow and\nreceive a new blessing\nIn the beautiful\ngarden of prayer.')]}]
'Just to bow and\nreceive a new blessing\nIn the beautiful\ngarden of prayer.', 'v')]}]
@skipUnless(CAN_RUN_TESTS, 'Not Windows, skipping test')
@ -233,11 +243,44 @@ class TestWorshipCenterProSongImport(TestCase):
verse_calls = song_data['verses']
add_verse_call_count += len(verse_calls)
for call in verse_calls:
mocked_add_verse.assert_any_call(call, 'v')
mocked_add_verse.assert_any_call(*call)
if 'author' in song_data:
mocked_parse_author.assert_any_call(song_data['author'])
if 'comments' in song_data:
mocked_add_comment.assert_any_call(song_data['comments'])
if 'copyright' in song_data:
mocked_add_copyright.assert_any_call(song_data['copyright'])
if 'subject' in song_data:
mocked_add_copyright.assert_any_call(song_data['subject'])
assert mocked_add_verse.call_count == add_verse_call_count, 'Incorrect number of calls made to add_verse'
def test_song_import_stop(self):
"""Test that the song importer stops when the flag is set"""
with patch('openlp.plugins.songs.lib.importers.worshipcenterpro.SongImport'), \
patch('openlp.plugins.songs.lib.importers.worshipcenterpro.pyodbc') as mocked_pyodbc, \
patch('openlp.plugins.songs.lib.importers.worshipcenterpro.translate') as mocked_translate:
mocked_manager = MagicMock()
mocked_import_wizard = MagicMock()
mocked_add_verse = MagicMock()
mocked_parse_author = MagicMock()
mocked_add_comment = MagicMock()
mocked_add_copyright = MagicMock()
mocked_finish = MagicMock()
mocked_pyodbc.connect().cursor().fetchall.return_value = RECORDSET_TEST_DATA
mocked_translate.return_value = 'Translated Text'
importer = WorshipCenterProImportLogger(mocked_manager)
importer.import_source = 'import_source'
importer.import_wizard = mocked_import_wizard
importer.add_verse = mocked_add_verse
importer.parse_author = mocked_parse_author
importer.add_comment = mocked_add_comment
importer.add_copyright = mocked_add_copyright
importer.stop_import_flag = True
importer.finish = mocked_finish
# WHEN: Calling the do_import method
importer.do_import()
# THEN: No songs should have been imported
assert len(importer._title_assignment_list) == 0
mocked_finish.assert_not_called()