fw update: implement example device

Introduce example-device and example-code-updater, which are used as
examples for how to implement devices and code updaters according to the
design [1]

Tested:

The example code updater allows us to perform manual testing without
a dependency on any specific device.

Running the example code updater results in follow dbus output:

```
busctl --full --no-pager tree xyz.openbmc_project.ExampleCodeUpdater

└─ /xyz
  └─ /xyz/openbmc_project
    └─ /xyz/openbmc_project/software
      └─ /xyz/openbmc_project/software/ExampleSoftware_4081

busctl --full --no-pager introspect xyz.openbmc_project.ExampleCodeUpdater /xyz/openbmc_project/software/ExampleSoftware_4081

CodeUpdater /xyz/openbmc_project/software/ExampleSoftware_4081
NAME                                    TYPE      SIGNATURE RESULT/VALUE                                                           FLAGS
...
xyz.openbmc_project.Software.Activation interface -         -                                                                      -
.Activation                             property  s         "xyz.openbmc_project.Software.Activation.Activations.Active"           emits-change writable
.RequestedActivation                    property  s         "xyz.openbmc_project.Software.Activation.RequestedActivations.None"    emits-change writable
xyz.openbmc_project.Software.Update     interface -         -                                                                      -
.StartUpdate                            method    hs        o                                                                      -
.AllowedApplyTimes                      property  as        1 "xyz.openbmc_project.Software.ApplyTime.RequestedApplyTimes.OnReset" emits-change
xyz.openbmc_project.Software.Version    interface -         -                                                                      -
.Purpose                                property  s         "xyz.openbmc_project.Software.Version.VersionPurpose.Unknown"          emits-change writable
.Version                                property  s         "v1.0"
```

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

Change-Id: I2bad241b3102e58eda5139174791adda82f6ca95
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/common/README.md b/common/README.md
index 9ac5054..584f9cb 100644
--- a/common/README.md
+++ b/common/README.md
@@ -9,6 +9,17 @@
 Device-specific class members can be added to implement the code update flow for
 different devices.
 
+## Example Code
+
+To understand the control flow, consider looking at 'ExampleDevice' and
+'ExampleCodeUpdater'.
+
+The ExampleCodeUpdater implements the classes from common firmware library and
+serves as a demonstration & testing tool.
+
+It implements everything expected of a device-specific code updater and can be
+used as a starting point.
+
 ## PLDM Package Parser
 
 The PackageParser in the pldm directory currently references a following
