AsyncLoop: Make NowMs() a non-static method

And introduced the ScopedTestClock class that can be used to
inject a deterministic clock in an existing AsyncLoop instance
during testing.

Fuchsia-Topic: advanced-ipc
Change-Id: I894ada19b92c1a5eb2389500e757024db141dbe4
Reviewed-on: https://fuchsia-review.googlesource.com/c/third_party/github.com/ninja-build/ninja/+/1037596
Fuchsia-Auto-Submit: David Turner <digit@google.com>
Reviewed-by: David Fang <fangism@google.com>
Commit-Queue: Auto-Submit <auto-submit@fuchsia-infra.iam.gserviceaccount.com>
diff --git a/src/async_loop-posix.h b/src/async_loop-posix.h
index c4664c7..3deaece 100644
--- a/src/async_loop-posix.h
+++ b/src/async_loop-posix.h
@@ -700,7 +700,7 @@
     int64_t timer_expiration_ms = timers_.ComputeNextExpiration();
     if (timer_expiration_ms >= 0) {
       has_timers = true;
-      int64_t now_ms = NowMs();
+      int64_t now_ms = loop.NowMs();
       int64_t timer_timeout_ms = timer_expiration_ms - now_ms;
       if (timer_timeout_ms < 0) {
         timer_timeout_ms = 0;
@@ -747,7 +747,7 @@
         result = ExitSuccess;
     }
 
-    if (has_timers && timers_.ProcessExpiration(NowMs()))
+    if (has_timers && timers_.ProcessExpiration(loop.NowMs()))
       result = ExitSuccess;
 
     return result;
diff --git a/src/async_loop-win32.h b/src/async_loop-win32.h
index 0cd1d7c..401ae73 100644
--- a/src/async_loop-win32.h
+++ b/src/async_loop-win32.h
@@ -335,7 +335,7 @@
     int64_t timer_expiration_ms = timers_.ComputeNextExpiration();
     if (timer_expiration_ms >= 0) {
       has_timers = true;
-      int64_t timer_timeout_ms = timer_expiration_ms - NowMs();
+      int64_t timer_timeout_ms = timer_expiration_ms - loop.NowMs();
       if (timer_timeout_ms < 0)
         timer_timeout_ms = 0;
 
@@ -401,7 +401,7 @@
       async_op->InvokeCallback();
     }
 
-    if (has_timers && !timers_.ProcessExpiration(NowMs()))
+    if (has_timers && !timers_.ProcessExpiration(loop.NowMs()))
       return ExitTimeout;
 
     return ExitSuccess;
diff --git a/src/async_loop.cc b/src/async_loop.cc
index e6fee7d..b3ac58c 100644
--- a/src/async_loop.cc
+++ b/src/async_loop.cc
@@ -27,6 +27,18 @@
   return std::string(strerror(error));
 }
 
+static int64_t AsyncLoopGlobalClock() {
+  static bool init = false;
+  static int64_t start_ms = 0;
+  int64_t result = GetTimeMillis();
+  if (!init) {
+    start_ms = result;
+    init = true;
+  }
+  return result - start_ms;
+}
+
+
 // Global instance.
 static std::unique_ptr<AsyncLoop> s_loop;
 
@@ -64,14 +76,16 @@
 
 // static
 int64_t AsyncLoop::NowMs() {
-  static bool init = false;
-  static int64_t start_ms = 0;
-  int64_t result = GetTimeMillis();
-  if (!init) {
-    start_ms = result;
-    init = true;
-  }
-  return result - start_ms;
+  if (clock_)
+    return (*clock_)();
+  else
+    return AsyncLoopGlobalClock();
+}
+
+AsyncLoop::Clock* AsyncLoop::ChangeInternalClock(Clock* clock) {
+  Clock* result = clock_;
+  clock_ = clock;
+  return result;
 }
 
 AsyncLoop::ExitStatus AsyncLoop::RunOnce(int64_t timeout_ms) {
@@ -241,9 +255,9 @@
     state_->Cancel();
 }
 
-AsyncHandle& AsyncHandle::ResetCallback(AsyncHandle::Callback&& callback) {
+AsyncHandle& AsyncHandle::ResetCallback(AsyncHandle::Callback&& cb) {
   assert(state_ && "ResetCallback() on invalid AsyncHandle value");
-  state_->ResetCallback(std::move(callback));
+  state_->ResetCallback(std::move(cb));
   return *this;
 }
 
diff --git a/src/async_loop.h b/src/async_loop.h
index 66f1b74..bd9ae50 100644
--- a/src/async_loop.h
+++ b/src/async_loop.h
@@ -327,7 +327,7 @@
 
   /// Return current time in milliseconds. Epoch is undetermined
   /// but all values are guaranteed to be non-negative.
-  static int64_t NowMs();
+  int64_t NowMs();
 
   /// Possible return values for RunOnce() method.
   enum ExitStatus {
@@ -415,6 +415,49 @@
   };
 
  private:
+  /// A callable object that returns the current time in milliseconds.
+  /// Result must always be >= 0.
+  using Clock = std::function<int64_t(void)>;
+
+ public:
+  /// Convenience struct used to inject a custom clock into an AsyncLoop instance
+  /// during tests. This changes the values returned by the NowMs() method, to
+  /// make them entirely deterministic (including when timers expire).
+  ///
+  /// Usage is:
+  ///
+  ///  {
+  ///    AsyncLoop::ScopedTestClock test_clock(async_loop);
+  ///
+  ///    EXPECT_EQ(0LL, async_loop.NowMs());
+  ///
+  ///    test_clock.AdvanceTimeMs(120);
+  ///    EXPECT_EQ(120LL, async_loop.NowMs());
+  ///
+  ///    test_clock.AdvanceTimeMs(120);
+  ///    EXPECT_EQ(240LL, async_loop.NowMs());
+  ///  }
+  ///
+  struct ScopedTestClock {
+    explicit ScopedTestClock(AsyncLoop& async_loop)
+        : async_loop_(async_loop),
+          prev_clock_(async_loop_.ChangeInternalClock(&clock_)) {}
+
+    ~ScopedTestClock() {
+      async_loop_.ChangeInternalClock(prev_clock_);
+    }
+
+    void AdvanceTimeMillis(uint64_t increment_ms) {
+      current_time_ms_ += static_cast<int64_t>(increment_ms);
+    }
+
+    int64_t current_time_ms_ = 0;
+    AsyncLoop& async_loop_;
+    AsyncLoop::Clock* prev_clock_;
+    AsyncLoop::Clock clock_ = [this]() -> int64_t { return current_time_ms_; };
+  };
+
+ private:
   friend class AsyncHandle::State;
   friend class AsyncTimer::State;
   friend class AsyncLoopTimers;
@@ -451,6 +494,9 @@
     ExitStatus status_ = ExitIdle;
   };
 
