Merge lp:~phill-ridout/openlp/wow_import_fixes into lp:openlp

Proposed by Phill
Status: Superseded
Proposed branch: lp:~phill-ridout/openlp/wow_import_fixes
Merge into: lp:openlp
Diff against target: 746 lines (+467/-100)
12 files modified
openlp/core/common/i18n.py (+2/-1)
openlp/core/lib/__init__.py (+54/-1)
openlp/core/ui/formattingtagcontroller.py (+1/-1)
openlp/core/widgets/edits.py (+1/-1)
openlp/plugins/songs/lib/importers/wordsofworship.py (+146/-79)
tests/functional/openlp_core/lib/test_lib.py (+180/-2)
tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py (+36/-12)
tests/resources/songs/wordsofworship/Amazing Grace (6 Verses)_v2_1_2.json (+1/-1)
tests/resources/songs/wordsofworship/Holy Holy Holy Lord God Almighty_v2_1_2.json (+1/-1)
tests/resources/songs/wordsofworship/Test_Song_v2_0_0.json (+18/-0)
tests/resources/songs/wordsofworship/Test_Song_v2_1_2.json (+26/-0)
tests/resources/songs/wordsofworship/When morning gilds the skies_v2_0_0.json (+1/-1)
To merge this branch: bzr merge lp:~phill-ridout/openlp/wow_import_fixes
Reviewer Review Type Date Requested Status
Tim Bentley Approve
Review via email: mp+369463@code.launchpad.net

This proposal supersedes a proposal from 2019-06-28.

This proposal has been superseded by a proposal from 2019-06-28.

To post a comment you must log in.
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linux tests failed, please see https://ci.openlp.io/job/MP-02-Linux_Tests/192/ for more details

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linux tests passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Linting failed, please see https://ci.openlp.io/job/MP-03-Linting/127/ for more details

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linux tests passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linting passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

macOS tests passed!

Revision history for this message
Tim Bentley (trb143) :
review: Approve
2883. By Phill

