Merge branch 'develop' into feature/support-telnet

Conflicts:
	tests/utils/network/lineprotocol_test.py
This commit is contained in:
Thomas Adamcik 2011-07-25 17:36:33 +02:00
commit 035af38b02
19 changed files with 688 additions and 101 deletions

View File

@ -25,6 +25,9 @@ v0.6.0 (in development)
- The local client now tries to lookup where your music is via XDG, it will
fall-back to ``~/music`` or use whatever setting you set manually.
- The idle command is now supported by mopidy for the following subsystems:
player, playlist, options and mixer (Fixes: :issue:`32`).
**Changes**
- Replace :attr:`mopidy.backends.base.Backend.uri_handlers` with

View File

@ -2,6 +2,7 @@ from copy import copy
import logging
import random
from mopidy.listeners import BackendListener
from mopidy.models import CpTrack
logger = logging.getLogger('mopidy.backends.base')
@ -16,6 +17,7 @@ class CurrentPlaylistController(object):
def __init__(self, backend):
self.backend = backend
self.cp_id = 0
self._cp_tracks = []
self._version = 0
@ -53,8 +55,9 @@ class CurrentPlaylistController(object):
def version(self, version):
self._version = version
self.backend.playback.on_current_playlist_change()
self._trigger_playlist_changed()
def add(self, track, at_position=None):
def add(self, track, at_position=None, increase_version=True):
"""
Add the track to the end of, or at the given position in the current
playlist.
@ -68,12 +71,14 @@ class CurrentPlaylistController(object):
"""
assert at_position <= len(self._cp_tracks), \
u'at_position can not be greater than playlist length'
cp_track = CpTrack(self.version, track)
cp_track = CpTrack(self.cp_id, track)
if at_position is not None:
self._cp_tracks.insert(at_position, cp_track)
else:
self._cp_tracks.append(cp_track)
self.version += 1
if increase_version:
self.version += 1
self.cp_id += 1
return cp_track
def append(self, tracks):
@ -84,7 +89,10 @@ class CurrentPlaylistController(object):
:type tracks: list of :class:`mopidy.models.Track`
"""
for track in tracks:
self.add(track)
self.add(track, increase_version=False)
if tracks:
self.version += 1
def clear(self):
"""Clear the current playlist."""
@ -199,3 +207,7 @@ class CurrentPlaylistController(object):
random.shuffle(shuffled)
self._cp_tracks = before + shuffled + after
self.version += 1
def _trigger_playlist_changed(self):
logger.debug(u'Triggering playlist changed event')
BackendListener.send('playlist_changed')

View File

@ -8,6 +8,17 @@ from mopidy.listeners import BackendListener
logger = logging.getLogger('mopidy.backends.base')
def option_wrapper(name, default):
def get_option(self):
return getattr(self, name, default)
def set_option(self, value):
if getattr(self, name, default) != value:
self._trigger_options_changed()
return setattr(self, name, value)
return property(get_option, set_option)
class PlaybackController(object):
"""
:param backend: the backend
@ -34,7 +45,7 @@ class PlaybackController(object):
#: Tracks are removed from the playlist when they have been played.
#: :class:`False`
#: Tracks are not removed from the playlist.
consume = False
consume = option_wrapper('_consume', False)
#: The currently playing or selected track.
#:
@ -46,21 +57,21 @@ class PlaybackController(object):
#: Tracks are selected at random from the playlist.
#: :class:`False`
#: Tracks are played in the order of the playlist.
random = False
random = option_wrapper('_random', False)
#: :class:`True`
#: The current playlist is played repeatedly. To repeat a single track,
#: select both :attr:`repeat` and :attr:`single`.
#: :class:`False`
#: The current playlist is played once.
repeat = False
repeat = option_wrapper('_repeat', False)
#: :class:`True`
#: Playback is stopped after current song, unless in :attr:`repeat`
#: mode.
#: :class:`False`
#: Playback continues after current song.
single = False
single = option_wrapper('_single', False)
def __init__(self, backend, provider):
self.backend = backend
@ -276,6 +287,9 @@ class PlaybackController(object):
def state(self, new_state):
(old_state, self._state) = (self.state, new_state)
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
self._trigger_playback_state_changed()
# FIXME play_time stuff assumes backend does not have a better way of
# handeling this stuff :/
if (old_state in (self.PLAYING, self.STOPPED)
@ -326,7 +340,7 @@ class PlaybackController(object):
original_cp_track = self.current_cp_track
if self.cp_track_at_eot:
self._trigger_stopped_playing_event()
self._trigger_track_playback_ended()
self.play(self.cp_track_at_eot)
else:
self.stop(clear_current_track=True)
@ -354,7 +368,7 @@ class PlaybackController(object):
return
if self.cp_track_at_next:
self._trigger_stopped_playing_event()
self._trigger_track_playback_ended()
self.play(self.cp_track_at_next)
else:
self.stop(clear_current_track=True)
@ -387,7 +401,6 @@ class PlaybackController(object):
self.resume()
if cp_track is not None:
self.state = self.STOPPED
self.current_cp_track = cp_track
self.state = self.PLAYING
if not self.provider.play(cp_track.track):
@ -402,7 +415,7 @@ class PlaybackController(object):
if self.random and self.current_cp_track in self._shuffled:
self._shuffled.remove(self.current_cp_track)
self._trigger_started_playing_event()
self._trigger_track_playback_started()
def previous(self):
"""Play the previous track."""
@ -410,7 +423,7 @@ class PlaybackController(object):
return
if self.state == self.STOPPED:
return
self._trigger_stopped_playing_event()
self._trigger_track_playback_ended()
self.play(self.cp_track_at_previous, on_error_step=-1)
def resume(self):
@ -454,37 +467,34 @@ class PlaybackController(object):
:type clear_current_track: boolean
"""
if self.state != self.STOPPED:
self._trigger_stopped_playing_event()
if self.provider.stop():
self._trigger_track_playback_ended()
self.state = self.STOPPED
if clear_current_track:
self.current_cp_track = None
def _trigger_started_playing_event(self):
logger.debug(u'Triggering started playing event')
def _trigger_track_playback_started(self):
logger.debug(u'Triggering track playback started event')
if self.current_track is None:
return
ActorRegistry.broadcast({
'command': 'pykka_call',
'attr_path': ('started_playing',),
'args': [],
'kwargs': {'track': self.current_track},
}, target_class=BackendListener)
BackendListener.send('track_playback_started',
track=self.current_track)
def _trigger_stopped_playing_event(self):
# TODO Test that this is called on next/prev/end-of-track
logger.debug(u'Triggering stopped playing event')
def _trigger_track_playback_ended(self):
logger.debug(u'Triggering track playback ended event')
if self.current_track is None:
return
ActorRegistry.broadcast({
'command': 'pykka_call',
'attr_path': ('stopped_playing',),
'args': [],
'kwargs': {
'track': self.current_track,
'time_position': self.time_position,
},
}, target_class=BackendListener)
BackendListener.send('track_playback_ended',
track=self.current_track,
time_position=self.time_position)
def _trigger_playback_state_changed(self):
logger.debug(u'Triggering playback state change event')
BackendListener.send('playback_state_changed')
def _trigger_options_changed(self):
logger.debug(u'Triggering options changed event')
BackendListener.send('options_changed')
class BasePlaybackProvider(object):

View File

@ -57,7 +57,7 @@ class LastfmFrontend(ThreadingActor, BackendListener):
logger.error(u'Error during Last.fm setup: %s', e)
self.stop()
def started_playing(self, track):
def track_playback_started(self, track):
artists = ', '.join([a.name for a in track.artists])
duration = track.length and track.length // 1000 or 0
self.last_start_time = int(time.time())
@ -74,7 +74,7 @@ class LastfmFrontend(ThreadingActor, BackendListener):
pylast.MalformedResponseError, pylast.WSError) as e:
logger.warning(u'Error submitting playing track to Last.fm: %s', e)
def stopped_playing(self, track, time_position):
def track_playback_ended(self, track, time_position):
artists = ', '.join([a.name for a in track.artists])
duration = track.length and track.length // 1000 or 0
time_position = time_position // 1000

View File

@ -1,15 +1,15 @@
import logging
import sys
from pykka.actor import ThreadingActor
from pykka import registry, actor
from mopidy import settings
from mopidy import listeners, settings
from mopidy.frontends.mpd import dispatcher, protocol
from mopidy.utils import network, process, log
logger = logging.getLogger('mopidy.frontends.mpd')
class MpdFrontend(ThreadingActor):
class MpdFrontend(actor.ThreadingActor, listeners.BackendListener):
"""
The MPD frontend.
@ -39,6 +39,28 @@ class MpdFrontend(ThreadingActor):
def on_stop(self):
process.stop_actors_by_class(MpdSession)
def send_idle(self, subsystem):
# FIXME this should be updated once pykka supports non-blocking calls
# on proxies or some similar solution
registry.ActorRegistry.broadcast({
'command': 'pykka_call',
'attr_path': ('on_idle',),
'args': [subsystem],
'kwargs': {},
}, target_class=MpdSession)
def playback_state_changed(self):
self.send_idle('player')
def playlist_changed(self):
self.send_idle('playlist')
def options_changed(self):
self.send_idle('options')
def volume_changed(self):
self.send_idle('mixer')
class MpdSession(network.LineProtocol):
"""
@ -71,5 +93,8 @@ class MpdSession(network.LineProtocol):
self.send_lines(response)
def on_idle(self, subsystem):
self.dispatcher.handle_idle(subsystem)
def close(self):
self.stop()

View File

@ -27,6 +27,8 @@ class MpdDispatcher(object):
back to the MPD session.
"""
_noidle = re.compile(r'^noidle$')
def __init__(self, session=None):
self.authenticated = False
self.command_list = False
@ -42,11 +44,28 @@ class MpdDispatcher(object):
self._catch_mpd_ack_errors_filter,
self._authenticate_filter,
self._command_list_filter,
self._idle_filter,
self._add_ok_filter,
self._call_handler_filter,
]
return self._call_next_filter(request, response, filter_chain)
def handle_idle(self, subsystem):
self.context.events.add(subsystem)
subsystems = self.context.subscriptions.intersection(
self.context.events)
if not subsystems:
return
response = []
for subsystem in subsystems:
response.append(u'changed: %s' % subsystem)
response.append(u'OK')
self.context.subscriptions = set()
self.context.events = set()
self.context.session.send_lines(response)
def _call_next_filter(self, request, response, filter_chain):
if filter_chain:
next_filter = filter_chain.pop(0)
@ -108,6 +127,29 @@ class MpdDispatcher(object):
and request != u'command_list_end')
### Filter: idle
def _idle_filter(self, request, response, filter_chain):
if self._is_currently_idle() and not self._noidle.match(request):
logger.debug(u'Client sent us %s, only %s is allowed while in '
'the idle state', repr(request), repr(u'noidle'))
self.context.session.close()
return []
if not self._is_currently_idle() and self._noidle.match(request):
return [] # noidle was called before idle
response = self._call_next_filter(request, response, filter_chain)
if self._is_currently_idle():
return []
else:
return response
def _is_currently_idle(self):
return bool(self.context.subscriptions)
### Filter: add OK
def _add_ok_filter(self, request, response, filter_chain):
@ -119,7 +161,6 @@ class MpdDispatcher(object):
def _has_error(self, response):
return response and response[-1].startswith(u'ACK')
### Filter: call handler
def _call_handler_filter(self, request, response, filter_chain):
@ -181,9 +222,17 @@ class MpdContext(object):
#: The current :class:`mopidy.frontends.mpd.MpdSession`.
session = None
#: The active subsystems that have pending events.
events = None
#: The subsytems that we want to be notified about in idle mode.
subscriptions = None
def __init__(self, dispatcher, session=None):
self.dispatcher = dispatcher
self.session = session
self.events = set()
self.subscriptions = set()
self._backend = None
self._mixer = None

