Initial implementation of HostEpoch

When host time is set, the diff between the BmcTime and the value is
saved to persistent storage;
When host time is retrieved, return the BmcTime plus the diff as
host's time.

Add the unit test cases for HostEpoch.

Change-Id: Ia55b93bfcba4f226ceaed8491136ea7afda7bd77
Signed-off-by: Lei YU <mine260309@gmail.com>
diff --git a/Makefile.am b/Makefile.am
index 3eef41a..0af260f 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -6,7 +6,8 @@
 
 libtimemanager_la_SOURCES = \
 	epoch_base.cpp \
-	bmc_epoch.cpp
+	bmc_epoch.cpp \
+	host_epoch.cpp
 
 phosphor_timemanager_SOURCES = \
 	main.cpp
diff --git a/bmc_epoch.cpp b/bmc_epoch.cpp
index 5923e8e..ed482ad 100644
--- a/bmc_epoch.cpp
+++ b/bmc_epoch.cpp
@@ -44,6 +44,6 @@
     return value;
 }
 
-}
-}
+} // namespace time
+} // namespace phosphor
 
diff --git a/bmc_epoch.hpp b/bmc_epoch.hpp
index 56d78e9..8c841a7 100644
--- a/bmc_epoch.hpp
+++ b/bmc_epoch.hpp
@@ -35,5 +35,5 @@
         uint64_t elapsed(uint64_t value) override;
 };
 
-}
-}
+} // namespace time
+} // namespace phosphor
diff --git a/configure.ac b/configure.ac
index 03f8347..7a2ee4e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -64,6 +64,10 @@
 AS_IF([test "x$OBJPATH_HOST" == "x"], [OBJPATH_HOST="/xyz/openbmc_project/time/host"])
 AC_DEFINE_UNQUOTED([OBJPATH_HOST], ["$OBJPATH_HOST"], [The host epoch Dbus root])
 
+AC_ARG_VAR(HOST_OFFSET_FILE, [The file to save host time offset])
+AS_IF([test "x$HOST_OFFSET_FILE" == "x"], [HOST_OFFSET_FILE="/var/lib/obmc/saved_host_offset"])
+AC_DEFINE_UNQUOTED([HOST_OFFSET_FILE], ["$HOST_OFFSET_FILE"], [The file to save host time offset])
+
 
 AC_CONFIG_FILES([Makefile test/Makefile])
 AC_OUTPUT
