def caseclass_factory(cls, case_group):
        """Create new clonned cls class contains only action methods"""
        test_steps, scenario = {}, []
        actions_method = cls.get_actions()

        #  Generate human readeble class_name, if was method docstring not
        #  described, use generated name
        class_name = "Case_{}_{}".format(cls.__name__, case_group)

        #  Make methods for new testcase class, folowing by order
        scenario.append("    Scenario:")
        for step, action in enumerate(cls.get_actions_order()):
            n_action = action.replace("_action_", "")
            #  Generate human readeble method name, if was method docstring not
            #  described, use generated name. Used when metod failed
            step_method_name = "{}.Step{:03d}_{}".format(class_name, step, n_action)
            method = utils.copy_func(actions_method[action], step_method_name)
            _step_name = getattr(actions_method[action], "__doc__").splitlines()[0]
            setattr(method, "_step_name", "Step {:03d}. {}".format(step, _step_name))

            #  Add step to scenario
            scenario.append("        {}. {}".format(step, _step_name))

            #  Add decorator to colonned method
            for deco in getattr(method, "_deferred_decorator_", []):
                method = deco(method)

            #  if not first step make dependency
            if step > 0:
                prev_step_name = "{}.Step{:03d}_{}".format(
                    class_name, step - 1, cls.get_actions_order()[step - 1].replace("_action_", "")
                )
                depends = [test_steps[prev_step_name]]
            else:
                depends = None

            #  Add start-stop step decorator for measuring time and print
            #  start and finish info
            method = step_start_stop(method)

            test_steps[step_method_name] = test(method, depends_on=depends)

        #  Create before and after case methods
        start_method = utils.copy_func(getattr(cls, "_start_case"), "{}.StartCase".format(class_name))
        test_steps["{}.StartCase".format(class_name)] = before_class(start_method)
        finish_method = utils.copy_func(getattr(cls, "_finish_case"), "{}.FinishCase".format(class_name))
        test_steps["{}.FinishCase".format(class_name)] = after_class(finish_method)

        # Generate test case groups
        groups = ["{}.{}".format(g, case_group) for g in cls.base_group]
        groups = cls.base_group + groups

        # Generate test case docstring
        test_steps["__doc__"] = "{}\n{}".format(cls.__doc__.splitlines()[0], "\n".join(scenario))
        ret = test(type(class_name, (cls,), test_steps), groups=groups)
        return ret
Exemple #2
0
def step(*args, **kwargs):
    from options import SKIP_DEPS
    """Uses Proboscis test decorator to create an ordered step."""
    if SKIP_DEPS:
        if 'depends_on' in kwargs:
            if 'runs_after' not in kwargs:
                kwargs['runs_after'] = kwargs['depends_on']
            del kwargs['depends_on']
    return test(*args, **kwargs)
Exemple #3
0
def step(*args, **kwargs):
    from options import SKIP_DEPS

    """Uses Proboscis test decorator to create an ordered step."""
    if SKIP_DEPS:
        if "depends_on" in kwargs:
            if "runs_after" not in kwargs:
                kwargs["runs_after"] = kwargs["depends_on"]
            del kwargs["depends_on"]
    return test(*args, **kwargs)
def wb_test(home=None, **kwargs):
    if not WHITE_BOX:
        kwargs.update(enabled=False)
    return test(home=home, **kwargs)