diff --git a/test/common/exampledevice/example_device.cpp b/test/common/exampledevice/example_device.cpp
new file mode 100644
index 0000000..59acd39
--- /dev/null
+++ b/test/common/exampledevice/example_device.cpp
@@ -0,0 +1,96 @@
+#include "example_device.hpp"
+
+#include "common/include/device.hpp"
+#include "common/include/software_config.hpp"
+#include "common/include/software_manager.hpp"
+
+#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>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software;
+using namespace phosphor::software::config;
+using namespace phosphor::software::manager;
+using namespace phosphor::software::device;
+using namespace phosphor::software::example_device;
+
+SoftwareConfig ExampleDevice::defaultConfig =
+    SoftwareConfig(exampleInvObjPath, exampleVendorIANA,
+                   exampleCompatibleHardware, "Nop", exampleName);
+
+long ExampleCodeUpdater::getRandomId()
+{
+    struct timespec ts;
+    clock_gettime(CLOCK_REALTIME, &ts);
+    unsigned int seed = ts.tv_nsec ^ getpid();
+    srandom(seed);
+    return random() % 10000;
+}
+
+// nop code updater needs unique suffix on dbus for parallel unit testing
+ExampleCodeUpdater::ExampleCodeUpdater(sdbusplus::async::context& ctx,
+                                       long uniqueSuffix) :
+    SoftwareManager(ctx, "ExampleUpdater" + std::to_string(uniqueSuffix))
+{}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> ExampleCodeUpdater::initDevice(
+    const std::string& /*unused*/, const std::string& /*unused*/,
+    SoftwareConfig& /*unused*/)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    auto device = std::make_unique<ExampleDevice>(ctx, this);
+
+    device->softwareCurrent = std::make_unique<Software>(ctx, *device);
+
+    device->softwareCurrent->setVersion("v1.0");
+    device->softwareCurrent->setActivation(
+        SoftwareActivation::Activations::Active);
+
+    auto applyTimes = {RequestedApplyTimes::OnReset};
+    device->softwareCurrent->enableUpdate(applyTimes);
+
+    devices.insert({exampleInvObjPath, std::move(device)});
+
+    co_return true;
+}
+
+ExampleDevice::ExampleDevice(sdbusplus::async::context& ctx,
+                             SoftwareManager* parent,
+                             const SoftwareConfig& config) :
+    Device(ctx, config, parent,
+           {RequestedApplyTimes::Immediate, RequestedApplyTimes::OnReset})
+{}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> ExampleDevice::updateDevice(
+    const uint8_t* /*unused*/, size_t compImageSize)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("Called device specific update function with image size {SIZE}",
+          "SIZE", compImageSize);
+
+    deviceSpecificUpdateFunctionCalled = true;
+
+    // Setting this property for demonstration purpose.
+    // For a real device, this could represent the
+    // percentage completion of writing the firmware,
+    // and any progress made in the update process within this function.
+    // There is no hard constraint on the values here,
+    // we do not have to reach any specific percentage.
+    // The percentage should be monotonic and increasing.
+    for (auto progress = 0; progress <= 100; progress += 20)
+    {
+        setUpdateProgress(90);
+    }
+
+    co_return true;
+}
diff --git a/test/common/exampledevice/example_device.hpp b/test/common/exampledevice/example_device.hpp
new file mode 100644
index 0000000..928af3b
--- /dev/null
+++ b/test/common/exampledevice/example_device.hpp
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "common/include/device.hpp"
+#include "common/include/software_manager.hpp"
+
+#include <phosphor-logging/lg2.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>
+
+namespace phosphor::software::example_device
+{
+
+class ExampleCodeUpdater : public phosphor::software::manager::SoftwareManager
+{
+  public:
+    ExampleCodeUpdater(sdbusplus::async::context& ctx,
+                       long uniqueSuffix = getRandomId());
+
+    sdbusplus::async::task<bool> initDevice(const std::string& service,
+                                            const std::string& path,
+                                            SoftwareConfig& config) final;
+
+  private:
+    static long getRandomId();
+};
+
+const std::string exampleName = "ExampleSoftware";
+
+const uint32_t exampleVendorIANA = 0x0000a015;
+const std::string exampleCompatibleHardware = "com.example.CompatibleDevice";
+
+const std::string exampleInvObjPath =
+    "/xyz/openbmc_project/inventory/system/board/ExampleBoard/ExampleDevice";
+
+class ExampleDevice : public Device
+{
+  public:
+    using Device::softwarePending;
+    using phosphor::software::device::Device::softwareCurrent;
+
+    static SoftwareConfig defaultConfig;
+
+    ExampleDevice(sdbusplus::async::context& ctx,
+                  phosphor::software::manager::SoftwareManager* parent,
+                  const SoftwareConfig& config = defaultConfig);
+
+    // NOLINTBEGIN(readability-static-accessed-through-instance)
+    sdbusplus::async::task<bool> updateDevice(const uint8_t* image,
+                                              size_t image_size) override;
+    // NOLINTEND(readability-static-accessed-through-instance)
+
+    bool deviceSpecificUpdateFunctionCalled = false;
+};
+
+} // namespace phosphor::software::example_device
diff --git a/test/common/exampledevice/example_updater_main.cpp b/test/common/exampledevice/example_updater_main.cpp
new file mode 100644
index 0000000..425ab35
--- /dev/null
+++ b/test/common/exampledevice/example_updater_main.cpp
@@ -0,0 +1,40 @@
+#include "example_device.hpp"
+
+#include <sdbusplus/async/context.hpp>
+
+using namespace phosphor::software::example_device;
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<void> init(ExampleCodeUpdater& updater)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    co_await updater.initDevice("", "", ExampleDevice::defaultConfig);
+
+    co_return;
+}
+
+int main()
+{
+    sdbusplus::async::context ctx;
+
+    ExampleCodeUpdater updater(ctx);
+
+    /*
+     * In Concrete updaters, the initDevices() function needs to be called,
+     * which in turn invokes the virtual initDevice() function implemented here.
+     * However, in ExampleUpdater, the initDevice() function is called directly
+     * because there is no example configuration from EM to consume, which would
+     * otherwise cause the initDevices() API to throw an error. Therefore,
+     * calling initDevice() directly in this case.
+     */
+
+    // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.Branch)
+    ctx.spawn(init(updater));
+
+    std::string busName = "xyz.openbmc_project.Software.ExampleDevice";
+    ctx.get_bus().request_name(busName.c_str());
+
+    ctx.run();
+
+    return 0;
+}
diff --git a/test/common/exampledevice/meson.build b/test/common/exampledevice/meson.build
new file mode 100644
index 0000000..eebead1
--- /dev/null
+++ b/test/common/exampledevice/meson.build
@@ -0,0 +1,30 @@
+libexampledevice = static_library('example_device',
+  'example_device.cpp',
+  include_directories: ['.', common_include],
+  dependencies: [
+    pdi_dep,
+    phosphor_logging_dep,
+    sdbusplus_dep,
+    libpldm_dep,
+  ],
+  link_with: [
+    software_common_lib,
+  ],
+)
+
+executable(
+  'example-code-updater',
+  'example_updater_main.cpp',
+  include_directories: ['.', common_include],
+  dependencies: [
+    pdi_dep,
+    phosphor_logging_dep,
+    sdbusplus_dep,
+    libpldm_dep,
+  ],
+  link_with: [
+    libpldmutil,
+    software_common_lib,
+    libexampledevice
+  ],
+)
diff --git a/test/common/meson.build b/test/common/meson.build
new file mode 100644
index 0000000..964f769
--- /dev/null
+++ b/test/common/meson.build
@@ -0,0 +1 @@
+subdir('exampledevice')
diff --git a/test/meson.build b/test/meson.build
index 20b6879..b7d75f8 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -1 +1,2 @@
 subdir('create_package')
+subdir('common')