utils: Add function to get the latest version

This repo does not know which PSU version is the latest, and it lets the
vendor tool to decide which one is the latest.

This commit defines PSU_VERSION_COMPARE_UTIL which is expected to be a
tool that accepts arguments of a list of versions, and returns the
latest version string.

Tested: Put and configure to use the example get_latest_version on
        Witherspoon, verify the versionId with a larger version string
        is returned.

Signed-off-by: Lei YU <mine260309@gmail.com>
Change-Id: Ic5a10d3724cf6a98b3979486c72d54f8deac7038
diff --git a/meson.build b/meson.build
index abf2ddf..10748e5 100644
--- a/meson.build
+++ b/meson.build
@@ -38,6 +38,7 @@
 cdata.set_quoted('MANIFEST_FILE', get_option('MANIFEST_FILE'))
 cdata.set_quoted('PSU_INVENTORY_PATH_BASE', get_option('PSU_INVENTORY_PATH_BASE'))
 cdata.set_quoted('PSU_VERSION_UTIL', get_option('PSU_VERSION_UTIL'))
+cdata.set_quoted('PSU_VERSION_COMPARE_UTIL', get_option('PSU_VERSION_COMPARE_UTIL'))
 cdata.set_quoted('PSU_UPDATE_SERVICE', get_option('PSU_UPDATE_SERVICE'))
 cdata.set_quoted('IMG_DIR', get_option('IMG_DIR'))
 cdata.set_quoted('IMG_DIR_PERSIST', get_option('IMG_DIR_PERSIST'))
diff --git a/meson_options.txt b/meson_options.txt
index 4212459..f4e84b4 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -26,12 +26,22 @@
 # inventory path as input, and output the version string, e.g
 #   psutils get-version /xyz/openbmc_project/inventory/system/chassis/motherboard/powersupply0
 # or in vendor-example
-#   get-version <some-psu-path>
+#   get_version <some-psu-path>
 option('PSU_VERSION_UTIL',
        type: 'string',
        value: '/usr/bin/psutils --getversion',
        description: 'The command and arguments to get PSU version')
 
+# The PSU_VERSION_COMPARE_UTIL specifies an executable that accepts the PSU
+# versions as input, and outputs which version is the newest, e.g.
+#   psutils get-version 0001 0002 0003 # May output 0003
+# or in vendor-example
+#   get_latest_version 0001 0002 0003 # output 0003
+option('PSU_VERSION_COMPARE_UTIL',
+       type: 'string',
+       value: '/usr/bin/psutils --compare',
+       description: 'The command and arguments to compare PSU versions')
+
 # The PSU update service
 # It shall take a path containing the PSU image(s) as the input
 option('PSU_UPDATE_SERVICE',
diff --git a/src/item_updater.cpp b/src/item_updater.cpp
index 6472fe1..13e6bf4 100644
--- a/src/item_updater.cpp
+++ b/src/item_updater.cpp
@@ -4,6 +4,7 @@
 
 #include "utils.hpp"
 
+#include <cassert>
 #include <filesystem>
 #include <phosphor-logging/elog-errors.hpp>
 #include <phosphor-logging/log.hpp>
@@ -271,6 +272,7 @@
     sdbusplus::xyz::openbmc_project::Software::server::Version::VersionPurpose
         versionPurpose)
 {
+    versionStrings.insert(versionString);
     auto version = std::make_unique<Version>(
         bus, objPath, versionId, versionString, versionPurpose,
         std::bind(&ItemUpdater::erase, this, std::placeholders::_1));
@@ -412,6 +414,27 @@
     }
 }
 
+std::optional<std::string> ItemUpdater::getLatestVersionId()
+{
+    auto latestVersion = utils::getLatestVersion(versionStrings);
+    if (latestVersion.empty())
+    {
+        return {};
+    }
+
+    std::optional<std::string> versionId;
+    for (const auto& v : versions)
+    {
+        if (v.second->version() == latestVersion)
+        {
+            versionId = v.first;
+            break;
+        }
+    }
+    assert(versionId.has_value());
+    return versionId;
+}
+
 } // namespace updater
 } // namespace software
 } // namespace phosphor
diff --git a/src/item_updater.hpp b/src/item_updater.hpp
index 2cccf68..2f23b1e 100644
--- a/src/item_updater.hpp
+++ b/src/item_updater.hpp
@@ -55,6 +55,7 @@
     {
         processPSUImage();
         processStoredImage();
+        getLatestVersionId();
     }
 
     /** @brief Deletes version
@@ -150,6 +151,9 @@
     /** @brief Scan a directory and create PSU Version from stored images */
     void scanDirectory(const fs::path& p);
 
+    /** @brief Get the versionId of the latest PSU version */
+    std::optional<std::string> getLatestVersionId();
+
     /** @brief Persistent sdbusplus D-Bus bus connection. */
     sdbusplus::bus::bus& bus;
 
@@ -174,6 +178,9 @@
 
     /** @brief This entry's associations */
     AssociationList assocs;
+
+    /** @brief A collection of the version strings */
+    std::set<std::string> versionStrings;
 };
 
 } // namespace updater