Exemple #5
0
class ClientTests(object):
    # set on the instance in login
    # wc = None  # webclient
    mm = None  # musicmanager
    mc = None  # mobileclient

    # These are set on the instance in eg create_song.

    # both are [TestSong]
    user_songs = None
    store_songs = None

    playlist_ids = None
    plentry_ids = None
    station_ids = None

    podcast_ids = None
    delete_podcast = True  # Set to false if user already subscribed to test podcast.

    @property
    def all_songs(self):
        return (self.user_songs or []) + (self.store_songs or [])

    def mc_get_playlist_songs(self, plid):
        """For convenience, since mc can only get all playlists at once."""
        all_contents = self.mc.get_all_user_playlist_contents()
        found = [p for p in all_contents if p['id'] == plid]

        assert_true(len(found), 1)

        return found[0]['tracks']

    @before_class
    def login(self):
        # self.wc = test_utils.new_test_client(Webclient)
        # assert_true(self.wc.is_authenticated())

        self.mm = test_utils.new_test_client(Musicmanager)
        assert_true(self.mm.is_authenticated())

        self.mc = test_utils.new_test_client(Mobileclient)
        assert_true(self.mc.is_authenticated())

    @after_class(always_run=True)
    def logout(self):
        # if self.wc is None:
        #     raise SkipTest('did not create wc')
        # assert_true(self.wc.logout())

        if self.mm is None:
            raise SkipTest('did not create mm')
        assert_true(self.mm.logout())

        if self.mc is None:
            raise SkipTest('did not create mc')
        assert_true(self.mc.logout())

    # This next section is a bit odd: it orders tests that create
    # required resources.

    # The intuitition: starting from an empty library, you need to create
    #  a song before you can eg add it to a playlist.

    # The dependencies end up with an ordering that might look like:
    #
    # with song
    #   with playlist
    #     with plentry
    #   with station
    #
    #
    # Suggestions to improve any of this are welcome!

    @staticmethod
    @retry
    def assert_songs_state(method, sids, present):
        """
        Assert presence/absence of sids and return a list of
        TestSongs found.

        :param method: eg self.mc.get_all_songs
        :param sids: list of song ids
        :param present: if True verify songs are present; False the opposite
        """

        library = method()

        found = [s for s in library if s['id'] in sids]

        expected_len = len(sids)
        if not present:
            expected_len = 0

        assert_equal(len(found), expected_len)

        return [
            TestSong(s['id'], s['title'], s['artist'], s['album'], s)
            for s in found
        ]

    @staticmethod
    @retry
    def assert_list_inc_equivalence(method, **kwargs):
        """
        Assert that some listing method returns the same
        contents for incremental=True/False.

        :param method: eg self.mc.get_all_songs, must support `incremental` kwarg
        :param **kwargs: passed to method
        """

        lib_chunk_gen = method(incremental=True, **kwargs)
        assert_true(isinstance(lib_chunk_gen, types.GeneratorType))

        assert_equal([e for chunk in lib_chunk_gen for e in chunk],
                     method(incremental=False, **kwargs))

    @test
    def song_create(self):
        # This can create more than one song: one through uploading, one through
        # adding a store track to the library.

        user_sids = []
        store_sids = []

        fname = test_utils.small_mp3

        uploaded, matched, not_uploaded = self.mm.upload(fname)

        if len(not_uploaded) == 1 and 'ALREADY_EXISTS' in not_uploaded[fname]:
            # delete the song if it exists already because a previous test failed
            self.mc.delete_songs(
                re.search(r'\(.*\)', not_uploaded[fname]).group().strip('()'))

            # and retry the upload
            uploaded, matched, not_uploaded = self.mm.upload(fname)

        # Otherwise, it should have been uploaded normally.
        assert_equal(not_uploaded, {})
        assert_equal(matched, {})
        assert_equal(list(uploaded.keys()), [fname])

        user_sids.append(uploaded[fname])

        if test_subscription_features():
            store_sids.append(self.mc.add_store_tracks(TEST_STORE_SONG_ID)[0])

        # we test get_all_songs here so that we can assume the existance
        # of the song for future tests (the servers take time to sync an upload)

        self.user_songs = self.assert_songs_state(self.mc.get_all_songs,
                                                  user_sids,
                                                  present=True)
        self.store_songs = self.assert_songs_state(self.mc.get_all_songs,
                                                   store_sids,
                                                   present=True)

    @test
    def playlist_create(self):
        mc_id = self.mc.create_playlist(TEST_PLAYLIST_NAME, "", public=True)
        # wc_id = self.wc.create_playlist(TEST_PLAYLIST_NAME, "", public=True)

        # like song_create, retry until the playlist appears
        @retry
        def assert_playlist_exists(plids):
            found = [
                p for p in self.mc.get_all_playlists() if p['id'] in plids
            ]

            assert_equal(len(found), 1)

        assert_playlist_exists([mc_id])
        self.playlist_ids = [mc_id]

    @test(depends_on=[playlist_create, song_create],
          runs_after_groups=['playlist.exists', 'song.exists'])
    def plentry_create(self):

        song_ids = [self.user_songs[0].sid]

        # create 3 entries total
        # 3 songs is the minimum to fully test reordering, and also includes the
        # duplicate song_id case
        double_id = self.user_songs[0].sid
        if test_subscription_features():
            double_id = TEST_STORE_SONG_ID

        song_ids += [double_id] * 2

        plentry_ids = self.mc.add_songs_to_playlist(self.playlist_ids[0],
                                                    song_ids)

        @retry
        def assert_plentries_exist(plid, plentry_ids):
            songs = self.mc_get_playlist_songs(plid)
            found = [e for e in songs if e['id'] in plentry_ids]

            assert_equal(len(found), len(plentry_ids))

        assert_plentries_exist(self.playlist_ids[0], plentry_ids)
        self.plentry_ids = plentry_ids

    @test(groups=['plentry'],
          depends_on=[plentry_create],
          runs_after_groups=['plentry.exists'],
          always_run=True)
    def plentry_delete(self):
        if self.plentry_ids is None:
            raise SkipTest('did not store self.plentry_ids')

        res = self.mc.remove_entries_from_playlist(self.plentry_ids)
        assert_equal(res, self.plentry_ids)

        @retry
        def assert_plentries_removed(plid, entry_ids):
            found = [
                e for e in self.mc_get_playlist_songs(plid)
                if e['id'] in entry_ids
            ]

            assert_equal(len(found), 0)

        assert_plentries_removed(self.playlist_ids[0], self.plentry_ids)

    @test(groups=['playlist'],
          depends_on=[playlist_create],
          runs_after=[plentry_delete],
          runs_after_groups=['playlist.exists'],
          always_run=True)
    def playlist_delete(self):
        if self.playlist_ids is None:
            raise SkipTest('did not store self.playlist_ids')

        for plid in self.playlist_ids:
            res = self.mc.delete_playlist(plid)
            assert_equal(res, plid)

        @retry
        def assert_playlist_does_not_exist(plid):
            found = [p for p in self.mc.get_all_playlists() if p['id'] == plid]

            assert_equal(len(found), 0)

        for plid in self.playlist_ids:
            assert_playlist_does_not_exist(plid)

    @test
    @subscription
    def station_create(self):
        station_ids = []
        for prefix, kwargs in (('Store song', {
                'track_id': TEST_STORE_SONG_ID
        }), ('Store-added song', {
                'track_id': self.store_songs[0].sid
        }), ('up song', {
                'track_id': self.user_songs[0].sid
        }), ('artist', {
                'artist_id': TEST_STORE_ARTIST_ID
        }), ('album', {
                'album_id': TEST_STORE_ALBUM_ID
        }), ('genre', {
                'genre_id': TEST_STORE_GENRE_ID
        }), ('playlist', {
                'playlist_token': TEST_PLAYLIST_SHARETOKEN
        }), ('curated station', {
                'curated_station_id': TEST_CURATED_STATION_ID
        })):
            station_ids.append(
                self.mc.create_station(prefix + ' ' + TEST_STATION_NAME,
                                       **kwargs))

        @retry
        def assert_station_exists(station_id):
            stations = self.mc.get_all_stations()

            found = [s for s in stations if s['id'] == station_id]

            assert_equal(len(found), 1)

        for station_id in station_ids:
            assert_station_exists(station_id)

        self.station_ids = station_ids

    @test(groups=['station'],
          depends_on=[station_create, song_create],
          runs_after_groups=['station.exists', 'song.exists'],
          always_run=True)
    def station_delete(self):
        if self.station_ids is None:
            raise SkipTest('did not store self.station_ids')

        res = self.mc.delete_stations(self.station_ids)
        assert_equal(res, self.station_ids)

        @retry
        def assert_station_deleted(station_id):
            stations = self.mc.get_all_stations()

            found = [s for s in stations if s['id'] == station_id]

            assert_equal(len(found), 0)

        for station_id in self.station_ids:
            assert_station_deleted(station_id)

    @test(groups=['song'],
          depends_on=[song_create],
          runs_after=[plentry_delete, station_delete],
          runs_after_groups=["song.exists"],
          always_run=True)
    def song_delete(self):
        # split deletion between wc and mc
        # mc is only to run if subscription testing not enabled
        with Check() as check:
            for i, testsong in enumerate(self.all_songs):
                if True:
                    res = self.mc.delete_songs(testsong.sid)
                else:
                    with warnings.catch_warnings():
                        warnings.simplefilter("ignore")
                        res = self.wc.delete_songs(testsong.sid)
                check.equal(res, [testsong.sid])

        self.assert_songs_state(self.mc.get_all_songs,
                                sids(self.all_songs),
                                present=False)

    @test
    def podcast_create(self):
        # Check to make sure podcast doesn't already exist to prevent deletion.
        already_exists = [
            pc for pc in self.mc.get_all_podcast_series()
            if pc['seriesId'] == TEST_PODCAST_SERIES_ID
        ]

        if already_exists:
            self.delete_podcast = False

        # like song_create, retry until the podcast appears
        @retry
        def assert_podcast_exists(pcids):
            found = [
                pc for pc in self.mc.get_all_podcast_series()
                if pc['seriesId'] in pcids
            ]

            assert_equal(len(found), 1)

        pc_id = self.mc.add_podcast_series(TEST_PODCAST_SERIES_ID)

        assert_podcast_exists([pc_id])
        self.podcast_ids = [pc_id]

    @test(groups=['podcast'],
          depends_on=[podcast_create],
          runs_after_groups=['podcast.exists'],
          always_run=True)
    def podcast_delete(self):
        if self.podcast_ids is None:
            raise SkipTest('did not store self.podcast_ids')

        if not self.delete_podcast:
            raise SkipTest('not deleting already existing podcast')

        for pcid in self.podcast_ids:
            res = self.mc.delete_podcast_series(pcid)
            assert_equal(res, pcid)

        @retry
        def assert_podcast_does_not_exist(pcid):
            found = [
                pc for pc in self.mc.get_all_podcast_series()
                if pc['seriesId'] == pcid
            ]

            assert_equal(len(found), 0)

        for pcid in self.podcast_ids:
            assert_podcast_does_not_exist(pcid)

    # These decorators just prevent setting groups and depends_on over and over.
    # They won't work right with additional settings; if that's needed this
    #  pattern should be factored out.

    # TODO it'd be nice to have per-client test groups
    song_test = test(groups=['song', 'song.exists'], depends_on=[song_create])
    playlist_test = test(groups=['playlist', 'playlist.exists'],
                         depends_on=[playlist_create])
    plentry_test = test(groups=['plentry', 'plentry.exists'],
                        depends_on=[plentry_create])
    station_test = test(groups=['station', 'station.exists'],
                        depends_on=[station_create])
    podcast_test = test(groups=['podcast', 'podcast.exists'],
                        depends_on=[podcast_create])

    # Non-wonky tests resume down here.

    # ---------
    #  MM tests
    # ---------

    def mm_get_quota(self):
        # just testing the call is successful
        self.mm.get_quota()

    @song_test
    def mm_list_new_songs(self):
        # mm only includes user-uploaded songs
        self.assert_songs_state(self.mm.get_uploaded_songs,
                                sids(self.user_songs),
                                present=True)
        self.assert_songs_state(self.mm.get_uploaded_songs,
                                sids(self.store_songs),
                                present=False)

    @test
    def mm_list_songs_inc_equal(self):
        self.assert_list_inc_equivalence(self.mm.get_uploaded_songs)

    @song_test
    def mm_download_song(self):
        @retry
        def assert_download(sid):
            filename, audio = self.mm.download_song(sid)

            # TODO could use original filename to verify this
            # but, when manually checking, got modified title occasionally
            assert_true(filename.endswith('.mp3'))
            assert_is_not_none(audio)

        assert_download(self.user_songs[0].sid)

    # ---------
    #  WC tests
    # ---------

    # @test
    # def wc_get_registered_devices(self):
    #     # no logic; just checking schema
    #     self.wc.get_registered_devices()

    # @test
    # def wc_get_shared_playlist_info(self):
    #     expected = {
    #         u'author': u'gmusic api',
    #         u'description': u'description here',
    #         u'title': u'public title here',
    #         u'num_tracks': 2
    #     }

    #     assert_equal(
    #         self.wc.get_shared_playlist_info(TEST_PLAYLIST_SHARETOKEN),
    #         expected
    #     )

    # @test
    # @subscription
    # def wc_get_store_stream_urls(self):
    #     urls = self.wc.get_stream_urls(TEST_STORE_SONG_ID)

    #     assert_true(len(urls) > 1)

    # @test
    # @subscription
    # def wc_stream_store_track_with_header(self):
    #     audio = self.wc.get_stream_audio(TEST_STORE_SONG_ID, use_range_header=True)

    #     assert_equal(md5(audio).hexdigest(), TEST_STORE_SONG_WC_HASH)

    # @test
    # @subscription
    # def wc_stream_store_track_without_header(self):
    #     audio = self.wc.get_stream_audio(TEST_STORE_SONG_ID, use_range_header=False)

    #     assert_equal(md5(audio).hexdigest(), TEST_STORE_SONG_WC_HASH)

    # @song_test
    # def wc_get_download_info(self):
    #     url, download_count = self.wc.get_song_download_info(self.user_songs[0].sid)

    #     assert_is_not_none(url)

    # @song_test
    # def wc_get_uploaded_stream_urls(self):
    #     urls = self.wc.get_stream_urls(self.user_songs[0].sid)

    #     assert_equal(len(urls), 1)

    #     url = urls[0]

    #     assert_is_not_none(url)
    #     assert_equal(url.split(':')[0], 'https')

    # @song_test
    # def wc_upload_album_art(self):
    #     url = self.wc.upload_album_art(self.user_songs[0].sid, test_utils.image_filename)
    #     assert_equal(url[:4], 'http')
    #     # TODO download the track and verify the metadata changed

    # ---------
    #  MC tests
    # ---------

    @test
    def mc_get_registered_devices(self):
        # no logic; just checking schema
        self.mc.get_registered_devices()

    @test
    def mc_get_browse_podcast_hierarchy(self):
        # no logic; just checking schema
        self.mc.get_browse_podcast_hierarchy()

    @test
    def mc_get_browse_podcast_series(self):
        # no logic; just checking schema
        self.mc.get_browse_podcast_series()

    @test
    def mc_get_listen_now_items(self):
        # no logic; just checking schema
        self.mc.get_listen_now_items()

    @test
    def mc_get_listen_now_situations(self):
        # no logic; just checking schema
        self.mc.get_listen_now_situations()

    @test
    def mc_list_stations_inc_equal(self):
        self.assert_list_inc_equivalence(self.mc.get_all_stations)

    @test
    def mc_list_shared_playlist_entries(self):
        entries = self.mc.get_shared_playlist_contents(
            TEST_PLAYLIST_SHARETOKEN)
        assert_true(len(entries) > 0)

    @test
    def mc_stream_podcast_episode(self):
        raise SkipTest('podcast ids keep changing')
        # uses frozen device_id
        # url = self.mc.get_podcast_episode_stream_url(TEST_PODCAST_EPISODE_ID)
        # audio = self.mc.session._rsession.get(url).content
        # assert_equal(md5(audio).hexdigest(), TEST_PODCAST_EPISODE_HASH)

    @test
    @subscription
    def mc_stream_store_track(self):
        url = self.mc.get_stream_url(
            TEST_STORE_SONG_ID)  # uses frozen device_id
        audio = self.mc.session._rsession.get(url).content
        assert_equal(md5(audio).hexdigest(), TEST_STORE_SONG_MC_HASH)

    @song_test
    def mc_get_uploaded_track_stream_url(self):
        url = self.mc.get_stream_url(self.user_songs[0].sid)

        assert_is_not_none(url)
        assert_equal(url[:4], 'http')

    @staticmethod
    @retry
    def _assert_song_key_equal_to(method, sid, key, value):
        """
        :param method: eg self.mc.get_all_songs
        :param sid: song id
        :param key: eg 'rating'
        :param value: eg '1'
        """
        songs = method()

        if not isinstance(songs, list):
            # kind of a hack to support get_track_info as well
            songs = [songs]

        found = [s for s in songs if id_or_nid(s) == sid]

        assert_equal(len(found), 1)

        assert_equal(found[0][key], value)
        return found[0]

    # how can I get the rating key to show up for store tracks?
    # it works in Google's clients!

    # @test
    # @subscription
    # def mc_change_store_song_rating(self):
    #     song = self.mc.get_track_info(TEST_STORE_SONG_ID)

    #     # increment by one but keep in rating range
    #     rating = int(song.get('rating', '0')) + 1
    #     rating = str(rating % 6)

    #     self.mc.rate_songs(song, rating)

    #     self._assert_song_key_equal_to(lambda: self.mc.get_track_info(TEST_STORE_SONG_ID),
    #                              id_or_nid(song),
    #                              song['rating'])

    @song_test
    def mc_change_uploaded_song_rating(self):
        song = self._assert_song_key_equal_to(self.mc.get_all_songs,
                                              self.all_songs[0].sid, 'rating',
                                              '0')  # initially unrated

        self.mc.rate_songs(song, 1)

        self._assert_song_key_equal_to(self.mc.get_all_songs, song['id'],
                                       'rating', '1')

        self.mc.rate_songs(song, 0)

    @song_test
    @retry
    def mc_get_promoted_songs(self):
        song = self.mc.get_track_info(TEST_STORE_SONG_ID)

        self.mc.rate_songs(song, 5)

        promoted = self.mc.get_promoted_songs()
        assert_true(len(promoted))

        self.mc.rate_songs(song, 0)

    def _test_increment_playcount(self, sid):
        matching = [t for t in self.mc.get_all_songs() if t['id'] == sid]
        assert_equal(len(matching), 1)

        # playCount is an optional field.
        initial_playcount = matching[0].get('playCount', 0)

        self.mc.increment_song_playcount(sid, 2)

        self._assert_song_key_equal_to(self.mc.get_all_songs, sid, 'playCount',
                                       initial_playcount + 2)

    @song_test
    def mc_increment_uploaded_song_playcount(self):
        self._test_increment_playcount(self.all_songs[0].sid)

    # Fails silently. See https://github.com/simon-weber/gmusicapi/issues/349.
    # @song_test
    # @subscription
    # def mc_increment_store_song_playcount(self):
    #     self._test_increment_playcount(self.all_songs[1].sid)

    @song_test
    def mc_change_uploaded_song_title_fails(self):
        # this used to work, but now only ratings can be changed.
        # this test is here so I can tell if this starts working again.
        song = self.assert_songs_state(self.mc.get_all_songs,
                                       [self.all_songs[0].sid],
                                       present=True)[0]

        old_title = song.title
        new_title = old_title + '_mod'
        # Mobileclient.change_song_metadata is deprecated, so
        # ignore its deprecation warning.
        with warnings.catch_warnings():
            warnings.simplefilter('ignore')
            self.mc.change_song_metadata({'id': song.sid, 'title': new_title})

        self._assert_song_key_equal_to(self.mc.get_all_songs, song.sid,
                                       'title', old_title)

    @song_test
    def mc_list_songs_inc_equal(self):
        self.assert_list_inc_equivalence(self.mc.get_all_songs)

    @song_test
    def mc_list_songs_updated_after(self):
        songs_last_minute = self.mc.get_all_songs(
            updated_after=datetime.datetime.now() -
            datetime.timedelta(minutes=1))
        assert_not_equal(len(songs_last_minute), 0)

        all_songs = self.mc.get_all_songs()
        assert_not_equal(len(songs_last_minute), len(all_songs))

    @podcast_test
    def mc_list_podcast_series_inc_equal(self):
        self.assert_list_inc_equivalence(self.mc.get_all_podcast_series)

    @playlist_test
    def mc_list_playlists_inc_equal(self):
        self.assert_list_inc_equivalence(self.mc.get_all_playlists)

    @playlist_test
    def mc_edit_playlist_name(self):
        new_name = TEST_PLAYLIST_NAME + '_mod'
        plid = self.mc.edit_playlist(self.playlist_ids[0], new_name=new_name)
        assert_equal(self.playlist_ids[0], plid)

        @retry  # change takes time to propogate
        def assert_name_equal(plid, name):
            playlists = self.mc.get_all_playlists()

            found = [p for p in playlists if p['id'] == plid]

            assert_equal(len(found), 1)
            assert_equal(found[0]['name'], name)

        assert_name_equal(self.playlist_ids[0], new_name)

        # revert
        self.mc.edit_playlist(self.playlist_ids[0],
                              new_name=TEST_PLAYLIST_NAME)
        assert_name_equal(self.playlist_ids[0], TEST_PLAYLIST_NAME)

    @playlist_test
    def mc_edit_playlist_description(self):
        new_description = TEST_PLAYLIST_DESCRIPTION + '_mod'
        plid = self.mc.edit_playlist(self.playlist_ids[0],
                                     new_description=new_description)
        assert_equal(self.playlist_ids[0], plid)

        @retry  # change takes time to propogate
        def assert_description_equal(plid, description):
            playlists = self.mc.get_all_playlists()

            found = [p for p in playlists if p['id'] == plid]

            assert_equal(len(found), 1)
            assert_equal(found[0]['description'], description)

        assert_description_equal(self.playlist_ids[0], new_description)

        # revert
        self.mc.edit_playlist(self.playlist_ids[0],
                              new_description=TEST_PLAYLIST_DESCRIPTION)
        assert_description_equal(self.playlist_ids[0],
                                 TEST_PLAYLIST_DESCRIPTION)

    @playlist_test
    def mc_edit_playlist_public(self):
        new_public = False
        plid = self.mc.edit_playlist(self.playlist_ids[0], public=new_public)
        assert_equal(self.playlist_ids[0], plid)

        @retry  # change takes time to propogate
        def assert_public_equal(plid, public):
            playlists = self.mc.get_all_playlists()

            found = [p for p in playlists if p['id'] == plid]

            assert_equal(len(found), 1)
            assert_equal(found[0]['accessControlled'], public)

        assert_public_equal(self.playlist_ids[0], new_public)

        # revert
        self.mc.edit_playlist(self.playlist_ids[0], public=True)
        assert_public_equal(self.playlist_ids[0], True)

    @playlist_test
    def mc_list_playlists_updated_after(self):
        pls_last_minute = self.mc.get_all_playlists(
            updated_after=datetime.datetime.now() -
            datetime.timedelta(minutes=1))
        assert_not_equal(len(pls_last_minute), 0)
        print(pls_last_minute)

        all_pls = self.mc.get_all_playlists()
        assert_not_equal(len(pls_last_minute), len(all_pls))

    @retry(tries=3)
    def _mc_assert_ple_position(self, entry, pos):
        """
        :param entry: entry dict
        :pos: 0-based position to assert
        """
        pl = self.mc_get_playlist_songs(entry['playlistId'])

        indices = [i for (i, e) in enumerate(pl) if e['id'] == entry['id']]

        assert_equal(len(indices), 1)

        assert_equal(indices[0], pos)

    @retry
    def _mc_test_ple_reodering(self, from_pos, to_pos):
        if from_pos == to_pos:
            raise ValueError('Will not test no-op reordering.')

        pl = self.mc_get_playlist_songs(self.playlist_ids[0])

        from_e = pl[from_pos]

        e_before_new_pos, e_after_new_pos = None, None

        if from_pos < to_pos:
            adj = 0
        else:
            adj = -1

        if to_pos - 1 >= 0:
            e_before_new_pos = pl[to_pos + adj]

        if to_pos + 1 < len(self.plentry_ids):
            e_after_new_pos = pl[to_pos + adj + 1]

        self.mc.reorder_playlist_entry(from_e,
                                       to_follow_entry=e_before_new_pos,
                                       to_precede_entry=e_after_new_pos)
        self._mc_assert_ple_position(from_e, to_pos)

        if e_before_new_pos:
            self._mc_assert_ple_position(e_before_new_pos, to_pos - 1)

        if e_after_new_pos:
            self._mc_assert_ple_position(e_after_new_pos, to_pos + 1)

    @plentry_test
    def mc_reorder_ple_forwards(self):
        for from_pos, to_pos in [
                pair
                for pair in itertools.product(range(len(self.plentry_ids)),
                                              repeat=2) if pair[0] < pair[1]
        ]:
            self._mc_test_ple_reodering(from_pos, to_pos)

    @plentry_test
    def mc_reorder_ple_backwards(self):
        playlist_len = len(self.plentry_ids)
        for from_pos, to_pos in [
                pair
                for pair in itertools.product(range(playlist_len), repeat=2)
                if pair[0] > pair[1]
        ]:
            self._mc_test_ple_reodering(from_pos, to_pos)

    # This fails, unfortunately, which means n reorderings mean n
    # separate calls in the general case.
    # @plentry_test
    # def mc_reorder_ples_forwards(self):
    #    pl = self.mc_get_playlist_songs(self.playlist_ids[0])
    #    # rot2, eg 0123 -> 2301
    #    pl.append(pl.pop(0))
    #    pl.append(pl.pop(0))

    #    mutate_call = mobileclient.BatchMutatePlaylistEntries
    #    mutations = [
    #        mutate_call.build_plentry_reorder(
    #            pl[-1], pl[-2]['clientId'], None),
    #        mutate_call.build_plentry_reorder(
    #            pl[-2], pl[-3]['clientId'], pl[-1]['clientId'])
    #    ]

    #    self.mc._make_call(mutate_call, [mutations])
    #    self._mc_assert_ple_position(pl[-1], len(pl) - 1)
    #    self._mc_assert_ple_position(pl[-2], len(pl) - 2)

    @station_test
    @retry  # sometimes this comes back with no data key
    @subscription
    def mc_list_station_tracks(self):
        for station_id in self.station_ids:
            self.mc.get_station_tracks(station_id, num_tracks=1)
            # used to assert that at least 1 track came back, but
            # our dummy uploaded track won't match anything
            self.mc.get_station_tracks(
                station_id,
                num_tracks=1,
                recently_played_ids=[TEST_STORE_SONG_ID])
            self.mc.get_station_tracks(
                station_id,
                num_tracks=1,
                recently_played_ids=[self.user_songs[0].sid])

    def mc_list_IFL_station_tracks(self):
        assert_equal(len(self.mc.get_station_tracks('IFL', num_tracks=1)), 1)

    @test(groups=['search'])
    def mc_search_store_no_playlists(self):
        res = self.mc.search('morning', max_results=100)

        res.pop(
            'genre_hits'
        )  # Genre cluster is returned but without results in the new response.

        # TODO playlist and situation results are not returned consistently.
        res.pop('playlist_hits')
        res.pop('situation_hits')

        with Check() as check:
            for type_, hits in res.items():
                if ((not test_subscription_features()
                     and type_ in ('artist_hits', 'song_hits', 'album_hits'))):
                    # These results aren't returned for non-sub accounts.
                    check.true(
                        len(hits) == 0,
                        "%s had %s hits, expected 0" % (type_, len(hits)))
                else:
                    check.true(
                        len(hits) > 0,
                        "%s had %s hits, expected > 0" % (type_, len(hits)))

    @test
    def mc_artist_info(self):
        aid = 'Apoecs6off3y6k4h5nvqqos4b5e'  # amorphis
        optional_keys = set(('albums', 'topTracks', 'related_artists'))

        include_all_res = self.mc.get_artist_info(aid,
                                                  include_albums=True,
                                                  max_top_tracks=1,
                                                  max_rel_artist=1)

        no_albums_res = self.mc.get_artist_info(aid, include_albums=False)
        no_rel_res = self.mc.get_artist_info(aid, max_rel_artist=0)
        no_tracks_res = self.mc.get_artist_info(aid, max_top_tracks=0)

        with Check() as check:
            check.true(
                set(include_all_res.keys()) & optional_keys == optional_keys)

            check.true(
                set(no_albums_res.keys()) & optional_keys == optional_keys -
                {'albums'})
            check.true(
                set(no_rel_res.keys()) & optional_keys == optional_keys -
                {'related_artists'})
            check.true(
                set(no_tracks_res.keys()) & optional_keys == optional_keys -
                {'topTracks'})

    @test
    @retry
    def mc_album_info(self):
        include_tracks = self.mc.get_album_info(TEST_STORE_ALBUM_ID,
                                                include_tracks=True)
        no_tracks = self.mc.get_album_info(TEST_STORE_ALBUM_ID,
                                           include_tracks=False)

        with Check() as check:
            check.true('tracks' in include_tracks)
            check.true('tracks' not in no_tracks)

            del include_tracks['tracks']
            check.equal(include_tracks, no_tracks)

    @test
    def mc_track_info(self):
        self.mc.get_track_info(TEST_STORE_SONG_ID)  # just for the schema

    @test
    def mc_podcast_series_info(self):
        optional_keys = {'episodes'}

        include_episodes = self.mc.get_podcast_series_info(
            TEST_PODCAST_SERIES_ID, max_episodes=1)
        no_episodes = self.mc.get_podcast_series_info(TEST_PODCAST_SERIES_ID,
                                                      max_episodes=0)

        with Check() as check:
            check.true(
                set(include_episodes.keys()) & optional_keys == optional_keys)

            check.true(
                set(no_episodes.keys()) & optional_keys == optional_keys -
                {'episodes'})

    @test(groups=['genres'])
    def mc_all_genres(self):
        expected_genres = {
            u'COMEDY_SPOKEN_WORD_OTHER', u'COUNTRY', u'HOLIDAY', u'R_B_SOUL',
            u'FOLK', u'LATIN', u'CHRISTIAN_GOSPEL', u'ALTERNATIVE_INDIE',
            u'POP', u'ROCK', u'WORLD', u'VOCAL_EASY_LISTENING', u'HIP_HOP_RAP',
            u'JAZZ', u'METAL', u'REGGAE_SKA', u'SOUNDTRACKS_CAST_ALBUMS',
            u'DANCE_ELECTRONIC', u'CLASSICAL', u'NEW_AGE', u'BLUES',
            u'CHILDREN_MUSIC'
        }
        res = self.mc.get_genres()
        assert_equal(set([e['id'] for e in res]), expected_genres)

    @test(groups=['genres'])
    def mc_specific_genre(self):
        expected_genres = {
            u'PROGRESSIVE_METAL', u'CLASSIC_METAL', u'HAIR_METAL',
            u'INDUSTRIAL', u'ALT_METAL', u'THRASH', u'METALCORE',
            u'BLACK_DEATH_METAL', u'DOOM_METAL'
        }
        res = self.mc.get_genres('METAL')
        assert_equal(set([e['id'] for e in res]), expected_genres)

    @test(groups=['genres'])
    def mc_leaf_parent_genre(self):
        assert_equal(self.mc.get_genres('AFRICA'), [])

    @test(groups=['genres'])
    def mc_invalid_parent_genre(self):
        assert_equal(self.mc.get_genres('bogus genre'), [])
