Add ability to mock/fake time intervals in tests

System.nanoTime can't be mocked in tests. This change replaces
System.nanoTime with Ticker and adds the TestTicker for mocking.

Gerrit already has @UseClockStep annotiations, but it works with jgit
and can't be reused because it doesn't provide nanoseconds resolution.

This change adds minimal implementation which can be used in tests.
Later it can be extended with annotations and other features (or
combined with @UseClockStep).

Change-Id: If7bebc41eae939b1ee1f0849a1927a3f79f4df6b
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index a3b5e8d..f7b343b 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -38,12 +38,14 @@
 
 import com.github.rholder.retry.BlockStrategy;
 import com.google.common.base.Strings;
+import com.google.common.base.Ticker;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.jimfs.Jimfs;
 import com.google.common.primitives.Chars;
+import com.google.common.testing.FakeTicker;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
@@ -290,6 +292,7 @@
   @Inject protected ChangeNotes.Factory notesFactory;
   @Inject protected BatchAbandon batchAbandon;
   @Inject protected TestSshKeys sshKeys;
+  @Inject protected TestTicker testTicker;
 
   protected EventRecorder eventRecorder;
   protected GerritServer server;
@@ -703,6 +706,8 @@
     }
     SystemReader.setInstance(oldSystemReader);
     oldSystemReader = null;
+    // Set useDefaultTicker in afterTest, so the next beforeTest will use the default ticker
+    testTicker.useDefaultTicker();
   }
 
   protected void closeSsh() {
@@ -1764,4 +1769,32 @@
           moduleClass.getName());
     }
   }
+
+  /** {@link Ticker} implementation for mocking without restarting GerritServer */
+  public static class TestTicker extends Ticker {
+    Ticker actualTicker;
+
+    public TestTicker() {
+      useDefaultTicker();
+    }
+
+    /** Switches to system ticker */
+    public Ticker useDefaultTicker() {
+      this.actualTicker = Ticker.systemTicker();
+      return actualTicker;
+    }
+
+    /** Switches to {@link FakeTicker} */
+    public FakeTicker useFakeTicker() {
+      if (!(this.actualTicker instanceof FakeTicker)) {
+        this.actualTicker = new FakeTicker();
+      }
+      return (FakeTicker) actualTicker;
+    }
+
+    @Override
+    public long read() {
+      return actualTicker.read();
+    }
+  }
 }
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index fa62cd9..fe6e160 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -124,6 +124,7 @@
     "//lib/truth",
     "//lib/truth:truth-java8-extension",
     "//lib/greenmail",
+    "//lib:guava-testlib",
 ] + TEST_DEPS
 
 java_library(
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 33abc68..402d21d 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -22,7 +22,9 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.base.Ticker;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest.TestTicker;
 import com.google.gerrit.acceptance.FakeGroupAuditService.FakeGroupAuditServiceModule;
 import com.google.gerrit.acceptance.ReindexGroupsAtStartup.ReindexGroupsAtStartupModule;
 import com.google.gerrit.acceptance.ReindexProjectsAtStartup.ReindexProjectsAtStartupModule;
@@ -75,6 +77,7 @@
 import com.google.inject.Module;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
+import com.google.inject.multibindings.OptionalBinder;
 import java.lang.annotation.Annotation;
 import java.lang.annotation.Retention;
 import java.lang.reflect.Field;
@@ -429,6 +432,23 @@
                 .to(GitObjectVisibilityChecker.class);
           }
         });
