]> kaliko git repositories - python-musicpd.git/blob - test.py
Releasing 0.9.0
[python-musicpd.git] / test.py
1 #!/usr/bin/env python3
2 # coding: utf-8
3 # SPDX-FileCopyrightText: 2012-2024  kaliko <kaliko@azylum.org>
4 # SPDX-License-Identifier: LGPL-3.0-or-later
5 # pylint: disable=missing-docstring
6 """
7 Test suite highly borrowed^Wsteal from python-mpd2 [0] project.
8
9 [0] https://github.com/Mic92/python-mpd2
10 """
11
12
13 import itertools
14 import os
15 import types
16 import unittest
17 import unittest.mock
18 import warnings
19
20 import musicpd
21
22 mock = unittest.mock
23
24 # show deprecation warnings
25 warnings.simplefilter('default')
26
27
28 TEST_MPD_HOST, TEST_MPD_PORT = ('example.com', 10000)
29
30
31 class TestEnvVar(unittest.TestCase):
32
33     def test_envvar(self):
34         # mock "os.path.exists" here to ensure there are no socket in
35         # XDG_RUNTIME_DIR/mpd or /run/mpd since with test defaults fallbacks
36         # when :
37         #   * neither MPD_HOST nor XDG_RUNTIME_DIR are not set
38         #   * /run/mpd does not expose a socket
39         with mock.patch('os.path.exists', return_value=False):
40             os.environ.pop('MPD_HOST', None)
41             os.environ.pop('MPD_PORT', None)
42             client = musicpd.MPDClient()
43             self.assertEqual(client.host, 'localhost')
44             self.assertEqual(client.port, '6600')
45
46             os.environ.pop('MPD_HOST', None)
47             os.environ['MPD_PORT'] = '6666'
48             client = musicpd.MPDClient()
49             self.assertEqual(client.pwd, None)
50             self.assertEqual(client.host, 'localhost')
51             self.assertEqual(client.port, '6666')
52
53         # Test password extraction
54         os.environ['MPD_HOST'] = 'pa55w04d@example.org'
55         client = musicpd.MPDClient()
56         self.assertEqual(client.pwd, 'pa55w04d')
57         self.assertEqual(client.host, 'example.org')
58
59         # Test host alone
60         os.environ['MPD_HOST'] = 'example.org'
61         client = musicpd.MPDClient()
62         self.assertFalse(client.pwd)
63         self.assertEqual(client.host, 'example.org')
64
65         # Test password extraction (no host)
66         os.environ['MPD_HOST'] = 'pa55w04d@'
67         with mock.patch('os.path.exists', return_value=False):
68             client = musicpd.MPDClient()
69         self.assertEqual(client.pwd, 'pa55w04d')
70         self.assertEqual(client.host, 'localhost')
71
72         # Test badly formatted MPD_HOST
73         os.environ['MPD_HOST'] = '@'
74         with mock.patch('os.path.exists', return_value=False):
75             client = musicpd.MPDClient()
76         self.assertEqual(client.pwd, None)
77         self.assertEqual(client.host, 'localhost')
78
79         # Test unix socket extraction
80         os.environ['MPD_HOST'] = 'pa55w04d@/unix/sock'
81         client = musicpd.MPDClient()
82         self.assertEqual(client.host, '/unix/sock')
83
84         # Test plain abstract socket extraction
85         os.environ['MPD_HOST'] = '@abstract'
86         client = musicpd.MPDClient()
87         self.assertEqual(client.host, '@abstract')
88
89         # Test password and abstract socket extraction
90         os.environ['MPD_HOST'] = 'pass@@abstract'
91         client = musicpd.MPDClient()
92         self.assertEqual(client.pwd, 'pass')
93         self.assertEqual(client.host, '@abstract')
94
95         # Test unix socket fallback
96         os.environ.pop('MPD_HOST', None)
97         os.environ.pop('MPD_PORT', None)
98         os.environ.pop('XDG_RUNTIME_DIR', None)
99         with mock.patch('os.path.exists', return_value=True):
100             client = musicpd.MPDClient()
101             self.assertEqual(client.host, '/run/mpd/socket')
102             os.environ['XDG_RUNTIME_DIR'] = '/run/user/1000'
103             client = musicpd.MPDClient()
104             self.assertEqual(client.host, '/run/user/1000/mpd/socket')
105
106         os.environ.pop('MPD_HOST', None)
107         os.environ.pop('MPD_PORT', None)
108         os.environ['XDG_RUNTIME_DIR'] = '/run/user/1000/'
109         with mock.patch('os.path.exists', return_value=True):
110             client = musicpd.MPDClient()
111             self.assertEqual(client.host, '/run/user/1000/mpd/socket')
112
113         # Test MPD_TIMEOUT
114         os.environ.pop('MPD_TIMEOUT', None)
115         client = musicpd.MPDClient()
116         self.assertEqual(client.mpd_timeout, musicpd.CONNECTION_TIMEOUT)
117         os.environ['MPD_TIMEOUT'] = 'garbage'
118         client = musicpd.MPDClient()
119         self.assertEqual(client.mpd_timeout,
120                          musicpd.CONNECTION_TIMEOUT,
121                          'Garbage is silently ignore to use default value')
122         os.environ['MPD_TIMEOUT'] = '42'
123         client = musicpd.MPDClient()
124         self.assertEqual(client.mpd_timeout, 42)
125
126
127 class TestMPDClient(unittest.TestCase):
128
129     longMessage = True
130     # last sync: musicpd 0.6.0 unreleased / Fri Feb 19 15:34:53 CET 2021
131     commands = {
132             # Status Commands
133             'clearerror':         'nothing',
134             'currentsong':        'object',
135             'idle':               'list',
136             'noidle':             None,
137             'status':             'object',
138             'stats':              'object',
139             # Playback Option Commands
140             'consume':            'nothing',
141             'crossfade':          'nothing',
142             'mixrampdb':          'nothing',
143             'mixrampdelay':       'nothing',
144             'random':             'nothing',
145             'repeat':             'nothing',
146             'setvol':             'nothing',
147             'getvol':             'object',
148             'single':             'nothing',
149             'replay_gain_mode':   'nothing',
150             'replay_gain_status': 'item',
151             'volume':             'nothing',
152             # Playback Control Commands
153             'next':               'nothing',
154             'pause':              'nothing',
155             'play':               'nothing',
156             'playid':             'nothing',
157             'previous':           'nothing',
158             'seek':               'nothing',
159             'seekid':             'nothing',
160             'seekcur':            'nothing',
161             'stop':               'nothing',
162             # Queue Commands
163             'add':                'nothing',
164             'addid':              'item',
165             'clear':              'nothing',
166             'delete':             'nothing',
167             'deleteid':           'nothing',
168             'move':               'nothing',
169             'moveid':             'nothing',
170             'playlist':           'playlist',
171             'playlistfind':       'songs',
172             'playlistid':         'songs',
173             'playlistinfo':       'songs',
174             'playlistsearch':     'songs',
175             'plchanges':          'songs',
176             'plchangesposid':     'changes',
177             'prio':               'nothing',
178             'prioid':             'nothing',
179             'rangeid':            'nothing',
180             'shuffle':            'nothing',
181             'swap':               'nothing',
182             'swapid':             'nothing',
183             'addtagid':           'nothing',
184             'cleartagid':         'nothing',
185             # Stored Playlist Commands
186             'listplaylist':       'list',
187             'listplaylistinfo':   'songs',
188             'listplaylists':      'playlists',
189             'load':               'nothing',
190             'playlistadd':        'nothing',
191             'playlistclear':      'nothing',
192             'playlistdelete':     'nothing',
193             'playlistmove':       'nothing',
194             'rename':             'nothing',
195             'rm':                 'nothing',
196             'save':               'nothing',
197             # Database Commands
198             'albumart':           'composite',
199             'count':              'object',
200             'getfingerprint':     'object',
201             'find':               'songs',
202             'findadd':            'nothing',
203             'list':               'list',
204             'listall':            'database',
205             'listallinfo':        'database',
206             'listfiles':          'database',
207             'lsinfo':             'database',
208             'readcomments':       'object',
209             'readpicture':        'composite',
210             'search':             'songs',
211             'searchadd':          'nothing',
212             'searchaddpl':        'nothing',
213             'update':             'item',
214             'rescan':             'item',
215             # Mounts and neighbors
216             'mount':              'nothing',
217             'unmount':            'nothing',
218             'listmounts':         'mounts',
219             'listneighbors':      'neighbors',
220             # Sticker Commands
221             'sticker get':        'item',
222             'sticker set':        'nothing',
223             'sticker delete':     'nothing',
224             'sticker list':       'list',
225             'sticker find':       'songs',
226             # Connection Commands
227             'close':              None,
228             'kill':               None,
229             'password':           'nothing',
230             'ping':               'nothing',
231             'binarylimit':        'nothing',
232             'tagtypes':           'list',
233             'tagtypes disable':   'nothing',
234             'tagtypes enable':    'nothing',
235             'tagtypes clear':     'nothing',
236             'tagtypes all':       'nothing',
237             # Partition Commands
238             'partition':          'nothing',
239             'listpartitions':     'list',
240             'newpartition':       'nothing',
241             'delpartition':       'nothing',
242             'moveoutput':         'nothing',
243             # Audio Output Commands
244             'disableoutput':      'nothing',
245             'enableoutput':       'nothing',
246             'toggleoutput':       'nothing',
247             'outputs':            'outputs',
248             'outputset':          'nothing',
249             # Reflection Commands
250             'config':             'object',
251             'commands':           'list',
252             'notcommands':        'list',
253             'urlhandlers':        'list',
254             'decoders':           'plugins',
255             # Client to Client
256             'subscribe':          'nothing',
257             'unsubscribe':        'nothing',
258             'channels':           'list',
259             'readmessages':       'messages',
260             'sendmessage':        'nothing',
261         }
262
263     def setUp(self):
264         self.socket_patch = mock.patch('musicpd.socket')
265         self.socket_mock = self.socket_patch.start()
266         self.socket_mock.getaddrinfo.return_value = [range(5)]
267
268         self.socket_mock.socket.side_effect = (
269             lambda *a, **kw:
270             # Create a new socket.socket() mock with default attributes,
271             # each time we are calling it back (otherwise, it keeps set
272             # attributes across calls).
273             # That's probably what we want, since reconnecting is like
274             # reinitializing the entire connection, and so, the mock.
275             mock.MagicMock(name='socket.socket'))
276
277         self.client = musicpd.MPDClient()
278         self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT)
279         self.client._sock.reset_mock()
280         self.MPDWillReturn('ACK don\'t forget to setup your mock\n')
281
282     def tearDown(self):
283         self.socket_patch.stop()
284
285     def MPDWillReturn(self, *lines):
286         # Return what the caller wants first, then do as if the socket was
287         # disconnected.
288         self.client._rfile.readline.side_effect = itertools.chain(
289             lines, itertools.repeat(''))
290
291     def MPDWillReturnBinary(self, lines):
292         data = bytearray(b''.join(lines))
293
294         def read(amount):
295             val = bytearray()
296             while amount > 0:
297                 amount -= 1
298                 # _ = data.pop(0)
299                 # print(hex(_))
300                 val.append(data.pop(0))
301             return val
302
303         def readline():
304             val = bytearray()
305             while not val.endswith(b'\x0a'):
306                 val.append(data.pop(0))
307             return val
308         self.client._rbfile.readline.side_effect = readline
309         self.client._rbfile.read.side_effect = read
310
311     def assertMPDReceived(self, *lines):
312         self.client._wfile.write.assert_called_with(*lines)
313
314     def test_metaclass_commands(self):
315         """Controls client has at least commands as last synchronized in
316         TestMPDClient.commands"""
317         for cmd, ret in TestMPDClient.commands.items():
318             self.assertTrue(hasattr(self.client, cmd), msg='cmd "{}" not available!'.format(cmd))
319             if ' ' in cmd:
320                 self.assertTrue(hasattr(self.client, cmd.replace(' ', '_')))
321
322     def test_fetch_nothing(self):
323         self.MPDWillReturn('OK\n')
324         self.assertIsNone(self.client.ping())
325         self.assertMPDReceived('ping\n')
326
327     def test_fetch_list(self):
328         self.MPDWillReturn('OK\n')
329         self.assertIsInstance(self.client.list('album'), list)
330         self.assertMPDReceived('list "album"\n')
331
332     def test_fetch_item(self):
333         self.MPDWillReturn('updating_db: 42\n', 'OK\n')
334         self.assertIsNotNone(self.client.update())
335
336     def test_fetch_object(self):
337         # XXX: _read_objects() doesn't wait for the final OK
338         self.MPDWillReturn('volume: 63\n', 'OK\n')
339         status = self.client.status()
340         self.assertMPDReceived('status\n')
341         self.assertIsInstance(status, dict)
342
343         # XXX: _read_objects() doesn't wait for the final OK
344         self.MPDWillReturn('OK\n')
345         stats = self.client.stats()
346         self.assertMPDReceived('stats\n')
347         self.assertIsInstance(stats, dict)
348
349         output = ['outputid: 0\n',
350                   'outputname: default detected output\n',
351                   'plugin: sndio\n',
352                   'outputenabled: 1\n']
353         self.MPDWillReturn(*output, 'OK\n')
354         outputs = self.client.outputs()
355         self.assertMPDReceived('outputs\n')
356         self.assertIsInstance(outputs, list)
357         self.assertEqual([{'outputid': '0', 'outputname': 'default detected output', 'plugin': 'sndio', 'outputenabled': '1'}], outputs)
358
359     def test_fetch_songs(self):
360         self.MPDWillReturn('file: my-song.ogg\n', 'Pos: 0\n', 'Id: 66\n', 'OK\n')
361         playlist = self.client.playlistinfo()
362
363         self.assertMPDReceived('playlistinfo\n')
364         self.assertIsInstance(playlist, list)
365         self.assertEqual(1, len(playlist))
366         e = playlist[0]
367         self.assertIsInstance(e, dict)
368         self.assertEqual('my-song.ogg', e['file'])
369         self.assertEqual('0', e['pos'])
370         self.assertEqual('66', e['id'])
371
372     def test_send_and_fetch(self):
373         self.MPDWillReturn('volume: 50\n', 'OK\n')
374         result = self.client.send_status()
375         self.assertEqual(None, result)
376         self.assertMPDReceived('status\n')
377
378         status = self.client.fetch_status()
379         self.assertEqual(1, self.client._wfile.write.call_count)
380         self.assertEqual({'volume': '50'}, status)
381
382     def test_iterating(self):
383         self.MPDWillReturn('file: my-song.ogg\n', 'Pos: 0\n', 'Id: 66\n',
384                            'file: my-song.ogg\n', 'Pos: 0\n', 'Id: 66\n', 'OK\n')
385         self.client.iterate = True
386         playlist = self.client.playlistinfo()
387         self.assertMPDReceived('playlistinfo\n')
388         self.assertIsInstance(playlist, types.GeneratorType)
389         self.assertTrue(self.client._iterating)
390         for song in playlist:
391             self.assertRaises(musicpd.IteratingError, self.client.status)
392             self.assertIsInstance(song, dict)
393             self.assertEqual('my-song.ogg', song['file'])
394             self.assertEqual('0', song['pos'])
395             self.assertEqual('66', song['id'])
396         self.assertFalse(self.client._iterating)
397
398     def test_noidle(self):
399         self.MPDWillReturn('OK\n') # nothing changed after idle-ing
400         self.client.send_idle()
401         self.MPDWillReturn('OK\n') # nothing changed after noidle
402         self.assertEqual(self.client.noidle(), [])
403         self.assertMPDReceived('noidle\n')
404         self.MPDWillReturn('volume: 50\n', 'OK\n')
405         self.client.status()
406         self.assertMPDReceived('status\n')
407
408     def test_noidle_while_idle_started_sending(self):
409         self.MPDWillReturn('OK\n') # nothing changed after idle
410         self.client.send_idle()
411         self.MPDWillReturn('CHANGED: player\n', 'OK\n')  # noidle response
412         self.assertEqual(self.client.noidle(), ['player'])
413         self.MPDWillReturn('volume: 50\n', 'OK\n')
414         status = self.client.status()
415         self.assertMPDReceived('status\n')
416         self.assertEqual({'volume': '50'}, status)
417
418     def test_throw_when_calling_noidle_withoutidling(self):
419         self.assertRaises(musicpd.CommandError, self.client.noidle)
420         self.client.send_status()
421         self.assertRaises(musicpd.CommandError, self.client.noidle)
422
423     def test_send_noidle_calls_noidle(self):
424         self.MPDWillReturn('OK\n') # nothing changed after idle
425         self.client.send_idle()
426         self.client.send_noidle()
427         self.assertMPDReceived('noidle\n')
428
429     def test_client_to_client(self):
430         self.MPDWillReturn('OK\n')
431         self.assertIsNone(self.client.subscribe("monty"))
432         self.assertMPDReceived('subscribe "monty"\n')
433
434         self.MPDWillReturn('channel: monty\n', 'OK\n')
435         channels = self.client.channels()
436         self.assertMPDReceived('channels\n')
437         self.assertEqual(['monty'], channels)
438
439         self.MPDWillReturn('OK\n')
440         self.assertIsNone(self.client.sendmessage('monty', 'SPAM'))
441         self.assertMPDReceived('sendmessage "monty" "SPAM"\n')
442
443         self.MPDWillReturn('channel: monty\n', 'message: SPAM\n', 'OK\n')
444         msg = self.client.readmessages()
445         self.assertMPDReceived('readmessages\n')
446         self.assertEqual(msg, [{'channel': 'monty', 'message': 'SPAM'}])
447
448         self.MPDWillReturn('OK\n')
449         self.assertIsNone(self.client.unsubscribe('monty'))
450         self.assertMPDReceived('unsubscribe "monty"\n')
451
452         self.MPDWillReturn('OK\n')
453         channels = self.client.channels()
454         self.assertMPDReceived('channels\n')
455         self.assertEqual([], channels)
456
457     def test_ranges_in_command_args(self):
458         self.MPDWillReturn('OK\n')
459         self.client.playlistinfo((10,))
460         self.assertMPDReceived('playlistinfo 10:\n')
461
462         self.MPDWillReturn('OK\n')
463         self.client.playlistinfo(('10',))
464         self.assertMPDReceived('playlistinfo 10:\n')
465
466         self.MPDWillReturn('OK\n')
467         self.client.playlistinfo((10, 12))
468         self.assertMPDReceived('playlistinfo 10:12\n')
469
470         self.MPDWillReturn('OK\n')
471         self.client.rangeid(())
472         self.assertMPDReceived('rangeid :\n')
473
474         for arg in [(10, 't'), (10, 1, 1), (None,1)]:
475             self.MPDWillReturn('OK\n')
476             with self.assertRaises(musicpd.CommandError):
477                 self.client.playlistinfo(arg)
478
479     def test_numbers_as_command_args(self):
480         self.MPDWillReturn('OK\n')
481         self.client.find('file', 1)
482         self.assertMPDReceived('find "file" "1"\n')
483
484     def test_commands_without_callbacks(self):
485         self.MPDWillReturn('\n')
486         self.client.close()
487         self.assertMPDReceived('close\n')
488
489         # XXX: what are we testing here?
490         self.client._reset()
491         self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT)
492
493     def test_connection_lost(self):
494         # Simulate a connection lost: the socket returns empty strings
495         self.MPDWillReturn('')
496
497         with self.assertRaises(musicpd.ConnectionError):
498             self.client.status()
499
500         # consistent behaviour
501         # solves <https://github.com/Mic92/python-mpd2/issues/11> (also present
502         # in python-mpd)
503         with self.assertRaises(musicpd.ConnectionError):
504             self.client.status()
505
506         self.assertIs(self.client._sock, None)
507
508     def test_parse_sticker_get_no_sticker(self):
509         self.MPDWillReturn('ACK [50@0] {sticker} no such sticker\n')
510         self.assertRaises(musicpd.CommandError,
511                           self.client.sticker_get, 'song', 'baz', 'foo')
512
513     def test_parse_sticker_list(self):
514         self.MPDWillReturn('sticker: foo=bar\n', 'sticker: lom=bok\n', 'OK\n')
515         res = self.client.sticker_list('song', 'baz')
516         self.assertEqual(['foo=bar', 'lom=bok'], res)
517
518         # Even with only one sticker, we get a dict
519         self.MPDWillReturn('sticker: foo=bar\n', 'OK\n')
520         res = self.client.sticker_list('song', 'baz')
521         self.assertEqual(['foo=bar'], res)
522
523     def test_albumart(self):
524         # here is a 34 bytes long data
525         data = bytes('\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01'
526                      '\x00\x01\x00\x00\xff\xdb\x00C\x00\x05\x03\x04',
527                      encoding='utf8')
528         read_lines = [b'size: 42\nbinary: 34\n', data, b'\nOK\n']
529         self.MPDWillReturnBinary(read_lines)
530         # Reading albumart / offset 0 should return the data
531         res = self.client.albumart('muse/Raised Fist/2002-Dedication/', 0)
532         self.assertEqual(res.get('data'), data)
533
534     def test_reading_binary(self):
535         # readpicture when there are no picture returns empty object
536         self.MPDWillReturnBinary([b'OK\n'])
537         res = self.client.readpicture('muse/Raised Fist/2002-Dedication/', 0)
538         self.assertEqual(res, {})
539
540     def test_command_list(self):
541         self.MPDWillReturn('updating_db: 42\n',
542                            f'{musicpd.NEXT}\n',
543                            'repeat: 0\n',
544                            'random: 0\n',
545                            f'{musicpd.NEXT}\n',
546                            f'{musicpd.NEXT}\n',
547                            'OK\n')
548         self.client.command_list_ok_begin()
549         self.client.update()
550         self.client.status()
551         self.client.repeat(1)
552         self.client.command_list_end()
553         self.assertMPDReceived('command_list_end\n')
554
555     def test_two_word_commands(self):
556         self.MPDWillReturn('OK\n')
557         self.client.tagtypes_clear()
558         self.assertMPDReceived('tagtypes clear\n')
559         self.MPDWillReturn('OK\n')
560         with self.assertRaises(AttributeError):
561             self.client.foo_bar()
562
563 class TestConnection(unittest.TestCase):
564
565     def test_exposing_fileno(self):
566         with mock.patch('musicpd.socket') as socket_mock:
567             sock = mock.MagicMock(name='socket')
568             socket_mock.socket.return_value = sock
569             cli = musicpd.MPDClient()
570             cli.connect()
571             cli.fileno()
572             cli._sock.fileno.assert_called_with()
573
574     def test_connect_abstract(self):
575         os.environ['MPD_HOST'] = '@abstract'
576         with mock.patch('musicpd.socket') as socket_mock:
577             sock = mock.MagicMock(name='socket')
578             socket_mock.socket.return_value = sock
579             cli = musicpd.MPDClient()
580             cli.connect()
581             sock.connect.assert_called_with('\0abstract')
582
583     def test_connect_unix(self):
584         os.environ['MPD_HOST'] = '/run/mpd/socket'
585         with mock.patch('musicpd.socket') as socket_mock:
586             sock = mock.MagicMock(name='socket')
587             socket_mock.socket.return_value = sock
588             cli = musicpd.MPDClient()
589             cli.connect()
590             sock.connect.assert_called_with('/run/mpd/socket')
591
592     def test_sockettimeout(self):
593         with mock.patch('musicpd.socket') as socket_mock:
594             sock = mock.MagicMock(name='socket')
595             socket_mock.socket.return_value = sock
596             cli = musicpd.MPDClient()
597             # Default is no socket timeout
598             cli.connect()
599             sock.settimeout.assert_called_with(None)
600             cli.disconnect()
601             # set a socket timeout before connection
602             cli.socket_timeout = 10
603             cli.connect()
604             sock.settimeout.assert_called_with(10)
605             # Set socket timeout while already connected
606             cli.socket_timeout = 42
607             sock.settimeout.assert_called_with(42)
608             # set a socket timeout using str
609             cli.socket_timeout = '10'
610             sock.settimeout.assert_called_with(10)
611             # Set socket timeout to None
612             cli.socket_timeout = None
613             sock.settimeout.assert_called_with(None)
614             # Set socket timeout Raises Exception
615             with self.assertRaises(ValueError):
616                 cli.socket_timeout = 'foo'
617             with self.assertRaises(ValueError,
618                     msg='socket_timeout expects a non zero positive integer'):
619                 cli.socket_timeout = '0'
620             with self.assertRaises(ValueError,
621                     msg='socket_timeout expects a non zero positive integer'):
622                 cli.socket_timeout = '-1'
623
624
625 class TestConnectionError(unittest.TestCase):
626
627     @mock.patch('socket.socket')
628     def test_connect_unix(self, socket_mock):
629         """Unix socket socket.error should raise a musicpd.ConnectionError"""
630         mocked_socket = socket_mock.return_value
631         mocked_socket.connect.side_effect = musicpd.socket.error(42, 'err 42')
632         os.environ['MPD_HOST'] = '/run/mpd/socket'
633         cli = musicpd.MPDClient()
634         with self.assertRaises(musicpd.ConnectionError) as cme:
635             cli.connect()
636         self.assertEqual('[Errno 42] err 42', str(cme.exception))
637
638     def test_non_available_unix_socket(self):
639         delattr(musicpd.socket, 'AF_UNIX')
640         os.environ['MPD_HOST'] = '/run/mpd/socket'
641         cli = musicpd.MPDClient()
642         with self.assertRaises(musicpd.ConnectionError) as cme:
643             cli.connect()
644         self.assertEqual('Unix domain sockets not supported on this platform',
645                          str(cme.exception))
646
647     @mock.patch('socket.getaddrinfo')
648     def test_connect_tcp_getaddrinfo(self, gai_mock):
649         """TCP socket.gaierror should raise a musicpd.ConnectionError"""
650         gai_mock.side_effect = musicpd.socket.error(42, 'gaierr 42')
651         cli = musicpd.MPDClient()
652         with self.assertRaises(musicpd.ConnectionError) as cme:
653             cli.connect(host=TEST_MPD_HOST)
654         self.assertEqual('[Errno 42] gaierr 42', str(cme.exception))
655
656     @mock.patch('socket.getaddrinfo')
657     @mock.patch('socket.socket')
658     def test_connect_tcp_connect(self, socket_mock, gai_mock):
659         """A socket.error should raise a musicpd.ConnectionError
660         Mocking getaddrinfo to prevent network access (DNS)
661         """
662         gai_mock.return_value = [range(5)]
663         mocked_socket = socket_mock.return_value
664         mocked_socket.connect.side_effect = musicpd.socket.error(42, 'tcp conn err 42')
665         cli = musicpd.MPDClient()
666         with self.assertRaises(musicpd.ConnectionError) as cme:
667             cli.connect(host=TEST_MPD_HOST)
668         self.assertEqual('[Errno 42] tcp conn err 42', str(cme.exception))
669
670     @mock.patch('socket.getaddrinfo')
671     def test_connect_tcp_connect_empty_gai(self, gai_mock):
672         """An empty getaddrinfo should raise a musicpd.ConnectionError"""
673         gai_mock.return_value = []
674         cli = musicpd.MPDClient()
675         with self.assertRaises(musicpd.ConnectionError) as cme:
676             cli.connect(host=TEST_MPD_HOST)
677         self.assertEqual('getaddrinfo returns an empty list', str(cme.exception))
678
679
680 class TestCommandErrorException(unittest.TestCase):
681
682     def test_error_on_newline(self):
683         os.environ['MPD_HOST'] = '/run/mpd/socket'
684         with mock.patch('musicpd.socket') as socket_mock:
685             sock = mock.MagicMock(name='socket')
686             socket_mock.socket.return_value = sock
687             cli = musicpd.MPDClient()
688             cli.connect()
689             with self.assertRaises(musicpd.CommandError):
690                 cli.find('(album == "foo\nbar")')
691
692 class testContextManager(unittest.TestCase):
693
694     def test_enter_exit(self):
695         os.environ['MPD_HOST'] = '@abstract'
696         with mock.patch('musicpd.socket') as socket_mock:
697             sock = mock.MagicMock(name='socket')
698             socket_mock.socket.return_value = sock
699             cli = musicpd.MPDClient()
700             with cli as c:
701                 sock.connect.assert_called_with('\0abstract')
702                 sock.close.assert_not_called()
703             sock.close.assert_called()
704
705 class testRange(unittest.TestCase):
706
707     def test_range(self):
708         tests = [
709                 ((),          ':'),
710                 ((None,None), ':'),
711                 (('',''),     ':'),
712                 (('',),       ':'),
713                 ((42,42),     '42:42'),
714                 ((42,),       '42:'),
715                 (('42',),     '42:'),
716                 (('42',None), '42:'),
717                 (('42',''),   '42:'),
718         ]
719         for tpl, result in tests:
720             self.assertEqual(str(musicpd.Range(tpl)), result)
721         with self.assertRaises(musicpd.CommandError):
722             #CommandError: Integer expected to start the range: (None, 42)
723             musicpd.Range((None,'42'))
724         with self.assertRaises(musicpd.CommandError):
725             # CommandError: Not an integer: "foo"
726             musicpd.Range(('foo',))
727         with self.assertRaises(musicpd.CommandError):
728             # CommandError: Wrong range: 42 > 41
729             musicpd.Range(('42',41))
730         with self.assertRaises(musicpd.CommandError):
731             # CommandError: Wrong range: 42 > 41
732             musicpd.Range(('42','42','42'))
733
734
735 if __name__ == '__main__':
736     unittest.main()