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