View File

@ -1,6 +1,6 @@
from mopidy.frontends.mpd.protocol import handle_request
@handle_request(r'^\s*$')
@handle_request(r'^[ ]*$')
def empty(context):
"""The original MPD server returns ``OK`` on an empty request."""
pass

View File

@ -11,28 +11,16 @@ def commands(context):
Shows which commands the current user has access to.
"""
if context.dispatcher.authenticated:
command_names = [command.name for command in mpd_commands]
command_names = set([command.name for command in mpd_commands])
else:
command_names = [command.name for command in mpd_commands
if not command.auth_required]
command_names = set([command.name for command in mpd_commands
if not command.auth_required])
# No permission to use
if 'kill' in command_names:
command_names.remove('kill')
# Not shown by MPD in its command list
if 'command_list_begin' in command_names:
command_names.remove('command_list_begin')
if 'command_list_ok_begin' in command_names:
command_names.remove('command_list_ok_begin')
if 'command_list_end' in command_names:
command_names.remove('command_list_end')
if 'idle' in command_names:
command_names.remove('idle')
if 'noidle' in command_names:
command_names.remove('noidle')
if 'sticker' in command_names:
command_names.remove('sticker')
# No one is permited to use kill, rest of commands are not listed by MPD,
# so we shouldn't either.
command_names = command_names - set(['kill', 'command_list_begin',
'command_list_ok_begin', 'command_list_ok_begin', 'command_list_end',
'idle', 'noidle', 'sticker'])
return [('command', command_name) for command_name in sorted(command_names)]

View File

@ -4,6 +4,10 @@ from mopidy.backends.base import PlaybackController
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
#: Subsystems that can be registered with idle command.
SUBSYSTEMS = ['database', 'mixer', 'options', 'output',
'player', 'playlist', 'stored_playlist', 'update', ]
@handle_request(r'^clearerror$')
def clearerror(context):
"""
@ -67,12 +71,36 @@ def idle(context, subsystems=None):
notifications when something changed in one of the specified
subsystems.
"""
pass # TODO
if subsystems:
subsystems = subsystems.split()
else:
subsystems = SUBSYSTEMS
for subsystem in subsystems:
context.subscriptions.add(subsystem)
active = context.subscriptions.intersection(context.events)
if not active:
context.session.prevent_timeout = True
return
response = []
context.events = set()
context.subscriptions = set()
for subsystem in active:
response.append(u'changed: %s' % subsystem)
return response
@handle_request(r'^noidle$')
def noidle(context):
"""See :meth:`_status_idle`."""
pass # TODO
if not context.subscriptions:
return
context.subscriptions = set()
context.events = set()
context.session.prevent_timeout = False
@handle_request(r'^stats$')
def stats(context):

View File

@ -1,3 +1,5 @@
from pykka import registry
class BackendListener(object):
"""
Marker interface for recipients of events sent by the backend.
@ -9,7 +11,19 @@ class BackendListener(object):
interested in all events.
"""
def started_playing(self, track):
@staticmethod
def send(event, **kwargs):
"""Helper to allow calling of backend listener events"""
# FIXME this should be updated once pykka supports non-blocking calls
# on proxies or some similar solution
registry.ActorRegistry.broadcast({
'command': 'pykka_call',
'attr_path': (event,),
'args': [],
'kwargs': kwargs,
}, target_class=BackendListener)
def track_playback_started(self, track):
"""
Called whenever a new track starts playing.
@ -20,9 +34,9 @@ class BackendListener(object):
"""
pass
def stopped_playing(self, track, time_position):
def track_playback_ended(self, track, time_position):
"""
Called whenever playback is stopped.
Called whenever playback of a track ends.
*MAY* be implemented by actor.
@ -32,3 +46,35 @@ class BackendListener(object):
:type time_position: int
"""
pass
def playback_state_changed(self):
"""
Called whenever playback state is changed.
*MAY* be implemented by actor.
"""
pass
def playlist_changed(self):
"""
Called whenever a playlist is changed.
*MAY* be implemented by actor.
"""
pass
def options_changed(self):
"""
Called whenever an option is changed.
*MAY* be implemented by actor.
"""
pass
def volume_changed(self):
"""
Called whenever the volume is changed.
*MAY* be implemented by actor.
"""
pass

View File

@ -1,4 +1,8 @@
from mopidy import settings
import logging
from mopidy import listeners, settings
logger = logging.getLogger('mopdy.mixers')
class BaseMixer(object):
"""
@ -30,6 +34,7 @@ class BaseMixer(object):
elif volume > 100:
volume = 100
self.set_volume(volume)
self._trigger_volume_changed()
def get_volume(self):
"""
@ -46,3 +51,7 @@ class BaseMixer(object):
*MUST be implemented by subclass.*
"""
raise NotImplementedError
def _trigger_volume_changed(self):
logger.debug(u'Triggering volume changed event')
listeners.BackendListener.send('volume_changed')

