test: pid: zone

Add unit-tests for the PID zone module.
Add zone_mock.

Tested: Ran on quanta-q71l board and it behaved as expected.

Change-Id: I51185b2d2daacea6ffb687e8f38c4fe2b2a1bed3
Signed-off-by: Patrick Venture <venture@google.com>
diff --git a/test/pid_zone_unittest.cpp b/test/pid_zone_unittest.cpp
new file mode 100644
index 0000000..c511b3c
--- /dev/null
+++ b/test/pid_zone_unittest.cpp
@@ -0,0 +1,422 @@
+#include "pid/zone.hpp"
+
+#include <chrono>
+#include <cstring>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <sdbusplus/test/sdbus_mock.hpp>
+#include <vector>
+
+#include "pid/ec/pid.hpp"
+#include "sensors/manager.hpp"
+#include "test/controller_mock.hpp"
+#include "test/sensor_mock.hpp"
+#include "test/helpers.hpp"
+
+using ::testing::IsNull;
+using ::testing::Return;
+using ::testing::StrEq;
+using ::testing::_;
+
+static std::string modeInterface = "xyz.openbmc_project.Control.Mode";
+
+namespace {
+
+TEST(PidZoneConstructorTest, BoringConstructorTest) {
+    // Build a PID Zone.
+
+    sdbusplus::SdBusMock sdbus_mock_passive, sdbus_mock_host, sdbus_mock_mode;
+    auto bus_mock_passive = sdbusplus::get_mocked_new(&sdbus_mock_passive);
+    auto bus_mock_host = sdbusplus::get_mocked_new(&sdbus_mock_host);
+    auto bus_mock_mode = sdbusplus::get_mocked_new(&sdbus_mock_mode);
+
+    EXPECT_CALL(sdbus_mock_host,
+                sd_bus_add_object_manager(
+                    IsNull(),
+                    _,
+                    StrEq("/xyz/openbmc_project/extsensors")))
+        .WillOnce(Return(0));
+
+    SensorManager m(std::move(bus_mock_passive),
+                     std::move(bus_mock_host));
+
+    bool defer = true;
+    const char *objPath = "/path/";
+    int64_t zone = 1;
+    float minThermalRpm = 1000.0;
+    float failSafePercent = 0.75;
+
+    int i;
+    std::vector<std::string> properties;
+    SetupDbusObject(&sdbus_mock_mode, defer, objPath, modeInterface,
+                    properties, &i);
+
+    PIDZone p(zone, minThermalRpm, failSafePercent, m, bus_mock_mode, objPath,
+              defer);
+    // Success.
+}
+
+}
+
+class PidZoneTest : public ::testing::Test {
+    protected:
+        PidZoneTest()
+        : property_index(),
+          properties(),
+          sdbus_mock_passive(),
+          sdbus_mock_host(),
+          sdbus_mock_mode()
+        {
+            EXPECT_CALL(sdbus_mock_host,
+                sd_bus_add_object_manager(
+                    IsNull(),
+                    _,
+                    StrEq("/xyz/openbmc_project/extsensors")))
+                .WillOnce(Return(0));
+
+            auto bus_mock_passive =
+                sdbusplus::get_mocked_new(&sdbus_mock_passive);
+            auto bus_mock_host = sdbusplus::get_mocked_new(&sdbus_mock_host);
+            auto bus_mock_mode = sdbusplus::get_mocked_new(&sdbus_mock_mode);
+
+            // Compiler weirdly not happy about just instantiating mgr(...);
+            SensorManager m(std::move(bus_mock_passive),
+                            std::move(bus_mock_host));
+            mgr = std::move(m);
+
+            SetupDbusObject(&sdbus_mock_mode, defer, objPath, modeInterface,
+                            properties, &property_index);
+
+            zone = std::make_unique<PIDZone>(zoneId, minThermalRpm,
+                                             failSafePercent, mgr,
+                                             bus_mock_mode, objPath, defer);
+        }
+
+        // unused
+        int property_index;
+        std::vector<std::string> properties;
+
+        sdbusplus::SdBusMock sdbus_mock_passive;
+        sdbusplus::SdBusMock sdbus_mock_host;
+        sdbusplus::SdBusMock sdbus_mock_mode;
+        int64_t zoneId = 1;
+        float minThermalRpm = 1000.0;
+        float failSafePercent = 0.75;
+        bool defer = true;
+        const char *objPath = "/path/";
+        SensorManager mgr;
+
+        std::unique_ptr<PIDZone> zone;
+};
+
+TEST_F(PidZoneTest, GetZoneId_ReturnsExpected) {
+    // Verifies the zoneId returned is what we expect.
+
+    EXPECT_EQ(zoneId, zone->getZoneId());
+}
+
+TEST_F(PidZoneTest, GetAndSetManualModeTest_BehavesAsExpected) {
+    // Verifies that the zone starts in manual mode.  Verifies that one can set
+    // the mode.
+    EXPECT_FALSE(zone->getManualMode());
+
+    zone->setManualMode(true);
+    EXPECT_TRUE(zone->getManualMode());
+}
+
+TEST_F(PidZoneTest, RpmSetPoints_AddMaxClear_BehaveAsExpected) {
+    // Tests addRPMSetPoint, clearRPMSetPoints, determineMaxRPMRequest
+    // and getMinThermalRpmSetPt.
+
+    // At least one value must be above the minimum thermal setpoint used in
+    // the constructor otherwise it'll choose that value
+    std::vector<float> values = {100, 200, 300, 400, 500, 5000};
+    for (auto v : values)
+    {
+        zone->addRPMSetPoint(v);
+    }
+
+    // This will pull the maximum RPM setpoint request.
+    zone->determineMaxRPMRequest();
+    EXPECT_EQ(5000, zone->getMaxRPMRequest());
+
+    // Clear the values, so it'll choose the minimum thermal setpoint.
+    zone->clearRPMSetPoints();
+
+    // This will go through the RPM set point values and grab the maximum.
+    zone->determineMaxRPMRequest();
+    EXPECT_EQ(zone->getMinThermalRpmSetPt(), zone->getMaxRPMRequest());
+}
+
+TEST_F(PidZoneTest, RpmSetPoints_AddBelowMinimum_BehavesAsExpected) {
+    // Tests adding several RPM setpoints, however, they're all lower than the
+    // configured minimal thermal set-point RPM value.
+
+    std::vector<float> values = {100, 200, 300, 400, 500};
+    for (auto v : values)
+    {
+        zone->addRPMSetPoint(v);
+    }
+
+    // This will pull the maximum RPM setpoint request.
+    zone->determineMaxRPMRequest();
+
+    // Verifies the value returned in the minimal thermal rpm set point.
+    EXPECT_EQ(zone->getMinThermalRpmSetPt(), zone->getMaxRPMRequest());
+}
+
+TEST_F(PidZoneTest, GetFailSafePercent_ReturnsExpected) {
+    // Verify the value used to create the object is stored.
+    EXPECT_EQ(failSafePercent, zone->getFailSafePercent());
+}
+
+TEST_F(PidZoneTest, ThermalInputs_FailsafeToValid_ReadsSensors) {
+    // This test will add a couple thermal inputs, and verify that the zone
+    // initializes into failsafe mode, and will read each sensor.
+
+    std::string name1 = "temp1";
+    int64_t timeout = 1;
+
+    std::unique_ptr<Sensor> sensor1 =
+        std::make_unique<SensorMock>(name1, timeout);
+    SensorMock *sensor_ptr1 = reinterpret_cast<SensorMock*>(sensor1.get());
+
+    std::string name2 = "temp2";
+    std::unique_ptr<Sensor> sensor2 =
+        std::make_unique<SensorMock>(name2, timeout);
+    SensorMock *sensor_ptr2 = reinterpret_cast<SensorMock*>(sensor2.get());
+
+    std::string type = "unchecked";
+    mgr.addSensor(type, name1, std::move(sensor1));
+    EXPECT_EQ(mgr.getSensor(name1), sensor_ptr1);
+    mgr.addSensor(type, name2, std::move(sensor2));
+    EXPECT_EQ(mgr.getSensor(name2), sensor_ptr2);
+
+    // Now that the sensors exist, add them to the zone.
+    zone->addThermalInput(name1);
+    zone->addThermalInput(name2);
+
+    // Initialize Zone
+    zone->initializeCache();
+
+    // Verify now in failsafe mode.
+    EXPECT_TRUE(zone->getFailSafeMode());
+
+    ReadReturn r1;
+    r1.value = 10.0;
+    r1.updated = std::chrono::high_resolution_clock::now();
+    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));
+
+    ReadReturn r2;
+    r2.value = 11.0;
+    r2.updated = std::chrono::high_resolution_clock::now();
+    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));
+
+    // Read the sensors, this will put the values into the cache.
+    zone->updateSensors();
+
+    // We should no longer be in failsafe mode.
+    EXPECT_FALSE(zone->getFailSafeMode());
+
+    EXPECT_EQ(r1.value, zone->getCachedValue(name1));
+    EXPECT_EQ(r2.value, zone->getCachedValue(name2));
+}
+
+TEST_F(PidZoneTest, FanInputTest_VerifiesFanValuesCached) {
+    // This will add a couple fan inputs, and verify the values are cached.
+
+    std::string name1 = "fan1";
+    int64_t timeout = 2;
+
+    std::unique_ptr<Sensor> sensor1 =
+        std::make_unique<SensorMock>(name1, timeout);
+    SensorMock *sensor_ptr1 = reinterpret_cast<SensorMock*>(sensor1.get());
+
+    std::string name2 = "fan2";
+    std::unique_ptr<Sensor> sensor2 =
+        std::make_unique<SensorMock>(name2, timeout);
+    SensorMock *sensor_ptr2 = reinterpret_cast<SensorMock*>(sensor2.get());
+
+    std::string type = "unchecked";
+    mgr.addSensor(type, name1, std::move(sensor1));
+    EXPECT_EQ(mgr.getSensor(name1), sensor_ptr1);
+    mgr.addSensor(type, name2, std::move(sensor2));
+    EXPECT_EQ(mgr.getSensor(name2), sensor_ptr2);
+
+    // Now that the sensors exist, add them to the zone.
+    zone->addFanInput(name1);
+    zone->addFanInput(name2);
+
+    // Initialize Zone
+    zone->initializeCache();
+
+    ReadReturn r1;
+    r1.value = 10.0;
+    r1.updated = std::chrono::high_resolution_clock::now();
+    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));
+
+    ReadReturn r2;
+    r2.value = 11.0;
+    r2.updated = std::chrono::high_resolution_clock::now();
+    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));
+
+    // Method under test will read through each fan sensor for the zone and
+    // cache the values.
+    zone->updateFanTelemetry();
+
+    EXPECT_EQ(r1.value, zone->getCachedValue(name1));
+    EXPECT_EQ(r2.value, zone->getCachedValue(name2));
+}
+
+TEST_F(PidZoneTest, ThermalInput_ValueTimeoutEntersFailSafeMode) {
+    // On the second updateSensors call, the updated timestamp will be beyond
+    // the timeout limit.
+
+    int64_t timeout = 1;
+
+    std::string name1 = "temp1";
+    std::unique_ptr<Sensor> sensor1 =
+        std::make_unique<SensorMock>(name1, timeout);
+    SensorMock *sensor_ptr1 = reinterpret_cast<SensorMock*>(sensor1.get());
+
+    std::string name2 = "temp2";
+    std::unique_ptr<Sensor> sensor2 =
+        std::make_unique<SensorMock>(name2, timeout);
+    SensorMock *sensor_ptr2 = reinterpret_cast<SensorMock*>(sensor2.get());
+
+    std::string type = "unchecked";
+    mgr.addSensor(type, name1, std::move(sensor1));
+    EXPECT_EQ(mgr.getSensor(name1), sensor_ptr1);
+    mgr.addSensor(type, name2, std::move(sensor2));
+    EXPECT_EQ(mgr.getSensor(name2), sensor_ptr2);
+
+    zone->addThermalInput(name1);
+    zone->addThermalInput(name2);
+
+    // Initialize Zone
+    zone->initializeCache();
+
+    // Verify now in failsafe mode.
+    EXPECT_TRUE(zone->getFailSafeMode());
+
+    ReadReturn r1;
+    r1.value = 10.0;
+    r1.updated = std::chrono::high_resolution_clock::now();
+    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));
+
+    ReadReturn r2;
+    r2.value = 11.0;
+    r2.updated = std::chrono::high_resolution_clock::now();
+    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));
+
+    zone->updateSensors();
+    EXPECT_FALSE(zone->getFailSafeMode());
+
+    // Ok, so we're not in failsafe mode, so let's set updated to the past.
+    // sensor1 will have an updated field older than its timeout value, but
+    // sensor2 will be fine. :D
+    r1.updated -= std::chrono::seconds(3);
+    r2.updated = std::chrono::high_resolution_clock::now();
+
+    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));
+    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));
+
+    // Method under test will read each sensor.  One sensor's value is older
+    // than the timeout for that sensor and this triggers failsafe mode.
+    zone->updateSensors();
+    EXPECT_TRUE(zone->getFailSafeMode());
+}
+
+TEST_F(PidZoneTest, GetSensorTest_ReturnsExpected) {
+    // One can grab a sensor from the manager through the zone.
+
+    int64_t timeout = 1;
+
+    std::string name1 = "temp1";
+    std::unique_ptr<Sensor> sensor1 =
+        std::make_unique<SensorMock>(name1, timeout);
+    SensorMock *sensor_ptr1 = reinterpret_cast<SensorMock*>(sensor1.get());
+
+    std::string type = "unchecked";
+    mgr.addSensor(type, name1, std::move(sensor1));
+    EXPECT_EQ(mgr.getSensor(name1), sensor_ptr1);
+
+    zone->addThermalInput(name1);
+
+    // Verify method under test returns the pointer we expect.
+    EXPECT_EQ(mgr.getSensor(name1), zone->getSensor(name1));
+}
+
+TEST_F(PidZoneTest, AddThermalPIDTest_VerifiesThermalPIDsProcessed) {
+    // Tests adding a thermal PID controller to the zone, and verifies it's
+    // touched during processing.
+
+    std::unique_ptr<PIDController> tpid =
+        std::make_unique<ControllerMock>("thermal1", zone.get());
+    ControllerMock *tmock = reinterpret_cast<ControllerMock*>(tpid.get());
+
+    // Access the internal pid configuration to clear it out (unrelated to the
+    // test).
+    ec::pid_info_t* info = tpid->get_pid_info();
+    std::memset(info, 0x00, sizeof(ec::pid_info_t));
+
+    zone->addThermalPID(std::move(tpid));
+
+    EXPECT_CALL(*tmock, setpt_proc()).WillOnce(Return(10.0));
+    EXPECT_CALL(*tmock, input_proc()).WillOnce(Return(11.0));
+    EXPECT_CALL(*tmock, output_proc(_));
+
+    // Method under test will, for each thermal PID, call setpt, input, and
+    // output.
+    zone->process_thermals();
+}
+
+TEST_F(PidZoneTest, AddFanPIDTest_VerifiesFanPIDsProcessed) {
+    // Tests adding a fan PID controller to the zone, and verifies it's
+    // touched during processing.
+
+    std::unique_ptr<PIDController> tpid =
+        std::make_unique<ControllerMock>("fan1", zone.get());
+    ControllerMock *tmock = reinterpret_cast<ControllerMock*>(tpid.get());
+
+    // Access the internal pid configuration to clear it out (unrelated to the
+    // test).
+    ec::pid_info_t* info = tpid->get_pid_info();
+    std::memset(info, 0x00, sizeof(ec::pid_info_t));
+
+    zone->addFanPID(std::move(tpid));
+
+    EXPECT_CALL(*tmock, setpt_proc()).WillOnce(Return(10.0));
+    EXPECT_CALL(*tmock, input_proc()).WillOnce(Return(11.0));
+    EXPECT_CALL(*tmock, output_proc(_));
+
+    // Method under test will, for each fan PID, call setpt, input, and output.
+    zone->process_fans();
+}
+
+TEST_F(PidZoneTest, ManualModeDbusTest_VerifySetManualBehavesAsExpected) {
+    // The manual(bool) method is inherited from the dbus mode interface.
+
+    // Verifies that someone doesn't remove the internal call to the dbus
+    // object from which we're inheriting.
+    EXPECT_CALL(sdbus_mock_mode,
+                sd_bus_emit_properties_changed_strv(IsNull(), StrEq(objPath),
+                                                    StrEq(modeInterface),
+                                                    NotNull()))
+        .WillOnce(Invoke([&](sd_bus *bus, const char *path,
+                             const char *interface, char **names) {
+            EXPECT_STREQ("Manual", names[0]);
+            return 0;
+        }));
+
+    // Method under test will set the manual mode to true and broadcast this
+    // change on dbus.
+    zone->manual(true);
+    EXPECT_TRUE(zone->getManualMode());
+}
+
+TEST_F(PidZoneTest, FailsafeDbusTest_VerifiesReturnsExpected) {
+    // This property is implemented by us as read-only, such that trying to
+    // write to it will have no effect.
+    EXPECT_EQ(zone->failSafe(), zone->getFailSafeMode());
+}