tests: Shared subtree for two sysrepo processes

Test whether Sysrepo correctly provides the data when the data are
handled by two processes. This is a simplified setup from the real
use-case where cla-sysrepo and velia will manage the data from the
the ietf-hardware module.

Change-Id: Iae9961e66110e567e52aad12e5cea229633999cf
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 184ca55..fd34127 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -212,6 +212,8 @@
         set(sysrepo_previous_fixture_name ${name} PARENT_SCOPE)
     endfunction()
 
+    sysrepo_fixture_env(sysrepo-ietf-hardware-state YANG ${CMAKE_CURRENT_SOURCE_DIR}/yang/iana-hardware@2018-03-13.yang YANG ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-hardware-state@2018-03-13.yang FEATURE hardware-sensor)
+    sysrepo_fixture_env(sysrepo-ietf-hardware YANG ${CMAKE_CURRENT_SOURCE_DIR}/yang/iana-hardware@2018-03-13.yang YANG ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-hardware@2018-03-13.yang FEATURE hardware-sensor)
 
     velia_test(health_state-manager velia-health)
     velia_test(health_input-semaphore velia-health DbusTesting)
@@ -221,10 +223,27 @@
     velia_test(hardware_emmc velia-ietf-hardware FsTestUtils)
     velia_test(hardware_hwmon velia-ietf-hardware FsTestUtils)
     velia_test(hardware_ietf-hardware velia-ietf-hardware velia-ietf-hardware-sysrepo)
-    sysrepo_fixture_env(sysrepo YANG ${CMAKE_CURRENT_SOURCE_DIR}/yang/iana-hardware@2018-03-13.yang YANG ${CMAKE_CURRENT_SOURCE_DIR}/yang/ietf-hardware-state@2018-03-13.yang FEATURE hardware-sensor)
     set_tests_properties(
             test-hardware_ietf-hardware
-            PROPERTIES FIXTURES_REQUIRED sysrepo:env:sysrepo
+            PROPERTIES FIXTURES_REQUIRED sysrepo:env:sysrepo-ietf-hardware-state
+            RESOURCE_LOCK sysrepo
+    )
+
+    velia_test(sysrepo_two-daemons velia-ietf-hardware-sysrepo)
+    # ctest dance for sysrepo_two-daemons: compile daemon, create a fixture for test
+    add_executable(test-sysrepo_test_merge-daemon ${CMAKE_SOURCE_DIR}/tests/sysrepo_two-daemons_daemon.cpp)
+    target_link_libraries(test-sysrepo_test_merge-daemon PkgConfig::SYSREPO PkgConfig::LIBYANG)
+
+    add_test(NAME sysrepo_test_merge-start COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/sysrepo_two-daemons_control.sh start)
+    add_test(NAME sysrepo_test_merge-stop  COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/sysrepo_two-daemons_control.sh stop)
+    set_tests_properties(sysrepo_test_merge-start PROPERTIES FIXTURES_SETUP   sysrepo:two-daemons RESOURCE_LOCK sysrepo)
+    set_tests_properties(sysrepo_test_merge-stop  PROPERTIES FIXTURES_CLEANUP sysrepo:two-daemons RESOURCE_LOCK sysrepo)
+    set_tests_properties(sysrepo:clean:sysrepo-ietf-hardware PROPERTIES DEPENDS sysrepo_test_merge-stop)
+
+    set_tests_properties(
+            test-sysrepo_two-daemons
+            PROPERTIES
+                FIXTURES_REQUIRED "sysrepo:two-daemons;sysrepo:env:sysrepo-ietf-hardware"
             RESOURCE_LOCK sysrepo
     )
 
