diff --git a/copyright.txt b/copyright.txt index f56b7712d..64d028205 100644 --- a/copyright.txt +++ b/copyright.txt @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 ############################################################################### # OpenLP - Open Source Lyrics Projection # diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 7d198db5e..4bb0f0d28 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -184,7 +184,8 @@ class OpenLP(QtGui.QApplication): ``traceback`` A traceback object with the details of where the exception occurred. """ - log.exception(''.join(format_exception(exctype, value, traceback))) + # We can't log.exception here because the last exception no longer exists, we're actually busy handling it. + log.critical(''.join(format_exception(exctype, value, traceback))) if not hasattr(self, 'exception_form'): self.exception_form = ExceptionForm() self.exception_form.exception_text_edit.setPlainText(''.join(format_exception(exctype, value, traceback))) diff --git a/openlp/core/common/uistrings.py b/openlp/core/common/uistrings.py index d7a4c1c42..7368e5011 100644 --- a/openlp/core/common/uistrings.py +++ b/openlp/core/common/uistrings.py @@ -83,6 +83,8 @@ class UiStrings(object): self.Error = translate('OpenLP.Ui', 'Error') self.Export = translate('OpenLP.Ui', 'Export') self.File = translate('OpenLP.Ui', 'File') + self.FileNotFound = translate('OpenLP.Ui', 'File Not Found') + self.FileNotFoundMessage = translate('OpenLP.Ui', 'File %s not found.\nPlease try selecting it individually.') self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit') self.Help = translate('OpenLP.Ui', 'Help') self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours') diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 67ac409df..a46e25249 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -330,6 +330,7 @@ def create_separated_list(string_list): from .registry import Registry +from .filedialog import FileDialog from .screen import ScreenList from .listwidgetwithdnd import ListWidgetWithDnD from .treewidgetwithdnd import TreeWidgetWithDnD @@ -345,4 +346,3 @@ from .dockwidget import OpenLPDockWidget from .imagemanager import ImageManager from .renderer import Renderer from .mediamanageritem import MediaManagerItem - diff --git a/openlp/core/lib/filedialog.py b/openlp/core/lib/filedialog.py new file mode 100644 index 000000000..bac1b5ce2 --- /dev/null +++ b/openlp/core/lib/filedialog.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2013 Raoul Snyman # +# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky # +# --------------------------------------------------------------------------- # +# 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + +""" +Provide a work around for a bug in QFileDialog +""" +import logging +import os +from urllib import parse + +from PyQt4 import QtGui + +from openlp.core.common import UiStrings + +log = logging.getLogger(__name__) + +class FileDialog(QtGui.QFileDialog): + """ + Subclass QFileDialog to work round a bug + """ + @staticmethod + def getOpenFileNames(parent, *args, **kwargs): + """ + Reimplement getOpenFileNames to fix the way it returns some file names that url encoded when selecting multiple + files + """ + files = QtGui.QFileDialog.getOpenFileNames(parent, *args, **kwargs) + file_list = [] + for file in files: + if not os.path.exists(file): + log.info('File %s not found. Attempting to unquote.') + file = parse.unquote(file) + if not os.path.exists(file): + log.error('File %s not found.' % file) + QtGui.QMessageBox.information(parent, UiStrings().FileNotFound, + UiStrings().FileNotFoundMessage % file) + continue + log.info('File %s found.') + file_list.append(file) + return file_list \ No newline at end of file diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 01e16eef3..22f03355f 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -36,7 +36,7 @@ import re from PyQt4 import QtCore, QtGui from openlp.core.common import Settings, UiStrings, translate -from openlp.core.lib import OpenLPToolbar, ServiceItem, StringContent, ListWidgetWithDnD, \ +from openlp.core.lib import FileDialog, OpenLPToolbar, ServiceItem, StringContent, ListWidgetWithDnD, \ ServiceItemContext, Registry from openlp.core.lib.searchedit import SearchEdit from openlp.core.lib.ui import create_widget_action, critical_error_message_box @@ -319,7 +319,7 @@ class MediaManagerItem(QtGui.QWidget): """ Add a file to the list widget to make it available for showing """ - files = QtGui.QFileDialog.getOpenFileNames(self, self.on_new_prompt, + files = FileDialog.getOpenFileNames(self, self.on_new_prompt, Settings().value(self.settings_section + '/last directory'), self.on_new_file_masks) log.info('New files(s) %s', files) if files: diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index 50bdc57fa..83bd83e77 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -283,7 +283,7 @@ class AdvancedTab(SettingsTab): self.service_name_day.setItemText(0, translate('OpenLP.AdvancedTab', 'Monday')) self.service_name_day.setItemText(1, translate('OpenLP.AdvancedTab', 'Tuesday')) self.service_name_day.setItemText(2, translate('OpenLP.AdvancedTab', 'Wednesday')) - self.service_name_day.setItemText(3, translate('OpenLP.AdvancedTab', 'Thurdsday')) + self.service_name_day.setItemText(3, translate('OpenLP.AdvancedTab', 'Thursday')) self.service_name_day.setItemText(4, translate('OpenLP.AdvancedTab', 'Friday')) self.service_name_day.setItemText(5, translate('OpenLP.AdvancedTab', 'Saturday')) self.service_name_day.setItemText(6, translate('OpenLP.AdvancedTab', 'Sunday')) diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index b2427e009..f1e57a545 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -101,7 +101,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog): """ Constructor. """ - super(ExceptionForm, self).__init__(self.main_window) + super(ExceptionForm, self).__init__() self.setupUi(self) self.settings_section = 'crashreport' diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 7e55a8b2c..68f5acceb 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -38,7 +38,7 @@ from xml.etree.ElementTree import ElementTree, XML from PyQt4 import QtCore, QtGui from openlp.core.common import AppLocation, Settings, check_directory_exists, UiStrings, translate -from openlp.core.lib import ImageSource, OpenLPToolbar, Registry, get_text_file_string, build_icon, \ +from openlp.core.lib import FileDialog, ImageSource, OpenLPToolbar, Registry, get_text_file_string, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.theme import ThemeXML, BackgroundType from openlp.core.lib.ui import critical_error_message_box, create_widget_action @@ -365,7 +365,7 @@ class ThemeManager(QtGui.QWidget, ThemeManagerHelper): Opens a file dialog to select the theme file(s) to import before attempting to extract OpenLP themes from those files. This process will load both OpenLP version 1 and version 2 themes. """ - files = QtGui.QFileDialog.getOpenFileNames(self, + files = FileDialog.getOpenFileNames(self, translate('OpenLP.ThemeManager', 'Select Theme Import File'), Settings().value(self.settings_section + '/last directory import'), translate('OpenLP.ThemeManager', 'OpenLP Themes (*.theme *.otz)')) diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index 22ff6185a..7c9c9808f 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -379,7 +379,7 @@ class BSExtract(object): send_error_message('parse') return None content = content.find_all('li') - return [book.contents[0].contents[0] for book in content] + return [book.contents[0].contents[0] for book in content if len(book.contents[0].contents)] def _get_application(self): """ diff --git a/openlp/plugins/remotes/lib/httprouter.py b/openlp/plugins/remotes/lib/httprouter.py index ce13ed812..deba92c10 100644 --- a/openlp/plugins/remotes/lib/httprouter.py +++ b/openlp/plugins/remotes/lib/httprouter.py @@ -128,6 +128,15 @@ from openlp.core.common import AppLocation, Settings, translate from openlp.core.lib import Registry, PluginStatus, StringContent, image_to_byte log = logging.getLogger(__name__) +FILE_TYPES = { + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.jpg': 'image/jpeg', + '.gif': 'image/gif', + '.ico': 'image/x-icon', + '.png': 'image/png' +} class HttpRouter(object): @@ -346,30 +355,13 @@ class HttpRouter(object): path = os.path.normpath(os.path.join(self.html_dir, file_name)) if not path.startswith(self.html_dir): return self.do_not_found() - ext = os.path.splitext(file_name)[1] - html = None - if ext == '.html': - self.send_header('Content-type', 'text/html') - variables = self.template_vars - html = Template(filename=path, input_encoding='utf-8', output_encoding='utf-8').render(**variables) - elif ext == '.css': - self.send_header('Content-type', 'text/css') - elif ext == '.js': - self.send_header('Content-type', 'application/javascript') - elif ext == '.jpg': - self.send_header('Content-type', 'image/jpeg') - elif ext == '.gif': - self.send_header('Content-type', 'image/gif') - elif ext == '.ico': - self.send_header('Content-type', 'image/x-icon') - elif ext == '.png': - self.send_header('Content-type', 'image/png') - else: - self.send_header('Content-type', 'text/plain') + content = None + ext, content_type = self.get_content_type(path) file_handle = None try: - if html: - content = html + if ext == '.html': + variables = self.template_vars + content = Template(filename=path, input_encoding='utf-8', output_encoding='utf-8').render(**variables) else: file_handle = open(path, 'rb') log.debug('Opened %s' % path) @@ -380,8 +372,22 @@ class HttpRouter(object): finally: if file_handle: file_handle.close() + self.send_response(200) + self.send_header('Content-type', content_type) + self.end_headers() return content + def get_content_type(self, file_name): + """ + Examines the extension of the file and determines + what the content_type should be, defaults to text/plain + Returns the extension and the content_type + """ + content_type = 'text/plain' + ext = os.path.splitext(file_name)[1] + content_type = FILE_TYPES.get(ext, 'text/plain') + return ext, content_type + def poll(self): """ Poll OpenLP to determine the current slide number and item name. diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index ed4676929..e14652765 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -39,7 +39,7 @@ import shutil from PyQt4 import QtCore, QtGui from openlp.core.common import AppLocation, UiStrings, check_directory_exists, translate -from openlp.core.lib import Registry, PluginStatus, MediaType, create_separated_list +from openlp.core.lib import FileDialog, Registry, PluginStatus, MediaType, create_separated_list from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box from openlp.plugins.songs.lib import VerseType, clean_song from openlp.plugins.songs.lib.db import Book, Song, Author, Topic, MediaFile @@ -758,7 +758,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): Loads file(s) from the filesystem. """ filters = '%s (*)' % UiStrings().AllFiles - filenames = QtGui.QFileDialog.getOpenFileNames(self, + filenames = FileDialog.getOpenFileNames(self, translate('SongsPlugin.EditSongForm', 'Open File(s)'), '', filters) for filename in filenames: item = QtGui.QListWidgetItem(os.path.split(str(filename))[1]) diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 2105e5e35..5976b41bc 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -37,7 +37,7 @@ from PyQt4 import QtCore, QtGui from openlp.core.common import UiStrings, translate from openlp.core.common import Settings -from openlp.core.lib import Registry +from openlp.core.lib import FileDialog, Registry from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib.importer import SongFormat, SongFormatSelect @@ -246,7 +246,7 @@ class SongImportForm(OpenLPWizard): if filters: filters += ';;' filters += '%s (*)' % UiStrings().AllFiles - filenames = QtGui.QFileDialog.getOpenFileNames(self, title, + filenames = FileDialog.getOpenFileNames(self, title, Settings().value(self.plugin.settings_section + '/last directory import'), filters) if filenames: listbox.addItems(filenames) diff --git a/openlp/plugins/songs/lib/songbeamerimport.py b/openlp/plugins/songs/lib/songbeamerimport.py index f00e98e63..79e6f6263 100644 --- a/openlp/plugins/songs/lib/songbeamerimport.py +++ b/openlp/plugins/songs/lib/songbeamerimport.py @@ -102,10 +102,10 @@ class SongBeamerImport(SongImport): """ Receive a single file or a list of files to import. """ - self.import_wizard.progress_bar.setMaximum(len(self.import_source)) if not isinstance(self.import_source, list): return - for file in self.import_source: + self.import_wizard.progress_bar.setMaximum(len(self.import_source)) + for import_file in self.import_source: # TODO: check that it is a valid SongBeamer file if self.stop_import_flag: return @@ -113,12 +113,13 @@ class SongBeamerImport(SongImport): self.currentVerse = '' self.currentVerseType = VerseType.tags[VerseType.Verse] read_verses = False - file_name = os.path.split(file)[1] - if os.path.isfile(file): - detect_file = open(file, 'r') + file_name = os.path.split(import_file)[1] + if os.path.isfile(import_file): + # First open in binary mode to detect the encoding + detect_file = open(import_file, 'rb') details = chardet.detect(detect_file.read()) detect_file.close() - infile = codecs.open(file, 'r', details['encoding']) + infile = codecs.open(import_file, 'r', details['encoding']) song_data = infile.readlines() infile.close() else: @@ -149,7 +150,7 @@ class SongBeamerImport(SongImport): self.replaceHtmlTags() self.addVerse(self.currentVerse, self.currentVerseType) if not self.finish(): - self.logError(file) + self.logError(import_file) def replaceHtmlTags(self): """ diff --git a/tests/functional/openlp_core_lib/test_file_dialog.py b/tests/functional/openlp_core_lib/test_file_dialog.py new file mode 100644 index 000000000..f42a865d7 --- /dev/null +++ b/tests/functional/openlp_core_lib/test_file_dialog.py @@ -0,0 +1,73 @@ +""" +Package to test the openlp.core.lib.filedialog package. +""" +from unittest import TestCase + +from openlp.core.common import UiStrings +from openlp.core.lib.filedialog import FileDialog +from tests.functional import MagicMock, patch + +class TestFileDialog(TestCase): + """ + Test the functions in the :mod:`filedialog` module. + """ + def setUp(self): + self.os_patcher = patch('openlp.core.lib.filedialog.os') + self.qt_gui_patcher = patch('openlp.core.lib.filedialog.QtGui') + self.ui_strings_patcher = patch('openlp.core.lib.filedialog.UiStrings') + self.mocked_os = self.os_patcher.start() + self.mocked_qt_gui = self.qt_gui_patcher.start() + self.mocked_ui_strings = self.ui_strings_patcher.start() + self.mocked_parent = MagicMock() + + def tearDown(self): + self.os_patcher.stop() + self.qt_gui_patcher.stop() + self.ui_strings_patcher.stop() + + def get_open_file_names_canceled_test(self): + """ + Test that FileDialog.getOpenFileNames() returns and empty QStringList when QFileDialog is canceled + (returns an empty QStringList) + """ + self.mocked_os.reset() + + # GIVEN: An empty QStringList as a return value from QFileDialog.getOpenFileNames + self.mocked_qt_gui.QFileDialog.getOpenFileNames.return_value = [] + + # WHEN: FileDialog.getOpenFileNames is called + result = FileDialog.getOpenFileNames(self.mocked_parent) + + # THEN: The returned value should be an empty QStringList and os.path.exists should not have been called + assert not self.mocked_os.path.exists.called + self.assertEqual(result, [], + 'FileDialog.getOpenFileNames should return and empty list when QFileDialog.getOpenFileNames is canceled') + + def returned_file_list_test(self): + """ + Test that FileDialog.getOpenFileNames handles a list of files properly when QFileList.getOpenFileNames + returns a good file name, a urlencoded file name and a non-existing file + """ + self.mocked_os.rest() + self.mocked_qt_gui.reset() + + # GIVEN: A List of known values as a return value from QFileDialog.getOpenFileNames and a list of valid + # file names. + self.mocked_qt_gui.QFileDialog.getOpenFileNames.return_value = [ + '/Valid File', '/url%20encoded%20file%20%231', '/non-existing'] + self.mocked_os.path.exists.side_effect = lambda file_name: file_name in [ + '/Valid File', '/url encoded file #1'] + + # WHEN: FileDialog.getOpenFileNames is called + result = FileDialog.getOpenFileNames(self.mocked_parent) + + # THEN: os.path.exists should have been called with known args. QmessageBox.information should have been + # called. The returned result should corrilate with the input. + self.mocked_os.path.exists.assert_callde_with('/Valid File') + self.mocked_os.path.exists.assert_callde_with('/url%20encoded%20file%20%231') + self.mocked_os.path.exists.assert_callde_with('/url encoded file #1') + self.mocked_os.path.exists.assert_callde_with('/non-existing') + self.mocked_os.path.exists.assert_callde_with('/non-existing') + self.mocked_qt_gui.QmessageBox.information.called_with(self.mocked_parent, UiStrings().FileNotFound, + UiStrings().FileNotFoundMessage % '/non-existing') + self.assertEqual(result, ['/Valid File', '/url encoded file #1'], 'The returned file list is incorrect') \ No newline at end of file diff --git a/tests/functional/openlp_core_lib/test_htmlbuilder.py b/tests/functional/openlp_core_lib/test_htmlbuilder.py index ec8e3a4a3..fafa277d7 100644 --- a/tests/functional/openlp_core_lib/test_htmlbuilder.py +++ b/tests/functional/openlp_core_lib/test_htmlbuilder.py @@ -3,13 +3,13 @@ Package to test the openlp.core.lib.htmlbuilder module. """ from unittest import TestCase -from mock import MagicMock, patch from PyQt4 import QtCore from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \ build_lyrics_format_css, build_footer_css from openlp.core.lib.theme import HorizontalType, VerticalType +from tests.functional import MagicMock, patch HTML = """ @@ -216,9 +216,9 @@ class Htmbuilder(TestCase): is_live = False background = None plugin = MagicMock() - plugin.get_display_css = MagicMock(return_value='plugin CSS') - plugin.get_display_javascript = MagicMock(return_value='plugin JS') - plugin.get_display_html = MagicMock(return_value='plugin HTML') + plugin.get_display_css.return_value = 'plugin CSS' + plugin.get_display_javascript.return_value = 'plugin JS' + plugin.get_display_html.return_value = 'plugin HTML' plugins = [plugin] # WHEN: Create the html. diff --git a/tests/functional/openlp_plugins/bibles/test_http.py b/tests/functional/openlp_plugins/bibles/test_http.py new file mode 100644 index 000000000..c156fad22 --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_http.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2013 Raoul Snyman # +# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +This module contains tests for the http module of the Bibles plugin. +""" +from unittest import TestCase +from bs4 import BeautifulSoup + +from tests.functional import patch, MagicMock +from openlp.plugins.bibles.lib.http import BSExtract + +#TODO: Items left to test +# BGExtract +# __init__ +# _remove_elements +# _extract_verse +# _clean_soup +# _extract_verses +# _extract_verses_old +# get_bible_chapter +# get_books_from_http +# _get_application +# CWExtract +# __init__ +# get_bible_chapter +# get_books_from_http +# _get_application +# HTTPBible +# __init__ +# do_import +# get_verses +# get_chapter +# get_books +# get_chapter_count +# get_verse_count +# _get_application +# get_soup_for_bible_ref +# send_error_message + +class TestBSExtract(TestCase): + """ + Test the BSExtractClass + """ + #TODO: Items left to test + # BSExtract + # __init__ + # get_bible_chapter + # get_books_from_http + # _get_application + def setUp(self): + self.get_soup_for_bible_ref_patcher = patch('openlp.plugins.bibles.lib.http.get_soup_for_bible_ref') + self.log_patcher = patch('openlp.plugins.bibles.lib.http.log') + self.send_error_message_patcher = patch('openlp.plugins.bibles.lib.http.send_error_message') + self.socket_patcher = patch('openlp.plugins.bibles.lib.http.socket') + self.urllib_patcher = patch('openlp.plugins.bibles.lib.http.urllib') + + self.mock_get_soup_for_bible_ref = self.get_soup_for_bible_ref_patcher.start() + self.mock_log = self.log_patcher.start() + self.mock_send_error_message = self.send_error_message_patcher.start() + self.mock_socket = self.socket_patcher.start() + self.mock_soup = MagicMock() + self.mock_urllib = self.urllib_patcher.start() + + def tearDown(self): + self.get_soup_for_bible_ref_patcher.stop() + self.log_patcher.stop() + self.send_error_message_patcher.stop() + self.socket_patcher.stop() + self.urllib_patcher.stop() + + def get_books_from_http_no_soup_test(self): + """ + Test the get_books_from_http method when get_soup_for_bible_ref returns a falsey value + """ + # GIVEN: An instance of BSExtract, and reset log, urllib & get_soup_for_bible_ref mocks + instance = BSExtract() + self.mock_log.debug.reset_mock() + self.mock_urllib.reset_mock() + self.mock_get_soup_for_bible_ref.reset_mock() + + # WHEN: get_books_from_http is called with 'NIV' and get_soup_for_bible_ref returns a None value + self.mock_urllib.parse.quote.return_value = 'NIV' + self.mock_get_soup_for_bible_ref.return_value = None + result = instance.get_books_from_http('NIV') + + # THEN: The rest mocks should be called with known values and get_books_from_http should return None + self.mock_log.debug.assert_called_once_with('BSExtract.get_books_from_http("%s")', 'NIV') + self.mock_urllib.parse.quote.assert_called_once_with(b'NIV') + self.mock_get_soup_for_bible_ref.assert_called_once_with( + 'http://m.bibleserver.com/overlay/selectBook?translation=NIV') + self.assertIsNone(result, + 'BSExtract.get_books_from_http should return None when get_soup_for_bible_ref returns a false value') + + def get_books_from_http_no_content_test(self): + """ + Test the get_books_from_http method when the specified element cannot be found in the tag object returned from + get_soup_for_bible_ref + """ + # GIVEN: An instance of BSExtract, and reset log, urllib, get_soup_for_bible_ref & soup mocks + instance = BSExtract() + self.mock_log.reset_mock() + self.mock_urllib.reset_mock() + self.mock_get_soup_for_bible_ref.reset_mock() + self.mock_soup.reset_mock() + + # WHEN: get_books_from_http is called with 'NIV', get_soup_for_bible_ref returns a mocked_soup object and + # mocked_soup.find returns None + self.mock_urllib.parse.quote.return_value = 'NIV' + self.mock_soup.find.return_value = None + self.mock_get_soup_for_bible_ref.return_value = self.mock_soup + result = instance.get_books_from_http('NIV') + + # THEN: The rest mocks should be called with known values and get_books_from_http should return None + self.mock_log.debug.assert_called_once_with('BSExtract.get_books_from_http("%s")', 'NIV') + self.mock_urllib.parse.quote.assert_called_once_with(b'NIV') + self.mock_get_soup_for_bible_ref.assert_called_once_with( + 'http://m.bibleserver.com/overlay/selectBook?translation=NIV') + self.mock_soup.find.assert_called_once_with('ul') + self.mock_log.error.assert_called_once_with('No books found in the Bibleserver response.') + self.mock_send_error_message.assert_called_once_with('parse') + self.assertIsNone(result, + 'BSExtract.get_books_from_http should return None when get_soup_for_bible_ref returns a false value') + + def get_books_from_http_content_test(self): + """ + Test the get_books_from_http method with sample HTML + Also a regression test for bug #1184869. (The anchor tag in the second list item is empty) + """ + # GIVEN: An instance of BSExtract, and reset log, urllib & get_soup_for_bible_ref mocks and sample HTML data + self.test_html = '' + self.test_soup = BeautifulSoup(self.test_html) + instance = BSExtract() + self.mock_log.reset_mock() + self.mock_urllib.reset_mock() + self.mock_get_soup_for_bible_ref.reset_mock() + self.mock_send_error_message.reset_mock() + + # WHEN: get_books_from_http is called with 'NIV' and get_soup_for_bible_ref returns tag object based on the + # supplied test data. + self.mock_urllib.parse.quote.return_value = 'NIV' + self.mock_get_soup_for_bible_ref.return_value = self.test_soup + result = instance.get_books_from_http('NIV') + + # THEN: The rest mocks should be called with known values and get_books_from_http should return the two books + # in the test data + self.mock_log.debug.assert_called_once_with('BSExtract.get_books_from_http("%s")', 'NIV') + self.mock_urllib.parse.quote.assert_called_once_with(b'NIV') + self.mock_get_soup_for_bible_ref.assert_called_once_with( + 'http://m.bibleserver.com/overlay/selectBook?translation=NIV') + self.assertFalse(self.mock_log.error.called, 'log.error should not have been called') + self.assertFalse(self.mock_send_error_message.called, 'send_error_message should not have been called') + self.assertEquals(result, ['Genesis', 'Leviticus']) diff --git a/tests/functional/openlp_plugins/remotes/test_remotetab.py b/tests/functional/openlp_plugins/remotes/test_remotetab.py index 067c5cff1..52aeeee99 100644 --- a/tests/functional/openlp_plugins/remotes/test_remotetab.py +++ b/tests/functional/openlp_plugins/remotes/test_remotetab.py @@ -62,7 +62,7 @@ class TestRemoteTab(TestCase): """ Create the UI """ - fd, self.ini_file = mkstemp('.ini') + self.fd, self.ini_file = mkstemp('.ini') Settings().set_filename(self.ini_file) self.application = QtGui.QApplication.instance() Settings().extend_default_settings(__default_settings__) @@ -76,6 +76,7 @@ class TestRemoteTab(TestCase): del self.application del self.parent del self.form + os.close(self.fd) os.unlink(self.ini_file) def get_ip_address_default_test(self): diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py index a9ba16bf8..4d7da2b91 100644 --- a/tests/functional/openlp_plugins/remotes/test_router.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -37,7 +37,8 @@ from PyQt4 import QtGui from openlp.core.common import Settings from openlp.plugins.remotes.lib.httpserver import HttpRouter -from tests.functional import MagicMock +from tests.functional import MagicMock, patch +from mock import mock_open __default_settings__ = { 'remotes/twelve hour': True, @@ -50,6 +51,7 @@ __default_settings__ = { 'remotes/ip address': '0.0.0.0' } +TEST_PATH = os.path.abspath(os.path.dirname(__file__)) class TestRouter(TestCase): """ @@ -59,7 +61,7 @@ class TestRouter(TestCase): """ Create the UI """ - fd, self.ini_file = mkstemp('.ini') + self.fd, self.ini_file = mkstemp('.ini') Settings().set_filename(self.ini_file) self.application = QtGui.QApplication.instance() Settings().extend_default_settings(__default_settings__) @@ -70,6 +72,7 @@ class TestRouter(TestCase): Delete all the C++ objects at the end so that we don't have a segfault """ del self.application + os.close(self.fd) os.unlink(self.ini_file) def password_encrypter_test(self): @@ -109,4 +112,63 @@ class TestRouter(TestCase): assert function['function'] == mocked_function, \ 'The mocked function should match defined value.' assert function['secure'] == False, \ - 'The mocked function should not require any security.' \ No newline at end of file + 'The mocked function should not require any security.' + + def get_content_type_test(self): + """ + Test the get_content_type logic + """ + # GIVEN: a set of files and their corresponding types + headers = [ ['test.html', 'text/html'], ['test.css', 'text/css'], + ['test.js', 'application/javascript'], ['test.jpg', 'image/jpeg'], + ['test.gif', 'image/gif'], ['test.ico', 'image/x-icon'], + ['test.png', 'image/png'], ['test.whatever', 'text/plain'], + ['test', 'text/plain'], ['', 'text/plain'], + [os.path.join(TEST_PATH,'test.html'), 'text/html']] + # WHEN: calling each file type + for header in headers: + ext, content_type = self.router.get_content_type(header[0]) + # THEN: all types should match + self.assertEqual(content_type, header[1], 'Mismatch of content type') + + def serve_file_without_params_test(self): + """ + Test the serve_file method without params + """ + # GIVEN: mocked environment + self.router.send_response = MagicMock() + self.router.send_header = MagicMock() + self.router.end_headers = MagicMock() + self.router.wfile = MagicMock() + self.router.html_dir = os.path.normpath('test/dir') + self.router.template_vars = MagicMock() + # WHEN: call serve_file with no file_name + self.router.serve_file() + # THEN: it should return a 404 + self.router.send_response.assert_called_once_with(404) + self.router.send_header.assert_called_once_with('Content-type','text/html') + self.assertEqual(self.router.end_headers.call_count, 1, + 'end_headers called once') + + def serve_file_with_valid_params_test(self): + """ + Test the serve_file method with an existing file + """ + # GIVEN: mocked environment + self.router.send_response = MagicMock() + self.router.send_header = MagicMock() + self.router.end_headers = MagicMock() + self.router.wfile = MagicMock() + self.router.html_dir = os.path.normpath('test/dir') + self.router.template_vars = MagicMock() + with patch('openlp.core.lib.os.path.exists') as mocked_exists, \ + patch('builtins.open', mock_open(read_data='123')): + mocked_exists.return_value = True + # WHEN: call serve_file with an existing html file + self.router.serve_file(os.path.normpath('test/dir/test.html')) + # THEN: it should return a 200 and the file + self.router.send_response.assert_called_once_with(200) + self.router.send_header.assert_called_once_with( + 'Content-type','text/html') + self.assertEqual(self.router.end_headers.call_count, 1, + 'end_headers called once') diff --git a/tests/functional/openlp_plugins/songs/test_songbeamerimport.py b/tests/functional/openlp_plugins/songs/test_songbeamerimport.py new file mode 100644 index 000000000..37d4a1223 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_songbeamerimport.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2013 Raoul Snyman # +# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +This module contains tests for the Songbeamer song importer. +""" + +import os +from unittest import TestCase + +from tests.functional import MagicMock, patch +from openlp.plugins.songs.lib.songbeamerimport import SongBeamerImport + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..', 'resources', 'songbeamersongs')) +SONG_TEST_DATA = {'Lobsinget dem Herrn.sng': + {'title': 'GL 1 - Lobsinget dem Herrn', + 'verses': + [('1. Lobsinget dem Herrn,\no preiset Ihn gern!\n' + 'Anbetung und Lob Ihm gebühret.\n', 'v'), + ('2. Lobsingt Seiner Lieb´,\ndie einzig ihn trieb,\n' + 'zu sterben für unsere Sünden!\n', 'v'), + ('3. Lobsingt Seiner Macht!\nSein Werk ist vollbracht:\n' + 'Er sitzet zur Rechten des Vaters.\n', 'v'), + ('4. Lobsingt seiner Treu´,\ndie immerdar neu,\n' + 'bis Er uns zur Herrlichket führet!\n\n', 'v')], + 'song_book_name': 'Glaubenslieder I', + 'song_number': "1"} + } + +class TestSongBeamerImport(TestCase): + """ + Test the functions in the :mod:`songbeamerimport` module. + """ + def create_importer_test(self): + """ + Test creating an instance of the SongBeamer file importer + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + with patch('openlp.plugins.songs.lib.songbeamerimport.SongImport'): + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = SongBeamerImport(mocked_manager) + + # THEN: The importer object should not be None + self.assertIsNotNone(importer, 'Import should not be none') + + def invalid_import_source_test(self): + """ + Test SongBeamerImport.doImport handles different invalid import_source values + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + with patch('openlp.plugins.songs.lib.songbeamerimport.SongImport'): + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = SongBeamerImport(mocked_manager) + importer.import_wizard = mocked_import_wizard + importer.stop_import_flag = True + + # WHEN: Import source is not a list + for source in ['not a list', 0]: + importer.import_source = source + + # THEN: doImport should return none and the progress bar maximum should not be set. + self.assertIsNone(importer.doImport(), 'doImport should return None when import_source is not a list') + self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False, + 'setMaxium on import_wizard.progress_bar should not have been called') + + def valid_import_source_test(self): + """ + Test SongBeamerImport.doImport handles different invalid import_source values + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + with patch('openlp.plugins.songs.lib.songbeamerimport.SongImport'): + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = SongBeamerImport(mocked_manager) + importer.import_wizard = mocked_import_wizard + importer.stop_import_flag = True + + # WHEN: Import source is a list + importer.import_source = ['List', 'of', 'files'] + + # THEN: doImport should return none and the progress bar setMaximum should be called with the length of + # import_source. + self.assertIsNone(importer.doImport(), + 'doImport should return None when import_source is a list and stop_import_flag is True') + mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source)) + + def file_import_test(self): + """ + Test the actual import of real song files and check that the imported data is correct. + """ + + # GIVEN: Test files with a mocked out SongImport class, a mocked out "manager", a mocked out "import_wizard", + # and mocked out "author", "add_copyright", "add_verse", "finish" methods. + with patch('openlp.plugins.songs.lib.songbeamerimport.SongImport'): + for song_file in SONG_TEST_DATA: + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + mocked_add_verse = MagicMock() + mocked_finish = MagicMock() + mocked_finish.return_value = True + importer = SongBeamerImport(mocked_manager) + importer.import_wizard = mocked_import_wizard + importer.stop_import_flag = False + importer.addVerse = mocked_add_verse + importer.finish = mocked_finish + + # WHEN: Importing each file + importer.import_source = [os.path.join(TEST_PATH, song_file)] + title = SONG_TEST_DATA[song_file]['title'] + add_verse_calls = SONG_TEST_DATA[song_file]['verses'] + song_book_name = SONG_TEST_DATA[song_file]['song_book_name'] + song_number = SONG_TEST_DATA[song_file]['song_number'] + + # THEN: doImport should return none, the song data should be as expected, and finish should have been + # called. + self.assertIsNone(importer.doImport(), 'doImport should return None when it has completed') + self.assertEquals(importer.title, title, 'title for %s should be "%s"' % (song_file, title)) + for verse_text, verse_tag in add_verse_calls: + mocked_add_verse.assert_any_call(verse_text, verse_tag) + if song_book_name: + self.assertEquals(importer.songBookName, song_book_name, 'songBookName for %s should be "%s"' + % (song_file, song_book_name)) + if song_number: + self.assertEquals(importer.songNumber, song_number, 'songNumber for %s should be %s' + % (song_file, song_number)) + mocked_finish.assert_called_with() diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py index 7fbb76917..aa0779f5c 100644 --- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -33,51 +33,25 @@ This module contains tests for the SongShow Plus song importer. import os from unittest import TestCase +from tests.helpers.songfileimport import SongImportTestHelper from openlp.plugins.songs.lib import VerseType from openlp.plugins.songs.lib.songshowplusimport import SongShowPlusImport from tests.functional import patch, MagicMock -TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../resources/songshowplussongs')) -SONG_TEST_DATA = {'Amazing Grace.sbsong': - {'title': 'Amazing Grace (Demonstration)', - 'authors': ['John Newton', 'Edwin Excell', 'John P. Rees'], - 'copyright': 'Public Domain ', - 'ccli_number': 22025, - 'verses': - [('Amazing grace! How sweet the sound!\r\nThat saved a wretch like me!\r\n' - 'I once was lost, but now am found;\r\nWas blind, but now I see.', 'v1'), - ('\'Twas grace that taught my heart to fear,\r\nAnd grace my fears relieved.\r\n' - 'How precious did that grace appear,\r\nThe hour I first believed.', 'v2'), - ('The Lord has promised good to me,\r\nHis Word my hope secures.\r\n' - 'He will my shield and portion be\r\nAs long as life endures.', 'v3'), - ('Thro\' many dangers, toils and snares\r\nI have already come.\r\n' - '\'Tis grace that brought me safe thus far,\r\nAnd grace will lead me home.', 'v4'), - ('When we\'ve been there ten thousand years,\r\nBright shining as the sun,\r\n' - 'We\'ve no less days to sing God\'s praise,\r\nThan when we first begun.', 'v5')], - 'topics': ['Assurance', 'Grace', 'Praise', 'Salvation'], - 'comments': '\n\n\n', - 'song_book_name': 'Demonstration Songs', - 'song_number': 0, - 'verse_order_list': []}, - 'Beautiful Garden Of Prayer.sbsong': - {'title': 'Beautiful Garden Of Prayer (Demonstration)', - 'authors': ['Eleanor Allen Schroll', 'James H. Fillmore'], - 'copyright': 'Public Domain ', - 'ccli_number': 60252, - 'verses': - [('There\'s a garden where Jesus is waiting,\r\nThere\'s a place that is wondrously fair.\r\n' - 'For it glows with the light of His presence,\r\n\'Tis the beautiful garden of prayer.', 'v1'), - ('There\'s a garden where Jesus is waiting,\r\nAnd I go with my burden and care.\r\n' - 'Just to learn from His lips, words of comfort,\r\nIn the beautiful garden of prayer.', 'v2'), - ('There\'s a garden where Jesus is waiting,\r\nAnd He bids you to come meet Him there,\r\n' - 'Just to bow and receive a new blessing,\r\nIn the beautiful garden of prayer.', 'v3'), - ('O the beautiful garden, the garden of prayer,\r\nO the beautiful garden of prayer.\r\n' - 'There my Savior awaits, and He opens the gates\r\nTo the beautiful garden of prayer.', 'c1')], - 'topics': ['Devotion', 'Prayer'], - 'comments': '', - 'song_book_name': '', - 'song_number': 0, - 'verse_order_list': []}} +TEST_PATH = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'songshowplussongs')) + +class TestSongShowPlusFileImport(SongImportTestHelper): + def __init__(self, *args, **kwargs): + self.importer_class_name = 'SongShowPlusImport' + self.importer_module_name = 'songshowplusimport' + super(TestSongShowPlusFileImport, self).__init__(*args, **kwargs) + + def test_song_import(self): + test_import = self.file_import(os.path.join(TEST_PATH, 'Amazing Grace.sbsong'), + self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json'))) + test_import = self.file_import(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.sbsong'), + self.load_external_result_data(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.json'))) class TestSongShowPlusImport(TestCase): @@ -117,7 +91,7 @@ class TestSongShowPlusImport(TestCase): # THEN: doImport should return none and the progress bar maximum should not be set. self.assertIsNone(importer.doImport(), 'doImport should return None when import_source is not a list') self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False, - 'setMaxium on import_wizard.progress_bar should not have been called') + 'setMaximum on import_wizard.progress_bar should not have been called') def valid_import_source_test(self): """ @@ -194,70 +168,3 @@ class TestSongShowPlusImport(TestCase): self.assertEquals(importer.to_openlp_verse_tag(original_tag, ignore_unique=True), openlp_tag, 'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"' % (openlp_tag, original_tag)) - - def file_import_test(self): - """ - Test the actual import of real song files and check that the imported data is correct. - """ - - # GIVEN: Test files with a mocked out SongImport class, a mocked out "manager", a mocked out "import_wizard", - # and mocked out "author", "add_copyright", "add_verse", "finish" methods. - with patch('openlp.plugins.songs.lib.songshowplusimport.SongImport'): - for song_file in SONG_TEST_DATA: - mocked_manager = MagicMock() - mocked_import_wizard = MagicMock() - mocked_parse_author = MagicMock() - mocked_add_copyright = MagicMock() - mocked_add_verse = MagicMock() - mocked_finish = MagicMock() - mocked_finish.return_value = True - importer = SongShowPlusImport(mocked_manager) - importer.import_wizard = mocked_import_wizard - importer.stop_import_flag = False - importer.parse_author = mocked_parse_author - importer.addCopyright = mocked_add_copyright - importer.addVerse = mocked_add_verse - importer.finish = mocked_finish - importer.topics = [] - - # WHEN: Importing each file - importer.import_source = [os.path.join(TEST_PATH, song_file)] - title = SONG_TEST_DATA[song_file]['title'] - author_calls = SONG_TEST_DATA[song_file]['authors'] - song_copyright = SONG_TEST_DATA[song_file]['copyright'] - ccli_number = SONG_TEST_DATA[song_file]['ccli_number'] - add_verse_calls = SONG_TEST_DATA[song_file]['verses'] - topics = SONG_TEST_DATA[song_file]['topics'] - comments = SONG_TEST_DATA[song_file]['comments'] - song_book_name = SONG_TEST_DATA[song_file]['song_book_name'] - song_number = SONG_TEST_DATA[song_file]['song_number'] - verse_order_list = SONG_TEST_DATA[song_file]['verse_order_list'] - - # THEN: doImport should return none, the song data should be as expected, and finish should have been - # called. - self.assertIsNone(importer.doImport(), 'doImport should return None when it has completed') - self.assertEquals(importer.title, title, 'title for %s should be "%s"' % (song_file, title)) - for author in author_calls: - mocked_parse_author.assert_any_call(author) - if song_copyright: - mocked_add_copyright.assert_called_with(song_copyright) - if ccli_number: - self.assertEquals(importer.ccliNumber, ccli_number, 'ccliNumber for %s should be %s' - % (song_file, ccli_number)) - for verse_text, verse_tag in add_verse_calls: - mocked_add_verse.assert_any_call(verse_text, verse_tag) - if topics: - self.assertEquals(importer.topics, topics, 'topics for %s should be %s' % (song_file, topics)) - if comments: - self.assertEquals(importer.comments, comments, 'comments for %s should be "%s"' - % (song_file, comments)) - if song_book_name: - self.assertEquals(importer.songBookName, song_book_name, 'songBookName for %s should be "%s"' - % (song_file, song_book_name)) - if song_number: - self.assertEquals(importer.songNumber, song_number, 'songNumber for %s should be %s' - % (song_file, song_number)) - if verse_order_list: - self.assertEquals(importer.verseOrderList, [], 'verseOrderList for %s should be %s' - % (song_file, verse_order_list)) - mocked_finish.assert_called_with() diff --git a/tests/helpers/songfileimport.py b/tests/helpers/songfileimport.py new file mode 100644 index 000000000..faf18999f --- /dev/null +++ b/tests/helpers/songfileimport.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2013 Raoul Snyman # +# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`songfileimporthelper` modules provides a helper class and methods to easily enable testing the import of +song files from third party applications. +""" +import json +from unittest import TestCase + +from tests.functional import patch, MagicMock + +class SongImportTestHelper(TestCase): + """ + This class is designed to be a helper class to reduce repition when testing the import of song files. + """ + def __init__(self, *args, **kwargs): + super(SongImportTestHelper, self).__init__(*args, **kwargs) + self.importer_module = __import__( + 'openlp.plugins.songs.lib.%s' % self.importer_module_name, fromlist=[self.importer_class_name]) + self.importer_class = getattr(self.importer_module, self.importer_class_name) + + def setUp(self): + """ + Patch and set up the mocks required. + """ + self.add_copyright_patcher = patch( + 'openlp.plugins.songs.lib.%s.%s.addCopyright' % (self.importer_module_name, self.importer_class_name)) + self.add_verse_patcher = patch( + 'openlp.plugins.songs.lib.%s.%s.addVerse' % (self.importer_module_name, self.importer_class_name)) + self.finish_patcher = patch( + 'openlp.plugins.songs.lib.%s.%s.finish' % (self.importer_module_name, self.importer_class_name)) + self.parse_author_patcher = patch( + 'openlp.plugins.songs.lib.%s.%s.parse_author' % (self.importer_module_name, self.importer_class_name)) + self.song_import_patcher = patch('openlp.plugins.songs.lib.%s.SongImport' % self.importer_module_name) + self.mocked_add_copyright = self.add_copyright_patcher.start() + self.mocked_add_verse = self.add_verse_patcher.start() + self.mocked_finish = self.finish_patcher.start() + self.mocked_parse_author = self.parse_author_patcher.start() + self.mocked_song_importer = self.song_import_patcher.start() + self.mocked_manager = MagicMock() + self.mocked_import_wizard = MagicMock() + self.mocked_finish.return_value = True + + def tearDown(self): + """ + Clean up + """ + self.add_copyright_patcher.stop() + self.add_verse_patcher.stop() + self.finish_patcher.stop() + self.parse_author_patcher.stop() + self.song_import_patcher.stop() + + def load_external_result_data(self, file_name): + """ + A method to load and return an object containing the song data from an external file. + """ + result_file = open(file_name, 'rb') + return json.loads(result_file.read().decode()) + + def file_import(self, source_file_name, result_data): + """ + Import the given file and check that it has imported correctly + """ + importer = self.importer_class(self.mocked_manager) + importer.import_wizard = self.mocked_import_wizard + importer.stop_import_flag = False + importer.topics = [] + + # WHEN: Importing the source file + importer.import_source = [source_file_name] + add_verse_calls = self._get_data(result_data, 'verses') + author_calls = self._get_data(result_data, 'authors') + ccli_number = self._get_data(result_data, 'ccli_number') + comments = self._get_data(result_data, 'comments') + song_book_name = self._get_data(result_data, 'song_book_name') + song_copyright = self._get_data(result_data, 'copyright') + song_number = self._get_data(result_data, 'song_number') + title = self._get_data(result_data, 'title') + topics = self._get_data(result_data, 'topics') + verse_order_list = self._get_data(result_data, 'verse_order_list') + + # THEN: doImport should return none, the song data should be as expected, and finish should have been + # called. + self.assertIsNone(importer.doImport(), 'doImport should return None when it has completed') + self.assertEquals(importer.title, title, 'title for %s should be "%s"' % (source_file_name, title)) + for author in author_calls: + self.mocked_parse_author.assert_any_call(author) + if song_copyright: + self.mocked_add_copyright.assert_called_with(song_copyright) + if ccli_number: + self.assertEquals(importer.ccliNumber, ccli_number, 'ccliNumber for %s should be %s' + % (source_file_name, ccli_number)) + for verse_text, verse_tag in add_verse_calls: + self.mocked_add_verse.assert_any_call(verse_text, verse_tag) + if topics: + self.assertEquals(importer.topics, topics, 'topics for %s should be %s' % (source_file_name, topics)) + if comments: + self.assertEquals(importer.comments, comments, 'comments for %s should be "%s"' + % (source_file_name, comments)) + if song_book_name: + self.assertEquals(importer.songBookName, song_book_name, 'songBookName for %s should be "%s"' + % (source_file_name, song_book_name)) + if song_number: + self.assertEquals(importer.songNumber, song_number, 'songNumber for %s should be %s' + % (source_file_name, song_number)) + if verse_order_list: + self.assertEquals(importer.verseOrderList, [], 'verseOrderList for %s should be %s' + % (source_file_name, verse_order_list)) + self.mocked_finish.assert_called_with() + + def _get_data(self, data, key): + if key in data: + return data[key] + return '' + diff --git a/tests/interfaces/openlp_plugins/songs/forms/test_editsongform.py b/tests/interfaces/openlp_plugins/songs/forms/test_editsongform.py index 405ba5cf6..75907e3c8 100644 --- a/tests/interfaces/openlp_plugins/songs/forms/test_editsongform.py +++ b/tests/interfaces/openlp_plugins/songs/forms/test_editsongform.py @@ -55,9 +55,9 @@ class TestEditSongForm(TestCase): self.form.verse_list_widget.rowCount = MagicMock(return_value=2) # Mock out the verse. first_verse = MagicMock() - first_verse.data = MagicMock(return_value='V1') + first_verse.data.return_value = 'V1' second_verse = MagicMock() - second_verse.data = MagicMock(return_value= 'V2') + second_verse.data.return_value = 'V2' self.form.verse_list_widget.item = MagicMock(side_effect=[first_verse, second_verse]) self.form._extract_verse_order = MagicMock(return_value=given_verse_order.split()) @@ -76,9 +76,9 @@ class TestEditSongForm(TestCase): self.form.verse_list_widget.rowCount = MagicMock(return_value=2) # Mock out the verse. first_verse = MagicMock() - first_verse.data = MagicMock(return_value='V1') + first_verse.data.return_value = 'V1' second_verse = MagicMock() - second_verse.data = MagicMock(return_value= 'V2') + second_verse.data.return_value = 'V2' self.form.verse_list_widget.item = MagicMock(side_effect=[first_verse, second_verse]) self.form._extract_verse_order = MagicMock(return_value=[given_verse_order]) @@ -98,7 +98,7 @@ class TestEditSongForm(TestCase): self.form.verse_list_widget.rowCount = MagicMock(return_value=1) # Mock out the verse. (We want a verse type to be returned). mocked_verse = MagicMock() - mocked_verse.data = MagicMock(return_value='V1') + mocked_verse.data.return_value = 'V1' self.form.verse_list_widget.item = MagicMock(return_value=mocked_verse) self.form._extract_verse_order = MagicMock(return_value=[]) self.form.verse_order_edit.text = MagicMock(return_value=given_verse_order) diff --git a/tests/resources/songbeamersongs/Lobsinget dem Herrn.sng b/tests/resources/songbeamersongs/Lobsinget dem Herrn.sng new file mode 100644 index 000000000..fbc9aa9fc --- /dev/null +++ b/tests/resources/songbeamersongs/Lobsinget dem Herrn.sng @@ -0,0 +1,25 @@ +#LangCount=1 +#Title=GL 1 - Lobsinget dem Herrn +#Editor=SongBeamer 4.20 +#Version=3 +#Format=F/K// +#TitleFormat=U +#ChurchSongID=0001 +#Songbook=Glaubenslieder I / 1 +--- +1. Lobsinget dem Herrn, +o preiset Ihn gern! +Anbetung und Lob Ihm gebühret. + --- +2. Lobsingt Seiner Lieb´, +die einzig ihn trieb, +zu sterben für unsere Sünden! + --- +3. Lobsingt Seiner Macht! +Sein Werk ist vollbracht: +Er sitzet zur Rechten des Vaters. + --- +4. Lobsingt seiner Treu´, +die immerdar neu, +bis Er uns zur Herrlichket führet! + diff --git a/tests/resources/songshowplussongs/Amazing Grace.json b/tests/resources/songshowplussongs/Amazing Grace.json new file mode 100644 index 000000000..878132881 --- /dev/null +++ b/tests/resources/songshowplussongs/Amazing Grace.json @@ -0,0 +1,42 @@ +{ + "authors": [ + "John Newton", + "Edwin Excell", + "John P. Rees" + ], + "ccli_number": 22025, + "comments": "\n\n\n", + "copyright": "Public Domain ", + "song_book_name": "Demonstration Songs", + "song_number": 0, + "title": "Amazing Grace (Demonstration)", + "topics": [ + "Assurance", + "Grace", + "Praise", + "Salvation" + ], + "verse_order_list": [], + "verses": [ + [ + "Amazing grace! How sweet the sound!\r\nThat saved a wretch like me!\r\nI once was lost, but now am found;\r\nWas blind, but now I see.", + "v1" + ], + [ + "'Twas grace that taught my heart to fear,\r\nAnd grace my fears relieved.\r\nHow precious did that grace appear,\r\nThe hour I first believed.", + "v2" + ], + [ + "The Lord has promised good to me,\r\nHis Word my hope secures.\r\nHe will my shield and portion be\r\nAs long as life endures.", + "v3" + ], + [ + "Thro' many dangers, toils and snares\r\nI have already come.\r\n'Tis grace that brought me safe thus far,\r\nAnd grace will lead me home.", + "v4" + ], + [ + "When we've been there ten thousand years,\r\nBright shining as the sun,\r\nWe've no less days to sing God's praise,\r\nThan when we first begun.", + "v5" + ] + ] +} \ No newline at end of file diff --git a/tests/resources/songshowplussongs/Beautiful Garden Of Prayer.json b/tests/resources/songshowplussongs/Beautiful Garden Of Prayer.json new file mode 100644 index 000000000..651af9100 --- /dev/null +++ b/tests/resources/songshowplussongs/Beautiful Garden Of Prayer.json @@ -0,0 +1,35 @@ +{ + "authors": [ + "Eleanor Allen Schroll", + "James H. Fillmore" + ], + "ccli_number": 60252, + "comments": "", + "copyright": "Public Domain ", + "song_book_name": "", + "song_number": 0, + "title": "Beautiful Garden Of Prayer (Demonstration)", + "topics": [ + "Devotion", + "Prayer" + ], + "verse_order_list": [], + "verses": [ + [ + "There's a garden where Jesus is waiting,\r\nThere's a place that is wondrously fair.\r\nFor it glows with the light of His presence,\r\n'Tis the beautiful garden of prayer.", + "v1" + ], + [ + "There's a garden where Jesus is waiting,\r\nAnd I go with my burden and care.\r\nJust to learn from His lips, words of comfort,\r\nIn the beautiful garden of prayer.", + "v2" + ], + [ + "There's a garden where Jesus is waiting,\r\nAnd He bids you to come meet Him there,\r\nJust to bow and receive a new blessing,\r\nIn the beautiful garden of prayer.", + "v3" + ], + [ + "O the beautiful garden, the garden of prayer,\r\nO the beautiful garden of prayer.\r\nThere my Savior awaits, and He opens the gates\r\nTo the beautiful garden of prayer.", + "c1" + ] + ] +} \ No newline at end of file