RecommenderTest.java revision 07b043dc3db83d6d20f0e8513b946830ab00e37b
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.tv.recommendation; 18 19import android.test.AndroidTestCase; 20import android.test.MoreAsserts; 21import android.test.suitebuilder.annotation.SmallTest; 22 23import com.android.tv.data.Channel; 24import com.android.tv.recommendation.RecommendationUtils.ChannelRecordSortedMapHelper; 25 26import java.util.ArrayList; 27import java.util.Arrays; 28import java.util.Collections; 29import java.util.Comparator; 30import java.util.HashMap; 31import java.util.List; 32import java.util.Map; 33import java.util.concurrent.TimeUnit; 34 35@SmallTest 36public class RecommenderTest extends AndroidTestCase { 37 private static final int DEFAULT_NUMBER_OF_CHANNELS = 5; 38 private static final long DEFAULT_WATCH_START_TIME_MS = 39 System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2); 40 private static final long DEFAULT_WATCH_END_TIME_MS = 41 System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1); 42 private static final long DEFAULT_MAX_WATCH_DURATION_MS = TimeUnit.HOURS.toMillis(1); 43 44 private final Comparator<Channel> CHANNEL_SORT_KEY_COMPARATOR = new Comparator<Channel>() { 45 @Override 46 public int compare(Channel lhs, Channel rhs) { 47 return mRecommender.getChannelSortKey(lhs.getId()) 48 .compareTo(mRecommender.getChannelSortKey(rhs.getId())); 49 } 50 }; 51 private final Runnable START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS = new Runnable() { 52 @Override 53 public void run() { 54 // Add 4 channels in ChannelRecordMap for testing. Store the added channels to 55 // mChannels_1 ~ mChannels_4. They are sorted by channel id in increasing order. 56 mChannel_1 = mChannelRecordSortedMap.addChannel(); 57 mChannel_2 = mChannelRecordSortedMap.addChannel(); 58 mChannel_3 = mChannelRecordSortedMap.addChannel(); 59 mChannel_4 = mChannelRecordSortedMap.addChannel(); 60 } 61 }; 62 63 private RecommendationDataManager mDataManager; 64 private Recommender mRecommender; 65 private FakeEvaluator mEvaluator; 66 private ChannelRecordSortedMapHelper mChannelRecordSortedMap; 67 private boolean mOnRecommenderReady; 68 private boolean mOnRecommendationChanged; 69 private Channel mChannel_1; 70 private Channel mChannel_2; 71 private Channel mChannel_3; 72 private Channel mChannel_4; 73 74 @Override 75 public void setUp() throws Exception { 76 super.setUp(); 77 78 mChannelRecordSortedMap = new ChannelRecordSortedMapHelper(getContext()); 79 mDataManager = RecommendationUtils 80 .createMockRecommendationDataManager(mChannelRecordSortedMap); 81 mChannelRecordSortedMap.resetRandom(RecommendationUtils.createTestRandom()); 82 } 83 84 public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveNoScore() { 85 createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); 86 87 // Recommender doesn't recommend any channels because all channels are not recommended. 88 assertEquals(0, mRecommender.recommendChannels().size()); 89 assertEquals(0, mRecommender.recommendChannels(-5).size()); 90 assertEquals(0, mRecommender.recommendChannels(0).size()); 91 assertEquals(0, mRecommender.recommendChannels(3).size()); 92 assertEquals(0, mRecommender.recommendChannels(4).size()); 93 assertEquals(0, mRecommender.recommendChannels(5).size()); 94 } 95 96 public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveNoScore() { 97 createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); 98 99 // Recommender recommends every channel because it recommends not-recommended channels too. 100 assertEquals(4, mRecommender.recommendChannels().size()); 101 assertEquals(0, mRecommender.recommendChannels(-5).size()); 102 assertEquals(0, mRecommender.recommendChannels(0).size()); 103 assertEquals(3, mRecommender.recommendChannels(3).size()); 104 assertEquals(4, mRecommender.recommendChannels(4).size()); 105 assertEquals(4, mRecommender.recommendChannels(5).size()); 106 } 107 108 public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveScore() { 109 createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); 110 111 setChannelScores_scoreIncreasesAsChannelIdIncreases(); 112 113 // recommendChannels must be sorted by score in decreasing order. 114 // (i.e. sorted by channel ID in decreasing order in this case) 115 MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(), 116 mChannel_4, mChannel_3, mChannel_2, mChannel_1); 117 assertEquals(0, mRecommender.recommendChannels(-5).size()); 118 assertEquals(0, mRecommender.recommendChannels(0).size()); 119 MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(3), 120 mChannel_4, mChannel_3, mChannel_2); 121 MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(4), 122 mChannel_4, mChannel_3, mChannel_2, mChannel_1); 123 MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(5), 124 mChannel_4, mChannel_3, mChannel_2, mChannel_1); 125 } 126 127 public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveScore() { 128 createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); 129 130 setChannelScores_scoreIncreasesAsChannelIdIncreases(); 131 132 // recommendChannels must be sorted by score in decreasing order. 133 // (i.e. sorted by channel ID in decreasing order in this case) 134 MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(), 135 mChannel_4, mChannel_3, mChannel_2, mChannel_1); 136 assertEquals(0, mRecommender.recommendChannels(-5).size()); 137 assertEquals(0, mRecommender.recommendChannels(0).size()); 138 MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(3), 139 mChannel_4, mChannel_3, mChannel_2); 140 MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(4), 141 mChannel_4, mChannel_3, mChannel_2, mChannel_1); 142 MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(5), 143 mChannel_4, mChannel_3, mChannel_2, mChannel_1); 144 } 145 146 public void testRecommendChannels_includeRecommendedOnly_fewChannelsHaveScore() { 147 createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); 148 149 mEvaluator.setChannelScore(mChannel_1.getId(), 1.0); 150 mEvaluator.setChannelScore(mChannel_2.getId(), 1.0); 151 152 // Only two channels are recommended because recommender doesn't recommend other channels. 153 MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(), 154 mChannel_1, mChannel_2); 155 assertEquals(0, mRecommender.recommendChannels(-5).size()); 156 assertEquals(0, mRecommender.recommendChannels(0).size()); 157 MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(3), 158 mChannel_1, mChannel_2); 159 MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(4), 160 mChannel_1, mChannel_2); 161 MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(5), 162 mChannel_1, mChannel_2); 163 } 164 165 public void testRecommendChannels_notIncludeRecommendedOnly_fewChannelsHaveScore() { 166 createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); 167 168 mEvaluator.setChannelScore(mChannel_1.getId(), 1.0); 169 mEvaluator.setChannelScore(mChannel_2.getId(), 1.0); 170 171 assertEquals(4, mRecommender.recommendChannels().size()); 172 MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels().subList(0, 2), 173 mChannel_1, mChannel_2); 174 175 assertEquals(0, mRecommender.recommendChannels(-5).size()); 176 assertEquals(0, mRecommender.recommendChannels(0).size()); 177 178 assertEquals(3, mRecommender.recommendChannels(3).size()); 179 MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(3).subList(0, 2), 180 mChannel_1, mChannel_2); 181 182 assertEquals(4, mRecommender.recommendChannels(4).size()); 183 MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(4).subList(0, 2), 184 mChannel_1, mChannel_2); 185 186 assertEquals(4, mRecommender.recommendChannels(5).size()); 187 MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(5).subList(0, 2), 188 mChannel_1, mChannel_2); 189 } 190 191 public void testGetChannelSortKey_recommendAllChannels() { 192 createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); 193 194 setChannelScores_scoreIncreasesAsChannelIdIncreases(); 195 196 List<Channel> expectedChannelList = mRecommender.recommendChannels(); 197 List<Channel> channelList = Arrays.asList(mChannel_1, mChannel_2, mChannel_3, mChannel_4); 198 Collections.sort(channelList, CHANNEL_SORT_KEY_COMPARATOR); 199 200 // Recommended channel list and channel list sorted by sort key must be the same. 201 MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray()); 202 assertSortKeyNotInvalid(channelList); 203 } 204 205 public void testGetChannelSortKey_recommendFewChannels() { 206 // Test with recommending 3 channels. 207 createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); 208 209 setChannelScores_scoreIncreasesAsChannelIdIncreases(); 210 211 List<Channel> expectedChannelList = mRecommender.recommendChannels(3); 212 // A channel which is not recommended by the recommender has to get an invalid sort key. 213 assertEquals(Recommender.INVALID_CHANNEL_SORT_KEY, 214 mRecommender.getChannelSortKey(mChannel_1.getId())); 215 216 List<Channel> channelList = Arrays.asList(mChannel_2, mChannel_3, mChannel_4); 217 Collections.sort(channelList, CHANNEL_SORT_KEY_COMPARATOR); 218 219 MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray()); 220 assertSortKeyNotInvalid(channelList); 221 } 222 223 public void testListener_onRecommendationChanged() { 224 createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); 225 // FakeEvaluator doesn't recommend a channel with empty watch log. As every channel 226 // doesn't have a watch log, nothing is recommended and recommendation isn't changed. 227 assertFalse(mOnRecommendationChanged); 228 229 // Set lastRecommendationUpdatedTimeUtcMs to check recommendation changed because, 230 // recommender has a minimum recommendation update period. 231 mRecommender.setLastRecommendationUpdatedTimeUtcMs( 232 System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10)); 233 long latestWatchEndTimeMs = DEFAULT_WATCH_START_TIME_MS; 234 for (long channelId : mChannelRecordSortedMap.keySet()) { 235 mEvaluator.setChannelScore(channelId, 1.0); 236 // Add a log to recalculate the recommendation score. 237 assertTrue(mChannelRecordSortedMap.addWatchLog(channelId, latestWatchEndTimeMs, 238 TimeUnit.MINUTES.toMillis(10))); 239 latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(10); 240 } 241 242 // onRecommendationChanged must be called, because recommend channels are not empty, 243 // by setting score to each channel. 244 assertTrue(mOnRecommendationChanged); 245 } 246 247 public void testListener_onRecommenderReady() { 248 createRecommender(true, new Runnable() { 249 @Override 250 public void run() { 251 mChannelRecordSortedMap.addChannels(DEFAULT_NUMBER_OF_CHANNELS); 252 mChannelRecordSortedMap.addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, 253 DEFAULT_WATCH_END_TIME_MS, DEFAULT_MAX_WATCH_DURATION_MS); 254 } 255 }); 256 257 // After loading channels and watch logs are finished, recommender must be available to use. 258 assertTrue(mOnRecommenderReady); 259 } 260 261 private void assertSortKeyNotInvalid(List<Channel> channelList) { 262 for (Channel channel : channelList) { 263 MoreAsserts.assertNotEqual(Recommender.INVALID_CHANNEL_SORT_KEY, 264 mRecommender.getChannelSortKey(channel.getId())); 265 } 266 } 267 268 private void createRecommender(boolean includeRecommendedOnly, 269 Runnable startDataManagerRunnable) { 270 mRecommender = new Recommender(new Recommender.Listener() { 271 @Override 272 public void onRecommenderReady() { 273 mOnRecommenderReady = true; 274 } 275 @Override 276 public void onRecommendationChanged() { 277 mOnRecommendationChanged = true; 278 } 279 }, includeRecommendedOnly, mDataManager); 280 281 mEvaluator = new FakeEvaluator(); 282 mRecommender.registerEvaluator(mEvaluator); 283 mChannelRecordSortedMap.setRecommender(mRecommender); 284 285 // When mRecommender is instantiated, its dataManager will be started, and load channels 286 // and watch history data if it is not started. 287 if (startDataManagerRunnable != null) { 288 startDataManagerRunnable.run(); 289 mRecommender.onChannelRecordChanged(); 290 } 291 // After loading channels and watch history data are finished, 292 // RecommendationDataManager calls listener.onChannelRecordLoaded() 293 // which will be mRecommender.onChannelRecordLoaded(). 294 mRecommender.onChannelRecordLoaded(); 295 } 296 297 private List<Long> getChannelIdListSorted() { 298 return new ArrayList<>(mChannelRecordSortedMap.keySet()); 299 } 300 301 private void setChannelScores_scoreIncreasesAsChannelIdIncreases() { 302 List<Long> channelIdList = getChannelIdListSorted(); 303 double score = Math.pow(0.5, channelIdList.size()); 304 for (long channelId : channelIdList) { 305 // Channel with smaller id has smaller score than channel with higher id. 306 mEvaluator.setChannelScore(channelId, score); 307 score *= 2.0; 308 } 309 } 310 311 private class FakeEvaluator extends Recommender.Evaluator { 312 private final Map<Long, Double> mChannelScore = new HashMap<>(); 313 314 @Override 315 public double evaluateChannel(long channelId) { 316 if (getRecommender().getChannelRecord(channelId) == null) { 317 return NOT_RECOMMENDED; 318 } 319 Double score = mChannelScore.get(channelId); 320 return score == null ? NOT_RECOMMENDED : score; 321 } 322 323 public void setChannelScore(long channelId, double score) { 324 mChannelScore.put(channelId, score); 325 } 326 } 327} 328