diff --git a/tests/sysrepo_two-daemons.cpp b/tests/sysrepo_two-daemons.cpp
new file mode 100644
index 0000000..bf67410
--- /dev/null
+++ b/tests/sysrepo_two-daemons.cpp
@@ -0,0 +1,43 @@
+#include "trompeloeil_doctest.h"
+#include "pretty_printers.h"
+#include "test_log_setup.h"
+#include "test_sysrepo_helpers.h"
+
+/* This is a generic test for the following use-case in the ietf-hardware model
+ *  - Process #1 starts and uses sr_set_item to set some data in the "/ietf-hardware:hardware/component" subtree
+ *  - Process #2 starts and implements sr_oper_get_items_subscribe for the data in the same subtree
+ *  - Process #3 should see all of the data.
+ *
+ *  Processes #1 and #2 are started (and stopped) by ctest wrapper script (sysrepo_test_merge_fixture.sh) and their code can be found in sysrepo_test_merge_daemon.cpp
+ *  The wrapper script ŕeturns *after* both processes report that sysrepo is initialised (ie., callback is added in #2, items are set in #1).
+ *  This is implemented simply via some checks whether file exists (see the sh file).
+ */
+
+using namespace std::chrono_literals;
+
+TEST_CASE("HardwareState with two daemons")
+{
+    TEST_SYSREPO_INIT;
+    TEST_SYSREPO_INIT_LOGS;
+
+    SECTION("Test when both processes are running")
+    {
+        srSess->session_switch_ds(SR_DS_OPERATIONAL);
+        REQUIRE(dataFromSysrepo(srSess, "/ietf-hardware:hardware") == std::map<std::string, std::string> {
+                    {"/component[name='ne']", ""},
+                    {"/component[name='ne']/name", "ne"},
+                    {"/component[name='ne']/class", "iana-hardware:module"},
+                    {"/component[name='ne']/description", "This data was brought to you by process 2 (subscr)."},
+                    {"/component[name='ne']/sensor-data", ""},
+                    {"/component[name='ne:edfa']", ""},
+                    {"/component[name='ne:edfa']/name", "ne:edfa"},
+                    {"/component[name='ne:edfa']/class", "iana-hardware:module"},
+                    {"/component[name='ne:edfa']/sensor-data", ""},
+                    {"/component[name='ne:ctrl']", ""},
+                    {"/component[name='ne:ctrl']/name", "ne:ctrl"},
+                    {"/component[name='ne:ctrl']/class", "iana-hardware:module"},
+                    {"/component[name='ne:ctrl']/sensor-data", ""},
+                });
+        srSess->session_switch_ds(SR_DS_RUNNING);
+    }
+}
diff --git a/tests/sysrepo_two-daemons_control.sh b/tests/sysrepo_two-daemons_control.sh
new file mode 100755
index 0000000..5b12b40
--- /dev/null
+++ b/tests/sysrepo_two-daemons_control.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+
+PIDFILE1="./test-merge1.pid"
+PIDFILE2="./test-merge2.pid"
+
+set -x
+
+stop() {
+  for pidfile in "$PIDFILE1" "$PIDFILE2"; do
+    [[ ! -r "$pidfile" ]] && continue  # no pidfile
+
+    PID="$(cat "$pidfile")"
+
+    if ps --pid "$PID" >/dev/null; then
+      kill -SIGTERM "$PID" 2>/dev/null  # please terminate
+      sleep 0.5
+      while ps --pid "$PID" >/dev/null; do
+        kill -SIGKILL "$PID" 2>/dev/null # shots fired
+      done
+    fi
+    echo "" > "$pidfile"
+  done
+}
+
+start() {
+  rm -f "$PID1.sysrepo" "$PID2.sysrepo" # in case these files already exist
+
+  ./test-sysrepo_test_merge-daemon --subscribe 2>/dev/null 1>/dev/null &  # ctest waits here if those file descriptors are open
+  PID1="$!"
+  echo "$PID1" > $PIDFILE1
+
+  ./test-sysrepo_test_merge-daemon --set-item  2>/dev/null 1>/dev/null &
+  PID2="$!"
+  echo "$PID2" > $PIDFILE2
+
+  echo "Started both daemons ("$PID1", "$PID2")" >&2
+  echo "Waiting for sysrepo initialization" >&2
+  while [ ! -e "$PID1.sysrepo" ] && [ ! -e "$PID2.sysrepo" ]; do
+    sleep 0.1
+  done
+  echo "Done" >&2
+
+}
+
+if [ $# -ne 1 ]; then
+  echo "Usage: $0 start|stop" >&2
+  exit 1
+elif [ "$1" == "start" ]; then
+  stop
+  start
+elif [ "$1" == "stop" ]; then
+  stop
+fi
+
+set +x
+exit 0
diff --git a/tests/sysrepo_two-daemons_daemon.cpp b/tests/sysrepo_two-daemons_daemon.cpp
new file mode 100644
index 0000000..6b79b2d
--- /dev/null
+++ b/tests/sysrepo_two-daemons_daemon.cpp
@@ -0,0 +1,109 @@
+#include <csignal>
+#include <cstring>
+#include <fstream>
+#include <map>
+#include <sysrepo-cpp/Session.hpp>
+#include <unistd.h>
+
+using namespace std::string_literals;
+
+volatile sig_atomic_t g_exit_application = 0;
+
+static const std::string MODULE_NAME = "ietf-hardware";
+static const std::string MODULE_PREFIX = "/" + MODULE_NAME + ":hardware";
+
+void valuesToYang(const std::map<std::string, std::string>& values, std::shared_ptr<::sysrepo::Session> session, std::shared_ptr<libyang::Data_Node>& parent, const std::string& prefix)
+{
+    for (const auto& [propertyName, value] : values) {
+        if (!parent) {
+            parent = std::make_shared<libyang::Data_Node>(
+                session->get_context(),
+                (prefix + propertyName).c_str(),
+                value.c_str(),
+                LYD_ANYDATA_CONSTSTRING,
+                LYD_PATH_OPT_OUTPUT);
+        } else {
+            parent->new_path(
+                session->get_context(),
+                (prefix + propertyName).c_str(),
+                value.c_str(),
+                LYD_ANYDATA_CONSTSTRING,
+                LYD_PATH_OPT_OUTPUT);
+        }
+    }
+}
+
+void usage(const char* progName)
+{
+    std::cout << "Usage: " << progName << "--subscribe|--setitem" << std::endl;
+}
+
+int main(int argc, char* argv[])
+{
+    if (argc != 2) {
+        usage(argv[0]);
+        return 1;
+    }
+
+    bool isDaemonSubscribe = argv[1] == "--subscribe"s;
+    bool isDaemonSetItem = argv[1] == "--set-item"s;
+
+    if (isDaemonSubscribe == isDaemonSetItem) {
+        usage(argv[0]);
+        return 1;
+    }
+
+    auto srConn = std::make_shared<sysrepo::Connection>();
+    auto srSess = std::make_shared<sysrepo::Session>(srConn);
+
+    std::shared_ptr<sysrepo::Subscribe> srSubs;
+    uint32_t srLastRequestId = 0; // for subscribe part
+
+    std::map<std::string, std::string> data;
+
+    if (isDaemonSubscribe) {
+        data = {
+            {"/component[name='ne']/description", "This data was brought to you by process 2 (subscr)."},
+            {"/component[name='ne:ctrl']/class", "iana-hardware:module"},
+        };
+
+        srSubs = std::make_shared<sysrepo::Subscribe>(srSess);
+
+        srSubs->oper_get_items_subscribe(
+            MODULE_NAME.c_str(),
+            [&](std::shared_ptr<::sysrepo::Session> session, [[maybe_unused]] const char* module_name, [[maybe_unused]] const char* xpath, [[maybe_unused]] const char* request_xpath, uint32_t request_id, std::shared_ptr<libyang::Data_Node>& parent) {
+                if (srLastRequestId == request_id) {
+                    return SR_ERR_OK;
+                }
+                srLastRequestId = request_id;
+
+                valuesToYang(data, session, parent, MODULE_PREFIX);
+                return SR_ERR_OK;
+            },
+            (MODULE_PREFIX + "/*").c_str(),
+            SR_SUBSCR_PASSIVE | SR_SUBSCR_OPER_MERGE | SR_SUBSCR_CTX_REUSE);
+    } else if (isDaemonSetItem) {
+        data = {
+            {"/component[name='ne']/class", "iana-hardware:module"},
+            {"/component[name='ne:edfa']/class", "iana-hardware:module"},
+        };
+
+        srSess->session_switch_ds(SR_DS_OPERATIONAL);
+        for (const auto& [k, v] : data) {
+            srSess->set_item_str((MODULE_PREFIX + k).c_str(), v.c_str());
+        }
+        srSess->apply_changes();
+        srSess->session_switch_ds(SR_DS_RUNNING);
+    }
+
+    // touch a file so somebody can read that sysrepo things are initialised
+    {
+        std::string filename = std::to_string(getpid()) + ".sysrepo";
+        std::ofstream ofs(filename);
+        ofs << "";
+    }
+
+    sleep(1000); // I guess, this is plenty of seconds, right?
+
+    return 0;
+}
diff --git a/tests/test_sysrepo_helpers.h b/tests/test_sysrepo_helpers.h
index aa221b8..7cc3640 100644
--- a/tests/test_sysrepo_helpers.h
+++ b/tests/test_sysrepo_helpers.h
@@ -31,3 +31,8 @@
     IMPL_TEST_INIT_LOGS_1                      \
     velia::ietf_hardware::sysrepo::initLogs(); \
     IMPL_TEST_INIT_LOGS_2
+
+#define TEST_SYSREPO_INIT                                     \
+    auto srConn = std::make_shared<sysrepo::Connection>();    \
+    auto srSess = std::make_shared<sysrepo::Session>(srConn); \
+    auto srSubs = std::make_shared<sysrepo::Subscribe>(srSess);