From 59ad261339fe89fccd20adba440ae5f5328b351a Mon Sep 17 00:00:00 2001
From: Jens Georg <jens.georg@desy.de>
Date: Fri, 30 Sep 2022 15:56:14 +0200
Subject: [PATCH] Fix testTrigger

---
 src/ConnectionMaker.cc                       |  10 +-
 tests/executables_src/testIllegalNetworks.cc |   5 -
 tests/executables_src/testTrigger.cc         | 706 ++++++++++++-------
 3 files changed, 442 insertions(+), 279 deletions(-)

diff --git a/src/ConnectionMaker.cc b/src/ConnectionMaker.cc
index 4438ed91..fb71410a 100644
--- a/src/ConnectionMaker.cc
+++ b/src/ConnectionMaker.cc
@@ -54,7 +54,7 @@ namespace ChimeraTK {
 
     if(not neededFeeder) {
       // Only add CS consumer if we did not previously add CS feeder, we will add one or the other, but never both
-      debug("  No CS feeder in network, creating additional ControlSystem consumer");
+      debug("  Network has a non-CS feeder, can create additional ControlSystem consumer");
       net.consumers.push_back(VariableNetworkNode(
           proxy.getFullyQualifiedPath(), {VariableDirection::consuming, false}, *net.valueType, net.valueLength));
     }
@@ -94,12 +94,12 @@ namespace ChimeraTK {
       debug("  Creating fixed implementation for feeder '", net.feeder.getName(), "'...");
 
       if(net.consumers.size() == 1 && !net.useExternalTrigger) {
-        debug("    One consumer, setting up direct connection without external trigger.");
+        debug("    One consumer without external trigger, creating direct connection");
         makeDirectConnectionForFeederWithImplementation(net);
       }
       else {
         // More than one consuming node
-        debug("    More than one consuming node, setting up FanOut");
+        debug("    More than one consuming node or having external trigger, setting up FanOut");
         makeFanOutConnectionForFeederWithImplementation(net, device, trigger);
       }
     }
@@ -623,8 +623,8 @@ namespace ChimeraTK {
           });
           break;
         case NodeType::TriggerReceiver:
-          // This cannot happen. In a network Application -> TriggerReceiver, the trigger
-          // collection code will always add a CS consumer, so there is never a 1:1 connection
+          // This cannot happen. In a network Application -> TriggerReceiver, the connectNetwork()
+          // code will always add a CS consumer, so there is never a 1:1 connection
           debug("       Node type is TriggerReceiver");
           assert(false);
           break;
diff --git a/tests/executables_src/testIllegalNetworks.cc b/tests/executables_src/testIllegalNetworks.cc
index e2e72e0b..85628b21 100644
--- a/tests/executables_src/testIllegalNetworks.cc
+++ b/tests/executables_src/testIllegalNetworks.cc
@@ -176,8 +176,3 @@ struct TestApplication6 : public ctk::Application {
     void mainLoop() override {}
   } testModule2{this, ".", ""};
 };
-
-BOOST_AUTO_TEST_CASE_TEMPLATE(testNoElements, T, TestTypes) {
-  TestApplication6<T> app;
-  BOOST_CHECK_THROW(ctk::TestFacility tf(app, false), ctk::logic_error);
-}
diff --git a/tests/executables_src/testTrigger.cc b/tests/executables_src/testTrigger.cc
index 9c30b345..35569e18 100644
--- a/tests/executables_src/testTrigger.cc
+++ b/tests/executables_src/testTrigger.cc
@@ -1,5 +1,7 @@
 // SPDX-FileCopyrightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de>
 // SPDX-License-Identifier: LGPL-3.0-or-later
+#include "VoidAccessor.h"
+
 #include <chrono>
 #include <future>
 
@@ -26,340 +28,503 @@
 using namespace boost::unit_test_framework;
 namespace ctk = ChimeraTK;
 
-constexpr char dummySdm[] = "sdm://./TestTransferGroupDummy=test.map";
+/**********************************************************************************************************************/
 
