Skip to content
Snippets Groups Projects
Commit 52c98b05 authored by Martin Christoph Hierholzer's avatar Martin Christoph Hierholzer
Browse files

implement the "testable mode" in which the application can be paused and...

implement the "testable mode" in which the application can be paused and resumed by the test routine
parent 5857a09c
No related branches found
No related tags found
No related merge requests found
......@@ -29,6 +29,9 @@ namespace ChimeraTK {
template<typename UserType>
class Accessor;
template<typename UserType>
class TestDecoratorRegisterAccessor;
class Application : public ApplicationBase, public EntityOwner {
public:
......@@ -65,8 +68,34 @@ namespace ChimeraTK {
* created by the control system adapter, or if the instance is not based on the Application class. */
static Application& getInstance();
/** Enable the testable mode. TODO @todo add further documentation */
/** Enable the testable mode. This allows to pause and resume the application for testing purposes using the
* functions pauseApplication() and resumeApplication(). The application will start in paused state.
*
* This function must be called before the application is initialised (i.e. before the call to initialise()).
*
* Note: Enabling the testable mode will have a singificant impact on the performance, since it will prevent
* any module threads to run at the same time! */
void enableTestableMode() { testableMode = true; }
/** Lock the mutex for the testable mode, which prevents any application thread from running. This works only if
* enableTestableMode() was called before. */
void pauseApplication() { assert(testableMode); testableMode_lock.lock(); }
/** Unlock the mutex for the testable mode, which allows the application threads to run again. This works only if
* enableTestableMode() was called before. */
void resumeApplication() { assert(testableMode); testableMode_lock.unlock(); }
/** Resume the application until all application threads are stuck in a blocking read operation. Must be called
* while the application is paused. */
void stepApplication() {
while(true) {
testableMode_counter = 0;
resumeApplication();
usleep(100);
pauseApplication();
if(testableMode_counter == 0) break;
}
}
protected:
......@@ -143,6 +172,22 @@ namespace ChimeraTK {
/** Flag if connections should be made in testable mode (i.e. the TestDecoratorRegisterAccessor is put around all
* push-type input accessors etc.). */
bool testableMode{false};
/** Mutex used in testable mode to take control over the application threads. */
std::mutex testableMode_mutex;
/** The lock used to lock the testableMode_mutex. */
std::unique_lock<std::mutex> testableMode_lock{testableMode_mutex};
/** Counter used in testable mode to check if application code was executed after releasing the testableMode_mutex.
* This value may only be accessed while holding the testableMode_mutex. */
size_t testableMode_counter{0};
template<typename UserType>
friend class TestDecoratorRegisterAccessor; // needs access to the testableMode_mutex and testableMode_counter
template<typename UserType>
friend class TestDecoratorTransferFuture; // needs access to the testableMode_mutex and testableMode_counter
};
......
......@@ -10,8 +10,51 @@
#include <mtca4u/NDRegisterAccessor.h>
#include "Application.h"
namespace ChimeraTK {
template<typename UserType>
class TestDecoratorRegisterAccessor;
/** Altered version of the TransferFuture since we need to deal with the lock in the wait() function. */
template<typename UserType>
class TestDecoratorTransferFuture : public TransferFuture {
public:
TestDecoratorTransferFuture() : _originalFuture{nullptr} {}
TestDecoratorTransferFuture(TransferFuture &originalFuture,
boost::shared_ptr<TestDecoratorRegisterAccessor<UserType>> accessor)
: _originalFuture(&originalFuture), _accessor(accessor)
{
TransferFuture::_theFuture = _originalFuture->getBoostFuture();
TransferFuture::_transferElement = &(_originalFuture->getTransferElement());
}
void wait() override {
if(!_accessor->firstReadTransfer) {
_accessor->application_lock.unlock();
}
else {
_accessor->firstReadTransfer = false;
}
_originalFuture->wait();
_accessor->postRead();
_accessor->hasActiveFuture = false;
_accessor->application_lock.lock();
++Application::getInstance().testableMode_counter;
}
protected:
TransferFuture *_originalFuture;
boost::shared_ptr<TestDecoratorRegisterAccessor<UserType>> _accessor;
};
/*******************************************************************************************************************/
/** Decorator of the NDRegisterAccessor which facilitates tests of the application */
template<typename UserType>
class TestDecoratorRegisterAccessor : public mtca4u::NDRegisterAccessor<UserType> {
......@@ -21,39 +64,49 @@ namespace ChimeraTK {
_accessor(accessor) {
buffer_2D.resize(_accessor->getNumberOfChannels());
for(size_t i=0; i<_accessor->getNumberOfChannels(); ++i) buffer_2D[i] = _accessor->accessChannel(i);
}
mtca4u::TransferFuture readAsync() override {
return _accessor->readAsync();
assert(_accessor->isReadOnly());
application_lock = std::unique_lock<std::mutex>(Application::getInstance().testableMode_mutex, std::defer_lock);
}
void write() override {
for(size_t i=0; i<_accessor->getNumberOfChannels(); ++i) buffer_2D[i].swap(_accessor->accessChannel(i));
_accessor->write();
for(size_t i=0; i<_accessor->getNumberOfChannels(); ++i) buffer_2D[i].swap(_accessor->accessChannel(i));
throw std::logic_error("Write operation called on read-only variable.");
}
void doReadTransfer() override {
if(!firstReadTransfer) {
application_lock.unlock();
}
else {
firstReadTransfer = false;
}
_accessor->doReadTransfer();
application_lock.lock();
++Application::getInstance().testableMode_counter;
}
bool doReadTransferNonBlocking() override {
return _accessor->doReadTransferNonBlocking();
}
TransferFuture& readAsync() override {
auto &future = _accessor->readAsync();
auto sharedThis = boost::static_pointer_cast<TestDecoratorRegisterAccessor<UserType>>(this->shared_from_this());
TransferElement::hasActiveFuture = true;
activeTestDecoratorFuture = TestDecoratorTransferFuture<UserType>(future, sharedThis);
return activeTestDecoratorFuture;
}
void postRead() override {
_accessor->postRead();
if(!TransferElement::hasActiveFuture) _accessor->postRead();
for(size_t i=0; i<_accessor->getNumberOfChannels(); ++i) buffer_2D[i].swap(_accessor->accessChannel(i));
}
void preWrite() override {
for(size_t i=0; i<_accessor->getNumberOfChannels(); ++i) buffer_2D[i].swap(_accessor->accessChannel(i));
_accessor->preWrite();
throw std::logic_error("Write operation called on read-only variable.");
}
void postWrite() override {
_accessor->postWrite();
for(size_t i=0; i<_accessor->getNumberOfChannels(); ++i) buffer_2D[i].swap(_accessor->accessChannel(i));
throw std::logic_error("Write operation called on read-only variable.");
}
bool isSameRegister(const boost::shared_ptr<mtca4u::TransferElement const> &other) const override {
......@@ -89,6 +142,16 @@ namespace ChimeraTK {
using mtca4u::NDRegisterAccessor<UserType>::buffer_2D;
boost::shared_ptr<mtca4u::NDRegisterAccessor<UserType>> _accessor;
/** Lock used to lock the testableMode_mutex of the application */
std::unique_lock<std::mutex> application_lock;
/** Flag whether the call to doReadTransfer() is the first call after construction or not */
bool firstReadTransfer{true};
friend class TestDecoratorTransferFuture<UserType>;
TestDecoratorTransferFuture<UserType> activeTestDecoratorFuture;
};
} /* namespace ChimeraTK */
......
......@@ -14,6 +14,8 @@
#include <boost/test/test_case_template.hpp>
#include <boost/mpl/list.hpp>
#include <mtca4u/ExperimentalFeatures.h>
#include "Application.h"
#include "ScalarAccessor.h"
#include "ApplicationModule.h"
......@@ -42,16 +44,58 @@ typedef boost::mpl::list<int8_t,uint8_t,
float,double> test_types;
/*********************************************************************************************************************/
/* the ApplicationModule for the test is a template of the user type */
/* the NoLoopTestModule is a dummy test module with an empty mainLoop */
template<typename T>
struct NoLoopTestModule : public ctk::ApplicationModule {
NoLoopTestModule(ctk::EntityOwner *owner, const std::string &name) : ctk::ApplicationModule(owner,name) {}
ctk::ScalarPushInput<T> someInput{this, "someInput", "cm", "This is just some input for testing"};
ctk::ScalarOutput<T> someOutput{this, "someOutput", "cm", "Description"};
void mainLoop() {
}
};
/*********************************************************************************************************************/
/* the BlockingReadTestModule blockingly reads its input in the main loop and writes the result to its output */
template<typename T>
struct BlockingReadTestModule : public ctk::ApplicationModule {
BlockingReadTestModule(ctk::EntityOwner *owner, const std::string &name) : ctk::ApplicationModule(owner,name) {}
ctk::ScalarPushInput<T> someInput{this, "someInput", "cm", "This is just some input for testing"};
ctk::ScalarOutput<T> someOutput{this, "someOutput", "cm", "Description"};
void mainLoop() {
while(true) {
someInput.read();
T val = someInput;
someOutput = val;
someOutput.write();
}
}
};
/*********************************************************************************************************************/
/* the AsyncReadTestModule asynchronously reads its input in the main loop and writes the result to its output */
template<typename T>
struct TestModule : public ctk::ApplicationModule {
TestModule(ctk::EntityOwner *owner, const std::string &name) : ctk::ApplicationModule(owner,name) {}
struct AsyncReadTestModule : public ctk::ApplicationModule {
AsyncReadTestModule(ctk::EntityOwner *owner, const std::string &name) : ctk::ApplicationModule(owner,name) {}
ctk::ScalarPushInput<T> someInput{this, "someInput", "cm", "Description"};
ctk::ScalarPushInput<T> someInput{this, "someInput", "cm", "This is just some input for testing"};
ctk::ScalarOutput<T> someOutput{this, "someOutput", "cm", "Description"};
void mainLoop() {}
void mainLoop() {
while(true) {
auto &future = someInput.readAsync();
future.wait();
T val = someInput;
someOutput = val;
someOutput.write();
}
}
};
/*********************************************************************************************************************/
......@@ -59,13 +103,17 @@ struct TestModule : public ctk::ApplicationModule {
template<typename T>
struct TestApplication : public ctk::Application {
TestApplication() : Application("test application") {}
TestApplication() : Application("test application") {
ChimeraTK::ExperimentalFeatures::enable();
}
~TestApplication() { shutdown(); }
using Application::makeConnections; // we call makeConnections() manually in the tests to catch exceptions etc.
void defineConnections() {} // the setup is done in the tests
TestModule<T> testModule{this,"testModule"};
NoLoopTestModule<T> noLoopTestModule{this,"noLoopTestModule"};
BlockingReadTestModule<T> blockingReadTestModule{this,"blockingReadTestModule"};
AsyncReadTestModule<T> asyncReadTestModule{this,"asyncReadTestModule"};
};
/*********************************************************************************************************************/
......@@ -77,16 +125,18 @@ BOOST_AUTO_TEST_CASE_TEMPLATE( testNoDecorator, T, test_types ) {
TestApplication<T> app;
app.testModule.someOutput >> app.testModule.someInput;
app.noLoopTestModule.someOutput >> app.noLoopTestModule.someInput;
app.blockingReadTestModule.someOutput >> app.blockingReadTestModule.someInput; // just to avoid runtime warning
app.asyncReadTestModule.someOutput >> app.asyncReadTestModule.someInput; // just to avoid runtime warning
app.initialise();
app.run();
// check if we got the decorator for the input
auto hlinput = app.testModule.someInput.getHighLevelImplElement();
auto hlinput = app.noLoopTestModule.someInput.getHighLevelImplElement();
BOOST_CHECK( boost::dynamic_pointer_cast<ctk::TestDecoratorRegisterAccessor<T>>(hlinput) == nullptr );
// check that we did not get the decorator for the output
auto hloutput = app.testModule.someOutput.getHighLevelImplElement();
auto hloutput = app.noLoopTestModule.someOutput.getHighLevelImplElement();
BOOST_CHECK( boost::dynamic_pointer_cast<ctk::TestDecoratorRegisterAccessor<T>>(hloutput) == nullptr );
}
......@@ -100,17 +150,113 @@ BOOST_AUTO_TEST_CASE_TEMPLATE( testDecorator, T, test_types ) {
TestApplication<T> app;
app.testModule.someOutput >> app.testModule.someInput;
app.noLoopTestModule.someOutput >> app.noLoopTestModule.someInput;
app.blockingReadTestModule.someOutput >> app.blockingReadTestModule.someInput; // just to avoid runtime warning
app.asyncReadTestModule.someOutput >> app.asyncReadTestModule.someInput; // just to avoid runtime warning
app.enableTestableMode();
app.initialise();
app.run();
app.resumeApplication(); // don't take control in this test
// check if we got the decorator for the input
auto hlinput = app.testModule.someInput.getHighLevelImplElement();
auto hlinput = app.noLoopTestModule.someInput.getHighLevelImplElement();
BOOST_CHECK( boost::dynamic_pointer_cast<ctk::TestDecoratorRegisterAccessor<T>>(hlinput) != nullptr );
// check that we did not get the decorator for the output
auto hloutput = app.testModule.someOutput.getHighLevelImplElement();
auto hloutput = app.noLoopTestModule.someOutput.getHighLevelImplElement();
BOOST_CHECK( boost::dynamic_pointer_cast<ctk::TestDecoratorRegisterAccessor<T>>(hloutput) == nullptr );
// check meta-data
BOOST_CHECK(app.noLoopTestModule.someInput.isReadOnly());
BOOST_CHECK(app.noLoopTestModule.someInput.isReadable());
BOOST_CHECK(!app.noLoopTestModule.someInput.isWriteable());
// test non blocking read
app.noLoopTestModule.someInput = 41;
for(int i=0; i<5; ++i) {
app.noLoopTestModule.someOutput = 42+i;
bool ret = app.noLoopTestModule.someInput.readNonBlocking();
BOOST_CHECK(ret == false);
int val = app.noLoopTestModule.someInput;
BOOST_CHECK(val == 42+i-1);
app.noLoopTestModule.someOutput.write();
ret = app.noLoopTestModule.someInput.readNonBlocking();
BOOST_CHECK(ret == true);
val = app.noLoopTestModule.someInput;
BOOST_CHECK(val == 42+i);
}
bool ret = app.noLoopTestModule.someInput.readNonBlocking();
BOOST_CHECK(ret == false);
// test blocking read
for(int i=0; i<5; ++i) {
auto future = std::async(std::launch::async, [&app] { app.noLoopTestModule.someInput.read(); });
app.noLoopTestModule.someOutput = 120+i;
BOOST_CHECK(future.wait_for(std::chrono::milliseconds(10)) == std::future_status::timeout);
app.noLoopTestModule.someOutput.write();
BOOST_CHECK(future.wait_for(std::chrono::milliseconds(100)) == std::future_status::ready);
int val = app.noLoopTestModule.someInput;
BOOST_CHECK(val == 120+i);
}
}
/*********************************************************************************************************************/
/* test blocking read in test mode */
BOOST_AUTO_TEST_CASE_TEMPLATE( testBlockingRead, T, test_types ) {
std::cout << "*********************************************************************************************************************" << std::endl;
std::cout << "==> testBlockingRead<" << typeid(T).name() << ">" << std::endl;
TestApplication<T> app;
app.noLoopTestModule.someOutput >> app.blockingReadTestModule.someInput;
app.blockingReadTestModule.someOutput >> app.noLoopTestModule.someInput;
app.asyncReadTestModule.someOutput >> app.asyncReadTestModule.someInput; // just to avoid runtime warning
app.enableTestableMode();
app.initialise();
app.run();
// test blocking read when taking control in the test thread
for(int i=0; i<5; ++i) {
app.noLoopTestModule.someOutput = 120+i;
app.noLoopTestModule.someOutput.write();
usleep(10000);
BOOST_CHECK(app.noLoopTestModule.someInput.readNonBlocking() == false);
app.stepApplication();
CHECK_TIMEOUT(app.noLoopTestModule.someInput.readNonBlocking() == true, 200);
int val = app.noLoopTestModule.someInput;
BOOST_CHECK(val == 120+i);
}
}
/*********************************************************************************************************************/
/* test async read in test mode */
BOOST_AUTO_TEST_CASE_TEMPLATE( testAsyncRead, T, test_types ) {
std::cout << "*********************************************************************************************************************" << std::endl;
std::cout << "==> testAsyncRead<" << typeid(T).name() << ">" << std::endl;
TestApplication<T> app;
app.noLoopTestModule.someOutput >> app.asyncReadTestModule.someInput;
app.asyncReadTestModule.someOutput >> app.noLoopTestModule.someInput;
app.blockingReadTestModule.someOutput >> app.blockingReadTestModule.someInput; // just to avoid runtime warning
app.enableTestableMode();
app.initialise();
app.run();
// test blocking read when taking control in the test thread
for(int i=0; i<5; ++i) {
app.noLoopTestModule.someOutput = 120+i;
app.noLoopTestModule.someOutput.write();
usleep(10000);
BOOST_CHECK(app.noLoopTestModule.someInput.readNonBlocking() == false);
app.stepApplication();
CHECK_TIMEOUT(app.noLoopTestModule.someInput.readNonBlocking() == true, 200);
int val = app.noLoopTestModule.someInput;
BOOST_CHECK(val == 120+i);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment