Merge lp:~phill-ridout/openlp/wow_import_fixes into lp:openlp
- wow_import_fixes
- Merge into trunk
Proposed by
Phill
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 2883 | ||||||||
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 | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tim Bentley | Approve | ||
Review via email: mp+369468@code.launchpad.net |
This proposal supersedes a proposal from 2019-06-28.
Commit message
Fix up and improve the Words Of Worship importer
Description of the change
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 | # |
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:/
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 passed!
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
macOS tests passed!
Revision history for this message
Tim Bentley (trb143) : Posted in a previous version of this proposal | # |
review:
Approve
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
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 19:29:36 +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 19:29:36 +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 19:29:36 +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 19:29:36 +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 19:29:36 +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 2.1.0, 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 >= (2, 1, 0): |
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, 20) |
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 'tests/functional/openlp_core/lib/test_lib.py' |
389 | --- tests/functional/openlp_core/lib/test_lib.py 2019-05-22 06:47:00 +0000 |
390 | +++ tests/functional/openlp_core/lib/test_lib.py 2019-06-28 19:29:36 +0000 |
391 | @@ -22,14 +22,16 @@ |
392 | """ |
393 | Package to test the openlp.core.lib package. |
394 | """ |
395 | +import io |
396 | +import os |
397 | from pathlib import Path |
398 | from unittest import TestCase |
399 | from unittest.mock import MagicMock, patch |
400 | |
401 | from PyQt5 import QtCore, QtGui |
402 | |
403 | -from openlp.core.lib import build_icon, check_item_selected, create_separated_list, create_thumb, \ |
404 | - get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb |
405 | +from openlp.core.lib import DataType, build_icon, check_item_selected, create_separated_list, create_thumb, \ |
406 | + get_text_file_string, image_to_byte, read_or_fail, read_int, resize_image, seek_or_fail, str_to_bool, validate_thumb |
407 | from tests.utils.constants import RESOURCE_PATH |
408 | |
409 | |
410 | @@ -680,3 +682,179 @@ |
411 | # THEN: We should have "Author 1, Author 2 and Author 3" |
412 | assert string_result == 'Author 1, Author 2 and Author 3', \ |
413 | 'The string should be "Author 1, Author 2, and Author 3".' |
414 | + |
415 | + def test_read_or_fail_fail(self): |
416 | + """ |
417 | + Test the :func:`read_or_fail` function when attempting to read more data than the buffer contains. |
418 | + """ |
419 | + # GIVEN: Some test data |
420 | + test_data = io.BytesIO(b'test data') |
421 | + |
422 | + # WHEN: Attempting to read past the end of the buffer |
423 | + # THEN: An OSError should be raised. |
424 | + with self.assertRaises(OSError): |
425 | + read_or_fail(test_data, 15) |
426 | + |
427 | + def test_read_or_fail_success(self): |
428 | + """ |
429 | + Test the :func:`read_or_fail` function when reading data that is in the buffer. |
430 | + """ |
431 | + # GIVEN: Some test data |
432 | + test_data = io.BytesIO(b'test data') |
433 | + |
434 | + # WHEN: Attempting to read data that should exist. |
435 | + result = read_or_fail(test_data, 4) |
436 | + |
437 | + # THEN: The data of the requested length should be returned |
438 | + assert result == b'test' |
439 | + |
440 | + def test_read_int_u8_big(self): |
441 | + """ |
442 | + Test the :func:`read_int` function when reading an unsigned 8-bit int using 'big' endianness. |
443 | + """ |
444 | + # GIVEN: Some test data |
445 | + test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0') |
446 | + |
447 | + # WHEN: Reading a an unsigned 8-bit int |
448 | + result = read_int(test_data, DataType.U8, 'big') |
449 | + |
450 | + # THEN: The an int should have been returned of the expected value |
451 | + assert result == 15 |
452 | + |
453 | + def test_read_int_u8_little(self): |
454 | + """ |
455 | + Test the :func:`read_int` function when reading an unsigned 8-bit int using 'little' 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, 'little') |
462 | + |
463 | + # THEN: The an int should have been returned of the expected value |
464 | + assert result == 15 |
465 | + |
466 | + def test_read_int_u16_big(self): |
467 | + """ |
468 | + Test the :func:`read_int` function when reading an unsigned 16-bit int using 'big' endianness. |
469 | + """ |
470 | + # GIVEN: Some test data |
471 | + test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0') |
472 | + |
473 | + # WHEN: Reading a an unsigned 16-bit int |
474 | + result = read_int(test_data, DataType.U16, 'big') |
475 | + |
476 | + # THEN: The an int should have been returned of the expected value |
477 | + assert result == 4080 |
478 | + |
479 | + def test_read_int_u16_little(self): |
480 | + """ |
481 | + Test the :func:`read_int` function when reading an unsigned 16-bit int using 'little' 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, 'little') |
488 | + |
489 | + # THEN: The an int should have been returned of the expected value |
490 | + assert result == 61455 |
491 | + |
492 | + def test_read_int_u32_big(self): |
493 | + """ |
494 | + Test the :func:`read_int` function when reading an unsigned 32-bit int using 'big' endianness. |
495 | + """ |
496 | + # GIVEN: Some test data |
497 | + test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0') |
498 | + |
499 | + # WHEN: Reading a an unsigned 32-bit int |
500 | + result = read_int(test_data, DataType.U32, 'big') |
501 | + |
502 | + # THEN: The an int should have been returned of the expected value |
503 | + assert result == 267390960 |
504 | + |
505 | + def test_read_int_u32_little(self): |
506 | + """ |
507 | + Test the :func:`read_int` function when reading an unsigned 32-bit int using 'little' 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, 'little') |
514 | + |
515 | + # THEN: The an int should have been returned of the expected value |
516 | + assert result == 4027576335 |
517 | + |
518 | + def test_seek_or_fail_default_method(self): |
519 | + """ |
520 | + Test the :func:`seek_or_fail` function when using the default value for the :arg:`how` |
521 | + """ |
522 | + # GIVEN: A mocked_file_like_object |
523 | + mocked_file_like_object = MagicMock(**{'seek.return_value': 5, 'tell.return_value': 0}) |
524 | + |
525 | + # WHEN: Calling seek_or_fail with out the how arg set |
526 | + seek_or_fail(mocked_file_like_object, 5) |
527 | + |
528 | + # THEN: seek should be called using the os.SEEK_SET constant |
529 | + mocked_file_like_object.seek.assert_called_once_with(5, os.SEEK_SET) |
530 | + |
531 | + def test_seek_or_fail_os_end(self): |
532 | + """ |
533 | + Test the :func:`seek_or_fail` function when called with an unsupported seek operation. |
534 | + """ |
535 | + # GIVEN: A Mocked object |
536 | + # WHEN: Attempting to seek relative to the end |
537 | + # THEN: An NotImplementedError should have been raised |
538 | + with self.assertRaises(NotImplementedError): |
539 | + seek_or_fail(MagicMock(), 1, os.SEEK_END) |
540 | + |
541 | + def test_seek_or_fail_valid_seek_set(self): |
542 | + """ |
543 | + Test that :func:`seek_or_fail` successfully seeks to the correct position. |
544 | + """ |
545 | + # GIVEN: A mocked file-like object |
546 | + mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 5}) |
547 | + |
548 | + # WHEN: Attempting to seek from the beginning |
549 | + result = seek_or_fail(mocked_file_like_object, 5, os.SEEK_SET) |
550 | + |
551 | + # THEN: The new position should be 5 from the beginning |
552 | + assert result == 5 |
553 | + |
554 | + def test_seek_or_fail_invalid_seek_set(self): |
555 | + """ |
556 | + Test that :func:`seek_or_fail` raises an exception when seeking past the end. |
557 | + """ |
558 | + # GIVEN: A Mocked file-like object |
559 | + mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 10}) |
560 | + |
561 | + # WHEN: Attempting to seek from the beginning past the end |
562 | + # THEN: An OSError should have been raised |
563 | + with self.assertRaises(OSError): |
564 | + seek_or_fail(mocked_file_like_object, 15, os.SEEK_SET) |
565 | + |
566 | + def test_seek_or_fail_valid_seek_cur(self): |
567 | + """ |
568 | + Test that :func:`seek_or_fail` successfully seeks to the correct position. |
569 | + """ |
570 | + # GIVEN: A mocked file_like object |
571 | + mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 8}) |
572 | + |
573 | + # WHEN: Attempting to seek from the current position |
574 | + result = seek_or_fail(mocked_file_like_object, 5, os.SEEK_CUR) |
575 | + |
576 | + # THEN: The new position should be 8 (5 from its starting position) |
577 | + assert result == 8 |
578 | + |
579 | + def test_seek_or_fail_invalid_seek_cur(self): |
580 | + """ |
581 | + Test that :func:`seek_or_fail` raises an exception when seeking past the end. |
582 | + """ |
583 | + # GIVEN: A mocked file_like object |
584 | + mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 10}) |
585 | + |
586 | + # WHEN: Attempting to seek from the current position pas the end. |
587 | + # THEN: An OSError should have been raised |
588 | + with self.assertRaises(OSError): |
589 | + seek_or_fail(mocked_file_like_object, 15, os.SEEK_CUR) |
590 | |
591 | === modified file 'tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py' |
592 | --- tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py 2019-04-13 13:00:22 +0000 |
593 | +++ tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py 2019-06-28 19:29:36 +0000 |
594 | @@ -34,15 +34,39 @@ |
595 | def __init__(self, *args, **kwargs): |
596 | self.importer_class_name = 'WordsOfWorshipImport' |
597 | self.importer_module_name = 'wordsofworship' |
598 | - super(TestWordsOfWorshipFileImport, self).__init__(*args, **kwargs) |
599 | - |
600 | - def test_song_import(self): |
601 | - """ |
602 | - Test that loading a Words of Worship file works correctly |
603 | - """ |
604 | - self.file_import([TEST_PATH / 'Amazing Grace (6 Verses).wow-song'], |
605 | - self.load_external_result_data(TEST_PATH / 'Amazing Grace (6 Verses).json')) |
606 | - self.file_import([TEST_PATH / 'When morning gilds the skies.wsg'], |
607 | - self.load_external_result_data(TEST_PATH / 'When morning gilds the skies.json')) |
608 | - self.file_import([TEST_PATH / 'Holy Holy Holy Lord God Almighty.wow-song'], |
609 | - self.load_external_result_data(TEST_PATH / 'Holy Holy Holy Lord God Almighty.json')) |
610 | + super().__init__(*args, **kwargs) |
611 | + |
612 | + def test_amazing_grace_song_import(self): |
613 | + """ |
614 | + Test that loading a Words of Worship file works correctly |
615 | + """ |
616 | + self.file_import([TEST_PATH / 'Amazing Grace (6 Verses)_v2_1_2.wow-song'], |
617 | + self.load_external_result_data(TEST_PATH / 'Amazing Grace (6 Verses)_v2_1_2.json')) |
618 | + |
619 | + def test_when_morning_gilds_song_import(self): |
620 | + """ |
621 | + Test that loading a Words of Worship file v2.0.0 works correctly |
622 | + """ |
623 | + self.file_import([TEST_PATH / 'When morning gilds the skies_v2_0_0.wsg'], |
624 | + self.load_external_result_data(TEST_PATH / 'When morning gilds the skies_v2_0_0.json')) |
625 | + |
626 | + def test_holy_holy_holy_song_import(self): |
627 | + """ |
628 | + Test that loading a Words of Worship file works correctly |
629 | + """ |
630 | + self.file_import([TEST_PATH / 'Holy Holy Holy Lord God Almighty_v2_1_2.wow-song'], |
631 | + self.load_external_result_data(TEST_PATH / 'Holy Holy Holy Lord God Almighty_v2_1_2.json')) |
632 | + |
633 | + def test_test_song_v2_0_0_song_import(self): |
634 | + """ |
635 | + Test that loading a Words of Worship file v2.0.0 works correctly |
636 | + """ |
637 | + self.file_import([TEST_PATH / 'Test_Song_v2_0_0.wsg'], |
638 | + self.load_external_result_data(TEST_PATH / 'Test_Song_v2_0_0.json')) |
639 | + |
640 | + def test_test_song_song_import(self): |
641 | + """ |
642 | + Test that loading a Words of Worship file v2.1.2 works correctly |
643 | + """ |
644 | + self.file_import([TEST_PATH / 'Test_Song_v2_1_2.wow-song'], |
645 | + self.load_external_result_data(TEST_PATH / 'Test_Song_v2_1_2.json')) |
646 | |
647 | === renamed file 'tests/resources/songs/wordsofworship/Amazing Grace (6 Verses).json' => 'tests/resources/songs/wordsofworship/Amazing Grace (6 Verses)_v2_1_2.json' |
648 | --- tests/resources/songs/wordsofworship/Amazing Grace (6 Verses).json 2014-11-03 14:36:27 +0000 |
649 | +++ tests/resources/songs/wordsofworship/Amazing Grace (6 Verses)_v2_1_2.json 2019-06-28 19:29:36 +0000 |
650 | @@ -2,7 +2,7 @@ |
651 | "authors": [ |
652 | "John Newton (1725-1807)" |
653 | ], |
654 | - "title": "Amazing Grace (6 Verses)", |
655 | + "title": "Amazing Grace (6 Verses)_v2_1_2", |
656 | "verse_order_list": [], |
657 | "verses": [ |
658 | [ |
659 | |
660 | === 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' |
661 | === 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' |
662 | --- tests/resources/songs/wordsofworship/Holy Holy Holy Lord God Almighty.json 2015-08-26 08:50:38 +0000 |
663 | +++ tests/resources/songs/wordsofworship/Holy Holy Holy Lord God Almighty_v2_1_2.json 2019-06-28 19:29:36 +0000 |
664 | @@ -2,7 +2,7 @@ |
665 | "authors": [ |
666 | "Words: Reginald Heber (1783-1826). Music: John B. Dykes (1823-1876)" |
667 | ], |
668 | - "title": "Holy Holy Holy Lord God Almighty", |
669 | + "title": "Holy Holy Holy Lord God Almighty_v2_1_2", |
670 | "verse_order_list": [], |
671 | "verses": [ |
672 | [ |
673 | |
674 | === 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' |
675 | === added file 'tests/resources/songs/wordsofworship/Test_Song_v2_0_0.json' |
676 | --- tests/resources/songs/wordsofworship/Test_Song_v2_0_0.json 1970-01-01 00:00:00 +0000 |
677 | +++ tests/resources/songs/wordsofworship/Test_Song_v2_0_0.json 2019-06-28 19:29:36 +0000 |
678 | @@ -0,0 +1,18 @@ |
679 | +{ |
680 | + "authors": [ |
681 | + "Author" |
682 | + ], |
683 | + "copyright": "Copyright", |
684 | + "title": "Test_Song_v2_0_0", |
685 | + "verse_order_list": [], |
686 | + "verses": [ |
687 | + [ |
688 | + "Verse 1 Line 1\nVerse 1 Line 2\nVerse 1 Line 3\nVerse 1 Line 4", |
689 | + "V" |
690 | + ], |
691 | + [ |
692 | + "Chorus 1 Line 1\nChorus 1 Line 2\nChorus 1 Line 3\nChorus 1 Line 4\nChorus 1 Line 5", |
693 | + "C" |
694 | + ] |
695 | + ] |
696 | +} |
697 | |
698 | === added file 'tests/resources/songs/wordsofworship/Test_Song_v2_0_0.wsg' |
699 | Binary 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:29:36 +0000 differ |
700 | === added file 'tests/resources/songs/wordsofworship/Test_Song_v2_1_2.json' |
701 | --- tests/resources/songs/wordsofworship/Test_Song_v2_1_2.json 1970-01-01 00:00:00 +0000 |
702 | +++ tests/resources/songs/wordsofworship/Test_Song_v2_1_2.json 2019-06-28 19:29:36 +0000 |
703 | @@ -0,0 +1,26 @@ |
704 | +{ |
705 | + "authors": [ |
706 | + "Author" |
707 | + ], |
708 | + "copyright": "Copyright", |
709 | + "title": "Test_Song_v2_1_2", |
710 | + "verse_order_list": [], |
711 | + "verses": [ |
712 | + [ |
713 | + "Verse 1 Line 1\n{minor}Verse 1 Line 2 Minor{/minor}", |
714 | + "V" |
715 | + ], |
716 | + [ |
717 | + "Chorus 1 Line 1\n{minor}Chorus 1 Line 2 Minor{/minor}", |
718 | + "C" |
719 | + ], |
720 | + [ |
721 | + "Bridge 1 Line 1\n{minor}Bridge 1 Line 2{/minor}", |
722 | + "B" |
723 | + ], |
724 | + [ |
725 | + "Verse 2 Line 1\n{minor}Verse 2 Line 2{/minor}", |
726 | + "V" |
727 | + ] |
728 | + ] |
729 | +} |
730 | |
731 | === added file 'tests/resources/songs/wordsofworship/Test_Song_v2_1_2.wow-song' |
732 | Binary 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:29:36 +0000 differ |
733 | === 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' |
734 | --- tests/resources/songs/wordsofworship/When morning gilds the skies.json 2014-11-05 13:04:43 +0000 |
735 | +++ tests/resources/songs/wordsofworship/When morning gilds the skies_v2_0_0.json 2019-06-28 19:29:36 +0000 |
736 | @@ -2,7 +2,7 @@ |
737 | "authors": [ |
738 | "Author Unknown. Tr. Edward Caswall" |
739 | ], |
740 | - "title": "When morning gilds the skies", |
741 | + "title": "When morning gilds the skies_v2_0_0", |
742 | "verse_order_list": [], |
743 | "verses": [ |
744 | [ |
745 | |
746 | === 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' |
Linux tests failed, please see https:/ /ci.openlp. io/job/ MP-02-Linux_ Tests/192/ for more details