[K/N] Add mockable clock. ^KT-48537
This commit is contained in:
committed by
Space
parent
e6b7a142b2
commit
eae9d05f50
@@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||||
|
* that can be found in the LICENSE file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <future>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "Saturating.hpp"
|
||||||
|
|
||||||
|
namespace kotlin {
|
||||||
|
|
||||||
|
namespace internal {
|
||||||
|
|
||||||
|
template <typename NowF, typename StepDuration, typename Clock, typename Rep, typename Period, typename Ret, typename WaitForF>
|
||||||
|
Ret waitUntilViaFor(
|
||||||
|
NowF&& nowF,
|
||||||
|
StepDuration step,
|
||||||
|
std::chrono::time_point<Clock, std::chrono::duration<saturating<Rep>, Period>> until,
|
||||||
|
Ret timeoutValue,
|
||||||
|
WaitForF&& waitForF) {
|
||||||
|
while (true) {
|
||||||
|
auto current = std::invoke(std::forward<NowF>(nowF));
|
||||||
|
if (current >= until) {
|
||||||
|
return timeoutValue;
|
||||||
|
}
|
||||||
|
auto left = until - current;
|
||||||
|
// Shield standard library from saturating types.
|
||||||
|
auto interval = left > step ? std::chrono::duration<Rep, Period>(step) : std::chrono::duration<Rep, Period>(left);
|
||||||
|
if (auto value = std::invoke(std::forward<WaitForF>(waitForF), interval); value != timeoutValue) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename NowF, typename StepDuration, typename Clock, typename Rep, typename Period, typename WaitForF>
|
||||||
|
void waitUntilViaFor(
|
||||||
|
NowF&& nowF,
|
||||||
|
StepDuration step,
|
||||||
|
std::chrono::time_point<Clock, std::chrono::duration<saturating<Rep>, Period>> until,
|
||||||
|
WaitForF&& waitForF) {
|
||||||
|
while (true) {
|
||||||
|
auto current = std::invoke(std::forward<NowF>(nowF));
|
||||||
|
if (current >= until) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto left = until - current;
|
||||||
|
// Shield standard library from saturating types.
|
||||||
|
auto interval = left > step ? std::chrono::duration<Rep, Period>(step) : std::chrono::duration<Rep, Period>(left);
|
||||||
|
std::invoke(std::forward<WaitForF>(waitForF), interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T, typename Lock>
|
||||||
|
struct IsStdCV : public std::false_type {};
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct IsStdCV<std::condition_variable, std::unique_lock<std::mutex>> : public std::true_type {};
|
||||||
|
|
||||||
|
template <typename Lock>
|
||||||
|
struct IsStdCV<std::condition_variable_any, Lock> : public std::true_type {};
|
||||||
|
|
||||||
|
template <typename T, typename Lock>
|
||||||
|
inline constexpr bool isStdCV = IsStdCV<T, Lock>::value;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct IsStdFuture : public std::false_type {};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct IsStdFuture<std::future<T>> : public std::true_type {};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct IsStdFuture<std::shared_future<T>> : public std::true_type {};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
inline constexpr bool isStdFuture = IsStdFuture<T>::value;
|
||||||
|
|
||||||
|
template <typename Clock>
|
||||||
|
class ClockWaitImpl {
|
||||||
|
public:
|
||||||
|
template <typename Rep, typename Period>
|
||||||
|
static void sleep_for(std::chrono::duration<Rep, Period> interval) {
|
||||||
|
// Not using this_thread::sleep_for, because it may mishandle "infinite" intervals. Use saturating arithmetics to address this.
|
||||||
|
return ClockWaitImpl<Clock>::sleep_until(Clock::now() + interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Rep, typename Period>
|
||||||
|
static void sleep_until(std::chrono::time_point<Clock, std::chrono::duration<Rep, Period>> until) {
|
||||||
|
if constexpr (is_saturating_v<Rep>) {
|
||||||
|
return Clock::sleepImpl(until);
|
||||||
|
} else {
|
||||||
|
return ClockWaitImpl<Clock>::sleep_until(std::chrono::time_point<Clock, std::chrono::duration<saturating<Rep>, Period>>(until));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename CV, typename Lock, typename Rep, typename Period, typename F, typename = std::enable_if_t<isStdCV<CV, Lock>>>
|
||||||
|
static bool wait_for(CV& cv, Lock& lock, std::chrono::duration<Rep, Period> interval, F&& f) {
|
||||||
|
// Not using cv.wait_for, because it may mishandle "infinite" intervals. Use saturating arithmetics to address this.
|
||||||
|
return ClockWaitImpl<Clock>::wait_until(cv, lock, Clock::now() + interval, std::forward<F>(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename CV, typename Lock, typename Rep, typename Period, typename F, typename = std::enable_if_t<isStdCV<CV, Lock>>>
|
||||||
|
static bool wait_until(CV& cv, Lock& lock, std::chrono::time_point<Clock, std::chrono::duration<Rep, Period>> until, F&& f) {
|
||||||
|
if constexpr (is_saturating_v<Rep>) {
|
||||||
|
[[maybe_unused]] auto pendingWait = Clock::addPendingWait(until);
|
||||||
|
// Implement in terms of repeated cv.wait_for of non-"infinite" intervals.
|
||||||
|
return internal::waitUntilViaFor(&Clock::now, Clock::wait_step, until, false, [&](auto interval) {
|
||||||
|
return cv.wait_for(lock, interval, std::forward<F>(f));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return ClockWaitImpl<Clock>::wait_until(
|
||||||
|
cv, lock, std::chrono::time_point<Clock, std::chrono::duration<saturating<Rep>, Period>>(until), std::forward<F>(f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Future, typename Rep, typename Period, typename = std::enable_if_t<isStdFuture<Future>>>
|
||||||
|
static std::future_status wait_for(const Future& future, std::chrono::duration<Rep, Period> interval) {
|
||||||
|
// Not using future.wait_for, because it may mishandle "infinite" intervals. Use saturating arithmetics to address this.
|
||||||
|
return ClockWaitImpl<Clock>::wait_until(future, Clock::now() + interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Future, typename Rep, typename Period, typename = std::enable_if_t<isStdFuture<Future>>>
|
||||||
|
static std::future_status wait_until(const Future& future, std::chrono::time_point<Clock, std::chrono::duration<Rep, Period>> until) {
|
||||||
|
if constexpr (is_saturating_v<Rep>) {
|
||||||
|
[[maybe_unused]] auto pendingWait = Clock::addPendingWait(until);
|
||||||
|
// Implement in terms of repeated future.wait_for of non-"infinite" intervals.
|
||||||
|
return internal::waitUntilViaFor(&Clock::now, Clock::wait_step, until, std::future_status::timeout, [&](auto interval) {
|
||||||
|
return future.wait_for(interval);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return ClockWaitImpl<Clock>::wait_until(
|
||||||
|
future, std::chrono::time_point<Clock, std::chrono::duration<saturating<Rep>, Period>>(until));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace internal
|
||||||
|
|
||||||
|
using nanoseconds = std::chrono::duration<saturating<std::chrono::nanoseconds::rep>, std::chrono::nanoseconds::period>;
|
||||||
|
using microseconds = std::chrono::duration<saturating<std::chrono::microseconds::rep>, std::chrono::microseconds::period>;
|
||||||
|
using milliseconds = std::chrono::duration<saturating<std::chrono::milliseconds::rep>, std::chrono::milliseconds::period>;
|
||||||
|
using seconds = std::chrono::duration<saturating<std::chrono::seconds::rep>, std::chrono::seconds::period>;
|
||||||
|
using minutes = std::chrono::duration<saturating<std::chrono::minutes::rep>, std::chrono::minutes::period>;
|
||||||
|
using hours = std::chrono::duration<saturating<std::chrono::hours::rep>, std::chrono::hours::period>;
|
||||||
|
|
||||||
|
class steady_clock : public internal::ClockWaitImpl<steady_clock> {
|
||||||
|
public:
|
||||||
|
using rep = saturating<std::chrono::steady_clock::rep>;
|
||||||
|
using period = std::chrono::steady_clock::period;
|
||||||
|
using duration = std::chrono::duration<rep, period>;
|
||||||
|
using time_point = std::chrono::time_point<steady_clock>;
|
||||||
|
|
||||||
|
static constexpr bool is_steady = true;
|
||||||
|
|
||||||
|
static time_point now() noexcept {
|
||||||
|
auto time = std::chrono::steady_clock::now().time_since_epoch();
|
||||||
|
return time_point(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class internal::ClockWaitImpl<steady_clock>;
|
||||||
|
|
||||||
|
// Use non-saturating type here, because step may be fed into the standard library.
|
||||||
|
static inline constexpr auto wait_step = std::chrono::hours(24);
|
||||||
|
|
||||||
|
template <typename Rep, typename Period>
|
||||||
|
static void sleepImpl(std::chrono::time_point<steady_clock, std::chrono::duration<saturating<Rep>, Period>> until) {
|
||||||
|
// Implement in terms of repeated this_thread::sleep_for of non-"infinite" intervals.
|
||||||
|
return internal::waitUntilViaFor(&now, wait_step, until, [&](auto interval) { std::this_thread::sleep_for(interval); });
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Rep, typename Period>
|
||||||
|
static int addPendingWait(std::chrono::time_point<steady_clock, std::chrono::duration<saturating<Rep>, Period>> until) {
|
||||||
|
// No need to register here.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace kotlin
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||||
|
* that can be found in the LICENSE file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ClockTestSupport.hpp"
|
||||||
|
|
||||||
|
using namespace kotlin;
|
||||||
|
|
||||||
|
// static
|
||||||
|
std::atomic<test_support::manual_clock::time_point> test_support::manual_clock::now_ = test_support::manual_clock::time_point::min();
|
||||||
|
|
||||||
|
// static
|
||||||
|
std::mutex test_support::manual_clock::pendingWaitsMutex_;
|
||||||
|
|
||||||
|
// static
|
||||||
|
KStdOrderedMultiset<test_support::manual_clock::time_point> test_support::manual_clock::pendingWaits_;
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||||
|
* that can be found in the LICENSE file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Clock.hpp"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "gtest/gtest.h"
|
||||||
|
|
||||||
|
#include "Types.h"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace kotlin::test_support {
|
||||||
|
|
||||||
|
// Clock that is manually advanced (cannot go backwards). For testing purposes.
|
||||||
|
// TODO: Should be reset at the start of each test.
|
||||||
|
class manual_clock : public internal::ClockWaitImpl<manual_clock> {
|
||||||
|
public:
|
||||||
|
using duration = nanoseconds;
|
||||||
|
using rep = duration::rep;
|
||||||
|
using period = duration::period;
|
||||||
|
using time_point = std::chrono::time_point<manual_clock>;
|
||||||
|
|
||||||
|
// Steady because it cannot go backwards.
|
||||||
|
static constexpr bool is_steady = true;
|
||||||
|
|
||||||
|
static time_point now() noexcept { return now_.load(); }
|
||||||
|
|
||||||
|
static std::optional<time_point> pending() noexcept {
|
||||||
|
std::unique_lock guard(pendingWaitsMutex_);
|
||||||
|
auto it = pendingWaits_.begin();
|
||||||
|
if (it == pendingWaits_.end()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return *it;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Duration, typename Rep, typename Period>
|
||||||
|
static void waitForPending(
|
||||||
|
std::chrono::time_point<manual_clock, Duration> expectedPending, std::chrono::duration<Rep, Period> timeout) {
|
||||||
|
// Use real clock for timeout.
|
||||||
|
auto until = std::chrono::steady_clock::now() + timeout;
|
||||||
|
while (!pending() || *pending() != expectedPending) {
|
||||||
|
if (std::chrono::steady_clock::now() > until) {
|
||||||
|
EXPECT_THAT(pending(), std::make_optional(expectedPending));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Duration>
|
||||||
|
static void waitForPending(std::chrono::time_point<manual_clock, Duration> expectedPending) {
|
||||||
|
waitForPending(expectedPending, std::chrono::minutes(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be done before the timer is used (in the beginning of the test, for example).
|
||||||
|
static void reset(time_point start = time_point::min()) noexcept {
|
||||||
|
std::unique_lock guard(pendingWaitsMutex_);
|
||||||
|
RuntimeAssert(pendingWaits_.empty(), "To reset, there must not be any pending waits");
|
||||||
|
now_ = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class internal::ClockWaitImpl<manual_clock>;
|
||||||
|
|
||||||
|
// Use non-saturating type here, because step may be fed into the standard library.
|
||||||
|
static inline constexpr auto wait_step = std::chrono::microseconds(1);
|
||||||
|
|
||||||
|
template <typename Rep, typename Period>
|
||||||
|
static void sleepImpl(std::chrono::time_point<manual_clock, std::chrono::duration<saturating<Rep>, Period>> until) {
|
||||||
|
time_point before = now();
|
||||||
|
while (before < until) {
|
||||||
|
now_.compare_exchange_weak(before, until);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PendingWaitRegistration : private Pinned {
|
||||||
|
public:
|
||||||
|
~PendingWaitRegistration() noexcept {
|
||||||
|
std::unique_lock guard(pendingWaitsMutex_);
|
||||||
|
pendingWaits_.erase(it_);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class manual_clock;
|
||||||
|
|
||||||
|
explicit PendingWaitRegistration(KStdOrderedMultiset<time_point>::iterator it) noexcept : it_(it) {}
|
||||||
|
|
||||||
|
KStdOrderedMultiset<time_point>::iterator it_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Rep, typename Period>
|
||||||
|
static PendingWaitRegistration addPendingWait(
|
||||||
|
std::chrono::time_point<manual_clock, std::chrono::duration<saturating<Rep>, Period>> until) {
|
||||||
|
std::unique_lock guard(pendingWaitsMutex_);
|
||||||
|
auto it = pendingWaits_.insert(until);
|
||||||
|
return PendingWaitRegistration(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::atomic<time_point> now_;
|
||||||
|
static std::mutex pendingWaitsMutex_;
|
||||||
|
static KStdOrderedMultiset<time_point> pendingWaits_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace kotlin::test_support
|
||||||
Reference in New Issue
Block a user