Fix timestamps in Bazel/Blaze JUnit runner

RELNOTES: None.
PiperOrigin-RevId: 255058249
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/JUnit4Bazel.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/JUnit4Bazel.java
index 2443583..7ce14d6 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/JUnit4Bazel.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/JUnit4Bazel.java
@@ -17,6 +17,7 @@
 import com.google.testing.junit.runner.internal.SignalHandlers;
 import com.google.testing.junit.runner.internal.SignalHandlersFactory;
 import com.google.testing.junit.runner.junit4.CancellableRequestFactoryFactory;
+import com.google.testing.junit.runner.junit4.ClockFactory;
 import com.google.testing.junit.runner.junit4.CurrentRunningTestFactory;
 import com.google.testing.junit.runner.junit4.JUnit4ConfigFactory;
 import com.google.testing.junit.runner.junit4.JUnit4InstanceModules;
@@ -37,7 +38,6 @@
 import com.google.testing.junit.runner.junit4.StackTraceListenerFactory;
 import com.google.testing.junit.runner.junit4.TestSuiteModelSupplierFactory;
 import com.google.testing.junit.runner.junit4.TextListenerFactory;
-import com.google.testing.junit.runner.junit4.TickerFactory;
 import com.google.testing.junit.runner.junit4.TopLevelSuiteFactory;
 import com.google.testing.junit.runner.junit4.TopLevelSuiteNameFactory;
 import com.google.testing.junit.runner.junit4.XmlListenerFactory;
@@ -153,7 +153,7 @@
 
     this.builderSupplier =
         TestSuiteModelBuilderFactory.create(
-            TickerFactory.create(),
+            ClockFactory.create(),
             shardingFiltersSupplier,
             ShardingEnvironmentFactory.create(),
             resultWriterSupplier);
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/TickerFactory.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/ClockFactory.java
similarity index 69%
rename from src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/TickerFactory.java
rename to src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/ClockFactory.java
index f3c6c9a..0ec085a 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/TickerFactory.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/ClockFactory.java
@@ -15,22 +15,20 @@
 package com.google.testing.junit.runner.junit4;
 
 import com.google.testing.junit.runner.util.Factory;
-import com.google.testing.junit.runner.util.Ticker;
+import com.google.testing.junit.runner.util.TestClock;
 
