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
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 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)
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'), [])
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, [])
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
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)