Exemple #6
0
def wb_test(home=None, **kwargs):
    if not WHITE_BOX:
        kwargs.update(enabled=False)
    return test(home=home, **kwargs)
Exemple #7
0
from mock import MagicMock as Mock
from proboscis.asserts import (assert_raises, assert_true, assert_false,
                               assert_equal, assert_is_not, Check)
from proboscis import test

import gmusicapi.session
from gmusicapi.clients import Webclient, Musicmanager
from gmusicapi.exceptions import AlreadyLoggedIn  # ,NotLoggedIn
from gmusicapi.protocol.shared import authtypes
from gmusicapi.protocol import mobileclient
from gmusicapi.utils import utils

#TODO test gather_local, transcoding

#All tests end up in the local group.
test = test(groups=['local'])

##
# clients
##
# this feels like a dumb pattern, but I can't think of a better way
names = ('Webclient', 'Musicmanager')
Clients = namedtuple('Clients', [n.lower() for n in names])


def create_clients():
    clients = []
    for name in names:
        cls = getattr(gmusicapi.clients, name)
        c = cls()
class UpauthTests(object):
    #These are set on the instance in create_song/playlist.
    wc = None  # webclient
    mm = None  # musicmanager
    song = None
    playlist_id = None

    @before_class
    def login(self):
        self.wc = test_utils.new_test_client(Webclient)
        assert_true(self.wc.is_authenticated())

        self.mm = test_utils.new_test_client(Musicmanager)
        assert_true(self.mm.is_authenticated())

    @after_class(always_run=True)
    def logout(self):
        if self.wc is None:
            raise SkipTest('did not create wc')
        assert_true(self.wc.logout())

        if self.mm is None:
            raise SkipTest('did not create mm')
        assert_true(self.mm.logout())

    # This next section is a bit odd: it nests playlist tests inside song tests.

    # The intuitition: starting from an empty library, you need to have
    #  a song before you can modify a playlist.

    # If x --> y means x runs after y, then the graph looks like:

    #    song_create <-- playlist_create
    #        ^                   ^
    #        |                   |
    #    song_test       playlist_test
    #        ^                   ^
    #        |                   |
    #    song_delete     playlist_delete

    # Singleton groups are used to ease code ordering restraints.
    # Suggestions to improve any of this are welcome!

    @test
    def song_create(self):
        fname = test_utils.small_mp3

        uploaded, matched, not_uploaded = self.mm.upload(fname)

        if len(not_uploaded) == 1 and 'ALREADY_EXISTS' in not_uploaded[fname]:
            # If a previous test went wrong, the track might be there already.
            #TODO This build will fail because of the warning - is that what we want?
            assert_equal(matched, {})
            assert_equal(uploaded, {})

            sid = re.search(r'\(.*\)', not_uploaded[fname]).group().strip('()')
        else:
            # Otherwise, it should have been uploaded normally.
            assert_equal(not_uploaded, {})
            assert_equal(matched, {})
            assert_equal(uploaded.keys(), [fname])

            sid = uploaded[fname]

        # we test get_all_songs here so that we can assume the existance
        # of the song for future tests (the servers take time to sync an upload)
        @retry
        def assert_song_exists(sid):
            songs = self.wc.get_all_songs()

            found = [s for s in songs if s['id'] == sid] or None

            assert_is_not_none(found)
            assert_equal(len(found), 1)

            s = found[0]
            return TestSong(s['id'], s['title'], s['artist'], s['album'])

        self.song = assert_song_exists(sid)

    @test(depends_on=[song_create], runs_after_groups=['song.exists'])
    def playlist_create(self):
        self.playlist_id = self.wc.create_playlist(TEST_PLAYLIST_NAME)

        # like song_create, retry until the playlist appears

        @retry
        def assert_playlist_exists(plid):
            playlists = self.wc.get_all_playlist_ids(auto=False, user=True)

            found = playlists['user'].get(TEST_PLAYLIST_NAME, None)

            assert_is_not_none(found)
            assert_equal(found[-1], self.playlist_id)

        assert_playlist_exists(self.playlist_id)

    #TODO consider listing/searching if the id isn't there
    # to ensure cleanup.
    @test(groups=['playlist'],
          depends_on=[playlist_create],
          runs_after_groups=['playlist.exists'],
          always_run=True)
    def playlist_delete(self):
        if self.playlist_id is None:
            raise SkipTest('did not store self.playlist_id')

        res = self.wc.delete_playlist(self.playlist_id)
        assert_equal(res, self.playlist_id)

    @test(groups=['song'],
          depends_on=[song_create],
          runs_after=[playlist_delete],
          runs_after_groups=["song.exists"],
          always_run=True)
    def song_delete(self):
        if self.song is None:
            raise SkipTest('did not store self.song')

        res = self.wc.delete_songs(self.song.sid)

        assert_equal(res, [self.song.sid])

    # These decorators just prevent setting groups and depends_on over and over.
    # They won't work right with additional settings; if that's needed this
    #  pattern should be factored out.

    song_test = test(groups=['song', 'song.exists'], depends_on=[song_create])
    playlist_test = test(groups=['playlist', 'playlist.exists'],
                         depends_on=[playlist_create])

    # Non-wonky tests resume down here.

    #-----------
    # Song tests
    #-----------

    #TODO album art

    def _assert_get_song(self, sid, client=None):
        """Return the song dictionary with this sid.

        (GM has no native get for songs, just list).

        :param client: a Webclient or Musicmanager
        """
        if client is None:
            client = self.wc

        songs = client.get_all_songs()

        found = [s for s in songs if s['id'] == sid] or None

        assert_is_not_none(found)
        assert_equal(len(found), 1)

        return found[0]

    @song_test
    def list_songs_wc(self):
        self._assert_get_song(self.song.sid, self.wc)

    @song_test
    def list_songs_mm(self):
        self._assert_get_song(self.song.sid, self.mm)

    @staticmethod
    def _list_songs_incrementally(client):
        lib_chunk_gen = client.get_all_songs(incremental=True)
        assert_true(isinstance(lib_chunk_gen, types.GeneratorType))

        assert_equal([s for chunk in lib_chunk_gen for s in chunk],
                     client.get_all_songs(incremental=False))

    @song_test
    def list_songs_incrementally_wc(self):
        self._list_songs_incrementally(self.wc)

    @song_test
    def list_songs_incrementally_mm(self):
        self._list_songs_incrementally(self.mm)

    @song_test
    def change_metadata(self):
        orig_md = self._assert_get_song(self.song.sid)

        # Change all mutable entries.

        new_md = copy(orig_md)

        for name, expt in md_expectations.items():
            if name in orig_md and expt.mutable:
                old_val = orig_md[name]
                new_val = test_utils.modify_md(name, old_val)

                assert_not_equal(new_val, old_val)
                new_md[name] = new_val

        #TODO check into attempting to mutate non mutables
        self.wc.change_song_metadata(new_md)

        #Recreate the dependent md to what they should be (based on how orig_md was changed)
        correct_dependent_md = {}
        for name, expt in md_expectations.items():
            if expt.depends_on and name in orig_md:
                master_name = expt.depends_on
                correct_dependent_md[name] = expt.dependent_transformation(
                    new_md[master_name])

        @retry
        def assert_metadata_is(sid, orig_md, correct_dependent_md):
            result_md = self._assert_get_song(sid)

            with Check() as check:
                for name, expt in md_expectations.items():
                    if name in orig_md:
                        #TODO really need to factor out to test_utils?

                        #Check mutability if it's not volatile or dependent.
                        if not expt.volatile and expt.depends_on is None:
                            same, message = test_utils.md_entry_same(
                                name, orig_md, result_md)
                            check.equal(
                                not expt.mutable, same,
                                "metadata mutability incorrect: " + message)

                        #Check dependent md.
                        if expt.depends_on is not None:
                            same, message = test_utils.md_entry_same(
                                name, correct_dependent_md, result_md)
                            check.true(
                                same,
                                "dependent metadata incorrect: " + message)

        assert_metadata_is(self.song.sid, orig_md, correct_dependent_md)

        #Revert the metadata.
        self.wc.change_song_metadata(orig_md)

        @retry
        def assert_metadata_reverted(sid, orig_md):
            result_md = self._assert_get_song(sid)

            with Check() as check:
                for name in orig_md:
                    #If it's not volatile, it should be back to what it was.
                    if not md_expectations[name].volatile:
                        same, message = test_utils.md_entry_same(
                            name, orig_md, result_md)
                        check.true(same, "failed to revert: " + message)

        assert_metadata_reverted(self.song.sid, orig_md)

    #TODO verify these better?

    @song_test
    def get_download_info(self):
        url, download_count = self.wc.get_song_download_info(self.song.sid)

        assert_is_not_none(url)

    @song_test
    def download_song_mm(self):
        @retry
        def assert_download(sid=self.song.sid):
            filename, audio = self.mm.download_song(sid)

            # there's some kind of a weird race happening here with CI;
            # usually one will succeed and one will fail

            #TODO could use original filename to verify this
            # but, when manually checking, got modified title occasionally
            assert_true(filename.endswith('.mp3'))  # depends on specific file
            assert_is_not_none(audio)

        assert_download()

    @song_test
    def get_stream_url(self):
        url = self.wc.get_stream_url(self.song.sid)

        assert_is_not_none(url)

    @song_test
    def upload_album_art(self):
        orig_md = self._assert_get_song(self.song.sid)

        self.wc.upload_album_art(self.song.sid, test_utils.image_filename)

        self.wc.change_song_metadata(orig_md)
        #TODO redownload and verify against original?

    # these search tests are all skipped: see
    # https://github.com/simon-weber/Unofficial-Google-Music-API/issues/114

    @staticmethod
    def _assert_search_hit(res, hit_type, hit_key, val):
        """Assert that the result (returned from wc.search) has
        ``hit[hit_type][hit_key] == val`` for only one result in hit_type."""

        raise SkipTest('search is unpredictable (#114)')

        #assert_equal(sorted(res.keys()), ['album_hits', 'artist_hits', 'song_hits'])
        #assert_not_equal(res[hit_type], [])

        #hitmap = (hit[hit_key] == val for hit in res[hit_type])
        #assert_equal(sum(hitmap), 1)  # eg sum(True, False, True) == 2

    @song_test
    def search_title(self):
        res = self.wc.search(self.song.title)

        self._assert_search_hit(res, 'song_hits', 'id', self.song.sid)

    @song_test
    def search_artist(self):
        res = self.wc.search(self.song.artist)

        self._assert_search_hit(res, 'artist_hits', 'id', self.song.sid)

    @song_test
    def search_album(self):
        res = self.wc.search(self.song.album)

        self._assert_search_hit(res, 'album_hits', 'albumName',
                                self.song.album)

    #---------------
    # Playlist tests
    #---------------

    #TODO copy, change (need two songs?)

    @playlist_test
    def change_name(self):
        new_name = TEST_PLAYLIST_NAME + '_mod'
        self.wc.change_playlist_name(self.playlist_id, new_name)

        @retry  # change takes time to propogate
        def assert_name_equal(plid, name):
            playlists = self.wc.get_all_playlist_ids()

            found = playlists['user'].get(name, None)

            assert_is_not_none(found)
            assert_equal(found[-1], self.playlist_id)

        assert_name_equal(self.playlist_id, new_name)

        # revert
        self.wc.change_playlist_name(self.playlist_id, TEST_PLAYLIST_NAME)
        assert_name_equal(self.playlist_id, TEST_PLAYLIST_NAME)

    @playlist_test
    def add_remove(self):
        @retry
        def assert_song_order(plid, order):
            songs = self.wc.get_playlist_songs(plid)
            server_order = [s['id'] for s in songs]

            assert_equal(server_order, order)

        # initially empty
        assert_song_order(self.playlist_id, [])

        # add two copies
        self.wc.add_songs_to_playlist(self.playlist_id, [self.song.sid] * 2)
        assert_song_order(self.playlist_id, [self.song.sid] * 2)

        # remove all copies
        self.wc.remove_songs_from_playlist(self.playlist_id, self.song.sid)
        assert_song_order(self.playlist_id, [])
