diff --git a/src/ConnectionMaker.cc b/src/ConnectionMaker.cc index 4438ed919531e6de85cf7ad92d221cbcaebfb48b..fb71410a1713cbfafbfaf7cf64d10e2cb7647161 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 e2e72e0b0ad4eab8f54b3cfa646610f9e19d81a6..85628b21e84eb6bc4a38d759e25e162222a31970 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 9c30b3453400dd7d6f616de2d0e72a9bf0aee5f6..35569e1833ba5c81b21e340b3cd7365a915ae5ec 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();