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