blob: c96d5b21e2f5cdeab41fa171c5f9a144038c7683 [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.feedsessionmanager.internal;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import com.google.android.libraries.feed.api.common.testing.ContentIdGenerators;
import com.google.android.libraries.feed.common.concurrent.testing.FakeTaskQueue;
import com.google.android.libraries.feed.common.concurrent.testing.FakeThreadUtils;
import com.google.android.libraries.feed.common.time.TimingUtils;
import com.google.android.libraries.feed.common.time.testing.FakeClock;
import com.google.android.libraries.feed.host.config.Configuration;
import com.google.android.libraries.feed.internalapi.modelprovider.ModelProvider.ViewDepthProvider;
import com.google.android.libraries.feed.internalapi.store.Store;
import com.google.android.libraries.feed.testing.modelprovider.FakeModelProvider;
import com.google.android.libraries.feed.testing.store.FakeStore;
import com.google.search.now.feed.client.StreamDataProto.SessionMetadata;
import com.google.search.now.feed.client.StreamDataProto.StreamPayload;
import com.google.search.now.feed.client.StreamDataProto.StreamSession;
import com.google.search.now.feed.client.StreamDataProto.StreamSessions;
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.UiContext;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Tests of the {@link SessionCache} class. */
@RunWith(RobolectricTestRunner.class)
public class SessionCacheTest {
private static final long DEFAULT_LIFETIME_MS = 10;
private final Configuration configuration = new Configuration.Builder().build();
private final ContentIdGenerators idGenerators = new ContentIdGenerators();
private final FakeClock fakeClock = new FakeClock();
private final FakeThreadUtils fakeThreadUtils = FakeThreadUtils.withThreadChecks();
private final TimingUtils timingUtils = new TimingUtils();
private FakeStore fakeStore;
private FakeTaskQueue fakeTaskQueue;
private SessionFactory sessionFactory;
private SessionCache sessionCache;
protected final FakeModelProvider fakeModelProvider = new FakeModelProvider();
@Before
public void setUp() {
initMocks(this);
fakeTaskQueue = new FakeTaskQueue(fakeClock, fakeThreadUtils);
fakeStore = new FakeStore(fakeThreadUtils, fakeTaskQueue, fakeClock);
fakeThreadUtils.enforceMainThread(false);
fakeTaskQueue.initialize(() -> {});
sessionCache = getSessionCache();
}
@Test
public void testInitialization() {
populateHead();
assertThat(sessionCache.getAttachedSessions()).isEmpty();
assertThat(sessionCache.isHeadInitialized()).isFalse();
assertThat(sessionCache.getHead()).isNotNull();
assertThat(sessionCache.getHead().isHeadEmpty()).isTrue();
sessionCache.initialize();
// Initialization adds $HEAD
assertThat(sessionCache.isHeadInitialized()).isTrue();
assertThat(sessionCache.getAttachedSessions()).isEmpty();
assertThat(sessionCache.getHead()).isNotNull();
assertThat(sessionCache.getHead().isHeadEmpty()).isFalse();
}
@Test
public void testPutGet() {
sessionCache.initialize();
Session session = populateSession(1, 2);
sessionCache.putAttachedAndRetainMetadata(session.getSessionId(), session);
Session ret = sessionCache.getAttached(session.getSessionId());
assertThat(ret).isEqualTo(session);
}
@Test
public void testPut_persisted() {
sessionCache.initialize();
Session session = populateSession(1, 2);
sessionCache.putAttached(session.getSessionId(), /* creationTimeMillis= */ 0L, session);
Session ret = sessionCache.getAttached(session.getSessionId());
assertThat(ret).isEqualTo(session);
session = populateSession(2, 2);
sessionCache.putAttached(session.getSessionId(), /* creationTimeMillis= */ 0L, session);
List<StreamSession> streamSessionList = sessionCache.getPersistedSessions();
assertThat(streamSessionList).hasSize(3);
}
@Test
public void testRemove() {
sessionCache.initialize();
Session session = populateSession(1, 2);
sessionCache.putAttached(session.getSessionId(), /* creationTimeMillis= */ 0L, session);
List<StreamSession> streamSessionList = sessionCache.getPersistedSessions();
assertThat(streamSessionList).hasSize(2);
String id = session.getSessionId();
sessionCache.removeAttached(id);
assertThat(sessionCache.getAttached(id)).isNull();
streamSessionList = sessionCache.getPersistedSessions();
assertThat(streamSessionList).hasSize(1);
}
@Test
public void testDetach() {
sessionCache.initialize();
Session s1 = populateSession(1, 2);
String s1Id = s1.getSessionId();
sessionCache.putAttachedAndRetainMetadata(s1Id, s1);
List<Session> sessions = sessionCache.getAttachedSessions();
assertThat(sessions).hasSize(1);
assertThat(sessions).contains(s1);
assertThat(s1.getModelProvider()).isNotNull();
sessionCache.detachModelProvider(s1Id);
assertThat(sessionCache.getAttachedSessions()).isEmpty();
assertThat(s1.getModelProvider()).isNull();
}
@Test
public void testGetAttachedSessions() {
sessionCache.initialize();
Session s1 = populateSession(1, 2);
sessionCache.putAttachedAndRetainMetadata(s1.getSessionId(), s1);
Session s2 = populateSession(2, 2);
sessionCache.putAttachedAndRetainMetadata(s2.getSessionId(), s2);
List<Session> sessions = sessionCache.getAttachedSessions();
assertThat(sessions).hasSize(2);
assertThat(sessions).contains(sessionCache.getAttached(s1.getSessionId()));
assertThat(sessions).contains(sessionCache.getAttached(s2.getSessionId()));
}
@Test
public void testGetAllSessions() {
sessionCache.initialize();
Session headSession = sessionCache.getHead();
Session s1 = populateSession(1, 2, /* commitToStore= */ true);
String s1Id = s1.getSessionId();
sessionCache.putAttached(s1Id, 1L, s1);
assertThat(sessionCache.getAttachedSessions()).containsExactly(s1);
assertThat(sessionCache.getAllSessions()).containsExactly(s1, headSession);
Session s2 = populateSession(2, 2, /* commitToStore= */ true);
String s2Id = s2.getSessionId();
sessionCache.putAttached(s2Id, 2L, s2);
assertThat(sessionCache.getAttachedSessions()).containsExactly(s1, s2);
assertThat(sessionCache.getAllSessions()).containsExactly(s1, s2, headSession);
// Detach the session, which will throw it away from SessionCache.
sessionCache.detachModelProvider(s1Id);
assertThat(sessionCache.getAttachedSessions()).containsExactly(s2);
List<Session> allSessions = new ArrayList<>(sessionCache.getAllSessions());
assertThat(allSessions).hasSize(3);
assertThat(allSessions).containsAtLeast(s2, headSession);
allSessions.remove(s2);
allSessions.remove(headSession);
// A new unbound session was created for the detached one, with the same ID.
Session unboundSession = allSessions.get(0);
assertThat(unboundSession.getSessionId()).isEqualTo(s1.getSessionId());
assertThat(unboundSession.getContentInSession()).isEqualTo(s1.getContentInSession());
assertThat(unboundSession).isNotSameAs(s1);
}
@Test
public void testReset_headOnly() {
sessionCache.initialize();
assertThat(sessionCache.getAttachedSessions()).isEmpty();
sessionCache.reset();
assertThat(sessionCache.getAttachedSessions()).isEmpty();
}
@Test
public void testReset_sessions() {
sessionCache.initialize();
int sessionCount = 2;
for (int i = 0; i < sessionCount; i++) {
Session session = populateSession(i, 2);
sessionCache.putAttachedAndRetainMetadata(session.getSessionId(), session);
}
List<Session> sessions = sessionCache.getAttachedSessions();
assertThat(sessions).hasSize(sessionCount);
sessionCache.reset();
assertThat(sessionCache.getAttachedSessions()).isEmpty();
}
@Test
public void testIsSessionAlive() {
sessionCache.initialize();
fakeClock.set(DEFAULT_LIFETIME_MS + 2);
assertThat(sessionCache.isSessionAlive("stream:1", SessionMetadata.getDefaultInstance()))
.isFalse();
assertThat(
sessionCache.isSessionAlive(
Store.HEAD_SESSION_ID, SessionMetadata.getDefaultInstance()))
.isTrue();
assertThat(
sessionCache.isSessionAlive(
"stream:2",
SessionMetadata.newBuilder()
.setCreationTimeMillis(DEFAULT_LIFETIME_MS - 1)
.build()))
.isTrue();
}
@Test
public void testGetPersistedSessions() {
StreamSession streamSession = StreamSession.getDefaultInstance();
mockStreamSessions(StreamSessions.newBuilder().addStreamSession(streamSession).build());
List<StreamSession> sessionList = sessionCache.getPersistedSessions();
assertThat(sessionList).containsExactly(streamSession);
}
@Test
public void testCleanupJournals() {
String sessionId1 = "stream:1";
String sessionId2 = "stream:2";
fakeStore
.setStreamStructures(sessionId1, StreamStructure.getDefaultInstance())
.setStreamStructures(sessionId2, StreamStructure.getDefaultInstance());
Session s2 = mock(Session.class);
when(s2.getSessionId()).thenReturn(sessionId2);
setSessions(s2);
sessionCache.cleanupSessionJournals();
assertThat(fakeStore.getAllSessions().getValue()).containsExactly(sessionId2);
assertThat(sessionCache.getAttached(sessionId2)).isEqualTo(s2);
}
@Test
public void testInitializePersistedSessions_emptyStreamSessions() {
fakeClock.set(DEFAULT_LIFETIME_MS + 2);
mockStreamSessions(StreamSessions.getDefaultInstance());
sessionCache.initialize();
assertThat(sessionCache.getAttachedSessions()).isEmpty();
}
@Test
public void testInitializePersistedSessions_legacyStreamSession() {
StreamSessions streamSessions =
StreamSessions.newBuilder()
.addStreamSession(
StreamSession.newBuilder().setSessionId("stream:1").setLegacyTimeMillis(0))
.addStreamSession(
StreamSession.newBuilder()
.setSessionId("stream:2")
.setLegacyTimeMillis(DEFAULT_LIFETIME_MS - 1))
.addStreamSession(
StreamSession.newBuilder()
.setSessionId(Store.HEAD_SESSION_ID)
.setLegacyTimeMillis(1))
.build();
fakeClock.set(DEFAULT_LIFETIME_MS + 2);
mockStreamSessions(streamSessions);
sessionCache.initialize();
assertThat(sessionCache.hasSession("stream:1")).isFalse();
assertThat(sessionCache.hasSession("stream:2")).isTrue();
assertThat(sessionCache.getCreationTimeMillis("stream:2")).isEqualTo(DEFAULT_LIFETIME_MS - 1);
assertThat(sessionCache.getHeadLastAddedTimeMillis()).isEqualTo(1);
}
@Test
public void testInitializePersistedSessions_sessionMetadata() {
StreamSessions streamSessions =
StreamSessions.newBuilder()
.addStreamSession(
StreamSession.newBuilder()
.setSessionId("stream:1")
.setSessionMetadata(SessionMetadata.getDefaultInstance()))
.addStreamSession(
StreamSession.newBuilder()
.setSessionId("stream:2")
.setSessionMetadata(
SessionMetadata.newBuilder()
.setCreationTimeMillis(DEFAULT_LIFETIME_MS - 1)))
.build();
fakeClock.set(DEFAULT_LIFETIME_MS + 2);
mockStreamSessions(streamSessions);
sessionCache.initialize();
assertThat(sessionCache.hasSession("stream:1")).isFalse();
assertThat(sessionCache.hasSession("stream:2")).isTrue();
assertThat(sessionCache.getCreationTimeMillis("stream:2")).isEqualTo(DEFAULT_LIFETIME_MS - 1);
}
@Test
public void testUpdatePersistedSessions() {
sessionCache.initialize();
// persist HEAD into the store
sessionCache.updatePersistedSessionsMetadata();
List<StreamSession> streamSessionList = sessionCache.getPersistedSessions();
assertThat(streamSessionList).hasSize(1);
// add additional sessions
StreamSession session1 =
StreamSession.newBuilder()
.setSessionId("stream:1")
.setSessionMetadata(SessionMetadata.newBuilder().setCreationTimeMillis(0L))
.build();
StreamSession session2 =
StreamSession.newBuilder()
.setSessionId("stream:2")
.setSessionMetadata(SessionMetadata.newBuilder().setCreationTimeMillis(0L))
.build();
Session s1 = mock(Session.class);
when(s1.getSessionId()).thenReturn(session1.getSessionId());
Session s2 = mock(Session.class);
when(s2.getSessionId()).thenReturn(session2.getSessionId());
setSessions(s1, s2);
sessionCache.updatePersistedSessionsMetadata();
streamSessionList = sessionCache.getPersistedSessions();
assertThat(streamSessionList).hasSize(3);
assertThat(streamSessionList).contains(session1);
assertThat(streamSessionList).contains(session2);
}
@Test
public void testErrors_persistedSession() {
sessionCache.initialize();
sessionCache.updatePersistedSessionsMetadata();
fakeStore.setAllowGetPayloads(false);
assertThat(sessionCache.getPersistedSessions()).isEmpty();
}
private SessionCache getSessionCache() {
sessionFactory =
new SessionFactory(fakeStore, fakeTaskQueue, timingUtils, fakeThreadUtils, configuration);
return new SessionCache(
fakeStore, fakeTaskQueue, sessionFactory, 10, timingUtils, fakeThreadUtils, fakeClock);
}
private void mockStreamSessions(StreamSessions streamSessions) {
fakeStore.setContent(
SessionCache.STREAM_SESSION_CONTENT_ID,
StreamPayload.newBuilder().setStreamSessions(streamSessions).build());
}
private Session populateSession(int id, int featureCnt) {
return populateSession(id, featureCnt, /* commitToStore= */ false);
}
private Session populateSession(int id, int featureCnt, boolean commitToStore) {
InitializableSession session = sessionFactory.getSession();
String rootId = idGenerators.createRootContentId(1);
List<StreamStructure> head = new ArrayList<>();
head.add(
StreamStructure.newBuilder()
.setOperation(Operation.UPDATE_OR_APPEND)
.setContentId(rootId)
.build());
for (int i = 0; i < featureCnt; i++) {
head.add(
StreamStructure.newBuilder()
.setOperation(Operation.UPDATE_OR_APPEND)
.setContentId(idGenerators.createFeatureContentId(i))
.setParentContentId(rootId)
.build());
}
session.setSessionId("stream:" + id);
session.bindModelProvider(fakeModelProvider, mock(ViewDepthProvider.class));
session.populateModelProvider(head, true, false, UiContext.getDefaultInstance());
if (commitToStore) {
fakeStore.setStreamStructures(session.getSessionId(), head);
}
return session;
}
private void populateHead() {
fakeStore.setStreamStructures(
Store.HEAD_SESSION_ID,
StreamStructure.newBuilder()
.setOperation(Operation.UPDATE_OR_APPEND)
.setContentId(idGenerators.createRootContentId(1))
.build());
}
private void setSessions(Session... testSessions) {
for (Session session : testSessions) {
sessionCache.putAttached(session.getSessionId(), /* creationTimeMillis= */ 0L, session);
}
}
}