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/.gitignore b/.gitignore
index 4d40130..b91a4df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,3 +54,4 @@
 /test/source/io
 /test/source/signal
 /test/source/time
+/test/utility/timer
diff --git a/src/Makefile.am b/src/Makefile.am
index 518ac1f..9cd7706 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -40,3 +40,6 @@
 libsdeventplus_la_SOURCES += sdeventplus/source/time.cpp
 
 nobase_include_HEADERS += sdeventplus/test/sdevent.hpp
+
+nobase_include_HEADERS += sdeventplus/utility/timer.hpp
+libsdeventplus_la_SOURCES += sdeventplus/utility/timer.cpp
diff --git a/src/sdeventplus/utility/timer.cpp b/src/sdeventplus/utility/timer.cpp
new file mode 100644
index 0000000..6ad684c
--- /dev/null
+++ b/src/sdeventplus/utility/timer.cpp
@@ -0,0 +1,117 @@
+#include <functional>
+#include <sdeventplus/clock.hpp>
+#include <sdeventplus/utility/timer.hpp>
+#include <stdexcept>
+
+namespace sdeventplus
+{
+namespace utility
+{
+
+template <ClockId Id>
+Timer<Id>::Timer(const Event& event, Callback&& callback, Duration interval,
+                 typename source::Time<Id>::Accuracy accuracy) :
+    expired(false),
+    callback(callback), clock(event), interval(interval),
+    timeSource(event, clock.now() + interval, accuracy,
+               std::bind(&Timer::internalCallback, this, std::placeholders::_1,
+                         std::placeholders::_2))
+{
+    timeSource.set_enabled(source::Enabled::On);
+}
+
+template <ClockId Id>
+bool Timer<Id>::hasExpired() const
+{
+    return expired;
+}
+
+template <ClockId Id>
+bool Timer<Id>::isEnabled() const
+{
+    return timeSource.get_enabled() != source::Enabled::Off;
+}
+
+template <ClockId Id>
+typename Timer<Id>::Duration Timer<Id>::getInterval() const
+{
+    return interval;
+}
+
+template <ClockId Id>
+typename Timer<Id>::Duration Timer<Id>::getRemaining() const
+{
+    if (!isEnabled())
+    {
+        throw std::runtime_error("Timer not running");
+    }
+
+    auto end = timeSource.get_time();
+    auto now = clock.now();
+    if (end < now)
+    {
+        return Duration{0};
+    }
+    return end - now;
+}
+
+template <ClockId Id>
+void Timer<Id>::setEnabled(bool enabled)
+{
+    timeSource.set_enabled(enabled ? source::Enabled::On
+                                   : source::Enabled::Off);
+}
+
+template <ClockId Id>
+void Timer<Id>::setRemaining(Duration remaining)
+{
+    timeSource.set_time(clock.now() + remaining);
+}
+
+template <ClockId Id>
+void Timer<Id>::resetRemaining()
+{
+    setRemaining(interval);
+}
+
+template <ClockId Id>
+void Timer<Id>::setInterval(Duration interval)
+{
+    this->interval = interval;
+}
+
+template <ClockId Id>
+void Timer<Id>::clearExpired()
+{
+    expired = false;
+}
+
+template <ClockId Id>
+void Timer<Id>::restart(Duration interval)
+{
+    clearExpired();
+    setInterval(interval);
+    resetRemaining();
+    setEnabled(true);
+}
+
+template <ClockId Id>
+void Timer<Id>::internalCallback(source::Time<Id>&,
+                                 typename source::Time<Id>::TimePoint)
+{
+    expired = true;
+    if (callback)
+    {
+        callback();
+    }
+    resetRemaining();
+}
+
+template class Timer<ClockId::RealTime>;
+template class Timer<ClockId::Monotonic>;
+template class Timer<ClockId::BootTime>;
+template class Timer<ClockId::RealTimeAlarm>;
+template class Timer<ClockId::BootTimeAlarm>;
+
+} // namespace utility
+} // namespace sdeventplus
diff --git a/src/sdeventplus/utility/timer.hpp b/src/sdeventplus/utility/timer.hpp
new file mode 100644
index 0000000..a9d4fbc
--- /dev/null
+++ b/src/sdeventplus/utility/timer.hpp
@@ -0,0 +1,147 @@
+#pragma once
+
+#include <chrono>
+#include <functional>
+#include <sdeventplus/clock.hpp>
+#include <sdeventplus/event.hpp>
+#include <sdeventplus/source/time.hpp>
+
+namespace sdeventplus
+{
+namespace utility
+{
+
+/** @class Timer<Id>
+ *  @brief A simple, repeating timer around an sd_event time source
+ *  @details Adds a timer to the SdEvent loop that runs a user defined callback
+ *           at specified intervals. Besides running callbacks, the timer tracks
+ *           whether or not it has expired since creation or since the last
+ *           clearExpired() or restart(). The concept of expiration is
+ *           orthogonal to the callback mechanism and can be ignored.
+ */
+template <ClockId Id>
+class Timer
+{
+  public:
+    /** @brief Type used to represent a time duration for the timer
+     *         interval or time remaining.
+     */
+    using Duration = typename Clock<Id>::duration;
+
+    /** @brief Type of the user provided callback function when the
+     *         timer elapses.
+     */
+    using Callback = std::function<void()>;
+
+    Timer(const Timer& other) = delete;
+    Timer(Timer&& other) = default;
+    Timer& operator=(const Timer& other) = delete;
+    Timer& operator=(Timer&& other) = default;
+    virtual ~Timer() = default;
+
+    /** @brief Creates a new timer on the given event loop.
+     *         This timer is created enabled by default.
+     *
+     *  @param[in] event    - The event we are attaching to
+     *  @param[in] callback - The user provided callback run when elapsing
+     *                        This can be empty
+     *  @param[in] interval - The amount of time in between timer expirations
+     *  @param[in] accuracy - Optional amount of error tolerable in timer
+     *                        expiration. Defaults to 1ms.
+     *  @throws SdEventError for underlying sd_event errors
+     */
+    Timer(const Event& event, Callback&& callback, Duration interval,
+          typename source::Time<Id>::Accuracy accuracy =
+              std::chrono::milliseconds{1});
+
+    /** @brief Has the timer expired since creation or reset of expiration
+     *         state.
+     *
+     *  @return True if expired, false otherwise
+     */
+    bool hasExpired() const;
+
+    /** @brief Is the timer currently running on the event loop.
+     *
+     *  @throws SdEventError for underlying sd_event errors
+     *  @return True if running, false otherwise
+     */
+    bool isEnabled() const;
+
+    /** @brief Gets interval between timer expirations
+     *
+     *  @return The interval as an std::chrono::duration
+     */
+    Duration getInterval() const;
+
+    /** @brief Gets time left before the timer expirations
+     *
+     *  @throws std::runtime_error if the timer is not enabled
+     *  @throws SdEventError for underlying sd_event errors
+     *  @return The remaining time as an std::chrono::duration
+     */
+    Duration getRemaining() const;
+
+    /** @brief Sets whether or not the timer is running on the event loop
+     *         This does not alter the expiration time of the timer.
+     *
+     *  @param[in] enabled - Should the timer be enabled or disabled
+     *  @throws SdEventError for underlying sd_event errors
+     */
+    void setEnabled(bool enabled);
+
+    /** @brief Sets the amount of time left until the timer expires.
+     *         This does not affect the interval used for subsequent runs.
+     *
+     *  @param[in] remaining - The new amount of time left on the timer
+     *  @throws SdEventError for underlying sd_event errors
+     */
+    void setRemaining(Duration remaining);
+
+    /** @brief Resets the amount of time left to the interval of the timer.
+     *
+     *  @throws SdEventError for underlying sd_event errors
+     */
+    void resetRemaining();
+
+    /** @brief Sets the interval of the timer for future timer expirations.
+     *         This does not alter the current expiration time of the timer.
+     *
+     *  @param[in] interval - The new interval for the timer
+     */
+    void setInterval(Duration interval);
+
+    /** @brief Resets the expired status of the timer. */
+    void clearExpired();
+
+    /** @brief Restarts the timer as though it has been completely
+     *         re-initialized. Expired status is reset, interval is updated,
+     *         time remaining is set to the new interval, and the timer is
+     *         enabled.
+     *
+     *  @param[in] interval - The new interval for the timer
+     *  @throws SdEventError for underlying sd_event errors
+     */
+    void restart(Duration interval);
+
+  private:
+    /** @brief Tracks the expiration status of the timer */
+    bool expired;
+    /** @brief User defined callback run on each expiration */
+    Callback callback;
+    /** @brief Clock used for updating the time source */
+    Clock<Id> clock;
+    /** @brief Interval between each timer expiration */
+    Duration interval;
+    /** @brief Underlying sd_event time source that backs the timer */
+    source::Time<Id> timeSource;
+
+    /** @brief Used as a helper to run our user defined callback on the
+     *         timeSource
+     */
+    void internalCallback(source::Time<Id>&,
+                          typename source::Time<Id>::TimePoint);
+};
+
+} // namespace utility
+} // namespace sdeventplus
diff --git a/test/Makefile.am b/test/Makefile.am
index 2223dfc..6cddc88 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -61,3 +61,8 @@
 source_time_SOURCES = source/time.cpp
 source_time_CPPFLAGS = $(gtest_cppflags)
 source_time_LDADD = $(gtest_ldadd)
+
+check_PROGRAMS += utility/timer
+utility_timer_SOURCES = utility/timer.cpp
+utility_timer_CPPFLAGS = $(gtest_cppflags)
+utility_timer_LDADD = $(gtest_ldadd)
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