pseq: Find config file using new compatible intf

Add support for finding the JSON configuration file using a list of
compatible names from the new
xyz.openbmc_project.Inventory.Decorator.Compatible D-Bus interface.

Change-Id: Ic24bafd1d77413015aa8eac7b312dbd604a10995
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/phosphor-power-sequencer/src/config_file_parser.cpp b/phosphor-power-sequencer/src/config_file_parser.cpp
index fe9c661..83754b1 100644
--- a/phosphor-power-sequencer/src/config_file_parser.cpp
+++ b/phosphor-power-sequencer/src/config_file_parser.cpp
@@ -23,10 +23,51 @@
 #include <optional>
 
 using json = nlohmann::json;
+namespace fs = std::filesystem;
 
 namespace phosphor::power::sequencer::config_file_parser
 {
 
+const std::filesystem::path standardConfigFileDirectory{
+    "/usr/share/phosphor-power-sequencer"};
+
+std::filesystem::path
+    find(const std::vector<std::string>& compatibleSystemTypes,
+         const std::filesystem::path& configFileDir)
+{
+    fs::path pathName, possiblePath;
+    std::string fileName;
+
+    for (const std::string& systemType : compatibleSystemTypes)
+    {
+        // Look for file name that is entire system type + ".json"
+        // Example: com.acme.Hardware.Chassis.Model.MegaServer.json
+        fileName = systemType + ".json";
+        possiblePath = configFileDir / fileName;
+        if (fs::is_regular_file(possiblePath))
+        {
+            pathName = possiblePath;
+            break;
+        }
+
+        // Look for file name that is last node of system type + ".json"
+        // Example: MegaServer.json
+        std::string::size_type pos = systemType.rfind('.');
+        if ((pos != std::string::npos) && ((systemType.size() - pos) > 1))
+        {
+            fileName = systemType.substr(pos + 1) + ".json";
+            possiblePath = configFileDir / fileName;
+            if (fs::is_regular_file(possiblePath))
+            {
+                pathName = possiblePath;
+                break;
+            }
+        }
+    }
+
+    return pathName;
+}
+
 std::vector<std::unique_ptr<Rail>> parse(const std::filesystem::path& pathName)
 {
     try
diff --git a/phosphor-power-sequencer/src/config_file_parser.hpp b/phosphor-power-sequencer/src/config_file_parser.hpp
index dbd41d7..77d9650 100644
--- a/phosphor-power-sequencer/src/config_file_parser.hpp
+++ b/phosphor-power-sequencer/src/config_file_parser.hpp
@@ -30,6 +30,36 @@
 {
 
 /**
+ * Standard JSON configuration file directory on the BMC.
+ */
+extern const std::filesystem::path standardConfigFileDirectory;
+
+/**
+ * Finds the JSON configuration file for the current system based on the
+ * specified compatible system types.
+ *
+ * This is required when a single BMC firmware image supports multiple system
+ * types and some system types require different configuration files.
+ *
+ * The compatible system types must be ordered from most to least specific.
+ * Example:
+ *   - com.acme.Hardware.Chassis.Model.MegaServer4CPU
+ *   - com.acme.Hardware.Chassis.Model.MegaServer
+ *   - com.acme.Hardware.Chassis.Model.Server
+ *
+ * Throws an exception if an error occurs.
+ *
+ * @param compatibleSystemTypes compatible system types for the current system
+ *                              ordered from most to least specific
+ * @param configFileDir directory containing configuration files
+ * @return path to the JSON configuration file, or an empty path if none was
+ *         found
+ */
+std::filesystem::path find(
+    const std::vector<std::string>& compatibleSystemTypes,
+    const std::filesystem::path& configFileDir = standardConfigFileDirectory);
+
+/**
  * Parses the specified JSON configuration file.
  *
  * Returns the corresponding C++ Rail objects.
diff --git a/phosphor-power-sequencer/test/config_file_parser_tests.cpp b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
index f28d827..6f8a878 100644
--- a/phosphor-power-sequencer/test/config_file_parser_tests.cpp
+++ b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
@@ -17,6 +17,7 @@
 #include "config_file_parser_error.hpp"
 #include "rail.hpp"
 #include "temporary_file.hpp"
+#include "temporary_subdirectory.hpp"
 
 #include <sys/stat.h> // for chmod()
 
@@ -37,23 +38,209 @@
 using namespace phosphor::power::sequencer;
 using namespace phosphor::power::sequencer::config_file_parser;
 using namespace phosphor::power::sequencer::config_file_parser::internal;
+using namespace phosphor::power::util;
 using json = nlohmann::json;
-using TemporaryFile = phosphor::power::util::TemporaryFile;
+namespace fs = std::filesystem;
 
-void writeConfigFile(const std::filesystem::path& pathName,
-                     const std::string& contents)
+void writeConfigFile(const fs::path& pathName, const std::string& contents)
 {
     std::ofstream file{pathName};
     file << contents;
 }
 
-void writeConfigFile(const std::filesystem::path& pathName,
-                     const json& contents)
+void writeConfigFile(const fs::path& pathName, const json& contents)
 {
     std::ofstream file{pathName};
     file << contents;
 }
 
+TEST(ConfigFileParserTests, Find)
+{
+    std::vector<std::string> compatibleSystemTypes{
+        "com.acme.Hardware.Chassis.Model.MegaServer4CPU",
+        "com.acme.Hardware.Chassis.Model.MegaServer",
+        "com.acme.Hardware.Chassis.Model.Server"};
+
+    // Test where works: Fully qualified system type: First in list
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path configFilePath = configFileDirPath;
+        configFilePath /= "com.acme.Hardware.Chassis.Model.MegaServer4CPU.json";
+        writeConfigFile(configFilePath, std::string{""});
+
+        fs::path pathFound = find(compatibleSystemTypes, configFileDirPath);
+        EXPECT_EQ(pathFound, configFilePath);
+    }
+
+    // Test where works: Fully qualified system type: Second in list
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path configFilePath = configFileDirPath;
+        configFilePath /= "com.acme.Hardware.Chassis.Model.MegaServer.json";
+        writeConfigFile(configFilePath, std::string{""});
+
+        fs::path pathFound = find(compatibleSystemTypes, configFileDirPath);
+        EXPECT_EQ(pathFound, configFilePath);
+    }
+
+    // Test where works: Last node in system type: Second in list
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path configFilePath = configFileDirPath;
+        configFilePath /= "MegaServer.json";
+        writeConfigFile(configFilePath, std::string{""});
+
+        fs::path pathFound = find(compatibleSystemTypes, configFileDirPath);
+        EXPECT_EQ(pathFound, configFilePath);
+    }
+
+    // Test where works: Last node in system type: Last in list
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path configFilePath = configFileDirPath;
+        configFilePath /= "Server.json";
+        writeConfigFile(configFilePath, std::string{""});
+
+        fs::path pathFound = find(compatibleSystemTypes, configFileDirPath);
+        EXPECT_EQ(pathFound, configFilePath);
+    }
+
+    // Test where works: System type has no '.'
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path configFilePath = configFileDirPath;
+        configFilePath /= "Server.json";
+        writeConfigFile(configFilePath, std::string{""});
+
+        std::vector<std::string> noDotSystemTypes{"MegaServer4CPU",
+                                                  "MegaServer", "Server"};
+        fs::path pathFound = find(noDotSystemTypes, configFileDirPath);
+        EXPECT_EQ(pathFound, configFilePath);
+    }
+
+    // Test where fails: System type list is empty
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path configFilePath = configFileDirPath;
+        configFilePath /= "Server.json";
+        writeConfigFile(configFilePath, std::string{""});
+
+        std::vector<std::string> emptySystemTypes{};
+        fs::path pathFound = find(emptySystemTypes, configFileDirPath);
+        EXPECT_TRUE(pathFound.empty());
+    }
+
+    // Test where fails: Configuration file directory is empty
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path pathFound = find(compatibleSystemTypes, configFileDirPath);
+        EXPECT_TRUE(pathFound.empty());
+    }
+
+    // Test where fails: Configuration file directory does not exist
+    {
+        fs::path configFileDirPath{"/tmp/does_not_exist_XYZ"};
+
+        fs::path pathFound = find(compatibleSystemTypes, configFileDirPath);
+        EXPECT_TRUE(pathFound.empty());
+    }
+
+    // Test where fails: Configuration file directory is not readable
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+        fs::permissions(configFileDirPath, fs::perms::none);
+
+        EXPECT_THROW(find(compatibleSystemTypes, configFileDirPath),
+                     std::exception);
+
+        fs::permissions(configFileDirPath, fs::perms::owner_all);
+    }
+
+    // Test where fails: No matching file name found
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path configFilePath = configFileDirPath;
+        configFilePath /= "com.acme.Hardware.Chassis.Model.MegaServer";
+        writeConfigFile(configFilePath, std::string{""});
+
+        fs::path pathFound = find(compatibleSystemTypes, configFileDirPath);
+        EXPECT_TRUE(pathFound.empty());
+    }
+
+    // Test where fails: Matching file name is a directory: Fully qualified
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path configFilePath = configFileDirPath;
+        configFilePath /= "com.acme.Hardware.Chassis.Model.MegaServer4CPU.json";
+        fs::create_directory(configFilePath);
+
+        fs::path pathFound = find(compatibleSystemTypes, configFileDirPath);
+        EXPECT_TRUE(pathFound.empty());
+    }
+
+    // Test where fails: Matching file name is a directory: Last node
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path configFilePath = configFileDirPath;
+        configFilePath /= "MegaServer.json";
+        fs::create_directory(configFilePath);
+
+        fs::path pathFound = find(compatibleSystemTypes, configFileDirPath);
+        EXPECT_TRUE(pathFound.empty());
+    }
+
+    // Test where fails: System type has no '.'
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path configFilePath = configFileDirPath;
+        configFilePath /= "MegaServer2CPU.json";
+        writeConfigFile(configFilePath, std::string{""});
+
+        std::vector<std::string> noDotSystemTypes{"MegaServer4CPU",
+                                                  "MegaServer", "Server", ""};
+        fs::path pathFound = find(noDotSystemTypes, configFileDirPath);
+        EXPECT_TRUE(pathFound.empty());
+    }
+
+    // Test where fails: System type ends with '.'
+    {
+        TemporarySubDirectory configFileDir;
+        fs::path configFileDirPath = configFileDir.getPath();
+
+        fs::path configFilePath = configFileDirPath;
+        configFilePath /= "MegaServer4CPU.json";
+        writeConfigFile(configFilePath, std::string{""});
+
+        std::vector<std::string> dotAtEndSystemTypes{
+            "com.acme.Hardware.Chassis.Model.MegaServer4CPU.", "a.", "."};
+        fs::path pathFound = find(dotAtEndSystemTypes, configFileDirPath);
+        EXPECT_TRUE(pathFound.empty());
+    }
+}
+
 TEST(ConfigFileParserTests, Parse)
 {
     // Test where works
@@ -76,7 +263,7 @@
         )"_json;
 
         TemporaryFile configFile;
-        std::filesystem::path pathName{configFile.getPath()};
+        fs::path pathName{configFile.getPath()};
         writeConfigFile(pathName, configFileContents);
 
         std::vector<std::unique_ptr<Rail>> rails = parse(pathName);
@@ -88,7 +275,7 @@
 
     // Test where fails: File does not exist
     {
-        std::filesystem::path pathName{"/tmp/non_existent_file"};
+        fs::path pathName{"/tmp/non_existent_file"};
         EXPECT_THROW(parse(pathName), ConfigFileParserError);
     }
 
@@ -105,7 +292,7 @@
         )"_json;
 
         TemporaryFile configFile;
-        std::filesystem::path pathName{configFile.getPath()};
+        fs::path pathName{configFile.getPath()};
         writeConfigFile(pathName, configFileContents);
 
         chmod(pathName.c_str(), 0222);
@@ -117,7 +304,7 @@
         const std::string configFileContents = "] foo [";
 
         TemporaryFile configFile;
-        std::filesystem::path pathName{configFile.getPath()};
+        fs::path pathName{configFile.getPath()};
         writeConfigFile(pathName, configFileContents);
 
         EXPECT_THROW(parse(pathName), ConfigFileParserError);
@@ -128,7 +315,7 @@
         const json configFileContents = R"( [ "foo", "bar" ] )"_json;
 
         TemporaryFile configFile;
-        std::filesystem::path pathName{configFile.getPath()};
+        fs::path pathName{configFile.getPath()};
         writeConfigFile(pathName, configFileContents);
 
         EXPECT_THROW(parse(pathName), ConfigFileParserError);
diff --git a/phosphor-power-sequencer/test/pmbus_driver_device_tests.cpp b/phosphor-power-sequencer/test/pmbus_driver_device_tests.cpp
index 92f7916..1fedb36 100644
--- a/phosphor-power-sequencer/test/pmbus_driver_device_tests.cpp
+++ b/phosphor-power-sequencer/test/pmbus_driver_device_tests.cpp
@@ -20,9 +20,7 @@
 #include "pmbus_driver_device.hpp"
 #include "rail.hpp"
 #include "services.hpp"
-
-#include <errno.h>
-#include <stdlib.h> // for mkdtemp()
+#include "temporary_subdirectory.hpp"
 
 #include <cstdint>
 #include <exception>
@@ -42,6 +40,8 @@
 
 using namespace phosphor::power::sequencer;
 using namespace phosphor::pmbus;
+using namespace phosphor::power::util;
+namespace fs = std::filesystem;
 
 using ::testing::Return;
 using ::testing::Throw;
@@ -56,34 +56,7 @@
      */
     PMBusDriverDeviceTests() : ::testing::Test{}
     {
-        char pathTemplate[] = "/tmp/pmbus_driver_device_testsXXXXXX";
-        char* retVal = mkdtemp(pathTemplate);
-        if (retVal == nullptr)
-        {
-            throw std::runtime_error{std::format(
-                "Unable to create temporary directory: errno={}", errno)};
-        }
-        tempDir = fs::path{pathTemplate};
-    }
-
-    /**
-     * Destructor.
-     *
-     * Deletes the temporary directory created in the constructor.
-     */
-    virtual ~PMBusDriverDeviceTests()
-    {
-        try
-        {
-            if (!tempDir.empty() && fs::exists(tempDir))
-            {
-                fs::remove_all(tempDir);
-            }
-        }
-        catch (...)
-        {
-            // Destructors must not throw exceptions
-        }
+        tempDirPath = tempDir.getPath();
     }
 
     /**
@@ -115,16 +88,21 @@
      */
     void createFile(const std::string& name, const std::string& contents = "")
     {
-        fs::path path{tempDir / name};
+        fs::path path{tempDirPath / name};
         std::ofstream out{path};
         out << contents;
         out.close();
     }
 
     /**
-     * Temporary directory that is used to create simulated sysfs / hmmon files.
+     * Temporary subdirectory used to create simulated sysfs / hmmon files.
      */
-    fs::path tempDir{};
+    TemporarySubDirectory tempDir;
+
+    /**
+     * Path to temporary subdirectory.
+     */
+    fs::path tempDirPath;
 };
 
 TEST_F(PMBusDriverDeviceTests, Constructor)
@@ -427,7 +405,7 @@
         MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
         EXPECT_CALL(pmbus, getPath(Type::Hwmon))
             .Times(1)
-            .WillOnce(Return(tempDir));
+            .WillOnce(Return(tempDirPath));
         EXPECT_CALL(pmbus, readString("in13_label", Type::Hwmon))
             .Times(1)
             .WillOnce(Return("vout10")); // PAGE number 9 + 1
@@ -456,7 +434,7 @@
         MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
         EXPECT_CALL(pmbus, getPath(Type::Hwmon))
             .Times(1)
-            .WillOnce(Return(tempDir));
+            .WillOnce(Return(tempDirPath));
         EXPECT_CALL(pmbus, readString("in13_label", Type::Hwmon))
             .Times(1)
             .WillOnce(Return("vout9")); // PAGE number 8 + 1
@@ -496,7 +474,7 @@
         MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
         EXPECT_CALL(pmbus, getPath(Type::Hwmon))
             .Times(1)
-            .WillOnce(Return(tempDir));
+            .WillOnce(Return(tempDirPath));
         EXPECT_CALL(pmbus, readString("in1_label", Type::Hwmon))
             .Times(1)
             .WillOnce(Return("vout7")); // PAGE number 6 + 1
@@ -525,7 +503,7 @@
         MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
         EXPECT_CALL(pmbus, getPath(Type::Hwmon))
             .Times(1)
-            .WillOnce(Return(tempDir));
+            .WillOnce(Return(tempDirPath));
         EXPECT_CALL(pmbus, readString("in1_label", Type::Hwmon))
             .Times(1)
             .WillOnce(Return("vout8")); // PAGE number 7 + 1
@@ -570,7 +548,7 @@
         MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
         EXPECT_CALL(pmbus, getPath(Type::Hwmon))
             .Times(1)
-            .WillOnce(Return(tempDir));
+            .WillOnce(Return(tempDirPath));
         EXPECT_CALL(pmbus, readString).Times(0);
 
         const std::map<uint8_t, unsigned int>& map =
@@ -603,7 +581,7 @@
         MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
         EXPECT_CALL(pmbus, getPath(Type::Hwmon))
             .Times(1)
-            .WillOnce(Return(tempDir));
+            .WillOnce(Return(tempDirPath));
         EXPECT_CALL(pmbus, readString("in9_label", Type::Hwmon))
             .Times(1)
             .WillOnce(Return("vout4")); // PAGE number 3 + 1
@@ -645,7 +623,7 @@
         MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
         EXPECT_CALL(pmbus, getPath(Type::Hwmon))
             .Times(1)
-            .WillOnce(Return(tempDir / "in9_label"));
+            .WillOnce(Return(tempDirPath / "in9_label"));
         EXPECT_CALL(pmbus, readString).Times(0);
 
         const std::map<uint8_t, unsigned int>& map =
@@ -667,7 +645,7 @@
         MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
         EXPECT_CALL(pmbus, getPath(Type::Hwmon))
             .Times(1)
-            .WillOnce(Return(tempDir / "does_not_exist"));
+            .WillOnce(Return(tempDirPath / "does_not_exist"));
         EXPECT_CALL(pmbus, readString).Times(0);
 
         const std::map<uint8_t, unsigned int>& map =
@@ -683,7 +661,7 @@
         createFile("in0_label");
 
         // Change temporary directory to be unreadable
-        fs::permissions(tempDir, fs::perms::none);
+        fs::permissions(tempDirPath, fs::perms::none);
 
         MockServices services;
 
@@ -697,7 +675,7 @@
         MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
         EXPECT_CALL(pmbus, getPath(Type::Hwmon))
             .Times(1)
-            .WillOnce(Return(tempDir));
+            .WillOnce(Return(tempDirPath));
         EXPECT_CALL(pmbus, readString).Times(0);
 
         try
@@ -711,7 +689,7 @@
         }
 
         // Change temporary directory to be readable/writable
-        fs::permissions(tempDir, fs::perms::owner_all);
+        fs::permissions(tempDirPath, fs::perms::owner_all);
     }
 }
 
@@ -735,7 +713,7 @@
         MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
         EXPECT_CALL(pmbus, getPath(Type::Hwmon))
             .Times(1)
-            .WillOnce(Return(tempDir));
+            .WillOnce(Return(tempDirPath));
         EXPECT_CALL(pmbus, readString("in0_label", Type::Hwmon))
             .Times(1)
             .WillOnce(Return("vout7")); // PAGE number 6 + 1
@@ -770,7 +748,7 @@
         MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
         EXPECT_CALL(pmbus, getPath(Type::Hwmon))
             .Times(1)
-            .WillOnce(Return(tempDir));
+            .WillOnce(Return(tempDirPath));
         EXPECT_CALL(pmbus, readString("in0_label", Type::Hwmon))
             .Times(1)
             .WillOnce(Return("vout7")); // PAGE number 6 + 1
@@ -817,7 +795,7 @@
     MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
     EXPECT_CALL(pmbus, getPath(Type::Hwmon))
         .Times(2)
-        .WillRepeatedly(Return(tempDir));
+        .WillRepeatedly(Return(tempDirPath));
     EXPECT_CALL(pmbus, readString("in1_label", Type::Hwmon))
         .Times(2)
         .WillRepeatedly(Return("vout7")); // PAGE number 6 + 1