1# -*- coding: utf-8 -*- 2 3import os 4import subprocess 5 6from mutagen import MutagenError 7from mutagen.id3 import ID3, TIT2, ID3NoHeaderError 8from mutagen.flac import to_int_be, Padding, VCFLACDict, MetadataBlock, error 9from mutagen.flac import StreamInfo, SeekTable, CueSheet, FLAC, delete, Picture 10from mutagen._compat import PY3 11 12from tests import TestCase, DATA_DIR, get_temp_copy 13from tests.test__vorbis import TVCommentDict, VComment 14 15 16def call_flac(*args): 17 with open(os.devnull, 'wb') as null: 18 return subprocess.call( 19 ["flac"] + list(args), stdout=null, stderr=subprocess.STDOUT) 20 21 22class Tto_int_be(TestCase): 23 24 def test_empty(self): 25 self.failUnlessEqual(to_int_be(b""), 0) 26 27 def test_0(self): 28 self.failUnlessEqual(to_int_be(b"\x00"), 0) 29 30 def test_1(self): 31 self.failUnlessEqual(to_int_be(b"\x01"), 1) 32 33 def test_256(self): 34 self.failUnlessEqual(to_int_be(b"\x01\x00"), 256) 35 36 def test_long(self): 37 self.failUnlessEqual(to_int_be(b"\x01\x00\x00\x00\x00"), 2 ** 32) 38 39 40class TVCFLACDict(TVCommentDict): 41 42 Kind = VCFLACDict 43 44 def test_roundtrip_vc(self): 45 self.failUnlessEqual(self.c, VComment(self.c.write() + b"\x01")) 46 47 48class TMetadataBlock(TestCase): 49 50 def test_empty(self): 51 self.failUnlessEqual(MetadataBlock(b"").write(), b"") 52 53 def test_not_empty(self): 54 self.failUnlessEqual(MetadataBlock(b"foobar").write(), b"foobar") 55 56 def test_change(self): 57 b = MetadataBlock(b"foobar") 58 b.data = b"quux" 59 self.failUnlessEqual(b.write(), b"quux") 60 61 def test_write_read_max_size(self): 62 63 class SomeBlock(MetadataBlock): 64 code = 255 65 66 max_data_size = 2 ** 24 - 1 67 block = SomeBlock(b"\x00" * max_data_size) 68 data = MetadataBlock._writeblock(block) 69 self.assertEqual(data[:4], b"\xff\xff\xff\xff") 70 header_size = 4 71 self.assertEqual(len(data), max_data_size + header_size) 72 73 block = SomeBlock(b"\x00" * (max_data_size + 1)) 74 self.assertRaises(error, MetadataBlock._writeblock, block) 75 76 def test_ctr_garbage(self): 77 self.failUnlessRaises(TypeError, StreamInfo, 12) 78 79 def test_too_large(self): 80 block = Picture() 81 block.data = b"\x00" * 0x1FFFFFF 82 self.assertRaises( 83 error, MetadataBlock._writeblocks, [block], 0, 0, None) 84 85 def test_too_large_padding(self): 86 block = Padding() 87 self.assertEqual( 88 len(MetadataBlock._writeblocks([block], 0, 0, lambda x: 2 ** 24)), 89 2**24 - 1 + 4) 90 91 92class TStreamInfo(TestCase): 93 94 data = (b'\x12\x00\x12\x00\x00\x00\x0e\x005\xea\n\xc4H\xf0\x00\xca0' 95 b'\x14(\x90\xf9\xe1)2\x13\x01\xd4\xa7\xa9\x11!8\xab\x91') 96 data_invalid = len(data) * b'\x00' 97 98 def setUp(self): 99 self.i = StreamInfo(self.data) 100 101 def test_bitrate(self): 102 assert self.i.bitrate == 0 103 104 def test_invalid(self): 105 # https://github.com/quodlibet/mutagen/issues/117 106 self.failUnlessRaises(error, StreamInfo, self.data_invalid) 107 108 def test_blocksize(self): 109 self.failUnlessEqual(self.i.max_blocksize, 4608) 110 self.failUnlessEqual(self.i.min_blocksize, 4608) 111 self.failUnless(self.i.min_blocksize <= self.i.max_blocksize) 112 113 def test_framesize(self): 114 self.failUnlessEqual(self.i.min_framesize, 14) 115 self.failUnlessEqual(self.i.max_framesize, 13802) 116 self.failUnless(self.i.min_framesize <= self.i.max_framesize) 117 118 def test_sample_rate(self): 119 self.failUnlessEqual(self.i.sample_rate, 44100) 120 121 def test_channels(self): 122 self.failUnlessEqual(self.i.channels, 5) 123 124 def test_bps(self): 125 self.failUnlessEqual(self.i.bits_per_sample, 16) 126 127 def test_length(self): 128 self.failUnlessAlmostEqual(self.i.length, 300.5, 1) 129 130 def test_total_samples(self): 131 self.failUnlessEqual(self.i.total_samples, 13250580) 132 133 def test_md5_signature(self): 134 self.failUnlessEqual(self.i.md5_signature, 135 int("2890f9e129321301d4a7a9112138ab91", 16)) 136 137 def test_eq(self): 138 self.failUnlessEqual(self.i, self.i) 139 140 def test_roundtrip(self): 141 self.failUnlessEqual(StreamInfo(self.i.write()), self.i) 142 143 144class TSeekTable(TestCase): 145 SAMPLE = os.path.join(DATA_DIR, "silence-44-s.flac") 146 147 def setUp(self): 148 self.flac = FLAC(self.SAMPLE) 149 self.st = self.flac.seektable 150 151 def test_seektable(self): 152 self.failUnlessEqual(self.st.seekpoints, 153 [(0, 0, 4608), 154 (41472, 11852, 4608), 155 (50688, 14484, 4608), 156 (87552, 25022, 4608), 157 (105984, 30284, 4608), 158 (0xFFFFFFFFFFFFFFFF, 0, 0)]) 159 160 def test_eq(self): 161 self.failUnlessEqual(self.st, self.st) 162 163 def test_neq(self): 164 self.failIfEqual(self.st, 12) 165 166 def test_repr(self): 167 repr(self.st) 168 169 def test_roundtrip(self): 170 self.failUnlessEqual(SeekTable(self.st.write()), self.st) 171 172 173class TCueSheet(TestCase): 174 SAMPLE = os.path.join(DATA_DIR, "silence-44-s.flac") 175 176 def setUp(self): 177 self.flac = FLAC(self.SAMPLE) 178 self.cs = self.flac.cuesheet 179 180 def test_cuesheet(self): 181 self.failUnlessEqual(self.cs.media_catalog_number, b"1234567890123") 182 self.failUnlessEqual(self.cs.lead_in_samples, 88200) 183 self.failUnlessEqual(self.cs.compact_disc, True) 184 self.failUnlessEqual(len(self.cs.tracks), 4) 185 186 def test_first_track(self): 187 self.failUnlessEqual(self.cs.tracks[0].track_number, 1) 188 self.failUnlessEqual(self.cs.tracks[0].start_offset, 0) 189 self.failUnlessEqual(self.cs.tracks[0].isrc, b'123456789012') 190 self.failUnlessEqual(self.cs.tracks[0].type, 0) 191 self.failUnlessEqual(self.cs.tracks[0].pre_emphasis, False) 192 self.failUnlessEqual(self.cs.tracks[0].indexes, [(1, 0)]) 193 194 def test_second_track(self): 195 self.failUnlessEqual(self.cs.tracks[1].track_number, 2) 196 self.failUnlessEqual(self.cs.tracks[1].start_offset, 44100) 197 self.failUnlessEqual(self.cs.tracks[1].isrc, b'') 198 self.failUnlessEqual(self.cs.tracks[1].type, 1) 199 self.failUnlessEqual(self.cs.tracks[1].pre_emphasis, True) 200 self.failUnlessEqual(self.cs.tracks[1].indexes, [(1, 0), 201 (2, 588)]) 202 203 def test_lead_out(self): 204 self.failUnlessEqual(self.cs.tracks[-1].track_number, 170) 205 self.failUnlessEqual(self.cs.tracks[-1].start_offset, 162496) 206 self.failUnlessEqual(self.cs.tracks[-1].isrc, b'') 207 self.failUnlessEqual(self.cs.tracks[-1].type, 0) 208 self.failUnlessEqual(self.cs.tracks[-1].pre_emphasis, False) 209 self.failUnlessEqual(self.cs.tracks[-1].indexes, []) 210 211 def test_track_eq(self): 212 track = self.cs.tracks[-1] 213 self.assertReallyEqual(track, track) 214 self.assertReallyNotEqual(track, 42) 215 216 def test_eq(self): 217 self.assertReallyEqual(self.cs, self.cs) 218 219 def test_neq(self): 220 self.assertReallyNotEqual(self.cs, 12) 221 222 def test_repr(self): 223 repr(self.cs) 224 225 def test_roundtrip(self): 226 self.failUnlessEqual(CueSheet(self.cs.write()), self.cs) 227 228 229class TPicture(TestCase): 230 SAMPLE = os.path.join(DATA_DIR, "silence-44-s.flac") 231 232 def setUp(self): 233 self.flac = FLAC(self.SAMPLE) 234 self.p = self.flac.pictures[0] 235 236 def test_count(self): 237 self.failUnlessEqual(len(self.flac.pictures), 1) 238 239 def test_picture(self): 240 self.failUnlessEqual(self.p.width, 1) 241 self.failUnlessEqual(self.p.height, 1) 242 self.failUnlessEqual(self.p.depth, 24) 243 self.failUnlessEqual(self.p.colors, 0) 244 self.failUnlessEqual(self.p.mime, u'image/png') 245 self.failUnlessEqual(self.p.desc, u'A pixel.') 246 self.failUnlessEqual(self.p.type, 3) 247 self.failUnlessEqual(len(self.p.data), 150) 248 249 def test_eq(self): 250 self.failUnlessEqual(self.p, self.p) 251 252 def test_neq(self): 253 self.failIfEqual(self.p, 12) 254 255 def test_repr(self): 256 repr(self.p) 257 258 def test_roundtrip(self): 259 self.failUnlessEqual(Picture(self.p.write()), self.p) 260 261 262class TPadding(TestCase): 263 264 def setUp(self): 265 self.b = Padding(b"\x00" * 100) 266 267 def test_padding(self): 268 self.failUnlessEqual(self.b.write(), b"\x00" * 100) 269 270 def test_blank(self): 271 self.failIf(Padding().write()) 272 273 def test_empty(self): 274 self.failIf(Padding(b"").write()) 275 276 def test_repr(self): 277 repr(Padding()) 278 279 def test_change(self): 280 self.b.length = 20 281 self.failUnlessEqual(self.b.write(), b"\x00" * 20) 282 283 284class TFLAC(TestCase): 285 SAMPLE = os.path.join(DATA_DIR, "silence-44-s.flac") 286 287 def setUp(self): 288 self.NEW = get_temp_copy(self.SAMPLE) 289 self.flac = FLAC(self.NEW) 290 291 def tearDown(self): 292 os.unlink(self.NEW) 293 294 def test_zero_samples(self): 295 # write back zero sample count and load again 296 self.flac.info.total_samples = 0 297 self.flac.save() 298 new = FLAC(self.flac.filename) 299 assert new.info.total_samples == 0 300 assert new.info.bitrate == 0 301 assert new.info.length == 0.0 302 303 def test_bitrate(self): 304 assert self.flac.info.bitrate == 101430 305 old_file_size = os.path.getsize(self.flac.filename) 306 self.flac.save(padding=lambda x: 9999) 307 new_flac = FLAC(self.flac.filename) 308 assert os.path.getsize(new_flac.filename) > old_file_size 309 assert new_flac.info.bitrate == 101430 310 311 def test_padding(self): 312 for pad in [0, 42, 2**24 - 1, 2 ** 24]: 313 self.flac.save(padding=lambda x: pad) 314 new = FLAC(self.flac.filename) 315 expected = min(2**24 - 1, pad) 316 self.assertEqual(new.metadata_blocks[-1].length, expected) 317 318 def test_save_multiple_padding(self): 319 # we don't touch existing padding blocks on save, but will 320 # replace them in the file with one at the end 321 322 def num_padding(f): 323 blocks = f.metadata_blocks 324 return len([b for b in blocks if isinstance(b, Padding)]) 325 326 num_blocks = num_padding(self.flac) 327 self.assertEqual(num_blocks, 1) 328 block = Padding() 329 block.length = 42 330 self.flac.metadata_blocks.append(block) 331 block = Padding() 332 block.length = 24 333 self.flac.metadata_blocks.append(block) 334 self.flac.save() 335 self.assertEqual(num_padding(self.flac), num_blocks + 2) 336 337 new = FLAC(self.flac.filename) 338 self.assertEqual(num_padding(new), 1) 339 self.assertTrue(isinstance(new.metadata_blocks[-1], Padding)) 340 341 def test_increase_size_new_padding(self): 342 self.assertEqual(self.flac.metadata_blocks[-1].length, 3060) 343 value = u"foo" * 100 344 self.flac[u"foo"] = [value] 345 self.flac.save() 346 new = FLAC(self.NEW) 347 self.assertEqual(new.metadata_blocks[-1].length, 2752) 348 self.assertEqual(new[u"foo"], [value]) 349 350 def test_delete(self): 351 self.failUnless(self.flac.tags) 352 self.flac.delete() 353 self.assertTrue(self.flac.tags is not None) 354 self.assertFalse(self.flac.tags) 355 flac = FLAC(self.NEW) 356 self.assertTrue(flac.tags is None) 357 358 def test_module_delete(self): 359 delete(self.NEW) 360 flac = FLAC(self.NEW) 361 self.failIf(flac.tags) 362 363 def test_info(self): 364 self.failUnlessAlmostEqual(FLAC(self.NEW).info.length, 3.7, 1) 365 366 def test_keys(self): 367 self.failUnlessEqual( 368 list(self.flac.keys()), list(self.flac.tags.keys())) 369 370 def test_values(self): 371 self.failUnlessEqual( 372 list(self.flac.values()), list(self.flac.tags.values())) 373 374 def test_items(self): 375 self.failUnlessEqual( 376 list(self.flac.items()), list(self.flac.tags.items())) 377 378 def test_vc(self): 379 self.failUnlessEqual(self.flac['title'][0], 'Silence') 380 381 def test_write_nochange(self): 382 f = FLAC(self.NEW) 383 f.save() 384 with open(self.SAMPLE, "rb") as a: 385 with open(self.NEW, "rb") as b: 386 self.failUnlessEqual(a.read(), b.read()) 387 388 def test_write_changetitle(self): 389 f = FLAC(self.NEW) 390 if PY3: 391 self.assertRaises( 392 TypeError, f.__setitem__, b'title', b"A New Title") 393 else: 394 f[b"title"] = b"A New Title" 395 f.save() 396 f = FLAC(self.NEW) 397 self.failUnlessEqual(f[b"title"][0], b"A New Title") 398 399 def test_write_changetitle_unicode_value(self): 400 f = FLAC(self.NEW) 401 if PY3: 402 self.assertRaises( 403 TypeError, f.__setitem__, b'title', u"A Unicode Title \u2022") 404 else: 405 f[b"title"] = u"A Unicode Title \u2022" 406 f.save() 407 f = FLAC(self.NEW) 408 self.failUnlessEqual(f[b"title"][0], u"A Unicode Title \u2022") 409 410 def test_write_changetitle_unicode_key(self): 411 f = FLAC(self.NEW) 412 f[u"title"] = b"A New Title" 413 if PY3: 414 self.assertRaises(ValueError, f.save) 415 else: 416 f.save() 417 f = FLAC(self.NEW) 418 self.failUnlessEqual(f[u"title"][0], b"A New Title") 419 420 def test_write_changetitle_unicode_key_and_value(self): 421 f = FLAC(self.NEW) 422 f[u"title"] = u"A Unicode Title \u2022" 423 f.save() 424 f = FLAC(self.NEW) 425 self.failUnlessEqual(f[u"title"][0], u"A Unicode Title \u2022") 426 427 def test_force_grow(self): 428 f = FLAC(self.NEW) 429 f["faketag"] = ["a" * 1000] * 1000 430 f.save() 431 f = FLAC(self.NEW) 432 self.failUnlessEqual(f["faketag"], ["a" * 1000] * 1000) 433 434 def test_force_shrink(self): 435 self.test_force_grow() 436 f = FLAC(self.NEW) 437 f["faketag"] = "foo" 438 f.save() 439 f = FLAC(self.NEW) 440 self.failUnlessEqual(f["faketag"], ["foo"]) 441 442 def test_add_vc(self): 443 f = FLAC(os.path.join(DATA_DIR, "no-tags.flac")) 444 self.failIf(f.tags) 445 f.add_tags() 446 self.failUnless(f.tags == []) 447 self.failUnlessRaises(ValueError, f.add_tags) 448 449 def test_add_vc_implicit(self): 450 f = FLAC(os.path.join(DATA_DIR, "no-tags.flac")) 451 self.failIf(f.tags) 452 f["foo"] = "bar" 453 self.failUnless(f.tags == [("foo", "bar")]) 454 self.failUnlessRaises(ValueError, f.add_tags) 455 456 def test_ooming_vc_header(self): 457 # issue 112: Malformed FLAC Vorbis header causes out of memory error 458 # https://github.com/quodlibet/mutagen/issues/112 459 self.assertRaises(error, FLAC, os.path.join(DATA_DIR, 460 'ooming-header.flac')) 461 462 def test_with_real_flac(self): 463 if not have_flac: 464 return 465 self.flac["faketag"] = "foobar" * 1000 466 self.flac.save() 467 self.failIf(call_flac("-t", self.flac.filename) != 0) 468 469 def test_save_unknown_block(self): 470 block = MetadataBlock(b"test block data") 471 block.code = 99 472 self.flac.metadata_blocks.append(block) 473 self.flac.save() 474 475 def test_load_unknown_block(self): 476 self.test_save_unknown_block() 477 flac = FLAC(self.NEW) 478 self.failUnlessEqual(len(flac.metadata_blocks), 7) 479 self.failUnlessEqual(flac.metadata_blocks[5].code, 99) 480 self.failUnlessEqual(flac.metadata_blocks[5].data, b"test block data") 481 482 def test_two_vorbis_blocks(self): 483 self.flac.metadata_blocks.append(self.flac.metadata_blocks[1]) 484 self.flac.save() 485 self.failUnlessRaises(error, FLAC, self.NEW) 486 487 def test_missing_streaminfo(self): 488 self.flac.metadata_blocks.pop(0) 489 self.flac.save() 490 self.failUnlessRaises(error, FLAC, self.NEW) 491 492 def test_load_invalid_flac(self): 493 self.failUnlessRaises( 494 error, FLAC, os.path.join(DATA_DIR, "xing.mp3")) 495 496 def test_save_invalid_flac(self): 497 self.failUnlessRaises( 498 error, self.flac.save, os.path.join(DATA_DIR, "xing.mp3")) 499 500 def test_pprint(self): 501 self.failUnless(self.flac.pprint()) 502 503 def test_double_load(self): 504 blocks = list(self.flac.metadata_blocks) 505 self.flac.load(self.flac.filename) 506 self.failUnlessEqual(blocks, self.flac.metadata_blocks) 507 508 def test_seektable(self): 509 self.failUnless(self.flac.seektable) 510 511 def test_cuesheet(self): 512 self.failUnless(self.flac.cuesheet) 513 514 def test_pictures(self): 515 self.failUnless(self.flac.pictures) 516 517 def test_add_picture(self): 518 f = FLAC(self.NEW) 519 c = len(f.pictures) 520 f.add_picture(Picture()) 521 f.save() 522 f = FLAC(self.NEW) 523 self.failUnlessEqual(len(f.pictures), c + 1) 524 525 def test_clear_pictures(self): 526 f = FLAC(self.NEW) 527 c1 = len(f.pictures) 528 c2 = len(f.metadata_blocks) 529 f.clear_pictures() 530 f.save() 531 f = FLAC(self.NEW) 532 self.failUnlessEqual(len(f.metadata_blocks), c2 - c1) 533 534 def test_ignore_id3(self): 535 id3 = ID3() 536 id3.add(TIT2(encoding=0, text='id3 title')) 537 id3.save(self.NEW) 538 f = FLAC(self.NEW) 539 f['title'] = 'vc title' 540 f.save() 541 id3 = ID3(self.NEW) 542 self.failUnlessEqual(id3['TIT2'].text, ['id3 title']) 543 f = FLAC(self.NEW) 544 self.failUnlessEqual(f['title'], ['vc title']) 545 546 def test_delete_id3(self): 547 id3 = ID3() 548 id3.add(TIT2(encoding=0, text='id3 title')) 549 id3.save(self.NEW, v1=2) 550 f = FLAC(self.NEW) 551 f['title'] = 'vc title' 552 f.save(deleteid3=True) 553 self.failUnlessRaises(ID3NoHeaderError, ID3, self.NEW) 554 f = FLAC(self.NEW) 555 self.failUnlessEqual(f['title'], ['vc title']) 556 557 def test_save_on_mp3(self): 558 path = os.path.join(DATA_DIR, "silence-44-s.mp3") 559 self.assertRaises(error, self.flac.save, path) 560 561 def test_mime(self): 562 self.failUnless("audio/x-flac" in self.flac.mime) 563 564 def test_variable_block_size(self): 565 FLAC(os.path.join(DATA_DIR, "variable-block.flac")) 566 567 def test_load_flac_with_application_block(self): 568 FLAC(os.path.join(DATA_DIR, "flac_application.flac")) 569 570 571class TFLACFile(TestCase): 572 573 def test_open_nonexistant(self): 574 """mutagen 1.2 raises UnboundLocalError, then it tries to open 575 non-existent FLAC files""" 576 filename = os.path.join(DATA_DIR, "doesntexist.flac") 577 self.assertRaises(MutagenError, FLAC, filename) 578 579 580class TFLACBadBlockSize(TestCase): 581 TOO_SHORT = os.path.join(DATA_DIR, "52-too-short-block-size.flac") 582 TOO_SHORT_2 = os.path.join(DATA_DIR, "106-short-picture-block-size.flac") 583 OVERWRITTEN = os.path.join(DATA_DIR, "52-overwritten-metadata.flac") 584 INVAL_INFO = os.path.join(DATA_DIR, "106-invalid-streaminfo.flac") 585 586 def test_too_short_read(self): 587 flac = FLAC(self.TOO_SHORT) 588 self.failUnlessEqual(flac["artist"], ["Tunng"]) 589 590 def test_too_short_read_picture(self): 591 flac = FLAC(self.TOO_SHORT_2) 592 self.failUnlessEqual(flac.pictures[0].width, 10) 593 594 def test_overwritten_read(self): 595 flac = FLAC(self.OVERWRITTEN) 596 self.failUnlessEqual(flac["artist"], ["Giora Feidman"]) 597 598 def test_inval_streaminfo(self): 599 self.assertRaises(error, FLAC, self.INVAL_INFO) 600 601 602class TFLACBadBlockSizeWrite(TestCase): 603 TOO_SHORT = os.path.join(DATA_DIR, "52-too-short-block-size.flac") 604 605 def setUp(self): 606 self.NEW = get_temp_copy(self.TOO_SHORT) 607 608 def tearDown(self): 609 os.unlink(self.NEW) 610 611 def test_write_reread(self): 612 flac = FLAC(self.NEW) 613 del(flac["artist"]) 614 flac.save() 615 flac2 = FLAC(self.NEW) 616 self.failUnlessEqual(flac["title"], flac2["title"]) 617 with open(self.NEW, "rb") as h: 618 data = h.read(1024) 619 self.failIf(b"Tunng" in data) 620 621 622class TFLACBadBlockSizeOverflow(TestCase): 623 624 def setUp(self): 625 self.filename = get_temp_copy( 626 os.path.join(DATA_DIR, "silence-44-s.flac")) 627 628 def tearDown(self): 629 os.unlink(self.filename) 630 631 def test_largest_valid(self): 632 f = FLAC(self.filename) 633 pic = Picture() 634 pic.data = b"\x00" * (2 ** 24 - 1 - 32) 635 self.assertEqual(len(pic.write()), 2 ** 24 - 1) 636 f.add_picture(pic) 637 f.save() 638 639 def test_smallest_invalid(self): 640 f = FLAC(self.filename) 641 pic = Picture() 642 pic.data = b"\x00" * (2 ** 24 - 32) 643 f.add_picture(pic) 644 self.assertRaises(error, f.save) 645 646 def test_invalid_overflow_recover_and_save_back(self): 647 # save a picture which is too large for flac, but still write it 648 # with a wrong block size 649 f = FLAC(self.filename) 650 f.clear_pictures() 651 pic = Picture() 652 pic.data = b"\x00" * (2 ** 24 - 32) 653 pic._invalid_overflow_size = 42 654 f.add_picture(pic) 655 f.save() 656 657 # make sure we can read it and save it again 658 f = FLAC(self.filename) 659 self.assertTrue(f.pictures) 660 self.assertEqual(len(f.pictures[0].data), 2 ** 24 - 32) 661 f.save() 662 663 664have_flac = True 665try: 666 call_flac() 667except OSError: 668 have_flac = False 669 print("WARNING: Skipping FLAC reference tests.") 670