diff --git a/docs/api/frontends/index.rst b/docs/api/frontends/index.rst new file mode 100644 index 00000000..052c7781 --- /dev/null +++ b/docs/api/frontends/index.rst @@ -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` diff --git a/docs/api/mpd.rst b/docs/api/frontends/mpd.rst similarity index 100% rename from docs/api/mpd.rst rename to docs/api/frontends/mpd.rst diff --git a/docs/api/mixers.rst b/docs/api/mixers.rst index 91c2e7aa..edaea306 100644 --- a/docs/api/mixers.rst +++ b/docs/api/mixers.rst @@ -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 diff --git a/docs/api/outputs.rst b/docs/api/outputs.rst new file mode 100644 index 00000000..8f4e33c0 --- /dev/null +++ b/docs/api/outputs.rst @@ -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: diff --git a/docs/changes.rst b/docs/changes.rst index 4fdc8c1f..e84d7aa9 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -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 `. - :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: diff --git a/docs/development/internals.rst b/docs/development/internals.rst index 0af13aa8..4b4d3b14 100644 --- a/docs/development/internals.rst +++ b/docs/development/internals.rst @@ -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``. diff --git a/docs/installation/index.rst b/docs/installation/index.rst index abd185f1..26b864d2 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -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 `. 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 `. To use Spotify with Mopidy, you also need +:doc:`libspotify and pyspotify `. 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 ` @@ -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``. diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 7cdcad6a..15b7b1ad 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -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): diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index f00ec1f0..07f3e2f7 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -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 diff --git a/mopidy/backends/libspotify/library.py b/mopidy/backends/libspotify/library.py index c2b70dca..ffb9ee57 100644 --- a/mopidy/backends/libspotify/library.py +++ b/mopidy/backends/libspotify/library.py @@ -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): diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index e9e86f34..50b3d84d 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -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): diff --git a/mopidy/core.py b/mopidy/core.py index 396a2091..3296fa6b 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -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): diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 2361d2bf..dc11e3a6 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -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): diff --git a/mopidy/mixers/__init__.py b/mopidy/mixers/__init__.py index c9543863..332718a6 100644 --- a/mopidy/mixers/__init__.py +++ b/mopidy/mixers/__init__.py @@ -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): diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index efcb1e98..6eef6da4 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -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): diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index 7cdf0d7b..32750f60 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -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() diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index f859791b..929d2e1d 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -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': diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index c0ffd0c0..79f23608 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -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): diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 002b54c8..0dd163ec 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -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? diff --git a/tests/backends/base/__init__.py b/tests/backends/base/__init__.py new file mode 100644 index 00000000..29f010e1 --- /dev/null +++ b/tests/backends/base/__init__.py @@ -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 diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py new file mode 100644 index 00000000..1b312c2f --- /dev/null +++ b/tests/backends/base/current_playlist.py @@ -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) diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py new file mode 100644 index 00000000..1239bd08 --- /dev/null +++ b/tests/backends/base/library.py @@ -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) diff --git a/tests/backends/base.py b/tests/backends/base/playback.py similarity index 60% rename from tests/backends/base.py rename to tests/backends/base/playback.py index e9b78453..f8e9dd87 100644 --- a/tests/backends/base.py +++ b/tests/backends/base/playback.py @@ -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) diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py new file mode 100644 index 00000000..630898de --- /dev/null +++ b/tests/backends/base/stored_playlists.py @@ -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 diff --git a/tests/backends/libspotify/__init__.py b/tests/backends/libspotify/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/backends/libspotify_integrationtest.py b/tests/backends/libspotify/backend_integrationtest.py similarity index 76% rename from tests/backends/libspotify_integrationtest.py rename to tests/backends/libspotify/backend_integrationtest.py index 1e7e9b97..8d1f0b0e 100644 --- a/tests/backends/libspotify_integrationtest.py +++ b/tests/backends/libspotify/backend_integrationtest.py @@ -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 diff --git a/tests/backends/local/__init__.py b/tests/backends/local/__init__.py index e69de29b..60a1bd4d 100644 --- a/tests/backends/local/__init__.py +++ b/tests/backends/local/__init__.py @@ -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) diff --git a/tests/backends/local/current_playlist_test.py b/tests/backends/local/current_playlist_test.py new file mode 100644 index 00000000..01354a06 --- /dev/null +++ b/tests/backends/local/current_playlist_test.py @@ -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 diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py new file mode 100644 index 00000000..75751e3d --- /dev/null +++ b/tests/backends/local/library_test.py @@ -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() diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py new file mode 100644 index 00000000..4a385a9d --- /dev/null +++ b/tests/backends/local/playback_test.py @@ -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) diff --git a/tests/backends/local/backend_test.py b/tests/backends/local/stored_playlists_test.py similarity index 50% rename from tests/backends/local/backend_test.py rename to tests/backends/local/stored_playlists_test.py index b95c6dde..bb03f997 100644 --- a/tests/backends/local/backend_test.py +++ b/tests/backends/local/stored_playlists_test.py @@ -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() diff --git a/tests/version_test.py b/tests/version_test.py index 6ab3ee2f..a44e4e89 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -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'))