From 66771dec68fed31a6720cb3e348cbc26ba62ecc4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 3 Apr 2015 20:39:51 +0200 Subject: [PATCH 1/9] core: Update LibraryController to catch backend exceptions --- mopidy/core/library.py | 83 +++++++++++++++++++++++++------------- tests/core/test_library.py | 68 +++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 27 deletions(-) diff --git a/mopidy/core/library.py b/mopidy/core/library.py index c787e013..4281b865 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -5,7 +5,6 @@ import logging import operator import urlparse -import pykka from mopidy.utils import deprecation @@ -70,9 +69,16 @@ class LibraryController(object): .. versionadded:: 0.18 """ if uri is None: + directories = set() backends = self.backends.with_library_browse.values() - unique_dirs = {b.library.root_directory.get() for b in backends} - return sorted(unique_dirs, key=operator.attrgetter('name')) + futures = {b: b.library.root_directory for b in backends} + for backend, future in futures.items(): + try: + directories.add(future.get()) + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) + return sorted(directories, key=operator.attrgetter('name')) scheme = urlparse.urlparse(uri).scheme backend = self.backends.with_library_browse.get(scheme) @@ -96,11 +102,15 @@ class LibraryController(object): .. versionadded:: 1.0 """ - futures = [b.library.get_distinct(field, query) - for b in self.backends.with_library.values()] result = set() - for r in pykka.get_all(futures): - result.update(r) + futures = {b: b.library.get_distinct(field, query) + for b in self.backends.with_library.values()} + for backend, future in futures.items(): + try: + result.update(future.get()) + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) return result def get_images(self, uris): @@ -118,15 +128,19 @@ class LibraryController(object): .. versionadded:: 1.0 """ - futures = [ - backend.library.get_images(backend_uris) + futures = { + backend: backend.library.get_images(backend_uris) for (backend, backend_uris) - in self._get_backends_to_uris(uris).items() if backend_uris] + in self._get_backends_to_uris(uris).items() if backend_uris} results = {uri: tuple() for uri in uris} - for r in pykka.get_all(futures): - for uri, images in r.items(): - results[uri] += tuple(images) + for backend, future in futures.items(): + try: + for uri, images in future.get().items(): + results[uri] += tuple(images) + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) return results def find_exact(self, query=None, uris=None, **kwargs): @@ -171,19 +185,19 @@ class LibraryController(object): uris = [uri] futures = {} - result = {} - backends = self._get_backends_to_uris(uris) + result = {u: [] for u in uris} # TODO: lookup(uris) to backend APIs - for backend, backend_uris in backends.items(): - for u in backend_uris or []: - futures[u] = backend.library.lookup(u) + for backend, backend_uris in self._get_backends_to_uris(uris).items(): + for u in backend_uris: + futures[(backend, u)] = backend.library.lookup(u) - for u in uris: - if u in futures: - result[u] = futures[u].get() - else: - result[u] = [] + for (backend, u), future in futures.items(): + try: + result[u] = future.get() + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) if uri: return result[uri] @@ -199,11 +213,20 @@ class LibraryController(object): if uri is not None: backend = self._get_backend(uri) if backend: - backend.library.refresh(uri).get() + try: + backend.library.refresh(uri).get() + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) else: - futures = [b.library.refresh(uri) - for b in self.backends.with_library.values()] - pykka.get_all(futures) + futures = {b: b.library.refresh(uri) + for b in self.backends.with_library.values()} + for backend, future in futures.items(): + try: + 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): """ @@ -273,6 +296,12 @@ class LibraryController(object): logger.warning( '%s does not implement library.search() with "exact" ' 'support. Please upgrade it.', backend_name) + except LookupError: + raise + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) + return [r for r in results if r] diff --git a/tests/core/test_library.py b/tests/core/test_library.py index 8d2195a2..ba6b859e 100644 --- a/tests/core/test_library.py +++ b/tests/core/test_library.py @@ -419,3 +419,71 @@ class LegacyFindExactToSearchLibraryTest(unittest.TestCase): self.backend.library.search.return_value.get.side_effect = TypeError self.core.library.search(query={'any': ['a']}, exact=True) # We are just testing that this doesn't fail. + + +@mock.patch('mopidy.core.library.logger') +class BackendFailuresCoreLibraryTest(unittest.TestCase): + + def setUp(self): # noqa: N802 + dummy_root = Ref.directory(uri='dummy:directory', name='dummy') + + self.library = mock.Mock(spec=backend.LibraryProvider) + self.library.root_directory.get.return_value = dummy_root + + self.backend = mock.Mock() + self.backend.actor_ref.actor_class.__name__ = 'DummyBackend' + self.backend.uri_schemes.get.return_value = ['dummy'] + self.backend.library = self.library + + self.core = core.Core(mixer=None, backends=[self.backend]) + + def test_browse_backend_get_root_exception_gets_ignored(self, logger): + # Might happen if root_directory is a property for some weird reason. + self.library.root_directory.get.side_effect = Exception + self.assertEqual([], self.core.library.browse(None)) + logger.exception.assert_called_with(mock.ANY, 'DummyBackend') + + def test_browse_backend_browse_uri_exception_gets_through(self, logger): + # TODO: is this behavior desired? + self.library.browse.return_value.get.side_effect = Exception + with self.assertRaises(Exception): + self.core.library.browse('dummy:directory') + + def test_get_distinct_backend_exception_gets_ignored(self, logger): + self.library.get_distinct.return_value.get.side_effect = Exception + self.assertEqual(set(), self.core.library.get_distinct('artist')) + logger.exception.assert_called_with(mock.ANY, 'DummyBackend') + + def test_get_images_backend_exception_get_ignored(self, logger): + self.library.get_images.return_value.get.side_effect = Exception + self.assertEqual( + {'dummy:/1': tuple()}, self.core.library.get_images(['dummy:/1'])) + logger.exception.assert_called_with(mock.ANY, 'DummyBackend') + + def test_lookup_backend_exceptiosn_gets_ignores(self, logger): + self.library.lookup.return_value.get.side_effect = Exception + self.assertEqual( + {'dummy:/1': []}, self.core.library.lookup(uris=['dummy:/1'])) + logger.exception.assert_called_with(mock.ANY, 'DummyBackend') + + def test_refresh_backend_exception_gets_ignored(self, logger): + self.library.refresh.return_value.get.side_effect = Exception + self.core.library.refresh() + logger.exception.assert_called_with(mock.ANY, 'DummyBackend') + + def test_refresh_uri_backend_exception_gets_ignored(self, logger): + self.library.refresh.return_value.get.side_effect = Exception + self.core.library.refresh('dummy:/1') + logger.exception.assert_called_with(mock.ANY, 'DummyBackend') + + def test_search_backend_exception_gets_ignored(self, logger): + self.library.search.return_value.get.side_effect = Exception + self.assertEqual([], self.core.library.search(query={'any': ['foo']})) + logger.exception.assert_called_with(mock.ANY, 'DummyBackend') + + def test_search_backend_lookup_error_gets_through(self, logger): + # TODO: is this behavior desired? Do we need to continue handling + # LookupError case specially. + self.library.search.return_value.get.side_effect = LookupError + with self.assertRaises(LookupError): + self.core.library.search(query={'any': ['foo']}) From 50f68064be1eb6f174d1ba7fb76cde3f8bb39be4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 3 Apr 2015 20:40:30 +0200 Subject: [PATCH 2/9] core: Update PlaylistsController to catch backend exceptions --- mopidy/core/playlists.py | 38 +++++++++++++++++++++++++---------- tests/core/test_playlists.py | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/mopidy/core/playlists.py b/mopidy/core/playlists.py index 2c997d84..500713a3 100644 --- a/mopidy/core/playlists.py +++ b/mopidy/core/playlists.py @@ -3,8 +3,6 @@ from __future__ import absolute_import, unicode_literals import logging import urlparse -import pykka - from mopidy.core import listener from mopidy.models import Playlist from mopidy.utils import deprecation @@ -32,17 +30,21 @@ class PlaylistsController(object): .. versionadded:: 1.0 """ futures = { - b.actor_ref.actor_class.__name__: b.playlists.as_list() - for b in set(self.backends.with_playlists.values())} + backend: backend.playlists.as_list() + for backend in set(self.backends.with_playlists.values())} results = [] - for backend_name, future in futures.items(): + for backend, future in futures.items(): try: results.extend(future.get()) except NotImplementedError: + backend_name = backend.actor_ref.actor_class.__name__ logger.warning( '%s does not implement playlists.as_list(). ' 'Please upgrade it.', backend_name) + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) return results @@ -191,6 +193,8 @@ class PlaylistsController(object): else: return None + # TODO: there is an inconsistency between library.refresh(uri) and this + # call, not sure how to sort this out. def refresh(self, uri_scheme=None): """ Refresh the playlists in :attr:`playlists`. @@ -204,15 +208,27 @@ class PlaylistsController(object): :type uri_scheme: string """ if uri_scheme is None: - futures = [b.playlists.refresh() - for b in self.backends.with_playlists.values()] - pykka.get_all(futures) - listener.CoreListener.send('playlists_loaded') + futures = {b: b.playlists.refresh() + for b in self.backends.with_playlists.values()} + playlists_loaded = False + for backend, future in futures.items(): + try: + future.get() + playlists_loaded = True + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) + if playlists_loaded: + listener.CoreListener.send('playlists_loaded') else: backend = self.backends.with_playlists.get(uri_scheme, None) if backend: - backend.playlists.refresh().get() - listener.CoreListener.send('playlists_loaded') + try: + backend.playlists.refresh().get() + listener.CoreListener.send('playlists_loaded') + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) def save(self, playlist): """ diff --git a/tests/core/test_playlists.py b/tests/core/test_playlists.py index 4ca3d6df..1ccc1815 100644 --- a/tests/core/test_playlists.py +++ b/tests/core/test_playlists.py @@ -279,3 +279,42 @@ class DeprecatedGetPlaylistsTest(BasePlaylistsTest): self.assertEqual(len(result[0].tracks), 0) self.assertEqual(result[1].name, 'B') self.assertEqual(len(result[1].tracks), 0) + + +@mock.patch('mopidy.core.playlists.logger') +class BackendFailuresCorePlaylistsTest(unittest.TestCase): + + def setUp(self): # noqa: N802 + self.playlists = mock.Mock(spec=backend.PlaylistsProvider) + + self.backend = mock.Mock() + self.backend.actor_ref.actor_class.__name__ = 'DummyBackend' + self.backend.uri_schemes.get.return_value = ['dummy'] + self.backend.playlists = self.playlists + + 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 + self.assertEqual([], self.core.playlists.as_list()) + logger.exception.assert_called_with(mock.ANY, 'DummyBackend') + + def test_get_items_backend_exception_gets_through(self, logger): + # TODO: is this behavior desired? + self.playlists.get_items.return_value.get.side_effect = Exception + with self.assertRaises(Exception): + self.core.playlists.get_items('dummy:/1') + + @mock.patch('mopidy.core.listener.CoreListener.send') + def test_refresh_backend_exception_gets_ignored(self, send, logger): + self.playlists.refresh.return_value.get.side_effect = Exception + self.core.playlists.refresh() + self.assertFalse(send.called) + logger.exception.assert_called_with(mock.ANY, 'DummyBackend') + + @mock.patch('mopidy.core.listener.CoreListener.send') + def test_refresh_uri_backend_exception_gets_ignored(self, send, logger): + self.playlists.refresh.return_value.get.side_effect = Exception + self.core.playlists.refresh('dummy') + self.assertFalse(send.called) + logger.exception.assert_called_with(mock.ANY, 'DummyBackend') From 34a88792f24d5be625e2bc72761aa19ca68982ca Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 3 Apr 2015 21:28:09 +0200 Subject: [PATCH 3/9] core: Create a unified code path for refresh calls --- mopidy/core/library.py | 34 +++++++++++++++---------------- mopidy/core/playlists.py | 43 ++++++++++++++++++++-------------------- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 4281b865..324786ad 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -210,23 +210,23 @@ class LibraryController(object): :param uri: directory or track URI :type uri: string """ - if uri is not None: - backend = self._get_backend(uri) - if backend: - try: - backend.library.refresh(uri).get() - except Exception: - logger.exception('%s backend caused an exception.', - backend.actor_ref.actor_class.__name__) - else: - futures = {b: b.library.refresh(uri) - for b in self.backends.with_library.values()} - for backend, future in futures.items(): - try: - future.get() - except Exception: - logger.exception('%s backend caused an exception.', - backend.actor_ref.actor_class.__name__) + futures = {} + backends = {} + uri_scheme = urlparse.urlparse(uri).scheme if uri else None + + for backend_scheme, backend in self.backends.with_playlists.items(): + backends.setdefault(backend, set()).add(backend_scheme) + + for backend, backend_schemes in backends.items(): + if uri_scheme is None or uri_scheme in backend_schemes: + futures[backend] = backend.library.refresh(uri) + + for backend, future in futures.items(): + try: + 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): """ diff --git a/mopidy/core/playlists.py b/mopidy/core/playlists.py index 500713a3..62001517 100644 --- a/mopidy/core/playlists.py +++ b/mopidy/core/playlists.py @@ -207,28 +207,27 @@ class PlaylistsController(object): :param uri_scheme: limit to the backend matching the URI scheme :type uri_scheme: string """ - if uri_scheme is None: - futures = {b: b.playlists.refresh() - for b in self.backends.with_playlists.values()} - playlists_loaded = False - for backend, future in futures.items(): - try: - future.get() - playlists_loaded = True - except Exception: - logger.exception('%s backend caused an exception.', - backend.actor_ref.actor_class.__name__) - if playlists_loaded: - listener.CoreListener.send('playlists_loaded') - else: - backend = self.backends.with_playlists.get(uri_scheme, None) - if backend: - try: - backend.playlists.refresh().get() - listener.CoreListener.send('playlists_loaded') - except Exception: - logger.exception('%s backend caused an exception.', - backend.actor_ref.actor_class.__name__) + futures = {} + backends = {} + playlists_loaded = False + + for backend_scheme, backend in self.backends.with_playlists.items(): + backends.setdefault(backend, set()).add(backend_scheme) + + for backend, backend_schemes in backends.items(): + if uri_scheme is None or uri_scheme in backend_schemes: + futures[backend] = backend.playlists.refresh() + + for backend, future in futures.items(): + try: + future.get() + playlists_loaded = True + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) + + if playlists_loaded: + listener.CoreListener.send('playlists_loaded') def save(self, playlist): """ From 5fdd5d08989a35a2284d960940fb0a284a5cc353 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 3 Apr 2015 21:36:46 +0200 Subject: [PATCH 4/9] docs: Add core changes to changelog --- docs/changelog.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 605a30fe..d161400e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,14 @@ Changelog This changelog is used to track all major changes to Mopidy. +v1.0.1 (unreleased) +=================== + +Core +---- + +- Update core controllers to handle backend exceptions in all calls that rely + on multiple backends. (Issue: :issue:`667`) v1.1.0 (UNRELEASED) =================== From 56eb08ea7e187b9ecdc0df4efd5dc1f9b7d0b09b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 6 Apr 2015 23:30:19 +0200 Subject: [PATCH 5/9] docs: Update changelog after rebase --- docs/changelog.rst | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d161400e..4595587d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,14 +4,6 @@ Changelog This changelog is used to track all major changes to Mopidy. -v1.0.1 (unreleased) -=================== - -Core ----- - -- Update core controllers to handle backend exceptions in all calls that rely - on multiple backends. (Issue: :issue:`667`) v1.1.0 (UNRELEASED) =================== @@ -22,6 +14,9 @@ Core API - Calling :meth:`mopidy.core.library.LibraryController.search`` with ``kwargs`` as the query is no longer supported (PR: :issue:`1090`) +- Update core controllers to handle backend exceptions in all calls that rely + on multiple backends. (Issue: :issue:`667`) + Internal changes ---------------- From 928b8df08c4445034f995aca2b0cb8210d399104 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 8 Apr 2015 21:10:21 +0200 Subject: [PATCH 6/9] core: Explain why we let LookupError through for search --- mopidy/core/library.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 35a43501..b226a378 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -297,6 +297,9 @@ class LibraryController(object): '%s does not implement library.search() with "exact" ' '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.', From 511cf4e32618e2db83cc23627b762664ec7de72f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 8 Apr 2015 21:12:07 +0200 Subject: [PATCH 7/9] core: Catch exceptions when browsing in backends Also splits browse into to method to better distinguish the two possible code paths. --- mopidy/core/library.py | 35 +++++++++++++++++++++-------------- tests/core/test_library.py | 5 ++--- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/mopidy/core/library.py b/mopidy/core/library.py index b226a378..35ed02c1 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -68,23 +68,30 @@ class LibraryController(object): .. versionadded:: 0.18 """ - if uri is None: - directories = set() - backends = self.backends.with_library_browse.values() - futures = {b: b.library.root_directory for b in backends} - for backend, future in futures.items(): - try: - directories.add(future.get()) - except Exception: - logger.exception('%s backend caused an exception.', - backend.actor_ref.actor_class.__name__) - return sorted(directories, key=operator.attrgetter('name')) + return self._roots() if uri is None else self._browse(uri) + def _roots(self): + directories = set() + backends = self.backends.with_library_browse.values() + futures = {b: b.library.root_directory for b in backends} + for backend, future in futures.items(): + try: + directories.add(future.get()) + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) + return sorted(directories, key=operator.attrgetter('name')) + + def _browse(self, uri): scheme = urlparse.urlparse(uri).scheme backend = self.backends.with_library_browse.get(scheme) - if not backend: - return [] - return backend.library.browse(uri).get() + try: + if backend: + return backend.library.browse(uri).get() # TODO: sort? + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) + return [] def get_distinct(self, field, query=None): """ diff --git a/tests/core/test_library.py b/tests/core/test_library.py index ba6b859e..6cbb00b3 100644 --- a/tests/core/test_library.py +++ b/tests/core/test_library.py @@ -444,10 +444,9 @@ class BackendFailuresCoreLibraryTest(unittest.TestCase): logger.exception.assert_called_with(mock.ANY, 'DummyBackend') def test_browse_backend_browse_uri_exception_gets_through(self, logger): - # TODO: is this behavior desired? self.library.browse.return_value.get.side_effect = Exception - with self.assertRaises(Exception): - self.core.library.browse('dummy:directory') + self.assertEqual([], self.core.library.browse('dummy:directory')) + logger.exception.assert_called_with(mock.ANY, 'DummyBackend') def test_get_distinct_backend_exception_gets_ignored(self, logger): self.library.get_distinct.return_value.get.side_effect = Exception From e5f59495fcf89285df5997a435d0bf20657ecbbe Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 8 Apr 2015 21:18:18 +0200 Subject: [PATCH 8/9] core: Update refresh test case to fail on multiple calls to same backend --- tests/core/test_library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/test_library.py b/tests/core/test_library.py index 6cbb00b3..9cb2588d 100644 --- a/tests/core/test_library.py +++ b/tests/core/test_library.py @@ -190,8 +190,8 @@ class CoreLibraryTest(BaseCoreLibraryTest): def test_refresh_without_uri_calls_all_backends(self): self.core.library.refresh() - self.library1.refresh.assert_called_once_with(None) - self.library2.refresh.assert_called_twice_with(None) + self.library1.refresh.return_value.get.assert_called_once_with() + self.library2.refresh.return_value.get.assert_called_once_with() def test_search_combines_results_from_all_backends(self): track1 = Track(uri='dummy1:a') From 2cc91c0a7fc95349012d9927a1e3918ffe453491 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 8 Apr 2015 23:13:07 +0200 Subject: [PATCH 9/9] core: Fix review comments for PR#1111 --- mopidy/core/library.py | 6 ++++-- tests/core/test_library.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 35ed02c1..6fc1ce38 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -68,7 +68,9 @@ class LibraryController(object): .. versionadded:: 0.18 """ - return self._roots() if uri is None else self._browse(uri) + if uri is None: + return self._roots() + return self._browse(uri) def _roots(self): directories = set() @@ -87,7 +89,7 @@ class LibraryController(object): backend = self.backends.with_library_browse.get(scheme) try: if backend: - return backend.library.browse(uri).get() # TODO: sort? + return backend.library.browse(uri).get() except Exception: logger.exception('%s backend caused an exception.', backend.actor_ref.actor_class.__name__) diff --git a/tests/core/test_library.py b/tests/core/test_library.py index 9cb2588d..89f3b284 100644 --- a/tests/core/test_library.py +++ b/tests/core/test_library.py @@ -443,7 +443,7 @@ class BackendFailuresCoreLibraryTest(unittest.TestCase): self.assertEqual([], self.core.library.browse(None)) logger.exception.assert_called_with(mock.ANY, 'DummyBackend') - def test_browse_backend_browse_uri_exception_gets_through(self, logger): + def test_browse_backend_browse_uri_exception_gets_ignored(self, logger): self.library.browse.return_value.get.side_effect = Exception self.assertEqual([], self.core.library.browse('dummy:directory')) logger.exception.assert_called_with(mock.ANY, 'DummyBackend')