Add integration test for garbage collection.

PiperOrigin-RevId: 242531323
Change-Id: I7fc58f799d696d59b3a43fc492e6eaa42b497353
diff --git a/src/main/java/com/google/android/libraries/feed/common/testing/InfrastructureIntegrationScope.java b/src/main/java/com/google/android/libraries/feed/common/testing/InfrastructureIntegrationScope.java
index 5f7581c..8763405 100644
--- a/src/main/java/com/google/android/libraries/feed/common/testing/InfrastructureIntegrationScope.java
+++ b/src/main/java/com/google/android/libraries/feed/common/testing/InfrastructureIntegrationScope.java
@@ -86,13 +86,21 @@
         }
       };
 
-  private final FeedSessionManager feedSessionManager;
-  private final FeedProtocolAdapter feedProtocolAdapter;
-  private final FeedModelProviderFactory modelProviderFactory;
+  private final Clock clock;
+  private final Configuration configuration;
+  private final ContentStorageDirect contentStorage;
+  private final ExecutorService executorService;
   private final FakeRequestManager fakeRequestManager;
   private final FeedAppLifecycleListener appLifecycleListener;
-  private final TaskQueue taskQueue;
+  private final FeedModelProviderFactory modelProviderFactory;
+  private final FeedProtocolAdapter feedProtocolAdapter;
+  private final FeedSessionManager feedSessionManager;
   private final FeedStore store;
