]> kaliko git repositories - python-musicpd.git/blob - test.py
Honor MPD_TIMEOUT environment variables (closes #11)
[python-musicpd.git] / test.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 # pylint: disable=missing-docstring
4 """
5 Test suite highly borrowed^Wsteal from python-mpd2 [0] project.
6
7 [0] https://github.com/Mic92/python-mpd2
8 """
9
10
11 import itertools
12 import os
13 import sys
14 import types
15 import unittest
16 import warnings
17
18 import musicpd
19
20 try:
21     import unittest.mock as mock
22 except ImportError:
23     try:
24         import mock
25     except ImportError:
26         print("Please install mock from PyPI to run tests!")
27         sys.exit(1)
28
29 # show deprecation warnings
30 warnings.simplefilter('default')
31
32
33 TEST_MPD_HOST, TEST_MPD_PORT = ('example.com', 10000)
34
35
36 class testEnvVar(unittest.TestCase):
37
38     def test_envvar(self):
39         os.environ.pop('MPD_HOST', None)
40         os.environ.pop('MPD_PORT', None)
41         client = musicpd.MPDClient()
42         self.assertEqual(client.host, 'localhost')
43         self.assertEqual(client.port, '6600')
44
45         os.environ['MPD_HOST'] = 'pa55w04d@example.org'
46         client = musicpd.MPDClient()
47         self.assertEqual(client.pwd, 'pa55w04d')
48         self.assertEqual(client.host, 'example.org')
49         self.assertEqual(client.port, '6600')
50
51         os.environ.pop('MPD_HOST', None)
52         os.environ['MPD_PORT'] = '6666'
53         client = musicpd.MPDClient()
54         self.assertEqual(client.pwd, None)
55         self.assertEqual(client.host, 'localhost')
56         self.assertEqual(client.port, '6666')
57
58         # Test unix socket fallback
59         os.environ.pop('MPD_HOST', None)
60         os.environ.pop('MPD_PORT', None)
61         os.environ.pop('XDG_RUNTIME_DIR', None)
62         with mock.patch('os.path.exists', return_value=True):
63             client = musicpd.MPDClient()
64             self.assertEqual(client.host, '/run/mpd/socket')
65
66         os.environ.pop('MPD_HOST', None)
67         os.environ.pop('MPD_PORT', None)
68         os.environ['XDG_RUNTIME_DIR'] = '/run/user/1000/'
69         with mock.patch('os.path.exists', return_value=True):
70             client = musicpd.MPDClient()
71             self.assertEqual(client.host, '/run/user/1000/mpd/socket')
72
73         os.environ.pop('MPD_TIMEOUT', None)
74         client = musicpd.MPDClient()
75         self.assertEqual(client.mpd_timeout, 30)
76         os.environ['MPD_TIMEOUT'] = 'garbage'
77         client = musicpd.MPDClient()
78         self.assertEqual(client.mpd_timeout, 30)
79         os.environ['MPD_TIMEOUT'] = '42'
80         client = musicpd.MPDClient()
81         self.assertEqual(client.mpd_timeout, 42)
82
83
84 class TestMPDClient(unittest.TestCase):
85
86     longMessage = True
87     # last sync: musicpd 0.4.2 unreleased / Mon Nov 17 21:45:22 CET 2014
88     commands = {
89             # Status Commands
90             'clearerror':         'nothing',
91             'currentsong':        'object',
92             'idle':               'list',
93             'noidle':             None,
94             'status':             'object',
95             'stats':              'object',
96             # Playback Option Commands
97             'consume':            'nothing',
98             'crossfade':          'nothing',
99             'mixrampdb':          'nothing',
100             'mixrampdelay':       'nothing',
101             'random':             'nothing',
102             'repeat':             'nothing',
103             'setvol':             'nothing',
104             'single':             'nothing',
105             'replay_gain_mode':   'nothing',
106             'replay_gain_status': 'item',
107             'volume':             'nothing',
108             # Playback Control Commands
109             'next':               'nothing',
110             'pause':              'nothing',
111             'play':               'nothing',
112             'playid':             'nothing',
113             'previous':           'nothing',
114             'seek':               'nothing',
115             'seekid':             'nothing',
116             'seekcur':            'nothing',
117             'stop':               'nothing',
118             # Playlist Commands
119             'add':                'nothing',
120             'addid':              'item',
121             'clear':              'nothing',
122             'delete':             'nothing',
123             'deleteid':           'nothing',
124             'move':               'nothing',
125             'moveid':             'nothing',
126             'playlist':           'playlist',
127             'playlistfind':       'songs',
128             'playlistid':         'songs',
129             'playlistinfo':       'songs',
130             'playlistsearch':     'songs',
131             'plchanges':          'songs',
132             'plchangesposid':     'changes',
133             'prio':               'nothing',
134             'prioid':             'nothing',
135             'rangeid':            'nothing',
136             'shuffle':            'nothing',
137             'swap':               'nothing',
138             'swapid':             'nothing',
139             'addtagid':           'nothing',
140             'cleartagid':         'nothing',
141             # Stored Playlist Commands
142             'listplaylist':       'list',
143             'listplaylistinfo':   'songs',
144             'listplaylists':      'playlists',
145             'load':               'nothing',
146             'playlistadd':        'nothing',
147             'playlistclear':      'nothing',
148             'playlistdelete':     'nothing',
149             'playlistmove':       'nothing',
150             'rename':             'nothing',
151             'rm':                 'nothing',
152             'save':               'nothing',
153             # Database Commands
154             'count':              'object',
155             'find':               'songs',
156             'findadd':            'nothing',
157             'list':               'list',
158             'listall':            'database',
159             'listallinfo':        'database',
160             'lsinfo':             'database',
161             'search':             'songs',
162             'searchadd':          'nothing',
163             'searchaddpl':        'nothing',
164             'update':             'item',
165             'rescan':             'item',
166             'readcomments':       'object',
167             # Mounts and neighbors
168             'mount':              'nothing',
169             'unmount':            'nothing',
170             'listmounts':         'mounts',
171             'listneighbors':      'neighbors',
172             # Sticker Commands
173             'sticker get':        'item',
174             'sticker set':        'nothing',
175             'sticker delete':     'nothing',
176             'sticker list':       'list',
177             'sticker find':       'songs',
178             # Connection Commands
179             'close':              None,
180             'kill':               None,
181             'password':           'nothing',
182             'ping':               'nothing',
183             # Partition Commands
184             'partition':          'nothing',
185             'listpartitions':     'list',
186             'newpartition':       'nothing',
187             # Audio Output Commands
188             'disableoutput':      'nothing',
189             'enableoutput':       'nothing',
190             'toggleoutput':       'nothing',
191             'outputs':            'outputs',
192             # Reflection Commands
193             'config':             'object',
194             'commands':           'list',
195             'notcommands':        'list',
196             'tagtypes':           'list',
197             'urlhandlers':        'list',
198             'decoders':           'plugins',
199             # Client to Client
200             'subscribe':          'nothing',
201             'unsubscribe':        'nothing',
202             'channels':           'list',
203             'readmessages':       'messages',
204             'sendmessage':        'nothing',
205         }
206
207     def setUp(self):
208         self.socket_patch = mock.patch('musicpd.socket')
209         self.socket_mock = self.socket_patch.start()
210         self.socket_mock.getaddrinfo.return_value = [range(5)]
211
212         self.socket_mock.socket.side_effect = (
213             lambda *a, **kw:
214             # Create a new socket.socket() mock with default attributes,
215             # each time we are calling it back (otherwise, it keeps set
216             # attributes across calls).
217             # That's probably what we want, since reconnecting is like
218             # reinitializing the entire connection, and so, the mock.
219             mock.MagicMock(name='socket.socket'))
220
221         self.client = musicpd.MPDClient()
222         self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT)
223         self.client._sock.reset_mock()
224         self.MPDWillReturn('ACK don\'t forget to setup your mock\n')
225
226     def tearDown(self):
227         self.socket_patch.stop()
228
229     def MPDWillReturn(self, *lines):
230         # Return what the caller wants first, then do as if the socket was
231         # disconnected.
232         self.client._rfile.readline.side_effect = itertools.chain(
233             lines, itertools.repeat(''))
234
235     def MPDWillReturnBinary(self, lines):
236         data = bytearray(b''.join(lines))
237
238         def read(amount):
239             val = bytearray()
240             while amount > 0:
241                 amount -= 1
242                 # _ = data.pop(0)
243                 # print(hex(_))
244                 val.append(data.pop(0))
245             return val
246
247         def readline():
248             val = bytearray()
249             while not val.endswith(b'\x0a'):
250                 val.append(data.pop(0))
251             return val
252         self.client._rbfile.readline.side_effect = readline
253         self.client._rbfile.read.side_effect = read
254
255     def assertMPDReceived(self, *lines):
256         self.client._wfile.write.assert_called_with(*lines)
257
258     def test_metaclass_commands(self):
259         """Controls client has at least commands as last synchronized in
260         TestMPDClient.commands"""
261         for cmd, ret in TestMPDClient.commands.items():
262             self.assertTrue(hasattr(self.client, cmd), msg='cmd "{}" not available!'.format(cmd))
263             if ' ' in cmd:
264                 self.assertTrue(hasattr(self.client, cmd.replace(' ', '_')))
265
266     def test_fetch_nothing(self):
267         self.MPDWillReturn('OK\n')
268         self.assertIsNone(self.client.ping())
269         self.assertMPDReceived('ping\n')
270
271     def test_fetch_list(self):
272         self.MPDWillReturn('OK\n')
273         self.assertIsInstance(self.client.list('album'), list)
274         self.assertMPDReceived('list "album"\n')
275
276     def test_fetch_item(self):
277         self.MPDWillReturn('updating_db: 42\n', 'OK\n')
278         self.assertIsNotNone(self.client.update())
279
280     def test_fetch_object(self):
281         # XXX: _read_objects() doesn't wait for the final OK
282         self.MPDWillReturn('volume: 63\n', 'OK\n')
283         status = self.client.status()
284         self.assertMPDReceived('status\n')
285         self.assertIsInstance(status, dict)
286
287         # XXX: _read_objects() doesn't wait for the final OK
288         self.MPDWillReturn('OK\n')
289         stats = self.client.stats()
290         self.assertMPDReceived('stats\n')
291         self.assertIsInstance(stats, dict)
292
293     def test_fetch_songs(self):
294         self.MPDWillReturn('file: my-song.ogg\n', 'Pos: 0\n', 'Id: 66\n', 'OK\n')
295         playlist = self.client.playlistinfo()
296
297         self.assertMPDReceived('playlistinfo\n')
298         self.assertIsInstance(playlist, list)
299         self.assertEqual(1, len(playlist))
300         e = playlist[0]
301         self.assertIsInstance(e, dict)
302         self.assertEqual('my-song.ogg', e['file'])
303         self.assertEqual('0', e['pos'])
304         self.assertEqual('66', e['id'])
305
306     def test_send_and_fetch(self):
307         self.MPDWillReturn('volume: 50\n', 'OK\n')
308         result = self.client.send_status()
309         self.assertEqual(None, result)
310         self.assertMPDReceived('status\n')
311
312         status = self.client.fetch_status()
313         self.assertEqual(1, self.client._wfile.write.call_count)
314         self.assertEqual({'volume': '50'}, status)
315
316     def test_iterating(self):
317         self.MPDWillReturn('file: my-song.ogg\n', 'Pos: 0\n', 'Id: 66\n',
318                            'file: my-song.ogg\n', 'Pos: 0\n', 'Id: 66\n', 'OK\n')
319         self.client.iterate = True
320         playlist = self.client.playlistinfo()
321         self.assertMPDReceived('playlistinfo\n')
322         self.assertIsInstance(playlist, types.GeneratorType)
323         self.assertTrue(self.client._iterating)
324         for song in playlist:
325             self.assertRaises(musicpd.IteratingError, self.client.status)
326             self.assertIsInstance(song, dict)
327             self.assertEqual('my-song.ogg', song['file'])
328             self.assertEqual('0', song['pos'])
329             self.assertEqual('66', song['id'])
330         self.assertFalse(self.client._iterating)
331
332     def test_noidle(self):
333         self.MPDWillReturn('OK\n') # nothing changed after idle-ing
334         self.client.send_idle()
335         self.MPDWillReturn('OK\n') # nothing changed after noidle
336         self.assertEqual(self.client.noidle(), [])
337         self.assertMPDReceived('noidle\n')
338         self.MPDWillReturn('volume: 50\n', 'OK\n')
339         self.client.status()
340         self.assertMPDReceived('status\n')
341
342     def test_noidle_while_idle_started_sending(self):
343         self.MPDWillReturn('OK\n') # nothing changed after idle
344         self.client.send_idle()
345         self.MPDWillReturn('CHANGED: player\n', 'OK\n')  # noidle response
346         self.assertEqual(self.client.noidle(), ['player'])
347         self.MPDWillReturn('volume: 50\n', 'OK\n')
348         status = self.client.status()
349         self.assertMPDReceived('status\n')
350         self.assertEqual({'volume': '50'}, status)
351
352     def test_throw_when_calling_noidle_withoutidling(self):
353         self.assertRaises(musicpd.CommandError, self.client.noidle)
354         self.client.send_status()
355         self.assertRaises(musicpd.CommandError, self.client.noidle)
356
357     def test_client_to_client(self):
358         # client to client is at this time in beta!
359
360         self.MPDWillReturn('OK\n')
361         self.assertIsNone(self.client.subscribe("monty"))
362         self.assertMPDReceived('subscribe "monty"\n')
363
364         self.MPDWillReturn('channel: monty\n', 'OK\n')
365         channels = self.client.channels()
366         self.assertMPDReceived('channels\n')
367         self.assertEqual(['monty'], channels)
368
369         self.MPDWillReturn('OK\n')
370         self.assertIsNone(self.client.sendmessage('monty', 'SPAM'))
371         self.assertMPDReceived('sendmessage "monty" "SPAM"\n')
372
373         self.MPDWillReturn('channel: monty\n', 'message: SPAM\n', 'OK\n')
374         msg = self.client.readmessages()
375         self.assertMPDReceived('readmessages\n')
376         self.assertEqual(msg, [{'channel': 'monty', 'message': 'SPAM'}])
377
378         self.MPDWillReturn('OK\n')
379         self.assertIsNone(self.client.unsubscribe('monty'))
380         self.assertMPDReceived('unsubscribe "monty"\n')
381
382         self.MPDWillReturn('OK\n')
383         channels = self.client.channels()
384         self.assertMPDReceived('channels\n')
385         self.assertEqual([], channels)
386
387     def test_ranges_in_command_args(self):
388         self.MPDWillReturn('OK\n')
389         self.client.playlistinfo((10,))
390         self.assertMPDReceived('playlistinfo 10:\n')
391
392         self.MPDWillReturn('OK\n')
393         self.client.playlistinfo(('10',))
394         self.assertMPDReceived('playlistinfo 10:\n')
395
396         self.MPDWillReturn('OK\n')
397         self.client.playlistinfo((10, 12))
398         self.assertMPDReceived('playlistinfo 10:12\n')
399
400         self.MPDWillReturn('OK\n')
401         self.client.rangeid(())
402         self.assertMPDReceived('rangeid :\n')
403
404         for arg in [(10, 't'), (10, 1, 1), (None,1)]:
405             self.MPDWillReturn('OK\n')
406             with self.assertRaises(musicpd.CommandError):
407                 self.client.playlistinfo(arg)
408
409     def test_numbers_as_command_args(self):
410         self.MPDWillReturn('OK\n')
411         self.client.find('file', 1)
412         self.assertMPDReceived('find "file" "1"\n')
413
414     def test_commands_without_callbacks(self):
415         self.MPDWillReturn('\n')
416         self.client.close()
417         self.assertMPDReceived('close\n')
418
419         # XXX: what are we testing here?
420         self.client._reset()
421         self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT)
422
423     def test_connection_lost(self):
424         # Simulate a connection lost: the socket returns empty strings
425         self.MPDWillReturn('')
426
427         with self.assertRaises(musicpd.ConnectionError):
428             self.client.status()
429
430         # consistent behaviour
431         # solves <https://github.com/Mic92/python-mpd2/issues/11> (also present
432         # in python-mpd)
433         with self.assertRaises(musicpd.ConnectionError):
434             self.client.status()
435
436         self.assertIs(self.client._sock, None)
437
438     def test_parse_sticker_get_no_sticker(self):
439         self.MPDWillReturn('ACK [50@0] {sticker} no such sticker\n')
440         self.assertRaises(musicpd.CommandError,
441                           self.client.sticker_get, 'song', 'baz', 'foo')
442
443     def test_parse_sticker_list(self):
444         self.MPDWillReturn('sticker: foo=bar\n', 'sticker: lom=bok\n', 'OK\n')
445         res = self.client.sticker_list('song', 'baz')
446         self.assertEqual(['foo=bar', 'lom=bok'], res)
447
448         # Even with only one sticker, we get a dict
449         self.MPDWillReturn('sticker: foo=bar\n', 'OK\n')
450         res = self.client.sticker_list('song', 'baz')
451         self.assertEqual(['foo=bar'], res)
452
453     def test_albumart(self):
454         # here is a 34 bytes long data
455         data = bytes('\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01'
456                      '\x00\x01\x00\x00\xff\xdb\x00C\x00\x05\x03\x04',
457                      encoding='utf8')
458         read_lines = [b'size: 42\nbinary: 34\n', data, b'\nOK\n']
459         self.MPDWillReturnBinary(read_lines)
460         # Reading albumart / offset 0 should return the data
461         res = self.client.albumart('muse/Raised Fist/2002-Dedication/', 0)
462         self.assertEqual(res.get('data'), data)
463
464
465 if __name__ == '__main__':
466     unittest.main()