diff --git a/host_epoch.cpp b/host_epoch.cpp
new file mode 100644
index 0000000..f45581d
--- /dev/null
+++ b/host_epoch.cpp
@@ -0,0 +1,74 @@
+#include "host_epoch.hpp"
+
+#include <phosphor-logging/log.hpp>
+
+#include <fstream>
+
+namespace phosphor
+{
+namespace time
+{
+using namespace sdbusplus::xyz::openbmc_project::Time;
+using namespace phosphor::logging;
+
+HostEpoch::HostEpoch(sdbusplus::bus::bus& bus,
+                     const char* objPath)
+    : EpochBase(bus, objPath),
+      offset(readData<decltype(offset)::rep>(offsetFile))
+{
+    // Empty
+}
+
+uint64_t HostEpoch::elapsed() const
+{
+    // It does not needs to check owner when getting time
+    return (getTime() + offset).count();
+}
+
+uint64_t HostEpoch::elapsed(uint64_t value)
+{
+    if (timeOwner == Owner::BMC)
+    {
+        log<level::ERR>("Setting HostTime in BMC owner is not allowed");
+        // TODO: throw NotAllowed exception
+        return 0;
+    }
+
+    // TODO: implement the logic of setting host time
+    // based on timeOwner and timeMode
+
+    auto time = std::chrono::microseconds(value);
+    offset = time - getTime();
+
+    // Store the offset to file
+    writeData(offsetFile, offset.count());
+
+    server::EpochTime::elapsed(value);
+    return value;
+}
+
+template <typename T>
+T HostEpoch::readData(const char* fileName)
+{
+    T data{};
+    std::ifstream fs(fileName);
+    if (fs.is_open())
+    {
+        fs >> data;
+    }
+    return data;
+}
+
+template <typename T>
+void HostEpoch::writeData(const char* fileName, T&& data)
+{
+    std::ofstream fs(fileName, std::ios::out);
+    if (fs.is_open())
+    {
+        fs << std::forward<T>(data);
+    }
+}
+
+} // namespace time
+} // namespace phosphor
+
diff --git a/host_epoch.hpp b/host_epoch.hpp
new file mode 100644
index 0000000..1252ff9
--- /dev/null
+++ b/host_epoch.hpp
@@ -0,0 +1,69 @@
+#pragma once
+
+#include "config.h"
+#include "epoch_base.hpp"
+
+#include <chrono>
+
+namespace phosphor
+{
+namespace time
+{
+
+/** @class HostEpoch
+ *  @brief OpenBMC HOST EpochTime implementation.
+ *  @details A concrete implementation for xyz.openbmc_project.Time.EpochTime
+ *  DBus API for HOST's epoch time.
+ */
+class HostEpoch : public EpochBase
+{
+    public:
+        friend class TestHostEpoch;
+        HostEpoch(sdbusplus::bus::bus& bus,
+                  const char* objPath);
+
+        /**
+         * @brief Get value of Elapsed property
+         *
+         * @return The elapsed microseconds since UTC
+         **/
+        uint64_t elapsed() const override;
+
+        /**
+         * @brief Set value of Elapsed property
+         *
+         * @param[in] value - The microseconds since UTC to set
+         *
+         * @return The updated elapsed microseconds since UTC
+         **/
+        uint64_t elapsed(uint64_t value) override;
+
+    private:
+        /** @brief The diff between BMC and Host time */
+        std::chrono::microseconds offset;
+
+        /** @brief The file to store the offset in File System.
+         *  Read back when starts
+         **/
+        static constexpr auto offsetFile = HOST_OFFSET_FILE;
+
+        /** @brief Read data with type T from file
+         *
+         * @param[in] fileName - The name of file to read from
+         *
+         * @return The data with type T
+         */
+        template <typename T>
+        static T readData(const char* fileName);
+
+        /** @brief Write data with type T to file
+         *
+         * @param[in] fileName - The name of file to write to
+         * @param[in] data - The data with type T to write to file
+         */
+        template <typename T>
+        static void writeData(const char* fileName, T&& data);
+};
+
+} // namespace time
+} // namespace phosphor
diff --git a/main.cpp b/main.cpp
index 05ac453..b3f4b57 100644
--- a/main.cpp
+++ b/main.cpp
@@ -2,11 +2,13 @@
 
 #include "config.h"
 #include "bmc_epoch.hpp"
