diff --git a/data/mopidy.desktop b/data/mopidy.desktop
index 70257d58..88dd5ae4 100644
--- a/data/mopidy.desktop
+++ b/data/mopidy.desktop
@@ -8,3 +8,4 @@ TryExec=mopidy
Exec=mopidy
Terminal=true
Categories=AudioVideo;Audio;Player;ConsoleOnly;
+StartupNotify=true
diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst
index 28112cf7..20dc2d61 100644
--- a/docs/api/backends/controllers.rst
+++ b/docs/api/backends/controllers.rst
@@ -15,7 +15,6 @@ The backend
.. autoclass:: mopidy.backends.base.Backend
:members:
- :undoc-members:
Playback controller
@@ -26,7 +25,6 @@ seek.
.. autoclass:: mopidy.backends.base.PlaybackController
:members:
- :undoc-members:
Mixer controller
@@ -42,7 +40,6 @@ Manages everything related to the currently loaded playlist.
.. autoclass:: mopidy.backends.base.CurrentPlaylistController
:members:
- :undoc-members:
Stored playlists controller
@@ -52,7 +49,6 @@ Manages stored playlist.
.. autoclass:: mopidy.backends.base.StoredPlaylistsController
:members:
- :undoc-members:
Library controller
@@ -62,4 +58,3 @@ Manages the music library, e.g. searching for tracks to be added to a playlist.
.. autoclass:: mopidy.backends.base.LibraryController
:members:
- :undoc-members:
diff --git a/docs/api/backends/providers.rst b/docs/api/backends/providers.rst
index 903e220b..61e5f68a 100644
--- a/docs/api/backends/providers.rst
+++ b/docs/api/backends/providers.rst
@@ -14,7 +14,6 @@ Playback provider
.. autoclass:: mopidy.backends.base.BasePlaybackProvider
:members:
- :undoc-members:
Stored playlists provider
@@ -22,7 +21,6 @@ Stored playlists provider
.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsProvider
:members:
- :undoc-members:
Library provider
@@ -30,7 +28,6 @@ Library provider
.. autoclass:: mopidy.backends.base.BaseLibraryProvider
:members:
- :undoc-members:
Backend provider implementations
diff --git a/docs/api/frontends.rst b/docs/api/frontends.rst
index 0c1e32a3..dc53cca2 100644
--- a/docs/api/frontends.rst
+++ b/docs/api/frontends.rst
@@ -2,25 +2,30 @@
Frontend API
************
-A frontend may do whatever it wants to, including creating threads, opening TCP
-ports and exposing Mopidy for a type of clients.
-
-Frontends got one main limitation: they are restricted to passing messages
-through the ``core_queue`` for all communication with the rest of Mopidy. Thus,
-the frontend API is very small and reveals little of what a frontend may do.
-
-.. warning::
-
- A stable frontend API is not available yet, as we've only implemented a
- couple of frontend modules.
-
-.. automodule:: mopidy.frontends.base
- :synopsis: Base class for frontends
- :members:
+The following requirements applies to any frontend implementation:
+- A frontend MAY do mostly whatever it wants to, including creating threads,
+ opening TCP ports and exposing Mopidy for a group of clients.
+- A frontend MUST implement at least one `Pykka
+ `_ actor, called the "main actor" from here
+ on.
+- It MAY use additional actors to implement whatever it does, and using actors
+ in frontend implementations is encouraged.
+- The frontend is activated by including its main actor in the
+ :attr:`mopidy.settings.FRONTENDS` setting.
+- The main actor MUST be able to start and stop the frontend when the main
+ actor is started and stopped.
+- The frontend MAY require additional settings to be set for it to
+ work.
+- Such settings MUST be documented.
+- The main actor MUST stop itself if the defined settings are not adequate for
+ the frontend to work properly.
+- Any actor which is part of the frontend MAY implement any listener interface
+ from :mod:`mopidy.listeners` to receive notification of the specified events.
Frontend implementations
========================
* :mod:`mopidy.frontends.lastfm`
* :mod:`mopidy.frontends.mpd`
+* :mod:`mopidy.frontends.mpris`
diff --git a/docs/api/listeners.rst b/docs/api/listeners.rst
new file mode 100644
index 00000000..609dc3c7
--- /dev/null
+++ b/docs/api/listeners.rst
@@ -0,0 +1,7 @@
+************
+Listener API
+************
+
+.. automodule:: mopidy.listeners
+ :synopsis: Listener API
+ :members:
diff --git a/docs/api/mixers.rst b/docs/api/mixers.rst
index 6daa7a4e..2459db8c 100644
--- a/docs/api/mixers.rst
+++ b/docs/api/mixers.rst
@@ -30,7 +30,6 @@ methods as described below.
.. automodule:: mopidy.mixers.base
:synopsis: Mixer API
:members:
- :undoc-members:
Mixer implementations
diff --git a/docs/api/models.rst b/docs/api/models.rst
index ef11547e..5833e58c 100644
--- a/docs/api/models.rst
+++ b/docs/api/models.rst
@@ -25,4 +25,3 @@ Data model API
.. automodule:: mopidy.models
:synopsis: Data model API
:members:
- :undoc-members:
diff --git a/docs/changes.rst b/docs/changes.rst
index 4ccf62c9..445e7984 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -5,6 +5,78 @@ Changes
This change log is used to track all major changes to Mopidy.
+v0.6.0 (2011-10-09)
+===================
+
+The development of Mopidy have been quite slow for the last couple of months,
+but we do have some goodies to release which have been idling in the
+develop branch since the warmer days of the summer. This release brings support
+for the MPD ``idle`` command, which makes it possible for a client wait for
+updates from the server instead of polling every second. Also, we've added
+support for the MPRIS standard, so that Mopidy can be controlled over D-Bus
+from e.g. the Ubuntu Sound Menu.
+
+Please note that 0.6.0 requires some updated dependencies, as listed under
+*Important changes* below.
+
+**Important changes**
+
+- Pykka 0.12.3 or greater is required.
+
+- pyspotify 1.4 or greater is required.
+
+- All config, data, and cache locations are now based on the XDG spec.
+
+ - This means that your settings file will need to be moved from
+ ``~/.mopidy/settings.py`` to ``~/.config/mopidy/settings.py``.
+ - Your Spotify cache will now be stored in ``~/.cache/mopidy`` instead of
+ ``~/.mopidy/spotify_cache``.
+ - The local backend's ``tag_cache`` should now be in
+ ``~/.local/share/mopidy/tag_cache``, likewise your playlists will be in
+ ``~/.local/share/mopidy/playlists``.
+ - 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 MPD command ``idle`` is now supported by Mopidy for the following
+ subsystems: player, playlist, options, and mixer. (Fixes: :issue:`32`)
+
+- A new frontend :mod:`mopidy.frontends.mpris` have been added. It exposes
+ Mopidy through the `MPRIS interface `_ over D-Bus. In
+ practice, this makes it possible to control Mopidy through the `Ubuntu Sound
+ Menu `_.
+
+**Changes**
+
+- Replace :attr:`mopidy.backends.base.Backend.uri_handlers` with
+ :attr:`mopidy.backends.base.Backend.uri_schemes`, which just takes the part
+ up to the colon of an URI, and not any prefix.
+
+- Add Listener API, :mod:`mopidy.listeners`, to be implemented by actors
+ wanting to receive events from the backend. This is a formalization of the
+ ad hoc events the Last.fm scrobbler has already been using for some time.
+
+- Replaced all of the MPD network code that was provided by asyncore with
+ custom stack. This change was made to facilitate support for the ``idle``
+ command, and to reduce the number of event loops being used.
+
+- Fix metadata update in Shoutcast streaming. (Fixes: :issue:`122`)
+
+- Unescape all incoming MPD requests. (Fixes: :issue:`113`)
+
+- Increase the maximum number of results returned by Spotify searches from 32
+ to 100.
+
+- Send Spotify search queries to pyspotify as unicode objects, as required by
+ pyspotify 1.4. (Fixes: :issue:`129`)
+
+- Add setting :attr:`mopidy.settings.MPD_SERVER_MAX_CONNECTIONS`. (Fixes:
+ :issue:`134`)
+
+- Remove `destroy()` methods from backend controller and provider APIs, as it
+ was not in use and actually not called by any code. Will reintroduce when
+ needed.
+
+
v0.5.0 (2011-06-15)
===================
@@ -87,6 +159,18 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
- Found and worked around strange WMA metadata behaviour.
+- Backend API:
+
+ - Calling on :meth:`mopidy.backends.base.playback.PlaybackController.next`
+ and :meth:`mopidy.backends.base.playback.PlaybackController.previous` no
+ longer implies that playback should be started. The playback state--whether
+ playing, paused or stopped--will now be kept.
+
+ - The method
+ :meth:`mopidy.backends.base.playback.PlaybackController.change_track`
+ has been added. Like ``next()``, and ``prev()``, it changes the current
+ track without changing the playback state.
+
v0.4.1 (2011-05-06)
===================
@@ -217,7 +301,7 @@ loading from Mopidy 0.3.0 is still present.
the debug log, to ease debugging of issues with attached debug logs.
-v0.3.1 (2010-01-22)
+v0.3.1 (2011-01-22)
===================
A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
@@ -231,7 +315,7 @@ A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
installed if the installation is executed as the root user.
-v0.3.0 (2010-01-22)
+v0.3.0 (2011-01-22)
===================
Mopidy 0.3.0 brings a bunch of small changes all over the place, but no large
diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst
index f5066210..4c789eba 100644
--- a/docs/clients/mpd.rst
+++ b/docs/clients/mpd.rst
@@ -20,9 +20,8 @@ A command line client. Version 0.14 had some issues with Mopidy (see
ncmpc
-----
-A console client. Uses the ``idle`` command heavily, which Mopidy doesn't
-support yet (see :issue:`32`). If you want a console client, use ncmpcpp
-instead.
+A console client. Works with Mopidy 0.6 and upwards. Uses the ``idle`` MPD
+command, but in a resource inefficient way.
ncmpcpp
@@ -48,15 +47,15 @@ from `Launchpad `_.
Communication mode
^^^^^^^^^^^^^^^^^^
-In newer versions of ncmpcpp, like 0.5.5 shipped with Ubuntu 11.04, ncmcpp
-defaults to "notifications" mode for MPD communications, which Mopidy currently
-does not support. To workaround this limitation in Mopidy, edit the ncmpcpp
-configuration file at ``~/.ncmpcpp/config`` and add the following setting::
+In newer versions of ncmpcpp, like ncmpcpp 0.5.5 shipped with Ubuntu 11.04,
+ncmcpp defaults to "notifications" mode for MPD communications, which Mopidy
+did not support before Mopidy 0.6. To workaround this limitation in earlier
+versions of Mopidy, edit the ncmpcpp configuration file at
+``~/.ncmpcpp/config`` and add the following setting::
mpd_communication_mode = "polling"
-You can track the development of "notifications" mode support in Mopidy in
-:issue:`32`.
+If you use Mopidy 0.6 or newer, you don't need to change anything.
Graphical clients
diff --git a/docs/installation/index.rst b/docs/installation/index.rst
index 5101cc84..198ac9e8 100644
--- a/docs/installation/index.rst
+++ b/docs/installation/index.rst
@@ -25,7 +25,7 @@ Otherwise, make sure you got the required dependencies installed.
- Python >= 2.6, < 3
-- `Pykka `_ >= 0.12
+- `Pykka `_ >= 0.12.3
- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.
diff --git a/docs/modules/frontends/mpd.rst b/docs/modules/frontends/mpd.rst
index 6120c2a6..b0c7e3c5 100644
--- a/docs/modules/frontends/mpd.rst
+++ b/docs/modules/frontends/mpd.rst
@@ -5,31 +5,8 @@
.. inheritance-diagram:: mopidy.frontends.mpd
.. automodule:: mopidy.frontends.mpd
- :synopsis: MPD frontend
+ :synopsis: MPD server frontend
:members:
- :undoc-members:
-
-
-MPD server
-==========
-
-.. inheritance-diagram:: mopidy.frontends.mpd.server
-
-.. automodule:: mopidy.frontends.mpd.server
- :synopsis: MPD server
- :members:
- :undoc-members:
-
-
-MPD session
-===========
-
-.. inheritance-diagram:: mopidy.frontends.mpd.session
-
-.. automodule:: mopidy.frontends.mpd.session
- :synopsis: MPD client session
- :members:
- :undoc-members:
MPD dispatcher
@@ -40,7 +17,6 @@ MPD dispatcher
.. automodule:: mopidy.frontends.mpd.dispatcher
:synopsis: MPD request dispatcher
:members:
- :undoc-members:
MPD protocol
diff --git a/docs/modules/frontends/mpris.rst b/docs/modules/frontends/mpris.rst
new file mode 100644
index 00000000..05a6e287
--- /dev/null
+++ b/docs/modules/frontends/mpris.rst
@@ -0,0 +1,7 @@
+***********************************************
+:mod:`mopidy.frontends.mpris` -- MPRIS frontend
+***********************************************
+
+.. automodule:: mopidy.frontends.mpris
+ :synopsis: MPRIS frontend
+ :members:
diff --git a/docs/settings.rst b/docs/settings.rst
index f0888670..76eb6315 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -92,7 +92,6 @@ To make a ``tag_cache`` of your local music available for Mopidy:
.. _use_mpd_on_a_network:
-
Connecting from other machines on the network
=============================================
@@ -120,6 +119,33 @@ file::
LASTFM_PASSWORD = u'mysecret'
+.. _install_desktop_file:
+
+Controlling Mopidy through the Ubuntu Sound Menu
+================================================
+
+If you are running Ubuntu and installed Mopidy using the Debian package from
+APT you should be able to control Mopidy through the `Ubuntu Sound Menu
+`_ without any changes.
+
+If you installed Mopidy in any other way and want to control Mopidy through the
+Ubuntu Sound Menu, you must install the ``mopidy.desktop`` file which can be
+found in the ``data/`` dir of the Mopidy source into the
+``/usr/share/applications`` dir by hand::
+
+ cd /path/to/mopidy/source
+ sudo cp data/mopidy.desktop /usr/share/applications/
+
+After you have installed the file, start Mopidy in any way, and Mopidy should
+appear in the Ubuntu Sound Menu. When you quit Mopidy, it will still be listed
+in the Ubuntu Sound Menu, and may be restarted by selecting it there.
+
+The Ubuntu Sound Menu interacts with Mopidy's MPRIS frontend,
+:mod:`mopidy.frontends.mpris`. The MPRIS frontend supports the minimum
+requirements of the `MPRIS specification `_. The
+``TrackList`` and the ``Playlists`` interfaces of the spec are not supported.
+
+
Streaming audio through a SHOUTcast/Icecast server
==================================================
@@ -151,4 +177,3 @@ Available settings
.. automodule:: mopidy.settings
:synopsis: Available settings and their default values
:members:
- :undoc-members:
diff --git a/mopidy/__init__.py b/mopidy/__init__.py
index 79a0aa29..1d820fd0 100644
--- a/mopidy/__init__.py
+++ b/mopidy/__init__.py
@@ -3,9 +3,17 @@ import sys
if not (2, 6) <= sys.version_info < (3,):
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
+import glib
+import os
+
from subprocess import PIPE, Popen
-VERSION = (0, 5, 0)
+VERSION = (0, 6, 0)
+
+DATA_PATH = os.path.join(glib.get_user_data_dir(), 'mopidy')
+CACHE_PATH = os.path.join(glib.get_user_cache_dir(), 'mopidy')
+SETTINGS_PATH = os.path.join(glib.get_user_config_dir(), 'mopidy')
+SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py')
def get_version():
try:
diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py
index 038e2d7b..76c7f078 100644
--- a/mopidy/backends/base/__init__.py
+++ b/mopidy/backends/base/__init__.py
@@ -25,5 +25,5 @@ class Backend(object):
#: :class:`mopidy.backends.base.StoredPlaylistsController`.
stored_playlists = None
- #: List of URI prefixes this backend can handle.
- uri_handlers = []
+ #: List of URI schemes this backend can handle.
+ uri_schemes = []
diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py
index 2633f166..17125ac0 100644
--- a/mopidy/backends/base/current_playlist.py
+++ b/mopidy/backends/base/current_playlist.py
@@ -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,13 +17,10 @@ class CurrentPlaylistController(object):
def __init__(self, backend):
self.backend = backend
+ self.cp_id = 0
self._cp_tracks = []
self._version = 0
- def destroy(self):
- """Cleanup after component."""
- pass
-
@property
def cp_tracks(self):
"""
@@ -53,8 +51,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 +67,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 +85,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 +203,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')
diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py
index a30ed412..9e3afe9a 100644
--- a/mopidy/backends/base/library.py
+++ b/mopidy/backends/base/library.py
@@ -16,10 +16,6 @@ class LibraryController(object):
self.backend = backend
self.provider = provider
- def destroy(self):
- """Cleanup after component."""
- self.provider.destroy()
-
def find_exact(self, **query):
"""
Search the library for tracks where ``field`` is ``values``.
@@ -89,14 +85,6 @@ class BaseLibraryProvider(object):
def __init__(self, backend):
self.backend = backend
- def destroy(self):
- """
- Cleanup after component.
-
- *MAY be implemented by subclasses.*
- """
- pass
-
def find_exact(self, **query):
"""
See :meth:`mopidy.backends.base.LibraryController.find_exact`.
diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py
index 530c4840..51fe0d3b 100644
--- a/mopidy/backends/base/playback.py
+++ b/mopidy/backends/base/playback.py
@@ -4,10 +4,21 @@ import time
from pykka.registry import ActorRegistry
-from mopidy.frontends.base import BaseFrontend
+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
@@ -71,12 +82,6 @@ class PlaybackController(object):
self.play_time_accumulated = 0
self.play_time_started = None
- def destroy(self):
- """
- Cleanup after component.
- """
- self.provider.destroy()
-
def _get_cpid(self, cp_track):
if cp_track is None:
return None
@@ -276,6 +281,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)
@@ -313,6 +321,26 @@ class PlaybackController(object):
def _current_wall_time(self):
return int(time.time() * 1000)
+ def change_track(self, cp_track, on_error_step=1):
+ """
+ Change to the given track, keeping the current playback state.
+
+ :param cp_track: track to change to
+ :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
+ or :class:`None`
+ :param on_error_step: direction to step at play error, 1 for next
+ track (default), -1 for previous track
+ :type on_error_step: int, -1 or 1
+
+ """
+ old_state = self.state
+ self.stop()
+ self.current_cp_track = cp_track
+ if old_state == self.PLAYING:
+ self.play(on_error_step=on_error_step)
+ elif old_state == self.PAUSED:
+ self.pause()
+
def on_end_of_track(self):
"""
Tell the playback controller that end of track is reached.
@@ -326,7 +354,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)
@@ -349,20 +377,23 @@ class PlaybackController(object):
self.stop(clear_current_track=True)
def next(self):
- """Play the next track."""
- if self.state == self.STOPPED:
- return
+ """
+ Change to the next track.
+ The current playback state will be kept. If it was playing, playing
+ will continue. If it was paused, it will still be paused, etc.
+ """
if self.cp_track_at_next:
- self._trigger_stopped_playing_event()
- self.play(self.cp_track_at_next)
+ self._trigger_track_playback_ended()
+ self.change_track(self.cp_track_at_next)
else:
self.stop(clear_current_track=True)
def pause(self):
"""Pause playback."""
- if self.state == self.PLAYING and self.provider.pause():
+ if self.provider.pause():
self.state = self.PAUSED
+ self._trigger_track_playback_paused()
def play(self, cp_track=None, on_error_step=1):
"""
@@ -379,15 +410,17 @@ class PlaybackController(object):
if cp_track is not None:
assert cp_track in self.backend.current_playlist.cp_tracks
-
- if cp_track is None and self.current_cp_track is None:
- cp_track = self.cp_track_at_next
-
- if cp_track is None and self.state == self.PAUSED:
- self.resume()
+ elif cp_track is None:
+ if self.state == self.PAUSED:
+ return self.resume()
+ elif self.current_cp_track is not None:
+ cp_track = self.current_cp_track
+ elif self.current_cp_track is None and on_error_step == 1:
+ cp_track = self.cp_track_at_next
+ elif self.current_cp_track is None and on_error_step == -1:
+ cp_track = self.cp_track_at_previous
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,21 +435,23 @@ 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."""
- if self.cp_track_at_previous is None:
- return
- if self.state == self.STOPPED:
- return
- self._trigger_stopped_playing_event()
- self.play(self.cp_track_at_previous, on_error_step=-1)
+ """
+ Change to the previous track.
+
+ The current playback state will be kept. If it was playing, playing
+ will continue. If it was paused, it will still be paused, etc.
+ """
+ self._trigger_track_playback_ended()
+ self.change_track(self.cp_track_at_previous, on_error_step=-1)
def resume(self):
"""If paused, resume playing the current track."""
if self.state == self.PAUSED and self.provider.resume():
self.state = self.PLAYING
+ self._trigger_track_playback_resumed()
def seek(self, time_position):
"""
@@ -443,7 +478,10 @@ class PlaybackController(object):
self.play_time_started = self._current_wall_time
self.play_time_accumulated = time_position
- return self.provider.seek(time_position)
+ success = self.provider.seek(time_position)
+ if success:
+ self._trigger_seeked()
+ return success
def stop(self, clear_current_track=False):
"""
@@ -454,45 +492,54 @@ 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):
- """
- Notifies frontends that a track has started playing.
-
- For internal use only. Should be called by the backend directly after a
- track has started playing.
- """
+ def _trigger_track_playback_paused(self):
+ logger.debug(u'Triggering track playback paused event')
if self.current_track is None:
return
- frontend_refs = ActorRegistry.get_by_class(BaseFrontend)
- for frontend_ref in frontend_refs:
- frontend_ref.send_one_way({
- 'command': 'started_playing',
- 'track': self.current_track,
- })
+ BackendListener.send('track_playback_paused',
+ track=self.current_track,
+ time_position=self.time_position)
- def _trigger_stopped_playing_event(self):
- """
- Notifies frontends that a track has stopped playing.
-
- For internal use only. Should be called by the backend before a track
- is stopped playing, e.g. at the next, previous, and stop actions and at
- end-of-track.
- """
+ def _trigger_track_playback_resumed(self):
+ logger.debug(u'Triggering track playback resumed event')
if self.current_track is None:
return
- frontend_refs = ActorRegistry.get_by_class(BaseFrontend)
- for frontend_ref in frontend_refs:
- frontend_ref.send_one_way({
- 'command': 'stopped_playing',
- 'track': self.current_track,
- 'stop_position': self.time_position,
- })
+ BackendListener.send('track_playback_resumed',
+ track=self.current_track,
+ time_position=self.time_position)
+
+ def _trigger_track_playback_started(self):
+ logger.debug(u'Triggering track playback started event')
+ if self.current_track is None:
+ return
+ BackendListener.send('track_playback_started',
+ track=self.current_track)
+
+ def _trigger_track_playback_ended(self):
+ logger.debug(u'Triggering track playback ended event')
+ if self.current_track is None:
+ return
+ 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')
+
+ def _trigger_seeked(self):
+ logger.debug(u'Triggering seeked event')
+ BackendListener.send('seeked')
class BasePlaybackProvider(object):
@@ -506,14 +553,6 @@ class BasePlaybackProvider(object):
def __init__(self, backend):
self.backend = backend
- def destroy(self):
- """
- Cleanup after component.
-
- *MAY be implemented by subclasses.*
- """
- pass
-
def pause(self):
"""
Pause playback.
diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py
index aca78a8c..0ce2e196 100644
--- a/mopidy/backends/base/stored_playlists.py
+++ b/mopidy/backends/base/stored_playlists.py
@@ -17,10 +17,6 @@ class StoredPlaylistsController(object):
self.backend = backend
self.provider = provider
- def destroy(self):
- """Cleanup after component."""
- self.provider.destroy()
-
@property
def playlists(self):
"""
@@ -133,14 +129,6 @@ class BaseStoredPlaylistsProvider(object):
self.backend = backend
self._playlists = []
- def destroy(self):
- """
- Cleanup after component.
-
- *MAY be implemented by subclass.*
- """
- pass
-
@property
def playlists(self):
"""
@@ -201,4 +189,3 @@ class BaseStoredPlaylistsProvider(object):
*MUST be implemented by subclass.*
"""
raise NotImplementedError
-
diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py
index 90c87dac..70efb028 100644
--- a/mopidy/backends/dummy/__init__.py
+++ b/mopidy/backends/dummy/__init__.py
@@ -32,7 +32,7 @@ class DummyBackend(ThreadingActor, Backend):
self.stored_playlists = StoredPlaylistsController(backend=self,
provider=stored_playlists_provider)
- self.uri_handlers = [u'dummy:']
+ self.uri_schemes = [u'dummy']
class DummyLibraryProvider(BaseLibraryProvider):
diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py
index 93cf3534..e1d11bcb 100644
--- a/mopidy/backends/local/__init__.py
+++ b/mopidy/backends/local/__init__.py
@@ -1,4 +1,5 @@
import glob
+import glib
import logging
import os
import shutil
@@ -6,7 +7,7 @@ import shutil
from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
-from mopidy import settings
+from mopidy import settings, DATA_PATH
from mopidy.backends.base import (Backend, CurrentPlaylistController,
LibraryController, BaseLibraryProvider, PlaybackController,
BasePlaybackProvider, StoredPlaylistsController,
@@ -18,6 +19,14 @@ from .translator import parse_m3u, parse_mpd_tag_cache
logger = logging.getLogger(u'mopidy.backends.local')
+DEFAULT_PLAYLIST_PATH = os.path.join(DATA_PATH, 'playlists')
+DEFAULT_TAG_CACHE_FILE = os.path.join(DATA_PATH, 'tag_cache')
+DEFAULT_MUSIC_PATH = glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC)
+
+if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'):
+ DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music')
+
+
class LocalBackend(ThreadingActor, Backend):
"""
A backend for playing music from a local music archive.
@@ -52,13 +61,14 @@ class LocalBackend(ThreadingActor, Backend):
self.stored_playlists = StoredPlaylistsController(backend=self,
provider=stored_playlists_provider)
- self.uri_handlers = [u'file://']
+ self.uri_schemes = [u'file']
self.gstreamer = None
def on_start(self):
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
- assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
+ assert len(gstreamer_refs) == 1, \
+ 'Expected exactly one running GStreamer.'
self.gstreamer = gstreamer_refs[0].proxy()
@@ -96,7 +106,7 @@ class LocalPlaybackProvider(BasePlaybackProvider):
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
def __init__(self, *args, **kwargs):
super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs)
- self._folder = settings.LOCAL_PLAYLIST_PATH
+ self._folder = settings.LOCAL_PLAYLIST_PATH or DEFAULT_PLAYLIST_PATH
self.refresh()
def lookup(self, uri):
@@ -173,8 +183,8 @@ class LocalLibraryProvider(BaseLibraryProvider):
self.refresh()
def refresh(self, uri=None):
- tag_cache = settings.LOCAL_TAG_CACHE_FILE
- music_folder = settings.LOCAL_MUSIC_PATH
+ tag_cache = settings.LOCAL_TAG_CACHE_FILE or DEFAULT_TAG_CACHE_FILE
+ music_folder = settings.LOCAL_MUSIC_PATH or DEFAULT_MUSIC_PATH
tracks = parse_mpd_tag_cache(tag_cache, music_folder)
diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py
index 66bcffd4..a50f1724 100644
--- a/mopidy/backends/spotify/__init__.py
+++ b/mopidy/backends/spotify/__init__.py
@@ -67,7 +67,7 @@ class SpotifyBackend(ThreadingActor, Backend):
self.stored_playlists = StoredPlaylistsController(backend=self,
provider=stored_playlists_provider)
- self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
+ self.uri_schemes = [u'spotify']
self.gstreamer = None
self.spotify = None
@@ -78,12 +78,16 @@ class SpotifyBackend(ThreadingActor, Backend):
def on_start(self):
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
- assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
+ assert len(gstreamer_refs) == 1, \
+ 'Expected exactly one running GStreamer.'
self.gstreamer = gstreamer_refs[0].proxy()
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
self.spotify = self._connect()
+ def on_stop(self):
+ self.spotify.logout()
+
def _connect(self):
from .session_manager import SpotifySessionManager
diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py
index 40d4a099..59aa9a2c 100644
--- a/mopidy/backends/spotify/library.py
+++ b/mopidy/backends/spotify/library.py
@@ -55,7 +55,7 @@ class SpotifyLibraryProvider(BaseLibraryProvider):
spotify_query = u' '.join(spotify_query)
logger.debug(u'Spotify search query: %s' % spotify_query)
queue = Queue.Queue()
- self.backend.spotify.search(spotify_query.encode(ENCODING), queue)
+ self.backend.spotify.search(spotify_query, queue)
try:
return queue.get(timeout=3) # XXX What is an reasonable timeout?
except Queue.Empty:
diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py
index fd71d861..5261f0cf 100644
--- a/mopidy/backends/spotify/session_manager.py
+++ b/mopidy/backends/spotify/session_manager.py
@@ -1,3 +1,4 @@
+import glib
import logging
import os
import threading
@@ -6,7 +7,7 @@ from spotify.manager import SpotifySessionManager as PyspotifySessionManager
from pykka.registry import ActorRegistry
-from mopidy import get_version, settings
+from mopidy import get_version, settings, CACHE_PATH
from mopidy.backends.base import Backend
from mopidy.backends.spotify import BITRATES
from mopidy.backends.spotify.container_manager import SpotifyContainerManager
@@ -21,9 +22,10 @@ logger = logging.getLogger('mopidy.backends.spotify.session_manager')
# pylint: disable = R0901
# SpotifySessionManager: Too many ancestors (9/7)
+
class SpotifySessionManager(BaseThread, PyspotifySessionManager):
- cache_location = settings.SPOTIFY_CACHE_PATH
- settings_location = settings.SPOTIFY_CACHE_PATH
+ cache_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH
+ settings_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
user_agent = 'Mopidy %s' % get_version()
@@ -118,6 +120,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
'channels': channels,
}
self.gstreamer.emit_data(capabilites, bytes(frames))
+ return num_frames
def play_token_lost(self, session):
"""Callback used by pyspotify"""
@@ -148,9 +151,17 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
"""Search method used by Mopidy backend"""
def callback(results, userdata=None):
# TODO Include results from results.albums(), etc. too
+ # TODO Consider launching a second search if results.total_tracks()
+ # is larger than len(results.tracks())
playlist = Playlist(tracks=[
SpotifyTranslator.to_mopidy_track(t)
for t in results.tracks()])
queue.put(playlist)
self.connected.wait()
- self.session.search(query, callback)
+ self.session.search(query, callback, track_count=100,
+ album_count=0, artist_count=0)
+
+ def logout(self):
+ """Log out from spotify"""
+ logger.debug(u'Logging out from spotify')
+ self.session.logout()
diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py
index 1bf7e5aa..95287d77 100644
--- a/mopidy/backends/spotify/translator.py
+++ b/mopidy/backends/spotify/translator.py
@@ -4,7 +4,7 @@ import logging
from spotify import Link, SpotifyError
from mopidy import settings
-from mopidy.backends.spotify import ENCODING, BITRATES
+from mopidy.backends.spotify import ENCODING
from mopidy.models import Artist, Album, Track, Playlist
logger = logging.getLogger('mopidy.backends.spotify.translator')
@@ -44,7 +44,7 @@ class SpotifyTranslator(object):
track_no=spotify_track.index(),
date=date,
length=spotify_track.duration(),
- bitrate=BITRATES[settings.SPOTIFY_BITRATE],
+ bitrate=settings.SPOTIFY_BITRATE,
)
@classmethod
diff --git a/mopidy/core.py b/mopidy/core.py
index 65472a29..08c5e0d7 100644
--- a/mopidy/core.py
+++ b/mopidy/core.py
@@ -1,8 +1,11 @@
import logging
import optparse
+import os
import signal
import sys
-import time
+
+import gobject
+gobject.threads_init()
# Extract any non-GStreamer arguments, and leave the GStreamer arguments for
# processing by GStreamer. This needs to be done before GStreamer is imported,
@@ -18,30 +21,30 @@ sys.argv[1:] = gstreamer_args
from pykka.registry import ActorRegistry
from mopidy import (get_version, settings, OptionalDependencyError,
- SettingsError)
+ SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE)
from mopidy.gstreamer import GStreamer
from mopidy.utils import get_class
from mopidy.utils.log import setup_logging
from mopidy.utils.path import get_or_create_folder, get_or_create_file
-from mopidy.utils.process import (GObjectEventThread, exit_handler,
- stop_all_actors)
+from mopidy.utils.process import (exit_handler, stop_remaining_actors,
+ stop_actors_by_class)
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.core')
def main():
signal.signal(signal.SIGTERM, exit_handler)
+ loop = gobject.MainLoop()
try:
options = parse_options()
setup_logging(options.verbosity_level, options.save_debug_log)
+ check_old_folders()
setup_settings(options.interactive)
- setup_gobject_loop()
setup_gstreamer()
setup_mixer()
setup_backend()
setup_frontends()
- while True:
- time.sleep(1)
+ loop.run()
except SettingsError as e:
logger.error(e.message)
except KeyboardInterrupt:
@@ -49,7 +52,12 @@ def main():
except Exception as e:
logger.exception(e)
finally:
- stop_all_actors()
+ loop.quit()
+ stop_frontends()
+ stop_backend()
+ stop_mixer()
+ stop_gstreamer()
+ stop_remaining_actors()
def parse_options():
parser = optparse.OptionParser(version=u'Mopidy %s' % get_version())
@@ -58,12 +66,12 @@ def parse_options():
help='show GStreamer help options')
parser.add_option('-i', '--interactive',
action='store_true', dest='interactive',
- help='ask interactively for required settings which is missing')
+ help='ask interactively for required settings which are missing')
parser.add_option('-q', '--quiet',
action='store_const', const=0, dest='verbosity_level',
help='less output (warning level)')
parser.add_option('-v', '--verbose',
- action='store_const', const=2, dest='verbosity_level',
+ action='count', default=1, dest='verbosity_level',
help='more output (debug level)')
parser.add_option('--save-debug-log',
action='store_true', dest='save_debug_log',
@@ -73,30 +81,54 @@ def parse_options():
help='list current settings')
return parser.parse_args(args=mopidy_args)[0]
+def check_old_folders():
+ old_settings_folder = os.path.expanduser(u'~/.mopidy')
+
+ if not os.path.isdir(old_settings_folder):
+ return
+
+ logger.warning(u'Old settings folder found at %s, settings.py should be '
+ 'moved to %s, any cache data should be deleted. See release notes '
+ 'for further instructions.', old_settings_folder, SETTINGS_PATH)
+
def setup_settings(interactive):
- get_or_create_folder('~/.mopidy/')
- get_or_create_file('~/.mopidy/settings.py')
+ get_or_create_folder(SETTINGS_PATH)
+ get_or_create_folder(DATA_PATH)
+ get_or_create_file(SETTINGS_FILE)
try:
settings.validate(interactive)
except SettingsError, e:
logger.error(e.message)
sys.exit(1)
-def setup_gobject_loop():
- GObjectEventThread().start()
-
def setup_gstreamer():
GStreamer.start()
+def stop_gstreamer():
+ stop_actors_by_class(GStreamer)
+
def setup_mixer():
get_class(settings.MIXER).start()
+def stop_mixer():
+ stop_actors_by_class(get_class(settings.MIXER))
+
def setup_backend():
get_class(settings.BACKENDS[0]).start()
+def stop_backend():
+ stop_actors_by_class(get_class(settings.BACKENDS[0]))
+
def setup_frontends():
for frontend_class_name in settings.FRONTENDS:
try:
get_class(frontend_class_name).start()
except OptionalDependencyError as e:
logger.info(u'Disabled: %s (%s)', frontend_class_name, e)
+
+def stop_frontends():
+ for frontend_class_name in settings.FRONTENDS:
+ try:
+ stop_actors_by_class(get_class(frontend_class_name))
+ except OptionalDependencyError:
+ pass
diff --git a/mopidy/frontends/base.py b/mopidy/frontends/base.py
deleted file mode 100644
index 811644b1..00000000
--- a/mopidy/frontends/base.py
+++ /dev/null
@@ -1,5 +0,0 @@
-class BaseFrontend(object):
- """
- Base class for frontends.
- """
- pass
diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py
index 04716c61..125457cd 100644
--- a/mopidy/frontends/lastfm.py
+++ b/mopidy/frontends/lastfm.py
@@ -10,14 +10,14 @@ except ImportError as import_error:
from pykka.actor import ThreadingActor
from mopidy import settings, SettingsError
-from mopidy.frontends.base import BaseFrontend
+from mopidy.listeners import BackendListener
logger = logging.getLogger('mopidy.frontends.lastfm')
API_KEY = '2236babefa8ebb3d93ea467560d00d04'
API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd'
-class LastfmFrontend(ThreadingActor, BaseFrontend):
+class LastfmFrontend(ThreadingActor, BackendListener):
"""
Frontend which scrobbles the music you play to your `Last.fm
`_ profile.
@@ -57,15 +57,7 @@ class LastfmFrontend(ThreadingActor, BaseFrontend):
logger.error(u'Error during Last.fm setup: %s', e)
self.stop()
- def on_receive(self, message):
- if message.get('command') == 'started_playing':
- self.started_playing(message['track'])
- elif message.get('command') == 'stopped_playing':
- self.stopped_playing(message['track'], message['stop_position'])
- else:
- pass # Ignore any other messages
-
- 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())
@@ -82,14 +74,14 @@ class LastfmFrontend(ThreadingActor, BaseFrontend):
pylast.MalformedResponseError, pylast.WSError) as e:
logger.warning(u'Error submitting playing track to Last.fm: %s', e)
- def stopped_playing(self, track, stop_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
- stop_position = stop_position // 1000
+ time_position = time_position // 1000
if duration < 30:
logger.debug(u'Track too short to scrobble. (30s)')
return
- if stop_position < duration // 2 and stop_position < 240:
+ if time_position < duration // 2 and time_position < 240:
logger.debug(
u'Track not played long enough to scrobble. (50% or 240s)')
return
diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py
index 175aa0ee..b6adc09d 100644
--- a/mopidy/frontends/mpd/__init__.py
+++ b/mopidy/frontends/mpd/__init__.py
@@ -1,15 +1,15 @@
-import asyncore
import logging
+import sys
-from pykka.actor import ThreadingActor
+from pykka import registry, actor
-from mopidy.frontends.base import BaseFrontend
-from mopidy.frontends.mpd.server import MpdServer
-from mopidy.utils.process import BaseThread
+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, BaseFrontend):
+class MpdFrontend(actor.ThreadingActor, listeners.BackendListener):
"""
The MPD frontend.
@@ -25,23 +25,85 @@ class MpdFrontend(ThreadingActor, BaseFrontend):
"""
def __init__(self):
- self._thread = None
+ hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
+ port = settings.MPD_SERVER_PORT
+
+ try:
+ network.Server(hostname, port, protocol=MpdSession,
+ max_connections=settings.MPD_SERVER_MAX_CONNECTIONS)
+ except IOError, e:
+ logger.error(u'MPD server startup failed: %s', e)
+ sys.exit(1)
+
+ logger.info(u'MPD server running at [%s]:%s', hostname, port)
+
+ 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):
+ """
+ The MPD client session. Keeps track of a single client session. Any
+ requests from the client is passed on to the MPD request dispatcher.
+ """
+
+ terminator = protocol.LINE_TERMINATOR
+ encoding = protocol.ENCODING
+ delimeter = r'\r?\n'
+
+ def __init__(self, connection):
+ super(MpdSession, self).__init__(connection)
+ self.dispatcher = dispatcher.MpdDispatcher(self)
def on_start(self):
- self._thread = MpdThread()
- self._thread.start()
+ logger.info(u'New MPD connection from [%s]:%s', self.host, self.port)
+ self.send_lines([u'OK MPD %s' % protocol.VERSION])
- def on_receive(self, message):
- pass # Ignore any messages
+ def on_line_received(self, line):
+ logger.debug(u'Request from [%s]:%s to %s: %s', self.host, self.port,
+ self.actor_urn, line)
+ response = self.dispatcher.handle_request(line)
+ if not response:
+ return
-class MpdThread(BaseThread):
- def __init__(self):
- super(MpdThread, self).__init__()
- self.name = u'MpdThread'
+ logger.debug(u'Response to [%s]:%s from %s: %s', self.host, self.port,
+ self.actor_urn, log.indent(self.terminator.join(response)))
- def run_inside_try(self):
- logger.debug(u'Starting MPD server thread')
- server = MpdServer()
- server.start()
- asyncore.loop()
+ self.send_lines(response)
+
+ def on_idle(self, subsystem):
+ self.dispatcher.handle_idle(subsystem)
+
+ def decode(self, line):
+ try:
+ return super(MpdSession, self).decode(line.decode('string_escape'))
+ except ValueError:
+ logger.warning(u'Stopping actor due to unescaping error, data '
+ 'supplied by client was not valid.')
+ self.stop()
+
+ def close(self):
+ self.stop()
diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py
index 91cdc5e7..5ee70a5b 100644
--- a/mopidy/frontends/mpd/dispatcher.py
+++ b/mopidy/frontends/mpd/dispatcher.py
@@ -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)
@@ -71,7 +90,7 @@ class MpdDispatcher(object):
def _authenticate_filter(self, request, response, filter_chain):
if self.authenticated:
return self._call_next_filter(request, response, filter_chain)
- elif settings.MPD_SERVER_PASSWORD is None:
+ elif settings.MPD_SERVER_PASSWORD is None:
self.authenticated = True
return self._call_next_filter(request, response, filter_chain)
else:
@@ -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):
@@ -128,7 +170,7 @@ class MpdDispatcher(object):
return self._call_next_filter(request, response, filter_chain)
except ActorDeadError as e:
logger.warning(u'Tried to communicate with dead actor.')
- raise exceptions.MpdSystemError(e.message)
+ raise exceptions.MpdSystemError(e)
def _call_handler(self, request):
(handler, kwargs) = self._find_handler(request)
@@ -178,12 +220,20 @@ class MpdContext(object):
#: The current :class:`MpdDispatcher`.
dispatcher = None
- #: The current :class:`mopidy.frontends.mpd.session.MpdSession`.
+ #: 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
@@ -192,11 +242,10 @@ class MpdContext(object):
"""
The backend. An instance of :class:`mopidy.backends.base.Backend`.
"""
- if self._backend is not None:
- return self._backend
- backend_refs = ActorRegistry.get_by_class(Backend)
- assert len(backend_refs) == 1, 'Expected exactly one running backend.'
- self._backend = backend_refs[0].proxy()
+ if self._backend is None:
+ backend_refs = ActorRegistry.get_by_class(Backend)
+ assert len(backend_refs) == 1, 'Expected exactly one running backend.'
+ self._backend = backend_refs[0].proxy()
return self._backend
@property
@@ -204,9 +253,8 @@ class MpdContext(object):
"""
The mixer. An instance of :class:`mopidy.mixers.base.BaseMixer`.
"""
- if self._mixer is not None:
- return self._mixer
- mixer_refs = ActorRegistry.get_by_class(BaseMixer)
- assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
- self._mixer = mixer_refs[0].proxy()
+ if self._mixer is None:
+ mixer_refs = ActorRegistry.get_by_class(BaseMixer)
+ assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
+ self._mixer = mixer_refs[0].proxy()
return self._mixer
diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py
index 8e26013d..c7136804 100644
--- a/mopidy/frontends/mpd/protocol/current_playlist.py
+++ b/mopidy/frontends/mpd/protocol/current_playlist.py
@@ -19,8 +19,8 @@ def add(context, uri):
"""
if not uri:
return
- for handler_prefix in context.backend.uri_handlers.get():
- if uri.startswith(handler_prefix):
+ for uri_scheme in context.backend.uri_schemes.get():
+ if uri.startswith(uri_scheme):
track = context.backend.library.lookup(uri).get()
if track is not None:
context.backend.current_playlist.add(track)
diff --git a/mopidy/frontends/mpd/protocol/empty.py b/mopidy/frontends/mpd/protocol/empty.py
index 0e418551..4cdafd87 100644
--- a/mopidy/frontends/mpd/protocol/empty.py
+++ b/mopidy/frontends/mpd/protocol/empty.py
@@ -1,6 +1,6 @@
from mopidy.frontends.mpd.protocol import handle_request
-@handle_request(r'^$')
+@handle_request(r'^[ ]*$')
def empty(context):
"""The original MPD server returns ``OK`` on an empty request."""
pass
diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py
index 920f48a5..df13b4b4 100644
--- a/mopidy/frontends/mpd/protocol/reflection.py
+++ b/mopidy/frontends/mpd/protocol/reflection.py
@@ -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)]
@@ -95,4 +83,5 @@ def urlhandlers(context):
Gets a list of available URL handlers.
"""
- return [(u'handler', uri) for uri in context.backend.uri_handlers.get()]
+ return [(u'handler', uri_scheme)
+ for uri_scheme in context.backend.uri_schemes.get()]
diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py
index abbb8d7f..20a66775 100644
--- a/mopidy/frontends/mpd/protocol/status.py
+++ b/mopidy/frontends/mpd/protocol/status.py
@@ -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):
@@ -125,12 +153,17 @@ def status(context):
- ``nextsongid``: playlist songid of the next song to be played
- ``time``: total time elapsed (of current playing/paused song)
- ``elapsed``: Total time elapsed within the current song, but with
- higher resolution.
+ higher resolution.
- ``bitrate``: instantaneous bitrate in kbps
- ``xfade``: crossfade in seconds
- ``audio``: sampleRate``:bits``:channels
- ``updatings_db``: job id
- ``error``: if there is an error, returns message here
+
+ *Clarifications based on experience implementing*
+ - ``volume``: can also be -1 if no output is set.
+ - ``elapsed``: Higher resolution means time in seconds with three
+ decimal places for millisecond precision.
"""
futures = {
'current_playlist.tracks': context.backend.current_playlist.tracks,
@@ -214,11 +247,11 @@ def _status_state(futures):
return u'pause'
def _status_time(futures):
- return u'%s:%s' % (_status_time_elapsed(futures) // 1000,
+ return u'%d:%d' % (futures['playback.time_position'].get() // 1000,
_status_time_total(futures) // 1000)
def _status_time_elapsed(futures):
- return futures['playback.time_position'].get()
+ return u'%.3f' % (futures['playback.time_position'].get() / 1000.0)
def _status_time_total(futures):
current_cp_track = futures['playback.current_cp_track'].get()
diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py
deleted file mode 100644
index 62e443fb..00000000
--- a/mopidy/frontends/mpd/server.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import asyncore
-import logging
-import sys
-
-from mopidy import settings
-from mopidy.utils import network
-from .session import MpdSession
-
-logger = logging.getLogger('mopidy.frontends.mpd.server')
-
-class MpdServer(asyncore.dispatcher):
- """
- The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession`
- for each client connection.
- """
-
- def start(self):
- """Start MPD server."""
- try:
- self.set_socket(network.create_socket())
- self.set_reuse_addr()
- hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
- port = settings.MPD_SERVER_PORT
- logger.debug(u'MPD server is binding to [%s]:%s', hostname, port)
- self.bind((hostname, port))
- self.listen(1)
- logger.info(u'MPD server running at [%s]:%s', hostname, port)
- except IOError, e:
- logger.error(u'MPD server startup failed: %s' %
- str(e).decode('utf-8'))
- sys.exit(1)
-
- def handle_accept(self):
- """Called by asyncore when a new client connects."""
- (client_socket, client_socket_address) = self.accept()
- logger.info(u'MPD client connection from [%s]:%s',
- client_socket_address[0], client_socket_address[1])
- MpdSession(self, client_socket, client_socket_address)
diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py
deleted file mode 100644
index ce5d3be7..00000000
--- a/mopidy/frontends/mpd/session.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import asynchat
-import logging
-
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
-from mopidy.utils.log import indent
-
-logger = logging.getLogger('mopidy.frontends.mpd.session')
-
-class MpdSession(asynchat.async_chat):
- """
- The MPD client session. Keeps track of a single client session. Any
- requests from the client is passed on to the MPD request dispatcher.
- """
-
- def __init__(self, server, client_socket, client_socket_address):
- asynchat.async_chat.__init__(self, sock=client_socket)
- self.server = server
- self.client_address = client_socket_address[0]
- self.client_port = client_socket_address[1]
- self.input_buffer = []
- self.authenticated = False
- self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
- self.dispatcher = MpdDispatcher(session=self)
- self.send_response([u'OK MPD %s' % VERSION])
-
- def collect_incoming_data(self, data):
- """Called by asynchat when new data arrives."""
- self.input_buffer.append(data)
-
- def found_terminator(self):
- """Called by asynchat when a terminator is found in incoming data."""
- data = ''.join(self.input_buffer).strip()
- self.input_buffer = []
- try:
- self.send_response(self.handle_request(data))
- except UnicodeDecodeError as e:
- logger.warning(u'Received invalid data: %s', e)
-
- def handle_request(self, request):
- """Handle the request using the MPD command handlers."""
- request = request.decode(ENCODING)
- logger.debug(u'Request from [%s]:%s: %s', self.client_address,
- self.client_port, indent(request))
- return self.dispatcher.handle_request(request)
-
- def send_response(self, response):
- """
- Format a response from the MPD command handlers and send it to the
- client.
- """
- if response:
- response = LINE_TERMINATOR.join(response)
- logger.debug(u'Response to [%s]:%s: %s', self.client_address,
- self.client_port, indent(response))
- response = u'%s%s' % (response, LINE_TERMINATOR)
- data = response.encode(ENCODING)
- self.push(data)
diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py
new file mode 100644
index 00000000..579038ca
--- /dev/null
+++ b/mopidy/frontends/mpris/__init__.py
@@ -0,0 +1,130 @@
+import logging
+
+logger = logging.getLogger('mopidy.frontends.mpris')
+
+try:
+ import indicate
+except ImportError as import_error:
+ indicate = None
+ logger.debug(u'Startup notification will not be sent (%s)', import_error)
+
+from pykka.actor import ThreadingActor
+
+from mopidy import settings
+from mopidy.frontends.mpris import objects
+from mopidy.listeners import BackendListener
+
+
+class MprisFrontend(ThreadingActor, BackendListener):
+ """
+ Frontend which lets you control Mopidy through the Media Player Remote
+ Interfacing Specification (`MPRIS `_) D-Bus
+ interface.
+
+ An example of an MPRIS client is the `Ubuntu Sound Menu
+ `_.
+
+ **Dependencies:**
+
+ - D-Bus Python bindings. The package is named ``python-dbus`` in
+ Ubuntu/Debian.
+ - ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the
+ Ubuntu Sound Menu. The package is named ``python-indicate`` in
+ Ubuntu/Debian.
+ - An ``.desktop`` file for Mopidy installed at the path set in
+ :attr:`mopidy.settings.DESKTOP_FILE`. See :ref:`install_desktop_file` for
+ details.
+
+ **Testing the frontend**
+
+ To test, start Mopidy, and then run the following in a Python shell::
+
+ import dbus
+ bus = dbus.SessionBus()
+ player = bus.get_object('org.mpris.MediaPlayer2.mopidy',
+ '/org/mpris/MediaPlayer2')
+
+ Now you can control Mopidy through the player object. Examples:
+
+ - To get some properties from Mopidy, run::
+
+ props = player.GetAll('org.mpris.MediaPlayer2',
+ dbus_interface='org.freedesktop.DBus.Properties')
+
+ - To quit Mopidy through D-Bus, run::
+
+ player.Quit(dbus_interface='org.mpris.MediaPlayer2')
+ """
+
+ def __init__(self):
+ self.indicate_server = None
+ self.mpris_object = None
+
+ def on_start(self):
+ try:
+ self.mpris_object = objects.MprisObject()
+ self._send_startup_notification()
+ except Exception as e:
+ logger.error(u'MPRIS frontend setup failed (%s)', e)
+ self.stop()
+
+ def on_stop(self):
+ logger.debug(u'Removing MPRIS object from D-Bus connection...')
+ if self.mpris_object:
+ self.mpris_object.remove_from_connection()
+ self.mpris_object = None
+ logger.debug(u'Removed MPRIS object from D-Bus connection')
+
+ def _send_startup_notification(self):
+ """
+ Send startup notification using libindicate to make Mopidy appear in
+ e.g. `Ubuntu's sound menu `_.
+
+ A reference to the libindicate server is kept for as long as Mopidy is
+ running. When Mopidy exits, the server will be unreferenced and Mopidy
+ will automatically be unregistered from e.g. the sound menu.
+ """
+ if not indicate:
+ return
+ logger.debug(u'Sending startup notification...')
+ self.indicate_server = indicate.Server()
+ self.indicate_server.set_type('music.mopidy')
+ self.indicate_server.set_desktop_file(settings.DESKTOP_FILE)
+ self.indicate_server.show()
+ logger.debug(u'Startup notification sent')
+
+ def _emit_properties_changed(self, *changed_properties):
+ if self.mpris_object is None:
+ return
+ props_with_new_values = [
+ (p, self.mpris_object.Get(objects.PLAYER_IFACE, p))
+ for p in changed_properties]
+ self.mpris_object.PropertiesChanged(objects.PLAYER_IFACE,
+ dict(props_with_new_values), [])
+
+ def track_playback_paused(self, track, time_position):
+ logger.debug(u'Received track playback paused event')
+ self._emit_properties_changed('PlaybackStatus')
+
+ def track_playback_resumed(self, track, time_position):
+ logger.debug(u'Received track playback resumed event')
+ self._emit_properties_changed('PlaybackStatus')
+
+ def track_playback_started(self, track):
+ logger.debug(u'Received track playback started event')
+ self._emit_properties_changed('PlaybackStatus', 'Metadata')
+
+ def track_playback_ended(self, track, time_position):
+ logger.debug(u'Received track playback ended event')
+ self._emit_properties_changed('PlaybackStatus', 'Metadata')
+
+ def volume_changed(self):
+ logger.debug(u'Received volume changed event')
+ self._emit_properties_changed('Volume')
+
+ def seeked(self):
+ logger.debug(u'Received seeked event')
+ if self.mpris_object is None:
+ return
+ self.mpris_object.Seeked(
+ self.mpris_object.Get(objects.PLAYER_IFACE, 'Position'))
diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py
new file mode 100644
index 00000000..77278778
--- /dev/null
+++ b/mopidy/frontends/mpris/objects.py
@@ -0,0 +1,436 @@
+import logging
+import os
+
+logger = logging.getLogger('mopidy.frontends.mpris')
+
+try:
+ import dbus
+ import dbus.mainloop.glib
+ import dbus.service
+ import gobject
+except ImportError as import_error:
+ from mopidy import OptionalDependencyError
+ raise OptionalDependencyError(import_error)
+
+from pykka.registry import ActorRegistry
+
+from mopidy import settings
+from mopidy.backends.base import Backend
+from mopidy.backends.base.playback import PlaybackController
+from mopidy.mixers.base import BaseMixer
+from mopidy.utils.process import exit_process
+
+# Must be done before dbus.SessionBus() is called
+gobject.threads_init()
+dbus.mainloop.glib.threads_init()
+dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+
+BUS_NAME = 'org.mpris.MediaPlayer2.mopidy'
+OBJECT_PATH = '/org/mpris/MediaPlayer2'
+ROOT_IFACE = 'org.mpris.MediaPlayer2'
+PLAYER_IFACE = 'org.mpris.MediaPlayer2.Player'
+
+
+class MprisObject(dbus.service.Object):
+ """Implements http://www.mpris.org/2.1/spec/"""
+
+ properties = None
+
+ def __init__(self):
+ self._backend = None
+ self._mixer = None
+ self.properties = {
+ ROOT_IFACE: self._get_root_iface_properties(),
+ PLAYER_IFACE: self._get_player_iface_properties(),
+ }
+ bus_name = self._connect_to_dbus()
+ super(MprisObject, self).__init__(bus_name, OBJECT_PATH)
+
+ def _get_root_iface_properties(self):
+ return {
+ 'CanQuit': (True, None),
+ 'CanRaise': (False, None),
+ # NOTE Change if adding optional track list support
+ 'HasTrackList': (False, None),
+ 'Identity': ('Mopidy', None),
+ 'DesktopEntry': (self.get_DesktopEntry, None),
+ 'SupportedUriSchemes': (self.get_SupportedUriSchemes, None),
+ # NOTE Return MIME types supported by local backend if support for
+ # reporting supported MIME types is added
+ 'SupportedMimeTypes': (dbus.Array([], signature='s'), None),
+ }
+
+ def _get_player_iface_properties(self):
+ return {
+ 'PlaybackStatus': (self.get_PlaybackStatus, None),
+ 'LoopStatus': (self.get_LoopStatus, self.set_LoopStatus),
+ 'Rate': (1.0, self.set_Rate),
+ 'Shuffle': (self.get_Shuffle, self.set_Shuffle),
+ 'Metadata': (self.get_Metadata, None),
+ 'Volume': (self.get_Volume, self.set_Volume),
+ 'Position': (self.get_Position, None),
+ 'MinimumRate': (1.0, None),
+ 'MaximumRate': (1.0, None),
+ 'CanGoNext': (self.get_CanGoNext, None),
+ 'CanGoPrevious': (self.get_CanGoPrevious, None),
+ 'CanPlay': (self.get_CanPlay, None),
+ 'CanPause': (self.get_CanPause, None),
+ 'CanSeek': (self.get_CanSeek, None),
+ 'CanControl': (self.get_CanControl, None),
+ }
+
+ def _connect_to_dbus(self):
+ logger.debug(u'Connecting to D-Bus...')
+ bus_name = dbus.service.BusName(BUS_NAME, dbus.SessionBus())
+ logger.info(u'Connected to D-Bus')
+ return bus_name
+
+ @property
+ def backend(self):
+ if self._backend is None:
+ backend_refs = ActorRegistry.get_by_class(Backend)
+ assert len(backend_refs) == 1, \
+ 'Expected exactly one running backend.'
+ self._backend = backend_refs[0].proxy()
+ return self._backend
+
+ @property
+ def mixer(self):
+ if self._mixer is None:
+ mixer_refs = ActorRegistry.get_by_class(BaseMixer)
+ assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
+ self._mixer = mixer_refs[0].proxy()
+ return self._mixer
+
+ def _get_track_id(self, cp_track):
+ return '/com/mopidy/track/%d' % cp_track.cpid
+
+ def _get_cpid(self, track_id):
+ assert track_id.startswith('/com/mopidy/track/')
+ return track_id.split('/')[-1]
+
+ ### Properties interface
+
+ @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
+ in_signature='ss', out_signature='v')
+ def Get(self, interface, prop):
+ logger.debug(u'%s.Get(%s, %s) called',
+ dbus.PROPERTIES_IFACE, repr(interface), repr(prop))
+ (getter, setter) = self.properties[interface][prop]
+ if callable(getter):
+ return getter()
+ else:
+ return getter
+
+ @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
+ in_signature='s', out_signature='a{sv}')
+ def GetAll(self, interface):
+ logger.debug(u'%s.GetAll(%s) called',
+ dbus.PROPERTIES_IFACE, repr(interface))
+ getters = {}
+ for key, (getter, setter) in self.properties[interface].iteritems():
+ getters[key] = getter() if callable(getter) else getter
+ return getters
+
+ @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
+ in_signature='ssv', out_signature='')
+ def Set(self, interface, prop, value):
+ logger.debug(u'%s.Set(%s, %s, %s) called',
+ dbus.PROPERTIES_IFACE, repr(interface), repr(prop), repr(value))
+ getter, setter = self.properties[interface][prop]
+ if setter is not None:
+ setter(value)
+ self.PropertiesChanged(interface,
+ {prop: self.Get(interface, prop)}, [])
+
+ @dbus.service.signal(dbus_interface=dbus.PROPERTIES_IFACE,
+ signature='sa{sv}as')
+ def PropertiesChanged(self, interface, changed_properties,
+ invalidated_properties):
+ logger.debug(u'%s.PropertiesChanged(%s, %s, %s) signaled',
+ dbus.PROPERTIES_IFACE, interface, changed_properties,
+ invalidated_properties)
+
+
+ ### Root interface methods
+
+ @dbus.service.method(dbus_interface=ROOT_IFACE)
+ def Raise(self):
+ logger.debug(u'%s.Raise called', ROOT_IFACE)
+ # Do nothing, as we do not have a GUI
+
+ @dbus.service.method(dbus_interface=ROOT_IFACE)
+ def Quit(self):
+ logger.debug(u'%s.Quit called', ROOT_IFACE)
+ exit_process()
+
+
+ ### Root interface properties
+
+ def get_DesktopEntry(self):
+ return os.path.splitext(os.path.basename(settings.DESKTOP_FILE))[0]
+
+ def get_SupportedUriSchemes(self):
+ return dbus.Array(self.backend.uri_schemes.get(), signature='s')
+
+
+ ### Player interface methods
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Next(self):
+ logger.debug(u'%s.Next called', PLAYER_IFACE)
+ if not self.get_CanGoNext():
+ logger.debug(u'%s.Next not allowed', PLAYER_IFACE)
+ return
+ self.backend.playback.next().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Previous(self):
+ logger.debug(u'%s.Previous called', PLAYER_IFACE)
+ if not self.get_CanGoPrevious():
+ logger.debug(u'%s.Previous not allowed', PLAYER_IFACE)
+ return
+ self.backend.playback.previous().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Pause(self):
+ logger.debug(u'%s.Pause called', PLAYER_IFACE)
+ if not self.get_CanPause():
+ logger.debug(u'%s.Pause not allowed', PLAYER_IFACE)
+ return
+ self.backend.playback.pause().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def PlayPause(self):
+ logger.debug(u'%s.PlayPause called', PLAYER_IFACE)
+ if not self.get_CanPause():
+ logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE)
+ return
+ state = self.backend.playback.state.get()
+ if state == PlaybackController.PLAYING:
+ self.backend.playback.pause().get()
+ elif state == PlaybackController.PAUSED:
+ self.backend.playback.resume().get()
+ elif state == PlaybackController.STOPPED:
+ self.backend.playback.play().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Stop(self):
+ logger.debug(u'%s.Stop called', PLAYER_IFACE)
+ if not self.get_CanControl():
+ logger.debug(u'%s.Stop not allowed', PLAYER_IFACE)
+ return
+ self.backend.playback.stop().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Play(self):
+ logger.debug(u'%s.Play called', PLAYER_IFACE)
+ if not self.get_CanPlay():
+ logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
+ return
+ state = self.backend.playback.state.get()
+ if state == PlaybackController.PAUSED:
+ self.backend.playback.resume().get()
+ else:
+ self.backend.playback.play().get()
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def Seek(self, offset):
+ logger.debug(u'%s.Seek called', PLAYER_IFACE)
+ if not self.get_CanSeek():
+ logger.debug(u'%s.Seek not allowed', PLAYER_IFACE)
+ return
+ offset_in_milliseconds = offset // 1000
+ current_position = self.backend.playback.time_position.get()
+ new_position = current_position + offset_in_milliseconds
+ self.backend.playback.seek(new_position)
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def SetPosition(self, track_id, position):
+ logger.debug(u'%s.SetPosition called', PLAYER_IFACE)
+ if not self.get_CanSeek():
+ logger.debug(u'%s.SetPosition not allowed', PLAYER_IFACE)
+ return
+ position = position // 1000
+ current_cp_track = self.backend.playback.current_cp_track.get()
+ if current_cp_track is None:
+ return
+ if track_id != self._get_track_id(current_cp_track):
+ return
+ if position < 0:
+ return
+ if current_cp_track.track.length < position:
+ return
+ self.backend.playback.seek(position)
+
+ @dbus.service.method(dbus_interface=PLAYER_IFACE)
+ def OpenUri(self, uri):
+ logger.debug(u'%s.OpenUri called', PLAYER_IFACE)
+ if not self.get_CanPlay():
+ # NOTE The spec does not explictly require this check, but guarding
+ # the other methods doesn't help much if OpenUri is open for use.
+ logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
+ return
+ # NOTE Check if URI has MIME type known to the backend, if MIME support
+ # is added to the backend.
+ uri_schemes = self.backend.uri_schemes.get()
+ if not any([uri.startswith(uri_scheme) for uri_scheme in uri_schemes]):
+ return
+ track = self.backend.library.lookup(uri).get()
+ if track is not None:
+ cp_track = self.backend.current_playlist.add(track).get()
+ self.backend.playback.play(cp_track)
+ else:
+ logger.debug(u'Track with URI "%s" not found in library.', uri)
+
+
+ ### Player interface signals
+
+ @dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x')
+ def Seeked(self, position):
+ logger.debug(u'%s.Seeked signaled', PLAYER_IFACE)
+ # Do nothing, as just calling the method is enough to emit the signal.
+
+
+ ### Player interface properties
+
+ def get_PlaybackStatus(self):
+ state = self.backend.playback.state.get()
+ if state == PlaybackController.PLAYING:
+ return 'Playing'
+ elif state == PlaybackController.PAUSED:
+ return 'Paused'
+ elif state == PlaybackController.STOPPED:
+ return 'Stopped'
+
+ def get_LoopStatus(self):
+ repeat = self.backend.playback.repeat.get()
+ single = self.backend.playback.single.get()
+ if not repeat:
+ return 'None'
+ else:
+ if single:
+ return 'Track'
+ else:
+ return 'Playlist'
+
+ def set_LoopStatus(self, value):
+ if not self.get_CanControl():
+ logger.debug(u'Setting %s.LoopStatus not allowed', PLAYER_IFACE)
+ return
+ if value == 'None':
+ self.backend.playback.repeat = False
+ self.backend.playback.single = False
+ elif value == 'Track':
+ self.backend.playback.repeat = True
+ self.backend.playback.single = True
+ elif value == 'Playlist':
+ self.backend.playback.repeat = True
+ self.backend.playback.single = False
+
+ def set_Rate(self, value):
+ if not self.get_CanControl():
+ # NOTE The spec does not explictly require this check, but it was
+ # added to be consistent with all the other property setters.
+ logger.debug(u'Setting %s.Rate not allowed', PLAYER_IFACE)
+ return
+ if value == 0:
+ self.Pause()
+
+ def get_Shuffle(self):
+ return self.backend.playback.random.get()
+
+ def set_Shuffle(self, value):
+ if not self.get_CanControl():
+ logger.debug(u'Setting %s.Shuffle not allowed', PLAYER_IFACE)
+ return
+ if value:
+ self.backend.playback.random = True
+ else:
+ self.backend.playback.random = False
+
+ def get_Metadata(self):
+ current_cp_track = self.backend.playback.current_cp_track.get()
+ if current_cp_track is None:
+ return {'mpris:trackid': ''}
+ else:
+ (cpid, track) = current_cp_track
+ metadata = {'mpris:trackid': self._get_track_id(current_cp_track)}
+ if track.length:
+ metadata['mpris:length'] = track.length * 1000
+ if track.uri:
+ metadata['xesam:url'] = track.uri
+ if track.name:
+ metadata['xesam:title'] = track.name
+ if track.artists:
+ artists = list(track.artists)
+ artists.sort(key=lambda a: a.name)
+ metadata['xesam:artist'] = dbus.Array(
+ [a.name for a in artists if a.name], signature='s')
+ if track.album and track.album.name:
+ metadata['xesam:album'] = track.album.name
+ if track.album and track.album.artists:
+ artists = list(track.album.artists)
+ artists.sort(key=lambda a: a.name)
+ metadata['xesam:albumArtist'] = dbus.Array(
+ [a.name for a in artists if a.name], signature='s')
+ if track.track_no:
+ metadata['xesam:trackNumber'] = track.track_no
+ return dbus.Dictionary(metadata, signature='sv')
+
+ def get_Volume(self):
+ volume = self.mixer.volume.get()
+ if volume is not None:
+ return volume / 100.0
+
+ def set_Volume(self, value):
+ if not self.get_CanControl():
+ logger.debug(u'Setting %s.Volume not allowed', PLAYER_IFACE)
+ return
+ if value is None:
+ return
+ elif value < 0:
+ self.mixer.volume = 0
+ elif value > 1:
+ self.mixer.volume = 100
+ elif 0 <= value <= 1:
+ self.mixer.volume = int(value * 100)
+
+ def get_Position(self):
+ return self.backend.playback.time_position.get() * 1000
+
+ def get_CanGoNext(self):
+ if not self.get_CanControl():
+ return False
+ return (self.backend.playback.cp_track_at_next.get() !=
+ self.backend.playback.current_cp_track.get())
+
+ def get_CanGoPrevious(self):
+ if not self.get_CanControl():
+ return False
+ return (self.backend.playback.cp_track_at_previous.get() !=
+ self.backend.playback.current_cp_track.get())
+
+ def get_CanPlay(self):
+ if not self.get_CanControl():
+ return False
+ return (self.backend.playback.current_track.get() is not None
+ or self.backend.playback.track_at_next.get() is not None)
+
+ def get_CanPause(self):
+ if not self.get_CanControl():
+ return False
+ # NOTE Should be changed to vary based on capabilities of the current
+ # track if Mopidy starts supporting non-seekable media, like streams.
+ return True
+
+ def get_CanSeek(self):
+ if not self.get_CanControl():
+ return False
+ # NOTE Should be changed to vary based on capabilities of the current
+ # track if Mopidy starts supporting non-seekable media, like streams.
+ return True
+
+ def get_CanControl(self):
+ # NOTE This could be a setting for the end user to change.
+ return True
diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py
index 166c487e..edcb3084 100644
--- a/mopidy/gstreamer.py
+++ b/mopidy/gstreamer.py
@@ -43,9 +43,6 @@ class GStreamer(ThreadingActor):
self._handlers = {}
def on_start(self):
- # **Warning:** :class:`GStreamer` requires
- # :class:`mopidy.utils.process.GObjectEventThread` to be running. This
- # is not enforced by :class:`GStreamer` itself.
self._setup_pipeline()
self._setup_outputs()
self._setup_message_processor()
@@ -277,10 +274,18 @@ class GStreamer(ThreadingActor):
taglist = gst.TagList()
artists = [a for a in (track.artists or []) if a.name]
+ # Default to blank data to trick shoutcast into clearing any previous
+ # values it might have.
+ taglist[gst.TAG_ARTIST] = u' '
+ taglist[gst.TAG_TITLE] = u' '
+ taglist[gst.TAG_ALBUM] = u' '
+
if artists:
taglist[gst.TAG_ARTIST] = u', '.join([a.name for a in artists])
+
if track.name:
taglist[gst.TAG_TITLE] = track.name
+
if track.album and track.album.name:
taglist[gst.TAG_ALBUM] = track.album.name
diff --git a/mopidy/listeners.py b/mopidy/listeners.py
new file mode 100644
index 00000000..ee360bf3
--- /dev/null
+++ b/mopidy/listeners.py
@@ -0,0 +1,116 @@
+from pykka import registry
+
+class BackendListener(object):
+ """
+ Marker interface for recipients of events sent by the backend.
+
+ Any Pykka actor that mixes in this class will receive calls to the methods
+ defined here when the corresponding events happen in the backend. This
+ interface is used both for looking up what actors to notify of the events,
+ and for providing default implementations for those listeners that are not
+ interested in all events.
+ """
+
+ @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_paused(self, track, time_position):
+ """
+ Called whenever track playback is paused.
+
+ *MAY* be implemented by actor.
+
+ :param track: the track that was playing when playback paused
+ :type track: :class:`mopidy.models.Track`
+ :param time_position: the time position in milliseconds
+ :type time_position: int
+ """
+ pass
+
+ def track_playback_resumed(self, track, time_position):
+ """
+ Called whenever track playback is resumed.
+
+ *MAY* be implemented by actor.
+
+ :param track: the track that was playing when playback resumed
+ :type track: :class:`mopidy.models.Track`
+ :param time_position: the time position in milliseconds
+ :type time_position: int
+ """
+ pass
+
+
+ def track_playback_started(self, track):
+ """
+ Called whenever a new track starts playing.
+
+ *MAY* be implemented by actor.
+
+ :param track: the track that just started playing
+ :type track: :class:`mopidy.models.Track`
+ """
+ pass
+
+ def track_playback_ended(self, track, time_position):
+ """
+ Called whenever playback of a track ends.
+
+ *MAY* be implemented by actor.
+
+ :param track: the track that was played before playback stopped
+ :type track: :class:`mopidy.models.Track`
+ :param time_position: the time position in milliseconds
+ :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
+
+ def seeked(self):
+ """
+ Called whenever the time position changes by an unexpected amount, e.g.
+ at seek to a new time position.
+
+ *MAY* be implemented by actor.
+ """
+ pass
diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py
index ec3d8ae5..8798076a 100644
--- a/mopidy/mixers/base.py
+++ b/mopidy/mixers/base.py
@@ -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')
diff --git a/mopidy/settings.py b/mopidy/settings.py
index 9ac63719..ccbf8457 100644
--- a/mopidy/settings.py
+++ b/mopidy/settings.py
@@ -4,7 +4,7 @@ Available settings and their default values.
.. warning::
Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a
- file called ``~/.mopidy/settings.py`` and redefine settings there.
+ file called ``~/.config/mopidy/settings.py`` and redefine settings there.
"""
#: List of playback backends to use. See :mod:`mopidy.backends` for all
@@ -26,7 +26,8 @@ BACKENDS = (
#: details on the format.
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(message)s'
-#: Which GStreamer bin description to use in :class:`mopidy.outputs.CustomOutput`.
+#: Which GStreamer bin description to use in
+#: :class:`mopidy.outputs.custom.CustomOutput`.
#:
#: Default::
#:
@@ -48,6 +49,15 @@ DEBUG_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \
#: DEBUG_LOG_FILENAME = u'mopidy.log'
DEBUG_LOG_FILENAME = u'mopidy.log'
+#: Location of the Mopidy .desktop file.
+#:
+#: Used by :mod:`mopidy.frontends.mpris`.
+#:
+#: Default::
+#:
+#: DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
+DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
+
#: List of server frontends to use.
#:
#: Default::
@@ -55,10 +65,12 @@ DEBUG_LOG_FILENAME = u'mopidy.log'
#: FRONTENDS = (
#: u'mopidy.frontends.mpd.MpdFrontend',
#: u'mopidy.frontends.lastfm.LastfmFrontend',
+#: u'mopidy.frontends.mpris.MprisFrontend',
#: )
FRONTENDS = (
u'mopidy.frontends.mpd.MpdFrontend',
u'mopidy.frontends.lastfm.LastfmFrontend',
+ u'mopidy.frontends.mpris.MprisFrontend',
)
#: Your `Last.fm `_ username.
@@ -77,8 +89,9 @@ LASTFM_PASSWORD = u''
#:
#: Default::
#:
-#: LOCAL_MUSIC_PATH = u'~/music'
-LOCAL_MUSIC_PATH = u'~/music'
+#: # Defaults to asking glib where music is stored, fallback is ~/music
+#: LOCAL_MUSIC_PATH = None
+LOCAL_MUSIC_PATH = None
#: Path to playlist folder with m3u files for local music.
#:
@@ -86,8 +99,8 @@ LOCAL_MUSIC_PATH = u'~/music'
#:
#: Default::
#:
-#: LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
-LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
+#: LOCAL_PLAYLIST_PATH = None # Implies $XDG_DATA_DIR/mopidy/playlists
+LOCAL_PLAYLIST_PATH = None
#: Path to tag cache for local music.
#:
@@ -95,8 +108,8 @@ LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
#:
#: Default::
#:
-#: LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache'
-LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache'
+#: LOCAL_TAG_CACHE_FILE = None # Implies $XDG_DATA_DIR/mopidy/tag_cache
+LOCAL_TAG_CACHE_FILE = None
#: Sound mixer to use. See :mod:`mopidy.mixers` for all available mixers.
#:
@@ -167,6 +180,11 @@ MPD_SERVER_PORT = 6600
#: Default: :class:`None`, which means no password required.
MPD_SERVER_PASSWORD = None
+#: The maximum number of concurrent connections the MPD server will accept.
+#:
+#: Default: 20
+MPD_SERVER_MAX_CONNECTIONS = 20
+
#: List of outputs to use. See :mod:`mopidy.outputs` for all available
#: backends
#:
@@ -236,7 +254,7 @@ SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320'
#: Path to the Spotify cache.
#:
#: Used by :mod:`mopidy.backends.spotify`.
-SPOTIFY_CACHE_PATH = u'~/.mopidy/spotify_cache'
+SPOTIFY_CACHE_PATH = None
#: Your Spotify Premium username.
#:
diff --git a/mopidy/utils/__init__.py b/mopidy/utils/__init__.py
index acbb4664..9d7532a0 100644
--- a/mopidy/utils/__init__.py
+++ b/mopidy/utils/__init__.py
@@ -18,9 +18,11 @@ def import_module(name):
return sys.modules[name]
def get_class(name):
+ logger.debug('Loading: %s', name)
+ if '.' not in name:
+ raise ImportError("Couldn't load: %s" % name)
module_name = name[:name.rindex('.')]
class_name = name[name.rindex('.') + 1:]
- logger.debug('Loading: %s', name)
try:
module = import_module(module_name)
class_object = getattr(module, class_name)
diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py
index 03b85b48..0e5dfc29 100644
--- a/mopidy/utils/log.py
+++ b/mopidy/utils/log.py
@@ -20,7 +20,7 @@ def setup_console_logging(verbosity_level):
if verbosity_level == 0:
log_level = logging.WARNING
log_format = settings.CONSOLE_LOG_FORMAT
- elif verbosity_level == 2:
+ elif verbosity_level >= 2:
log_level = logging.DEBUG
log_format = settings.DEBUG_LOG_FORMAT
else:
@@ -33,6 +33,9 @@ def setup_console_logging(verbosity_level):
root = logging.getLogger('')
root.addHandler(handler)
+ if verbosity_level < 3:
+ logging.getLogger('pykka').setLevel(logging.INFO)
+
def setup_debug_logging_to_file():
formatter = logging.Formatter(settings.DEBUG_LOG_FORMAT)
handler = logging.handlers.RotatingFileHandler(
diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py
index 1dedf7d7..5079fe7c 100644
--- a/mopidy/utils/network.py
+++ b/mopidy/utils/network.py
@@ -1,10 +1,20 @@
+import errno
+import gobject
import logging
import re
import socket
+import threading
+
+from pykka import ActorDeadError
+from pykka.actor import ThreadingActor
+from pykka.registry import ActorRegistry
logger = logging.getLogger('mopidy.utils.server')
-def _try_ipv6_socket():
+class ShouldRetrySocketCall(Exception):
+ """Indicate that attempted socket call should be retried"""
+
+def try_ipv6_socket():
"""Determine if system really supports IPv6"""
if not socket.has_ipv6:
return False
@@ -17,7 +27,7 @@ def _try_ipv6_socket():
return False
#: Boolean value that indicates if creating an IPv6 socket will succeed.
-has_ipv6 = _try_ipv6_socket()
+has_ipv6 = try_ipv6_socket()
def create_socket():
"""Create a TCP socket with or without IPv6 depending on system support"""
@@ -27,6 +37,7 @@ def create_socket():
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return sock
def format_hostname(hostname):
@@ -34,3 +45,350 @@ def format_hostname(hostname):
if (has_ipv6 and re.match('\d+.\d+.\d+.\d+', hostname) is not None):
hostname = '::ffff:%s' % hostname
return hostname
+
+class Server(object):
+ """Setup listener and register it with gobject's event loop."""
+
+ def __init__(self, host, port, protocol, max_connections=5, timeout=30):
+ self.protocol = protocol
+ self.max_connections = max_connections
+ self.timeout = timeout
+ self.server_socket = self.create_server_socket(host, port)
+
+ self.register_server_socket(self.server_socket.fileno())
+
+ def create_server_socket(self, host, port):
+ sock = create_socket()
+ sock.setblocking(False)
+ sock.bind((host, port))
+ sock.listen(1)
+ return sock
+
+ def register_server_socket(self, fileno):
+ gobject.io_add_watch(fileno, gobject.IO_IN, self.handle_connection)
+
+ def handle_connection(self, fd, flags):
+ try:
+ sock, addr = self.accept_connection()
+ except ShouldRetrySocketCall:
+ return True
+
+ if self.maximum_connections_exceeded():
+ self.reject_connection(sock, addr)
+ else:
+ self.init_connection(sock, addr)
+ return True
+
+ def accept_connection(self):
+ try:
+ return self.server_socket.accept()
+ except socket.error as e:
+ if e.errno in (errno.EAGAIN, errno.EINTR):
+ raise ShouldRetrySocketCall
+ raise
+
+ def maximum_connections_exceeded(self):
+ return (self.max_connections is not None and
+ self.number_of_connections() >= self.max_connections)
+
+ def number_of_connections(self):
+ return len(ActorRegistry.get_by_class(self.protocol))
+
+ def reject_connection(self, sock, addr):
+ # FIXME provide more context in logging?
+ logger.warning(u'Rejected connection from [%s]:%s', addr[0], addr[1])
+ try:
+ sock.close()
+ except socket.error:
+ pass
+
+ def init_connection(self, sock, addr):
+ Connection(self.protocol, sock, addr, self.timeout)
+
+
+class Connection(object):
+ # NOTE: the callback code is _not_ run in the actor's thread, but in the
+ # same one as the event loop. If code in the callbacks blocks, the rest of
+ # gobject code will likely be blocked as well...
+ #
+ # Also note that source_remove() return values are ignored on purpose, a
+ # false return value would only tell us that what we thought was registered
+ # is already gone, there is really nothing more we can do.
+
+ def __init__(self, protocol, sock, addr, timeout):
+ sock.setblocking(False)
+
+ self.host, self.port = addr[:2] # IPv6 has larger addr
+
+ self.sock = sock
+ self.protocol = protocol
+ self.timeout = timeout
+
+ self.send_lock = threading.Lock()
+ self.send_buffer = ''
+
+ self.stopping = False
+
+ self.recv_id = None
+ self.send_id = None
+ self.timeout_id = None
+
+ self.actor_ref = self.protocol.start(self)
+
+ self.enable_recv()
+ self.enable_timeout()
+
+ def stop(self, reason, level=logging.DEBUG):
+ if self.stopping:
+ logger.log(level, 'Already stopping: %s' % reason)
+ return
+ else:
+ self.stopping = True
+
+ logger.log(level, reason)
+
+ try:
+ self.actor_ref.stop()
+ except ActorDeadError:
+ pass
+
+ self.disable_timeout()
+ self.disable_recv()
+ self.disable_send()
+
+ try:
+ self.sock.close()
+ except socket.error:
+ pass
+
+ def queue_send(self, data):
+ """Try to send data to client exactly as is and queue rest."""
+ self.send_lock.acquire(True)
+ self.send_buffer = self.send(self.send_buffer + data)
+ self.send_lock.release()
+ if self.send_buffer:
+ self.enable_send()
+
+ def send(self, data):
+ """Send data to client, return any unsent data."""
+ try:
+ sent = self.sock.send(data)
+ return data[sent:]
+ except socket.error as e:
+ if e.errno in (errno.EWOULDBLOCK, errno.EINTR):
+ return data
+ self.stop(u'Unexpected client error: %s' % e)
+ return ''
+
+ def enable_timeout(self):
+ """Reactivate timeout mechanism."""
+ if self.timeout <= 0:
+ return
+
+ self.disable_timeout()
+ self.timeout_id = gobject.timeout_add_seconds(
+ self.timeout, self.timeout_callback)
+
+ def disable_timeout(self):
+ """Deactivate timeout mechanism."""
+ if self.timeout_id is None:
+ return
+ gobject.source_remove(self.timeout_id)
+ self.timeout_id = None
+
+ def enable_recv(self):
+ if self.recv_id is not None:
+ return
+
+ try:
+ self.recv_id = gobject.io_add_watch(self.sock.fileno(),
+ gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
+ self.recv_callback)
+ except socket.error as e:
+ self.stop(u'Problem with connection: %s' % e)
+
+ def disable_recv(self):
+ if self.recv_id is None:
+ return
+ gobject.source_remove(self.recv_id)
+ self.recv_id = None
+
+ def enable_send(self):
+ if self.send_id is not None:
+ return
+
+ try:
+ self.send_id = gobject.io_add_watch(self.sock.fileno(),
+ gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP,
+ self.send_callback)
+ except socket.error as e:
+ self.stop(u'Problem with connection: %s' % e)
+
+ def disable_send(self):
+ if self.send_id is None:
+ return
+
+ gobject.source_remove(self.send_id)
+ self.send_id = None
+
+ def recv_callback(self, fd, flags):
+ if flags & (gobject.IO_ERR | gobject.IO_HUP):
+ self.stop(u'Bad client flags: %s' % flags)
+ return True
+
+ try:
+ data = self.sock.recv(4096)
+ except socket.error as e:
+ if e.errno not in (errno.EWOULDBLOCK, errno.EINTR):
+ self.stop(u'Unexpected client error: %s' % e)
+ return True
+
+ if not data:
+ self.stop(u'Client most likely disconnected.')
+ return True
+
+ try:
+ self.actor_ref.send_one_way({'received': data})
+ except ActorDeadError:
+ self.stop(u'Actor is dead.')
+
+ return True
+
+ def send_callback(self, fd, flags):
+ if flags & (gobject.IO_ERR | gobject.IO_HUP):
+ self.stop(u'Bad client flags: %s' % flags)
+ return True
+
+ # If with can't get the lock, simply try again next time socket is
+ # ready for sending.
+ if not self.send_lock.acquire(False):
+ return True
+
+ try:
+ self.send_buffer = self.send(self.send_buffer)
+ if not self.send_buffer:
+ self.disable_send()
+ finally:
+ self.send_lock.release()
+
+ return True
+
+ def timeout_callback(self):
+ self.stop(u'Client timeout out after %s seconds' % self.timeout)
+ return False
+
+
+class LineProtocol(ThreadingActor):
+ """
+ Base class for handling line based protocols.
+
+ Takes care of receiving new data from server's client code, decoding and
+ then splitting data along line boundaries.
+ """
+
+ #: Line terminator to use for outputed lines.
+ terminator = '\n'
+
+ #: Regex to use for spliting lines, will be set compiled version of its
+ #: own value, or to ``terminator``s value if it is not set itself.
+ delimeter = None
+
+ #: What encoding to expect incomming data to be in, can be :class:`None`.
+ encoding = 'utf-8'
+
+ def __init__(self, connection):
+ self.connection = connection
+ self.prevent_timeout = False
+ self.recv_buffer = ''
+
+ if self.delimeter:
+ self.delimeter = re.compile(self.delimeter)
+ else:
+ self.delimeter = re.compile(self.terminator)
+
+ @property
+ def host(self):
+ return self.connection.host
+
+ @property
+ def port(self):
+ return self.connection.port
+
+ def on_line_received(self, line):
+ """
+ Called whenever a new line is found.
+
+ Should be implemented by subclasses.
+ """
+ raise NotImplementedError
+
+ def on_receive(self, message):
+ """Handle messages with new data from server."""
+ if 'received' not in message:
+ return
+
+ self.connection.disable_timeout()
+ self.recv_buffer += message['received']
+
+ for line in self.parse_lines():
+ line = self.decode(line)
+ if line is not None:
+ self.on_line_received(line)
+
+ if not self.prevent_timeout:
+ self.connection.enable_timeout()
+
+ def on_stop(self):
+ """Ensure that cleanup when actor stops."""
+ self.connection.stop(u'Actor is shutting down.')
+
+ def parse_lines(self):
+ """Consume new data and yield any lines found."""
+ while re.search(self.terminator, self.recv_buffer):
+ line, self.recv_buffer = self.delimeter.split(
+ self.recv_buffer, 1)
+ yield line
+
+ def encode(self, line):
+ """
+ Handle encoding of line.
+
+ Can be overridden by subclasses to change encoding behaviour.
+ """
+ try:
+ return line.encode(self.encoding)
+ except UnicodeError:
+ logger.warning(u'Stopping actor due to encode problem, data '
+ 'supplied by client was not valid %s', self.encoding)
+ self.stop()
+
+ def decode(self, line):
+ """
+ Handle decoding of line.
+
+ Can be overridden by subclasses to change decoding behaviour.
+ """
+ try:
+ return line.decode(self.encoding)
+ except UnicodeError:
+ logger.warning(u'Stopping actor due to decode problem, data '
+ 'supplied by client was not valid %s', self.encoding)
+ self.stop()
+
+ def join_lines(self, lines):
+ if not lines:
+ return u''
+ return self.terminator.join(lines) + self.terminator
+
+ def send_lines(self, lines):
+ """
+ Send array of lines to client via connection.
+
+ Join lines using the terminator that is set for this class, encode it
+ and send it to the client.
+ """
+ if not lines:
+ return
+
+ data = self.join_lines(lines)
+ self.connection.queue_send(self.encode(data))
diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py
index 540cb4fa..8bd39f06 100644
--- a/mopidy/utils/path.py
+++ b/mopidy/utils/path.py
@@ -60,6 +60,7 @@ def find_files(path):
yield filename
# pylint: enable = W0612
+# FIXME replace with mock usage in tests.
class Mtime(object):
def __init__(self):
self.fake = None
diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py
index c1d1c9f5..80d850fe 100644
--- a/mopidy/utils/process.py
+++ b/mopidy/utils/process.py
@@ -3,9 +3,6 @@ import signal
import thread
import threading
-import gobject
-gobject.threads_init()
-
from pykka import ActorDeadError
from pykka.registry import ActorRegistry
@@ -25,9 +22,17 @@ def exit_handler(signum, frame):
logger.info(u'Got %s signal', signals[signum])
exit_process()
-def stop_all_actors():
+def stop_actors_by_class(klass):
+ actors = ActorRegistry.get_by_class(klass)
+ logger.debug(u'Stopping %d instance(s) of %s', len(actors), klass.__name__)
+ for actor in actors:
+ actor.stop()
+
+def stop_remaining_actors():
num_actors = len(ActorRegistry.get_all())
while num_actors:
+ logger.error(
+ u'There are actor threads still running, this is probably a bug')
logger.debug(u'Seeing %d actor and %d non-actor thread(s): %s',
num_actors, threading.active_count() - num_actors,
', '.join([t.name for t in threading.enumerate()]))
@@ -60,25 +65,3 @@ class BaseThread(threading.Thread):
def run_inside_try(self):
raise NotImplementedError
-
-
-class GObjectEventThread(BaseThread):
- """
- A GObject event loop which is shared by all Mopidy components that uses
- libraries that need a GObject event loop, like GStreamer and D-Bus.
-
- Should be started by Mopidy's core and used by
- :mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc.
- """
-
- def __init__(self):
- super(GObjectEventThread, self).__init__()
- self.name = u'GObjectEventThread'
- self.loop = None
-
- def run_inside_try(self):
- self.loop = gobject.MainLoop().run()
-
- def destroy(self):
- self.loop.quit()
- super(GObjectEventThread, self).destroy()
diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py
index cab94089..fca4f337 100644
--- a/mopidy/utils/settings.py
+++ b/mopidy/utils/settings.py
@@ -2,12 +2,13 @@
from __future__ import absolute_import
from copy import copy
import getpass
+import glib
import logging
import os
from pprint import pformat
import sys
-from mopidy import SettingsError
+from mopidy import SettingsError, SETTINGS_PATH, SETTINGS_FILE
from mopidy.utils.log import indent
logger = logging.getLogger('mopidy.utils.settings')
@@ -20,11 +21,9 @@ class SettingsProxy(object):
self.runtime = {}
def _get_local_settings(self):
- dotdir = os.path.expanduser(u'~/.mopidy/')
- settings_file = os.path.join(dotdir, u'settings.py')
- if not os.path.isfile(settings_file):
+ if not os.path.isfile(SETTINGS_FILE):
return {}
- sys.path.insert(0, dotdir)
+ sys.path.insert(0, SETTINGS_PATH)
# pylint: disable = F0401
import settings as local_settings_module
# pylint: enable = F0401
@@ -53,6 +52,8 @@ class SettingsProxy(object):
value = self.current[attr]
if isinstance(value, basestring) and len(value) == 0:
raise SettingsError(u'Setting "%s" is empty.' % attr)
+ if not value:
+ return value
if attr.endswith('_PATH') or attr.endswith('_FILE'):
value = os.path.expanduser(value)
value = os.path.abspath(value)
diff --git a/requirements/core.txt b/requirements/core.txt
index aaae84f8..8f9da622 100644
--- a/requirements/core.txt
+++ b/requirements/core.txt
@@ -1 +1 @@
-Pykka >= 0.12
+Pykka >= 0.12.3
diff --git a/requirements/tests.txt b/requirements/tests.txt
index f8cf2eb3..922ef6dc 100644
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -1,4 +1,5 @@
coverage
-mock
+mock >= 0.7
nose
tox
+yappi
diff --git a/tests/__init__.py b/tests/__init__.py
index 1d4d2e3d..833ff239 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,24 +1,41 @@
import os
+import sys
-try: # 2.7
- # pylint: disable = E0611,F0401
- from unittest.case import SkipTest
- # pylint: enable = E0611,F0401
-except ImportError:
- try: # Nose
- from nose.plugins.skip import SkipTest
- except ImportError: # Failsafe
- class SkipTest(Exception):
- pass
+if sys.version_info < (2, 7):
+ import unittest2 as unittest
+else:
+ import unittest
from mopidy import settings
# Nuke any local settings to ensure same test env all over
settings.local.clear()
+
def path_to_data_dir(name):
path = os.path.dirname(__file__)
path = os.path.join(path, 'data')
path = os.path.abspath(path)
return os.path.join(path, name)
+
+class IsA(object):
+ def __init__(self, klass):
+ self.klass = klass
+
+ def __eq__(self, rhs):
+ try:
+ return isinstance(rhs, self.klass)
+ except TypeError:
+ return type(rhs) == type(self.klass)
+
+ def __ne__(self, rhs):
+ return not self.__eq__(rhs)
+
+ def __repr__(self):
+ return str(self.klass)
+
+
+any_int = IsA(int)
+any_str = IsA(str)
+any_unicode = IsA(unicode)
diff --git a/tests/__main__.py b/tests/__main__.py
index e2bb3e72..69113580 100644
--- a/tests/__main__.py
+++ b/tests/__main__.py
@@ -1,4 +1,8 @@
import nose
+import yappi
-if __name__ == '__main__':
+try:
+ yappi.start()
nose.main()
+finally:
+ yappi.print_stats()
diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py
index b84391af..c81f4a0d 100644
--- a/tests/backends/base/current_playlist.py
+++ b/tests/backends/base/current_playlist.py
@@ -1,5 +1,4 @@
import mock
-import multiprocessing
import random
from mopidy.models import Playlist, Track
@@ -7,6 +6,7 @@ from mopidy.gstreamer import GStreamer
from tests.backends.base import populate_playlist
+
class CurrentPlaylistControllerTest(object):
tracks = []
diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py
index 2a3de730..4b3ef5c0 100644
--- a/tests/backends/base/library.py
+++ b/tests/backends/base/library.py
@@ -1,6 +1,7 @@
from mopidy.models import Playlist, Track, Album, Artist
-from tests import SkipTest, path_to_data_dir
+from tests import unittest, path_to_data_dir
+
class LibraryControllerTest(object):
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
@@ -20,11 +21,13 @@ class LibraryControllerTest(object):
def test_refresh(self):
self.library.refresh()
+ @unittest.SkipTest
def test_refresh_uri(self):
- raise SkipTest
+ pass
+ @unittest.SkipTest
def test_refresh_missing_uri(self):
- raise SkipTest
+ pass
def test_lookup(self):
track = self.library.lookup(self.tracks[0].uri)
diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py
index 2d455225..40c49709 100644
--- a/tests/backends/base/playback.py
+++ b/tests/backends/base/playback.py
@@ -1,16 +1,16 @@
import mock
-import multiprocessing
import random
import time
from mopidy.models import Track
from mopidy.gstreamer import GStreamer
-from tests import SkipTest
+from tests import unittest
from tests.backends.base import populate_playlist
# TODO Test 'playlist repeat', e.g. repeat=1,single=0
+
class PlaybackControllerTest(object):
tracks = []
@@ -520,7 +520,7 @@ class PlaybackControllerTest(object):
self.assert_(wrapper.called)
- @SkipTest # Blocks for 10ms
+ @unittest.SkipTest # Blocks for 10ms
@populate_playlist
def test_end_of_track_callback_gets_called(self):
self.playback.play()
@@ -555,7 +555,7 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_pause_when_stopped(self):
self.playback.pause()
- self.assertEqual(self.playback.state, self.playback.STOPPED)
+ self.assertEqual(self.playback.state, self.playback.PAUSED)
@populate_playlist
def test_pause_when_playing(self):
@@ -599,7 +599,7 @@ class PlaybackControllerTest(object):
self.playback.pause()
self.assertEqual(self.playback.resume(), None)
- @SkipTest # Uses sleep and might not work with LocalBackend
+ @unittest.SkipTest # Uses sleep and might not work with LocalBackend
@populate_playlist
def test_resume_continues_from_right_position(self):
self.playback.play()
@@ -668,7 +668,7 @@ class PlaybackControllerTest(object):
self.playback.seek(0)
self.assertEqual(self.playback.state, self.playback.PLAYING)
- @SkipTest
+ @unittest.SkipTest
@populate_playlist
def test_seek_beyond_end_of_song(self):
# FIXME need to decide return value
@@ -688,7 +688,7 @@ class PlaybackControllerTest(object):
self.playback.seek(self.current_playlist.tracks[-1].length * 100)
self.assertEqual(self.playback.state, self.playback.STOPPED)
- @SkipTest
+ @unittest.SkipTest
@populate_playlist
def test_seek_beyond_start_of_song(self):
# FIXME need to decide return value
@@ -741,7 +741,7 @@ class PlaybackControllerTest(object):
self.assertEqual(self.playback.time_position, 0)
- @SkipTest # Uses sleep and does might not work with LocalBackend
+ @unittest.SkipTest # Uses sleep and does might not work with LocalBackend
@populate_playlist
def test_time_position_when_playing(self):
self.playback.play()
@@ -750,7 +750,7 @@ class PlaybackControllerTest(object):
second = self.playback.time_position
self.assert_(second > first, '%s - %s' % (first, second))
- @SkipTest # Uses sleep
+ @unittest.SkipTest # Uses sleep
@populate_playlist
def test_time_position_when_paused(self):
self.playback.play()
diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py
index 839d5bed..54315e62 100644
--- a/tests/backends/base/stored_playlists.py
+++ b/tests/backends/base/stored_playlists.py
@@ -5,7 +5,8 @@ import tempfile
from mopidy import settings
from mopidy.models import Playlist
-from tests import SkipTest, path_to_data_dir
+from tests import unittest, path_to_data_dir
+
class StoredPlaylistsControllerTest(object):
def setUp(self):
@@ -78,11 +79,13 @@ class StoredPlaylistsControllerTest(object):
except LookupError as e:
self.assertEqual(u'"name=c" match no playlists', e[0])
+ @unittest.SkipTest
def test_lookup(self):
- raise SkipTest
+ pass
+ @unittest.SkipTest
def test_refresh(self):
- raise SkipTest
+ pass
def test_rename(self):
playlist = self.stored.create('test')
@@ -100,5 +103,6 @@ class StoredPlaylistsControllerTest(object):
self.stored.save(playlist)
self.assert_(playlist in self.stored.playlists)
+ @unittest.SkipTest
def test_playlist_with_unknown_track(self):
- raise SkipTest
+ pass
diff --git a/tests/backends/events_test.py b/tests/backends/events_test.py
new file mode 100644
index 00000000..d761676d
--- /dev/null
+++ b/tests/backends/events_test.py
@@ -0,0 +1,53 @@
+import mock
+
+from pykka.registry import ActorRegistry
+
+from mopidy.backends.dummy import DummyBackend
+from mopidy.listeners import BackendListener
+from mopidy.models import Track
+
+from tests import unittest
+
+
+@mock.patch.object(BackendListener, 'send')
+class BackendEventsTest(unittest.TestCase):
+ def setUp(self):
+ self.backend = DummyBackend.start().proxy()
+
+ def tearDown(self):
+ ActorRegistry.stop_all()
+
+ def test_pause_sends_track_playback_paused_event(self, send):
+ self.backend.current_playlist.add(Track(uri='a'))
+ self.backend.playback.play().get()
+ send.reset_mock()
+ self.backend.playback.pause().get()
+ self.assertEqual(send.call_args[0][0], 'track_playback_paused')
+
+ def test_resume_sends_track_playback_resumed(self, send):
+ self.backend.current_playlist.add(Track(uri='a'))
+ self.backend.playback.play()
+ self.backend.playback.pause().get()
+ send.reset_mock()
+ self.backend.playback.resume().get()
+ self.assertEqual(send.call_args[0][0], 'track_playback_resumed')
+
+ def test_play_sends_track_playback_started_event(self, send):
+ self.backend.current_playlist.add(Track(uri='a'))
+ send.reset_mock()
+ self.backend.playback.play().get()
+ self.assertEqual(send.call_args[0][0], 'track_playback_started')
+
+ def test_stop_sends_track_playback_ended_event(self, send):
+ self.backend.current_playlist.add(Track(uri='a'))
+ self.backend.playback.play().get()
+ send.reset_mock()
+ self.backend.playback.stop().get()
+ self.assertEqual(send.call_args_list[0][0][0], 'track_playback_ended')
+
+ def test_seek_sends_seeked_event(self, send):
+ self.backend.current_playlist.add(Track(uri='a', length=40000))
+ self.backend.playback.play().get()
+ send.reset_mock()
+ self.backend.playback.seek(1000).get()
+ self.assertEqual(send.call_args[0][0], 'seeked')
diff --git a/tests/backends/local/current_playlist_test.py b/tests/backends/local/current_playlist_test.py
index 6f72d7d5..a475a6fd 100644
--- a/tests/backends/local/current_playlist_test.py
+++ b/tests/backends/local/current_playlist_test.py
@@ -1,18 +1,16 @@
-import unittest
-
-# FIXME Our Windows build server does not support GStreamer yet
import sys
-if sys.platform == 'win32':
- from tests import SkipTest
- raise SkipTest
from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.models import Track
+from tests import unittest
from tests.backends.base.current_playlist import CurrentPlaylistControllerTest
from tests.backends.local import generate_song
+
+@unittest.skipIf(sys.platform == 'win32',
+ 'Our Windows build server does not support GStreamer yet')
class LocalCurrentPlaylistControllerTest(CurrentPlaylistControllerTest,
unittest.TestCase):
diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py
index 68ab22e9..046e747a 100644
--- a/tests/backends/local/library_test.py
+++ b/tests/backends/local/library_test.py
@@ -1,17 +1,14 @@
-import unittest
-
-# FIXME Our Windows build server does not support GStreamer yet
import sys
-if sys.platform == 'win32':
- from tests import SkipTest
- raise SkipTest
from mopidy import settings
from mopidy.backends.local import LocalBackend
-from tests import path_to_data_dir
+from tests import unittest, path_to_data_dir
from tests.backends.base.library import LibraryControllerTest
+
+@unittest.skipIf(sys.platform == 'win32',
+ 'Our Windows build server does not support GStreamer yet')
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
backend_class = LocalBackend
diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py
index 2cdeadb9..788fe33c 100644
--- a/tests/backends/local/playback_test.py
+++ b/tests/backends/local/playback_test.py
@@ -1,20 +1,17 @@
-import unittest
-
-# FIXME Our Windows build server does not support GStreamer yet
import sys
-if sys.platform == 'win32':
- from tests import SkipTest
- raise SkipTest
from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.models import Track
from mopidy.utils.path import path_to_uri
-from tests import path_to_data_dir
+from tests import unittest, path_to_data_dir
from tests.backends.base.playback import PlaybackControllerTest
from tests.backends.local import generate_song
+
+@unittest.skipIf(sys.platform == 'win32',
+ 'Our Windows build server does not support GStreamer yet')
class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
backend_class = LocalBackend
tracks = [Track(uri=generate_song(i), length=4464)
@@ -36,8 +33,8 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
track = Track(uri=uri, length=4464)
self.backend.current_playlist.add(track)
- def test_uri_handler(self):
- self.assert_('file://' in self.backend.uri_handlers)
+ def test_uri_scheme(self):
+ self.assert_('file' in self.backend.uri_schemes)
def test_play_mp3(self):
self.add_track('blank.mp3')
diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py
index b426e9ce..56be92c4 100644
--- a/tests/backends/local/stored_playlists_test.py
+++ b/tests/backends/local/stored_playlists_test.py
@@ -1,24 +1,19 @@
-import unittest
import os
-
-from tests import SkipTest
-
-# FIXME Our Windows build server does not support GStreamer yet
import sys
-if sys.platform == 'win32':
- raise SkipTest
from mopidy import settings
from mopidy.backends.local import LocalBackend
-from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track
from mopidy.utils.path import path_to_uri
-from tests import path_to_data_dir
-from tests.backends.base.stored_playlists import \
- StoredPlaylistsControllerTest
+from tests import unittest, path_to_data_dir
+from tests.backends.base.stored_playlists import (
+ StoredPlaylistsControllerTest)
from tests.backends.local import generate_song
+
+@unittest.skipIf(sys.platform == 'win32',
+ 'Our Windows build server does not support GStreamer yet')
class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
unittest.TestCase):
@@ -77,14 +72,18 @@ class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
self.assertEqual('test', self.stored.playlists[0].name)
self.assertEqual(track.uri, self.stored.playlists[0].tracks[0].uri)
+ @unittest.SkipTest
def test_santitising_of_playlist_filenames(self):
- raise SkipTest
+ pass
+ @unittest.SkipTest
def test_playlist_folder_is_createad(self):
- raise SkipTest
+ pass
+ @unittest.SkipTest
def test_create_sets_playlist_uri(self):
- raise SkipTest
+ pass
+ @unittest.SkipTest
def test_save_sets_playlist_uri(self):
- raise SkipTest
+ pass
diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py
index a4e9f317..1dceb737 100644
--- a/tests/backends/local/translator_test.py
+++ b/tests/backends/local/translator_test.py
@@ -2,13 +2,12 @@
import os
import tempfile
-import unittest
from mopidy.utils.path import path_to_uri
from mopidy.backends.local.translator import parse_m3u, parse_mpd_tag_cache
from mopidy.models import Track, Artist, Album
-from tests import SkipTest, path_to_data_dir
+from tests import unittest, path_to_data_dir
song1_path = path_to_data_dir('song1.mp3')
song2_path = path_to_data_dir('song2.mp3')
@@ -17,6 +16,9 @@ song1_uri = path_to_uri(song1_path)
song2_uri = path_to_uri(song2_path)
encoded_uri = path_to_uri(encoded_path)
+# FIXME use mock instead of tempfile.NamedTemporaryFile
+
+
class M3UToUriTest(unittest.TestCase):
def test_empty_file(self):
uris = parse_m3u(path_to_data_dir('empty.m3u'))
@@ -127,9 +129,10 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
self.assertEqual(track, list(tracks)[0])
+ @unittest.SkipTest
def test_misencoded_cache(self):
# FIXME not sure if this can happen
- raise SkipTest
+ pass
def test_cache_with_blank_track_info(self):
tracks = parse_mpd_tag_cache(path_to_data_dir('blank_tag_cache'),
diff --git a/tests/frontends/mpd/audio_output_test.py b/tests/frontends/mpd/audio_output_test.py
deleted file mode 100644
index 82d9e203..00000000
--- a/tests/frontends/mpd/audio_output_test.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
-
-class AudioOutputHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_enableoutput(self):
- result = self.dispatcher.handle_request(u'enableoutput "0"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_disableoutput(self):
- result = self.dispatcher.handle_request(u'disableoutput "0"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_outputs(self):
- result = self.dispatcher.handle_request(u'outputs')
- self.assert_(u'outputid: 0' in result)
- self.assert_(u'outputname: None' in result)
- self.assert_(u'outputenabled: 1' in result)
- self.assert_(u'OK' in result)
diff --git a/tests/frontends/mpd/command_list_test.py b/tests/frontends/mpd/command_list_test.py
deleted file mode 100644
index 8fd4c828..00000000
--- a/tests/frontends/mpd/command_list_test.py
+++ /dev/null
@@ -1,63 +0,0 @@
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
-from mopidy.mixers.dummy import DummyMixer
-
-class CommandListsTest(unittest.TestCase):
- def setUp(self):
- self.b = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = dispatcher.MpdDispatcher()
-
- def tearDown(self):
- self.b.stop().get()
- self.mixer.stop().get()
-
- def test_command_list_begin(self):
- result = self.dispatcher.handle_request(u'command_list_begin')
- self.assertEquals(result, [])
-
- def test_command_list_end(self):
- self.dispatcher.handle_request(u'command_list_begin')
- result = self.dispatcher.handle_request(u'command_list_end')
- self.assert_(u'OK' in result)
-
- def test_command_list_end_without_start_first_is_an_unknown_command(self):
- result = self.dispatcher.handle_request(u'command_list_end')
- self.assertEquals(result[0],
- u'ACK [5@0] {} unknown command "command_list_end"')
-
- def test_command_list_with_ping(self):
- self.dispatcher.handle_request(u'command_list_begin')
- self.assertEqual([], self.dispatcher.command_list)
- self.assertEqual(False, self.dispatcher.command_list_ok)
- self.dispatcher.handle_request(u'ping')
- self.assert_(u'ping' in self.dispatcher.command_list)
- result = self.dispatcher.handle_request(u'command_list_end')
- self.assert_(u'OK' in result)
- self.assertEqual(False, self.dispatcher.command_list)
-
- def test_command_list_with_error_returns_ack_with_correct_index(self):
- self.dispatcher.handle_request(u'command_list_begin')
- self.dispatcher.handle_request(u'play') # Known command
- self.dispatcher.handle_request(u'paly') # Unknown command
- result = self.dispatcher.handle_request(u'command_list_end')
- self.assertEqual(len(result), 1, result)
- self.assertEqual(result[0], u'ACK [5@1] {} unknown command "paly"')
-
- def test_command_list_ok_begin(self):
- result = self.dispatcher.handle_request(u'command_list_ok_begin')
- self.assertEquals(result, [])
-
- def test_command_list_ok_with_ping(self):
- self.dispatcher.handle_request(u'command_list_ok_begin')
- self.assertEqual([], self.dispatcher.command_list)
- self.assertEqual(True, self.dispatcher.command_list_ok)
- self.dispatcher.handle_request(u'ping')
- self.assert_(u'ping' in self.dispatcher.command_list)
- result = self.dispatcher.handle_request(u'command_list_end')
- self.assert_(u'list_OK' in result)
- self.assert_(u'OK' in result)
- self.assertEqual(False, self.dispatcher.command_list)
- self.assertEqual(False, self.dispatcher.command_list_ok)
diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py
deleted file mode 100644
index bc995a5e..00000000
--- a/tests/frontends/mpd/connection_test.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import mock
-import unittest
-
-from mopidy import settings
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.frontends.mpd.session import MpdSession
-from mopidy.mixers.dummy import DummyMixer
-
-class ConnectionHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.session = mock.Mock(spec=MpdSession)
- self.dispatcher = MpdDispatcher(session=self.session)
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
- settings.runtime.clear()
-
- def test_close_closes_the_client_connection(self):
- result = self.dispatcher.handle_request(u'close')
- self.assert_(self.session.close.called,
- u'Should call close() on MpdSession')
- self.assert_(u'OK' in result)
-
- def test_empty_request(self):
- result = self.dispatcher.handle_request(u'')
- self.assert_(u'OK' in result)
-
- def test_kill(self):
- result = self.dispatcher.handle_request(u'kill')
- self.assert_(u'ACK [4@0] {kill} you don\'t have permission for "kill"' in result)
-
- def test_valid_password_is_accepted(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- result = self.dispatcher.handle_request(u'password "topsecret"')
- self.assert_(u'OK' in result)
-
- def test_invalid_password_is_not_accepted(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- result = self.dispatcher.handle_request(u'password "secret"')
- self.assert_(u'ACK [3@0] {password} incorrect password' in result)
-
- def test_any_password_is_not_accepted_when_password_check_turned_off(self):
- settings.MPD_SERVER_PASSWORD = None
- result = self.dispatcher.handle_request(u'password "secret"')
- self.assert_(u'ACK [3@0] {password} incorrect password' in result)
-
- def test_ping(self):
- result = self.dispatcher.handle_request(u'ping')
- self.assert_(u'OK' in result)
diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py
index 7708ce31..bfa7c548 100644
--- a/tests/frontends/mpd/dispatcher_test.py
+++ b/tests/frontends/mpd/dispatcher_test.py
@@ -1,11 +1,12 @@
-import unittest
-
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.exceptions import MpdAckError
from mopidy.frontends.mpd.protocol import request_handlers, handle_request
from mopidy.mixers.dummy import DummyMixer
+from tests import unittest
+
+
class MpdDispatcherTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
diff --git a/tests/frontends/mpd/exception_test.py b/tests/frontends/mpd/exception_test.py
index df2cd65e..2ea3fe62 100644
--- a/tests/frontends/mpd/exception_test.py
+++ b/tests/frontends/mpd/exception_test.py
@@ -1,8 +1,9 @@
-import unittest
-
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdPermissionError,
MpdUnknownCommand, MpdSystemError, MpdNotImplemented)
+from tests import unittest
+
+
class MpdExceptionsTest(unittest.TestCase):
def test_key_error_wrapped_in_mpd_ack_error(self):
try:
diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py
deleted file mode 100644
index 3793db9e..00000000
--- a/tests/frontends/mpd/music_db_test.py
+++ /dev/null
@@ -1,412 +0,0 @@
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
-
-class MusicDatabaseHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_count(self):
- result = self.dispatcher.handle_request(u'count "tag" "needle"')
- self.assert_(u'songs: 0' in result)
- self.assert_(u'playtime: 0' in result)
- self.assert_(u'OK' in result)
-
- def test_findadd(self):
- result = self.dispatcher.handle_request(u'findadd "album" "what"')
- self.assert_(u'OK' in result)
-
- def test_listall(self):
- result = self.dispatcher.handle_request(
- u'listall "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_listallinfo(self):
- result = self.dispatcher.handle_request(
- u'listallinfo "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_lsinfo_without_path_returns_same_as_listplaylists(self):
- lsinfo_result = self.dispatcher.handle_request(u'lsinfo')
- listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
- self.assertEqual(lsinfo_result, listplaylists_result)
-
- def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
- lsinfo_result = self.dispatcher.handle_request(u'lsinfo ""')
- listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
- self.assertEqual(lsinfo_result, listplaylists_result)
-
- def test_lsinfo_for_root_returns_same_as_listplaylists(self):
- lsinfo_result = self.dispatcher.handle_request(u'lsinfo "/"')
- listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
- self.assertEqual(lsinfo_result, listplaylists_result)
-
- def test_update_without_uri(self):
- result = self.dispatcher.handle_request(u'update')
- self.assert_(u'OK' in result)
- self.assert_(u'updating_db: 0' in result)
-
- def test_update_with_uri(self):
- result = self.dispatcher.handle_request(u'update "file:///dev/urandom"')
- self.assert_(u'OK' in result)
- self.assert_(u'updating_db: 0' in result)
-
- def test_rescan_without_uri(self):
- result = self.dispatcher.handle_request(u'rescan')
- self.assert_(u'OK' in result)
- self.assert_(u'updating_db: 0' in result)
-
- def test_rescan_with_uri(self):
- result = self.dispatcher.handle_request(u'rescan "file:///dev/urandom"')
- self.assert_(u'OK' in result)
- self.assert_(u'updating_db: 0' in result)
-
-
-class MusicDatabaseFindTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_find_album(self):
- result = self.dispatcher.handle_request(u'find "album" "what"')
- self.assert_(u'OK' in result)
-
- def test_find_album_without_quotes(self):
- result = self.dispatcher.handle_request(u'find album "what"')
- self.assert_(u'OK' in result)
-
- def test_find_artist(self):
- result = self.dispatcher.handle_request(u'find "artist" "what"')
- self.assert_(u'OK' in result)
-
- def test_find_artist_without_quotes(self):
- result = self.dispatcher.handle_request(u'find artist "what"')
- self.assert_(u'OK' in result)
-
- def test_find_title(self):
- result = self.dispatcher.handle_request(u'find "title" "what"')
- self.assert_(u'OK' in result)
-
- def test_find_title_without_quotes(self):
- result = self.dispatcher.handle_request(u'find title "what"')
- self.assert_(u'OK' in result)
-
- def test_find_date(self):
- result = self.dispatcher.handle_request(u'find "date" "2002-01-01"')
- self.assert_(u'OK' in result)
-
- def test_find_date_without_quotes(self):
- result = self.dispatcher.handle_request(u'find date "2002-01-01"')
- self.assert_(u'OK' in result)
-
- def test_find_date_with_capital_d_and_incomplete_date(self):
- result = self.dispatcher.handle_request(u'find Date "2005"')
- self.assert_(u'OK' in result)
-
- def test_find_else_should_fail(self):
-
- result = self.dispatcher.handle_request(u'find "somethingelse" "what"')
- self.assertEqual(result[0], u'ACK [2@0] {find} incorrect arguments')
-
- def test_find_album_and_artist(self):
- result = self.dispatcher.handle_request(
- u'find album "album_what" artist "artist_what"')
- self.assert_(u'OK' in result)
-
-
-class MusicDatabaseListTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_list_foo_returns_ack(self):
- result = self.dispatcher.handle_request(u'list "foo"')
- self.assertEqual(result[0],
- u'ACK [2@0] {list} incorrect arguments')
-
- ### Artist
-
- def test_list_artist_with_quotes(self):
- result = self.dispatcher.handle_request(u'list "artist"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_without_quotes(self):
- result = self.dispatcher.handle_request(u'list artist')
- self.assert_(u'OK' in result)
-
- def test_list_artist_without_quotes_and_capitalized(self):
- result = self.dispatcher.handle_request(u'list Artist')
- self.assert_(u'OK' in result)
-
- def test_list_artist_with_query_of_one_token(self):
- result = self.dispatcher.handle_request(u'list "artist" "anartist"')
- self.assertEqual(result[0],
- u'ACK [2@0] {list} should be "Album" for 3 arguments')
-
- def test_list_artist_with_unknown_field_in_query_returns_ack(self):
- result = self.dispatcher.handle_request(u'list "artist" "foo" "bar"')
- self.assertEqual(result[0],
- u'ACK [2@0] {list} not able to parse args')
-
- def test_list_artist_by_artist(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "artist" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_by_album(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_by_full_date(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "date" "2001-01-01"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_by_year(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "date" "2001"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_by_genre(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "genre" "agenre"')
- self.assert_(u'OK' in result)
-
- def test_list_artist_by_artist_and_album(self):
- result = self.dispatcher.handle_request(
- u'list "artist" "artist" "anartist" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- ### Album
-
- def test_list_album_with_quotes(self):
- result = self.dispatcher.handle_request(u'list "album"')
- self.assert_(u'OK' in result)
-
- def test_list_album_without_quotes(self):
- result = self.dispatcher.handle_request(u'list album')
- self.assert_(u'OK' in result)
-
- def test_list_album_without_quotes_and_capitalized(self):
- result = self.dispatcher.handle_request(u'list Album')
- self.assert_(u'OK' in result)
-
- def test_list_album_with_artist_name(self):
- result = self.dispatcher.handle_request(u'list "album" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_artist(self):
- result = self.dispatcher.handle_request(
- u'list "album" "artist" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_album(self):
- result = self.dispatcher.handle_request(
- u'list "album" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_full_date(self):
- result = self.dispatcher.handle_request(
- u'list "album" "date" "2001-01-01"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_year(self):
- result = self.dispatcher.handle_request(
- u'list "album" "date" "2001"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_genre(self):
- result = self.dispatcher.handle_request(
- u'list "album" "genre" "agenre"')
- self.assert_(u'OK' in result)
-
- def test_list_album_by_artist_and_album(self):
- result = self.dispatcher.handle_request(
- u'list "album" "artist" "anartist" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- ### Date
-
- def test_list_date_with_quotes(self):
- result = self.dispatcher.handle_request(u'list "date"')
- self.assert_(u'OK' in result)
-
- def test_list_date_without_quotes(self):
- result = self.dispatcher.handle_request(u'list date')
- self.assert_(u'OK' in result)
-
- def test_list_date_without_quotes_and_capitalized(self):
- result = self.dispatcher.handle_request(u'list Date')
- self.assert_(u'OK' in result)
-
- def test_list_date_with_query_of_one_token(self):
- result = self.dispatcher.handle_request(u'list "date" "anartist"')
- self.assertEqual(result[0],
- u'ACK [2@0] {list} should be "Album" for 3 arguments')
-
- def test_list_date_by_artist(self):
- result = self.dispatcher.handle_request(
- u'list "date" "artist" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_list_date_by_album(self):
- result = self.dispatcher.handle_request(
- u'list "date" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- def test_list_date_by_full_date(self):
- result = self.dispatcher.handle_request(
- u'list "date" "date" "2001-01-01"')
- self.assert_(u'OK' in result)
-
- def test_list_date_by_year(self):
- result = self.dispatcher.handle_request(u'list "date" "date" "2001"')
- self.assert_(u'OK' in result)
-
- def test_list_date_by_genre(self):
- result = self.dispatcher.handle_request(u'list "date" "genre" "agenre"')
- self.assert_(u'OK' in result)
-
- def test_list_date_by_artist_and_album(self):
- result = self.dispatcher.handle_request(
- u'list "date" "artist" "anartist" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- ### Genre
-
- def test_list_genre_with_quotes(self):
- result = self.dispatcher.handle_request(u'list "genre"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_without_quotes(self):
- result = self.dispatcher.handle_request(u'list genre')
- self.assert_(u'OK' in result)
-
- def test_list_genre_without_quotes_and_capitalized(self):
- result = self.dispatcher.handle_request(u'list Genre')
- self.assert_(u'OK' in result)
-
- def test_list_genre_with_query_of_one_token(self):
- result = self.dispatcher.handle_request(u'list "genre" "anartist"')
- self.assertEqual(result[0],
- u'ACK [2@0] {list} should be "Album" for 3 arguments')
-
- def test_list_genre_by_artist(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "artist" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_by_album(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "album" "analbum"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_by_full_date(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "date" "2001-01-01"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_by_year(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "date" "2001"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_by_genre(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "genre" "agenre"')
- self.assert_(u'OK' in result)
-
- def test_list_genre_by_artist_and_album(self):
- result = self.dispatcher.handle_request(
- u'list "genre" "artist" "anartist" "album" "analbum"')
- self.assert_(u'OK' in result)
-
-
-class MusicDatabaseSearchTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_search_album(self):
- result = self.dispatcher.handle_request(u'search "album" "analbum"')
- self.assert_(u'OK' in result)
-
- def test_search_album_without_quotes(self):
- result = self.dispatcher.handle_request(u'search album "analbum"')
- self.assert_(u'OK' in result)
-
- def test_search_artist(self):
- result = self.dispatcher.handle_request(u'search "artist" "anartist"')
- self.assert_(u'OK' in result)
-
- def test_search_artist_without_quotes(self):
- result = self.dispatcher.handle_request(u'search artist "anartist"')
- self.assert_(u'OK' in result)
-
- def test_search_filename(self):
- result = self.dispatcher.handle_request(
- u'search "filename" "afilename"')
- self.assert_(u'OK' in result)
-
- def test_search_filename_without_quotes(self):
- result = self.dispatcher.handle_request(u'search filename "afilename"')
- self.assert_(u'OK' in result)
-
- def test_search_title(self):
- result = self.dispatcher.handle_request(u'search "title" "atitle"')
- self.assert_(u'OK' in result)
-
- def test_search_title_without_quotes(self):
- result = self.dispatcher.handle_request(u'search title "atitle"')
- self.assert_(u'OK' in result)
-
- def test_search_any(self):
- result = self.dispatcher.handle_request(u'search "any" "anything"')
- self.assert_(u'OK' in result)
-
- def test_search_any_without_quotes(self):
- result = self.dispatcher.handle_request(u'search any "anything"')
- self.assert_(u'OK' in result)
-
- def test_search_date(self):
- result = self.dispatcher.handle_request(u'search "date" "2002-01-01"')
- self.assert_(u'OK' in result)
-
- def test_search_date_without_quotes(self):
- result = self.dispatcher.handle_request(u'search date "2002-01-01"')
- self.assert_(u'OK' in result)
-
- def test_search_date_with_capital_d_and_incomplete_date(self):
- result = self.dispatcher.handle_request(u'search Date "2005"')
- self.assert_(u'OK' in result)
-
- def test_search_else_should_fail(self):
- result = self.dispatcher.handle_request(
- u'search "sometype" "something"')
- self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments')
-
-
diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py
new file mode 100644
index 00000000..b54906be
--- /dev/null
+++ b/tests/frontends/mpd/protocol/__init__.py
@@ -0,0 +1,62 @@
+import mock
+
+from mopidy import settings
+from mopidy.backends import dummy as backend
+from mopidy.frontends import mpd
+from mopidy.mixers import dummy as mixer
+
+from tests import unittest
+
+
+class MockConnection(mock.Mock):
+ def __init__(self, *args, **kwargs):
+ super(MockConnection, self).__init__(*args, **kwargs)
+ self.host = mock.sentinel.host
+ self.port = mock.sentinel.port
+ self.response = []
+
+ def queue_send(self, data):
+ lines = (line for line in data.split('\n') if line)
+ self.response.extend(lines)
+
+
+class BaseTestCase(unittest.TestCase):
+ def setUp(self):
+ self.backend = backend.DummyBackend.start().proxy()
+ self.mixer = mixer.DummyMixer.start().proxy()
+
+ self.connection = MockConnection()
+ 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):
+ self.connection.response = []
+ 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)))
+
+ def assertEqualResponse(self, value):
+ self.assertEqual(1, len(self.connection.response))
+ self.assertEqual(value, self.connection.response[0])
diff --git a/tests/frontends/mpd/protocol/audio_output_test.py b/tests/frontends/mpd/protocol/audio_output_test.py
new file mode 100644
index 00000000..3bb8dce8
--- /dev/null
+++ b/tests/frontends/mpd/protocol/audio_output_test.py
@@ -0,0 +1,18 @@
+from tests.frontends.mpd import protocol
+
+
+class AudioOutputHandlerTest(protocol.BaseTestCase):
+ def test_enableoutput(self):
+ self.sendRequest(u'enableoutput "0"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_disableoutput(self):
+ self.sendRequest(u'disableoutput "0"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_outputs(self):
+ self.sendRequest(u'outputs')
+ self.assertInResponse(u'outputid: 0')
+ self.assertInResponse(u'outputname: None')
+ self.assertInResponse(u'outputenabled: 1')
+ self.assertInResponse(u'OK')
diff --git a/tests/frontends/mpd/authentication_test.py b/tests/frontends/mpd/protocol/authentication_test.py
similarity index 53%
rename from tests/frontends/mpd/authentication_test.py
rename to tests/frontends/mpd/protocol/authentication_test.py
index d795d726..20422f5b 100644
--- a/tests/frontends/mpd/authentication_test.py
+++ b/tests/frontends/mpd/protocol/authentication_test.py
@@ -1,63 +1,62 @@
-import mock
-import unittest
-
from mopidy import settings
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.frontends.mpd.session import MpdSession
-class AuthenticationTest(unittest.TestCase):
- def setUp(self):
- self.session = mock.Mock(spec=MpdSession)
- self.dispatcher = MpdDispatcher(session=self.session)
+from tests.frontends.mpd import protocol
- def tearDown(self):
- settings.runtime.clear()
+class AuthenticationTest(protocol.BaseTestCase):
def test_authentication_with_valid_password_is_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'password "topsecret"')
+
+ self.sendRequest(u'password "topsecret"')
self.assertTrue(self.dispatcher.authenticated)
- self.assert_(u'OK' in response)
+ self.assertInResponse(u'OK')
def test_authentication_with_invalid_password_is_not_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'password "secret"')
+
+ self.sendRequest(u'password "secret"')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(u'ACK [3@0] {password} incorrect password' in response)
+ self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
def test_authentication_with_anything_when_password_check_turned_off(self):
settings.MPD_SERVER_PASSWORD = None
- response = self.dispatcher.handle_request(u'any request at all')
+
+ self.sendRequest(u'any request at all')
self.assertTrue(self.dispatcher.authenticated)
- self.assert_('ACK [5@0] {} unknown command "any"' in response)
+ self.assertEqualResponse('ACK [5@0] {} unknown command "any"')
def test_anything_when_not_authenticated_should_fail(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'any request at all')
+
+ self.sendRequest(u'any request at all')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(
- u'ACK [4@0] {any} you don\'t have permission for "any"' in response)
+ self.assertEqualResponse(
+ u'ACK [4@0] {any} you don\'t have permission for "any"')
def test_close_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'close')
+
+ self.sendRequest(u'close')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(u'OK' in response)
+ self.assertInResponse(u'OK')
def test_commands_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'commands')
+
+ self.sendRequest(u'commands')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(u'OK' in response)
+ self.assertInResponse(u'OK')
def test_notcommands_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'notcommands')
+
+ self.sendRequest(u'notcommands')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(u'OK' in response)
+ self.assertInResponse(u'OK')
def test_ping_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- response = self.dispatcher.handle_request(u'ping')
+
+ self.sendRequest(u'ping')
self.assertFalse(self.dispatcher.authenticated)
- self.assert_(u'OK' in response)
+ self.assertInResponse(u'OK')
diff --git a/tests/frontends/mpd/protocol/command_list_test.py b/tests/frontends/mpd/protocol/command_list_test.py
new file mode 100644
index 00000000..a81725ad
--- /dev/null
+++ b/tests/frontends/mpd/protocol/command_list_test.py
@@ -0,0 +1,54 @@
+from tests.frontends.mpd import protocol
+
+
+class CommandListsTest(protocol.BaseTestCase):
+ def test_command_list_begin(self):
+ response = self.sendRequest(u'command_list_begin')
+ self.assertEquals([], response)
+
+ def test_command_list_end(self):
+ self.sendRequest(u'command_list_begin')
+ self.sendRequest(u'command_list_end')
+ self.assertInResponse(u'OK')
+
+ def test_command_list_end_without_start_first_is_an_unknown_command(self):
+ self.sendRequest(u'command_list_end')
+ self.assertEqualResponse(
+ u'ACK [5@0] {} unknown command "command_list_end"')
+
+ def test_command_list_with_ping(self):
+ self.sendRequest(u'command_list_begin')
+ self.assertEqual([], self.dispatcher.command_list)
+ self.assertEqual(False, self.dispatcher.command_list_ok)
+ self.sendRequest(u'ping')
+ self.assert_(u'ping' in self.dispatcher.command_list)
+ self.sendRequest(u'command_list_end')
+ self.assertInResponse(u'OK')
+ self.assertEqual(False, self.dispatcher.command_list)
+
+ def test_command_list_with_error_returns_ack_with_correct_index(self):
+ self.sendRequest(u'command_list_begin')
+ self.sendRequest(u'play') # Known command
+ self.sendRequest(u'paly') # Unknown command
+ self.sendRequest(u'command_list_end')
+ self.assertEqualResponse(u'ACK [5@1] {} unknown command "paly"')
+
+ def test_command_list_ok_begin(self):
+ response = self.sendRequest(u'command_list_ok_begin')
+ self.assertEquals([], response)
+
+ def test_command_list_ok_with_ping(self):
+ self.sendRequest(u'command_list_ok_begin')
+ self.assertEqual([], self.dispatcher.command_list)
+ self.assertEqual(True, self.dispatcher.command_list_ok)
+ self.sendRequest(u'ping')
+ self.assert_(u'ping' in self.dispatcher.command_list)
+ self.sendRequest(u'command_list_end')
+ self.assertInResponse(u'list_OK')
+ self.assertInResponse(u'OK')
+ self.assertEqual(False, self.dispatcher.command_list)
+ self.assertEqual(False, self.dispatcher.command_list_ok)
+
+ # FIXME this should also include the special handling of idle within a
+ # command list. That is that once a idle/noidle command is found inside a
+ # commad list, the rest of the list seems to be ignored.
diff --git a/tests/frontends/mpd/protocol/connection_test.py b/tests/frontends/mpd/protocol/connection_test.py
new file mode 100644
index 00000000..cd08313f
--- /dev/null
+++ b/tests/frontends/mpd/protocol/connection_test.py
@@ -0,0 +1,44 @@
+from mock import patch
+
+from mopidy import settings
+
+from tests.frontends.mpd import protocol
+
+
+class ConnectionHandlerTest(protocol.BaseTestCase):
+ def test_close_closes_the_client_connection(self):
+ with patch.object(self.session, 'close') as close_mock:
+ response = self.sendRequest(u'close')
+ close_mock.assertEqualResponsecalled_once_with()
+ self.assertEqualResponse(u'OK')
+
+ def test_empty_request(self):
+ self.sendRequest(u'')
+ self.assertEqualResponse(u'OK')
+
+ self.sendRequest(u' ')
+ self.assertEqualResponse(u'OK')
+
+ def test_kill(self):
+ self.sendRequest(u'kill')
+ self.assertEqualResponse(
+ u'ACK [4@0] {kill} you don\'t have permission for "kill"')
+
+ def test_valid_password_is_accepted(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ self.sendRequest(u'password "topsecret"')
+ self.assertEqualResponse(u'OK')
+
+ def test_invalid_password_is_not_accepted(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ self.sendRequest(u'password "secret"')
+ self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
+
+ def test_any_password_is_not_accepted_when_password_check_turned_off(self):
+ settings.MPD_SERVER_PASSWORD = None
+ self.sendRequest(u'password "secret"')
+ self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
+
+ def test_ping(self):
+ self.sendRequest(u'ping')
+ self.assertEqualResponse(u'OK')
diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/protocol/current_playlist_test.py
similarity index 59%
rename from tests/frontends/mpd/current_playlist_test.py
rename to tests/frontends/mpd/protocol/current_playlist_test.py
index c7f47429..343b230b 100644
--- a/tests/frontends/mpd/current_playlist_test.py
+++ b/tests/frontends/mpd/protocol/current_playlist_test.py
@@ -1,20 +1,9 @@
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
-class CurrentPlaylistHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
+from tests.frontends.mpd import protocol
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
+class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
def test_add(self):
needle = Track(uri='dummy://foo')
self.backend.library.provider.dummy_library = [
@@ -22,21 +11,21 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'add "dummy://foo"')
- self.assertEqual(len(result), 1)
- self.assertEqual(result[0], u'OK')
+
+ self.sendRequest(u'add "dummy://foo"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
+ self.assertEqualResponse(u'OK')
def test_add_with_uri_not_found_in_library_should_ack(self):
- result = self.dispatcher.handle_request(u'add "dummy://foo"')
- self.assertEqual(result[0],
+ self.sendRequest(u'add "dummy://foo"')
+ self.assertEqualResponse(
u'ACK [50@0] {add} directory or file not found')
def test_add_with_empty_uri_should_add_all_known_tracks_and_ok(self):
- result = self.dispatcher.handle_request(u'add ""')
+ self.sendRequest(u'add ""')
# TODO check that we add all tracks (we currently don't)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_addid_without_songpos(self):
needle = Track(uri='dummy://foo')
@@ -45,16 +34,17 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'addid "dummy://foo"')
+
+ self.sendRequest(u'addid "dummy://foo"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
- self.assert_(u'Id: %d' %
- self.backend.current_playlist.cp_tracks.get()[5][0] in result)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'Id: %d' %
+ self.backend.current_playlist.cp_tracks.get()[5][0])
+ self.assertInResponse(u'OK')
def test_addid_with_empty_uri_acks(self):
- result = self.dispatcher.handle_request(u'addid ""')
- self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
+ self.sendRequest(u'addid ""')
+ self.assertEqualResponse(u'ACK [50@0] {addid} No such song')
def test_addid_with_songpos(self):
needle = Track(uri='dummy://foo')
@@ -63,12 +53,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'addid "dummy://foo" "3"')
+
+ self.sendRequest(u'addid "dummy://foo" "3"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.backend.current_playlist.tracks.get()[3], needle)
- self.assert_(u'Id: %d' %
- self.backend.current_playlist.cp_tracks.get()[3][0] in result)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'Id: %d' %
+ self.backend.current_playlist.cp_tracks.get()[3][0])
+ self.assertInResponse(u'OK')
def test_addid_with_songpos_out_of_bounds_should_ack(self):
needle = Track(uri='dummy://foo')
@@ -77,83 +68,93 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'addid "dummy://foo" "6"')
- self.assertEqual(result[0], u'ACK [2@0] {addid} Bad song index')
+
+ self.sendRequest(u'addid "dummy://foo" "6"')
+ self.assertEqualResponse(u'ACK [2@0] {addid} Bad song index')
def test_addid_with_uri_not_found_in_library_should_ack(self):
- result = self.dispatcher.handle_request(u'addid "dummy://foo"')
- self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
+ self.sendRequest(u'addid "dummy://foo"')
+ self.assertEqualResponse(u'ACK [50@0] {addid} No such song')
def test_clear(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'clear')
+
+ self.sendRequest(u'clear')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
self.assertEqual(self.backend.playback.current_track.get(), None)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_delete_songpos(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'delete "%d"' %
+
+ self.sendRequest(u'delete "%d"' %
self.backend.current_playlist.cp_tracks.get()[2][0])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 4)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_delete_songpos_out_of_bounds(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'delete "5"')
+
+ self.sendRequest(u'delete "5"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
+ self.assertEqualResponse(u'ACK [2@0] {delete} Bad song index')
def test_delete_open_range(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'delete "1:"')
+
+ self.sendRequest(u'delete "1:"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_delete_closed_range(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'delete "1:3"')
+
+ self.sendRequest(u'delete "1:3"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 3)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_delete_range_out_of_bounds(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- result = self.dispatcher.handle_request(u'delete "5:7"')
+
+ self.sendRequest(u'delete "5:7"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
+ self.assertEqualResponse(u'ACK [2@0] {delete} Bad song index')
def test_deleteid(self):
self.backend.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
- result = self.dispatcher.handle_request(u'deleteid "1"')
+
+ self.sendRequest(u'deleteid "1"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_deleteid_does_not_exist(self):
self.backend.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
- result = self.dispatcher.handle_request(u'deleteid "12345"')
+
+ self.sendRequest(u'deleteid "12345"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
- self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song')
+ self.assertEqualResponse(u'ACK [50@0] {deleteid} No such song')
def test_move_songpos(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'move "1" "0"')
+
+ self.sendRequest(u'move "1" "0"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'b')
self.assertEqual(tracks[1].name, 'a')
@@ -161,14 +162,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'e')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_move_open_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'move "2:" "0"')
+
+ self.sendRequest(u'move "2:" "0"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'c')
self.assertEqual(tracks[1].name, 'd')
@@ -176,14 +178,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'f')
self.assertEqual(tracks[4].name, 'a')
self.assertEqual(tracks[5].name, 'b')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_move_closed_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'move "1:3" "0"')
+
+ self.sendRequest(u'move "1:3" "0"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'b')
self.assertEqual(tracks[1].name, 'c')
@@ -191,14 +194,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'e')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_moveid(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'moveid "4" "2"')
+
+ self.sendRequest(u'moveid "4" "2"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'b')
@@ -206,179 +210,182 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'c')
self.assertEqual(tracks[4].name, 'd')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_playlist_returns_same_as_playlistinfo(self):
- playlist_result = self.dispatcher.handle_request(u'playlist')
- playlistinfo_result = self.dispatcher.handle_request(u'playlistinfo')
- self.assertEqual(playlist_result, playlistinfo_result)
+ playlist_response = self.sendRequest(u'playlist')
+ playlistinfo_response = self.sendRequest(u'playlistinfo')
+ self.assertEqual(playlist_response, playlistinfo_response)
def test_playlistfind(self):
- result = self.dispatcher.handle_request(u'playlistfind "tag" "needle"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'playlistfind "tag" "needle"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_playlistfind_by_filename_not_in_current_playlist(self):
- result = self.dispatcher.handle_request(
- u'playlistfind "filename" "file:///dev/null"')
- self.assertEqual(len(result), 1)
- self.assert_(u'OK' in result)
+ self.sendRequest(u'playlistfind "filename" "file:///dev/null"')
+ self.assertEqualResponse(u'OK')
def test_playlistfind_by_filename_without_quotes(self):
- result = self.dispatcher.handle_request(
- u'playlistfind filename "file:///dev/null"')
- self.assertEqual(len(result), 1)
- self.assert_(u'OK' in result)
+ self.sendRequest(u'playlistfind filename "file:///dev/null"')
+ self.assertEqualResponse(u'OK')
def test_playlistfind_by_filename_in_current_playlist(self):
self.backend.current_playlist.append([
Track(uri='file:///exists')])
- result = self.dispatcher.handle_request(
- u'playlistfind filename "file:///exists"')
- self.assert_(u'file: file:///exists' in result)
- self.assert_(u'Id: 0' in result)
- self.assert_(u'Pos: 0' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest( u'playlistfind filename "file:///exists"')
+ self.assertInResponse(u'file: file:///exists')
+ self.assertInResponse(u'Id: 0')
+ self.assertInResponse(u'Pos: 0')
+ self.assertInResponse(u'OK')
def test_playlistid_without_songid(self):
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
- result = self.dispatcher.handle_request(u'playlistid')
- self.assert_(u'Title: a' in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistid')
+ self.assertInResponse(u'Title: a')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'OK')
def test_playlistid_with_songid(self):
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
- result = self.dispatcher.handle_request(u'playlistid "1"')
- self.assert_(u'Title: a' not in result)
- self.assert_(u'Id: 0' not in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'Id: 1' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistid "1"')
+ self.assertNotInResponse(u'Title: a')
+ self.assertNotInResponse(u'Id: 0')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'Id: 1')
+ self.assertInResponse(u'OK')
def test_playlistid_with_not_existing_songid_fails(self):
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
- result = self.dispatcher.handle_request(u'playlistid "25"')
- self.assertEqual(result[0], u'ACK [50@0] {playlistid} No such song')
+
+ self.sendRequest(u'playlistid "25"')
+ self.assertEqualResponse(u'ACK [50@0] {playlistid} No such song')
def test_playlistinfo_without_songpos_or_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'playlistinfo')
- self.assert_(u'Title: a' in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'Title: d' in result)
- self.assert_(u'Title: e' in result)
- self.assert_(u'Title: f' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistinfo')
+ self.assertInResponse(u'Title: a')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'Title: d')
+ self.assertInResponse(u'Title: e')
+ self.assertInResponse(u'Title: f')
+ self.assertInResponse(u'OK')
def test_playlistinfo_with_songpos(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'playlistinfo "4"')
- self.assert_(u'Title: a' not in result)
- self.assert_(u'Title: b' not in result)
- self.assert_(u'Title: c' not in result)
- self.assert_(u'Title: d' not in result)
- self.assert_(u'Title: e' in result)
- self.assert_(u'Title: f' not in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistinfo "4"')
+ self.assertNotInResponse(u'Title: a')
+ self.assertNotInResponse(u'Title: b')
+ self.assertNotInResponse(u'Title: c')
+ self.assertNotInResponse(u'Title: d')
+ self.assertInResponse(u'Title: e')
+ self.assertNotInResponse(u'Title: f')
+ self.assertInResponse(u'OK')
def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self):
- result1 = self.dispatcher.handle_request(u'playlistinfo "-1"')
- result2 = self.dispatcher.handle_request(u'playlistinfo')
- self.assertEqual(result1, result2)
+ response1 = self.sendRequest(u'playlistinfo "-1"')
+ response2 = self.sendRequest(u'playlistinfo')
+ self.assertEqual(response1, response2)
def test_playlistinfo_with_open_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'playlistinfo "2:"')
- self.assert_(u'Title: a' not in result)
- self.assert_(u'Title: b' not in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'Title: d' in result)
- self.assert_(u'Title: e' in result)
- self.assert_(u'Title: f' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistinfo "2:"')
+ self.assertNotInResponse(u'Title: a')
+ self.assertNotInResponse(u'Title: b')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'Title: d')
+ self.assertInResponse(u'Title: e')
+ self.assertInResponse(u'Title: f')
+ self.assertInResponse(u'OK')
def test_playlistinfo_with_closed_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'playlistinfo "2:4"')
- self.assert_(u'Title: a' not in result)
- self.assert_(u'Title: b' not in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'Title: d' in result)
- self.assert_(u'Title: e' not in result)
- self.assert_(u'Title: f' not in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playlistinfo "2:4"')
+ self.assertNotInResponse(u'Title: a')
+ self.assertNotInResponse(u'Title: b')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'Title: d')
+ self.assertNotInResponse(u'Title: e')
+ self.assertNotInResponse(u'Title: f')
+ self.assertInResponse(u'OK')
def test_playlistinfo_with_too_high_start_of_range_returns_arg_error(self):
- result = self.dispatcher.handle_request(u'playlistinfo "10:20"')
- self.assert_(u'ACK [2@0] {playlistinfo} Bad song index' in result)
+ self.sendRequest(u'playlistinfo "10:20"')
+ self.assertEqualResponse(u'ACK [2@0] {playlistinfo} Bad song index')
def test_playlistinfo_with_too_high_end_of_range_returns_ok(self):
- result = self.dispatcher.handle_request(u'playlistinfo "0:20"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'playlistinfo "0:20"')
+ self.assertInResponse(u'OK')
def test_playlistsearch(self):
- result = self.dispatcher.handle_request(
- u'playlistsearch "any" "needle"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest( u'playlistsearch "any" "needle"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_playlistsearch_without_quotes(self):
- result = self.dispatcher.handle_request(u'playlistsearch any "needle"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'playlistsearch any "needle"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_plchanges(self):
self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
- result = self.dispatcher.handle_request(u'plchanges "0"')
- self.assert_(u'Title: a' in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'plchanges "0"')
+ self.assertInResponse(u'Title: a')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'OK')
def test_plchanges_with_minus_one_returns_entire_playlist(self):
self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
- result = self.dispatcher.handle_request(u'plchanges "-1"')
- self.assert_(u'Title: a' in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'plchanges "-1"')
+ self.assertInResponse(u'Title: a')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'OK')
def test_plchanges_without_quotes_works(self):
self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
- result = self.dispatcher.handle_request(u'plchanges 0')
- self.assert_(u'Title: a' in result)
- self.assert_(u'Title: b' in result)
- self.assert_(u'Title: c' in result)
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'plchanges 0')
+ self.assertInResponse(u'Title: a')
+ self.assertInResponse(u'Title: b')
+ self.assertInResponse(u'Title: c')
+ self.assertInResponse(u'OK')
def test_plchangesposid(self):
self.backend.current_playlist.append([Track(), Track(), Track()])
- result = self.dispatcher.handle_request(u'plchangesposid "0"')
+
+ self.sendRequest(u'plchangesposid "0"')
cp_tracks = self.backend.current_playlist.cp_tracks.get()
- self.assert_(u'cpos: 0' in result)
- self.assert_(u'Id: %d' % cp_tracks[0][0]
- in result)
- self.assert_(u'cpos: 2' in result)
- self.assert_(u'Id: %d' % cp_tracks[1][0]
- in result)
- self.assert_(u'cpos: 2' in result)
- self.assert_(u'Id: %d' % cp_tracks[2][0]
- in result)
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'cpos: 0')
+ self.assertInResponse(u'Id: %d' % cp_tracks[0][0])
+ self.assertInResponse(u'cpos: 2')
+ self.assertInResponse(u'Id: %d' % cp_tracks[1][0])
+ self.assertInResponse(u'cpos: 2')
+ self.assertInResponse(u'Id: %d' % cp_tracks[2][0])
+ self.assertInResponse(u'OK')
def test_shuffle_without_range(self):
self.backend.current_playlist.append([
@@ -386,9 +393,10 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
version = self.backend.current_playlist.version.get()
- result = self.dispatcher.handle_request(u'shuffle')
+
+ self.sendRequest(u'shuffle')
self.assert_(version < self.backend.current_playlist.version.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_shuffle_with_open_range(self):
self.backend.current_playlist.append([
@@ -396,14 +404,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
version = self.backend.current_playlist.version.get()
- result = self.dispatcher.handle_request(u'shuffle "4:"')
+
+ self.sendRequest(u'shuffle "4:"')
self.assert_(version < self.backend.current_playlist.version.get())
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'b')
self.assertEqual(tracks[2].name, 'c')
self.assertEqual(tracks[3].name, 'd')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_shuffle_with_closed_range(self):
self.backend.current_playlist.append([
@@ -411,21 +420,23 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
version = self.backend.current_playlist.version.get()
- result = self.dispatcher.handle_request(u'shuffle "1:3"')
+
+ self.sendRequest(u'shuffle "1:3"')
self.assert_(version < self.backend.current_playlist.version.get())
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'e')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_swap(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'swap "1" "4"')
+
+ self.sendRequest(u'swap "1" "4"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'e')
@@ -433,14 +444,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'b')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_swapid(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.dispatcher.handle_request(u'swapid "1" "4"')
+
+ self.sendRequest(u'swapid "1" "4"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'e')
@@ -448,4 +460,4 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'b')
self.assertEqual(tracks[5].name, 'f')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
diff --git a/tests/frontends/mpd/protocol/idle_test.py b/tests/frontends/mpd/protocol/idle_test.py
new file mode 100644
index 00000000..ae23c88e
--- /dev/null
+++ b/tests/frontends/mpd/protocol/idle_test.py
@@ -0,0 +1,206 @@
+from mock import patch
+
+from mopidy.frontends.mpd.protocol.status import SUBSYSTEMS
+
+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')
diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py
new file mode 100644
index 00000000..088502c4
--- /dev/null
+++ b/tests/frontends/mpd/protocol/music_db_test.py
@@ -0,0 +1,344 @@
+from tests.frontends.mpd import protocol
+
+
+class MusicDatabaseHandlerTest(protocol.BaseTestCase):
+ def test_count(self):
+ self.sendRequest(u'count "tag" "needle"')
+ self.assertInResponse(u'songs: 0')
+ self.assertInResponse(u'playtime: 0')
+ self.assertInResponse(u'OK')
+
+ def test_findadd(self):
+ self.sendRequest(u'findadd "album" "what"')
+ self.assertInResponse(u'OK')
+
+ def test_listall(self):
+ self.sendRequest(u'listall "file:///dev/urandom"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_listallinfo(self):
+ self.sendRequest(u'listallinfo "file:///dev/urandom"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_lsinfo_without_path_returns_same_as_listplaylists(self):
+ lsinfo_response = self.sendRequest(u'lsinfo')
+ listplaylists_response = self.sendRequest(u'listplaylists')
+ self.assertEqual(lsinfo_response, listplaylists_response)
+
+ def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
+ lsinfo_response = self.sendRequest(u'lsinfo ""')
+ listplaylists_response = self.sendRequest(u'listplaylists')
+ self.assertEqual(lsinfo_response, listplaylists_response)
+
+ def test_lsinfo_for_root_returns_same_as_listplaylists(self):
+ lsinfo_response = self.sendRequest(u'lsinfo "/"')
+ listplaylists_response = self.sendRequest(u'listplaylists')
+ self.assertEqual(lsinfo_response, listplaylists_response)
+
+ def test_update_without_uri(self):
+ self.sendRequest(u'update')
+ self.assertInResponse(u'updating_db: 0')
+ self.assertInResponse(u'OK')
+
+ def test_update_with_uri(self):
+ self.sendRequest(u'update "file:///dev/urandom"')
+ self.assertInResponse(u'updating_db: 0')
+ self.assertInResponse(u'OK')
+
+ def test_rescan_without_uri(self):
+ self.sendRequest(u'rescan')
+ self.assertInResponse(u'updating_db: 0')
+ self.assertInResponse(u'OK')
+
+ def test_rescan_with_uri(self):
+ self.sendRequest(u'rescan "file:///dev/urandom"')
+ self.assertInResponse(u'updating_db: 0')
+ self.assertInResponse(u'OK')
+
+
+class MusicDatabaseFindTest(protocol.BaseTestCase):
+ def test_find_album(self):
+ self.sendRequest(u'find "album" "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_album_without_quotes(self):
+ self.sendRequest(u'find album "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_artist(self):
+ self.sendRequest(u'find "artist" "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_artist_without_quotes(self):
+ self.sendRequest(u'find artist "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_title(self):
+ self.sendRequest(u'find "title" "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_title_without_quotes(self):
+ self.sendRequest(u'find title "what"')
+ self.assertInResponse(u'OK')
+
+ def test_find_date(self):
+ self.sendRequest(u'find "date" "2002-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_find_date_without_quotes(self):
+ self.sendRequest(u'find date "2002-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_find_date_with_capital_d_and_incomplete_date(self):
+ self.sendRequest(u'find Date "2005"')
+ self.assertInResponse(u'OK')
+
+ def test_find_else_should_fail(self):
+ self.sendRequest(u'find "somethingelse" "what"')
+ self.assertEqualResponse(u'ACK [2@0] {find} incorrect arguments')
+
+ def test_find_album_and_artist(self):
+ self.sendRequest(u'find album "album_what" artist "artist_what"')
+ self.assertInResponse(u'OK')
+
+
+class MusicDatabaseListTest(protocol.BaseTestCase):
+ def test_list_foo_returns_ack(self):
+ self.sendRequest(u'list "foo"')
+ self.assertEqualResponse(u'ACK [2@0] {list} incorrect arguments')
+
+ ### Artist
+
+ def test_list_artist_with_quotes(self):
+ self.sendRequest(u'list "artist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_without_quotes(self):
+ self.sendRequest(u'list artist')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_without_quotes_and_capitalized(self):
+ self.sendRequest(u'list Artist')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_with_query_of_one_token(self):
+ self.sendRequest(u'list "artist" "anartist"')
+ self.assertEqualResponse(
+ u'ACK [2@0] {list} should be "Album" for 3 arguments')
+
+ def test_list_artist_with_unknown_field_in_query_returns_ack(self):
+ self.sendRequest(u'list "artist" "foo" "bar"')
+ self.assertEqualResponse(u'ACK [2@0] {list} not able to parse args')
+
+ def test_list_artist_by_artist(self):
+ self.sendRequest(u'list "artist" "artist" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_by_album(self):
+ self.sendRequest(u'list "artist" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_by_full_date(self):
+ self.sendRequest(u'list "artist" "date" "2001-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_by_year(self):
+ self.sendRequest(u'list "artist" "date" "2001"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_by_genre(self):
+ self.sendRequest(u'list "artist" "genre" "agenre"')
+ self.assertInResponse(u'OK')
+
+ def test_list_artist_by_artist_and_album(self):
+ self.sendRequest(
+ u'list "artist" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ ### Album
+
+ def test_list_album_with_quotes(self):
+ self.sendRequest(u'list "album"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_without_quotes(self):
+ self.sendRequest(u'list album')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_without_quotes_and_capitalized(self):
+ self.sendRequest(u'list Album')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_with_artist_name(self):
+ self.sendRequest(u'list "album" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_artist(self):
+ self.sendRequest(u'list "album" "artist" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_album(self):
+ self.sendRequest(u'list "album" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_full_date(self):
+ self.sendRequest(u'list "album" "date" "2001-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_year(self):
+ self.sendRequest(u'list "album" "date" "2001"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_genre(self):
+ self.sendRequest(u'list "album" "genre" "agenre"')
+ self.assertInResponse(u'OK')
+
+ def test_list_album_by_artist_and_album(self):
+ self.sendRequest(
+ u'list "album" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ ### Date
+
+ def test_list_date_with_quotes(self):
+ self.sendRequest(u'list "date"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_without_quotes(self):
+ self.sendRequest(u'list date')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_without_quotes_and_capitalized(self):
+ self.sendRequest(u'list Date')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_with_query_of_one_token(self):
+ self.sendRequest(u'list "date" "anartist"')
+ self.assertEqualResponse(
+ u'ACK [2@0] {list} should be "Album" for 3 arguments')
+
+ def test_list_date_by_artist(self):
+ self.sendRequest(u'list "date" "artist" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_by_album(self):
+ self.sendRequest(u'list "date" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_by_full_date(self):
+ self.sendRequest(u'list "date" "date" "2001-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_by_year(self):
+ self.sendRequest(u'list "date" "date" "2001"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_by_genre(self):
+ self.sendRequest(u'list "date" "genre" "agenre"')
+ self.assertInResponse(u'OK')
+
+ def test_list_date_by_artist_and_album(self):
+ self.sendRequest(u'list "date" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ ### Genre
+
+ def test_list_genre_with_quotes(self):
+ self.sendRequest(u'list "genre"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_without_quotes(self):
+ self.sendRequest(u'list genre')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_without_quotes_and_capitalized(self):
+ self.sendRequest(u'list Genre')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_with_query_of_one_token(self):
+ self.sendRequest(u'list "genre" "anartist"')
+ self.assertEqualResponse(
+ u'ACK [2@0] {list} should be "Album" for 3 arguments')
+
+ def test_list_genre_by_artist(self):
+ self.sendRequest(u'list "genre" "artist" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_by_album(self):
+ self.sendRequest(u'list "genre" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_by_full_date(self):
+ self.sendRequest(u'list "genre" "date" "2001-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_by_year(self):
+ self.sendRequest(u'list "genre" "date" "2001"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_by_genre(self):
+ self.sendRequest(u'list "genre" "genre" "agenre"')
+ self.assertInResponse(u'OK')
+
+ def test_list_genre_by_artist_and_album(self):
+ self.sendRequest(
+ u'list "genre" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+
+class MusicDatabaseSearchTest(protocol.BaseTestCase):
+ def test_search_album(self):
+ self.sendRequest(u'search "album" "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_search_album_without_quotes(self):
+ self.sendRequest(u'search album "analbum"')
+ self.assertInResponse(u'OK')
+
+ def test_search_artist(self):
+ self.sendRequest(u'search "artist" "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_search_artist_without_quotes(self):
+ self.sendRequest(u'search artist "anartist"')
+ self.assertInResponse(u'OK')
+
+ def test_search_filename(self):
+ self.sendRequest(u'search "filename" "afilename"')
+ self.assertInResponse(u'OK')
+
+ def test_search_filename_without_quotes(self):
+ self.sendRequest(u'search filename "afilename"')
+ self.assertInResponse(u'OK')
+
+ def test_search_title(self):
+ self.sendRequest(u'search "title" "atitle"')
+ self.assertInResponse(u'OK')
+
+ def test_search_title_without_quotes(self):
+ self.sendRequest(u'search title "atitle"')
+ self.assertInResponse(u'OK')
+
+ def test_search_any(self):
+ self.sendRequest(u'search "any" "anything"')
+ self.assertInResponse(u'OK')
+
+ def test_search_any_without_quotes(self):
+ self.sendRequest(u'search any "anything"')
+ self.assertInResponse(u'OK')
+
+ def test_search_date(self):
+ self.sendRequest(u'search "date" "2002-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_search_date_without_quotes(self):
+ self.sendRequest(u'search date "2002-01-01"')
+ self.assertInResponse(u'OK')
+
+ def test_search_date_with_capital_d_and_incomplete_date(self):
+ self.sendRequest(u'search Date "2005"')
+ self.assertInResponse(u'OK')
+
+ def test_search_else_should_fail(self):
+ self.sendRequest(u'search "sometype" "something"')
+ self.assertEqualResponse(u'ACK [2@0] {search} incorrect arguments')
diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py
similarity index 53%
rename from tests/frontends/mpd/playback_test.py
rename to tests/frontends/mpd/protocol/playback_test.py
index e80943d6..01658f6d 100644
--- a/tests/frontends/mpd/playback_test.py
+++ b/tests/frontends/mpd/protocol/playback_test.py
@@ -1,247 +1,238 @@
-import unittest
-
-from mopidy.backends.base import PlaybackController
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
+from mopidy.backends import base as backend
from mopidy.models import Track
-from tests import SkipTest
+from tests import unittest
+from tests.frontends.mpd import protocol
-PAUSED = PlaybackController.PAUSED
-PLAYING = PlaybackController.PLAYING
-STOPPED = PlaybackController.STOPPED
+PAUSED = backend.PlaybackController.PAUSED
+PLAYING = backend.PlaybackController.PLAYING
+STOPPED = backend.PlaybackController.STOPPED
-class PlaybackOptionsHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
+class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
def test_consume_off(self):
- result = self.dispatcher.handle_request(u'consume "0"')
+ self.sendRequest(u'consume "0"')
self.assertFalse(self.backend.playback.consume.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_consume_off_without_quotes(self):
- result = self.dispatcher.handle_request(u'consume 0')
+ self.sendRequest(u'consume 0')
self.assertFalse(self.backend.playback.consume.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_consume_on(self):
- result = self.dispatcher.handle_request(u'consume "1"')
+ self.sendRequest(u'consume "1"')
self.assertTrue(self.backend.playback.consume.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_consume_on_without_quotes(self):
- result = self.dispatcher.handle_request(u'consume 1')
+ self.sendRequest(u'consume 1')
self.assertTrue(self.backend.playback.consume.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_crossfade(self):
- result = self.dispatcher.handle_request(u'crossfade "10"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'crossfade "10"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_random_off(self):
- result = self.dispatcher.handle_request(u'random "0"')
+ self.sendRequest(u'random "0"')
self.assertFalse(self.backend.playback.random.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_random_off_without_quotes(self):
- result = self.dispatcher.handle_request(u'random 0')
+ self.sendRequest(u'random 0')
self.assertFalse(self.backend.playback.random.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_random_on(self):
- result = self.dispatcher.handle_request(u'random "1"')
+ self.sendRequest(u'random "1"')
self.assertTrue(self.backend.playback.random.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_random_on_without_quotes(self):
- result = self.dispatcher.handle_request(u'random 1')
+ self.sendRequest(u'random 1')
self.assertTrue(self.backend.playback.random.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_repeat_off(self):
- result = self.dispatcher.handle_request(u'repeat "0"')
+ self.sendRequest(u'repeat "0"')
self.assertFalse(self.backend.playback.repeat.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_repeat_off_without_quotes(self):
- result = self.dispatcher.handle_request(u'repeat 0')
+ self.sendRequest(u'repeat 0')
self.assertFalse(self.backend.playback.repeat.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_repeat_on(self):
- result = self.dispatcher.handle_request(u'repeat "1"')
+ self.sendRequest(u'repeat "1"')
self.assertTrue(self.backend.playback.repeat.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_repeat_on_without_quotes(self):
- result = self.dispatcher.handle_request(u'repeat 1')
+ self.sendRequest(u'repeat 1')
self.assertTrue(self.backend.playback.repeat.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_setvol_below_min(self):
- result = self.dispatcher.handle_request(u'setvol "-10"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "-10"')
self.assertEqual(0, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_min(self):
- result = self.dispatcher.handle_request(u'setvol "0"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "0"')
self.assertEqual(0, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_middle(self):
- result = self.dispatcher.handle_request(u'setvol "50"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "50"')
self.assertEqual(50, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_max(self):
- result = self.dispatcher.handle_request(u'setvol "100"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "100"')
self.assertEqual(100, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_above_max(self):
- result = self.dispatcher.handle_request(u'setvol "110"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "110"')
self.assertEqual(100, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_plus_is_ignored(self):
- result = self.dispatcher.handle_request(u'setvol "+10"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol "+10"')
self.assertEqual(10, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_setvol_without_quotes(self):
- result = self.dispatcher.handle_request(u'setvol 50')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'setvol 50')
self.assertEqual(50, self.mixer.volume.get())
+ self.assertInResponse(u'OK')
def test_single_off(self):
- result = self.dispatcher.handle_request(u'single "0"')
+ self.sendRequest(u'single "0"')
self.assertFalse(self.backend.playback.single.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_single_off_without_quotes(self):
- result = self.dispatcher.handle_request(u'single 0')
+ self.sendRequest(u'single 0')
self.assertFalse(self.backend.playback.single.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_single_on(self):
- result = self.dispatcher.handle_request(u'single "1"')
+ self.sendRequest(u'single "1"')
self.assertTrue(self.backend.playback.single.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_single_on_without_quotes(self):
- result = self.dispatcher.handle_request(u'single 1')
+ self.sendRequest(u'single 1')
self.assertTrue(self.backend.playback.single.get())
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
def test_replay_gain_mode_off(self):
- result = self.dispatcher.handle_request(u'replay_gain_mode "off"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'replay_gain_mode "off"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_replay_gain_mode_track(self):
- result = self.dispatcher.handle_request(u'replay_gain_mode "track"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'replay_gain_mode "track"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_replay_gain_mode_album(self):
- result = self.dispatcher.handle_request(u'replay_gain_mode "album"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.sendRequest(u'replay_gain_mode "album"')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_replay_gain_status_default(self):
- expected = u'off'
- result = self.dispatcher.handle_request(u'replay_gain_status')
- self.assert_(u'OK' in result)
- self.assert_(expected in result)
+ self.sendRequest(u'replay_gain_status')
+ self.assertInResponse(u'OK')
+ self.assertInResponse(u'off')
+ @unittest.SkipTest
def test_replay_gain_status_off(self):
- raise SkipTest # TODO
+ pass
+ @unittest.SkipTest
def test_replay_gain_status_track(self):
- raise SkipTest # TODO
+ pass
+ @unittest.SkipTest
def test_replay_gain_status_album(self):
- raise SkipTest # TODO
+ pass
-class PlaybackControlHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
+class PlaybackControlHandlerTest(protocol.BaseTestCase):
def test_next(self):
- result = self.dispatcher.handle_request(u'next')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'next')
+ self.assertInResponse(u'OK')
def test_pause_off(self):
self.backend.current_playlist.append([Track()])
- self.dispatcher.handle_request(u'play "0"')
- self.dispatcher.handle_request(u'pause "1"')
- result = self.dispatcher.handle_request(u'pause "0"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "0"')
+ self.sendRequest(u'pause "1"')
+ self.sendRequest(u'pause "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_pause_on(self):
self.backend.current_playlist.append([Track()])
- self.dispatcher.handle_request(u'play "0"')
- result = self.dispatcher.handle_request(u'pause "1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "0"')
+ self.sendRequest(u'pause "1"')
self.assertEqual(PAUSED, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_pause_toggle(self):
self.backend.current_playlist.append([Track()])
- result = self.dispatcher.handle_request(u'play "0"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'pause')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
+
+ self.sendRequest(u'pause')
self.assertEqual(PAUSED, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'pause')
- self.assert_(u'OK' in result)
+ self.assertInResponse(u'OK')
+
+ self.sendRequest(u'pause')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_play_without_pos(self):
self.backend.current_playlist.append([Track()])
self.backend.playback.state = PAUSED
- result = self.dispatcher.handle_request(u'play')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_play_with_pos(self):
self.backend.current_playlist.append([Track()])
- result = self.dispatcher.handle_request(u'play "0"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_play_with_pos_without_quotes(self):
self.backend.current_playlist.append([Track()])
- result = self.dispatcher.handle_request(u'play 0')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play 0')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_play_with_pos_out_of_bounds(self):
self.backend.current_playlist.append([])
- result = self.dispatcher.handle_request(u'play "0"')
- self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index')
+
+ self.sendRequest(u'play "0"')
self.assertEqual(STOPPED, self.backend.playback.state.get())
+ self.assertInResponse(u'ACK [2@0] {play} Bad song index')
def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.backend.playback.current_track.get(), None)
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- result = self.dispatcher.handle_request(u'play "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEqual('a', self.backend.playback.current_track.get().uri)
+ self.assertInResponse(u'OK')
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
@@ -250,27 +241,30 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.backend.playback.next()
self.backend.playback.stop()
self.assertNotEqual(self.backend.playback.current_track.get(), None)
- result = self.dispatcher.handle_request(u'play "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEqual('b', self.backend.playback.current_track.get().uri)
+ self.assertInResponse(u'OK')
def test_play_minus_one_on_empty_playlist_does_not_ack(self):
self.backend.current_playlist.clear()
- result = self.dispatcher.handle_request(u'play "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "-1"')
self.assertEqual(STOPPED, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get(), None)
+ self.assertEqual(None, self.backend.playback.current_track.get())
+ self.assertInResponse(u'OK')
def test_play_minus_is_ignored_if_playing(self):
self.backend.current_playlist.append([Track(length=40000)])
self.backend.playback.seek(30000)
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertEquals(PLAYING, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'play "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_play_minus_one_resumes_if_paused(self):
self.backend.current_playlist.append([Track(length=40000)])
@@ -279,24 +273,27 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEquals(PLAYING, self.backend.playback.state.get())
self.backend.playback.pause()
self.assertEquals(PAUSED, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'play "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_playid(self):
self.backend.current_playlist.append([Track()])
- result = self.dispatcher.handle_request(u'playid "0"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playid "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.backend.playback.current_track.get(), None)
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- result = self.dispatcher.handle_request(u'playid "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEqual('a', self.backend.playback.current_track.get().uri)
+ self.assertInResponse(u'OK')
def test_playid_minus_one_plays_current_track_if_current_track_is_set(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
@@ -304,28 +301,31 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.backend.playback.play()
self.backend.playback.next()
self.backend.playback.stop()
- self.assertNotEqual(self.backend.playback.current_track.get(), None)
- result = self.dispatcher.handle_request(u'playid "-1"')
- self.assert_(u'OK' in result)
+ self.assertNotEqual(None, self.backend.playback.current_track.get())
+
+ self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEqual('b', self.backend.playback.current_track.get().uri)
+ self.assertInResponse(u'OK')
def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
self.backend.current_playlist.clear()
- result = self.dispatcher.handle_request(u'playid "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playid "-1"')
self.assertEqual(STOPPED, self.backend.playback.state.get())
- self.assertEqual(self.backend.playback.current_track.get(), None)
+ self.assertEqual(None, self.backend.playback.current_track.get())
+ self.assertInResponse(u'OK')
def test_playid_minus_is_ignored_if_playing(self):
self.backend.current_playlist.append([Track(length=40000)])
self.backend.playback.seek(30000)
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertEquals(PLAYING, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'playid "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_playid_minus_one_resumes_if_paused(self):
self.backend.current_playlist.append([Track(length=40000)])
@@ -334,58 +334,64 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEquals(PLAYING, self.backend.playback.state.get())
self.backend.playback.pause()
self.assertEquals(PAUSED, self.backend.playback.state.get())
- result = self.dispatcher.handle_request(u'playid "-1"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_playid_which_does_not_exist(self):
self.backend.current_playlist.append([Track()])
- result = self.dispatcher.handle_request(u'playid "12345"')
- self.assertEqual(result[0], u'ACK [50@0] {playid} No such song')
+
+ self.sendRequest(u'playid "12345"')
+ self.assertInResponse(u'ACK [50@0] {playid} No such song')
def test_previous(self):
- result = self.dispatcher.handle_request(u'previous')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'previous')
+ self.assertInResponse(u'OK')
def test_seek(self):
self.backend.current_playlist.append([Track(length=40000)])
- self.dispatcher.handle_request(u'seek "0"')
- result = self.dispatcher.handle_request(u'seek "0" "30"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'seek "0"')
+ self.sendRequest(u'seek "0" "30"')
self.assert_(self.backend.playback.time_position >= 30000)
+ self.assertInResponse(u'OK')
def test_seek_with_songpos(self):
seek_track = Track(uri='2', length=40000)
self.backend.current_playlist.append(
[Track(uri='1', length=40000), seek_track])
- result = self.dispatcher.handle_request(u'seek "1" "30"')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'seek "1" "30"')
self.assertEqual(self.backend.playback.current_track.get(), seek_track)
+ self.assertInResponse(u'OK')
def test_seek_without_quotes(self):
self.backend.current_playlist.append([Track(length=40000)])
- self.dispatcher.handle_request(u'seek 0')
- result = self.dispatcher.handle_request(u'seek 0 30')
- self.assert_(u'OK' in result)
+
+ self.sendRequest(u'seek 0')
+ self.sendRequest(u'seek 0 30')
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_seekid(self):
self.backend.current_playlist.append([Track(length=40000)])
- result = self.dispatcher.handle_request(u'seekid "0" "30"')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'seekid "0" "30"')
self.assert_(self.backend.playback.time_position.get() >= 30000)
+ self.assertInResponse(u'OK')
def test_seekid_with_cpid(self):
seek_track = Track(uri='2', length=40000)
self.backend.current_playlist.append(
[Track(length=40000), seek_track])
- result = self.dispatcher.handle_request(u'seekid "1" "30"')
- self.assert_(u'OK' in result)
- self.assertEqual(self.backend.playback.current_cpid.get(), 1)
- self.assertEqual(self.backend.playback.current_track.get(), seek_track)
+
+ self.sendRequest(u'seekid "1" "30"')
+ self.assertEqual(1, self.backend.playback.current_cpid.get())
+ self.assertEqual(seek_track, self.backend.playback.current_track.get())
+ self.assertInResponse(u'OK')
def test_stop(self):
- result = self.dispatcher.handle_request(u'stop')
- self.assert_(u'OK' in result)
+ self.sendRequest(u'stop')
self.assertEqual(STOPPED, self.backend.playback.state.get())
+ self.assertInResponse(u'OK')
diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py
new file mode 100644
index 00000000..8bd9b7e0
--- /dev/null
+++ b/tests/frontends/mpd/protocol/reflection_test.py
@@ -0,0 +1,67 @@
+from mopidy import settings
+
+from tests.frontends.mpd import protocol
+
+
+class ReflectionHandlerTest(protocol.BaseTestCase):
+ def test_commands_returns_list_of_all_commands(self):
+ self.sendRequest(u'commands')
+ # Check if some random commands are included
+ self.assertInResponse(u'command: commands')
+ self.assertInResponse(u'command: play')
+ self.assertInResponse(u'command: status')
+ # Check if commands you do not have access to are not present
+ self.assertNotInResponse(u'command: kill')
+ # Check if the blacklisted commands are not present
+ self.assertNotInResponse(u'command: command_list_begin')
+ self.assertNotInResponse(u'command: command_list_ok_begin')
+ self.assertNotInResponse(u'command: command_list_end')
+ self.assertNotInResponse(u'command: idle')
+ self.assertNotInResponse(u'command: noidle')
+ self.assertNotInResponse(u'command: sticker')
+ self.assertInResponse(u'OK')
+
+ def test_commands_show_less_if_auth_required_and_not_authed(self):
+ settings.MPD_SERVER_PASSWORD = u'secret'
+ self.sendRequest(u'commands')
+ # Not requiring auth
+ self.assertInResponse(u'command: close')
+ self.assertInResponse(u'command: commands')
+ self.assertInResponse(u'command: notcommands')
+ self.assertInResponse(u'command: password')
+ self.assertInResponse(u'command: ping')
+ # Requiring auth
+ self.assertNotInResponse(u'command: play')
+ self.assertNotInResponse(u'command: status')
+
+ def test_decoders(self):
+ self.sendRequest(u'decoders')
+ self.assertInResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_notcommands_returns_only_kill_and_ok(self):
+ response = self.sendRequest(u'notcommands')
+ self.assertEqual(2, len(response))
+ self.assertInResponse(u'command: kill')
+ self.assertInResponse(u'OK')
+
+ def test_notcommands_returns_more_if_auth_required_and_not_authed(self):
+ settings.MPD_SERVER_PASSWORD = u'secret'
+ self.sendRequest(u'notcommands')
+ # Not requiring auth
+ self.assertNotInResponse(u'command: close')
+ self.assertNotInResponse(u'command: commands')
+ self.assertNotInResponse(u'command: notcommands')
+ self.assertNotInResponse(u'command: password')
+ self.assertNotInResponse(u'command: ping')
+ # Requiring auth
+ self.assertInResponse(u'command: play')
+ self.assertInResponse(u'command: status')
+
+ def test_tagtypes(self):
+ self.sendRequest(u'tagtypes')
+ self.assertInResponse(u'OK')
+
+ def test_urlhandlers(self):
+ self.sendRequest(u'urlhandlers')
+ self.assertInResponse(u'OK')
+ self.assertInResponse(u'handler: dummy')
diff --git a/tests/frontends/mpd/protocol/regression_test.py b/tests/frontends/mpd/protocol/regression_test.py
new file mode 100644
index 00000000..d4e4b2aa
--- /dev/null
+++ b/tests/frontends/mpd/protocol/regression_test.py
@@ -0,0 +1,148 @@
+import random
+
+from mopidy.models import Track
+
+from tests.frontends.mpd import protocol
+
+
+class IssueGH17RegressionTest(protocol.BaseTestCase):
+ """
+ The issue: http://github.com/mopidy/mopidy/issues/17
+
+ How to reproduce:
+
+ - Play a playlist where one track cannot be played
+ - Turn on random mode
+ - Press next until you get to the unplayable track
+ """
+ def test(self):
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), None,
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ random.seed(1) # Playlist order: abcfde
+
+ self.sendRequest(u'play')
+ self.assertEquals('a', self.backend.playback.current_track.get().uri)
+ self.sendRequest(u'random "1"')
+ self.sendRequest(u'next')
+ self.assertEquals('b', self.backend.playback.current_track.get().uri)
+ self.sendRequest(u'next')
+ # Should now be at track 'c', but playback fails and it skips ahead
+ self.assertEquals('f', self.backend.playback.current_track.get().uri)
+ self.sendRequest(u'next')
+ self.assertEquals('d', self.backend.playback.current_track.get().uri)
+ self.sendRequest(u'next')
+ self.assertEquals('e', self.backend.playback.current_track.get().uri)
+
+
+class IssueGH18RegressionTest(protocol.BaseTestCase):
+ """
+ The issue: http://github.com/mopidy/mopidy/issues/18
+
+ How to reproduce:
+
+ Play, random on, next, random off, next, next.
+
+ At this point it gives the same song over and over.
+ """
+
+ def test(self):
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), Track(uri='c'),
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ random.seed(1)
+
+ self.sendRequest(u'play')
+ self.sendRequest(u'random "1"')
+ self.sendRequest(u'next')
+ self.sendRequest(u'random "0"')
+ self.sendRequest(u'next')
+
+ self.sendRequest(u'next')
+ cp_track_1 = self.backend.playback.current_cp_track.get()
+ self.sendRequest(u'next')
+ cp_track_2 = self.backend.playback.current_cp_track.get()
+ self.sendRequest(u'next')
+ cp_track_3 = self.backend.playback.current_cp_track.get()
+
+ self.assertNotEqual(cp_track_1, cp_track_2)
+ self.assertNotEqual(cp_track_2, cp_track_3)
+
+
+class IssueGH22RegressionTest(protocol.BaseTestCase):
+ """
+ The issue: http://github.com/mopidy/mopidy/issues/22
+
+ How to reproduce:
+
+ Play, random on, remove all tracks from the current playlist (as in
+ "delete" each one, not "clear").
+
+ Alternatively: Play, random on, remove a random track from the current
+ playlist, press next until it crashes.
+ """
+
+ def test(self):
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), Track(uri='c'),
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ random.seed(1)
+
+ self.sendRequest(u'play')
+ self.sendRequest(u'random "1"')
+ self.sendRequest(u'deleteid "1"')
+ self.sendRequest(u'deleteid "2"')
+ self.sendRequest(u'deleteid "3"')
+ self.sendRequest(u'deleteid "4"')
+ self.sendRequest(u'deleteid "5"')
+ self.sendRequest(u'deleteid "6"')
+ self.sendRequest(u'status')
+
+
+class IssueGH69RegressionTest(protocol.BaseTestCase):
+ """
+ The issue: https://github.com/mopidy/mopidy/issues/69
+
+ How to reproduce:
+
+ Play track, stop, clear current playlist, load a new playlist, status.
+
+ The status response now contains "song: None".
+ """
+
+ def test(self):
+ self.backend.stored_playlists.create('foo')
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), Track(uri='c'),
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+
+ self.sendRequest(u'play')
+ self.sendRequest(u'stop')
+ self.sendRequest(u'clear')
+ self.sendRequest(u'load "foo"')
+ self.assertNotInResponse('song: None')
+
+
+class IssueGH113RegressionTest(protocol.BaseTestCase):
+ """
+ The issue: https://github.com/mopidy/mopidy/issues/113
+
+ How to reproduce:
+
+ - Have a playlist with a name contining backslashes, like
+ "all lart spotify:track:\w\{22\} pastes".
+ - Try to load the playlist with the backslashes in the playlist name
+ escaped.
+ """
+
+ def test(self):
+ self.backend.stored_playlists.create(
+ u'all lart spotify:track:\w\{22\} pastes')
+
+ self.sendRequest(u'lsinfo "/"')
+ self.assertInResponse(
+ u'playlist: all lart spotify:track:\w\{22\} pastes')
+
+ self.sendRequest(
+ r'listplaylistinfo "all lart spotify:track:\\w\\{22\\} pastes"')
+ self.assertInResponse('OK')
diff --git a/tests/frontends/mpd/protocol/status_test.py b/tests/frontends/mpd/protocol/status_test.py
new file mode 100644
index 00000000..e6572eab
--- /dev/null
+++ b/tests/frontends/mpd/protocol/status_test.py
@@ -0,0 +1,37 @@
+from mopidy.models import Track
+
+from tests.frontends.mpd import protocol
+
+
+class StatusHandlerTest(protocol.BaseTestCase):
+ def test_clearerror(self):
+ self.sendRequest(u'clearerror')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_currentsong(self):
+ track = Track()
+ self.backend.current_playlist.append([track])
+ self.backend.playback.play()
+ self.sendRequest(u'currentsong')
+ self.assertInResponse(u'file: ')
+ self.assertInResponse(u'Time: 0')
+ self.assertInResponse(u'Artist: ')
+ self.assertInResponse(u'Title: ')
+ self.assertInResponse(u'Album: ')
+ self.assertInResponse(u'Track: 0')
+ self.assertInResponse(u'Date: ')
+ self.assertInResponse(u'Pos: 0')
+ self.assertInResponse(u'Id: 0')
+ self.assertInResponse(u'OK')
+
+ def test_currentsong_without_song(self):
+ self.sendRequest(u'currentsong')
+ self.assertInResponse(u'OK')
+
+ def test_stats_command(self):
+ self.sendRequest(u'stats')
+ self.assertInResponse(u'OK')
+
+ def test_status_command(self):
+ self.sendRequest(u'status')
+ self.assertInResponse(u'OK')
diff --git a/tests/frontends/mpd/protocol/stickers_test.py b/tests/frontends/mpd/protocol/stickers_test.py
new file mode 100644
index 00000000..3e8b687f
--- /dev/null
+++ b/tests/frontends/mpd/protocol/stickers_test.py
@@ -0,0 +1,33 @@
+from tests.frontends.mpd import protocol
+
+
+class StickersHandlerTest(protocol.BaseTestCase):
+ def test_sticker_get(self):
+ self.sendRequest(
+ u'sticker get "song" "file:///dev/urandom" "a_name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_sticker_set(self):
+ self.sendRequest(
+ u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_sticker_delete_with_name(self):
+ self.sendRequest(
+ u'sticker delete "song" "file:///dev/urandom" "a_name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_sticker_delete_without_name(self):
+ self.sendRequest(
+ u'sticker delete "song" "file:///dev/urandom"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_sticker_list(self):
+ self.sendRequest(
+ u'sticker list "song" "file:///dev/urandom"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_sticker_find(self):
+ self.sendRequest(
+ u'sticker find "song" "file:///dev/urandom" "a_name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
diff --git a/tests/frontends/mpd/protocol/stored_playlists_test.py b/tests/frontends/mpd/protocol/stored_playlists_test.py
new file mode 100644
index 00000000..45d6a09a
--- /dev/null
+++ b/tests/frontends/mpd/protocol/stored_playlists_test.py
@@ -0,0 +1,94 @@
+import datetime
+
+from mopidy.models import Track, Playlist
+
+from tests.frontends.mpd import protocol
+
+
+class StoredPlaylistsHandlerTest(protocol.BaseTestCase):
+ def test_listplaylist(self):
+ self.backend.stored_playlists.playlists = [
+ Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
+
+ self.sendRequest(u'listplaylist "name"')
+ self.assertInResponse(u'file: file:///dev/urandom')
+ self.assertInResponse(u'OK')
+
+ def test_listplaylist_fails_if_no_playlist_is_found(self):
+ self.sendRequest(u'listplaylist "name"')
+ self.assertEqualResponse(u'ACK [50@0] {listplaylist} No such playlist')
+
+ def test_listplaylistinfo(self):
+ self.backend.stored_playlists.playlists = [
+ Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
+
+ self.sendRequest(u'listplaylistinfo "name"')
+ self.assertInResponse(u'file: file:///dev/urandom')
+ self.assertInResponse(u'Track: 0')
+ self.assertNotInResponse(u'Pos: 0')
+ self.assertInResponse(u'OK')
+
+ def test_listplaylistinfo_fails_if_no_playlist_is_found(self):
+ self.sendRequest(u'listplaylistinfo "name"')
+ self.assertEqualResponse(
+ u'ACK [50@0] {listplaylistinfo} No such playlist')
+
+ def test_listplaylists(self):
+ last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345)
+ self.backend.stored_playlists.playlists = [Playlist(name='a',
+ last_modified=last_modified)]
+
+ self.sendRequest(u'listplaylists')
+ self.assertInResponse(u'playlist: a')
+ # Date without microseconds and with time zone information
+ self.assertInResponse(u'Last-Modified: 2001-03-17T13:41:17Z')
+ self.assertInResponse(u'OK')
+
+ def test_load_known_playlist_appends_to_current_playlist(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
+ self.backend.stored_playlists.playlists = [Playlist(name='A-list',
+ tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
+
+ self.sendRequest(u'load "A-list"')
+ tracks = self.backend.current_playlist.tracks.get()
+ self.assertEqual(5, len(tracks))
+ self.assertEqual('a', tracks[0].uri)
+ self.assertEqual('b', tracks[1].uri)
+ self.assertEqual('c', tracks[2].uri)
+ self.assertEqual('d', tracks[3].uri)
+ self.assertEqual('e', tracks[4].uri)
+ self.assertInResponse(u'OK')
+
+ def test_load_unknown_playlist_acks(self):
+ self.sendRequest(u'load "unknown playlist"')
+ self.assertEqual(0, len(self.backend.current_playlist.tracks.get()))
+ self.assertEqualResponse(u'ACK [50@0] {load} No such playlist')
+
+ def test_playlistadd(self):
+ self.sendRequest(u'playlistadd "name" "file:///dev/urandom"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_playlistclear(self):
+ self.sendRequest(u'playlistclear "name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_playlistdelete(self):
+ self.sendRequest(u'playlistdelete "name" "5"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_playlistmove(self):
+ self.sendRequest(u'playlistmove "name" "5" "10"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_rename(self):
+ self.sendRequest(u'rename "old_name" "new_name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_rm(self):
+ self.sendRequest(u'rm "name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+
+ def test_save(self):
+ self.sendRequest(u'save "name"')
+ self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
diff --git a/tests/frontends/mpd/reflection_test.py b/tests/frontends/mpd/reflection_test.py
deleted file mode 100644
index 2abf5acc..00000000
--- a/tests/frontends/mpd/reflection_test.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import unittest
-
-from mopidy import settings
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
-
-class ReflectionHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- settings.runtime.clear()
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_commands_returns_list_of_all_commands(self):
- result = self.dispatcher.handle_request(u'commands')
- # Check if some random commands are included
- self.assert_(u'command: commands' in result)
- self.assert_(u'command: play' in result)
- self.assert_(u'command: status' in result)
- # Check if commands you do not have access to are not present
- self.assert_(u'command: kill' not in result)
- # Check if the blacklisted commands are not present
- self.assert_(u'command: command_list_begin' not in result)
- self.assert_(u'command: command_list_ok_begin' not in result)
- self.assert_(u'command: command_list_end' not in result)
- self.assert_(u'command: idle' not in result)
- self.assert_(u'command: noidle' not in result)
- self.assert_(u'command: sticker' not in result)
- self.assert_(u'OK' in result)
-
- def test_commands_show_less_if_auth_required_and_not_authed(self):
- settings.MPD_SERVER_PASSWORD = u'secret'
- result = self.dispatcher.handle_request(u'commands')
- # Not requiring auth
- self.assert_(u'command: close' in result, result)
- self.assert_(u'command: commands' in result, result)
- self.assert_(u'command: notcommands' in result, result)
- self.assert_(u'command: password' in result, result)
- self.assert_(u'command: ping' in result, result)
- # Requiring auth
- self.assert_(u'command: play' not in result, result)
- self.assert_(u'command: status' not in result, result)
-
- def test_decoders(self):
- result = self.dispatcher.handle_request(u'decoders')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_notcommands_returns_only_kill_and_ok(self):
- result = self.dispatcher.handle_request(u'notcommands')
- self.assertEqual(2, len(result))
- self.assert_(u'command: kill' in result)
- self.assert_(u'OK' in result)
-
- def test_notcommands_returns_more_if_auth_required_and_not_authed(self):
- settings.MPD_SERVER_PASSWORD = u'secret'
- result = self.dispatcher.handle_request(u'notcommands')
- # Not requiring auth
- self.assert_(u'command: close' not in result, result)
- self.assert_(u'command: commands' not in result, result)
- self.assert_(u'command: notcommands' not in result, result)
- self.assert_(u'command: password' not in result, result)
- self.assert_(u'command: ping' not in result, result)
- # Requiring auth
- self.assert_(u'command: play' in result, result)
- self.assert_(u'command: status' in result, result)
-
- def test_tagtypes(self):
- result = self.dispatcher.handle_request(u'tagtypes')
- self.assert_(u'OK' in result)
-
- def test_urlhandlers(self):
- result = self.dispatcher.handle_request(u'urlhandlers')
- self.assert_(u'OK' in result)
- self.assert_(u'handler: dummy:' in result)
diff --git a/tests/frontends/mpd/regression_test.py b/tests/frontends/mpd/regression_test.py
deleted file mode 100644
index f786cf0a..00000000
--- a/tests/frontends/mpd/regression_test.py
+++ /dev/null
@@ -1,158 +0,0 @@
-import random
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import dispatcher
-from mopidy.mixers.dummy import DummyMixer
-from mopidy.models import Track
-
-class IssueGH17RegressionTest(unittest.TestCase):
- """
- The issue: http://github.com/mopidy/mopidy/issues#issue/17
-
- How to reproduce:
-
- - Play a playlist where one track cannot be played
- - Turn on random mode
- - Press next until you get to the unplayable track
- """
-
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), None,
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
- self.mixer = DummyMixer.start().proxy()
- self.mpd = dispatcher.MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test(self):
- random.seed(1) # Playlist order: abcfde
- self.mpd.handle_request(u'play')
- self.assertEquals('a', self.backend.playback.current_track.get().uri)
- self.mpd.handle_request(u'random "1"')
- self.mpd.handle_request(u'next')
- self.assertEquals('b', self.backend.playback.current_track.get().uri)
- self.mpd.handle_request(u'next')
- # Should now be at track 'c', but playback fails and it skips ahead
- self.assertEquals('f', self.backend.playback.current_track.get().uri)
- self.mpd.handle_request(u'next')
- self.assertEquals('d', self.backend.playback.current_track.get().uri)
- self.mpd.handle_request(u'next')
- self.assertEquals('e', self.backend.playback.current_track.get().uri)
-
-
-class IssueGH18RegressionTest(unittest.TestCase):
- """
- The issue: http://github.com/mopidy/mopidy/issues#issue/18
-
- How to reproduce:
-
- Play, random on, next, random off, next, next.
-
- At this point it gives the same song over and over.
- """
-
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), Track(uri='c'),
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
- self.mixer = DummyMixer.start().proxy()
- self.mpd = dispatcher.MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test(self):
- random.seed(1)
- self.mpd.handle_request(u'play')
- self.mpd.handle_request(u'random "1"')
- self.mpd.handle_request(u'next')
- self.mpd.handle_request(u'random "0"')
- self.mpd.handle_request(u'next')
-
- self.mpd.handle_request(u'next')
- cp_track_1 = self.backend.playback.current_cp_track.get()
- self.mpd.handle_request(u'next')
- cp_track_2 = self.backend.playback.current_cp_track.get()
- self.mpd.handle_request(u'next')
- cp_track_3 = self.backend.playback.current_cp_track.get()
-
- self.assertNotEqual(cp_track_1, cp_track_2)
- self.assertNotEqual(cp_track_2, cp_track_3)
-
-
-class IssueGH22RegressionTest(unittest.TestCase):
- """
- The issue: http://github.com/mopidy/mopidy/issues/#issue/22
-
- How to reproduce:
-
- Play, random on, remove all tracks from the current playlist (as in
- "delete" each one, not "clear").
-
- Alternatively: Play, random on, remove a random track from the current
- playlist, press next until it crashes.
- """
-
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), Track(uri='c'),
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
- self.mixer = DummyMixer.start().proxy()
- self.mpd = dispatcher.MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test(self):
- random.seed(1)
- self.mpd.handle_request(u'play')
- self.mpd.handle_request(u'random "1"')
- self.mpd.handle_request(u'deleteid "1"')
- self.mpd.handle_request(u'deleteid "2"')
- self.mpd.handle_request(u'deleteid "3"')
- self.mpd.handle_request(u'deleteid "4"')
- self.mpd.handle_request(u'deleteid "5"')
- self.mpd.handle_request(u'deleteid "6"')
- self.mpd.handle_request(u'status')
-
-
-class IssueGH69RegressionTest(unittest.TestCase):
- """
- The issue: https://github.com/mopidy/mopidy/issues#issue/69
-
- How to reproduce:
-
- Play track, stop, clear current playlist, load a new playlist, status.
-
- The status response now contains "song: None".
- """
-
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), Track(uri='c'),
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
- self.backend.stored_playlists.create('foo')
- self.mixer = DummyMixer.start().proxy()
- self.mpd = dispatcher.MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test(self):
- self.mpd.handle_request(u'play')
- self.mpd.handle_request(u'stop')
- self.mpd.handle_request(u'clear')
- self.mpd.handle_request(u'load "foo"')
- response = self.mpd.handle_request(u'status')
- self.assert_('song: None' not in response)
diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py
index b0c57588..681ab20f 100644
--- a/tests/frontends/mpd/serializer_test.py
+++ b/tests/frontends/mpd/serializer_test.py
@@ -1,12 +1,14 @@
-import datetime as dt
+import datetime
import os
-import unittest
from mopidy import settings
from mopidy.utils.path import mtime, uri_to_path
from mopidy.frontends.mpd import translator, protocol
from mopidy.models import Album, Artist, Playlist, Track
+from tests import unittest
+
+
class TrackMpdFormatTest(unittest.TestCase):
track = Track(
uri=u'a uri',
@@ -15,7 +17,7 @@ class TrackMpdFormatTest(unittest.TestCase):
album=Album(name=u'an album', num_tracks=13,
artists=[Artist(name=u'an other artist')]),
track_no=7,
- date=dt.date(1977, 1, 1),
+ date=datetime.date(1977, 1, 1),
length=137000,
)
@@ -61,7 +63,7 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assert_(('Album', 'an album') in result)
self.assert_(('AlbumArtist', 'an other artist') in result)
self.assert_(('Track', '7/13') in result)
- self.assert_(('Date', dt.date(1977, 1, 1)) in result)
+ self.assert_(('Date', datetime.date(1977, 1, 1)) in result)
self.assert_(('Pos', 9) in result)
self.assert_(('Id', 122) in result)
self.assertEqual(len(result), 10)
diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py
deleted file mode 100644
index b2e27559..00000000
--- a/tests/frontends/mpd/server_test.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import unittest
-
-from mopidy import settings
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd import server
-from mopidy.mixers.dummy import DummyMixer
-
-class MpdSessionTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.session = server.MpdSession(None, None, (None, None))
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
- settings.runtime.clear()
-
- def test_found_terminator_catches_decode_error(self):
- # Pressing Ctrl+C in a telnet session sends a 0xff byte to the server.
- self.session.input_buffer = ['\xff']
- self.session.found_terminator()
- self.assertEqual(len(self.session.input_buffer), 0)
diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py
index a7ed921f..bdd2dab8 100644
--- a/tests/frontends/mpd/status_test.py
+++ b/tests/frontends/mpd/status_test.py
@@ -1,67 +1,30 @@
-import unittest
-
-from mopidy.backends.base import PlaybackController
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
+from mopidy.backends import dummy as backend
+from mopidy.frontends.mpd import dispatcher
from mopidy.frontends.mpd.protocol import status
-from mopidy.mixers.dummy import DummyMixer
+from mopidy.mixers import dummy as mixer
from mopidy.models import Track
-PAUSED = PlaybackController.PAUSED
-PLAYING = PlaybackController.PLAYING
-STOPPED = PlaybackController.STOPPED
+from tests import unittest
+
+PAUSED = backend.PlaybackController.PAUSED
+PLAYING = backend.PlaybackController.PLAYING
+STOPPED = backend.PlaybackController.STOPPED
+
+# FIXME migrate to using protocol.BaseTestCase instead of status.stats
+# directly?
+
class StatusHandlerTest(unittest.TestCase):
def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
+ self.backend = backend.DummyBackend.start().proxy()
+ self.mixer = mixer.DummyMixer.start().proxy()
+ self.dispatcher = dispatcher.MpdDispatcher()
self.context = self.dispatcher.context
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
- def test_clearerror(self):
- result = self.dispatcher.handle_request(u'clearerror')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_currentsong(self):
- track = Track()
- self.backend.current_playlist.append([track])
- self.backend.playback.play()
- result = self.dispatcher.handle_request(u'currentsong')
- self.assert_(u'file: ' in result)
- self.assert_(u'Time: 0' in result)
- self.assert_(u'Artist: ' in result)
- self.assert_(u'Title: ' in result)
- self.assert_(u'Album: ' in result)
- self.assert_(u'Track: 0' in result)
- self.assert_(u'Date: ' in result)
- self.assert_(u'Pos: 0' in result)
- self.assert_(u'Id: 0' in result)
- self.assert_(u'OK' in result)
-
- def test_currentsong_without_song(self):
- result = self.dispatcher.handle_request(u'currentsong')
- self.assert_(u'OK' in result)
-
- def test_idle_without_subsystems(self):
- result = self.dispatcher.handle_request(u'idle')
- self.assert_(u'OK' in result)
-
- def test_idle_with_subsystems(self):
- result = self.dispatcher.handle_request(u'idle database playlist')
- self.assert_(u'OK' in result)
-
- def test_noidle(self):
- result = self.dispatcher.handle_request(u'noidle')
- self.assert_(u'OK' in result)
-
- def test_stats_command(self):
- result = self.dispatcher.handle_request(u'stats')
- self.assert_(u'OK' in result)
-
def test_stats_method(self):
result = status.stats(self.context)
self.assert_('artists' in result)
@@ -79,10 +42,6 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_('playtime' in result)
self.assert_(int(result['playtime']) >= 0)
- def test_status_command(self):
- result = self.dispatcher.handle_request(u'status')
- self.assert_(u'OK' in result)
-
def test_status_method_contains_volume_which_defaults_to_0(self):
result = dict(status.status(self.context))
self.assert_('volume' in result)
@@ -205,7 +164,14 @@ class StatusHandlerTest(unittest.TestCase):
self.backend.playback.play_time_accumulated = 59123
result = dict(status.status(self.context))
self.assert_('elapsed' in result)
- self.assertEqual(int(result['elapsed']), 59123)
+ self.assertEqual(result['elapsed'], '59.123')
+
+ def test_status_method_when_starting_playing_contains_elapsed_zero(self):
+ self.backend.playback.state = PAUSED
+ self.backend.playback.play_time_accumulated = 123 # Less than 1000ms
+ result = dict(status.status(self.context))
+ self.assert_('elapsed' in result)
+ self.assertEqual(result['elapsed'], '0.123')
def test_status_method_when_playing_contains_bitrate(self):
self.backend.current_playlist.append([Track(bitrate=320)])
diff --git a/tests/frontends/mpd/stickers_test.py b/tests/frontends/mpd/stickers_test.py
deleted file mode 100644
index 86ac8aec..00000000
--- a/tests/frontends/mpd/stickers_test.py
+++ /dev/null
@@ -1,45 +0,0 @@
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
-
-class StickersHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_sticker_get(self):
- result = self.dispatcher.handle_request(
- u'sticker get "song" "file:///dev/urandom" "a_name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_sticker_set(self):
- result = self.dispatcher.handle_request(
- u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_sticker_delete_with_name(self):
- result = self.dispatcher.handle_request(
- u'sticker delete "song" "file:///dev/urandom" "a_name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_sticker_delete_without_name(self):
- result = self.dispatcher.handle_request(
- u'sticker delete "song" "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_sticker_list(self):
- result = self.dispatcher.handle_request(
- u'sticker list "song" "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_sticker_find(self):
- result = self.dispatcher.handle_request(
- u'sticker find "song" "file:///dev/urandom" "a_name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py
deleted file mode 100644
index 04bab6f1..00000000
--- a/tests/frontends/mpd/stored_playlists_test.py
+++ /dev/null
@@ -1,102 +0,0 @@
-import datetime as dt
-import unittest
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.mixers.dummy import DummyMixer
-from mopidy.models import Track, Playlist
-
-class StoredPlaylistsHandlerTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
- self.mixer = DummyMixer.start().proxy()
- self.dispatcher = MpdDispatcher()
-
- def tearDown(self):
- self.backend.stop().get()
- self.mixer.stop().get()
-
- def test_listplaylist(self):
- self.backend.stored_playlists.playlists = [
- Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
- result = self.dispatcher.handle_request(u'listplaylist "name"')
- self.assert_(u'file: file:///dev/urandom' in result)
- self.assert_(u'OK' in result)
-
- def test_listplaylist_fails_if_no_playlist_is_found(self):
- result = self.dispatcher.handle_request(u'listplaylist "name"')
- self.assertEqual(result[0],
- u'ACK [50@0] {listplaylist} No such playlist')
-
- def test_listplaylistinfo(self):
- self.backend.stored_playlists.playlists = [
- Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
- result = self.dispatcher.handle_request(u'listplaylistinfo "name"')
- self.assert_(u'file: file:///dev/urandom' in result)
- self.assert_(u'Track: 0' in result)
- self.assert_(u'Pos: 0' not in result)
- self.assert_(u'OK' in result)
-
- def test_listplaylistinfo_fails_if_no_playlist_is_found(self):
- result = self.dispatcher.handle_request(u'listplaylistinfo "name"')
- self.assertEqual(result[0],
- u'ACK [50@0] {listplaylistinfo} No such playlist')
-
- def test_listplaylists(self):
- last_modified = dt.datetime(2001, 3, 17, 13, 41, 17, 12345)
- self.backend.stored_playlists.playlists = [Playlist(name='a',
- last_modified=last_modified)]
- result = self.dispatcher.handle_request(u'listplaylists')
- self.assert_(u'playlist: a' in result)
- # Date without microseconds and with time zone information
- self.assert_(u'Last-Modified: 2001-03-17T13:41:17Z' in result)
- self.assert_(u'OK' in result)
-
- def test_load_known_playlist_appends_to_current_playlist(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
- self.backend.stored_playlists.playlists = [Playlist(name='A-list',
- tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
- result = self.dispatcher.handle_request(u'load "A-list"')
- self.assert_(u'OK' in result)
- tracks = self.backend.current_playlist.tracks.get()
- self.assertEqual(len(tracks), 5)
- self.assertEqual(tracks[0].uri, 'a')
- self.assertEqual(tracks[1].uri, 'b')
- self.assertEqual(tracks[2].uri, 'c')
- self.assertEqual(tracks[3].uri, 'd')
- self.assertEqual(tracks[4].uri, 'e')
-
- def test_load_unknown_playlist_acks(self):
- result = self.dispatcher.handle_request(u'load "unknown playlist"')
- self.assert_(u'ACK [50@0] {load} No such playlist' in result)
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
-
- def test_playlistadd(self):
- result = self.dispatcher.handle_request(
- u'playlistadd "name" "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_playlistclear(self):
- result = self.dispatcher.handle_request(u'playlistclear "name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_playlistdelete(self):
- result = self.dispatcher.handle_request(u'playlistdelete "name" "5"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_playlistmove(self):
- result = self.dispatcher.handle_request(u'playlistmove "name" "5" "10"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_rename(self):
- result = self.dispatcher.handle_request(u'rename "old_name" "new_name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_rm(self):
- result = self.dispatcher.handle_request(u'rm "name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
-
- def test_save(self):
- result = self.dispatcher.handle_request(u'save "name"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
diff --git a/tests/frontends/mpris/__init__.py b/tests/frontends/mpris/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py
new file mode 100644
index 00000000..90cdab6a
--- /dev/null
+++ b/tests/frontends/mpris/events_test.py
@@ -0,0 +1,70 @@
+import mock
+
+from mopidy.frontends.mpris import MprisFrontend, objects
+from mopidy.models import Track
+
+from tests import unittest
+
+
+class BackendEventsTest(unittest.TestCase):
+ def setUp(self):
+ self.mpris_frontend = MprisFrontend() # As a plain class, not an actor
+ self.mpris_object = mock.Mock(spec=objects.MprisObject)
+ self.mpris_frontend.mpris_object = self.mpris_object
+
+ def test_track_playback_paused_event_changes_playback_status(self):
+ self.mpris_object.Get.return_value = 'Paused'
+ self.mpris_frontend.track_playback_paused(Track(), 0)
+ self.assertListEqual(self.mpris_object.Get.call_args_list, [
+ ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
+ ])
+ self.mpris_object.PropertiesChanged.assert_called_with(
+ objects.PLAYER_IFACE, {'PlaybackStatus': 'Paused'}, [])
+
+ def test_track_playback_resumed_event_changes_playback_status(self):
+ self.mpris_object.Get.return_value = 'Playing'
+ self.mpris_frontend.track_playback_resumed(Track(), 0)
+ self.assertListEqual(self.mpris_object.Get.call_args_list, [
+ ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
+ ])
+ self.mpris_object.PropertiesChanged.assert_called_with(
+ objects.PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, [])
+
+ def test_track_playback_started_event_changes_playback_status_and_metadata(self):
+ self.mpris_object.Get.return_value = '...'
+ self.mpris_frontend.track_playback_started(Track())
+ self.assertListEqual(self.mpris_object.Get.call_args_list, [
+ ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
+ ((objects.PLAYER_IFACE, 'Metadata'), {}),
+ ])
+ self.mpris_object.PropertiesChanged.assert_called_with(
+ objects.PLAYER_IFACE,
+ {'Metadata': '...', 'PlaybackStatus': '...'}, [])
+
+ def test_track_playback_ended_event_changes_playback_status_and_metadata(self):
+ self.mpris_object.Get.return_value = '...'
+ self.mpris_frontend.track_playback_ended(Track(), 0)
+ self.assertListEqual(self.mpris_object.Get.call_args_list, [
+ ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
+ ((objects.PLAYER_IFACE, 'Metadata'), {}),
+ ])
+ self.mpris_object.PropertiesChanged.assert_called_with(
+ objects.PLAYER_IFACE,
+ {'Metadata': '...', 'PlaybackStatus': '...'}, [])
+
+ def test_volume_changed_event_changes_volume(self):
+ self.mpris_object.Get.return_value = 1.0
+ self.mpris_frontend.volume_changed()
+ self.assertListEqual(self.mpris_object.Get.call_args_list, [
+ ((objects.PLAYER_IFACE, 'Volume'), {}),
+ ])
+ self.mpris_object.PropertiesChanged.assert_called_with(
+ objects.PLAYER_IFACE, {'Volume': 1.0}, [])
+
+ def test_seeked_event_causes_mpris_seeked_event(self):
+ self.mpris_object.Get.return_value = 31000000
+ self.mpris_frontend.seeked()
+ self.assertListEqual(self.mpris_object.Get.call_args_list, [
+ ((objects.PLAYER_IFACE, 'Position'), {}),
+ ])
+ self.mpris_object.Seeked.assert_called_with(31000000)
diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py
new file mode 100644
index 00000000..a966403e
--- /dev/null
+++ b/tests/frontends/mpris/player_interface_test.py
@@ -0,0 +1,826 @@
+import mock
+
+from mopidy.backends.dummy import DummyBackend
+from mopidy.backends.base.playback import PlaybackController
+from mopidy.frontends.mpris import objects
+from mopidy.mixers.dummy import DummyMixer
+from mopidy.models import Album, Artist, Track
+
+from tests import unittest
+
+PLAYING = PlaybackController.PLAYING
+PAUSED = PlaybackController.PAUSED
+STOPPED = PlaybackController.STOPPED
+
+
+class PlayerInterfaceTest(unittest.TestCase):
+ def setUp(self):
+ objects.MprisObject._connect_to_dbus = mock.Mock()
+ self.mixer = DummyMixer.start().proxy()
+ self.backend = DummyBackend.start().proxy()
+ self.mpris = objects.MprisObject()
+ self.mpris._backend = self.backend
+
+ def tearDown(self):
+ self.backend.stop()
+ self.mixer.stop()
+
+ def test_get_playback_status_is_playing_when_playing(self):
+ self.backend.playback.state = PLAYING
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
+ self.assertEqual('Playing', result)
+
+ def test_get_playback_status_is_paused_when_paused(self):
+ self.backend.playback.state = PAUSED
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
+ self.assertEqual('Paused', result)
+
+ def test_get_playback_status_is_stopped_when_stopped(self):
+ self.backend.playback.state = STOPPED
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
+ self.assertEqual('Stopped', result)
+
+ def test_get_loop_status_is_none_when_not_looping(self):
+ self.backend.playback.repeat = False
+ self.backend.playback.single = False
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
+ self.assertEqual('None', result)
+
+ def test_get_loop_status_is_track_when_looping_a_single_track(self):
+ self.backend.playback.repeat = True
+ self.backend.playback.single = True
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
+ self.assertEqual('Track', result)
+
+ def test_get_loop_status_is_playlist_when_looping_the_current_playlist(self):
+ self.backend.playback.repeat = True
+ self.backend.playback.single = False
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
+ self.assertEqual('Playlist', result)
+
+ def test_set_loop_status_is_ignored_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ self.backend.playback.repeat = True
+ self.backend.playback.single = True
+ self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None')
+ self.assertEquals(self.backend.playback.repeat.get(), True)
+ self.assertEquals(self.backend.playback.single.get(), True)
+
+ def test_set_loop_status_to_none_unsets_repeat_and_single(self):
+ self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None')
+ self.assertEquals(self.backend.playback.repeat.get(), False)
+ self.assertEquals(self.backend.playback.single.get(), False)
+
+ def test_set_loop_status_to_track_sets_repeat_and_single(self):
+ self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Track')
+ self.assertEquals(self.backend.playback.repeat.get(), True)
+ self.assertEquals(self.backend.playback.single.get(), True)
+
+ def test_set_loop_status_to_playlists_sets_repeat_and_not_single(self):
+ self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Playlist')
+ self.assertEquals(self.backend.playback.repeat.get(), True)
+ self.assertEquals(self.backend.playback.single.get(), False)
+
+ def test_get_rate_is_greater_or_equal_than_minimum_rate(self):
+ rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate')
+ minimum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate')
+ self.assert_(rate >= minimum_rate)
+
+ def test_get_rate_is_less_or_equal_than_maximum_rate(self):
+ rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate')
+ maximum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate')
+ self.assert_(rate >= maximum_rate)
+
+ def test_set_rate_is_ignored_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_set_rate_to_zero_pauses_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0)
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_get_shuffle_returns_true_if_random_is_active(self):
+ self.backend.playback.random = True
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Shuffle')
+ self.assertTrue(result)
+
+ def test_get_shuffle_returns_false_if_random_is_inactive(self):
+ self.backend.playback.random = False
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Shuffle')
+ self.assertFalse(result)
+
+ def test_set_shuffle_is_ignored_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ self.backend.playback.random = False
+ result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
+ self.assertFalse(self.backend.playback.random.get())
+
+ def test_set_shuffle_to_true_activates_random_mode(self):
+ self.backend.playback.random = False
+ self.assertFalse(self.backend.playback.random.get())
+ result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
+ self.assertTrue(self.backend.playback.random.get())
+
+ def test_set_shuffle_to_false_deactivates_random_mode(self):
+ self.backend.playback.random = True
+ self.assertTrue(self.backend.playback.random.get())
+ result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', False)
+ self.assertFalse(self.backend.playback.random.get())
+
+ def test_get_metadata_has_trackid_even_when_no_current_track(self):
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assert_('mpris:trackid' in result.keys())
+ self.assertEquals(result['mpris:trackid'], '')
+
+ def test_get_metadata_has_trackid_based_on_cpid(self):
+ self.backend.current_playlist.append([Track(uri='a')])
+ self.backend.playback.play()
+ (cpid, track) = self.backend.playback.current_cp_track.get()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('mpris:trackid', result.keys())
+ self.assertEquals(result['mpris:trackid'],
+ '/com/mopidy/track/%d' % cpid)
+
+ def test_get_metadata_has_track_length(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('mpris:length', result.keys())
+ self.assertEquals(result['mpris:length'], 40000000)
+
+ def test_get_metadata_has_track_uri(self):
+ self.backend.current_playlist.append([Track(uri='a')])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:url', result.keys())
+ self.assertEquals(result['xesam:url'], 'a')
+
+ def test_get_metadata_has_track_title(self):
+ self.backend.current_playlist.append([Track(name='a')])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:title', result.keys())
+ self.assertEquals(result['xesam:title'], 'a')
+
+ def test_get_metadata_has_track_artists(self):
+ self.backend.current_playlist.append([Track(artists=[
+ Artist(name='a'), Artist(name='b'), Artist(name=None)])])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:artist', result.keys())
+ self.assertEquals(result['xesam:artist'], ['a', 'b'])
+
+ def test_get_metadata_has_track_album(self):
+ self.backend.current_playlist.append([Track(album=Album(name='a'))])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:album', result.keys())
+ self.assertEquals(result['xesam:album'], 'a')
+
+ def test_get_metadata_has_track_album_artists(self):
+ self.backend.current_playlist.append([Track(album=Album(artists=[
+ Artist(name='a'), Artist(name='b'), Artist(name=None)]))])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:albumArtist', result.keys())
+ self.assertEquals(result['xesam:albumArtist'], ['a', 'b'])
+
+ def test_get_metadata_has_track_number_in_album(self):
+ self.backend.current_playlist.append([Track(track_no=7)])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
+ self.assertIn('xesam:trackNumber', result.keys())
+ self.assertEquals(result['xesam:trackNumber'], 7)
+
+ def test_get_volume_should_return_volume_between_zero_and_one(self):
+ self.mixer.volume = 0
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
+ self.assertEquals(result, 0)
+
+ self.mixer.volume = 50
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
+ self.assertEquals(result, 0.5)
+
+ self.mixer.volume = 100
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
+ self.assertEquals(result, 1)
+
+ def test_set_volume_is_ignored_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ self.mixer.volume = 0
+ self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0)
+ self.assertEquals(self.mixer.volume.get(), 0)
+
+ def test_set_volume_to_one_should_set_mixer_volume_to_100(self):
+ self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0)
+ self.assertEquals(self.mixer.volume.get(), 100)
+
+ def test_set_volume_to_anything_above_one_should_set_mixer_volume_to_100(self):
+ self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 2.0)
+ self.assertEquals(self.mixer.volume.get(), 100)
+
+ def test_set_volume_to_anything_not_a_number_does_not_change_volume(self):
+ self.mixer.volume = 10
+ self.mpris.Set(objects.PLAYER_IFACE, 'Volume', None)
+ self.assertEquals(self.mixer.volume.get(), 10)
+
+ def test_get_position_returns_time_position_in_microseconds(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(10000)
+ result_in_microseconds = self.mpris.Get(objects.PLAYER_IFACE, 'Position')
+ result_in_milliseconds = result_in_microseconds // 1000
+ self.assert_(result_in_milliseconds >= 10000)
+
+ def test_get_position_when_no_current_track_should_be_zero(self):
+ result_in_microseconds = self.mpris.Get(objects.PLAYER_IFACE, 'Position')
+ result_in_milliseconds = result_in_microseconds // 1000
+ self.assertEquals(result_in_milliseconds, 0)
+
+ def test_get_minimum_rate_is_one_or_less(self):
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate')
+ self.assert_(result <= 1.0)
+
+ def test_get_maximum_rate_is_one_or_more(self):
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate')
+ self.assert_(result >= 1.0)
+
+ def test_can_go_next_is_true_if_can_control_and_other_next_track(self):
+ self.mpris.get_CanControl = lambda *_: True
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
+ self.assertTrue(result)
+
+ def test_can_go_next_is_false_if_next_track_is_the_same(self):
+ self.mpris.get_CanControl = lambda *_: True
+ self.backend.current_playlist.append([Track(uri='a')])
+ self.backend.playback.repeat = True
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
+ self.assertFalse(result)
+
+ def test_can_go_next_is_false_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
+ self.assertFalse(result)
+
+ def test_can_go_previous_is_true_if_can_control_and_other_previous_track(self):
+ self.mpris.get_CanControl = lambda *_: True
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
+ self.assertTrue(result)
+
+ def test_can_go_previous_is_false_if_previous_track_is_the_same(self):
+ self.mpris.get_CanControl = lambda *_: True
+ self.backend.current_playlist.append([Track(uri='a')])
+ self.backend.playback.repeat = True
+ self.backend.playback.play()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
+ self.assertFalse(result)
+
+ def test_can_go_previous_is_false_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
+ self.assertFalse(result)
+
+ def test_can_play_is_true_if_can_control_and_current_track(self):
+ self.mpris.get_CanControl = lambda *_: True
+ self.backend.current_playlist.append([Track(uri='a')])
+ self.backend.playback.play()
+ self.assertTrue(self.backend.playback.current_track.get())
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay')
+ self.assertTrue(result)
+
+ def test_can_play_is_false_if_no_current_track(self):
+ self.mpris.get_CanControl = lambda *_: True
+ self.assertFalse(self.backend.playback.current_track.get())
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay')
+ self.assertFalse(result)
+
+ def test_can_play_if_false_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay')
+ self.assertFalse(result)
+
+ def test_can_pause_is_true_if_can_control_and_track_can_be_paused(self):
+ self.mpris.get_CanControl = lambda *_: True
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPause')
+ self.assertTrue(result)
+
+ def test_can_pause_if_false_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPause')
+ self.assertFalse(result)
+
+ def test_can_seek_is_true_if_can_control_is_true(self):
+ self.mpris.get_CanControl = lambda *_: True
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanSeek')
+ self.assertTrue(result)
+
+ def test_can_seek_is_false_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanSeek')
+ self.assertFalse(result)
+
+ def test_can_control_is_true(self):
+ result = self.mpris.Get(objects.PLAYER_IFACE, 'CanControl')
+ self.assertTrue(result)
+
+ def test_next_is_ignored_if_can_go_next_is_false(self):
+ self.mpris.get_CanGoNext = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.mpris.Next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ def test_next_when_playing_should_skip_to_next_track_and_keep_playing(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_next_when_at_end_of_list_should_stop_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Next()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_next_when_paused_should_skip_to_next_track_and_stay_paused(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.pause()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.mpris.Next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_next_when_stopped_should_skip_to_next_track_and_stay_stopped(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.stop()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.Next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_previous_is_ignored_if_can_go_previous_is_false(self):
+ self.mpris.get_CanGoPrevious = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.mpris.Previous()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+
+ def test_previous_when_playing_should_skip_to_prev_track_and_keep_playing(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Previous()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_previous_when_at_start_of_list_should_stop_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Previous()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_previous_when_paused_should_skip_to_previous_track_and_stay_paused(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.backend.playback.pause()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.mpris.Previous()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_previous_when_stopped_should_skip_to_previous_track_and_stay_stopped(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.next()
+ self.backend.playback.stop()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.Previous()
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_pause_is_ignored_if_can_pause_is_false(self):
+ self.mpris.get_CanPause = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Pause()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_pause_when_playing_should_pause_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_pause_when_paused_has_no_effect(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.mpris.Pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_playpause_is_ignored_if_can_pause_is_false(self):
+ self.mpris.get_CanPause = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.PlayPause()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_playpause_when_playing_should_pause_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.PlayPause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+
+ def test_playpause_when_paused_should_resume_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.pause()
+
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ at_pause = self.backend.playback.time_position.get()
+ self.assert_(at_pause >= 0)
+
+ self.mpris.PlayPause()
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ after_pause = self.backend.playback.time_position.get()
+ self.assert_(after_pause >= at_pause)
+
+ def test_playpause_when_stopped_should_start_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.PlayPause()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_stop_is_ignored_if_can_control_is_false(self):
+ self.mpris.get_CanControl = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Stop()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_stop_when_playing_should_stop_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.mpris.Stop()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_stop_when_paused_should_stop_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.mpris.Stop()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_play_is_ignored_if_can_play_is_false(self):
+ self.mpris.get_CanPlay = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.Play()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_play_when_stopped_starts_playback(self):
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.Play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ def test_play_after_pause_resumes_from_same_position(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+
+ before_pause = self.backend.playback.time_position.get()
+ self.assert_(before_pause >= 0)
+
+ self.mpris.Pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ at_pause = self.backend.playback.time_position.get()
+ self.assert_(at_pause >= before_pause)
+
+ self.mpris.Play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ after_pause = self.backend.playback.time_position.get()
+ self.assert_(after_pause >= at_pause)
+
+ def test_play_when_there_is_no_track_has_no_effect(self):
+ self.backend.current_playlist.clear()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.mpris.Play()
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ def test_seek_is_ignored_if_can_seek_is_false(self):
+ self.mpris.get_CanSeek = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+
+ before_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek >= 0)
+
+ milliseconds_to_seek = 10000
+ microseconds_to_seek = milliseconds_to_seek * 1000
+
+ self.mpris.Seek(microseconds_to_seek)
+
+ after_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek <= after_seek < (
+ before_seek + milliseconds_to_seek))
+
+ def test_seek_seeks_given_microseconds_forward_in_the_current_track(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+
+ before_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek >= 0)
+
+ milliseconds_to_seek = 10000
+ microseconds_to_seek = milliseconds_to_seek * 1000
+
+ self.mpris.Seek(microseconds_to_seek)
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ after_seek = self.backend.playback.time_position.get()
+ self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
+
+ def test_seek_seeks_given_microseconds_backward_if_negative(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek >= 20000)
+
+ milliseconds_to_seek = -10000
+ microseconds_to_seek = milliseconds_to_seek * 1000
+
+ self.mpris.Seek(microseconds_to_seek)
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ after_seek = self.backend.playback.time_position.get()
+ self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
+ self.assert_(after_seek < before_seek)
+
+ def test_seek_seeks_to_start_of_track_if_new_position_is_negative(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek >= 20000)
+
+ milliseconds_to_seek = -30000
+ microseconds_to_seek = milliseconds_to_seek * 1000
+
+ self.mpris.Seek(microseconds_to_seek)
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ after_seek = self.backend.playback.time_position.get()
+ self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
+ self.assert_(after_seek < before_seek)
+ self.assert_(after_seek >= 0)
+
+ def test_seek_skips_to_next_track_if_new_position_larger_than_track_length(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000),
+ Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_seek = self.backend.playback.time_position.get()
+ self.assert_(before_seek >= 20000)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ milliseconds_to_seek = 50000
+ microseconds_to_seek = milliseconds_to_seek * 1000
+
+ self.mpris.Seek(microseconds_to_seek)
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+
+ after_seek = self.backend.playback.time_position.get()
+ self.assert_(after_seek >= 0)
+ self.assert_(after_seek < before_seek)
+
+ def test_set_position_is_ignored_if_can_seek_is_false(self):
+ self.mpris.get_CanSeek = lambda *_: False
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+
+ before_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position <= 5000)
+
+ track_id = 'a'
+
+ position_to_set_in_milliseconds = 20000
+ position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+
+ self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+
+ after_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position <= after_set_position <
+ position_to_set_in_milliseconds)
+
+ def test_set_position_sets_the_current_track_position_in_microsecs(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+
+ before_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position <= 5000)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ track_id = '/com/mopidy/track/0'
+
+ position_to_set_in_milliseconds = 20000
+ position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+
+ self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+
+ after_set_position = self.backend.playback.time_position.get()
+ self.assert_(after_set_position >= position_to_set_in_milliseconds)
+
+ def test_set_position_does_nothing_if_the_position_is_negative(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position >= 20000)
+ self.assert_(before_set_position <= 25000)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ track_id = '/com/mopidy/track/0'
+
+ position_to_set_in_milliseconds = -1000
+ position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+
+ self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+
+ after_set_position = self.backend.playback.time_position.get()
+ self.assert_(after_set_position >= before_set_position)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ def test_set_position_does_nothing_if_position_is_larger_than_track_length(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position >= 20000)
+ self.assert_(before_set_position <= 25000)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ track_id = 'a'
+
+ position_to_set_in_milliseconds = 50000
+ position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+
+ self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+
+ after_set_position = self.backend.playback.time_position.get()
+ self.assert_(after_set_position >= before_set_position)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ def test_set_position_does_nothing_if_track_id_does_not_match_current_track(self):
+ self.backend.current_playlist.append([Track(uri='a', length=40000)])
+ self.backend.playback.play()
+ self.backend.playback.seek(20000)
+
+ before_set_position = self.backend.playback.time_position.get()
+ self.assert_(before_set_position >= 20000)
+ self.assert_(before_set_position <= 25000)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ track_id = 'b'
+
+ position_to_set_in_milliseconds = 0
+ position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+
+ self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+
+ after_set_position = self.backend.playback.time_position.get()
+ self.assert_(after_set_position >= before_set_position)
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ def test_open_uri_is_ignored_if_can_play_is_false(self):
+ self.mpris.get_CanPlay = lambda *_: False
+ self.backend.library.provider.dummy_library = [
+ Track(uri='dummy:/test/uri')]
+ self.mpris.OpenUri('dummy:/test/uri')
+ self.assertEquals(len(self.backend.current_playlist.tracks.get()), 0)
+
+ def test_open_uri_ignores_uris_with_unknown_uri_scheme(self):
+ self.assertListEqual(self.backend.uri_schemes.get(), ['dummy'])
+ self.mpris.get_CanPlay = lambda *_: True
+ self.backend.library.provider.dummy_library = [
+ Track(uri='notdummy:/test/uri')]
+ self.mpris.OpenUri('notdummy:/test/uri')
+ self.assertEquals(len(self.backend.current_playlist.tracks.get()), 0)
+
+ def test_open_uri_adds_uri_to_current_playlist(self):
+ self.mpris.get_CanPlay = lambda *_: True
+ self.backend.library.provider.dummy_library = [
+ Track(uri='dummy:/test/uri')]
+ self.mpris.OpenUri('dummy:/test/uri')
+ self.assertEquals(self.backend.current_playlist.tracks.get()[0].uri,
+ 'dummy:/test/uri')
+
+ def test_open_uri_starts_playback_of_new_track_if_stopped(self):
+ self.mpris.get_CanPlay = lambda *_: True
+ self.backend.library.provider.dummy_library = [
+ Track(uri='dummy:/test/uri')]
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEquals(self.backend.playback.state.get(), STOPPED)
+
+ self.mpris.OpenUri('dummy:/test/uri')
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri,
+ 'dummy:/test/uri')
+
+ def test_open_uri_starts_playback_of_new_track_if_paused(self):
+ self.mpris.get_CanPlay = lambda *_: True
+ self.backend.library.provider.dummy_library = [
+ Track(uri='dummy:/test/uri')]
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.backend.playback.pause()
+ self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ self.mpris.OpenUri('dummy:/test/uri')
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri,
+ 'dummy:/test/uri')
+
+ def test_open_uri_starts_playback_of_new_track_if_playing(self):
+ self.mpris.get_CanPlay = lambda *_: True
+ self.backend.library.provider.dummy_library = [
+ Track(uri='dummy:/test/uri')]
+ self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.backend.playback.play()
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+
+ self.mpris.OpenUri('dummy:/test/uri')
+
+ self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEquals(self.backend.playback.current_track.get().uri,
+ 'dummy:/test/uri')
diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py
new file mode 100644
index 00000000..443efdd3
--- /dev/null
+++ b/tests/frontends/mpris/root_interface_test.py
@@ -0,0 +1,63 @@
+import mock
+
+from mopidy import settings
+from mopidy.backends.dummy import DummyBackend
+from mopidy.frontends.mpris import objects
+
+from tests import unittest
+
+
+class RootInterfaceTest(unittest.TestCase):
+ def setUp(self):
+ objects.exit_process = mock.Mock()
+ objects.MprisObject._connect_to_dbus = mock.Mock()
+ self.backend = DummyBackend.start().proxy()
+ self.mpris = objects.MprisObject()
+
+ def tearDown(self):
+ self.backend.stop()
+
+ def test_constructor_connects_to_dbus(self):
+ self.assert_(self.mpris._connect_to_dbus.called)
+
+ def test_can_raise_returns_false(self):
+ result = self.mpris.Get(objects.ROOT_IFACE, 'CanRaise')
+ self.assertFalse(result)
+
+ def test_raise_does_nothing(self):
+ self.mpris.Raise()
+
+ def test_can_quit_returns_true(self):
+ result = self.mpris.Get(objects.ROOT_IFACE, 'CanQuit')
+ self.assertTrue(result)
+
+ def test_quit_should_stop_all_actors(self):
+ self.mpris.Quit()
+ self.assert_(objects.exit_process.called)
+
+ def test_has_track_list_returns_false(self):
+ result = self.mpris.Get(objects.ROOT_IFACE, 'HasTrackList')
+ self.assertFalse(result)
+
+ def test_identify_is_mopidy(self):
+ result = self.mpris.Get(objects.ROOT_IFACE, 'Identity')
+ self.assertEquals(result, 'Mopidy')
+
+ def test_desktop_entry_is_mopidy(self):
+ result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry')
+ self.assertEquals(result, 'mopidy')
+
+ def test_desktop_entry_is_based_on_DESKTOP_FILE_setting(self):
+ settings.runtime['DESKTOP_FILE'] = '/tmp/foo.desktop'
+ result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry')
+ self.assertEquals(result, 'foo')
+ settings.runtime.clear()
+
+ def test_supported_uri_schemes_is_empty(self):
+ result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedUriSchemes')
+ self.assertEquals(len(result), 1)
+ self.assertEquals(result[0], 'dummy')
+
+ def test_supported_mime_types_is_empty(self):
+ result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedMimeTypes')
+ self.assertEquals(len(result), 0)
diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py
index 0b9a559e..66e0995e 100644
--- a/tests/gstreamer_test.py
+++ b/tests/gstreamer_test.py
@@ -1,21 +1,16 @@
-import multiprocessing
-import unittest
-
-from tests import SkipTest
-
-# FIXME Our Windows build server does not support GStreamer yet
import sys
-if sys.platform == 'win32':
- raise SkipTest
from mopidy import settings
from mopidy.gstreamer import GStreamer
from mopidy.utils.path import path_to_uri
-from tests import path_to_data_dir
+from tests import unittest, path_to_data_dir
# TODO BaseOutputTest?
+
+@unittest.skipIf(sys.platform == 'win32',
+ 'Our Windows build server does not support GStreamer yet')
class GStreamerTest(unittest.TestCase):
def setUp(self):
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
@@ -48,11 +43,11 @@ class GStreamerTest(unittest.TestCase):
self.gstreamer.start_playback()
self.assertTrue(self.gstreamer.stop_playback())
- @SkipTest
+ @unittest.SkipTest
def test_deliver_data(self):
pass # TODO
- @SkipTest
+ @unittest.SkipTest
def test_end_of_data_stream(self):
pass # TODO
@@ -71,10 +66,10 @@ class GStreamerTest(unittest.TestCase):
self.assertTrue(self.gstreamer.set_volume(100))
self.assertEqual(100, self.gstreamer.get_volume())
- @SkipTest
+ @unittest.SkipTest
def test_set_state_encapsulation(self):
pass # TODO
- @SkipTest
+ @unittest.SkipTest
def test_set_position(self):
pass # TODO
diff --git a/tests/help_test.py b/tests/help_test.py
index 25f534c2..1fa22c2f 100644
--- a/tests/help_test.py
+++ b/tests/help_test.py
@@ -1,10 +1,12 @@
import os
import subprocess
import sys
-import unittest
import mopidy
+from tests import unittest
+
+
class HelpTest(unittest.TestCase):
def test_help_has_mopidy_options(self):
mopidy_dir = os.path.dirname(mopidy.__file__)
diff --git a/tests/listeners_test.py b/tests/listeners_test.py
new file mode 100644
index 00000000..486dcf9c
--- /dev/null
+++ b/tests/listeners_test.py
@@ -0,0 +1,36 @@
+from mopidy.listeners import BackendListener
+from mopidy.models import Track
+
+from tests import unittest
+
+
+class BackendListenerTest(unittest.TestCase):
+ def setUp(self):
+ self.listener = BackendListener()
+
+ def test_listener_has_default_impl_for_track_playback_paused(self):
+ self.listener.track_playback_paused(Track(), 0)
+
+ def test_listener_has_default_impl_for_track_playback_resumed(self):
+ self.listener.track_playback_resumed(Track(), 0)
+
+ def test_listener_has_default_impl_for_track_playback_started(self):
+ self.listener.track_playback_started(Track())
+
+ def test_listener_has_default_impl_for_track_playback_ended(self):
+ self.listener.track_playback_ended(Track(), 0)
+
+ def test_listener_has_default_impl_for_playback_state_changed(self):
+ self.listener.playback_state_changed()
+
+ def test_listener_has_default_impl_for_playlist_changed(self):
+ self.listener.playlist_changed()
+
+ def test_listener_has_default_impl_for_options_changed(self):
+ self.listener.options_changed()
+
+ def test_listener_has_default_impl_for_volume_changed(self):
+ self.listener.volume_changed()
+
+ def test_listener_has_default_impl_for_seeked(self):
+ self.listener.seeked()
diff --git a/tests/mixers/denon_test.py b/tests/mixers/denon_test.py
index 5370f155..7fec3c82 100644
--- a/tests/mixers/denon_test.py
+++ b/tests/mixers/denon_test.py
@@ -1,8 +1,9 @@
-import unittest
-
from mopidy.mixers.denon import DenonMixer
from tests.mixers.base_test import BaseMixerTest
+from tests import unittest
+
+
class DenonMixerDeviceMock(object):
def __init__(self):
self._open = True
@@ -24,6 +25,7 @@ class DenonMixerDeviceMock(object):
def open(self):
self._open = True
+
class DenonMixerTest(BaseMixerTest, unittest.TestCase):
ACTUAL_MAX = 99
INITIAL = 1
diff --git a/tests/mixers/dummy_test.py b/tests/mixers/dummy_test.py
index 334dc8a1..8ae8623c 100644
--- a/tests/mixers/dummy_test.py
+++ b/tests/mixers/dummy_test.py
@@ -1,8 +1,9 @@
-import unittest
-
from mopidy.mixers.dummy import DummyMixer
+
+from tests import unittest
from tests.mixers.base_test import BaseMixerTest
+
class DenonMixerTest(BaseMixerTest, unittest.TestCase):
mixer_class = DummyMixer
diff --git a/tests/models_test.py b/tests/models_test.py
index 637a8209..978f35b6 100644
--- a/tests/models_test.py
+++ b/tests/models_test.py
@@ -1,9 +1,9 @@
-import datetime as dt
-import unittest
+import datetime
from mopidy.models import Artist, Album, CpTrack, Track, Playlist
-from tests import SkipTest
+from tests import unittest
+
class GenericCopyTets(unittest.TestCase):
def compare(self, orig, other):
@@ -49,6 +49,7 @@ class GenericCopyTets(unittest.TestCase):
test = lambda: Track().copy(invalid_key=True)
self.assertRaises(TypeError, test)
+
class ArtistTest(unittest.TestCase):
def test_uri(self):
uri = u'an_uri'
@@ -321,7 +322,7 @@ class TrackTest(unittest.TestCase):
self.assertRaises(AttributeError, setattr, track, 'track_no', None)
def test_date(self):
- date = dt.date(1977, 1, 1)
+ date = datetime.date(1977, 1, 1)
track = Track(date=date)
self.assertEqual(track.date, date)
self.assertRaises(AttributeError, setattr, track, 'date', None)
@@ -400,7 +401,7 @@ class TrackTest(unittest.TestCase):
self.assertEqual(hash(track1), hash(track2))
def test_eq_date(self):
- date = dt.date.today()
+ date = datetime.date.today()
track1 = Track(date=date)
track2 = Track(date=date)
self.assertEqual(track1, track2)
@@ -425,7 +426,7 @@ class TrackTest(unittest.TestCase):
self.assertEqual(hash(track1), hash(track2))
def test_eq(self):
- date = dt.date.today()
+ date = datetime.date.today()
artists = [Artist()]
album = Album()
track1 = Track(uri=u'uri', name=u'name', artists=artists, album=album,
@@ -474,8 +475,8 @@ class TrackTest(unittest.TestCase):
self.assertNotEqual(hash(track1), hash(track2))
def test_ne_date(self):
- track1 = Track(date=dt.date.today())
- track2 = Track(date=dt.date.today()-dt.timedelta(days=1))
+ track1 = Track(date=datetime.date.today())
+ track2 = Track(date=datetime.date.today()-datetime.timedelta(days=1))
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
@@ -500,11 +501,11 @@ class TrackTest(unittest.TestCase):
def test_ne(self):
track1 = Track(uri=u'uri1', name=u'name1',
artists=[Artist(name=u'name1')], album=Album(name=u'name1'),
- track_no=1, date=dt.date.today(), length=100, bitrate=100,
+ track_no=1, date=datetime.date.today(), length=100, bitrate=100,
musicbrainz_id='id1')
track2 = Track(uri=u'uri2', name=u'name2',
artists=[Artist(name=u'name2')], album=Album(name=u'name2'),
- track_no=2, date=dt.date.today()-dt.timedelta(days=1),
+ track_no=2, date=datetime.date.today()-datetime.timedelta(days=1),
length=200, bitrate=200, musicbrainz_id='id2')
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
@@ -535,7 +536,7 @@ class PlaylistTest(unittest.TestCase):
self.assertEqual(playlist.length, 3)
def test_last_modified(self):
- last_modified = dt.datetime.now()
+ last_modified = datetime.datetime.now()
playlist = Playlist(last_modified=last_modified)
self.assertEqual(playlist.last_modified, last_modified)
self.assertRaises(AttributeError, setattr, playlist, 'last_modified',
@@ -543,7 +544,7 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_uri(self):
tracks = [Track()]
- last_modified = dt.datetime.now()
+ last_modified = datetime.datetime.now()
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_playlist = playlist.copy(uri=u'another uri')
@@ -554,7 +555,7 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_name(self):
tracks = [Track()]
- last_modified = dt.datetime.now()
+ last_modified = datetime.datetime.now()
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_playlist = playlist.copy(name=u'another name')
@@ -565,7 +566,7 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_tracks(self):
tracks = [Track()]
- last_modified = dt.datetime.now()
+ last_modified = datetime.datetime.now()
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_tracks = [Track(), Track()]
@@ -577,8 +578,8 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_last_modified(self):
tracks = [Track()]
- last_modified = dt.datetime.now()
- new_last_modified = last_modified + dt.timedelta(1)
+ last_modified = datetime.datetime.now()
+ new_last_modified = last_modified + datetime.timedelta(1)
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_playlist = playlist.copy(last_modified=new_last_modified)
diff --git a/tests/scanner_test.py b/tests/scanner_test.py
index f403a221..91e67e11 100644
--- a/tests/scanner_test.py
+++ b/tests/scanner_test.py
@@ -1,10 +1,10 @@
-import unittest
from datetime import date
from mopidy.scanner import Scanner, translator
from mopidy.models import Track, Artist, Album
-from tests import path_to_data_dir, SkipTest
+from tests import unittest, path_to_data_dir
+
class FakeGstDate(object):
def __init__(self, year, month, day):
@@ -12,6 +12,7 @@ class FakeGstDate(object):
self.month = month
self.day = day
+
class TranslatorTest(unittest.TestCase):
def setUp(self):
self.data = {
@@ -126,6 +127,7 @@ class TranslatorTest(unittest.TestCase):
del self.track['date']
self.check()
+
class ScannerTest(unittest.TestCase):
def setUp(self):
self.errors = {}
@@ -185,6 +187,6 @@ class ScannerTest(unittest.TestCase):
self.scan('scanner/image')
self.assert_(self.errors)
- @SkipTest
+ @unittest.SkipTest
def test_song_without_time_is_handeled(self):
pass
diff --git a/tests/utils/init_test.py b/tests/utils/init_test.py
index fb38e2ea..2097e3e6 100644
--- a/tests/utils/init_test.py
+++ b/tests/utils/init_test.py
@@ -1,15 +1,17 @@
-import unittest
-
from mopidy.utils import get_class
+from tests import unittest
+
+
class GetClassTest(unittest.TestCase):
def test_loading_module_that_does_not_exist(self):
- test = lambda: get_class('foo.bar.Baz')
- self.assertRaises(ImportError, test)
+ self.assertRaises(ImportError, get_class, 'foo.bar.Baz')
def test_loading_class_that_does_not_exist(self):
- test = lambda: get_class('unittest.FooBarBaz')
- self.assertRaises(ImportError, test)
+ self.assertRaises(ImportError, get_class, 'unittest.FooBarBaz')
+
+ def test_loading_incorrect_class_path(self):
+ self.assertRaises(ImportError, get_class, 'foobarbaz')
def test_import_error_message_contains_complete_class_path(self):
try:
diff --git a/tests/utils/network/__init__.py b/tests/utils/network/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py
new file mode 100644
index 00000000..aa1be2b6
--- /dev/null
+++ b/tests/utils/network/connection_test.py
@@ -0,0 +1,539 @@
+import errno
+import gobject
+import logging
+import pykka
+import socket
+from mock import patch, sentinel, Mock
+
+from mopidy.utils import network
+
+from tests import unittest, any_int, any_unicode
+
+
+class ConnectionTest(unittest.TestCase):
+ def setUp(self):
+ self.mock = Mock(spec=network.Connection)
+
+ def test_init_ensure_nonblocking_io(self):
+ sock = Mock(spec=socket.SocketType)
+
+ network.Connection.__init__(self.mock, Mock(), sock,
+ (sentinel.host, sentinel.port), sentinel.timeout)
+ sock.setblocking.assert_called_once_with(False)
+
+ def test_init_starts_actor(self):
+ protocol = Mock(spec=network.LineProtocol)
+
+ network.Connection.__init__(self.mock, protocol, Mock(),
+ (sentinel.host, sentinel.port), sentinel.timeout)
+ protocol.start.assert_called_once_with(self.mock)
+
+ def test_init_enables_recv_and_timeout(self):
+ network.Connection.__init__(self.mock, Mock(), Mock(),
+ (sentinel.host, sentinel.port), sentinel.timeout)
+ self.mock.enable_recv.assert_called_once_with()
+ self.mock.enable_timeout.assert_called_once_with()
+
+ def test_init_stores_values_in_attributes(self):
+ addr = (sentinel.host, sentinel.port)
+ protocol = Mock(spec=network.LineProtocol)
+ sock = Mock(spec=socket.SocketType)
+
+ network.Connection.__init__(
+ self.mock, protocol, sock, addr, sentinel.timeout)
+ self.assertEqual(sock, self.mock.sock)
+ self.assertEqual(protocol, self.mock.protocol)
+ self.assertEqual(sentinel.timeout, self.mock.timeout)
+ self.assertEqual(sentinel.host, self.mock.host)
+ self.assertEqual(sentinel.port, self.mock.port)
+
+ def test_init_handles_ipv6_addr(self):
+ addr = (sentinel.host, sentinel.port,
+ sentinel.flowinfo, sentinel.scopeid)
+ protocol = Mock(spec=network.LineProtocol)
+ sock = Mock(spec=socket.SocketType)
+
+ network.Connection.__init__(
+ self.mock, protocol, sock, addr, sentinel.timeout)
+ self.assertEqual(sentinel.host, self.mock.host)
+ self.assertEqual(sentinel.port, self.mock.port)
+
+ def test_stop_disables_recv_send_and_timeout(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.mock.disable_timeout.assert_called_once_with()
+ self.mock.disable_recv.assert_called_once_with()
+ self.mock.disable_send.assert_called_once_with()
+
+ def test_stop_closes_socket(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.mock.sock.close.assert_called_once_with()
+
+ def test_stop_closes_socket_error(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.close.side_effect = socket.error
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.mock.sock.close.assert_called_once_with()
+
+ def test_stop_stops_actor(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.mock.actor_ref.stop.assert_called_once_with()
+
+ def test_stop_handles_actor_already_being_stopped(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.actor_ref.stop.side_effect = pykka.ActorDeadError()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.mock.actor_ref.stop.assert_called_once_with()
+
+ def test_stop_sets_stopping_to_true(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.assertEqual(True, self.mock.stopping)
+
+ def test_stop_does_not_proceed_when_already_stopping(self):
+ self.mock.stopping = True
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ self.assertEqual(0, self.mock.actor_ref.stop.call_count)
+ self.assertEqual(0, self.mock.sock.close.call_count)
+
+ @patch.object(network.logger, 'log', new=Mock())
+ def test_stop_logs_reason(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ network.logger.log.assert_called_once_with(
+ logging.DEBUG, sentinel.reason)
+
+ @patch.object(network.logger, 'log', new=Mock())
+ def test_stop_logs_reason_with_level(self):
+ self.mock.stopping = False
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason,
+ level=sentinel.level)
+ network.logger.log.assert_called_once_with(
+ sentinel.level, sentinel.reason)
+
+ @patch.object(network.logger, 'log', new=Mock())
+ def test_stop_logs_that_it_is_calling_itself(self):
+ self.mock.stopping = True
+ self.mock.actor_ref = Mock()
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.stop(self.mock, sentinel.reason)
+ network.logger.log(any_int, any_unicode)
+
+ @patch.object(gobject, 'io_add_watch', new=Mock())
+ def test_enable_recv_registers_with_gobject(self):
+ self.mock.recv_id = None
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.fileno.return_value = sentinel.fileno
+ gobject.io_add_watch.return_value = sentinel.tag
+
+ network.Connection.enable_recv(self.mock)
+ gobject.io_add_watch.assert_called_once_with(sentinel.fileno,
+ gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
+ self.mock.recv_callback)
+ self.assertEqual(sentinel.tag, self.mock.recv_id)
+
+ @patch.object(gobject, 'io_add_watch', new=Mock())
+ def test_enable_recv_already_registered(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.recv_id = sentinel.tag
+
+ network.Connection.enable_recv(self.mock)
+ self.assertEqual(0, gobject.io_add_watch.call_count)
+
+ def test_enable_recv_does_not_change_tag(self):
+ self.mock.recv_id = sentinel.tag
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.enable_recv(self.mock)
+ self.assertEqual(sentinel.tag, self.mock.recv_id)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_recv_deregisters(self):
+ self.mock.recv_id = sentinel.tag
+
+ network.Connection.disable_recv(self.mock)
+ gobject.source_remove.assert_called_once_with(sentinel.tag)
+ self.assertEqual(None, self.mock.recv_id)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_recv_already_deregistered(self):
+ self.mock.recv_id = None
+
+ network.Connection.disable_recv(self.mock)
+ self.assertEqual(0, gobject.source_remove.call_count)
+ self.assertEqual(None, self.mock.recv_id)
+
+ def test_enable_recv_on_closed_socket(self):
+ self.mock.recv_id = None
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.fileno.side_effect = socket.error(errno.EBADF, '')
+
+ network.Connection.enable_recv(self.mock)
+ self.mock.stop.assert_called_once_with(any_unicode)
+ self.assertEqual(None, self.mock.recv_id)
+
+ @patch.object(gobject, 'io_add_watch', new=Mock())
+ def test_enable_send_registers_with_gobject(self):
+ self.mock.send_id = None
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.fileno.return_value = sentinel.fileno
+ gobject.io_add_watch.return_value = sentinel.tag
+
+ network.Connection.enable_send(self.mock)
+ gobject.io_add_watch.assert_called_once_with(sentinel.fileno,
+ gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP,
+ self.mock.send_callback)
+ self.assertEqual(sentinel.tag, self.mock.send_id)
+
+ @patch.object(gobject, 'io_add_watch', new=Mock())
+ def test_enable_send_already_registered(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.send_id = sentinel.tag
+
+ network.Connection.enable_send(self.mock)
+ self.assertEqual(0, gobject.io_add_watch.call_count)
+
+ def test_enable_send_does_not_change_tag(self):
+ self.mock.send_id = sentinel.tag
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ network.Connection.enable_send(self.mock)
+ self.assertEqual(sentinel.tag, self.mock.send_id)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_send_deregisters(self):
+ self.mock.send_id = sentinel.tag
+
+ network.Connection.disable_send(self.mock)
+ gobject.source_remove.assert_called_once_with(sentinel.tag)
+ self.assertEqual(None, self.mock.send_id)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_send_already_deregistered(self):
+ self.mock.send_id = None
+
+ network.Connection.disable_send(self.mock)
+ self.assertEqual(0, gobject.source_remove.call_count)
+ self.assertEqual(None, self.mock.send_id)
+
+ def test_enable_send_on_closed_socket(self):
+ self.mock.send_id = None
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.fileno.side_effect = socket.error(errno.EBADF, '')
+
+ network.Connection.enable_send(self.mock)
+ self.assertEqual(None, self.mock.send_id)
+
+ @patch.object(gobject, 'timeout_add_seconds', new=Mock())
+ def test_enable_timeout_clears_existing_timeouts(self):
+ self.mock.timeout = 10
+
+ network.Connection.enable_timeout(self.mock)
+ self.mock.disable_timeout.assert_called_once_with()
+
+ @patch.object(gobject, 'timeout_add_seconds', new=Mock())
+ def test_enable_timeout_add_gobject_timeout(self):
+ self.mock.timeout = 10
+ gobject.timeout_add_seconds.return_value = sentinel.tag
+
+ network.Connection.enable_timeout(self.mock)
+ gobject.timeout_add_seconds.assert_called_once_with(10,
+ self.mock.timeout_callback)
+ self.assertEqual(sentinel.tag, self.mock.timeout_id)
+
+ @patch.object(gobject, 'timeout_add_seconds', new=Mock())
+ def test_enable_timeout_does_not_add_timeout(self):
+ self.mock.timeout = 0
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, gobject.timeout_add_seconds.call_count)
+
+ self.mock.timeout = -1
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, gobject.timeout_add_seconds.call_count)
+
+ self.mock.timeout = None
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, gobject.timeout_add_seconds.call_count)
+
+ def test_enable_timeout_does_not_call_disable_for_invalid_timeout(self):
+ self.mock.timeout = 0
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, self.mock.disable_timeout.call_count)
+
+ self.mock.timeout = -1
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, self.mock.disable_timeout.call_count)
+
+ self.mock.timeout = None
+ network.Connection.enable_timeout(self.mock)
+ self.assertEqual(0, self.mock.disable_timeout.call_count)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_timeout_deregisters(self):
+ self.mock.timeout_id = sentinel.tag
+
+ network.Connection.disable_timeout(self.mock)
+ gobject.source_remove.assert_called_once_with(sentinel.tag)
+ self.assertEqual(None, self.mock.timeout_id)
+
+ @patch.object(gobject, 'source_remove', new=Mock())
+ def test_disable_timeout_already_deregistered(self):
+ self.mock.timeout_id = None
+
+ network.Connection.disable_timeout(self.mock)
+ self.assertEqual(0, gobject.source_remove.call_count)
+ self.assertEqual(None, self.mock.timeout_id)
+
+ def test_queue_send_acquires_and_releases_lock(self):
+ self.mock.send_lock = Mock()
+ self.mock.send_buffer = ''
+
+ network.Connection.queue_send(self.mock, 'data')
+ self.mock.send_lock.acquire.assert_called_once_with(True)
+ self.mock.send_lock.release.assert_called_once_with()
+
+ def test_queue_send_calls_send(self):
+ self.mock.send_buffer = ''
+ self.mock.send_lock = Mock()
+ self.mock.send.return_value = ''
+
+ network.Connection.queue_send(self.mock, 'data')
+ self.mock.send.assert_called_once_with('data')
+ self.assertEqual(0, self.mock.enable_send.call_count)
+ self.assertEqual('', self.mock.send_buffer)
+
+ def test_queue_send_calls_enable_send_for_partial_send(self):
+ self.mock.send_buffer = ''
+ self.mock.send_lock = Mock()
+ self.mock.send.return_value = 'ta'
+
+ network.Connection.queue_send(self.mock, 'data')
+ self.mock.send.assert_called_once_with('data')
+ self.mock.enable_send.assert_called_once_with()
+ self.assertEqual('ta', self.mock.send_buffer)
+
+ def test_queue_send_calls_send_with_existing_buffer(self):
+ self.mock.send_buffer = 'foo'
+ self.mock.send_lock = Mock()
+ self.mock.send.return_value = ''
+
+ network.Connection.queue_send(self.mock, 'bar')
+ self.mock.send.assert_called_once_with('foobar')
+ self.assertEqual(0, self.mock.enable_send.call_count)
+ self.assertEqual('', self.mock.send_buffer)
+
+ def test_recv_callback_respects_io_err(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.actor_ref = Mock()
+
+ self.assertTrue(network.Connection.recv_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_ERR))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_recv_callback_respects_io_hup(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.actor_ref = Mock()
+
+ self.assertTrue(network.Connection.recv_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_HUP))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_recv_callback_respects_io_hup_and_io_err(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.actor_ref = Mock()
+
+ self.assertTrue(network.Connection.recv_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_recv_callback_sends_data_to_actor(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.recv.return_value = 'data'
+ self.mock.actor_ref = Mock()
+
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.actor_ref.send_one_way.assert_called_once_with(
+ {'received': 'data'})
+
+ def test_recv_callback_handles_dead_actors(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.recv.return_value = 'data'
+ self.mock.actor_ref = Mock()
+ self.mock.actor_ref.send_one_way.side_effect = pykka.ActorDeadError()
+
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_recv_callback_gets_no_data(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.recv.return_value = ''
+ self.mock.actor_ref = Mock()
+
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_recv_callback_recoverable_error(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ for error in (errno.EWOULDBLOCK, errno.EINTR):
+ self.mock.sock.recv.side_effect = socket.error(error, '')
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.assertEqual(0, self.mock.stop.call_count)
+
+ def test_recv_callback_unrecoverable_error(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.recv.side_effect = socket.error
+
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_send_callback_respects_io_err(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 1
+ self.mock.send_lock = Mock()
+ self.mock.actor_ref = Mock()
+ self.mock.send_buffer = ''
+
+ self.assertTrue(network.Connection.send_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_ERR))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_send_callback_respects_io_hup(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 1
+ self.mock.send_lock = Mock()
+ self.mock.actor_ref = Mock()
+ self.mock.send_buffer = ''
+
+ self.assertTrue(network.Connection.send_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_HUP))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_send_callback_respects_io_hup_and_io_err(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 1
+ self.mock.send_lock = Mock()
+ self.mock.actor_ref = Mock()
+ self.mock.send_buffer = ''
+
+ self.assertTrue(network.Connection.send_callback(self.mock,
+ sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_send_callback_acquires_and_releases_lock(self):
+ self.mock.send_lock = Mock()
+ self.mock.send_lock.acquire.return_value = True
+ self.mock.send_buffer = ''
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 0
+
+ self.assertTrue(network.Connection.send_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.send_lock.acquire.assert_called_once_with(False)
+ self.mock.send_lock.release.assert_called_once_with()
+
+ def test_send_callback_fails_to_acquire_lock(self):
+ self.mock.send_lock = Mock()
+ self.mock.send_lock.acquire.return_value = False
+ self.mock.send_buffer = ''
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 0
+
+ self.assertTrue(network.Connection.send_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.send_lock.acquire.assert_called_once_with(False)
+ self.assertEqual(0, self.mock.sock.send.call_count)
+
+ def test_send_callback_sends_all_data(self):
+ self.mock.send_lock = Mock()
+ self.mock.send_lock.acquire.return_value = True
+ self.mock.send_buffer = 'data'
+ self.mock.send.return_value = ''
+
+ self.assertTrue(network.Connection.send_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.disable_send.assert_called_once_with()
+ self.mock.send.assert_called_once_with('data')
+ self.assertEqual('', self.mock.send_buffer)
+
+ def test_send_callback_sends_partial_data(self):
+ self.mock.send_lock = Mock()
+ self.mock.send_lock.acquire.return_value = True
+ self.mock.send_buffer = 'data'
+ self.mock.send.return_value = 'ta'
+
+ self.assertTrue(network.Connection.send_callback(
+ self.mock, sentinel.fd, gobject.IO_IN))
+ self.mock.send.assert_called_once_with('data')
+ self.assertEqual('ta', self.mock.send_buffer)
+
+ def test_send_recoverable_error(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+
+ for error in (errno.EWOULDBLOCK, errno.EINTR):
+ self.mock.sock.send.side_effect = socket.error(error, '')
+
+ network.Connection.send(self.mock, 'data')
+ self.assertEqual(0, self.mock.stop.call_count)
+
+ def test_send_calls_socket_send(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 4
+
+ self.assertEqual('', network.Connection.send(self.mock, 'data'))
+ self.mock.sock.send.assert_called_once_with('data')
+
+ def test_send_calls_socket_send_partial_send(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.return_value = 2
+
+ self.assertEqual('ta', network.Connection.send(self.mock, 'data'))
+ self.mock.sock.send.assert_called_once_with('data')
+
+ def test_send_unrecoverable_error(self):
+ self.mock.sock = Mock(spec=socket.SocketType)
+ self.mock.sock.send.side_effect = socket.error
+
+ self.assertEqual('', network.Connection.send(self.mock, 'data'))
+ self.mock.stop.assert_called_once_with(any_unicode)
+
+ def test_timeout_callback(self):
+ self.mock.timeout = 10
+
+ self.assertFalse(network.Connection.timeout_callback(self.mock))
+ self.mock.stop.assert_called_once_with(any_unicode)
diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py
new file mode 100644
index 00000000..b323de09
--- /dev/null
+++ b/tests/utils/network/lineprotocol_test.py
@@ -0,0 +1,290 @@
+#encoding: utf-8
+
+import re
+from mock import sentinel, Mock
+
+from mopidy.utils import network
+
+from tests import unittest
+
+
+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)
+ network.LineProtocol.__init__(self.mock, sentinel.connection)
+ 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'
+ delimeter = re.compile('\r?\n')
+
+ network.LineProtocol.__init__(self.mock, sentinel.connection)
+ self.assertEqual(delimeter, self.mock.delimeter)
+
+ def test_on_receive_no_new_lines_adds_to_recv_buffer(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = []
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data'})
+ self.assertEqual('data', self.mock.recv_buffer)
+ self.mock.parse_lines.assert_called_once_with()
+ self.assertEqual(0, self.mock.on_line_received.call_count)
+
+ def test_on_receive_toggles_timeout(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = []
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data'})
+ 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 = ''
+ self.mock.parse_lines.return_value = []
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data'})
+ self.mock.parse_lines.assert_called_once_with()
+ self.assertEqual(0, self.mock.on_line_received.call_count)
+
+ def test_on_receive_with_new_line_calls_decode(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = [sentinel.line]
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data\n'})
+ self.mock.parse_lines.assert_called_once_with()
+ self.mock.decode.assert_called_once_with(sentinel.line)
+
+ def test_on_receive_with_new_line_calls_on_recieve(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = [sentinel.line]
+ self.mock.decode.return_value = sentinel.decoded
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data\n'})
+ self.mock.on_line_received.assert_called_once_with(sentinel.decoded)
+
+ def test_on_receive_with_new_line_with_failed_decode(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = [sentinel.line]
+ self.mock.decode.return_value = None
+
+ network.LineProtocol.on_receive(self.mock, {'received': 'data\n'})
+ self.assertEqual(0, self.mock.on_line_received.call_count)
+
+ def test_on_receive_with_new_lines_calls_on_recieve(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.recv_buffer = ''
+ self.mock.parse_lines.return_value = ['line1', 'line2']
+ self.mock.decode.return_value = sentinel.decoded
+
+ network.LineProtocol.on_receive(self.mock,
+ {'received': 'line1\nline2\n'})
+ self.assertEqual(2, self.mock.on_line_received.call_count)
+
+ def test_parse_lines_emtpy_buffer(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = ''
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertRaises(StopIteration, lines.next)
+
+ def test_parse_lines_no_terminator(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = 'data'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertRaises(StopIteration, lines.next)
+
+ def test_parse_lines_termintor(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = 'data\n'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('data', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('', self.mock.recv_buffer)
+
+ def test_parse_lines_termintor_with_carriage_return(self):
+ self.mock.delimeter = re.compile(r'\r?\n')
+ self.mock.recv_buffer = 'data\r\n'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('data', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('', self.mock.recv_buffer)
+
+ def test_parse_lines_no_data_before_terminator(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = '\n'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('', self.mock.recv_buffer)
+
+ def test_parse_lines_extra_data_after_terminator(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = 'data1\ndata2'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('data1', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('data2', self.mock.recv_buffer)
+
+ def test_parse_lines_unicode(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = u'æøå\n'.encode('utf-8')
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual(u'æøå'.encode('utf-8'), lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('', self.mock.recv_buffer)
+
+ def test_parse_lines_multiple_lines(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = 'abc\ndef\nghi\njkl'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('abc', lines.next())
+ self.assertEqual('def', lines.next())
+ self.assertEqual('ghi', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('jkl', self.mock.recv_buffer)
+
+ def test_parse_lines_multiple_calls(self):
+ self.mock.delimeter = re.compile(r'\n')
+ self.mock.recv_buffer = 'data1'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('data1', self.mock.recv_buffer)
+
+ self.mock.recv_buffer += '\ndata2'
+
+ lines = network.LineProtocol.parse_lines(self.mock)
+ self.assertEqual('data1', lines.next())
+ self.assertRaises(StopIteration, lines.next)
+ self.assertEqual('data2', self.mock.recv_buffer)
+
+ def test_send_lines_called_with_no_lines(self):
+ self.mock.connection = Mock(spec=network.Connection)
+
+ network.LineProtocol.send_lines(self.mock, [])
+ self.assertEqual(0, self.mock.encode.call_count)
+ self.assertEqual(0, self.mock.connection.queue_send.call_count)
+
+ def test_send_lines_calls_join_lines(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.join_lines.return_value = 'lines'
+
+ network.LineProtocol.send_lines(self.mock, sentinel.lines)
+ self.mock.join_lines.assert_called_once_with(sentinel.lines)
+
+ def test_send_line_encodes_joined_lines_with_final_terminator(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.join_lines.return_value = u'lines\n'
+
+ network.LineProtocol.send_lines(self.mock, sentinel.lines)
+ self.mock.encode.assert_called_once_with(u'lines\n')
+
+ def test_send_lines_sends_encoded_string(self):
+ self.mock.connection = Mock(spec=network.Connection)
+ self.mock.join_lines.return_value = 'lines'
+ self.mock.encode.return_value = sentinel.data
+
+ network.LineProtocol.send_lines(self.mock, sentinel.lines)
+ self.mock.connection.queue_send.assert_called_once_with(sentinel.data)
+
+ def test_join_lines_returns_empty_string_for_no_lines(self):
+ self.assertEqual(u'', network.LineProtocol.join_lines(self.mock, []))
+
+ def test_join_lines_returns_joined_lines(self):
+ self.assertEqual(u'1\n2\n', network.LineProtocol.join_lines(
+ self.mock, [u'1', u'2']))
+
+ def test_decode_calls_decode_on_string(self):
+ string = Mock()
+
+ network.LineProtocol.decode(self.mock, string)
+ string.decode.assert_called_once_with(self.mock.encoding)
+
+ def test_decode_plain_ascii(self):
+ result = network.LineProtocol.decode(self.mock, 'abc')
+ self.assertEqual(u'abc', result)
+ self.assertEqual(unicode, type(result))
+
+ def test_decode_utf8(self):
+ result = network.LineProtocol.decode(
+ self.mock, u'æøå'.encode('utf-8'))
+ self.assertEqual(u'æøå', result)
+ self.assertEqual(unicode, type(result))
+
+ def test_decode_invalid_data(self):
+ string = Mock()
+ string.decode.side_effect = UnicodeError
+
+ network.LineProtocol.decode(self.mock, string)
+ self.mock.stop.assert_called_once_with()
+
+ def test_encode_calls_encode_on_string(self):
+ string = Mock()
+
+ network.LineProtocol.encode(self.mock, string)
+ string.encode.assert_called_once_with(self.mock.encoding)
+
+ def test_encode_plain_ascii(self):
+ result = network.LineProtocol.encode(self.mock, u'abc')
+ self.assertEqual('abc', result)
+ self.assertEqual(str, type(result))
+
+ def test_encode_utf8(self):
+ result = network.LineProtocol.encode(self.mock, u'æøå')
+ self.assertEqual(u'æøå'.encode('utf-8'), result)
+ self.assertEqual(str, type(result))
+
+ def test_encode_invalid_data(self):
+ string = Mock()
+ string.encode.side_effect = UnicodeError
+
+ network.LineProtocol.encode(self.mock, string)
+ self.mock.stop.assert_called_once_with()
+
+ def test_host_property(self):
+ mock = Mock(spec=network.Connection)
+ mock.host = sentinel.host
+
+ lineprotocol = network.LineProtocol(mock)
+ self.assertEqual(sentinel.host, lineprotocol.host)
+
+ def test_port_property(self):
+ mock = Mock(spec=network.Connection)
+ mock.port = sentinel.port
+
+ lineprotocol = network.LineProtocol(mock)
+ self.assertEqual(sentinel.port, lineprotocol.port)
diff --git a/tests/utils/network/server_test.py b/tests/utils/network/server_test.py
new file mode 100644
index 00000000..e0399525
--- /dev/null
+++ b/tests/utils/network/server_test.py
@@ -0,0 +1,186 @@
+import errno
+import gobject
+import socket
+from mock import patch, sentinel, Mock
+
+from mopidy.utils import network
+
+from tests import unittest, any_int
+
+
+class ServerTest(unittest.TestCase):
+ def setUp(self):
+ self.mock = Mock(spec=network.Server)
+
+ def test_init_calls_create_server_socket(self):
+ network.Server.__init__(self.mock, sentinel.host,
+ sentinel.port, sentinel.protocol)
+ self.mock.create_server_socket.assert_called_once_with(
+ sentinel.host, sentinel.port)
+
+ def test_init_calls_register_server(self):
+ sock = Mock(spec=socket.SocketType)
+ sock.fileno.return_value = sentinel.fileno
+ self.mock.create_server_socket.return_value = sock
+
+ network.Server.__init__(self.mock, sentinel.host,
+ sentinel.port, sentinel.protocol)
+ self.mock.register_server_socket.assert_called_once_with(
+ sentinel.fileno)
+
+ def test_init_fails_on_fileno_call(self):
+ sock = Mock(spec=socket.SocketType)
+ sock.fileno.side_effect = socket.error
+ self.mock.create_server_socket.return_value = sock
+
+ self.assertRaises(socket.error, network.Server.__init__,
+ self.mock, sentinel.host, sentinel.port, sentinel.protocol)
+
+ def test_init_stores_values_in_attributes(self):
+ # This need to be a mock and no a sentinel as fileno() is called on it
+ sock = Mock(spec=socket.SocketType)
+ self.mock.create_server_socket.return_value = sock
+
+ network.Server.__init__(self.mock, sentinel.host, sentinel.port,
+ sentinel.protocol, max_connections=sentinel.max_connections,
+ timeout=sentinel.timeout)
+ self.assertEqual(sentinel.protocol, self.mock.protocol)
+ self.assertEqual(sentinel.max_connections, self.mock.max_connections)
+ self.assertEqual(sentinel.timeout, self.mock.timeout)
+ self.assertEqual(sock, self.mock.server_socket)
+
+ @patch.object(network, 'create_socket', spec=socket.SocketType)
+ def test_create_server_socket_sets_up_listener(self, create_socket):
+ sock = create_socket.return_value
+
+ network.Server.create_server_socket(self.mock,
+ sentinel.host, sentinel.port)
+ sock.setblocking.assert_called_once_with(False)
+ sock.bind.assert_called_once_with((sentinel.host, sentinel.port))
+ sock.listen.assert_called_once_with(any_int)
+
+ @patch.object(network, 'create_socket', new=Mock())
+ def test_create_server_socket_fails(self):
+ network.create_socket.side_effect = socket.error
+ self.assertRaises(socket.error, network.Server.create_server_socket,
+ self.mock, sentinel.host, sentinel.port)
+
+ @patch.object(network, 'create_socket', new=Mock())
+ def test_create_server_bind_fails(self):
+ sock = network.create_socket.return_value
+ sock.bind.side_effect = socket.error
+
+ self.assertRaises(socket.error, network.Server.create_server_socket,
+ self.mock, sentinel.host, sentinel.port)
+
+ @patch.object(network, 'create_socket', new=Mock())
+ def test_create_server_listen_fails(self):
+ sock = network.create_socket.return_value
+ sock.listen.side_effect = socket.error
+
+ self.assertRaises(socket.error, network.Server.create_server_socket,
+ self.mock, sentinel.host, sentinel.port)
+
+ @patch.object(gobject, 'io_add_watch', new=Mock())
+ def test_register_server_socket_sets_up_io_watch(self):
+ network.Server.register_server_socket(self.mock, sentinel.fileno)
+ gobject.io_add_watch.assert_called_once_with(sentinel.fileno,
+ gobject.IO_IN, self.mock.handle_connection)
+
+ def test_handle_connection(self):
+ self.mock.accept_connection.return_value = (
+ sentinel.sock, sentinel.addr)
+ self.mock.maximum_connections_exceeded.return_value = False
+
+ self.assertTrue(network.Server.handle_connection(
+ self.mock, sentinel.fileno, gobject.IO_IN))
+ self.mock.accept_connection.assert_called_once_with()
+ self.mock.maximum_connections_exceeded.assert_called_once_with()
+ self.mock.init_connection.assert_called_once_with(
+ sentinel.sock, sentinel.addr)
+ self.assertEqual(0, self.mock.reject_connection.call_count)
+
+ def test_handle_connection_exceeded_connections(self):
+ self.mock.accept_connection.return_value = (
+ sentinel.sock, sentinel.addr)
+ self.mock.maximum_connections_exceeded.return_value = True
+
+ self.assertTrue(network.Server.handle_connection(
+ self.mock, sentinel.fileno, gobject.IO_IN))
+ self.mock.accept_connection.assert_called_once_with()
+ self.mock.maximum_connections_exceeded.assert_called_once_with()
+ self.mock.reject_connection.assert_called_once_with(
+ sentinel.sock, sentinel.addr)
+ self.assertEqual(0, self.mock.init_connection.call_count)
+
+ def test_accept_connection(self):
+ sock = Mock(spec=socket.SocketType)
+ sock.accept.return_value = (sentinel.sock, sentinel.addr)
+ self.mock.server_socket = sock
+
+ sock, addr = network.Server.accept_connection(self.mock)
+ self.assertEqual(sentinel.sock, sock)
+ self.assertEqual(sentinel.addr, addr)
+
+ def test_accept_connection_recoverable_error(self):
+ sock = Mock(spec=socket.SocketType)
+ self.mock.server_socket = sock
+
+ for error in (errno.EAGAIN, errno.EINTR):
+ sock.accept.side_effect = socket.error(error, '')
+ self.assertRaises(network.ShouldRetrySocketCall,
+ network.Server.accept_connection, self.mock)
+
+ # FIXME decide if this should be allowed to propegate
+ def test_accept_connection_unrecoverable_error(self):
+ sock = Mock(spec=socket.SocketType)
+ self.mock.server_socket = sock
+ sock.accept.side_effect = socket.error
+ self.assertRaises(socket.error,
+ network.Server.accept_connection, self.mock)
+
+ def test_maximum_connections_exceeded(self):
+ self.mock.max_connections = 10
+
+ self.mock.number_of_connections.return_value = 11
+ self.assertTrue(network.Server.maximum_connections_exceeded(self.mock))
+
+ self.mock.number_of_connections.return_value = 10
+ self.assertTrue(network.Server.maximum_connections_exceeded(self.mock))
+
+ self.mock.number_of_connections.return_value = 9
+ self.assertFalse(network.Server.maximum_connections_exceeded(self.mock))
+
+ @patch('pykka.registry.ActorRegistry.get_by_class')
+ def test_number_of_connections(self, get_by_class):
+ self.mock.protocol = sentinel.protocol
+
+ get_by_class.return_value = [1, 2, 3]
+ self.assertEqual(3, network.Server.number_of_connections(self.mock))
+
+ get_by_class.return_value = []
+ self.assertEqual(0, network.Server.number_of_connections(self.mock))
+
+ @patch.object(network, 'Connection', new=Mock())
+ def test_init_connection(self):
+ self.mock.protocol = sentinel.protocol
+ self.mock.timeout = sentinel.timeout
+
+ network.Server.init_connection(self.mock, sentinel.sock, sentinel.addr)
+ network.Connection.assert_called_once_with(sentinel.protocol,
+ sentinel.sock, sentinel.addr, sentinel.timeout)
+
+ def test_reject_connection(self):
+ sock = Mock(spec=socket.SocketType)
+
+ network.Server.reject_connection(self.mock, sock,
+ (sentinel.host, sentinel.port))
+ sock.close.assert_called_once_with()
+
+ def test_reject_connection_error(self):
+ sock = Mock(spec=socket.SocketType)
+ sock.close.side_effect = socket.error
+
+ network.Server.reject_connection(self.mock, sock,
+ (sentinel.host, sentinel.port))
+ sock.close.assert_called_once_with()
diff --git a/tests/utils/network_test.py b/tests/utils/network/utils_test.py
similarity index 58%
rename from tests/utils/network_test.py
rename to tests/utils/network/utils_test.py
index 66229036..1e11673e 100644
--- a/tests/utils/network_test.py
+++ b/tests/utils/network/utils_test.py
@@ -1,57 +1,57 @@
-import mock
import socket
-import unittest
+from mock import patch, Mock
from mopidy.utils import network
-from tests import SkipTest
+from tests import unittest
+
class FormatHostnameTest(unittest.TestCase):
- @mock.patch('mopidy.utils.network.has_ipv6', True)
+ @patch('mopidy.utils.network.has_ipv6', True)
def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self):
network.has_ipv6 = True
self.assertEqual(network.format_hostname('0.0.0.0'), '::ffff:0.0.0.0')
self.assertEqual(network.format_hostname('1.0.0.1'), '::ffff:1.0.0.1')
- @mock.patch('mopidy.utils.network.has_ipv6', False)
+ @patch('mopidy.utils.network.has_ipv6', False)
def test_format_hostname_does_nothing_when_only_ipv4_available(self):
network.has_ipv6 = False
- self.assertEquals(network.format_hostname('0.0.0.0'), '0.0.0.0')
+ self.assertEqual(network.format_hostname('0.0.0.0'), '0.0.0.0')
class TryIPv6SocketTest(unittest.TestCase):
- @mock.patch('socket.has_ipv6', False)
+ @patch('socket.has_ipv6', False)
def test_system_that_claims_no_ipv6_support(self):
- self.assertFalse(network._try_ipv6_socket())
+ self.assertFalse(network.try_ipv6_socket())
- @mock.patch('socket.has_ipv6', True)
- @mock.patch('socket.socket')
+ @patch('socket.has_ipv6', True)
+ @patch('socket.socket')
def test_system_with_broken_ipv6(self, socket_mock):
socket_mock.side_effect = IOError()
- self.assertFalse(network._try_ipv6_socket())
+ self.assertFalse(network.try_ipv6_socket())
- @mock.patch('socket.has_ipv6', True)
- @mock.patch('socket.socket')
+ @patch('socket.has_ipv6', True)
+ @patch('socket.socket')
def test_with_working_ipv6(self, socket_mock):
- socket_mock.return_value = mock.Mock()
- self.assertTrue(network._try_ipv6_socket())
+ socket_mock.return_value = Mock()
+ self.assertTrue(network.try_ipv6_socket())
class CreateSocketTest(unittest.TestCase):
- @mock.patch('mopidy.utils.network.has_ipv6', False)
- @mock.patch('socket.socket')
+ @patch('mopidy.utils.network.has_ipv6', False)
+ @patch('socket.socket')
def test_ipv4_socket(self, socket_mock):
network.create_socket()
self.assertEqual(socket_mock.call_args[0],
(socket.AF_INET, socket.SOCK_STREAM))
- @mock.patch('mopidy.utils.network.has_ipv6', True)
- @mock.patch('socket.socket')
+ @patch('mopidy.utils.network.has_ipv6', True)
+ @patch('socket.socket')
def test_ipv6_socket(self, socket_mock):
network.create_socket()
self.assertEqual(socket_mock.call_args[0],
(socket.AF_INET6, socket.SOCK_STREAM))
- @SkipTest
+ @unittest.SkipTest
def test_ipv6_only_is_set(self):
pass
diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py
index 088a7049..ba1fcf97 100644
--- a/tests/utils/path_test.py
+++ b/tests/utils/path_test.py
@@ -4,12 +4,12 @@ import os
import shutil
import sys
import tempfile
-import unittest
from mopidy.utils.path import (get_or_create_folder, mtime,
path_to_uri, uri_to_path, split_path, find_files)
-from tests import path_to_data_dir
+from tests import unittest, path_to_data_dir
+
class GetOrCreateFolderTest(unittest.TestCase):
def setUp(self):
diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py
index 973c2280..55e1156b 100644
--- a/tests/utils/settings_test.py
+++ b/tests/utils/settings_test.py
@@ -1,10 +1,12 @@
import os
-import unittest
from mopidy import settings as default_settings_module, SettingsError
from mopidy.utils.settings import (format_settings_list, mask_value_if_secret,
SettingsProxy, validate_settings)
+from tests import unittest
+
+
class ValidateSettingsTest(unittest.TestCase):
def setUp(self):
self.defaults = {
@@ -150,6 +152,14 @@ class SettingsProxyTest(unittest.TestCase):
actual = self.settings.TEST
self.assertEqual(actual, './test')
+ def test_value_ending_in_file_can_be_none(self):
+ self.settings.TEST_FILE = None
+ self.assertEqual(self.settings.TEST_FILE, None)
+
+ def test_value_ending_in_path_can_be_none(self):
+ self.settings.TEST_PATH = None
+ self.assertEqual(self.settings.TEST_PATH, None)
+
def test_interactive_input_of_missing_defaults(self):
self.settings.default['TEST'] = ''
interactive_input = 'input'
diff --git a/tests/version_test.py b/tests/version_test.py
index 7bfb540e..4544349d 100644
--- a/tests/version_test.py
+++ b/tests/version_test.py
@@ -1,8 +1,10 @@
from distutils.version import StrictVersion as SV
-import unittest
import platform
-from mopidy import get_version, get_plain_version, get_platform, get_python
+from mopidy import get_plain_version, get_platform, get_python
+
+from tests import unittest
+
class VersionTest(unittest.TestCase):
def test_current_version_is_parsable_as_a_strict_version_number(self):
@@ -19,8 +21,9 @@ class VersionTest(unittest.TestCase):
self.assert_(SV('0.3.0') < SV('0.3.1'))
self.assert_(SV('0.3.1') < SV('0.4.0'))
self.assert_(SV('0.4.0') < SV('0.4.1'))
- self.assert_(SV('0.4.1') < SV(get_plain_version()))
- self.assert_(SV(get_plain_version()) < SV('0.5.1'))
+ self.assert_(SV('0.4.1') < SV('0.5.0'))
+ self.assert_(SV('0.5.0') < SV(get_plain_version()))
+ self.assert_(SV(get_plain_version()) < SV('0.6.1'))
def test_get_platform_contains_platform(self):
self.assert_(platform.platform() in get_platform())
diff --git a/tools/idle.py b/tools/idle.py
new file mode 100644
index 00000000..aa56dce2
--- /dev/null
+++ b/tools/idle.py
@@ -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()