diff --git a/src/utils.cpp b/src/utils.cpp
index 33e56be..171de29 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -154,6 +154,21 @@
     return (rc == 0) ? r : "";
 }
 
+std::string Utils::getLatestVersion(const std::set<std::string>& versions) const
+{
+    if (versions.empty())
+    {
+        return {};
+    }
+    std::stringstream args;
+    for (const auto& s : versions)
+    {
+        args << s << " ";
+    }
+    auto [rc, r] = internal::exec(PSU_VERSION_COMPARE_UTIL, args.str());
+    return (rc == 0) ? r : "";
+}
+
 any Utils::getPropertyImpl(sdbusplus::bus::bus& bus, const char* service,
                            const char* path, const char* interface,
                            const char* propertyName) const
diff --git a/src/utils.hpp b/src/utils.hpp
index fe376a8..bb5f13f 100644
--- a/src/utils.hpp
+++ b/src/utils.hpp
@@ -2,6 +2,7 @@
 
 #include <experimental/any>
 #include <sdbusplus/bus.hpp>
+#include <set>
 #include <string>
 #include <vector>
 
@@ -82,6 +83,14 @@
  */
 std::string getVersion(const std::string& inventoryPath);
 
+/** @brief Get latest version from the PSU versions
+ *
+ * @param[in] versions - The list of the versions
+ *
+ * @return The latest version string
+ */
+std::string getLatestVersion(const std::set<std::string>& versions);
+
 /**
  * @brief The interface for utils
  */
@@ -107,6 +116,9 @@
 
     virtual std::string getVersion(const std::string& inventoryPath) const = 0;
 
+    virtual std::string
+        getLatestVersion(const std::set<std::string>& versions) const = 0;
+
     virtual any getPropertyImpl(sdbusplus::bus::bus& bus, const char* service,
                                 const char* path, const char* interface,
                                 const char* propertyName) const = 0;
@@ -140,6 +152,9 @@
 
     std::string getVersion(const std::string& inventoryPath) const override;
 
+    std::string
+        getLatestVersion(const std::set<std::string>& versions) const override;
+
     any getPropertyImpl(sdbusplus::bus::bus& bus, const char* service,
                         const char* path, const char* interface,
                         const char* propertyName) const override;
@@ -173,6 +188,11 @@
     return getUtils().getVersion(inventoryPath);
 }
 
+inline std::string getLatestVersion(const std::set<std::string>& versions)
+{
+    return getUtils().getLatestVersion(versions);
+}
+
 template <typename T>
 T getProperty(sdbusplus::bus::bus& bus, const char* service, const char* path,
               const char* interface, const char* propertyName)
diff --git a/test/mocked_utils.hpp b/test/mocked_utils.hpp
index 8c58a8e..cd85c3c 100644
--- a/test/mocked_utils.hpp
+++ b/test/mocked_utils.hpp
@@ -27,6 +27,9 @@
     MOCK_CONST_METHOD1(getVersion,
                        std::string(const std::string& psuInventoryPath));
 
+    MOCK_CONST_METHOD1(getLatestVersion,
+                       std::string(const std::set<std::string>& versions));
+
     MOCK_CONST_METHOD5(getPropertyImpl,
                        any(sdbusplus::bus::bus& bus, const char* service,
                            const char* path, const char* interface,
diff --git a/test/test_item_updater.cpp b/test/test_item_updater.cpp
index ed55906..b4ecafc 100644
--- a/test/test_item_updater.cpp
+++ b/test/test_item_updater.cpp
@@ -65,6 +65,7 @@
 
 TEST_F(TestItemUpdater, ctordtor)
 {
+    EXPECT_CALL(mockedUtils, getLatestVersion(_)).Times(1);
     itemUpdater = std::make_unique<ItemUpdater>(mockedBus, dBusPath);
 }
 
diff --git a/vendor-example/get_latest_version.cpp b/vendor-example/get_latest_version.cpp
new file mode 100644
index 0000000..38dea2c
--- /dev/null
+++ b/vendor-example/get_latest_version.cpp
@@ -0,0 +1,29 @@
+#include <cstdio>
+#include <string>
+#include <vector>
+
+// Get the version string for a PSU and output to stdout
+// In this example, it just returns the last 8 bytes as the version
+constexpr int NUM_OF_BYTES = 8;
+
+int main(int argc, char** argv)
+{
+    if (argc < 2)
+    {
+        printf("Usage: %s versions...\n", argv[0]);
+        return 1;
+    }
+
+    std::vector<std::string> versions(argv + 1, argv + argc);
+    std::string latest;
+    for (const auto& s : versions)
+    {
+        if (latest < s)
+        {
+            latest = s;
+        }
+    }
+
+    printf("%s", latest.c_str());
+    return 0;
+}
diff --git a/vendor-example/meson.build b/vendor-example/meson.build
index 73e30c2..cfb28c7 100644
--- a/vendor-example/meson.build
+++ b/vendor-example/meson.build
@@ -15,6 +15,7 @@
 
 examples = [
   'get_version',
+  'get_latest_version',
 ]
 
 foreach example : examples