Newer
Older
/*
* TestFacility.h
*
* Created on: Feb 17, 2017
* Author: Martin Hierholzer
*/
#ifndef CHIMERATK_TEST_FACILITY
#define CHIMERATK_TEST_FACILITY
Martin Christoph Hierholzer
committed
#include <boost/fusion/include/at_key.hpp>
#include <ChimeraTK/ControlSystemAdapter/ControlSystemPVManager.h>
#include <ChimeraTK/OneDRegisterAccessor.h>
#include <ChimeraTK/ScalarRegisterAccessor.h>
#include "Application.h"
#include "DeviceModule.h"
Martin Christoph Hierholzer
committed
#include "TestableModeAccessorDecorator.h"
namespace ChimeraTK {
/** Helper class to facilitate tests of applications based on ApplicationCore */
class TestFacility {
public:
/** The constructor will internally obtain the instance of the application, so
* the instance of the TestFacility must not be created before the application
* (i.e. usually not before the main() routine). The application will
* automatically be put into the testable mode and initialised. */
Martin Christoph Hierholzer
committed
explicit TestFacility(bool enableTestableMode = true) {
auto pvManagers = createPVManager();
pvManager = pvManagers.first;
Application::getInstance().setPVManager(pvManagers.second);
Martin Christoph Hierholzer
committed
if(enableTestableMode) Application::getInstance().enableTestableMode();
Application::getInstance().initialise();
Martin Christoph Hierholzer
committed
/** Start the application in testable mode. */
Application::getInstance().testFacilityRunApplicationCalled = true;
Martin Christoph Hierholzer
committed
// send default values for all control system variables
for(auto& pv : pvManager->getAllProcessVariables()) {
callForType(pv->getValueType(), [&pv, this](auto arg) {
// Applies only to writeable variables. @todo FIXME It should also NOT apply for application-to-controlsystem
// variables with a return channel, despite being writeable here!
Martin Christoph Hierholzer
committed
if(!pv->isWriteable()) return;
// Safety check against incorrect usage
Martin Christoph Hierholzer
committed
if(pv->getVersionNumber() != VersionNumber(nullptr)) {
throw ChimeraTK::logic_error("The variable '" + pv->getName() +
"' has been written before TestFacility::runApplication() was called. Instead use "
"TestFacility::setScalarDefault() resp. setArrayDefault() to set initial values.");
}
Martin Christoph Hierholzer
committed
typedef decltype(arg) T;
auto pv_casted = boost::dynamic_pointer_cast<NDRegisterAccessor<T>>(pv);
auto table = boost::fusion::at_key<T>(defaults.table);
// If default value has been stored, copy the default value to the PV.
Martin Christoph Hierholzer
committed
if(table.find(pv->getName()) != table.end()) {
/// Since pv_casted is the undecorated PV (lacking the TestableModeAccessorDecorator), we need to copy the
/// value also to the decorator. We still have to write through the undecorated PV, otherwise the tests are
/// stalled. @todo It is not understood why this happens!
/// Decorated accessors are stored in different maps for scalars are arrays...
if(pv_casted->getNumberOfSamples() == 1) { // scalar
auto accessor = this->getScalar<T>(pv->getName());
accessor = table.at(pv->getName())[0];
}
else { // array
auto accessor = this->getArray<T>(pv->getName());
accessor = table.at(pv->getName());
}
// copy value also to undecorated PV
Martin Christoph Hierholzer
committed
pv_casted->accessChannel(0) = table.at(pv->getName());
}
// Write the initial value. This must be done even if no default value has been stored, since it is expected
// by the application.
Martin Christoph Hierholzer
committed
pv_casted->write();
});
}
// start the application
Martin Christoph Hierholzer
committed
// set thread name
Application::registerThread("TestThread");
Martin Christoph Hierholzer
committed
// wait until all devices are opened
Application::testableModeUnlock("waitDevicesToOpen");
while(true) {
boost::this_thread::yield();
bool allOpened = true;
Martin Killenberg
committed
for(auto dm : Application::getInstance().deviceModuleMap) {
if(!dm.second->device.isOpened()) allOpened = false;
}
if(allOpened) break;
}
Application::testableModeLock("waitDevicesToOpen");
// make sure all initial values have been propagated when in testable mode
if(Application::getInstance().isTestableModeEnabled()) {
// call stepApplication() only in testable mode and only if the queues are not empty
if(Application::getInstance().testableMode_counter != 0 ||
Application::getInstance().testableMode_deviceInitialisationCounter != 0) {
stepApplication();
}
}
Martin Christoph Hierholzer
committed
// receive all initial values for the control system variables
Martin Christoph Hierholzer
committed
if(Application::getInstance().isTestableModeEnabled()) {
for(auto& pv : pvManager->getAllProcessVariables()) {
if(!pv->isReadable()) continue;
Martin Christoph Hierholzer
committed
callForType(pv->getValueType(), [&](auto t) {
typedef decltype(t) UserType;
this->getArray<UserType>(pv->getName()).readNonBlocking();
});
Martin Christoph Hierholzer
committed
}
Martin Christoph Hierholzer
committed
}
/** Perform a "step" of the application. This runs the application until all
* input provided to it has been processed and all application modules wait
* for new data in blocking read calls. This function returns only after the
* application has reached that stated and was paused again. After returning
* from this function, the result can be checked and new data can be provided
* to the application. The new data will not be
* processed until the next call to step(). */
void stepApplication(bool waitForDeviceInitialisation = true) const {
Application::getInstance().stepApplication(waitForDeviceInitialisation);
}
/** Obtain a scalar process variable from the application, which is published
* to the control system. */
template<typename T>
ChimeraTK::ScalarRegisterAccessor<T> getScalar(const ChimeraTK::RegisterPath& name) const {
// check for existing accessor in cache
Martin Christoph Hierholzer
committed
if(boost::fusion::at_key<T>(accessorMap.table).count(name) > 0) {
return boost::fusion::at_key<T>(accessorMap.table)[name];
}
// obtain accessor from ControlSystemPVManager
auto pv = pvManager->getProcessArray<T>(name);
if(pv == nullptr) {
throw ChimeraTK::logic_error("Process variable '" + name + "' does not exist.");
}
// obtain variable id from pvIdMap and transfer it to idMap (required by the
// TestableModeAccessorDecorator)
size_t varId = Application::getInstance().pvIdMap[pv->getUniqueId()];
// decorate with TestableModeAccessorDecorator if variable is sender and
// receiver is not poll-type, and store it in cache
if(pv->isWriteable() && !Application::getInstance().testableMode_isPollMode[varId]) {
auto deco = boost::make_shared<TestableModeAccessorDecorator<T>>(pv, false, true, varId, varId);
Application::getInstance().testableMode_names[varId] = "ControlSystem:" + name;
Martin Christoph Hierholzer
committed
boost::fusion::at_key<T>(accessorMap.table)[name] = deco;
Martin Christoph Hierholzer
committed
boost::fusion::at_key<T>(accessorMap.table)[name] = pv;
}
// return the accessor as stored in the cache
Martin Christoph Hierholzer
committed
return boost::fusion::at_key<T>(accessorMap.table)[name];
/** Obtain an array-type process variable from the application, which is
* published to the control system. */
template<typename T>
ChimeraTK::OneDRegisterAccessor<T> getArray(const ChimeraTK::RegisterPath& name) const {
// check for existing accessor in cache
Martin Christoph Hierholzer
committed
if(boost::fusion::at_key<T>(accessorMap.table).count(name) > 0) {
return boost::fusion::at_key<T>(accessorMap.table)[name];
}
// obtain accessor from ControlSystemPVManager
auto pv = pvManager->getProcessArray<T>(name);
if(pv == nullptr) {
throw ChimeraTK::logic_error("Process variable '" + name + "' does not exist.");
}
// obtain variable id from pvIdMap and transfer it to idMap (required by the
// TestableModeAccessorDecorator)
size_t varId = Application::getInstance().pvIdMap[pv->getUniqueId()];
// decorate with TestableModeAccessorDecorator if variable is sender and
// receiver is not poll-type, and store it in cache
if(pv->isWriteable() && !Application::getInstance().testableMode_isPollMode[varId]) {
auto deco = boost::make_shared<TestableModeAccessorDecorator<T>>(pv, false, true, varId, varId);
Application::getInstance().testableMode_names[varId] = "ControlSystem:" + name;
Martin Christoph Hierholzer
committed
boost::fusion::at_key<T>(accessorMap.table)[name] = deco;
Martin Christoph Hierholzer
committed
boost::fusion::at_key<T>(accessorMap.table)[name] = pv;
}
// return the accessor as stored in the cache
Martin Christoph Hierholzer
committed
return boost::fusion::at_key<T>(accessorMap.table)[name];
/** Convenience function to write a scalar process variable in a single call
*/
template<typename TYPE>
void writeScalar(const std::string& name, const TYPE value) {
auto acc = getScalar<TYPE>(name);
acc = value;
acc.write();
}
/** Convenience function to write an array process variable in a single call
*/
template<typename TYPE>
void writeArray(const std::string& name, const std::vector<TYPE>& value) {
auto acc = getArray<TYPE>(name);
acc = value;
acc.write();
/** Convenience function to read the latest value of a scalar process variable
* in a single call */
template<typename TYPE>
TYPE readScalar(const std::string& name) {
auto acc = getScalar<TYPE>(name);
acc.readLatest();
return acc;
/** Convenience function to read the latest value of an array process variable
* in a single call */
template<typename TYPE>
std::vector<TYPE> readArray(const std::string& name) {
auto acc = getArray<TYPE>(name);
acc.readLatest();
return acc;
Martin Christoph Hierholzer
committed
/** Set default value for scalar process variable. */
template<typename T>
void setScalarDefault(const ChimeraTK::RegisterPath& name, const T& value) {
std::vector<T> vv;
vv.push_back(value);
Martin Christoph Hierholzer
committed
}
/** Set default value for array process variable. */
template<typename T>
void setArrayDefault(const ChimeraTK::RegisterPath& name, const std::vector<T>& value) {
// check if PV exists
auto pv = pvManager->getProcessArray<T>(name);
if(pv == nullptr) {
throw ChimeraTK::logic_error("Process variable '" + name + "' does not exist.");
}
// store default value in map
boost::fusion::at_key<T>(defaults.table)[name] = value;
}
protected:
boost::shared_ptr<ControlSystemPVManager> pvManager;
Martin Christoph Hierholzer
committed
// Cache (possible decorated) accessors to avoid the need to create accessors multiple times. This would not work
// if the accessor is decorated, since the buffer would be lost and thus the current value could no longer be
// obtained. This has to be done in dependence of the user type. Since this is a cache and does not change the
// logical behaviour of the class, the maps are defined mutable.
Martin Christoph Hierholzer
committed
using AccessorMap = std::map<std::string, boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>>;
mutable ChimeraTK::TemplateUserTypeMap<AccessorMap> accessorMap;
Martin Christoph Hierholzer
committed
// default values for process variables
template<typename UserType>
using Defaults = std::map<std::string, std::vector<UserType>>;
ChimeraTK::TemplateUserTypeMap<Defaults> defaults;
} /* namespace ChimeraTK */
#endif /* CHIMERATK_TEST_FACILITY */