-// list of user types the accessors are tested with
-typedef boost::mpl::list<int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, float, double> test_types;
+// Application that has one polling consumer for a polling provider
+// It should work without any trigger
+struct TestApp1 : ctk::Application {
+  TestApp1() : ctk::Application("testApp1") {}
+  ~TestApp1() override { shutdown(); }
 
-/**********************************************************************************************************************/
+  struct : ctk::ApplicationModule {
+    using ctk::ApplicationModule::ApplicationModule;
 
-class TestTransferGroupDummy : public ChimeraTK::DummyBackend {
- public:
-  TestTransferGroupDummy(std::string mapFileName) : DummyBackend(mapFileName) {}
+    ctk::ScalarPollInput<int> readBack{this, "/MyModule/readBack", "unit", "description"};
+
+    // This is just here so that we do not need a trigger - otherwise it would be connected to a pushing CS consumer
+    // automatically which would require a trigger
+    ctk::ScalarPollInput<int> tests{this, "/Deeper/hierarchies/need/tests", "unit", "description"};
+
+    ctk::VoidInput finger{this, "/finger", "", ""};
+
+    void mainLoop() override {
+      while(true) {
+        readAll();
+      }
+    }
+  } someModule{this, ".", ""};
+
+  ctk::SetDMapFilePath path{"test.dmap"};
+  ctk::DeviceModule dev;
+};
 
-  static boost::shared_ptr<DeviceBackend> createInstance(
-      std::string, std::string, std::list<std::string> parameters, std::string) {
-    return boost::shared_ptr<DeviceBackend>(new TestTransferGroupDummy(parameters.front()));
+BOOST_AUTO_TEST_CASE(testDev2AppWithPollTrigger) {
+  // TestApp1 should work without specifying any trigger
+  {
+    TestApp1 app;
+    app.dev = {&app, "Dummy0"};
+    ChimeraTK::TestFacility tf{app};
+    auto finger = tf.getVoid("/finger");
+    auto rb = tf.getScalar<int>("/MyModule/readBack");
+
+    tf.runApplication();
+
+    ctk::Device dev("Dummy0");
+    dev.open();
+    dev.write("MyModule/actuator", 1);
+
+    BOOST_TEST(rb.readNonBlocking() == false);
+    finger.write();
+    tf.stepApplication();
+    BOOST_TEST(rb.readNonBlocking() == true);
+    BOOST_TEST(rb == 1);
+
+    dev.write("MyModule/actuator", 10);
+    finger.write();
+    tf.stepApplication();
+    BOOST_TEST(rb.readNonBlocking() == true);
+    BOOST_TEST(rb == 10);
   }
 
-  void read(uint64_t bar, uint64_t address, int32_t* data, size_t sizeInBytes) override {
-    last_bar = bar;
-    last_address = address;
-    last_sizeInBytes = sizeInBytes;
-    numberOfTransfers++;
-    DummyBackend::read(bar, address, data, sizeInBytes);
+  // TestApp1 should also work with any trigger, but the trigger should be ignored
+  {
+    TestApp1 app;
+    app.dev = {&app, "Dummy0", "/cs/tick"};
+    ChimeraTK::TestFacility tf{app};
+    auto tick = tf.getVoid("/cs/tick");
+    auto finger = tf.getVoid("/finger");
+    auto rb = tf.getScalar<int>("/MyModule/readBack");
+
+    tf.runApplication();
+
+    ctk::Device dev("Dummy0");
+    dev.open();
+    dev.write("MyModule/actuator", 2);
+
+    BOOST_TEST(rb.readNonBlocking() == false);
+    finger.write();
+    tf.stepApplication();
+    BOOST_TEST(rb.readNonBlocking() == true);
+    BOOST_TEST(rb == 2);
+
+    // Trigger device trigger - values should not change
+    tick.write();
+    tf.stepApplication();
+    BOOST_TEST(rb.readNonBlocking() == false);
+    BOOST_TEST(rb == 2);
+
+    dev.write("MyModule/actuator", 20);
+
+    // Trigger read-out of poll variables in main loop
+    finger.write();
+    tf.stepApplication();
+    BOOST_TEST(rb.readNonBlocking() == true);
+    BOOST_TEST(rb == 20);
+
+    // Trigger device trigger - values should not change
+    tick.write();
+    tf.stepApplication();
+    BOOST_TEST(rb.readNonBlocking() == false);
+    BOOST_TEST(rb == 20);
   }
+}
 
-  std::atomic<size_t> numberOfTransfers{0};
-  std::atomic<uint64_t> last_bar;
-  std::atomic<uint64_t> last_address;
-  std::atomic<size_t> last_sizeInBytes;
+/**********************************************************************************************************************/
+
+struct TestApp2 : ctk::Application {
+  TestApp2() : ctk::Application("testApp2") {}
+  ~TestApp2() override { shutdown(); }
+
+  struct : ctk::ApplicationModule {
+    using ctk::ApplicationModule::ApplicationModule;
+
+    ctk::ScalarPushInput<int> readBack{this, "/MyModule/readBack", "unit", "description"};
+
+    void mainLoop() override {
+      while(true) {
+        readAll();
+      }
+    }
+  } someModule{this, ".", ""};
+
+  ctk::SetDMapFilePath path{"test.dmap"};
+  ctk::DeviceModule dev;
 };
 
-/*********************************************************************************************************************/
-/* the ApplicationModule for the test is a template of the user type */
+// Device that requires trigger, the trigger is 1:1 put into the CS
+BOOST_AUTO_TEST_CASE(testDev2AppWithCsDirectTrigger) {
+  // TestApp2 should not work without specifying any trigger
+  {
+    TestApp2 app;
+    app.dev = {&app, "Dummy0"};
+    BOOST_CHECK_THROW(ChimeraTK::TestFacility(app, true), ChimeraTK::logic_error);
+  }
 
-template<typename T>
-struct TestModule : public ctk::ApplicationModule {
-  TestModule(ctk::ModuleGroup* owner, const std::string& name, const std::string& description,
-      const std::unordered_set<std::string>& tags = {})
-  : ApplicationModule(owner, name, description, tags), mainLoopStarted(2) {}
+  // TestApp2 also works with a trigger. If the trigger is triggered, no data transfer should happen
+  {
+    TestApp2 app;
+    app.dev = {&app, "Dummy0", "/cs/trigger"};
 
-  ctk::ScalarPushInput<T> consumingPush{this, "consumingPush", "MV/m", "Description"};
-  ctk::ScalarPushInput<T> consumingPush2{this, "consumingPush2", "MV/m", "Description"};
-  ctk::ScalarPushInput<T> consumingPush3{this, "consumingPush3", "MV/m", "Description"};
+    ChimeraTK::TestFacility tf{app, true};
+    auto tick = tf.getVoid("/cs/trigger");
+    auto rb = tf.getScalar<int>("/MyModule/readBack");
 
-  ctk::ScalarPollInput<T> consumingPoll{this, "consumingPoll", "MV/m", "Description"};
-  ctk::ScalarPollInput<T> consumingPoll2{this, "consumingPoll2", "MV/m", "Description"};
-  ctk::ScalarPollInput<T> consumingPoll3{this, "consumingPoll3", "MV/m", "Description"};
+    tf.runApplication();
 
-  ctk::ScalarOutput<T> theTrigger{this, "theTrigger", "MV/m", "Description"};
-  ctk::ScalarOutput<T> feedingToDevice{this, "feedingToDevice", "MV/m", "Description"};
+    ctk::Device dev("Dummy0");
+    dev.open();
+    dev.write("MyModule/actuator", 1);
 
-  // We do not use testable mode for this test, so we need this barrier to synchronise to the beginning of the
-  // mainLoop(). This is required since the mainLoopWrapper accesses the module variables before the start of the
-  // mainLoop.
-  // execute this right after the Application::run():
-  //   app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered
-  boost::barrier mainLoopStarted;
+    tick.write();
+    tf.stepApplication();
 
-  void prepare() override {
-    incrementDataFaultCounter(); // force data to be flagged as faulty
-    feedingToDevice = 13;        // the initial value
-    writeAll();
-    decrementDataFaultCounter(); // data validity depends on inputs
+    BOOST_TEST(rb.readNonBlocking() == true);
+    BOOST_TEST(rb == 1);
+
+    dev.write("MyModule/actuator", 12);
+    BOOST_TEST(rb.readNonBlocking() == false);
+    BOOST_TEST(rb == 1);
+
+    tick.write();
+    tf.stepApplication();
+    BOOST_TEST(rb.readNonBlocking() == true);
+    BOOST_TEST(rb == 12);
   }
+}
 
-  void mainLoop() override { mainLoopStarted.wait(); }
+/**********************************************************************************************************************/
+
+struct TestApp3 : ctk::Application {
+  TestApp3() : ctk::Application("testApp3") {}
+  ~TestApp3() override { shutdown(); }
+
+  struct : ctk::ApplicationModule {
+    using ctk::ApplicationModule::ApplicationModule;
+
+    ctk::VoidInput tick{this, "/cs/trigger", "unit", "description"};
+    ctk::ScalarOutput<int> tock{this, "/tock", "", ""};
+
+    void mainLoop() override {
+      tock = 0;
+      while(true) {
+        tock.write();
+        tock++;
+        readAll();
+      }
+    }
+  } tock{this, ".", ""};
+
+  struct : ctk::ApplicationModule {
+    using ctk::ApplicationModule::ApplicationModule;
+
+    ctk::ScalarPushInput<int> readBack{this, "/MyModule/readBack", "unit", "description"};
+    ctk::ScalarPollInput<int> tests{this, "/Deeper/hierarchies/need/tests", "unit", "description"};
+
+    void mainLoop() override {
+      while(true) {
+        readAll();
+      }
+    }
+  } someModule{this, ".", ""};
+
+  ctk::SetDMapFilePath path{"test.dmap"};
+  ctk::DeviceModule dev{this, "Dummy0", "/cs/trigger"};
 };
 
-/*********************************************************************************************************************/
-/* dummy application */
+// Device that requires trigger, the trigger is distributed in the Application as well
+BOOST_AUTO_TEST_CASE(testDev2AppWithCsDistributedTrigger) {
+  TestApp3 app;
 
-template<typename T>
-struct TestApplication : public ctk::Application {
-  TestApplication() : Application("testSuite") {
-    ChimeraTK::BackendFactory::getInstance().registerBackendType(
-        "TestTransferGroupDummy", "", &TestTransferGroupDummy::createInstance, CHIMERATK_DEVICEACCESS_VERSION);
-  }
-  ~TestApplication() { shutdown(); }
+  ChimeraTK::TestFacility tf{app, true};
+  auto tick = tf.getVoid("/cs/trigger");
+  auto tock = tf.getScalar<int>("/tock");
+  auto rb = tf.getScalar<int>("/MyModule/readBack");
 
-  TestModule<T> testModule{this, "testModule", "The test module"};
-  ctk::DeviceModule dev{this, "Dummy0"};
-  ctk::DeviceModule dev2{this, dummySdm};
-};
+  tf.runApplication();
 
-/*********************************************************************************************************************/
-/* test trigger by app variable when connecting a polled device register to an
- * app variable */
+  ctk::Device dev("Dummy0");
+  dev.open();
+  dev.write("MyModule/actuator", 1);
+
+  tick.write();
+  tf.stepApplication();
+
+  BOOST_TEST(tock.readNonBlocking() == true);
+  BOOST_TEST(tock == 1);
+  BOOST_TEST(rb.readNonBlocking() == true);
+  BOOST_TEST(rb == 1);
+
+  dev.write("MyModule/actuator", 12);
+  BOOST_TEST(tock.readNonBlocking() == false);
+  BOOST_TEST(tock == 1);
+  BOOST_TEST(rb.readNonBlocking() == false);
+  BOOST_TEST(rb == 1);
+
+  tick.write();
+  tf.stepApplication();
+  BOOST_TEST(tock.readNonBlocking() == true);
+  BOOST_TEST(tock == 2);
+  BOOST_TEST(rb.readNonBlocking() == true);
+  BOOST_TEST(rb == 12);
+}
 
-BOOST_AUTO_TEST_CASE_TEMPLATE(testTriggerDevToApp, T, test_types) {
-  std::cout << "***************************************************************"
-               "******************************************************"
-            << std::endl;
-  std::cout << "==> testTriggerDevToApp<" << typeid(T).name() << ">" << std::endl;
+/**********************************************************************************************************************/
 
-  ChimeraTK::BackendFactory::getInstance().setDMapFilePath("test.dmap");
+struct TestApp4 : ctk::Application {
+  TestApp4() : ctk::Application("testApp4") {}
+  ~TestApp4() override { shutdown(); }
 
-  TestApplication<T> app;
-  auto pvManagers = ctk::createPVManager();
-  app.setPVManager(pvManagers.second);
+  struct : ctk::ApplicationModule {
+    using ctk::ApplicationModule::ApplicationModule;
 
-  // app.testModule.feedingToDevice >> app.dev["MyModule"]("actuator");
+    ctk::ScalarPushInput<float> singed32{this, "/Device/signed32", "unit", "description"};
 
-  // app.dev["MyModule"]("readBack")[app.testModule.theTrigger] >> app.testModule.consumingPush;
-  app.initialise();
+    void mainLoop() override {
+      while(true) {
+        readAll();
+      }
+    }
+  } someOtherModule{this, ".", ""};
 
-  app.run();
-  app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered
+  struct : ctk::ApplicationModule {
+    using ctk::ApplicationModule::ApplicationModule;
 
-  // check for initial value. Should be there when entering the main loop.
-  BOOST_CHECK_EQUAL(static_cast<T>(app.testModule.consumingPush), 13);
+    ctk::ScalarPushInput<int> readBack{this, "/MyModule/readBack", "unit", "description"};
+    ctk::ScalarPollInput<int> tests{this, "/Deeper/hierarchies/need/tests", "unit", "description"};
 
-  // single theaded test
-  app.testModule.feedingToDevice = 42;
-  app.testModule.feedingToDevice.write();
-  BOOST_CHECK(app.testModule.consumingPush.readNonBlocking() == false);
-  BOOST_CHECK_EQUAL(static_cast<T>(app.testModule.consumingPush), 13);
-  app.testModule.theTrigger.write();
-  app.testModule.consumingPush.read();
-  BOOST_CHECK(app.testModule.consumingPush == 42);
+    void mainLoop() override {
+      while(true) {
+        readAll();
+      }
+    }
+  } someModule{this, ".", ""};
+
+  ctk::SetDMapFilePath path{"test.dmap"};
+  ctk::DeviceModule dev{this, "Dummy0", "/cs/trigger"};
+  ctk::DeviceModule dev2{this, "Dummy1Mapped", "/cs/trigger"};
+};
+
+// Two devices using the same trigger
+BOOST_AUTO_TEST_CASE(testDev2App1Trigger2Devices) {
+  TestApp4 app;
+
+  ChimeraTK::TestFacility tf{app, true};
+  auto tick = tf.getVoid("/cs/trigger");
+  auto f = tf.getScalar<float>("/Device/signed32");
+  auto rb = tf.getScalar<int>("/MyModule/readBack");
 
-  // launch read() on the consumer asynchronously and make sure it does not yet
-  // receive anything
-  auto futRead = std::async(std::launch::async, [&app] { app.testModule.consumingPush.read(); });
-  BOOST_CHECK(futRead.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout);
+  ctk::Device dev("Dummy0");
+  dev.open();
 
-  BOOST_CHECK(app.testModule.consumingPush == 42);
+  ctk::Device dev2("Dummy1");
+  dev2.open();
+  dev2.write("FixedPoint/value", 12.4);
 
-  // write to the feeder
-  app.testModule.feedingToDevice = 120;
-  app.testModule.feedingToDevice.write();
-  BOOST_CHECK(futRead.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout);
-  BOOST_CHECK(app.testModule.consumingPush == 42);
+  tf.runApplication();
 
-  // send trigger
-  app.testModule.theTrigger.write();
+  dev.write("MyModule/actuator", 1);
 
-  // check that the consumer now receives the just written value
-  BOOST_CHECK(futRead.wait_for(std::chrono::milliseconds(2000)) == std::future_status::ready);
-  BOOST_CHECK(app.testModule.consumingPush == 120);
+  BOOST_TEST(f.readNonBlocking() == false);
+  BOOST_TEST(rb.readNonBlocking() == false);
+
+  tick.write();
+  tf.stepApplication();
+  BOOST_TEST(f.readNonBlocking() == true);
+  BOOST_TEST(rb.readNonBlocking() == true);
+
+  BOOST_TEST((f - 12.4) < 0.01);
+  BOOST_TEST(rb == 1);
+
+  dev.write("MyModule/actuator", 2);
+  dev2.write("FixedPoint/value", 24.8);
+
+  BOOST_TEST(f.readNonBlocking() == false);
+  BOOST_TEST(rb.readNonBlocking() == false);
+
+  tick.write();
+  tf.stepApplication();
+  BOOST_TEST(f.readNonBlocking() == true);
+  BOOST_TEST(rb.readNonBlocking() == true);
+
+  BOOST_TEST((f - 24.8) < 0.001);
+  BOOST_TEST(rb == 2);
 }
 
-/*********************************************************************************************************************/
-/* test trigger by app variable when connecting a polled device register to
- * control system variable */
-#if 0
-BOOST_AUTO_TEST_CASE_TEMPLATE(testTriggerDevToCS, T, test_types) {
-  std::cout << "***************************************************************"
-               "******************************************************"
-            << std::endl;
-  std::cout << "==> testTriggerDevToCS<" << typeid(T).name() << ">" << std::endl;
+/**********************************************************************************************************************/
 
-  ChimeraTK::BackendFactory::getInstance().setDMapFilePath("test.dmap");
+struct TestApp5 : ctk::Application {
+  TestApp5() : ctk::Application("testApp5") { }
+  ~TestApp5() override { shutdown(); }
 
-  TestApplication<T> app;
+  struct : ctk::ApplicationModule {
+    using ctk::ApplicationModule::ApplicationModule;
 
-  auto pvManagers = ctk::createPVManager();
-  app.setPVManager(pvManagers.second);
+    ctk::VoidInput finger{this, "/finger", "", ""};
+    ctk::VoidOutput trigger{this, "/trigger", "", ""};
 
-  app.dev("/MyModule/readBack", typeid(T), 1)[app.testModule.theTrigger] >> app.cs("myCSVar");
+    void mainLoop() override {
+      while(true) {
+        readAll();
+        trigger.write();
+      }
+    }
+  } someModule{this, ".", ""};
 
-  ctk::Device dev("Dummy0");
-  dev.open();
-  dev.write("MyModule/actuator", 1); // write initial value
+  ctk::SetDMapFilePath path{"test.dmap"};
+  ctk::DeviceModule dev;
+};
 
-  app.initialise();
-  app.run();
-  app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered
+BOOST_AUTO_TEST_CASE(testDev2CSCsTrigger) {
+  TestApp5 app;
+  app.dev = {&app, "Dummy0", "/cs/trigger"};
 
-  auto myCSVar = pvManagers.first->getProcessArray<T>("/myCSVar");
+  ChimeraTK::TestFacility tf{app, true};
+  auto tick = tf.getVoid("/cs/trigger");
+  auto rb = tf.getScalar<int>("/MyModule/readBack");
 
-  // single theaded test only, since the receiving process scalar does not support blocking
-  myCSVar->read(); // read initial value
-  BOOST_CHECK_EQUAL(myCSVar->accessData(0), 1);
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  dev.write("MyModule/actuator", 42);
-  usleep(10000);
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  app.testModule.setCurrentVersionNumber({});
-  app.testModule.theTrigger.write();
-  myCSVar->read();
-  BOOST_CHECK_EQUAL(myCSVar->accessData(0), 42);
+  tf.runApplication();
 
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  dev.write("MyModule/actuator", 120);
-  usleep(10000);
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  app.testModule.theTrigger.write();
-  myCSVar->read();
-  BOOST_CHECK_EQUAL(myCSVar->accessData(0), 120);
+  ctk::Device dev("Dummy0");
+  dev.open();
+  dev.write("MyModule/actuator", 1);
 
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-}
+  tick.write();
+  tf.stepApplication();
 
-/*********************************************************************************************************************/
-/* test trigger by app variable when connecting a polled device register to
- * control system variable */
+  BOOST_TEST(rb.readNonBlocking() == true);
+  BOOST_TEST(rb == 1);
 
-BOOST_AUTO_TEST_CASE_TEMPLATE(testTriggerByCS, T, test_types) {
-  std::cout << "***************************************************************"
-               "******************************************************"
-            << std::endl;
-  std::cout << "==> testTriggerByCS<" << typeid(T).name() << ">" << std::endl;
+  dev.write("MyModule/actuator", 12);
+  BOOST_TEST(rb.readNonBlocking() == false);
+  BOOST_TEST(rb == 1);
 
-  ChimeraTK::BackendFactory::getInstance().setDMapFilePath("test.dmap");
+  tick.write();
+  tf.stepApplication();
+  BOOST_TEST(rb.readNonBlocking() == true);
+  BOOST_TEST(rb == 12);
+}
 
-  TestApplication<T> app;
+BOOST_AUTO_TEST_CASE(testDev2CSAppTrigger) {
+  TestApp5 app;
+  app.dev = {&app, "Dummy0", "/trigger"};
 
-  auto pvManagers = ctk::createPVManager();
-  app.setPVManager(pvManagers.second);
+  ChimeraTK::TestFacility tf{app, true};
+  auto tick = tf.getVoid("/finger");
+  auto rb = tf.getScalar<int>("/MyModule/readBack");
 
-  app.dev("/MyModule/readBack", typeid(T), 1)[app.cs("theTrigger", typeid(T), 1)] >> app.cs("myCSVar");
+  tf.runApplication();
 
   ctk::Device dev("Dummy0");
   dev.open();
-  dev.write("MyModule/actuator", 1); // write initial value
+  dev.write("MyModule/actuator", 1);
 
-  app.initialise();
-  app.run();
-  app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered
+  tick.write();
+  tf.stepApplication();
+
+  BOOST_TEST(rb.readNonBlocking() == true);
+  BOOST_TEST(rb == 1);
+
+  dev.write("MyModule/actuator", 12);
+  BOOST_TEST(rb.readNonBlocking() == false);
+  BOOST_TEST(rb == 1);
 
-  auto myCSVar = pvManagers.first->getProcessArray<T>("/myCSVar");
-  auto theTrigger = pvManagers.first->getProcessArray<T>("/theTrigger");
-
-  // Need to send the trigger once, since ApplicationCore expects all CS variables to be written once by the
-  // ControlSystemAdapter. We do not use the TestFacility here, so we have to do it ourself.
-  theTrigger->write();
-
-  // single theaded test only, since the receiving process scalar does not support blocking
-  myCSVar->read(); // read initial value
-  BOOST_CHECK_EQUAL(myCSVar->accessData(0), 1);
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  dev.write("MyModule/actuator", 42);
-  usleep(10000);
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  myCSVar->accessData(0) = 0;
-  theTrigger->write();
-  myCSVar->read();
-  BOOST_CHECK_EQUAL(myCSVar->accessData(0), 42);
-
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  dev.write("MyModule/actuator", 120);
-  usleep(10000);
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  myCSVar->accessData(0) = 0;
-  theTrigger->write();
-  myCSVar->read();
-  BOOST_CHECK_EQUAL(myCSVar->accessData(0), 120);
-
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
+  tick.write();
+  tf.stepApplication();
+  BOOST_TEST(rb.readNonBlocking() == true);
+  BOOST_TEST(rb == 12);
 }
 
+constexpr char dummySdm[] = "(TestTransferGroupDummy?map=test_readonly.map)";
+
+// list of user types the accessors are tested with
+typedef boost::mpl::list<int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, float, double> test_types;
+
+/**********************************************************************************************************************/
+
+class TestTransferGroupDummy : public ChimeraTK::DummyBackend {
+ public:
+  TestTransferGroupDummy(std::string mapFileName) : DummyBackend(mapFileName) {}
+
+  static boost::shared_ptr<DeviceBackend> createInstance(std::string, std::map<std::string, std::string> parameters) {
+    return boost::shared_ptr<DeviceBackend>(new TestTransferGroupDummy(parameters["map"]));
+  }
+
+  void read(uint64_t bar, uint64_t address, int32_t* data, size_t sizeInBytes) override {
+    last_bar = bar;
+    last_address = address;
+    last_sizeInBytes = sizeInBytes;
+    numberOfTransfers++;
+    DummyBackend::read(bar, address, data, sizeInBytes);
+  }
+
+  std::atomic<size_t> numberOfTransfers{0};
+  std::atomic<uint64_t> last_bar;
+  std::atomic<uint64_t> last_address;
+  std::atomic<size_t> last_sizeInBytes;
+};
+
 /*********************************************************************************************************************/
-/* test trigger by app variable through FanOut (i.e. trigger variable is also used else where) */
+/* the ApplicationModule for the test is a template of the user type */
 
-BOOST_AUTO_TEST_CASE_TEMPLATE(testTriggerByCSFanOut, T, test_types) {
-  std::cout << "***************************************************************"
-               "******************************************************"
-            << std::endl;
-  std::cout << "==> testTriggerByCS<" << typeid(T).name() << ">" << std::endl;
+struct TestModule : public ctk::ApplicationModule {
+  TestModule(ctk::ModuleGroup* owner, const std::string& name, const std::string& description,
+      const std::unordered_set<std::string>& tags = {})
+  : ApplicationModule(owner, name, description, tags), mainLoopStarted(2) {}
 
-  ChimeraTK::BackendFactory::getInstance().setDMapFilePath("test.dmap");
+  ctk::ScalarPushInput<int> consumingPush{this, "/REG1", "MV/m", "Description"};
+  ctk::ScalarPushInput<int> consumingPush2{this, "/REG2", "MV/m", "Description"};
+  ctk::ScalarPushInput<int> consumingPush3{this, "/REG3", "MV/m", "Description"};
 
-  TestApplication<T> app;
+  ctk::ScalarOutput<int> theTrigger{this, "theTrigger", "MV/m", "Description"};
 
-  auto pvManagers = ctk::createPVManager();
-  app.setPVManager(pvManagers.second);
+  // We do not use testable mode for this test, so we need this barrier to synchronise to the beginning of the
+  // mainLoop(). This is required since the mainLoopWrapper accesses the module variables before the start of the
+  // mainLoop.
+  // execute this right after the Application::run():
+  //   app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered
+  boost::barrier mainLoopStarted;
 
-  auto trigger = app.cs("theTrigger", typeid(T), 1);
-  trigger >> app.cs("theTriggerCopied");
-  app.dev("/MyModule/readBack", typeid(T), 1)[trigger] >> app.cs("myCSVar");
+  void prepare() override {
+    incrementDataFaultCounter(); // force data to be flagged as faulty
+    writeAll();
+    decrementDataFaultCounter(); // data validity depends on inputs
+  }
 
-  ctk::Device dev("Dummy0");
-  dev.open();
-  dev.write("MyModule/actuator", 1); // write initial value
+  void mainLoop() override {
+    std::cout << "Start of main loop" << std::endl;
+    mainLoopStarted.wait();
+    std::cout << "End of main loop" << std::endl;
+  }
+};
 
-  app.initialise();
+/*********************************************************************************************************************/
+/* dummy application */
 
-  app.run();
-  app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered
+struct TestApplication : public ctk::Application {
+  TestApplication() : Application("testSuite") {
+    ChimeraTK::BackendFactory::getInstance().registerBackendType(
+        "TestTransferGroupDummy", &TestTransferGroupDummy::createInstance);
+    dev2 = {this, dummySdm, "/testModule/theTrigger"};
+  }
+  ~TestApplication() override { shutdown(); }
+
+  TestModule testModule{this, "testModule", "The test module"};
+  ctk::DeviceModule dev2;
+};
 
-  auto myCSVar = pvManagers.first->getProcessArray<T>("/myCSVar");
-  auto theTrigger = pvManagers.first->getProcessArray<T>("/theTrigger");
-  auto theTriggerCopied = pvManagers.first->getProcessArray<T>("/theTriggerCopied");
-
-  // Need to send the trigger once, since ApplicationCore expects all CS variables to be written once by the
-  // ControlSystemAdapter. We do not use the TestFacility here, so we have to do it ourself.
-  theTrigger->accessData(0) = 2;
-  theTrigger->write();
-  theTriggerCopied->read();
-  BOOST_CHECK_EQUAL(theTriggerCopied->accessData(0), 2);
-
-  // single theaded test only, since the receiving process scalar does not support blocking
-  myCSVar->read(); // read initial value
-  BOOST_CHECK_EQUAL(myCSVar->accessData(0), 1);
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  dev.write("MyModule/actuator", 42);
-  usleep(10000);
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  myCSVar->accessData(0) = 0;
-  theTrigger->accessData(0) = 3;
-  theTrigger->write();
-  myCSVar->read();
-  BOOST_CHECK_EQUAL(myCSVar->accessData(0), 42);
-  theTriggerCopied->read();
-  BOOST_CHECK_EQUAL(theTriggerCopied->accessData(0), 3);
-
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  dev.write("MyModule/actuator", 120);
-  usleep(10000);
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  myCSVar->accessData(0) = 0;
-  theTrigger->accessData(0) = 4;
-  theTrigger->write();
-  myCSVar->read();
-  BOOST_CHECK_EQUAL(myCSVar->accessData(0), 120);
-  theTriggerCopied->read();
-  BOOST_CHECK_EQUAL(theTriggerCopied->accessData(0), 4);
-
-  BOOST_CHECK(myCSVar->readNonBlocking() == false);
-  BOOST_CHECK(theTriggerCopied->readNonBlocking() == false);
-}
-#endif
 /*********************************************************************************************************************/
 /* test that multiple variables triggered by the same source are put into the
  * same TransferGroup */
 
-BOOST_AUTO_TEST_CASE_TEMPLATE(testTriggerTransferGroup, T, test_types) {
+BOOST_AUTO_TEST_CASE(testTriggerTransferGroup) {
   std::cout << "***************************************************************"
                "******************************************************"
             << std::endl;
-  std::cout << "==> testTriggerTransferGroup<" << typeid(T).name() << ">" << std::endl;
+  std::cout << "==> testTriggerTransferGroup" << std::endl;
 
   ChimeraTK::BackendFactory::getInstance().setDMapFilePath("test.dmap");
 
-  TestApplication<T> app;
+  TestApplication app;
   auto pvManagers = ctk::createPVManager();
   app.setPVManager(pvManagers.second);
 
@@ -369,9 +534,6 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(testTriggerTransferGroup, T, test_types) {
       ChimeraTK::BackendFactory::getInstance().createBackend(dummySdm));
   BOOST_CHECK(backend != NULL);
 
-  /*app.dev2("/REG1")[app.testModule.theTrigger] >> app.testModule.consumingPush;
-  app.dev2("/REG2")[app.testModule.theTrigger] >> app.testModule.consumingPush2;
-  app.dev2("/REG3")[app.testModule.theTrigger] >> app.testModule.consumingPush3;*/
   app.initialise();
   app.run();
   app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered
@@ -380,19 +542,22 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(testTriggerTransferGroup, T, test_types) {
   app.testModule.consumingPush = 0;
   app.testModule.consumingPush2 = 0;
   app.testModule.consumingPush3 = 0;
-  dev.write("/REG1", 11);
-  dev.write("/REG2", 22);
-  dev.write("/REG3", 33);
+  dev.write("/REG1.DUMMY_WRITEABLE", 11);
+  dev.write("/REG2.DUMMY_WRITEABLE", 22);
+  dev.write("/REG3.DUMMY_WRITEABLE", 33);
 
-  // from the inital value transfer
+  // from the initial value transfer
   CHECK_TIMEOUT(backend->numberOfTransfers == 1, 10000);
 
   // trigger the transfer
   app.testModule.theTrigger.write();
   CHECK_TIMEOUT(backend->numberOfTransfers == 2, 10000);
-  BOOST_CHECK(backend->last_bar == 0);
-  BOOST_CHECK(backend->last_address == 0);
-  BOOST_CHECK(backend->last_sizeInBytes == 12);
+  BOOST_TEST(backend->last_bar == 0);
+  BOOST_TEST(backend->last_address == 0);
+
+  // We only explicitly connect the three registers in the app, but the connection code will also connect the other
+  // registers into the CS, hence we need to check for the full size
+  BOOST_TEST(backend->last_sizeInBytes == 32);
 
   // check result
   app.testModule.consumingPush.read();
@@ -403,16 +568,19 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(testTriggerTransferGroup, T, test_types) {
   BOOST_CHECK_EQUAL(app.testModule.consumingPush3, 33);
 
   // prepare a second transfer
-  dev.write("/REG1", 12);
-  dev.write("/REG2", 23);
-  dev.write("/REG3", 34);
+  dev.write("/REG1.DUMMY_WRITEABLE", 12);
+  dev.write("/REG2.DUMMY_WRITEABLE", 23);
+  dev.write("/REG3.DUMMY_WRITEABLE", 34);
 
   // trigger the transfer
   app.testModule.theTrigger.write();
   CHECK_TIMEOUT(backend->numberOfTransfers == 3, 10000);
-  BOOST_CHECK(backend->last_bar == 0);
-  BOOST_CHECK(backend->last_address == 0);
-  BOOST_CHECK(backend->last_sizeInBytes == 12);
+  BOOST_TEST(backend->last_bar == 0);
+  BOOST_TEST(backend->last_address == 0);
+
+  // We only explicitly connect the three registers in the app, but the connection code will also connect the other
+  // registers into the CS, hence we need to check for the full size
+  BOOST_TEST(backend->last_sizeInBytes == 32);
 
   // check result
   app.testModule.consumingPush.read();
-- 
GitLab