Merge changes If7bebc41,I5d826655

* changes:
  Add ability to mock/fake time intervals in tests
  Reduce timeout granularity in MultiProgressMonitor
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 b182deb..6c25bae 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;
@@ -225,6 +226,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;
@@ -338,6 +340,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 3de8ff3..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,16 +283,33 @@
     synchronized (this) {
       long left = maxIntervalNanos;
       while (!done) {
-        long start = System.nanoTime();
+        long start = ticker.read();
         try {
-          NANOSECONDS.timedWait(this, left);
+          // Conditions below gives better granularity for timeouts.
+          // Originally, code always used fixed interval:
+          // NANOSECONDS.timedWait(this, maxIntervalNanos);
+          // As a result, the actual check for timeouts happened only every maxIntervalNanos
+          // (default value 500ms); so even if timout was set to 1ms, the actual timeout was 500ms.
+          // This is not a big issue, however it made our tests for timeouts flaky. For example,
+          // some tests in the CancellationIT set timeout to 1ms and expect that server returns
+          // timeout. However, server often returned OK result, because a request takes less than
+          // 500ms.
+          if (deadlineExceeded || deadline == 0) {
+            // We want to set deadlineExceeded flag as earliest as possible. If it is already
+            // set - there is no reason to wait less than maxIntervalNanos
+            NANOSECONDS.timedWait(this, maxIntervalNanos);
+          } else if (start <= deadline) {
+            // if deadlineExceeded is not set, then we should wait until deadline, but no longer
+            // than maxIntervalNanos (because we want to report a progress every maxIntervalNanos).
+            NANOSECONDS.timedWait(this, Math.min(deadline - start + 1, maxIntervalNanos));
+          }
         } catch (InterruptedException e) {
           throw new UncheckedExecutionException(e);
         }
 
         // 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(