Add some error handling around the getting a song.

Add some more tests.
This commit is contained in:
Raoul Snyman 2014-03-04 23:28:15 +02:00
parent 831f2f3b69
commit b094d48678
4 changed files with 126 additions and 28 deletions

View File

@ -193,7 +193,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
self.song_progress_bar.setValue(0) self.song_progress_bar.setValue(0)
self.main_window.application.process_events() self.main_window.application.process_events()
# Get the full song # Get the full song
self.song_select_importer.get_song(song, self._update_song_progress) song = self.song_select_importer.get_song(song, self._update_song_progress)
# Update the UI # Update the UI
self.title_edit.setText(song['title']) self.title_edit.setText(song['title'])
self.copyright_edit.setText(song['copyright']) self.copyright_edit.setText(song['copyright'])

View File

@ -137,38 +137,44 @@ class SongSelectImport(object):
""" """
Get the full song from SongSelect Get the full song from SongSelect
:param song: :param song: The song dictionary to update
:param callback: :param callback: A callback which can be used to indicate progress
:return: The updated song dictionary
""" """
if callback: if callback:
callback() callback()
song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml') try:
song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
except (TypeError, HTTPError) as e:
log.exception(u'Could not get song from SongSelect, %s', e)
return None
if callback: if callback:
callback() callback()
try: try:
lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/lyrics').read(), 'lxml') lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/lyrics').read(), 'lxml')
except HTTPError: except (TypeError, HTTPError):
lyrics_page = None log.exception(u'Could not get lyrics from SongSelect')
return None
if callback: if callback:
callback() callback()
song['copyright'] = '/'.join([li.string for li in song_page.find('ul', 'copyright').find_all('li')]) 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['copyright'] = self.html_parser.unescape(song['copyright'])
song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip() song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip()
song['verses'] = [] song['verses'] = []
if lyrics_page: verses = lyrics_page.find('section', 'lyrics').find_all('p')
verses = lyrics_page.find('section', 'lyrics').find_all('p') verse_labels = lyrics_page.find('section', 'lyrics').find_all('h3')
verse_labels = lyrics_page.find('section', 'lyrics').find_all('h3') for counter in range(len(verses)):
for counter in range(len(verses)): verse = {'label': verse_labels[counter].string, 'lyrics': ''}
verse = {'label': verse_labels[counter].string, 'lyrics': ''} for v in verses[counter].contents:
for v in verses[counter].contents: if isinstance(v, NavigableString):
if isinstance(v, NavigableString): verse['lyrics'] = verse['lyrics'] + v.string
verse['lyrics'] = verse['lyrics'] + v.string else:
else: verse['lyrics'] += '\n'
verse['lyrics'] += '\n' verse['lyrics'] = verse['lyrics'].strip(' \n\r\t')
verse['lyrics'] = verse['lyrics'].strip(' \n\r\t') song['verses'].append(self.html_parser.unescape(verse))
song['verses'].append(self.html_parser.unescape(verse))
for counter, author in enumerate(song['authors']): for counter, author in enumerate(song['authors']):
song['authors'][counter] = self.html_parser.unescape(author) song['authors'][counter] = self.html_parser.unescape(author)
return song
def save_song(self, song): def save_song(self, song):
""" """

View File

@ -1,3 +1,7 @@
# -*- coding: utf-8 -*-
"""
Base directory for tests
"""
import sip import sip
sip.setapi('QDate', 2) sip.setapi('QDate', 2)
sip.setapi('QDateTime', 2) sip.setapi('QDateTime', 2)
@ -11,9 +15,11 @@ import sys
from PyQt4 import QtGui from PyQt4 import QtGui
if sys.version_info[1] >= 3: if sys.version_info[1] >= 3:
from unittest.mock import MagicMock, patch, mock_open from unittest.mock import MagicMock, patch, mock_open, call
else: else:
from mock import MagicMock, patch, mock_open from mock import MagicMock, patch, mock_open, call
# Only one QApplication can be created. Use QtGui.QApplication.instance() when you need to "create" a QApplication. # Only one QApplication can be created. Use QtGui.QApplication.instance() when you need to "create" a QApplication.
application = QtGui.QApplication([]) application = QtGui.QApplication([])
__all__ = ['MagicMock', 'patch', 'mock_open', 'call', 'application']

View File