Exemple #9
0
from gmusicapi.protocol.shared import authtypes
from gmusicapi.protocol import mobileclient
from gmusicapi.utils import utils, jsarray

jsarray_samples = []
jsarray_filenames = [base + ".jsarray" for base in ("searchresult", "fetchartist")]

test_file_dir = os.path.dirname(os.path.abspath(__file__))
for filepath in [os.path.join(test_file_dir, p) for p in jsarray_filenames]:
    with open(filepath, "r") as f:
        jsarray_samples.append(f.read().decode("utf-8"))

# TODO test gather_local, transcoding

# All tests end up in the local group.
test = test(groups=["local"])


@test
def longest_increasing_sub():
    lisi = utils.longest_increasing_subseq
    assert_equal(lisi([]), [])
    assert_equal(lisi(range(10, 0, -1)), [1])
    assert_equal(lisi(range(10, 20)), range(10, 20))
    assert_equal(lisi([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]), [1, 2, 3, 5, 8, 9])


#
# clients
#
# this feels like a dumb pattern, but I can't think of a better way
Exemple #10
0
    def caseclass_factory(cls, case_group):
        """Create new cloned cls class contains only action methods"""
        test_steps, scenario = {}, []

        #  Generate human readable class_name, if was method docstring not
        #  described, use generated name
        class_name = "Case_{}__Config_{}".format(cls.__name__, case_group)

        #  Make methods for new testcase class, following by order
        scenario.append("    Scenario:")
        for step, action in enumerate(cls.get_actions_order()):
            n_action = action['action'].replace("_action_", "")
            #  Generate human readable method name, if was method docstring not
            #  described, use generated name. Used when method failed
            step_method_name = "{}.Step{:03d}_{}".format(
                class_name, step, n_action)

            method = utils.copy_func(action['method'], step_method_name)
            _step_name = getattr(action['method'], "__doc__").splitlines()[0]
            setattr(method, "_step_name",
                    "Step {:03d}. {}".format(step, _step_name))
            setattr(method, "_step_num", step)
            setattr(method, "_base_class", cls.__name__)
            setattr(method, "_config_case_group", case_group)

            #  Add step to scenario
            scenario.append("        {}. {}".format(step, _step_name))

            #  Add decorator to cloned method
            for deco in getattr(method, '_deferred_decorator_', []):
                method = deco(method)

            #  if not first step make dependency
            if step > 0:
                prev_step_name = "{}.Step{:03d}_{}".format(
                    class_name, step - 1,
                    cls.get_actions_order()[step - 1]['action'].replace(
                        "_action_", ""))
                depends = [test_steps[prev_step_name]]
            else:
                depends = None

            #  Add start-stop step decorator for measuring time and print
            #  start and finish info
            method = step_start_stop(method)

            test_steps[step_method_name] = test(method, depends_on=depends)

        #  Create before case methods, start case and setup
        start_method = utils.copy_func(getattr(cls, "_start_case"),
                                       "{}.StartCase".format(class_name))
        test_steps["{}.StartCase".format(class_name)] = before_class(
            start_method)

        if hasattr(cls, 'case_setup'):
            setup_method = utils.copy_func(getattr(cls, "case_setup"),
                                           "{}.CaseSetup".format(class_name))
            setattr(setup_method, "_step_name", "CaseSetup")
            test_steps["{}.CaseSetup".format(class_name)] = before_class(
                step_start_stop(setup_method), runs_after=[start_method])

        if hasattr(cls, 'case_teardown'):
            teardown_method = utils.copy_func(
                getattr(cls, "case_teardown"),
                "{}.CaseTeardown".format(class_name))
            setattr(teardown_method, "_step_name", "CaseTeardown")
            test_steps["{}.CaseTeardown".format(class_name)] = after_class(
                step_start_stop(teardown_method), always_run=True)
        else:
            teardown_method = None

        #  Create case methods, teardown and finish case
        finish_method = utils.copy_func(getattr(cls, "_finish_case"),
                                        "{}.FinishCase".format(class_name))
        test_steps["{}.FinishCase".format(class_name)] = after_class(
            finish_method,
            always_run=True,
            runs_after=[teardown_method] if teardown_method else [])

        # Generate test case groups
        groups = ['{}({})'.format(g, case_group) for g in cls.base_group]
        groups = cls.base_group + groups

        # Generate test case docstring
        test_steps["__doc__"] = "{}\n\n{}\n\nDuration {}".format(
            cls.__doc__.splitlines()[0], '\n'.join(scenario),
            getattr(cls, 'est_duration', '180m') or '180m')
        ret = test(type(class_name, (cls, ), test_steps), groups=groups)
        return ret
