blob: 8638d4b3b3d7f63cd31fea2e4dfecdd61e42a047 [file] [log] [blame]
// Copyright 2018 The Feed Authors.
//
// 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.google.android.libraries.feed.feedmodelprovider;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import com.google.android.libraries.feed.api.host.config.Configuration;
import com.google.android.libraries.feed.api.host.config.Configuration.ConfigKey;
import com.google.android.libraries.feed.api.host.logging.RequestReason;
import com.google.android.libraries.feed.api.internal.common.PayloadWithId;
import com.google.android.libraries.feed.api.internal.common.ThreadUtils;
import com.google.android.libraries.feed.api.internal.common.testing.ContentIdGenerators;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelChild;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelChild.Type;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelCursor;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelError;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelError.ErrorType;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelFeature;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelMutation;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelProvider;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelProvider.State;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelProvider.ViewDepthProvider;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelProviderObserver;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelToken;
import com.google.android.libraries.feed.api.internal.modelprovider.TokenCompleted;
import com.google.android.libraries.feed.api.internal.modelprovider.TokenCompletedObserver;
import com.google.android.libraries.feed.api.internal.sessionmanager.FeedSessionManager;
import com.google.android.libraries.feed.common.Result;
import com.google.android.libraries.feed.common.concurrent.TaskQueue;
import com.google.android.libraries.feed.common.concurrent.testing.FakeMainThreadRunner;
import com.google.android.libraries.feed.common.functional.Consumer;
import com.google.android.libraries.feed.common.time.TimingUtils;
import com.google.android.libraries.feed.feedmodelprovider.FeedModelProvider.InitializeModel;
import com.google.android.libraries.feed.feedmodelprovider.FeedModelProvider.TokenMutation;
import com.google.android.libraries.feed.feedmodelprovider.FeedModelProvider.TokenTracking;
import com.google.android.libraries.feed.feedmodelprovider.internal.UpdatableModelChild;
import com.google.android.libraries.feed.feedmodelprovider.internal.UpdatableModelToken;
import com.google.protobuf.ByteString;
import com.google.search.now.feed.client.StreamDataProto.StreamFeature;
import com.google.search.now.feed.client.StreamDataProto.StreamPayload;
import com.google.search.now.feed.client.StreamDataProto.StreamSharedState;
import com.google.search.now.feed.client.StreamDataProto.StreamStructure;
import com.google.search.now.feed.client.StreamDataProto.StreamStructure.Operation;
import com.google.search.now.feed.client.StreamDataProto.StreamToken;
import com.google.search.now.feed.client.StreamDataProto.UiContext;
import com.google.search.now.ui.stream.StreamStructureProto.Card;
import com.google.search.now.wire.feed.ContentIdProto.ContentId;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.robolectric.RobolectricTestRunner;
/** Tests of the {@link FeedModelProvider}. */
@RunWith(RobolectricTestRunner.class)
public class FeedModelProviderTest {
private static final String SESSION_ID = "session";
private final ContentIdGenerators idGenerators = new ContentIdGenerators();
private final String rootContentId = idGenerators.createRootContentId(0);
@Mock private TaskQueue taskQueue;
@Mock private FeedSessionManager feedSessionManager;
@Mock private ThreadUtils threadUtils;
@Mock private Configuration config;
private ModelChild continuationToken = null;
private TimingUtils timingUtils = new TimingUtils();
private FakeMainThreadRunner fakeMainThreadRunner;
private List<PayloadWithId> childBindings = new ArrayList<>();
@Before
public void setUp() {
initMocks(this);
childBindings.clear();
doAnswer(
invocation -> {
@SuppressWarnings("unchecked")
Consumer<Result<List<PayloadWithId>>> consumer =
(Consumer<Result<List<PayloadWithId>>>) invocation.getArguments()[1];
consumer.accept(Result.success(childBindings));
return null;
})
.when(feedSessionManager)
.getStreamFeatures(any(), any());
fakeMainThreadRunner = FakeMainThreadRunner.runTasksImmediately();
}
@Test
public void testMinimalModelProvider() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
assertThat(modelProvider.getCurrentState()).isEqualTo(State.INITIALIZING);
// Add a root to the model provider
ModelMutation mutator = modelProvider.edit();
assertThat(mutator).isNotNull();
StreamFeature rootStreamFeature = getRootFeature();
mutator.addChild(createStreamStructureAndBinding(rootStreamFeature));
mutator.commit();
assertThat(modelProvider.getCurrentState()).isEqualTo(State.READY);
// Verify that we have a root
ModelFeature rootFeature = modelProvider.getRootFeature();
assertThat(rootFeature).isNotNull();
assertThat(rootFeature.getStreamFeature()).isEqualTo(rootStreamFeature);
// Verify that the cursor exists but is at end
ModelCursor modelCursor = rootFeature.getCursor();
assertThat(modelCursor).isNotNull();
assertThat(modelCursor.isAtEnd()).isTrue();
}
@Test
public void testRemoveWithAppend() {
// Enable synthetic tokens.
initSyntheticTokenConfig(/* initialPageSize= */ 10, /* minPageSize= */ 0, /* pageSize= */ 0);
FeedModelProvider modelProvider = createFeedModelProvider();
assertThat(modelProvider.getCurrentState()).isEqualTo(State.INITIALIZING);
// Create a root with three children attached.
modelProvider
.edit()
.addChild(createStreamStructureAndBinding(getRootFeature()))
.addChild(createStreamStructureAndBinding(createFeature(1, rootContentId)))
.addChild(createStreamStructureAndBinding(createFeature(2, rootContentId)))
.addChild(createStreamStructureAndBinding(createFeature(3, rootContentId)))
.commit();
assertThat(modelProvider.getCurrentState()).isEqualTo(State.READY);
ModelFeature rootFeature = modelProvider.getRootFeature();
ModelCursor modelCursor = rootFeature.getCursor();
assertThat(modelCursor.getNextItem().getModelFeature().getStreamFeature())
.isEqualTo(childBindings.get(1).payload.getStreamFeature());
assertThat(modelCursor.getNextItem().getModelFeature().getStreamFeature())
.isEqualTo(childBindings.get(2).payload.getStreamFeature());
assertThat(modelCursor.getNextItem().getModelFeature().getStreamFeature())
.isEqualTo(childBindings.get(3).payload.getStreamFeature());
// Now remove the second and third children, and add more children.
modelProvider
.edit()
.removeChild(createRemove(rootContentId, createFeatureContentId(2)))
.removeChild(createRemove(rootContentId, createFeatureContentId(3)))
.addChild(createStreamStructureAndBinding(createFeature(4, rootContentId)))
.addChild(createStreamStructureAndBinding(createFeature(5, rootContentId)))
.commit();
// The children should now be #1, #4, and #5; they all should be bound.
modelCursor = rootFeature.getCursor();
assertThat(modelCursor.getNextItem().getModelFeature().getStreamFeature())
.isEqualTo(childBindings.get(1).payload.getStreamFeature());
assertThat(modelCursor.getNextItem().getModelFeature().getStreamFeature())
.isEqualTo(childBindings.get(4).payload.getStreamFeature());
assertThat(modelCursor.getNextItem().getModelFeature().getStreamFeature())
.isEqualTo(childBindings.get(5).payload.getStreamFeature());
}
@Test
public void testEmptyStream() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
modelProvider.edit().commit();
assertThat(modelProvider.getCurrentState()).isEqualTo(State.READY);
assertThat(modelProvider.getRootFeature()).isNull();
}
@Test
public void testCursor() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
int featureCnt = 2;
createTopLevelFeatures(modelProvider, featureCnt).commit();
ModelFeature rootFeature = modelProvider.getRootFeature();
assertThat(rootFeature).isNotNull();
ModelCursor modelCursor = rootFeature.getCursor();
assertThat(modelCursor).isNotNull();
assertThat(modelCursor.isAtEnd()).isFalse();
int cnt = 0;
while (modelCursor.getNextItem() != null) {
cnt++;
}
assertThat(cnt).isEqualTo(featureCnt);
assertThat(modelCursor.isAtEnd()).isTrue();
}
@Test
public void testRemove() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
int featureCnt = 2;
createTopLevelFeatures(modelProvider, featureCnt)
.removeChild(createRemove(rootContentId, createFeatureContentId(2)))
.commit();
ModelFeature rootFeature = modelProvider.getRootFeature();
assertThat(rootFeature).isNotNull();
ModelCursor modelCursor = rootFeature.getCursor();
// Verify that one of the features was removed by a the last operation
int cnt = 0;
while (modelCursor.getNextItem() != null) {
cnt++;
}
assertThat(cnt).isEqualTo(featureCnt - 1);
}
@Test
public void testMultiLevelCursors() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
int featureCnt = 3;
ModelMutation mutator = createTopLevelFeatures(modelProvider, featureCnt);
String featureParent = createFeatureContentId(2);
for (int i = 0; i < featureCnt; i++) {
mutator.addChild(
createStreamStructureAndBinding(createFeature(i + 1 + featureCnt, featureParent)));
}
mutator.commit();
ModelFeature rootFeature = modelProvider.getRootFeature();
assertThat(rootFeature).isNotNull();
ModelCursor modelCursor = rootFeature.getCursor();
assertThat(modelCursor).isNotNull();
assertThat(modelCursor.isAtEnd()).isFalse();
int cnt = 0;
ModelChild child;
while ((child = modelCursor.getNextItem()) != null) {
cnt++;
if (child.getType() == Type.FEATURE) {
ModelFeature feature = child.getModelFeature();
ModelCursor nextCursor = feature.getCursor();
while (nextCursor.getNextItem() != null) {
cnt++;
}
}
}
assertThat(cnt).isEqualTo(featureCnt + featureCnt);
}
@Test
public void testSharedState() {
ContentId contentId = ContentId.getDefaultInstance();
StreamSharedState streamSharedState = StreamSharedState.getDefaultInstance();
when(feedSessionManager.getSharedState(contentId)).thenReturn(streamSharedState);
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
assertThat(modelProvider.getSharedState(contentId)).isEqualTo(streamSharedState);
}
@Test
public void testTokenTracking() {
UpdatableModelToken continuationToken =
new UpdatableModelToken(StreamToken.getDefaultInstance(), false);
ArrayList<UpdatableModelChild> location = new ArrayList<>();
String parentContentId = "parent.content.id";
TokenTracking tokenTracking = new TokenTracking(continuationToken, parentContentId, location);
assertThat(tokenTracking.tokenChild).isEqualTo(continuationToken);
assertThat(tokenTracking.parentContentId).isEqualTo(parentContentId);
assertThat(tokenTracking.location).isEqualTo(location);
}
@Test
public void testHandleToken() {
StreamToken streamToken = StreamToken.getDefaultInstance();
ModelToken modelToken = mock(ModelToken.class);
when(modelToken.getStreamToken()).thenReturn(streamToken);
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
modelProvider.sessionId = SESSION_ID;
modelProvider.handleToken(modelToken);
verify(feedSessionManager).handleToken(SESSION_ID, streamToken);
}
@Test
public void testForceRefresh() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
modelProvider.sessionId = SESSION_ID;
modelProvider.triggerRefresh(RequestReason.OPEN_WITH_CONTENT);
assertThat(modelProvider.getDelayedTriggerRefreshForTest()).isFalse();
verify(feedSessionManager)
.triggerRefresh(
SESSION_ID, RequestReason.OPEN_WITH_CONTENT, UiContext.getDefaultInstance());
}
@Test
public void testForceRefresh_differentReason() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
modelProvider.sessionId = SESSION_ID;
modelProvider.triggerRefresh(RequestReason.HOST_REQUESTED);
assertThat(modelProvider.getDelayedTriggerRefreshForTest()).isFalse();
verify(feedSessionManager)
.triggerRefresh(SESSION_ID, RequestReason.HOST_REQUESTED, UiContext.getDefaultInstance());
}
@Test
public void testForceRefresh_delayed() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
assertThat(modelProvider.getDelayedTriggerRefreshForTest()).isFalse();
assertThat(modelProvider.getRequestReasonForTest()).isEqualTo(RequestReason.UNKNOWN);
modelProvider.triggerRefresh(RequestReason.HOST_REQUESTED);
assertThat(modelProvider.getDelayedTriggerRefreshForTest()).isTrue();
assertThat(modelProvider.getRequestReasonForTest()).isEqualTo(RequestReason.HOST_REQUESTED);
verify(feedSessionManager, never()).triggerRefresh(eq(null), anyInt(), any(UiContext.class));
}
@Test
public void testInvalidate() {
// Create a valid model
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
StreamFeature rootStreamFeature = getRootFeature();
modelProvider.edit().addChild(createStreamStructureAndBinding(rootStreamFeature)).commit();
assertThat(modelProvider.getCurrentState()).isEqualTo(State.READY);
ModelFeature rootFeature = modelProvider.getRootFeature();
assertThat(rootFeature).isNotNull();
ModelCursor modelCursor = rootFeature.getCursor();
modelProvider.invalidate();
assertThat(modelProvider.getCurrentState()).isEqualTo(State.INVALIDATED);
assertThat(modelCursor.isAtEnd()).isTrue();
}
@Test
public void testInvalidate_unregister() {
fakeMainThreadRunner = FakeMainThreadRunner.queueAllTasks();
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
fakeMainThreadRunner.runAllTasks();
ModelProviderObserver observer = mock(ModelProviderObserver.class);
modelProvider.registerObserver(observer);
modelProvider.invalidate();
modelProvider.unregisterObserver(observer);
fakeMainThreadRunner.runAllTasks();
verify(observer, never()).onSessionFinished(UiContext.getDefaultInstance());
}
@Test
public void testObserverLifecycle() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
ModelProviderObserver observer1 = mock(ModelProviderObserver.class);
modelProvider.registerObserver(observer1);
verify(observer1, never()).onSessionStart(UiContext.getDefaultInstance());
StreamFeature rootStreamFeature = getRootFeature();
modelProvider.edit().addChild(createStreamStructureAndBinding(rootStreamFeature)).commit();
verify(observer1).onSessionStart(UiContext.getDefaultInstance());
ModelProviderObserver observer2 = mock(ModelProviderObserver.class);
modelProvider.registerObserver(observer2);
verify(observer2).onSessionStart(UiContext.getDefaultInstance());
modelProvider.invalidate();
verify(observer1).onSessionFinished(UiContext.getDefaultInstance());
verify(observer2).onSessionFinished(UiContext.getDefaultInstance());
ModelProviderObserver observer3 = mock(ModelProviderObserver.class);
modelProvider.registerObserver(observer3);
verify(observer3).onSessionFinished(UiContext.getDefaultInstance());
}
@Test
public void testObserverLifecycle_resetRoot() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
ModelProviderObserver observer = mock(ModelProviderObserver.class);
modelProvider.registerObserver(observer);
verify(observer, never()).onSessionStart(UiContext.getDefaultInstance());
StreamFeature rootStreamFeature = getRootFeature();
modelProvider.edit().addChild(createStreamStructureAndBinding(rootStreamFeature)).commit();
verify(observer).onSessionStart(UiContext.getDefaultInstance());
String anotherRootId = idGenerators.createRootContentId(100);
rootStreamFeature = StreamFeature.newBuilder().setContentId(anotherRootId).build();
modelProvider.edit().addChild(createStreamStructureAndBinding(rootStreamFeature)).commit();
verify(observer).onSessionFinished(UiContext.getDefaultInstance());
assertThat(modelProvider.getCurrentState()).isEqualTo(State.INVALIDATED);
}
@Test
public void testObserverList() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
ModelProviderObserver observer = mock(ModelProviderObserver.class);
modelProvider.registerObserver(observer);
Collection<ModelProviderObserver> observers = modelProvider.getObserversToNotify();
assertThat(observers.size()).isEqualTo(1);
assertThat(observers).contains(observer);
}
@Test
public void testRaiseError_noCards() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
ModelProviderObserver observer = mock(ModelProviderObserver.class);
modelProvider.registerObserver(observer);
ModelError modelError = new ModelError(ErrorType.NO_CARDS_ERROR, null);
modelProvider.raiseError(modelError);
verify(observer).onError(modelError);
}
@Test
public void testRaiseError_pagination() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
int featureCnt = 3;
StreamToken streamToken = initializeStreamWithToken(modelProvider, featureCnt);
ModelChild tokenChild = getContinuationToken(modelProvider.getRootFeature());
assertThat(tokenChild).isNotNull();
ModelError modelError =
new ModelError(ErrorType.PAGINATION_ERROR, streamToken.getNextPageToken());
TokenCompletedObserver observer = mock(TokenCompletedObserver.class);
tokenChild.getModelToken().registerObserver(observer);
modelProvider.raiseError(modelError);
verify(observer).onError(modelError);
}
@Test
public void testInitializationModelMutationHandler() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
ModelProviderObserver observer = mock(ModelProviderObserver.class);
modelProvider.registerObserver(observer);
assertThat(modelProvider.getCurrentState()).isEqualTo(State.INITIALIZING);
InitializeModel initializeModel =
modelProvider.new InitializeModel(UiContext.getDefaultInstance());
initializeModel.postMutation();
assertThat(modelProvider.getCurrentState()).isEqualTo(State.READY);
verify(observer).onSessionStart(UiContext.getDefaultInstance());
}
@Test
public void testTokens() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
int featureCnt = 3;
StreamToken streamToken = initializeStreamWithToken(modelProvider, featureCnt);
ModelFeature rootFeature = modelProvider.getRootFeature();
assertThat(rootFeature).isNotNull();
ModelCursor modelCursor = rootFeature.getCursor();
int cnt = 0;
continuationToken = null;
ModelChild child;
while ((child = modelCursor.getNextItem()) != null) {
cnt++;
if (child.getType() == Type.TOKEN) {
continuationToken = child;
}
}
assertThat(cnt).isEqualTo(featureCnt + 1);
assertThat(continuationToken).isNotNull();
assertThat(continuationToken.getModelToken().getStreamToken()).isEqualTo(streamToken);
}
@Test
public void testTokenMutation() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
int featureCnt = 3;
StreamToken streamToken = initializeStreamWithToken(modelProvider, featureCnt);
Map<ByteString, TokenTracking> tokens = modelProvider.getTokensForTest();
assertThat(tokens).hasSize(1);
ModelFeature rootFeature = modelProvider.getRootFeature();
assertThat(rootFeature).isNotNull();
ModelCursor modelCursor = rootFeature.getCursor();
continuationToken = null;
ModelChild child;
while ((child = modelCursor.getNextItem()) != null) {
if (child.getType() == Type.TOKEN) {
continuationToken = child;
}
}
assertThat(continuationToken).isNotNull();
TokenMutation tokenMutation = modelProvider.new TokenMutation(streamToken);
TokenTracking tokenTracking = tokenMutation.getTokenTrackingForTest();
assertThat(tokenTracking.location.size()).isEqualTo(featureCnt + 1);
tokenMutation.preMutation();
assertThat(tokenTracking.location.size()).isEqualTo(featureCnt + 1);
assertThat(tokenMutation.newCursorStart).isEqualTo(tokenTracking.location.size() - 1);
tokens = modelProvider.getTokensForTest();
assertThat(tokens).hasSize(0);
TokenCompletedObserver tokenCompletedObserver =
new TokenCompletedObserver() {
@Override
public void onTokenCompleted(TokenCompleted tokenCompleted) {
assertThat(tokenCompleted).isNotNull();
}
@Override
public void onError(ModelError error) {}
};
continuationToken.getModelToken().registerObserver(tokenCompletedObserver);
tokenMutation.postMutation();
}
@Test
public void testSyntheticToken() {
int initialPageSize = 4;
int pageSize = 4;
initSyntheticTokenConfig(
/* initialPageSize= */ initialPageSize, /* minPageSize= */ 2, /* pageSize= */ pageSize);
doAnswer(
invocation -> {
Object[] args = invocation.getArguments();
((Runnable) args[2]).run();
return null;
})
.when(taskQueue)
.execute(anyInt(), anyInt(), any());
FeedModelProvider modelProvider = createFeedModelProvider();
int featureCnt = 11;
createTopLevelFeatures(modelProvider, featureCnt).commit();
ModelFeature rootFeature = modelProvider.getRootFeature();
assertThat(rootFeature).isNotNull();
ModelCursor modelCursor = rootFeature.getCursor();
assertThat(modelCursor).isNotNull();
ModelChild lastChild = assertCursorSize(modelCursor, initialPageSize + 1);
assertThat(lastChild.getType()).isEqualTo(Type.TOKEN);
assertThat(lastChild.getModelToken().isSynthetic()).isTrue();
// The token should be handled in the FeedModelProvider
modelProvider.handleToken(lastChild.getModelToken());
modelCursor = rootFeature.getCursor();
assertThat(modelCursor).isNotNull();
lastChild = assertCursorSize(modelCursor, initialPageSize + pageSize + 1);
assertThat(lastChild.getType()).isEqualTo(Type.TOKEN);
assertThat(lastChild.getModelToken().isSynthetic()).isTrue();
// last page
modelProvider.handleToken(lastChild.getModelToken());
modelCursor = rootFeature.getCursor();
assertThat(modelCursor).isNotNull();
lastChild = assertCursorSize(modelCursor, featureCnt);
assertThat(lastChild.getType()).isEqualTo(Type.FEATURE);
}
@Test
public void testSyntheticToken_missingToken() {
int initialPageSize = 4;
int pageSize = 4;
initSyntheticTokenConfig(
/* initialPageSize= */ initialPageSize, /* minPageSize= */ 2, /* pageSize= */ pageSize);
doAnswer(
invocation -> {
Object[] args = invocation.getArguments();
((Runnable) args[2]).run();
return null;
})
.when(taskQueue)
.execute(anyInt(), anyInt(), any());
FeedModelProvider modelProvider = createFeedModelProvider();
createTopLevelFeatures(modelProvider, /* featureCount= */ 11).commit();
ModelCursor modelCursor = modelProvider.getRootFeature().getCursor();
ModelChild lastChild = assertCursorSize(modelCursor, initialPageSize + 1);
assertThat(lastChild.getType()).isEqualTo(Type.TOKEN);
assertThat(lastChild.getModelToken().isSynthetic()).isTrue();
// Simulate removing the synthetic token from the root.
FakeTokenCompletedObserver observer = new FakeTokenCompletedObserver();
lastChild.getModelToken().registerObserver(observer);
modelProvider.clearRootChildrenForTest();
modelProvider.handleToken(lastChild.getModelToken());
assertThat(observer.errorThrown).isNotNull();
assertThat(observer.errorThrown.getErrorType()).isEqualTo(ErrorType.SYNTHETIC_TOKEN_ERROR);
}
@Test
public void testViewDepthProvider_null() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
assertThat(modelProvider.getViewDepthProvider(null)).isNull();
}
@Test
public void testViewDepthProvider_delegatedNull() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
ViewDepthProvider mockProvider = mock(ViewDepthProvider.class);
when(mockProvider.getChildViewDepth()).thenReturn(null);
assertThat(modelProvider.getViewDepthProvider(null)).isNull();
}
@Test
public void testViewDepthProvider_delegated() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
int featureCnt = 3;
ModelMutation mutator = createTopLevelFeatures(modelProvider, featureCnt);
String featureParent = createFeatureContentId(2);
for (int i = 0; i < featureCnt; i++) {
mutator.addChild(
createStreamStructureAndBinding(createFeature(i + 1 + featureCnt, featureParent)));
}
String childFeatureContentId = createFeatureContentId(1 + featureCnt);
mutator.commit();
ViewDepthProvider mockProvider = mock(ViewDepthProvider.class);
when(mockProvider.getChildViewDepth()).thenReturn(childFeatureContentId);
ViewDepthProvider provider = modelProvider.getViewDepthProvider(mockProvider);
assertThat(provider.getChildViewDepth()).isEqualTo(featureParent);
}
@Test
public void testGetAllRootChildren() {
FeedModelProvider modelProvider = createFeedModelProviderWithConfig();
int featureCnt = 3;
createTopLevelFeatures(modelProvider, featureCnt).commit();
List<ModelChild> rootChildren = modelProvider.getAllRootChildren();
assertThat(rootChildren).hasSize(featureCnt);
}
private ModelChild getContinuationToken(ModelFeature rootFeature) {
ModelCursor modelCursor = rootFeature.getCursor();
ModelChild token = null;
ModelChild child;
while ((child = modelCursor.getNextItem()) != null) {
if (child.getType() == Type.TOKEN) {
token = child;
}
}
assertThat(token).isNotNull();
return token;
}
private ModelChild assertCursorSize(ModelCursor cursor, int size) {
int cnt = 0;
ModelChild lastChild = null;
ModelChild child;
while ((child = cursor.getNextItem()) != null) {
cnt++;
lastChild = child;
}
assertThat(cnt).isEqualTo(size);
assertThat(lastChild).isNotNull();
return lastChild;
}
private void initDefaultConfig() {
initSyntheticTokenConfig(/* initialPageSize= */ 0L, /* minPageSize= */ 0L, /* pageSize= */ 0L);
}
private void initSyntheticTokenConfig(long initialPageSize, long minPageSize, long pageSize) {
when(config.getValueOrDefault(ConfigKey.INITIAL_NON_CACHED_PAGE_SIZE, 0L))
.thenReturn(initialPageSize);
when(config.getValueOrDefault(ConfigKey.NON_CACHED_PAGE_SIZE, 0L)).thenReturn(pageSize);
when(config.getValueOrDefault(ConfigKey.NON_CACHED_MIN_PAGE_SIZE, 0L)).thenReturn(minPageSize);
}
private StreamToken initializeStreamWithToken(FeedModelProvider modelProvider, int featureCnt) {
ModelMutation mutator = createTopLevelFeatures(modelProvider, featureCnt);
// Populate the model provider with a continuation token at the end.
ByteString bytes = ByteString.copyFrom("continuation", Charset.defaultCharset());
StreamToken streamToken =
StreamToken.newBuilder().setNextPageToken(bytes).setParentId(rootContentId).build();
mutator.addChild(createStreamStructureAndBinding(streamToken));
mutator.commit();
assertThat(modelProvider.getCurrentState()).isEqualTo(State.READY);
return streamToken;
}
private FeedModelProvider createFeedModelProviderWithConfig() {
initDefaultConfig();
return createFeedModelProvider();
}
private FeedModelProvider createFeedModelProvider() {
return new FeedModelProvider(
feedSessionManager,
threadUtils,
timingUtils,
taskQueue,
fakeMainThreadRunner,
null,
config);
}
private ModelMutation createTopLevelFeatures(ModelProvider modelProvider, int featureCount) {
ModelMutation mutator = getRootedModelMutator(modelProvider);
for (int i = 0; i < featureCount; i++) {
mutator.addChild(createStreamStructureAndBinding(createFeature(i + 1, rootContentId)));
}
return mutator;
}
private StreamFeature createFeature(int i, String parentContentId) {
return StreamFeature.newBuilder()
.setParentId(parentContentId)
.setContentId(createFeatureContentId(i))
.setCard(Card.getDefaultInstance())
.build();
}
private String createFeatureContentId(int i) {
return idGenerators.createFeatureContentId(i);
}
private ModelMutation getRootedModelMutator(ModelProvider modelProvider) {
ModelMutation mutator = modelProvider.edit();
assertThat(mutator).isNotNull();
StreamFeature rootStreamFeature = getRootFeature();
mutator.addChild(createStreamStructureAndBinding(rootStreamFeature));
return mutator;
}
private StreamStructure createStreamStructureFromFeature(StreamFeature feature) {
StreamStructure.Builder builder =
StreamStructure.newBuilder()
.setContentId(feature.getContentId())
.setOperation(Operation.UPDATE_OR_APPEND);
if (feature.hasParentId()) {
builder.setParentContentId(feature.getParentId());
}
return builder.build();
}
private StreamStructure createStreamStructureFromToken(StreamToken token) {
StreamStructure.Builder builder =
StreamStructure.newBuilder()
.setContentId(token.getContentId())
.setOperation(Operation.UPDATE_OR_APPEND);
if (token.hasParentId()) {
builder.setParentContentId(token.getParentId());
}
return builder.build();
}
private StreamStructure createRemove(String parentContentId, String contentId) {
return StreamStructure.newBuilder()
.setContentId(contentId)
.setParentContentId(parentContentId)
.setOperation(Operation.REMOVE)
.build();
}
/** This has the side affect of populating {@code childBindings} with a {@code PayloadWithId}. */
private StreamStructure createStreamStructureAndBinding(StreamFeature feature) {
StreamPayload payload = StreamPayload.newBuilder().setStreamFeature(feature).build();
childBindings.add(new PayloadWithId(feature.getContentId(), payload));
return createStreamStructureFromFeature(feature);
}
/** This has the side affect of populating {@code childBindings} with a {@code PayloadWithId}. */
private StreamStructure createStreamStructureAndBinding(StreamToken token) {
StreamPayload payload = StreamPayload.newBuilder().setStreamToken(token).build();
childBindings.add(new PayloadWithId(token.getContentId(), payload));
return createStreamStructureFromToken(token);
}
private StreamFeature getRootFeature() {
return StreamFeature.newBuilder().setContentId(rootContentId).build();
}
private static class FakeTokenCompletedObserver implements TokenCompletedObserver {
private ModelError errorThrown;
@Override
public void onTokenCompleted(TokenCompleted tokenCompleted) {}
@Override
public void onError(ModelError modelError) {
errorThrown = modelError;
}
}
}