diff --git a/doc/spec_exceptionHandling.dox b/doc/spec_exceptionHandling.dox index e6fe5889b92d9ef01683760b19338ebc83b10ee4..093cf355570f3c991d02c503e9be96e01300d773 100644 --- a/doc/spec_exceptionHandling.dox +++ b/doc/spec_exceptionHandling.dox @@ -40,7 +40,7 @@ namespace ChimeraTK { \subsection spec_exceptionHandling_behaviour_runtime_errors Runtime error handling - \anchor b_2 2. When a ChimeraTK::runtime_error has been received by the framework (thrown by a device register accessor): - - 2.1 The exception status is published as a process variable together with an error message. + - \anchor exceptionHandling_b_2_1 2.1 The exception status is published as a process variable together with an error message. [\ref testExceptionHandling_b_2_1 "T"] - 2.1.1 The variable \c Devices/\<alias\>/status contains a boolean flag whether the device is in an error state. - 2.1.2 The variable \c Devices/\<alias\>/message contains an error message, if the device is in an error state, or an empty string otherwise. - \anchor b_2_2 2.2 Read operations will propagate the DataValidity::faulty flag to the owning module / fan out (without changing the actual data value of the process variable): diff --git a/tests/executables_src/testExceptionHandling.cc b/tests/executables_src/testExceptionHandling.cc index 6eb4c20bec1a6a8fabce2b3457155590a028f83f..68bf7fc61882b6cfa95d6eca21b060fef32f08e6 100644 --- a/tests/executables_src/testExceptionHandling.cc +++ b/tests/executables_src/testExceptionHandling.cc @@ -1,3 +1,4 @@ +#include <cstring> #include <future> #define BOOST_TEST_MODULE testExceptionHandling @@ -21,6 +22,146 @@ using namespace boost::unit_test_framework; namespace ctk = ChimeraTK; +// TODO: extend/rename test application as required/ make new test applications as the exception +// handling cases are covered. +/**************************/ +struct Module : ctk::ApplicationModule { + using ctk::ApplicationModule::ApplicationModule; + + ctk::ScalarPollInput<int> fromDevice{this, "REG1", "", "", {"DEVICE"}}; + ctk::ScalarOutput<int> toCs{this, "o1", "", "", {"CS"}}; + + void mainLoop() override { + while(true) { + readAll(); + toCs = static_cast<int>(fromDevice); + writeAll(); + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); + } + } +}; + +struct DummyApplication : ctk::Application { + // +----------+ poll_type_variable +------+ + // |Module x<------------------------+Device| + // +----------+ +------+ + + constexpr static char const* ExceptionDummyCDD1 = "(ExceptionDummy:1?map=test.map)"; + DummyApplication() : Application("testFault") {} + ~DummyApplication() { shutdown(); } + + Module m{this, "", ""}; + + ctk::ControlSystemModule cs; + ctk::DeviceModule device{this, ExceptionDummyCDD1}; + + void defineConnections() override { + findTag("CS").connectTo(cs); + findTag("DEVICE").connectTo(device); + } +}; + +struct Fixture_noTestFacility { + Fixture_noTestFacility() + : deviceBackend(boost::dynamic_pointer_cast<ctk::ExceptionDummy>( + ChimeraTK::BackendFactory::getInstance().createBackend(DummyApplication::ExceptionDummyCDD1))) { + + deviceBackend->open(); + testFacitiy.runApplication(); + + // + // As a test precondition, the DeviceModule must have completed its startup procedure. The code + // block below is a workaround to ensure this. + // + // Code below depends on behavior of process variable 'status', where 'status' is initialized + // with 0 as default after which its written with: + // - 1 (before DeviceModule begins opening the device), + // - 0 (once DeviceModule successfully opens the device/device startup is complete). + // This sequence is similar for the associated 'message' variable; replace 0/1 values above + // with an empty/non-empty string. + // + // Thus checking for two writes on these variables is a resonable indication that device startup + // is done. (provided the assumption explained above holds). + /************************************************************************************************/ + auto status = + testFacitiy.getScalar<int>(ctk::RegisterPath("/Devices") / DummyApplication::ExceptionDummyCDD1 / "status"); + auto message = testFacitiy.getScalar<std::string>( + ctk::RegisterPath("/Devices") / DummyApplication::ExceptionDummyCDD1 / "message"); + + CHECK_TIMEOUT(status.readNonBlocking() == true, 100000); + CHECK_TIMEOUT(status.readNonBlocking() == true, 100000); + + CHECK_TIMEOUT(message.readNonBlocking() == true, 100000); + CHECK_TIMEOUT(message.readNonBlocking() == true, 100000); + /************************************************************************************************/ + } + ~Fixture_noTestFacility() { deviceBackend->throwExceptionRead = false; } + + ctk::TestFacility testFacitiy{false}; + boost::shared_ptr<ctk::ExceptionDummy> deviceBackend; + DummyApplication app; +}; +/* **********************************/ + +/* + * This test suite checks behavior on a device related runtime error. + */ +BOOST_AUTO_TEST_SUITE(checkRuntimeErrorHandling) + +/* + * Verify the framework creates fault indicator process variables for a device. + * + * These are mapped on the control system as: + * - /Devices/<device_alias or cdd>/status + * - /Devices/<device_alias or cdd>/message + * + * A runtime errror on <device_alias> changes status to 1, with a non empty message + * string. + * + * \anchor testExceptionHandling_b_2_1 \ref exceptionHandling_b_2_1 "B.2.1" + */ +BOOST_FIXTURE_TEST_CASE(testFaultReporting, Fixture_noTestFacility) { + auto status = + testFacitiy.getScalar<int>(ctk::RegisterPath("/Devices") / DummyApplication::ExceptionDummyCDD1 / "status"); + auto message = testFacitiy.getScalar<std::string>( + ctk::RegisterPath("/Devices") / DummyApplication::ExceptionDummyCDD1 / "message"); + + BOOST_CHECK_EQUAL(status, 0); + BOOST_CHECK_EQUAL(static_cast<std::string>(message), ""); + + deviceBackend->throwExceptionRead = true; + + CHECK_TIMEOUT(status.readNonBlocking() == true, 10000); + CHECK_TIMEOUT(message.readNonBlocking() == true, 10000); + BOOST_CHECK_EQUAL(status, 1); + BOOST_CHECK(static_cast<std::string>(message) != ""); + + deviceBackend->throwExceptionRead = false; + + CHECK_TIMEOUT(status.readNonBlocking() == true, 10000); + CHECK_TIMEOUT(message.readNonBlocking() == true, 10000); + BOOST_CHECK_EQUAL(status, 0); + BOOST_CHECK(static_cast<std::string>(message) == ""); +} + +BOOST_AUTO_TEST_CASE(testBlockingRead) { // wait_for_new_data + // how does the api of the accessor look like + //device1DummyBackend["m1"]("i3")[cs("trigger", typeid(int), 1)] >> cs("i3", typeid(int), 1); + // make one with wait_for_new_data + + // The framework decides the accessmode flags based on how the wiring looks like: + // + // we will have to make up wiring to get what we desire. +} + +BOOST_AUTO_TEST_CASE(testReadLatest) {} + +BOOST_AUTO_TEST_CASE(testReadNonBlocking) {} + +BOOST_AUTO_TEST_CASE(testWrite) {} + +BOOST_AUTO_TEST_SUITE_END() + constexpr char ExceptionDummyCDD1[] = "(ExceptionDummy:1?map=test3.map)"; constexpr char ExceptionDummyCDD2[] = "(ExceptionDummy:2?map=test3.map)"; constexpr char ExceptionDummyCDD3[] = "(ExceptionDummy:3?map=test4.map)";