Skip to content
Snippets Groups Projects
TestFacility.h 11.4 KiB
Newer Older
/*
 * TestFacility.h
 *
 *  Created on: Feb 17, 2017
 *      Author: Martin Hierholzer
 */

#ifndef CHIMERATK_TEST_FACILITY
#define CHIMERATK_TEST_FACILITY

#include <ChimeraTK/ControlSystemAdapter/ControlSystemPVManager.h>
#include <ChimeraTK/OneDRegisterAccessor.h>
#include <ChimeraTK/ScalarRegisterAccessor.h>
#include "DeviceModule.h"
  /** 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. */
    explicit TestFacility(bool enableTestableMode = true) {
      auto pvManagers = createPVManager();
      pvManager = pvManagers.first;
      Application::getInstance().setPVManager(pvManagers.second);
      if(enableTestableMode) Application::getInstance().enableTestableMode();
      Application::getInstance().initialise();
    void runApplication() const {
      Application::getInstance().testFacilityRunApplicationCalled = true;
      // 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!
          // Safety check against incorrect usage
          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.");
          }
          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.
            /// 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
            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.
      Application::getInstance().run();
      Application::registerThread("TestThread");
      Application::testableModeUnlock("waitDevicesToOpen");
      while(true) {
        boost::this_thread::yield();
        bool allOpened = true;
        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) {
      // receive all initial values for the control system variables
      if(Application::getInstance().isTestableModeEnabled()) {
        for(auto& pv : pvManager->getAllProcessVariables()) {
          if(!pv->isReadable()) continue;
          callForType(pv->getValueType(), [&](auto t) {
            typedef decltype(t) UserType;
            this->getArray<UserType>(pv->getName()).readNonBlocking();
          });
    /** 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
      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;
        boost::fusion::at_key<T>(accessorMap.table)[name] = deco;
        boost::fusion::at_key<T>(accessorMap.table)[name] = pv;
      }

      // return the accessor as stored in the cache
      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
      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;
        boost::fusion::at_key<T>(accessorMap.table)[name] = deco;
        boost::fusion::at_key<T>(accessorMap.table)[name] = pv;
      }

      // return the accessor as stored in the cache
      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;
    /** 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's avatar
Martin Christoph Hierholzer committed
      setArrayDefault(name, vv);
    }

    /** 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;

    // 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.
    template<typename UserType>
    using AccessorMap = std::map<std::string, boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>>;
    mutable ChimeraTK::TemplateUserTypeMap<AccessorMap> accessorMap;

    // 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 */