+    daemon.addAdditionalSysModuleForTesting(
+        new AbstractModule() {
+          @Override
+          protected void configure() {
+            super.configure();
+            // GerritServer isn't restarted between tests. TestTicker allows to replace actual
+            // Ticker in tests without restarting server and transparently for other code.
+            // Alternative option with Provider<Ticker> is less convinient, because it affects how
+            // gerrit code should be written - i.e. Ticker must not be stored in fields and must
+            // always be obtained from the provider.
+            TestTicker testTicker = new TestTicker();
+            OptionalBinder.newOptionalBinder(binder(), Ticker.class)
+                .setBinding()
+                .toInstance(testTicker);
+            bind(TestTicker.class).toInstance(testTicker);
+          }
+        });
 
     if (desc.memory()) {
       checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server");
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 3a7f2b2..b309dee 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -16,6 +16,7 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.common.base.Ticker;
 import com.google.common.cache.Cache;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.changes.ActionVisitor;
@@ -224,6 +225,7 @@
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
 import com.google.inject.internal.UniqueAnnotations;
+import com.google.inject.multibindings.OptionalBinder;
 import com.google.template.soy.jbcsrc.api.SoySauce;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
@@ -337,6 +339,9 @@
     bind(PatchSetInfoFactory.class);
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
     bind(AccountControl.Factory.class);
+    OptionalBinder.newOptionalBinder(binder(), Ticker.class)
+        .setDefault()
+        .toInstance(Ticker.systemTicker());
 
     bind(UiActions.class);
 
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 0550827..09f08bd 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -19,6 +19,7 @@
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
 import com.google.common.base.Strings;
+import com.google.common.base.Ticker;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.UncheckedExecutionException;
 import com.google.gerrit.server.CancellationMetrics;
@@ -180,6 +181,7 @@
   private Optional<Long> timeout = Optional.empty();
 
   private final long maxIntervalNanos;
+  private final Ticker ticker;
 
   /**
    * Create a new progress monitor for multiple sub-tasks.
@@ -190,10 +192,11 @@
   @AssistedInject
   private MultiProgressMonitor(
       CancellationMetrics cancellationMetrics,
+      Ticker ticker,
       @Assisted OutputStream out,
       @Assisted TaskKind taskKind,
       @Assisted String taskName) {
-    this(cancellationMetrics, out, taskKind, taskName, 500, MILLISECONDS);
+    this(cancellationMetrics, ticker, out, taskKind, taskName, 500, MILLISECONDS);
   }
 
   /**
@@ -207,12 +210,14 @@
   @AssistedInject
   private MultiProgressMonitor(
       CancellationMetrics cancellationMetrics,
+      Ticker ticker,
       @Assisted OutputStream out,
       @Assisted TaskKind taskKind,
       @Assisted String taskName,
       @Assisted long maxIntervalTime,
       @Assisted TimeUnit maxIntervalUnit) {
     this.cancellationMetrics = cancellationMetrics;
+    this.ticker = ticker;
     this.out = out;
     this.taskKind = taskKind;
     this.taskName = taskName;
@@ -262,7 +267,7 @@
       long cancellationTimeoutTime,
       TimeUnit cancellationTimeoutUnit)
       throws TimeoutException {
-    long overallStart = System.nanoTime();
+    long overallStart = ticker.read();
     long cancellationNanos =
         cancellationTimeoutTime > 0
             ? NANOSECONDS.convert(cancellationTimeoutTime, cancellationTimeoutUnit)
@@ -278,7 +283,7 @@
     synchronized (this) {
       long left = maxIntervalNanos;
       while (!done) {
-        long start = System.nanoTime();
+        long start = ticker.read();
         try {
           // Conditions below gives better granularity for timeouts.
           // Originally, code always used fixed interval:
@@ -304,7 +309,7 @@
 
         // Send an update on every wakeup (manual or spurious), but only move
         // the spinner every maxInterval.
-        long now = System.nanoTime();
+        long now = ticker.read();
 
         if (deadline > 0 && now > deadline) {
           if (!deadlineExceeded) {
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index e580f50..a2ef070 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
 
+import com.google.common.base.Ticker;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
@@ -60,6 +61,7 @@
 import com.google.inject.Provides;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
+import com.google.inject.multibindings.OptionalBinder;
 import java.util.Collection;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -112,6 +114,9 @@
   @Override
   protected void configure() {
     factory(MultiProgressMonitor.Factory.class);
+    OptionalBinder.newOptionalBinder(binder(), Ticker.class)
+        .setDefault()
+        .toInstance(Ticker.systemTicker());
 
     bind(AccountIndexRewriter.class);
     bind(AccountIndexCollection.class);
diff --git a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
index ed5e559..c868d0b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.http.message.BasicHeader;
@@ -194,6 +195,7 @@
 
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   public void abortIfServerDeadlineExceeded() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
     assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
@@ -203,6 +205,7 @@
   @GerritConfig(name = "deadline.foo.timeout", value = "1ms")
   @GerritConfig(name = "deadline.bar.timeout", value = "100ms")
   public void stricterDeadlineTakesPrecedence() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
     assertThat(response.getEntityContent())
@@ -213,6 +216,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.requestType", value = "REST")
   public void abortIfServerDeadlineExceeded_requestType() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
     assertThat(response.getEntityContent())
@@ -223,6 +227,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.requestUriPattern", value = "/projects/.*")
   public void abortIfServerDeadlineExceeded_requestUriPattern() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
     assertThat(response.getEntityContent())
@@ -235,6 +240,7 @@
       name = "deadline.default.excludedRequestUriPattern",
       value = "/projects/non-matching")
   public void abortIfServerDeadlineExceeded_excludedRequestUriPattern() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
     assertThat(response.getEntityContent())
@@ -249,6 +255,7 @@
       value = "/projects/non-matching")
   public void abortIfServerDeadlineExceeded_requestUriPatternAndExcludedRequestUriPattern()
       throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
     assertThat(response.getEntityContent())
@@ -259,6 +266,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.projectPattern", value = ".*new.*")
   public void abortIfServerDeadlineExceeded_projectPattern() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
     assertThat(response.getEntityContent())
@@ -269,6 +277,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.account", value = "1000000")
   public void abortIfServerDeadlineExceeded_account() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
     assertThat(response.getEntityContent())
@@ -279,6 +288,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.requestType", value = "SSH")
   public void nonMatchingServerDeadlineIsIgnored_requestType() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -287,6 +297,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.requestUriPattern", value = "/changes/.*")
   public void nonMatchingServerDeadlineIsIgnored_requestUriPattern() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -295,6 +306,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "/projects/.*")
   public void nonMatchingServerDeadlineIsIgnored_excludedRequestUriPattern() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -305,6 +317,7 @@
   @GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "/projects/.*new")
   public void nonMatchingServerDeadlineIsIgnored_requestUriPatternAndExcludedRequestUriPattern()
       throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -313,6 +326,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.projectPattern", value = ".*foo.*")
   public void nonMatchingServerDeadlineIsIgnored_projectPattern() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -321,6 +335,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.account", value = "999")
   public void nonMatchingServerDeadlineIsIgnored_account() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -329,6 +344,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.isAdvisory", value = "true")
   public void advisoryServerDeadlineIsIgnored() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -338,6 +354,7 @@
   @GerritConfig(name = "deadline.test.isAdvisory", value = "true")
   @GerritConfig(name = "deadline.default.timeout", value = "2ms")
   public void nonAdvisoryDeadlineIsAppliedIfStricterAdvisoryDeadlineExists() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(4));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
     assertThat(response.getEntityContent())
@@ -347,6 +364,7 @@
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1")
   public void invalidServerDeadlineIsIgnored_missingTimeUnit() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -354,6 +372,7 @@
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1x")
   public void invalidServerDeadlineIsIgnored_invalidTimeUnit() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -369,6 +388,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.requestType", value = "INVALID")
   public void invalidServerDeadlineIsIgnored_invalidRequestType() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -377,6 +397,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.requestUriPattern", value = "][")
   public void invalidServerDeadlineIsIgnored_invalidRequestUriPattern() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -385,6 +406,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "][")
   public void invalidServerDeadlineIsIgnored_invalidExcludedRequestUriPattern() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -393,6 +415,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.projectPattern", value = "][")
   public void invalidServerDeadlineIsIgnored_invalidProjectPattern() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -401,6 +424,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.account", value = "invalid")
   public void invalidServerDeadlineIsIgnored_invalidAccount() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -416,6 +440,7 @@
   @GerritConfig(name = "deadline.default.timeout", value = "0ms")
   @GerritConfig(name = "deadline.default.requestType", value = "REST")
   public void deadlineConfigWithZeroTimeoutIsIgnored() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     response.assertCreated();
   }
@@ -449,6 +474,7 @@
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   public void clientProvidedDeadlineOverridesServerDeadline() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response =
         adminRestSession.putWithHeaders(
             "/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "2ms"));
@@ -460,6 +486,7 @@
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   public void clientCanDisableDeadlineBySettingZeroAsDeadline() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     RestResponse response =
         adminRestSession.putWithHeaders(
             "/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "0"));
@@ -574,6 +601,7 @@
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   public void abortPushIfServerDeadlineExceeded() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertErrorStatus("Server Deadline Exceeded (default.timeout=1ms)");
@@ -582,6 +610,7 @@
   @Test
   @GerritConfig(name = "receive.timeout", value = "1ms")
   public void abortPushIfTimeoutExceeded() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertErrorStatus("Server Deadline Exceeded (receive.timeout=1ms)");
@@ -591,6 +620,7 @@
   @GerritConfig(name = "receive.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.timeout", value = "10s")
   public void receiveTimeoutTakesPrecedence() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertErrorStatus("Server Deadline Exceeded (receive.timeout=1ms)");
@@ -649,6 +679,7 @@
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   public void clientProvidedDeadlineOnPushOverridesServerDeadline() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     List<String> pushOptions = new ArrayList<>();
     pushOptions.add("deadline=2ms");
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
@@ -660,6 +691,7 @@
   @Test
   @GerritConfig(name = "receive.timeout", value = "1ms")
   public void clientProvidedDeadlineOnPushDoesntOverrideServerTimeout() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     List<String> pushOptions = new ArrayList<>();
     pushOptions.add("deadline=10m");
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
@@ -671,6 +703,7 @@
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   public void clientCanDisableDeadlineOnPushBySettingZeroAsDeadline() throws Exception {
+    testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
     List<String> pushOptions = new ArrayList<>();
     pushOptions.add("deadline=0");
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
diff --git a/lib/BUILD b/lib/BUILD
index f924e4ca..b2810cf 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -128,6 +128,15 @@
 )
 
 java_library(
+    name = "guava-testlib",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = [
+        "@guava-testlib//jar",
+    ],
+)
+
+java_library(
     name = "caffeine",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = [
diff --git a/lib/nongoogle_test.sh b/lib/nongoogle_test.sh
index 591e76e..90d38b0 100755
--- a/lib/nongoogle_test.sh
+++ b/lib/nongoogle_test.sh
@@ -23,6 +23,7 @@
 flogger-log4j-backend
 flogger-system-backend
 guava
+guava-testlib
 guice-assistedinject
 guice-library
 guice-servlet
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index b1ebb5c..51be39f 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -4,6 +4,8 @@
 
 GUAVA_BIN_SHA1 = "00d0c3ce2311c9e36e73228da25a6e99b2ab826f"
 
+GUAVA_TESTLIB_BIN_SHA1 = "798c3827308605cd69697d8f1596a1735d3ef6e2"
+
 GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
 
 TESTCONTAINERS_VERSION = "1.15.3"
@@ -147,6 +149,12 @@
         sha1 = GUAVA_BIN_SHA1,
     )
 
+    maven_jar(
+        name = "guava-testlib",
+        artifact = "com.google.guava:guava-testlib:" + GUAVA_VERSION,
+        sha1 = GUAVA_TESTLIB_BIN_SHA1,
+    )
+
     GUICE_VERS = "5.0.1"
 
     maven_jar(