Merge 'develop' into 'feature/threads-not-processes'

This commit is contained in:
Stein Magnus Jodal 2010-08-23 13:34:33 +02:00
commit 5a9d19b326
32 changed files with 872 additions and 693 deletions

View File

@ -0,0 +1,18 @@
***********************
:mod:`mopidy.frontends`
***********************
A frontend is responsible for exposing Mopidy for a type of clients.
Frontend API
============
A stable frontend API is not available yet, as we've only implemented a single
frontend module.
Frontends
=========
* :mod:`mopidy.frontends.mpd`

View File

@ -40,58 +40,58 @@ methods as described below.
:mod:`mopidy.mixers.alsa` -- ALSA mixer for Linux
=================================================
.. inheritance-diagram:: mopidy.mixers.alsa
.. automodule:: mopidy.mixers.alsa
:synopsis: ALSA mixer for Linux
:members:
.. inheritance-diagram:: mopidy.mixers.alsa.AlsaMixer
:mod:`mopidy.mixers.denon` -- Hardware mixer for Denon amplifiers
=================================================================
.. inheritance-diagram:: mopidy.mixers.denon
.. automodule:: mopidy.mixers.denon
:synopsis: Hardware mixer for Denon amplifiers
:members:
.. inheritance-diagram:: mopidy.mixers.denon
:mod:`mopidy.mixers.dummy` -- Dummy mixer for testing
=====================================================
.. inheritance-diagram:: mopidy.mixers.dummy
.. automodule:: mopidy.mixers.dummy
:synopsis: Dummy mixer for testing
:members:
.. inheritance-diagram:: mopidy.mixers.dummy
:mod:`mopidy.mixers.gstreamer_software` -- Software mixer for all platforms
===========================================================================
.. inheritance-diagram:: mopidy.mixers.gstreamer_software
.. automodule:: mopidy.mixers.gstreamer_software
:synopsis: Software mixer for all platforms
:members:
.. inheritance-diagram:: mopidy.mixers.gstreamer_software
:mod:`mopidy.mixers.osa` -- Osa mixer for OS X
==============================================
.. inheritance-diagram:: mopidy.mixers.osa
.. automodule:: mopidy.mixers.osa
:synopsis: Osa mixer for OS X
:members:
.. inheritance-diagram:: mopidy.mixers.osa
:mod:`mopidy.mixers.nad` -- Hardware mixer for NAD amplifiers
=============================================================
.. inheritance-diagram:: mopidy.mixers.nad
.. automodule:: mopidy.mixers.nad
:synopsis: Hardware mixer for NAD amplifiers
:members:
.. inheritance-diagram:: mopidy.mixers.nad

22
docs/api/outputs.rst Normal file
View File

@ -0,0 +1,22 @@
*********************
:mod:`mopidy.outputs`
*********************
Outputs are responsible for playing audio.
Output API
==========
A stable output API is not available yet, as we've only implemented a single
output module.
:mod:`mopidy.outputs.gstreamer` -- GStreamer output for all platforms
=====================================================================
.. inheritance-diagram:: mopidy.outputs.gstreamer
.. automodule:: mopidy.outputs.gstreamer
:synopsis: GStreamer output for all platforms
:members:

View File

