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: 759 lines (+468/-101)
13 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)
openlp/plugins/songs/lib/openlyricsxml.py (+1/-1)
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
OpenLP Core Pending
Review via email: mp+369462@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 :

Linux tests passed!

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

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

2882. By Phill

PEP Fixes

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
1=== modified file 'openlp/core/common/i18n.py'
2--- openlp/core/common/i18n.py 2019-04-13 13:00:22 +0000
3+++ openlp/core/common/i18n.py 2019-06-28 18:31:26 +0000
4@@ -385,7 +385,8 @@
5 self.Error = translate('OpenLP.Ui', 'Error')
6 self.Export = translate('OpenLP.Ui', 'Export')
7 self.File = translate('OpenLP.Ui', 'File')
8- self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit')
9+ self.FileCorrupt = translate('OpenLP.Ui', 'File appears to be corrupt.')
10+ self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font point size unit')
11 self.Help = translate('OpenLP.Ui', 'Help')
12 self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')
13 self.IFdSs = translate('OpenLP.Ui', 'Invalid Folder Selected', 'Singular')
14
15=== modified file 'openlp/core/lib/__init__.py'
16--- openlp/core/lib/__init__.py 2019-06-11 19:48:34 +0000
17+++ openlp/core/lib/__init__.py 2019-06-28 18:31:26 +0000
18@@ -24,15 +24,23 @@
19 OpenLP work.
20 """
21 import logging
22+import os
23+from enum import IntEnum
24 from pathlib import Path
25
26 from PyQt5 import QtCore, QtGui, QtWidgets
27
28-from openlp.core.common.i18n import translate
29+from openlp.core.common.i18n import UiStrings, translate
30
31 log = logging.getLogger(__name__ + '.__init__')
32
33
34+class DataType(IntEnum):
35+ U8 = 1
36+ U16 = 2
37+ U32 = 4
38+
39+
40 class ServiceItemContext(object):
41 """
42 The context in which a Service Item is being generated
43@@ -397,3 +405,48 @@
44 else:
45 list_to_string = ''
46 return list_to_string
47+
48+
49+def read_or_fail(file_object, length):
50+ """
51+ Ensure that the data read is as the exact length requested. Otherwise raise an OSError.
52+
53+ :param io.IOBase file_object: The file-lke object ot read from.
54+ :param int length: The length of the data to read.
55+ :return: The data read.
56+ """
57+ data = file_object.read(length)
58+ if len(data) != length:
59+ raise OSError(UiStrings().FileCorrupt)
60+ return data
61+
62+
63+def read_int(file_object, data_type, endian='big'):
64+ """
65+ Read the correct amount of data from a file-like object to decode it to the specified type.
66+
67+ :param io.IOBase file_object: The file-like object to read from.
68+ :param DataType data_type: A member from the :enum:`DataType`
69+ :param endian: The endianess of the data to be read
70+ :return int: The decoded int
71+ """
72+ data = read_or_fail(file_object, data_type)
73+ return int.from_bytes(data, endian)
74+
75+
76+def seek_or_fail(file_object, offset, how=os.SEEK_SET):
77+ """
78+ See to a set position and return an error if the cursor has not moved to that position.
79+
80+ :param io.IOBase file_object: The file-like object to attempt to seek.
81+ :param int offset: The offset / position to seek by / to.
82+ :param [os.SEEK_CUR | os.SEEK_SET how: Currently only supports os.SEEK_CUR (0) or os.SEEK_SET (1)
83+ :return int: The new position in the file.
84+ """
85+ if how not in (os.SEEK_CUR, os.SEEK_SET):
86+ raise NotImplementedError
87+ prev_pos = file_object.tell()
88+ new_pos = file_object.seek(offset, how)
89+ if how == os.SEEK_SET and new_pos != offset or how == os.SEEK_CUR and new_pos != prev_pos + offset:
90+ raise OSError(UiStrings().FileCorrupt)
91+ return new_pos
92
93=== modified file 'openlp/core/ui/formattingtagcontroller.py'
94--- openlp/core/ui/formattingtagcontroller.py 2019-04-13 13:00:22 +0000
95+++ openlp/core/ui/formattingtagcontroller.py 2019-06-28 18:31:26 +0000
96@@ -84,7 +84,7 @@
97 'desc': desc,
98 'start tag': '{{{tag}}}'.format(tag=tag),
99 'start html': start_html,
100- 'end tag': '{{{tag}}}'.format(tag=tag),
101+ 'end tag': '{{/{tag}}}'.format(tag=tag),
102 'end html': end_html,
103 'protected': False,
104 'temporary': False
105
106=== modified file 'openlp/core/widgets/edits.py'
107--- openlp/core/widgets/edits.py 2019-05-22 20:46:51 +0000
108+++ openlp/core/widgets/edits.py 2019-06-28 18:31:26 +0000
109@@ -353,7 +353,7 @@
110 :rtype: None
111 """
112 if self._path != path:
113- self._path = path
114+ self.path = path
115 self.pathChanged.emit(path)
116
117
118
119=== modified file 'openlp/plugins/songs/lib/importers/wordsofworship.py'
120--- openlp/plugins/songs/lib/importers/wordsofworship.py 2019-04-13 13:00:22 +0000
121+++ openlp/plugins/songs/lib/importers/wordsofworship.py 2019-06-28 18:31:26 +0000
122@@ -26,7 +26,8 @@
123 import logging
124 import os
125
126-from openlp.core.common.i18n import translate
127+from openlp.core.common.i18n import UiStrings, translate
128+from openlp.core.lib import DataType, read_int, read_or_fail, seek_or_fail
129 from openlp.plugins.songs.lib.importers.songimport import SongImport
130
131
132@@ -48,52 +49,138 @@
133 the author and the copyright.
134 * A block can be a verse, chorus or bridge.
135
136+ Little endian is used.
137+
138 File Header:
139- Bytes are counted from one, i.e. the first byte is byte 1. The first 19
140- bytes should be "WoW File \\nSong Words" The bytes after this and up to
141- the 56th byte, can change but no real meaning has been found. The
142- 56th byte specifies how many blocks there are. The first block starts
143- with byte 83 after the "CSongDoc::CBlock" declaration.
144+ Bytes are counted from one, i.e. the first byte is byte 1.
145+
146+ 0x00 - 0x13 Should be "WoW File \nSong Words\n"
147+ 0x14 - 0x1F Minimum version of Words Of Worship required to open this file
148+ 0x20 - 0x2B Minimum version of Words Of Worship required to save this file without data loss
149+ 0x2C - 0x37 The version of Words of Worship that this file is from. From test data, it looks like this might be
150+ the version that originally created this file, not the last version to save it.
151+
152+ The Words Of Worship versioning system seems to be in the format:
153+ ``Major.Minor.Patch``
154+
155+ Where each part of the version number is stored by a 32-bit int
156+
157+ 0x38 - 0x3B Specifies how many blocks there are.
158+
159+ 0x42 - 0x51 Should be "CSongDoc::CBlock"
160+
161+ 0x52 The first song blocks start from here.
162
163 Blocks:
164- Each block has a starting header, some lines of text, and an ending
165- footer. Each block starts with a 32 bit number, which specifies how
166- many lines are in that block.
167+ Each block starts with a 32-bit int which specifies how many lines are in that block.
168+
169+ Then there are a number of lines corresponding to the value above.
170
171 Each block ends with a 32 bit number, which defines what type of
172 block it is:
173
174- * ``NUL`` (0x00) - Verse
175- * ``SOH`` (0x01) - Chorus
176- * ``STX`` (0x02) - Bridge
177+ * 0x00000000 = Verse
178+ * 0x01000000 = Chorus
179+ * 0x02000000 = Bridge
180
181 Blocks are separated by two bytes. The first byte is 0x01, and the
182 second byte is 0x80.
183
184 Lines:
185- Each line starts with a byte which specifies how long that line is,
186- the line text, and ends with a null byte.
187+ Each line consists of a "Pascal" string.
188+ In later versions, a byte follows which denotes the formatting of the line:
189+
190+ * 0x00 = Normal
191+ * 0x01 = Minor
192+
193+ It looks like this may have been introduced in Words of Worship song version 1.2.2, though this is an educated
194+ guess.
195
196 Footer:
197- The footer follows on after the last block, the first byte specifies
198- the length of the author text, followed by the author text, if
199- this byte is null, then there is no author text. The byte after the
200- author text specifies the length of the copyright text, followed
201- by the copyright text.
202-
203- The file is ended with four null bytes.
204+ The footer follows on after the last block. Its format is as follows:
205+
206+ Author String (as a 'Pascal' string)
207+ Copyright String (as a 'Pascal' string)
208+
209+ Finally in newer versions of Word Of Worship song files there is a 32 bit int describing the copyright.
210+
211+ 0x00000000 = Covered by CCL
212+ 0x01000000 = Authors explicit permission
213+ 0x02000000 = Public Domain
214+ 0x03000000 = Copyright expired
215+ 0x04000000 = Other
216+
217+ Pascal Strings:
218+ Strings are preceded by a variable length integer which specifies how many bytes are in the string. An example
219+ of the variable length integer is below.
220+
221+ Lentgh bytes 'Little'| Str len
222+ -------------------------------
223+ 01 | 01
224+ 02 | 02
225+ .... |
226+ FD | FD
227+ FE | FE
228+ FF FF 00 | FF
229+ FF 00 01 | 01 00
230+ FF 01 01 | 01 01
231+ FF 02 01 | 01 02
232+ .... |
233+ FF FC FF | FF FC
234+ FF FD FF | FF FD
235+ FF FF FF FE FF | FF FE
236+ FF FF FF FF FF 00 00 | FF FF
237+ FF FF FF 00 00 01 00 | 01 00 00
238+ FF FF FF 01 00 01 00 | 01 00 01
239+ FF FF FF 02 00 02 00 | 01 00 02
240
241 Valid extensions for a Words of Worship song file are:
242
243 * .wsg
244 * .wow-song
245 """
246-
247- def __init__(self, manager, **kwargs):
248- """
249- Initialise the Words of Worship importer.
250- """
251- super(WordsOfWorshipImport, self).__init__(manager, **kwargs)
252+ @staticmethod
253+ def parse_string(song_data):
254+ length_bytes = song_data.read(DataType.U8)
255+ if length_bytes == b'\xff':
256+ length_bytes = song_data.read(DataType.U16)
257+ length = int.from_bytes(length_bytes, 'little')
258+ return read_or_fail(song_data, length).decode('cp1252')
259+
260+ def parse_lines(self, song_data):
261+ lines = []
262+ lines_to_read = read_int(song_data, DataType.U32, 'little')
263+ for line_no in range(0, lines_to_read):
264+ line_text = self.parse_string(song_data)
265+ if self.read_version >= (1, 2, 2):
266+ if read_or_fail(song_data, DataType.U8) == b'\x01':
267+ line_text = '{{minor}}{text}{{/minor}}'.format(text=line_text)
268+ lines.append(line_text)
269+ return '\n'.join(lines)
270+
271+ @staticmethod
272+ def parse_version(song_data):
273+ return (read_int(song_data, DataType.U32, 'little'),
274+ read_int(song_data, DataType.U32, 'little'),
275+ read_int(song_data, DataType.U32, 'little'))
276+
277+ def vaildate(self, file_path, song_data):
278+ seek_or_fail(song_data, 0x00)
279+ err_text = b''
280+ data = read_or_fail(song_data, 20)
281+ if data != b'WoW File\nSong Words\n':
282+ err_text = data
283+ seek_or_fail(song_data, 0x42)
284+ data = read_or_fail(song_data, 16)
285+ if data != b'CSongDoc::CBlock':
286+ err_text = data
287+ if err_text:
288+ self.log_error(file_path,
289+ translate('SongsPlugin.WordsofWorshipSongImport',
290+ 'Invalid Words of Worship song file. Missing {text!r} header.'
291+ ).format(text=err_text))
292+ return False
293+ return True
294
295 def do_import(self):
296 """
297@@ -104,57 +191,37 @@
298 for file_path in self.import_source:
299 if self.stop_import_flag:
300 return
301- self.set_defaults()
302- with file_path.open('rb') as song_data:
303- if song_data.read(19).decode() != 'WoW File\nSong Words':
304- self.log_error(file_path,
305- translate('SongsPlugin.WordsofWorshipSongImport',
306- 'Invalid Words of Worship song file. Missing "{text}" '
307- 'header.').format(text='WoW File\\nSong Words'))
308- continue
309- # Seek to byte which stores number of blocks in the song
310- song_data.seek(56)
311- no_of_blocks = ord(song_data.read(1))
312- song_data.seek(66)
313- if song_data.read(16).decode() != 'CSongDoc::CBlock':
314- self.log_error(file_path,
315- translate('SongsPlugin.WordsofWorshipSongImport',
316- 'Invalid Words of Worship song file. Missing "{text}" '
317- 'string.').format(text='CSongDoc::CBlock'))
318- continue
319- # Seek to the beginning of the first block
320- song_data.seek(82)
321- for block in range(no_of_blocks):
322- skip_char_at_end = True
323- self.lines_to_read = ord(song_data.read(4)[:1])
324- block_text = ''
325- while self.lines_to_read:
326- self.line_text = str(song_data.read(ord(song_data.read(1))), 'cp1252')
327- if skip_char_at_end:
328- skip_char = ord(song_data.read(1))
329- # Check if we really should skip a char. In some wsg files we shouldn't
330- if skip_char != 0:
331- song_data.seek(-1, os.SEEK_CUR)
332- skip_char_at_end = False
333- if block_text:
334- block_text += '\n'
335- block_text += self.line_text
336- self.lines_to_read -= 1
337- block_type = BLOCK_TYPES[ord(song_data.read(4)[:1])]
338- # Blocks are separated by 2 bytes, skip them, but not if
339- # this is the last block!
340- if block + 1 < no_of_blocks:
341- song_data.seek(2, os.SEEK_CUR)
342- self.add_verse(block_text, block_type)
343- # Now to extract the author
344- author_length = ord(song_data.read(1))
345- if author_length:
346- self.parse_author(str(song_data.read(author_length), 'cp1252'))
347- # Finally the copyright
348- copyright_length = ord(song_data.read(1))
349- if copyright_length:
350- self.add_copyright(str(song_data.read(copyright_length), 'cp1252'))
351+ log.debug('Importing %s', file_path)
352+ try:
353+ self.set_defaults()
354 # Get the song title
355 self.title = file_path.stem
356- if not self.finish():
357- self.log_error(file_path)
358+ with file_path.open('rb') as song_data:
359+ if not self.vaildate(file_path, song_data):
360+ continue
361+ seek_or_fail(song_data, 24)
362+ self.read_version = self.parse_version(song_data)
363+ # Seek to byte which stores number of blocks in the song
364+ seek_or_fail(song_data, 56)
365+ no_of_blocks = read_int(song_data, DataType.U8)
366+
367+ # Seek to the beginning of the first block
368+ seek_or_fail(song_data, 82)
369+ for block_no in range(no_of_blocks):
370+ # Blocks are separated by 2 bytes, skip them, but not if this is the last block!
371+ if block_no != 0:
372+ seek_or_fail(song_data, 2, os.SEEK_CUR)
373+ text = self.parse_lines(song_data)
374+ block_type = BLOCK_TYPES[read_int(song_data, DataType.U32, 'little')]
375+ self.add_verse(text, block_type)
376+
377+ # Now to extract the author
378+ self.parse_author(self.parse_string(song_data))
379+ # Finally the copyright
380+ self.add_copyright(self.parse_string(song_data))
381+ if not self.finish():
382+ self.log_error(file_path)
383+ except IndexError:
384+ self.log_error(file_path, UiStrings().FileCorrupt)
385+ except Exception as e:
386+ self.log_error(file_path, e)
387
388=== modified file 'openlp/plugins/songs/lib/openlyricsxml.py'
389--- openlp/plugins/songs/lib/openlyricsxml.py 2019-04-13 13:00:22 +0000
390+++ openlp/plugins/songs/lib/openlyricsxml.py 2019-06-28 18:31:26 +0000
391@@ -336,7 +336,7 @@
392 :return: the lyrics with the converted chords
393 """
394 # Process chords.
395- new_text = re.sub(r'\[(\w.*?)\]', r'<chord name="\1"/>', text)
396+ new_text = re.sub(r'\[(?!CDATA)(\w.*?)\]', r'<chord name="\1"/>', text)
397 return new_text
398
399 def _get_missing_tags(self, text):
400
401=== modified file 'tests/functional/openlp_core/lib/test_lib.py'
402--- tests/functional/openlp_core/lib/test_lib.py 2019-05-22 06:47:00 +0000
403+++ tests/functional/openlp_core/lib/test_lib.py 2019-06-28 18:31:26 +0000
404@@ -22,14 +22,16 @@
405 """
406 Package to test the openlp.core.lib package.
407 """
408+import io
409+import os
410 from pathlib import Path
411 from unittest import TestCase
412 from unittest.mock import MagicMock, patch
413
414 from PyQt5 import QtCore, QtGui
415
416-from openlp.core.lib import build_icon, check_item_selected, create_separated_list, create_thumb, \
417- get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb
418+from openlp.core.lib import DataType, build_icon, check_item_selected, create_separated_list, create_thumb, \
419+ get_text_file_string, image_to_byte, read_or_fail, read_int, resize_image, seek_or_fail, str_to_bool, validate_thumb
420 from tests.utils.constants import RESOURCE_PATH
421
422
423@@ -680,3 +682,179 @@
424 # THEN: We should have "Author 1, Author 2 and Author 3"
425 assert string_result == 'Author 1, Author 2 and Author 3', \
426 'The string should be "Author 1, Author 2, and Author 3".'
427+
428+ def test_read_or_fail_fail(self):
429+ """
430+ Test the :func:`read_or_fail` function when attempting to read more data than the buffer contains.
431+ """
432+ # GIVEN: Some test data
433+ test_data = io.BytesIO(b'test data')
434+
435+ # WHEN: Attempting to read past the end of the buffer
436+ # THEN: An OSError should be raised.
437+ with self.assertRaises(OSError):
438+ read_or_fail(test_data, 15)
439+
440+ def test_read_or_fail_success(self):
441+ """
442+ Test the :func:`read_or_fail` function when reading data that is in the buffer.
443+ """
444+ # GIVEN: Some test data
445+ test_data = io.BytesIO(b'test data')
446+
447+ # WHEN: Attempting to read data that should exist.
448+ result = read_or_fail(test_data, 4)
449+
450+ # THEN: The data of the requested length should be returned
451+ assert result == b'test'
452+
453+ def test_read_int_u8_big(self):
454+ """
455+ Test the :func:`read_int` function when reading an unsigned 8-bit int using 'big' endianness.
456+ """
457+ # GIVEN: Some test data
458+ test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
459+
460+ # WHEN: Reading a an unsigned 8-bit int
461+ result = read_int(test_data, DataType.U8, 'big')
462+
463+ # THEN: The an int should have been returned of the expected value
464+ assert result == 15
465+
466+ def test_read_int_u8_little(self):
467+ """
468+ Test the :func:`read_int` function when reading an unsigned 8-bit int using 'little' endianness.
469+ """
470+ # GIVEN: Some test data
471+ test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
472+
473+ # WHEN: Reading a an unsigned 8-bit int
474+ result = read_int(test_data, DataType.U8, 'little')
475+
476+ # THEN: The an int should have been returned of the expected value
477+ assert result == 15
478+
479+ def test_read_int_u16_big(self):
480+ """
481+ Test the :func:`read_int` function when reading an unsigned 16-bit int using 'big' endianness.
482+ """
483+ # GIVEN: Some test data
484+ test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
485+
486+ # WHEN: Reading a an unsigned 16-bit int
487+ result = read_int(test_data, DataType.U16, 'big')
488+
489+ # THEN: The an int should have been returned of the expected value
490+ assert result == 4080
491+
492+ def test_read_int_u16_little(self):
493+ """
494+ Test the :func:`read_int` function when reading an unsigned 16-bit int using 'little' endianness.
495+ """
496+ # GIVEN: Some test data
497+ test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
498+
499+ # WHEN: Reading a an unsigned 16-bit int
500+ result = read_int(test_data, DataType.U16, 'little')
501+
502+ # THEN: The an int should have been returned of the expected value
503+ assert result == 61455
504+
505+ def test_read_int_u32_big(self):
506+ """
507+ Test the :func:`read_int` function when reading an unsigned 32-bit int using 'big' endianness.
508+ """
509+ # GIVEN: Some test data
510+ test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
511+
512+ # WHEN: Reading a an unsigned 32-bit int
513+ result = read_int(test_data, DataType.U32, 'big')
514+
515+ # THEN: The an int should have been returned of the expected value
516+ assert result == 267390960
517+
518+ def test_read_int_u32_little(self):
519+ """
520+ Test the :func:`read_int` function when reading an unsigned 32-bit int using 'little' endianness.
521+ """
522+ # GIVEN: Some test data
523+ test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
524+
525+ # WHEN: Reading a an unsigned 32-bit int
526+ result = read_int(test_data, DataType.U32, 'little')
527+
528+ # THEN: The an int should have been returned of the expected value
529+ assert result == 4027576335
530+
531+ def test_seek_or_fail_default_method(self):
532+ """
533+ Test the :func:`seek_or_fail` function when using the default value for the :arg:`how`
534+ """
535+ # GIVEN: A mocked_file_like_object
536+ mocked_file_like_object = MagicMock(**{'seek.return_value': 5, 'tell.return_value': 0})
537+
538+ # WHEN: Calling seek_or_fail with out the how arg set
539+ seek_or_fail(mocked_file_like_object, 5)
540+
541+ # THEN: seek should be called using the os.SEEK_SET constant
542+ mocked_file_like_object.seek.assert_called_once_with(5, os.SEEK_SET)
543+
544+ def test_seek_or_fail_os_end(self):
545+ """
546+ Test the :func:`seek_or_fail` function when called with an unsupported seek operation.
547+ """
548+ # GIVEN: A Mocked object
549+ # WHEN: Attempting to seek relative to the end
550+ # THEN: An NotImplementedError should have been raised
551+ with self.assertRaises(NotImplementedError):
552+ seek_or_fail(MagicMock(), 1, os.SEEK_END)
553+
554+ def test_seek_or_fail_valid_seek_set(self):
555+ """
556+ Test that :func:`seek_or_fail` successfully seeks to the correct position.
557+ """
558+ # GIVEN: A mocked file-like object
559+ mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 5})
560+
561+ # WHEN: Attempting to seek from the beginning
562+ result = seek_or_fail(mocked_file_like_object, 5, os.SEEK_SET)
563+
564+ # THEN: The new position should be 5 from the beginning
565+ assert result == 5
566+
567+ def test_seek_or_fail_invalid_seek_set(self):
568+ """
569+ Test that :func:`seek_or_fail` raises an exception when seeking past the end.
570+ """
571+ # GIVEN: A Mocked file-like object
572+ mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 10})
573+
574+ # WHEN: Attempting to seek from the beginning past the end
575+ # THEN: An OSError should have been raised
576+ with self.assertRaises(OSError):
577+ seek_or_fail(mocked_file_like_object, 15, os.SEEK_SET)
578+
579+ def test_seek_or_fail_valid_seek_cur(self):
580+ """
581+ Test that :func:`seek_or_fail` successfully seeks to the correct position.
582+ """
583+ # GIVEN: A mocked file_like object
584+ mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 8})
585+
586+ # WHEN: Attempting to seek from the current position
587+ result = seek_or_fail(mocked_file_like_object, 5, os.SEEK_CUR)
588+
589+ # THEN: The new position should be 8 (5 from its starting position)
590+ assert result == 8
591+
592+ def test_seek_or_fail_invalid_seek_cur(self):
593+ """
594+ Test that :func:`seek_or_fail` raises an exception when seeking past the end.
595+ """
596+ # GIVEN: A mocked file_like object
597+ mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 10})
598+
599+ # WHEN: Attempting to seek from the current position pas the end.
600+ # THEN: An OSError should have been raised
601+ with self.assertRaises(OSError):
602+ seek_or_fail(mocked_file_like_object, 15, os.SEEK_CUR)
603
604=== modified file 'tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py'
605--- tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py 2019-04-13 13:00:22 +0000
606+++ tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py 2019-06-28 18:31:26 +0000
607@@ -34,15 +34,39 @@
608 def __init__(self, *args, **kwargs):
609 self.importer_class_name = 'WordsOfWorshipImport'
610 self.importer_module_name = 'wordsofworship'
611- super(TestWordsOfWorshipFileImport, self).__init__(*args, **kwargs)
612-
613- def test_song_import(self):
614- """
615- Test that loading a Words of Worship file works correctly
616- """
617- self.file_import([TEST_PATH / 'Amazing Grace (6 Verses).wow-song'],
618- self.load_external_result_data(TEST_PATH / 'Amazing Grace (6 Verses).json'))
619- self.file_import([TEST_PATH / 'When morning gilds the skies.wsg'],
620- self.load_external_result_data(TEST_PATH / 'When morning gilds the skies.json'))
621- self.file_import([TEST_PATH / 'Holy Holy Holy Lord God Almighty.wow-song'],
622- self.load_external_result_data(TEST_PATH / 'Holy Holy Holy Lord God Almighty.json'))
623+ super().__init__(*args, **kwargs)
624+
625+ def test_amazing_grace_song_import(self):
626+ """
627+ Test that loading a Words of Worship file works correctly
628+ """
629+ self.file_import([TEST_PATH / 'Amazing Grace (6 Verses)_v2_1_2.wow-song'],
630+ self.load_external_result_data(TEST_PATH / 'Amazing Grace (6 Verses)_v2_1_2.json'))
631+
632+ def test_when_morning_gilds_song_import(self):
633+ """
634+ Test that loading a Words of Worship file v2.0.0 works correctly
635+ """
636+ self.file_import([TEST_PATH / 'When morning gilds the skies_v2_0_0.wsg'],
637+ self.load_external_result_data(TEST_PATH / 'When morning gilds the skies_v2_0_0.json'))
638+
639+ def test_holy_holy_holy_song_import(self):
640+ """
641+ Test that loading a Words of Worship file works correctly
642+ """
643+ self.file_import([TEST_PATH / 'Holy Holy Holy Lord God Almighty_v2_1_2.wow-song'],
644+ self.load_external_result_data(TEST_PATH / 'Holy Holy Holy Lord God Almighty_v2_1_2.json'))
645+
646+ def test_test_song_v2_0_0_song_import(self):
647+ """
648+ Test that loading a Words of Worship file v2.0.0 works correctly
649+ """
650+ self.file_import([TEST_PATH / 'Test_Song_v2_0_0.wsg'],
651+ self.load_external_result_data(TEST_PATH / 'Test_Song_v2_0_0.json'))
652+
653+ def test_test_song_song_import(self):
654+ """
655+ Test that loading a Words of Worship file v2.1.2 works correctly
656+ """
657+ self.file_import([TEST_PATH / 'Test_Song_v2_1_2.wow-song'],
658+ self.load_external_result_data(TEST_PATH / 'Test_Song_v2_1_2.json'))
659
660=== renamed file 'tests/resources/songs/wordsofworship/Amazing Grace (6 Verses).json' => 'tests/resources/songs/wordsofworship/Amazing Grace (6 Verses)_v2_1_2.json'
661--- tests/resources/songs/wordsofworship/Amazing Grace (6 Verses).json 2014-11-03 14:36:27 +0000
662+++ tests/resources/songs/wordsofworship/Amazing Grace (6 Verses)_v2_1_2.json 2019-06-28 18:31:26 +0000
663@@ -2,7 +2,7 @@
664 "authors": [
665 "John Newton (1725-1807)"
666 ],
667- "title": "Amazing Grace (6 Verses)",
668+ "title": "Amazing Grace (6 Verses)_v2_1_2",
669 "verse_order_list": [],
670 "verses": [
671 [
672
673=== 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'
674=== 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'
675--- tests/resources/songs/wordsofworship/Holy Holy Holy Lord God Almighty.json 2015-08-26 08:50:38 +0000
676+++ tests/resources/songs/wordsofworship/Holy Holy Holy Lord God Almighty_v2_1_2.json 2019-06-28 18:31:26 +0000
677@@ -2,7 +2,7 @@
678 "authors": [
679 "Words: Reginald Heber (1783-1826). Music: John B. Dykes (1823-1876)"
680 ],
681- "title": "Holy Holy Holy Lord God Almighty",
682+ "title": "Holy Holy Holy Lord God Almighty_v2_1_2",
683 "verse_order_list": [],
684 "verses": [
685 [
686
687=== 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'
688=== added file 'tests/resources/songs/wordsofworship/Test_Song_v2_0_0.json'
689--- tests/resources/songs/wordsofworship/Test_Song_v2_0_0.json 1970-01-01 00:00:00 +0000
690+++ tests/resources/songs/wordsofworship/Test_Song_v2_0_0.json 2019-06-28 18:31:26 +0000
691@@ -0,0 +1,18 @@
692+{
693+ "authors": [
694+ "Author"
695+ ],
696+ "copyright": "Copyright",
697+ "title": "Test_Song_v2_0_0",
698+ "verse_order_list": [],
699+ "verses": [
700+ [
701+ "Verse 1 Line 1\nVerse 1 Line 2\nVerse 1 Line 3\nVerse 1 Line 4",
702+ "V"
703+ ],
704+ [
705+ "Chorus 1 Line 1\nChorus 1 Line 2\nChorus 1 Line 3\nChorus 1 Line 4\nChorus 1 Line 5",
706+ "C"
707+ ]
708+ ]
709+}
710
711=== added file 'tests/resources/songs/wordsofworship/Test_Song_v2_0_0.wsg'
712Binary 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 18:31:26 +0000 differ
713=== added file 'tests/resources/songs/wordsofworship/Test_Song_v2_1_2.json'
714--- tests/resources/songs/wordsofworship/Test_Song_v2_1_2.json 1970-01-01 00:00:00 +0000
715+++ tests/resources/songs/wordsofworship/Test_Song_v2_1_2.json 2019-06-28 18:31:26 +0000
716@@ -0,0 +1,26 @@
717+{
718+ "authors": [
719+ "Author"
720+ ],
721+ "copyright": "Copyright",
722+ "title": "Test_Song_v2_1_2",
723+ "verse_order_list": [],
724+ "verses": [
725+ [
726+ "Verse 1 Line 1\n{minor}Verse 1 Line 2 Minor{/minor}",
727+ "V"
728+ ],
729+ [
730+ "Chorus 1 Line 1\n{minor}Chorus 1 Line 2 Minor{/minor}",
731+ "C"
732+ ],
733+ [
734+ "Bridge 1 Line 1\n{minor}Bridge 1 Line 2{/minor}",
735+ "B"
736+ ],
737+ [
738+ "Verse 2 Line 1\n{minor}Verse 2 Line 2{/minor}",
739+ "V"
740+ ]
741+ ]
742+}
743
744=== added file 'tests/resources/songs/wordsofworship/Test_Song_v2_1_2.wow-song'
745Binary 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 18:31:26 +0000 differ
746=== 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'
747--- tests/resources/songs/wordsofworship/When morning gilds the skies.json 2014-11-05 13:04:43 +0000
748+++ tests/resources/songs/wordsofworship/When morning gilds the skies_v2_0_0.json 2019-06-28 18:31:26 +0000
749@@ -2,7 +2,7 @@
750 "authors": [
751 "Author Unknown. Tr. Edward Caswall"
752 ],
753- "title": "When morning gilds the skies",
754+ "title": "When morning gilds the skies_v2_0_0",
755 "verse_order_list": [],
756 "verses": [
757 [
758
759=== 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'