pseq: Add Services class for automated testing
Add a Services class hierarchy to enable mocking and automated testing.
The class methods provide a high level abstraction for system services
like error logging and presence detection.
* Services: Abstract base class
* BMCServices: Sub-class with real implementation using BMC services
* MockServices: Sub-class with mock implementation for automated testing
* Tested:
* Tested all methods in BMCServices class
* When method succeeds
* When method fails; verify all error paths
* The detailed test plan is available in a gist:
* https://gist.github.com/smccarney/e7a250011133c7e3040a8bce240705c5
Change-Id: If17ef8c4540b3ee07cced947bc49a950141b38ae
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/phosphor-power-sequencer/src/config_file_parser.hpp b/phosphor-power-sequencer/src/config_file_parser.hpp
index 3764b7c..dbd41d7 100644
--- a/phosphor-power-sequencer/src/config_file_parser.hpp
+++ b/phosphor-power-sequencer/src/config_file_parser.hpp
@@ -21,6 +21,7 @@
#include <cstdint>
#include <filesystem>
+#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
diff --git a/phosphor-power-sequencer/src/meson.build b/phosphor-power-sequencer/src/meson.build
index faab116..db3ac4a 100644
--- a/phosphor-power-sequencer/src/meson.build
+++ b/phosphor-power-sequencer/src/meson.build
@@ -21,6 +21,7 @@
'power_control.cpp',
'power_interface.cpp',
'power_sequencer_monitor.cpp',
+ 'services.cpp',
'ucd90x_monitor.cpp',
'ucd90160_monitor.cpp',
'ucd90320_monitor.cpp',
diff --git a/phosphor-power-sequencer/src/services.cpp b/phosphor-power-sequencer/src/services.cpp
new file mode 100644
index 0000000..80ad464
--- /dev/null
+++ b/phosphor-power-sequencer/src/services.cpp
@@ -0,0 +1,141 @@
+/**
+ * Copyright © 2024 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "services.hpp"
+
+#include "types.hpp"
+#include "utility.hpp"
+
+#include <sys/types.h> // for getpid()
+#include <unistd.h> // for getpid()
+
+#include <gpiod.hpp>
+
+#include <exception>
+
+namespace phosphor::power::sequencer
+{
+
+void BMCServices::logError(const std::string& message, Entry::Level severity,
+ std::map<std::string, std::string>& additionalData)
+{
+ try
+ {
+ // Add PID to AdditionalData
+ additionalData.emplace("_PID", std::to_string(getpid()));
+
+ // If severity is critical, set error as system terminating
+ if (severity == Entry::Level::Critical)
+ {
+ additionalData.emplace("SEVERITY_DETAIL", "SYSTEM_TERM");
+ }
+
+ auto method = bus.new_method_call(
+ "xyz.openbmc_project.Logging", "/xyz/openbmc_project/logging",
+ "xyz.openbmc_project.Logging.Create", "Create");
+ method.append(message, severity, additionalData);
+ bus.call_noreply(method);
+ }
+ catch (const std::exception& e)
+ {
+ lg2::error("Unable to log error {ERROR}: {EXCEPTION}", "ERROR", message,
+ "EXCEPTION", e);
+ }
+}
+
+bool BMCServices::isPresent(const std::string& inventoryPath)
+{
+ // Initially assume hardware is not present
+ bool present{false};
+
+ // Get presence from D-Bus interface/property
+ try
+ {
+ util::getProperty(INVENTORY_IFACE, PRESENT_PROP, inventoryPath,
+ INVENTORY_MGR_IFACE, bus, present);
+ }
+ catch (const sdbusplus::exception_t& e)
+ {
+ // If exception type is expected and indicates hardware not present
+ if (isExpectedException(e))
+ {
+ present = false;
+ }
+ else
+ {
+ // Re-throw unexpected exception
+ throw;
+ }
+ }
+
+ return present;
+}
+
+std::vector<int> BMCServices::getGPIOValues(const std::string& chipLabel)
+{
+ // Set up the chip object
+ gpiod::chip chip{chipLabel, gpiod::chip::OPEN_BY_LABEL};
+ unsigned int numLines = chip.num_lines();
+ lg2::info(
+ "Reading GPIO values from chip {NAME} with label {LABEL} and {NUM_LINES} lines",
+ "NAME", chip.name(), "LABEL", chipLabel, "NUM_LINES", numLines);
+
+ // Read GPIO values. Work around libgpiod bulk line maximum by getting
+ // values from individual lines.
+ std::vector<int> values;
+ for (unsigned int offset = 0; offset < numLines; ++offset)
+ {
+ gpiod::line line = chip.get_line(offset);
+ line.request({"phosphor-power-control",
+ gpiod::line_request::DIRECTION_INPUT, 0});
+ values.push_back(line.get_value());
+ line.release();
+ }
+ return values;
+}
+
+bool BMCServices::isExpectedException(const sdbusplus::exception_t& e)
+{
+ // Initially assume exception is not one of the expected types
+ bool isExpected{false};
+
+ // If the D-Bus error name is set within the exception
+ if (e.name() != nullptr)
+ {
+ // Check if the error name is one of the expected values when hardware
+ // is not present.
+ //
+ // Sometimes the object path does not exist. Sometimes the object path
+ // exists, but it does not implement the D-Bus interface that contains
+ // the present property. Both of these cases result in exceptions.
+ //
+ // In the case where the interface is not implemented, the systemd
+ // documentation seems to indicate that the error name should be
+ // SD_BUS_ERROR_UNKNOWN_INTERFACE. However, in OpenBMC the
+ // SD_BUS_ERROR_UNKNOWN_PROPERTY error name can occur.
+ std::string name = e.name();
+ if ((name == SD_BUS_ERROR_UNKNOWN_OBJECT) ||
+ (name == SD_BUS_ERROR_UNKNOWN_INTERFACE) ||
+ (name == SD_BUS_ERROR_UNKNOWN_PROPERTY))
+ {
+ isExpected = true;
+ }
+ }
+
+ return isExpected;
+}
+
+} // namespace phosphor::power::sequencer
diff --git a/phosphor-power-sequencer/src/services.hpp b/phosphor-power-sequencer/src/services.hpp
new file mode 100644
index 0000000..743c034
--- /dev/null
+++ b/phosphor-power-sequencer/src/services.hpp
@@ -0,0 +1,211 @@
+/**
+ * Copyright © 2024 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include "pmbus.hpp"
+#include "xyz/openbmc_project/Logging/Entry/server.hpp"
+
+#include <fmt/format.h>
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/exception.hpp>
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace phosphor::power::sequencer
+{
+
+using namespace sdbusplus::xyz::openbmc_project::Logging::server;
+using PMBusBase = phosphor::pmbus::PMBusBase;
+using PMBus = phosphor::pmbus::PMBus;
+
+/**
+ * @class Services
+ *
+ * Abstract base class that provides an interface to system services like error
+ * logging and the journal.
+ */
+class Services
+{
+ public:
+ // Specify which compiler-generated methods we want
+ Services() = default;
+ Services(const Services&) = delete;
+ Services(Services&&) = delete;
+ Services& operator=(const Services&) = delete;
+ Services& operator=(Services&&) = delete;
+ virtual ~Services() = default;
+
+ /**
+ * Returns the D-Bus bus object.
+ *
+ * @return D-Bus bus
+ */
+ virtual sdbusplus::bus_t& getBus() = 0;
+
+ /**
+ * Logs an error message in the system journal.
+ *
+ * @param message message to log
+ */
+ virtual void logErrorMsg(const std::string& message) = 0;
+
+ /**
+ * Logs an informational message in the system journal.
+ *
+ * @param message message to log
+ */
+ virtual void logInfoMsg(const std::string& message) = 0;
+
+ /**
+ * Logs an error.
+ *
+ * If logging fails, a message is written to the system journal but an
+ * exception is not thrown.
+ *
+ * @param message Message property of the error log entry
+ * @param severity Severity property of the error log entry
+ * @param additionalData AdditionalData property of the error log entry
+ */
+ virtual void
+ logError(const std::string& message, Entry::Level severity,
+ std::map<std::string, std::string>& additionalData) = 0;
+
+ /**
+ * Returns whether the hardware with the specified inventory path is
+ * present.
+ *
+ * Throws an exception if an error occurs while obtaining the presence
+ * value.
+ *
+ * @param inventoryPath D-Bus inventory path of the hardware
+ * @return true if hardware is present, false otherwise
+ */
+ virtual bool isPresent(const std::string& inventoryPath) = 0;
+
+ /**
+ * Reads all the GPIO values on the chip with the specified label.
+ *
+ * Throws an exception if an error occurs while obtaining the values.
+ *
+ * @param chipLabel label identifying the chip with the GPIOs
+ * @return GPIO values
+ */
+ virtual std::vector<int> getGPIOValues(const std::string& chipLabel) = 0;
+
+ /**
+ * Creates object for communicating with a PMBus device by reading and
+ * writing sysfs files.
+ *
+ * Throws an exception if an error occurs.
+ *
+ * @param bus I2C bus
+ * @param address I2C address
+ * @param driverName Device driver name
+ * @param instance Chip instance number
+ * @return object for communicating with PMBus device
+ */
+ virtual std::unique_ptr<PMBusBase>
+ createPMBus(uint8_t bus, uint16_t address,
+ const std::string& driverName = "",
+ size_t instance = 0) = 0;
+};
+
+/**
+ * @class BMCServices
+ *
+ * Implementation of the Services interface using standard BMC system services.
+ */
+class BMCServices : public Services
+{
+ public:
+ // Specify which compiler-generated methods we want
+ BMCServices() = delete;
+ BMCServices(const BMCServices&) = delete;
+ BMCServices(BMCServices&&) = delete;
+ BMCServices& operator=(const BMCServices&) = delete;
+ BMCServices& operator=(BMCServices&&) = delete;
+ virtual ~BMCServices() = default;
+
+ /**
+ * Constructor.
+ *
+ * @param bus D-Bus bus object
+ */
+ explicit BMCServices(sdbusplus::bus_t& bus) : bus{bus} {}
+
+ /** @copydoc Services::getBus() */
+ virtual sdbusplus::bus_t& getBus() override
+ {
+ return bus;
+ }
+
+ /** @copydoc Services::logErrorMsg() */
+ virtual void logErrorMsg(const std::string& message) override
+ {
+ lg2::error(message.c_str());
+ }
+
+ /** @copydoc Services::logInfoMsg() */
+ virtual void logInfoMsg(const std::string& message) override
+ {
+ lg2::info(message.c_str());
+ }
+
+ /** @copydoc Services::logError() */
+ virtual void
+ logError(const std::string& message, Entry::Level severity,
+ std::map<std::string, std::string>& additionalData) override;
+
+ /** @copydoc Services::isPresent() */
+ virtual bool isPresent(const std::string& inventoryPath) override;
+
+ /** @copydoc Services::getGPIOValues() */
+ virtual std::vector<int>
+ getGPIOValues(const std::string& chipLabel) override;
+
+ /** @copydoc Services::createPMBus() */
+ virtual std::unique_ptr<PMBusBase>
+ createPMBus(uint8_t bus, uint16_t address,
+ const std::string& driverName = "",
+ size_t instance = 0) override
+ {
+ std::string path = fmt::format("/sys/bus/i2c/devices/{}-{:04x}", bus,
+ address);
+ return std::make_unique<PMBus>(path, driverName, instance);
+ }
+
+ private:
+ /**
+ * Returns whether the specified D-Bus exception is one of the expected
+ * types that can be thrown if hardware is not present.
+ *
+ * @return true if exception type is expected, false otherwise
+ */
+ bool isExpectedException(const sdbusplus::exception_t& e);
+
+ /**
+ * D-Bus bus object.
+ */
+ sdbusplus::bus_t& bus;
+};
+
+} // namespace phosphor::power::sequencer
diff --git a/phosphor-power-sequencer/test/mock_pmbus.hpp b/phosphor-power-sequencer/test/mock_pmbus.hpp
new file mode 100644
index 0000000..587e08b
--- /dev/null
+++ b/phosphor-power-sequencer/test/mock_pmbus.hpp
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2024 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include "pmbus.hpp"
+
+#include <gmock/gmock.h>
+
+namespace phosphor::pmbus
+{
+
+/**
+ * @class MockPMBus
+ *
+ * Mock implementation of the PMBusBase interface.
+ */
+class MockPMBus : public PMBusBase
+{
+ public:
+ // Specify which compiler-generated methods we want
+ MockPMBus() = default;
+ MockPMBus(const MockPMBus&) = delete;
+ MockPMBus(MockPMBus&&) = delete;
+ MockPMBus& operator=(const MockPMBus&) = delete;
+ MockPMBus& operator=(MockPMBus&&) = delete;
+ virtual ~MockPMBus() = default;
+
+ MOCK_METHOD(uint64_t, read,
+ (const std::string& name, Type type, bool errTrace),
+ (override));
+ MOCK_METHOD(std::string, readString, (const std::string& name, Type type),
+ (override));
+ MOCK_METHOD(std::vector<uint8_t>, readBinary,
+ (const std::string& name, Type type, size_t length),
+ (override));
+ MOCK_METHOD(void, writeBinary,
+ (const std::string& name, std::vector<uint8_t> data, Type type),
+ (override));
+ MOCK_METHOD(void, findHwmonDir, (), (override));
+ MOCK_METHOD(const fs::path&, path, (), (const, override));
+ MOCK_METHOD(std::string, insertPageNum,
+ (const std::string& templateName, size_t page), (override));
+};
+
+} // namespace phosphor::pmbus
diff --git a/phosphor-power-sequencer/test/mock_services.hpp b/phosphor-power-sequencer/test/mock_services.hpp
new file mode 100644
index 0000000..78ccbc2
--- /dev/null
+++ b/phosphor-power-sequencer/test/mock_services.hpp
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2024 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include "mock_pmbus.hpp"
+#include "services.hpp"
+
+#include <gmock/gmock.h>
+
+namespace phosphor::power::sequencer
+{
+
+/**
+ * @class MockServices
+ *
+ * Mock implementation of the Services interface.
+ */
+class MockServices : public Services
+{
+ public:
+ // Specify which compiler-generated methods we want
+ MockServices() = default;
+ MockServices(const MockServices&) = delete;
+ MockServices(MockServices&&) = delete;
+ MockServices& operator=(const MockServices&) = delete;
+ MockServices& operator=(MockServices&&) = delete;
+ virtual ~MockServices() = default;
+
+ MOCK_METHOD(sdbusplus::bus_t&, getBus, (), (override));
+ MOCK_METHOD(void, logErrorMsg, (const std::string& message), (override));
+ MOCK_METHOD(void, logInfoMsg, (const std::string& message), (override));
+ MOCK_METHOD(void, logError,
+ (const std::string& message, Entry::Level severity,
+ std::map<std::string, std::string>& additionalData),
+ (override));
+ MOCK_METHOD(bool, isPresent, (const std::string& inventoryPath),
+ (override));
+ MOCK_METHOD(std::vector<int>, getGPIOValues, (const std::string& chipLabel),
+ (override));
+
+ virtual std::unique_ptr<PMBusBase>
+ createPMBus(uint8_t bus, uint16_t address,
+ const std::string& driverName = "",
+ size_t instance = 0) override
+ {
+ return std::make_unique<MockPMBus>();
+ }
+};
+
+} // namespace phosphor::power::sequencer