From f91231827a6ff1f8426cd35a27be437ddcbe57ba Mon Sep 17 00:00:00 2001 From: Giulio Eulisse <10544+ktf@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:25:12 +0100 Subject: [PATCH 1/3] DPL: protect against missing monitoring --- Framework/AnalysisSupport/src/DataInputDirector.cxx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Framework/AnalysisSupport/src/DataInputDirector.cxx b/Framework/AnalysisSupport/src/DataInputDirector.cxx index ace4565449c4b..7027655b7abe7 100644 --- a/Framework/AnalysisSupport/src/DataInputDirector.cxx +++ b/Framework/AnalysisSupport/src/DataInputDirector.cxx @@ -314,7 +314,9 @@ void DataInputDescriptor::printFileOpening() monitoringInfo += fmt::format(",se={},open_time={:.1f}", alienFile->GetSE(), alienFile->GetElapsed()); } #endif - mContext.monitoring->send(o2::monitoring::Metric{monitoringInfo, "aod-file-open-info"}.addTag(o2::monitoring::tags::Key::Subsystem, o2::monitoring::tags::Value::DPL)); + if (mContext.monitoring) { + mContext.monitoring->send(o2::monitoring::Metric{monitoringInfo, "aod-file-open-info"}.addTag(o2::monitoring::tags::Key::Subsystem, o2::monitoring::tags::Value::DPL)); + } LOGP(info, "Opening file: {}", monitoringInfo); } @@ -335,7 +337,9 @@ void DataInputDescriptor::printFileStatistics() monitoringInfo += fmt::format(",se={},open_time={:.1f}", alienFile->GetSE(), alienFile->GetElapsed()); } #endif - mContext.monitoring->send(o2::monitoring::Metric{monitoringInfo, "aod-file-read-info"}.addTag(o2::monitoring::tags::Key::Subsystem, o2::monitoring::tags::Value::DPL)); + if (mContext.monitoring) { + mContext.monitoring->send(o2::monitoring::Metric{monitoringInfo, "aod-file-read-info"}.addTag(o2::monitoring::tags::Key::Subsystem, o2::monitoring::tags::Value::DPL)); + } LOGP(info, "Read info: {}", monitoringInfo); } From c9d07d8384e130ffc78c538b819e9d6e5cc964b0 Mon Sep 17 00:00:00 2001 From: Giulio Eulisse <10544+ktf@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:25:12 +0100 Subject: [PATCH 2/3] DPL: allow non-owning TFileFileSystem Given this filesystem is really a virtual entity, it makes sense to allow for actual ownership of the TFile elsewhere. --- .../Core/include/Framework/RootArrowFilesystem.h | 3 ++- Framework/Core/src/RootArrowFilesystem.cxx | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Framework/Core/include/Framework/RootArrowFilesystem.h b/Framework/Core/include/Framework/RootArrowFilesystem.h index 5aceaed077001..07aaa348c220a 100644 --- a/Framework/Core/include/Framework/RootArrowFilesystem.h +++ b/Framework/Core/include/Framework/RootArrowFilesystem.h @@ -146,7 +146,7 @@ class TFileFileSystem : public VirtualRootFileSystemBase public: arrow::Result GetFileInfo(const std::string& path) override; - TFileFileSystem(TDirectoryFile* f, size_t readahead, RootObjectReadingFactory&); + TFileFileSystem(TDirectoryFile* f, size_t readahead, RootObjectReadingFactory&, bool ownsFile = true); ~TFileFileSystem() override; @@ -172,6 +172,7 @@ class TFileFileSystem : public VirtualRootFileSystemBase private: TDirectoryFile* mFile; RootObjectReadingFactory& mObjectFactory; + bool mOwnsFile = true; }; class TBufferFileFS : public VirtualRootFileSystemBase diff --git a/Framework/Core/src/RootArrowFilesystem.cxx b/Framework/Core/src/RootArrowFilesystem.cxx index 403e393ec6090..6976c710062e6 100644 --- a/Framework/Core/src/RootArrowFilesystem.cxx +++ b/Framework/Core/src/RootArrowFilesystem.cxx @@ -34,18 +34,21 @@ namespace o2::framework { using arrow::Status; -TFileFileSystem::TFileFileSystem(TDirectoryFile* f, size_t readahead, RootObjectReadingFactory& factory) +TFileFileSystem::TFileFileSystem(TDirectoryFile* f, size_t readahead, RootObjectReadingFactory& factory, bool ownsFile) : VirtualRootFileSystemBase(), mFile(f), - mObjectFactory(factory) + mObjectFactory(factory), + mOwnsFile(ownsFile) { ((TFile*)mFile)->SetReadaheadSize(50 * 1024 * 1024); } TFileFileSystem::~TFileFileSystem() { - mFile->Close(); - delete mFile; + if (mOwnsFile) { + mFile->Close(); + delete mFile; + } } std::shared_ptr TFileFileSystem::GetObjectHandler(arrow::dataset::FileSource source) From 6ada843480a52bbf6c69dd00fc7764dabc0d9c91 Mon Sep 17 00:00:00 2001 From: Giulio Eulisse <10544+ktf@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:25:12 +0100 Subject: [PATCH 3/3] DPL: allow determining the origin from user provide input In order to support embedding, we need allow the user to provide a mapping between the desired origin and the level in the parent file chain where the table should be found. --- Framework/AnalysisSupport/CMakeLists.txt | 9 ++ .../src/AODJAlienReaderHelpers.cxx | 34 ++++- .../AnalysisSupport/src/DataInputDirector.cxx | 84 +++++++++-- .../AnalysisSupport/src/DataInputDirector.h | 26 +++- .../test/test_NavigateToLevel.cxx | 135 ++++++++++++++++++ Framework/Core/src/Plugin.cxx | 3 + 6 files changed, 268 insertions(+), 23 deletions(-) create mode 100644 Framework/AnalysisSupport/test/test_NavigateToLevel.cxx diff --git a/Framework/AnalysisSupport/CMakeLists.txt b/Framework/AnalysisSupport/CMakeLists.txt index 6024134a5495d..956c4a44c5684 100644 --- a/Framework/AnalysisSupport/CMakeLists.txt +++ b/Framework/AnalysisSupport/CMakeLists.txt @@ -47,6 +47,15 @@ o2_add_test(DataInputDirector NAME test_Framework_test_DataInputDirector LABELS framework PUBLIC_LINK_LIBRARIES O2::FrameworkAnalysisSupport) +add_executable(o2-test-framework-analysis-support + test/test_NavigateToLevel.cxx) +target_link_libraries(o2-test-framework-analysis-support PRIVATE O2::FrameworkAnalysisSupport O2::Catch2) + +get_filename_component(outdir ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../tests ABSOLUTE) +set_property(TARGET o2-test-framework-analysis-support PROPERTY RUNTIME_OUTPUT_DIRECTORY ${outdir}) + +add_test(NAME framework:analysis-support COMMAND o2-test-framework-analysis-support) + o2_add_test(TableToTree NAME benchmark_TableToTree SOURCES test/benchmark_TableToTree.cxx COMPONENT_NAME Framework diff --git a/Framework/AnalysisSupport/src/AODJAlienReaderHelpers.cxx b/Framework/AnalysisSupport/src/AODJAlienReaderHelpers.cxx index 57a397822d167..8fde9f52a0e09 100644 --- a/Framework/AnalysisSupport/src/AODJAlienReaderHelpers.cxx +++ b/Framework/AnalysisSupport/src/AODJAlienReaderHelpers.cxx @@ -10,7 +10,10 @@ // or submit itself to any jurisdiction. #include "AODJAlienReaderHelpers.h" +#include #include +#include +#include #include "Framework/TableTreeHelpers.h" #include "Framework/AnalysisHelpers.h" #include "Framework/DataProcessingStats.h" @@ -111,10 +114,31 @@ AlgorithmSpec AODJAlienReaderHelpers::rootFileReaderCallback(ConfigContext const if (ctx.options().isSet("aod-parent-access-level")) { parentAccessLevel = ctx.options().get("aod-parent-access-level"); } - auto callback = AlgorithmSpec{adaptStateful([parentFileReplacement, parentAccessLevel](ConfigParamRegistry const& options, - DeviceSpec const& spec, - Monitoring& monitoring, - DataProcessingStats& stats) { + std::vector> originLevelMapping; + if (ctx.options().isSet("aod-origin-level-mapping")) { + auto originLevelMappingStr = ctx.options().get("aod-origin-level-mapping"); + for (auto pairRange : originLevelMappingStr | std::views::split(',')) { + std::string_view pair{pairRange.begin(), pairRange.end()}; + auto colonPos = pair.find(':'); + if (colonPos == std::string_view::npos) { + LOGP(fatal, "Badly formatted aod-origin-level-mapping entry: \"{}\"", pair); + continue; + } + std::string key(pair.substr(0, colonPos)); + std::string_view valueStr = pair.substr(colonPos + 1); + int value{}; + auto [ptr, ec] = std::from_chars(valueStr.data(), valueStr.data() + valueStr.size(), value); + if (ec == std::errc{}) { + originLevelMapping.emplace_back(std::move(key), value); + } else { + LOGP(fatal, "Unable to parse level in aod-origin-level-mapping entry: \"{}\"", pair); + } + } + } + auto callback = AlgorithmSpec{adaptStateful([parentFileReplacement, parentAccessLevel, originLevelMapping](ConfigParamRegistry const& options, + DeviceSpec const& spec, + Monitoring& monitoring, + DataProcessingStats& stats) { // FIXME: not actually needed, since data processing stats can specify that we should // send the initial value. stats.updateStats({static_cast(ProcessingStatsId::ARROW_BYTES_CREATED), DataProcessingStats::Op::Set, 0}); @@ -134,7 +158,7 @@ AlgorithmSpec AODJAlienReaderHelpers::rootFileReaderCallback(ConfigContext const auto maxRate = options.get("aod-max-io-rate"); // create a DataInputDirector - auto didir = std::make_shared(std::vector{filename}, DataInputDirectorContext{&monitoring, parentAccessLevel, parentFileReplacement}); + auto didir = std::make_shared(std::vector{filename}, DataInputDirectorContext{&monitoring, parentAccessLevel, parentFileReplacement, originLevelMapping}); if (options.isSet("aod-reader-json")) { auto jsonFile = options.get("aod-reader-json"); if (!didir->readJson(jsonFile)) { diff --git a/Framework/AnalysisSupport/src/DataInputDirector.cxx b/Framework/AnalysisSupport/src/DataInputDirector.cxx index 7027655b7abe7..46674f19400a6 100644 --- a/Framework/AnalysisSupport/src/DataInputDirector.cxx +++ b/Framework/AnalysisSupport/src/DataInputDirector.cxx @@ -122,7 +122,7 @@ void DataInputDescriptor::addFileNameHolder(FileNameHolder* fn) mfilenames.emplace_back(fn); } -bool DataInputDescriptor::setFile(int counter, std::string_view origin) +bool DataInputDescriptor::setFile(int counter, int wantedParentLevel, std::string_view origin) { // no files left if (counter >= getNumberInputfiles()) { @@ -133,7 +133,9 @@ bool DataInputDescriptor::setFile(int counter, std::string_view origin) // of the filename. In the future we might expand this for proper rewriting of the // filename based on the origin and the original file information. std::string filename = mfilenames[counter]->fileName; - if (!origin.starts_with("AOD")) { + // In case we do not need to remap parent levels, the requested origin is what + // drives the filename. + if (wantedParentLevel == -1 && !origin.starts_with("AOD")) { filename = std::regex_replace(filename, std::regex("[.]root$"), fmt::format("_{}.root", origin)); } @@ -146,7 +148,19 @@ bool DataInputDescriptor::setFile(int counter, std::string_view origin) closeInputFile(); } - mCurrentFilesystem = std::make_shared(TFile::Open(filename.c_str()), 50 * 1024 * 1024, mFactory); + TFile* tfile = nullptr; + bool externalFile = false; + for (auto& [name, f] : mContext.openFiles) { + if (name == filename) { + tfile = f; + externalFile = true; + break; + } + } + if (tfile == nullptr) { + tfile = TFile::Open(filename.c_str()); + } + mCurrentFilesystem = std::make_shared(tfile, 50 * 1024 * 1024, mFactory, !externalFile); if (!mCurrentFilesystem.get()) { throw std::runtime_error(fmt::format("Couldn't open file \"{}\"!", filename)); } @@ -218,11 +232,11 @@ bool DataInputDescriptor::setFile(int counter, std::string_view origin) return true; } -uint64_t DataInputDescriptor::getTimeFrameNumber(int counter, int numTF, std::string_view origin) +uint64_t DataInputDescriptor::getTimeFrameNumber(int counter, int numTF, int wantedParentLevel, std::string_view wantedOrigin) { // open file - if (!setFile(counter, origin)) { + if (!setFile(counter, wantedParentLevel, wantedOrigin)) { return 0ul; } @@ -234,10 +248,32 @@ uint64_t DataInputDescriptor::getTimeFrameNumber(int counter, int numTF, std::st return (mfilenames[counter]->listOfTimeFrameNumbers)[numTF]; } -arrow::dataset::FileSource DataInputDescriptor::getFileFolder(int counter, int numTF, std::string_view origin) +std::pair DataInputDescriptor::navigateToLevel(int counter, int numTF, int wantedParentLevel, std::string_view wantedOrigin) +{ + if (!setFile(counter, wantedParentLevel, wantedOrigin)) { + return {nullptr, -1}; + } + auto folderName = fmt::format("DF_{}", mfilenames[counter]->listOfTimeFrameNumbers[numTF]); + auto parentFile = getParentFile(counter, numTF, "", wantedParentLevel, wantedOrigin); + if (parentFile == nullptr) { + return {nullptr, -1}; + } + return {parentFile, parentFile->findDFNumber(0, folderName)}; +} + +arrow::dataset::FileSource DataInputDescriptor::getFileFolder(int counter, int numTF, int wantedParentLevel, std::string_view wantedOrigin) { + // If mapped to a parent level deeper than current, skip directly to the right level. + if (wantedParentLevel != -1 && mLevel < wantedParentLevel) { + auto [parentFile, parentNumTF] = navigateToLevel(counter, numTF, wantedParentLevel, wantedOrigin); + if (parentFile == nullptr || parentNumTF == -1) { + return {}; + } + return parentFile->getFileFolder(0, parentNumTF, wantedParentLevel, wantedOrigin); + } + // open file - if (!setFile(counter, origin)) { + if (!setFile(counter, wantedParentLevel, wantedOrigin)) { return {}; } @@ -251,7 +287,7 @@ arrow::dataset::FileSource DataInputDescriptor::getFileFolder(int counter, int n return {fmt::format("DF_{}", mfilenames[counter]->listOfTimeFrameNumbers[numTF]), mCurrentFilesystem}; } -DataInputDescriptor* DataInputDescriptor::getParentFile(int counter, int numTF, std::string treename, std::string_view origin) +DataInputDescriptor* DataInputDescriptor::getParentFile(int counter, int numTF, std::string treename, int wantedParentLevel, std::string_view wantedOrigin) { if (!mParentFileMap) { // This file has no parent map @@ -288,7 +324,7 @@ DataInputDescriptor* DataInputDescriptor::getParentFile(int counter, int numTF, mParentFile->mdefaultFilenamesPtr = new std::vector; mParentFile->mdefaultFilenamesPtr->emplace_back(makeFileNameHolder(parentFileName->GetString().Data())); mParentFile->fillInputfiles(); - mParentFile->setFile(0, origin); + mParentFile->setFile(0, wantedParentLevel, wantedOrigin); return mParentFile; } @@ -450,8 +486,26 @@ struct CalculateDelta { bool DataInputDescriptor::readTree(DataAllocator& outputs, header::DataHeader dh, int counter, int numTF, std::string treename, size_t& totalSizeCompressed, size_t& totalSizeUncompressed) { CalculateDelta t(mIOTime); - std::string origin = dh.dataOrigin.as(); - auto folder = getFileFolder(counter, numTF, origin); + std::string wantedOrigin = dh.dataOrigin.as(); + int wantedLevel = mContext.levelForOrigin(wantedOrigin); + + // If this origin is mapped to a parent level deeper than current, skip directly without + // attempting to read from this level. + if (wantedLevel != -1 && mLevel < wantedLevel) { + auto [parentFile, parentNumTF] = navigateToLevel(counter, numTF, wantedLevel, wantedOrigin); + if (parentFile == nullptr) { + auto rootFS = std::dynamic_pointer_cast(mCurrentFilesystem); + throw std::runtime_error(fmt::format(R"(No parent file found for "{}" while looking for level {} in "{}")", treename, wantedLevel, rootFS->GetFile()->GetName())); + } + if (parentNumTF == -1) { + auto parentRootFS = std::dynamic_pointer_cast(parentFile->mCurrentFilesystem); + throw std::runtime_error(fmt::format(R"(DF not found in parent file "{}")", parentRootFS->GetFile()->GetName())); + } + t.deactivate(); + return parentFile->readTree(outputs, dh, 0, parentNumTF, treename, totalSizeCompressed, totalSizeUncompressed); + } + + auto folder = getFileFolder(counter, numTF, wantedLevel, wantedOrigin); if (!folder.filesystem()) { t.deactivate(); return false; @@ -484,7 +538,7 @@ bool DataInputDescriptor::readTree(DataAllocator& outputs, header::DataHeader dh if (!format) { t.deactivate(); LOGP(debug, "Could not find tree {}. Trying in parent file.", fullpath.path()); - auto parentFile = getParentFile(counter, numTF, treename, origin); + auto parentFile = getParentFile(counter, numTF, treename, wantedLevel, wantedOrigin); if (parentFile != nullptr) { int parentNumTF = parentFile->findDFNumber(0, folder.path()); if (parentNumTF == -1) { @@ -817,8 +871,9 @@ arrow::dataset::FileSource DataInputDirector::getFileFolder(header::DataHeader d didesc = mdefaultDataInputDescriptor; } std::string origin = dh.dataOrigin.as(); + int wantedLevel = mContext.levelForOrigin(origin); - return didesc->getFileFolder(counter, numTF, origin); + return didesc->getFileFolder(counter, numTF, wantedLevel, origin); } int DataInputDirector::getTimeFramesInFile(header::DataHeader dh, int counter) @@ -840,8 +895,9 @@ uint64_t DataInputDirector::getTimeFrameNumber(header::DataHeader dh, int counte didesc = mdefaultDataInputDescriptor; } std::string origin = dh.dataOrigin.as(); + int wantedLevel = mContext.levelForOrigin(origin); - return didesc->getTimeFrameNumber(counter, numTF, origin); + return didesc->getTimeFrameNumber(counter, numTF, wantedLevel, origin); } bool DataInputDirector::readTree(DataAllocator& outputs, header::DataHeader dh, int counter, int numTF, size_t& totalSizeCompressed, size_t& totalSizeUncompressed) diff --git a/Framework/AnalysisSupport/src/DataInputDirector.h b/Framework/AnalysisSupport/src/DataInputDirector.h index 2d63a1c71ea77..18ab5c0c1382e 100644 --- a/Framework/AnalysisSupport/src/DataInputDirector.h +++ b/Framework/AnalysisSupport/src/DataInputDirector.h @@ -21,6 +21,7 @@ #include #include +#include #include "rapidjson/fwd.h" namespace o2::monitoring @@ -44,6 +45,20 @@ struct DataInputDirectorContext { o2::monitoring::Monitoring* monitoring = nullptr; int allowedParentLevel = 0; std::string parentFileReplacement = ""; + std::vector> parentLevelToOrigin = {}; + // Optional registry of pre-opened TFiles (keyed by name) used to bypass + // TFile::Open for testing with in-memory TMemFile instances. + std::vector> openFiles = {}; + + int levelForOrigin(std::string_view origin) const + { + for (auto& [o, level] : parentLevelToOrigin) { + if (o == origin) { + return level; + } + } + return -1; + } }; class DataInputDescriptor @@ -71,7 +86,7 @@ class DataInputDescriptor void addFileNameHolder(FileNameHolder* fn); int fillInputfiles(); - bool setFile(int counter, std::string_view origin); + bool setFile(int counter, int wantedParentLevel, std::string_view wantedOrigin); // getters std::string getInputfilesFilename(); @@ -81,9 +96,12 @@ class DataInputDescriptor int getNumberTimeFrames() { return mtotalNumberTimeFrames; } int findDFNumber(int file, std::string dfName); - uint64_t getTimeFrameNumber(int counter, int numTF, std::string_view origin); - arrow::dataset::FileSource getFileFolder(int counter, int numTF, std::string_view origin); - DataInputDescriptor* getParentFile(int counter, int numTF, std::string treename, std::string_view origin); + uint64_t getTimeFrameNumber(int counter, int numTF, int wantedParentLevel, std::string_view wantedOrigin); + arrow::dataset::FileSource getFileFolder(int counter, int numTF, int wantedParentLevel, std::string_view wantedOrigin); + // Open the current file to populate the parent map, then return the parent descriptor and + // the TF index within it that corresponds to numTF at this level. Returns {nullptr, -1} on failure. + std::pair navigateToLevel(int counter, int numTF, int wantedParentLevel, std::string_view wantedOrigin); + DataInputDescriptor* getParentFile(int counter, int numTF, std::string treename, int wantedParentLevel, std::string_view wantedOrigin); int getTimeFramesInFile(int counter); int getReadTimeFramesInFile(int counter); diff --git a/Framework/AnalysisSupport/test/test_NavigateToLevel.cxx b/Framework/AnalysisSupport/test/test_NavigateToLevel.cxx new file mode 100644 index 0000000000000..0072ee3b67d37 --- /dev/null +++ b/Framework/AnalysisSupport/test/test_NavigateToLevel.cxx @@ -0,0 +1,135 @@ +// 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. + +#include + +#include "../src/DataInputDirector.h" + +#include +#include +#include +#include + +using namespace o2::framework; + +// Tests for DataInputDirectorContext::levelForOrigin + +TEST_CASE("levelForOrigin empty mapping") +{ + DataInputDirectorContext ctx; + CHECK(ctx.levelForOrigin("AOD") == -1); + CHECK(ctx.levelForOrigin("DYN") == -1); +} + +TEST_CASE("levelForOrigin single entry") +{ + DataInputDirectorContext ctx; + ctx.parentLevelToOrigin = {{"DYN", 1}}; + CHECK(ctx.levelForOrigin("DYN") == 1); + CHECK(ctx.levelForOrigin("AOD") == -1); +} + +TEST_CASE("levelForOrigin multiple entries") +{ + DataInputDirectorContext ctx; + ctx.parentLevelToOrigin = {{"DYN", 1}, {"EMB", 2}, {"EXT", 1}}; + CHECK(ctx.levelForOrigin("DYN") == 1); + CHECK(ctx.levelForOrigin("EMB") == 2); + CHECK(ctx.levelForOrigin("EXT") == 1); + CHECK(ctx.levelForOrigin("AOD") == -1); + CHECK(ctx.levelForOrigin("") == -1); +} + +// Tests for DataInputDescriptor::navigateToLevel + +TEST_CASE("navigateToLevel returns null with no input files") +{ + // With no input files setFile fails immediately → {nullptr, -1} + DataInputDirectorContext ctx; + ctx.allowedParentLevel = 2; + DataInputDescriptor desc(false, 0, ctx); + + auto [parentFile, parentNumTF] = desc.navigateToLevel(0, 0, 1, "DYN"); + CHECK(parentFile == nullptr); + CHECK(parentNumTF == -1); +} + +// --------------------------------------------------------------------------- +// Helpers: build an AO2D-shaped TMemFile with one DF directory. +// The AO2D format uses top-level TDirectory entries named DF_. +// An optional "parentFiles" TMap maps each DF name to its parent file path. +// --------------------------------------------------------------------------- + +static TMemFile* makeAODFile(const char* name) +{ + auto* f = new TMemFile(name, "RECREATE"); + f->mkdir("DF_1"); + f->Write(); + return f; +} + +static TMemFile* makeAODFileWithParent(const char* name, const char* parentName) +{ + auto* f = new TMemFile(name, "RECREATE"); + f->mkdir("DF_1"); + auto* parentMap = new TMap(); + parentMap->Add(new TObjString("DF_1"), new TObjString(parentName)); + parentMap->Write("parentFiles", TObject::kSingleKey); + f->Write(); + return f; +} + +TEST_CASE("navigateToLevel finds parent TMemFile") +{ + // child.root DF_1 parentFiles: {DF_1 -> parent.root} + // parent.root DF_1 + auto* parentMF = makeAODFile("parent.root"); + auto* childMF = makeAODFileWithParent("child.root", "parent.root"); + + DataInputDirectorContext ctx; + ctx.allowedParentLevel = 2; + ctx.openFiles = {{"child.root", childMF}, {"parent.root", parentMF}}; + + DataInputDescriptor desc(false, 0, ctx); + desc.addFileNameHolder(makeFileNameHolder("child.root")); + + auto [parentDesc, parentNumTF] = desc.navigateToLevel(0, 0, 1, "AOD"); + + REQUIRE(parentDesc != nullptr); + // DF_1 is the only timeframe in the parent, so its index is 0 + CHECK(parentNumTF == 0); +} + +TEST_CASE("navigateToLevel returns -1 for missing DF in parent") +{ + // child has DF_2 but parent only has DF_1 — findDFNumber returns -1 + auto* parentMF = makeAODFile("parent2.root"); + + auto* childMF = new TMemFile("child2.root", "RECREATE"); + childMF->mkdir("DF_2"); + auto* parentMap = new TMap(); + parentMap->Add(new TObjString("DF_2"), new TObjString("parent2.root")); + parentMap->Write("parentFiles", TObject::kSingleKey); + childMF->Write(); + + DataInputDirectorContext ctx; + ctx.allowedParentLevel = 2; + ctx.openFiles = {{"child2.root", childMF}, {"parent2.root", parentMF}}; + + DataInputDescriptor desc(false, 0, ctx); + desc.addFileNameHolder(makeFileNameHolder("child2.root")); + + auto [parentDesc, parentNumTF] = desc.navigateToLevel(0, 0, 1, "AOD"); + + // Parent has DF_1 but child references DF_2 — not found in parent + REQUIRE(parentDesc != nullptr); + CHECK(parentNumTF == -1); +} diff --git a/Framework/Core/src/Plugin.cxx b/Framework/Core/src/Plugin.cxx index 8ed683d501906..503133442e794 100644 --- a/Framework/Core/src/Plugin.cxx +++ b/Framework/Core/src/Plugin.cxx @@ -168,6 +168,9 @@ struct DiscoverAODOptionsInCommandLine : o2::framework::ConfigDiscoveryPlugin { if (key == "aod-parent-access-level") { results.push_back(ConfigParamSpec{"aod-parent-access-level", VariantType::String, value, {"Allow parent file access up to specified level. Default: no (0)"}}); } + if (key == "aod-origin-level-mapping") { + results.push_back(ConfigParamSpec{"aod-origin-level-mapping", VariantType::String, value, {"Map origin to parent level for AOD reading. Syntax: ORIGIN:LEVEL[,ORIGIN2:LEVEL2,...]. E.g. \"DYN:1\"."}}); + } } if (injectOption) { results.push_back(ConfigParamSpec{"aod-writer-compression", VariantType::Int, 505, {"AOD Compression options"}});