core: Address review comments for do not trust backends PR

This commit is contained in:
Thomas Adamcik 2015-05-06 22:34:44 +02:00
parent 9f64a8719a
commit c01f8679bc
5 changed files with 39 additions and 40 deletions

View File

@ -14,13 +14,15 @@ logger = logging.getLogger(__name__)
@contextlib.contextmanager @contextlib.contextmanager
def _backend_error_handling(backend): def _backend_error_handling(backend, reraise=None):
try: try:
yield yield
except exceptions.ValidationError as e: except exceptions.ValidationError as e:
logger.error('%s backend returned bad data: %s', logger.error('%s backend returned bad data: %s',
backend.actor_ref.actor_class.__name__, e) backend.actor_ref.actor_class.__name__, e)
except Exception: except Exception as e:
if reraise and isinstance(e, reraise):
raise
logger.exception('%s backend caused an exception.', logger.exception('%s backend caused an exception.',
backend.actor_ref.actor_class.__name__) backend.actor_ref.actor_class.__name__)
@ -174,10 +176,8 @@ class LibraryController(object):
validation.check_instance(future.get(), collections.Mapping) 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: if uri not in uris:
name = backend.actor_ref.actor_class.__name__ raise exceptions.ValidationError(
logger.warning( 'Got unknown image URI: %s' % uri)
'%s backend returned image for URI we did not '
'ask for: %s', name, uri)
validation.check_instances(images, models.Image) validation.check_instances(images, models.Image)
results[uri] += tuple(images) results[uri] += tuple(images)
return results return results
@ -330,31 +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: # TODO: fix all these cases so we can use common helper try:
with _backend_error_handling(backend, reraise=reraise):
result = future.get() result = future.get()
if result is not None: if result is not None:
validation.check_instance(result, models.SearchResult) validation.check_instance(result, models.SearchResult)
results.append(result) results.append(result)
except exceptions.ValidationError as e:
logger.error('%s backend returned bad data: %s',
backend.actor_ref.actor_class.__name__, e)
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):

View File

@ -7,6 +7,9 @@ from mopidy import exceptions
from mopidy.utils import validation from mopidy.utils import validation
logger = logging.getLogger(__name__)
@contextlib.contextmanager @contextlib.contextmanager
def _mixer_error_handling(mixer): def _mixer_error_handling(mixer):
try: try:
@ -19,9 +22,6 @@ def _mixer_error_handling(mixer):
mixer.actor_ref.actor_class.__name__) mixer.actor_ref.actor_class.__name__)
logger = logging.getLogger(__name__)
class MixerController(object): class MixerController(object):
pykka_traversable = True pykka_traversable = True
@ -60,10 +60,11 @@ class MixerController(object):
return False return False
with _mixer_error_handling(self._mixer): with _mixer_error_handling(self._mixer):
# TODO: log non-bool return values? result = self._mixer.set_volume(volume).get()
return bool(self._mixer.set_volume(volume).get()) validation.check_instance(result, bool)
return result
return False return None
def get_mute(self): def get_mute(self):
"""Get mute state. """Get mute state.
@ -93,7 +94,8 @@ class MixerController(object):
return False return False
with _mixer_error_handling(self._mixer): with _mixer_error_handling(self._mixer):
# TODO: log non-bool return values? result = self._mixer.set_mute(bool(mute)).get()
return bool(self._mixer.set_mute(bool(mute)).get()) validation.check_instance(result, bool)
return result
return None return None

View File

@ -50,15 +50,15 @@ 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:
with _backend_error_handling(backend, NotImplementedError): with _backend_error_handling(b, reraise=NotImplementedError):
playlists = future.get() playlists = future.get()
if playlists is not None: if playlists is not None:
validation.check_instances(playlists, Ref) validation.check_instances(playlists, Ref)
results.extend(playlists) 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)
@ -110,7 +110,7 @@ class PlaylistsController(object):
""" """
deprecation.warn('core.playlists.get_playlists') deprecation.warn('core.playlists.get_playlists')
playlist_refs = self.as_list() or [] playlist_refs = self.as_list()
if include_tracks: if include_tracks:
playlists = {r.uri: self.lookup(r.uri) for r in playlist_refs} playlists = {r.uri: self.lookup(r.uri) for r in playlist_refs}
@ -145,7 +145,7 @@ 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:
backend = self.backends.with_playlists[uri_scheme] backend = self.backends.with_playlists[uri_scheme]
@ -310,7 +310,7 @@ class PlaylistsController(object):
return None return None
# TODO: we let AssertionError error through due to legacy tests :/ # TODO: we let AssertionError error through due to legacy tests :/
with _backend_error_handling(backend, AssertionError): with _backend_error_handling(backend, reraise=AssertionError):
playlist = backend.playlists.save(playlist).get() playlist = backend.playlists.save(playlist).get()
playlist is None or validation.check_instance(playlist, Playlist) playlist is None or validation.check_instance(playlist, Playlist)
if playlist: if playlist:

View File

@ -536,7 +536,7 @@ class GetImagesBadBackendTest(MockBackendCoreLibraryBase):
uri = 'dummy:/1' uri = 'dummy:/1'
self.library.get_images.return_value.get.return_value = {'foo': []} self.library.get_images.return_value.get.return_value = {'foo': []}
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri])) self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
logger.warning.assert_called_with(mock.ANY, 'DummyBackend', 'foo') logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
@mock.patch('mopidy.core.library.logger') @mock.patch('mopidy.core.library.logger')

View File

@ -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)
@ -129,7 +131,7 @@ class SetVolumeBadBackendTest(MockBackendCoreMixerBase):
def test_backend_returns_wrong_type(self): def test_backend_returns_wrong_type(self):
self.mixer.set_volume.return_value.get.return_value = 'done' self.mixer.set_volume.return_value.get.return_value = 'done'
self.assertIs(self.core.mixer.set_volume(30), True) self.assertIs(self.core.mixer.set_volume(30), None)
class GetMuteBadBackendTest(MockBackendCoreMixerBase): class GetMuteBadBackendTest(MockBackendCoreMixerBase):
@ -151,4 +153,4 @@ class SetMuteBadBackendTest(MockBackendCoreMixerBase):
def test_backend_returns_wrong_type(self): def test_backend_returns_wrong_type(self):
self.mixer.set_mute.return_value.get.return_value = 'done' self.mixer.set_mute.return_value.get.return_value = 'done'
self.assertIs(self.core.mixer.set_mute(True), True) self.assertIs(self.core.mixer.set_mute(True), None)