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) {