+  /// Used internally by ScopedTestClock class.
+  Clock* ChangeInternalClock(Clock* clock);
+
   /// Used internally by ScopedInterruptCatcher class.
   void ChangeInterruptCatcher(bool increment);
 
@@ -458,6 +504,7 @@
   class Impl;
   std::unique_ptr<Impl> impl_;
   int interrupt_catcher_count_ = 0;
+  Clock* clock_ = nullptr;
 };
 
 #endif  // NINJA_ASYNC_LOOP_H
diff --git a/src/async_loop_test.cc b/src/async_loop_test.cc
index cbc1e23..3c6c7a9 100644
--- a/src/async_loop_test.cc
+++ b/src/async_loop_test.cc
@@ -36,6 +36,30 @@
   EXPECT_GE(loop.NowMs(), 0LL);
 }
 
+TEST(AsyncLoop, ScopedTestClock) {
+  AsyncLoop& loop = GetNewLoop();
+  // Ensure the result of NowMs() is always positive, otherwise many
+  // things will not work correctly since a negative expiration date is
+  // interpreted as infinite.
+  int64_t start_time_ms = loop.NowMs();
+  EXPECT_GE(start_time_ms, 0LL);
+
+  {
+    AsyncLoop::ScopedTestClock test_clock(loop);
+    EXPECT_EQ(0LL, loop.NowMs());
+    test_clock.AdvanceTimeMillis(120);
+    EXPECT_EQ(120LL, loop.NowMs());
+    test_clock.AdvanceTimeMillis(120);
+    EXPECT_EQ(240LL, loop.NowMs());
+    test_clock.AdvanceTimeMillis(10000);
+    EXPECT_EQ(10240LL, loop.NowMs());
+  }
+
+  int64_t end_time_ms = loop.NowMs();
+  ASSERT_GE(end_time_ms, start_time_ms);
+  ASSERT_LT(end_time_ms - start_time_ms, 10240LL);
+}
+
 // Helper type to store async operation results.
 struct AsyncResult {
   bool completed = false;
@@ -170,6 +194,7 @@
 
 TEST(AsyncLoop, RunUntil) {
   AsyncLoop& loop = GetNewLoop();
+  AsyncLoop::ScopedTestClock test_clock(loop);
 
   auto always_false = []() { return false; };
 
@@ -185,6 +210,10 @@
 
   timer.SetDurationMs(100LL);
   status = loop.RunUntil(flag_is_set, -1);
+  EXPECT_EQ(AsyncLoop::ExitTimeout, status);
+
+  test_clock.AdvanceTimeMillis(200);
+  status = loop.RunUntil(flag_is_set, -1);
   EXPECT_EQ(AsyncLoop::ExitSuccess, status);
 
   flag = false;
@@ -193,19 +222,40 @@
   EXPECT_EQ(AsyncLoop::ExitTimeout, status);
 
   status = loop.RunUntil(flag_is_set, -1);
+  EXPECT_EQ(AsyncLoop::ExitTimeout, status);
+
+  test_clock.AdvanceTimeMillis(1000);
+  status = loop.RunUntil(flag_is_set, -1);
   EXPECT_EQ(AsyncLoop::ExitSuccess, status);
 }
 
 TEST(AsyncLoop, TimerTest) {
   AsyncLoop& loop = GetNewLoop();
+  AsyncLoop::ScopedTestClock test_clock(loop);
+
   int counter = 0;
   AsyncTimer timer_1(loop, [&counter]() { counter += 1; });
   timer_1.SetDurationMs(100LL);
+
   EXPECT_EQ(AsyncLoop::ExitTimeout, loop.RunOnce(0));
   EXPECT_EQ(0, counter);
 
-  EXPECT_EQ(AsyncLoop::ExitSuccess, loop.RunOnce(200));
+  test_clock.AdvanceTimeMillis(99);
+  EXPECT_EQ(AsyncLoop::ExitTimeout, loop.RunOnce(0));
+  EXPECT_EQ(0, counter);
+
+  test_clock.AdvanceTimeMillis(1);
+  EXPECT_EQ(AsyncLoop::ExitSuccess, loop.RunOnce(0));
+
+  timer_1.SetDurationMs(100LL);
+  EXPECT_EQ(AsyncLoop::ExitTimeout, loop.RunOnce(0));
   EXPECT_EQ(1, counter);
+
+  test_clock.AdvanceTimeMillis(100);
+  EXPECT_EQ(AsyncLoop::ExitSuccess, loop.RunOnce(300));
+  EXPECT_EQ(2, counter);
+
+  EXPECT_EQ(200LL, loop.NowMs());
 }
 
 TEST(AsyncLoopTimers, Test) {