1# -*- coding: utf-8 -*-
2# FIXME: This test suite is a mess, a lot of it dates from PyMusepack so
3# it doesn't match the other Mutagen test conventions/quality.
4
5import os
6import shutil
7
8from tests import TestCase, DATA_DIR, get_temp_copy
9
10import mutagen.apev2
11from mutagen import MutagenError
12from mutagen._compat import PY3, text_type, izip, xrange
13from mutagen.apev2 import APEv2File, APEv2, is_valid_apev2_key, \
14    APEBadItemError, error as APEv2Error
15
16
17SAMPLE = os.path.join(DATA_DIR, "click.mpc")
18OLD = os.path.join(DATA_DIR, "oldtag.apev2")
19BROKEN = os.path.join(DATA_DIR, "brokentag.apev2")
20LYRICS2 = os.path.join(DATA_DIR, "apev2-lyricsv2.mp3")
21INVAL_ITEM_COUNT = os.path.join(DATA_DIR, "145-invalid-item-count.apev2")
22
23
24class Tis_valid_apev2_key(TestCase):
25
26    def test_yes(self):
27        for key in ["foo", "Foo", "   f ~~~"]:
28            self.failUnless(is_valid_apev2_key(key))
29
30    def test_no(self):
31        for key in ["\x11hi", "ffoo\xFF", u"\u1234", "a", "", "foo" * 100]:
32            self.failIf(is_valid_apev2_key(key))
33
34    if PY3:
35        def test_py3(self):
36            self.assertRaises(TypeError, is_valid_apev2_key, b"abc")
37
38
39class TAPEInvalidItemCount(TestCase):
40    # https://github.com/quodlibet/mutagen/issues/145
41
42    def test_load(self):
43        x = mutagen.apev2.APEv2(INVAL_ITEM_COUNT)
44        self.failUnlessEqual(len(x.keys()), 17)
45
46
47class TAPEWriter(TestCase):
48    offset = 0
49
50    def setUp(self):
51        shutil.copy(SAMPLE, SAMPLE + ".new")
52        shutil.copy(BROKEN, BROKEN + ".new")
53        tag = mutagen.apev2.APEv2()
54        self.values = {"artist": "Joe Wreschnig\0unittest",
55                       "album": "Mutagen tests",
56                       "title": "Not really a song"}
57        for k, v in self.values.items():
58            tag[k] = v
59        tag.save(SAMPLE + ".new")
60        tag.save(SAMPLE + ".justtag")
61        tag.save(SAMPLE + ".tag_at_start")
62        fileobj = open(SAMPLE + ".tag_at_start", "ab")
63        fileobj.write(b"tag garbage" * 1000)
64        fileobj.close()
65        self.tag = mutagen.apev2.APEv2(SAMPLE + ".new")
66
67    def test_changed(self):
68        size = os.path.getsize(SAMPLE + ".new")
69        self.tag.save()
70        self.failUnlessEqual(
71            os.path.getsize(SAMPLE + ".new"), size - self.offset)
72
73    def test_fix_broken(self):
74        # Clean up garbage from a bug in pre-Mutagen APEv2.
75        # This also tests removing ID3v1 tags on writes.
76        self.failIfEqual(os.path.getsize(OLD), os.path.getsize(BROKEN))
77        tag = mutagen.apev2.APEv2(BROKEN)
78        tag.save(BROKEN + ".new")
79        self.failUnlessEqual(
80            os.path.getsize(OLD), os.path.getsize(BROKEN + ".new"))
81
82    def test_readback(self):
83        for k, v in self.tag.items():
84            self.failUnlessEqual(str(v), self.values[k])
85
86    def test_size(self):
87        self.failUnlessEqual(
88            os.path.getsize(SAMPLE + ".new"),
89            os.path.getsize(SAMPLE) + os.path.getsize(SAMPLE + ".justtag"))
90
91    def test_delete(self):
92        mutagen.apev2.delete(SAMPLE + ".justtag")
93        tag = mutagen.apev2.APEv2(SAMPLE + ".new")
94        tag.delete()
95        self.failUnlessEqual(os.path.getsize(SAMPLE + ".justtag"), self.offset)
96        self.failUnlessEqual(os.path.getsize(SAMPLE) + self.offset,
97                             os.path.getsize(SAMPLE + ".new"))
98        self.failIf(tag)
99
100    def test_empty(self):
101        self.failUnlessRaises(
102            APEv2Error, mutagen.apev2.APEv2,
103            os.path.join(DATA_DIR, "emptyfile.mp3"))
104
105    def test_tag_at_start(self):
106        filename = SAMPLE + ".tag_at_start"
107        tag = mutagen.apev2.APEv2(filename)
108        self.failUnlessEqual(tag["album"], "Mutagen tests")
109
110    def test_tag_at_start_write(self):
111        filename = SAMPLE + ".tag_at_start"
112        tag = mutagen.apev2.APEv2(filename)
113        tag.save()
114        tag = mutagen.apev2.APEv2(filename)
115        self.failUnlessEqual(tag["album"], "Mutagen tests")
116        self.failUnlessEqual(
117            os.path.getsize(SAMPLE + ".justtag"),
118            os.path.getsize(filename) - (len("tag garbage") * 1000))
119
120    def test_tag_at_start_delete(self):
121        filename = SAMPLE + ".tag_at_start"
122        tag = mutagen.apev2.APEv2(filename)
123        tag.delete()
124        self.failUnlessRaises(APEv2Error, mutagen.apev2.APEv2, filename)
125        self.failUnlessEqual(
126            os.path.getsize(filename), len("tag garbage") * 1000)
127
128    def test_case_preservation(self):
129        mutagen.apev2.delete(SAMPLE + ".justtag")
130        tag = mutagen.apev2.APEv2(SAMPLE + ".new")
131        tag["FoObaR"] = "Quux"
132        tag.save()
133        tag = mutagen.apev2.APEv2(SAMPLE + ".new")
134        self.failUnless("FoObaR" in tag.keys())
135        self.failIf("foobar" in tag.keys())
136
137    def test_unicode_key(self):
138        # https://github.com/quodlibet/mutagen/issues/123
139        tag = mutagen.apev2.APEv2(SAMPLE + ".new")
140        tag["abc"] = u'\xf6\xe4\xfc'
141        tag[u"cba"] = "abc"
142        tag.save()
143
144    def test_save_sort_is_deterministic(self):
145        tag = mutagen.apev2.APEv2(SAMPLE + ".new")
146        tag["cba"] = "my cba value"
147        tag["abc"] = "my abc value"
148        tag.save()
149        with open(SAMPLE + ".new", 'rb') as fobj:
150            content = fobj.read()
151            self.assertTrue(content.index(b"abc") < content.index(b"cba"))
152
153    def tearDown(self):
154        os.unlink(SAMPLE + ".new")
155        os.unlink(BROKEN + ".new")
156        os.unlink(SAMPLE + ".justtag")
157        os.unlink(SAMPLE + ".tag_at_start")
158
159
160class TAPEv2ThenID3v1Writer(TAPEWriter):
161    offset = 128
162
163    def setUp(self):
164        super(TAPEv2ThenID3v1Writer, self).setUp()
165        f = open(SAMPLE + ".new", "ab+")
166        f.write(b"TAG" + b"\x00" * 125)
167        f.close()
168        f = open(BROKEN + ".new", "ab+")
169        f.write(b"TAG" + b"\x00" * 125)
170        f.close()
171        f = open(SAMPLE + ".justtag", "ab+")
172        f.write(b"TAG" + b"\x00" * 125)
173        f.close()
174
175    def test_tag_at_start_write(self):
176        pass
177
178
179class TAPEv2(TestCase):
180
181    def setUp(self):
182        self.filename = get_temp_copy(OLD)
183        self.audio = APEv2(self.filename)
184
185    def tearDown(self):
186        os.unlink(self.filename)
187
188    def test_invalid_key(self):
189        self.failUnlessRaises(
190            KeyError, self.audio.__setitem__, u"\u1234", "foo")
191
192    def test_guess_text(self):
193        from mutagen.apev2 import APETextValue
194        self.audio["test"] = u"foobar"
195        self.failUnlessEqual(self.audio["test"], "foobar")
196        self.failUnless(isinstance(self.audio["test"], APETextValue))
197
198    def test_guess_text_list(self):
199        from mutagen.apev2 import APETextValue
200        self.audio["test"] = [u"foobar", "quuxbarz"]
201        self.failUnlessEqual(self.audio["test"], "foobar\x00quuxbarz")
202        self.failUnless(isinstance(self.audio["test"], APETextValue))
203
204    def test_guess_utf8(self):
205        from mutagen.apev2 import APETextValue
206        self.audio["test"] = "foobar"
207        self.failUnlessEqual(self.audio["test"], "foobar")
208        self.failUnless(isinstance(self.audio["test"], APETextValue))
209
210    def test_guess_not_utf8(self):
211        from mutagen.apev2 import APEBinaryValue
212        self.audio["test"] = b"\xa4woo"
213        self.failUnless(isinstance(self.audio["test"], APEBinaryValue))
214        self.failUnlessEqual(4, len(self.audio["test"]))
215
216    def test_bad_value_type(self):
217        from mutagen.apev2 import APEValue
218        self.failUnlessRaises(ValueError, APEValue, "foo", 99)
219
220    def test_module_delete_empty(self):
221        from mutagen.apev2 import delete
222        delete(os.path.join(DATA_DIR, "emptyfile.mp3"))
223
224    def test_invalid(self):
225        self.failUnlessRaises(MutagenError, mutagen.apev2.APEv2, "dne")
226
227    def test_no_tag(self):
228        self.failUnlessRaises(MutagenError, mutagen.apev2.APEv2,
229                              os.path.join(DATA_DIR, "empty.mp3"))
230
231    def test_cases(self):
232        self.failUnlessEqual(self.audio["artist"], self.audio["ARTIST"])
233        self.failUnless("artist" in self.audio)
234        self.failUnless("artisT" in self.audio)
235
236    def test_keys(self):
237        self.failUnless("Track" in self.audio.keys())
238        self.failUnless("AnArtist" in self.audio.values())
239
240        self.failUnlessEqual(
241            self.audio.items(),
242            list(izip(self.audio.keys(), self.audio.values())))
243
244    def test_key_type(self):
245        key = self.audio.keys()[0]
246        if PY3:
247            self.assertTrue(isinstance(key, text_type))
248        else:
249            self.assertTrue(isinstance(key, bytes))
250
251    def test_invalid_keys(self):
252        self.failUnlessRaises(KeyError, self.audio.__getitem__, "\x00")
253        self.failUnlessRaises(KeyError, self.audio.__setitem__, "\x00", "")
254        self.failUnlessRaises(KeyError, self.audio.__delitem__, "\x00")
255
256    def test_dictlike(self):
257        self.failUnless(self.audio.get("track"))
258        self.failUnless(self.audio.get("Track"))
259
260    def test_del(self):
261        s = self.audio["artist"]
262        del(self.audio["artist"])
263        self.failIf("artist" in self.audio)
264        self.failUnlessRaises(KeyError, self.audio.__getitem__, "artist")
265        self.audio["Artist"] = s
266        self.failUnlessEqual(self.audio["artist"], "AnArtist")
267
268    def test_values(self):
269        self.failUnlessEqual(self.audio["artist"], self.audio["artist"])
270        self.failUnless(self.audio["artist"] < self.audio["title"])
271        self.failUnlessEqual(self.audio["artist"], "AnArtist")
272        self.failUnlessEqual(self.audio["title"], "Some Music")
273        self.failUnlessEqual(self.audio["album"], "A test case")
274        self.failUnlessEqual("07", self.audio["track"])
275
276        self.failIfEqual(self.audio["album"], "A test Case")
277
278    def test_pprint(self):
279        self.failUnless(self.audio.pprint())
280
281
282class TAPEv2ThenID3v1(TAPEv2):
283
284    def setUp(self):
285        super(TAPEv2ThenID3v1, self).setUp()
286        f = open(self.filename, "ab+")
287        f.write(b"TAG" + b"\x00" * 125)
288        f.close()
289        self.audio = APEv2(self.filename)
290
291
292class TAPEv2WithLyrics2(TestCase):
293
294    def setUp(self):
295        self.tag = mutagen.apev2.APEv2(LYRICS2)
296
297    def test_values(self):
298        self.failUnlessEqual(self.tag["MP3GAIN_MINMAX"], "000,179")
299        self.failUnlessEqual(self.tag["REPLAYGAIN_TRACK_GAIN"], "-4.080000 dB")
300        self.failUnlessEqual(self.tag["REPLAYGAIN_TRACK_PEAK"], "1.008101")
301
302
303class TAPEBinaryValue(TestCase):
304
305    from mutagen.apev2 import APEBinaryValue as BV
306    BV = BV
307
308    def setUp(self):
309        self.sample = b"\x12\x45\xde"
310        self.value = mutagen.apev2.APEValue(self.sample, mutagen.apev2.BINARY)
311
312    def test_type(self):
313        self.failUnless(isinstance(self.value, self.BV))
314
315    def test_const(self):
316        self.failUnlessEqual(self.sample, bytes(self.value))
317
318    def test_repr(self):
319        repr(self.value)
320
321    def test_pprint(self):
322        self.assertEqual(self.value.pprint(), "[3 bytes]")
323
324    def test_type2(self):
325        self.assertRaises(TypeError,
326                          mutagen.apev2.APEValue, u"abc", mutagen.apev2.BINARY)
327
328
329class TAPETextValue(TestCase):
330
331    from mutagen.apev2 import APETextValue as TV
332    TV = TV
333
334    def setUp(self):
335        self.sample = ["foo", "bar", "baz"]
336        self.value = mutagen.apev2.APEValue(
337            "\0".join(self.sample), mutagen.apev2.TEXT)
338
339    def test_parse(self):
340        self.assertRaises(APEBadItemError, self.TV._new, b"\xff")
341
342    def test_type(self):
343        self.failUnless(isinstance(self.value, self.TV))
344
345    def test_construct(self):
346        self.assertEqual(text_type(self.TV(u"foo")), u"foo")
347        if not PY3:
348            self.assertEqual(text_type(self.TV(b"foo")), u"foo")
349            self.assertRaises(ValueError, self.TV, b"\xff")
350
351    def test_list(self):
352        self.failUnlessEqual(self.sample, list(self.value))
353
354    def test_setitem_list(self):
355        self.value[2] = self.sample[2] = 'quux'
356        self.test_list()
357        self.test_getitem()
358        self.value[2] = self.sample[2] = 'baz'
359
360    def test_getitem(self):
361        for i in xrange(len(self.value)):
362            self.failUnlessEqual(self.sample[i], self.value[i])
363
364    def test_delitem(self):
365        del self.sample[1]
366        self.assertEqual(list(self.sample), ["foo", "baz"])
367        del self.sample[1:]
368        self.assertEqual(list(self.sample), ["foo"])
369
370    def test_insert(self):
371        self.sample.insert(0, "a")
372        self.assertEqual(len(self.sample), 4)
373        self.assertEqual(self.sample[0], "a")
374        if PY3:
375            self.assertRaises(TypeError, self.value.insert, 2, b"abc")
376
377    def test_types(self):
378        if PY3:
379            self.assertRaises(TypeError, self.value.__setitem__, 2, b"abc")
380            self.assertRaises(
381                TypeError, mutagen.apev2.APEValue, b"abc", mutagen.apev2.TEXT)
382
383    def test_repr(self):
384        repr(self.value)
385
386    def test_str(self):
387        self.assertEqual(text_type(self.value), u"foo\x00bar\x00baz")
388
389    def test_pprint(self):
390        self.assertEqual(self.value.pprint(), "foo / bar / baz")
391
392
393class TAPEExtValue(TestCase):
394
395    from mutagen.apev2 import APEExtValue as EV
396    EV = EV
397
398    def setUp(self):
399        self.sample = "http://foo"
400        self.value = mutagen.apev2.APEValue(
401            self.sample, mutagen.apev2.EXTERNAL)
402
403    def test_type(self):
404        self.failUnless(isinstance(self.value, self.EV))
405
406    def test_const(self):
407        self.failUnlessEqual(self.sample, self.value)
408
409    def test_repr(self):
410        repr(self.value)
411
412    if PY3:
413        def test_py3(self):
414            self.assertRaises(
415                TypeError, mutagen.apev2.APEValue, b"abc",
416                mutagen.apev2.EXTERNAL)
417
418    def test_pprint(self):
419        self.assertEqual(self.value.pprint(), "[External] http://foo")
420
421
422class TAPEv2File(TestCase):
423
424    def setUp(self):
425        self.audio = APEv2File(os.path.join(DATA_DIR, "click.mpc"))
426
427    def test_empty(self):
428        f = APEv2File(os.path.join(DATA_DIR, "xing.mp3"))
429        self.assertFalse(f.items())
430
431    def test_add_tags(self):
432        self.failUnless(self.audio.tags is None)
433        self.audio.add_tags()
434        self.failUnless(self.audio.tags is not None)
435        self.failUnlessRaises(APEv2Error, self.audio.add_tags)
436
437    def test_unknown_info(self):
438        info = self.audio.info
439        info.pprint()
440