diff --git a/Event/LumiEvent/CMakeLists.txt b/Event/LumiEvent/CMakeLists.txt index c2317d50099f747473e34b4b5a69073b85257953..c37ebad0e8752e5bdfae4d9dcd738699a82203b5 100644 --- a/Event/LumiEvent/CMakeLists.txt +++ b/Event/LumiEvent/CMakeLists.txt @@ -16,6 +16,8 @@ Event/LumiEvent gaudi_add_header_only_library(LumiEventLib LINK Gaudi::GaudiKernel + LHCb::LHCbKernel + nlohmann_json::nlohmann_json ) gaudi_add_dictionary(LumiEventDict @@ -23,3 +25,14 @@ gaudi_add_dictionary(LumiEventDict SELECTION dict/selection.xml LINK LHCb::LumiEventLib ) + +if(BUILD_TESTING) + gaudi_add_executable(test_LumiEventCounter + SOURCES + tests/src/test_LumiEventCounter.cpp + LINK + Boost::unit_test_framework + LumiEventLib + TEST + ) +endif() diff --git a/Event/LumiEvent/include/Event/LumiEventCounter.h b/Event/LumiEvent/include/Event/LumiEventCounter.h new file mode 100644 index 0000000000000000000000000000000000000000..51cbf0e9fede9ee4c1f3ba328fb240c6f6ad1368 --- /dev/null +++ b/Event/LumiEvent/include/Event/LumiEventCounter.h @@ -0,0 +1,97 @@ +/*****************************************************************************\ +* (c) Copyright 2023 CERN for the benefit of the LHCb Collaboration * +* * +* This software is distributed under the terms of the GNU General Public * +* Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING". * +* * +* In applying this licence, 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. * +\*****************************************************************************/ +#pragma once + +#include <Gaudi/MonitoringHub.h> +#include <Kernel/SynchronizedValue.h> +#include <algorithm> +#include <cstdint> +#include <map> +#include <nlohmann/json.hpp> +#include <string> +#include <utility> + +namespace LHCb { + /// Special counter to split counts of events by run. + /// + /// A LumiEventCounter instance can be used as any Gaudi::Accumulator::Counter. To increment the counter + /// for a given run number one has to invoke the method LumiEventCounter::inc(std::uint32_t) passing the + /// current run number and optionally the increase step (by default it's 1). + /// + /// Example of use: + /// ```cpp + /// #include <Event/LumiEventCounter.h> + /// #include <LHCbAlgs/Consumer.h> + /// #include <Event/ODIN.h> + /// struct LumiCountAlg final : public LHCb::Algorithm::Consumer<void( const LHCb::ODIN& )> { + /// LumiCountAlg( const std::string& name, ISvcLocator* pSvcLocator ) + /// : Consumer( name, pSvcLocator, KeyValue{"ODIN", ODINLocation::Default} ) {} + /// void operator()( const LHCb::ODIN& odin ) const override { m_lumiCount.inc( odin.runNumber() ); } + /// + /// mutable LHCb::LumiEventCounter m_lumiCount{this, "eventsByRun"}; + /// }; + /// ``` + class LumiEventCounter final { + public: + LumiEventCounter() = default; + + template <typename OWNER> + LumiEventCounter( OWNER* o, std::string name ) : m_monitoringHub{&o->serviceLocator()->monitoringHub()} { + m_monitoringHub->registerEntity( o->name(), std::move( name ), "LumiEventCounter", *this ); + } + + ~LumiEventCounter() { + if ( m_monitoringHub ) m_monitoringHub->removeEntity( *this ); + } + + nlohmann::json toJSON() const { return *this; } + void reset() { + m_counts.with_lock( []( map_type& counts ) { counts.clear(); } ); + } + + void inc( std::uint32_t run, std::size_t count = 1 ) { + m_counts.with_lock( [run, count]( map_type& counts ) { counts[run] += count; } ); + } + + std::size_t get( std::uint32_t run ) const { + return m_counts.with_lock( [run]( auto& counts ) -> std::size_t { + if ( auto it = counts.find( run ); it != counts.end() ) { + return it->second; + } else { + return 0; + } + } ); + } + + bool empty() const { + return m_counts.with_lock( []( auto& counts ) { + return std::all_of( counts.begin(), counts.end(), []( const auto& item ) { return item.second == 0; } ); + } ); + } + + private: + using map_type = std::map<std::uint32_t, std::size_t>; + cxx::SynchronizedValue<map_type> m_counts; + + Gaudi::Monitoring::Hub* m_monitoringHub{nullptr}; + friend void to_json( nlohmann::json&, const LumiEventCounter& ); + }; + + void to_json( nlohmann::json& j, const LumiEventCounter& c ) { + j["type"] = "LumiEventCounter"; + j["empty"] = c.empty(); + j["counts"] = c.m_counts.with_lock( []( auto& counts ) { + auto tmp = nlohmann::json::object(); + for ( const auto [run, count] : counts ) { tmp.emplace( std::to_string( run ), count ); } + return tmp; + } ); + } +} // namespace LHCb diff --git a/Event/LumiEvent/tests/src/test_LumiEventCounter.cpp b/Event/LumiEvent/tests/src/test_LumiEventCounter.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0da4bf35c72a9b147ca3393510c41780aa9234fc --- /dev/null +++ b/Event/LumiEvent/tests/src/test_LumiEventCounter.cpp @@ -0,0 +1,129 @@ +/*****************************************************************************\ +* (c) Copyright 2023 CERN for the benefit of the LHCb Collaboration * +* * +* This software is distributed under the terms of the GNU General Public * +* Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING". * +* * +* In applying this licence, 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. * +\*****************************************************************************/ +#include <Event/LumiEventCounter.h> + +#define BOOST_TEST_MODULE test_LumiEventCounter +#define BOOST_TEST_DYN_LINK +#include <boost/test/unit_test.hpp> + +#include <Gaudi/MonitoringHub.h> +#include <algorithm> +#include <utility> +#include <vector> + +using LC = LHCb::LumiEventCounter; + +BOOST_AUTO_TEST_CASE( empty ) { + LC cnt; + + BOOST_TEST( cnt.empty() ); + BOOST_TEST( cnt.get( 1000 ) == 0 ); + BOOST_TEST( cnt.empty() ); +} + +BOOST_AUTO_TEST_CASE( filling ) { + LC cnt; + + cnt.inc( 1000 ); + BOOST_TEST( !cnt.empty() ); + BOOST_TEST( cnt.get( 1000 ) == 1 ); + + cnt.inc( 1000 ); + cnt.inc( 1000 ); + BOOST_TEST( cnt.get( 1000 ) == 3 ); + + cnt.inc( 2002, 100 ); + BOOST_TEST( cnt.get( 1000 ) == 3 ); + BOOST_TEST( cnt.get( 2002 ) == 100 ); + + BOOST_TEST( cnt.get( 3001 ) == 0 ); + cnt.inc( 3001 ); + BOOST_TEST( cnt.get( 3001 ) == 1 ); + + BOOST_TEST( !cnt.empty() ); +} + +BOOST_AUTO_TEST_CASE( reset ) { + LC cnt; + + BOOST_TEST( cnt.empty() ); + cnt.inc( 1000 ); + BOOST_TEST( !cnt.empty() ); + BOOST_TEST( cnt.get( 1000 ) == 1 ); + cnt.reset(); + BOOST_TEST( cnt.empty() ); +} + +BOOST_AUTO_TEST_CASE( to_json ) { + LC cnt; + + nlohmann::json j = cnt; + BOOST_TEST( j["type"].get<std::string>() == "LumiEventCounter" ); + BOOST_TEST( j["empty"].get<bool>() ); + BOOST_TEST( j["counts"].empty() ); + + cnt.inc( 1000 ); + cnt.inc( 2000, 5 ); + j = cnt; + BOOST_TEST( !j["empty"].get<bool>() ); + BOOST_TEST( j["counts"].size() == 2 ); + BOOST_TEST( j["counts"]["1000"].get<std::size_t>() == 1 ); + BOOST_TEST( j["counts"]["2000"].get<std::size_t>() == 5 ); +} + +BOOST_AUTO_TEST_CASE( monitoring ) { + using Gaudi::Monitoring::Hub; + using Entity = Gaudi::Monitoring::Hub::Entity; + using Sink = Gaudi::Monitoring::Hub::Sink; + // Mock code for the test + struct MonitoringHub : Hub {}; + struct ServiceLocator { + MonitoringHub& monitoringHub() { return m_monitHub; } + MonitoringHub m_monitHub{}; + }; + struct Component { + ServiceLocator* serviceLocator() { return &m_serviceLocator; } + std::string name() const { return "owner"; } + ServiceLocator m_serviceLocator{}; + } alg; + struct MySink : Sink { + void registerEntity( Entity ent ) override { entities.emplace_back( std::move( ent ) ); } + void removeEntity( Entity const& ent ) override { + auto it = std::remove( entities.begin(), entities.end(), ent ); + entities.erase( it, entities.end() ); + } + std::vector<Entity> entities; + Hub& hub; + MySink( Hub& h ) : hub{h} { hub.addSink( this ); } + ~MySink() { hub.removeSink( this ); } + }; + MySink sink( alg.serviceLocator()->monitoringHub() ); + + BOOST_TEST( sink.entities.empty() ); + + { + LC cnt{&alg, "lumi_cnt"}; + BOOST_TEST( sink.entities.size() == 1 ); + + auto j = sink.entities[0].toJSON(); + BOOST_TEST( j["type"].get<std::string>() == "LumiEventCounter" ); + BOOST_TEST( j["empty"].get<bool>() ); + BOOST_TEST( j["counts"].empty() ); + + cnt.inc( 1000 ); + j = sink.entities[0].toJSON(); + BOOST_TEST( !j["empty"].get<bool>() ); + BOOST_TEST( j["counts"].size() == 1 ); + BOOST_TEST( j["counts"]["1000"].get<std::size_t>() == 1 ); + } + + BOOST_TEST( sink.entities.empty() ); +} diff --git a/Kernel/FileSummaryRecord/CMakeLists.txt b/Kernel/FileSummaryRecord/CMakeLists.txt index 914590e7d8c1a724ba62bcdbe2624c14487e0a76..92bc1075bebebfb61df9bb3a056b647d16e230b9 100644 --- a/Kernel/FileSummaryRecord/CMakeLists.txt +++ b/Kernel/FileSummaryRecord/CMakeLists.txt @@ -50,7 +50,9 @@ if(BUILD_TESTING) tests/src/FSRTestAlgs.cpp LINK Gaudi::GaudiKernel + LHCb::DAQEventLib LHCb::LHCbAlgsLib + LHCb::LumiEventLib ) lhcb_env(PRIVATE PREPEND PYTHONPATH ${CMAKE_CURRENT_SOURCE_DIR}/tests/python) diff --git a/Kernel/FileSummaryRecord/tests/python/FSRTests/write_lumi.py b/Kernel/FileSummaryRecord/tests/python/FSRTests/write_lumi.py new file mode 100644 index 0000000000000000000000000000000000000000..97bacdea5f411af59ab4e514d04103ccdd35bf22 --- /dev/null +++ b/Kernel/FileSummaryRecord/tests/python/FSRTests/write_lumi.py @@ -0,0 +1,143 @@ +############################################################################### +# (c) Copyright 2023 CERN for the benefit of the LHCb Collaboration # +# # +# This software is distributed under the terms of the GNU General Public # +# Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING". # +# # +# In applying this licence, 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. # +############################################################################### +import os +import json +from traceback import format_exc +from unittest import TestCase +from pprint import pformat + +FILENAMEFSR = f"{__name__}.fsr.json" +FILENAME = f"{__name__}.root" +FILENAMEJSON = f"{__name__}.json" + + +def checkDiff(a, b): + try: + TestCase().assertEqual(a, b) + except AssertionError as err: + return str(err) + + +def config(): + from PyConf.application import configure, configure_input, ApplicationOptions, root_writer + from PyConf.control_flow import CompositeNode + from PyConf.components import setup_component + from PyConf.Algorithms import LHCb__Tests__RunEventCountAlg, Gaudi__Examples__IntDataProducer, LHCb__Tests__FakeRunNumberProducer + from Configurables import ApplicationMgr + + options = ApplicationOptions(_enabled=False) + # No data from the input is used, but something should be there for the configuration + options.input_files = ["dummy_input_file_name.dst"] + options.input_type = 'ROOT' + options.output_file = FILENAME + options.output_type = 'ROOT' + options.data_type = 'Upgrade' + options.dddb_tag = 'upgrade/dddb-20220705' + options.conddb_tag = 'upgrade/sim-20220705-vc-mu100' + options.geometry_version = 'trunk' + options.conditions_version = 'master' + options.simulation = True + options.evt_max = 5 + options.monitoring_file = FILENAMEJSON + # options.output_level = 2 + + config = configure_input(options) + + app = ApplicationMgr() + app.EvtSel = "NONE" # ignore input configuration + app.ExtSvc.append( + config.add( + setup_component( + "LHCb__FSR__Sink", + instance_name="FileSummaryRecord", + AcceptRegex=r"^LumiCounter\.eventsByRun$", + OutputFile=FILENAMEFSR, + ))) + + producer = Gaudi__Examples__IntDataProducer() + odin_producer = LHCb__Tests__FakeRunNumberProducer() + + cf = CompositeNode("test", [ + odin_producer, + LHCb__Tests__RunEventCountAlg( + name="LumiCounter", ODIN=odin_producer.ODIN), + producer, + root_writer(options.output_file, [producer.OutputLocation]), + ]) + app.OutStream.clear() + config.update(configure(options, cf)) + + # make sure the histogram file is not already there + for name in [FILENAME, FILENAMEFSR, FILENAMEJSON]: + if os.path.exists(name): + os.remove(name) + + +def check(causes, result): + result["root_output_file"] = FILENAME + + missing_files = [ + name for name in [FILENAME, FILENAMEFSR, FILENAMEJSON] + if not os.path.exists(name) + ] + if missing_files: + causes.append("missing output file(s)") + result["missing_files"] = ", ".join(missing_files) + return False + + expected = { + "LumiCounter.eventsByRun": { + "counts": { + "1": 1, + "2": 1, + "3": 1, + "4": 1, + "5": 1 + }, + "empty": False, + "type": "LumiEventCounter" + } + } + + try: + import ROOT + + fsr_dump = json.load(open(FILENAMEFSR)) + + f = ROOT.TFile.Open(FILENAME) + fsr_root = json.loads(str(f.FileSummaryRecord)) + + except Exception as err: + causes.append("failure in root file") + result["python_exception"] = result.Quote(format_exc()) + return False + + result["FSR"] = result.Quote(pformat(fsr_dump)) + + checking = "no check yet" + try: + guid = fsr_dump.get("guid") + assert guid, "missing or invalid GUID in FSR dump" + + expected["guid"] = guid # GUID is random + + tester = TestCase() + checking = "JSON dump" + tester.assertEqual(expected, fsr_dump) + checking = "ROOT file" + tester.assertEqual(expected, fsr_root) + + except AssertionError as err: + causes.append("FSR content") + result[f"FSR problem ({checking})"] = result.Quote(str(err)) + return False + + return True diff --git a/Kernel/FileSummaryRecord/tests/qmtest/filesummaryrecord.qms/write_lumi.qmt b/Kernel/FileSummaryRecord/tests/qmtest/filesummaryrecord.qms/write_lumi.qmt new file mode 100644 index 0000000000000000000000000000000000000000..e00492c5e7c51b2876a7285348dfbcb66f7ddaec --- /dev/null +++ b/Kernel/FileSummaryRecord/tests/qmtest/filesummaryrecord.qms/write_lumi.qmt @@ -0,0 +1,20 @@ +<?xml version="1.0" ?><!DOCTYPE extension PUBLIC '-//QM/2.3/Extension//EN' 'http://www.codesourcery.com/qm/dtds/2.3/-//qm/2.3/extension//en.dtd'> +<!-- + (c) Copyright 2023 CERN for the benefit of the LHCb Collaboration + + This software is distributed under the terms of the GNU General Public + Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING". + + In applying this licence, 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. +--> +<extension class="GaudiTest.GaudiExeTest" kind="test"> +<argument name="program"><text>gaudirun.py</text></argument> +<argument name="args"><set><text>FSRTests.write_lumi:config</text></set></argument> +<argument name="use_temp_dir"><enumeral>true</enumeral></argument> +<argument name="validator"><text> +from FSRTests.write_lumi import check +check(causes, result) +</text></argument> +</extension> diff --git a/Kernel/FileSummaryRecord/tests/src/FSRTestAlgs.cpp b/Kernel/FileSummaryRecord/tests/src/FSRTestAlgs.cpp index bcd8db720474f97cd111c3afeb57a483c3f60002..51aef568f8f57784bd7d7253890e5c851b655f62 100644 --- a/Kernel/FileSummaryRecord/tests/src/FSRTestAlgs.cpp +++ b/Kernel/FileSummaryRecord/tests/src/FSRTestAlgs.cpp @@ -1,5 +1,5 @@ /*****************************************************************************\ -* (c) Copyright 2022 CERN for the benefit of the LHCb Collaboration * +* (c) Copyright 2022-2023 CERN for the benefit of the LHCb Collaboration * * * * This software is distributed under the terms of the GNU General Public * * Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING". * @@ -8,6 +8,8 @@ * granted to it by virtue of its status as an Intergovernmental Organization * * or submit itself to any jurisdiction. * \*****************************************************************************/ +#include <Event/LumiEventCounter.h> +#include <Event/ODIN.h> #include <Gaudi/Accumulators.h> #include <LHCbAlgs/Consumer.h> @@ -20,4 +22,14 @@ namespace LHCb ::Tests { mutable Gaudi::Accumulators::Counter<> m_events{this, "count"}; }; DECLARE_COMPONENT( EventCountAlg ) + + /// Simple algorithm that counts how many times it is invoked per run + struct RunEventCountAlg final : public LHCb::Algorithm::Consumer<void( const LHCb::ODIN& )> { + RunEventCountAlg( const std::string& name, ISvcLocator* pSvcLocator ) + : Consumer( name, pSvcLocator, KeyValue{"ODIN", ODINLocation::Default} ) {} + void operator()( const LHCb::ODIN& odin ) const override { m_lumiCount.inc( odin.runNumber() ); } + + mutable LHCb::LumiEventCounter m_lumiCount{this, "eventsByRun"}; + }; + DECLARE_COMPONENT( RunEventCountAlg ) } // namespace LHCb::Tests