monitor: Create PowerOffRules class

This class contains a PowerOffCause and a PowerOffAction.  It provides a
check() method that takes the FanHealth map which it then checks against
the cause.  If the cause is satisfied, it then starts the power off
action.  It provides a cancel method that will force cancel a running
action in the case that the object owner detects a system power off and
so doesn't need to run this power off anymore.

The class's configuration data is read from the JSON config file.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I5c0c168591d6d62c894c4d036ec762797fd759af
diff --git a/monitor/test/Makefile.am b/monitor/test/Makefile.am
index 32b8c3b..e6d0f9b 100644
--- a/monitor/test/Makefile.am
+++ b/monitor/test/Makefile.am
@@ -7,7 +7,8 @@
 TESTS = $(check_PROGRAMS)
 
 check_PROGRAMS += \
-	power_off_cause_test
+	power_off_cause_test \
+	power_off_rule_test
 
 power_off_cause_test_SOURCES = \
 	power_off_cause_test.cpp
@@ -17,3 +18,18 @@
 	$(OESDK_TESTCASE_FLAGS)
 power_off_cause_test_LDADD = \
 	$(gtest_ldadd)
+
+power_off_rule_test_SOURCES = \
+	power_off_rule_test.cpp \
+	../conditions.cpp \
+	../json_parser.cpp \
+	../logging.cpp
+power_off_rule_test_CXXFLAGS = \
+	$(gtest_cflags)
+power_off_rule_test_LDFLAGS = \
+	$(OESDK_TESTCASE_FLAGS)
+power_off_rule_test_LDADD = \
+	$(gtest_ldadd) \
+	$(FMT_LIBS) \
+	$(SDBUSPLUS_LIBS) \
+	$(SDEVENTPLUS_LIBS)
diff --git a/monitor/test/mock_power_interface.hpp b/monitor/test/mock_power_interface.hpp
new file mode 100644
index 0000000..c070182
--- /dev/null
+++ b/monitor/test/mock_power_interface.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "../power_interface.hpp"
+
+#include <gmock/gmock.h>
+
+namespace phosphor::fan::monitor
+{
+
+class MockPowerInterface : public PowerInterfaceBase
+{
+  public:
+    MOCK_METHOD(void, softPowerOff, (), (override));
+    MOCK_METHOD(void, hardPowerOff, (), (override));
+};
+
+} // namespace phosphor::fan::monitor
diff --git a/monitor/test/power_off_rule_test.cpp b/monitor/test/power_off_rule_test.cpp
new file mode 100644
index 0000000..b708bb9
--- /dev/null
+++ b/monitor/test/power_off_rule_test.cpp
@@ -0,0 +1,148 @@
+#include "../json_parser.hpp"
+#include "../power_off_rule.hpp"
+#include "mock_power_interface.hpp"
+
+#include <gtest/gtest.h>
+
+using namespace phosphor::fan::monitor;
+using json = nlohmann::json;
+
+TEST(PowerOffRuleTest, TestRules)
+{
+    sd_event* event;
+    sd_event_default(&event);
+    sdeventplus::Event sdEvent{event};
+
+    const auto faultConfig = R"(
+    {
+        "fault_handling":
+        {
+            "power_off_config": [
+                {
+                    "type": "hard",
+                    "cause": "missing_fan_frus",
+                    "count": 2,
+                    "delay": 0,
+                    "state": "at_pgood"
+                },
+                {
+                    "type": "soft",
+                    "cause": "nonfunc_fan_rotors",
+                    "count": 3,
+                    "delay": 0,
+                    "state": "runtime"
+                },
+                {
+                    "type": "soft",
+                    "cause": "nonfunc_fan_rotors",
+                    "count": 4,
+                    "delay": 1,
+                    "state": "runtime"
+                },
+                {
+                    "type": "hard",
+                    "cause": "missing_fan_frus",
+                    "count": 4,
+                    "delay": 1,
+                    "state": "runtime"
+                }
+            ]
+        }
+    })"_json;
+
+    std::shared_ptr<PowerInterfaceBase> powerIface =
+        std::make_shared<MockPowerInterface>();
+
+    MockPowerInterface& mockIface =
+        static_cast<MockPowerInterface&>(*powerIface);
+
+    EXPECT_CALL(mockIface, hardPowerOff).Times(1);
+    EXPECT_CALL(mockIface, softPowerOff).Times(1);
+
+    auto rules = getPowerOffRules(faultConfig, powerIface);
+    ASSERT_EQ(rules.size(), 4);
+
+    FanHealth health{{"fan0", {false, {true, true}}},
+                     {"fan1", {false, {true, true}}}};
+
+    {
+        // Check rule 0
+
+        // wrong state, won't be active
+        rules[0]->check(PowerRuleState::runtime, health);
+        EXPECT_FALSE(rules[0]->active());
+
+        rules[0]->check(PowerRuleState::atPgood, health);
+        EXPECT_TRUE(rules[0]->active());
+
+        // Run the event loop, since the timeout is 0 it should
+        // run the power off and satisfy the EXPECT_CALL above
+        sdEvent.run(std::chrono::milliseconds(1));
+
+        // It doesn't really make much sense to cancel a rule after it
+        // powered off, but it should at least say it isn't active.
+        rules[0]->cancel();
+        EXPECT_FALSE(rules[0]->active());
+    }
+
+    {
+        // Check the second rule.
+        rules[1]->check(PowerRuleState::runtime, health);
+        EXPECT_FALSE(rules[1]->active());
+
+        // > 2 nonfunc rotors
+        health["fan0"] = {true, {true, false}};
+        health["fan1"] = {true, {false, false}};
+
+        rules[1]->check(PowerRuleState::runtime, health);
+        EXPECT_TRUE(rules[1]->active());
+
+        // Run the event loop, since the timeout is 0 it should
+        // run the power off and satisfy the EXPECT_CALL above
+        sdEvent.run(std::chrono::milliseconds(1));
+    }
+
+    {
+        // Check the third rule.  It has a timeout so long we can
+        // cancel it before it runs.
+        health["fan0"] = {true, {false, false}};
+        health["fan1"] = {true, {false, false}};
+
+        rules[2]->check(PowerRuleState::runtime, health);
+        EXPECT_TRUE(rules[2]->active());
+
+        sdEvent.run(std::chrono::milliseconds(1));
+
+        rules[2]->cancel();
+        EXPECT_FALSE(rules[2]->active());
+
+        // This will go past the timeout, it should have been canceled so the
+        // soft power off won't have run and the EXPECT_CALL above
+        // should be happy.
+        sdEvent.run(std::chrono::seconds(1));
+    }
+
+    {
+        // Check the 4th rule. Resolve it before it completes
+        health["fan0"] = {false, {true, true}};
+        health["fan1"] = {false, {true, true}};
+        health["fan2"] = {false, {true, true}};
+        health["fan3"] = {false, {true, true}};
+
+        rules[3]->check(PowerRuleState::runtime, health);
+        EXPECT_TRUE(rules[3]->active());
+
+        // Won't complete yet
+        sdEvent.run(std::chrono::milliseconds(1));
+
+        // Make them present
+        health["fan0"] = {true, {true, true}};
+        health["fan1"] = {true, {true, true}};
+        health["fan2"] = {true, {true, true}};
+        health["fan3"] = {true, {true, true}};
+
+        //  It should be inactive now
+        rules[3]->check(PowerRuleState::runtime, health);
+        EXPECT_FALSE(rules[3]->active());
+    }
+}