fw update: tests for device

Tests for the device update flow.

Minimal PLDM packages are constructed in testcases and the update flow
is run on device instance.

These tests should check that the common code behaves as outlined in the
design [1]

References:
[1] https://github.com/openbmc/docs/blob/master/designs/code-update.md

Change-Id: I8f12839afd47ef3403a80439af54fedcc00f10be
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/test/common/device/device.cpp b/test/common/device/device.cpp
new file mode 100644
index 0000000..119af8f
--- /dev/null
+++ b/test/common/device/device.cpp
@@ -0,0 +1,216 @@
+#include "../exampledevice/example_device.hpp"
+#include "test/create_package/create_pldm_fw_package.hpp"
+
+#include <sys/mman.h>
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Software/Update/server.hpp>
+
+#include <memory>
+
+#include <gtest/gtest.h>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software;
+using namespace phosphor::software::example_device;
+using SoftwareActivationProgress =
+    sdbusplus::aserver::xyz::openbmc_project::software::ActivationProgress<
+        Software>;
+
+class DeviceTest : public testing::Test
+{
+  protected:
+    DeviceTest() :
+        exampleUpdater(ctx, true, "vUnknown"),
+        device(exampleUpdater.getDevice())
+    {}
+    ~DeviceTest() noexcept override {}
+
+    sdbusplus::async::context ctx;
+    ExampleCodeUpdater exampleUpdater;
+    std::unique_ptr<ExampleDevice>& device;
+
+  public:
+    DeviceTest(const DeviceTest&) = delete;
+    DeviceTest(DeviceTest&&) = delete;
+    DeviceTest& operator=(const DeviceTest&) = delete;
+    DeviceTest& operator=(DeviceTest&&) = delete;
+
+    // @returns memfd
+    // @returns -1 on failure
+    static int createTestPkgMemfd();
+};
+
+int DeviceTest::createTestPkgMemfd()
+{
+    uint8_t component_image[] = {0x12, 0x34, 0x83, 0x21};
+
+    size_t sizeOut;
+    std::unique_ptr<uint8_t[]> buf = create_pldm_package_buffer(
+        component_image, sizeof(component_image),
+        std::optional<uint32_t>(exampleVendorIANA),
+        std::optional<std::string>(exampleCompatibleHardware), sizeOut);
+
+    const int fd = memfd_create("test_memfd", 0);
+
+    EXPECT_TRUE(fd >= 0);
+
+    debug("create fd {FD}", "FD", fd);
+
+    if (write(fd, (void*)buf.get(), sizeOut) == -1)
+    {
+        std::cerr << "Failed to write to memfd: " << strerror(errno)
+                  << std::endl;
+        close(fd);
+        EXPECT_TRUE(false);
+        return -1;
+    }
+
+    if (lseek(fd, 0, SEEK_SET) != 0)
+    {
+        error("could not seek to the beginning of the file");
+        close(fd);
+        EXPECT_TRUE(false);
+        return -1;
+    }
+
+    return fd;
+}
+
+TEST_F(DeviceTest, TestDeviceConstructor)
+{
+    EXPECT_TRUE(device->getEMConfigType().starts_with("Nop"));
+
+    // the software version is initialized
+    EXPECT_NE(device->softwareCurrent, nullptr);
+
+    // there is no pending update
+    EXPECT_EQ(device->softwarePending, nullptr);
+}
+
+sdbusplus::async::task<> testDeviceStartUpdateCommon(
+    sdbusplus::async::context& ctx, std::unique_ptr<ExampleDevice>& device,
+    RequestedApplyTimes applyTime)
+{
+    const Software* oldSoftware = device->softwareCurrent.get();
+
+    const int fd = DeviceTest::createTestPkgMemfd();
+
+    EXPECT_TRUE(fd >= 0);
+
+    if (fd < 0)
+    {
+        co_return;
+    }
+
+    std::unique_ptr<Software> softwareUpdate =
+        std::make_unique<Software>(ctx, *device);
+
+    const Software* newSoftware = softwareUpdate.get();
+
+    co_await device->startUpdateAsync(fd, applyTime, std::move(softwareUpdate));
+
+    EXPECT_TRUE(device->deviceSpecificUpdateFunctionCalled);
+
+    if (applyTime == RequestedApplyTimes::Immediate)
+    {
+        EXPECT_NE(device->softwareCurrent.get(), oldSoftware);
+        EXPECT_EQ(device->softwareCurrent.get(), newSoftware);
+
+        EXPECT_FALSE(device->softwarePending);
+    }
+
+    if (applyTime == RequestedApplyTimes::OnReset)
+    {
+        // assert that the old software is still the running version,
+        // since the apply time is 'OnReset'
+        EXPECT_EQ(device->softwareCurrent.get(), oldSoftware);
+
+        // assert that the updated software is present
+        EXPECT_EQ(device->softwarePending.get(), newSoftware);
+    }
+
+    close(fd);
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST_F(DeviceTest, TestDeviceStartUpdateImmediateSuccess)
+{
+    ctx.spawn(testDeviceStartUpdateCommon(ctx, device,
+                                          RequestedApplyTimes::Immediate));
+    ctx.run();
+}
+
+TEST_F(DeviceTest, TestDeviceStartUpdateOnResetSuccess)
+{
+    ctx.spawn(
+        testDeviceStartUpdateCommon(ctx, device, RequestedApplyTimes::OnReset));
+    ctx.run();
+}
+
+sdbusplus::async::task<> testDeviceStartUpdateInvalidFD(
+    sdbusplus::async::context& ctx, std::unique_ptr<ExampleDevice>& device)
+{
+    std::unique_ptr<SoftwareActivationProgress> activationProgress =
+        std::make_unique<SoftwareActivationProgress>(ctx, "/");
+
+    sdbusplus::message::unix_fd image;
+    image.fd = -1;
+
+    std::unique_ptr<Software> softwareUpdate =
+        std::make_unique<Software>(ctx, *device);
+
+    co_await device->startUpdateAsync(image, RequestedApplyTimes::Immediate,
+                                      std::move(softwareUpdate));
+
+    // assert the bad file descriptor was caught and we did not proceed
+    EXPECT_FALSE(device->deviceSpecificUpdateFunctionCalled);
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST_F(DeviceTest, TestDeviceStartUpdateInvalidFD)
+{
+    ctx.spawn(testDeviceStartUpdateInvalidFD(ctx, device));
+    ctx.run();
+}
+
+sdbusplus::async::task<> testDeviceSpecificUpdateFunction(
+    sdbusplus::async::context& ctx, std::unique_ptr<ExampleDevice>& device)
+{
+    uint8_t buffer[10];
+    size_t buffer_size = 10;
+
+    auto previousVersion = device->softwareCurrent->swid;
+
+    bool success =
+        co_await device->updateDevice((const uint8_t*)buffer, buffer_size);
+
+    EXPECT_TRUE(success);
+
+    EXPECT_NE(device->softwareCurrent, nullptr);
+    EXPECT_EQ(device->softwarePending, nullptr);
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST_F(DeviceTest, TestDeviceSpecificUpdateFunction)
+{
+    // NOLINTBEGIN(clang-analyzer-core.uninitialized.Branch)
+    ctx.spawn(testDeviceSpecificUpdateFunction(ctx, device));
+    // NOLINTEND(clang-analyzer-core.uninitialized.Branch)
+    ctx.run();
+}
diff --git a/test/common/device/meson.build b/test/common/device/meson.build
new file mode 100644
index 0000000..314618a
--- /dev/null
+++ b/test/common/device/meson.build
@@ -0,0 +1,26 @@
+
+testcases = ['device']
+
+foreach t : testcases
+    test(
+        t,
+        executable(
+            t,
+            f'@t@.cpp',
+            include_directories: [common_include],
+            dependencies: [
+                libpldm_dep,
+                sdbusplus_dep,
+                phosphor_logging_dep,
+                gtest,
+            ],
+            link_with: [
+                libpldmutil,
+                libpldmcreatepkg,
+                software_common_lib,
+                libexampledevice,
+            ],
+        ),
+    )
+endforeach
+
diff --git a/test/common/meson.build b/test/common/meson.build
index 3b7912f..27d2b4c 100644
--- a/test/common/meson.build
+++ b/test/common/meson.build
@@ -1,2 +1,3 @@
 subdir('exampledevice')
+subdir('device')
 subdir('software')