View File

@ -290,6 +290,7 @@ class LineProtocol(ThreadingActor):
def __init__(self, connection):
self.connection = connection
self.prevent_timeout = False
self.recv_buffer = ''
if self.delimeter:
@ -326,7 +327,8 @@ class LineProtocol(ThreadingActor):
if line is not None:
self.on_line_received(line)
self.connection.enable_timeout()
if not self.prevent_timeout:
self.connection.enable_timeout()
def on_stop(self):
"""Ensure that cleanup when actor stops."""

View File

@ -11,8 +11,8 @@ from mopidy.models import Track
class BackendEventsTest(unittest.TestCase):
def setUp(self):
self.events = {
'started_playing': threading.Event(),
'stopped_playing': threading.Event(),
'track_playback_started': threading.Event(),
'track_playback_ended': threading.Event(),
}
self.backend = DummyBackend.start().proxy()
self.listener = DummyBackendListener.start(self.events).proxy()
@ -20,26 +20,26 @@ class BackendEventsTest(unittest.TestCase):
def tearDown(self):
ActorRegistry.stop_all()
def test_play_sends_started_playing_event(self):
def test_play_sends_track_playback_started_event(self):
self.backend.current_playlist.add([Track(uri='a')])
self.backend.playback.play()
self.events['started_playing'].wait(timeout=1)
self.assertTrue(self.events['started_playing'].is_set())
self.events['track_playback_started'].wait(timeout=1)
self.assertTrue(self.events['track_playback_started'].is_set())
def test_stop_sends_stopped_playing_event(self):
def test_stop_sends_track_playback_ended_event(self):
self.backend.current_playlist.add([Track(uri='a')])
self.backend.playback.play()
self.backend.playback.stop()
self.events['stopped_playing'].wait(timeout=1)
self.assertTrue(self.events['stopped_playing'].is_set())
self.events['track_playback_ended'].wait(timeout=1)
self.assertTrue(self.events['track_playback_ended'].is_set())
class DummyBackendListener(ThreadingActor, BackendListener):
def __init__(self, events):
self.events = events
def started_playing(self, track):
self.events['started_playing'].set()
def track_playback_started(self, track):
self.events['track_playback_started'].set()
def stopped_playing(self, track, time_position):
self.events['stopped_playing'].set()
def track_playback_ended(self, track, time_position):
self.events['track_playback_ended'].set()

