1# This program is free software; you can redistribute it and/or modify 2# it under the terms of the GNU General Public License as published by 3# the Free Software Foundation; either version 2 of the License, or 4# (at your option) any later version. 5 6import shutil 7import os 8from collections import defaultdict 9 10from senf import fsnative 11 12from quodlibet import config 13 14from tests import TestCase, mkdtemp 15from quodlibet.formats import AudioFile as Fakesong 16from quodlibet.formats._audio import NUMERIC_ZERO_DEFAULT, PEOPLE 17from quodlibet.util.collection import Album, Playlist, avg, bayesian_average, \ 18 FileBackedPlaylist 19from quodlibet.library.libraries import FileLibrary 20from quodlibet.util import format_rating 21 22config.RATINGS = config.HardCodedRatingsPrefs() 23 24NUMERIC_SONGS = [ 25 Fakesong({"~filename": fsnative(u"fake1-\xf0.mp3"), 26 "~#length": 4, "~#added": 5, "~#lastplayed": 1, 27 "~#bitrate": 200, "date": "100", "~#rating": 0.1, 28 "originaldate": "2004-01-01", "~#filesize": 101}), 29 Fakesong({"~filename": fsnative(u"fake2.mp3"), 30 "~#length": 7, "~#added": 7, "~#lastplayed": 88, 31 "~#bitrate": 220, "date": "99", "~#rating": 0.3, 32 "originaldate": "2002-01-01", "~#filesize": 202}), 33 Fakesong({"~filename": fsnative(u"fake3.mp3"), 34 "~#length": 1, "~#added": 3, "~#lastplayed": 43, 35 "~#bitrate": 60, "date": "33", "~#rating": 0.5, 36 "tracknumber": "4/6", "discnumber": "1/2"}) 37] 38AMAZING_SONG = Fakesong({"~#length": 123, "~#rating": 1.0}) 39 40 41class TAlbum(TestCase): 42 def setUp(self): 43 config.init() 44 45 def test_people_sort(s): 46 songs = [ 47 Fakesong({"albumartist": "aa", "artist": "b\na"}), 48 Fakesong({"albumartist": "aa", "artist": "a\na"}) 49 ] 50 51 album = Album(songs[0]) 52 album.songs = set(songs) 53 54 s.failUnlessEqual(album.comma("~people"), "aa, a, b") 55 56 def test_peoplesort_sort(s): 57 songs = [ 58 Fakesong({"albumartistsort": "aa", "artist": "b\na"}), 59 Fakesong({"albumartist": "aa", "artistsort": "a\na"}) 60 ] 61 62 album = Album(songs[0]) 63 album.songs = set(songs) 64 65 s.failUnlessEqual(album.comma("~peoplesort"), "aa, a, b") 66 67 def test_tied_tags(s): 68 songs = [ 69 Fakesong({"artist": "a", "title": "c"}), 70 Fakesong({"artist": "a", "dummy": "d\ne"}) 71 ] 72 73 album = Album(songs[0]) 74 album.songs = set(songs) 75 76 s.failUnlessEqual(album.comma("~artist~dummy"), "a - d, e") 77 78 def test_tied_num_tags(s): 79 songs = [ 80 Fakesong({"~#length": 5, "title": "c", "~#rating": 0.4}), 81 Fakesong({"~#length": 7, "dummy": "d\ne", "~#rating": 0.6}), 82 Fakesong({"~#length": 0, "dummy2": 5, "~#rating": 0.5}) 83 ] 84 85 album = Album(songs[0]) 86 album.songs = set(songs) 87 88 s.failUnlessEqual(album.comma("~foo~~s~~~"), "") 89 s.failUnlessEqual(album.comma("~#length~dummy"), "12 - d, e") 90 s.failUnlessEqual(album.comma("~#rating~dummy"), "0.50 - d, e") 91 s.failUnlessEqual(album.comma("~#length:sum~dummy"), "12 - d, e") 92 s.failUnlessEqual(album.comma("~#dummy2"), 5) 93 s.failUnlessEqual(album.comma("~#dummy3"), "") 94 95 def test_internal_tags(s): 96 songs = [ 97 Fakesong({"~#length": 5, "discnumber": "1", "date": "2038"}), 98 Fakesong({"~#length": 7, "dummy": "d\ne", "discnumber": "2"}) 99 ] 100 101 album = Album(songs[0]) 102 album.songs = set(songs) 103 104 s.failIfEqual(album.comma("~long-length"), "") 105 s.failIfEqual(album.comma("~tracks"), "") 106 s.failIfEqual(album.comma("~discs"), "") 107 s.failUnlessEqual(album.comma("~foo"), "") 108 109 s.failUnlessEqual(album.comma(""), "") 110 s.failUnlessEqual(album.comma("~"), "") 111 s.failUnlessEqual(album.get("~#"), "") 112 113 def test_numeric_ops(s): 114 songs = NUMERIC_SONGS 115 album = Album(songs[0]) 116 album.songs = set(songs) 117 118 s.failUnlessEqual(album.get("~#length"), 12) 119 s.failUnlessEqual(album.get("~#length:sum"), 12) 120 s.failUnlessEqual(album.get("~#length:max"), 7) 121 s.failUnlessEqual(album.get("~#length:min"), 1) 122 s.failUnlessEqual(album.get("~#length:avg"), 4) 123 s.failUnlessEqual(album.get("~#length:foo"), 0) 124 125 s.failUnlessEqual(album.get("~#added"), 7) 126 s.failUnlessEqual(album.get("~#lastplayed"), 88) 127 s.failUnlessEqual(album.get("~#bitrate"), 200) 128 s.failUnlessEqual(album.get("~#year"), 33) 129 s.failUnlessEqual(album.get("~#rating"), 0.3) 130 s.failUnlessEqual(album.get("~#originalyear"), 2002) 131 132 def test_numeric_comma(self): 133 songs = [Fakesong({ 134 "~#added": 1, 135 "~#rating": 0.5, 136 "~#bitrate": 42, 137 "~#length": 1, 138 })] 139 140 album = Album(songs[0]) 141 album.songs = set(songs) 142 143 self.assertEqual(album.comma("~#added"), 1) 144 self.assertEqual(album.comma("~#rating"), 0.5) 145 self.assertEqual(album.comma("~#bitrate"), 42) 146 147 def test_numeric_funcs_text(self): 148 songs = NUMERIC_SONGS 149 album = Album(songs[0]) 150 album.songs = set(songs) 151 152 self.assertEqual(album("~length:sum"), "0:12") 153 self.assertEqual(album("~length:min"), "0:01") 154 self.assertEqual(album("~long-length:min"), "1 second") 155 self.assertEqual(album("~tracks:min"), "6 tracks") 156 self.assertEqual(album("~discs:min"), "2 discs") 157 self.assertEqual(album("~rating:min"), format_rating(0.1)) 158 self.assertEqual(album("~filesize:min"), "0 B") 159 160 def test_single_rating(s): 161 songs = [Fakesong({"~#rating": 0.75})] 162 album = Album(songs[0]) 163 album.songs = set(songs) 164 # One song should average to its own rating 165 s.failUnlessEqual(album.get("~#rating:avg"), songs[0]("~#rating")) 166 # BAV should now be default for rating 167 s.failUnlessEqual(album.get("~#rating:bav"), album.get("~#rating:avg")) 168 169 def test_multiple_ratings(s): 170 r1, r2 = 1.0, 0.5 171 songs = [Fakesong({"~#rating": r1}), Fakesong({"~#rating": r2})] 172 album = Album(songs[0]) 173 album.songs = set(songs) 174 # Standard averaging still available 175 s.failUnlessEqual(album("~#rating:avg"), avg([r1, r2])) 176 177 # C = 0.0 => emulate arithmetic mean 178 config.set("settings", "bayesian_rating_factor", 0.0) 179 s.failUnlessEqual(album("~#rating:bav"), album("~#rating:avg")) 180 181 def test_bayesian_multiple_ratings(s): 182 # separated from above to avoid caching 183 c, r1, r2 = 5, 1.0, 0.5 184 songs = [Fakesong({"~#rating": r1}), Fakesong({"~#rating": r2})] 185 album = Album(songs[0]) 186 album.songs = set(songs) 187 188 config.set("settings", "bayesian_rating_factor", float(c)) 189 s.failUnlessEqual( 190 config.getfloat("settings", "bayesian_rating_factor"), float(c)) 191 expected = avg(c * [config.RATINGS.default] + [r1, r2]) 192 s.failUnlessEqual(album("~#rating:bav"), expected) 193 s.failUnlessEqual(album("~#rating"), expected) 194 195 def test_bayesian_average(s): 196 bav = bayesian_average 197 l = [1, 2, 3, 4] 198 a = avg(l) 199 # c=0 => this becomes a mean regardless of m 200 s.failUnlessEqual(bav(l, 0, 0), a) 201 s.failUnlessEqual(bav(l, 0, 999), a) 202 # c=1, m = a (i.e. just adding another mean score) => no effect 203 s.failUnlessEqual(bav(l, 1, a), a) 204 # Harder ones 205 s.failUnlessEqual(bav(l, 5, 2), 20.0 / 9) 206 expected = 40.0 / 14 207 s.failUnlessEqual(bav(l, 10, 3), expected) 208 # Also check another iterable 209 s.failUnlessEqual(bav(tuple(l), 10, 3), expected) 210 211 def test_defaults(s): 212 failUnlessEq = s.failUnlessEqual 213 song = Fakesong({}) 214 album = Album(song) 215 216 failUnlessEq(album("foo", "x"), "x") 217 218 album.songs.add(song) 219 220 failUnlessEq(album("~#length", "x"), song("~#length", "x")) 221 failUnlessEq(album("~#bitrate", "x"), song("~#bitrate", "x")) 222 failUnlessEq(album("~#rating", "x"), song("~#rating", "x")) 223 failUnlessEq(album("~#playcount", "x"), song("~#playcount", "x")) 224 failUnlessEq(album("~#mtime", "x"), song("~#mtime", "x")) 225 failUnlessEq(album("~#year", "x"), song("~#year", "x")) 226 227 failUnlessEq(album("~#foo", "x"), song("~#foo", "x")) 228 failUnlessEq(album("foo", "x"), song("foo", "x")) 229 failUnlessEq(album("~foo", "x"), song("~foo", "x")) 230 231 failUnlessEq(album("~people", "x"), song("~people", "x")) 232 failUnlessEq(album("~peoplesort", "x"), song("~peoplesort", "x")) 233 failUnlessEq(album("~performer", "x"), song("~performer", "x")) 234 failUnlessEq(album("~performersort", "x"), song("~performersort", "x")) 235 236 failUnlessEq(album("~rating", "x"), song("~rating", "x")) 237 238 for p in PEOPLE: 239 failUnlessEq(album(p, "x"), song(p, "x")) 240 241 for p in NUMERIC_ZERO_DEFAULT: 242 failUnlessEq(album(p, "x"), song(p, "x")) 243 244 def test_methods(s): 245 songs = [ 246 Fakesong({"b": "bb4\nbb1\nbb1", 247 "c": "cc1\ncc3\ncc3", 248 "#d": 0.1}), 249 Fakesong({"b": "bb1\nbb1\nbb4", 250 "c": "cc3\ncc1\ncc3", 251 "#d": 0.2}) 252 ] 253 254 album = Album(songs[0]) 255 album.songs = set(songs) 256 257 s.failUnlessEqual(album.list("c"), ["cc3", "cc1"]) 258 s.failUnlessEqual(album.list("~c~b"), ["cc3", "cc1", "bb1", "bb4"]) 259 s.failUnlessEqual(album.list("#d"), ["0.1", "0.2"]) 260 261 s.failUnlessEqual(album.comma("c"), "cc3, cc1") 262 s.failUnlessEqual(album.comma("~c~b"), "cc3, cc1 - bb1, bb4") 263 264 def tearDown(self): 265 config.quit() 266 267 268class MockPlaylistResource(object): 269 def __init__(self, pl): 270 self.pl = pl 271 272 def __enter__(self): 273 return self.pl 274 275 def __exit__(self, *exc_info): 276 self.pl.delete() 277 278 279class TPlaylist(TestCase): 280 TWO_SONGS = [ 281 Fakesong({"~#length": 5, "discnumber": "1", "date": "2038"}), 282 Fakesong({"~#length": 7, "dummy": "d\ne", "discnumber": "2"}) 283 ] 284 285 class FakeLib(object): 286 287 def __init__(self): 288 self.reset() 289 290 def emit(self, name, songs): 291 self.emitted[name].extend(songs) 292 293 def masked(self, songs): 294 return False 295 296 def reset(self): 297 self.emitted = defaultdict(list) 298 299 @property 300 def changed(self): 301 return self.emitted.get('changed', []) 302 303 FAKE_LIB = FakeLib() 304 305 def setUp(self): 306 self.FAKE_LIB.reset() 307 308 def pl(self, name, lib=None): 309 return Playlist(name, lib) 310 311 def wrap(self, name, lib=FAKE_LIB): 312 return MockPlaylistResource(self.pl(name, lib)) 313 314 def test_equality(s): 315 pl = s.pl("playlist") 316 pl2 = s.pl("playlist") 317 pl3 = s.pl("playlist") 318 s.failUnlessEqual(pl, pl2) 319 # Debatable 320 s.failUnlessEqual(pl, pl3) 321 pl4 = s.pl("foobar") 322 s.failIfEqual(pl, pl4) 323 pl.delete() 324 pl2.delete() 325 pl3.delete() 326 pl4.delete() 327 328 def test_index(s): 329 with s.wrap("playlist") as pl: 330 songs = s.TWO_SONGS 331 pl.extend(songs) 332 # Just a sanity check... 333 s.failUnlessEqual(songs.index(songs[1]), 1) 334 # And now the happy paths.. 335 s.failUnlessEqual(pl.index(songs[0]), 0) 336 s.failUnlessEqual(pl.index(songs[1]), 1) 337 # ValueError is what we want here 338 try: 339 pl.index(Fakesong({})) 340 s.fail() 341 except ValueError: 342 pass 343 344 def test_name_tag(s): 345 with s.wrap("a playlist") as pl: 346 s.failUnlessEqual(pl("~name"), "a playlist") 347 s.failUnlessEqual(pl.get("~name"), "a playlist") 348 349 def test_internal_tags(s): 350 with s.wrap("playlist") as pl: 351 pl.extend(s.TWO_SONGS) 352 353 s.failIfEqual(pl.comma("~long-length"), "") 354 s.failIfEqual(pl.comma("~tracks"), "") 355 s.failIfEqual(pl.comma("~discs"), "") 356 s.failUnlessEqual(pl.comma("~foo"), "") 357 358 s.failUnlessEqual(pl.comma(""), "") 359 s.failUnlessEqual(pl.comma("~"), "") 360 s.failUnlessEqual(pl.get("~#"), "") 361 362 def test_numeric_ops(s): 363 songs = NUMERIC_SONGS 364 with s.wrap("playlist") as pl: 365 pl.extend(songs) 366 367 s.failUnlessEqual(pl.get("~#length"), 12) 368 s.failUnlessEqual(pl.get("~#length:sum"), 12) 369 s.failUnlessEqual(pl.get("~#length:max"), 7) 370 s.failUnlessEqual(pl.get("~#length:min"), 1) 371 s.failUnlessEqual(pl.get("~#length:avg"), 4) 372 s.failUnlessEqual(pl.get("~#length:foo"), 0) 373 374 s.failUnlessEqual(pl.get("~#rating:avg"), avg([0.1, 0.3, 0.5])) 375 376 s.failUnlessEqual(pl.get("~#filesize"), 303) 377 378 s.failUnlessEqual(pl.get("~#added"), 7) 379 s.failUnlessEqual(pl.get("~#lastplayed"), 88) 380 s.failUnlessEqual(pl.get("~#bitrate"), 200) 381 s.failUnlessEqual(pl.get("~#year"), 33) 382 s.failUnlessEqual(pl.get("~#rating"), 0.3) 383 s.failUnlessEqual(pl.get("~#originalyear"), 2002) 384 385 def test_updating_aggregates_extend(s): 386 with s.wrap("playlist") as pl: 387 pl.extend(NUMERIC_SONGS) 388 old_length = pl.get("~#length") 389 old_size = pl.get("~#filesize") 390 391 # Double the playlist 392 pl.extend(NUMERIC_SONGS) 393 394 new_length = pl.get("~#length") 395 new_size = pl.get("~#filesize") 396 s.failUnless(new_length > old_length, 397 msg="Ooops, %d <= %d" % (new_length, old_length)) 398 399 s.failUnless(new_size > old_size, 400 msg="Ooops, %d <= %d" % (new_size, old_size)) 401 402 def test_updating_aggregates_append(s): 403 with s.wrap("playlist") as pl: 404 pl.extend(NUMERIC_SONGS) 405 old_rating = pl.get("~#rating") 406 407 pl.append(AMAZING_SONG) 408 409 new_rating = pl.get("~#filesize") 410 s.failUnless(new_rating > old_rating) 411 412 def test_updating_aggregates_clear(s): 413 with s.wrap("playlist") as pl: 414 pl.extend(NUMERIC_SONGS) 415 s.failUnless(pl.get("~#length")) 416 417 pl.clear() 418 s.failIf(pl.get("~#length")) 419 420 def test_updating_aggregates_remove_songs(s): 421 with s.wrap("playlist") as pl: 422 pl.extend(NUMERIC_SONGS) 423 s.failUnless(pl.get("~#length")) 424 425 pl.remove_songs(NUMERIC_SONGS) 426 s.failIf(pl.get("~#length")) 427 428 def test_listlike(s): 429 with s.wrap("playlist") as pl: 430 pl.extend(NUMERIC_SONGS) 431 s.failUnlessEqual(NUMERIC_SONGS[0], pl[0]) 432 s.failUnlessEqual(NUMERIC_SONGS[1:2], pl[1:2]) 433 s.failUnless(NUMERIC_SONGS[1] in pl) 434 435 def test_extend_signals(s): 436 with s.wrap("playlist") as pl: 437 pl.extend(NUMERIC_SONGS) 438 s.failUnlessEqual(s.FAKE_LIB.changed, NUMERIC_SONGS) 439 440 def test_append_signals(s): 441 with s.wrap("playlist") as pl: 442 song = NUMERIC_SONGS[0] 443 pl.append(song) 444 s.failUnlessEqual(s.FAKE_LIB.changed, [song]) 445 446 def test_clear_signals(s): 447 with s.wrap("playlist") as pl: 448 pl.extend(NUMERIC_SONGS) 449 pl.clear() 450 s.failUnlessEqual(s.FAKE_LIB.changed, NUMERIC_SONGS * 2) 451 452 def test_make(self): 453 with self.wrap("Does not exist") as pl: 454 self.failIf(len(pl)) 455 self.failUnlessEqual(pl.name, "Does not exist") 456 457 def test_rename_working(self): 458 with self.wrap("Foobar") as pl: 459 assert pl.name == "Foobar" 460 pl.rename("Foo Quuxly") 461 assert pl.name == "Foo Quuxly" 462 # Rename should not fire signals 463 self.failIf(self.FAKE_LIB.changed) 464 465 def test_rename_nothing(self): 466 with self.wrap("Foobar") as pl: 467 self.failUnlessRaises(ValueError, pl.rename, "") 468 469 def test_no_op_rename(self): 470 with self.wrap("playlist") as pl: 471 pl.rename("playlist") 472 self.failUnlessEqual(pl.name, "playlist") 473 474 def test_playlists_featuring(s): 475 with s.wrap("playlist") as pl: 476 pl.extend(NUMERIC_SONGS) 477 playlists = Playlist.playlists_featuring(NUMERIC_SONGS[0]) 478 s.failUnlessEqual(set(playlists), {pl}) 479 # Now add a second one, check that instance tracking works 480 with s.wrap("playlist2") as pl2: 481 pl2.append(NUMERIC_SONGS[0]) 482 playlists = Playlist.playlists_featuring(NUMERIC_SONGS[0]) 483 s.failUnlessEqual(set(playlists), {pl, pl2}) 484 485 def test_playlists_tag(self): 486 # Arguably belongs in _audio 487 songs = NUMERIC_SONGS 488 pl_name = "playlist 123!" 489 with self.wrap(pl_name) as pl: 490 pl.extend(songs) 491 for song in songs: 492 self.assertEquals(pl_name, song("~playlists")) 493 494 def test_duplicates_single_item(self): 495 with self.wrap("playlist") as pl: 496 pl.append(self.TWO_SONGS[0]) 497 self.failIf(pl.has_duplicates) 498 pl.append(self.TWO_SONGS[0]) 499 self.failUnless(pl.has_duplicates) 500 501 def test_duplicates(self): 502 with self.wrap("playlist") as pl: 503 pl.extend(self.TWO_SONGS) 504 pl.extend(self.TWO_SONGS) 505 self.failUnlessEqual(len(pl), 4) 506 self.failUnless(pl.has_duplicates, 507 ("Playlist has un-detected duplicates: %s " 508 % "\n".join([str(s) for s in pl._list]))) 509 510 def test_remove_leaving_duplicates(self): 511 with self.wrap("playlist") as pl: 512 pl.extend(self.TWO_SONGS) 513 [first, second] = self.TWO_SONGS 514 pl.extend(NUMERIC_SONGS + self.TWO_SONGS) 515 self.failUnlessEqual(len(self.FAKE_LIB.changed), 7) 516 self.FAKE_LIB.reset() 517 pl.remove_songs(self.TWO_SONGS, leave_dupes=True) 518 self.failUnless(first in pl) 519 self.failUnless(second in pl) 520 self.failIf(len(self.FAKE_LIB.changed)) 521 522 def test_remove_fully(self): 523 with self.wrap("playlist") as pl: 524 pl.extend(self.TWO_SONGS * 2) 525 self.FAKE_LIB.reset() 526 pl.remove_songs(self.TWO_SONGS, leave_dupes=False) 527 self.failIf(len(pl)) 528 self.failUnlessEqual(self.FAKE_LIB.changed, self.TWO_SONGS) 529 530 531class TFileBackedPlaylist(TPlaylist): 532 533 def setUp(self): 534 super(TFileBackedPlaylist, self).setUp() 535 self.temp = mkdtemp() 536 self.temp2 = mkdtemp() 537 538 def tearDown(self): 539 shutil.rmtree(self.temp) 540 shutil.rmtree(self.temp2) 541 542 def pl(self, name, lib=None): 543 return FileBackedPlaylist(self.temp, name, lib) 544 545 def test_from_songs(self): 546 pl = FileBackedPlaylist.from_songs(self.temp, NUMERIC_SONGS) 547 self.failUnlessEqual(pl.songs, NUMERIC_SONGS) 548 pl.delete() 549 550 def test_read(self): 551 with self.wrap("playlist") as pl: 552 pl.extend(NUMERIC_SONGS) 553 pl.write() 554 555 lib = FileLibrary("foobar") 556 lib.add(NUMERIC_SONGS) 557 pl = self.pl("playlist", lib) 558 self.assertEqual(len(pl), len(NUMERIC_SONGS)) 559 560 def test_write(self): 561 with self.wrap("playlist") as pl: 562 pl.extend(NUMERIC_SONGS) 563 pl.extend([fsnative(u"xf0xf0")]) 564 pl.write() 565 566 with open(pl.filename, "rb") as h: 567 self.assertEqual(len(h.read().splitlines()), 568 len(NUMERIC_SONGS) + 1) 569 570 def test_make_dup(self): 571 p1 = FileBackedPlaylist.new(self.temp, "Does not exist") 572 p2 = FileBackedPlaylist.new(self.temp, "Does not exist") 573 self.failUnlessEqual(p1.name, "Does not exist") 574 self.failUnless(p2.name.startswith("Does not exist")) 575 self.failIfEqual(p1.name, p2.name) 576 p1.delete() 577 p2.delete() 578 579 def test_rename_removes(self): 580 with self.wrap("foo") as pl: 581 pl.rename("bar") 582 self.failUnless(os.path.exists(os.path.join(self.temp, 'bar'))) 583 self.failIf(os.path.exists(os.path.join(self.temp, 'foo'))) 584 585 def test_rename_fails_if_file_exists(self): 586 with self.wrap("foo") as foo: 587 with self.wrap("bar") as bar: 588 try: 589 foo.rename("bar") 590 self.fail("Should have raised, %s exists" % bar.filename) 591 except ValueError: 592 pass 593 594 def test_masked_handling(self): 595 if os.name == "nt": 596 # FIXME: masking isn't properly implemented on Windows 597 return 598 # playlists can contain songs and paths for masked handling.. 599 lib = FileLibrary("foobar") 600 with self.wrap("playlist", lib) as pl: 601 song = Fakesong({"date": "2038", "~filename": fsnative(u"/fake")}) 602 song.sanitize() 603 lib.add([song]) 604 605 # mask and update 606 lib.mask("/") 607 pl.append(song) 608 pl.remove_songs([song]) 609 self.failUnless("/fake" in pl) 610 611 pl.extend(self.TWO_SONGS) 612 613 # check if collections can handle the mix 614 self.failUnlessEqual(pl("date"), "2038") 615 616 # unmask and update 617 lib.unmask("/") 618 pl.add_songs(["/fake"], lib) 619 self.failUnless(song in pl) 620 621 lib.destroy() 622