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