View File

@ -27,21 +27,31 @@ class BaseTestCase(unittest.TestCase):
self.connection = MockConnetion()
self.session = mpd.MpdSession(self.connection)
self.dispatcher = self.session.dispatcher
self.context = self.dispatcher.context
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
settings.runtime.clear()
def sendRequest(self, request, clear=False):
def sendRequest(self, request):
self.connection.response = []
self.session.on_line_received(request)
request = '%s\n' % request.encode('utf-8')
self.session.on_receive({'received': request})
return self.connection.response
def assertNoResponse(self):
self.assertEqual([], self.connection.response)
def assertInResponse(self, value):
self.assert_(value in self.connection.response, u'Did not find %s '
'in %s' % (repr(value), repr(self.connection.response)))
def assertOnceInResponse(self, value):
matched = len([r for r in self.connection.response if r == value])
self.assertEqual(1, matched, 'Expected to find %s once in %s' %
(repr(value), repr(self.connection.response)))
def assertNotInResponse(self, value):
self.assert_(value not in self.connection.response, u'Found %s in %s' %
(repr(value), repr(self.connection.response)))

View File

@ -0,0 +1,206 @@
from mock import patch
from mopidy.frontends.mpd.protocol.status import SUBSYSTEMS
from mopidy.models import Track
from tests.frontends.mpd import protocol
class IdleHandlerTest(protocol.BaseTestCase):
def idleEvent(self, subsystem):
self.session.on_idle(subsystem)
def assertEqualEvents(self, events):
self.assertEqual(set(events), self.context.events)
def assertEqualSubscriptions(self, events):
self.assertEqual(set(events), self.context.subscriptions)
def assertNoEvents(self):
self.assertEqualEvents([])
def assertNoSubscriptions(self):
self.assertEqualSubscriptions([])
def test_base_state(self):
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertNoResponse()
def test_idle(self):
self.sendRequest(u'idle')
self.assertEqualSubscriptions(SUBSYSTEMS)
self.assertNoEvents()
self.assertNoResponse()
def test_idle_disables_timeout(self):
self.sendRequest(u'idle')
self.connection.disable_timeout.assert_called_once_with()
def test_noidle(self):
self.sendRequest(u'noidle')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertNoResponse()
def test_idle_player(self):
self.sendRequest(u'idle player')
self.assertEqualSubscriptions(['player'])
self.assertNoEvents()
self.assertNoResponse()
def test_idle_player_playlist(self):
self.sendRequest(u'idle player playlist')
self.assertEqualSubscriptions(['player', 'playlist'])
self.assertNoEvents()
self.assertNoResponse()
def test_idle_then_noidle(self):
self.sendRequest(u'idle')
self.sendRequest(u'noidle')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse(u'OK')
def test_idle_then_noidle_enables_timeout(self):
self.sendRequest(u'idle')
self.sendRequest(u'noidle')
self.connection.enable_timeout.assert_called_once_with()
def test_idle_then_play(self):
with patch.object(self.session, 'stop') as stop_mock:
self.sendRequest(u'idle')
self.sendRequest(u'play')
stop_mock.assert_called_once_with()
def test_idle_then_idle(self):
with patch.object(self.session, 'stop') as stop_mock:
self.sendRequest(u'idle')
self.sendRequest(u'idle')
stop_mock.assert_called_once_with()
def test_idle_player_then_play(self):
with patch.object(self.session, 'stop') as stop_mock:
self.sendRequest(u'idle player')
self.sendRequest(u'play')
stop_mock.assert_called_once_with()
def test_idle_then_player(self):
self.sendRequest(u'idle')
self.idleEvent(u'player')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse(u'changed: player')
self.assertOnceInResponse(u'OK')
def test_idle_player_then_event_player(self):
self.sendRequest(u'idle player')
self.idleEvent(u'player')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse(u'changed: player')
self.assertOnceInResponse(u'OK')
def test_idle_player_then_noidle(self):
self.sendRequest(u'idle player')
self.sendRequest(u'noidle')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse(u'OK')
def test_idle_player_playlist_then_noidle(self):
self.sendRequest(u'idle player playlist')
self.sendRequest(u'noidle')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'OK')
def test_idle_player_playlist_then_player(self):
self.sendRequest(u'idle player playlist')
self.idleEvent(u'player')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'changed: player')
self.assertNotInResponse(u'changed: playlist')
self.assertOnceInResponse(u'OK')
def test_idle_playlist_then_player(self):
self.sendRequest(u'idle playlist')
self.idleEvent(u'player')
self.assertEqualEvents(['player'])
self.assertEqualSubscriptions(['playlist'])
self.assertNoResponse()
def test_idle_playlist_then_player_then_playlist(self):
self.sendRequest(u'idle playlist')
self.idleEvent(u'player')
self.idleEvent(u'playlist')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertNotInResponse(u'changed: player')
self.assertOnceInResponse(u'changed: playlist')
self.assertOnceInResponse(u'OK')
def test_player(self):
self.idleEvent(u'player')
self.assertEqualEvents(['player'])
self.assertNoSubscriptions()
self.assertNoResponse()
def test_player_then_idle_player(self):
self.idleEvent(u'player')
self.sendRequest(u'idle player')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'changed: player')
self.assertNotInResponse(u'changed: playlist')
self.assertOnceInResponse(u'OK')
def test_player_then_playlist(self):
self.idleEvent(u'player')
self.idleEvent(u'playlist')
self.assertEqualEvents(['player', 'playlist'])
self.assertNoSubscriptions()
self.assertNoResponse()
def test_player_then_idle(self):
self.idleEvent(u'player')
self.sendRequest(u'idle')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'changed: player')
self.assertOnceInResponse(u'OK')
def test_player_then_playlist_then_idle(self):
self.idleEvent(u'player')
self.idleEvent(u'playlist')
self.sendRequest(u'idle')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'changed: player')
self.assertOnceInResponse(u'changed: playlist')
self.assertOnceInResponse(u'OK')
def test_player_then_idle_playlist(self):
self.idleEvent(u'player')
self.sendRequest(u'idle playlist')
self.assertEqualEvents(['player'])
self.assertEqualSubscriptions(['playlist'])
self.assertNoResponse()
def test_player_then_idle_playlist_then_noidle(self):
self.idleEvent(u'player')
self.sendRequest(u'idle playlist')
self.sendRequest(u'noidle')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'OK')
def test_player_then_playlist_then_idle_playlist(self):
self.idleEvent(u'player')
self.idleEvent(u'playlist')
self.sendRequest(u'idle playlist')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertNotInResponse(u'changed: player')
self.assertOnceInResponse(u'changed: playlist')
self.assertOnceInResponse(u'OK')

