utility/timer: Implement
We often need a continually ticking timer for our daemons. This utility
wraps an sd_event time source as a convenience. This is meant to be a
usable replacement for the timer.hpp found in other openbmc projects.
Tested:
New tests pass with full coverage. Changes to the phosphor-watchdog
that rely on this utility work as expected.
Change-Id: Id12aed9e5b018e7eca825c4a7ac7b4f46e2f04c6
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/test/utility/timer.cpp b/test/utility/timer.cpp
new file mode 100644
index 0000000..c919f7e
--- /dev/null
+++ b/test/utility/timer.cpp
@@ -0,0 +1,263 @@
+#include <chrono>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <memory>
+#include <sdeventplus/clock.hpp>
+#include <sdeventplus/event.hpp>
+#include <sdeventplus/test/sdevent.hpp>
+#include <sdeventplus/utility/timer.hpp>
+#include <stdexcept>
+#include <systemd/sd-event.h>
+
+namespace sdeventplus
+{
+namespace utility
+{
+namespace
+{
+
+constexpr ClockId testClock = ClockId::Monotonic;
+
+using std::chrono::microseconds;
+using std::chrono::milliseconds;
+using testing::DoAll;
+using testing::Return;
+using testing::SaveArg;
+using testing::SetArgPointee;
+using TestTimer = Timer<testClock>;
+
+ssize_t event_ref_times = 0;
+
+ACTION(EventRef)
+{
+ event_ref_times++;
+}
+
+ACTION(EventUnref)
+{
+ ASSERT_LT(0, event_ref_times);
+ event_ref_times--;
+}
+
+class TimerTest : public testing::Test
+{
+ protected:
+ testing::StrictMock<test::SdEventMock> mock;
+ sd_event* const expected_event = reinterpret_cast<sd_event*>(1234);
+ sd_event_source* const expected_source =
+ reinterpret_cast<sd_event_source*>(2345);
+ const milliseconds interval{134};
+ const milliseconds starting_time{10};
+ sd_event_time_handler_t handler = nullptr;
+ void* handler_userdata;
+ std::unique_ptr<TestTimer> timer;
+ std::function<void()> callback;
+
+ void expectNow(microseconds ret)
+ {
+ EXPECT_CALL(mock,
+ sd_event_now(expected_event,
+ static_cast<clockid_t>(testClock), testing::_))
+ .WillOnce(DoAll(SetArgPointee<2>(ret.count()), Return(0)));
+ }
+
+ void expectSetTime(microseconds time)
+ {
+ EXPECT_CALL(mock,
+ sd_event_source_set_time(expected_source, time.count()))
+ .WillOnce(Return(0));
+ }
+
+ void expectSetEnabled(source::Enabled enabled)
+ {
+ EXPECT_CALL(mock, sd_event_source_set_enabled(
+ expected_source, static_cast<int>(enabled)))
+ .WillOnce(Return(0));
+ }
+
+ void expectGetEnabled(source::Enabled enabled)
+ {
+ EXPECT_CALL(mock,
+ sd_event_source_get_enabled(expected_source, testing::_))
+ .WillOnce(
+ DoAll(SetArgPointee<1>(static_cast<int>(enabled)), Return(0)));
+ }
+
+ void SetUp()
+ {
+ EXPECT_CALL(mock, sd_event_ref(expected_event))
+ .WillRepeatedly(DoAll(EventRef(), Return(expected_event)));
+ EXPECT_CALL(mock, sd_event_unref(expected_event))
+ .WillRepeatedly(DoAll(EventUnref(), Return(nullptr)));
+ Event event(expected_event, &mock);
+
+ auto runCallback = [&]() {
+ if (callback)
+ {
+ callback();
+ }
+ };
+ expectNow(starting_time);
+ EXPECT_CALL(mock, sd_event_add_time(
+ expected_event, testing::_,
+ static_cast<clockid_t>(testClock),
+ microseconds(starting_time + interval).count(),
+ 1000, testing::_, nullptr))
+ .WillOnce(DoAll(SetArgPointee<1>(expected_source),
+ SaveArg<5>(&handler), Return(0)));
+ EXPECT_CALL(mock,
+ sd_event_source_set_userdata(expected_source, testing::_))
+ .WillOnce(DoAll(SaveArg<1>(&handler_userdata), Return(nullptr)));
+ // Timer always enables the source to keep ticking
+ expectSetEnabled(source::Enabled::On);
+ timer = std::make_unique<TestTimer>(event, runCallback, interval);
+ }
+
+ void TearDown()
+ {
+ expectSetEnabled(source::Enabled::Off);
+ EXPECT_CALL(mock, sd_event_source_unref(expected_source))
+ .WillOnce(Return(nullptr));
+ timer.reset();
+ EXPECT_EQ(0, event_ref_times);
+ }
+};
+
+TEST_F(TimerTest, NewTimer)
+{
+ EXPECT_FALSE(timer->hasExpired());
+ EXPECT_EQ(interval, timer->getInterval());
+}
+
+TEST_F(TimerTest, IsEnabled)
+{
+ expectGetEnabled(source::Enabled::On);
+ EXPECT_TRUE(timer->isEnabled());
+ expectGetEnabled(source::Enabled::Off);
+ EXPECT_FALSE(timer->isEnabled());
+}
+
+TEST_F(TimerTest, GetRemainingDisabled)
+{
+ expectGetEnabled(source::Enabled::Off);
+ EXPECT_THROW(timer->getRemaining(), std::runtime_error);
+}
+
+TEST_F(TimerTest, GetRemainingNegative)
+{
+ milliseconds now(675), end(453);
+ expectGetEnabled(source::Enabled::On);
+ EXPECT_CALL(mock, sd_event_source_get_time(expected_source, testing::_))
+ .WillOnce(
+ DoAll(SetArgPointee<1>(microseconds(end).count()), Return(0)));
+ expectNow(now);
+ EXPECT_EQ(milliseconds(0), timer->getRemaining());
+}
+
+TEST_F(TimerTest, GetRemainingPositive)
+{
+ milliseconds now(453), end(675);
+ expectGetEnabled(source::Enabled::On);
+ EXPECT_CALL(mock, sd_event_source_get_time(expected_source, testing::_))
+ .WillOnce(
+ DoAll(SetArgPointee<1>(microseconds(end).count()), Return(0)));
+ expectNow(now);
+ EXPECT_EQ(end - now, timer->getRemaining());
+}
+
+TEST_F(TimerTest, SetEnabled)
+{
+ expectSetEnabled(source::Enabled::On);
+ timer->setEnabled(true);
+ EXPECT_FALSE(timer->hasExpired());
+ // Value should always be passed through regardless of current state
+ expectSetEnabled(source::Enabled::On);
+ timer->setEnabled(true);
+ EXPECT_FALSE(timer->hasExpired());
+
+ expectSetEnabled(source::Enabled::Off);
+ timer->setEnabled(false);
+ EXPECT_FALSE(timer->hasExpired());
+ // Value should always be passed through regardless of current state
+ expectSetEnabled(source::Enabled::Off);
+ timer->setEnabled(false);
+ EXPECT_FALSE(timer->hasExpired());
+}
+
+TEST_F(TimerTest, SetRemaining)
+{
+ const milliseconds now(90), remaining(30);
+ expectNow(now);
+ expectSetTime(now + remaining);
+ timer->setRemaining(remaining);
+ EXPECT_EQ(interval, timer->getInterval());
+ EXPECT_FALSE(timer->hasExpired());
+}
+
+TEST_F(TimerTest, ResetRemaining)
+{
+ const milliseconds now(90);
+ expectNow(now);
+ expectSetTime(now + interval);
+ timer->resetRemaining();
+ EXPECT_EQ(interval, timer->getInterval());
+ EXPECT_FALSE(timer->hasExpired());
+}
+
+TEST_F(TimerTest, SetInterval)
+{
+ const milliseconds new_interval(40);
+ timer->setInterval(new_interval);
+ EXPECT_EQ(new_interval, timer->getInterval());
+ EXPECT_FALSE(timer->hasExpired());
+}
+
+TEST_F(TimerTest, SetValuesExpiredTimer)
+{
+ const milliseconds new_time(90);
+ expectNow(new_time);
+ expectSetTime(new_time + interval);
+ EXPECT_EQ(0, handler(nullptr, 0, handler_userdata));
+ EXPECT_TRUE(timer->hasExpired());
+ EXPECT_EQ(interval, timer->getInterval());
+
+ // Timer should remain expired unless clearExpired() or reset()
+ expectSetEnabled(source::Enabled::On);
+ timer->setEnabled(true);
+ EXPECT_TRUE(timer->hasExpired());
+ expectNow(milliseconds(20));
+ expectSetTime(milliseconds(50));
+ timer->setRemaining(milliseconds(30));
+ EXPECT_TRUE(timer->hasExpired());
+ timer->setInterval(milliseconds(10));
+ EXPECT_TRUE(timer->hasExpired());
+ expectNow(milliseconds(20));
+ expectSetTime(milliseconds(30));
+ timer->resetRemaining();
+ EXPECT_TRUE(timer->hasExpired());
+
+ timer->clearExpired();
+ EXPECT_FALSE(timer->hasExpired());
+}
+
+TEST_F(TimerTest, Restart)
+{
+ const milliseconds new_time(90);
+ expectNow(new_time);
+ expectSetTime(new_time + interval);
+ EXPECT_EQ(0, handler(nullptr, 0, handler_userdata));
+ EXPECT_TRUE(timer->hasExpired());
+ EXPECT_EQ(interval, timer->getInterval());
+
+ const milliseconds new_interval(471);
+ expectNow(starting_time);
+ expectSetTime(starting_time + new_interval);
+ expectSetEnabled(source::Enabled::On);
+ timer->restart(new_interval);
+ EXPECT_FALSE(timer->hasExpired());
+ EXPECT_EQ(new_interval, timer->getInterval());
+}
+
+} // namespace
+} // namespace utility
+} // namespace sdeventplus