// SPDX-FileCopyrightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de> // SPDX-License-Identifier: LGPL-3.0-or-later #include "Application.h" #include "ApplicationModule.h" #include "ArrayAccessor.h" #include "ScalarAccessor.h" #include "TestFacility.h" #include <ChimeraTK/BackendFactory.h> #include <boost/mpl/list.hpp> #include <future> #define BOOST_NO_EXCEPTIONS #define BOOST_TEST_MODULE testAppModuleConnections #include <boost/test/included/unit_test.hpp> #undef BOOST_NO_EXCEPTIONS using namespace boost::unit_test_framework; namespace ctk = ChimeraTK; // 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; /*********************************************************************************************************************/ /* the ApplicationModule for the test is a template of the user type */ 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) {} ctk::ScalarOutput<T> feedingPush; ctk::ScalarPushInput<T> consumingPush; ctk::ScalarPushInput<T> consumingPush2; ctk::ScalarPushInput<T> consumingPush3; ctk::ScalarPollInput<T> consumingPoll; ctk::ArrayPushInput<T> consumingPushArray; ctk::ArrayOutput<T> feedingArray; ctk::ArrayOutput<T> feedingPseudoArray; // 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; void prepare() override { incrementDataFaultCounter(); // force all outputs to invalid writeAll(); // write initial values decrementDataFaultCounter(); // validity according to input validity } void mainLoop() override { mainLoopStarted.wait(); } }; /*********************************************************************************************************************/ /* dummy application */ template<typename T> struct TestApplication : public ctk::Application { TestApplication() : Application("testSuite") {} ~TestApplication() override { shutdown(); } void defineConnections() {} // the setup is done in the tests TestModule<T> testModule{this, "testModule", "The test module"}; }; /*********************************************************************************************************************/ /* test case for two scalar accessors in push mode */ BOOST_AUTO_TEST_CASE_TEMPLATE(testTwoScalarPushAccessors, T, test_types) { // FIXME: With the new scheme, there cannot be a 1:1 module connection any more, it will always be a network involving // the ControlSystem std::cout << "*** testTwoScalarPushAccessors<" << typeid(T).name() << ">" << std::endl; TestApplication<T> app; app.testModule.feedingPush = {&app.testModule, "testTwoScalarPushAccessors", "", ""}; app.testModule.consumingPush = {&app.testModule, "testTwoScalarPushAccessors", "", ""}; ctk::TestFacility tf{app, false}; tf.runApplication(); app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered // single threaded test app.testModule.consumingPush = 0; app.testModule.feedingPush = 42; BOOST_CHECK(app.testModule.consumingPush == 0); app.testModule.feedingPush.write(); BOOST_CHECK(app.testModule.consumingPush == 0); app.testModule.consumingPush.read(); BOOST_CHECK(app.testModule.consumingPush == 42); // 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); BOOST_CHECK(app.testModule.consumingPush == 42); // write to the feeder app.testModule.feedingPush = 120; app.testModule.feedingPush.write(); // 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); } /*********************************************************************************************************************/ /* test case for four scalar accessors in push mode: one feeder and three * consumers */ BOOST_AUTO_TEST_CASE_TEMPLATE(testFourScalarPushAccessors, T, test_types) { std::cout << "*** testFourScalarPushAccessors<" << typeid(T).name() << ">" << std::endl; TestApplication<T> app; app.testModule.consumingPush = {&app.testModule, "testFourScalarPushAccessors", "", ""}; app.testModule.consumingPush2 = {&app.testModule, "testFourScalarPushAccessors", "", ""}; app.testModule.feedingPush = {&app.testModule, "testFourScalarPushAccessors", "", ""}; app.testModule.consumingPush3 = {&app.testModule, "testFourScalarPushAccessors", "", ""}; ctk::TestFacility tf{app, false}; tf.runApplication(); app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered // single threaded test app.testModule.consumingPush = 0; app.testModule.consumingPush2 = 2; app.testModule.consumingPush3 = 3; app.testModule.feedingPush = 42; BOOST_CHECK(app.testModule.consumingPush == 0); BOOST_CHECK(app.testModule.consumingPush2 == 2); BOOST_CHECK(app.testModule.consumingPush3 == 3); app.testModule.feedingPush.write(); BOOST_CHECK(app.testModule.consumingPush == 0); BOOST_CHECK(app.testModule.consumingPush2 == 2); BOOST_CHECK(app.testModule.consumingPush3 == 3); app.testModule.consumingPush.read(); BOOST_CHECK(app.testModule.consumingPush == 42); BOOST_CHECK(app.testModule.consumingPush2 == 2); BOOST_CHECK(app.testModule.consumingPush3 == 3); app.testModule.consumingPush2.read(); BOOST_CHECK(app.testModule.consumingPush == 42); BOOST_CHECK(app.testModule.consumingPush2 == 42); BOOST_CHECK(app.testModule.consumingPush3 == 3); app.testModule.consumingPush3.read(); BOOST_CHECK(app.testModule.consumingPush == 42); BOOST_CHECK(app.testModule.consumingPush2 == 42); BOOST_CHECK(app.testModule.consumingPush3 == 42); // launch read() on the consumers asynchronously and make sure it does not yet // receive anything auto futRead = std::async(std::launch::async, [&app] { app.testModule.consumingPush.read(); }); auto futRead2 = std::async(std::launch::async, [&app] { app.testModule.consumingPush2.read(); }); auto futRead3 = std::async(std::launch::async, [&app] { app.testModule.consumingPush3.read(); }); BOOST_CHECK(futRead.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout); BOOST_CHECK(futRead2.wait_for(std::chrono::milliseconds(1)) == std::future_status::timeout); BOOST_CHECK(futRead3.wait_for(std::chrono::milliseconds(1)) == std::future_status::timeout); BOOST_CHECK(app.testModule.consumingPush == 42); BOOST_CHECK(app.testModule.consumingPush2 == 42); BOOST_CHECK(app.testModule.consumingPush3 == 42); // write to the feeder app.testModule.feedingPush = 120; app.testModule.feedingPush.write(); // check that the consumers now receive the just written value BOOST_CHECK(futRead.wait_for(std::chrono::milliseconds(2000)) == std::future_status::ready); BOOST_CHECK(futRead2.wait_for(std::chrono::milliseconds(2000)) == std::future_status::ready); BOOST_CHECK(futRead3.wait_for(std::chrono::milliseconds(2000)) == std::future_status::ready); BOOST_CHECK(app.testModule.consumingPush == 120); BOOST_CHECK(app.testModule.consumingPush2 == 120); BOOST_CHECK(app.testModule.consumingPush3 == 120); } /*********************************************************************************************************************/ /* test case for two scalar accessors, feeder in push mode and consumer in poll * mode */ BOOST_AUTO_TEST_CASE_TEMPLATE(testTwoScalarPushPollAccessors, T, test_types) { std::cout << "*** testTwoScalarPushPollAccessors<" << typeid(T).name() << ">" << std::endl; TestApplication<T> app; app.testModule.feedingPush = {&app.testModule, "testTwoScalarPushPollAccessors", "", ""}; app.testModule.consumingPoll = {&app.testModule, "testTwoScalarPushPollAccessors", "", ""}; ctk::TestFacility tf{app, false}; tf.runApplication(); app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered // single threaded test only, since read() does not block in this case app.testModule.consumingPoll = 0; app.testModule.feedingPush = 42; BOOST_CHECK(app.testModule.consumingPoll == 0); app.testModule.feedingPush.write(); BOOST_CHECK(app.testModule.consumingPoll == 0); app.testModule.consumingPoll.read(); BOOST_CHECK(app.testModule.consumingPoll == 42); app.testModule.consumingPoll.read(); BOOST_CHECK(app.testModule.consumingPoll == 42); app.testModule.consumingPoll.read(); BOOST_CHECK(app.testModule.consumingPoll == 42); app.testModule.feedingPush = 120; BOOST_CHECK(app.testModule.consumingPoll == 42); app.testModule.feedingPush.write(); BOOST_CHECK(app.testModule.consumingPoll == 42); app.testModule.consumingPoll.read(); BOOST_CHECK(app.testModule.consumingPoll == 120); app.testModule.consumingPoll.read(); BOOST_CHECK(app.testModule.consumingPoll == 120); app.testModule.consumingPoll.read(); BOOST_CHECK(app.testModule.consumingPoll == 120); } /*********************************************************************************************************************/ /* test case for two array accessors in push mode */ BOOST_AUTO_TEST_CASE_TEMPLATE(testTwoArrayAccessors, T, test_types) { std::cout << "*** testTwoArrayAccessors<" << typeid(T).name() << ">" << std::endl; TestApplication<T> app; // app.testModule.feedingArray >> app.testModule.consumingPushArray; app.testModule.feedingArray = {&app.testModule, "testFourScalarPushAccessors", "", 10, ""}; app.testModule.consumingPushArray = {&app.testModule, "testFourScalarPushAccessors", "", 10, ""}; ctk::TestFacility tf{app, false}; tf.runApplication(); app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered BOOST_CHECK(app.testModule.feedingArray.getNElements() == 10); BOOST_CHECK(app.testModule.consumingPushArray.getNElements() == 10); // single threaded test for(auto& val : app.testModule.consumingPushArray) val = 0; for(unsigned int i = 0; i < 10; ++i) app.testModule.feedingArray[i] = 99 + (T)i; for(auto& val : app.testModule.consumingPushArray) BOOST_CHECK(val == 0); app.testModule.feedingArray.write(); for(auto& val : app.testModule.consumingPushArray) BOOST_CHECK(val == 0); app.testModule.consumingPushArray.read(); for(unsigned int i = 0; i < 10; ++i) BOOST_CHECK(app.testModule.consumingPushArray[i] == 99 + (T)i); // 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.consumingPushArray.read(); }); BOOST_CHECK(futRead.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout); for(unsigned int i = 0; i < 10; ++i) BOOST_CHECK(app.testModule.consumingPushArray[i] == 99 + (T)i); // write to the feeder for(unsigned int i = 0; i < 10; ++i) app.testModule.feedingArray[i] = 42 - (T)i; app.testModule.feedingArray.write(); // check that the consumer now receives the just written value BOOST_CHECK(futRead.wait_for(std::chrono::milliseconds(2000)) == std::future_status::ready); for(unsigned int i = 0; i < 10; ++i) BOOST_CHECK(app.testModule.consumingPushArray[i] == 42 - (T)i); } /*********************************************************************************************************************/ /* test case for connecting array of length 1 with scalar */ BOOST_AUTO_TEST_CASE_TEMPLATE(testPseudoArray, T, test_types) { std::cout << "*** testPseudoArray<" << typeid(T).name() << ">" << std::endl; TestApplication<T> app; // app.testModule.feedingPseudoArray >> app.testModule.consumingPush; app.testModule.feedingPseudoArray = {&app.testModule, "testPseudoArray", "", 1, ""}; app.testModule.consumingPush = {&app.testModule, "testPseudoArray", "", ""}; // run the app ctk::TestFacility tf{app, false}; tf.runApplication(); app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered // test data transfer app.testModule.feedingPseudoArray[0] = 33; app.testModule.feedingPseudoArray.write(); app.testModule.consumingPush.read(); BOOST_CHECK(app.testModule.consumingPush == 33); } /*********************************************************************************************************************/ /* test case for EntityOwner::constant() */ BOOST_AUTO_TEST_CASE_TEMPLATE(testConstants, T, test_types) { std::cout << "*** testConstants<" << typeid(T).name() << ">" << std::endl; TestApplication<T> app; app.testModule.consumingPush = {&app.testModule, app.testModule.constant(T(66)), "", ""}; app.testModule.consumingPoll = {&app.testModule, app.testModule.constant(T(77)), "", ""}; // test a second accessor of a different type but defining the constant with the same type as before ctk::ScalarPollInput<std::string> myStringConstant{&app.testModule, app.testModule.constant(T(66)), "", ""}; ctk::TestFacility tf{app, false}; tf.runApplication(); app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered BOOST_TEST(app.testModule.consumingPush == 66); BOOST_TEST(app.testModule.consumingPoll == 77); BOOST_TEST(boost::starts_with(std::string(myStringConstant), "66")); // might be 66 or 66.000000 BOOST_TEST(app.testModule.consumingPush.readNonBlocking() == false); app.testModule.consumingPoll = 0; app.testModule.consumingPoll.read(); BOOST_TEST(app.testModule.consumingPoll == 77); } /*********************************************************************************************************************/