View File

@ -27,21 +27,6 @@ class StatusHandlerTest(protocol.BaseTestCase):
self.sendRequest(u'currentsong')
self.assertInResponse(u'OK')
def test_idle_without_subsystems(self):
# FIXME this is not the correct behaviour for idle...
self.sendRequest(u'idle')
self.assertInResponse(u'OK')
def test_idle_with_subsystems(self):
# FIXME this is not the correct behaviour for idle...
self.sendRequest(u'idle database playlist')
self.assertInResponse(u'OK')
def test_noidle(self):
# FIXME this is not the correct behaviour for idle...
self.sendRequest(u'noidle')
self.assertInResponse(u'OK')
def test_stats_command(self):
self.sendRequest(u'stats')
self.assertInResponse(u'OK')

View File

@ -7,8 +7,8 @@ class BackendListenerTest(unittest.TestCase):
def setUp(self):
self.listener = BackendListener()
def test_listener_has_default_impl_for_the_started_playing_event(self):
self.listener.started_playing(Track())
def test_listener_has_default_impl_for_the_track_playback_started(self):
self.listener.track_playback_started(Track())
def test_listener_has_default_impl_for_the_stopped_playing_event(self):
self.listener.stopped_playing(Track(), 0)
def test_listener_has_default_impl_for_the_track_playback_ended(self):
self.listener.track_playback_ended(Track(), 0)