-/**
- * A factory that supplies {@link Ticker}.
- */
-public enum TickerFactory implements Factory<Ticker> {
+/** A factory that supplies {@link TestClock}. */
+public enum ClockFactory implements Factory<TestClock> {
   INSTANCE;
 
   @Override
-  public Ticker get() {
-    Ticker ticker = JUnit4RunnerModule.ticker();
-    assert ticker != null;
-    return ticker;
+  public TestClock get() {
+    TestClock testClock = JUnit4RunnerModule.clock();
+    assert testClock != null;
+    return testClock;
   }
 
-  public static Factory<Ticker> create() {
+  public static Factory<TestClock> create() {
     return INSTANCE;
   }
 }
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerModule.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerModule.java
index 1bf6443..73ff05f 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerModule.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerModule.java
@@ -20,8 +20,8 @@
 import com.google.testing.junit.runner.internal.junit4.JUnit4TestStackTraceListener;
 import com.google.testing.junit.runner.internal.junit4.JUnit4TestXmlListener;
 import com.google.testing.junit.runner.internal.junit4.SettableCurrentRunningTest;
+import com.google.testing.junit.runner.util.TestClock;
 import com.google.testing.junit.runner.util.TestNameProvider;
-import com.google.testing.junit.runner.util.Ticker;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.OutputStream;
@@ -34,8 +34,8 @@
  * Utility class for real test runs. This is a legacy Dagger module.
  */
 public final class JUnit4RunnerModule {
-  static Ticker ticker() {
-    return Ticker.systemTicker();
+  static TestClock clock() {
+    return TestClock.systemClock();
   }
 
   static SignalHandlers.HandlerInstaller signalHandlerInstaller() {
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestCaseNode.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestCaseNode.java
index 8662523..581dac6 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestCaseNode.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestCaseNode.java
@@ -17,6 +17,7 @@
 import static com.google.testing.junit.runner.util.TestPropertyExporter.INITIAL_INDEX_FOR_REPEATED_PROPERTY;
 
 import com.google.testing.junit.runner.model.TestResult.Status;
+import com.google.testing.junit.runner.util.TestClock.TestInstant;
 import com.google.testing.junit.runner.util.TestIntegration;
 import com.google.testing.junit.runner.util.TestIntegrationsExporter;
 import com.google.testing.junit.runner.util.TestPropertyExporter;
@@ -66,7 +67,7 @@
    * Indicates that the test represented by this node is scheduled to start.
    */
   void pending() {
-    compareAndSetState(State.INITIAL, State.PENDING, -1);
+    compareAndSetState(State.INITIAL, State.PENDING, TestInstant.UNKNOWN);
   }
 
   /**
@@ -74,12 +75,12 @@
    *
    * @param now Time that the test started
    */
-  public void started(long now) {
+  public void started(TestInstant now) {
     compareAndSetState(INITIAL_STATES, State.STARTED, now);
   }
 
   @Override
-  public void testInterrupted(long now) {
+  public void testInterrupted(TestInstant now) {
     if (compareAndSetState(State.STARTED, State.INTERRUPTED, now)) {
       globalFailures.add(new Exception("Test interrupted"));
       return;
@@ -107,13 +108,12 @@
   }
 
   @Override
-  public void testSkipped(long now) {
+  public void testSkipped(TestInstant now) {
     compareAndSetState(State.STARTED, State.SKIPPED, now);
   }
 
-
   @Override
-  public void testSuppressed(long now) {
+  public void testSuppressed(TestInstant now) {
     compareAndSetState(INITIAL_STATES, State.SUPPRESSED, now);
   }
 
@@ -122,18 +122,18 @@
    *
    * @param now Time that the test finished
    */
-  public void finished(long now) {
+  public void finished(TestInstant now) {
     compareAndSetState(State.STARTED, State.FINISHED, now);
   }
 
   @Override
-  public void testFailure(Throwable throwable, long now) {
+  public void testFailure(Throwable throwable, TestInstant now) {
     compareAndSetState(INITIAL_STATES, State.FINISHED, now);
     globalFailures.add(throwable);
   }
 
   @Override
-  public void dynamicTestFailure(Description test, Throwable throwable, long now) {
+  public void dynamicTestFailure(Description test, Throwable throwable, TestInstant now) {
     compareAndSetState(INITIAL_STATES, State.FINISHED, now);
     addThrowableToDynamicTestToFailures(test, throwable);
   }
@@ -168,7 +168,7 @@
     return previousRepetitionsNr;
   }
 
-  private boolean compareAndSetState(State fromState, State toState, long now) {
+  private boolean compareAndSetState(State fromState, State toState, TestInstant now) {
     if (fromState == null) {
       throw new NullPointerException();
     }
@@ -176,14 +176,14 @@
   }
 
   // TODO(bazel-team): Use AtomicReference instead of a synchronized method.
-  private synchronized boolean compareAndSetState(Set<State> fromStates, State toState, long now) {
+  private synchronized boolean compareAndSetState(
+      Set<State> fromStates, State toState, TestInstant now) {
     if (fromStates == null || toState == null || state == null) {
       throw new NullPointerException();
     }
     if (fromStates.isEmpty()) {
       throw new IllegalArgumentException();
     }
-
     if (fromStates.contains(state) && toState != state) {
       state = toState;
       if (toState != State.PENDING) {
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestInterval.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestInterval.java
index 455fb04..e2ab8fc 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestInterval.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestInterval.java
@@ -14,6 +14,7 @@
 
 package com.google.testing.junit.runner.model;
 
+import com.google.testing.junit.runner.util.TestClock.TestInstant;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.Date;
@@ -25,11 +26,11 @@
  * <p>This class is thread-safe and immutable.
  */
 public final class TestInterval {
-  private final long startInstant;
-  private final long endInstant;
+  private final TestInstant startInstant;
+  private final TestInstant endInstant;
 
-  public TestInterval(long startInstant, long endInstant) {
-    if (startInstant > endInstant) {
+  public TestInterval(TestInstant startInstant, TestInstant endInstant) {
+    if (startInstant.monotonicTime().compareTo(endInstant.monotonicTime()) > 0) {
       throw new IllegalArgumentException("Start must be before end");
     }
     this.startInstant = startInstant;
@@ -37,19 +38,19 @@
   }
 
   public long getStartMillis() {
-    return startInstant;
+    return startInstant.wallTime().toEpochMilli();
   }
 
   public long getEndMillis() {
-    return endInstant;
+    return endInstant.wallTime().toEpochMilli();
   }
 
   public long toDurationMillis() {
-    return endInstant - startInstant;
+    return endInstant.monotonicTime().minus(startInstant.monotonicTime()).toMillis();
   }
 
-  public TestInterval withEndMillis(long millis) {
-    return new TestInterval(startInstant, millis);
+  public TestInterval withEndMillis(TestInstant now) {
+    return new TestInterval(startInstant, now);
   }
 
   public String startInstantToString() {
@@ -62,6 +63,19 @@
   String startInstantToString(TimeZone tz) {
     DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
     format.setTimeZone(tz);
-    return format.format(new Date(startInstant));
+    return format.format(Date.from(startInstant.wallTime()));
+  }
+
+  /** Returns a TestInterval that contains both TestIntervals passed as parameter. */
+  public static TestInterval around(TestInterval a, TestInterval b) {
+    TestInstant start =
+        a.startInstant.monotonicTime().compareTo(b.startInstant.monotonicTime()) < 0
+            ? a.startInstant
+            : b.startInstant;
+    TestInstant end =
+        a.endInstant.monotonicTime().compareTo(b.endInstant.monotonicTime()) > 0
+            ? a.endInstant
+            : b.endInstant;
+    return new TestInterval(start, end);
   }
 }
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestNode.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestNode.java
index 4ba8aad..a7e842c 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestNode.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestNode.java
@@ -14,6 +14,7 @@
 
 package com.google.testing.junit.runner.model;
 
+import com.google.testing.junit.runner.util.TestClock.TestInstant;
 import java.util.List;
 import javax.annotation.Nullable;
 import org.junit.runner.Description;
@@ -52,31 +53,23 @@
    */
   public abstract boolean isTestCase();
 
-  /**
-   * Indicates that the test represented by this node was skipped.
-   */
-  public abstract void testSkipped(long now);
+  /** Indicates that the test represented by this node was skipped. */
+  public abstract void testSkipped(TestInstant now);
 
   /**
    * Indicates that the test represented by this node was ignored or suppressed due to being
    * annotated with {@code @Ignore} or {@code @Suppress}.
    */
-  public abstract void testSuppressed(long now);
+  public abstract void testSuppressed(TestInstant now);
 
-  /**
-   * Indicates that the test represented by this node was interrupted.
-   */
-  public abstract void testInterrupted(long now);
+  /** Indicates that the test represented by this node was interrupted. */
+  public abstract void testInterrupted(TestInstant now);
 
-  /**
-   * Adds a failure to the test represented by this node.
-   */
-  public abstract void testFailure(Throwable throwable, long now);
+  /** Adds a failure to the test represented by this node. */
+  public abstract void testFailure(Throwable throwable, TestInstant now);
 
-  /**
-   * Indicates that a dynamically generated test case or suite failed.
-   */
-  public abstract void dynamicTestFailure(Description test, Throwable throwable, long now);
+  /** Indicates that a dynamically generated test case or suite failed. */
+  public abstract void dynamicTestFailure(Description test, Throwable throwable, TestInstant now);
 
   /**
    * Template-method that creates a {@link TestResult} object that represents the test outcome of
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java
index d97f320..8dafb2d 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java
@@ -14,14 +14,13 @@
 
 package com.google.testing.junit.runner.model;
 
-import static java.util.concurrent.TimeUnit.NANOSECONDS;
-
 import com.google.testing.junit.junit4.runner.DynamicTestException;
 import com.google.testing.junit.runner.sharding.ShardingEnvironment;
 import com.google.testing.junit.runner.sharding.ShardingFilters;
+import com.google.testing.junit.runner.util.TestClock;
+import com.google.testing.junit.runner.util.TestClock.TestInstant;
 import com.google.testing.junit.runner.util.TestIntegrationsRunnerIntegration;
 import com.google.testing.junit.runner.util.TestPropertyRunnerIntegration;
-import com.google.testing.junit.runner.util.Ticker;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.StringWriter;
@@ -48,7 +47,7 @@
   private final TestSuiteNode rootNode;
   private final Map<Description, TestCaseNode> testCaseMap;
   private final Map<Description, TestNode> testsMap;
-  private final Ticker ticker;
+  private final TestClock testClock;
   private final AtomicBoolean wroteXml = new AtomicBoolean(false);
   private final XmlResultWriter xmlResultWriter;
   @Nullable private final Filter shardingFilter;
@@ -57,7 +56,7 @@
     rootNode = builder.rootNode;
     testsMap = builder.testsMap;
     testCaseMap = filterTestCases(builder.testsMap);
-    ticker = builder.ticker;
+    testClock = builder.testClock;
     shardingFilter = builder.shardingFilter;
     xmlResultWriter = builder.xmlResultWriter;
   }
@@ -142,7 +141,7 @@
   public void testStarted(Description description) {
     TestCaseNode testCase = getTestCase(description);
     if (testCase != null) {
-      testCase.started(currentMillis());
+      testCase.started(now());
       TestPropertyRunnerIntegration.setTestCaseForThread(testCase);
       TestIntegrationsRunnerIntegration.setTestCaseForThread(testCase);
     }
@@ -152,7 +151,7 @@
    * Indicate that the entire test run was interrupted.
    */
   public void testRunInterrupted() {
-    rootNode.testInterrupted(currentMillis());
+    rootNode.testInterrupted(now());
   }
 
   /**
@@ -181,10 +180,9 @@
     if (test != null) {
       if (throwable instanceof DynamicTestException) {
         DynamicTestException dynamicFailure = (DynamicTestException) throwable;
-        test.dynamicTestFailure(
-            dynamicFailure.getTest(), dynamicFailure.getCause(), currentMillis());
+        test.dynamicTestFailure(dynamicFailure.getTest(), dynamicFailure.getCause(), now());
       } else {
-        test.testFailure(throwable, currentMillis());
+        test.testFailure(throwable, now());
       }
     }
   }
@@ -197,7 +195,7 @@
   public void testSkipped(Description description) {
     TestNode test = getTest(description);
     if (test != null) {
-      test.testSkipped(currentMillis());
+      test.testSkipped(now());
     }
   }
 
@@ -210,7 +208,7 @@
   public void testSuppressed(Description description) {
     TestNode test = getTest(description);
     if (test != null) {
-      test.testSuppressed(currentMillis());
+      test.testSuppressed(now());
     }
   }
 
@@ -220,7 +218,7 @@
   public void testFinished(Description description) {
     TestCaseNode testCase = getTestCase(description);
     if (testCase != null) {
-      testCase.finished(currentMillis());
+      testCase.finished(now());
     }
 
     /*
@@ -230,8 +228,8 @@
      */
   }
 
-  private long currentMillis() {
-    return NANOSECONDS.toMillis(ticker.read());
+  private TestInstant now() {
+    return testClock.now();
   }
 
   /**
@@ -285,7 +283,7 @@
    * A builder for creating a model of a test suite.
    */
   public static class Builder {
-    private final Ticker ticker;
+    private final TestClock testClock;
     private final Map<Description, TestNode> testsMap = new ConcurrentHashMap<>();
     private final ShardingEnvironment shardingEnvironment;
     private final ShardingFilters shardingFilters;
@@ -295,9 +293,12 @@
     private boolean buildWasCalled = false;
 
     @Inject
-    public Builder(Ticker ticker, ShardingFilters shardingFilters,
-        ShardingEnvironment shardingEnvironment, XmlResultWriter xmlResultWriter) {
-      this.ticker = ticker;
+    public Builder(
+        TestClock testClock,
+        ShardingFilters shardingFilters,
+        ShardingEnvironment shardingEnvironment,
+        XmlResultWriter xmlResultWriter) {
+      this.testClock = testClock;
       this.shardingFilters = shardingFilters;
       this.shardingEnvironment = shardingEnvironment;
       this.xmlResultWriter = xmlResultWriter;
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModelBuilderFactory.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModelBuilderFactory.java
index 5f5bb30..3faffd2 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModelBuilderFactory.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModelBuilderFactory.java
@@ -18,13 +18,13 @@
 import com.google.testing.junit.runner.sharding.ShardingFilters;
 import com.google.testing.junit.runner.util.Factory;
 import com.google.testing.junit.runner.util.Supplier;
-import com.google.testing.junit.runner.util.Ticker;
+import com.google.testing.junit.runner.util.TestClock;
 
 /**
  * A factory that supplies a top level suite {@link TestSuiteModel.Builder}.
  */
 public final class TestSuiteModelBuilderFactory implements Factory<TestSuiteModel.Builder> {
-  private final Supplier<Ticker> tickerSupplier;
+  private final Supplier<TestClock> tickerSupplier;
 
   private final Supplier<ShardingFilters> shardingFiltersSupplier;
 
@@ -33,7 +33,7 @@
   private final Supplier<XmlResultWriter> xmlResultWriterSupplier;
 
   public TestSuiteModelBuilderFactory(
-      Supplier<Ticker> tickerSupplier,
+      Supplier<TestClock> tickerSupplier,
       Supplier<ShardingFilters> shardingFiltersSupplier,
       Supplier<ShardingEnvironment> shardingEnvironmentSupplier,
       Supplier<XmlResultWriter> xmlResultWriterSupplier) {
@@ -57,7 +57,7 @@
   }
 
   public static Factory<TestSuiteModel.Builder> create(
-      Supplier<Ticker> tickerSupplier,
+      Supplier<TestClock> tickerSupplier,
       Supplier<ShardingFilters> shardingFiltersSupplier,
       Supplier<ShardingEnvironment> shardingEnvironmentSupplier,
       Supplier<XmlResultWriter> xmlResultWriterSupplier) {
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteNode.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteNode.java
index 73996b1..79a9e96 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteNode.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteNode.java
@@ -15,6 +15,7 @@
 package com.google.testing.junit.runner.model;
 
 import com.google.testing.junit.runner.model.TestResult.Status;
+import com.google.testing.junit.runner.util.TestClock.TestInstant;
 import com.google.testing.junit.runner.util.TestIntegration;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -44,35 +45,35 @@
   }
 
   @Override
-  public void testFailure(Throwable throwable, long now) {
+  public void testFailure(Throwable throwable, TestInstant now) {
     for (TestNode child : getChildren()) {
       child.testFailure(throwable, now);
     }
   }
 
   @Override
-  public void dynamicTestFailure(Description test, Throwable throwable, long now) {
+  public void dynamicTestFailure(Description test, Throwable throwable, TestInstant now) {
     for (TestNode child : getChildren()) {
       child.dynamicTestFailure(test, throwable, now);
     }
   }
 
   @Override
-  public void testInterrupted(long now) {
+  public void testInterrupted(TestInstant now) {
     for (TestNode child : getChildren()) {
       child.testInterrupted(now);
     }
   }
 
   @Override
-  public void testSkipped(long now) {
+  public void testSkipped(TestInstant now) {
     for (TestNode child : getChildren()) {
       child.testSkipped(now);
     }
   }
 
   @Override
-  public void testSuppressed(long now) {
+  public void testSuppressed(TestInstant now) {
     for (TestNode child : getChildren()) {
       child.testSuppressed(now);
     }
@@ -101,12 +102,7 @@
 
       TestInterval childRunTime = childResult.getRunTimeInterval();
       if (childRunTime != null) {
-        runTime =
-            runTime == null
-                ? childRunTime
-                : new TestInterval(
-                    Math.min(runTime.getStartMillis(), childRunTime.getStartMillis()),
-                    Math.max(runTime.getEndMillis(), childRunTime.getEndMillis()));
+        runTime = runTime == null ? childRunTime : TestInterval.around(runTime, childRunTime);
       }
     }
 
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/FakeTestClock.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/FakeTestClock.java
new file mode 100644
index 0000000..5929ac5
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/FakeTestClock.java
@@ -0,0 +1,70 @@
+// Copyright 2010 The Bazel Authors. All Rights Reserved.
+//
+// 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.testing.junit.runner.util;
+
+import java.time.Duration;
+import java.time.Instant;
+
+/**
+ * A Ticker whose value can be advanced programmatically in test.
+ *
+ * <p>The ticker can be configured so that the time is incremented whenever {@link #now()} is
+ * called.
+ *
+ * <p>This class is thread-safe.
+ */
+public class FakeTestClock extends TestClock {
+
+  private final Instant wallTimeStart = Instant.EPOCH;
+  private Duration monotonic = Duration.ZERO;
+  private Duration autoIncrementStep = Duration.ZERO;
+
+  /** Advances the ticker value by {@code time} in {@code timeUnit}. */
+  public synchronized FakeTestClock advance(Duration duration) {
+    monotonic = monotonic.plus(duration);
+    return this;
+  }
+
+  /**
+   * Sets the increment applied to the ticker whenever it is queried.
+   *
+   * <p>The default behavior is to auto increment by zero. i.e: The ticker is left unchanged when
+   * queried.
+   */
+  public synchronized FakeTestClock setAutoIncrementStep(Duration autoIncrementStep) {
+    if (autoIncrementStep.toNanos() < 0) {
+      throw new IllegalArgumentException("May not auto-increment by a negative amount");
+    }
+    this.autoIncrementStep = autoIncrementStep;
+    return this;
+  }
+
+  @Override
+  Duration monotonicTime() {
+    return monotonic;
+  }
+
+  @Override
+  Instant wallTime() {
+    return wallTimeStart.plus(monotonic);
+  }
+
+  @Override
+  public synchronized TestInstant now() {
+    advance(autoIncrementStep);
+    return super.now();
+  }
+}
+
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/FakeTicker.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/FakeTicker.java
deleted file mode 100644
index 400a96c..0000000
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/FakeTicker.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright 2010 The Bazel Authors. All Rights Reserved.
-//
-// 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.testing.junit.runner.util;
-
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * A Ticker whose value can be advanced programmatically in test.
- * <p> The ticker can be configured so that the time is incremented whenever {@link #read} is
- * called.
- *
- * <p> This class is thread-safe.
- */
-public class FakeTicker extends Ticker {
-
-  private final AtomicLong nanos = new AtomicLong();
-  private volatile long autoIncrementStepNanos;
-
-  /** Advances the ticker value by {@code time} in {@code timeUnit}. */
-  public FakeTicker advance(long time, TimeUnit timeUnit) {
-    return advance(timeUnit.toNanos(time));
-  }
-
-  /** Advances the ticker value by {@code nanoseconds}. */
-  public FakeTicker advance(long nanoseconds) {
-    nanos.addAndGet(nanoseconds);
-    return this;
-  }
-
-  /**
-   * Sets the increment applied to the ticker whenever it is queried.
-   *
-   * <p>The default behavior is to auto increment by zero. i.e: The ticker is left unchanged when
-   * queried.
-   */
-  public FakeTicker setAutoIncrementStep(long autoIncrementStep, TimeUnit timeUnit) {
-    if (autoIncrementStep < 0) {
-      throw new IllegalArgumentException("May not auto-increment by a negative amount");
-    }
-    this.autoIncrementStepNanos = timeUnit.toNanos(autoIncrementStep);
-    return this;
-  }
-
-  @Override
-  public long read() {
-    return nanos.getAndAdd(autoIncrementStepNanos);
-  }
-}
-
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestClock.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestClock.java
new file mode 100644
index 0000000..9bf3cad
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestClock.java
@@ -0,0 +1,109 @@
+// Copyright 2016 The Bazel Authors. All Rights Reserved.
+//
+// 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.testing.junit.runner.util;
+
+import java.time.Duration;
+import java.time.Instant;
+
+/**
+ * A time source used to obtain:
+ * <li>a monotonic timestamp with no relation to a wall time;
+ * <li>a timestamp that can be used to obtain wall time but is not guaranteed to be monotonic.
+ */
+public abstract class TestClock {
+  /** Constructor for use by subclasses. */
+  protected TestClock() {}
+
+  /**
+   * Returns an immutable value type that contains both a monotonic timestamp (used to measure
+   * relative time but unrelated to wall time) and an EPOCH relative timestamp.
+   */
+  public TestInstant now() {
+    return new TestInstant(wallTime(), monotonicTime());
+  }
+
+  /**
+   * Returns a monotonic timestamp that can only be used to compute relative time.
+   *
+   * <p><b>Warning:</b> the returned timestamp can only be used to measure elapsed time, not wall
+   * time.
+   */
+  abstract Duration monotonicTime();
+
+  /**
+   * A timestamp that may be used to obtain wall time, but is not guaranteed to be monotonic.
+   *
+   * <p><b>Warning:</b> the returned timestamp is not guaranteed to be monotonic, and it may appear
+   * to go back in time in certain cases (e.g. daylight saving time).
+   */
+  abstract Instant wallTime();
+
+  /**
+   * A time source that produces an epoch timestamp using {@link System#currentTimeMillis} and a
+   * monotonic timestamp using {@link System#nanoTime}.
+   */
+  public static TestClock systemClock() {
+    return SYSTEM_TEST_CLOCK;
+  }
+
+  private static final TestClock SYSTEM_TEST_CLOCK =
+      new TestClock() {
+        @Override
+        public Duration monotonicTime() {
+          return Duration.ofNanos(System.nanoTime());
+        }
+
+        @Override
+        public Instant wallTime() {
+          return Instant.ofEpochMilli(System.currentTimeMillis());
+        }
+      };
+
+  /**
+   * An immutable value type that contains both a monotonic timestamp (used to measure relative time
+   * but unrelated to wall time) and an EPOCH timestamp.
+   */
+  public static class TestInstant {
+    public static final TestInstant UNKNOWN = new TestInstant(Instant.EPOCH, Duration.ZERO);
+
+    private final Instant wallTime;
+    private final Duration monotonicTime;
+
+    public TestInstant(Instant wallTime, Duration monotonicTime) {
+      this.wallTime = wallTime;
+      this.monotonicTime = monotonicTime;
+    }
+
+    /**
+     * A timestamp that may be used to obtain wall time, but is not guaranteed to be monotonic.
+     *
+     * <p><b>Warning:</b> the returned timestamp is not guaranteed to be monotonic, and it may
+     * appear to go back in time in certain cases (e.g. daylight saving time).
+     */
+    public Instant wallTime() {
+      return wallTime;
+    }
+
+    /**
+     * Returns a monotonic timestamp that can only be used to compute relative time.
+     *
+     * <p><b>Warning:</b> the returned timestamp can only be used to measure elapsed time, not wall
+     * time.
+     */
+    public Duration monotonicTime() {
+      return monotonicTime;
+    }
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/Ticker.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/Ticker.java
deleted file mode 100644
index 0b00a98..0000000
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/Ticker.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2016 The Bazel Authors. All Rights Reserved.
-//
-// 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.testing.junit.runner.util;
-
-/**
- * A time source; returns a time value representing the number of nanoseconds elapsed since some
- * fixed but arbitrary point in time.
- *
- * <p><b>Warning:</b> this interface can only be used to measure elapsed time, not wall time.
- */
-public abstract class Ticker {
-  /**
-   * Constructor for use by subclasses.
-   */
-  protected Ticker() {}
-
-  /**
-   * Returns the number of nanoseconds elapsed since this ticker's fixed point of reference.
-   */
-  public abstract long read();
-
-  /**
-   * A ticker that reads the current time using {@link System#nanoTime}.
-   *
-   * @since 10.0
-   */
-  public static Ticker systemTicker() {
-    return SYSTEM_TICKER;
-  }
-
-  private static final Ticker SYSTEM_TICKER =
-      new Ticker() {
-        @Override
-        public long read() {
-          return System.nanoTime();
-        }
-      };
-}
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4BazelMock.java b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4BazelMock.java
index 02c1874..bffc655 100644
--- a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4BazelMock.java
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4BazelMock.java
@@ -24,7 +24,7 @@
 import com.google.testing.junit.runner.util.MemoizingSupplier;
 import com.google.testing.junit.runner.util.SetFactory;
 import com.google.testing.junit.runner.util.Supplier;
-import com.google.testing.junit.runner.util.Ticker;
+import com.google.testing.junit.runner.util.TestClock;
 import java.io.PrintStream;
 import java.util.Set;
 import org.junit.internal.TextListener;
@@ -44,7 +44,7 @@
 
   private Supplier<String> topLevelSuiteNameSupplier;
 
-  private Supplier<Ticker> tickerSupplier;
+  private Supplier<TestClock> tickerSupplier;
 
   private Supplier<ShardingEnvironment> shardingEnvironmentSupplier;
 
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4RunnerTest.java b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4RunnerTest.java
index a061a92..c3377ec 100644
--- a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4RunnerTest.java
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4RunnerTest.java
@@ -40,10 +40,10 @@
 import com.google.testing.junit.runner.sharding.api.ShardingFilterFactory;
 import com.google.testing.junit.runner.sharding.testing.FakeShardingFilters;
 import com.google.testing.junit.runner.util.CurrentRunningTest;
-import com.google.testing.junit.runner.util.FakeTicker;
+import com.google.testing.junit.runner.util.FakeTestClock;
 import com.google.testing.junit.runner.util.GoogleTestSecurityManager;
+import com.google.testing.junit.runner.util.TestClock;
 import com.google.testing.junit.runner.util.TestNameProvider;
-import com.google.testing.junit.runner.util.Ticker;
 import java.io.ByteArrayOutputStream;
 import java.io.OutputStream;
 import java.io.PrintStream;
@@ -571,8 +571,8 @@
       return shardingEnvironment;
     }
 
-    Ticker ticker() {
-      return new FakeTicker();
+    TestClock clock() {
+      return new FakeTestClock();
     }
 
     JUnit4Config config() {
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4TestModelBuilderTest.java b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4TestModelBuilderTest.java
index c1a4efb..5fc4bf9 100644
--- a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4TestModelBuilderTest.java
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/JUnit4TestModelBuilderTest.java
@@ -29,8 +29,8 @@
 import com.google.testing.junit.runner.sharding.ShardingEnvironment;
 import com.google.testing.junit.runner.sharding.ShardingFilters;
 import com.google.testing.junit.runner.sharding.testing.StubShardingEnvironment;
-import com.google.testing.junit.runner.util.FakeTicker;
-import com.google.testing.junit.runner.util.Ticker;
+import com.google.testing.junit.runner.util.FakeTestClock;
+import com.google.testing.junit.runner.util.TestClock;
 import java.util.List;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -46,15 +46,18 @@
  */
 @RunWith(JUnit4.class)
 public class JUnit4TestModelBuilderTest {
-  private final Ticker fakeTicker = new FakeTicker();
+  private final TestClock fakeTestClock = new FakeTestClock();
   private final ShardingEnvironment stubShardingEnvironment = new StubShardingEnvironment();
   private final XmlResultWriter xmlResultWriter = new AntXmlResultWriter();
 
   private JUnit4TestModelBuilder builder(Request request, String suiteName,
       ShardingEnvironment shardingEnvironment, ShardingFilters shardingFilters,
       XmlResultWriter xmlResultWriter) {
-    return new JUnit4TestModelBuilder(request, suiteName, new TestSuiteModel.Builder(
-        fakeTicker, shardingFilters, shardingEnvironment, xmlResultWriter));
+    return new JUnit4TestModelBuilder(
+        request,
+        suiteName,
+        new TestSuiteModel.Builder(
+            fakeTestClock, shardingFilters, shardingEnvironment, xmlResultWriter));
   }
 
   @Test
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/TestModuleTickerFactory.java b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/TestModuleTickerFactory.java
index 9537666..5975b3c 100644
--- a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/TestModuleTickerFactory.java
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4/TestModuleTickerFactory.java
@@ -15,12 +15,10 @@
 package com.google.testing.junit.runner.junit4;
 
 import com.google.testing.junit.runner.util.Factory;
-import com.google.testing.junit.runner.util.Ticker;
+import com.google.testing.junit.runner.util.TestClock;
 
-/**
- * A factory that supplies a {@link Ticker} for testing purposes.
- */
-public final class TestModuleTickerFactory implements Factory<Ticker> {
+/** A factory that supplies a {@link TestClock} for testing purposes. */
+public final class TestModuleTickerFactory implements Factory<TestClock> {
   private final JUnit4RunnerTest.TestModule module;
 
   public TestModuleTickerFactory(JUnit4RunnerTest.TestModule module) {
@@ -29,15 +27,15 @@
   }
 
   @Override
-  public Ticker get() {
-    Ticker ticker = module.ticker();
-    if (ticker == null) {
+  public TestClock get() {
+    TestClock testClock = module.clock();
+    if (testClock == null) {
       throw new NullPointerException();
     }
-    return ticker;
+    return testClock;
   }
 
-  public static Factory<Ticker> create(JUnit4RunnerTest.TestModule module) {
+  public static Factory<TestClock> create(JUnit4RunnerTest.TestModule module) {
     return new TestModuleTickerFactory(module);
   }
 }
\ No newline at end of file
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/AntXmlResultWriterTest.java b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/AntXmlResultWriterTest.java
index 3b208388..f4187d9 100644
--- a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/AntXmlResultWriterTest.java
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/AntXmlResultWriterTest.java
@@ -15,18 +15,31 @@
 package com.google.testing.junit.runner.model;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.testing.junit.runner.model.TestInstantUtil.testInstant;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.testing.junit.runner.util.TestClock.TestInstant;
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.StringWriter;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.Description;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
 
 @RunWith(JUnit4.class)
 public class AntXmlResultWriterTest {
-  private static final long NOW = 1;
+
   private static TestSuiteNode root;
   private static XmlWriter writer;
   private static AntXmlResultWriter resultWriter;
@@ -69,9 +82,37 @@
     assertThat(resultXml).doesNotContain("<testcase name='testCase2'");
   }
 
+  @Test
+  public void testWallTimeAndMonotonicTimestamp() throws Exception {
+    TestSuiteNode parent = createTestSuite();
+    TestCaseNode test = createTestCase(parent);
+
+    // wall time may appear to go back in time in exceptional cases (e.g. daylight saving time)
+    test.started(new TestInstant(Instant.ofEpochMilli(1560786184600L), Duration.ZERO));
+    test.finished(new TestInstant(Instant.EPOCH, Duration.ofMillis(1L)));
+
+    resultWriter.writeTestSuites(writer, root.getResult());
+
+    String resultXml = stringWriter.toString();
+    assertThat(resultXml).contains("time=");
+    assertThat(resultXml).contains("timestamp=");
+
+    Document document = parseXml(resultXml);
+    Element testSuites = document.getDocumentElement();
+    Element testSuite = (Element) testSuites.getElementsByTagName("testsuite").item(0);
+    assertThat(testSuite.getTagName()).isEqualTo("testsuite");
+    assertThat(testSuite.getAttribute("name"))
+        .isEqualTo("com.google.testing.junit.runner.model.TestCaseNodeTest$TestSuite");
+    assertThat(testSuite.getAttribute("time")).isEqualTo("0.001");
+    assertThat(
+            Instant.from(
+                DateTimeFormatter.ISO_DATE_TIME.parse(testSuite.getAttribute("timestamp"))))
+        .isEqualTo(Instant.ofEpochMilli(1560786184600L));
+  }
+
   private void runToCompletion(TestCaseNode test) {
-    test.started(NOW);
-    test.finished(NOW + 1);
+    test.started(testInstant(Instant.ofEpochMilli(1)));
+    test.finished(testInstant(Instant.ofEpochMilli(2)));
   }
 
   private TestCaseNode createTestCase(TestSuiteNode parent) {
@@ -88,4 +129,12 @@
     root.addTestSuite(parent);
     return parent;
   }
+
+  private static Document parseXml(String testXml)
+      throws SAXException, ParserConfigurationException, IOException {
+    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+    factory.setIgnoringElementContentWhitespace(true);
+    DocumentBuilder documentBuilder = factory.newDocumentBuilder();
+    return documentBuilder.parse(new ByteArrayInputStream(testXml.getBytes(UTF_8)));
+  }
 }
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestCaseNodeTest.java b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestCaseNodeTest.java
index 852dd7b..9310eb7 100644
--- a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestCaseNodeTest.java
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestCaseNodeTest.java
@@ -15,7 +15,12 @@
 package com.google.testing.junit.runner.model;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.testing.junit.runner.model.TestInstantUtil.advance;
+import static com.google.testing.junit.runner.model.TestInstantUtil.testInstant;
 
+import com.google.testing.junit.runner.util.TestClock.TestInstant;
+import java.time.Duration;
+import java.time.Instant;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.Description;
@@ -28,7 +33,7 @@
 @RunWith(JUnit4.class)
 public class TestCaseNodeTest {
 
-  private static final long NOW = 1;
+  private static final TestInstant NOW = testInstant(Instant.ofEpochMilli(1));
   private static Description suite;
   private static Description testCase;
 
@@ -88,7 +93,7 @@
     TestCaseNode testCaseNode = new TestCaseNode(testCase, new TestSuiteNode(suite));
     testCaseNode.pending();
     testCaseNode.started(NOW);
-    testCaseNode.testInterrupted(NOW + 1);
+    testCaseNode.testInterrupted(advance(NOW, Duration.ofMillis(1)));
     assertStatusAndTiming(testCaseNode, TestResult.Status.INTERRUPTED, NOW, 1);
   }
 
@@ -97,7 +102,7 @@
     TestCaseNode testCaseNode = new TestCaseNode(testCase, new TestSuiteNode(suite));
     testCaseNode.pending();
     testCaseNode.started(NOW);
-    testCaseNode.testSkipped(NOW + 1);
+    testCaseNode.testSkipped(advance(NOW, Duration.ofMillis(1)));
     assertStatusAndTiming(testCaseNode, TestResult.Status.SKIPPED, NOW, 1);
   }
 
@@ -106,7 +111,7 @@
     TestCaseNode testCaseNode = new TestCaseNode(testCase, new TestSuiteNode(suite));
     testCaseNode.pending();
     testCaseNode.started(NOW);
-    testCaseNode.finished(NOW + 1);
+    testCaseNode.finished(advance(NOW, Duration.ofMillis(1)));
     assertStatusAndTiming(testCaseNode, TestResult.Status.COMPLETED, NOW, 1);
   }
 
@@ -115,8 +120,8 @@
     TestCaseNode testCaseNode = new TestCaseNode(testCase, new TestSuiteNode(suite));
     testCaseNode.pending();
     testCaseNode.started(NOW);
-    testCaseNode.testFailure(new Exception(), NOW + 1);
-    testCaseNode.finished(NOW + 2);
+    testCaseNode.testFailure(new Exception(), advance(NOW, Duration.ofMillis(1)));
+    testCaseNode.finished(advance(NOW, Duration.ofMillis(2)));
     assertStatusAndTiming(testCaseNode, TestResult.Status.COMPLETED, NOW, 2);
   }
 
@@ -125,8 +130,8 @@
     TestCaseNode testCaseNode = new TestCaseNode(testCase, new TestSuiteNode(suite));
     testCaseNode.pending();
     testCaseNode.started(NOW);
-    testCaseNode.testFailure(new Exception(), NOW + 1);
-    testCaseNode.testInterrupted(NOW + 2);
+    testCaseNode.testFailure(new Exception(), advance(NOW, Duration.ofMillis(1)));
+    testCaseNode.testInterrupted(advance(NOW, Duration.ofMillis(2)));
     assertStatusAndTiming(testCaseNode, TestResult.Status.INTERRUPTED, NOW, 2);
   }
 
@@ -139,11 +144,12 @@
   }
 
   private void assertStatusAndTiming(
-      TestCaseNode testCase, TestResult.Status status, long start, long duration) {
+      TestCaseNode testCase, TestResult.Status status, TestInstant start, long duration) {
     TestResult result = testCase.getResult();
     assertThat(result.getStatus()).isEqualTo(status);
     assertThat(result.getRunTimeInterval()).isNotNull();
-    assertThat(result.getRunTimeInterval().getStartMillis()).isEqualTo(start);
+    assertThat(result.getRunTimeInterval().getStartMillis())
+        .isEqualTo(start.wallTime().toEpochMilli());
     assertThat(result.getRunTimeInterval().toDurationMillis()).isEqualTo(duration);
   }
 
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestInstantUtil.java b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestInstantUtil.java
new file mode 100644
index 0000000..ac168fe
--- /dev/null
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestInstantUtil.java
@@ -0,0 +1,38 @@
+// Copyright 2015 The Bazel Authors. All Rights Reserved.
+//
+// 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.testing.junit.runner.model;
+
+import com.google.testing.junit.runner.util.TestClock.TestInstant;
+import java.time.Duration;
+import java.time.Instant;
+
+/** Utility class used for quick creation of TestInstant for testing. */
+public class TestInstantUtil {
+
+  // Added to the monotonic nano timestamp to assert it is not used as absolute time
+  private static final long INITIAL_RELATIVE_TIMESTAMP = 111111L;
+
+  /** Creates a TestInstant with a monotonic timestamp that is offset from the wall time. */
+  public static TestInstant testInstant(Instant wallTime) {
+    return new TestInstant(
+        wallTime, Duration.ofMillis(INITIAL_RELATIVE_TIMESTAMP + wallTime.toEpochMilli()));
+  }
+
+  /** Returns a TestInstant advanced in time by the specified duration. */
+  public static TestInstant advance(TestInstant instant, Duration duration) {
+    return new TestInstant(
+        instant.wallTime().plus(duration), instant.monotonicTime().plus(duration));
+  }
+}
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestIntervalTest.java b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestIntervalTest.java
index 26573e8f..70b70f0 100644
--- a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestIntervalTest.java
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestIntervalTest.java
@@ -15,7 +15,11 @@
 package com.google.testing.junit.runner.model;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.testing.junit.runner.model.TestInstantUtil.testInstant;
 
+import com.google.testing.junit.runner.util.TestClock.TestInstant;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Date;
 import java.util.TimeZone;
 import org.junit.Rule;
@@ -30,11 +34,13 @@
 
   @Test
   public void testCreation() {
-    TestInterval interval = new TestInterval(123456, 234567);
+    Instant start = Instant.ofEpochMilli(123456);
+    Instant end = Instant.ofEpochMilli(234567);
+    TestInterval interval = new TestInterval(testInstant(start), testInstant(end));
     assertThat(interval.getStartMillis()).isEqualTo(123456);
     assertThat(interval.getEndMillis()).isEqualTo(234567);
 
-    interval = new TestInterval(123456, 123456);
+    interval = new TestInterval(testInstant(start), testInstant(start));
     assertThat(interval.getStartMillis()).isEqualTo(123456);
     assertThat(interval.getEndMillis()).isEqualTo(123456);
   }
@@ -43,19 +49,43 @@
   public void testCreationFailure() {
     thrown.expect(IllegalArgumentException.class);
     thrown.expectMessage("Start must be before end");
-    new TestInterval(35, 23);
+    new TestInterval(testInstant(Instant.ofEpochMilli(35)), testInstant(Instant.ofEpochMilli(23)));
   }
 
   @Test
   public void testToDuration() {
-    assertThat(new TestInterval(50, 150).toDurationMillis()).isEqualTo(100);
-    assertThat(new TestInterval(100, 100).toDurationMillis()).isEqualTo(0);
+    assertThat(
+            new TestInterval(
+                    testInstant(Instant.ofEpochMilli(50)), testInstant(Instant.ofEpochMilli(150)))
+                .toDurationMillis())
+        .isEqualTo(100);
+    assertThat(
+            new TestInterval(
+                    testInstant(Instant.ofEpochMilli(100)), testInstant(Instant.ofEpochMilli(100)))
+                .toDurationMillis())
+        .isEqualTo(0);
+  }
+
+  @Test
+  public void testToDurationOnNonMonotonicWallTime() {
+    Instant start = Instant.ofEpochMilli(123456);
+    Instant end = Instant.ofEpochMilli(123456);
+    Duration monotonicStart = Duration.ofMillis(50);
+    Duration monotonicEnd = Duration.ofMillis(150);
+    TestInterval interval =
+        new TestInterval(
+            new TestInstant(start, monotonicStart), new TestInstant(end, monotonicEnd));
+    assertThat(interval.getStartMillis()).isEqualTo(123456);
+    assertThat(interval.getEndMillis()).isEqualTo(123456);
+    assertThat(interval.toDurationMillis()).isEqualTo(100);
   }
 
   @Test
   public void testDateFormat() {
     Date date = new Date(1471709734000L);
-    TestInterval interval = new TestInterval(date.getTime(), date.getTime() + 100);
+    TestInterval interval =
+        new TestInterval(
+            testInstant(date.toInstant()), testInstant(date.toInstant().plusMillis(100)));
     assertThat(interval.startInstantToString(TimeZone.getTimeZone("America/New_York")))
         .isEqualTo("2016-08-20T12:15:34.000-04:00");
     assertThat(interval.startInstantToString(TimeZone.getTimeZone("GMT")))
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestSuiteNodeTest.java b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestSuiteNodeTest.java
index e6448c5..09ef917 100644
--- a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestSuiteNodeTest.java
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/model/TestSuiteNodeTest.java
@@ -20,6 +20,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 
+import com.google.testing.junit.runner.util.TestClock.TestInstant;
+import java.time.Duration;
+import java.time.Instant;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.Description;
@@ -33,7 +36,7 @@
 @RunWith(MockitoJUnitRunner.class)
 public class TestSuiteNodeTest {
 
-  private static final long NOW = 1;
+  private static final TestInstant NOW = new TestInstant(Instant.EPOCH, Duration.ZERO);
 
   @Mock private TestCaseNode testCaseNode;
   private TestSuiteNode testSuiteNode;