Refactor SongSelect search into a separate class, and start writing some tests for that class.

This commit is contained in:
Raoul Snyman 2014-02-27 23:36:33 +02:00
parent e2fcd3e8cb
commit d2a5e8a4c4
7 changed files with 526 additions and 158 deletions

View File

@ -31,30 +31,15 @@ The :mod:`~openlp.plugins.songs.forms.songselectform` module contains the GUI fo
"""
import logging
from http.cookiejar import CookieJar
from urllib.parse import urlencode
from urllib.request import HTTPCookieProcessor, HTTPError, build_opener
from html.parser import HTMLParser
from time import sleep
from PyQt4 import QtCore, QtGui
from bs4 import BeautifulSoup, NavigableString
from openlp.core import Settings
from openlp.core import Settings
from openlp.core.common import Registry
from openlp.core.lib import translate
from openlp.plugins.songs.lib import VerseType, clean_song
from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog
from openlp.plugins.songs.lib.db import Author, Song
from openlp.plugins.songs.lib.xml import SongXML
USER_AGENT = 'Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; GT-I9000 ' \
'Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 ' \
'Mobile Safari/534.30'
BASE_URL = 'https://mobile.songselect.com'
LOGIN_URL = BASE_URL + '/account/login'
LOGOUT_URL = BASE_URL + '/account/logout'
SEARCH_URL = BASE_URL + '/search/results'
from openlp.plugins.songs.lib.songselect import SongSelectImport
log = logging.getLogger(__name__)
@ -65,53 +50,36 @@ class SearchWorker(QtCore.QObject):
"""
show_info = QtCore.pyqtSignal(str, str)
found_song = QtCore.pyqtSignal(dict)
finished = QtCore.pyqtSignal(list)
finished = QtCore.pyqtSignal()
quit = QtCore.pyqtSignal()
def __init__(self, opener, params):
def __init__(self, importer, search_text):
super().__init__()
self.opener = opener
self.params = params
self.html_parser = HTMLParser()
def _search_and_parse_results(self, params):
params = urlencode(params)
results_page = BeautifulSoup(self.opener.open(SEARCH_URL + '?' + params).read(), 'lxml')
search_results = results_page.find_all('li', 'result pane')
songs = []
for result in search_results:
song = {
'title': self.html_parser.unescape(result.find('h3').string),
'authors': [self.html_parser.unescape(author.string) for author in result.find_all('li')],
'link': BASE_URL + result.find('a')['href']
}
self.found_song.emit(song)
songs.append(song)
return songs
self.importer = importer
self.search_text = search_text
def start(self):
"""
Run a search and then parse the results page of the search.
"""
songs = self._search_and_parse_results(self.params)
search_results = []
self.params['page'] = 1
total = 0
while songs:
search_results.extend(songs)
self.params['page'] += 1
total += len(songs)
if total >= 1000:
songs = self.importer.search(self.search_text, 1000, self._found_song_callback)
if len(songs) >= 1000:
self.show_info.emit(
translate('SongsPlugin.SongSelectForm', 'More than 1000 results'),
translate('SongsPlugin.SongSelectForm', 'Your search has returned more than 1000 results, it has '
'been stopped. Please refine your search to fetch better '
'results.'))
break
songs = self._search_and_parse_results(self.params)
self.finished.emit(search_results)
self.finished.emit()
self.quit.emit()
def _found_song_callback(self, song):
"""
A callback used by the paginate function to notify watching processes when it finds a song.
:param song: The song that was found
"""
self.found_song.emit(song)
class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
"""
@ -126,10 +94,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
self.song_count = 0
self.song = None
self.plugin = plugin
self.db_manager = db_manager
self.html_parser = HTMLParser()
self.opener = build_opener(HTTPCookieProcessor(CookieJar()))
self.opener.addheaders = [('User-Agent', USER_AGENT)]
self.song_select_importer = SongSelectImport(db_manager)
self.save_password_checkbox.toggled.connect(self.on_save_password_checkbox_toggled)
self.login_button.clicked.connect(self.on_login_button_clicked)
self.search_button.clicked.connect(self.on_search_button_clicked)
@ -182,7 +147,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
self.main_window.application.process_events()
sleep(0.5)
self.main_window.application.process_events()
self.opener.open(LOGOUT_URL)
self.song_select_importer.logout()
self.main_window.application.process_events()
progress_dialog.setValue(2)
return QtGui.QDialog.done(self, r)
@ -194,6 +159,14 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
main_window = property(_get_main_window)
def _update_login_progress(self):
self.login_progress_bar.setValue(self.login_progress_bar.value() + 1)
self.main_window.application.process_events()
def _update_song_progress(self):
self.song_progress_bar.setValue(self.song_progress_bar.value() + 1)
self.main_window.application.process_events()
def _view_song(self, current_item):
if not current_item:
return
@ -217,40 +190,17 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
song = {}
for key, value in current_item.items():
song[key] = value
self.song_progress_bar.setValue(1)
self.song_progress_bar.setValue(0)
self.main_window.application.process_events()
song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
self.song_progress_bar.setValue(2)
self.main_window.application.process_events()
try:
lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/lyrics').read(), 'lxml')
except HTTPError:
lyrics_page = None
self.song_progress_bar.setValue(3)
self.main_window.application.process_events()
song['copyright'] = '/'.join([li.string for li in song_page.find('ul', 'copyright').find_all('li')])
song['copyright'] = self.html_parser.unescape(song['copyright'])
song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip()
song['verses'] = []
if lyrics_page:
verses = lyrics_page.find('section', 'lyrics').find_all('p')
verse_labels = lyrics_page.find('section', 'lyrics').find_all('h3')
for counter in range(len(verses)):
verse = {'label': verse_labels[counter].string, 'lyrics': ''}
for v in verses[counter].contents:
if isinstance(v, NavigableString):
verse['lyrics'] = verse['lyrics'] + v.string
else:
verse['lyrics'] += '\n'
verse['lyrics'] = verse['lyrics'].strip(' \n\r\t')
song['verses'].append(self.html_parser.unescape(verse))
# Get the full song
self.song_select_importer.get_song(song, self._update_song_progress)
# Update the UI
self.title_edit.setText(song['title'])
self.copyright_edit.setText(song['copyright'])
self.ccli_edit.setText(song['ccli_number'])
for author in song['authors']:
QtGui.QListWidgetItem(self.html_parser.unescape(author), self.author_list_widget)
QtGui.QListWidgetItem(author, self.author_list_widget)
for counter, verse in enumerate(song['verses']):
log.debug('Verse type: %s', verse['label'])
self.lyrics_table_widget.setRowCount(self.lyrics_table_widget.rowCount() + 1)
item = QtGui.QTableWidgetItem(verse['lyrics'])
item.setData(QtCore.Qt.UserRole, verse['label'])
@ -296,23 +246,12 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
self.save_password_checkbox.setEnabled(False)
self.login_button.setEnabled(False)
self.login_spacer.setVisible(False)
self.login_progress_bar.setValue(0)
self.login_progress_bar.setVisible(True)
self.login_progress_bar.setValue(1)
self.main_window.application.process_events()
login_page = BeautifulSoup(self.opener.open(LOGIN_URL).read(), 'lxml')
self.login_progress_bar.setValue(2)
self.main_window.application.process_events()
token_input = login_page.find('input', attrs={'name': '__RequestVerificationToken'})
data = urlencode({
'__RequestVerificationToken': token_input['value'],
'UserName': self.username_edit.text(),
'Password': self.password_edit.text(),
'RememberMe': 'false'
})
posted_page = BeautifulSoup(self.opener.open(LOGIN_URL, data.encode('utf-8')).read(), 'lxml')
self.login_progress_bar.setValue(3)
self.main_window.application.process_events()
if posted_page.find('input', attrs={'name': '__RequestVerificationToken'}):
# Log the user in
if not self.song_select_importer.login(
self.username_edit.text(), self.password_edit.text(), self._update_login_progress):
QtGui.QMessageBox.critical(
self,
translate('SongsPlugin.SongSelectForm', 'Error Logging In'),
@ -340,10 +279,10 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
"""
self.view_button.setEnabled(False)
self.search_button.setEnabled(False)
self.search_progress_bar.setVisible(True)
self.search_progress_bar.setMinimum(0)
self.search_progress_bar.setMaximum(0)
self.search_progress_bar.setValue(0)
self.search_progress_bar.setVisible(True)
self.search_results_widget.clear()
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count)
self.main_window.application.process_events()
@ -353,8 +292,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
# Create thread and run search
self.thread = QtCore.QThread()
self.worker = SearchWorker(self.opener, {'SearchTerm': self.search_combobox.currentText(),
'allowredirect': 'false'})
self.worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText())
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.start)
self.worker.show_info.connect(self.on_search_show_info)
@ -378,16 +316,16 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
Add a song to the list when one is found.
:param song:
"""
log.debug('SongSelect (title = "%s"), (link = "%s")', song['title'], song['link'])
self.song_count += 1
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count)
item_title = song['title'] + ' (' + ', '.join(song['authors']) + ')'
song_item = QtGui.QListWidgetItem(item_title, self.search_results_widget)
song_item.setData(QtCore.Qt.UserRole, song)
def on_search_finished(self, songs):
def on_search_finished(self):
"""
Slot which is called when the search is completed.
:param songs:
"""
self.main_window.application.process_events()
@ -410,6 +348,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
def on_search_results_widget_double_clicked(self, current_item):
"""
View a song from SongSelect
:param current_item:
"""
self._view_song(current_item)
@ -425,39 +364,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
"""
Import a song from SongSelect.
"""
song = Song.populate(
title=self.song['title'],
copyright=self.song['copyright'],
ccli_number=self.song['ccli_number']
)
song_xml = SongXML()
verse_order = []
for verse in self.song['verses']:
verse_type, verse_number = verse['label'].split(' ')[:2]
verse_type = VerseType.from_loose_input(verse_type)
verse_number = int(verse_number)
song_xml.add_verse_to_lyrics(
VerseType.tags[verse_type],
verse_number,
verse['lyrics']
)
verse_order.append('%s%s' % (VerseType.tags[verse_type], verse_number))
song.verse_order = ' '.join(verse_order)
song.lyrics = song_xml.extract_xml()
clean_song(self.db_manager, song)
self.db_manager.save_object(song)
song.authors = []
for author_name in self.song['authors']:
#author_name = unicode(author_name)
author = self.db_manager.get_object_filtered(Author, Author.display_name == author_name)
if not author:
author = Author.populate(
first_name=author_name.rsplit(' ', 1)[0],
last_name=author_name.rsplit(' ', 1)[1],
display_name=author_name
)
song.authors.append(author)
self.db_manager.save_object(song)
self.song_select_importer.save_song(self.song)
question_dialog = QtGui.QMessageBox()
question_dialog.setWindowTitle(translate('SongsPlugin.SongSelectForm', 'Song Imported'))
question_dialog.setText(translate('SongsPlugin.SongSelectForm', 'Your song has been imported, would you like '

View File

@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2014 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:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect importer itself.
"""
import logging
from http.cookiejar import CookieJar
from urllib.parse import urlencode
from urllib.request import HTTPCookieProcessor, HTTPError, build_opener
from html.parser import HTMLParser
from bs4 import BeautifulSoup, NavigableString
from openlp.plugins.songs.lib import Song, VerseType, clean_song, Author
from openlp.plugins.songs.lib.xml import SongXML
USER_AGENT = 'Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; GT-I9000 ' \
'Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 ' \
'Mobile Safari/534.30'
BASE_URL = 'https://mobile.songselect.com'
LOGIN_URL = BASE_URL + '/account/login'
LOGOUT_URL = BASE_URL + '/account/logout'
SEARCH_URL = BASE_URL + '/search/results'
log = logging.getLogger(__name__)
class SongSelectImport(object):
"""
The :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class contains all the code which interfaces
with CCLI's SongSelect service and downloads the songs.
"""
def __init__(self, db_manager):
"""
Set up the song select importer
:param db_manager: The song database manager
"""
self.db_manager = db_manager
self.html_parser = HTMLParser()
self.opener = build_opener(HTTPCookieProcessor(CookieJar()))
self.opener.addheaders = [('User-Agent', USER_AGENT)]
def login(self, username, password, callback=None):
"""
Log the user into SongSelect. This method takes a username and password, and runs ``callback()`` at various
points which can be used to give the user some form of feedback.
:param username: SongSelect username
:param password: SongSelect password
:param callback: Method to notify of progress.
:return: True on success, False on failure.
"""
if callback:
callback()
login_page = BeautifulSoup(self.opener.open(LOGIN_URL).read(), 'lxml')
if callback:
callback()
token_input = login_page.find('input', attrs={'name': '__RequestVerificationToken'})
data = urlencode({
'__RequestVerificationToken': token_input['value'],
'UserName': username,
'Password': password,
'RememberMe': 'false'
})
posted_page = BeautifulSoup(self.opener.open(LOGIN_URL, data.encode('utf-8')).read(), 'lxml')
if callback:
callback()
return not posted_page.find('input', attrs={'name': '__RequestVerificationToken'})
def logout(self):
"""
Log the user out of SongSelect
"""
self.opener.open(LOGOUT_URL)
def search(self, search_text, max_results, callback=None):
"""
Set up a search.
:param search_text: The text to search for.
:param max_results: Maximum number of results to fetch.
:param callback: A method which is called when each song is found, with the song as a parameter.
:return: List of songs
"""
params = {'allowredirect': 'false', 'SearchTerm': search_text}
current_page = 1
songs = []
while True:
if current_page > 1:
params['page'] = current_page
results_page = BeautifulSoup(self.opener.open(SEARCH_URL + '?' + urlencode(params)).read(), 'lxml')
search_results = results_page.find_all('li', 'result pane')
if not search_results:
break
for result in search_results:
song = {
'title': self.html_parser.unescape(result.find('h3').string),
'authors': [self.html_parser.unescape(author.string) for author in result.find_all('li')],
'link': BASE_URL + result.find('a')['href']
}
if callback:
callback(song)
songs.append(song)
if len(songs) >= max_results:
break
current_page += 1
return songs
def get_song(self, song, callback=None):
"""
Get the full song from SongSelect
:param song:
:param callback:
"""
if callback:
callback()
song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
if callback:
callback()
try:
lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/lyrics').read(), 'lxml')
except HTTPError:
lyrics_page = None
if callback:
callback()
song['copyright'] = '/'.join([li.string for li in song_page.find('ul', 'copyright').find_all('li')])
song['copyright'] = self.html_parser.unescape(song['copyright'])
song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip()
song['verses'] = []
if lyrics_page:
verses = lyrics_page.find('section', 'lyrics').find_all('p')
verse_labels = lyrics_page.find('section', 'lyrics').find_all('h3')
for counter in range(len(verses)):
verse = {'label': verse_labels[counter].string, 'lyrics': ''}
for v in verses[counter].contents:
if isinstance(v, NavigableString):
verse['lyrics'] = verse['lyrics'] + v.string
else:
verse['lyrics'] += '\n'
verse['lyrics'] = verse['lyrics'].strip(' \n\r\t')
song['verses'].append(self.html_parser.unescape(verse))
for counter, author in enumerate(song['authors']):
song['authors'][counter] = self.html_parser.unescape(author)
def save_song(self, song):
"""
Save a song to the database, using the db_manager
:param song:
:return:
"""
db_song = Song.populate(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number'])
song_xml = SongXML()
verse_order = []
for verse in song['verses']:
verse_type, verse_number = verse['label'].split(' ')[:2]
verse_type = VerseType.from_loose_input(verse_type)
verse_number = int(verse_number)
song_xml.add_verse_to_lyrics(VerseType.tags[verse_type], verse_number, verse['lyrics'])
verse_order.append('%s%s' % (VerseType.tags[verse_type], verse_number))
db_song.verse_order = ' '.join(verse_order)
db_song.lyrics = song_xml.extract_xml()
clean_song(self.db_manager, db_song)
self.db_manager.save_object(db_song)
db_song.authors = []
for author_name in song['authors']:
author = self.db_manager.get_object_filtered(Author, Author.display_name == author_name)
if not author:
author = Author.populate(first_name=author_name.rsplit(' ', 1)[0],
last_name=author_name.rsplit(' ', 1)[1],
display_name=author_name)
db_song.authors.append(author)
self.db_manager.save_object(db_song)

View File

@ -38,20 +38,20 @@ 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_TEST_DATA = {
'Lobsinget dem Herrn.sng': {
'title': 'GL 1 - Lobsinget dem Herrn',
'verses': [
('1. Lobsinget dem Herrn,\no preiset Ihn gern!\nAnbetung und Lob Ihm gebühret.\n', 'v'),
('2. Lobsingt Seiner Lieb´,\ndie einzig ihn trieb,\nzu sterben für unsere Sünden!\n', 'v'),
('3. Lobsingt Seiner Macht!\nSein Werk ist vollbracht:\nEr sitzet zur Rechten des Vaters.\n', 'v'),
('4. Lobsingt seiner Treu´,\ndie immerdar neu,\nbis Er uns zur Herrlichket führet!\n\n', 'v')
],
'song_book_name': 'Glaubenslieder I',
'song_number': "1"}
'song_number': "1"
}
}
class TestSongBeamerImport(TestCase):
"""

View File

@ -0,0 +1,224 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2014 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 CCLI SongSelect importer.
"""
from unittest import TestCase
from openlp.plugins.songs.lib.songselect import SongSelectImport, LOGIN_URL, LOGOUT_URL, BASE_URL
from tests.functional import MagicMock, patch
class TestSongSelect(TestCase):
"""
Test the :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class
"""
def constructor_test(self):
"""
Test that constructing a basic SongSelectImport object works correctly
"""
# GIVEN: The SongSelectImporter class and a mocked out build_opener
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener:
# WHEN: An object is instantiated
importer = SongSelectImport(None)
# THEN: The object should have the correct properties
self.assertIsNone(importer.db_manager, 'The db_manager should be None')
self.assertIsNotNone(importer.html_parser, 'There should be a valid html_parser object')
self.assertIsNotNone(importer.opener, 'There should be a valid opener object')
self.assertEqual(1, mocked_build_opener.call_count, 'The build_opener method should have been called once')
def login_fails_test(self):
"""
Test that when logging in to SongSelect fails, the login method returns False
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_login_page = MagicMock()
mocked_login_page.find.return_value = {'value': 'blah'}
MockedBeautifulSoup.return_value = mocked_login_page
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
result = importer.login('username', 'password', mock_callback)
# THEN: callback was called 3 times, open was called twice, find was called twice, and False was returned
self.assertEqual(3, mock_callback.call_count, 'callback should have been called 3 times')
self.assertEqual(2, mocked_login_page.find.call_count, 'find should have been called twice')
self.assertEqual(2, mocked_opener.open.call_count, 'opener should have been called twice')
self.assertFalse(result, 'The login method should have returned False')
def login_succeeds_test(self):
"""
Test that when logging in to SongSelect succeeds, the login method returns True
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_login_page = MagicMock()
mocked_login_page.find.side_effect = [{'value': 'blah'}, None]
MockedBeautifulSoup.return_value = mocked_login_page
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
result = importer.login('username', 'password', mock_callback)
# THEN: callback was called 3 times, open was called twice, find was called twice, and True was returned
self.assertEqual(3, mock_callback.call_count, 'callback should have been called 3 times')
self.assertEqual(2, mocked_login_page.find.call_count, 'find should have been called twice')
self.assertEqual(2, mocked_opener.open.call_count, 'opener should have been called twice')
self.assertTrue(result, 'The login method should have returned True')
def logout_test(self):
"""
Test that when the logout method is called, it logs the user out of SongSelect
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener:
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
importer.logout()
# THEN: The opener is called once with the logout url
self.assertEqual(1, mocked_opener.open.call_count, 'opener should have been called once')
mocked_opener.open.assert_called_with(LOGOUT_URL)
def search_returns_no_results_test(self):
"""
Test that when the search finds no results, it simply returns an empty list
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, patch(
'openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_results_page = MagicMock()
mocked_results_page.find_all.return_value = []
MockedBeautifulSoup.return_value = mocked_results_page
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
results = importer.search('text', 1000, mock_callback)
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned
self.assertEqual(0, mock_callback.call_count, 'callback should not have been called')
self.assertEqual(1, mocked_opener.open.call_count, 'open should have been called once')
self.assertEqual(1, mocked_results_page.find_all.call_count, 'find_all should have been called once')
mocked_results_page.find_all.assert_called_with('li', 'result pane')
self.assertEqual([], results, 'The search method should have returned an empty list')
def search_returns_two_results_test(self):
"""
Test that when the search finds 2 results, it simply returns a list with 2 results
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, patch(
'openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
# first search result
mocked_result1 = MagicMock()
mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
mocked_result1.find_all.return_value = [MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2')]
# second search result
mocked_result2 = MagicMock()
mocked_result2.find.side_effect = [MagicMock(string='Title 2'), {'href': '/url2'}]
mocked_result2.find_all.return_value = [MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2')]
# rest of the stuff
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_results_page = MagicMock()
mocked_results_page.find_all.side_effect = [[mocked_result1, mocked_result2], []]
MockedBeautifulSoup.return_value = mocked_results_page
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
results = importer.search('text', 1000, mock_callback)
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned
self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
mocked_results_page.find_all.assert_called_with('li', 'result pane')
expected_list = [
{'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'},
{'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}
]
self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
def search_reaches_max_results_test(self):
"""
Test that when the search finds MAX (2) results, it simply returns a list with those (2)
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, patch(
'openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
# first search result
mocked_result1 = MagicMock()
mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
mocked_result1.find_all.return_value = [MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2')]
# second search result
mocked_result2 = MagicMock()
mocked_result2.find.side_effect = [MagicMock(string='Title 2'), {'href': '/url2'}]
mocked_result2.find_all.return_value = [MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2')]
# third search result
mocked_result3 = MagicMock()
mocked_result3.find.side_effect = [MagicMock(string='Title 3'), {'href': '/url3'}]
mocked_result3.find_all.return_value = [MagicMock(string='Author 3-1'), MagicMock(string='Author 3-2')]
# rest of the stuff
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_results_page = MagicMock()
mocked_results_page.find_all.side_effect = [[mocked_result1, mocked_result2, mocked_result3], []]
MockedBeautifulSoup.return_value = mocked_results_page
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
results = importer.search('text', 2, mock_callback)
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned
self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
mocked_results_page.find_all.assert_called_with('li', 'result pane')
expected_list = [{'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'},
{'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}]
self.assertListEqual(expected_list, results, 'The search method should have returned two songs')

View File

@ -41,6 +41,7 @@ from tests.functional import patch, MagicMock
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'
@ -48,9 +49,12 @@ class TestSongShowPlusFileImport(SongImportTestHelper):
super(TestSongShowPlusFileImport, self).__init__(*args, **kwargs)
def test_song_import(self):
test_import = self.file_import(os.path.join(TEST_PATH, 'Amazing Grace.sbsong'),
"""
Test that loading a SongShow Plus file works correctly on various files
"""
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.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')))

31
tests/helpers/__init__.py Normal file
View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2014 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:`~tests.helpers` module provides helper classes for use in the tests.
"""

View File

@ -35,6 +35,7 @@ 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.