View File

@ -10,9 +10,11 @@ from mock import sentinel, Mock
class LineProtocolTest(unittest.TestCase):
def setUp(self):
self.mock = Mock(spec=network.LineProtocol)
self.mock.terminator = network.LineProtocol.terminator
self.mock.encoding = network.LineProtocol.encoding
self.mock.delimeter = network.LineProtocol.delimeter
self.mock.prevent_timeout = False
def test_init_stores_values_in_attributes(self):
delimeter = re.compile(network.LineProtocol.terminator)
@ -20,6 +22,7 @@ class LineProtocolTest(unittest.TestCase):
self.assertEqual(sentinel.connection, self.mock.connection)
self.assertEqual('', self.mock.recv_buffer)
self.assertEqual(delimeter, self.mock.delimeter)
self.assertFalse(self.mock.prevent_timeout)
def test_init_compiles_delimeter(self):
self.mock.delimeter = '\r?\n'
@ -47,6 +50,16 @@ class LineProtocolTest(unittest.TestCase):
self.mock.connection.disable_timeout.assert_called_once_with()
self.mock.connection.enable_timeout.assert_called_once_with()
def test_on_receive_toggles_unless_prevent_timeout_is_set(self):
self.mock.connection = Mock(spec=network.Connection)
self.mock.recv_buffer = ''
self.mock.parse_lines.return_value = []
self.mock.prevent_timeout = True
network.LineProtocol.on_receive(self.mock, {'received': 'data'})
self.mock.connection.disable_timeout.assert_called_once_with()
self.assertEqual(0, self.mock.connection.enable_timeout.call_count)
def test_on_receive_no_new_lines_calls_parse_lines(self):
self.mock.connection = Mock(spec=network.Connection)
self.mock.recv_buffer = ''

