diff --git a/docs/changelog.rst b/docs/changelog.rst index 5abee98c..1f950bc8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -59,6 +59,18 @@ Cleanups - Removed warning if :file:`~/.config/mopidy/settings.py` exists. We stopped using this settings file in 0.14, released in April 2013. +Gapless +------- + +- Add partial support for gapless playback. Gapless now works as long as you + don't change tracks or use next/previous. (PR: :issue:`1288`) + +- Core playback has been refactored to better handle gapless, and async state + changes. + +- Tests have been updated to always use a core actor so async state changes + don't trip us up. + v1.1.1 (2015-09-14) =================== diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 60e88a9d..b8b3d9a4 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals import logging import os +import threading import gobject @@ -406,6 +407,7 @@ class Audio(pykka.ThreadingActor): self.mixer = SoftwareMixer(mixer) def on_start(self): + self._thread = threading.current_thread() try: self._setup_preferences() self._setup_playbin() @@ -499,6 +501,11 @@ class Audio(pykka.ThreadingActor): self.mixer.teardown() def _on_about_to_finish(self, element): + if self._thread == threading.current_thread(): + logger.error( + 'about-to-finish in actor, aborting to avoid deadlock.') + return + gst_logger.debug('Got about-to-finish event.') if self._about_to_finish_callback: logger.debug('Running about to finish callback.') diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index 6d10a593..e365e4b7 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -54,7 +54,8 @@ class Core( self.library = LibraryController(backends=self.backends, core=self) self.history = HistoryController() self.mixer = MixerController(mixer=mixer) - self.playback = PlaybackController(backends=self.backends, core=self) + self.playback = PlaybackController( + audio=audio, backends=self.backends, core=self) self.playlists = PlaylistsController(backends=self.backends, core=self) self.tracklist = TracklistController(core=self) @@ -84,7 +85,7 @@ class Core( """ def reached_end_of_stream(self): - self.playback._on_end_of_track() + self.playback._on_end_of_stream() def stream_changed(self, uri): self.playback._on_stream_changed(uri) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 389e780f..79e0adb2 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -14,21 +14,26 @@ logger = logging.getLogger(__name__) class PlaybackController(object): pykka_traversable = True - def __init__(self, backends, core): + def __init__(self, audio, backends, core): + # TODO: these should be internal self.backends = backends self.core = core + self._audio = audio - self._current_tl_track = None self._stream_title = None self._state = PlaybackState.STOPPED - def _get_backend(self): - # TODO: take in track instead - track = self.get_current_track() - if track is None: + self._current_tl_track = None + self._pending_tl_track = None + + if self._audio: + self._audio.set_about_to_finish_callback( + self._on_about_to_finish_callback) + + def _get_backend(self, tl_track): + if tl_track is None: return None - uri = track.uri - uri_scheme = urlparse.urlparse(uri).scheme + uri_scheme = urlparse.urlparse(tl_track.track.uri).scheme return self.backends.with_playback.get(uri_scheme, None) # Properties @@ -122,7 +127,7 @@ class PlaybackController(object): def get_time_position(self): """Get time position in milliseconds.""" - backend = self._get_backend() + backend = self._get_backend(self.get_current_tl_track()) if backend: return backend.playback.get_time_position().get() else: @@ -190,49 +195,45 @@ class PlaybackController(object): # Methods - # TODO: remove this. - def _change_track(self, tl_track, on_error_step=1): - """ - Change to the given track, keeping the current playback state. + def _on_end_of_stream(self): + self.set_state(PlaybackState.STOPPED) + self._set_current_tl_track(None) + # TODO: self._trigger_track_playback_ended? - :param tl_track: track to change to - :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` - :param on_error_step: direction to step at play error, 1 for next - track (default), -1 for previous track. **INTERNAL** - :type on_error_step: int, -1 or 1 - """ - old_state = self.get_state() - self.stop() - self._set_current_tl_track(tl_track) - if old_state == PlaybackState.PLAYING: - self._play(on_error_step=on_error_step) - elif old_state == PlaybackState.PAUSED: - # NOTE: this is just a quick hack to fix #1177 as this code has - # already been killed in the gapless branch. - backend = self._get_backend() - if backend: - backend.playback.prepare_change() - backend.playback.change_track(tl_track.track).get() - self.pause() + def _on_stream_changed(self, uri): + self._stream_title = None + if self._pending_tl_track: + self._set_current_tl_track(self._pending_tl_track) + self._pending_tl_track = None + self._trigger_track_playback_started() - # TODO: this is not really end of track, this is on_need_next_track - def _on_end_of_track(self): - """ - Tell the playback controller that end of track is reached. + def _on_about_to_finish_callback(self): + """Callback that performs a blocking actor call to the real callback. - Used by event handler in :class:`mopidy.core.Core`. + This is passed to audio, which is allowed to call this code from the + audio thread. We pass execution into the core actor to ensure that + there is no unsafe access of state in core. This must block until + we get a response. """ - if self.get_state() == PlaybackState.STOPPED: - return + self.core.actor_ref.ask({ + 'command': 'pykka_call', 'args': tuple(), 'kwargs': {}, + 'attr_path': ('playback', '_on_about_to_finish'), + }) + def _on_about_to_finish(self): + self._trigger_track_playback_ended(self.get_time_position()) + + # TODO: check that we always have a current track original_tl_track = self.get_current_tl_track() next_tl_track = self.core.tracklist.eot_track(original_tl_track) - if next_tl_track: - self._change_track(next_tl_track) - else: - self.stop() - self._set_current_tl_track(None) + # TODO: only set pending if we have a backend that can play it? + # TODO: skip tracks that don't have a backend? + self._pending_tl_track = next_tl_track + backend = self._get_backend(next_tl_track) + + if backend: + backend.playback.change_track(next_tl_track.track).get() self.core.tracklist._mark_played(original_tl_track) @@ -242,13 +243,11 @@ class PlaybackController(object): Used by :class:`mopidy.core.TracklistController`. """ - tracklist = self.core.tracklist.get_tl_tracks() - if self.get_current_tl_track() not in tracklist: + if not self.core.tracklist.tl_tracks: self.stop() self._set_current_tl_track(None) - - def _on_stream_changed(self, uri): - self._stream_title = None + elif self.get_current_tl_track() not in self.core.tracklist.tl_tracks: + self._set_current_tl_track(None) def next(self): """ @@ -257,23 +256,29 @@ class PlaybackController(object): The current playback state will be kept. If it was playing, playing will continue. If it was paused, it will still be paused, etc. """ - original_tl_track = self.get_current_tl_track() - next_tl_track = self.core.tracklist.next_track(original_tl_track) + state = self.get_state() + current = self._pending_tl_track or self._current_tl_track - if next_tl_track: - # TODO: switch to: - # backend.play(track) - # wait for state change? - self._change_track(next_tl_track) - else: - self.stop() - self._set_current_tl_track(None) + # TODO: move to pending track? + self._trigger_track_playback_ended(self.get_time_position()) + self.core.tracklist._mark_played(self._current_tl_track) - self.core.tracklist._mark_played(original_tl_track) + while current: + pending = self.core.tracklist.next_track(current) + if self._change(pending, state): + break + else: + self.core.tracklist._mark_unplayable(pending) + # TODO: this could be needed to prevent a loop in rare cases + # if current == pending: + # break + current = pending + + # TODO return result? def pause(self): """Pause playback.""" - backend = self._get_backend() + backend = self._get_backend(self.get_current_tl_track()) if not backend or backend.playback.pause().get(): # TODO: switch to: # backend.track(pause) @@ -302,9 +307,6 @@ class PlaybackController(object): if tl_track: deprecation.warn('core.playback.play:tl_track_kwarg', pending=True) - self._play(tl_track=tl_track, tlid=tlid, on_error_step=1) - - def _play(self, tl_track=None, tlid=None, on_error_step=1): if tl_track is None and tlid is not None: for tl_track in self.core.tracklist.get_tl_tracks(): if tl_track.tlid == tlid: @@ -312,60 +314,71 @@ class PlaybackController(object): else: tl_track = None - if tl_track is None: - if self.get_state() == PlaybackState.PAUSED: - return self.resume() + if tl_track is not None: + # TODO: allow from outside tracklist, would make sense given refs? + assert tl_track in self.core.tracklist.get_tl_tracks() + elif tl_track is None and self.get_state() == PlaybackState.PAUSED: + self.resume() + return - if self.get_current_tl_track() is not None: - tl_track = self.get_current_tl_track() + original = self._current_tl_track + current = self._pending_tl_track or self._current_tl_track + pending = tl_track or current or self.core.tracklist.next_track(None) + + if original != pending and self.get_state() != PlaybackState.STOPPED: + self._trigger_track_playback_ended(self.get_time_position()) + + if pending: + # TODO: remove? + self.set_state(PlaybackState.PLAYING) + + while pending: + # TODO: should we consume unplayable tracks in this loop? + if self._change(pending, PlaybackState.PLAYING): + break else: - if on_error_step == 1: - tl_track = self.core.tracklist.next_track(tl_track) - elif on_error_step == -1: - tl_track = self.core.tracklist.previous_track(tl_track) + self.core.tracklist._mark_unplayable(pending) + current = pending + pending = self.core.tracklist.next_track(current) - if tl_track is None: - return + # TODO: move to top and get rid of original? + self.core.tracklist._mark_played(original) + # TODO return result? - assert tl_track in self.core.tracklist.get_tl_tracks() + def _change(self, pending_tl_track, state): + self._pending_tl_track = pending_tl_track - # TODO: switch to: - # backend.play(track) - # wait for state change? - - if self.get_state() == PlaybackState.PLAYING: + if not pending_tl_track: self.stop() + self._on_end_of_stream() # pretend an EOS happened for cleanup + return True - self._set_current_tl_track(tl_track) - self.set_state(PlaybackState.PLAYING) - backend = self._get_backend() - success = False + backend = self._get_backend(pending_tl_track) + if not backend: + return False - if backend: - backend.playback.prepare_change() + backend.playback.prepare_change() + if not backend.playback.change_track(pending_tl_track.track).get(): + return False # TODO: test for this path + + if state == PlaybackState.PLAYING: try: - success = ( - backend.playback.change_track(tl_track.track).get() and - backend.playback.play().get()) + return backend.playback.play().get() except TypeError: - logger.error( - '%s needs to be updated to work with this ' - 'version of Mopidy.', - backend.actor_ref.actor_class.__name__) - logger.debug('Backend exception', exc_info=True) + # TODO: check by binding against underlying play method using + # inspect and otherwise re-raise? + logger.error('%s needs to be updated to work with this ' + 'version of Mopidy.', backend) + return False + elif state == PlaybackState.PAUSED: + return backend.playback.pause().get() + elif state == PlaybackState.STOPPED: + # TODO: emit some event now? + self._current_tl_track = self._pending_tl_track + self._pending_tl_track = None + return True - if success: - self.core.tracklist._mark_playing(tl_track) - self.core.history._add_track(tl_track.track) - # TODO: replace with stream-changed - self._trigger_track_playback_started() - else: - self.core.tracklist._mark_unplayable(tl_track) - if on_error_step == 1: - # TODO: can cause an endless loop for single track repeat. - self.next() - elif on_error_step == -1: - self.previous() + raise Exception('Unknown state: %s' % state) def previous(self): """ @@ -374,18 +387,29 @@ class PlaybackController(object): The current playback state will be kept. If it was playing, playing will continue. If it was paused, it will still be paused, etc. """ - tl_track = self.get_current_tl_track() - # TODO: switch to: - # self.play(....) - # wait for state change? - self._change_track( - self.core.tracklist.previous_track(tl_track), on_error_step=-1) + self._trigger_track_playback_ended(self.get_time_position()) + + state = self.get_state() + current = self._pending_tl_track or self._current_tl_track + + while current: + pending = self.core.tracklist.previous_track(current) + if self._change(pending, state): + break + else: + self.core.tracklist._mark_unplayable(pending) + # TODO: this could be needed to prevent a loop in rare cases + # if current == pending: + # break + current = pending + + # TODO: no return value? def resume(self): """If paused, resume playing the current track.""" if self.get_state() != PlaybackState.PAUSED: return - backend = self._get_backend() + backend = self._get_backend(self.get_current_tl_track()) if backend and backend.playback.resume().get(): self.set_state(PlaybackState.PLAYING) # TODO: trigger via gst messages @@ -402,6 +426,7 @@ class PlaybackController(object): :type time_position: int :rtype: :class:`True` if successful, else :class:`False` """ + # TODO: seek needs to take pending tracks into account :( validation.check_integer(time_position) if time_position < 0: @@ -412,19 +437,28 @@ class PlaybackController(object): if not self.core.tracklist.tracks: return False - if self.current_track and self.current_track.length is None: - return False - if self.get_state() == PlaybackState.STOPPED: self.play() + # TODO: uncomment once we have tests for this. Should fix seek after + # about to finish doing wrong track. + # if self._current_tl_track and self._pending_tl_track: + # self.play(self._current_tl_track) + + # We need to prefer the still playing track, but if nothing is playing + # we fall back to the pending one. + tl_track = self._current_tl_track or self._pending_tl_track + if tl_track and tl_track.track.length is None: + return False + if time_position < 0: time_position = 0 - elif time_position > self.current_track.length: + elif time_position > tl_track.track.length: + # TODO: gstreamer will trigger a about to finish for us, use that? self.next() return True - backend = self._get_backend() + backend = self._get_backend(self.get_current_tl_track()) if not backend: return False @@ -436,7 +470,7 @@ class PlaybackController(object): def stop(self): """Stop playing.""" if self.get_state() != PlaybackState.STOPPED: - backend = self._get_backend() + backend = self._get_backend(self.get_current_tl_track()) time_position_before_stop = self.get_time_position() if not backend or backend.playback.stop().get(): self.set_state(PlaybackState.STOPPED) @@ -461,12 +495,15 @@ class PlaybackController(object): time_position=self.get_time_position()) def _trigger_track_playback_started(self): + # TODO: replace with stream-changed logger.debug('Triggering track playback started event') if self.get_current_tl_track() is None: return - listener.CoreListener.send( - 'track_playback_started', - tl_track=self.get_current_tl_track()) + + tl_track = self.get_current_tl_track() + self.core.tracklist._mark_playing(tl_track) + self.core.history._add_track(tl_track.track) + listener.CoreListener.send('track_playback_started', tl_track=tl_track) def _trigger_track_playback_ended(self, time_position_before_stop): logger.debug('Triggering track playback ended event') diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index dbe0c150..02508c97 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -318,10 +318,11 @@ class TracklistController(object): return self._shuffled[0] return None - if tl_track is None: + next_index = self.index(tl_track) + if next_index is None: next_index = 0 else: - next_index = self.index(tl_track) + 1 + next_index += 1 if self.get_repeat(): next_index %= len(self._tl_tracks) diff --git a/mopidy/mpd/protocol/playback.py b/mopidy/mpd/protocol/playback.py index 333e1ccb..9124d99a 100644 --- a/mopidy/mpd/protocol/playback.py +++ b/mopidy/mpd/protocol/playback.py @@ -137,13 +137,13 @@ def pause(context, state=None): playback_state = context.core.playback.get_state().get() if (playback_state == PlaybackState.PLAYING): - context.core.playback.pause() + context.core.playback.pause().get() elif (playback_state == PlaybackState.PAUSED): - context.core.playback.resume() + context.core.playback.resume().get() elif state: - context.core.playback.pause() + context.core.playback.pause().get() else: - context.core.playback.resume() + context.core.playback.resume().get() @protocol.commands.add('play', songpos=protocol.INT) diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 046971a8..314d1d42 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -59,7 +59,7 @@ class BaseTest(unittest.TestCase): def tearDown(self): # noqa pykka.ActorRegistry.stop_all() - def possibly_trigger_fake_playback_error(self): + def possibly_trigger_fake_playback_error(self, uri): pass def possibly_trigger_fake_about_to_finish(self): @@ -69,8 +69,8 @@ class BaseTest(unittest.TestCase): class DummyMixin(object): audio_class = dummy_audio.DummyAudio - def possibly_trigger_fake_playback_error(self): - self.audio.trigger_fake_playback_failure() + def possibly_trigger_fake_playback_error(self, uri): + self.audio.trigger_fake_playback_failure(uri) def possibly_trigger_fake_about_to_finish(self): callback = self.audio.get_about_to_finish_callback().get() @@ -86,7 +86,7 @@ class AudioTest(BaseTest): self.assertTrue(self.audio.start_playback().get()) def test_start_playback_non_existing_file(self): - self.possibly_trigger_fake_playback_error() + self.possibly_trigger_fake_playback_error(self.uris[0] + 'bogus') self.audio.prepare_change() self.audio.set_uri(self.uris[0] + 'bogus') @@ -133,186 +133,253 @@ class AudioDummyTest(DummyMixin, AudioTest): pass -@mock.patch.object(audio.AudioListener, 'send') +class DummyAudioListener(pykka.ThreadingActor, audio.AudioListener): + def __init__(self): + super(DummyAudioListener, self).__init__() + self.events = [] + self.waiters = {} + + def on_event(self, event, **kwargs): + self.events.append((event, kwargs)) + if event in self.waiters: + self.waiters[event].set() + + def wait(self, event): + self.waiters[event] = threading.Event() + return self.waiters[event] + + def get_events(self): + return self.events + + def clear_events(self): + self.events = [] + + class AudioEventTest(BaseTest): def setUp(self): # noqa: N802 super(AudioEventTest, self).setUp() self.audio.enable_sync_handler().get() + self.listener = DummyAudioListener.start().proxy() + + def tearDown(self): # noqa: N802 + super(AudioEventTest, self).setUp() + + def assertEvent(self, event, **kwargs): # noqa: N802 + self.assertIn((event, kwargs), self.listener.get_events().get()) + + def assertNotEvent(self, event, **kwargs): # noqa: N802 + self.assertNotIn((event, kwargs), self.listener.get_events().get()) # TODO: test without uri set, with bad uri and gapless... # TODO: playing->playing triggered by seek should be removed # TODO: codify expected state after EOS # TODO: consider returning a future or a threading event? - def test_state_change_stopped_to_playing_event(self, send_mock): + def test_state_change_stopped_to_playing_event(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.start_playback() self.audio.wait_for_state_change().get() - call = mock.call('state_changed', old_state=PlaybackState.STOPPED, + self.assertEvent('state_changed', old_state=PlaybackState.STOPPED, new_state=PlaybackState.PLAYING, target_state=None) - self.assertIn(call, send_mock.call_args_list) - def test_state_change_stopped_to_paused_event(self, send_mock): + def test_state_change_stopped_to_paused_event(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.pause_playback() self.audio.wait_for_state_change().get() - call = mock.call('state_changed', old_state=PlaybackState.STOPPED, + self.assertEvent('state_changed', old_state=PlaybackState.STOPPED, new_state=PlaybackState.PAUSED, target_state=None) - self.assertIn(call, send_mock.call_args_list) - def test_state_change_paused_to_playing_event(self, send_mock): + def test_state_change_paused_to_playing_event(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.pause_playback() + self.audio.wait_for_state_change() + self.listener.clear_events() self.audio.start_playback() self.audio.wait_for_state_change().get() - call = mock.call('state_changed', old_state=PlaybackState.PAUSED, + self.assertEvent('state_changed', old_state=PlaybackState.PAUSED, new_state=PlaybackState.PLAYING, target_state=None) - self.assertIn(call, send_mock.call_args_list) - def test_state_change_paused_to_stopped_event(self, send_mock): + def test_state_change_paused_to_stopped_event(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.pause_playback() + self.audio.wait_for_state_change() + self.listener.clear_events() self.audio.stop_playback() self.audio.wait_for_state_change().get() - call = mock.call('state_changed', old_state=PlaybackState.PAUSED, + self.assertEvent('state_changed', old_state=PlaybackState.PAUSED, new_state=PlaybackState.STOPPED, target_state=None) - self.assertIn(call, send_mock.call_args_list) - def test_state_change_playing_to_paused_event(self, send_mock): + def test_state_change_playing_to_paused_event(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.start_playback() + self.audio.wait_for_state_change() + self.listener.clear_events() self.audio.pause_playback() self.audio.wait_for_state_change().get() - call = mock.call('state_changed', old_state=PlaybackState.PLAYING, + self.assertEvent('state_changed', old_state=PlaybackState.PLAYING, new_state=PlaybackState.PAUSED, target_state=None) - self.assertIn(call, send_mock.call_args_list) - def test_state_change_playing_to_stopped_event(self, send_mock): + def test_state_change_playing_to_stopped_event(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.start_playback() + self.audio.wait_for_state_change() + self.listener.clear_events() self.audio.stop_playback() self.audio.wait_for_state_change().get() - call = mock.call('state_changed', old_state=PlaybackState.PLAYING, + self.assertEvent('state_changed', old_state=PlaybackState.PLAYING, new_state=PlaybackState.STOPPED, target_state=None) - self.assertIn(call, send_mock.call_args_list) - def test_stream_changed_event_on_playing(self, send_mock): + def test_stream_changed_event_on_playing(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) + self.listener.clear_events() self.audio.start_playback() # Since we are going from stopped to playing, the state change is # enough to ensure the stream changed. self.audio.wait_for_state_change().get() + self.assertEvent('stream_changed', uri=self.uris[0]) - call = mock.call('stream_changed', uri=self.uris[0]) - self.assertIn(call, send_mock.call_args_list) + def test_stream_changed_event_on_multiple_changes(self): + self.audio.prepare_change() + self.audio.set_uri(self.uris[0]) + self.listener.clear_events() + self.audio.start_playback() - def test_stream_changed_event_on_paused_to_stopped(self, send_mock): + self.audio.wait_for_state_change().get() + self.assertEvent('stream_changed', uri=self.uris[0]) + + self.audio.prepare_change() + self.audio.set_uri(self.uris[1]) + self.audio.pause_playback() + + self.audio.wait_for_state_change().get() + self.assertEvent('stream_changed', uri=self.uris[1]) + + def test_stream_changed_event_on_playing_to_paused(self): + self.audio.prepare_change() + self.audio.set_uri(self.uris[0]) + self.listener.clear_events() + self.audio.start_playback() + + self.audio.wait_for_state_change().get() + self.assertEvent('stream_changed', uri=self.uris[0]) + + self.listener.clear_events() + self.audio.pause_playback() + + self.audio.wait_for_state_change().get() + self.assertNotEvent('stream_changed', uri=self.uris[0]) + + def test_stream_changed_event_on_paused_to_stopped(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.pause_playback() + self.audio.wait_for_state_change() + self.listener.clear_events() self.audio.stop_playback() self.audio.wait_for_state_change().get() + self.assertEvent('stream_changed', uri=None) - call = mock.call('stream_changed', uri=None) - self.assertIn(call, send_mock.call_args_list) - - def test_position_changed_on_pause(self, send_mock): + def test_position_changed_on_pause(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.pause_playback() self.audio.wait_for_state_change() self.audio.wait_for_state_change().get() + self.assertEvent('position_changed', position=0) - call = mock.call('position_changed', position=0) - self.assertIn(call, send_mock.call_args_list) + def test_stream_changed_event_on_paused_to_playing(self): + self.audio.prepare_change() + self.audio.set_uri(self.uris[0]) + self.listener.clear_events() + self.audio.pause_playback() - def test_position_changed_on_play(self, send_mock): + self.audio.wait_for_state_change().get() + self.assertEvent('stream_changed', uri=self.uris[0]) + + self.listener.clear_events() + self.audio.start_playback() + + self.audio.wait_for_state_change().get() + self.assertNotEvent('stream_changed', uri=self.uris[0]) + + def test_position_changed_on_play(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.start_playback() self.audio.wait_for_state_change() self.audio.wait_for_state_change().get() + self.assertEvent('position_changed', position=0) - call = mock.call('position_changed', position=0) - self.assertIn(call, send_mock.call_args_list) - - def test_position_changed_on_seek(self, send_mock): + def test_position_changed_on_seek_while_stopped(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.set_position(2000) self.audio.wait_for_state_change().get() + self.assertNotEvent('position_changed', position=0) - call = mock.call('position_changed', position=0) - self.assertNotIn(call, send_mock.call_args_list) - - def test_position_changed_on_seek_after_play(self, send_mock): + def test_position_changed_on_seek_after_play(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.start_playback() + self.audio.wait_for_state_change() + self.listener.clear_events() self.audio.set_position(2000) self.audio.wait_for_state_change().get() + self.assertEvent('position_changed', position=2000) - call = mock.call('position_changed', position=2000) - self.assertIn(call, send_mock.call_args_list) - - def test_position_changed_on_seek_after_pause(self, send_mock): + def test_position_changed_on_seek_after_pause(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.pause_playback() + self.audio.wait_for_state_change() + self.listener.clear_events() self.audio.set_position(2000) self.audio.wait_for_state_change().get() + self.assertEvent('position_changed', position=2000) - call = mock.call('position_changed', position=2000) - self.assertIn(call, send_mock.call_args_list) - - def test_tags_changed_on_playback(self, send_mock): + def test_tags_changed_on_playback(self): self.audio.prepare_change() self.audio.set_uri(self.uris[0]) self.audio.start_playback() self.audio.wait_for_state_change().get() - send_mock.assert_any_call('tags_changed', tags=mock.ANY) + self.assertEvent('tags_changed', tags=mock.ANY) # Unlike the other events, having the state changed done is not # enough to ensure our event is called. So we setup a threading # event that we can wait for with a timeout while the track playback # completes. - def test_stream_changed_event_on_paused(self, send_mock): - event = threading.Event() - - def send(name, **kwargs): - if name == 'stream_changed': - event.set() - send_mock.side_effect = send + def test_stream_changed_event_on_paused(self): + event = self.listener.wait('stream_changed').get() self.audio.prepare_change() self.audio.set_uri(self.uris[0]) @@ -322,13 +389,10 @@ class AudioEventTest(BaseTest): if not event.wait(timeout=1.0): self.fail('Stream changed not reached within deadline') - def test_reached_end_of_stream_event(self, send_mock): - event = threading.Event() + self.assertEvent('stream_changed', uri=self.uris[0]) - def send(name, **kwargs): - if name == 'reached_end_of_stream': - event.set() - send_mock.side_effect = send + def test_reached_end_of_stream_event(self): + event = self.listener.wait('reached_end_of_stream').get() self.audio.prepare_change() self.audio.set_uri(self.uris[0]) @@ -341,21 +405,14 @@ class AudioEventTest(BaseTest): self.assertFalse(self.audio.get_current_tags().get()) - def test_gapless(self, send_mock): + def test_gapless(self): uris = self.uris[1:] - events = [] - done = threading.Event() + event = self.listener.wait('reached_end_of_stream').get() def callback(): if uris: self.audio.set_uri(uris.pop()).get() - def send(name, **kwargs): - events.append((name, kwargs)) - if name == 'reached_end_of_stream': - done.set() - - send_mock.side_effect = send self.audio.set_about_to_finish_callback(callback).get() self.audio.prepare_change() @@ -367,15 +424,15 @@ class AudioEventTest(BaseTest): self.possibly_trigger_fake_about_to_finish() self.audio.wait_for_state_change().get() - if not done.wait(timeout=1.0): + if not event.wait(timeout=1.0): self.fail('EOS not received') # Check that both uris got played - self.assertIn(('stream_changed', {'uri': self.uris[0]}), events) - self.assertIn(('stream_changed', {'uri': self.uris[1]}), events) + self.assertEvent('stream_changed', uri=self.uris[0]) + self.assertEvent('stream_changed', uri=self.uris[1]) # Check that events counts check out. - keys = [k for k, v in events] + keys = [k for k, v in self.listener.get_events().get()] self.assertEqual(2, keys.count('stream_changed')) self.assertEqual(2, keys.count('position_changed')) self.assertEqual(1, keys.count('state_changed')) @@ -383,17 +440,12 @@ class AudioEventTest(BaseTest): # TODO: test tag states within gaples - def test_current_tags_are_blank_to_begin_with(self, send_mock): + # TODO: this does not belong in this testcase + def test_current_tags_are_blank_to_begin_with(self): self.assertFalse(self.audio.get_current_tags().get()) - def test_current_tags_blank_after_end_of_stream(self, send_mock): - done = threading.Event() - - def send(name, **kwargs): - if name == 'reached_end_of_stream': - done.set() - - send_mock.side_effect = send + def test_current_tags_blank_after_end_of_stream(self): + event = self.listener.wait('reached_end_of_stream').get() self.audio.prepare_change() self.audio.set_uri(self.uris[0]) @@ -402,23 +454,18 @@ class AudioEventTest(BaseTest): self.possibly_trigger_fake_about_to_finish() self.audio.wait_for_state_change().get() - if not done.wait(timeout=1.0): + if not event.wait(timeout=1.0): self.fail('EOS not received') self.assertFalse(self.audio.get_current_tags().get()) - def test_current_tags_stored(self, send_mock): - done = threading.Event() + def test_current_tags_stored(self): + event = self.listener.wait('reached_end_of_stream').get() tags = [] def callback(): tags.append(self.audio.get_current_tags().get()) - def send(name, **kwargs): - if name == 'reached_end_of_stream': - done.set() - - send_mock.side_effect = send self.audio.set_about_to_finish_callback(callback).get() self.audio.prepare_change() @@ -428,7 +475,7 @@ class AudioEventTest(BaseTest): self.possibly_trigger_fake_about_to_finish() self.audio.wait_for_state_change().get() - if not done.wait(timeout=1.0): + if not event.wait(timeout=1.0): self.fail('EOS not received') self.assertTrue(tags[0]) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 5a8c9649..0869b3ec 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -10,183 +10,315 @@ from mopidy import backend, core from mopidy.internal import deprecation from mopidy.models import Track -from tests import dummy_audio as audio +from tests import dummy_audio -# TODO: split into smaller easier to follow tests. setup is way to complex. -# TODO: just mock tracklist? -class CorePlaybackTest(unittest.TestCase): +# TODO: Replace this with dummy_backend now that it uses a real +# playbackprovider Since we rely on our DummyAudio to actually emit events we +# need a "real" backend and not a mock so the right calls make it through to +# audio. +class TestBackend(pykka.ThreadingActor, backend.Backend): + uri_schemes = ['dummy'] + + def __init__(self, config, audio): + super(TestBackend, self).__init__() + self.playback = backend.PlaybackProvider(audio=audio, backend=self) + + +class BaseTest(unittest.TestCase): + config = {'core': {'max_tracklist_length': 10000}} + tracks = [Track(uri='dummy:a', length=1234), + Track(uri='dummy:b', length=1234), + Track(uri='dummy:c', length=1234)] def setUp(self): # noqa: N802 - config = { - 'core': { - 'max_tracklist_length': 10000, - } - } + # TODO: use create_proxy helpers. + self.audio = dummy_audio.DummyAudio.start().proxy() + self.backend = TestBackend.start( + audio=self.audio, config=self.config).proxy() + self.core = core.Core( + audio=self.audio, backends=[self.backend], config=self.config) + self.playback = self.core.playback - self.backend1 = mock.Mock() - self.backend1.uri_schemes.get.return_value = ['dummy1'] - self.playback1 = mock.Mock(spec=backend.PlaybackProvider) - self.playback1.get_time_position.return_value.get.return_value = 1000 - self.backend1.playback = self.playback1 + # We don't have a core actor running, so call about to finish directly. + self.audio.set_about_to_finish_callback( + self.playback._on_about_to_finish) - self.backend2 = mock.Mock() - self.backend2.uri_schemes.get.return_value = ['dummy2'] - self.playback2 = mock.Mock(spec=backend.PlaybackProvider) - self.playback2.get_time_position.return_value.get.return_value = 2000 - self.backend2.playback = self.playback2 + with deprecation.ignore('core.tracklist.add:tracks_arg'): + self.core.tracklist.add(self.tracks) - # A backend without the optional playback provider - self.backend3 = mock.Mock() - self.backend3.uri_schemes.get.return_value = ['dummy3'] - self.backend3.has_playback().get.return_value = False + self.events = [] + self.patcher = mock.patch('mopidy.audio.listener.AudioListener.send') + self.send_mock = self.patcher.start() - self.tracks = [ - Track(uri='dummy1:a', length=40000), - Track(uri='dummy2:a', length=40000), - Track(uri='dummy3:a', length=40000), # Unplayable - Track(uri='dummy1:b', length=40000), - Track(uri='dummy1:c', length=None), # No duration - ] + def send(event, **kwargs): + self.events.append((event, kwargs)) - self.uris = [ - 'dummy1:a', 'dummy2:a', 'dummy3:a', 'dummy1:b', 'dummy1:c'] - - self.core = core.Core(config, mixer=None, backends=[ - self.backend1, self.backend2, self.backend3]) - - def lookup(uris): - result = {uri: [] for uri in uris} - for track in self.tracks: - if track.uri in result: - result[track.uri].append(track) - return result - - self.lookup_patcher = mock.patch.object(self.core.library, 'lookup') - self.lookup_mock = self.lookup_patcher.start() - self.lookup_mock.side_effect = lookup - - self.core.tracklist.add(uris=self.uris) - - self.tl_tracks = self.core.tracklist.tl_tracks - self.unplayable_tl_track = self.tl_tracks[2] - self.duration_less_tl_track = self.tl_tracks[4] + self.send_mock.side_effect = send def tearDown(self): # noqa: N802 - self.lookup_patcher.stop() + pykka.ActorRegistry.stop_all() + self.patcher.stop() - def trigger_end_of_track(self): - self.core.playback._on_end_of_track() + def replay_events(self, until=None): + while self.events: + if self.events[0][0] == until: + break + event, kwargs = self.events.pop(0) + self.core.on_event(event, **kwargs) - def set_current_tl_track(self, tl_track): - self.core.playback._set_current_tl_track(tl_track) + def trigger_about_to_finish(self, replay_until=None): + self.replay_events() + callback = self.audio.get_about_to_finish_callback().get() + callback() + self.replay_events(until=replay_until) - def test_get_current_tl_track_none(self): - self.set_current_tl_track(None) - self.assertEqual( - self.core.playback.get_current_tl_track(), None) +class TestPlayHandling(BaseTest): def test_get_current_tl_track_play(self): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() self.assertEqual( - self.core.playback.get_current_tl_track(), self.tl_tracks[0]) - - def test_get_current_tl_track_next(self): - self.core.playback.play(self.tl_tracks[0]) - self.core.playback.next() - - self.assertEqual( - self.core.playback.get_current_tl_track(), self.tl_tracks[1]) - - def test_get_current_tl_track_prev(self): - self.core.playback.play(self.tl_tracks[1]) - self.core.playback.previous() - - self.assertEqual( - self.core.playback.get_current_tl_track(), self.tl_tracks[0]) + self.core.playback.get_current_tl_track(), tl_tracks[0]) def test_get_current_track_play(self): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() self.assertEqual( self.core.playback.get_current_track(), self.tracks[0]) - def test_get_current_track_next(self): - self.core.playback.play(self.tl_tracks[0]) - self.core.playback.next() - - self.assertEqual( - self.core.playback.get_current_track(), self.tracks[1]) - - def test_get_current_track_prev(self): - self.core.playback.play(self.tl_tracks[1]) - self.core.playback.previous() - - self.assertEqual( - self.core.playback.get_current_track(), self.tracks[0]) - - def test_get_current_tlid_none(self): - self.set_current_tl_track(None) - - self.assertEqual(self.core.playback.get_current_tlid(), None) - def test_get_current_tlid_play(self): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() self.assertEqual( - self.core.playback.get_current_tlid(), self.tl_tracks[0].tlid) - - # TODO Test state - - def test_play_selects_dummy1_backend(self): - self.core.playback.play(self.tl_tracks[0]) - - self.playback1.prepare_change.assert_called_once_with() - self.playback1.change_track.assert_called_once_with(self.tracks[0]) - self.playback1.play.assert_called_once_with() - self.assertFalse(self.playback2.play.called) - - def test_play_selects_dummy2_backend(self): - self.core.playback.play(self.tl_tracks[1]) - - self.assertFalse(self.playback1.play.called) - self.playback2.prepare_change.assert_called_once_with() - self.playback2.change_track.assert_called_once_with(self.tracks[1]) - self.playback2.play.assert_called_once_with() - - def test_play_skips_to_next_on_track_without_playback_backend(self): - self.core.playback.play(self.unplayable_tl_track) - - self.playback1.prepare_change.assert_called_once_with() - self.playback1.change_track.assert_called_once_with(self.tracks[3]) - self.playback1.play.assert_called_once_with() - self.assertFalse(self.playback2.play.called) - - self.assertEqual( - self.core.playback.current_tl_track, self.tl_tracks[3]) + self.core.playback.get_current_tlid(), tl_tracks[0].tlid) def test_play_skips_to_next_on_unplayable_track(self): """Checks that we handle backend.change_track failing.""" - self.playback2.change_track.return_value.get.return_value = False + tl_tracks = self.core.tracklist.get_tl_tracks() - self.core.tracklist.clear() - self.core.tracklist.add(uris=self.uris[:2]) - tl_tracks = self.core.tracklist.tl_tracks + self.audio.trigger_fake_playback_failure(tl_tracks[0].track.uri) self.core.playback.play(tl_tracks[0]) + self.replay_events() + + current_tl_track = self.core.playback.get_current_tl_track() + self.assertEqual(tl_tracks[1], current_tl_track) + + def test_play_tlid(self): + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tlid=tl_tracks[1].tlid) + self.replay_events() + + current_tl_track = self.core.playback.get_current_tl_track() + self.assertEqual(tl_tracks[1], current_tl_track) + + +class TestNextHandling(BaseTest): + + def test_get_current_tl_track_next(self): + self.core.playback.play() + self.replay_events() + + self.core.playback.next() + self.replay_events() + + tl_tracks = self.core.tracklist.get_tl_tracks() + current_tl_track = self.core.playback.get_current_tl_track() + self.assertEqual(current_tl_track, tl_tracks[1]) + + def test_get_pending_tl_track_next(self): + self.core.playback.play() + self.replay_events() + + self.core.playback.next() + + tl_tracks = self.core.tracklist.get_tl_tracks() + self.assertEqual(self.core.playback._pending_tl_track, tl_tracks[1]) + + def test_get_current_track_next(self): + self.core.playback.play() + self.replay_events() + + self.core.playback.next() + self.replay_events() + + current_track = self.core.playback.get_current_track() + self.assertEqual(current_track, self.tracks[1]) + + def test_next_keeps_finished_track_in_tracklist(self): + tl_track = self.core.tracklist.get_tl_tracks()[0] + + self.core.playback.play(tl_track) + self.replay_events() + + self.core.playback.next() + self.replay_events() + + self.assertIn(tl_track, self.core.tracklist.tl_tracks) + + +class TestPreviousHandling(BaseTest): + # TODO Test previous() more + + def test_get_current_tl_track_prev(self): + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[1]) + self.core.playback.previous() + self.replay_events() + + self.assertEqual( + self.core.playback.get_current_tl_track(), tl_tracks[0]) + + def test_get_current_track_prev(self): + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[1]) + self.core.playback.previous() + self.replay_events() + + self.assertEqual( + self.core.playback.get_current_track(), self.tracks[0]) + + def test_previous_keeps_finished_track_in_tracklist(self): + tl_tracks = self.core.tracklist.get_tl_tracks() + self.core.playback.play(tl_tracks[1]) - # TODO: we really want to check that the track was marked unplayable - # and that next was called. This is just an indirect way of checking - # this :( - self.assertEqual(self.core.playback.state, core.PlaybackState.STOPPED) + self.core.playback.previous() + self.replay_events() + + self.assertIn(tl_tracks[1], self.core.tracklist.tl_tracks) + + def test_previous_keeps_finished_track_even_in_consume_mode(self): + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[1]) + self.core.tracklist.consume = True + + self.core.playback.previous() + self.replay_events() + + self.assertIn(tl_tracks[1], self.core.tracklist.tl_tracks) + + +class TestPlayUnknownHanlding(BaseTest): + + tracks = [Track(uri='unknown:a', length=1234), + Track(uri='dummy:b', length=1234)] + + # TODO: move to UnplayableTest? + def test_play_skips_to_next_on_track_without_playback_backend(self): + self.core.playback.play() + + self.replay_events() + + current_track = self.core.playback.get_current_track() + self.assertEqual(current_track, self.tracks[1]) + + +class OnAboutToFinishTest(BaseTest): + + def test_on_about_to_finish_keeps_finished_track_in_tracklist(self): + tl_track = self.core.tracklist.get_tl_tracks()[0] + + self.core.playback.play(tl_track) + self.trigger_about_to_finish() + + self.assertIn(tl_track, self.core.tracklist.tl_tracks) + + +class TestConsumeHandling(BaseTest): + + def test_next_in_consume_mode_removes_finished_track(self): + tl_track = self.core.tracklist.get_tl_tracks()[0] + + self.core.playback.play(tl_track) + self.core.tracklist.consume = True + self.replay_events() + + self.core.playback.next() + self.replay_events() + + self.assertNotIn(tl_track, self.core.tracklist.get_tl_tracks()) + + def test_on_about_to_finish_in_consume_mode_removes_finished_track(self): + tl_track = self.core.tracklist.get_tl_tracks()[0] + + self.core.playback.play(tl_track) + self.core.tracklist.consume = True + self.trigger_about_to_finish() + + self.assertNotIn(tl_track, self.core.tracklist.get_tl_tracks()) + + +class TestCurrentAndPendingTlTrack(BaseTest): + + def test_get_current_tl_track_none(self): + self.assertEqual( + self.core.playback.get_current_tl_track(), None) + + def test_get_current_tlid_none(self): + self.assertEqual(self.core.playback.get_current_tlid(), None) + + def test_pending_tl_track_is_none(self): + self.core.playback.play() + self.replay_events() + self.assertEqual(self.playback._pending_tl_track, None) + + def test_pending_tl_track_after_about_to_finish(self): + self.core.playback.play() + self.replay_events() + + self.trigger_about_to_finish(replay_until='stream_changed') + self.assertEqual(self.playback._pending_tl_track.track.uri, 'dummy:b') + + def test_pending_tl_track_after_stream_changed(self): + self.trigger_about_to_finish() + self.assertEqual(self.playback._pending_tl_track, None) + + def test_current_tl_track_after_about_to_finish(self): + self.core.playback.play() + self.replay_events() + self.trigger_about_to_finish(replay_until='stream_changed') + self.assertEqual(self.playback.current_tl_track.track.uri, 'dummy:a') + + def test_current_tl_track_after_stream_changed(self): + self.core.playback.play() + self.replay_events() + self.trigger_about_to_finish() + self.assertEqual(self.playback.current_tl_track.track.uri, 'dummy:b') + + def test_current_tl_track_after_end_of_stream(self): + self.core.playback.play() + self.replay_events() + self.trigger_about_to_finish() + self.trigger_about_to_finish() + self.trigger_about_to_finish() # EOS + self.assertEqual(self.playback.current_tl_track, None) + + +@mock.patch( + 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) +class EventEmissionTest(BaseTest): - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) def test_play_when_stopped_emits_events(self, listener_mock): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() self.assertListEqual( listener_mock.send.mock_calls, @@ -195,78 +327,66 @@ class CorePlaybackTest(unittest.TestCase): 'playback_state_changed', old_state='stopped', new_state='playing'), mock.call( - 'track_playback_started', tl_track=self.tl_tracks[0]), + 'track_playback_started', tl_track=tl_tracks[0]), ]) - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) def test_play_when_paused_emits_events(self, listener_mock): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() + self.core.playback.pause() + self.replay_events() listener_mock.reset_mock() - self.core.playback.play(self.tl_tracks[1]) + self.core.playback.play(tl_tracks[1]) + self.replay_events() self.assertListEqual( listener_mock.send.mock_calls, [ + mock.call( + 'track_playback_ended', + tl_track=tl_tracks[0], time_position=mock.ANY), mock.call( 'playback_state_changed', old_state='paused', new_state='playing'), mock.call( - 'track_playback_started', tl_track=self.tl_tracks[1]), + 'track_playback_started', tl_track=tl_tracks[1]), ]) - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) def test_play_when_playing_emits_events(self, listener_mock): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() listener_mock.reset_mock() - self.core.playback.play(self.tl_tracks[3]) + self.core.playback.play(tl_tracks[2]) + self.replay_events() + # TODO: Do we want to emit playing->playing for this case? self.assertListEqual( listener_mock.send.mock_calls, [ - mock.call( - 'playback_state_changed', - old_state='playing', new_state='stopped'), mock.call( 'track_playback_ended', - tl_track=self.tl_tracks[0], time_position=1000), + tl_track=tl_tracks[0], time_position=mock.ANY), mock.call( - 'playback_state_changed', - old_state='stopped', new_state='playing'), + 'playback_state_changed', old_state='playing', + new_state='playing'), mock.call( - 'track_playback_started', tl_track=self.tl_tracks[3]), + 'track_playback_started', tl_track=tl_tracks[2]), ]) - def test_pause_selects_dummy1_backend(self): - self.core.playback.play(self.tl_tracks[0]) - self.core.playback.pause() - - self.playback1.pause.assert_called_once_with() - self.assertFalse(self.playback2.pause.called) - - def test_pause_selects_dummy2_backend(self): - self.core.playback.play(self.tl_tracks[1]) - self.core.playback.pause() - - self.assertFalse(self.playback1.pause.called) - self.playback2.pause.assert_called_once_with() - - def test_pause_changes_state_even_if_track_is_unplayable(self): - self.set_current_tl_track(self.unplayable_tl_track) - self.core.playback.pause() - - self.assertEqual(self.core.playback.state, core.PlaybackState.PAUSED) - self.assertFalse(self.playback1.pause.called) - self.assertFalse(self.playback2.pause.called) - - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) def test_pause_emits_events(self, listener_mock): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() + + self.core.playback.seek(1000) listener_mock.reset_mock() self.core.playback.pause() @@ -279,39 +399,17 @@ class CorePlaybackTest(unittest.TestCase): old_state='playing', new_state='paused'), mock.call( 'track_playback_paused', - tl_track=self.tl_tracks[0], time_position=1000), + tl_track=tl_tracks[0], time_position=1000), ]) - def test_resume_selects_dummy1_backend(self): - self.core.playback.play(self.tl_tracks[0]) - self.core.playback.pause() - self.core.playback.resume() - - self.playback1.resume.assert_called_once_with() - self.assertFalse(self.playback2.resume.called) - - def test_resume_selects_dummy2_backend(self): - self.core.playback.play(self.tl_tracks[1]) - self.core.playback.pause() - self.core.playback.resume() - - self.assertFalse(self.playback1.resume.called) - self.playback2.resume.assert_called_once_with() - - def test_resume_does_nothing_if_track_is_unplayable(self): - self.set_current_tl_track(self.unplayable_tl_track) - self.core.playback.state = core.PlaybackState.PAUSED - self.core.playback.resume() - - self.assertEqual(self.core.playback.state, core.PlaybackState.PAUSED) - self.assertFalse(self.playback1.resume.called) - self.assertFalse(self.playback2.resume.called) - - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) def test_resume_emits_events(self, listener_mock): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() + self.core.playback.pause() + self.core.playback.seek(1000) listener_mock.reset_mock() self.core.playback.resume() @@ -324,39 +422,19 @@ class CorePlaybackTest(unittest.TestCase): old_state='paused', new_state='playing'), mock.call( 'track_playback_resumed', - tl_track=self.tl_tracks[0], time_position=1000), + tl_track=tl_tracks[0], time_position=1000), ]) - def test_stop_selects_dummy1_backend(self): - self.core.playback.play(self.tl_tracks[0]) - self.core.playback.stop() - - self.playback1.stop.assert_called_once_with() - self.assertFalse(self.playback2.stop.called) - - def test_stop_selects_dummy2_backend(self): - self.core.playback.play(self.tl_tracks[1]) - self.core.playback.stop() - - self.assertFalse(self.playback1.stop.called) - self.playback2.stop.assert_called_once_with() - - def test_stop_changes_state_even_if_track_is_unplayable(self): - self.set_current_tl_track(self.unplayable_tl_track) - self.core.playback.state = core.PlaybackState.PAUSED - self.core.playback.stop() - - self.assertEqual(self.core.playback.state, core.PlaybackState.STOPPED) - self.assertFalse(self.playback1.stop.called) - self.assertFalse(self.playback2.stop.called) - - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) def test_stop_emits_events(self, listener_mock): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() + self.core.playback.seek(1000) listener_mock.reset_mock() self.core.playback.stop() + self.replay_events() self.assertListEqual( listener_mock.send.mock_calls, @@ -366,218 +444,55 @@ class CorePlaybackTest(unittest.TestCase): old_state='playing', new_state='stopped'), mock.call( 'track_playback_ended', - tl_track=self.tl_tracks[0], time_position=1000), + tl_track=tl_tracks[0], time_position=1000), ]) - # TODO Test next() more - - def test_next_keeps_finished_track_in_tracklist(self): - tl_track = self.tl_tracks[0] - self.core.playback.play(tl_track) - - self.core.playback.next() - - self.assertIn(tl_track, self.core.tracklist.tl_tracks) - - def test_next_in_consume_mode_removes_finished_track(self): - tl_track = self.tl_tracks[0] - self.core.playback.play(tl_track) - self.core.tracklist.consume = True - - self.core.playback.next() - - self.assertNotIn(tl_track, self.core.tracklist.tl_tracks) - - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) def test_next_emits_events(self, listener_mock): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() + self.core.playback.seek(1000) listener_mock.reset_mock() self.core.playback.next() + self.replay_events() + # TODO: should we be emitting playing -> playing? self.assertListEqual( listener_mock.send.mock_calls, [ - mock.call( - 'playback_state_changed', - old_state='playing', new_state='stopped'), mock.call( 'track_playback_ended', - tl_track=self.tl_tracks[0], time_position=mock.ANY), + tl_track=tl_tracks[0], time_position=mock.ANY), mock.call( - 'playback_state_changed', - old_state='stopped', new_state='playing'), - mock.call( - 'track_playback_started', tl_track=self.tl_tracks[1]), + 'track_playback_started', tl_track=tl_tracks[1]), ]) - # TODO Test previous() more - - def test_previous_keeps_finished_track_in_tracklist(self): - tl_track = self.tl_tracks[1] - self.core.playback.play(tl_track) - - self.core.playback.previous() - - self.assertIn(tl_track, self.core.tracklist.tl_tracks) - - def test_previous_keeps_finished_track_even_in_consume_mode(self): - tl_track = self.tl_tracks[1] - self.core.playback.play(tl_track) - self.core.tracklist.consume = True - - self.core.playback.previous() - - self.assertIn(tl_track, self.core.tracklist.tl_tracks) - - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) - def test_previous_emits_events(self, listener_mock): - self.core.playback.play(self.tl_tracks[1]) - listener_mock.reset_mock() - - self.core.playback.previous() - - self.assertListEqual( - listener_mock.send.mock_calls, - [ - mock.call( - 'playback_state_changed', - old_state='playing', new_state='stopped'), - mock.call( - 'track_playback_ended', - tl_track=self.tl_tracks[1], time_position=mock.ANY), - mock.call( - 'playback_state_changed', - old_state='stopped', new_state='playing'), - mock.call( - 'track_playback_started', tl_track=self.tl_tracks[0]), - ]) - - # TODO Test on_end_of_track() more - - def test_on_end_of_track_keeps_finished_track_in_tracklist(self): - tl_track = self.tl_tracks[0] - self.core.playback.play(tl_track) - - self.trigger_end_of_track() - - self.assertIn(tl_track, self.core.tracklist.tl_tracks) - - def test_on_end_of_track_in_consume_mode_removes_finished_track(self): - tl_track = self.tl_tracks[0] - self.core.playback.play(tl_track) - self.core.tracklist.consume = True - - self.trigger_end_of_track() - - self.assertNotIn(tl_track, self.core.tracklist.tl_tracks) - - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) def test_on_end_of_track_emits_events(self, listener_mock): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() listener_mock.reset_mock() - self.trigger_end_of_track() + self.trigger_about_to_finish() self.assertListEqual( listener_mock.send.mock_calls, [ - mock.call( - 'playback_state_changed', - old_state='playing', new_state='stopped'), mock.call( 'track_playback_ended', - tl_track=self.tl_tracks[0], time_position=mock.ANY), + tl_track=tl_tracks[0], time_position=mock.ANY), mock.call( - 'playback_state_changed', - old_state='stopped', new_state='playing'), - mock.call( - 'track_playback_started', tl_track=self.tl_tracks[1]), + 'track_playback_started', tl_track=tl_tracks[1]), ]) - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) - def test_seek_past_end_of_track_emits_events(self, listener_mock): - self.core.playback.play(self.tl_tracks[0]) - listener_mock.reset_mock() - - self.core.playback.seek(self.tracks[0].length * 5) - - self.assertListEqual( - listener_mock.send.mock_calls, - [ - mock.call( - 'playback_state_changed', - old_state='playing', new_state='stopped'), - mock.call( - 'track_playback_ended', - tl_track=self.tl_tracks[0], time_position=mock.ANY), - mock.call( - 'playback_state_changed', - old_state='stopped', new_state='playing'), - mock.call( - 'track_playback_started', tl_track=self.tl_tracks[1]), - ]) - - def test_seek_selects_dummy1_backend(self): - self.core.playback.play(self.tl_tracks[0]) - self.core.playback.seek(10000) - - self.playback1.seek.assert_called_once_with(10000) - self.assertFalse(self.playback2.seek.called) - - def test_seek_selects_dummy2_backend(self): - self.core.playback.play(self.tl_tracks[1]) - self.core.playback.seek(10000) - - self.assertFalse(self.playback1.seek.called) - self.playback2.seek.assert_called_once_with(10000) - - def test_seek_normalizes_negative_positions_to_zero(self): - self.core.playback.play(self.tl_tracks[0]) - self.core.playback.seek(-100) - - self.playback1.seek.assert_called_once_with(0) - - def test_seek_fails_for_unplayable_track(self): - self.set_current_tl_track(self.unplayable_tl_track) - self.core.playback.state = core.PlaybackState.PLAYING - success = self.core.playback.seek(1000) - - self.assertFalse(success) - self.assertFalse(self.playback1.seek.called) - self.assertFalse(self.playback2.seek.called) - - def test_seek_fails_for_track_without_duration(self): - self.set_current_tl_track(self.duration_less_tl_track) - self.core.playback.state = core.PlaybackState.PLAYING - success = self.core.playback.seek(1000) - - self.assertFalse(success) - self.assertFalse(self.playback1.seek.called) - self.assertFalse(self.playback2.seek.called) - - def test_seek_play_stay_playing(self): - self.core.playback.play(self.tl_tracks[0]) - self.core.playback.state = core.PlaybackState.PLAYING - self.core.playback.seek(1000) - - self.assertEqual(self.core.playback.state, core.PlaybackState.PLAYING) - - def test_seek_paused_stay_paused(self): - self.core.playback.play(self.tl_tracks[0]) - self.core.playback.state = core.PlaybackState.PAUSED - self.core.playback.seek(1000) - - self.assertEqual(self.core.playback.state, core.PlaybackState.PAUSED) - - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) def test_seek_emits_seeked_event(self, listener_mock): - self.core.playback.play(self.tl_tracks[0]) + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() listener_mock.reset_mock() self.core.playback.seek(1000) @@ -585,45 +500,180 @@ class CorePlaybackTest(unittest.TestCase): listener_mock.send.assert_called_once_with( 'seeked', time_position=1000) - def test_time_position_selects_dummy1_backend(self): - self.core.playback.play(self.tl_tracks[0]) - self.core.playback.seek(10000) - self.core.playback.time_position + def test_seek_past_end_of_track_emits_events(self, listener_mock): + tl_tracks = self.core.tracklist.get_tl_tracks() - self.playback1.get_time_position.assert_called_once_with() - self.assertFalse(self.playback2.get_time_position.called) + self.core.playback.play(tl_tracks[0]) + self.replay_events() + listener_mock.reset_mock() - def test_time_position_selects_dummy2_backend(self): - self.core.playback.play(self.tl_tracks[1]) - self.core.playback.seek(10000) - self.core.playback.time_position + self.core.playback.seek(self.tracks[0].length * 5) + self.replay_events() - self.assertFalse(self.playback1.get_time_position.called) - self.playback2.get_time_position.assert_called_once_with() + self.assertListEqual( + listener_mock.send.mock_calls, + [ + mock.call( + 'track_playback_ended', + tl_track=tl_tracks[0], time_position=mock.ANY), + mock.call( + 'track_playback_started', tl_track=tl_tracks[1]), + ]) + + def test_previous_emits_events(self, listener_mock): + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[1]) + self.replay_events() + listener_mock.reset_mock() + + self.core.playback.previous() + self.replay_events() + + self.assertListEqual( + listener_mock.send.mock_calls, + [ + mock.call( + 'track_playback_ended', + tl_track=tl_tracks[1], time_position=mock.ANY), + mock.call( + 'track_playback_started', tl_track=tl_tracks[0]), + ]) + + +class UnplayableURITest(BaseTest): + + def setUp(self): # noqa: N802 + super(UnplayableURITest, self).setUp() + self.core.tracklist.clear() + tl_tracks = self.core.tracklist.add([Track(uri='unplayable://')]) + self.core.playback._set_current_tl_track(tl_tracks[0]) + + def test_pause_changes_state_even_if_track_is_unplayable(self): + self.core.playback.pause() + self.assertEqual(self.core.playback.state, core.PlaybackState.PAUSED) + + def test_resume_does_nothing_if_track_is_unplayable(self): + self.core.playback.state = core.PlaybackState.PAUSED + self.core.playback.resume() + + self.assertEqual(self.core.playback.state, core.PlaybackState.PAUSED) + + def test_stop_changes_state_even_if_track_is_unplayable(self): + self.core.playback.state = core.PlaybackState.PAUSED + self.core.playback.stop() + + self.assertEqual(self.core.playback.state, core.PlaybackState.STOPPED) def test_time_position_returns_0_if_track_is_unplayable(self): - self.set_current_tl_track(self.unplayable_tl_track) - result = self.core.playback.time_position self.assertEqual(result, 0) - self.assertFalse(self.playback1.get_time_position.called) - self.assertFalse(self.playback2.get_time_position.called) - # TODO Test on_tracklist_change + def test_seek_fails_for_unplayable_track(self): + self.core.playback.state = core.PlaybackState.PLAYING + success = self.core.playback.seek(1000) + + self.assertFalse(success) -# Since we rely on our DummyAudio to actually emit events we need a "real" -# backend and not a mock so the right calls make it through to audio. -class TestBackend(pykka.ThreadingActor, backend.Backend): - uri_schemes = ['dummy'] +class SeekTest(BaseTest): - def __init__(self, config, audio): - super(TestBackend, self).__init__() - self.playback = backend.PlaybackProvider(audio=audio, backend=self) + def test_seek_normalizes_negative_positions_to_zero(self): + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() + + self.core.playback.seek(-100) # Dummy audio doesn't progress time. + self.assertEqual(0, self.core.playback.get_time_position()) + + def test_seek_fails_for_track_without_duration(self): + track = self.tracks[0].replace(length=None) + self.core.tracklist.clear() + self.core.tracklist.add([track]) + + self.core.playback.play() + self.replay_events() + + self.assertFalse(self.core.playback.seek(1000)) + self.assertEqual(0, self.core.playback.get_time_position()) + + def test_seek_play_stay_playing(self): + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() + + self.core.playback.seek(1000) + self.assertEqual(self.core.playback.state, core.PlaybackState.PLAYING) + + def test_seek_paused_stay_paused(self): + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.core.playback.pause() + self.replay_events() + + self.core.playback.seek(1000) + self.assertEqual(self.core.playback.state, core.PlaybackState.PAUSED) -class TestStream(unittest.TestCase): +class TestStream(BaseTest): + + def test_get_stream_title_before_playback(self): + self.assertEqual(self.playback.get_stream_title(), None) + + def test_get_stream_title_during_playback(self): + self.core.playback.play() + self.replay_events() + + self.assertEqual(self.playback.get_stream_title(), None) + + def test_get_stream_title_during_playback_with_tags_change(self): + self.core.playback.play() + self.audio.trigger_fake_tags_changed({'organization': ['baz']}) + self.audio.trigger_fake_tags_changed({'title': ['foobar']}).get() + self.replay_events() + + self.assertEqual(self.playback.get_stream_title(), 'foobar') + + def test_get_stream_title_after_next(self): + self.core.playback.play() + self.audio.trigger_fake_tags_changed({'organization': ['baz']}) + self.audio.trigger_fake_tags_changed({'title': ['foobar']}).get() + self.replay_events() + + self.core.playback.next() + self.replay_events() + + self.assertEqual(self.playback.get_stream_title(), None) + + def test_get_stream_title_after_next_with_tags_change(self): + self.core.playback.play() + self.audio.trigger_fake_tags_changed({'organization': ['baz']}) + self.audio.trigger_fake_tags_changed({'title': ['foo']}).get() + self.replay_events() + + self.core.playback.next() + self.audio.trigger_fake_tags_changed({'organization': ['baz']}) + self.audio.trigger_fake_tags_changed({'title': ['bar']}).get() + self.replay_events() + + self.assertEqual(self.playback.get_stream_title(), 'bar') + + def test_get_stream_title_after_stop(self): + self.core.playback.play() + self.audio.trigger_fake_tags_changed({'organization': ['baz']}) + self.audio.trigger_fake_tags_changed({'title': ['foobar']}).get() + self.replay_events() + + self.core.playback.stop() + self.replay_events() + self.assertEqual(self.playback.get_stream_title(), None) + + +class BackendSelectionTest(unittest.TestCase): def setUp(self): # noqa: N802 config = { @@ -632,86 +682,146 @@ class TestStream(unittest.TestCase): } } - self.audio = audio.DummyAudio.start().proxy() - self.backend = TestBackend.start(config={}, audio=self.audio).proxy() - self.core = core.Core( - config, audio=self.audio, backends=[self.backend]) - self.playback = self.core.playback + self.backend1 = mock.Mock() + self.backend1.uri_schemes.get.return_value = ['dummy1'] + self.playback1 = mock.Mock(spec=backend.PlaybackProvider) + self.backend1.playback = self.playback1 - self.tracks = [Track(uri='dummy:a', length=1234), - Track(uri='dummy:b', length=1234)] + self.backend2 = mock.Mock() + self.backend2.uri_schemes.get.return_value = ['dummy2'] + self.playback2 = mock.Mock(spec=backend.PlaybackProvider) + self.backend2.playback = self.playback2 - self.lookup_patcher = mock.patch.object(self.core.library, 'lookup') - self.lookup_mock = self.lookup_patcher.start() - self.lookup_mock.return_value = {t.uri: [t] for t in self.tracks} + self.tracks = [ + Track(uri='dummy1:a', length=40000), + Track(uri='dummy2:a', length=40000), + ] - self.core.tracklist.add(uris=[t.uri for t in self.tracks]) + self.core = core.Core(config, mixer=None, backends=[ + self.backend1, self.backend2]) - self.events = [] - self.send_patcher = mock.patch( - 'mopidy.audio.listener.AudioListener.send') - self.send_mock = self.send_patcher.start() + self.tl_tracks = self.core.tracklist.add(self.tracks) - def send(event, **kwargs): - self.events.append((event, kwargs)) + def trigger_stream_changed(self): + pending = self.core.playback._pending_tl_track + if pending: + self.core.stream_changed(uri=pending.track.uri) + else: + self.core.stream_changed(uri=None) - self.send_mock.side_effect = send + def test_play_selects_dummy1_backend(self): + self.core.playback.play(self.tl_tracks[0]) + self.trigger_stream_changed() - def tearDown(self): # noqa: N802 - pykka.ActorRegistry.stop_all() - self.lookup_patcher.stop() - self.send_patcher.stop() + self.playback1.prepare_change.assert_called_once_with() + self.playback1.change_track.assert_called_once_with(self.tracks[0]) + self.playback1.play.assert_called_once_with() + self.assertFalse(self.playback2.play.called) - def replay_audio_events(self): - while self.events: - event, kwargs = self.events.pop(0) - self.core.on_event(event, **kwargs) + def test_play_selects_dummy2_backend(self): + self.core.playback.play(self.tl_tracks[1]) + self.trigger_stream_changed() - def test_get_stream_title_before_playback(self): - self.assertEqual(self.playback.get_stream_title(), None) + self.assertFalse(self.playback1.play.called) + self.playback2.prepare_change.assert_called_once_with() + self.playback2.change_track.assert_called_once_with(self.tracks[1]) + self.playback2.play.assert_called_once_with() - def test_get_stream_title_during_playback(self): - self.core.playback.play() + def test_pause_selects_dummy1_backend(self): + self.core.playback.play(self.tl_tracks[0]) + self.trigger_stream_changed() - self.replay_audio_events() - self.assertEqual(self.playback.get_stream_title(), None) + self.core.playback.pause() - def test_get_stream_title_during_playback_with_tags_change(self): - self.core.playback.play() - self.audio.trigger_fake_tags_changed({'organization': ['baz']}) - self.audio.trigger_fake_tags_changed({'title': ['foobar']}).get() + self.playback1.pause.assert_called_once_with() + self.assertFalse(self.playback2.pause.called) - self.replay_audio_events() - self.assertEqual(self.playback.get_stream_title(), 'foobar') + def test_pause_selects_dummy2_backend(self): + self.core.playback.play(self.tl_tracks[1]) + self.trigger_stream_changed() - def test_get_stream_title_after_next(self): - self.core.playback.play() - self.audio.trigger_fake_tags_changed({'organization': ['baz']}) - self.audio.trigger_fake_tags_changed({'title': ['foobar']}).get() - self.core.playback.next() + self.core.playback.pause() - self.replay_audio_events() - self.assertEqual(self.playback.get_stream_title(), None) + self.assertFalse(self.playback1.pause.called) + self.playback2.pause.assert_called_once_with() - def test_get_stream_title_after_next_with_tags_change(self): - self.core.playback.play() - self.audio.trigger_fake_tags_changed({'organization': ['baz']}) - self.audio.trigger_fake_tags_changed({'title': ['foo']}).get() - self.core.playback.next() - self.audio.trigger_fake_tags_changed({'organization': ['baz']}) - self.audio.trigger_fake_tags_changed({'title': ['bar']}).get() + def test_resume_selects_dummy1_backend(self): + self.core.playback.play(self.tl_tracks[0]) + self.trigger_stream_changed() - self.replay_audio_events() - self.assertEqual(self.playback.get_stream_title(), 'bar') + self.core.playback.pause() + self.core.playback.resume() + + self.playback1.resume.assert_called_once_with() + self.assertFalse(self.playback2.resume.called) + + def test_resume_selects_dummy2_backend(self): + self.core.playback.play(self.tl_tracks[1]) + self.trigger_stream_changed() + + self.core.playback.pause() + self.core.playback.resume() + + self.assertFalse(self.playback1.resume.called) + self.playback2.resume.assert_called_once_with() + + def test_stop_selects_dummy1_backend(self): + self.core.playback.play(self.tl_tracks[0]) + self.trigger_stream_changed() - def test_get_stream_title_after_stop(self): - self.core.playback.play() - self.audio.trigger_fake_tags_changed({'organization': ['baz']}) - self.audio.trigger_fake_tags_changed({'title': ['foobar']}).get() self.core.playback.stop() + self.trigger_stream_changed() - self.replay_audio_events() - self.assertEqual(self.playback.get_stream_title(), None) + self.playback1.stop.assert_called_once_with() + self.assertFalse(self.playback2.stop.called) + + def test_stop_selects_dummy2_backend(self): + self.core.playback.play(self.tl_tracks[1]) + self.trigger_stream_changed() + + self.core.playback.stop() + self.trigger_stream_changed() + + self.assertFalse(self.playback1.stop.called) + self.playback2.stop.assert_called_once_with() + + def test_seek_selects_dummy1_backend(self): + self.core.playback.play(self.tl_tracks[0]) + self.trigger_stream_changed() + + self.core.playback.seek(10000) + + self.playback1.seek.assert_called_once_with(10000) + self.assertFalse(self.playback2.seek.called) + + def test_seek_selects_dummy2_backend(self): + self.core.playback.play(self.tl_tracks[1]) + self.trigger_stream_changed() + + self.core.playback.seek(10000) + + self.assertFalse(self.playback1.seek.called) + self.playback2.seek.assert_called_once_with(10000) + + def test_time_position_selects_dummy1_backend(self): + self.core.playback.play(self.tl_tracks[0]) + self.trigger_stream_changed() + + self.core.playback.seek(10000) + self.core.playback.time_position + + self.playback1.get_time_position.assert_called_once_with() + self.assertFalse(self.playback2.get_time_position.called) + + def test_time_position_selects_dummy2_backend(self): + self.core.playback.play(self.tl_tracks[1]) + self.trigger_stream_changed() + + self.core.playback.seek(10000) + self.core.playback.time_position + + self.assertFalse(self.playback1.get_time_position.called) + self.playback2.get_time_position.assert_called_once_with() class CorePlaybackWithOldBackendTest(unittest.TestCase): @@ -737,31 +847,6 @@ class CorePlaybackWithOldBackendTest(unittest.TestCase): b.playback.play.assert_called_once_with() -class TestPlay(unittest.TestCase): - - def setUp(self): # noqa: N802 - config = { - 'core': { - 'max_tracklist_length': 10000, - } - } - - self.backend = mock.Mock() - self.backend.uri_schemes.get.return_value = ['dummy'] - self.core = core.Core(config, backends=[self.backend]) - - self.tracks = [Track(uri='dummy:a', length=1234), - Track(uri='dummy:b', length=1234)] - - with deprecation.ignore('core.tracklist.add:tracks_arg'): - self.tl_tracks = self.core.tracklist.add(tracks=self.tracks) - - def test_play_tlid(self): - self.core.playback.play(tlid=self.tl_tracks[1].tlid) - self.backend.playback.change_track.assert_called_once_with( - self.tl_tracks[1].track) - - class Bug1177RegressionTest(unittest.TestCase): def test(self): config = { diff --git a/tests/dummy_audio.py b/tests/dummy_audio.py index 7c48d9f0..fdd57d7e 100644 --- a/tests/dummy_audio.py +++ b/tests/dummy_audio.py @@ -15,6 +15,7 @@ def create_proxy(config=None, mixer=None): return DummyAudio.start(config, mixer).proxy() +# TODO: reset position on track change? class DummyAudio(pykka.ThreadingActor): def __init__(self, config=None, mixer=None): @@ -24,13 +25,15 @@ class DummyAudio(pykka.ThreadingActor): self._position = 0 self._callback = None self._uri = None - self._state_change_result = True + self._stream_changed = False self._tags = {} + self._bad_uris = set() def set_uri(self, uri): assert self._uri is None, 'prepare change not called before set' self._tags = {} self._uri = uri + self._stream_changed = True def set_appsrc(self, *args, **kwargs): pass @@ -88,12 +91,15 @@ class DummyAudio(pykka.ThreadingActor): if not self._uri: return False - if self.state == audio.PlaybackState.STOPPED and self._uri: - audio.AudioListener.send('position_changed', position=0) - audio.AudioListener.send('stream_changed', uri=self._uri) - - if new_state == audio.PlaybackState.STOPPED: + if new_state == audio.PlaybackState.STOPPED and self._uri: + self._stream_changed = True self._uri = None + + if self._uri is not None: + audio.AudioListener.send('position_changed', position=0) + + if self._stream_changed: + self._stream_changed = False audio.AudioListener.send('stream_changed', uri=self._uri) old_state, self.state = self.state, new_state @@ -105,10 +111,10 @@ class DummyAudio(pykka.ThreadingActor): self._tags['audio-codec'] = [u'fake info...'] audio.AudioListener.send('tags_changed', tags=['audio-codec']) - return self._state_change_result + return self._uri not in self._bad_uris - def trigger_fake_playback_failure(self): - self._state_change_result = False + def trigger_fake_playback_failure(self, uri): + self._bad_uris.add(uri) def trigger_fake_tags_changed(self, tags): self._tags.update(tags) diff --git a/tests/dummy_backend.py b/tests/dummy_backend.py index 9ce8e38f..465aeab6 100644 --- a/tests/dummy_backend.py +++ b/tests/dummy_backend.py @@ -22,7 +22,10 @@ class DummyBackend(pykka.ThreadingActor, backend.Backend): super(DummyBackend, self).__init__() self.library = DummyLibraryProvider(backend=self) - self.playback = DummyPlaybackProvider(audio=audio, backend=self) + if audio: + self.playback = backend.PlaybackProvider(audio=audio, backend=self) + else: + self.playback = DummyPlaybackProvider(audio=audio, backend=self) self.playlists = DummyPlaylistsProvider(backend=self) self.uri_schemes = ['dummy'] diff --git a/tests/local/test_playback.py b/tests/local/test_playback.py index 617044ba..92fbe5b9 100644 --- a/tests/local/test_playback.py +++ b/tests/local/test_playback.py @@ -111,17 +111,17 @@ class LocalPlaybackProviderTest(unittest.TestCase): def test_play_mp3(self): self.add_track('local:track:blank.mp3') - self.playback.play() + self.playback.play().get() self.assert_state_is(PlaybackState.PLAYING) def test_play_ogg(self): self.add_track('local:track:blank.ogg') - self.playback.play() + self.playback.play().get() self.assert_state_is(PlaybackState.PLAYING) def test_play_flac(self): self.add_track('local:track:blank.flac') - self.playback.play() + self.playback.play().get() self.assert_state_is(PlaybackState.PLAYING) def test_play_uri_with_non_ascii_bytes(self): @@ -129,7 +129,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): # string will be decoded from ASCII to Unicode, which will crash on # non-ASCII strings, like the bytestring the following URI decodes to. self.add_track('local:track:12%20Doin%E2%80%99%20It%20Right.flac') - self.playback.play() + self.playback.play().get() self.assert_state_is(PlaybackState.PLAYING) def test_initial_state_is_stopped(self): @@ -137,7 +137,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): def test_play_with_empty_playlist(self): self.assert_state_is(PlaybackState.STOPPED) - self.playback.play() + self.playback.play().get() self.assert_state_is(PlaybackState.STOPPED) def test_play_with_empty_playlist_return_value(self): @@ -146,7 +146,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_play_state(self): self.assert_state_is(PlaybackState.STOPPED) - self.playback.play() + self.playback.play().get() self.assert_state_is(PlaybackState.PLAYING) @populate_tracklist @@ -156,7 +156,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_play_track_state(self): self.assert_state_is(PlaybackState.STOPPED) - self.playback.play(self.tl_tracks.get()[-1]) + self.playback.play(self.tl_tracks.get()[-1]).get() self.assert_state_is(PlaybackState.PLAYING) @populate_tracklist @@ -165,139 +165,141 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_play_when_playing(self): - self.playback.play() + self.playback.play().get() track = self.playback.get_current_track().get() - self.playback.play() + self.playback.play().get() self.assert_current_track_is(track) @populate_tracklist def test_play_when_paused(self): - self.playback.play() + self.playback.play().get() track = self.playback.get_current_track().get() - self.playback.pause() - self.playback.play() + self.playback.pause().get() + self.playback.play().get() self.assert_state_is(PlaybackState.PLAYING) self.assert_current_track_is(track) @populate_tracklist - def test_play_when_pause_after_next(self): - self.playback.play() - self.playback.next() - self.playback.next() + def test_play_when_paused_after_next(self): + self.playback.play().get() + self.playback.next().get() + self.playback.next().get() track = self.playback.get_current_track().get() - self.playback.pause() - self.playback.play() + self.playback.pause().get() + self.playback.play().get() self.assert_state_is(PlaybackState.PLAYING) self.assert_current_track_is(track) @populate_tracklist def test_play_sets_current_track(self): - self.playback.play() + self.playback.play().get() self.assert_current_track_is(self.tracks[0]) @populate_tracklist def test_play_track_sets_current_track(self): - self.playback.play(self.tl_tracks.get()[-1]) + self.playback.play(self.tl_tracks.get()[-1]).get() self.assert_current_track_is(self.tracks[-1]) @populate_tracklist def test_play_skips_to_next_track_on_failure(self): # If backend's play() returns False, it is a failure. - return_values = [True, False] - self.backend.playback.play = lambda: return_values.pop() - self.playback.play() + uri = self.backend.playback.translate_uri(self.tracks[0].uri).get() + self.audio.trigger_fake_playback_failure(uri) + + self.playback.play().get() self.assert_current_track_is_not(self.tracks[0]) self.assert_current_track_is(self.tracks[1]) @populate_tracklist def test_current_track_after_completed_playlist(self): - self.playback.play(self.tl_tracks.get()[-1]) + self.playback.play(self.tl_tracks.get()[-1]).get() self.trigger_about_to_finish() # EOS should have triggered self.assert_state_is(PlaybackState.STOPPED) self.assert_current_track_is(None) - self.playback.play(self.tl_tracks.get()[-1]) - self.playback.next() + self.playback.play(self.tl_tracks.get()[-1]).get() + self.playback.next().get() self.assert_state_is(PlaybackState.STOPPED) self.assert_current_track_is(None) @populate_tracklist def test_previous(self): - self.playback.play() - self.playback.next() - self.playback.previous() + self.playback.play().get() + self.playback.next().get() + self.playback.previous().get() self.assert_current_track_is(self.tracks[0]) @populate_tracklist def test_previous_more(self): - self.playback.play() # At track 0 - self.playback.next() # At track 1 - self.playback.next() # At track 2 - self.playback.previous() # At track 1 + self.playback.play().get() # At track 0 + self.playback.next().get() # At track 1 + self.playback.next().get() # At track 2 + self.playback.previous().get() # At track 1 self.assert_current_track_is(self.tracks[1]) @populate_tracklist def test_previous_return_value(self): - self.playback.play() - self.playback.next() + self.playback.play().get() + self.playback.next().get() self.assertIsNone(self.playback.previous().get()) @populate_tracklist def test_previous_does_not_trigger_playback(self): - self.playback.play() - self.playback.next() + self.playback.play().get() + self.playback.next().get() self.playback.stop() - self.playback.previous() + self.playback.previous().get() self.assert_state_is(PlaybackState.STOPPED) @populate_tracklist def test_previous_at_start_of_playlist(self): - self.playback.previous() + self.playback.previous().get() self.assert_state_is(PlaybackState.STOPPED) self.assert_current_track_is(None) def test_previous_for_empty_playlist(self): - self.playback.previous() + self.playback.previous().get() self.assert_state_is(PlaybackState.STOPPED) self.assert_current_track_is(None) @populate_tracklist def test_previous_skips_to_previous_track_on_failure(self): # If backend's play() returns False, it is a failure. - return_values = [True, False, True] - self.backend.playback.play = lambda: return_values.pop() - self.playback.play(self.tl_tracks.get()[2]) + uri = self.backend.playback.translate_uri(self.tracks[1].uri).get() + self.audio.trigger_fake_playback_failure(uri) + + self.playback.play(self.tl_tracks.get()[2]).get() self.assert_current_track_is(self.tracks[2]) - self.playback.previous() + self.playback.previous().get() self.assert_current_track_is_not(self.tracks[1]) self.assert_current_track_is(self.tracks[0]) @populate_tracklist def test_next(self): - self.playback.play() + self.playback.play().get() old_track = self.playback.get_current_track().get() old_position = self.tracklist.index().get() - self.playback.next() + self.playback.next().get() self.assertEqual(self.tracklist.index().get(), old_position + 1) self.assert_current_track_is_not(old_track) @populate_tracklist def test_next_return_value(self): - self.playback.play() + self.playback.play().get() self.assertEqual(self.playback.next().get(), None) @populate_tracklist def test_next_does_not_trigger_playback(self): - self.playback.next() + self.playback.next().get() self.assert_state_is(PlaybackState.STOPPED) @populate_tracklist def test_next_at_end_of_playlist(self): - self.playback.play() + self.playback.play().get() for i, track in enumerate(self.tracks): self.assert_state_is(PlaybackState.PLAYING) @@ -310,30 +312,31 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_next_until_end_of_playlist_and_play_from_start(self): - self.playback.play() + self.playback.play().get() for _ in self.tracks: - self.playback.next() + self.playback.next().get() self.assert_current_track_is(None) self.assert_state_is(PlaybackState.STOPPED) - self.playback.play() + self.playback.play().get() self.assert_state_is(PlaybackState.PLAYING) self.assert_current_track_is(self.tracks[0]) def test_next_for_empty_playlist(self): - self.playback.next() + self.playback.next().get() self.assert_state_is(PlaybackState.STOPPED) @populate_tracklist def test_next_skips_to_next_track_on_failure(self): # If backend's play() returns False, it is a failure. - return_values = [True, False, True] - self.backend.playback.play = lambda: return_values.pop() - self.playback.play() + uri = self.backend.playback.translate_uri(self.tracks[1].uri).get() + self.audio.trigger_fake_playback_failure(uri) + + self.playback.play().get() self.assert_current_track_is(self.tracks[0]) - self.playback.next() + self.playback.next().get() self.assert_current_track_is_not(self.tracks[1]) self.assert_current_track_is(self.tracks[2]) @@ -343,14 +346,14 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_next_track_during_play(self): - self.playback.play() + self.playback.play().get() self.assert_next_tl_track_is(self.tl_tracks.get()[1]) @populate_tracklist def test_next_track_after_previous(self): - self.playback.play() - self.playback.next() - self.playback.previous() + self.playback.play().get() + self.playback.next().get() + self.playback.previous().get() self.assert_next_tl_track_is(self.tl_tracks.get()[1]) def test_next_track_empty_playlist(self): @@ -358,17 +361,17 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_next_track_at_end_of_playlist(self): - self.playback.play() + self.playback.play().get() for _ in self.tl_tracks.get()[1:]: - self.playback.next() + self.playback.next().get() self.assert_next_tl_track_is(None) @populate_tracklist def test_next_track_at_end_of_playlist_with_repeat(self): self.tracklist.repeat = True - self.playback.play() + self.playback.play().get() for _ in self.tracks[1:]: - self.playback.next() + self.playback.next().get() self.assert_next_tl_track_is(self.tl_tracks.get()[0]) @populate_tracklist @@ -382,17 +385,17 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_next_with_consume(self): self.tracklist.consume = True - self.playback.play() - self.playback.next() + self.playback.play().get() + self.playback.next().get() self.assertNotIn(self.tracks[0], self.tracklist.get_tracks().get()) @populate_tracklist def test_next_with_single_and_repeat(self): self.tracklist.single = True self.tracklist.repeat = True - self.playback.play() + self.playback.play().get() self.assert_current_track_is(self.tracks[0]) - self.playback.next() + self.playback.next().get() self.assert_current_track_is(self.tracks[1]) @populate_tracklist @@ -401,9 +404,9 @@ class LocalPlaybackProviderTest(unittest.TestCase): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True - self.playback.play() + self.playback.play().get() self.assert_current_track_is(self.tracks[-1]) - self.playback.next() + self.playback.next().get() self.assert_current_track_is(self.tracks[-2]) @populate_tracklist @@ -434,7 +437,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_end_of_track(self): - self.playback.play() + self.playback.play().get() old_track = self.playback.get_current_track().get() old_position = self.tracklist.index().get() @@ -447,7 +450,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_end_of_track_return_value(self): - self.playback.play() + self.playback.play().get() self.assertEqual(self.trigger_about_to_finish(), None) @populate_tracklist @@ -457,7 +460,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_end_of_track_at_end_of_playlist(self): - self.playback.play() + self.playback.play().get() for i, track in enumerate(self.tracks): self.assert_state_is(PlaybackState.PLAYING) @@ -470,7 +473,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_end_of_track_until_end_of_playlist_and_play_from_start(self): - self.playback.play() + self.playback.play().get() for _ in self.tracks: self.trigger_about_to_finish() @@ -478,7 +481,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): self.assertEqual(self.playback.get_current_track().get(), None) self.assert_state_is(PlaybackState.STOPPED) - self.playback.play() + self.playback.play().get() self.assert_state_is(PlaybackState.PLAYING) self.assert_current_track_is(self.tracks[0]) @@ -486,12 +489,14 @@ class LocalPlaybackProviderTest(unittest.TestCase): self.trigger_about_to_finish() self.assert_state_is(PlaybackState.STOPPED) + # TODO: On about to finish does not handle skipping to next track yet. + @unittest.expectedFailure @populate_tracklist def test_end_of_track_skips_to_next_track_on_failure(self): # If backend's play() returns False, it is a failure. return_values = [True, False, True] self.backend.playback.play = lambda: return_values.pop() - self.playback.play() + self.playback.play().get() self.assert_current_track_is(self.tracks[0]) self.trigger_about_to_finish() self.assert_current_track_is_not(self.tracks[1]) @@ -503,7 +508,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_end_of_track_track_during_play(self): - self.playback.play() + self.playback.play().get() self.assert_next_tl_track_is(self.tl_tracks.get()[1]) @populate_tracklist @@ -518,7 +523,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_end_of_track_track_at_end_of_playlist(self): - self.playback.play() + self.playback.play().get() for _ in self.tracks[1:]: self.trigger_about_to_finish() @@ -527,7 +532,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_end_of_track_track_at_end_of_playlist_with_repeat(self): self.tracklist.repeat = True - self.playback.play() + self.playback.play().get() for _ in self.tracks[1:]: self.trigger_about_to_finish() @@ -544,7 +549,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_end_of_track_with_consume(self): self.tracklist.consume = True - self.playback.play() + self.playback.play().get() self.trigger_about_to_finish() self.assertNotIn(self.tracks[0], self.tracklist.get_tracks().get()) @@ -554,7 +559,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True - self.playback.play() + self.playback.play().get() self.assert_current_track_is(self.tracks[-1]) self.trigger_about_to_finish() self.assert_current_track_is(self.tracks[-2]) @@ -592,21 +597,21 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_previous_track_after_play(self): - self.playback.play() + self.playback.play().get() self.assert_previous_tl_track_is(None) @populate_tracklist def test_previous_track_after_next(self): - self.playback.play() - self.playback.next() + self.playback.play().get() + self.playback.next().get() self.assert_previous_tl_track_is(self.tl_tracks.get()[0]) @populate_tracklist def test_previous_track_after_previous(self): - self.playback.play() # At track 0 - self.playback.next() # At track 1 - self.playback.next() # At track 2 - self.playback.previous() # At track 1 + self.playback.play().get() # At track 0 + self.playback.next().get() # At track 1 + self.playback.next().get() # At track 2 + self.playback.previous().get() # At track 1 self.assert_previous_tl_track_is(self.tl_tracks.get()[0]) def test_previous_track_empty_playlist(self): @@ -634,13 +639,13 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_current_track_during_play(self): - self.playback.play() + self.playback.play().get() self.assert_current_track_is(self.tracks[0]) @populate_tracklist def test_current_track_after_next(self): self.playback.play() - self.playback.next() + self.playback.next().get() self.assert_current_track_is(self.tracks[1]) @populate_tracklist @@ -649,18 +654,18 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_tracklist_position_during_play(self): - self.playback.play() + self.playback.play().get() self.assert_current_track_index_is(0) @populate_tracklist def test_tracklist_position_after_next(self): - self.playback.play() - self.playback.next() + self.playback.play().get() + self.playback.next().get() self.assert_current_track_index_is(1) @populate_tracklist def test_tracklist_position_at_end_of_playlist(self): - self.playback.play(self.tl_tracks.get()[-1]) + self.playback.play(self.tl_tracks.get()[-1]).get() self.trigger_about_to_finish() # EOS should have triggered self.assert_current_track_index_is(None) @@ -672,7 +677,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_on_tracklist_change_when_playing(self): - self.playback.play() + self.playback.play().get() current_track = self.playback.get_current_track().get() self.tracklist.add([self.tracks[2]]) self.assert_state_is(PlaybackState.PLAYING) @@ -686,7 +691,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_on_tracklist_change_when_paused(self): - self.playback.play() + self.playback.play().get() self.playback.pause() current_track = self.playback.get_current_track().get() self.tracklist.add([self.tracks[2]]) @@ -700,20 +705,20 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_pause_when_playing(self): - self.playback.play() + self.playback.play().get() self.playback.pause() self.assert_state_is(PlaybackState.PAUSED) @populate_tracklist def test_pause_when_paused(self): - self.playback.play() + self.playback.play().get() self.playback.pause() self.playback.pause() self.assert_state_is(PlaybackState.PAUSED) @populate_tracklist def test_pause_return_value(self): - self.playback.play() + self.playback.play().get() self.assertIsNone(self.playback.pause().get()) @populate_tracklist @@ -723,27 +728,27 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_resume_when_playing(self): - self.playback.play() + self.playback.play().get() self.playback.resume() self.assert_state_is(PlaybackState.PLAYING) @populate_tracklist def test_resume_when_paused(self): - self.playback.play() + self.playback.play().get() self.playback.pause() self.playback.resume() self.assert_state_is(PlaybackState.PLAYING) @populate_tracklist def test_resume_return_value(self): - self.playback.play() + self.playback.play().get() self.playback.pause() self.assertIsNone(self.playback.resume().get()) @unittest.SkipTest # Uses sleep and might not work with LocalBackend @populate_tracklist def test_resume_continues_from_right_position(self): - self.playback.play() + self.playback.play().get() time.sleep(0.2) self.playback.pause() self.playback.resume() @@ -756,7 +761,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_seek_when_stopped_updates_position(self): - self.playback.seek(1000) + self.playback.seek(1000).get() position = self.playback.time_position self.assertGreaterEqual(position, 990) @@ -764,31 +769,31 @@ class LocalPlaybackProviderTest(unittest.TestCase): self.assertFalse(self.playback.seek(0).get()) def test_seek_on_empty_playlist_updates_position(self): - self.playback.seek(0) + self.playback.seek(0).get() self.assert_state_is(PlaybackState.STOPPED) @populate_tracklist def test_seek_when_stopped_triggers_play(self): - self.playback.seek(0) + self.playback.seek(0).get() self.assert_state_is(PlaybackState.PLAYING) @populate_tracklist def test_seek_when_playing(self): - self.playback.play() + self.playback.play().get() result = self.playback.seek(self.tracks[0].length - 1000) self.assert_(result, 'Seek return value was %s' % result) @populate_tracklist def test_seek_when_playing_updates_position(self): length = self.tracks[0].length - self.playback.play() - self.playback.seek(length - 1000) + self.playback.play().get() + self.playback.seek(length - 1000).get() position = self.playback.get_time_position().get() self.assertGreaterEqual(position, length - 1010) @populate_tracklist def test_seek_when_paused(self): - self.playback.play() + self.playback.play().get() self.playback.pause() result = self.playback.seek(self.tracks[0].length - 1000) self.assert_(result, 'Seek return value was %s' % result) @@ -797,7 +802,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_seek_when_paused_updates_position(self): length = self.tracks[0].length - self.playback.play() + self.playback.play().get() self.playback.pause() self.playback.seek(length - 1000) position = self.playback.get_time_position().get() @@ -807,19 +812,19 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_seek_beyond_end_of_song(self): # FIXME need to decide return value - self.playback.play() + self.playback.play().get() result = self.playback.seek(self.tracks[0].length * 100) self.assert_(not result, 'Seek return value was %s' % result) @populate_tracklist def test_seek_beyond_end_of_song_jumps_to_next_song(self): - self.playback.play() - self.playback.seek(self.tracks[0].length * 100) + self.playback.play().get() + self.playback.seek(self.tracks[0].length * 100).get() self.assert_current_track_is(self.tracks[1]) @populate_tracklist def test_seek_beyond_end_of_song_for_last_track(self): - self.playback.play(self.tl_tracks.get()[-1]) + self.playback.play(self.tl_tracks.get()[-1]).get() self.playback.seek(self.tracks[-1].length * 100) self.assert_state_is(PlaybackState.STOPPED) @@ -830,19 +835,19 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_stop_when_playing(self): - self.playback.play() + self.playback.play().get() self.playback.stop() self.assert_state_is(PlaybackState.STOPPED) @populate_tracklist def test_stop_when_paused(self): - self.playback.play() + self.playback.play().get() self.playback.pause() self.playback.stop() self.assert_state_is(PlaybackState.STOPPED) def test_stop_return_value(self): - self.playback.play() + self.playback.play().get() self.assertIsNone(self.playback.stop().get()) def test_time_position_when_stopped(self): @@ -855,7 +860,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @unittest.SkipTest # Uses sleep and does might not work with LocalBackend @populate_tracklist def test_time_position_when_playing(self): - self.playback.play() + self.playback.play().get() first = self.playback.time_position time.sleep(1) second = self.playback.time_position @@ -863,7 +868,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_time_position_when_paused(self): - self.playback.play() + self.playback.play().get() self.playback.pause().get() first = self.playback.get_time_position().get() second = self.playback.get_time_position().get() @@ -872,13 +877,13 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_play_with_consume(self): self.tracklist.consume = True - self.playback.play() + self.playback.play().get() self.assert_current_track_is(self.tracks[0]) @populate_tracklist def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self): self.tracklist.consume = True - self.playback.play() + self.playback.play().get() for t in self.tracks: self.trigger_about_to_finish() @@ -892,7 +897,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True - self.playback.play() + self.playback.play().get() self.assert_current_track_is(self.tracks[-1]) @populate_tracklist @@ -901,15 +906,15 @@ class LocalPlaybackProviderTest(unittest.TestCase): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True - self.playback.play() - self.playback.next() + self.playback.play().get() + self.playback.next().get() current_track = self.playback.get_current_track().get() self.playback.previous() self.assert_current_track_is(current_track) @populate_tracklist def test_end_of_song_starts_next_track(self): - self.playback.play() + self.playback.play().get() self.trigger_about_to_finish() self.assert_current_track_is(self.tracks[1]) @@ -917,7 +922,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): def test_end_of_song_with_single_and_repeat_starts_same(self): self.tracklist.single = True self.tracklist.repeat = True - self.playback.play() + self.playback.play().get() self.assert_current_track_is(self.tracks[0]) self.trigger_about_to_finish() self.assert_current_track_is(self.tracks[0]) @@ -927,7 +932,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): self.tracklist.single = True self.tracklist.repeat = True self.tracklist.random = True - self.playback.play() + self.playback.play().get() current_track = self.playback.get_current_track().get() self.trigger_about_to_finish() self.assert_current_track_is(current_track) @@ -935,7 +940,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_end_of_song_with_single_stops(self): self.tracklist.single = True - self.playback.play() + self.playback.play().get() self.assert_current_track_is(self.tracks[0]) self.trigger_about_to_finish() self.assert_current_track_is(None) @@ -946,7 +951,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): def test_end_of_song_with_single_and_random_stops(self): self.tracklist.single = True self.tracklist.random = True - self.playback.play() + self.playback.play().get() self.trigger_about_to_finish() # EOS should have triggered self.assert_current_track_is(None) @@ -954,7 +959,7 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_end_of_playlist_stops(self): - self.playback.play(self.tl_tracks.get()[-1]) + self.playback.play(self.tl_tracks.get()[-1]).get() self.trigger_about_to_finish() # EOS should have triggered self.assert_state_is(PlaybackState.STOPPED) @@ -971,15 +976,15 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_random_until_end_of_playlist(self): self.tracklist.random = True - self.playback.play() + self.playback.play().get() for _ in self.tracks[1:]: - self.playback.next() + self.playback.next().get() self.assert_next_tl_track_is(None) @populate_tracklist def test_random_with_eot_until_end_of_playlist(self): self.tracklist.random = True - self.playback.play() + self.playback.play().get() for _ in self.tracks[1:]: self.trigger_about_to_finish() @@ -988,9 +993,9 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_random_until_end_of_playlist_and_play_from_start(self): self.tracklist.random = True - self.playback.play() + self.playback.play().get() for _ in self.tracks: - self.playback.next() + self.playback.next().get() self.assert_next_tl_track_is_not(None) self.assert_state_is(PlaybackState.STOPPED) self.playback.play() @@ -999,21 +1004,21 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_random_with_eot_until_end_of_playlist_and_play_from_start(self): self.tracklist.random = True - self.playback.play() + self.playback.play().get() for _ in self.tracks: self.trigger_about_to_finish() # EOS should have triggered self.assert_eot_tl_track_is_not(None) self.assert_state_is(PlaybackState.STOPPED) - self.playback.play() + self.playback.play().get() self.assert_state_is(PlaybackState.PLAYING) @populate_tracklist def test_random_until_end_of_playlist_with_repeat(self): self.tracklist.repeat = True self.tracklist.random = True - self.playback.play() + self.playback.play().get() for _ in self.tracks[1:]: self.playback.next() self.assert_next_tl_track_is_not(None) @@ -1021,13 +1026,13 @@ class LocalPlaybackProviderTest(unittest.TestCase): @populate_tracklist def test_played_track_during_random_not_played_again(self): self.tracklist.random = True - self.playback.play() + self.playback.play().get() played = [] for _ in self.tracks: track = self.playback.get_current_track().get() self.assertNotIn(track, played) played.append(track) - self.playback.next() + self.playback.next().get() @populate_tracklist @mock.patch('random.shuffle') @@ -1038,10 +1043,10 @@ class LocalPlaybackProviderTest(unittest.TestCase): expected = self.tl_tracks.get()[::-1] + [None] actual = [] - self.playback.play() + self.playback.play().get() self.tracklist.random = True while self.playback.get_state().get() != PlaybackState.STOPPED: - self.playback.next() + self.playback.next().get() actual.append(self.playback.get_current_tl_track().get()) if len(actual) > len(expected): break diff --git a/tests/mpd/protocol/__init__.py b/tests/mpd/protocol/__init__.py index 754b4418..f34ad4f0 100644 --- a/tests/mpd/protocol/__init__.py +++ b/tests/mpd/protocol/__init__.py @@ -10,7 +10,7 @@ from mopidy import core from mopidy.internal import deprecation from mopidy.mpd import session, uri_mapper -from tests import dummy_backend, dummy_mixer +from tests import dummy_audio, dummy_backend, dummy_mixer class MockConnection(mock.Mock): @@ -44,11 +44,13 @@ class BaseTestCase(unittest.TestCase): self.mixer = dummy_mixer.create_proxy() else: self.mixer = None - self.backend = dummy_backend.create_proxy() + self.audio = dummy_audio.create_proxy() + self.backend = dummy_backend.create_proxy(audio=self.audio) with deprecation.ignore(): self.core = core.Core.start( self.get_config(), + audio=self.audio, mixer=self.mixer, backends=[self.backend]).proxy() diff --git a/tests/mpd/protocol/test_playback.py b/tests/mpd/protocol/test_playback.py index 6cfa30fc..51112057 100644 --- a/tests/mpd/protocol/test_playback.py +++ b/tests/mpd/protocol/test_playback.py @@ -248,7 +248,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertEqual(self.core.playback.current_track.get(), None) self.core.playback.play() self.core.playback.next() - self.core.playback.stop() + self.core.playback.stop().get() self.assertNotEqual(self.core.playback.current_track.get(), None) self.send_request('play "-1"') @@ -266,6 +266,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_play_minus_is_ignored_if_playing(self): + self.core.playback.play().get() self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) @@ -278,6 +279,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_play_minus_one_resumes_if_paused(self): + self.core.playback.play().get() self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) @@ -312,8 +314,8 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): def test_playid_minus_1_plays_current_track_if_current_track_is_set(self): self.assertEqual(self.core.playback.current_track.get(), None) - self.core.playback.play() - self.core.playback.next() + self.core.playback.play().get() + self.core.playback.next().get() self.core.playback.stop() self.assertNotEqual(None, self.core.playback.current_track.get()) @@ -332,6 +334,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_playid_minus_is_ignored_if_playing(self): + self.core.playback.play().get() self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) @@ -344,6 +347,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_playid_minus_one_resumes_if_paused(self): + self.core.playback.play().get() self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) @@ -417,7 +421,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_seekcur_absolute_value(self): - self.core.playback.play() + self.core.playback.play().get() self.send_request('seekcur "30"') @@ -425,7 +429,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_seekcur_positive_diff(self): - self.core.playback.play() + self.core.playback.play().get() self.core.playback.seek(10000) self.assertGreaterEqual(self.core.playback.time_position.get(), 10000) @@ -435,7 +439,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_seekcur_negative_diff(self): - self.core.playback.play() + self.core.playback.play().get() self.core.playback.seek(30000) self.assertGreaterEqual(self.core.playback.time_position.get(), 30000) diff --git a/tests/mpd/protocol/test_regression.py b/tests/mpd/protocol/test_regression.py index 565b369e..1688d064 100644 --- a/tests/mpd/protocol/test_regression.py +++ b/tests/mpd/protocol/test_regression.py @@ -31,6 +31,7 @@ class IssueGH17RegressionTest(protocol.BaseTestCase): Track(uri='dummy:e'), Track(uri='dummy:f'), ] + self.audio.trigger_fake_playback_failure('dummy:error') self.backend.library.dummy_library = tracks self.core.tracklist.add(uris=[t.uri for t in tracks]).get() diff --git a/tests/mpd/protocol/test_status.py b/tests/mpd/protocol/test_status.py index f65a7d3c..6369a2e7 100644 --- a/tests/mpd/protocol/test_status.py +++ b/tests/mpd/protocol/test_status.py @@ -16,7 +16,7 @@ class StatusHandlerTest(protocol.BaseTestCase): self.backend.library.dummy_library = [track] self.core.tracklist.add(uris=[track.uri]).get() - self.core.playback.play() + self.core.playback.play().get() self.send_request('currentsong') self.assertInResponse('file: dummy:/a') self.assertInResponse('Time: 0') diff --git a/tests/mpd/test_status.py b/tests/mpd/test_status.py index bce4d350..25b8dd72 100644 --- a/tests/mpd/test_status.py +++ b/tests/mpd/test_status.py @@ -11,7 +11,7 @@ from mopidy.models import Track from mopidy.mpd import dispatcher from mopidy.mpd.protocol import status -from tests import dummy_backend, dummy_mixer +from tests import dummy_audio, dummy_backend, dummy_mixer PAUSED = PlaybackState.PAUSED @@ -31,12 +31,14 @@ class StatusHandlerTest(unittest.TestCase): } } + self.audio = dummy_audio.create_proxy() self.mixer = dummy_mixer.create_proxy() - self.backend = dummy_backend.create_proxy() + self.backend = dummy_backend.create_proxy(audio=self.audio) with deprecation.ignore(): self.core = core.Core.start( config, + audio=self.audio, mixer=self.mixer, backends=[self.backend]).proxy() @@ -154,21 +156,21 @@ class StatusHandlerTest(unittest.TestCase): def test_status_method_when_playlist_loaded_contains_song(self): self.set_tracklist(Track(uri='dummy:/a')) - self.core.playback.play() + self.core.playback.play().get() result = dict(status.status(self.context)) self.assertIn('song', result) self.assertGreaterEqual(int(result['song']), 0) def test_status_method_when_playlist_loaded_contains_tlid_as_songid(self): self.set_tracklist(Track(uri='dummy:/a')) - self.core.playback.play() + self.core.playback.play().get() result = dict(status.status(self.context)) self.assertIn('songid', result) self.assertEqual(int(result['songid']), 1) def test_status_method_when_playing_contains_time_with_no_length(self): self.set_tracklist(Track(uri='dummy:/a', length=None)) - self.core.playback.play() + self.core.playback.play().get() result = dict(status.status(self.context)) self.assertIn('time', result) (position, total) = result['time'].split(':') @@ -188,7 +190,7 @@ class StatusHandlerTest(unittest.TestCase): def test_status_method_when_playing_contains_elapsed(self): self.set_tracklist(Track(uri='dummy:/a', length=60000)) - self.core.playback.play() + self.core.playback.play().get() self.core.playback.pause() self.core.playback.seek(59123) result = dict(status.status(self.context)) @@ -197,7 +199,7 @@ class StatusHandlerTest(unittest.TestCase): def test_status_method_when_starting_playing_contains_elapsed_zero(self): self.set_tracklist(Track(uri='dummy:/a', length=10000)) - self.core.playback.play() + self.core.playback.play().get() self.core.playback.pause() result = dict(status.status(self.context)) self.assertIn('elapsed', result) @@ -205,7 +207,7 @@ class StatusHandlerTest(unittest.TestCase): def test_status_method_when_playing_contains_bitrate(self): self.set_tracklist(Track(uri='dummy:/a', bitrate=3200)) - self.core.playback.play() + self.core.playback.play().get() result = dict(status.status(self.context)) self.assertIn('bitrate', result) self.assertEqual(int(result['bitrate']), 3200)