Merge pull request #1163 from adamcik/feature/core-dont-trust-backends
Don't trust backends...
This commit is contained in:
commit
ad585d60d4
@ -30,6 +30,8 @@ Core API
|
|||||||
- Add :meth:`mopidy.core.playback.PlaybackController.get_current_tlid`.
|
- Add :meth:`mopidy.core.playback.PlaybackController.get_current_tlid`.
|
||||||
(Part of: :issue:`1137`)
|
(Part of: :issue:`1137`)
|
||||||
|
|
||||||
|
- Update core to handle backend crashes and bad data. (Fixes: :issue:`1161`)
|
||||||
|
|
||||||
Models
|
Models
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,32 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
|
from mopidy import compat, exceptions, models
|
||||||
from mopidy.utils import deprecation, validation
|
from mopidy.utils import deprecation, validation
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _backend_error_handling(backend, reraise=None):
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except exceptions.ValidationError as e:
|
||||||
|
logger.error('%s backend returned bad data: %s',
|
||||||
|
backend.actor_ref.actor_class.__name__, e)
|
||||||
|
except Exception as e:
|
||||||
|
if reraise and isinstance(e, reraise):
|
||||||
|
raise
|
||||||
|
logger.exception('%s backend caused an exception.',
|
||||||
|
backend.actor_ref.actor_class.__name__)
|
||||||
|
|
||||||
|
|
||||||
class LibraryController(object):
|
class LibraryController(object):
|
||||||
pykka_traversable = True
|
pykka_traversable = True
|
||||||
|
|
||||||
@ -79,22 +95,24 @@ class LibraryController(object):
|
|||||||
backends = self.backends.with_library_browse.values()
|
backends = self.backends.with_library_browse.values()
|
||||||
futures = {b: b.library.root_directory for b in backends}
|
futures = {b: b.library.root_directory for b in backends}
|
||||||
for backend, future in futures.items():
|
for backend, future in futures.items():
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
directories.add(future.get())
|
root = future.get()
|
||||||
except Exception:
|
validation.check_instance(root, models.Ref)
|
||||||
logger.exception('%s backend caused an exception.',
|
directories.add(root)
|
||||||
backend.actor_ref.actor_class.__name__)
|
|
||||||
return sorted(directories, key=operator.attrgetter('name'))
|
return sorted(directories, key=operator.attrgetter('name'))
|
||||||
|
|
||||||
def _browse(self, uri):
|
def _browse(self, uri):
|
||||||
scheme = urlparse.urlparse(uri).scheme
|
scheme = urlparse.urlparse(uri).scheme
|
||||||
backend = self.backends.with_library_browse.get(scheme)
|
backend = self.backends.with_library_browse.get(scheme)
|
||||||
try:
|
|
||||||
if backend:
|
if not backend:
|
||||||
return backend.library.browse(uri).get()
|
return []
|
||||||
except Exception:
|
|
||||||
logger.exception('%s backend caused an exception.',
|
with _backend_error_handling(backend):
|
||||||
backend.actor_ref.actor_class.__name__)
|
result = backend.library.browse(uri).get()
|
||||||
|
validation.check_instances(result, models.Ref)
|
||||||
|
return result
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_distinct(self, field, query=None):
|
def get_distinct(self, field, query=None):
|
||||||
@ -120,11 +138,11 @@ class LibraryController(object):
|
|||||||
futures = {b: b.library.get_distinct(field, query)
|
futures = {b: b.library.get_distinct(field, query)
|
||||||
for b in self.backends.with_library.values()}
|
for b in self.backends.with_library.values()}
|
||||||
for backend, future in futures.items():
|
for backend, future in futures.items():
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
result.update(future.get())
|
values = future.get()
|
||||||
except Exception:
|
if values is not None:
|
||||||
logger.exception('%s backend caused an exception.',
|
validation.check_instances(values, compat.text_type)
|
||||||
backend.actor_ref.actor_class.__name__)
|
result.update(values)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_images(self, uris):
|
def get_images(self, uris):
|
||||||
@ -152,12 +170,16 @@ class LibraryController(object):
|
|||||||
|
|
||||||
results = {uri: tuple() for uri in uris}
|
results = {uri: tuple() for uri in uris}
|
||||||
for backend, future in futures.items():
|
for backend, future in futures.items():
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
|
if future.get() is None:
|
||||||
|
continue
|
||||||
|
validation.check_instance(future.get(), collections.Mapping)
|
||||||
for uri, images in future.get().items():
|
for uri, images in future.get().items():
|
||||||
|
if uri not in uris:
|
||||||
|
raise exceptions.ValidationError(
|
||||||
|
'Got unknown image URI: %s' % uri)
|
||||||
|
validation.check_instances(images, models.Image)
|
||||||
results[uri] += tuple(images)
|
results[uri] += tuple(images)
|
||||||
except Exception:
|
|
||||||
logger.exception('%s backend caused an exception.',
|
|
||||||
backend.actor_ref.actor_class.__name__)
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def find_exact(self, query=None, uris=None, **kwargs):
|
def find_exact(self, query=None, uris=None, **kwargs):
|
||||||
@ -202,7 +224,7 @@ class LibraryController(object):
|
|||||||
uris = [uri]
|
uris = [uri]
|
||||||
|
|
||||||
futures = {}
|
futures = {}
|
||||||
result = {u: [] for u in uris}
|
results = {u: [] for u in uris}
|
||||||
|
|
||||||
# TODO: lookup(uris) to backend APIs
|
# TODO: lookup(uris) to backend APIs
|
||||||
for backend, backend_uris in self._get_backends_to_uris(uris).items():
|
for backend, backend_uris in self._get_backends_to_uris(uris).items():
|
||||||
@ -210,15 +232,15 @@ class LibraryController(object):
|
|||||||
futures[(backend, u)] = backend.library.lookup(u)
|
futures[(backend, u)] = backend.library.lookup(u)
|
||||||
|
|
||||||
for (backend, u), future in futures.items():
|
for (backend, u), future in futures.items():
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
result[u] = future.get()
|
result = future.get()
|
||||||
except Exception:
|
if result is not None:
|
||||||
logger.exception('%s backend caused an exception.',
|
validation.check_instances(result, models.Track)
|
||||||
backend.actor_ref.actor_class.__name__)
|
results[u] = result
|
||||||
|
|
||||||
if uri:
|
if uri:
|
||||||
return result[uri]
|
return results[uri]
|
||||||
return result
|
return results
|
||||||
|
|
||||||
def refresh(self, uri=None):
|
def refresh(self, uri=None):
|
||||||
"""
|
"""
|
||||||
@ -241,11 +263,8 @@ class LibraryController(object):
|
|||||||
futures[backend] = backend.library.refresh(uri)
|
futures[backend] = backend.library.refresh(uri)
|
||||||
|
|
||||||
for backend, future in futures.items():
|
for backend, future in futures.items():
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
future.get()
|
future.get()
|
||||||
except Exception:
|
|
||||||
logger.exception('%s backend caused an exception.',
|
|
||||||
backend.actor_ref.actor_class.__name__)
|
|
||||||
|
|
||||||
def search(self, query=None, uris=None, exact=False, **kwargs):
|
def search(self, query=None, uris=None, exact=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -311,25 +330,26 @@ class LibraryController(object):
|
|||||||
futures[backend] = backend.library.search(
|
futures[backend] = backend.library.search(
|
||||||
query=query, uris=backend_uris, exact=exact)
|
query=query, uris=backend_uris, exact=exact)
|
||||||
|
|
||||||
|
# Some of our tests check for LookupError to catch bad queries. This is
|
||||||
|
# silly and should be replaced with query validation before passing it
|
||||||
|
# to the backends.
|
||||||
|
reraise = (TypeError, LookupError)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for backend, future in futures.items():
|
for backend, future in futures.items():
|
||||||
try:
|
try:
|
||||||
results.append(future.get())
|
with _backend_error_handling(backend, reraise=reraise):
|
||||||
|
result = future.get()
|
||||||
|
if result is not None:
|
||||||
|
validation.check_instance(result, models.SearchResult)
|
||||||
|
results.append(result)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
backend_name = backend.actor_ref.actor_class.__name__
|
backend_name = backend.actor_ref.actor_class.__name__
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'%s does not implement library.search() with "exact" '
|
'%s does not implement library.search() with "exact" '
|
||||||
'support. Please upgrade it.', backend_name)
|
'support. Please upgrade it.', backend_name)
|
||||||
except LookupError:
|
|
||||||
# Some of our tests check for this to catch bad queries. This
|
|
||||||
# is silly and should be replaced with query validation before
|
|
||||||
# passing it to the backends.
|
|
||||||
raise
|
|
||||||
except Exception:
|
|
||||||
logger.exception('%s backend caused an exception.',
|
|
||||||
backend.actor_ref.actor_class.__name__)
|
|
||||||
|
|
||||||
return [r for r in results if r]
|
return results
|
||||||
|
|
||||||
|
|
||||||
def _normalize_query(query):
|
def _normalize_query(query):
|
||||||
|
|||||||
@ -1,13 +1,27 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from mopidy import exceptions
|
||||||
from mopidy.utils import validation
|
from mopidy.utils import validation
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _mixer_error_handling(mixer):
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except exceptions.ValidationError as e:
|
||||||
|
logger.error('%s mixer returned bad data: %s',
|
||||||
|
mixer.actor_ref.actor_class.__name__, e)
|
||||||
|
except Exception:
|
||||||
|
logger.exception('%s mixer caused an exception.',
|
||||||
|
mixer.actor_ref.actor_class.__name__)
|
||||||
|
|
||||||
|
|
||||||
class MixerController(object):
|
class MixerController(object):
|
||||||
pykka_traversable = True
|
pykka_traversable = True
|
||||||
|
|
||||||
@ -21,8 +35,15 @@ class MixerController(object):
|
|||||||
|
|
||||||
The volume scale is linear.
|
The volume scale is linear.
|
||||||
"""
|
"""
|
||||||
if self._mixer is not None:
|
if self._mixer is None:
|
||||||
return self._mixer.get_volume().get()
|
return None
|
||||||
|
|
||||||
|
with _mixer_error_handling(self._mixer):
|
||||||
|
volume = self._mixer.get_volume().get()
|
||||||
|
volume is None or validation.check_integer(volume, min=0, max=100)
|
||||||
|
return volume
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def set_volume(self, volume):
|
def set_volume(self, volume):
|
||||||
"""Set the volume.
|
"""Set the volume.
|
||||||
@ -36,9 +57,14 @@ class MixerController(object):
|
|||||||
validation.check_integer(volume, min=0, max=100)
|
validation.check_integer(volume, min=0, max=100)
|
||||||
|
|
||||||
if self._mixer is None:
|
if self._mixer is None:
|
||||||
return False
|
return False # TODO: 2.0 return None
|
||||||
else:
|
|
||||||
return self._mixer.set_volume(volume).get()
|
with _mixer_error_handling(self._mixer):
|
||||||
|
result = self._mixer.set_volume(volume).get()
|
||||||
|
validation.check_instance(result, bool)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def get_mute(self):
|
def get_mute(self):
|
||||||
"""Get mute state.
|
"""Get mute state.
|
||||||
@ -46,8 +72,15 @@ class MixerController(object):
|
|||||||
:class:`True` if muted, :class:`False` unmuted, :class:`None` if
|
:class:`True` if muted, :class:`False` unmuted, :class:`None` if
|
||||||
unknown.
|
unknown.
|
||||||
"""
|
"""
|
||||||
if self._mixer is not None:
|
if self._mixer is None:
|
||||||
return self._mixer.get_mute().get()
|
return None
|
||||||
|
|
||||||
|
with _mixer_error_handling(self._mixer):
|
||||||
|
mute = self._mixer.get_mute().get()
|
||||||
|
mute is None or validation.check_instance(mute, bool)
|
||||||
|
return mute
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def set_mute(self, mute):
|
def set_mute(self, mute):
|
||||||
"""Set mute state.
|
"""Set mute state.
|
||||||
@ -58,6 +91,11 @@ class MixerController(object):
|
|||||||
"""
|
"""
|
||||||
validation.check_boolean(mute)
|
validation.check_boolean(mute)
|
||||||
if self._mixer is None:
|
if self._mixer is None:
|
||||||
return False
|
return False # TODO: 2.0 return None
|
||||||
else:
|
|
||||||
return self._mixer.set_mute(bool(mute)).get()
|
with _mixer_error_handling(self._mixer):
|
||||||
|
result = self._mixer.set_mute(bool(mute)).get()
|
||||||
|
validation.check_instance(result, bool)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return False
|
||||||
|
|||||||
@ -1,15 +1,31 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
|
from mopidy import exceptions
|
||||||
from mopidy.core import listener
|
from mopidy.core import listener
|
||||||
from mopidy.models import Playlist
|
from mopidy.models import Playlist, Ref
|
||||||
from mopidy.utils import deprecation, validation
|
from mopidy.utils import deprecation, validation
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _backend_error_handling(backend, reraise=None):
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except exceptions.ValidationError as e:
|
||||||
|
logger.error('%s backend returned bad data: %s',
|
||||||
|
backend.actor_ref.actor_class.__name__, e)
|
||||||
|
except Exception as e:
|
||||||
|
if reraise and isinstance(e, reraise):
|
||||||
|
raise
|
||||||
|
logger.exception('%s backend caused an exception.',
|
||||||
|
backend.actor_ref.actor_class.__name__)
|
||||||
|
|
||||||
|
|
||||||
class PlaylistsController(object):
|
class PlaylistsController(object):
|
||||||
pykka_traversable = True
|
pykka_traversable = True
|
||||||
|
|
||||||
@ -34,17 +50,18 @@ class PlaylistsController(object):
|
|||||||
for backend in set(self.backends.with_playlists.values())}
|
for backend in set(self.backends.with_playlists.values())}
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for backend, future in futures.items():
|
for b, future in futures.items():
|
||||||
try:
|
try:
|
||||||
results.extend(future.get())
|
with _backend_error_handling(b, reraise=NotImplementedError):
|
||||||
|
playlists = future.get()
|
||||||
|
if playlists is not None:
|
||||||
|
validation.check_instances(playlists, Ref)
|
||||||
|
results.extend(playlists)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
backend_name = backend.actor_ref.actor_class.__name__
|
backend_name = b.actor_ref.actor_class.__name__
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'%s does not implement playlists.as_list(). '
|
'%s does not implement playlists.as_list(). '
|
||||||
'Please upgrade it.', backend_name)
|
'Please upgrade it.', backend_name)
|
||||||
except Exception:
|
|
||||||
logger.exception('%s backend caused an exception.',
|
|
||||||
backend.actor_ref.actor_class.__name__)
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@ -66,8 +83,16 @@ class PlaylistsController(object):
|
|||||||
|
|
||||||
uri_scheme = urlparse.urlparse(uri).scheme
|
uri_scheme = urlparse.urlparse(uri).scheme
|
||||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||||
if backend:
|
|
||||||
return backend.playlists.get_items(uri).get()
|
if not backend:
|
||||||
|
return None
|
||||||
|
|
||||||
|
with _backend_error_handling(backend):
|
||||||
|
items = backend.playlists.get_items(uri).get()
|
||||||
|
items is None or validation.check_instances(items, Ref)
|
||||||
|
return items
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def get_playlists(self, include_tracks=True):
|
def get_playlists(self, include_tracks=True):
|
||||||
"""
|
"""
|
||||||
@ -120,22 +145,23 @@ class PlaylistsController(object):
|
|||||||
:type name: string
|
:type name: string
|
||||||
:param uri_scheme: use the backend matching the URI scheme
|
:param uri_scheme: use the backend matching the URI scheme
|
||||||
:type uri_scheme: string
|
:type uri_scheme: string
|
||||||
:rtype: :class:`mopidy.models.Playlist`
|
:rtype: :class:`mopidy.models.Playlist` or :class:`None`
|
||||||
"""
|
"""
|
||||||
if uri_scheme in self.backends.with_playlists:
|
if uri_scheme in self.backends.with_playlists:
|
||||||
backends = [self.backends.with_playlists[uri_scheme]]
|
backends = [self.backends.with_playlists[uri_scheme]]
|
||||||
else:
|
else:
|
||||||
backends = self.backends.with_playlists.values()
|
backends = self.backends.with_playlists.values()
|
||||||
|
|
||||||
for backend in backends:
|
for backend in backends:
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
playlist = backend.playlists.create(name).get()
|
result = backend.playlists.create(name).get()
|
||||||
except Exception:
|
if result is None:
|
||||||
playlist = None
|
continue
|
||||||
# Workaround for playlist providers that return None from create()
|
validation.check_instance(result, Playlist)
|
||||||
if not playlist:
|
listener.CoreListener.send('playlist_changed', playlist=result)
|
||||||
continue
|
return result
|
||||||
listener.CoreListener.send('playlist_changed', playlist=playlist)
|
|
||||||
return playlist
|
return None
|
||||||
|
|
||||||
def delete(self, uri):
|
def delete(self, uri):
|
||||||
"""
|
"""
|
||||||
@ -151,8 +177,14 @@ class PlaylistsController(object):
|
|||||||
|
|
||||||
uri_scheme = urlparse.urlparse(uri).scheme
|
uri_scheme = urlparse.urlparse(uri).scheme
|
||||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||||
if backend:
|
if not backend:
|
||||||
|
return
|
||||||
|
|
||||||
|
with _backend_error_handling(backend):
|
||||||
backend.playlists.delete(uri).get()
|
backend.playlists.delete(uri).get()
|
||||||
|
# TODO: emit playlist changed?
|
||||||
|
|
||||||
|
# TODO: return value?
|
||||||
|
|
||||||
def filter(self, criteria=None, **kwargs):
|
def filter(self, criteria=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -198,11 +230,16 @@ class PlaylistsController(object):
|
|||||||
"""
|
"""
|
||||||
uri_scheme = urlparse.urlparse(uri).scheme
|
uri_scheme = urlparse.urlparse(uri).scheme
|
||||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||||
if backend:
|
if not backend:
|
||||||
return backend.playlists.lookup(uri).get()
|
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
with _backend_error_handling(backend):
|
||||||
|
playlist = backend.playlists.lookup(uri).get()
|
||||||
|
playlist is None or validation.check_instance(playlist, Playlist)
|
||||||
|
return playlist
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
# TODO: there is an inconsistency between library.refresh(uri) and this
|
# TODO: there is an inconsistency between library.refresh(uri) and this
|
||||||
# call, not sure how to sort this out.
|
# call, not sure how to sort this out.
|
||||||
def refresh(self, uri_scheme=None):
|
def refresh(self, uri_scheme=None):
|
||||||
@ -231,12 +268,9 @@ class PlaylistsController(object):
|
|||||||
futures[backend] = backend.playlists.refresh()
|
futures[backend] = backend.playlists.refresh()
|
||||||
|
|
||||||
for backend, future in futures.items():
|
for backend, future in futures.items():
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
future.get()
|
future.get()
|
||||||
playlists_loaded = True
|
playlists_loaded = True
|
||||||
except Exception:
|
|
||||||
logger.exception('%s backend caused an exception.',
|
|
||||||
backend.actor_ref.actor_class.__name__)
|
|
||||||
|
|
||||||
if playlists_loaded:
|
if playlists_loaded:
|
||||||
listener.CoreListener.send('playlists_loaded')
|
listener.CoreListener.send('playlists_loaded')
|
||||||
@ -270,7 +304,16 @@ class PlaylistsController(object):
|
|||||||
|
|
||||||
uri_scheme = urlparse.urlparse(playlist.uri).scheme
|
uri_scheme = urlparse.urlparse(playlist.uri).scheme
|
||||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||||
if backend:
|
if not backend:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# TODO: we let AssertionError error through due to legacy tests :/
|
||||||
|
with _backend_error_handling(backend, reraise=AssertionError):
|
||||||
playlist = backend.playlists.save(playlist).get()
|
playlist = backend.playlists.save(playlist).get()
|
||||||
listener.CoreListener.send('playlist_changed', playlist=playlist)
|
playlist is None or validation.check_instance(playlist, Playlist)
|
||||||
|
if playlist:
|
||||||
|
listener.CoreListener.send(
|
||||||
|
'playlist_changed', playlist=playlist)
|
||||||
return playlist
|
return playlist
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class BaseCoreLibraryTest(unittest.TestCase):
|
|||||||
dummy1_root = Ref.directory(uri='dummy1:directory', name='dummy1')
|
dummy1_root = Ref.directory(uri='dummy1:directory', name='dummy1')
|
||||||
self.backend1 = mock.Mock()
|
self.backend1 = mock.Mock()
|
||||||
self.backend1.uri_schemes.get.return_value = ['dummy1']
|
self.backend1.uri_schemes.get.return_value = ['dummy1']
|
||||||
|
self.backend1.actor_ref.actor_class.__name__ = 'DummyBackend1'
|
||||||
self.library1 = mock.Mock(spec=backend.LibraryProvider)
|
self.library1 = mock.Mock(spec=backend.LibraryProvider)
|
||||||
self.library1.get_images.return_value.get.return_value = {}
|
self.library1.get_images.return_value.get.return_value = {}
|
||||||
self.library1.root_directory.get.return_value = dummy1_root
|
self.library1.root_directory.get.return_value = dummy1_root
|
||||||
@ -23,6 +24,7 @@ class BaseCoreLibraryTest(unittest.TestCase):
|
|||||||
dummy2_root = Ref.directory(uri='dummy2:directory', name='dummy2')
|
dummy2_root = Ref.directory(uri='dummy2:directory', name='dummy2')
|
||||||
self.backend2 = mock.Mock()
|
self.backend2 = mock.Mock()
|
||||||
self.backend2.uri_schemes.get.return_value = ['dummy2', 'du2']
|
self.backend2.uri_schemes.get.return_value = ['dummy2', 'du2']
|
||||||
|
self.backend2.actor_ref.actor_class.__name__ = 'DummyBackend2'
|
||||||
self.library2 = mock.Mock(spec=backend.LibraryProvider)
|
self.library2 = mock.Mock(spec=backend.LibraryProvider)
|
||||||
self.library2.get_images.return_value.get.return_value = {}
|
self.library2.get_images.return_value.get.return_value = {}
|
||||||
self.library2.root_directory.get.return_value = dummy2_root
|
self.library2.root_directory.get.return_value = dummy2_root
|
||||||
@ -31,6 +33,7 @@ class BaseCoreLibraryTest(unittest.TestCase):
|
|||||||
# A backend without the optional library provider
|
# A backend without the optional library provider
|
||||||
self.backend3 = mock.Mock()
|
self.backend3 = mock.Mock()
|
||||||
self.backend3.uri_schemes.get.return_value = ['dummy3']
|
self.backend3.uri_schemes.get.return_value = ['dummy3']
|
||||||
|
self.backend3.actor_ref.actor_class.__name__ = 'DummyBackend3'
|
||||||
self.backend3.has_library().get.return_value = False
|
self.backend3.has_library().get.return_value = False
|
||||||
self.backend3.has_library_browse().get.return_value = False
|
self.backend3.has_library_browse().get.return_value = False
|
||||||
|
|
||||||
@ -148,11 +151,14 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
|||||||
self.core.library.lookup('dummy1:a', ['dummy2:a'])
|
self.core.library.lookup('dummy1:a', ['dummy2:a'])
|
||||||
|
|
||||||
def test_lookup_can_handle_uris(self):
|
def test_lookup_can_handle_uris(self):
|
||||||
self.library1.lookup().get.return_value = [1234]
|
track1 = Track(name='abc')
|
||||||
self.library2.lookup().get.return_value = [5678]
|
track2 = Track(name='def')
|
||||||
|
|
||||||
|
self.library1.lookup().get.return_value = [track1]
|
||||||
|
self.library2.lookup().get.return_value = [track2]
|
||||||
|
|
||||||
result = self.core.library.lookup(uris=['dummy1:a', 'dummy2:a'])
|
result = self.core.library.lookup(uris=['dummy1:a', 'dummy2:a'])
|
||||||
self.assertEqual(result, {'dummy2:a': [5678], 'dummy1:a': [1234]})
|
self.assertEqual(result, {'dummy2:a': [track2], 'dummy1:a': [track1]})
|
||||||
|
|
||||||
def test_lookup_uris_returns_empty_list_for_dummy3_track(self):
|
def test_lookup_uris_returns_empty_list_for_dummy3_track(self):
|
||||||
result = self.core.library.lookup(uris=['dummy3:a'])
|
result = self.core.library.lookup(uris=['dummy3:a'])
|
||||||
@ -349,12 +355,14 @@ class DeprecatedLookupCoreLibraryTest(BaseCoreLibraryTest):
|
|||||||
return super(DeprecatedLookupCoreLibraryTest, self).run(result)
|
return super(DeprecatedLookupCoreLibraryTest, self).run(result)
|
||||||
|
|
||||||
def test_lookup_selects_dummy1_backend(self):
|
def test_lookup_selects_dummy1_backend(self):
|
||||||
|
self.library1.lookup.return_value.get.return_value = []
|
||||||
self.core.library.lookup('dummy1:a')
|
self.core.library.lookup('dummy1:a')
|
||||||
|
|
||||||
self.library1.lookup.assert_called_once_with('dummy1:a')
|
self.library1.lookup.assert_called_once_with('dummy1:a')
|
||||||
self.assertFalse(self.library2.lookup.called)
|
self.assertFalse(self.library2.lookup.called)
|
||||||
|
|
||||||
def test_lookup_selects_dummy2_backend(self):
|
def test_lookup_selects_dummy2_backend(self):
|
||||||
|
self.library2.lookup.return_value.get.return_value = []
|
||||||
self.core.library.lookup('dummy2:a')
|
self.core.library.lookup('dummy2:a')
|
||||||
|
|
||||||
self.assertFalse(self.library1.lookup.called)
|
self.assertFalse(self.library1.lookup.called)
|
||||||
@ -407,8 +415,7 @@ class LegacyFindExactToSearchLibraryTest(unittest.TestCase):
|
|||||||
# We are just testing that this doesn't fail.
|
# We are just testing that this doesn't fail.
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('mopidy.core.library.logger')
|
class MockBackendCoreLibraryBase(unittest.TestCase):
|
||||||
class BackendFailuresCoreLibraryTest(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self): # noqa: N802
|
def setUp(self): # noqa: N802
|
||||||
dummy_root = Ref.directory(uri='dummy:directory', name='dummy')
|
dummy_root = Ref.directory(uri='dummy:directory', name='dummy')
|
||||||
@ -423,52 +430,182 @@ class BackendFailuresCoreLibraryTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.core = core.Core(mixer=None, backends=[self.backend])
|
self.core = core.Core(mixer=None, backends=[self.backend])
|
||||||
|
|
||||||
def test_browse_backend_get_root_exception_gets_ignored(self, logger):
|
|
||||||
|
@mock.patch('mopidy.core.library.logger')
|
||||||
|
class BrowseBadBackendTest(MockBackendCoreLibraryBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception_for_root(self, logger):
|
||||||
# Might happen if root_directory is a property for some weird reason.
|
# Might happen if root_directory is a property for some weird reason.
|
||||||
self.library.root_directory.get.side_effect = Exception
|
self.library.root_directory.get.side_effect = Exception
|
||||||
self.assertEqual([], self.core.library.browse(None))
|
self.assertEqual([], self.core.library.browse(None))
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
def test_browse_backend_browse_uri_exception_gets_ignored(self, logger):
|
def test_backend_returns_none_for_root(self, logger):
|
||||||
|
self.library.root_directory.get.return_value = None
|
||||||
|
self.assertEqual([], self.core.library.browse(None))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type_for_root(self, logger):
|
||||||
|
self.library.root_directory.get.return_value = 123
|
||||||
|
self.assertEqual([], self.core.library.browse(None))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
def test_backend_raises_exception_for_browse(self, logger):
|
||||||
self.library.browse.return_value.get.side_effect = Exception
|
self.library.browse.return_value.get.side_effect = Exception
|
||||||
self.assertEqual([], self.core.library.browse('dummy:directory'))
|
self.assertEqual([], self.core.library.browse('dummy:directory'))
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
def test_get_distinct_backend_exception_gets_ignored(self, logger):
|
def test_backend_returns_wrong_type_for_browse(self, logger):
|
||||||
|
self.library.browse.return_value.get.return_value = [123]
|
||||||
|
self.assertEqual([], self.core.library.browse('dummy:directory'))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('mopidy.core.library.logger')
|
||||||
|
class GetDistinctBadBackendTest(MockBackendCoreLibraryBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self, logger):
|
||||||
self.library.get_distinct.return_value.get.side_effect = Exception
|
self.library.get_distinct.return_value.get.side_effect = Exception
|
||||||
self.assertEqual(set(), self.core.library.get_distinct('artist'))
|
self.assertEqual(set(), self.core.library.get_distinct('artist'))
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
def test_get_images_backend_exception_get_ignored(self, logger):
|
def test_backend_returns_none(self, logger):
|
||||||
|
self.library.get_distinct.return_value.get.return_value = None
|
||||||
|
self.assertEqual(set(), self.core.library.get_distinct('artist'))
|
||||||
|
self.assertFalse(logger.error.called)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self, logger):
|
||||||
|
self.library.get_distinct.return_value.get.return_value = 'abc'
|
||||||
|
self.assertEqual(set(), self.core.library.get_distinct('artist'))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
def test_backend_returns_iterable_containing_wrong_types(self, logger):
|
||||||
|
self.library.get_distinct.return_value.get.return_value = [1, 2, 3]
|
||||||
|
self.assertEqual(set(), self.core.library.get_distinct('artist'))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('mopidy.core.library.logger')
|
||||||
|
class GetImagesBadBackendTest(MockBackendCoreLibraryBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
self.library.get_images.return_value.get.side_effect = Exception
|
self.library.get_images.return_value.get.side_effect = Exception
|
||||||
self.assertEqual(
|
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||||
{'dummy:/1': tuple()}, self.core.library.get_images(['dummy:/1']))
|
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
def test_lookup_backend_exceptiosn_gets_ignores(self, logger):
|
def test_backend_returns_none(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
|
self.library.get_images.return_value.get.return_value = None
|
||||||
|
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||||
|
self.assertFalse(logger.error.called)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
|
self.library.get_images.return_value.get.return_value = 'abc'
|
||||||
|
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
def test_backend_returns_mapping_containing_wrong_types(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
|
self.library.get_images.return_value.get.return_value = {uri: 'abc'}
|
||||||
|
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
def test_backend_returns_mapping_containing_none(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
|
self.library.get_images.return_value.get.return_value = {uri: None}
|
||||||
|
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
def test_backend_returns_unknown_uri(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
|
self.library.get_images.return_value.get.return_value = {'foo': []}
|
||||||
|
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('mopidy.core.library.logger')
|
||||||
|
class LookupByUrisBadBackendTest(MockBackendCoreLibraryBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
self.library.lookup.return_value.get.side_effect = Exception
|
self.library.lookup.return_value.get.side_effect = Exception
|
||||||
self.assertEqual(
|
self.assertEqual({uri: []}, self.core.library.lookup(uris=[uri]))
|
||||||
{'dummy:/1': []}, self.core.library.lookup(uris=['dummy:/1']))
|
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
def test_refresh_backend_exception_gets_ignored(self, logger):
|
def test_backend_returns_none(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
|
self.library.lookup.return_value.get.return_value = None
|
||||||
|
self.assertEqual({uri: []}, self.core.library.lookup(uris=[uri]))
|
||||||
|
self.assertFalse(logger.error.called)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
|
self.library.lookup.return_value.get.return_value = 'abc'
|
||||||
|
self.assertEqual({uri: []}, self.core.library.lookup(uris=[uri]))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
def test_backend_returns_iterable_containing_wrong_types(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
|
self.library.lookup.return_value.get.return_value = [123]
|
||||||
|
self.assertEqual({uri: []}, self.core.library.lookup(uris=[uri]))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
def test_backend_returns_none_with_uri(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
|
self.library.lookup.return_value.get.return_value = None
|
||||||
|
self.assertEqual([], self.core.library.lookup(uri))
|
||||||
|
self.assertFalse(logger.error.called)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type_with_uri(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
|
self.library.lookup.return_value.get.return_value = 'abc'
|
||||||
|
self.assertEqual([], self.core.library.lookup(uri))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
def test_backend_returns_iterable_wrong_types_with_uri(self, logger):
|
||||||
|
uri = 'dummy:/1'
|
||||||
|
self.library.lookup.return_value.get.return_value = [123]
|
||||||
|
self.assertEqual([], self.core.library.lookup(uri))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('mopidy.core.library.logger')
|
||||||
|
class RefreshBadBackendTest(MockBackendCoreLibraryBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self, logger):
|
||||||
self.library.refresh.return_value.get.side_effect = Exception
|
self.library.refresh.return_value.get.side_effect = Exception
|
||||||
self.core.library.refresh()
|
self.core.library.refresh()
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
def test_refresh_uri_backend_exception_gets_ignored(self, logger):
|
def test_backend_raises_exception_with_uri(self, logger):
|
||||||
self.library.refresh.return_value.get.side_effect = Exception
|
self.library.refresh.return_value.get.side_effect = Exception
|
||||||
self.core.library.refresh('dummy:/1')
|
self.core.library.refresh('dummy:/1')
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
def test_search_backend_exception_gets_ignored(self, logger):
|
|
||||||
|
@mock.patch('mopidy.core.library.logger')
|
||||||
|
class SearchBadBackendTest(MockBackendCoreLibraryBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self, logger):
|
||||||
self.library.search.return_value.get.side_effect = Exception
|
self.library.search.return_value.get.side_effect = Exception
|
||||||
self.assertEqual([], self.core.library.search(query={'any': ['foo']}))
|
self.assertEqual([], self.core.library.search(query={'any': ['foo']}))
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
def test_search_backend_lookup_error_gets_through(self, logger):
|
def test_backend_raises_lookuperror(self, logger):
|
||||||
# TODO: is this behavior desired? Do we need to continue handling
|
# TODO: is this behavior desired? Do we need to continue handling
|
||||||
# LookupError case specially.
|
# LookupError case specially.
|
||||||
self.library.search.return_value.get.side_effect = LookupError
|
self.library.search.return_value.get.side_effect = LookupError
|
||||||
with self.assertRaises(LookupError):
|
with self.assertRaises(LookupError):
|
||||||
self.core.library.search(query={'any': ['foo']})
|
self.core.library.search(query={'any': ['foo']})
|
||||||
|
|
||||||
|
def test_backend_returns_none(self, logger):
|
||||||
|
self.library.search.return_value.get.return_value = None
|
||||||
|
self.assertEqual([], self.core.library.search(query={'any': ['foo']}))
|
||||||
|
self.assertFalse(logger.error.called)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self, logger):
|
||||||
|
self.library.search.return_value.get.return_value = 'abc'
|
||||||
|
self.assertEqual([], self.core.library.search(query={'any': ['foo']}))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|||||||
@ -23,6 +23,7 @@ class CoreMixerTest(unittest.TestCase):
|
|||||||
self.mixer.get_volume.assert_called_once_with()
|
self.mixer.get_volume.assert_called_once_with()
|
||||||
|
|
||||||
def test_set_volume(self):
|
def test_set_volume(self):
|
||||||
|
self.mixer.set_volume.return_value.get.return_value = True
|
||||||
self.core.mixer.set_volume(30)
|
self.core.mixer.set_volume(30)
|
||||||
|
|
||||||
self.mixer.set_volume.assert_called_once_with(30)
|
self.mixer.set_volume.assert_called_once_with(30)
|
||||||
@ -34,6 +35,7 @@ class CoreMixerTest(unittest.TestCase):
|
|||||||
self.mixer.get_mute.assert_called_once_with()
|
self.mixer.get_mute.assert_called_once_with()
|
||||||
|
|
||||||
def test_set_mute(self):
|
def test_set_mute(self):
|
||||||
|
self.mixer.set_mute.return_value.get.return_value = True
|
||||||
self.core.mixer.set_mute(True)
|
self.core.mixer.set_mute(True)
|
||||||
|
|
||||||
self.mixer.set_mute.assert_called_once_with(True)
|
self.mixer.set_mute.assert_called_once_with(True)
|
||||||
@ -92,3 +94,63 @@ class CoreNoneMixerListenerTest(unittest.TestCase):
|
|||||||
def test_forwards_mixer_mute_changed_event_to_frontends(self, send):
|
def test_forwards_mixer_mute_changed_event_to_frontends(self, send):
|
||||||
self.core.mixer.set_mute(mute=True)
|
self.core.mixer.set_mute(mute=True)
|
||||||
self.assertEqual(send.call_count, 0)
|
self.assertEqual(send.call_count, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class MockBackendCoreMixerBase(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self): # noqa: N802
|
||||||
|
self.mixer = mock.Mock()
|
||||||
|
self.mixer.actor_ref.actor_class.__name__ = 'DummyMixer'
|
||||||
|
self.core = core.Core(mixer=self.mixer, backends=[])
|
||||||
|
|
||||||
|
|
||||||
|
class GetVolumeBadBackendTest(MockBackendCoreMixerBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self):
|
||||||
|
self.mixer.get_volume.return_value.get.side_effect = Exception
|
||||||
|
self.assertEqual(self.core.mixer.get_volume(), None)
|
||||||
|
|
||||||
|
def test_backend_returns_too_small_value(self):
|
||||||
|
self.mixer.get_volume.return_value.get.return_value = -1
|
||||||
|
self.assertEqual(self.core.mixer.get_volume(), None)
|
||||||
|
|
||||||
|
def test_backend_returns_too_large_value(self):
|
||||||
|
self.mixer.get_volume.return_value.get.return_value = 1000
|
||||||
|
self.assertEqual(self.core.mixer.get_volume(), None)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self):
|
||||||
|
self.mixer.get_volume.return_value.get.return_value = '12'
|
||||||
|
self.assertEqual(self.core.mixer.get_volume(), None)
|
||||||
|
|
||||||
|
|
||||||
|
class SetVolumeBadBackendTest(MockBackendCoreMixerBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self):
|
||||||
|
self.mixer.set_volume.return_value.get.side_effect = Exception
|
||||||
|
self.assertFalse(self.core.mixer.set_volume(30))
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self):
|
||||||
|
self.mixer.set_volume.return_value.get.return_value = 'done'
|
||||||
|
self.assertFalse(self.core.mixer.set_volume(30))
|
||||||
|
|
||||||
|
|
||||||
|
class GetMuteBadBackendTest(MockBackendCoreMixerBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self):
|
||||||
|
self.mixer.get_mute.return_value.get.side_effect = Exception
|
||||||
|
self.assertEqual(self.core.mixer.get_mute(), None)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self):
|
||||||
|
self.mixer.get_mute.return_value.get.return_value = '12'
|
||||||
|
self.assertEqual(self.core.mixer.get_mute(), None)
|
||||||
|
|
||||||
|
|
||||||
|
class SetMuteBadBackendTest(MockBackendCoreMixerBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self):
|
||||||
|
self.mixer.set_mute.return_value.get.side_effect = Exception
|
||||||
|
self.assertFalse(self.core.mixer.set_mute(True))
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self):
|
||||||
|
self.mixer.set_mute.return_value.get.return_value = 'done'
|
||||||
|
self.assertFalse(self.core.mixer.set_mute(True))
|
||||||
|
|||||||
@ -298,8 +298,7 @@ class DeprecatedGetPlaylistsTest(BasePlaylistsTest):
|
|||||||
self.assertEqual(len(result[1].tracks), 0)
|
self.assertEqual(len(result[1].tracks), 0)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('mopidy.core.playlists.logger')
|
class MockBackendCorePlaylistsBase(unittest.TestCase):
|
||||||
class BackendFailuresCorePlaylistsTest(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self): # noqa: N802
|
def setUp(self): # noqa: N802
|
||||||
self.playlists = mock.Mock(spec=backend.PlaylistsProvider)
|
self.playlists = mock.Mock(spec=backend.PlaylistsProvider)
|
||||||
@ -311,27 +310,127 @@ class BackendFailuresCorePlaylistsTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.core = core.Core(mixer=None, backends=[self.backend])
|
self.core = core.Core(mixer=None, backends=[self.backend])
|
||||||
|
|
||||||
def test_as_list_backend_exception_gets_ignored(self, logger):
|
|
||||||
self.playlists.as_list.get.side_effect = Exception
|
@mock.patch('mopidy.core.playlists.logger')
|
||||||
|
class AsListBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self, logger):
|
||||||
|
self.playlists.as_list.return_value.get.side_effect = Exception
|
||||||
self.assertEqual([], self.core.playlists.as_list())
|
self.assertEqual([], self.core.playlists.as_list())
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
def test_get_items_backend_exception_gets_through(self, logger):
|
def test_backend_returns_none(self, logger):
|
||||||
# TODO: is this behavior desired?
|
self.playlists.as_list.return_value.get.return_value = None
|
||||||
|
self.assertEqual([], self.core.playlists.as_list())
|
||||||
|
self.assertFalse(logger.error.called)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self, logger):
|
||||||
|
self.playlists.as_list.return_value.get.return_value = 'abc'
|
||||||
|
self.assertEqual([], self.core.playlists.as_list())
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('mopidy.core.playlists.logger')
|
||||||
|
class GetItemsBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self, logger):
|
||||||
self.playlists.get_items.return_value.get.side_effect = Exception
|
self.playlists.get_items.return_value.get.side_effect = Exception
|
||||||
with self.assertRaises(Exception):
|
self.assertIsNone(self.core.playlists.get_items('dummy:/1'))
|
||||||
self.core.playlists.get_items('dummy:/1')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
|
def test_backend_returns_none(self, logger):
|
||||||
|
self.playlists.get_items.return_value.get.return_value = None
|
||||||
|
self.assertIsNone(self.core.playlists.get_items('dummy:/1'))
|
||||||
|
self.assertFalse(logger.error.called)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self, logger):
|
||||||
|
self.playlists.get_items.return_value.get.return_value = 'abc'
|
||||||
|
self.assertIsNone(self.core.playlists.get_items('dummy:/1'))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('mopidy.core.playlists.logger')
|
||||||
|
class CreateBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self, logger):
|
||||||
|
self.playlists.create.return_value.get.side_effect = Exception
|
||||||
|
self.assertIsNone(self.core.playlists.create('foobar'))
|
||||||
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
|
def test_backend_returns_none(self, logger):
|
||||||
|
self.playlists.create.return_value.get.return_value = None
|
||||||
|
self.assertIsNone(self.core.playlists.create('foobar'))
|
||||||
|
self.assertFalse(logger.error.called)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self, logger):
|
||||||
|
self.playlists.create.return_value.get.return_value = 'abc'
|
||||||
|
self.assertIsNone(self.core.playlists.create('foobar'))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('mopidy.core.playlists.logger')
|
||||||
|
class DeleteBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self, logger):
|
||||||
|
self.playlists.delete.return_value.get.side_effect = Exception
|
||||||
|
self.assertIsNone(self.core.playlists.delete('dummy:/1'))
|
||||||
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('mopidy.core.playlists.logger')
|
||||||
|
class LookupBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self, logger):
|
||||||
|
self.playlists.lookup.return_value.get.side_effect = Exception
|
||||||
|
self.assertIsNone(self.core.playlists.lookup('dummy:/1'))
|
||||||
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
|
def test_backend_returns_none(self, logger):
|
||||||
|
self.playlists.lookup.return_value.get.return_value = None
|
||||||
|
self.assertIsNone(self.core.playlists.lookup('dummy:/1'))
|
||||||
|
self.assertFalse(logger.error.called)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self, logger):
|
||||||
|
self.playlists.lookup.return_value.get.return_value = 'abc'
|
||||||
|
self.assertIsNone(self.core.playlists.lookup('dummy:/1'))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('mopidy.core.playlists.logger')
|
||||||
|
class RefreshBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||||
|
|
||||||
@mock.patch('mopidy.core.listener.CoreListener.send')
|
@mock.patch('mopidy.core.listener.CoreListener.send')
|
||||||
def test_refresh_backend_exception_gets_ignored(self, send, logger):
|
def test_backend_raises_exception(self, send, logger):
|
||||||
self.playlists.refresh.return_value.get.side_effect = Exception
|
self.playlists.refresh.return_value.get.side_effect = Exception
|
||||||
self.core.playlists.refresh()
|
self.core.playlists.refresh()
|
||||||
self.assertFalse(send.called)
|
self.assertFalse(send.called)
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
@mock.patch('mopidy.core.listener.CoreListener.send')
|
@mock.patch('mopidy.core.listener.CoreListener.send')
|
||||||
def test_refresh_uri_backend_exception_gets_ignored(self, send, logger):
|
def test_backend_raises_exception_called_with_uri(self, send, logger):
|
||||||
self.playlists.refresh.return_value.get.side_effect = Exception
|
self.playlists.refresh.return_value.get.side_effect = Exception
|
||||||
self.core.playlists.refresh('dummy')
|
self.core.playlists.refresh('dummy')
|
||||||
self.assertFalse(send.called)
|
self.assertFalse(send.called)
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('mopidy.core.playlists.logger')
|
||||||
|
class SaveBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||||
|
|
||||||
|
def test_backend_raises_exception(self, logger):
|
||||||
|
playlist = Playlist(uri='dummy:/1')
|
||||||
|
self.playlists.save.return_value.get.side_effect = Exception
|
||||||
|
self.assertIsNone(self.core.playlists.save(playlist))
|
||||||
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
|
def test_backend_returns_none(self, logger):
|
||||||
|
playlist = Playlist(uri='dummy:/1')
|
||||||
|
self.playlists.save.return_value.get.return_value = None
|
||||||
|
self.assertIsNone(self.core.playlists.save(playlist))
|
||||||
|
self.assertFalse(logger.error.called)
|
||||||
|
|
||||||
|
def test_backend_returns_wrong_type(self, logger):
|
||||||
|
playlist = Playlist(uri='dummy:/1')
|
||||||
|
self.playlists.save.return_value.get.return_value = 'abc'
|
||||||
|
self.assertIsNone(self.core.playlists.save(playlist))
|
||||||
|
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user