+  private final JournalStorageDirect journalStorage;
+  private final SchedulerApi schedulerApi;
+  private final TaskQueue taskQueue;
+  private final ThreadUtils threadUtils;
+  private final long requestDelayMs;
 
   private InfrastructureIntegrationScope(
       ThreadUtils threadUtils,
@@ -100,15 +108,23 @@
       SchedulerApi schedulerApi,
       Clock clock,
       Configuration configuration,
-      long requestDelayMs) {
+      long requestDelayMs,
+      ContentStorageDirect contentStorage,
+      JournalStorageDirect journalStorage) {
+    this.clock = clock;
+    this.configuration = configuration;
+    this.contentStorage = contentStorage;
+    this.executorService = executorService;
+    this.journalStorage = journalStorage;
+    this.requestDelayMs = requestDelayMs;
+    this.schedulerApi = schedulerApi;
+    this.threadUtils = threadUtils;
     TimingUtils timingUtils = new TimingUtils();
     MainThreadRunner mainThreadRunner = new MainThreadRunner();
     appLifecycleListener = new FeedAppLifecycleListener(threadUtils);
     FakeBasicLoggingApi fakeBasicLoggingApi = new FakeBasicLoggingApi();
 
     FeedExtensionRegistry extensionRegistry = new FeedExtensionRegistry(new ExtensionProvider());
-    ContentStorageDirect contentStorage = new InMemoryContentStorage();
-    JournalStorageDirect journalStorage = new InMemoryJournalStorage();
     taskQueue =
         new TaskQueue(
             fakeBasicLoggingApi,
@@ -189,6 +205,19 @@
     return appLifecycleListener;
   }
 
+  @Override
+  public InfrastructureIntegrationScope clone() {
+    return new InfrastructureIntegrationScope(
+        threadUtils,
+        executorService,
+        schedulerApi,
+        clock,
+        configuration,
+        requestDelayMs,
+        contentStorage,
+        journalStorage);
+  }
+
   private static class ExtensionProvider implements ProtoExtensionProvider {
     @Override
     public List<GeneratedExtension<?, ?>> getProtoExtensions() {
@@ -206,7 +235,7 @@
 
   /** Builder for creating the {@link InfrastructureIntegrationScope} */
   public static class Builder {
-    private final ThreadUtils mockThreadUtils;
+    private final ThreadUtils threadUtils;
     private final ExecutorService executorService;
 
     private SchedulerApi schedulerApi = DISABLED_EMPTY_HEAD_REQUEST;
@@ -214,8 +243,8 @@
     private Clock clock = new SystemClockImpl();
     private long requestDelayMs = 0;
 
-    public Builder(ThreadUtils mockThreadUtils, ExecutorService executorService) {
-      this.mockThreadUtils = mockThreadUtils;
+    public Builder(ThreadUtils threadUtils, ExecutorService executorService) {
+      this.threadUtils = threadUtils;
       this.executorService = executorService;
     }
 
@@ -241,7 +270,14 @@
 
     public InfrastructureIntegrationScope build() {
       return new InfrastructureIntegrationScope(
-          mockThreadUtils, executorService, schedulerApi, clock, configuration, requestDelayMs);
+          threadUtils,
+          executorService,
+          schedulerApi,
+          clock,
+          configuration,
+          requestDelayMs,
+          new InMemoryContentStorage(),
+          new InMemoryJournalStorage());
     }
   }
 }
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD b/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD
index 9151595..285bd7c 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD
@@ -170,6 +170,28 @@
 )
 
 android_local_test(
+    name = "GcTest",
+    size = "small",
+    timeout = "moderate",
+    srcs = ["GcTest.java"],
+    aapt_version = "aapt2",
+    manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
+    deps = [
+        "//src/main/java/com/google/android/libraries/feed/api/common",
+        "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
+        "//src/main/java/com/google/android/libraries/feed/common/testing",
+        "//src/main/java/com/google/android/libraries/feed/common/time/testing",
+        "//src/main/java/com/google/android/libraries/feed/host/config",
+        "//src/main/java/com/google/android/libraries/feed/host/logging",
+        "//src/main/proto/search/now/wire/feed:feed_java_proto_lite",
+        "@com_google_protobuf_javalite//:protobuf_java_lite",
+        "@maven//:com_google_guava_guava",
+        "@maven//:com_google_truth_truth",
+        "@robolectric//bazel:robolectric",
+    ],
+)
+
+android_local_test(
     name = "LimitedPagingTest",
     size = "small",
     timeout = "moderate",
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/GcTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/GcTest.java
new file mode 100644
index 0000000..3554d99
--- /dev/null
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/GcTest.java
@@ -0,0 +1,147 @@
+// Copyright 2019 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.infraintegration;
+
+import static com.google.android.libraries.feed.common.testing.WireProtocolResponseBuilder.ROOT_CONTENT_ID;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.android.libraries.feed.api.common.MutationContext;
+import com.google.android.libraries.feed.api.common.PayloadWithId;
+import com.google.android.libraries.feed.common.concurrent.testing.FakeThreadUtils;
+import com.google.android.libraries.feed.common.testing.InfrastructureIntegrationScope;
+import com.google.android.libraries.feed.common.testing.WireProtocolResponseBuilder;
+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.host.config.Configuration.ConfigKey;
+import com.google.android.libraries.feed.host.logging.RequestReason;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.search.now.wire.feed.ContentIdProto.ContentId;
+import com.google.search.now.wire.feed.ResponseProto.Response;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests that assert the behavior of garbage collection. */
+@RunWith(RobolectricTestRunner.class)
+public final class GcTest {
+  private static final ContentId[] REQUEST_1 =
+      new ContentId[] {
+        WireProtocolResponseBuilder.createFeatureContentId(1),
+        WireProtocolResponseBuilder.createFeatureContentId(2)
+      };
+  private static final ContentId[] REQUEST_2 =
+      new ContentId[] {
+        WireProtocolResponseBuilder.createFeatureContentId(3),
+        WireProtocolResponseBuilder.createFeatureContentId(4)
+      };
+  private static final long LIFETIME_MS = Duration.ofHours(1).toMillis();
+
+  private final FakeClock fakeClock = new FakeClock();
+  private final InfrastructureIntegrationScope scope =
+      new InfrastructureIntegrationScope.Builder(
+              new FakeThreadUtils(/* enforceThreadChecks= */ false),
+              MoreExecutors.newDirectExecutorService())
+          .setClock(fakeClock)
+          .setConfiguration(
+              new Configuration.Builder().put(ConfigKey.SESSION_LIFETIME_MS, LIFETIME_MS).build())
+          .build();
+
+  @Test
+  public void testGc_contentInLiveSessionRetained() {
+    scope
+        .getRequestManager()
+        .queueResponse(createResponse(REQUEST_1))
+        .triggerRefresh(
+            RequestReason.OPEN_WITHOUT_CONTENT,
+            scope.getSessionManager().getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
+
+    // Create a new session based on this request.
+    scope.getModelProviderFactory().createNew(/* viewDepthProvider= */ null).detachModelProvider();
+    assertPayloads(REQUEST_1, scope, /* shouldExist= */ true);
+
+    // Populate HEAD with new data.
+    scope
+        .getRequestManager()
+        .queueResponse(createResponse(REQUEST_2))
+        .triggerRefresh(
+            RequestReason.OPEN_WITHOUT_CONTENT,
+            scope.getSessionManager().getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
+
+    // Advance the clock without expiring the first session.
+    fakeClock.advance(LIFETIME_MS / 2);
+    InfrastructureIntegrationScope secondScope = scope.clone();
+    assertPayloads(REQUEST_1, secondScope, /* shouldExist= */ true);
+    assertPayloads(REQUEST_2, secondScope, /* shouldExist= */ true);
+  }
+
+  @Test
+  public void testGc_contentInExpiredSessionDeleted() {
+    scope
+        .getRequestManager()
+        .queueResponse(createResponse(REQUEST_1))
+        .triggerRefresh(
+            RequestReason.OPEN_WITHOUT_CONTENT,
+            scope.getSessionManager().getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
+
+    // Create a new session based on this request.
+    scope.getModelProviderFactory().createNew(/* viewDepthProvider= */ null).detachModelProvider();
+    assertPayloads(REQUEST_1, scope, /* shouldExist= */ true);
+
+    // Populate HEAD with new data.
+    scope
+        .getRequestManager()
+        .queueResponse(createResponse(REQUEST_2))
+        .triggerRefresh(
+            RequestReason.OPEN_WITHOUT_CONTENT,
+            scope.getSessionManager().getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
+
+    // Advance the clock to expire the first session, create a new scope that will run
+    // initialization and delete content from the expired session.
+    fakeClock.advance(LIFETIME_MS + 1L);
+    InfrastructureIntegrationScope secondScope = scope.clone();
+    assertPayloads(REQUEST_1, secondScope, /* shouldExist= */ false);
+    assertPayloads(REQUEST_2, secondScope, /* shouldExist= */ true);
+  }
+
+  private static void assertPayloads(
+      ContentId[] contentIds, InfrastructureIntegrationScope scope, boolean shouldExist) {
+    for (ContentId contentId : contentIds) {
+      List<PayloadWithId> payloads =
+          scope
+              .getStore()
+              .getPayloads(
+                  Arrays.asList(
+                      new String[] {scope.getProtocolAdapter().getStreamContentId(contentId)}))
+              .getValue();
+      if (shouldExist) {
+        assertThat(payloads).hasSize(1);
+      } else {
+        assertThat(payloads).isEmpty();
+      }
+    }
+  }
+
+  private static Response createResponse(ContentId[] contentIds) {
+    WireProtocolResponseBuilder responseBuilder =
+        new WireProtocolResponseBuilder().addClearOperation().addRootFeature();
+    for (ContentId contentId : contentIds) {
+      responseBuilder.addCard(contentId, ROOT_CONTENT_ID);
+    }
+    return responseBuilder.build();
+  }
+}