+#include "host_epoch.hpp"
 
 int main()
 {
     auto bus = sdbusplus::bus::new_default();
     phosphor::time::BmcEpoch bmc(bus, OBJPATH_BMC);
+    phosphor::time::HostEpoch host(bus,OBJPATH_HOST);
 
     bus.request_name(BUSNAME);
 
diff --git a/test/Makefile.am b/test/Makefile.am
index 26c4c15..bdd258e 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -7,7 +7,8 @@
 
 test_SOURCES = \
     TestEpochBase.cpp \
-    TestBmcEpoch.cpp
+    TestBmcEpoch.cpp \
+    TestHostEpoch.cpp
 
 test_LDADD = $(top_builddir)/libtimemanager.la
 
diff --git a/test/TestHostEpoch.cpp b/test/TestHostEpoch.cpp
new file mode 100644
index 0000000..dca8d72
--- /dev/null
+++ b/test/TestHostEpoch.cpp
@@ -0,0 +1,173 @@
+#include <sdbusplus/bus.hpp>
+#include <gtest/gtest.h>
+
+#include "host_epoch.hpp"
+#include "config.h"
+
+namespace phosphor
+{
+namespace time
+{
+
+using namespace std::chrono;
+using namespace std::chrono_literals;
+
+class TestHostEpoch : public testing::Test
+{
+    public:
+        using Mode = EpochBase::Mode;
+        using Owner = EpochBase::Owner;
+
+        sdbusplus::bus::bus bus;
+        HostEpoch hostEpoch;
+
+        static constexpr auto FILE_NOT_EXIST = "path/to/file-not-exist";
+        static constexpr auto FILE_OFFSET = "saved_host_offset";
+        static constexpr auto delta = 2s;
+
+        TestHostEpoch()
+            : bus(sdbusplus::bus::new_default()),
+              hostEpoch(bus, OBJPATH_HOST)
+        {
+            // Make sure the file does not exist
+            std::remove(FILE_NOT_EXIST);
+        }
+        ~TestHostEpoch()
+        {
+            // Cleanup test file
+            std::remove(FILE_OFFSET);
+        }
+
+        // Proxies for HostEpoch's private members and functions
+        Mode getTimeMode()
+        {
+            return hostEpoch.timeMode;
+        }
+        Owner getTimeOwner()
+        {
+            return hostEpoch.timeOwner;
+        }
+        template <typename T>
+        T readData(const char* fileName)
+        {
+            return HostEpoch::readData<T>(fileName);
+        }
+        template <typename T>
+        void writeData(const char* fileName, T&& data)
+        {
+            HostEpoch::writeData<T>(fileName, std::forward<T>(data));
+        }
+        microseconds getOffset()
+        {
+            return hostEpoch.offset;
+        }
+        void setTimeOwner(Owner owner)
+        {
+            hostEpoch.timeOwner = owner;
+        }
+};
+
+TEST_F(TestHostEpoch, empty)
+{
+    EXPECT_EQ(Mode::NTP, getTimeMode());
+    EXPECT_EQ(Owner::BMC, getTimeOwner());
+}
+
+TEST_F(TestHostEpoch, readDataFileNotExist)
+{
+    // When file does not exist, the default offset shall be 0
+    microseconds offset(0);
+    auto value = readData<decltype(offset)::rep>(FILE_NOT_EXIST);
+    EXPECT_EQ(0, value);
+}
+
+TEST_F(TestHostEpoch, writeAndReadData)
+{
+    // Write offset to file
+    microseconds offsetToWrite(1234567);
+    writeData<decltype(offsetToWrite)::rep>(FILE_OFFSET, offsetToWrite.count());
+
+    // Read it back
+    microseconds offsetToRead;
+    offsetToRead = microseconds(
+        readData<decltype(offsetToRead)::rep>(FILE_OFFSET));
+    EXPECT_EQ(offsetToWrite, offsetToRead);
+}
+
+TEST_F(TestHostEpoch, setElapsedNotAllowed)
+{
+    // By default offset shall be 0
+    EXPECT_EQ(0, getOffset().count());
+
+    // Set time in BMC mode is not allowed,
+    // so verify offset is still 0 after set time
+    microseconds diff = 1min;
+    hostEpoch.elapsed(hostEpoch.elapsed() + diff.count());
+    EXPECT_EQ(0, getOffset().count());
+}
+
+TEST_F(TestHostEpoch, setElapsedInFutureAndGet)
+{
+    // Set to HOST owner so that we can set elapsed
+    setTimeOwner(Owner::HOST);
+
+    // Get current time, and set future +1min time
+    auto t1 = hostEpoch.elapsed();
+    EXPECT_NE(0, t1);
+    microseconds diff = 1min;
+    auto t2 = t1 + diff.count();
+    hostEpoch.elapsed(t2);
+
+    // Verify that the offset shall be positive,
+    // and less or equal to diff, and shall be not too less.
+    auto offset = getOffset();
+    EXPECT_GT(offset, microseconds(0));
+    EXPECT_LE(offset, diff);
+    diff -= delta;
+    EXPECT_GE(offset, diff);
+
+    // Now get time shall be around future +1min time
+    auto epochNow = duration_cast<microseconds>(
+        system_clock::now().time_since_epoch()).count();
+    auto elapsedGot = hostEpoch.elapsed();
+    EXPECT_LT(epochNow, elapsedGot);
+    auto epochDiff = elapsedGot - epochNow;
+    diff = 1min;
+    EXPECT_GT(epochDiff, (diff - delta).count());
+    EXPECT_LT(epochDiff, (diff + delta).count());
+}
+
+TEST_F(TestHostEpoch, setElapsedInPastAndGet)
+{
+    // Set to HOST owner so that we can set elapsed
+    setTimeOwner(Owner::HOST);
+
+    // Get current time, and set past -1min time
+    auto t1 = hostEpoch.elapsed();
+    EXPECT_NE(0, t1);
+    microseconds diff = 1min;
+    auto t2 = t1 - diff.count();
+    hostEpoch.elapsed(t2);
+
+    // Verify that the offset shall be negative, and the absolute value
+    // shall be equal or greater than diff, and shall not be too greater
+    auto offset = getOffset();
+    EXPECT_LT(offset, microseconds(0));
+    offset = -offset;
+    EXPECT_GE(offset, diff);
+    diff += 10s;
+    EXPECT_LE(offset, diff);
+
+    // Now get time shall be around past -1min time
+    auto epochNow = duration_cast<microseconds>(
+        system_clock::now().time_since_epoch()).count();
+    auto elapsedGot = hostEpoch.elapsed();
+    EXPECT_LT(elapsedGot, epochNow);
+    auto epochDiff = epochNow - elapsedGot;
+    diff = 1min;
+    EXPECT_GT(epochDiff, (diff - delta).count());
+    EXPECT_LT(epochDiff, (diff + delta).count());
+}
+
+}
+}