From c3f04c2db73e00b85a72a4013f3f44f7052ddb69 Mon Sep 17 00:00:00 2001 From: Piotr Konopka Date: Wed, 1 Apr 2026 18:43:52 +0200 Subject: [PATCH 1/4] QC-1346 Scaffolding to share common code among QC data processors 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. This commit paves the path for refactoring existing QC data processors as Actor specializations/children. --- Framework/CMakeLists.txt | 13 +- Framework/include/QualityControl/Actor.h | 374 ++++++++++++++++++ .../include/QualityControl/ActorHelpers.h | 83 ++++ .../include/QualityControl/ActorTraits.h | 109 +++++ .../include/QualityControl/Criticality.h | 40 ++ .../QualityControl/DataProcessorAdapter.h | 197 +++++++++ Framework/include/QualityControl/InputUtils.h | 6 +- .../include/QualityControl/ServiceRequest.h | 34 ++ .../include/QualityControl/ServicesConfig.h | 29 ++ .../include/QualityControl/TaskFactory.h | 1 - .../QualityControl/UserCodeCardinality.h | 31 ++ .../include/QualityControl/stringUtils.h | 50 +++ Framework/src/Actor.cxx | 103 +++++ Framework/src/ActorHelpers.cxx | 48 +++ Framework/src/DataProcessorAdapter.cxx | 32 ++ Framework/src/InfrastructureSpecReader.cxx | 6 + Framework/test/testActor.cxx | 113 ++++++ Framework/test/testActorCallbacks.cxx | 129 ++++++ Framework/test/testActorHelpers.cxx | 142 +++++++ Framework/test/testActorTraits.cxx | 138 +++++++ Framework/test/testDataProcessorAdapter.cxx | 272 +++++++++++++ Framework/test/testStringUtils.cxx | 53 ++- 22 files changed, 1986 insertions(+), 17 deletions(-) create mode 100644 Framework/include/QualityControl/Actor.h create mode 100644 Framework/include/QualityControl/ActorHelpers.h create mode 100644 Framework/include/QualityControl/ActorTraits.h create mode 100644 Framework/include/QualityControl/Criticality.h create mode 100644 Framework/include/QualityControl/DataProcessorAdapter.h create mode 100644 Framework/include/QualityControl/ServiceRequest.h create mode 100644 Framework/include/QualityControl/ServicesConfig.h create mode 100644 Framework/include/QualityControl/UserCodeCardinality.h create mode 100644 Framework/src/Actor.cxx create mode 100644 Framework/src/ActorHelpers.cxx create mode 100644 Framework/src/DataProcessorAdapter.cxx create mode 100644 Framework/test/testActor.cxx create mode 100644 Framework/test/testActorCallbacks.cxx create mode 100644 Framework/test/testActorHelpers.cxx create mode 100644 Framework/test/testActorTraits.cxx create mode 100644 Framework/test/testDataProcessorAdapter.cxx diff --git a/Framework/CMakeLists.txt b/Framework/CMakeLists.txt index e2dd27055b..e659e344c1 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 0000000000..c289fea8db --- /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) + requires(std::derived_from>) + : 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() + { + // 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 0000000000..40a6bf683e --- /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 0000000000..5a31660aaa --- /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 0000000000..588b8bee65 --- /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 0000000000..2efd890e50 --- /dev/null +++ b/Framework/include/QualityControl/DataProcessorAdapter.h @@ -0,0 +1,197 @@ +// 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 { + // that's the default in DPL + } + } + + 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 fca66c3ce7..2e014117b8 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 0000000000..d04ee0d1aa --- /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 0000000000..9893a18457 --- /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 225c32b208..efb5c8086d 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 0000000000..e531ca6812 --- /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 82c5c8c8d5..974fb38f67 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 0000000000..6997ed8040 --- /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 0000000000..6ead020c5c --- /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 0000000000..df4d3f8bcd --- /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 91f18cd71c..05168abcbe 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 0000000000..587be121a2 --- /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 + consteval 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 0000000000..cebd25977e --- /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 0000000000..ef52069010 --- /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 0000000000..c1e3d38c0a --- /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 0000000000..a74398a599 --- /dev/null +++ b/Framework/test/testDataProcessorAdapter.cxx @@ -0,0 +1,272 @@ +// 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{}); + CHECK_FALSE(hasLabel(spec.labels, "expendable")); + } + + 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 7d320fb37c..7f1d770524 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 From ed7fe8c4a04612f329f1d00d203b05739a096a78 Mon Sep 17 00:00:00 2001 From: Piotr Konopka Date: Thu, 2 Apr 2026 15:19:50 +0200 Subject: [PATCH 2/4] keep clang happy even though I don't believe it's right Clang complains that: ``` SOURCES/QualityControl/2660/0/Framework/test/testActor.cxx /Volumes/build/alice-ci-workdir/qualitycontrol-o2/sw/SOURCES/QualityControl/2660/0/Framework/test/testActor.cxx:97:5: error: call to consteval function 'o2::quality_control::core::UnrequestedAccessActor::assertNoAccessToServices' is not a constant expression 97 | assertNoAccessToServices(); | ^ /Volumes/build/alice-ci-workdir/qualitycontrol-o2/sw/SOURCES/QualityControl/2660/0/Framework/test/testActor.cxx:97:5: note: implicit use of 'this' pointer is only allowed within the evaluation of a call to a 'constexpr' member function 1 error generated. ``` GCC is fine and is able to process it as a consteval expression. Let's keep both happy by using constexpr, given it's just a test. --- Framework/test/testActor.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/test/testActor.cxx b/Framework/test/testActor.cxx index 587be121a2..afe33b8f5d 100644 --- a/Framework/test/testActor.cxx +++ b/Framework/test/testActor.cxx @@ -84,7 +84,7 @@ class UnrequestedAccessActor : public Actor explicit UnrequestedAccessActor(const ServicesConfig& cfg) : Actor(cfg) {} template - consteval void assertNoAccessToServices() + constexpr void assertNoAccessToServices() { static_assert(!(requires(T& t) { t.getMonitoring(); })); static_assert(!(requires(T& t) { t.getBookkeeping(); })); From 4d8662bfc2eec7f0bb5d8e132db09414ee6bc649 Mon Sep 17 00:00:00 2001 From: Piotr Konopka Date: Thu, 2 Apr 2026 15:47:58 +0200 Subject: [PATCH 3/4] Assume "resilient" for critical data processors with uncertain upstream --- Framework/include/QualityControl/DataProcessorAdapter.h | 4 +++- Framework/test/testDataProcessorAdapter.cxx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Framework/include/QualityControl/DataProcessorAdapter.h b/Framework/include/QualityControl/DataProcessorAdapter.h index 2efd890e50..479d15c2bb 100644 --- a/Framework/include/QualityControl/DataProcessorAdapter.h +++ b/Framework/include/QualityControl/DataProcessorAdapter.h @@ -88,7 +88,9 @@ struct DataProcessorAdapter { if (!actor.isCritical()) { dataProcessor.labels.emplace_back("expendable"); } else { - // that's the default in DPL + // 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"); } } diff --git a/Framework/test/testDataProcessorAdapter.cxx b/Framework/test/testDataProcessorAdapter.cxx index a74398a599..bad158fdb2 100644 --- a/Framework/test/testDataProcessorAdapter.cxx +++ b/Framework/test/testDataProcessorAdapter.cxx @@ -122,7 +122,9 @@ TEST_CASE("DataProcessorAdapter::adapt uses actor critical flag for user-defined SECTION("critical actor instance") { auto spec = DataProcessorAdapter::adapt(UserDefinedCriticalityActor{ true }, "userdefined-critical", Inputs{}, Outputs{}, Options{}); - CHECK_FALSE(hasLabel(spec.labels, "expendable")); + // 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") From 42a5bf37885b5af6d32b411fe5d1a0a99c51ee71 Mon Sep 17 00:00:00 2001 From: Piotr Konopka Date: Thu, 2 Apr 2026 15:57:48 +0200 Subject: [PATCH 4/4] Move correct inheritance check to happen after the type is fully known to make clang happy again --- Framework/include/QualityControl/Actor.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/include/QualityControl/Actor.h b/Framework/include/QualityControl/Actor.h index c289fea8db..741db58beb 100644 --- a/Framework/include/QualityControl/Actor.h +++ b/Framework/include/QualityControl/Actor.h @@ -130,7 +130,6 @@ class Actor private: explicit Actor(const ServicesConfig& servicesConfig) - requires(std::derived_from>) : mServicesConfig{ servicesConfig }, mActivity{ servicesConfig.activity } { @@ -213,6 +212,7 @@ class Actor 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; });