diff options
Diffstat (limited to 'tests/unit/src/com/android/tv/recommendation/RecommenderTest.java')
-rw-r--r-- | tests/unit/src/com/android/tv/recommendation/RecommenderTest.java | 324 |
1 files changed, 324 insertions, 0 deletions
diff --git a/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java new file mode 100644 index 00000000..4f16d168 --- /dev/null +++ b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.recommendation; + +import android.test.AndroidTestCase; +import android.test.MoreAsserts; + +import com.android.tv.data.Channel; +import com.android.tv.recommendation.RecommendationUtils.ChannelRecordSortedMapHelper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class RecommenderTest extends AndroidTestCase { + private static final int DEFAULT_NUMBER_OF_CHANNELS = 5; + private static final long DEFAULT_WATCH_START_TIME_MS = + System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2); + private static final long DEFAULT_WATCH_END_TIME_MS = + System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1); + private static final long DEFAULT_MAX_WATCH_DURATION_MS = TimeUnit.HOURS.toMillis(1); + + private final Comparator<Channel> CHANNEL_SORT_KEY_COMPARATOR = new Comparator<Channel>() { + @Override + public int compare(Channel lhs, Channel rhs) { + return mRecommender.getChannelSortKey(lhs.getId()) + .compareTo(mRecommender.getChannelSortKey(rhs.getId())); + } + }; + private final Runnable START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS = new Runnable() { + @Override + public void run() { + // Add 4 channels in ChannelRecordMap for testing. Store the added channels to + // mChannels_1 ~ mChannels_4. They are sorted by channel id in increasing order. + mChannel_1 = mChannelRecordSortedMap.addChannel(); + mChannel_2 = mChannelRecordSortedMap.addChannel(); + mChannel_3 = mChannelRecordSortedMap.addChannel(); + mChannel_4 = mChannelRecordSortedMap.addChannel(); + } + }; + + private RecommendationDataManager mDataManager; + private Recommender mRecommender; + private FakeEvaluator mEvaluator; + private ChannelRecordSortedMapHelper mChannelRecordSortedMap; + private boolean mOnRecommenderReady; + private boolean mOnRecommendationChanged; + private Channel mChannel_1; + private Channel mChannel_2; + private Channel mChannel_3; + private Channel mChannel_4; + + public void setUp() throws Exception { + super.setUp(); + + mChannelRecordSortedMap = new ChannelRecordSortedMapHelper(getContext()); + mDataManager = RecommendationUtils + .createMockRecommendationDataManager(mChannelRecordSortedMap); + mChannelRecordSortedMap.resetRandom(RecommendationUtils.createTestRandom()); + } + + public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveNoScore() { + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + // Recommender doesn't recommend any channels because all channels are not recommended. + assertEquals(0, mRecommender.recommendChannels().size()); + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + assertEquals(0, mRecommender.recommendChannels(3).size()); + assertEquals(0, mRecommender.recommendChannels(4).size()); + assertEquals(0, mRecommender.recommendChannels(5).size()); + } + + public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveNoScore() { + createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + // Recommender recommends every channel because it recommends not-recommended channels too. + assertEquals(4, mRecommender.recommendChannels().size()); + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + assertEquals(3, mRecommender.recommendChannels(3).size()); + assertEquals(4, mRecommender.recommendChannels(4).size()); + assertEquals(4, mRecommender.recommendChannels(5).size()); + } + + public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveScore() { + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + setChannelScores_scoreIncreasesAsChannelIdIncreases(); + + // recommendChannels must be sorted by score in decreasing order. + // (i.e. sorted by channel ID in decreasing order in this case) + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(3), + mChannel_4, mChannel_3, mChannel_2); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(4), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(5), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + } + + public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveScore() { + createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + setChannelScores_scoreIncreasesAsChannelIdIncreases(); + + // recommendChannels must be sorted by score in decreasing order. + // (i.e. sorted by channel ID in decreasing order in this case) + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(3), + mChannel_4, mChannel_3, mChannel_2); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(4), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(5), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + } + + public void testRecommendChannels_includeRecommendedOnly_fewChannelsHaveScore() { + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + mEvaluator.setChannelScore(mChannel_1.getId(), 1.0); + mEvaluator.setChannelScore(mChannel_2.getId(), 1.0); + + // Only two channels are recommended because recommender doesn't recommend other channels. + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(), + mChannel_1, mChannel_2); + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(3), + mChannel_1, mChannel_2); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(4), + mChannel_1, mChannel_2); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(5), + mChannel_1, mChannel_2); + } + + public void testRecommendChannels_notIncludeRecommendedOnly_fewChannelsHaveScore() { + createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + mEvaluator.setChannelScore(mChannel_1.getId(), 1.0); + mEvaluator.setChannelScore(mChannel_2.getId(), 1.0); + + assertEquals(4, mRecommender.recommendChannels().size()); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels().subList(0, 2), + mChannel_1, mChannel_2); + + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + + assertEquals(3, mRecommender.recommendChannels(3).size()); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(3).subList(0, 2), + mChannel_1, mChannel_2); + + assertEquals(4, mRecommender.recommendChannels(4).size()); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(4).subList(0, 2), + mChannel_1, mChannel_2); + + assertEquals(4, mRecommender.recommendChannels(5).size()); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(5).subList(0, 2), + mChannel_1, mChannel_2); + } + + public void testGetChannelSortKey_recommendAllChannels() { + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + setChannelScores_scoreIncreasesAsChannelIdIncreases(); + + List<Channel> expectedChannelList = mRecommender.recommendChannels(); + List<Channel> channelList = Arrays.asList(mChannel_1, mChannel_2, mChannel_3, mChannel_4); + Collections.sort(channelList, CHANNEL_SORT_KEY_COMPARATOR); + + // Recommended channel list and channel list sorted by sort key must be the same. + MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray()); + assertSortKeyNotInvalid(channelList); + } + + public void testGetChannelSortKey_recommendFewChannels() { + // Test with recommending 3 channels. + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + setChannelScores_scoreIncreasesAsChannelIdIncreases(); + + List<Channel> expectedChannelList = mRecommender.recommendChannels(3); + // A channel which is not recommended by the recommender has to get an invalid sort key. + assertEquals(Recommender.INVALID_CHANNEL_SORT_KEY, + mRecommender.getChannelSortKey(mChannel_1.getId())); + + List<Channel> channelList = Arrays.asList(mChannel_2, mChannel_3, mChannel_4); + Collections.sort(channelList, CHANNEL_SORT_KEY_COMPARATOR); + + MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray()); + assertSortKeyNotInvalid(channelList); + } + + public void testListener_onRecommendationChanged() { + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + // FakeEvaluator doesn't recommend a channel with empty watch log. As every channel + // doesn't have a watch log, nothing is recommended and recommendation isn't changed. + assertFalse(mOnRecommendationChanged); + + // Set lastRecommendationUpdatedTimeUtcMs to check recommendation changed because, + // recommender has a minimum recommendation update period. + mRecommender.setLastRecommendationUpdatedTimeUtcMs( + System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10)); + long latestWatchEndTimeMs = DEFAULT_WATCH_START_TIME_MS; + for (long channelId : mChannelRecordSortedMap.keySet()) { + mEvaluator.setChannelScore(channelId, 1.0); + // Add a log to recalculate the recommendation score. + assertTrue(mChannelRecordSortedMap.addWatchLog(channelId, latestWatchEndTimeMs, + TimeUnit.MINUTES.toMillis(10))); + latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(10); + } + + // onRecommendationChanged must be called, because recommend channels are not empty, + // by setting score to each channel. + assertTrue(mOnRecommendationChanged); + } + + public void testListener_onRecommenderReady() { + createRecommender(true, new Runnable() { + @Override + public void run() { + mChannelRecordSortedMap.addChannels(DEFAULT_NUMBER_OF_CHANNELS); + mChannelRecordSortedMap.addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, + DEFAULT_WATCH_END_TIME_MS, DEFAULT_MAX_WATCH_DURATION_MS); + } + }); + + // After loading channels and watch logs are finished, recommender must be available to use. + assertTrue(mOnRecommenderReady); + } + + private void assertSortKeyNotInvalid(List<Channel> channelList) { + for (Channel channel : channelList) { + MoreAsserts.assertNotEqual(Recommender.INVALID_CHANNEL_SORT_KEY, + mRecommender.getChannelSortKey(channel.getId())); + } + } + + private void createRecommender(boolean includeRecommendedOnly, + Runnable startDataManagerRunnable) { + mRecommender = new Recommender(new Recommender.Listener() { + @Override + public void onRecommenderReady() { + mOnRecommenderReady = true; + } + @Override + public void onRecommendationChanged() { + mOnRecommendationChanged = true; + } + }, includeRecommendedOnly, mDataManager); + + mEvaluator = new FakeEvaluator(); + mRecommender.registerEvaluator(mEvaluator); + mChannelRecordSortedMap.setRecommender(mRecommender); + + // When mRecommender is instantiated, its dataManager will be started, and load channels + // and watch history data if it is not started. + if (startDataManagerRunnable != null) { + startDataManagerRunnable.run(); + mRecommender.onChannelRecordChanged(); + } + // After loading channels and watch history data are finished, + // RecommendationDataManager calls listener.onChannelRecordLoaded() + // which will be mRecommender.onChannelRecordLoaded(). + mRecommender.onChannelRecordLoaded(); + } + + private List<Long> getChannelIdListSorted() { + return new ArrayList<>(mChannelRecordSortedMap.keySet()); + } + + private void setChannelScores_scoreIncreasesAsChannelIdIncreases() { + List<Long> channelIdList = getChannelIdListSorted(); + double score = Math.pow(0.5, channelIdList.size()); + for (long channelId : channelIdList) { + // Channel with smaller id has smaller score than channel with higher id. + mEvaluator.setChannelScore(channelId, score); + score *= 2.0; + } + } + + private class FakeEvaluator extends Recommender.Evaluator { + private Map<Long, Double> mChannelScore = new HashMap<>(); + + @Override + public double evaluateChannel(long channelId) { + if (getRecommender().getChannelRecord(channelId) == null) { + return NOT_RECOMMENDED; + } + Double score = mChannelScore.get(channelId); + return score == null ? NOT_RECOMMENDED : score; + } + + public void setChannelScore(long channelId, double score) { + mChannelScore.put(channelId, score); + } + } +} |