Home | History | Annotate | Download | only in recommendation
      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
     83     public void setUp() {
     84         mChannelRecordSortedMap = new ChannelRecordSortedMapHelper(getContext());
     85         mDataManager = RecommendationUtils
     86                 .createMockRecommendationDataManager(mChannelRecordSortedMap);
     87         mChannelRecordSortedMap.resetRandom(Utils.createTestRandom());
     88     }
     89 
     90     @Test
     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
    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
    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
    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
    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
    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
    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
    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
    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
    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 
    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 
    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 
    313     private List<Long> getChannelIdListSorted() {
    314         return new ArrayList<>(mChannelRecordSortedMap.keySet());
    315     }
    316 
    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
    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 
    339         public void setChannelScore(long channelId, double score) {
    340             mChannelScore.put(channelId, score);
    341         }
    342     }
    343 }
    344