From 37e5bcc646508aec36fdeca0a747fbe1056b186d Mon Sep 17 00:00:00 2001
From: Martin Hierholzer <martin.hierholzer@desy.de>
Date: Fri, 28 May 2021 16:22:30 +0200
Subject: [PATCH] [wip #7982] tests for StatusAggregator

---
 tests/executables_src/testStatusAggregator.cc | 352 ++++++++++++++++--
 1 file changed, 316 insertions(+), 36 deletions(-)

diff --git a/tests/executables_src/testStatusAggregator.cc b/tests/executables_src/testStatusAggregator.cc
index 6a2656c8..aa7a3ea8 100644
--- a/tests/executables_src/testStatusAggregator.cc
+++ b/tests/executables_src/testStatusAggregator.cc
@@ -15,65 +15,345 @@ using namespace boost::unit_test_framework;
 
 namespace ctk = ChimeraTK;
 
-struct OuterGroup : public ctk::ModuleGroup {
-  using ctk::ModuleGroup::ModuleGroup;
+/**********************************************************************************************************************/
 
-  ctk::StateMonitor<uint8_t> outerStateMonitor{this, "outerStateMonitor", "", "watch", "status",
-      ctk::HierarchyModifier::none, {"OUTER_MON_OUTPUT"}, {"OUTER_MON_PARAMS"}, {"OUTER_MON_INPUT"}};
+struct StatusGenerator : ctk::ApplicationModule {
+  using ctk::ApplicationModule::ApplicationModule;
+  ctk::StatusOutput status{this, getName(), ""};
+  void mainLoop() override {}
+};
+
+/**********************************************************************************************************************/
+
+struct TestApplication : ctk::Application {
+  TestApplication() : Application("testApp") {}
+  ~TestApplication() override { shutdown(); }
 
-  struct InnerGroup : public ctk::ModuleGroup {
+  StatusGenerator s{this, "s", "Status", ctk::HierarchyModifier::hideThis};
+
+  struct OuterGroup : ctk::ModuleGroup {
     using ctk::ModuleGroup::ModuleGroup;
 
-    ctk::StateMonitor<uint8_t> innerStateMonitorNone{this, "innerMinMonitorNone", "", "watch", "status",
-        ctk::HierarchyModifier::none, {"INNER_MON_OUTPUT"}, {"INNER_MON_PARAMS"}, {"INNER_MON_INPUT"}};
+    StatusGenerator s1{this, "s1", "Status 1", ctk::HierarchyModifier::hideThis};
+    StatusGenerator s2{this, "s2", "Status 2", ctk::HierarchyModifier::hideThis};
+
+    struct InnerGroup : ctk::ModuleGroup {
+      using ctk::ModuleGroup::ModuleGroup;
+      StatusGenerator s{this, "s", "Status", ctk::HierarchyModifier::hideThis};
+      StatusGenerator deep{this, "deep", "Status"};
+    };
+    InnerGroup innerGroup1{this, "InnerGroup1", ""};
+    InnerGroup innerGroup2{this, "InnerGroup2", ""};
+
+  } outerGroup{this, "OuterGroup", ""};
+
+  ctk::StatusAggregator aggregator{
+      this, "Aggregated/status", "aggregated status description", ctk::StatusAggregator::PriorityMode::fwko};
+};
+
+/**********************************************************************************************************************/
+
+BOOST_AUTO_TEST_CASE(testSingleNoTags) {
+  std::cout << "testSingleNoTags" << std::endl;
 
-    ctk::StateMonitor<uint8_t> innerStateMonitorHideThis{this, "innerStateMonitorHideThis", "", "watch", "status",
-        ctk::HierarchyModifier::hideThis, {"INNER_MON_OUTPUT"}, {"INNER_MON_PARAMS"}, {"INNER_MON_INPUT"}};
+  TestApplication app;
+  ctk::TestFacility test;
 
-    ctk::StateMonitor<uint8_t> innerStateMonitorOneUp{this, "innerStateMonitorOneUp", "", "watch", "status",
-        ctk::HierarchyModifier::oneLevelUp, {"INNER_MON_OUTPUT"}, {"INNER_MON_PARAMS"}, {"INNER_MON_INPUT"}};
+  auto status = test.getScalar<int>("/Aggregated/status");
 
-  } innerGroup{this, "innerModuleGroup", "", ctk::HierarchyModifier::none};
+  // write initial values (all in Status::OFF = 0)
+  app.s.status.write();
+  app.outerGroup.s1.status.write();
+  app.outerGroup.s2.status.write();
+  app.outerGroup.innerGroup1.s.status.write();
+  app.outerGroup.innerGroup1.deep.status.write();
+  app.outerGroup.innerGroup2.s.status.write();
+  app.outerGroup.innerGroup2.deep.status.write();
+
+  test.runApplication();
+
+  // check that statuses on different levels are correctly aggregated
+  auto check = [&](auto& var) {
+    var = ctk::StatusOutput::Status::OK;
+    var.write();
+    test.stepApplication();
+    BOOST_CHECK(status.readNonBlocking() == true);
+    BOOST_CHECK_EQUAL(int(status), int(ctk::StatusOutput::Status::OK));
+    var = ctk::StatusOutput::Status::OFF;
+    var.write();
+    test.stepApplication();
+    BOOST_CHECK(status.readNonBlocking() == true);
+    BOOST_CHECK_EQUAL(int(status), int(ctk::StatusOutput::Status::OFF));
+  };
+  check(app.s.status);
+  check(app.outerGroup.s1.status);
+  check(app.outerGroup.s2.status);
+  check(app.outerGroup.innerGroup1.s.status);
+  check(app.outerGroup.innerGroup1.deep.status);
+  check(app.outerGroup.innerGroup2.s.status);
+  check(app.outerGroup.innerGroup2.deep.status);
+}
 
-  ctk::StatusAggregator outerStatusAggregator{this, "outerStatusAggregator", "StatusAggregator of OuterGroup",
-      "groupStatus", ctk::HierarchyModifier::none, {"STATUS"}};
+/**********************************************************************************************************************/
+
+struct TestPrioApplication : ctk::Application {
+  TestPrioApplication() : Application("testApp") {}
+  ~TestPrioApplication() override { shutdown(); }
+
+  StatusGenerator s1{this, "s1", "Status 1", ctk::HierarchyModifier::hideThis};
+  StatusGenerator s2{this, "s2", "Status 2", ctk::HierarchyModifier::hideThis};
+
+  ctk::StatusAggregator aggregator;
 };
 
-struct TestApplication : public ctk::Application {
-  TestApplication() : Application("testApp") {}
-  ~TestApplication() override { shutdown(); }
+/**********************************************************************************************************************/
+
+BOOST_AUTO_TEST_CASE(testPriorities) {
+  std::cout << "testPriorities" << std::endl;
+
+  // Define repeated check for a given priority mode
+  auto check = [&](ctk::StatusAggregator::PriorityMode mode, auto prio0, auto prio1, auto prio2, auto prio3,
+                   bool warnMixed01 = false) {
+    TestPrioApplication app;
+    app.aggregator = ctk::StatusAggregator{&app, "Aggregated/status", "aggregated status description", mode};
+
+    ctk::TestFacility test;
+
+    auto status = test.getScalar<int>("/Aggregated/status");
+
+    // write initial values (all in lowest prio value)
+    app.s1.status = prio0;
+    app.s1.status.write();
+    app.s2.status = prio0;
+    app.s2.status.write();
+
+    test.runApplication();
+
+    // check initial value
+    status.readNonBlocking(); // do not check return value, as it will only be written when changed
+    BOOST_CHECK_EQUAL(int(status), int(prio0));
+
+    // define repeated check to test all combinations of two given values with different priority
+    auto subcheck = [&](auto lower, auto higher, bool warnMixed = false) {
+      std::cout << int(lower) << " vs. " << int(higher) << std::endl;
+      app.s1.status = lower;
+      app.s1.status.write();
+      app.s2.status = lower;
+      app.s2.status.write();
+      test.stepApplication();
+      status.readLatest();
+      BOOST_CHECK_EQUAL(int(status), int(lower));
+      app.s1.status = lower;
+      app.s1.status.write();
+      app.s2.status = higher;
+      app.s2.status.write();
+      test.stepApplication();
+      status.readLatest();
+      if(!warnMixed) {
+        BOOST_CHECK_EQUAL(int(status), int(higher));
+      }
+      else {
+        BOOST_CHECK_EQUAL(int(status), int(ctk::StatusOutput::Status::WARNING));
+      }
+      app.s1.status = higher;
+      app.s1.status.write();
+      app.s2.status = lower;
+      app.s2.status.write();
+      test.stepApplication();
+      status.readLatest();
+      if(!warnMixed) {
+        BOOST_CHECK_EQUAL(int(status), int(higher));
+      }
+      else {
+        BOOST_CHECK_EQUAL(int(status), int(ctk::StatusOutput::Status::WARNING));
+      }
+      app.s1.status = higher;
+      app.s1.status.write();
+      app.s2.status = higher;
+      app.s2.status.write();
+      test.stepApplication();
+      status.readLatest();
+      BOOST_CHECK_EQUAL(int(status), int(higher));
+    };
+
+    // all prios against each other
+    subcheck(prio0, prio1, warnMixed01);
+    subcheck(prio0, prio2);
+    subcheck(prio0, prio3);
+    subcheck(prio1, prio2);
+    subcheck(prio1, prio3);
+    subcheck(prio2, prio3);
+  };
+
+  // check all priority modes
+  std::cout << "PriorityMode::fwko" << std::endl;
+  check(ctk::StatusAggregator::PriorityMode::fwko, ctk::StatusOutput::Status::OFF, ctk::StatusOutput::Status::OK,
+      ctk::StatusOutput::Status::WARNING, ctk::StatusOutput::Status::FAULT);
+  std::cout << "PriorityMode::fwok" << std::endl;
+  check(ctk::StatusAggregator::PriorityMode::fwok, ctk::StatusOutput::Status::OK, ctk::StatusOutput::Status::OFF,
+      ctk::StatusOutput::Status::WARNING, ctk::StatusOutput::Status::FAULT);
+  std::cout << "PriorityMode::ofwk" << std::endl;
+  check(ctk::StatusAggregator::PriorityMode::ofwk, ctk::StatusOutput::Status::OK, ctk::StatusOutput::Status::WARNING,
+      ctk::StatusOutput::Status::FAULT, ctk::StatusOutput::Status::OFF);
+  std::cout << "PriorityMode::fw_warn_mixed" << std::endl;
+  check(ctk::StatusAggregator::PriorityMode::fw_warn_mixed, ctk::StatusOutput::Status::OFF,
+      ctk::StatusOutput::Status::OK, ctk::StatusOutput::Status::WARNING, ctk::StatusOutput::Status::FAULT, true);
+}
+
+/**********************************************************************************************************************/
 
-  OuterGroup outerModuleGroup1{this, "outerModuleGroup1", "", ctk::HierarchyModifier::none};
-  //OuterGroup outerModuleGroup2{this, "outerModuleGroup2", "", ctk::HierarchyModifier::hideThis};
+struct TestApplication2Levels : ctk::Application {
+  TestApplication2Levels() : Application("testApp") {}
+  ~TestApplication2Levels() override { shutdown(); }
 
-  ctk::StateMonitor<uint8_t> globalStateMonitor{this, "globalStateMonitor", "", "stateWatch", "stateStatus",
-      ctk::HierarchyModifier::none, {"GLOBAL_MON_OUTPUT"}, {"GLOBAL_MON_PARAMS"}, {"GLOBAL_MON_INPUT"}};
+  StatusGenerator s{this, "s", "Status", ctk::HierarchyModifier::hideThis};
 
-  ctk::StatusAggregator globalStatusAggregator{this, "globalStatusAggregator", "Global StatusAggregator of testApp",
-      "globalStatus", ctk::HierarchyModifier::none, {"STATUS"}};
+  struct OuterGroup : ctk::ModuleGroup {
+    using ctk::ModuleGroup::ModuleGroup;
+
+    StatusGenerator s1{this, "s1", "Status 1", ctk::HierarchyModifier::hideThis};
+    StatusGenerator s2{this, "s2", "Status 2", ctk::HierarchyModifier::hideThis};
+
+    ctk::StatusAggregator extraAggregator{
+        this, "/Aggregated/extraStatus", "aggregated status description", ctk::StatusAggregator::PriorityMode::ofwk};
+
+  } outerGroup{this, "OuterGroup", ""};
+
+  ctk::StatusAggregator aggregator{
+      this, "Aggregated/status", "aggregated status description", ctk::StatusAggregator::PriorityMode::fwko};
 };
 
-BOOST_AUTO_TEST_CASE(testStatusAggregator) {
-  std::cout << "testStatusAggregator" << std::endl;
+/**********************************************************************************************************************/
 
-  TestApplication app;
+BOOST_AUTO_TEST_CASE(testTwoLevels) {
+  std::cout << "testTwoLevels" << std::endl;
+  TestApplication2Levels app;
+
+  ctk::TestFacility test;
+
+  auto status = test.getScalar<int>("/Aggregated/status");
+  auto extraStatus = test.getScalar<int>("/Aggregated/extraStatus");
+
+  // write initial values [default/0 value is OFF]
+  app.s.status.write();
+  app.outerGroup.s2.status.write();
+  // Set one of the inputs for the extraAggregator to fault, which has no effect, since one other is OFF which is
+  // prioritised. If the top-level aggregator would wrongly aggregate this input directly, it would go to FAULT.
+  app.outerGroup.s1.status = ctk::StatusOutput::Status::FAULT;
+  app.outerGroup.s1.status.write();
+
+  test.runApplication();
+
+  // check the initial values
+  extraStatus.readLatest();
+  BOOST_CHECK_EQUAL(int(extraStatus), int(ctk::StatusOutput::Status::OFF));
+  status.readLatest();
+  BOOST_CHECK_EQUAL(int(status), int(ctk::StatusOutput::Status::OFF));
+
+  // change status which goes directly into the upper aggregator
+  app.s.status = ctk::StatusOutput::Status::OK;
+  app.s.status.write();
+  test.stepApplication();
+  status.readLatest();
+  BOOST_CHECK_EQUAL(int(status), int(ctk::StatusOutput::Status::OK));
+  app.s.status = ctk::StatusOutput::Status::OFF;
+  app.s.status.write();
+  test.stepApplication();
+  status.readLatest();
+  BOOST_CHECK_EQUAL(int(status), int(ctk::StatusOutput::Status::OFF));
+
+  // change status which goes into the lower aggregator (then the FAULT of s1 will win)
+  app.outerGroup.s2.status = ctk::StatusOutput::Status::OK;
+  app.outerGroup.s2.status.write();
+  test.stepApplication();
+  status.readLatest();
+  BOOST_CHECK_EQUAL(int(status), int(ctk::StatusOutput::Status::FAULT));
+  app.outerGroup.s2.status = ctk::StatusOutput::Status::OFF;
+  app.outerGroup.s2.status.write();
+  test.stepApplication();
+  status.readLatest();
+  BOOST_CHECK_EQUAL(int(status), int(ctk::StatusOutput::Status::OFF));
+}
+
+/**********************************************************************************************************************/
+
+struct TestApplicationTags : ctk::Application {
+  TestApplicationTags() : Application("testApp") {}
+  ~TestApplicationTags() override { shutdown(); }
+
+  struct OuterGroup : ctk::ModuleGroup {
+    using ctk::ModuleGroup::ModuleGroup;
+
+    StatusGenerator sA{this, "sA", "Status 1", ctk::HierarchyModifier::hideThis, {"A"}};
+    StatusGenerator sAB{this, "sAB", "Status 2", ctk::HierarchyModifier::hideThis, {"A", "B"}};
+
+    ctk::StatusAggregator aggregateA{
+        this, "aggregateA", "aggregated status description", ctk::StatusAggregator::PriorityMode::fwko, {"A"}};
+    ctk::StatusAggregator aggregateB{this, "aggregateB", "aggregated status description",
+        ctk::StatusAggregator::PriorityMode::fwko, {"B"}, {"A"}}; // the "A" tag should be ignored by other aggregators
 
+  } group{this, "Group", ""};
+
+  // Use other priority mode here to make sure only the aggregators are aggregated, not the generators
+  ctk::StatusAggregator aggregateA{
+      this, "aggregateA", "aggregated status description", ctk::StatusAggregator::PriorityMode::ofwk, {"A"}};
+  ctk::StatusAggregator aggregateB{
+      this, "aggregateB", "aggregated status description", ctk::StatusAggregator::PriorityMode::ofwk, {"B"}};
+  ctk::StatusAggregator aggregateAll{
+      this, "aggregateAll", "aggregated status description", ctk::StatusAggregator::PriorityMode::fw_warn_mixed};
+};
+
+/**********************************************************************************************************************/
+
+BOOST_AUTO_TEST_CASE(testTags) {
+  std::cout << "testTags" << std::endl;
+
+  TestApplicationTags app;
   ctk::TestFacility test;
 
-  auto innerNoneInput = test.getScalar<uint8_t>("/outerModuleGroup1/innerModuleGroup/watch");
-  auto innerHideThisInput = test.getScalar<uint8_t>("/outerModuleGroup1/watch");
-  auto innerStateMonitorOneUp = test.getScalar<uint8_t>("/outerModuleGroup1/watch");
-  auto outerInput = test.getScalar<uint8_t>("/outerModuleGroup1/watch");
-  auto outerAggregated = test.getScalar<int32_t>("/outerModuleGroup1/outerStatusAggregator/groupStatus");
+  auto aggregateA = test.getScalar<int>("/aggregateA");
+  auto aggregateB = test.getScalar<int>("/aggregateB");
+  auto aggregateAll = test.getScalar<int>("/aggregateAll");
+  auto Group_aggregateA = test.getScalar<int>("/Group/aggregateA");
+  auto Group_aggregateB = test.getScalar<int>("/Group/aggregateB");
+
+  // write initial values
+  app.group.sA.status = ctk::StatusOutput::Status::WARNING;
+  app.group.sA.status.write();
+  app.group.sAB.status = ctk::StatusOutput::Status::OFF;
+  app.group.sAB.status.write();
 
   test.runApplication();
 
-  // FIXME test initial value propagation
+  // check initial values
+  aggregateA.readLatest();
+  aggregateB.readLatest();
+  aggregateAll.readLatest();
+  Group_aggregateA.readLatest();
+  Group_aggregateB.readLatest();
+  BOOST_CHECK_EQUAL(int(aggregateA), int(ctk::StatusOutput::Status::WARNING));
+  BOOST_CHECK_EQUAL(int(aggregateB), int(ctk::StatusOutput::Status::OFF));
+  BOOST_CHECK_EQUAL(int(aggregateAll), int(ctk::StatusOutput::Status::WARNING));
+  BOOST_CHECK_EQUAL(int(Group_aggregateA), int(ctk::StatusOutput::Status::WARNING));
+  BOOST_CHECK_EQUAL(int(Group_aggregateB), int(ctk::StatusOutput::Status::OFF));
+
+  app.group.sAB.status = ctk::StatusOutput::Status::FAULT;
+  app.group.sAB.status.write();
 
-  innerNoneInput = 1;
-  innerNoneInput.write();
   test.stepApplication();
 
-  //BOOST_CHECK(outerAggregated.readLatest() == true);
-  //BOOST_CHECK_EQUAL(int(outerAggregated), 0);
+  // change value tagged with 'A' and 'B', affecting all aggregators to highest priority value, so it is visible
+  // everywhere
+  aggregateA.readLatest();
+  aggregateB.readLatest();
+  aggregateAll.readLatest();
+  Group_aggregateA.readLatest();
+  Group_aggregateB.readLatest();
+  BOOST_CHECK_EQUAL(int(aggregateA), int(ctk::StatusOutput::Status::FAULT));
+  BOOST_CHECK_EQUAL(int(aggregateB), int(ctk::StatusOutput::Status::FAULT));
+  BOOST_CHECK_EQUAL(int(aggregateAll), int(ctk::StatusOutput::Status::FAULT));
+  BOOST_CHECK_EQUAL(int(Group_aggregateA), int(ctk::StatusOutput::Status::FAULT));
+  BOOST_CHECK_EQUAL(int(Group_aggregateB), int(ctk::StatusOutput::Status::FAULT));
 }
+
+/**********************************************************************************************************************/
-- 
GitLab