blob: 812a3eb1bcc8d9afc1ec494251abf8f4f7d18993 [file] [log] [blame]
/*
* 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 static android.support.test.InstrumentationRegistry.getContext;
import static com.google.common.truth.Truth.assertThat;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.test.MoreAsserts;
import com.android.tv.data.api.Channel;
import com.android.tv.recommendation.RecommendationUtils.ChannelRecordSortedMapHelper;
import com.android.tv.testing.utils.Utils;
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;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class RecommenderTest {
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> mChannelSortKeyComparator =
new Comparator<Channel>() {
@Override
public int compare(Channel lhs, Channel rhs) {
return mRecommender
.getChannelSortKey(lhs.getId())
.compareTo(mRecommender.getChannelSortKey(rhs.getId()));
}
};
private final Runnable mStartDatamanagerRunnableAddFourChannels =
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;
@Before
public void setUp() {
mChannelRecordSortedMap = new ChannelRecordSortedMapHelper(getContext());
mDataManager =
RecommendationUtils.createMockRecommendationDataManager(mChannelRecordSortedMap);
mChannelRecordSortedMap.resetRandom(Utils.createTestRandom());
}
@Test
public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveNoScore() {
createRecommender(true, mStartDatamanagerRunnableAddFourChannels);
// Recommender doesn't recommend any channels because all channels are not recommended.
assertThat(mRecommender.recommendChannels()).isEmpty();
assertThat(mRecommender.recommendChannels(-5)).isEmpty();
assertThat(mRecommender.recommendChannels(0)).isEmpty();
assertThat(mRecommender.recommendChannels(3)).isEmpty();
assertThat(mRecommender.recommendChannels(4)).isEmpty();
assertThat(mRecommender.recommendChannels(5)).isEmpty();
}
@Test
public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveNoScore() {
createRecommender(false, mStartDatamanagerRunnableAddFourChannels);
// Recommender recommends every channel because it recommends not-recommended channels too.
assertThat(mRecommender.recommendChannels()).hasSize(4);
assertThat(mRecommender.recommendChannels(-5)).isEmpty();
assertThat(mRecommender.recommendChannels(0)).isEmpty();
assertThat(mRecommender.recommendChannels(3)).hasSize(3);
assertThat(mRecommender.recommendChannels(4)).hasSize(4);
assertThat(mRecommender.recommendChannels(5)).hasSize(4);
}
@Test
public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveScore() {
createRecommender(true, mStartDatamanagerRunnableAddFourChannels);
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);
assertThat(mRecommender.recommendChannels(-5)).isEmpty();
assertThat(mRecommender.recommendChannels(0)).isEmpty();
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);
}
@Test
public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveScore() {
createRecommender(false, mStartDatamanagerRunnableAddFourChannels);
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);
assertThat(mRecommender.recommendChannels(-5)).isEmpty();
assertThat(mRecommender.recommendChannels(0)).isEmpty();
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);
}
@Test
public void testRecommendChannels_includeRecommendedOnly_fewChannelsHaveScore() {
createRecommender(true, mStartDatamanagerRunnableAddFourChannels);
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);
assertThat(mRecommender.recommendChannels(-5)).isEmpty();
assertThat(mRecommender.recommendChannels(0)).isEmpty();
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);
}
@Test
public void testRecommendChannels_notIncludeRecommendedOnly_fewChannelsHaveScore() {
createRecommender(false, mStartDatamanagerRunnableAddFourChannels);
mEvaluator.setChannelScore(mChannel_1.getId(), 1.0);
mEvaluator.setChannelScore(mChannel_2.getId(), 1.0);
assertThat(mRecommender.recommendChannels()).hasSize(4);
MoreAsserts.assertContentsInAnyOrder(
mRecommender.recommendChannels().subList(0, 2), mChannel_1, mChannel_2);
assertThat(mRecommender.recommendChannels(-5)).isEmpty();
assertThat(mRecommender.recommendChannels(0)).isEmpty();
assertThat(mRecommender.recommendChannels(3)).hasSize(3);
MoreAsserts.assertContentsInAnyOrder(
mRecommender.recommendChannels(3).subList(0, 2), mChannel_1, mChannel_2);
assertThat(mRecommender.recommendChannels(4)).hasSize(4);
MoreAsserts.assertContentsInAnyOrder(
mRecommender.recommendChannels(4).subList(0, 2), mChannel_1, mChannel_2);
assertThat(mRecommender.recommendChannels(5)).hasSize(4);
MoreAsserts.assertContentsInAnyOrder(
mRecommender.recommendChannels(5).subList(0, 2), mChannel_1, mChannel_2);
}
@Test
public void testGetChannelSortKey_recommendAllChannels() {
createRecommender(true, mStartDatamanagerRunnableAddFourChannels);
setChannelScores_scoreIncreasesAsChannelIdIncreases();
List<Channel> expectedChannelList = mRecommender.recommendChannels();
List<Channel> channelList = Arrays.asList(mChannel_1, mChannel_2, mChannel_3, mChannel_4);
Collections.sort(channelList, mChannelSortKeyComparator);
// Recommended channel list and channel list sorted by sort key must be the same.
MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray());
assertSortKeyNotInvalid(channelList);
}
@Test
public void testGetChannelSortKey_recommendFewChannels() {
// Test with recommending 3 channels.
createRecommender(true, mStartDatamanagerRunnableAddFourChannels);
setChannelScores_scoreIncreasesAsChannelIdIncreases();
List<Channel> expectedChannelList = mRecommender.recommendChannels(3);
// A channel which is not recommended by the recommender has to get an invalid sort key.
assertThat(mRecommender.getChannelSortKey(mChannel_1.getId()))
.isEqualTo(Recommender.INVALID_CHANNEL_SORT_KEY);
List<Channel> channelList = Arrays.asList(mChannel_2, mChannel_3, mChannel_4);
Collections.sort(channelList, mChannelSortKeyComparator);
MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray());
assertSortKeyNotInvalid(channelList);
}
@Test
public void testListener_onRecommendationChanged() {
createRecommender(true, mStartDatamanagerRunnableAddFourChannels);
// 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.
assertThat(mOnRecommendationChanged).isFalse();
// 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.
assertThat(
mChannelRecordSortedMap.addWatchLog(
channelId, latestWatchEndTimeMs, TimeUnit.MINUTES.toMillis(10)))
.isTrue();
latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(10);
}
// onRecommendationChanged must be called, because recommend channels are not empty,
// by setting score to each channel.
assertThat(mOnRecommendationChanged).isTrue();
}
@Test
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.
assertThat(mOnRecommenderReady).isTrue();
}
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 final 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);
}
}
}