@ -5,17 +5,26 @@ Changes
This change log is used to track all major changes to Mopidy.
0.1.0a4 (in development)
========================
0.1.0 (2010-08-23)
==================
The greatest release ever! We present to you important improvements in search
functionality, working track position seeking, no known stability issues, and
greatly improved MPD client support.
After three weeks of long nights and sprints we're finally pleased enough with
the state of Mopidy to remove the alpha label, and do a regular release.
Mopidy 0.1.0 got important improvements in search functionality, working track
position seeking, no known stability issues, and greatly improved MPD client
support. There are lots of changes since 0.1.0a3, and we urge you to at least
read the *important changes* below.
This release does not support OS X. We're sorry about that, and are working on
fixing the OS X issues for a future release. You can track the progress at
:issue:`14`.
**Important changes**
- License changed from GPLv2 to Apache License, version 2.0.
- GStreamer is now a required dependency.
- GStreamer is now a required dependency. See our :doc:`GStreamer installation
docs <installation/gstreamer>`.
- :mod:`mopidy.backends.libspotify` is now the default backend.
:mod:`mopidy.backends.despotify` is no longer available. This means that you
need to install the :doc:`dependencies for libspotify
@ -72,6 +81,9 @@ greatly improved MPD client support.
- A Spotify application key is now bundled with the source.
:attr:`mopidy.settings.SPOTIFY_LIB_APPKEY` is thus removed.
- If failing to play a track, playback will skip to the next track.
- Both :mod:`mopidy.backends.libspotify` and :mod:`mopidy.backends.local`
have been rewritten to use the new common GStreamer audio output module,
:mod:`mopidy.outputs.gstreamer`.
- Mixers:

View File

@ -20,7 +20,7 @@ the Spotify service, and the speakers.
- Filled red boxes are the key external systems.
- Gray boxes are external dependencies.
- Blue circles lives in the ``main`` process, also known as ``CoreProcess``.
It processing messages on the core queue.
It is processing messages put on the core queue.
- Purple circles lives in a process named ``MpdProcess``, running an
:mod:`asyncore` loop.
- Green circles lives in a process named ``GStreamerProcess``.

View File

@ -2,10 +2,10 @@
Installation
************
To get a basic version of Mopidy running, you need Python and the GStreamer
library. To use Spotify with Mopidy, you also need :doc:`libspotify and
pyspotify <libspotify>`. Mopidy itself can either be installed from the Python
package index, PyPI, or from git.
To get a basic version of Mopidy running, you need Python and the
:doc:`GStreamer library <gstreamer>`. To use Spotify with Mopidy, you also need
:doc:`libspotify and pyspotify <libspotify>`. Mopidy itself can either be
installed from the Python package index, PyPI, or from git.
Install dependencies
@ -31,6 +31,11 @@ Make sure you got the required dependencies installed.
- pyserial (Debian/Ubuntu package: python-serial)
- *Default:* :mod:`mopidy.mixers.gstreamer_software` (Linux, OS X, and
Windows)
- No additional dependencies.
- :mod:`mopidy.mixers.nad` (Linux, OS X, and Windows)
- pyserial (Debian/Ubuntu package: python-serial)
@ -41,7 +46,7 @@ Make sure you got the required dependencies installed.
- Dependencies for at least one Mopidy backend:
- :mod:`mopidy.backends.libspotify` (Linux, OS X, and Windows)
- *Default:* :mod:`mopidy.backends.libspotify` (Linux, OS X, and Windows)
- :doc:`libspotify and pyspotify <libspotify>`
@ -91,20 +96,42 @@ For an introduction to ``git``, please visit `git-scm.com
Settings
========
Create a file named ``settings.py`` in the directory ``~/.mopidy/``.
Mopidy reads settings from the file ``~/.mopidy/settings.py``, where ``~``
means your *home directory*. If your username is ``alice`` and you are running
Linux, the settings file should probably be at
``/home/alice/.mopidy/settings.py``.
If you are using a Spotify backend, enter your Spotify Premium account's
username and password into the file, like this::
You can either create this file yourself, or run the ``mopidy`` command, and it
will create an empty settings file for you.
Music from Spotify
------------------
If you are using the Spotify backend, which is the default, enter your Spotify
Premium account's username and password into the file, like this::
SPOTIFY_USERNAME = u'myusername'
SPOTIFY_PASSWORD = u'mysecret'
Currently :mod:`mopidy.backends.libspotify` is the default backend. If you want
to use :mod:`mopidy.backends.local`, add the following setting::
Music from local storage
------------------------
If you want use Mopidy to play music you have locally at your machine instead
of using Spotify, you need to change the backend from the default to
:mod:`mopidy.backends.local` by adding the following line to your settings
file::
BACKENDS = (u'mopidy.backends.local.LocalBackend',)
For a full list of available settings, see :mod:`mopidy.settings`.
You may also want to change some of the ``LOCAL_*`` settings. See
:mod:`mopidy.settings`, for a full list of available settings.
Connecting from other machines on the network
---------------------------------------------
As a secure default, Mopidy only accepts connections from ``localhost``. If you
want to open it for connections from other machines on your network, see
the documentation for :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`.
Running Mopidy
@ -114,10 +141,9 @@ To start Mopidy, simply open a terminal and run::
mopidy
When Mopidy says ``MPD server running at [localhost]:6600`` it's ready to
accept connections by any MPD client. You can find a list of tons of MPD
clients at http://mpd.wikia.com/wiki/Clients. We use GMPC and
ncmpcpp during development. The first is a GUI client, and the second is a
terminal client.
When Mopidy says ``MPD server running at [127.0.0.1]:6600`` it's ready to
accept connections by any MPD client. You can find tons of MPD clients at
http://mpd.wikia.com/wiki/Clients. We use GMPC and ncmpcpp during development.
The first is a GUI client, and the second is a terminal client.
To stop Mopidy, press ``CTRL+C``.

View File

@ -3,7 +3,7 @@ if not (2, 6) <= sys.version_info < (3,):
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
def get_version():
return u'0.1.0a4'
return u'0.1.0'
class MopidyException(Exception):
def __init__(self, message, *args, **kwargs):

View File

@ -16,6 +16,12 @@ class LibspotifyBackend(BaseBackend):
**Issues:** http://github.com/jodal/mopidy/issues/labels/backend-libspotify
**Settings:**
- :attr:`mopidy.settings.SPOTIFY_LIB_CACHE`
- :attr:`mopidy.settings.SPOTIFY_USERNAME`
- :attr:`mopidy.settings.SPOTIFY_PASSWORD`
.. note::
This product uses SPOTIFY(R) CORE but is not endorsed, certified or

View File

@ -15,6 +15,8 @@ class LibspotifyLibraryController(BaseLibraryController):
def lookup(self, uri):
spotify_track = Link.from_string(uri).as_track()
# TODO Block until metadata_updated callback is called. Before that the
# track will be unloaded, unless it's already in the stored playlists.
return LibspotifyTranslator.to_mopidy_track(spotify_track)
def refresh(self, uri=None):

View File

@ -18,6 +18,12 @@ class LocalBackend(BaseBackend):
A backend for playing music from a local music archive.
**Issues:** http://github.com/jodal/mopidy/issues/labels/backend-local
**Settings:**
- :attr:`mopidy.settings.LOCAL_MUSIC_FOLDER`
- :attr:`mopidy.settings.LOCAL_PLAYLIST_FOLDER`
- :attr:`mopidy.settings.LOCAL_TAG_CACHE`
"""
def __init__(self, *args, **kwargs):

View File

@ -5,7 +5,7 @@ import optparse
from mopidy import get_version, settings
from mopidy.utils import get_class
from mopidy.utils.log import setup_logging
from mopidy.utils.path import get_or_create_folder
from mopidy.utils.path import get_or_create_folder, get_or_create_file
from mopidy.utils.process import BaseProcess, unpickle_connection
from mopidy.utils.settings import list_settings_optparse_callback
@ -55,6 +55,7 @@ class CoreProcess(BaseProcess):
def setup_settings(self):
get_or_create_folder('~/.mopidy/')
get_or_create_file('~/.mopidy/settings.py')
settings.validate()
def setup_output(self, core_queue):

View File

@ -4,6 +4,11 @@ from mopidy.frontends.mpd.thread import MpdThread
class MpdFrontend(object):
"""
The MPD frontend.
**Settings:**
- :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`
- :attr:`mopidy.settings.MPD_SERVER_PORT`
"""
def __init__(self):

View File

@ -4,6 +4,10 @@ class BaseMixer(object):
"""
:param backend: a backend instance
:type mixer: :class:`mopidy.backends.base.BaseBackend`
**Settings:**
- :attr:`mopidy.settings.MIXER_MAX_VOLUME`
"""
def __init__(self, backend, *args, **kwargs):

View File

@ -10,6 +10,10 @@ class AlsaMixer(BaseMixer):
"""
Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control
volume.
**Settings:**
- :attr:`mopidy.settings.MIXER_ALSA_CONTROL`
"""
def __init__(self, *args, **kwargs):

View File

@ -3,8 +3,8 @@ from threading import Lock
from serial import Serial
from mopidy import settings
from mopidy.mixers import BaseMixer
from mopidy.settings import MIXER_EXT_PORT
logger = logging.getLogger(u'mopidy.mixers.denon')
@ -33,7 +33,8 @@ class DenonMixer(BaseMixer):
"""
super(DenonMixer, self).__init__(*args, **kwargs)
device = kwargs.get('device', None)
self._device = device or Serial(port=MIXER_EXT_PORT, timeout=0.2)
self._device = device or Serial(port=settings.MIXER_EXT_PORT,
timeout=0.2)
self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)]
self._volume = 0
self._lock = Lock()

View File

