From 21c2a1843902d21f559af15cdd3eb8a9f35a8b86 Mon Sep 17 00:00:00 2001 From: Kaliko Jack Date: Fri, 20 Jun 2014 13:56:54 +0200 Subject: [PATCH] Add unittest --- CHANGES.txt | 5 + musicpd.py | 2 +- test.py | 360 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 366 insertions(+), 1 deletion(-) create mode 100755 test.py diff --git a/CHANGES.txt b/CHANGES.txt index 3c097e5..921e0f6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,11 @@ python-musicpd Changes List =========================== +Changes in 0.4.2 UNRELEASED +---------------- + +* Add unittest (requires mock) + Changes in 0.4.1 ---------------- diff --git a/musicpd.py b/musicpd.py index fe14987..0fe3b67 100644 --- a/musicpd.py +++ b/musicpd.py @@ -24,7 +24,7 @@ HELLO_PREFIX = "OK MPD " ERROR_PREFIX = "ACK " SUCCESS = "OK" NEXT = "list_OK" -VERSION = '0.4.1' +VERSION = '0.4.2' class MPDError(Exception): diff --git a/test.py b/test.py new file mode 100755 index 0000000..fccf921 --- /dev/null +++ b/test.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test suite highly borrowed^Wsteal from python-mpd2 [0] project. + +[0] https://github.com/Mic92/python-mpd2 +""" + + +import itertools +import sys +import types +import unittest +import warnings + +import musicpd + +try: + import unittest.mock as mock +except ImportError: + try: + import mock + except ImportError: + print("Please install mock from PyPI to run tests!") + sys.exit(1) + +# show deprecation warnings +warnings.simplefilter('default') + + +TEST_MPD_HOST, TEST_MPD_PORT = ('example.com', 10000) + + +class TestMPDClient(unittest.TestCase): + + longMessage = True + # last sync: musicpd 0.4.0 + commands = { + # Status Commands + "clearerror": "nothing", + "currentsong": "object", + "idle": "list", + "noidle": None, + "status": "object", + "stats": "object", + # Playback Option Commands + "consume": "nothing", + "crossfade": "nothing", + "mixrampdb": "nothing", + "mixrampdelay": "nothing", + "random": "nothing", + "repeat": "nothing", + "setvol": "nothing", + "single": "nothing", + "replay_gain_mode": "nothing", + "replay_gain_status": "item", + "volume": "nothing", + # Playback Control Commands + "next": "nothing", + "pause": "nothing", + "play": "nothing", + "playid": "nothing", + "previous": "nothing", + "seek": "nothing", + "seekid": "nothing", + "seekcur": "nothing", + "stop": "nothing", + # Playlist Commands + "add": "nothing", + "addid": "item", + "clear": "nothing", + "delete": "nothing", + "deleteid": "nothing", + "move": "nothing", + "moveid": "nothing", + "playlist": "playlist", + "playlistfind": "songs", + "playlistid": "songs", + "playlistinfo": "songs", + "playlistsearch": "songs", + "plchanges": "songs", + "plchangesposid": "changes", + "shuffle": "nothing", + "swap": "nothing", + "swapid": "nothing", + # Stored Playlist Commands + "listplaylist": "list", + "listplaylistinfo": "songs", + "listplaylists": "playlists", + "load": "nothing", + "playlistadd": "nothing", + "playlistclear": "nothing", + "playlistdelete": "nothing", + "playlistmove": "nothing", + "rename": "nothing", + "rm": "nothing", + "save": "nothing", + # Database Commands + "count": "object", + "find": "songs", + "findadd": "nothing", + "list": "list", + "listall": "database", + "listallinfo": "database", + "lsinfo": "database", + "search": "songs", + "searchadd": "nothing", + "searchaddpl": "nothing", + "update": "item", + "rescan": "item", + "readcomments": "object", + # Sticker Commands + "sticker get": "item", + "sticker set": "nothing", + "sticker delete": "nothing", + "sticker list": "list", + "sticker find": "songs", + # Connection Commands + "close": None, + "kill": None, + "password": "nothing", + "ping": "nothing", + # Audio Output Commands + "disableoutput": "nothing", + "enableoutput": "nothing", + "toggleoutput": "nothing", + "outputs": "outputs", + # Reflection Commands + "commands": "list", + "notcommands": "list", + "tagtypes": "list", + "urlhandlers": "list", + "decoders": "plugins", + # Client to Client + "subscribe": "nothing", + "unsubscribe": "nothing", + "channels": "list", + "readmessages": "messages", + "sendmessage": "nothing", + } + + def setUp(self): + self.socket_patch = mock.patch("musicpd.socket") + self.socket_mock = self.socket_patch.start() + self.socket_mock.getaddrinfo.return_value = [range(5)] + + self.socket_mock.socket.side_effect = ( + lambda *a, **kw: + # Create a new socket.socket() mock with default attributes, + # each time we are calling it back (otherwise, it keeps set + # attributes across calls). + # That's probably what we want, since reconnecting is like + # reinitializing the entire connection, and so, the mock. + mock.MagicMock(name="socket.socket")) + + self.client = musicpd.MPDClient() + self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT) + self.client._sock.reset_mock() + self.MPDWillReturn("ACK don't forget to setup your mock\n") + + def tearDown(self): + self.socket_patch.stop() + + def MPDWillReturn(self, *lines): + # Return what the caller wants first, then do as if the socket was + # disconnected. + self.client._rfile.readline.side_effect = itertools.chain( + lines, itertools.repeat('')) + + def assertMPDReceived(self, *lines): + self.client._wfile.write.assert_called_with(*lines) + + def test_metaclass_commands(self): + for cmd, ret in TestMPDClient.commands.items(): + self.assertTrue(hasattr(self.client, cmd), msg='fails for{}'.format(cmd)) + if ' ' in cmd: + self.assertTrue(hasattr(self.client, cmd.replace(' ', '_'))) + + def test_fetch_nothing(self): + self.MPDWillReturn('OK\n') + self.assertIsNone(self.client.ping()) + self.assertMPDReceived('ping\n') + + def test_fetch_list(self): + self.MPDWillReturn('OK\n') + self.assertIsInstance(self.client.list('album'), list) + self.assertMPDReceived('list "album"\n') + + def test_fetch_item(self): + self.MPDWillReturn('updating_db: 42\n', 'OK\n') + self.assertIsNotNone(self.client.update()) + + def test_fetch_object(self): + # XXX: _read_objects() doesn't wait for the final OK + self.MPDWillReturn('volume: 63\n', 'OK\n') + status = self.client.status() + self.assertMPDReceived('status\n') + self.assertIsInstance(status, dict) + + # XXX: _read_objects() doesn't wait for the final OK + self.MPDWillReturn('OK\n') + stats = self.client.stats() + self.assertMPDReceived('stats\n') + self.assertIsInstance(stats, dict) + + def test_fetch_songs(self): + self.MPDWillReturn("file: my-song.ogg\n", "Pos: 0\n", "Id: 66\n", "OK\n") + playlist = self.client.playlistinfo() + + self.assertMPDReceived('playlistinfo\n') + self.assertIsInstance(playlist, list) + self.assertEqual(1, len(playlist)) + e = playlist[0] + self.assertIsInstance(e, dict) + self.assertEqual('my-song.ogg', e['file']) + self.assertEqual('0', e['pos']) + self.assertEqual('66', e['id']) + + def test_send_and_fetch(self): + self.MPDWillReturn("volume: 50\n", "OK\n") + result = self.client.send_status() + self.assertEqual(None, result) + self.assertMPDReceived('status\n') + + status = self.client.fetch_status() + self.assertEqual(1, self.client._wfile.write.call_count) + self.assertEqual({'volume': '50'}, status) + + def test_iterating(self): + self.MPDWillReturn("file: my-song.ogg\n", "Pos: 0\n", "Id: 66\n", "OK\n") + self.client.iterate = True + playlist = self.client.playlistinfo() + self.assertMPDReceived('playlistinfo\n') + self.assertIsInstance(playlist, types.GeneratorType) + for song in playlist: + self.assertIsInstance(song, dict) + self.assertEqual('my-song.ogg', song['file']) + self.assertEqual('0', song['pos']) + self.assertEqual('66', song['id']) + + def test_noidle(self): + self.MPDWillReturn('OK\n') # nothing changed after idle-ing + self.client.send_idle() + self.MPDWillReturn('OK\n') # nothing changed after noidle + self.assertEqual(self.client.noidle(), []) + self.assertMPDReceived('noidle\n') + self.MPDWillReturn("volume: 50\n", "OK\n") + self.client.status() + self.assertMPDReceived('status\n') + + def test_noidle_while_idle_started_sending(self): + self.MPDWillReturn('OK\n') # nothing changed after idle + self.client.send_idle() + self.MPDWillReturn('CHANGED: player\n', 'OK\n') # noidle response + self.assertEqual(self.client.noidle(), ['player']) + self.MPDWillReturn("volume: 50\n", "OK\n") + status = self.client.status() + self.assertMPDReceived('status\n') + self.assertEqual({'volume': '50'}, status) + + def test_throw_when_calling_noidle_withoutidling(self): + self.assertRaises(musicpd.CommandError, self.client.noidle) + self.client.send_status() + self.assertRaises(musicpd.CommandError, self.client.noidle) + + def test_client_to_client(self): + # client to client is at this time in beta! + + self.MPDWillReturn('OK\n') + self.assertIsNone(self.client.subscribe("monty")) + self.assertMPDReceived('subscribe "monty"\n') + + self.MPDWillReturn('channel: monty\n', 'OK\n') + channels = self.client.channels() + self.assertMPDReceived('channels\n') + self.assertEqual(["monty"], channels) + + self.MPDWillReturn('OK\n') + self.assertIsNone(self.client.sendmessage("monty", "SPAM")) + self.assertMPDReceived('sendmessage "monty" "SPAM"\n') + + self.MPDWillReturn('channel: monty\n', 'message: SPAM\n', 'OK\n') + msg = self.client.readmessages() + self.assertMPDReceived('readmessages\n') + self.assertEqual(msg, [{"channel":"monty", "message": "SPAM"}]) + + self.MPDWillReturn('OK\n') + self.assertIsNone(self.client.unsubscribe("monty")) + self.assertMPDReceived('unsubscribe "monty"\n') + + self.MPDWillReturn('OK\n') + channels = self.client.channels() + self.assertMPDReceived('channels\n') + self.assertEqual([], channels) + + def test_ranges_in_command_args(self): + self.MPDWillReturn("OK\n") + self.client.playlistinfo((10,)) + self.assertMPDReceived('playlistinfo 10:\n') + + self.MPDWillReturn("OK\n") + self.client.playlistinfo(("10",)) + self.assertMPDReceived('playlistinfo 10:\n') + + self.MPDWillReturn("OK\n") + self.client.playlistinfo((10, 12)) + self.assertMPDReceived('playlistinfo 10:12\n') + + for arg in [(10, "t"), (10, 1, 1)]: + self.MPDWillReturn("OK\n") + with self.assertRaises(musicpd.CommandError): + self.client.playlistinfo(arg) + + def test_numbers_as_command_args(self): + self.MPDWillReturn("OK\n") + self.client.find("file", 1) + self.assertMPDReceived('find "file" "1"\n') + + def test_commands_without_callbacks(self): + self.MPDWillReturn("\n") + self.client.close() + self.assertMPDReceived('close\n') + + # XXX: what are we testing here? + self.client._reset() + self.client.connect(TEST_MPD_HOST, TEST_MPD_PORT) + + def test_connection_lost(self): + # Simulate a connection lost: the socket returns empty strings + self.MPDWillReturn('') + + with self.assertRaises(musicpd.ConnectionError): + self.client.status() + + # consistent behaviour + # solves (also present + # in python-mpd) + with self.assertRaises(musicpd.ConnectionError): + self.client.status() + + self.assertIs(self.client._sock, None) + + def test_parse_sticker_get_no_sticker(self): + self.MPDWillReturn("ACK [50@0] {sticker} no such sticker\n") + self.assertRaises(musicpd.CommandError, + self.client.sticker_get, 'song', 'baz', 'foo') + + def test_parse_sticker_list(self): + self.MPDWillReturn("sticker: foo=bar\n", "sticker: lom=bok\n", "OK\n") + res = self.client.sticker_list('song', 'baz') + self.assertEqual(['foo=bar', 'lom=bok'], res) + + # Even with only one sticker, we get a dict + self.MPDWillReturn("sticker: foo=bar\n", "OK\n") + res = self.client.sticker_list('song', 'baz') + self.assertEqual(['foo=bar'], res) + + +if __name__ == '__main__': + unittest.main() -- 2.39.2