-
Martin Killenberg authored
testProcessVariableRecovery: waiting for device to be opened avoids race conditin with runtime error
Martin Killenberg authoredtestProcessVariableRecovery: waiting for device to be opened avoids race conditin with runtime error
testProcessVariableRecovery.cc 9.75 KiB
#define BOOST_TEST_MODULE testProcessVariableRecovery
#include <boost/test/included/unit_test.hpp>
#include "Application.h"
#include "ControlSystemModule.h"
#include "DeviceModule.h"
#include "TestFacility.h"
#include "check_timeout.h"
#include "ApplicationModule.h"
#include "ArrayAccessor.h"
#include "ConfigReader.h"
#include <ChimeraTK/ExceptionDummyBackend.h>
#include <ChimeraTK/Device.h>
#include <stdlib.h>
#include <regex>
#include <boost/thread/barrier.hpp>
using namespace boost::unit_test_framework;
namespace ctk = ChimeraTK;
static constexpr char deviceCDD[] = "(ExceptionDummy?map=test5.map)";
/* The test module is writing to the device. It is the "module under test".
* This is the one whose variables are to be recovered. It is not the place the the
* application first sees the exception.
*/
struct TestModule : public ctk::ApplicationModule {
TestModule(EntityOwner* owner, const std::string& name, const std::string& description,
ctk::HierarchyModifier hierarchyModifier = ctk::HierarchyModifier::none,
const std::unordered_set<std::string>& tags = {})
: ApplicationModule(owner, name, description, hierarchyModifier, tags), mainLoopStarted(2) {}
ctk::ScalarPushInput<int32_t> trigger{this, "trigger", "", "This is my trigger."};
ctk::ScalarOutput<int32_t> scalarOutput{this, "TO_DEV_SCALAR1", "", "Here I write a scalar"};
ctk::ArrayOutput<int32_t> arrayOutput{this, "TO_DEV_ARRAY1", "", 4, "Here I write an array"};
// 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 to make sure the initial value propagation is done.
// execute this right after the Application::run():
// app.testModule.mainLoopStarted.wait(); // make sure the module's mainLoop() is entered
boost::barrier mainLoopStarted;
void mainLoop() override {
mainLoopStarted.wait();
while(true) {
scalarOutput = int32_t(trigger);
scalarOutput.write();
for(uint i = 0; i < 4; i++) {
arrayOutput[i] = int32_t(trigger);
}
arrayOutput.write();
trigger.read(); // read the blocking variable at the end so the initial values are propagated
}
}
};
/* dummy application */
struct TestApplication : public ctk::Application {
TestApplication() : Application("testSuite") {}
~TestApplication() { shutdown(); }
void defineConnections() {} // the setup is done in the tests
ctk::ControlSystemModule cs;
ctk::DeviceModule dev{this, deviceCDD};
TestModule module{this, "TEST", "The test module"};
};
/* Test application for the specific case of wrinting to a read-only
* accessor. Provides an input to an ApplicationModule from a read-only
* accessor of the device. For the test, the accessor must not be routed
* through the control system, the illegal write would be catched by the
* ControlSystemAdapter, not by the ExceptionHandlingDecorator under test here.
*/
struct ReadOnlyTestApplication : public ctk::Application {
ReadOnlyTestApplication() : Application("ReadOnlytestApp") {}
~ReadOnlyTestApplication() { shutdown(); }
void defineConnections() {
dev["TEST"]("FROM_DEV_SCALAR2") >> module("FROM_DEV_SCALAR2");
findTag("CS").connectTo(cs);
}
ctk::ControlSystemModule cs;
ctk::DeviceModule dev{this, deviceCDD};
struct TestModule : public ctk::ApplicationModule {
using ctk::ApplicationModule::ApplicationModule;
ctk::ScalarPushInput<int> start{
this, "startTest", "", "This has to be written once, before writing to the device", {"CS"}};
ctk::ScalarPollInput<int32_t> scalarROInput{this, "FROM_DEV_SCALAR2", "", "Here I read from a scalar RO-register"};
void mainLoop() override {
// Just to have a blocking read, gives the test
// time to dumpConnections and explicitly trigger
// before terminating.
start.read();
scalarROInput = 42;
try {
scalarROInput.write();
BOOST_CHECK_MESSAGE(
false, "ReadOnlyTestApplication: Calling write() on input to read-only device register did not throw.");
}
catch(ChimeraTK::logic_error& e) {
const std::string exMsg{e.what()};
std::regex exceptionHandlingRegex{"^ChimeraTK::ExceptionhandlingDecorator:*"};
std::smatch exMatch;
BOOST_CHECK(std::regex_search(exMsg, exMatch, exceptionHandlingRegex));
}
}
} module{this, "READ_ONLY_TEST", "The test module"};
};
/*********************************************************************************************************************/
BOOST_AUTO_TEST_CASE(testWriteToReadOnly) {
std::cout << "testWriteToReadOnly" << std::endl;
ReadOnlyTestApplication app;
ctk::TestFacility test;
test.runApplication();
// Should trigger the blocking read in ReadOnlyTestApplication's
// ApplicationModule. It then writes to a read-only register of the device,
// which should throw. Check is done in the module's mainLoop. We can not check
// here, as the exception gets thrown in the thread of the module.
test.writeScalar("/READ_ONLY_TEST/startTest", 1);
}
/*********************************************************************************************************************/
BOOST_AUTO_TEST_CASE(testProcessVariableRecovery) {
std::cout << "testProcessVariableRecovery" << std::endl;
TestApplication app;
app.findTag(".*").connectTo(app.cs); // creates /TEST/TO_DEV_SCALAR1 and /TEST/TO/DEV/ARRAY1
// devices are not automatically connected (yet)
app.dev.connectTo(app.cs,
app.cs("deviceTrigger", typeid(int),
1)); // In TEST it connects to TO_DEV_SCALAR1 and TO_DEV_ARRAY1, and creates TO_DEV_SCALAR2, FROM_DEV1, FROM_DEV2, TO_DEV_AREA2, FROM_DEV_AREA1 and FROM_DEV_AREA2
// make a constant and connect to the device
auto constante = ctk::VariableNetworkNode::makeConstant(1, 44252, 1);
constante >> app.dev["CONSTANT"]("VAR32");
ctk::TestFacility test(false);
// Write initial values manually since we do not use the testable mode.
// Otherwise the main loops never start.
// initial value for the direct CS->DEV register
test.writeScalar("/TEST/TO_DEV_SCALAR2", 42);
std::vector<int32_t> array = {99, 99, 99, 99};
test.writeArray("/TEST/TO_DEV_ARRAY2", array);
// initial value for the trigger
test.writeScalar("/TEST/trigger", 0);
app.run();
app.module.mainLoopStarted.wait();
ctk::Device dummy;
dummy.open(deviceCDD);
// wait for the device to be opened successfully so the access to the dummy does not throw
// (as they use the same backend it now throws if there has been an exception somewhere else)
CHECK_EQUAL_TIMEOUT(test.readScalar<int32_t>(std::string("/Devices/") + deviceCDD + "/status"), 0, 10000);
//Check that the initial values are there.
//auto reg2 = dummy.getScalarRegisterAccessor<int32_t>("/TEST/TO_DEV_SCALAR2");
//CHECK_EQUAL_TIMEOUT([=]()mutable{reg2.readLatest(); return int32_t(reg2);},0,10000);
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/CONSTANT/VAR32"), 44252, 10000);
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/TEST/TO_DEV_SCALAR2"), 42, 10000);
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/TEST/TO_DEV_ARRAY2", 1, 0)[0], 99, 10000);
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/TEST/TO_DEV_ARRAY2", 1, 1)[0], 99, 10000);
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/TEST/TO_DEV_ARRAY2", 1, 2)[0], 99, 10000);
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/TEST/TO_DEV_ARRAY2", 1, 3)[0], 99, 10000);
//Update device register via application module.
auto trigger = test.getScalar<int32_t>("/TEST/trigger");
trigger = 100;
trigger.write();
//Check if the values are updated.
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/TEST/TO_DEV_SCALAR1"), 100, 10000);
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/TEST/TO_DEV_ARRAY1", 1, 0)[0], 100, 10000);
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/TEST/TO_DEV_ARRAY1", 1, 1)[0], 100, 10000);
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/TEST/TO_DEV_ARRAY1", 1, 2)[0], 100, 10000);
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/TEST/TO_DEV_ARRAY1", 1, 3)[0], 100, 10000);
auto dummyBackend =
boost::dynamic_pointer_cast<ctk::ExceptionDummy>(ctk::BackendFactory::getInstance().createBackend(deviceCDD));
//Set the device to throw.
dummyBackend->throwExceptionOpen = true;
//Set dummy registers to 0.
dummy.write<int32_t>("/CONSTANT/VAR32", 0);
dummy.write<int32_t>("/TEST/TO_DEV_SCALAR1", 0);
dummy.write<int32_t>("/TEST/TO_DEV_SCALAR2", 0);
array = {0, 0, 0, 0};
dummy.write("/TEST/TO_DEV_ARRAY1", array);
dummy.write("/TEST/TO_DEV_ARRAY2", array);
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/CONSTANT/VAR32"), 0, 10000);
dummyBackend->throwExceptionWrite = true;
dummyBackend->throwExceptionRead = true;
// Now we trigger the reading module. This should put the device into an error state
auto trigger2 = test.getScalar<int32_t>("/deviceTrigger");
trigger2.write();
//Verify that the device is in error state.
CHECK_EQUAL_TIMEOUT(test.readScalar<int32_t>(ctk::RegisterPath("/Devices") / deviceCDD / "status"), 1, 10000);
//Set device back to normal.
dummyBackend->throwExceptionWrite = false;
dummyBackend->throwExceptionRead = false;
dummyBackend->throwExceptionOpen = false;
//Verify if the device is ready.
CHECK_EQUAL_TIMEOUT(test.readScalar<int32_t>(ctk::RegisterPath("/Devices") / deviceCDD / "status"), 0, 10000);
//Device should have the correct values now. Notice that we did not trigger the writer module!
BOOST_CHECK_EQUAL(dummy.read<int32_t>("/TEST/TO_DEV_SCALAR2"), 42);
BOOST_CHECK((dummy.read<int32_t>("/TEST/TO_DEV_ARRAY2", 0) == std::vector<int32_t>{99, 99, 99, 99}));
BOOST_CHECK_EQUAL(dummy.read<int32_t>("/TEST/TO_DEV_SCALAR1"), 100);
BOOST_CHECK((dummy.read<int32_t>("/TEST/TO_DEV_ARRAY1", 0) == std::vector<int32_t>{100, 100, 100, 100}));
// check if the constant is written back after recovery
CHECK_EQUAL_TIMEOUT(dummy.read<int32_t>("/CONSTANT/VAR32"), 44252, 10000);
}