@ -2,9 +2,8 @@ import logging
from serial import Serial
from multiprocessing import Pipe
from mopidy import settings
from mopidy.mixers import BaseMixer
from mopidy.settings import (MIXER_EXT_PORT, MIXER_EXT_SOURCE,
MIXER_EXT_SPEAKERS_A, MIXER_EXT_SPEAKERS_B)
from mopidy.utils.process import BaseProcess
logger = logging.getLogger('mopidy.mixers.nad')
@ -91,8 +90,9 @@ class NadTalker(BaseProcess):
def _open_connection(self):
# Opens serial connection to the device.
# Communication settings: 115200 bps 8N1
logger.info(u'Connecting to serial device "%s"', MIXER_EXT_PORT)
self._device = Serial(port=MIXER_EXT_PORT, baudrate=115200,
logger.info(u'Connecting to serial device "%s"',
settings.MIXER_EXT_PORT)
self._device = Serial(port=settings.MIXER_EXT_PORT, baudrate=115200,
timeout=self.TIMEOUT)
self._get_device_model()
@ -114,20 +114,27 @@ class NadTalker(BaseProcess):
self._command_device('Main.Power', 'On')
def _select_speakers(self):
if MIXER_EXT_SPEAKERS_A is not None:
while self._ask_device('Main.SpeakerA') != MIXER_EXT_SPEAKERS_A:
logger.info(u'Setting speakers A "%s"', MIXER_EXT_SPEAKERS_A)
self._command_device('Main.SpeakerA', MIXER_EXT_SPEAKERS_A)
if MIXER_EXT_SPEAKERS_B is not None:
while self._ask_device('Main.SpeakerB') != MIXER_EXT_SPEAKERS_B:
logger.info(u'Setting speakers B "%s"', MIXER_EXT_SPEAKERS_B)
self._command_device('Main.SpeakerB', MIXER_EXT_SPEAKERS_B)
if settings.MIXER_EXT_SPEAKERS_A is not None:
while (self._ask_device('Main.SpeakerA')
!= settings.MIXER_EXT_SPEAKERS_A):
logger.info(u'Setting speakers A "%s"',
settings.MIXER_EXT_SPEAKERS_A)
self._command_device('Main.SpeakerA',
settings.MIXER_EXT_SPEAKERS_A)
if settings.MIXER_EXT_SPEAKERS_B is not None:
while (self._ask_device('Main.SpeakerB') !=
settings.MIXER_EXT_SPEAKERS_B):
logger.info(u'Setting speakers B "%s"',
settings.MIXER_EXT_SPEAKERS_B)
self._command_device('Main.SpeakerB',
settings.MIXER_EXT_SPEAKERS_B)
def _select_input_source(self):
if MIXER_EXT_SOURCE is not None:
while self._ask_device('Main.Source') != MIXER_EXT_SOURCE:
logger.info(u'Selecting input source "%s"', MIXER_EXT_SOURCE)
self._command_device('Main.Source', MIXER_EXT_SOURCE)
if settings.MIXER_EXT_SOURCE is not None:
while self._ask_device('Main.Source') != settings.MIXER_EXT_SOURCE:
logger.info(u'Selecting input source "%s"',
settings.MIXER_EXT_SOURCE)
self._command_device('Main.Source', settings.MIXER_EXT_SOURCE)
def _unmute(self):
while self._ask_device('Main.Mute') != 'Off':

View File

@ -18,6 +18,10 @@ class GStreamerOutput(object):
Audio output through GStreamer.
Starts :class:`GStreamerMessagesThread` and :class:`GStreamerPlayerThread`.
**Settings:**
- :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK`
"""
def __init__(self, core_queue, output_queue):

View File

@ -8,10 +8,17 @@ logger = logging.getLogger('mopidy.utils.path')
def get_or_create_folder(folder):
folder = os.path.expanduser(folder)
if not os.path.isdir(folder):
logger.info(u'Creating %s', folder)
logger.info(u'Creating dir %s', folder)
os.mkdir(folder, 0755)
return folder
def get_or_create_file(filename):
filename = os.path.expanduser(filename)
if not os.path.isfile(filename):
logger.info(u'Creating file %s', filename)
open(filename, 'w')
return filename
def path_to_uri(*paths):
path = os.path.join(*paths)
#path = os.path.expanduser(path) # FIXME Waiting for test case?

View File

@ -0,0 +1,9 @@
def populate_playlist(func):
def wrapper(self):
for track in self.tracks:
self.backend.current_playlist.add(track)
return func(self)
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper

View File

@ -0,0 +1,256 @@
import multiprocessing
import random
from mopidy import settings
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track
from mopidy.utils import get_class
from tests.backends.base import populate_playlist
class BaseCurrentPlaylistControllerTest(object):
tracks = []
def setUp(self):
self.output_queue = multiprocessing.Queue()
self.core_queue = multiprocessing.Queue()
self.output = get_class(settings.OUTPUT)(
self.core_queue, self.output_queue)
self.backend = self.backend_class(
self.core_queue, self.output_queue, DummyMixer)
self.controller = self.backend.current_playlist
self.playback = self.backend.playback
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
def tearDown(self):
self.backend.destroy()
self.output.destroy()
def test_add(self):
for track in self.tracks:
cp_track = self.controller.add(track)
self.assertEqual(track, self.controller.tracks[-1])
self.assertEqual(cp_track, self.controller.cp_tracks[-1])
self.assertEqual(track, cp_track[1])
def test_add_at_position(self):
for track in self.tracks[:-1]:
cp_track = self.controller.add(track, 0)
self.assertEqual(track, self.controller.tracks[0])
self.assertEqual(cp_track, self.controller.cp_tracks[0])
self.assertEqual(track, cp_track[1])
@populate_playlist
def test_add_at_position_outside_of_playlist(self):
test = lambda: self.controller.add(self.tracks[0], len(self.tracks)+2)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_get_by_cpid(self):
cp_track = self.controller.cp_tracks[1]
self.assertEqual(cp_track, self.controller.get(cpid=cp_track[0]))
@populate_playlist
def test_get_by_uri(self):
cp_track = self.controller.cp_tracks[1]
self.assertEqual(cp_track, self.controller.get(uri=cp_track[1].uri))
@populate_playlist
def test_get_by_uri_raises_error_for_invalid_uri(self):
test = lambda: self.controller.get(uri='foobar')
self.assertRaises(LookupError, test)
@populate_playlist
def test_clear(self):
self.controller.clear()
self.assertEqual(len(self.controller.tracks), 0)
def test_clear_empty_playlist(self):
self.controller.clear()
self.assertEqual(len(self.controller.tracks), 0)
@populate_playlist
def test_clear_when_playing(self):
self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING)
self.controller.clear()
self.assertEqual(self.playback.state, self.playback.STOPPED)
def test_get_by_uri_returns_unique_match(self):
track = Track(uri='a')
self.controller.append([Track(uri='z'), track, Track(uri='y')])
self.assertEqual(track, self.controller.get(uri='a')[1])
def test_get_by_uri_raises_error_if_multiple_matches(self):
track = Track(uri='a')
self.controller.append([Track(uri='z'), track, track])
try:
self.controller.get(uri='a')
self.fail(u'Should raise LookupError if multiple matches')
except LookupError as e:
self.assertEqual(u'"uri=a" match multiple tracks', e[0])
def test_get_by_uri_raises_error_if_no_match(self):
self.controller.playlist = Playlist(
tracks=[Track(uri='z'), Track(uri='y')])
try:
self.controller.get(uri='a')
self.fail(u'Should raise LookupError if no match')
except LookupError as e:
self.assertEqual(u'"uri=a" match no tracks', e[0])
def test_get_by_multiple_criteria_returns_elements_matching_all(self):
track1 = Track(uri='a', name='x')
track2 = Track(uri='b', name='x')
track3 = Track(uri='b', name='y')
self.controller.append([track1, track2, track3])
self.assertEqual(track1, self.controller.get(uri='a', name='x')[1])
self.assertEqual(track2, self.controller.get(uri='b', name='x')[1])
self.assertEqual(track3, self.controller.get(uri='b', name='y')[1])
def test_get_by_criteria_that_is_not_present_in_all_elements(self):
track1 = Track()
track2 = Track(uri='b')
track3 = Track()
self.controller.append([track1, track2, track3])
self.assertEqual(track2, self.controller.get(uri='b')[1])
def test_append_appends_to_the_current_playlist(self):
self.controller.append([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.controller.tracks), 2)
self.controller.append([Track(uri='c'), Track(uri='d')])
self.assertEqual(len(self.controller.tracks), 4)
self.assertEqual(self.controller.tracks[0].uri, 'a')
self.assertEqual(self.controller.tracks[1].uri, 'b')
self.assertEqual(self.controller.tracks[2].uri, 'c')
self.assertEqual(self.controller.tracks[3].uri, 'd')
def test_append_does_not_reset_version(self):
version = self.controller.version
self.controller.append([])
self.assertEqual(self.controller.version, version + 1)
@populate_playlist
def test_append_preserves_playing_state(self):
self.playback.play()
track = self.playback.current_track
self.controller.append(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, self.playback.PLAYING)
self.assertEqual(self.playback.current_track, track)
@populate_playlist
def test_append_preserves_stopped_state(self):
self.controller.append(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
@populate_playlist
def test_move_single(self):
self.controller.move(0, 0, 2)
tracks = self.controller.tracks
self.assertEqual(tracks[2], self.tracks[0])
@populate_playlist
def test_move_group(self):
self.controller.move(0, 2, 1)
tracks = self.controller.tracks
self.assertEqual(tracks[1], self.tracks[0])
self.assertEqual(tracks[2], self.tracks[1])
@populate_playlist
def test_moving_track_outside_of_playlist(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(0, 0, tracks+5)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_move_group_outside_of_playlist(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(0, 2, tracks+5)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_move_group_out_of_range(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(tracks+2, tracks+3, 0)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_move_group_invalid_group(self):
test = lambda: self.controller.move(2, 1, 0)
self.assertRaises(AssertionError, test)
def test_tracks_attribute_is_immutable(self):
tracks1 = self.controller.tracks
tracks2 = self.controller.tracks
self.assertNotEqual(id(tracks1), id(tracks2))
@populate_playlist
def test_remove(self):
track1 = self.controller.tracks[1]
track2 = self.controller.tracks[2]
version = self.controller.version
self.controller.remove(uri=track1.uri)
self.assert_(version < self.controller.version)
self.assert_(track1 not in self.controller.tracks)
self.assertEqual(track2, self.controller.tracks[1])
@populate_playlist
def test_removing_track_that_does_not_exist(self):
test = lambda: self.controller.remove(uri='/nonexistant')
self.assertRaises(LookupError, test)
def test_removing_from_empty_playlist(self):
test = lambda: self.controller.remove(uri='/nonexistant')
self.assertRaises(LookupError, test)
@populate_playlist
def test_shuffle(self):
random.seed(1)
self.controller.shuffle()
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(set(self.tracks), set(shuffled_tracks))
@populate_playlist
def test_shuffle_subset(self):
random.seed(1)
self.controller.shuffle(1, 3)
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
@populate_playlist
def test_shuffle_invalid_subset(self):
test = lambda: self.controller.shuffle(3, 1)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_shuffle_superset(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.shuffle(1, tracks+5)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_shuffle_open_subset(self):
random.seed(1)
self.controller.shuffle(1)
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
def test_version(self):
version = self.controller.version
self.controller.append([])
self.assert_(version < self.controller.version)

View File

@ -0,0 +1,158 @@
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track, Album, Artist
from tests import SkipTest, data_folder
class BaseLibraryControllerTest(object):
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
albums = [Album(name='album1', artists=artists[:1]),
Album(name='album2', artists=artists[1:2]),
Album()]
tracks = [Track(name='track1', length=4000, artists=artists[:1],
album=albums[0], uri='file://' + data_folder('uri1')),
Track(name='track2', length=4000, artists=artists[1:2],
album=albums[1], uri='file://' + data_folder('uri2')),
Track()]
def setUp(self):
self.backend = self.backend_class(mixer_class=DummyMixer)
self.library = self.backend.library
def tearDown(self):
self.backend.destroy()
def test_refresh(self):
self.library.refresh()
def test_refresh_uri(self):
raise SkipTest
def test_refresh_missing_uri(self):
raise SkipTest
def test_lookup(self):
track = self.library.lookup(self.tracks[0].uri)
self.assertEqual(track, self.tracks[0])
def test_lookup_unknown_track(self):
test = lambda: self.library.lookup('fake uri')
self.assertRaises(LookupError, test)
def test_find_exact_no_hits(self):
result = self.library.find_exact(track=['unknown track'])
self.assertEqual(result, Playlist())
result = self.library.find_exact(artist=['unknown artist'])
self.assertEqual(result, Playlist())
result = self.library.find_exact(album=['unknown artist'])
self.assertEqual(result, Playlist())
def test_find_exact_artist(self):
result = self.library.find_exact(artist=['artist1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.find_exact(artist=['artist2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_find_exact_track(self):
result = self.library.find_exact(track=['track1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.find_exact(track=['track2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_find_exact_album(self):
result = self.library.find_exact(album=['album1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.find_exact(album=['album2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_find_exact_wrong_type(self):
test = lambda: self.library.find_exact(wrong=['test'])
self.assertRaises(LookupError, test)
def test_find_exact_with_empty_query(self):
test = lambda: self.library.find_exact(artist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(track=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(album=[''])
self.assertRaises(LookupError, test)
def test_search_no_hits(self):
result = self.library.search(track=['unknown track'])
self.assertEqual(result, Playlist())
result = self.library.search(artist=['unknown artist'])
self.assertEqual(result, Playlist())
result = self.library.search(album=['unknown artist'])
self.assertEqual(result, Playlist())
result = self.library.search(uri=['unknown'])
self.assertEqual(result, Playlist())
result = self.library.search(any=['unknown'])
self.assertEqual(result, Playlist())
def test_search_artist(self):
result = self.library.search(artist=['Tist1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(artist=['Tist2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_search_track(self):
result = self.library.search(track=['Rack1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(track=['Rack2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_search_album(self):
result = self.library.search(album=['Bum1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(album=['Bum2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_search_uri(self):
result = self.library.search(uri=['RI1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(uri=['RI2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_search_any(self):
result = self.library.search(any=['Tist1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(any=['Rack1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(any=['Bum1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(any=['RI1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
def test_search_wrong_type(self):
test = lambda: self.library.search(wrong=['test'])
self.assertRaises(LookupError, test)
def test_search_with_empty_query(self):
test = lambda: self.library.search(artist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(track=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(album=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(uri=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(any=[''])
self.assertRaises(LookupError, test)

View File

@ -1,289 +1,25 @@
import multiprocessing
import os
import random
import shutil
import tempfile
import time
from mopidy import settings
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track, Album, Artist
from mopidy.models import Track
from mopidy.utils import get_class
from tests import SkipTest, data_folder
__all__ = ['BaseCurrentPlaylistControllerTest',
'BasePlaybackControllerTest',
'BaseStoredPlaylistsControllerTest',
'BaseLibraryControllerTest']
def populate_playlist(func):
def wrapper(self):
for track in self.tracks:
self.backend.current_playlist.add(track)
return func(self)
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
class BaseCurrentPlaylistControllerTest(object):
tracks = []
backend_class = None
def setUp(self):
self.output_queue = multiprocessing.Queue()
self.core_queue = multiprocessing.Queue()
self.output = get_class(settings.OUTPUT)(self.core_queue, self.output_queue)
self.backend = self.backend_class(self.core_queue, self.output_queue, DummyMixer)
self.controller = self.backend.current_playlist
self.playback = self.backend.playback
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
def tearDown(self):
self.backend.destroy()
self.output.destroy()
def test_add(self):
for track in self.tracks:
cp_track = self.controller.add(track)
self.assertEqual(track, self.controller.tracks[-1])
self.assertEqual(cp_track, self.controller.cp_tracks[-1])
self.assertEqual(track, cp_track[1])
def test_add_at_position(self):
for track in self.tracks[:-1]:
cp_track = self.controller.add(track, 0)
self.assertEqual(track, self.controller.tracks[0])
self.assertEqual(cp_track, self.controller.cp_tracks[0])
self.assertEqual(track, cp_track[1])
@populate_playlist
def test_add_at_position_outside_of_playlist(self):
test = lambda: self.controller.add(self.tracks[0], len(self.tracks)+2)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_get_by_cpid(self):
cp_track = self.controller.cp_tracks[1]
self.assertEqual(cp_track, self.controller.get(cpid=cp_track[0]))
@populate_playlist
def test_get_by_uri(self):
cp_track = self.controller.cp_tracks[1]
self.assertEqual(cp_track, self.controller.get(uri=cp_track[1].uri))
@populate_playlist
def test_get_by_uri_raises_error_for_invalid_uri(self):
test = lambda: self.controller.get(uri='foobar')
self.assertRaises(LookupError, test)
@populate_playlist
def test_clear(self):
self.controller.clear()
self.assertEqual(len(self.controller.tracks), 0)
def test_clear_empty_playlist(self):
self.controller.clear()
self.assertEqual(len(self.controller.tracks), 0)
@populate_playlist
def test_clear_when_playing(self):
self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING)
self.controller.clear()
self.assertEqual(self.playback.state, self.playback.STOPPED)
def test_get_by_uri_returns_unique_match(self):
track = Track(uri='a')
self.controller.append([Track(uri='z'), track, Track(uri='y')])
self.assertEqual(track, self.controller.get(uri='a')[1])
def test_get_by_uri_raises_error_if_multiple_matches(self):
track = Track(uri='a')
self.controller.append([Track(uri='z'), track, track])
try:
self.controller.get(uri='a')
self.fail(u'Should raise LookupError if multiple matches')
except LookupError as e:
self.assertEqual(u'"uri=a" match multiple tracks', e[0])
def test_get_by_uri_raises_error_if_no_match(self):
self.controller.playlist = Playlist(
tracks=[Track(uri='z'), Track(uri='y')])
try:
self.controller.get(uri='a')
self.fail(u'Should raise LookupError if no match')
except LookupError as e:
self.assertEqual(u'"uri=a" match no tracks', e[0])
def test_get_by_multiple_criteria_returns_elements_matching_all(self):
track1 = Track(uri='a', name='x')
track2 = Track(uri='b', name='x')
track3 = Track(uri='b', name='y')
self.controller.append([track1, track2, track3])
self.assertEqual(track1, self.controller.get(uri='a', name='x')[1])
self.assertEqual(track2, self.controller.get(uri='b', name='x')[1])
self.assertEqual(track3, self.controller.get(uri='b', name='y')[1])
def test_get_by_criteria_that_is_not_present_in_all_elements(self):
track1 = Track()
track2 = Track(uri='b')
track3 = Track()
self.controller.append([track1, track2, track3])
self.assertEqual(track2, self.controller.get(uri='b')[1])
def test_append_appends_to_the_current_playlist(self):
self.controller.append([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.controller.tracks), 2)
self.controller.append([Track(uri='c'), Track(uri='d')])
self.assertEqual(len(self.controller.tracks), 4)
self.assertEqual(self.controller.tracks[0].uri, 'a')
self.assertEqual(self.controller.tracks[1].uri, 'b')
self.assertEqual(self.controller.tracks[2].uri, 'c')
self.assertEqual(self.controller.tracks[3].uri, 'd')
def test_append_does_not_reset_version(self):
version = self.controller.version
self.controller.append([])
self.assertEqual(self.controller.version, version + 1)
@populate_playlist
def test_append_preserves_playing_state(self):
self.playback.play()
track = self.playback.current_track
self.controller.append(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, self.playback.PLAYING)
self.assertEqual(self.playback.current_track, track)
@populate_playlist
def test_append_preserves_stopped_state(self):
self.controller.append(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
@populate_playlist
def test_move_single(self):
self.controller.move(0, 0, 2)
tracks = self.controller.tracks
self.assertEqual(tracks[2], self.tracks[0])
@populate_playlist
def test_move_group(self):
self.controller.move(0, 2, 1)
tracks = self.controller.tracks
self.assertEqual(tracks[1], self.tracks[0])
self.assertEqual(tracks[2], self.tracks[1])
@populate_playlist
def test_moving_track_outside_of_playlist(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(0, 0, tracks+5)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_move_group_outside_of_playlist(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(0, 2, tracks+5)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_move_group_out_of_range(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(tracks+2, tracks+3, 0)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_move_group_invalid_group(self):
test = lambda: self.controller.move(2, 1, 0)
self.assertRaises(AssertionError, test)
def test_tracks_attribute_is_immutable(self):
tracks1 = self.controller.tracks
tracks2 = self.controller.tracks
self.assertNotEqual(id(tracks1), id(tracks2))
@populate_playlist
def test_remove(self):
track1 = self.controller.tracks[1]
track2 = self.controller.tracks[2]
version = self.controller.version
self.controller.remove(uri=track1.uri)
self.assert_(version < self.controller.version)
self.assert_(track1 not in self.controller.tracks)
self.assertEqual(track2, self.controller.tracks[1])
@populate_playlist
def test_removing_track_that_does_not_exist(self):
test = lambda: self.controller.remove(uri='/nonexistant')
self.assertRaises(LookupError, test)
def test_removing_from_empty_playlist(self):
test = lambda: self.controller.remove(uri='/nonexistant')
self.assertRaises(LookupError, test)
@populate_playlist
def test_shuffle(self):
random.seed(1)
self.controller.shuffle()
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(set(self.tracks), set(shuffled_tracks))
@populate_playlist
def test_shuffle_subset(self):
random.seed(1)
self.controller.shuffle(1, 3)
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
@populate_playlist
def test_shuffle_invalid_subset(self):
test = lambda: self.controller.shuffle(3, 1)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_shuffle_superset(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.shuffle(1, tracks+5)
self.assertRaises(AssertionError, test)
@populate_playlist
def test_shuffle_open_subset(self):
random.seed(1)
self.controller.shuffle(1)
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
def test_version(self):
version = self.controller.version
self.controller.append([])
self.assert_(version < self.controller.version)
from tests import SkipTest
from tests.backends.base import populate_playlist
class BasePlaybackControllerTest(object):
tracks = []
backend_class = None
def setUp(self):
self.output_queue = multiprocessing.Queue()
self.core_queue = multiprocessing.Queue()
self.output = get_class(settings.OUTPUT)(self.core_queue, self.output_queue)
self.backend = self.backend_class(self.core_queue, self.output_queue, DummyMixer)
self.output = get_class(settings.OUTPUT)(
self.core_queue, self.output_queue)
self.backend = self.backend_class(
self.core_queue, self.output_queue, DummyMixer)
self.playback = self.backend.playback
self.current_playlist = self.backend.current_playlist
@ -1123,265 +859,3 @@ class BasePlaybackControllerTest(object):
def test_playing_track_that_isnt_in_playlist(self):
test = lambda: self.playback.play((17, Track()))
self.assertRaises(AssertionError, test)
class BaseStoredPlaylistsControllerTest(object):
backend_class = None
def setUp(self):
self.original_playlist_folder = settings.LOCAL_PLAYLIST_FOLDER
self.original_tag_cache = settings.LOCAL_TAG_CACHE
self.original_music_folder = settings.LOCAL_MUSIC_FOLDER
settings.LOCAL_PLAYLIST_FOLDER = tempfile.mkdtemp()
settings.LOCAL_TAG_CACHE = data_folder('library_tag_cache')
settings.LOCAL_MUSIC_FOLDER = data_folder('')
self.backend = self.backend_class(mixer_class=DummyMixer)
self.stored = self.backend.stored_playlists
def tearDown(self):
self.backend.destroy()
if os.path.exists(settings.LOCAL_PLAYLIST_FOLDER):
shutil.rmtree(settings.LOCAL_PLAYLIST_FOLDER)
settings.LOCAL_PLAYLIST_FOLDER = self.original_playlist_folder
settings.LOCAL_TAG_CACHE = self.original_tag_cache
settings.LOCAL_MUSIC_FOLDER = self.original_music_folder
def test_create(self):
playlist = self.stored.create('test')
self.assertEqual(playlist.name, 'test')
def test_create_in_playlists(self):
playlist = self.stored.create('test')
self.assert_(self.stored.playlists)
self.assert_(playlist in self.stored.playlists)
def test_playlists_empty_to_start_with(self):
self.assert_(not self.stored.playlists)
def test_delete_non_existant_playlist(self):
self.stored.delete(Playlist())
def test_delete_playlist(self):
playlist = self.stored.create('test')
self.stored.delete(playlist)
self.assert_(not self.stored.playlists)
def test_get_without_criteria(self):
test = self.stored.get
self.assertRaises(LookupError, test)
def test_get_with_wrong_cirteria(self):
test = lambda: self.stored.get(name='foo')
self.assertRaises(LookupError, test)
def test_get_with_right_criteria(self):
playlist1 = self.stored.create('test')
playlist2 = self.stored.get(name='test')
self.assertEqual(playlist1, playlist2)
def test_get_by_name_returns_unique_match(self):
playlist = Playlist(name='b')
self.stored.playlists = [Playlist(name='a'), playlist]
self.assertEqual(playlist, self.stored.get(name='b'))
def test_get_by_name_returns_first_of_multiple_matches(self):
playlist = Playlist(name='b')
self.stored.playlists = [
playlist, Playlist(name='a'), Playlist(name='b')]
try:
self.stored.get(name='b')
self.fail(u'Should raise LookupError if multiple matches')
except LookupError as e:
self.assertEqual(u'"name=b" match multiple playlists', e[0])
def test_get_by_name_raises_keyerror_if_no_match(self):
self.stored.playlists = [Playlist(name='a'), Playlist(name='b')]
try:
self.stored.get(name='c')
self.fail(u'Should raise LookupError if no match')
except LookupError as e:
self.assertEqual(u'"name=c" match no playlists', e[0])
def test_lookup(self):
raise SkipTest
def test_refresh(self):
raise SkipTest
def test_rename(self):
playlist = self.stored.create('test')
self.stored.rename(playlist, 'test2')
self.stored.get(name='test2')
def test_rename_unknown_playlist(self):
self.stored.rename(Playlist(), 'test2')
test = lambda: self.stored.get(name='test2')
self.assertRaises(LookupError, test)
def test_save(self):
# FIXME should we handle playlists without names?
playlist = Playlist(name='test')
self.stored.save(playlist)
self.assert_(playlist in self.stored.playlists)
def test_playlist_with_unknown_track(self):
raise SkipTest
class BaseLibraryControllerTest(object):
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
albums = [Album(name='album1', artists=artists[:1]),
Album(name='album2', artists=artists[1:2]),
Album()]
tracks = [Track(name='track1', length=4000, artists=artists[:1],
album=albums[0], uri='file://' + data_folder('uri1')),
Track(name='track2', length=4000, artists=artists[1:2],
album=albums[1], uri='file://' + data_folder('uri2')),
Track()]
def setUp(self):
self.backend = self.backend_class(mixer_class=DummyMixer)
self.library = self.backend.library
def tearDown(self):
self.backend.destroy()
def test_refresh(self):
self.library.refresh()
def test_refresh_uri(self):
raise SkipTest
def test_refresh_missing_uri(self):
raise SkipTest
def test_lookup(self):
track = self.library.lookup(self.tracks[0].uri)
self.assertEqual(track, self.tracks[0])
def test_lookup_unknown_track(self):
test = lambda: self.library.lookup('fake uri')
self.assertRaises(LookupError, test)
def test_find_exact_no_hits(self):
result = self.library.find_exact(track=['unknown track'])
self.assertEqual(result, Playlist())
result = self.library.find_exact(artist=['unknown artist'])
self.assertEqual(result, Playlist())
result = self.library.find_exact(album=['unknown artist'])
self.assertEqual(result, Playlist())
def test_find_exact_artist(self):
result = self.library.find_exact(artist=['artist1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.find_exact(artist=['artist2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_find_exact_track(self):
result = self.library.find_exact(track=['track1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.find_exact(track=['track2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_find_exact_album(self):
result = self.library.find_exact(album=['album1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.find_exact(album=['album2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_find_exact_wrong_type(self):
test = lambda: self.library.find_exact(wrong=['test'])
self.assertRaises(LookupError, test)
def test_find_exact_with_empty_query(self):
test = lambda: self.library.find_exact(artist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(track=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(album=[''])
self.assertRaises(LookupError, test)
def test_search_no_hits(self):
result = self.library.search(track=['unknown track'])
self.assertEqual(result, Playlist())
result = self.library.search(artist=['unknown artist'])
self.assertEqual(result, Playlist())
result = self.library.search(album=['unknown artist'])
self.assertEqual(result, Playlist())
result = self.library.search(uri=['unknown'])
self.assertEqual(result, Playlist())
result = self.library.search(any=['unknown'])
self.assertEqual(result, Playlist())
def test_search_artist(self):
result = self.library.search(artist=['Tist1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(artist=['Tist2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_search_track(self):
result = self.library.search(track=['Rack1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(track=['Rack2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_search_album(self):
result = self.library.search(album=['Bum1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(album=['Bum2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_search_uri(self):
result = self.library.search(uri=['RI1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(uri=['RI2'])
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
def test_search_any(self):
result = self.library.search(any=['Tist1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(any=['Rack1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(any=['Bum1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
result = self.library.search(any=['RI1'])
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
def test_search_wrong_type(self):
test = lambda: self.library.search(wrong=['test'])
self.assertRaises(LookupError, test)
def test_search_with_empty_query(self):
test = lambda: self.library.search(artist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(track=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(album=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(uri=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(any=[''])
self.assertRaises(LookupError, test)

View File

@ -0,0 +1,113 @@
import os
import shutil
import tempfile
from mopidy import settings
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist
from tests import SkipTest, data_folder
class BaseStoredPlaylistsControllerTest(object):
def setUp(self):
self.original_playlist_folder = settings.LOCAL_PLAYLIST_FOLDER
self.original_tag_cache = settings.LOCAL_TAG_CACHE
self.original_music_folder = settings.LOCAL_MUSIC_FOLDER
settings.LOCAL_PLAYLIST_FOLDER = tempfile.mkdtemp()
settings.LOCAL_TAG_CACHE = data_folder('library_tag_cache')
settings.LOCAL_MUSIC_FOLDER = data_folder('')
self.backend = self.backend_class(mixer_class=DummyMixer)
self.stored = self.backend.stored_playlists
def tearDown(self):
self.backend.destroy()
if os.path.exists(settings.LOCAL_PLAYLIST_FOLDER):
shutil.rmtree(settings.LOCAL_PLAYLIST_FOLDER)
settings.LOCAL_PLAYLIST_FOLDER = self.original_playlist_folder
settings.LOCAL_TAG_CACHE = self.original_tag_cache
settings.LOCAL_MUSIC_FOLDER = self.original_music_folder
def test_create(self):
playlist = self.stored.create('test')
self.assertEqual(playlist.name, 'test')
def test_create_in_playlists(self):
playlist = self.stored.create('test')
self.assert_(self.stored.playlists)
self.assert_(playlist in self.stored.playlists)
def test_playlists_empty_to_start_with(self):
self.assert_(not self.stored.playlists)
def test_delete_non_existant_playlist(self):
self.stored.delete(Playlist())
def test_delete_playlist(self):
playlist = self.stored.create('test')
self.stored.delete(playlist)
self.assert_(not self.stored.playlists)
def test_get_without_criteria(self):
test = self.stored.get
self.assertRaises(LookupError, test)
def test_get_with_wrong_cirteria(self):
test = lambda: self.stored.get(name='foo')
self.assertRaises(LookupError, test)
def test_get_with_right_criteria(self):
playlist1 = self.stored.create('test')
playlist2 = self.stored.get(name='test')
self.assertEqual(playlist1, playlist2)
def test_get_by_name_returns_unique_match(self):
playlist = Playlist(name='b')
self.stored.playlists = [Playlist(name='a'), playlist]
self.assertEqual(playlist, self.stored.get(name='b'))
def test_get_by_name_returns_first_of_multiple_matches(self):
playlist = Playlist(name='b')
self.stored.playlists = [
playlist, Playlist(name='a'), Playlist(name='b')]
try:
self.stored.get(name='b')
self.fail(u'Should raise LookupError if multiple matches')
except LookupError as e:
self.assertEqual(u'"name=b" match multiple playlists', e[0])
def test_get_by_name_raises_keyerror_if_no_match(self):
self.stored.playlists = [Playlist(name='a'), Playlist(name='b')]
try:
self.stored.get(name='c')
self.fail(u'Should raise LookupError if no match')
except LookupError as e:
self.assertEqual(u'"name=c" match no playlists', e[0])
def test_lookup(self):
raise SkipTest
def test_refresh(self):
raise SkipTest
def test_rename(self):
playlist = self.stored.create('test')
self.stored.rename(playlist, 'test2')
self.stored.get(name='test2')
def test_rename_unknown_playlist(self):
self.stored.rename(Playlist(), 'test2')
test = lambda: self.stored.get(name='test2')
self.assertRaises(LookupError, test)
def test_save(self):
# FIXME should we handle playlists without names?
playlist = Playlist(name='test')
self.stored.save(playlist)
self.assert_(playlist in self.stored.playlists)
def test_playlist_with_unknown_track(self):
raise SkipTest

View File

View File

@ -5,7 +5,12 @@ import unittest
from mopidy.backends.libspotify import LibspotifyBackend
from mopidy.models import Track
from tests.backends.base import *
from tests.backends.base.current_playlist import \
BaseCurrentPlaylistControllerTest
from tests.backends.base.library import BaseLibraryControllerTest
from tests.backends.base.playback import BasePlaybackControllerTest
from tests.backends.base.stored_playlists import \
BaseStoredPlaylistsControllerTest
uris = [
'spotify:track:6vqcpVcbI3Zu6sH3ieLDNt',
@ -15,28 +20,25 @@ uris = [
class LibspotifyCurrentPlaylistControllerTest(
BaseCurrentPlaylistControllerTest, unittest.TestCase):
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
backend_class = LibspotifyBackend
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
class LibspotifyPlaybackControllerTest(
BasePlaybackControllerTest, unittest.TestCase):
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
backend_class = LibspotifyBackend
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
class LibspotifyStoredPlaylistsControllerTest(
BaseStoredPlaylistsControllerTest, unittest.TestCase):
backend_class = LibspotifyBackend
class LibspotifyLibraryControllerTest(
BaseLibraryControllerTest, unittest.TestCase):
backend_class = LibspotifyBackend
# TODO Plug this into the backend under test to avoid music output during
# testing.
class DummyAudioController(object):
def music_delivery(self, *args, **kwargs):
pass

View File

@ -0,0 +1,6 @@
from mopidy.utils.path import path_to_uri
from tests import data_folder
song = data_folder('song%s.wav')
generate_song = lambda i: path_to_uri(song % i)

View File

@ -0,0 +1,31 @@
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.backends.base.current_playlist import \
BaseCurrentPlaylistControllerTest
from tests.backends.local import generate_song
class LocalCurrentPlaylistControllerTest(BaseCurrentPlaylistControllerTest,
unittest.TestCase):
backend_class = LocalBackend
tracks = [Track(uri=generate_song(i), length=4464)
for i in range(1, 4)]
def setUp(self):
self.original_backends = settings.BACKENDS
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
super(LocalCurrentPlaylistControllerTest, self).setUp()
def tearDown(self):
super(LocalCurrentPlaylistControllerTest, self).tearDown()
settings.BACKENDS = settings.original_backends

View File

@ -0,0 +1,32 @@
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 data_folder
from tests.backends.base.library import BaseLibraryControllerTest
class LocalLibraryControllerTest(BaseLibraryControllerTest, unittest.TestCase):
backend_class = LocalBackend
def setUp(self):
self.original_tag_cache = settings.LOCAL_TAG_CACHE
self.original_music_folder = settings.LOCAL_MUSIC_FOLDER
settings.LOCAL_TAG_CACHE = data_folder('library_tag_cache')
settings.LOCAL_MUSIC_FOLDER = data_folder('')
super(LocalLibraryControllerTest, self).setUp()
def tearDown(self):
settings.LOCAL_TAG_CACHE = self.original_tag_cache
settings.LOCAL_MUSIC_FOLDER = self.original_music_folder
super(LocalLibraryControllerTest, self).tearDown()

View File

@ -0,0 +1,58 @@
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 data_folder
from tests.backends.base.playback import BasePlaybackControllerTest
from tests.backends.local import generate_song
class LocalPlaybackControllerTest(BasePlaybackControllerTest,
unittest.TestCase):
backend_class = LocalBackend
tracks = [Track(uri=generate_song(i), length=4464)
for i in range(1, 4)]
def setUp(self):
self.original_backends = settings.BACKENDS
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
super(LocalPlaybackControllerTest, self).setUp()
# Two tests does not work at all when using the fake sink
#self.backend.playback.use_fake_sink()
def tearDown(self):
super(LocalPlaybackControllerTest, self).tearDown()
settings.BACKENDS = settings.original_backends
def add_track(self, path):
uri = path_to_uri(data_folder(path))
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_play_mp3(self):
self.add_track('blank.mp3')
self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING)
def test_play_ogg(self):
self.add_track('blank.ogg')
self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING)
def test_play_flac(self):
self.add_track('blank.flac')
self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING)

View File

@ -1,10 +1,11 @@
import unittest
import os
from tests import SkipTest
# 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
@ -13,71 +14,10 @@ from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track
from mopidy.utils.path import path_to_uri
from tests.backends.base import *
from tests import SkipTest, data_folder
song = data_folder('song%s.wav')
generate_song = lambda i: path_to_uri(song % i)
# FIXME can be switched to generic test
class LocalCurrentPlaylistControllerTest(BaseCurrentPlaylistControllerTest,
unittest.TestCase):
tracks = [Track(uri=generate_song(i), length=4464)
for i in range(1, 4)]
backend_class = LocalBackend
def setUp(self):
self.original_backends = settings.BACKENDS
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
super(LocalCurrentPlaylistControllerTest, self).setUp()
def tearDown(self):
super(LocalCurrentPlaylistControllerTest, self).tearDown()
settings.BACKENDS = settings.original_backends
class LocalPlaybackControllerTest(BasePlaybackControllerTest,
unittest.TestCase):
tracks = [Track(uri=generate_song(i), length=4464)
for i in range(1, 4)]
backend_class = LocalBackend
def setUp(self):
self.original_backends = settings.BACKENDS
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
super(LocalPlaybackControllerTest, self).setUp()
# Two tests does not work at all when using the fake sink
#self.backend.playback.use_fake_sink()
def tearDown(self):
super(LocalPlaybackControllerTest, self).tearDown()
settings.BACKENDS = settings.original_backends
def add_track(self, path):
uri = path_to_uri(data_folder(path))
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_play_mp3(self):
self.add_track('blank.mp3')
self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING)
def test_play_ogg(self):
self.add_track('blank.ogg')
self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING)
def test_play_flac(self):
self.add_track('blank.flac')
self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING)
from tests import data_folder
from tests.backends.base.stored_playlists import \
BaseStoredPlaylistsControllerTest
from tests.backends.local import generate_song
class LocalStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest,
unittest.TestCase):
@ -149,27 +89,3 @@ class LocalStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest,
def test_save_sets_playlist_uri(self):
raise SkipTest
class LocalLibraryControllerTest(BaseLibraryControllerTest,
unittest.TestCase):
backend_class = LocalBackend
def setUp(self):
self.original_tag_cache = settings.LOCAL_TAG_CACHE
self.original_music_folder = settings.LOCAL_MUSIC_FOLDER
settings.LOCAL_TAG_CACHE = data_folder('library_tag_cache')
settings.LOCAL_MUSIC_FOLDER = data_folder('')
super(LocalLibraryControllerTest, self).setUp()
def tearDown(self):
settings.LOCAL_TAG_CACHE = self.original_tag_cache
settings.LOCAL_MUSIC_FOLDER = self.original_music_folder
super(LocalLibraryControllerTest, self).tearDown()
if __name__ == '__main__':
unittest.main()

View File

@ -9,10 +9,9 @@ class VersionTest(unittest.TestCase):
def test_versions_can_be_strictly_ordered(self):
self.assert_(SV('0.1.0a0') < SV('0.1.0a1'))
self.assert_(SV('0.1.0a2') < SV(get_version()))
self.assert_(SV('0.1.0a1') < SV('0.1.0a2'))
self.assert_(SV('0.1.0a2') < SV('0.1.0a3'))
self.assert_(SV('0.1.0a3') < SV(get_version()))
self.assert_(SV(get_version()) < SV('0.1.0a5'))
self.assert_(SV('0.1.0a0') < SV('0.1.0'))
self.assert_(SV('0.1.0') < SV('0.1.1'))
self.assert_(SV(get_version()) < SV('0.1.1'))
self.assert_(SV('0.1.1') < SV('0.2.0'))
self.assert_(SV('0.2.0') < SV('1.0.0'))