diff --git a/Framework/CMakeLists.txt b/Framework/CMakeLists.txt index e2dd27055..e659e344c 100644 --- a/Framework/CMakeLists.txt +++ b/Framework/CMakeLists.txt @@ -140,6 +140,9 @@ add_library(O2QualityControl src/QCInputsAdapters.cxx src/QCInputsFactory.cxx src/UserInputOutput.cxx + src/Actor.cxx + src/ActorHelpers.cxx + src/DataProcessorAdapter.cxx ) target_include_directories( @@ -270,12 +273,16 @@ endforeach() add_executable(o2-qc-test-core test/testActivity.cxx test/testActivityHelpers.cxx + test/testActorHelpers.cxx + test/testActorTraits.cxx + test/testActor.cxx test/testAggregatorInterface.cxx test/testAggregatorRunner.cxx test/testCheck.cxx test/testCheckInterface.cxx test/testCheckRunner.cxx test/testCustomParameters.cxx + test/testDataProcessorAdapter.cxx test/testDataHeaderHelpers.cxx test/testInfrastructureGenerator.cxx test/testMonitorObject.cxx @@ -295,6 +302,7 @@ add_executable(o2-qc-test-core test/testQualitiesToFlagCollectionConverter.cxx test/testQCInputs.cxx test/testUserInputOutput.cxx + test/testStringUtils.cxx ) set_property(TARGET o2-qc-test-core PROPERTY RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests) @@ -310,6 +318,7 @@ target_include_directories(o2-qc-test-core PRIVATE ${CMAKE_SOURCE_DIR}) target_include_directories(o2-qc-test-core PRIVATE $) set(TEST_SRCS + test/testActorCallbacks.cxx test/testDbFactory.cxx test/testPublisher.cxx test/testQcInfoLogger.cxx @@ -325,12 +334,12 @@ set(TEST_SRCS test/testWorkflow.cxx test/testRepoPathUtils.cxx test/testUserCodeInterface.cxx - test/testStringUtils.cxx test/testRunnerUtils.cxx test/testBookkeepingQualitySink.cxx ) set(TEST_ARGS + "-b --run" "" "" "" @@ -380,6 +389,8 @@ endforeach() target_include_directories(testCcdbDatabase PRIVATE $) +set_property(TEST testActorCallbacks PROPERTY TIMEOUT 30) +set_property(TEST testActorCallbacks PROPERTY LABELS slow) set_property(TEST testWorkflow PROPERTY TIMEOUT 40) set_property(TEST testWorkflow PROPERTY LABELS slow) set_property(TEST testCheckWorkflow PROPERTY TIMEOUT 50) diff --git a/Framework/include/QualityControl/Actor.h b/Framework/include/QualityControl/Actor.h new file mode 100644 index 000000000..741db58be --- /dev/null +++ b/Framework/include/QualityControl/Actor.h @@ -0,0 +1,374 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file Actor.h +/// \author Piotr Konopka +/// + +#ifndef ACTOR_H +#define ACTOR_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "QualityControl/ActorTraits.h" +#include "QualityControl/ActorHelpers.h" +#include "QualityControl/runnerUtils.h" +#include "QualityControl/ServicesConfig.h" + +namespace o2::monitoring +{ +class Monitoring; +} + +namespace o2::bkp +{ +enum class DplProcessType; +} + +namespace o2::quality_control::repository +{ +class DatabaseInterface; +} + +namespace o2::ccdb +{ +class CCDBManagerInstance; +} + +namespace o2::quality_control::core +{ + +class Bookkeeping; + +// impl contains anything we want to hide in the source file to avoid exposing headers +namespace impl +{ +std::shared_ptr initMonitoring(std::string_view url, std::string_view detector = ""); +void startMonitoring(monitoring::Monitoring&, int runNumber); + +void initBookkeeping(std::string_view url); +void startBookkeeping(int runNumber, std::string_view actorName, std::string_view detectorName, const o2::bkp::DplProcessType& processType, std::string_view args); +Bookkeeping& getBookkeeping(); + +std::shared_ptr initRepository(const std::unordered_map& config); + +void initCCDB(const std::string& url); +ccdb::CCDBManagerInstance& getCCDB(); + +void handleExceptions(std::string_view when, const std::function&); +} // namespace impl + +// Actor is a template base class for all QC Data Processors. It is supposed to bring their commonalities together, +// such as: service initialization, Data Processing Layer adoption, retrieving configuration and runtime parameters, +// interactions with controlling entities (DPL driver, AliECS, ODC). +// +// The design is based on CRTP (see the web for explanation), which allows us to: +// - avoid code repetition in implementing aforementioned commonalities +// - optionally perform certain actions depending on traits of an Actor specialization. +// CRTP, in contrast to dynamic inheritance, is also advertised to avoid performance impact due to vtable lookups. +// It is certainly a nice bonus in our case, but it was not the main motivation for CRTP-based approach. +// +// To allow for compile-time customization of centralized Actor features, we require each concrete Actor to implement +// an ActorTraits structure with certain parameters which is enforced with ValidActorTraits concept. +// The traits are separated from the main Actor class to improve readability and allow for shorter compilation times +// by allowing many helper functions avoid including Actor.h and a corresponding actor specialization. For additional +// savings on compilation time and clutter in code, we validate ActorTraits specializations with a concept only in +// Actor, but this could be revisited if proven wrong. +// +// To add a new QC Actor (please extend if something turns out to be missing): +// - define its ActorTraits +// - inherit Actor and implement it, e.g. class LateTaskRunner : public Actor +// - define a factory for the new Actor which uses DataProcessorAdapter to produce DataProcessorSpec +// - use the factory in relevant bits in InfrastructureGenerator +// - if the new Actor runs user code, one might need to add a *Spec structure and a corresponding reader in InfrastructureReader +// - add Actor-specific customizeInfrastructure to the rest in InfrastructureGenerator::customizeInfrastructure +// +// Next steps / ideas: +// - have a trait for CompletionPolicy, so that it is handled in one place, i.e. we don't have to add the same +// boiler-plate for almost all actors. + +template + requires ValidActorTraits> +class Actor +{ + private: + // internal helpers + using traits = ActorTraits; + + static consteval bool runsUserCode() { return actor_helpers::runsUserCode(); } + + template + static consteval bool requiresService() + { + return actor_helpers::requiresService(); + } + + // a trick to prevent bugs like "class TaskRunner : public Actor" + // by keeping the constructor private, thus allowing only CheckRunner to initialize Actor + // see https://www.fluentcpp.com/2017/05/12/curiously-recurring-template-pattern/ + friend ConcreteActor; + + private: + explicit Actor(const ServicesConfig& servicesConfig) + : mServicesConfig{ servicesConfig }, + mActivity{ servicesConfig.activity } + { + // compile-time (!) checks which can be performed only once ConcreteActor is a complete type, i.e. inside a function body + // given that we declare mandatory methods as deleted, the compilation would still fail later. + // this allows us to compile earlier and with clearer messages. + assertCorrectConcreteActor(); + } + + public: + void init(framework::InitContext& ictx) + { + impl::handleExceptions("process", [&] { + // we set the fallback activity. fields might get overwritten once runtime values become available + mActivity = mServicesConfig.activity; + + initServices(ictx); + initDplCallbacks(ictx); + + concreteActor().onInit(ictx); + }); + } + + void process(framework::ProcessingContext& ctx) + { + impl::handleExceptions("process", [&] { + concreteActor().onProcess(ctx); + }); + } + + protected: + // mandatory methods to be implemented by concrete actor + void onInit(framework::InitContext&) = delete; + void onProcess(framework::ProcessingContext&) = delete; + + // mandatory methods to be implemented by concrete actor if specific features are enabled + bool isCritical() const + requires(traits::sCriticality == Criticality::UserDefined) + = delete; + std::string_view getDetectorName() const + requires(traits::sDetectorSpecific) + = delete; + std::string_view getUserCodeName() const + requires(runsUserCode()) + = delete; + + // optional methods that can be implemented by concrete actor + void onStart(framework::ServiceRegistryRef services, const Activity& activity) {} + void onStop(framework::ServiceRegistryRef services, const Activity& activity) {} + void onReset(framework::ServiceRegistryRef services, const Activity& activity) {} + void onEndOfStream(framework::EndOfStreamContext& eosContext) {} + void onFinaliseCCDB(framework::ConcreteDataMatcher& matcher, void* obj) {} + + // service access for concrete actor + std::reference_wrapper getMonitoring() + requires(requiresService()) + { + return *mMonitoring; + } + std::reference_wrapper getBookkeeping() + requires(requiresService()) + { + return impl::getBookkeeping(); + } + std::reference_wrapper getRepository() + requires(requiresService()) + { + return *mRepository; + } + std::reference_wrapper getCCDB() + requires(requiresService()) + { + return impl::getCCDB(); + } + const Activity& getActivity() const + { + return mActivity; + } + + private: + static consteval void assertCorrectConcreteActor() + { + static_assert(std::derived_from>); + // mandatory methods + static_assert(requires(ConcreteActor& actor, framework::ProcessingContext& pCtx) { { actor.onProcess(pCtx) } -> std::convertible_to; }); + static_assert(requires(ConcreteActor& actor, framework::InitContext& iCtx) { { actor.onInit(iCtx) } -> std::convertible_to; }); + + // mandatory if specific features are enabled + if constexpr (traits::sDetectorSpecific) { + static_assert(requires(const ConcreteActor& actor) { { actor.getDetectorName() } -> std::convertible_to; }); + } + + if constexpr (traits::sCriticality == Criticality::UserDefined) { + static_assert(requires(const ConcreteActor& actor) { { actor.isCritical() } -> std::convertible_to; }); + } + + if constexpr (runsUserCode()) { + static_assert(requires(const ConcreteActor& actor) { { actor.getUserCodeName() } -> std::convertible_to; }); + } + } + + // helpers to avoid repeated static_casts to call ConcreteActor methods + ConcreteActor& concreteActor() { return static_cast(*this); } + const ConcreteActor& concreteActor() const { return static_cast(*this); } + + void initServices(framework::InitContext& ictx) + { + std::string detectorName; + if constexpr (traits::sDetectorSpecific) { + detectorName = std::string{ concreteActor().getDetectorName() }; + } + + if constexpr (requiresService()) { + std::string facility; + if constexpr (runsUserCode()) { + facility = std::format("{}/{}", traits::sActorTypeShort, concreteActor().getUserCodeName()); + } else { + facility = std::format("{}/", traits::sActorTypeShort); + } + + // todo now we use the version from runnerUtils, but the implementation could be moved to Actor.cxx once we migrate all actors + initInfologger(ictx, mServicesConfig.infologgerDiscardParameters, facility, detectorName); + } + if constexpr (requiresService()) { + mMonitoring = impl::initMonitoring(mServicesConfig.monitoringUrl, detectorName); + } + if constexpr (requiresService()) { + impl::initBookkeeping(mServicesConfig.bookkeepingUrl); + } + if constexpr (requiresService()) { + mRepository = impl::initRepository(mServicesConfig.database); + } + if constexpr (requiresService()) { + impl::initCCDB(mServicesConfig.conditionDBUrl); + } + } + + void initDplCallbacks(framework::InitContext& ictx) + { + try { + auto& callbacks = ictx.services().get(); + + // we steal services reference, because it is not available as an argument of these callbacks + framework::ServiceRegistryRef services = ictx.services(); + + callbacks.set([this, services]() { this->start(services); }); + callbacks.set([this, services]() { this->stop(services); }); + callbacks.set([this, services]() { this->reset(services); }); + callbacks.set( + [this](framework::EndOfStreamContext& eosContext) { this->endOfStream(eosContext); }); + callbacks.set( + [this](framework::ConcreteDataMatcher& matcher, void* obj) { this->finaliseCCDB(matcher, obj); }); + } catch (framework::RuntimeErrorRef& ref) { + ILOG(Fatal) << "Error during callback registration: " << framework::error_from_ref(ref).what << ENDM; + throw; + } + } + + void start(framework::ServiceRegistryRef services) + { + impl::handleExceptions("start", [&] { + ILOG(Debug, Trace) << traits::sActorTypeKebabCase << " start" << ENDM; + + mActivity = computeActivity(services, mActivity); + + if constexpr (requiresService()) { + QcInfoLogger::setRun(mActivity.mId); + QcInfoLogger::setPartition(mActivity.mPartitionName); + } + if constexpr (requiresService()) { + impl::startMonitoring(*mMonitoring, mActivity.mId); + } + if constexpr (requiresService()) { + std::string actorName; + if constexpr (runsUserCode()) { + actorName = concreteActor().getUserCodeName(); + } else { + actorName = traits::sActorTypeKebabCase; + } + + std::string detectorName; + if constexpr (traits::sDetectorSpecific) { + detectorName = concreteActor().getDetectorName(); + } + + // todo: get args + impl::startBookkeeping(mActivity.mId, actorName, detectorName, traits::sDplProcessType, ""); + } + + concreteActor().onStart(services, mActivity); + }); + } + + void stop(framework::ServiceRegistryRef services) + { + impl::handleExceptions("stop", [&] { + ILOG(Debug, Trace) << traits::sActorTypeKebabCase << " stop" << ENDM; + + mActivity = computeActivity(services, mActivity); + + concreteActor().onStop(services, mActivity); + }); + } + + void reset(framework::ServiceRegistryRef services) + { + impl::handleExceptions("reset", [&] { + ILOG(Debug, Trace) << traits::sActorTypeKebabCase << " reset" << ENDM; + + mActivity = mServicesConfig.activity; + + concreteActor().onReset(services, mActivity); + }); + } + + void endOfStream(framework::EndOfStreamContext& eosContext) + { + impl::handleExceptions("endOfStream", [&] { + ILOG(Debug, Trace) << traits::sActorTypeKebabCase << " endOfStream" << ENDM; + + concreteActor().onEndOfStream(eosContext); + }); + } + void finaliseCCDB(framework::ConcreteDataMatcher& matcher, void* obj) + { + impl::handleExceptions("finaliseCCDB", [&] { + ILOG(Debug, Trace) << traits::sActorTypeKebabCase << " finaliseCCDB" << ENDM; + + concreteActor().onFinaliseCCDB(matcher, obj); + }); + } + + private: + Activity mActivity; + const ServicesConfig mServicesConfig; + + std::shared_ptr mMonitoring; + std::shared_ptr mRepository; +}; + +} // namespace o2::quality_control::core +#endif // ACTOR_H \ No newline at end of file diff --git a/Framework/include/QualityControl/ActorHelpers.h b/Framework/include/QualityControl/ActorHelpers.h new file mode 100644 index 000000000..40a6bf683 --- /dev/null +++ b/Framework/include/QualityControl/ActorHelpers.h @@ -0,0 +1,83 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file ActorHelpers.h +/// \author Piotr Konopka +/// + +#ifndef ACTORFACTORY_H +#define ACTORFACTORY_H + +#include + +#include "QualityControl/ActorTraits.h" +#include "QualityControl/ServicesConfig.h" +#include "QualityControl/UserCodeConfig.h" +#include "QualityControl/UserCodeCardinality.h" +#include "QualityControl/Criticality.h" +#include "QualityControl/ServiceRequest.h" + +namespace o2::quality_control::core +{ + +struct CommonSpec; + +namespace actor_helpers +{ + +/// \brief extracts common services configuration from CommonSpec +ServicesConfig extractConfig(const CommonSpec& commonSpec); + +/// \brief checks if concrete Actor requests Service S +template +consteval bool requiresService() +{ + using traits = ActorTraits; + // todo: when we can use C++23: std::ranges::contains(ActorTraitsT::sRequiredServices, S); + for (const auto& required : traits::sRequiredServices) { + if (required == S) { + return true; + } + } + return false; +} + +/// \brief checks if an Actor is effectively a Runner as well, i.e. runs user code +template +constexpr bool runsUserCode() +{ + using traits = ActorTraits; + return traits::sUserCodeCardinality != UserCodeCardinality::None; +} + +/// \brief checks if an Actor is allowed to publish a given data source type +template +consteval bool publishesDataSource(DataSourceType dataSourceType) +{ + using traits = ActorTraits; + for (auto t : traits::sPublishedDataSources) { + if (t == dataSourceType) { + return true; + } + } + return false; +} + +/// \brief checks if an Actor is allowed to publish a given data source type +template +concept ValidDataSourceForActor = publishesDataSource(dataSourceType); + +} // namespace actor_helpers + +} // namespace o2::quality_control::core + +#endif // ACTORFACTORY_H diff --git a/Framework/include/QualityControl/ActorTraits.h b/Framework/include/QualityControl/ActorTraits.h new file mode 100644 index 000000000..5a31660aa --- /dev/null +++ b/Framework/include/QualityControl/ActorTraits.h @@ -0,0 +1,109 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file ActorTraits.h +/// \author Piotr Konopka +/// + +#ifndef ACTORTRAITS_H +#define ACTORTRAITS_H + +#include +#include + +#include +#include + +#include "QualityControl/DataSourceSpec.h" +#include "QualityControl/stringUtils.h" +#include "QualityControl/UserCodeCardinality.h" +#include "QualityControl/ServiceRequest.h" +#include "QualityControl/Criticality.h" + +// ActorTraits and their specializations should not include heavy dependencies. +// They should define the expected traits for each QC Actor and basic choices in behaviours. + +namespace o2::quality_control::core +{ + +// internal helpers for validating actor traits +namespace impl +{ +/// \brief checks if actor traits contain a request for Service S +template +consteval bool requiresService() +{ + // todo: when we can use C++23: std::ranges::contains(ActorTraitsT::sRequiredServices, S); + for (const auto& required : ActorTraitsT::sRequiredServices) { + if (required == S) { + return true; + } + } + return false; +} +} // namespace impl + +/// \brief Defines what are valid Actor traits +template +concept ValidActorTraits = requires { + // Concrete ActorTraits must have the following static constants: + + // names in different forms for use in registering the actor in different services, etc... + { ActorTraitsT::sActorTypeShort } -> std::convertible_to; + + { ActorTraitsT::sActorTypeKebabCase } -> std::convertible_to; + requires isKebabCase(ActorTraitsT::sActorTypeKebabCase); + + { ActorTraitsT::sActorTypeUpperCamelCase } -> std::convertible_to; + requires isUpperCamelCase(ActorTraitsT::sActorTypeUpperCamelCase); + + // supported inputs and outputs by a given actor + { ActorTraitsT::sConsumedDataSources } -> std::ranges::input_range; + requires std::convertible_to, DataSourceType>; + + { ActorTraitsT::sPublishedDataSources } -> std::ranges::input_range; + requires std::convertible_to, DataSourceType>; + + // a list of required services, Actor will take care of initializing them + { ActorTraitsT::sRequiredServices } -> std::ranges::input_range; + requires std::convertible_to, ServiceRequest>; + // for certain services, we require additional fields + requires( + impl::requiresService() + ? requires { { ActorTraitsT::sDplProcessType } -> std::convertible_to; } + : true); + + // we want to know if this Actor runs any user code. + // now it could be simplified to a bool, but maybe some future usage will need One/Many distinction. + { ActorTraitsT::sUserCodeCardinality } -> std::convertible_to; + + // do we normally associate this Actor with a specific detector (in the worst case, with "MANY" or "MISC")? + { ActorTraitsT::sDetectorSpecific } -> std::convertible_to; + + // specifies how an actor should be treated by a control system if it crashes + { ActorTraitsT::sCriticality } -> std::convertible_to; + + // used to create data description when provided strings are too long + { ActorTraitsT::sDataDescriptionHashLength } -> std::convertible_to; + requires(ActorTraitsT::sDataDescriptionHashLength <= o2::header::DataDescription::size); + + // todo: a constant to set how actor consumes inputs, i.e. how to customize CompletionPolicy +}; + +// this is a fallback struct which is activated only if a proper specialization is missing (SFINAE). +template +struct ActorTraits { + static_assert(false, "ActorTraits must be specialized for each Actor specialization."); +}; + +} // namespace o2::quality_control::core +#endif // ACTORTRAITS_H diff --git a/Framework/include/QualityControl/Criticality.h b/Framework/include/QualityControl/Criticality.h new file mode 100644 index 000000000..588b8bee6 --- /dev/null +++ b/Framework/include/QualityControl/Criticality.h @@ -0,0 +1,40 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file Criticality.h +/// \author Piotr Konopka +/// +#ifndef QUALITYCONTROL_CRITICALITY_H +#define QUALITYCONTROL_CRITICALITY_H + +namespace o2::quality_control::core +{ + +/// \brief Defines how a Control system should react to task failures for a concrete Actor +enum class Criticality { + // If a critical task goes to ERROR or crashes, it brings the computing node to ERROR. + // If a node is critical (e.g. an FLP or a QC node workflow), that implies stopping a data-taking run or grid job + // If a node is non-critical (e.g. an EPN), this implies dropping that node from data taking or grid job. + // A critical task can only depend on outputs of other critical tasks, otherwise it's a DPL workflow error. + Critical, + // When an expendable (non-critical) task goes to ERROR or crashes, it does NOT bring the computing node to ERROR. + Expendable, + // A resilient task brings down the computing node upon ERROR or crash, but it can survive a failure + // of an upstream expendable task. + Resilient, + // The decision on criticality is delegated to user, but we take care of critical/resilient distinction. + UserDefined +}; + +} // namespace o2::quality_control::core + +#endif // QUALITYCONTROL_CRITICALITY_H \ No newline at end of file diff --git a/Framework/include/QualityControl/DataProcessorAdapter.h b/Framework/include/QualityControl/DataProcessorAdapter.h new file mode 100644 index 000000000..479d15c2b --- /dev/null +++ b/Framework/include/QualityControl/DataProcessorAdapter.h @@ -0,0 +1,199 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef QUALITYCONTROL_DATAPROCESSORADAPTER_H +#define QUALITYCONTROL_DATAPROCESSORADAPTER_H + +/// +/// \file DataProcessorAdapter.h +/// \author Piotr Konopka +/// + +#include + +#include "QualityControl/Actor.h" +#include "QualityControl/ActorTraits.h" +#include "QualityControl/ActorHelpers.h" +#include "QualityControl/UserInputOutput.h" + +namespace o2::quality_control::core +{ + +// helpers for DataProcessorAdapter +namespace impl +{ + +/// \brief checks if a type is derived a single UserCodeConfig +template +concept UserCodeConfigSingle = + std::derived_from, UserCodeConfig>; + +/// \brief checks if a type is a range of UserCodeConfig children +template +concept UserCodeConfigRange = + std::ranges::input_range && + std::derived_from>, UserCodeConfig>; + +/// \brief converts scalars into ranges of length 1, preserves ranges +template +auto as_range(T&& t) +{ + using U = std::remove_reference_t; + + if constexpr (std::ranges::range) { + // Already a range, just wrap in a view for consistency + return std::views::all(std::forward(t)); + } else { + // a scalar, we wrap it into a single-element range + return std::views::single(std::forward(t)); + } +} + +} // namespace impl + +struct DataProcessorAdapter { + + /// \brief creates a DataProcessorSpec for a concrete actor + template + static o2::framework::DataProcessorSpec + adapt(ConcreteActor&& actor, std::string&& dataProcessorName, framework::Inputs&& inputs, framework::Outputs&& outputs, framework::Options&& options) + { + using traits = ActorTraits; + + auto actorPtr = std::make_shared(std::move(actor)); + o2::framework::DataProcessorSpec dataProcessor; + + dataProcessor.name = std::move(dataProcessorName); + dataProcessor.inputs = std::move(inputs); + dataProcessor.outputs = std::move(outputs); + dataProcessor.options = std::move(options); + + dataProcessor.labels = { dataProcessorLabel() }; + + if constexpr (traits::sCriticality == Criticality::Resilient) { + dataProcessor.labels.emplace_back("resilient"); + } else if constexpr (traits::sCriticality == Criticality::Critical) { + // that's the default in DPL + } else if constexpr (traits::sCriticality == Criticality::Expendable) { + dataProcessor.labels.emplace_back("expendable"); + } else if constexpr (traits::sCriticality == Criticality::UserDefined) { + if (!actor.isCritical()) { + dataProcessor.labels.emplace_back("expendable"); + } else { + // we make it resilient so we can support upstream data processors with are either expendable and critical, + // and hide the unnecessary complexity from the user. + dataProcessor.labels.emplace_back("resilient"); + } + } + + dataProcessor.algorithm = { + [actorPtr](framework::InitContext& ictx) { + actorPtr->init(ictx); + return [actorPtr](framework::ProcessingContext& ctx) { + actorPtr->process(ctx); + }; + } + }; + return dataProcessor; + } + + /// \brief Produces a standard QC Data Processor name for cases when it runs user code and is associated with a detector. + static std::string dataProcessorName(std::string_view userCodeName, std::string_view detectorName, std::string_view actorTypeKebabCase); + + /// \brief Produces a standard QC Data Processor name for cases when it runs user code and is associated with a detector. + template + requires(actor_helpers::runsUserCode() && ActorTraits::sDetectorSpecific) + static std::string dataProcessorName(std::string_view userCodeName, std::string_view detectorName) + { + using traits = ActorTraits; + return dataProcessorName(detectorName, userCodeName, traits::sActorTypeKebabCase); + } + + /// \brief Produces standardized QC Data Processor name for cases were no user code is ran and it's not detector specific. + template + requires(!actor_helpers::runsUserCode() || !ActorTraits::sDetectorSpecific) + static std::string dataProcessorName() + { + using traits = ActorTraits; + return std::string{ traits::sActorTypeKebabCase }; + } + + /// \brief collects all user inputs in the provided UserCodeConfig(s) and returns framework::Inputs + template + requires(impl::UserCodeConfigSingle || impl::UserCodeConfigRange) + static framework::Inputs collectUserInputs(ConfigT&& config) + { + using traits = ActorTraits; + + // normalize to a range, even if it's a single config + auto configRange = impl::as_range(std::forward(config)); + + // get a view over all data sources + auto dataSources = configRange // + | std::views::transform([](const UserCodeConfig& config) -> const auto& { + return config.dataSources; + }) | + std::views::join; + + // validate + auto firstInvalid = std::ranges::find_if(dataSources, [](const DataSourceSpec& dataSource) { + return std::ranges::none_of(traits::sConsumedDataSources, [&](const DataSourceType& allowed) { + return dataSource.type == allowed; + }); + }); + if (firstInvalid != dataSources.end()) { + throw std::invalid_argument( + std::format("DataSource '{}' is not one of supported types for '{}'", firstInvalid->id, traits::sActorTypeUpperCamelCase)); + } + + // copy into the results + framework::Inputs inputs{}; + std::ranges::copy(dataSources // + | std::views::transform([](const auto& ds) -> const auto& { return ds.inputs; }) // + | std::views::join, + std::back_inserter(inputs)); + + // fixme: CheckRunner might have overlapping or repeating inputs. we should handle that here. + // There is some existing code in DataSampling which already does that, it could be copied here. + + return inputs; + } + + /// \brief collects all user outputs in the provided UserCodeConfig(s) and returns framework::Outputs + template + requires(impl::UserCodeConfigSingle || impl::UserCodeConfigRange) + static framework::Outputs collectUserOutputs(ConfigT&& config) + { + using traits = ActorTraits; + + // normalize to a range, even if it's a single config + auto configRange = impl::as_range(std::forward(config)); + + framework::Outputs outputs{}; + std::ranges::copy(configRange // + | std::views::transform([](const UserCodeConfig& config) { + return createUserOutputSpec(dataSourceType, config.detectorName, config.name); + }), + std::back_inserter(outputs)); + return outputs; + } + + template + static framework::DataProcessorLabel dataProcessorLabel() + { + using traits = ActorTraits; + return { std::string{ traits::sActorTypeKebabCase } }; + } +}; + +}; // namespace o2::quality_control::core + +#endif // QUALITYCONTROL_DATAPROCESSORADAPTER_H \ No newline at end of file diff --git a/Framework/include/QualityControl/InputUtils.h b/Framework/include/QualityControl/InputUtils.h index fca66c3ce..2e014117b 100644 --- a/Framework/include/QualityControl/InputUtils.h +++ b/Framework/include/QualityControl/InputUtils.h @@ -19,6 +19,10 @@ #include #include +namespace o2::quality_control::core +{ + +// fixme: rename to stringifyInputs? inline std::vector stringifyInput(const o2::framework::Inputs& inputs) { std::vector vec; @@ -27,5 +31,5 @@ inline std::vector stringifyInput(const o2::framework::Inputs& inpu } return vec; } - +} // namespace o2::quality_control::core #endif diff --git a/Framework/include/QualityControl/ServiceRequest.h b/Framework/include/QualityControl/ServiceRequest.h new file mode 100644 index 000000000..d04ee0d1a --- /dev/null +++ b/Framework/include/QualityControl/ServiceRequest.h @@ -0,0 +1,34 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file ServiceRequest.h +/// \author Piotr Konopka +/// + +#ifndef QUALITYCONTROL_SERVICEREQUEST_H +#define QUALITYCONTROL_SERVICEREQUEST_H + +namespace o2::quality_control::core +{ + +/// \brief Used to specify which services are needed by a concrete Actor +enum class ServiceRequest { + Monitoring, + InfoLogger, + CCDB, + Bookkeeping, + QCDB +}; + +} // namespace o2::quality_control::core + +#endif // QUALITYCONTROL_SERVICEREQUEST_H \ No newline at end of file diff --git a/Framework/include/QualityControl/ServicesConfig.h b/Framework/include/QualityControl/ServicesConfig.h new file mode 100644 index 000000000..9893a1845 --- /dev/null +++ b/Framework/include/QualityControl/ServicesConfig.h @@ -0,0 +1,29 @@ +// +// Created by pkonopka on 04/12/2025. +// + +#ifndef QUALITYCONTROL_SERVICESCONFIG_H +#define QUALITYCONTROL_SERVICESCONFIG_H + +#include "QualityControl/Activity.h" +#include "QualityControl/LogDiscardParameters.h" +#include +#include + +namespace o2::quality_control::core +{ + +struct ServicesConfig { + std::unordered_map database; + Activity activity; + std::string monitoringUrl = "infologger:///debug?qc"; + std::string conditionDBUrl = "http://ccdb-test.cern.ch:8080"; + LogDiscardParameters infologgerDiscardParameters; + std::string bookkeepingUrl; + std::string kafkaBrokersUrl; + std::string kafkaTopicAliECSRun = "aliecs.run"; +}; + +} // namespace o2::quality_control::core + +#endif // QUALITYCONTROL_SERVICESCONFIG_H \ No newline at end of file diff --git a/Framework/include/QualityControl/TaskFactory.h b/Framework/include/QualityControl/TaskFactory.h index 225c32b20..efb5c8086 100644 --- a/Framework/include/QualityControl/TaskFactory.h +++ b/Framework/include/QualityControl/TaskFactory.h @@ -41,7 +41,6 @@ class TaskFactory /// \brief Create a new instance of a TaskInterface. /// The TaskInterface actual class is decided based on the parameters passed. - /// \todo make it static ? /// \author Barthelemy von Haller static TaskInterface* create(const TaskRunnerConfig& taskConfig, std::shared_ptr objectsManager); }; diff --git a/Framework/include/QualityControl/UserCodeCardinality.h b/Framework/include/QualityControl/UserCodeCardinality.h new file mode 100644 index 000000000..e531ca681 --- /dev/null +++ b/Framework/include/QualityControl/UserCodeCardinality.h @@ -0,0 +1,31 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file UserCodeCardinality.h +/// \author Piotr Konopka +/// +#ifndef QUALITYCONTROL_USERCODECARDINALITY_H +#define QUALITYCONTROL_USERCODECARDINALITY_H + +namespace o2::quality_control::core +{ + +// Indicates whether an Actor runs none, one or multiple user tasks/checks/aggregators/... +enum class UserCodeCardinality { + None = 0, + One = 1, + Many = 2 +}; + +} // namespace o2::quality_control::core + +#endif // QUALITYCONTROL_USERCODECARDINALITY_H \ No newline at end of file diff --git a/Framework/include/QualityControl/stringUtils.h b/Framework/include/QualityControl/stringUtils.h index 82c5c8c8d..974fb38f6 100644 --- a/Framework/include/QualityControl/stringUtils.h +++ b/Framework/include/QualityControl/stringUtils.h @@ -45,6 +45,56 @@ bool parseBoolParam(const CustomParameters& customParameters, const std::string& */ bool isUnsignedInteger(const std::string& s); +/// \brief checks if a string is in kebab-case format +/// +/// checks if the string is not empty, does not start or end with a dash, +/// contains only lowercase letters, digits, and dashes. Two dashes in a row +/// are not allowed. +constexpr bool isKebabCase(std::string_view str) +{ + if (str.empty() || str.front() == '-' || str.back() == '-') { + return false; + } + for (size_t i = 0; i < str.size(); ++i) { + char c = str[i]; + // only lower case, digit or '-' are allowed + if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-')) { + return false; + } + // two '-' characters in a row are not allowed + if (c == '-' && (i == 0 || i == str.size() - 1 || str[i - 1] == '-')) { + return false; + } + } + return true; +} + +/// \brief checks if a string is in upper camel case format +/// +/// checks if the string is not empty, starts with an uppercase ASCII letter and +/// then contains only ASCII letters and digits. No separators allowed, we +/// tolerate multiple uppercase letters in a row (e.g. TaskLHC) +constexpr bool isUpperCamelCase(std::string_view str) +{ + if (str.empty()) { + return false; + } + const char first = str.front(); + if (!(first >= 'A' && first <= 'Z')) { + return false; + } + for (size_t i = 1; i < str.size(); ++i) { + const char c = str[i]; + const bool isUpper = (c >= 'A' && c <= 'Z'); + const bool isLower = (c >= 'a' && c <= 'z'); + const bool isDigit = (c >= '0' && c <= '9'); + if (!(isUpper || isLower || isDigit)) { + return false; + } + } + return true; +} + } // namespace o2::quality_control::core #endif // QC_STRING_UTILS_H diff --git a/Framework/src/Actor.cxx b/Framework/src/Actor.cxx new file mode 100644 index 000000000..6997ed804 --- /dev/null +++ b/Framework/src/Actor.cxx @@ -0,0 +1,103 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file Actor.cxx +/// \author Piotr Konopka +/// + +#include "QualityControl/Actor.h" + +#include +#include +#include +#include +#include + +#include "QualityControl/Bookkeeping.h" +#include "QualityControl/DatabaseFactory.h" + +namespace o2::quality_control::core +{ + +namespace impl +{ +std::shared_ptr initMonitoring(std::string_view url, std::string_view detector) +{ + auto monitoring = monitoring::MonitoringFactory::Get(std::string{ url }); + monitoring->addGlobalTag(monitoring::tags::Key::Subsystem, monitoring::tags::Value::QC); + // todo not urgent, but we should have a more generic tag key for user component name once we refactor existing (non)actors + // mMonitoring->addGlobalTag("TaskName", mTaskConfig.taskName); + if (!detector.empty()) { + monitoring->addGlobalTag("DetectorName", detector); + } + + return std::move(monitoring); +} + +void startMonitoring(monitoring::Monitoring& monitoring, int runNumber) +{ + monitoring.setRunNumber(runNumber); +} + +void initBookkeeping(std::string_view url) +{ + Bookkeeping::getInstance().init(url.data()); +} + +void startBookkeeping(int runNumber, std::string_view actorName, std::string_view detectorName, const o2::bkp::DplProcessType& processType, std::string_view args) +{ + Bookkeeping::getInstance().registerProcess(runNumber, actorName.data(), detectorName.data(), processType, args.data()); +} + +Bookkeeping& getBookkeeping() +{ + return Bookkeeping::getInstance(); +} + +std::shared_ptr initRepository(const std::unordered_map& config) +{ + auto db = quality_control::repository::DatabaseFactory::create(config.at("implementation")); + assert(db != nullptr); + db->connect(config); + ILOG(Info, Devel) << "Database that is going to be used > Implementation : " << config.at("implementation") << " / Host : " << config.at("host") << ENDM; + return std::move(db); +} + +void initCCDB(const std::string& url) +{ + auto& mgr = o2::ccdb::BasicCCDBManager::instance(); + mgr.setURL(url); + mgr.setFatalWhenNull(false); +} + +ccdb::CCDBManagerInstance& getCCDB() +{ + return o2::ccdb::BasicCCDBManager::instance(); +} + +void handleExceptions(std::string_view when, const std::function& f) +{ + try { + f(); + } catch (o2::framework::RuntimeErrorRef& ref) { + ILOG(Error) << "Error occurred during " << when << ": " << o2::framework::error_from_ref(ref).what << ENDM; + throw; + } catch (...) { + ILOG(Error) << "Error occurred during " << when << " :" + << boost::current_exception_diagnostic_information(true) << ENDM; + throw; + } +} + +} // namespace impl + +} // namespace o2::quality_control::core diff --git a/Framework/src/ActorHelpers.cxx b/Framework/src/ActorHelpers.cxx new file mode 100644 index 000000000..6ead020c5 --- /dev/null +++ b/Framework/src/ActorHelpers.cxx @@ -0,0 +1,48 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file ActorHelpers.cxx +/// \author Piotr Konopka +/// + +#include "QualityControl/ActorHelpers.h" +#include "QualityControl/CommonSpec.h" +#include "QualityControl/InfrastructureSpecReader.h" + +namespace o2::quality_control::core::actor_helpers +{ + +ServicesConfig extractConfig(const CommonSpec& commonSpec) +{ + return ServicesConfig{ + .database = commonSpec.database, + .activity{ + commonSpec.activityNumber, + commonSpec.activityType, + commonSpec.activityPeriodName, + commonSpec.activityPassName, + commonSpec.activityProvenance, + { commonSpec.activityStart, commonSpec.activityEnd }, + commonSpec.activityBeamType, + commonSpec.activityPartitionName, + commonSpec.activityFillNumber, + commonSpec.activityOriginalNumber }, + .monitoringUrl = commonSpec.monitoringUrl, + .conditionDBUrl = commonSpec.conditionDBUrl, + .infologgerDiscardParameters = commonSpec.infologgerDiscardParameters, + .bookkeepingUrl = commonSpec.bookkeepingUrl, + .kafkaBrokersUrl = commonSpec.kafkaBrokersUrl, + .kafkaTopicAliECSRun = commonSpec.kafkaTopicAliECSRun + }; +} + +} // namespace o2::quality_control::core::actor_helpers \ No newline at end of file diff --git a/Framework/src/DataProcessorAdapter.cxx b/Framework/src/DataProcessorAdapter.cxx new file mode 100644 index 000000000..df4d3f8bc --- /dev/null +++ b/Framework/src/DataProcessorAdapter.cxx @@ -0,0 +1,32 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file DataProcessorAdapter.cxx +/// \author Piotr Konopka +/// + +#include + +#include "QualityControl/DataProcessorAdapter.h" +#include "QualityControl/CommonSpec.h" +#include "QualityControl/InfrastructureSpecReader.h" + +namespace o2::quality_control::core +{ + +std::string DataProcessorAdapter::dataProcessorName(std::string_view userCodeName, std::string_view detectorName, std::string_view actorTypeKebabCase) +{ + // todo perhaps detector name validation should happen earlier, just once and throw in case of configuration errors + return std::format("{}-{}-{}", actorTypeKebabCase, InfrastructureSpecReader::validateDetectorName(std::string{ detectorName }), userCodeName); +} + +} // namespace o2::quality_control::core \ No newline at end of file diff --git a/Framework/src/InfrastructureSpecReader.cxx b/Framework/src/InfrastructureSpecReader.cxx index 91f18cd71..05168abcb 100644 --- a/Framework/src/InfrastructureSpecReader.cxx +++ b/Framework/src/InfrastructureSpecReader.cxx @@ -15,11 +15,17 @@ #include "QualityControl/InfrastructureSpecReader.h" #include "QualityControl/QcInfoLogger.h" +#include "QualityControl/TaskRunner.h" +#include "QualityControl/PostProcessingDevice.h" +#include "QualityControl/Check.h" +#include "QualityControl/AggregatorRunner.h" #include "QualityControl/UserInputOutput.h" #include #include +#include + using namespace o2::utilities; using namespace o2::framework; using namespace o2::quality_control::postprocessing; diff --git a/Framework/test/testActor.cxx b/Framework/test/testActor.cxx new file mode 100644 index 000000000..afe33b8f5 --- /dev/null +++ b/Framework/test/testActor.cxx @@ -0,0 +1,113 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file testActor.cxx +/// \author Piotr Konopka +/// + +#include + +#include "QualityControl/Actor.h" +#include "QualityControl/ActorTraits.h" +#include "QualityControl/ActorHelpers.h" +#include "QualityControl/ServicesConfig.h" + +#include + +namespace o2::quality_control::core +{ + +struct DummyActor; + +template <> +struct ActorTraits { + constexpr static std::string_view sActorTypeShort{ "dummy" }; + constexpr static std::string_view sActorTypeKebabCase{ "qc-dummy-actor" }; + constexpr static std::string_view sActorTypeUpperCamelCase{ "DummyActor" }; + constexpr static size_t sDataDescriptionHashLength{ 4 }; + constexpr static std::array sConsumedDataSources{}; + constexpr static std::array sPublishedDataSources{}; + constexpr static std::array sRequiredServices{}; + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::None }; + constexpr static bool sDetectorSpecific{ false }; + constexpr static Criticality sCriticality{ Criticality::Expendable }; +}; + +// Minimal concrete actor satisfying mandatory interface +class DummyActor : public Actor +{ + public: + explicit DummyActor(const ServicesConfig& cfg) : Actor(cfg) {} + + void onInit(framework::InitContext&) {} + void onProcess(framework::ProcessingContext&) {} +}; + +TEST_CASE("A minimal dummy actor") +{ + // Traits must satisfy the concept enforced by Actor + STATIC_CHECK(ValidActorTraits>); + + // Basic construction should be possible and not throw + ServicesConfig cfg; // default activity and URLs are fine for construction (no services started) + REQUIRE_NOTHROW(DummyActor{ cfg }); +} + +struct UnrequestedAccessActor; + +template <> +struct ActorTraits { + constexpr static std::string_view sActorTypeShort{ "unrequested" }; + constexpr static std::string_view sActorTypeKebabCase{ "greedy-actor" }; + constexpr static std::string_view sActorTypeUpperCamelCase{ "GreedyActor" }; + constexpr static size_t sDataDescriptionHashLength{ 4 }; + constexpr static std::array sConsumedDataSources{}; + constexpr static std::array sPublishedDataSources{}; + constexpr static std::array sRequiredServices{}; + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::None }; + constexpr static bool sDetectorSpecific{ false }; + constexpr static Criticality sCriticality{ Criticality::Expendable }; +}; + +class UnrequestedAccessActor : public Actor +{ + public: + explicit UnrequestedAccessActor(const ServicesConfig& cfg) : Actor(cfg) {} + + template + constexpr void assertNoAccessToServices() + { + static_assert(!(requires(T& t) { t.getMonitoring(); })); + static_assert(!(requires(T& t) { t.getBookkeeping(); })); + static_assert(!(requires(T& t) { t.getRepository(); })); + static_assert(!(requires(T& t) { t.getCCDB(); })); + } + + void onInit(framework::InitContext&) + { + assertNoAccessToServices(); + } + + void onProcess(framework::ProcessingContext&) {} +}; + +TEST_CASE("An actor which tries to access services which it did not request") +{ + // Traits must satisfy the concept enforced by Actor + STATIC_CHECK(ValidActorTraits>); + + // Basic construction should be possible and not throw + ServicesConfig cfg; // default activity and URLs are fine for construction (no services started) + REQUIRE_NOTHROW(UnrequestedAccessActor{ cfg }); +} + +} // namespace o2::quality_control::core diff --git a/Framework/test/testActorCallbacks.cxx b/Framework/test/testActorCallbacks.cxx new file mode 100644 index 000000000..cebd25977 --- /dev/null +++ b/Framework/test/testActorCallbacks.cxx @@ -0,0 +1,129 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file testActorCallbacks.cxx +/// \author Piotr Konopka +/// + +#include "QualityControl/Actor.h" +#include "QualityControl/ActorTraits.h" +#include "QualityControl/ActorHelpers.h" +#include "QualityControl/DataProcessorAdapter.h" + +#include +#include + +using namespace o2::framework; +using namespace o2::quality_control::core; + +struct DummyActor; + +template <> +struct o2::quality_control::core::ActorTraits { + constexpr static std::string_view sActorTypeShort{ "dummy" }; + constexpr static std::string_view sActorTypeKebabCase{ "qc-dummy-actor" }; + constexpr static std::string_view sActorTypeUpperCamelCase{ "DummyActor" }; + constexpr static size_t sDataDescriptionHashLength{ 4 }; + constexpr static std::array sConsumedDataSources{}; + constexpr static std::array sPublishedDataSources{}; + constexpr static std::array sRequiredServices{}; + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::None }; + constexpr static bool sDetectorSpecific{ false }; + constexpr static Criticality sCriticality{ Criticality::Critical }; +}; + +// test helpers +constexpr std::string_view sEventCreated = "Created"; +constexpr std::string_view sEventOnInitCalled = "onInit called"; +constexpr std::string_view sEventOnStartCalled = "onStart called"; +constexpr std::string_view sEventOnProcessCalled = "onProcess called"; +constexpr std::string_view sEventOnStopCalled = "onStop called"; +constexpr std::string_view sEventOnResetCalled = "onReset called"; + +// Minimal concrete actor satisfying mandatory interface +class DummyActor : public Actor +{ + public: + explicit DummyActor(const ServicesConfig& cfg) : Actor(cfg) {} + ~DummyActor() = default; + + void onInit(InitContext&) + { + LOG(info) << "onInit called"; + if (mLastEvent != sEventCreated) { + LOG(fatal) << "test failed in onInit, last event should have been '" << sEventCreated << "', but was '" << mLastEvent << "'"; + } + mLastEvent = sEventOnInitCalled; + } + + void onStart(ServiceRegistryRef services, const Activity& activity) + { + LOG(info) << "onStart called"; + if (mLastEvent != sEventOnInitCalled) { + LOG(fatal) << "test failed in onStart, last event should have been '" << sEventOnInitCalled << "', but was '" << mLastEvent << "'"; + } + mLastEvent = sEventOnStartCalled; + } + + void onProcess(ProcessingContext& ctx) + { + LOG(info) << "onProcess called"; + if (mLastEvent != sEventOnStartCalled) { + LOG(fatal) << "test failed in onProcess, last event should have been '" << sEventOnStartCalled << "', but was '" << mLastEvent << "'"; + } + mLastEvent = sEventOnProcessCalled; + ctx.services().get().endOfStream(); + } + + void onStop(ServiceRegistryRef services, const Activity& activity) + { + LOG(info) << "onStop called"; + if (mLastEvent != sEventOnProcessCalled) { + LOG(fatal) << "test failed in onStop, last event should have been '" << sEventOnProcessCalled << "', but was '" << mLastEvent << "'"; + } + mLastEvent = sEventOnStopCalled; + } + + void onReset(ServiceRegistryRef services, const Activity& activity) + { + LOG(info) << "onReset called"; + if (mLastEvent != sEventOnStopCalled) { + LOG(fatal) << "test failed in onReset, last event should have been '" << sEventOnStopCalled << "', but was '" << mLastEvent << "'"; + } + mLastEvent = sEventOnResetCalled; + } + + private: + std::string_view mLastEvent = sEventCreated; +}; + +WorkflowSpec defineDataProcessing(ConfigContext const&) +{ + WorkflowSpec specs; + + ServicesConfig cfg; + DummyActor dummyActor{ cfg }; + + specs.push_back(DataProcessorAdapter::adapt( + std::move(dummyActor), + "dummy-actor", + Inputs{}, + Outputs{ { { "out" }, "TST", "DUMMY", 0 } }, + Options{})); + + // if dummy actor never sends EoS, this receiver idles until timeout and the test fails. + specs.push_back({ "receiver", + Inputs{ { { "in" }, "TST", "DUMMY", 0 } }, + Outputs{} }); + + return specs; +} \ No newline at end of file diff --git a/Framework/test/testActorHelpers.cxx b/Framework/test/testActorHelpers.cxx new file mode 100644 index 000000000..ef5206901 --- /dev/null +++ b/Framework/test/testActorHelpers.cxx @@ -0,0 +1,142 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file testActorHelpers.cxx +/// \author Piotr Konopka +/// + +#include "QualityControl/ActorHelpers.h" +#include "QualityControl/CommonSpec.h" +#include + +namespace o2::quality_control::core +{ + +using namespace actor_helpers; + +struct ActorWithTwoServices; +template <> +struct ActorTraits { + constexpr static std::array sRequiredServices{ ServiceRequest::Monitoring, ServiceRequest::CCDB }; + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::None }; +}; + +TEST_CASE("requiresService") +{ + STATIC_CHECK(requiresService() == true); + STATIC_CHECK(requiresService() == true); + STATIC_CHECK(requiresService() == false); + STATIC_CHECK(requiresService() == false); +} + +struct ActorUserCodeNone; +template <> +struct ActorTraits { + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::None }; +}; + +struct ActorUserCodeOne; +template <> +struct ActorTraits { + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::One }; +}; + +struct ActorUserCodeMany; +template <> +struct ActorTraits { + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::Many }; +}; + +TEST_CASE("runsUserCode") +{ + STATIC_CHECK(runsUserCode() == false); + STATIC_CHECK(runsUserCode() == true); + STATIC_CHECK(runsUserCode() == true); +} + +struct ActorPublishesTwoDataSources; +template <> +struct ActorTraits { + constexpr static std::array sPublishedDataSources{ DataSourceType::Task, DataSourceType::Check }; +}; + +TEST_CASE("ValidDataSourceForActor") +{ + STATIC_CHECK(ValidDataSourceForActor); + STATIC_CHECK(ValidDataSourceForActor); + STATIC_CHECK(!ValidDataSourceForActor); + STATIC_CHECK(!ValidDataSourceForActor); +} + +TEST_CASE("extractConfig copies CommonSpec into ServicesConfig") +{ + CommonSpec spec; + spec.database = { { "implementation", "ccdb" }, { "host", "example.invalid" } }; + + spec.activityNumber = 42; + spec.activityType = "PHYSICS"; + spec.activityPeriodName = "LHCxx"; + spec.activityPassName = "pass1"; + spec.activityProvenance = "qc"; + spec.activityStart = 1234; + spec.activityEnd = 5678; + spec.activityBeamType = "pp"; + spec.activityPartitionName = "physics_1"; + spec.activityFillNumber = 777; + spec.activityOriginalNumber = 4242; + + spec.monitoringUrl = "infologger:///debug?qc_test"; + spec.conditionDBUrl = "http://ccdb.example.invalid:8080"; + + spec.infologgerDiscardParameters.debug = false; + spec.infologgerDiscardParameters.fromLevel = 10; + spec.infologgerDiscardParameters.file = "/tmp/qc-discard.log"; + spec.infologgerDiscardParameters.rotateMaxBytes = 123456; + spec.infologgerDiscardParameters.rotateMaxFiles = 7; + spec.infologgerDiscardParameters.debugInDiscardFile = true; + + spec.bookkeepingUrl = "http://bookkeeping.example.invalid"; + spec.kafkaBrokersUrl = "broker1:9092,broker2:9092"; + spec.kafkaTopicAliECSRun = "aliecs.run.test"; + + const auto cfg = extractConfig(spec); + + REQUIRE(cfg.database == spec.database); + + REQUIRE(cfg.activity.mId == spec.activityNumber); + REQUIRE(cfg.activity.mType == spec.activityType); + REQUIRE(cfg.activity.mPeriodName == spec.activityPeriodName); + REQUIRE(cfg.activity.mPassName == spec.activityPassName); + REQUIRE(cfg.activity.mProvenance == spec.activityProvenance); + REQUIRE(cfg.activity.mValidity.getMin() == spec.activityStart); + REQUIRE(cfg.activity.mValidity.getMax() == spec.activityEnd); + REQUIRE(cfg.activity.mBeamType == spec.activityBeamType); + REQUIRE(cfg.activity.mPartitionName == spec.activityPartitionName); + REQUIRE(cfg.activity.mFillNumber == spec.activityFillNumber); + REQUIRE(cfg.activity.mOriginalId == spec.activityOriginalNumber); + + REQUIRE(cfg.monitoringUrl == spec.monitoringUrl); + REQUIRE(cfg.conditionDBUrl == spec.conditionDBUrl); + + REQUIRE(cfg.infologgerDiscardParameters.debug == spec.infologgerDiscardParameters.debug); + REQUIRE(cfg.infologgerDiscardParameters.fromLevel == spec.infologgerDiscardParameters.fromLevel); + REQUIRE(cfg.infologgerDiscardParameters.file == spec.infologgerDiscardParameters.file); + REQUIRE(cfg.infologgerDiscardParameters.rotateMaxBytes == spec.infologgerDiscardParameters.rotateMaxBytes); + REQUIRE(cfg.infologgerDiscardParameters.rotateMaxFiles == spec.infologgerDiscardParameters.rotateMaxFiles); + REQUIRE(cfg.infologgerDiscardParameters.debugInDiscardFile == spec.infologgerDiscardParameters.debugInDiscardFile); + + REQUIRE(cfg.bookkeepingUrl == spec.bookkeepingUrl); + REQUIRE(cfg.kafkaBrokersUrl == spec.kafkaBrokersUrl); + REQUIRE(cfg.kafkaTopicAliECSRun == spec.kafkaTopicAliECSRun); +} + +} // namespace o2::quality_control::core diff --git a/Framework/test/testActorTraits.cxx b/Framework/test/testActorTraits.cxx new file mode 100644 index 000000000..c1e3d38c0 --- /dev/null +++ b/Framework/test/testActorTraits.cxx @@ -0,0 +1,138 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file testActorTraits.cxx +/// \author Piotr Konopka +/// + +#include "QualityControl/ActorTraits.h" +#include + +namespace o2::quality_control::core +{ + +struct CorrectActor; +template <> +struct ActorTraits { + constexpr static std::string_view sActorTypeShort{ "wheel" }; + constexpr static std::string_view sActorTypeKebabCase{ "qc-wheel-runer" }; + constexpr static std::string_view sActorTypeUpperCamelCase{ "WheelRunner" }; + constexpr static size_t sDataDescriptionHashLength{ 4 }; + constexpr static std::array sConsumedDataSources{ DataSourceType::Task }; + constexpr static std::array sPublishedDataSources{ DataSourceType::Check }; + constexpr static std::array sRequiredServices{ ServiceRequest::InfoLogger, ServiceRequest::Monitoring }; + constexpr static o2::bkp::DplProcessType sDplProcessType{ o2::bkp::DplProcessType::MERGER }; + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::One }; + constexpr static bool sDetectorSpecific{ true }; + constexpr static Criticality sCriticality{ Criticality::UserDefined }; +}; +using CorrectActorTraits = ActorTraits; + +struct WrongActorA; +template <> +struct ActorTraits { + constexpr static std::string_view sActorTypeShort{ "wheel" }; + constexpr static std::string_view sActorTypeKebabCase{ "qc-wheel-runer" }; + constexpr static std::string_view sActorTypeUpperCamelCase{ "WheelRunner" }; + constexpr static size_t sDataDescriptionHashLength{ 4 }; + constexpr static std::array sConsumedDataSources{ DataSourceType::Task }; + constexpr static std::array sPublishedDataSources{ DataSourceType::Check }; + constexpr static std::array sRequiredServices{ ServiceRequest::InfoLogger, ServiceRequest::Monitoring, ServiceRequest::Bookkeeping }; + // <---- missing o2::bkp::DplProcessType sDplProcessType + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::One }; + constexpr static bool sDetectorSpecific{ true }; + constexpr static Criticality sCriticality{ Criticality::UserDefined }; +}; +using MissingDplProcessTypeForBKPTraits = ActorTraits; + +struct WrongActorB; +template <> +struct ActorTraits { +}; +using EmptyActorTraits = ActorTraits; + +struct WrongActorC; +template <> +struct ActorTraits { + std::string_view sActorTypeShort{ "wheel" }; // <---- wrong + constexpr static std::string_view sActorTypeKebabCase{ "qc-wheel-runer" }; + constexpr static std::string_view sActorTypeUpperCamelCase{ "WheelRunner" }; + constexpr static size_t sDataDescriptionHashLength{ 4 }; + constexpr static std::array sConsumedDataSources{ DataSourceType::Task }; + constexpr static std::array sPublishedDataSources{ DataSourceType::Check }; + constexpr static std::array sRequiredServices{ ServiceRequest::InfoLogger, ServiceRequest::Monitoring, ServiceRequest::Bookkeeping }; + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::One }; + constexpr static bool sDetectorSpecific{ true }; + constexpr static Criticality sCriticality{ Criticality::UserDefined }; +}; +using NonConstStaticActorTraits = ActorTraits; + +struct WrongActorD; +template <> +struct ActorTraits { + std::string_view sActorTypeShort{ "wheel" }; + constexpr static std::string_view sActorTypeKebabCase{ "qc-wheel-runer" }; + constexpr static std::string_view sActorTypeUpperCamelCase{ "WheelRunner" }; + constexpr static size_t sDataDescriptionHashLength{ o2::header::DataDescription::size + 555 }; // <---- wrong + constexpr static std::array sConsumedDataSources{ DataSourceType::Task }; + constexpr static std::array sPublishedDataSources{ DataSourceType::Check }; + constexpr static std::array sRequiredServices{ ServiceRequest::InfoLogger, ServiceRequest::Monitoring, ServiceRequest::Bookkeeping }; + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::One }; + constexpr static bool sDetectorSpecific{ true }; + constexpr static Criticality sCriticality{ Criticality::UserDefined }; +}; +using TooLongDataDescriptionHashTraits = ActorTraits; + +struct WrongActorE; +template <> +struct ActorTraits { + std::string_view sActorTypeShort{ "wheel" }; + constexpr static std::string_view sActorTypeKebabCase{ "WheelRunner" }; // <---- wrong + constexpr static std::string_view sActorTypeUpperCamelCase{ "WheelRunner" }; + constexpr static size_t sDataDescriptionHashLength{ o2::header::DataDescription::size + 555 }; + constexpr static std::array sConsumedDataSources{ DataSourceType::Task }; + constexpr static std::array sPublishedDataSources{ DataSourceType::Check }; + constexpr static std::array sRequiredServices{ ServiceRequest::InfoLogger, ServiceRequest::Monitoring, ServiceRequest::Bookkeeping }; + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::One }; + constexpr static bool sDetectorSpecific{ true }; + constexpr static Criticality sCriticality{ Criticality::UserDefined }; +}; +using KebabCaseTypeNotRespected = ActorTraits; + +struct WrongActorF; +template <> +struct ActorTraits { + std::string_view sActorTypeShort{ "wheel" }; + constexpr static std::string_view sActorTypeKebabCase{ "qc-wheel-runer" }; + constexpr static std::string_view sActorTypeUpperCamelCase{ "qc-wheel-runner" }; // <---- wrong + constexpr static size_t sDataDescriptionHashLength{ o2::header::DataDescription::size + 555 }; + constexpr static std::array sConsumedDataSources{ DataSourceType::Task }; + constexpr static std::array sPublishedDataSources{ DataSourceType::Check }; + constexpr static std::array sRequiredServices{ ServiceRequest::InfoLogger, ServiceRequest::Monitoring, ServiceRequest::Bookkeeping }; + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::One }; + constexpr static bool sDetectorSpecific{ true }; + constexpr static Criticality sCriticality{ Criticality::UserDefined }; +}; +using UpperCamelCaseTypeNotRespected = ActorTraits; + +TEST_CASE("valid actor traits") +{ + STATIC_CHECK(ValidActorTraits); + STATIC_CHECK(ValidActorTraits == false); + STATIC_CHECK(ValidActorTraits == false); + STATIC_CHECK(ValidActorTraits == false); + STATIC_CHECK(ValidActorTraits == false); + STATIC_CHECK(ValidActorTraits == false); + STATIC_CHECK(ValidActorTraits == false); +} + +} // namespace o2::quality_control::core diff --git a/Framework/test/testDataProcessorAdapter.cxx b/Framework/test/testDataProcessorAdapter.cxx new file mode 100644 index 000000000..bad158fdb --- /dev/null +++ b/Framework/test/testDataProcessorAdapter.cxx @@ -0,0 +1,274 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file testDataProcessorAdapter.cxx +/// \author Piotr Konopka +/// + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "QualityControl/DataProcessorAdapter.h" +#include "QualityControl/UserCodeConfig.h" +#include "QualityControl/UserInputOutput.h" + +namespace o2::quality_control::core +{ + +using o2::framework::DataProcessorLabel; +using o2::framework::DataSpecUtils; +using o2::framework::Inputs; +using o2::framework::Options; +using o2::framework::Outputs; + +bool hasLabel(const std::vector& labels, std::string_view value) +{ + return std::find(labels.begin(), labels.end(), DataProcessorLabel{ std::string{ value } }) != labels.end(); +} + +struct ResilientActor { + void init(o2::framework::InitContext&) {} + void process(o2::framework::ProcessingContext&) {} +}; + +template <> +struct ActorTraits { + constexpr static std::string_view sActorTypeKebabCase{ "qc-criticality-resilient-actor" }; + constexpr static Criticality sCriticality{ Criticality::Resilient }; +}; + +TEST_CASE("DataProcessorAdapter::adapt adds resilient label for resilient criticality") +{ + auto spec = DataProcessorAdapter::adapt(ResilientActor{}, "resilient-dp", Inputs{}, Outputs{}, Options{}); + CHECK(hasLabel(spec.labels, "resilient")); + CHECK_FALSE(hasLabel(spec.labels, "expendable")); +} + +struct CriticalActor { + void init(o2::framework::InitContext&) {} + void process(o2::framework::ProcessingContext&) {} +}; + +template <> +struct ActorTraits { + constexpr static std::string_view sActorTypeKebabCase{ "qc-criticality-critical-actor" }; + constexpr static Criticality sCriticality{ Criticality::Critical }; +}; + +TEST_CASE("DataProcessorAdapter::adapt keeps default labels for critical criticality") +{ + auto spec = DataProcessorAdapter::adapt(CriticalActor{}, "critical-dp", Inputs{}, Outputs{}, Options{}); + CHECK_FALSE(hasLabel(spec.labels, "resilient")); + CHECK_FALSE(hasLabel(spec.labels, "expendable")); +} + +struct ExpendableActor { + void init(o2::framework::InitContext&) {} + void process(o2::framework::ProcessingContext&) {} +}; + +template <> +struct ActorTraits { + constexpr static std::string_view sActorTypeKebabCase{ "qc-criticality-expendable-actor" }; + constexpr static Criticality sCriticality{ Criticality::Expendable }; +}; + +TEST_CASE("DataProcessorAdapter::adapt adds expendable label for expendable criticality") +{ + auto spec = DataProcessorAdapter::adapt(ExpendableActor{}, "expendable-dp", Inputs{}, Outputs{}, Options{}); + CHECK(hasLabel(spec.labels, "expendable")); + CHECK_FALSE(hasLabel(spec.labels, "resilient")); +} + +struct UserDefinedCriticalityActor { + explicit UserDefinedCriticalityActor(bool isCritical) + : mIsCritical{ isCritical } + { + } + + bool isCritical() const { return mIsCritical; } + void init(o2::framework::InitContext&) {} + void process(o2::framework::ProcessingContext&) {} + + private: + bool mIsCritical; +}; + +template <> +struct ActorTraits { + constexpr static std::string_view sActorTypeKebabCase{ "qc-userdefined-criticality-actor" }; + constexpr static Criticality sCriticality{ Criticality::UserDefined }; +}; + +TEST_CASE("DataProcessorAdapter::adapt uses actor critical flag for user-defined criticality") +{ + SECTION("critical actor instance") + { + auto spec = DataProcessorAdapter::adapt(UserDefinedCriticalityActor{ true }, "userdefined-critical", Inputs{}, Outputs{}, Options{}); + // that's not a mistake, "resilient" means the task itself critical, but can survive crashes of upstream data processors. + // this way we allow for upstream data processors to be either critical or expendable and hide this complexity from the user. + CHECK(hasLabel(spec.labels, "resilient")); + } + + SECTION("non-critical actor instance") + { + auto spec = DataProcessorAdapter::adapt(UserDefinedCriticalityActor{ false }, "userdefined-expendable", Inputs{}, Outputs{}, Options{}); + CHECK(hasLabel(spec.labels, "expendable")); + } +} + +struct ActorAlice; + +template <> +struct ActorTraits { + constexpr static std::string_view sActorTypeKebabCase{ "qc-actor-a" }; + constexpr static UserCodeCardinality sUserCodeCardinality{ UserCodeCardinality::None }; + constexpr static bool sDetectorSpecific{ false }; +}; + +struct ActorBob { + void init(o2::framework::InitContext&) {} + void process(o2::framework::ProcessingContext&) {} +}; + +template <> +struct ActorTraits { + constexpr static std::string_view sActorTypeKebabCase{ "qc-actor-b" }; + constexpr static std::string_view sActorTypeUpperCamelCase{ "QcActorBob" }; + constexpr static std::array sConsumedDataSources{ DataSourceType::Task, DataSourceType::Check }; + constexpr static Criticality sCriticality{ Criticality::Critical }; +}; + +TEST_CASE("DataProcessorAdapter::dataProcessorName validates detector") +{ + CHECK(DataProcessorAdapter::dataProcessorName("taskName", "TPC", "qc-task") == "qc-task-TPC-taskName"); + CHECK(DataProcessorAdapter::dataProcessorName("taskName", "INVALID", "qc-task") == "qc-task-MISC-taskName"); +} + +TEST_CASE("DataProcessorAdapter::dataProcessorName without user code") +{ + CHECK(DataProcessorAdapter::dataProcessorName() == "qc-actor-a"); +} + +TEST_CASE("DataProcessorAdapter::adapt forwards processor specs") +{ + const Inputs inputs{ createUserInputSpec(DataSourceType::Task, "TPC", "taskInput") }; + const Outputs outputs{ createUserOutputSpec(DataSourceType::Task, "TPC", "taskOutput") }; + + auto spec = DataProcessorAdapter::adapt(ActorBob{}, "io-dp", Inputs{ inputs }, Outputs{ outputs }, Options{}); + + CHECK(spec.name == "io-dp"); + REQUIRE(spec.inputs.size() == 1); + REQUIRE(spec.outputs.size() == 1); + CHECK(DataSpecUtils::match(spec.inputs[0], DataSpecUtils::asConcreteDataMatcher(inputs[0]))); + CHECK(DataSpecUtils::match(spec.outputs[0], DataSpecUtils::asConcreteDataMatcher(outputs[0]))); +} + +TEST_CASE("DataProcessorAdapter::collectUserInputs handles single config and ranges") +{ + UserCodeConfig configA; + configA.name = "taskA"; + configA.detectorName = "TPC"; + configA.dataSources = { + DataSourceSpec{ DataSourceType::Task }, + DataSourceSpec{ DataSourceType::Check } + }; + configA.dataSources[0].id = "task-source"; + configA.dataSources[0].inputs = { createUserInputSpec(DataSourceType::Task, "TPC", "taskA") }; + configA.dataSources[1].id = "check-source"; + configA.dataSources[1].inputs = { + createUserInputSpec(DataSourceType::Check, "TPC", "checkA", 0, "checkA_binding"), + createUserInputSpec(DataSourceType::Check, "TPC", "checkA", 1, "checkA_binding_1") + }; + + SECTION("single config") + { + const auto inputs = DataProcessorAdapter::collectUserInputs(configA); + + REQUIRE(inputs.size() == 3); + CHECK(DataSpecUtils::match(inputs[0], DataSpecUtils::asConcreteDataMatcher(configA.dataSources[0].inputs[0]))); + CHECK(DataSpecUtils::match(inputs[1], DataSpecUtils::asConcreteDataMatcher(configA.dataSources[1].inputs[0]))); + CHECK(DataSpecUtils::match(inputs[2], DataSpecUtils::asConcreteDataMatcher(configA.dataSources[1].inputs[1]))); + } + + SECTION("range of configs") + { + UserCodeConfig configB; + configB.name = "taskB"; + configB.detectorName = "TPC"; + configB.dataSources = { DataSourceSpec{ DataSourceType::Task } }; + configB.dataSources[0].id = "taskB-source"; + configB.dataSources[0].inputs = { + createUserInputSpec(DataSourceType::Task, "TPC", "taskB", 2, "taskB_binding") + }; + + std::vector configs{ configA, configB }; + const auto inputs = DataProcessorAdapter::collectUserInputs(configs); + + REQUIRE(inputs.size() == 4); + CHECK(DataSpecUtils::match(inputs[0], DataSpecUtils::asConcreteDataMatcher(configA.dataSources[0].inputs[0]))); + CHECK(DataSpecUtils::match(inputs[1], DataSpecUtils::asConcreteDataMatcher(configA.dataSources[1].inputs[0]))); + CHECK(DataSpecUtils::match(inputs[2], DataSpecUtils::asConcreteDataMatcher(configA.dataSources[1].inputs[1]))); + CHECK(DataSpecUtils::match(inputs[3], DataSpecUtils::asConcreteDataMatcher(configB.dataSources[0].inputs[0]))); + } +} + +TEST_CASE("DataProcessorAdapter::collectUserInputs rejects unsupported source type") +{ + UserCodeConfig config; + config.dataSources = { DataSourceSpec{ DataSourceType::Direct } }; + config.dataSources[0].id = "unsupported-source"; + + REQUIRE_THROWS_AS((DataProcessorAdapter::collectUserInputs(config)), std::invalid_argument); +} + +TEST_CASE("DataProcessorAdapter::collectUserOutputs handles single config and ranges") +{ + UserCodeConfig configA; + configA.name = "taskA"; + configA.detectorName = "TPC"; + + UserCodeConfig configB; + configB.name = "taskB"; + configB.detectorName = "TRD"; + + SECTION("single config") + { + const auto outputs = DataProcessorAdapter::collectUserOutputs(configA); + + REQUIRE(outputs.size() == 1); + CHECK(outputs[0].binding.value == "taskA"); + CHECK(DataSpecUtils::match(outputs[0], DataSpecUtils::asConcreteDataMatcher(createUserOutputSpec(DataSourceType::Task, "TPC", "taskA")))); + } + + SECTION("range of configs") + { + std::vector configs{ configA, configB }; + const auto outputs = DataProcessorAdapter::collectUserOutputs(configs); + + REQUIRE(outputs.size() == 2); + CHECK(outputs[0].binding.value == "taskA"); + CHECK(outputs[1].binding.value == "taskB"); + CHECK(DataSpecUtils::match(outputs[0], DataSpecUtils::asConcreteDataMatcher(createUserOutputSpec(DataSourceType::Aggregator, "TPC", "taskA")))); + CHECK(DataSpecUtils::match(outputs[1], DataSpecUtils::asConcreteDataMatcher(createUserOutputSpec(DataSourceType::Aggregator, "TRD", "taskB")))); + } +} + +} // namespace o2::quality_control::core diff --git a/Framework/test/testStringUtils.cxx b/Framework/test/testStringUtils.cxx index 7d320fb37..7f1d77052 100644 --- a/Framework/test/testStringUtils.cxx +++ b/Framework/test/testStringUtils.cxx @@ -16,22 +16,47 @@ #include "QualityControl/stringUtils.h" -#define BOOST_TEST_MODULE Triggers test -#define BOOST_TEST_MAIN -#define BOOST_TEST_DYN_LINK - -#include +#include using namespace o2::quality_control::core; -BOOST_AUTO_TEST_CASE(test_is_number) +TEST_CASE("isUnsignedInteger() accepts only unsigned integers") +{ + CHECK(isUnsignedInteger("1") == true); + CHECK(isUnsignedInteger("-1") == false); + CHECK(isUnsignedInteger("1000000") == true); + CHECK(isUnsignedInteger("0.1") == false); + CHECK(isUnsignedInteger(".2") == false); + CHECK(isUnsignedInteger("x") == false); + CHECK(isUnsignedInteger("1x") == false); + CHECK(isUnsignedInteger("......") == false); +} + +TEST_CASE("test_kebab_case") { - BOOST_CHECK_EQUAL(isUnsignedInteger("1"), true); - BOOST_CHECK_EQUAL(isUnsignedInteger("-1"), false); - BOOST_CHECK_EQUAL(isUnsignedInteger("1000000"), true); - BOOST_CHECK_EQUAL(isUnsignedInteger("0.1"), false); - BOOST_CHECK_EQUAL(isUnsignedInteger(".2"), false); - BOOST_CHECK_EQUAL(isUnsignedInteger("x"), false); - BOOST_CHECK_EQUAL(isUnsignedInteger("1x"), false); - BOOST_CHECK_EQUAL(isUnsignedInteger("......"), false); + STATIC_CHECK(isKebabCase("a")); + STATIC_CHECK(isKebabCase("asdf-fdsa-321")); + STATIC_CHECK(isKebabCase("a-b-c")); + STATIC_CHECK_FALSE(isKebabCase("ASDF-fdsa-321")); + STATIC_CHECK_FALSE(isKebabCase("ASDF--fdsa-321")); + STATIC_CHECK_FALSE(isKebabCase("-asdf")); + STATIC_CHECK_FALSE(isKebabCase("asdf-")); + STATIC_CHECK_FALSE(isKebabCase("")); +} + +TEST_CASE("isUpperCamelCase() validates UpperCamelCase identifiers") +{ + STATIC_CHECK(isUpperCamelCase("TaskRunner")); + STATIC_CHECK(isUpperCamelCase("URLParser")); + STATIC_CHECK(isUpperCamelCase("A")); + STATIC_CHECK(isUpperCamelCase("A1")); + STATIC_CHECK(isUpperCamelCase("My2DPlot")); + + STATIC_CHECK(isUpperCamelCase("") == false); + STATIC_CHECK(isUpperCamelCase("taskRunner") == false); + STATIC_CHECK(isUpperCamelCase("1Task") == false); + STATIC_CHECK(isUpperCamelCase("Task_Runner") == false); + STATIC_CHECK(isUpperCamelCase("Task-Runner") == false); + STATIC_CHECK(isUpperCamelCase("task-runner") == false); + STATIC_CHECK(isUpperCamelCase("Task Runner") == false); } \ No newline at end of file