/* * 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 androidx.test.InstrumentationRegistry.getContext; import static com.google.common.truth.Truth.assertThat; import android.test.MoreAsserts; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; 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 mChannelSortKeyComparator = new Comparator() { @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 expectedChannelList = mRecommender.recommendChannels(); List 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 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 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 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 getChannelIdListSorted() { return new ArrayList<>(mChannelRecordSortedMap.keySet()); } private void setChannelScores_scoreIncreasesAsChannelIdIncreases() { List 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 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); } } }