from gmusicapi.protocol.shared import authtypes
from gmusicapi.protocol import mobileclient
from gmusicapi.utils import utils, jsarray

jsarray_samples = []
jsarray_filenames = [base + '.jsarray' for base in ('searchresult', 'fetchartist')]

test_file_dir = os.path.dirname(os.path.abspath(__file__))
for filepath in [os.path.join(test_file_dir, p) for p in jsarray_filenames]:
    with open(filepath, 'r') as f:
        jsarray_samples.append(f.read().decode('utf-8'))

#TODO test gather_local, transcoding

#All tests end up in the local group.
test = test(groups=['local'])


@test
def longest_increasing_sub():
    lisi = utils.longest_increasing_subseq
    assert_equal(lisi([]), [])
    assert_equal(lisi(range(10, 0, -1)), [1])
    assert_equal(lisi(range(10, 20)), range(10, 20))
    assert_equal(lisi([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]),
                 [1, 2, 3, 5, 8, 9])

##
# clients
##
# this feels like a dumb pattern, but I can't think of a better way
class UpauthTests(object):
    # set on the instance in login
    wc = None  # webclient
    mm = None  # musicmanager
    mc = None  # mobileclient

    #These are set on the instance in create_song/playlist.
    songs = None  # [TestSong]
    playlist_id = None
    plentry_ids = None

    def mc_get_playlist_songs(self, plid):
        """For convenience, since mc can only get all playlists at once."""
        all_contents = self.mc.get_all_playlist_contents()
        found = [p for p in all_contents if p['id'] == plid]

        assert_true(len(found), 1)

        return found[0]['tracks']

    @before_class
    def login(self):
        self.wc = test_utils.new_test_client(Webclient)
        assert_true(self.wc.is_authenticated())

        self.mm = test_utils.new_test_client(Musicmanager)
        assert_true(self.mm.is_authenticated())

        self.mc = test_utils.new_test_client(Mobileclient)
        assert_true(self.mc.is_authenticated())

    @after_class(always_run=True)
    def logout(self):
        if self.wc is None:
            raise SkipTest('did not create wc')
        assert_true(self.wc.logout())

        if self.mm is None:
            raise SkipTest('did not create mm')
        assert_true(self.mm.logout())

        if self.mc is None:
            raise SkipTest('did not create mc')
        assert_true(self.mc.logout())

    # This next section is a bit odd: it orders tests that create
    # required resources.

    # The intuitition: starting from an empty library, you need to create
    #  a song before you can add it to a playlist.

    # If x --> y means x runs after y, then the graph looks like:

    #    song_create <-- plentry_create --> playlist_create
    #        ^                  ^                   ^
    #        |                  |                   |
    #    song_test       plentry_test       playlist_test
    #        ^                  ^                   ^
    #        |                  |                   |
    #    song_delete     plentry_delete     playlist_delete

    # Singleton groups are used to ease code ordering restraints.
    # Suggestions to improve any of this are welcome!

    @retry
    def assert_songs_state(self, sids, present):
        """
        Assert presence/absence of sids and return a list of
        TestSongs found.

        :param sids: list of song ids
        :param present: if True verify songs are present; False the opposite
        """

        library = self.mc.get_all_songs()

        found = [s for s in library if s['id'] in sids]

        expected_len = len(sids)
        if not present:
            expected_len = 0

        assert_equal(len(found), expected_len)

        return [
            TestSong(s['id'], s['title'], s['artist'], s['album'])
            for s in found
        ]

    @staticmethod
    @retry
    def assert_list_inc_equivalence(method, **kwargs):
        """
        Assert that some listing method returns the same
        contents for incremental=True/False.

        :param method: eg self.mc.get_all_songs
        :param **kwargs: passed to method
        """

        lib_chunk_gen = method(incremental=True, **kwargs)
        assert_true(isinstance(lib_chunk_gen, types.GeneratorType))

        assert_equal([e for chunk in lib_chunk_gen for e in chunk],
                     method(incremental=False, **kwargs))

    @staticmethod
    @retry
    def assert_list_with_deleted(method):
        """
        Assert that some listing method includes deleted tracks.

        :param method: eg self.mc.get_all_songs
        """
        lib = method(incremental=False, include_deleted=True)

        # how long do deleted tracks get returned for?
        # will this return tracks I've deleted since...ever?

        num_deleted = [t for t in lib if t['deleted']]
        assert_true(num_deleted > 0)

    @test
    def song_create(self):
        # This can create more than one song: one through uploading, one through
        # adding an AA track to the library.

        fname = test_utils.small_mp3

        uploaded, matched, not_uploaded = self.mm.upload(fname)

        sids = []

        if len(not_uploaded) == 1 and 'ALREADY_EXISTS' in not_uploaded[fname]:
            # If a previous test went wrong, the track might be there already.
            #TODO This build will fail because of the warning - is that what we want?
            assert_equal(matched, {})
            assert_equal(uploaded, {})

            sids.append(
                re.search(r'\(.*\)', not_uploaded[fname]).group().strip('()'))
        else:
            # Otherwise, it should have been uploaded normally.
            assert_equal(not_uploaded, {})
            assert_equal(matched, {})
            assert_equal(uploaded.keys(), [fname])

            sids.append(uploaded[fname])

        if test_all_access_features():
            sids.append(self.mc.add_aa_track(test_utils.aa_song_id))

        # we test get_all_songs here so that we can assume the existance
        # of the song for future tests (the servers take time to sync an upload)

        self.songs = self.assert_songs_state(sids, present=True)

    @test
    def playlist_create(self):
        playlist_id = self.mc.create_playlist(TEST_PLAYLIST_NAME)

        # like song_create, retry until the playlist appears
        @retry
        def assert_playlist_exists(plid):
            found = [p for p in self.mc.get_all_playlists() if p['id'] == plid]

            assert_equal(len(found), 1)

        assert_playlist_exists(playlist_id)
        self.playlist_id = playlist_id

    @test(depends_on=[playlist_create, song_create],
          runs_after_groups=['playlist.exists', 'song.exists'])
    def plentry_create(self):

        song_ids = [self.songs[0].sid]

        # create 3 entries
        # 3 songs is the minimum to fully test reordering, and also includes the
        # duplicate song_id case
        double_id = self.songs[0].sid
        if test_all_access_features():
            double_id = test_utils.aa_song_id

        song_ids += [double_id] * 2

        plentry_ids = self.mc.add_songs_to_playlist(self.playlist_id, song_ids)

        @retry(tries=2)
        def assert_plentries_exist(plid, plentry_ids):
            songs = self.mc_get_playlist_songs(plid)
            found = [e for e in songs if e['id'] in plentry_ids]

            assert_equal(len(found), len(plentry_ids))

        assert_plentries_exist(self.playlist_id, plentry_ids)
        self.plentry_ids = plentry_ids

    @test(groups=['plentry'],
          depends_on=[plentry_create],
          runs_after_groups=['plentry.exists'],
          always_run=True)
    def plentry_delete(self):
        if self.plentry_ids is None:
            raise SkipTest('did not store self.plentry_ids')

        res = self.mc.remove_entries_from_playlist(self.plentry_ids)
        assert_equal(res, self.plentry_ids)

        @retry
        def assert_plentries_removed(plid, entry_ids):
            found = [
                e for e in self.mc_get_playlist_songs(plid)
                if e['id'] in entry_ids
            ]

            assert_equal(len(found), 0)

        assert_plentries_removed(self.playlist_id, self.plentry_ids)
        #self.assert_list_with_deleted(self.mc_get_playlist_songs)

    @test(groups=['playlist'],
          depends_on=[playlist_create],
          runs_after=[plentry_delete],
          runs_after_groups=['playlist.exists'],
          always_run=True)
    def playlist_delete(self):
        if self.playlist_id is None:
            raise SkipTest('did not store self.playlist_id')

        res = self.mc.delete_playlist(self.playlist_id)
        assert_equal(res, self.playlist_id)

        @retry
        def assert_playlist_does_not_exist(plid):
            found = [
                p for p in self.mc.get_all_playlists(include_deleted=False)
                if p['id'] == plid
            ]

            assert_equal(len(found), 0)

        assert_playlist_does_not_exist(self.playlist_id)
        self.assert_list_with_deleted(self.mc.get_all_playlists)

    @test(groups=['song'],
          depends_on=[song_create],
          runs_after=[plentry_delete],
          runs_after_groups=["song.exists"],
          always_run=True)
    def song_delete(self):
        if self.songs is None:
            raise SkipTest('did not store self.songs')

        # split deletion between wc and mc
        # mc is the only to run if AA testing not enabled
        with Check() as check:
            for i, testsong in enumerate(self.songs):
                if i % 2 == 0:
                    res = self.mc.delete_songs(testsong.sid)
                else:
                    res = self.wc.delete_songs(testsong.sid)
                check.equal(res, [testsong.sid])

        self.assert_songs_state([s.sid for s in self.songs], present=False)
        self.assert_list_with_deleted(self.mc.get_all_songs)

    ## These decorators just prevent setting groups and depends_on over and over.
    ## They won't work right with additional settings; if that's needed this
    ##  pattern should be factored out.

    ##TODO it'd be nice to have per-client test groups
    song_test = test(groups=['song', 'song.exists'], depends_on=[song_create])
    playlist_test = test(groups=['playlist', 'playlist.exists'],
                         depends_on=[playlist_create])
    plentry_test = test(groups=['plentry', 'plentry.exists'],
                        depends_on=[plentry_create])

    ## Non-wonky tests resume down here.

    ##---------
    ## WC tests
    ##---------

    @test
    def wc_get_registered_devices(self):
        # no logic; just checking schema
        self.wc.get_registered_devices()

    ##---------
    ## MC tests
    ##---------

    @test
    def mc_list_stations_inc_equal(self):
        self.assert_list_inc_equivalence(self.mc.get_all_stations)

    @test
    def mc_list_stations_inc_equal_with_deleted(self):
        self.assert_list_inc_equivalence(self.mc.get_all_stations,
                                         include_deleted=True)

    @song_test
    def mc_list_songs_inc_equal(self):
        self.assert_list_inc_equivalence(self.mc.get_all_songs)

    @song_test
    def mc_list_songs_inc_equal_with_deleted(self):
        self.assert_list_inc_equivalence(self.mc.get_all_songs,
                                         include_deleted=True)

    @playlist_test
    def mc_list_playlists_inc_equal(self):
        self.assert_list_inc_equivalence(self.mc.get_all_playlists)

    @playlist_test
    def mc_list_playlists_inc_equal_with_deleted(self):
        self.assert_list_inc_equivalence(self.mc.get_all_playlists,
                                         include_deleted=True)

    @plentry_test
    def pt(self):
        raise SkipTest('remove this')

    #@plentry_test
    #def mc_list_plentries_inc_equal(self):
    #    self.assert_list_inc_equivalence(self.mc_get_playlist_songs, playlist_id=self.playlist_id)

    @test
    @all_access
    def mc_search_aa(self):
        res = self.mc.search_all_access('amorphis')
        with Check() as check:
            for hits in res.values():
                check.true(len(hits) > 0)
    def caseclass_factory(cls, case_group):
        """Create new cloned cls class contains only action methods"""
        test_steps, scenario = {}, []

        #  Generate human readable class_name, if was method docstring not
        #  described, use generated name
        class_name = "Case_{}__Config_{}".format(cls.__name__, case_group)

        #  Make methods for new testcase class, following by order
        scenario.append("    Scenario:")
        for step, action in enumerate(cls.get_actions_order()):
            n_action = action['action'].replace("_action_", "")
            #  Generate human readable method name, if was method docstring not
            #  described, use generated name. Used when method failed
            step_method_name = "{}.Step{:03d}_{}".format(class_name,
                                                         step,
                                                         n_action)

            method = utils.copy_func(action['method'], step_method_name)
            _step_name = getattr(action['method'],
                                 "__doc__").splitlines()[0]
            setattr(method, "_step_name", "Step {:03d}. {}".format(step,
                                                                   _step_name))
            setattr(method, "_step_num", step)
            setattr(method, "_base_class", cls.__name__)
            setattr(method, "_config_case_group", case_group)

            #  Add step to scenario
            scenario.append("        {}. {}".format(step, _step_name))

            #  Add decorator to cloned method
            for deco in getattr(method, '_deferred_decorator_', []):
                method = deco(method)

            #  if not first step make dependency
            if step > 0:
                prev_step_name = "{}.Step{:03d}_{}".format(
                    class_name,
                    step - 1,
                    cls.get_actions_order()[step - 1]['action'].replace(
                        "_action_", ""))
                depends = [test_steps[prev_step_name]]
            else:
                depends = None

            #  Add start-stop step decorator for measuring time and print
            #  start and finish info
            method = step_start_stop(method)

            test_steps[step_method_name] = test(
                method,
                depends_on=depends)

        #  Create before case methods, start case and setup
        start_method = utils.copy_func(
            getattr(cls, "_start_case"),
            "{}.StartCase".format(class_name))
        test_steps["{}.StartCase".format(class_name)] = before_class(
            start_method)

        if hasattr(cls, 'case_setup'):
            setup_method = utils.copy_func(
                getattr(cls, "case_setup"),
                "{}.CaseSetup".format(class_name))
            setattr(setup_method, "_step_name", "CaseSetup")
            test_steps["{}.CaseSetup".format(class_name)] = before_class(
                step_start_stop(setup_method), runs_after=[start_method])

        if hasattr(cls, 'case_teardown'):
            teardown_method = utils.copy_func(
                getattr(cls, "case_teardown"),
                "{}.CaseTeardown".format(class_name))
            setattr(teardown_method, "_step_name", "CaseTeardown")
            test_steps["{}.CaseTeardown".format(class_name)] = after_class(
                step_start_stop(teardown_method), always_run=True)
        else:
            teardown_method = None

        #  Create case methods, teardown and finish case
        finish_method = utils.copy_func(
            getattr(cls, "_finish_case"),
            "{}.FinishCase".format(class_name))
        test_steps["{}.FinishCase".format(class_name)] = after_class(
            finish_method, always_run=True,
            runs_after=[teardown_method] if teardown_method else [])

        # Generate test case groups
        groups = ['{}({})'.format(g, case_group) for g in cls._base_groups]
        groups = cls._base_groups + groups

        # Generate test case docstring
        test_steps["__doc__"] = "{}\n\n{}\n\nDuration {}".format(
            cls.__doc__.splitlines()[0],
            '\n'.join(scenario),
            getattr(cls, 'est_duration', '180m') or '180m')
        ret = test(
            type(class_name, (cls,), test_steps),
            groups=groups)
        return ret