Moar minor changes

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'openlp/core/common/i18n.py'
--- openlp/core/common/i18n.py 2019-04-13 13:00:22 +0000
+++ openlp/core/common/i18n.py 2019-06-28 19:27:19 +0000
@@ -385,7 +385,8 @@
385 self.Error = translate('OpenLP.Ui', 'Error')385 self.Error = translate('OpenLP.Ui', 'Error')
386 self.Export = translate('OpenLP.Ui', 'Export')386 self.Export = translate('OpenLP.Ui', 'Export')
387 self.File = translate('OpenLP.Ui', 'File')387 self.File = translate('OpenLP.Ui', 'File')
388 self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit')388 self.FileCorrupt = translate('OpenLP.Ui', 'File appears to be corrupt.')
389 self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font point size unit')
389 self.Help = translate('OpenLP.Ui', 'Help')390 self.Help = translate('OpenLP.Ui', 'Help')
390 self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')391 self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')
391 self.IFdSs = translate('OpenLP.Ui', 'Invalid Folder Selected', 'Singular')392 self.IFdSs = translate('OpenLP.Ui', 'Invalid Folder Selected', 'Singular')
392393
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py 2019-06-11 19:48:34 +0000
+++ openlp/core/lib/__init__.py 2019-06-28 19:27:19 +0000
@@ -24,15 +24,23 @@
24OpenLP work.24OpenLP work.
25"""25"""
26import logging26import logging
27import os
28from enum import IntEnum
27from pathlib import Path29from pathlib import Path
2830
29from PyQt5 import QtCore, QtGui, QtWidgets31from PyQt5 import QtCore, QtGui, QtWidgets
3032
31from openlp.core.common.i18n import translate33from openlp.core.common.i18n import UiStrings, translate
3234
33log = logging.getLogger(__name__ + '.__init__')35log = logging.getLogger(__name__ + '.__init__')
3436
3537
38class DataType(IntEnum):
39 U8 = 1
40 U16 = 2
41 U32 = 4
42
43
36class ServiceItemContext(object):44class ServiceItemContext(object):
37 """45 """
38 The context in which a Service Item is being generated46 The context in which a Service Item is being generated
@@ -397,3 +405,48 @@
397 else:405 else:
398 list_to_string = ''406 list_to_string = ''
399 return list_to_string407 return list_to_string
408
409
410def read_or_fail(file_object, length):
411 """
412 Ensure that the data read is as the exact length requested. Otherwise raise an OSError.
413
414 :param io.IOBase file_object: The file-lke object ot read from.
415 :param int length: The length of the data to read.
416 :return: The data read.
417 """
418 data = file_object.read(length)
419 if len(data) != length:
420 raise OSError(UiStrings().FileCorrupt)
421 return data
422
423
424def read_int(file_object, data_type, endian='big'):
425 """
426 Read the correct amount of data from a file-like object to decode it to the specified type.
427
428 :param io.IOBase file_object: The file-like object to read from.
429 :param DataType data_type: A member from the :enum:`DataType`
430 :param endian: The endianess of the data to be read
431 :return int: The decoded int
432 """
433 data = read_or_fail(file_object, data_type)
434 return int.from_bytes(data, endian)
435
436
437def seek_or_fail(file_object, offset, how=os.SEEK_SET):
438 """
439 See to a set position and return an error if the cursor has not moved to that position.
440
441 :param io.IOBase file_object: The file-like object to attempt to seek.
442 :param int offset: The offset / position to seek by / to.
443 :param [os.SEEK_CUR | os.SEEK_SET how: Currently only supports os.SEEK_CUR (0) or os.SEEK_SET (1)
444 :return int: The new position in the file.
445 """
446 if how not in (os.SEEK_CUR, os.SEEK_SET):
447 raise NotImplementedError
448 prev_pos = file_object.tell()
449 new_pos = file_object.seek(offset, how)
450 if how == os.SEEK_SET and new_pos != offset or how == os.SEEK_CUR and new_pos != prev_pos + offset:
451 raise OSError(UiStrings().FileCorrupt)
452 return new_pos
400453
=== modified file 'openlp/core/ui/formattingtagcontroller.py'
--- openlp/core/ui/formattingtagcontroller.py 2019-04-13 13:00:22 +0000
+++ openlp/core/ui/formattingtagcontroller.py 2019-06-28 19:27:19 +0000
@@ -84,7 +84,7 @@
84 'desc': desc,84 'desc': desc,
85 'start tag': '{{{tag}}}'.format(tag=tag),85 'start tag': '{{{tag}}}'.format(tag=tag),
86 'start html': start_html,86 'start html': start_html,
87 'end tag': '{{{tag}}}'.format(tag=tag),87 'end tag': '{{/{tag}}}'.format(tag=tag),
88 'end html': end_html,88 'end html': end_html,
89 'protected': False,89 'protected': False,
90 'temporary': False90 'temporary': False
9191
=== modified file 'openlp/core/widgets/edits.py'
--- openlp/core/widgets/edits.py 2019-05-22 20:46:51 +0000
+++ openlp/core/widgets/edits.py 2019-06-28 19:27:19 +0000
@@ -353,7 +353,7 @@
353 :rtype: None353 :rtype: None
354 """354 """
355 if self._path != path:355 if self._path != path:
356 self._path = path356 self.path = path
357 self.pathChanged.emit(path)357 self.pathChanged.emit(path)
358358
359359
360360
=== modified file 'openlp/plugins/songs/lib/importers/wordsofworship.py'
--- openlp/plugins/songs/lib/importers/wordsofworship.py 2019-04-13 13:00:22 +0000
+++ openlp/plugins/songs/lib/importers/wordsofworship.py 2019-06-28 19:27:19 +0000
@@ -26,7 +26,8 @@
26import logging26import logging
27import os27import os
2828
29from openlp.core.common.i18n import translate29from openlp.core.common.i18n import UiStrings, translate
30from openlp.core.lib import DataType, read_int, read_or_fail, seek_or_fail
30from openlp.plugins.songs.lib.importers.songimport import SongImport31from openlp.plugins.songs.lib.importers.songimport import SongImport
3132
3233
@@ -48,52 +49,138 @@
48 the author and the copyright.49 the author and the copyright.
49 * A block can be a verse, chorus or bridge.50 * A block can be a verse, chorus or bridge.
5051
52 Little endian is used.
53
51 File Header:54 File Header:
52 Bytes are counted from one, i.e. the first byte is byte 1. The first 1955 Bytes are counted from one, i.e. the first byte is byte 1.
53 bytes should be "WoW File \\nSong Words" The bytes after this and up to56
54 the 56th byte, can change but no real meaning has been found. The57 0x00 - 0x13 Should be "WoW File \nSong Words\n"
55 56th byte specifies how many blocks there are. The first block starts58 0x14 - 0x1F Minimum version of Words Of Worship required to open this file
56 with byte 83 after the "CSongDoc::CBlock" declaration.59 0x20 - 0x2B Minimum version of Words Of Worship required to save this file without data loss
60 0x2C - 0x37 The version of Words of Worship that this file is from. From test data, it looks like this might be
61 the version that originally created this file, not the last version to save it.
62
63 The Words Of Worship versioning system seems to be in the format:
64 ``Major.Minor.Patch``
65
66 Where each part of the version number is stored by a 32-bit int
67
68 0x38 - 0x3B Specifies how many blocks there are.
69
70 0x42 - 0x51 Should be "CSongDoc::CBlock"
71
72 0x52 The first song blocks start from here.
5773
58 Blocks:74 Blocks:
59 Each block has a starting header, some lines of text, and an ending75 Each block starts with a 32-bit int which specifies how many lines are in that block.
60 footer. Each block starts with a 32 bit number, which specifies how76
61 many lines are in that block.77 Then there are a number of lines corresponding to the value above.
6278
63 Each block ends with a 32 bit number, which defines what type of79 Each block ends with a 32 bit number, which defines what type of
64 block it is:80 block it is:
6581
66 * ``NUL`` (0x00) - Verse82 * 0x00000000 = Verse
67 * ``SOH`` (0x01) - Chorus83 * 0x01000000 = Chorus
68 * ``STX`` (0x02) - Bridge84 * 0x02000000 = Bridge
6985
70 Blocks are separated by two bytes. The first byte is 0x01, and the86 Blocks are separated by two bytes. The first byte is 0x01, and the
71 second byte is 0x80.87 second byte is 0x80.
7288
73 Lines:89 Lines:
74 Each line starts with a byte which specifies how long that line is,90 Each line consists of a "Pascal" string.
75 the line text, and ends with a null byte.91 In later versions, a byte follows which denotes the formatting of the line:
92
93 * 0x00 = Normal
94 * 0x01 = Minor
95
96 It looks like this may have been introduced in Words of Worship song version 2.1.0, though this is an educated
97 guess.
7698
77 Footer:99 Footer:
78 The footer follows on after the last block, the first byte specifies100 The footer follows on after the last block. Its format is as follows:
79 the length of the author text, followed by the author text, if101
80 this byte is null, then there is no author text. The byte after the102 Author String (as a 'Pascal' string)
81 author text specifies the length of the copyright text, followed103 Copyright String (as a 'Pascal' string)
82 by the copyright text.104
83105 Finally in newer versions of Word Of Worship song files there is a 32 bit int describing the copyright.
84 The file is ended with four null bytes.106
107 0x00000000 = Covered by CCL
108 0x01000000 = Authors explicit permission
109 0x02000000 = Public Domain
110 0x03000000 = Copyright expired
111 0x04000000 = Other
112
113 Pascal Strings:
114 Strings are preceded by a variable length integer which specifies how many bytes are in the string. An example
115 of the variable length integer is below.
116
117 Lentgh bytes 'Little'| Str len
118 -------------------------------
119 01 | 01
120 02 | 02
121 .... |
122 FD | FD
123 FE | FE
124 FF FF 00 | FF
125 FF 00 01 | 01 00
126 FF 01 01 | 01 01
127 FF 02 01 | 01 02
128 .... |
129 FF FC FF | FF FC
130 FF FD FF | FF FD
131 FF FF FF FE FF | FF FE
132 FF FF FF FF FF 00 00 | FF FF
133 FF FF FF 00 00 01 00 | 01 00 00
134 FF FF FF 01 00 01 00 | 01 00 01
135 FF FF FF 02 00 02 00 | 01 00 02
85136
86 Valid extensions for a Words of Worship song file are:137 Valid extensions for a Words of Worship song file are:
87138
88 * .wsg139 * .wsg
89 * .wow-song140 * .wow-song
90 """141 """
91142 @staticmethod
92 def __init__(self, manager, **kwargs):143 def parse_string(song_data):
93 """144 length_bytes = song_data.read(DataType.U8)
94 Initialise the Words of Worship importer.145 if length_bytes == b'\xff':
95 """146 length_bytes = song_data.read(DataType.U16)
96 super(WordsOfWorshipImport, self).__init__(manager, **kwargs)147 length = int.from_bytes(length_bytes, 'little')
148 return read_or_fail(song_data, length).decode('cp1252')
149
150 def parse_lines(self, song_data):
151 lines = []
152 lines_to_read = read_int(song_data, DataType.U32, 'little')
153 for line_no in range(0, lines_to_read):
154 line_text = self.parse_string(song_data)
155 if self.read_version >= (2, 1, 0):
156 if read_or_fail(song_data, DataType.U8) == b'\x01':
157 line_text = '{{minor}}{text}{{/minor}}'.format(text=line_text)
158 lines.append(line_text)
159 return '\n'.join(lines)
160
161 @staticmethod
162 def parse_version(song_data):
163 return (read_int(song_data, DataType.U32, 'little'),
164 read_int(song_data, DataType.U32, 'little'),
165 read_int(song_data, DataType.U32, 'little'))
166
167 def vaildate(self, file_path, song_data):
168 seek_or_fail(song_data, 0x00)
169 err_text = b''
170 data = read_or_fail(song_data, 20)
171 if data != b'WoW File\nSong Words\n':
172 err_text = data
173 seek_or_fail(song_data, 0x42)
174 data = read_or_fail(song_data, 16)
175 if data != b'CSongDoc::CBlock':
176 err_text = data
177 if err_text:
178 self.log_error(file_path,
179 translate('SongsPlugin.WordsofWorshipSongImport',
180 'Invalid Words of Worship song file. Missing {text!r} header.'
181 ).format(text=err_text))
182 return False
183 return True
97184
98 def do_import(self):185 def do_import(self):
99 """186 """
@@ -104,57 +191,37 @@
104 for file_path in self.import_source:191 for file_path in self.import_source:
105 if self.stop_import_flag:192 if self.stop_import_flag:
106 return193 return
107 self.set_defaults()194 log.debug('Importing %s', file_path)
108 with file_path.open('rb') as song_data:195 try:
109 if song_data.read(19).decode() != 'WoW File\nSong Words':196 self.set_defaults()
110 self.log_error(file_path,
111 translate('SongsPlugin.WordsofWorshipSongImport',
112 'Invalid Words of Worship song file. Missing "{text}" '
113 'header.').format(text='WoW File\\nSong Words'))
114 continue
115 # Seek to byte which stores number of blocks in the song
116 song_data.seek(56)
117 no_of_blocks = ord(song_data.read(1))
118 song_data.seek(66)
119 if song_data.read(16).decode() != 'CSongDoc::CBlock':
120 self.log_error(file_path,
121 translate('SongsPlugin.WordsofWorshipSongImport',
122 'Invalid Words of Worship song file. Missing "{text}" '
123 'string.').format(text='CSongDoc::CBlock'))
124 continue
125 # Seek to the beginning of the first block
126 song_data.seek(82)
127 for block in range(no_of_blocks):
128 skip_char_at_end = True
129 self.lines_to_read = ord(song_data.read(4)[:1])
130 block_text = ''
131 while self.lines_to_read:
132 self.line_text = str(song_data.read(ord(song_data.read(1))), 'cp1252')
133 if skip_char_at_end:
134 skip_char = ord(song_data.read(1))
135 # Check if we really should skip a char. In some wsg files we shouldn't
136 if skip_char != 0:
137 song_data.seek(-1, os.SEEK_CUR)
138 skip_char_at_end = False
139 if block_text:
140 block_text += '\n'
141 block_text += self.line_text
142 self.lines_to_read -= 1
143 block_type = BLOCK_TYPES[ord(song_data.read(4)[:1])]
144 # Blocks are separated by 2 bytes, skip them, but not if
145 # this is the last block!
146 if block + 1 < no_of_blocks:
147 song_data.seek(2, os.SEEK_CUR)
148 self.add_verse(block_text, block_type)
149 # Now to extract the author
150 author_length = ord(song_data.read(1))
151 if author_length:
152 self.parse_author(str(song_data.read(author_length), 'cp1252'))
153 # Finally the copyright
154 copyright_length = ord(song_data.read(1))
155 if copyright_length:
156 self.add_copyright(str(song_data.read(copyright_length), 'cp1252'))
157 # Get the song title197 # Get the song title
158 self.title = file_path.stem198 self.title = file_path.stem
159 if not self.finish():199 with file_path.open('rb') as song_data:
160 self.log_error(file_path)200 if not self.vaildate(file_path, song_data):
201 continue
202 seek_or_fail(song_data, 20)
203 self.read_version = self.parse_version(song_data)
204 # Seek to byte which stores number of blocks in the song
205 seek_or_fail(song_data, 56)
206 no_of_blocks = read_int(song_data, DataType.U8)
207
208 # Seek to the beginning of the first block
209 seek_or_fail(song_data, 82)
210 for block_no in range(no_of_blocks):
211 # Blocks are separated by 2 bytes, skip them, but not if this is the last block!
212 if block_no != 0:
213 seek_or_fail(song_data, 2, os.SEEK_CUR)
214 text = self.parse_lines(song_data)
215 block_type = BLOCK_TYPES[read_int(song_data, DataType.U32, 'little')]
216 self.add_verse(text, block_type)
217
218 # Now to extract the author
219 self.parse_author(self.parse_string(song_data))
220 # Finally the copyright
221 self.add_copyright(self.parse_string(song_data))
222 if not self.finish():
223 self.log_error(file_path)
224 except IndexError:
225 self.log_error(file_path, UiStrings().FileCorrupt)
226 except Exception as e:
227 self.log_error(file_path, e)
161228
=== modified file 'tests/functional/openlp_core/lib/test_lib.py'
--- tests/functional/openlp_core/lib/test_lib.py 2019-05-22 06:47:00 +0000
+++ tests/functional/openlp_core/lib/test_lib.py 2019-06-28 19:27:19 +0000
@@ -22,14 +22,16 @@
22"""22"""
23Package to test the openlp.core.lib package.23Package to test the openlp.core.lib package.
24"""24"""
25import io
26import os
25from pathlib import Path27from pathlib import Path
26from unittest import TestCase28from unittest import TestCase
27from unittest.mock import MagicMock, patch29from unittest.mock import MagicMock, patch
2830
29from PyQt5 import QtCore, QtGui31from PyQt5 import QtCore, QtGui
3032
31from openlp.core.lib import build_icon, check_item_selected, create_separated_list, create_thumb, \33from openlp.core.lib import DataType, build_icon, check_item_selected, create_separated_list, create_thumb, \
32 get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb34 get_text_file_string, image_to_byte, read_or_fail, read_int, resize_image, seek_or_fail, str_to_bool, validate_thumb
33from tests.utils.constants import RESOURCE_PATH35from tests.utils.constants import RESOURCE_PATH
3436
3537
@@ -680,3 +682,179 @@
680 # THEN: We should have "Author 1, Author 2 and Author 3"682 # THEN: We should have "Author 1, Author 2 and Author 3"
681 assert string_result == 'Author 1, Author 2 and Author 3', \683 assert string_result == 'Author 1, Author 2 and Author 3', \
682 'The string should be "Author 1, Author 2, and Author 3".'684 'The string should be "Author 1, Author 2, and Author 3".'
685
686 def test_read_or_fail_fail(self):
687 """
688 Test the :func:`read_or_fail` function when attempting to read more data than the buffer contains.
689 """
690 # GIVEN: Some test data
691 test_data = io.BytesIO(b'test data')
692
693 # WHEN: Attempting to read past the end of the buffer
694 # THEN: An OSError should be raised.
695 with self.assertRaises(OSError):
696 read_or_fail(test_data, 15)
697
698 def test_read_or_fail_success(self):
699 """
700 Test the :func:`read_or_fail` function when reading data that is in the buffer.
701 """
702 # GIVEN: Some test data
703 test_data = io.BytesIO(b'test data')
704
705 # WHEN: Attempting to read data that should exist.
706 result = read_or_fail(test_data, 4)
707
708 # THEN: The data of the requested length should be returned
709 assert result == b'test'
710
711 def test_read_int_u8_big(self):
712 """
713 Test the :func:`read_int` function when reading an unsigned 8-bit int using 'big' endianness.
714 """
715 # GIVEN: Some test data
716 test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
717
718 # WHEN: Reading a an unsigned 8-bit int
719 result = read_int(test_data, DataType.U8, 'big')
720
721 # THEN: The an int should have been returned of the expected value
722 assert result == 15
723
724 def test_read_int_u8_little(self):
725 """
726 Test the :func:`read_int` function when reading an unsigned 8-bit int using 'little' endianness.
727 """
728 # GIVEN: Some test data
729 test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
730
731 # WHEN: Reading a an unsigned 8-bit int
732 result = read_int(test_data, DataType.U8, 'little')
733
734 # THEN: The an int should have been returned of the expected value
735 assert result == 15
736
737 def test_read_int_u16_big(self):
738 """
739 Test the :func:`read_int` function when reading an unsigned 16-bit int using 'big' endianness.
740 """
741 # GIVEN: Some test data
742 test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
743
744 # WHEN: Reading a an unsigned 16-bit int
745 result = read_int(test_data, DataType.U16, 'big')
746
747 # THEN: The an int should have been returned of the expected value
748 assert result == 4080
749
750 def test_read_int_u16_little(self):
751 """
752 Test the :func:`read_int` function when reading an unsigned 16-bit int using 'little' endianness.
753 """
754 # GIVEN: Some test data
755 test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
756
757 # WHEN: Reading a an unsigned 16-bit int
758 result = read_int(test_data, DataType.U16, 'little')
759
760 # THEN: The an int should have been returned of the expected value
761 assert result == 61455
762
763 def test_read_int_u32_big(self):
764 """
765 Test the :func:`read_int` function when reading an unsigned 32-bit int using 'big' endianness.
766 """
767 # GIVEN: Some test data
768 test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
769
770 # WHEN: Reading a an unsigned 32-bit int
771 result = read_int(test_data, DataType.U32, 'big')
772
773 # THEN: The an int should have been returned of the expected value
774 assert result == 267390960
775
776 def test_read_int_u32_little(self):
777 """
778 Test the :func:`read_int` function when reading an unsigned 32-bit int using 'little' endianness.
779 """
780 # GIVEN: Some test data
781 test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
782
783 # WHEN: Reading a an unsigned 32-bit int
784 result = read_int(test_data, DataType.U32, 'little')
785
786 # THEN: The an int should have been returned of the expected value
787 assert result == 4027576335
788
789 def test_seek_or_fail_default_method(self):
790 """
791 Test the :func:`seek_or_fail` function when using the default value for the :arg:`how`
792 """
793 # GIVEN: A mocked_file_like_object
794 mocked_file_like_object = MagicMock(**{'seek.return_value': 5, 'tell.return_value': 0})
795
796 # WHEN: Calling seek_or_fail with out the how arg set
797 seek_or_fail(mocked_file_like_object, 5)
798
799 # THEN: seek should be called using the os.SEEK_SET constant
800 mocked_file_like_object.seek.assert_called_once_with(5, os.SEEK_SET)
801
802 def test_seek_or_fail_os_end(self):
803 """
804 Test the :func:`seek_or_fail` function when called with an unsupported seek operation.
805 """
806 # GIVEN: A Mocked object
807 # WHEN: Attempting to seek relative to the end
808 # THEN: An NotImplementedError should have been raised
809 with self.assertRaises(NotImplementedError):
810 seek_or_fail(MagicMock(), 1, os.SEEK_END)
811
812 def test_seek_or_fail_valid_seek_set(self):
813 """
814 Test that :func:`seek_or_fail` successfully seeks to the correct position.
815 """
816 # GIVEN: A mocked file-like object
817 mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 5})
818
819 # WHEN: Attempting to seek from the beginning
820 result = seek_or_fail(mocked_file_like_object, 5, os.SEEK_SET)
821
822 # THEN: The new position should be 5 from the beginning
823 assert result == 5
824
825 def test_seek_or_fail_invalid_seek_set(self):
826 """
827 Test that :func:`seek_or_fail` raises an exception when seeking past the end.
828 """
829 # GIVEN: A Mocked file-like object
830 mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 10})
831
832 # WHEN: Attempting to seek from the beginning past the end
833 # THEN: An OSError should have been raised
834 with self.assertRaises(OSError):
835 seek_or_fail(mocked_file_like_object, 15, os.SEEK_SET)
836
837 def test_seek_or_fail_valid_seek_cur(self):
838 """
839 Test that :func:`seek_or_fail` successfully seeks to the correct position.
840 """
841 # GIVEN: A mocked file_like object
842 mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 8})
843
844 # WHEN: Attempting to seek from the current position
845 result = seek_or_fail(mocked_file_like_object, 5, os.SEEK_CUR)
846
847 # THEN: The new position should be 8 (5 from its starting position)
848 assert result == 8
849
850 def test_seek_or_fail_invalid_seek_cur(self):
851 """
852 Test that :func:`seek_or_fail` raises an exception when seeking past the end.
853 """
854 # GIVEN: A mocked file_like object
855 mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 10})
856
857 # WHEN: Attempting to seek from the current position pas the end.
858 # THEN: An OSError should have been raised
859 with self.assertRaises(OSError):
860 seek_or_fail(mocked_file_like_object, 15, os.SEEK_CUR)
683861
=== modified file 'tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py'
--- tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py 2019-04-13 13:00:22 +0000
+++ tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py 2019-06-28 19:27:19 +0000
@@ -34,15 +34,39 @@
34 def __init__(self, *args, **kwargs):34 def __init__(self, *args, **kwargs):
35 self.importer_class_name = 'WordsOfWorshipImport'35 self.importer_class_name = 'WordsOfWorshipImport'
36 self.importer_module_name = 'wordsofworship'36 self.importer_module_name = 'wordsofworship'
37 super(TestWordsOfWorshipFileImport, self).__init__(*args, **kwargs)37 super().__init__(*args, **kwargs)
3838
39 def test_song_import(self):39 def test_amazing_grace_song_import(self):
40 """40 """
41 Test that loading a Words of Worship file works correctly41 Test that loading a Words of Worship file works correctly
42 """42 """
43 self.file_import([TEST_PATH / 'Amazing Grace (6 Verses).wow-song'],43 self.file_import([TEST_PATH / 'Amazing Grace (6 Verses)_v2_1_2.wow-song'],
44 self.load_external_result_data(TEST_PATH / 'Amazing Grace (6 Verses).json'))44 self.load_external_result_data(TEST_PATH / 'Amazing Grace (6 Verses)_v2_1_2.json'))
45 self.file_import([TEST_PATH / 'When morning gilds the skies.wsg'],45
46 self.load_external_result_data(TEST_PATH / 'When morning gilds the skies.json'))46 def test_when_morning_gilds_song_import(self):
47 self.file_import([TEST_PATH / 'Holy Holy Holy Lord God Almighty.wow-song'],47 """
48 self.load_external_result_data(TEST_PATH / 'Holy Holy Holy Lord God Almighty.json'))48 Test that loading a Words of Worship file v2.0.0 works correctly
49 """
50 self.file_import([TEST_PATH / 'When morning gilds the skies_v2_0_0.wsg'],
51 self.load_external_result_data(TEST_PATH / 'When morning gilds the skies_v2_0_0.json'))
52
53 def test_holy_holy_holy_song_import(self):
54 """
55 Test that loading a Words of Worship file works correctly
56 """
57 self.file_import([TEST_PATH / 'Holy Holy Holy Lord God Almighty_v2_1_2.wow-song'],
58 self.load_external_result_data(TEST_PATH / 'Holy Holy Holy Lord God Almighty_v2_1_2.json'))
59
60 def test_test_song_v2_0_0_song_import(self):
61 """
62 Test that loading a Words of Worship file v2.0.0 works correctly
63 """
64 self.file_import([TEST_PATH / 'Test_Song_v2_0_0.wsg'],
65 self.load_external_result_data(TEST_PATH / 'Test_Song_v2_0_0.json'))
66
67 def test_test_song_song_import(self):
68 """
69 Test that loading a Words of Worship file v2.1.2 works correctly
70 """
71 self.file_import([TEST_PATH / 'Test_Song_v2_1_2.wow-song'],
72 self.load_external_result_data(TEST_PATH / 'Test_Song_v2_1_2.json'))
4973
=== renamed file 'tests/resources/songs/wordsofworship/Amazing Grace (6 Verses).json' => 'tests/resources/songs/wordsofworship/Amazing Grace (6 Verses)_v2_1_2.json'
--- tests/resources/songs/wordsofworship/Amazing Grace (6 Verses).json 2014-11-03 14:36:27 +0000
+++ tests/resources/songs/wordsofworship/Amazing Grace (6 Verses)_v2_1_2.json 2019-06-28 19:27:19 +0000
@@ -2,7 +2,7 @@
2 "authors": [2 "authors": [
3 "John Newton (1725-1807)"3 "John Newton (1725-1807)"
4 ],4 ],
5 "title": "Amazing Grace (6 Verses)",5 "title": "Amazing Grace (6 Verses)_v2_1_2",
6 "verse_order_list": [],6 "verse_order_list": [],
7 "verses": [7 "verses": [
8 [8 [
99
=== renamed file 'tests/resources/songs/wordsofworship/Amazing Grace (6 Verses).wow-song' => 'tests/resources/songs/wordsofworship/Amazing Grace (6 Verses)_v2_1_2.wow-song'
=== renamed file 'tests/resources/songs/wordsofworship/Holy Holy Holy Lord God Almighty.json' => 'tests/resources/songs/wordsofworship/Holy Holy Holy Lord God Almighty_v2_1_2.json'
--- tests/resources/songs/wordsofworship/Holy Holy Holy Lord God Almighty.json 2015-08-26 08:50:38 +0000
+++ tests/resources/songs/wordsofworship/Holy Holy Holy Lord God Almighty_v2_1_2.json 2019-06-28 19:27:19 +0000
@@ -2,7 +2,7 @@
2 "authors": [2 "authors": [
3 "Words: Reginald Heber (1783-1826). Music: John B. Dykes (1823-1876)"3 "Words: Reginald Heber (1783-1826). Music: John B. Dykes (1823-1876)"
4 ],4 ],
5 "title": "Holy Holy Holy Lord God Almighty",5 "title": "Holy Holy Holy Lord God Almighty_v2_1_2",
6 "verse_order_list": [],6 "verse_order_list": [],
7 "verses": [7 "verses": [
8 [8 [
99
=== renamed file 'tests/resources/songs/wordsofworship/Holy Holy Holy Lord God Almighty.wow-song' => 'tests/resources/songs/wordsofworship/Holy Holy Holy Lord God Almighty_v2_1_2.wow-song'
=== added file 'tests/resources/songs/wordsofworship/Test_Song_v2_0_0.json'
--- tests/resources/songs/wordsofworship/Test_Song_v2_0_0.json 1970-01-01 00:00:00 +0000
+++ tests/resources/songs/wordsofworship/Test_Song_v2_0_0.json 2019-06-28 19:27:19 +0000
@@ -0,0 +1,18 @@
1{
2 "authors": [
3 "Author"
4 ],
5 "copyright": "Copyright",
6 "title": "Test_Song_v2_0_0",
7 "verse_order_list": [],
8 "verses": [
9 [
10 "Verse 1 Line 1\nVerse 1 Line 2\nVerse 1 Line 3\nVerse 1 Line 4",
11 "V"
12 ],
13 [
14 "Chorus 1 Line 1\nChorus 1 Line 2\nChorus 1 Line 3\nChorus 1 Line 4\nChorus 1 Line 5",
15 "C"
16 ]
17 ]
18}
019
=== added file 'tests/resources/songs/wordsofworship/Test_Song_v2_0_0.wsg'
1Binary files tests/resources/songs/wordsofworship/Test_Song_v2_0_0.wsg 1970-01-01 00:00:00 +0000 and tests/resources/songs/wordsofworship/Test_Song_v2_0_0.wsg 2019-06-28 19:27:19 +0000 differ20Binary files tests/resources/songs/wordsofworship/Test_Song_v2_0_0.wsg 1970-01-01 00:00:00 +0000 and tests/resources/songs/wordsofworship/Test_Song_v2_0_0.wsg 2019-06-28 19:27:19 +0000 differ
=== added file 'tests/resources/songs/wordsofworship/Test_Song_v2_1_2.json'
--- tests/resources/songs/wordsofworship/Test_Song_v2_1_2.json 1970-01-01 00:00:00 +0000
+++ tests/resources/songs/wordsofworship/Test_Song_v2_1_2.json 2019-06-28 19:27:19 +0000
@@ -0,0 +1,26 @@
1{
2 "authors": [
3 "Author"
4 ],
5 "copyright": "Copyright",
6 "title": "Test_Song_v2_1_2",
7 "verse_order_list": [],
8 "verses": [
9 [
10 "Verse 1 Line 1\n{minor}Verse 1 Line 2 Minor{/minor}",
11 "V"
12 ],
13 [
14 "Chorus 1 Line 1\n{minor}Chorus 1 Line 2 Minor{/minor}",
15 "C"
16 ],
17 [
18 "Bridge 1 Line 1\n{minor}Bridge 1 Line 2{/minor}",
19 "B"
20 ],
21 [
22 "Verse 2 Line 1\n{minor}Verse 2 Line 2{/minor}",
23 "V"
24 ]
25 ]
26}
027
=== added file 'tests/resources/songs/wordsofworship/Test_Song_v2_1_2.wow-song'
1Binary files tests/resources/songs/wordsofworship/Test_Song_v2_1_2.wow-song 1970-01-01 00:00:00 +0000 and tests/resources/songs/wordsofworship/Test_Song_v2_1_2.wow-song 2019-06-28 19:27:19 +0000 differ28Binary files tests/resources/songs/wordsofworship/Test_Song_v2_1_2.wow-song 1970-01-01 00:00:00 +0000 and tests/resources/songs/wordsofworship/Test_Song_v2_1_2.wow-song 2019-06-28 19:27:19 +0000 differ
=== renamed file 'tests/resources/songs/wordsofworship/When morning gilds the skies.json' => 'tests/resources/songs/wordsofworship/When morning gilds the skies_v2_0_0.json'
--- tests/resources/songs/wordsofworship/When morning gilds the skies.json 2014-11-05 13:04:43 +0000
+++ tests/resources/songs/wordsofworship/When morning gilds the skies_v2_0_0.json 2019-06-28 19:27:19 +0000
@@ -2,7 +2,7 @@
2 "authors": [2 "authors": [
3 "Author Unknown. Tr. Edward Caswall"3 "Author Unknown. Tr. Edward Caswall"
4 ],4 ],
5 "title": "When morning gilds the skies",5 "title": "When morning gilds the skies_v2_0_0",
6 "verse_order_list": [],6 "verse_order_list": [],
7 "verses": [7 "verses": [
8 [8 [
99
=== renamed file 'tests/resources/songs/wordsofworship/When morning gilds the skies.wsg' => 'tests/resources/songs/wordsofworship/When morning gilds the skies_v2_0_0.wsg'