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