201
tools/idle.py Normal file
View File

@ -0,0 +1,201 @@
#! /usr/bin/env python
# This script is helper to systematicly test the behaviour of MPD's idle
# command. It is simply provided as a quick hack, expect nothing more.
import logging
import pprint
import socket
host = ''
port = 6601
url = "13 - a-ha - White Canvas.mp3"
artist = "a-ha"
data = {'id': None, 'id2': None, 'url': url, 'artist': artist}
# Commands to run before test requests to coerce MPD into right state
setup_requests = [
'clear',
'add "%(url)s"',
'add "%(url)s"',
'add "%(url)s"',
'play',
# 'pause', # Uncomment to test paused idle behaviour
# 'stop', # Uncomment to test stopped idle behaviour
]
# List of commands to test for idle behaviour. Ordering of list is important in
# order to keep MPD state as intended. Commands that are obviously
# informational only or "harmfull" have been excluded.
test_requests = [
'add "%(url)s"',
'addid "%(url)s" "1"',
'clear',
# 'clearerror',
# 'close',
# 'commands',
'consume "1"',
'consume "0"',
# 'count',
'crossfade "1"',
'crossfade "0"',
# 'currentsong',
# 'delete "1:2"',
'delete "0"',
'deleteid "%(id)s"',
'disableoutput "0"',
'enableoutput "0"',
# 'find',
# 'findadd "artist" "%(artist)s"',
# 'idle',
# 'kill',
# 'list',
# 'listall',
# 'listallinfo',
# 'listplaylist',
# 'listplaylistinfo',
# 'listplaylists',
# 'lsinfo',
'move "0:1" "2"',
'move "0" "1"',
'moveid "%(id)s" "1"',
'next',
# 'notcommands',
# 'outputs',
# 'password',
'pause',
# 'ping',
'play',
'playid "%(id)s"',
# 'playlist',
'playlistadd "foo" "%(url)s"',
'playlistclear "foo"',
'playlistadd "foo" "%(url)s"',
'playlistdelete "foo" "0"',
# 'playlistfind',
# 'playlistid',
# 'playlistinfo',
'playlistadd "foo" "%(url)s"',
'playlistadd "foo" "%(url)s"',
'playlistmove "foo" "0" "1"',
# 'playlistsearch',
# 'plchanges',
# 'plchangesposid',
'previous',
'random "1"',
'random "0"',
'rm "bar"',
'rename "foo" "bar"',
'repeat "0"',
'rm "bar"',
'save "bar"',
'load "bar"',
# 'search',
'seek "1" "10"',
'seekid "%(id)s" "10"',
# 'setvol "10"',
'shuffle',
'shuffle "0:1"',
'single "1"',
'single "0"',
# 'stats',
# 'status',
'stop',
'swap "1" "2"',
'swapid "%(id)s" "%(id2)s"',
# 'tagtypes',
# 'update',
# 'urlhandlers',
# 'volume',
]
def create_socketfile():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
sock.settimeout(0.5)
fd = sock.makefile('rw', 1) # 1 = line buffered
fd.readline() # Read banner
return fd
def wait(fd, prefix=None, collect=None):
while True:
line = fd.readline().rstrip()
if prefix:
logging.debug('%s: %s', prefix, repr(line))
if line.split()[0] in ('OK', 'ACK'):
break
def collect_ids(fd):
fd.write('playlistinfo\n')
ids = []
while True:
line = fd.readline()
if line.split()[0] == 'OK':
break
if line.split()[0] == 'Id:':
ids.append(line.split()[1])
return ids
def main():
subsystems = {}
command = create_socketfile()
for test in test_requests:
# Remove any old ids
del data['id']
del data['id2']
# Run setup code to force MPD into known state
for setup in setup_requests:
command.write(setup % data + '\n')
wait(command)
data['id'], data['id2'] = collect_ids(command)[:2]
# This connection needs to be make after setup commands are done or
# else they will cause idle events.
idle = create_socketfile()
# Wait for new idle events
idle.write('idle\n')
test = test % data
logging.debug('idle: %s', repr('idle'))
logging.debug('command: %s', repr(test))
command.write(test + '\n')
wait(command, prefix='command')
while True:
try:
line = idle.readline().rstrip()
except socket.timeout:
# Abort try if we time out.
idle.write('noidle\n')
break
logging.debug('idle: %s', repr(line))
if line == 'OK':
break
request_type = test.split()[0]
subsystem = line.split()[1]
subsystems.setdefault(request_type, set()).add(subsystem)
logging.debug('---')
pprint.pprint(subsystems)
if __name__ == '__main__':
main()