diff --git a/openlp/core/ui/media/systemplayer.py b/openlp/core/ui/media/systemplayer.py index a36ce445b..e632f9b7b 100644 --- a/openlp/core/ui/media/systemplayer.py +++ b/openlp/core/ui/media/systemplayer.py @@ -95,6 +95,16 @@ class SystemPlayer(MediaPlayer): mime_type_list.append(ext) log.info('MediaPlugin: %s extensions: %s', mime_type, ' '.join(extensions)) + def disconnect_slots(self, signal): + """ + Safely disconnect the slots from `signal` + """ + try: + signal.disconnect() + except TypeError: + # If disconnect() is called on a signal without slots, it throws a TypeError + pass + def setup(self, display): """ Set up the player widgets @@ -160,6 +170,7 @@ class SystemPlayer(MediaPlayer): if start_time > 0: self.seek(display, controller.media_info.start_time * 1000) self.volume(display, controller.media_info.volume) + self.disconnect_slots(display.media_player.durationChanged) display.media_player.durationChanged.connect(functools.partial(self.set_duration, controller)) self.set_state(MediaState.Playing, display) display.video_widget.raise_() diff --git a/tests/functional/openlp_core_ui_media/test_systemplayer.py b/tests/functional/openlp_core_ui_media/test_systemplayer.py index d1873d2df..2bac0089b 100644 --- a/tests/functional/openlp_core_ui_media/test_systemplayer.py +++ b/tests/functional/openlp_core_ui_media/test_systemplayer.py @@ -107,6 +107,22 @@ class TestSystemPlayer(TestCase): mocked_video_widget.hide.assert_called_once_with() self.assertTrue(player.has_own_widget) + def test_disconnect_slots(self): + """ + Test that we the disconnect slots method catches the TypeError + """ + # GIVEN: A SystemPlayer class and a signal that throws a TypeError + player = SystemPlayer(self) + mocked_signal = MagicMock() + mocked_signal.disconnect.side_effect = \ + TypeError('disconnect() failed between \'durationChanged\' and all its connections') + + # WHEN: disconnect_slots() is called + player.disconnect_slots(mocked_signal) + + # THEN: disconnect should have been called and the exception should have been ignored + mocked_signal.disconnect.assert_called_once_with() + def test_check_available(self): """ Test the check_available() method on SystemPlayer @@ -198,7 +214,8 @@ class TestSystemPlayer(TestCase): with patch.object(player, 'get_live_state') as mocked_get_live_state, \ patch.object(player, 'seek') as mocked_seek, \ patch.object(player, 'volume') as mocked_volume, \ - patch.object(player, 'set_state') as mocked_set_state: + patch.object(player, 'set_state') as mocked_set_state, \ + patch.object(player, 'disconnect_slots') as mocked_disconnect_slots: mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PlayingState result = player.play(mocked_display) @@ -207,6 +224,7 @@ class TestSystemPlayer(TestCase): mocked_display.media_player.play.assert_called_once_with() mocked_seek.assert_called_once_with(mocked_display, 1000) mocked_volume.assert_called_once_with(mocked_display, 1) + mocked_disconnect_slots.assert_called_once_with(mocked_display.media_player.durationChanged) mocked_display.media_player.durationChanged.connect.assert_called_once_with('function') mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display) mocked_display.video_widget.raise_.assert_called_once_with() diff --git a/tests/functional/openlp_plugins/songs/test_mediashout.py b/tests/functional/openlp_plugins/songs/test_mediashout.py new file mode 100644 index 000000000..4f03c4f94 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_mediashout.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; 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 # +############################################################################### +""" +Test the MediaShout importer +""" +from unittest import TestCase +from collections import namedtuple + +from openlp.core.common import Registry +from openlp.plugins.songs.lib.importers.mediashout import MediaShoutImport + +from tests.functional import MagicMock, patch, call + + +class TestMediaShoutImport(TestCase): + """ + Test the MediaShout importer + """ + def setUp(self): + """ + Set the tests up + """ + Registry().create() + + def test_constructor(self): + """ + Test the MediaShoutImport constructor + """ + # GIVEN: A MediaShoutImport class + # WHEN: It is created + importer = MediaShoutImport(MagicMock(), filename='mediashout.db') + + # THEN: It should not be None + self.assertIsNotNone(importer) + + @patch('openlp.plugins.songs.lib.importers.mediashout.pyodbc') + def test_do_import_fails_to_connect(self, mocked_pyodbc): + """ + Test that do_import exits early when unable to connect to the database + """ + # GIVEN: A MediaShoutImport instance + importer = MediaShoutImport(MagicMock(), filename='mediashout.db') + mocked_pyodbc.connect.side_effect = Exception('Unable to connect') + + # WHEN: do_import is called + with patch.object(importer, 'log_error') as mocked_log_error: + importer.do_import() + + # THEN: The songs should have been imported + mocked_log_error.assert_called_once_with('mediashout.db', 'Unable to open the MediaShout database.') + + @patch('openlp.plugins.songs.lib.importers.mediashout.pyodbc') + def test_do_import(self, mocked_pyodbc): + """ + Test the MediaShoutImport do_import method + """ + SongRecord = namedtuple('SongRecord', 'Record, Title, Author, Copyright, SongID, CCLI, Notes') + VerseRecord = namedtuple('VerseRecord', 'Type, Number, Text') + PlayOrderRecord = namedtuple('PlayOrderRecord', 'Type, Number, POrder') + ThemeRecord = namedtuple('ThemeRecord', 'Name') + GroupRecord = namedtuple('GroupRecord', 'Name') + song = SongRecord(1, 'Amazing Grace', 'William Wilberforce', 'Public Domain', 1, '654321', '') + verse = VerseRecord('Verse', 1, 'Amazing grace, how sweet the sound\nThat saved a wretch like me') + play_order = PlayOrderRecord('Verse', 1, 1) + theme = ThemeRecord('Grace') + group = GroupRecord('Hymns') + + # GIVEN: A MediaShoutImport instance and a bunch of stuff mocked out + importer = MediaShoutImport(MagicMock(), filename='mediashout.db') + mocked_cursor = MagicMock() + mocked_cursor.fetchall.side_effect = [[song], [verse], [play_order], [theme], [group]] + mocked_cursor.tables.fetchone.return_value = True + mocked_connection = MagicMock() + mocked_connection.cursor.return_value = mocked_cursor + mocked_pyodbc.connect.return_value = mocked_connection + + # WHEN: do_import is called + with patch.object(importer, 'import_wizard') as mocked_import_wizard, \ + patch.object(importer, 'process_song') as mocked_process_song: + importer.do_import() + + # THEN: The songs should have been imported + expected_execute_calls = [ + call('SELECT Record, Title, Author, Copyright, SongID, CCLI, Notes FROM Songs ORDER BY Title'), + call('SELECT Type, Number, Text FROM Verses WHERE Record = ? ORDER BY Type, Number', 1.0), + call('SELECT Type, Number, POrder FROM PlayOrder WHERE Record = ? ORDER BY POrder', 1.0), + call('SELECT Name FROM Themes INNER JOIN SongThemes ON SongThemes.ThemeId = Themes.ThemeId ' + 'WHERE SongThemes.Record = ?', 1.0), + call('SELECT Name FROM Groups INNER JOIN SongGroups ON SongGroups.GroupId = Groups.GroupId ' + 'WHERE SongGroups.Record = ?', 1.0) + ] + self.assertEqual(expected_execute_calls, mocked_cursor.execute.call_args_list) + mocked_process_song.assert_called_once_with(song, [verse], [play_order], [theme, group]) + + @patch('openlp.plugins.songs.lib.importers.mediashout.pyodbc') + def test_do_import_breaks_on_stop(self, mocked_pyodbc): + """ + Test the MediaShoutImport do_import stops when the user presses the cancel button + """ + SongRecord = namedtuple('SongRecord', 'Record, Title, Author, Copyright, SongID, CCLI, Notes') + song = SongRecord(1, 'Amazing Grace', 'William Wilberforce', 'Public Domain', 1, '654321', '') + + # GIVEN: A MediaShoutImport instance and a bunch of stuff mocked out + importer = MediaShoutImport(MagicMock(), filename='mediashout.db') + mocked_cursor = MagicMock() + mocked_cursor.fetchall.return_value = [song] + mocked_connection = MagicMock() + mocked_connection.cursor.return_value = mocked_cursor + mocked_pyodbc.connect.return_value = mocked_connection + + # WHEN: do_import is called, but cancelled + with patch.object(importer, 'import_wizard') as mocked_import_wizard, \ + patch.object(importer, 'process_song') as mocked_process_song: + importer.stop_import_flag = True + importer.do_import() + + # THEN: The songs should have been imported + mocked_cursor.execute.assert_called_once_with( + 'SELECT Record, Title, Author, Copyright, SongID, CCLI, Notes FROM Songs ORDER BY Title') + mocked_process_song.assert_not_called() + + def test_process_song(self): + """ + Test the process_song method of the MediaShoutImport + """ + # GIVEN: An importer and a song + SongRecord = namedtuple('SongRecord', 'Record, Title, Author, Copyright, SongID, CCLI, Notes') + VerseRecord = namedtuple('VerseRecord', 'Type, Number, Text') + PlayOrderRecord = namedtuple('PlayOrderRecord', 'Type, Number, POrder') + ThemeRecord = namedtuple('ThemeRecord', 'Name') + GroupRecord = namedtuple('GroupRecord', 'Name') + song = SongRecord(1, 'Amazing Grace', 'William Wilberforce', 'Public Domain', 'Hymns', '654321', + 'Great old hymn') + verse = VerseRecord(0, 1, 'Amazing grace, how sweet the sound\nThat saved a wretch like me') + play_order = PlayOrderRecord(0, 1, 1) + theme = ThemeRecord('Grace') + group = GroupRecord('Hymns') + importer = MediaShoutImport(MagicMock(), filename='mediashout.db') + + # WHEN: A song is processed + with patch.object(importer, 'set_defaults') as mocked_set_defaults, \ + patch.object(importer, 'parse_author') as mocked_parse_author, \ + patch.object(importer, 'add_copyright') as mocked_add_copyright, \ + patch.object(importer, 'add_verse') as mocked_add_verse, \ + patch.object(importer, 'finish') as mocked_finish: + importer.topics = [] + importer.verse_order_list = [] + importer.process_song(song, [verse], [play_order], [theme, group]) + + # THEN: It should be added to the database + mocked_set_defaults.assert_called_once_with() + self.assertEqual('Amazing Grace', importer.title) + mocked_parse_author.assert_called_once_with('William Wilberforce') + mocked_add_copyright.assert_called_once_with('Public Domain') + self.assertEqual('Great old hymn', importer.comments) + self.assertEqual(['Grace', 'Hymns'], importer.topics) + self.assertEqual('Hymns', importer.song_book_name) + self.assertEqual('', importer.song_number) + mocked_add_verse.assert_called_once_with( + 'Amazing grace, how sweet the sound\nThat saved a wretch like me', 'V1') + self.assertEqual(['V1'], importer.verse_order_list) + mocked_finish.assert_called_once_with() + + def test_process_song_with_song_number(self): + """ + Test the process_song method with a song that has a song number + """ + # GIVEN: An importer and a song + SongRecord = namedtuple('SongRecord', 'Record, Title, Author, Copyright, SongID, CCLI, Notes') + VerseRecord = namedtuple('VerseRecord', 'Type, Number, Text') + PlayOrderRecord = namedtuple('PlayOrderRecord', 'Type, Number, POrder') + ThemeRecord = namedtuple('ThemeRecord', 'Name') + GroupRecord = namedtuple('GroupRecord', 'Name') + song = SongRecord(1, 'Amazing Grace', 'William Wilberforce', 'Public Domain', 'Hymns-2', '654321', + 'Great old hymn') + verse = VerseRecord(0, 1, 'Amazing grace, how sweet the sound\nThat saved a wretch like me') + play_order = PlayOrderRecord(0, 1, 1) + theme = ThemeRecord('Grace') + group = GroupRecord('Hymns') + importer = MediaShoutImport(MagicMock(), filename='mediashout.db') + + # WHEN: A song is processed + with patch.object(importer, 'set_defaults') as mocked_set_defaults, \ + patch.object(importer, 'parse_author') as mocked_parse_author, \ + patch.object(importer, 'add_copyright') as mocked_add_copyright, \ + patch.object(importer, 'add_verse') as mocked_add_verse, \ + patch.object(importer, 'finish') as mocked_finish: + importer.topics = [] + importer.verse_order_list = [] + importer.process_song(song, [verse], [play_order], [theme, group]) + + # THEN: It should be added to the database + mocked_set_defaults.assert_called_once_with() + self.assertEqual('Amazing Grace', importer.title) + mocked_parse_author.assert_called_once_with('William Wilberforce') + mocked_add_copyright.assert_called_once_with('Public Domain') + self.assertEqual('Great old hymn', importer.comments) + self.assertEqual(['Grace', 'Hymns'], importer.topics) + self.assertEqual('Hymns', importer.song_book_name) + self.assertEqual('2', importer.song_number) + mocked_add_verse.assert_called_once_with( + 'Amazing grace, how sweet the sound\nThat saved a wretch like me', 'V1') + self.assertEqual(['V1'], importer.verse_order_list) + mocked_finish.assert_called_once_with()