class ClientTests(object):
    # set on the instance in login
    wc = None  # webclient
    mm = None  # musicmanager
    mc = None  # mobileclient

    #These are set on the instance in eg create_song.

    # both are [TestSong]
    user_songs = None
    aa_songs = None

    playlist_ids = None
    plentry_ids = None
    station_ids = None

    @property
    def all_songs(self):
        return (self.user_songs or []) + (self.aa_songs or [])

    def mc_get_playlist_songs(self, plid):
        """For convenience, since mc can only get all playlists at once."""
        all_contents = self.mc.get_all_user_playlist_contents()
        found = [p for p in all_contents if p['id'] == plid]

        assert_true(len(found), 1)

        return found[0]['tracks']

    @before_class
    def login(self):
        self.wc = test_utils.new_test_client(Webclient)
        assert_true(self.wc.is_authenticated())

        self.mm = test_utils.new_test_client(Musicmanager)
        assert_true(self.mm.is_authenticated())

        self.mc = test_utils.new_test_client(Mobileclient)
        assert_true(self.mc.is_authenticated())

    @after_class(always_run=True)
    def logout(self):
        if self.wc is None:
            raise SkipTest('did not create wc')
        assert_true(self.wc.logout())

        if self.mm is None:
            raise SkipTest('did not create mm')
        assert_true(self.mm.logout())

        if self.mc is None:
            raise SkipTest('did not create mc')
        assert_true(self.mc.logout())

    # This next section is a bit odd: it orders tests that create
    # required resources.

    # The intuitition: starting from an empty library, you need to create
    #  a song before you can eg add it to a playlist.

    # The dependencies end up with an ordering that might look like:
    #
    # with song
    #   with playlist
    #     with plentry
    #   with station
    #
    #
    # Suggestions to improve any of this are welcome!

    @staticmethod
    @retry
    def assert_songs_state(method, sids, present):
        """
        Assert presence/absence of sids and return a list of
        TestSongs found.

        :param method: eg self.mc.get_all_songs
        :param sids: list of song ids
        :param present: if True verify songs are present; False the opposite
        """

        library = method()

        found = [s for s in library if s['id'] in sids]

        expected_len = len(sids)
        if not present:
            expected_len = 0

        assert_equal(len(found), expected_len)

        return [
            TestSong(s['id'], s['title'], s['artist'], s['album'], s)
            for s in found
        ]

    @staticmethod
    @retry
    def assert_list_inc_equivalence(method, **kwargs):
        """
        Assert that some listing method returns the same
        contents for incremental=True/False.

        :param method: eg self.mc.get_all_songs, must support `incremental` kwarg
        :param **kwargs: passed to method
        """

        lib_chunk_gen = method(incremental=True, **kwargs)
        assert_true(isinstance(lib_chunk_gen, types.GeneratorType))

        assert_equal([e for chunk in lib_chunk_gen for e in chunk],
                     method(incremental=False, **kwargs))

    @staticmethod
    @retry
    def assert_listing_contains_deleted_items(method):
        """
        Assert that some listing method includes deleted tracks.

        :param method: eg self.mc.get_all_songs
        """
        lib = method(incremental=False, include_deleted=True)

        # how long do deleted tracks get returned for?
        # will this return tracks I've deleted since...ever?

        num_deleted = [t for t in lib if t['deleted']]
        assert_true(num_deleted > 0)

    @test
    def song_create(self):
        # This can create more than one song: one through uploading, one through
        # adding an AA track to the library.

        user_sids = []
        aa_sids = []

        fname = test_utils.small_mp3

        uploaded, matched, not_uploaded = self.mm.upload(fname)

        if len(not_uploaded) == 1 and 'ALREADY_EXISTS' in not_uploaded[fname]:
            # delete the song if it exists already because a previous test failed
            self.mc.delete_songs(
                re.search(r'\(.*\)', not_uploaded[fname]).group().strip('()'))

            # and retry the upload
            uploaded, matched, not_uploaded = self.mm.upload(fname)

        # Otherwise, it should have been uploaded normally.
        assert_equal(not_uploaded, {})
        assert_equal(matched, {})
        assert_equal(uploaded.keys(), [fname])

        user_sids.append(uploaded[fname])

        if test_all_access_features():
            aa_sids.append(self.mc.add_aa_track(TEST_AA_SONG_ID))

        # we test get_all_songs here so that we can assume the existance
        # of the song for future tests (the servers take time to sync an upload)

        self.user_songs = self.assert_songs_state(self.mc.get_all_songs,
                                                  user_sids,
                                                  present=True)
        self.aa_songs = self.assert_songs_state(self.mc.get_all_songs,
                                                aa_sids,
                                                present=True)

    @test
    def playlist_create(self):
        mc_id = self.mc.create_playlist(TEST_PLAYLIST_NAME)
        wc_id = self.wc.create_playlist(TEST_PLAYLIST_NAME, "", public=True)

        # like song_create, retry until the playlist appears
        @retry
        def assert_playlist_exists(plids):
            found = [
                p for p in self.mc.get_all_playlists() if p['id'] in plids
            ]

            assert_equal(len(found), 2)

        assert_playlist_exists([mc_id, wc_id])
        self.playlist_ids = [mc_id, wc_id]

    @test(depends_on=[playlist_create, song_create],
          runs_after_groups=['playlist.exists', 'song.exists'])
    def plentry_create(self):

        song_ids = [self.user_songs[0].sid]

        # create 3 entries total
        # 3 songs is the minimum to fully test reordering, and also includes the
        # duplicate song_id case
        double_id = self.user_songs[0].sid
        if test_all_access_features():
            double_id = TEST_AA_SONG_ID

        song_ids += [double_id] * 2

        plentry_ids = self.mc.add_songs_to_playlist(self.playlist_ids[0],
                                                    song_ids)

        @retry
        def assert_plentries_exist(plid, plentry_ids):
            songs = self.mc_get_playlist_songs(plid)
            found = [e for e in songs if e['id'] in plentry_ids]

            assert_equal(len(found), len(plentry_ids))

        assert_plentries_exist(self.playlist_ids[0], plentry_ids)
        self.plentry_ids = plentry_ids

    @test(groups=['plentry'],
          depends_on=[plentry_create],
          runs_after_groups=['plentry.exists'],
          always_run=True)
    def plentry_delete(self):
        if self.plentry_ids is None:
            raise SkipTest('did not store self.plentry_ids')

        res = self.mc.remove_entries_from_playlist(self.plentry_ids)
        assert_equal(res, self.plentry_ids)

        @retry
        def assert_plentries_removed(plid, entry_ids):
            found = [
                e for e in self.mc_get_playlist_songs(plid)
                if e['id'] in entry_ids
            ]

            assert_equal(len(found), 0)

        assert_plentries_removed(self.playlist_ids[0], self.plentry_ids)
        #self.assert_listing_contains_deleted_items(self.mc_get_playlist_songs)

    @test(groups=['playlist'],
          depends_on=[playlist_create],
          runs_after=[plentry_delete],
          runs_after_groups=['playlist.exists'],
          always_run=True)
    def playlist_delete(self):
        if self.playlist_ids is None:
            raise SkipTest('did not store self.playlist_ids')

        for plid in self.playlist_ids:
            res = self.mc.delete_playlist(plid)
            assert_equal(res, plid)

        @retry
        def assert_playlist_does_not_exist(plid):
            found = [
                p for p in self.mc.get_all_playlists(include_deleted=False)
                if p['id'] == plid
            ]

            assert_equal(len(found), 0)

        for plid in self.playlist_ids:
            assert_playlist_does_not_exist(plid)
            self.assert_listing_contains_deleted_items(
                self.mc.get_all_playlists)

    @test
    def station_create(self):
        if not test_all_access_features():
            raise SkipTest('AA testing not enabled')

        station_ids = []
        for prefix, kwargs in (('AA song', {
                'track_id': TEST_AA_SONG_ID
        }), ('AA-added song', {
                'track_id': self.aa_songs[0].sid
        }), ('up song', {
                'track_id': self.user_songs[0].sid
        }), ('artist', {
                'artist_id': TEST_AA_ARTIST_ID
        }), ('album', {
                'album_id': TEST_AA_ALBUM_ID
        }), ('genre', {
                'genre_id': TEST_AA_GENRE_ID
        })):
            station_ids.append(
                self.mc.create_station(prefix + ' ' + TEST_STATION_NAME,
                                       **kwargs))

        @retry
        def assert_station_exists(station_id):
            stations = self.mc.get_all_stations()

            found = [s for s in stations if s['id'] == station_id]

            assert_equal(len(found), 1)

        for station_id in station_ids:
            assert_station_exists(station_id)

        self.station_ids = station_ids

    @test(groups=['station'],
          depends_on=[station_create, song_create],
          runs_after_groups=['station.exists', 'song.exists'],
          always_run=True)
    def station_delete(self):
        if self.station_ids is None:
            raise SkipTest('did not store self.station_ids')

        res = self.mc.delete_stations(self.station_ids)
        assert_equal(res, self.station_ids)

        @retry
        def assert_station_deleted(station_id):
            stations = self.mc.get_all_stations()

            found = [s for s in stations if s['id'] == station_id]

            assert_equal(len(found), 0)

        for station_id in self.station_ids:
            assert_station_deleted(station_id)
        self.assert_listing_contains_deleted_items(self.mc.get_all_stations)

    @test(groups=['song'],
          depends_on=[song_create],
          runs_after=[plentry_delete, station_delete],
          runs_after_groups=["song.exists"],
          always_run=True)
    def song_delete(self):
        # split deletion between wc and mc
        # mc is the only to run if AA testing not enabled
        with Check() as check:
            for i, testsong in enumerate(self.all_songs):
                if i % 2 == 0:
                    res = self.mc.delete_songs(testsong.sid)
                else:
                    with warnings.catch_warnings():
                        warnings.simplefilter("ignore")
                        res = self.wc.delete_songs(testsong.sid)
                check.equal(res, [testsong.sid])

        self.assert_songs_state(self.mc.get_all_songs,
                                sids(self.all_songs),
                                present=False)
        self.assert_listing_contains_deleted_items(self.mc.get_all_songs)

    ## These decorators just prevent setting groups and depends_on over and over.
    ## They won't work right with additional settings; if that's needed this
    ##  pattern should be factored out.

    ##TODO it'd be nice to have per-client test groups
    song_test = test(groups=['song', 'song.exists'], depends_on=[song_create])
    playlist_test = test(groups=['playlist', 'playlist.exists'],
                         depends_on=[playlist_create])
    plentry_test = test(groups=['plentry', 'plentry.exists'],
                        depends_on=[plentry_create])
    station_test = test(groups=['station', 'station.exists'],
                        depends_on=[station_create])

    ## Non-wonky tests resume down here.

    ##---------
    ## MM tests
    ##---------

    @song_test
    def mm_list_new_songs(self):
        # mm only includes user-uploaded songs
        self.assert_songs_state(self.mm.get_uploaded_songs,
                                sids(self.user_songs),
                                present=True)
        self.assert_songs_state(self.mm.get_uploaded_songs,
                                sids(self.aa_songs),
                                present=False)

    @test
    def mm_list_songs_inc_equal(self):
        self.assert_list_inc_equivalence(self.mm.get_uploaded_songs)

    @song_test
    def mm_download_song(self):
        @retry
        def assert_download(sid):
            filename, audio = self.mm.download_song(sid)

            #TODO could use original filename to verify this
            # but, when manually checking, got modified title occasionally
            assert_true(filename.endswith('.mp3'))
            assert_is_not_none(audio)

        assert_download(self.user_songs[0].sid)

    ##---------
    ## WC tests
    ##---------

    @test
    def wc_get_registered_devices(self):
        # no logic; just checking schema
        self.wc.get_registered_devices()

    @test
    def wc_get_shared_playlist_info(self):
        expected = {
            u'author': u'gmusic api',
            u'description': u'description here',
            u'title': u'public title here',
            u'num_tracks': 2
        }

        assert_equal(
            self.wc.get_shared_playlist_info(TEST_PLAYLIST_SHARETOKEN),
            expected)

    @test
    @all_access
    def wc_get_aa_stream_urls(self):
        urls = self.wc.get_stream_urls(TEST_AA_SONG_ID)

        assert_true(len(urls) > 1)

    @test
    @all_access
    def wc_stream_aa_track_with_header(self):
        audio = self.wc.get_stream_audio(TEST_AA_SONG_ID,
                                         use_range_header=True)

        assert_equal(md5(audio).hexdigest(), TEST_AA_SONG_WC_HASH)

    @test
    @all_access
    def wc_stream_aa_track_without_header(self):
        audio = self.wc.get_stream_audio(TEST_AA_SONG_ID,
                                         use_range_header=False)

        assert_equal(md5(audio).hexdigest(), TEST_AA_SONG_WC_HASH)

    @song_test
    def wc_get_download_info(self):
        url, download_count = self.wc.get_song_download_info(
            self.user_songs[0].sid)

        assert_is_not_none(url)

    @song_test
    def wc_get_uploaded_stream_urls(self):
        urls = self.wc.get_stream_urls(self.user_songs[0].sid)

        assert_equal(len(urls), 1)

        url = urls[0]

        assert_is_not_none(url)
        assert_equal(url.split(':')[0], 'https')

    @song_test
    def wc_upload_album_art(self):
        url = self.wc.upload_album_art(self.user_songs[0].sid,
                                       test_utils.image_filename)
        assert_equal(url[:7], 'http://')
        #TODO download the track and verify the metadata changed

    ##---------
    ## MC tests
    ##---------

    @test
    def mc_list_stations_inc_equal(self):
        self.assert_list_inc_equivalence(self.mc.get_all_stations)

    @test
    def mc_list_stations_inc_equal_with_deleted(self):
        self.assert_list_inc_equivalence(self.mc.get_all_stations,
                                         include_deleted=True)

    @test
    def mc_list_shared_playlist_entries(self):
        entries = self.mc.get_shared_playlist_contents(
            TEST_PLAYLIST_SHARETOKEN)
        assert_true(len(entries) > 0)

    @test
    @all_access
    def mc_stream_aa_track(self):
        url = self.mc.get_stream_url(TEST_AA_SONG_ID)  # uses frozen device_id
        audio = self.mc.session._rsession.get(url).content
        assert_equal(md5(audio).hexdigest(), TEST_AA_SONG_MC_HASH)

    @song_test
    def mc_get_uploaded_track_stream_url(self):
        url = self.mc.get_stream_url(self.user_songs[0].sid)

        assert_is_not_none(url)
        assert_equal(url[:7], 'http://')

    @staticmethod
    @retry
    def _assert_song_key_equal_to(method, sid, key, value):
        """
        :param method: eg self.mc.get_all_songs
        :param sid: song id
        :param key: eg 'rating'
        :param value: eg '1'
        """
        songs = method()

        if not isinstance(songs, list):
            # kind of a hack to support get_track_info as well
            songs = [songs]

        found = [s for s in songs if id_or_nid(s) == sid]

        assert_equal(len(found), 1)

        assert_equal(found[0][key], value)
        return found[0]

    # how can I get the rating key to show up for store tracks?
    # it works in Google's clients!

    # @test
    # @all_access
    # def mc_change_store_song_rating(self):
    #     song = self.mc.get_track_info(TEST_AA_SONG_ID)

    #     # increment by one but keep in rating range
    #     song['rating'] = int(song.get('rating', '0')) + 1
    #     song['rating'] = str(song['rating'] % 6)

    #     self.mc.change_song_metadata(song)

    #     self._assert_song_key_equal_to(lambda: self.mc.get_track_info(TEST_AA_SONG_ID),
    #                              id_or_nid(song),
    #                              song['rating'])

    @song_test
    def mc_change_uploaded_song_rating(self):
        song = self._assert_song_key_equal_to(self.mc.get_all_songs,
                                              self.all_songs[0].sid, 'rating',
                                              '0')  # initially unrated

        song['rating'] = '1'
        self.mc.change_song_metadata(song)

        self._assert_song_key_equal_to(self.mc.get_all_songs, song['id'],
                                       'rating', '1')

        song['rating'] = '0'
        self.mc.change_song_metadata(song)

    @song_test
    @all_access
    def mc_get_thumbs_up_songs(self):
        song = self.mc.get_track_info(TEST_AA_SONG_ID)

        song['rating'] = '5'
        self.mc.change_song_metadata(song)

        thumbs_up_songs = self.mc.get_thumbs_up_songs()

        found = [e for e in thumbs_up_songs if e['nid'] == song['nid']]

        assert_equal(len(found), 1)

        song['rating'] = '0'
        self.mc.change_song_metadata(song)

    def _test_increment_playcount(self, sid):
        matching = [t for t in self.mc.get_all_songs() if t['id'] == sid]
        assert_equal(len(matching), 1)

        # playCount is an optional field.
        initial_playcount = matching[0].get('playCount', 0)

        self.mc.increment_song_playcount(sid, 2)

        self._assert_song_key_equal_to(self.mc.get_all_songs, sid, 'playCount',
                                       initial_playcount + 2)

    @song_test
    def mc_increment_uploaded_song_playcount(self):
        self._test_increment_playcount(self.all_songs[0].sid)

    @song_test
    @all_access
    def mc_increment_aa_song_playcount(self):
        self._test_increment_playcount(self.all_songs[1].sid)

    @song_test
    def mc_change_uploaded_song_title_fails(self):
        # this used to work, but now only ratings can be changed.
        # this test is here so I can tell if this starts working again.
        song = self.assert_songs_state(self.mc.get_all_songs,
                                       [self.all_songs[0].sid],
                                       present=True)[0]

        old_title = song.title
        new_title = old_title + '_mod'
        self.mc.change_song_metadata({'id': song.sid, 'title': new_title})

        self._assert_song_key_equal_to(self.mc.get_all_songs, song.sid,
                                       'title', old_title)

    @song_test
    def mc_list_songs_inc_equal(self):
        self.assert_list_inc_equivalence(self.mc.get_all_songs)

    @song_test
    def mc_list_songs_inc_equal_with_deleted(self):
        self.assert_list_inc_equivalence(self.mc.get_all_songs,
                                         include_deleted=True)

    @playlist_test
    def mc_list_playlists_inc_equal(self):
        self.assert_list_inc_equivalence(self.mc.get_all_playlists)

    @playlist_test
    def mc_list_playlists_inc_equal_with_deleted(self):
        self.assert_list_inc_equivalence(self.mc.get_all_playlists,
                                         include_deleted=True)

    @playlist_test
    def mc_change_playlist_name(self):
        new_name = TEST_PLAYLIST_NAME + '_mod'
        plid = self.mc.change_playlist_name(self.playlist_ids[0], new_name)
        assert_equal(self.playlist_ids[0], plid)

        @retry  # change takes time to propogate
        def assert_name_equal(plid, name):
            playlists = self.mc.get_all_playlists()

            found = [p for p in playlists if p['id'] == plid]

            assert_equal(len(found), 1)
            assert_equal(found[0]['name'], name)

        assert_name_equal(self.playlist_ids[0], new_name)

        # revert
        self.mc.change_playlist_name(self.playlist_ids[0], TEST_PLAYLIST_NAME)
        assert_name_equal(self.playlist_ids[0], TEST_PLAYLIST_NAME)

    @retry
    def _mc_assert_ple_position(self, entry, pos):
        """
        :param entry: entry dict
        :pos: 0-based position to assert
        """
        pl = self.mc_get_playlist_songs(entry['playlistId'])

        indices = [i for (i, e) in enumerate(pl) if e['id'] == entry['id']]

        assert_equal(len(indices), 1)

        assert_equal(indices[0], pos)

    @plentry_test
    def mc_reorder_ple_forwards(self):
        playlist_len = len(self.plentry_ids)
        for from_pos, to_pos in [
                pair
                for pair in itertools.product(range(playlist_len), repeat=2)
                if pair[0] < pair[1]
        ]:
            pl = self.mc_get_playlist_songs(self.playlist_ids[0])

            from_e = pl[from_pos]

            e_before_new_pos, e_after_new_pos = None, None

            if to_pos - 1 >= 0:
                e_before_new_pos = pl[to_pos]

            if to_pos + 1 < playlist_len:
                e_after_new_pos = pl[to_pos + 1]

            self.mc.reorder_playlist_entry(from_e,
                                           to_follow_entry=e_before_new_pos,
                                           to_precede_entry=e_after_new_pos)
            self._mc_assert_ple_position(from_e, to_pos)

            if e_before_new_pos:
                self._mc_assert_ple_position(e_before_new_pos, to_pos - 1)

            if e_after_new_pos:
                self._mc_assert_ple_position(e_after_new_pos, to_pos + 1)

    @plentry_test
    def mc_reorder_ple_backwards(self):
        playlist_len = len(self.plentry_ids)
        for from_pos, to_pos in [
                pair
                for pair in itertools.product(range(playlist_len), repeat=2)
                if pair[0] > pair[1]
        ]:
            pl = self.mc_get_playlist_songs(self.playlist_ids[0])

            from_e = pl[from_pos]

            e_before_new_pos, e_after_new_pos = None, None

            if to_pos - 1 >= 0:
                e_before_new_pos = pl[to_pos - 1]

            if to_pos + 1 < playlist_len:
                e_after_new_pos = pl[to_pos]

            self.mc.reorder_playlist_entry(from_e,
                                           to_follow_entry=e_before_new_pos,
                                           to_precede_entry=e_after_new_pos)
            self._mc_assert_ple_position(from_e, to_pos)

            if e_before_new_pos:
                self._mc_assert_ple_position(e_before_new_pos, to_pos - 1)

            if e_after_new_pos:
                self._mc_assert_ple_position(e_after_new_pos, to_pos + 1)

    # This fails, unfortunately, which means n reorderings mean n
    # separate calls in the general case.
    #@plentry_test
    #def mc_reorder_ples_forwards(self):
    #    pl = self.mc_get_playlist_songs(self.playlist_ids[0])
    #    # rot2, eg 0123 -> 2301
    #    pl.append(pl.pop(0))
    #    pl.append(pl.pop(0))

    #    mutate_call = mobileclient.BatchMutatePlaylistEntries
    #    mutations = [
    #        mutate_call.build_plentry_reorder(
    #            pl[-1], pl[-2]['clientId'], None),
    #        mutate_call.build_plentry_reorder(
    #            pl[-2], pl[-3]['clientId'], pl[-1]['clientId'])
    #    ]

    #    self.mc._make_call(mutate_call, [mutations])
    #    self._mc_assert_ple_position(pl[-1], len(pl) - 1)
    #    self._mc_assert_ple_position(pl[-2], len(pl) - 2)

    @station_test
    @all_access
    def mc_list_station_tracks(self):
        for station_id in self.station_ids:
            self.mc.get_station_tracks(station_id, num_tracks=1)
            # used to assert that at least 1 track came back, but
            # our dummy uploaded track won't match anything

    @all_access
    def mc_list_IFL_station_tracks(self):
        assert_equal(len(self.mc.get_station_tracks('IFL', num_tracks=1)), 1)

    @test
    @all_access
    def mc_search_aa(self):
        res = self.mc.search_all_access('amorphis')
        with Check() as check:
            for hits in res.values():
                check.true(len(hits) > 0)

    @test
    @all_access
    def mc_artist_info(self):
        aid = 'Apoecs6off3y6k4h5nvqqos4b5e'  # amorphis
        optional_keys = set(('albums', 'topTracks', 'related_artists'))

        include_all_res = self.mc.get_artist_info(aid,
                                                  include_albums=True,
                                                  max_top_tracks=1,
                                                  max_rel_artist=1)

        no_albums_res = self.mc.get_artist_info(aid, include_albums=False)
        no_rel_res = self.mc.get_artist_info(aid, max_rel_artist=0)
        no_tracks_res = self.mc.get_artist_info(aid, max_top_tracks=0)

        with Check() as check:
            check.true(
                set(include_all_res.keys()) & optional_keys == optional_keys)

            check.true(
                set(no_albums_res.keys()) & optional_keys == optional_keys -
                set(['albums']))
            check.true(
                set(no_rel_res.keys()) & optional_keys == optional_keys -
                set(['related_artists']))
            check.true(
                set(no_tracks_res.keys()) & optional_keys == optional_keys -
                set(['topTracks']))

    @test
    @retry
    @all_access
    def mc_album_info(self):
        include_tracks = self.mc.get_album_info(TEST_AA_ALBUM_ID,
                                                include_tracks=True)
        no_tracks = self.mc.get_album_info(TEST_AA_ALBUM_ID,
                                           include_tracks=False)

        with Check() as check:
            check.true('tracks' in include_tracks)
            check.true('tracks' not in no_tracks)

            del include_tracks['tracks']
            check.equal(include_tracks, no_tracks)

    @test
    @all_access
    def mc_track_info(self):
        self.mc.get_track_info(TEST_AA_SONG_ID)  # just for the schema

    @test
    @all_access
    def mc_genres(self):
        self.mc.get_genres()  # just for the schema
        self.mc.get_genres('METAL')  # just for the schema
    @before_class
    def setUp1(self):
        """BasicTest setUp. #1"""
        assert_true(True)

    before_class(depends_on=[setUp1])(setUp2)

@test(depends_on=[ClientTest, BasicTest])
def after_cls():
    """after_cls - Run this after the factory methods and BasicTest."""
    assert_true(True)



test(depends_on=[SetUp])(ClientTest)
test(depends_on=[SetUp])(BasicTest)


@test(depends_on_groups=["something_else"], groups=["normal"])
def sometime_later():
    """Run this after the factory method #3."""
    assert_true(True)

@test(groups=["before_one_more_thing"])
def sometime_before():
    """Run this before factory method #2."""
    assert_true(True)