@ -30,10 +30,11 @@
This module contains tests for the CCLI SongSelect importer. This module contains tests for the CCLI SongSelect importer.
""" """
from unittest import TestCase from unittest import TestCase
from urllib.error import URLError
from openlp.plugins.songs.lib.songselect import SongSelectImport, LOGIN_URL, LOGOUT_URL, BASE_URL from openlp.plugins.songs.lib.songselect import SongSelectImport, LOGOUT_URL, BASE_URL
from tests.functional import MagicMock, patch from tests.functional import MagicMock, patch, call
class TestSongSelect(TestCase): class TestSongSelect(TestCase):
@ -125,8 +126,8 @@ class TestSongSelect(TestCase):
Test that when the search finds no results, it simply returns an empty list 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 # 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( with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
'openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup: patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
mocked_opener = MagicMock() mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener mocked_build_opener.return_value = mocked_opener
mocked_results_page = MagicMock() mocked_results_page = MagicMock()
@ -150,8 +151,8 @@ class TestSongSelect(TestCase):
Test that when the search finds 2 results, it simply returns a list with 2 results 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 # 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( with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
'openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup: patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
# first search result # first search result
mocked_result1 = MagicMock() mocked_result1 = MagicMock()
mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}] mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
@ -188,8 +189,8 @@ class TestSongSelect(TestCase):
Test that when the search finds MAX (2) results, it simply returns a list with those (2) 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 # 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( with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
'openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup: patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
# first search result # first search result
mocked_result1 = MagicMock() mocked_result1 = MagicMock()
mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}] mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
@ -222,3 +223,88 @@ class TestSongSelect(TestCase):
expected_list = [{'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'}, 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'}] {'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') self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
def get_song_page_raises_exception_test(self):
"""
Test that when BeautifulSoup gets a bad song page the get_song() method returns None
"""
# 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
mocked_opener.open.read.side_effect = URLError('[Errno -2] Name or service not known')
mocked_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: get_song is called
result = importer.get_song({'link': 'link'}, callback=mocked_callback)
# THEN: The callback should have been called once and None should be returned
mocked_callback.assert_called_with()
self.assertIsNone(result, 'The get_song() method should have returned None')
def get_song_lyrics_raise_exception_test(self):
"""
Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener'), \
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
MockedBeautifulSoup.side_effect = [None, TypeError('Test Error')]
mocked_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: get_song is called
result = importer.get_song({'link': 'link'}, callback=mocked_callback)
# THEN: The callback should have been called twice and None should be returned
self.assertEqual(2, mocked_callback.call_count, 'The callback should have been called twice')
self.assertIsNone(result, 'The get_song() method should have returned None')
def get_song_test(self):
"""
Test that the get_song() method returns the correct song details
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener'), \
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
mocked_song_page = MagicMock()
mocked_copyright = MagicMock()
mocked_copyright.find_all.return_value = [MagicMock(string='Copyright 1'), MagicMock(string='Copyright 2')]
mocked_song_page.find.side_effect = [
mocked_copyright,
MagicMock(find=MagicMock(string='CCLI: 123456'))
]
mocked_lyrics_page = MagicMock()
mocked_find_all = MagicMock()
mocked_find_all.side_effect = [
[
MagicMock(contents='The Lord told Noah: there\'s gonna be a floody, floody'),
MagicMock(contents='So, rise and shine, and give God the glory, glory'),
MagicMock(contents='The Lord told Noah to build him an arky, arky')
],
[MagicMock(string='Verse 1'), MagicMock(string='Chorus'), MagicMock(string='Verse 2')]
]
mocked_lyrics_page.find.return_value = MagicMock(find_all=mocked_find_all)
MockedBeautifulSoup.side_effect = [mocked_song_page, mocked_lyrics_page]
mocked_callback = MagicMock()
importer = SongSelectImport(None)
fake_song = {'title': 'Title', 'authors': ['Author 1', 'Author 2'], 'link': 'url'}
# WHEN: get_song is called
result = importer.get_song(fake_song, callback=mocked_callback)
# THEN: The callback should have been called three times and the song should be returned
self.assertEqual(3, mocked_callback.call_count, 'The callback should have been called twice')
self.assertIsNotNone(result, 'The get_song() method should have returned a song dictionary')
self.assertEqual(2, mocked_lyrics_page.find.call_count, 'The find() method should have been called twice')
self.assertEqual(2, mocked_find_all.call_count, 'The find_all() method should have been called twice')
self.assertEqual([call('section', 'lyrics'), call('section', 'lyrics')],
mocked_lyrics_page.find.call_args_list,
'The find() method should have been called with the right arguments')
self.assertEqual([call('p'), call('h3')], mocked_find_all.call_args_list,
'The find_all() method should have been called with the right arguments')
self.assertIn('copyright', result, 'The returned song should have a copyright')
self.assertIn('ccli_number', result, 'The returned song should have a CCLI number')
self.assertIn('verses', result, 'The returned song should have verses')
self.assertEqual(3, len(result['verses']), 'Three verses should have been returned')