From 20c5e4899e709df50cb1ba4e224957a15707f19b Mon Sep 17 00:00:00 2001 From: Florine de Geus Date: Wed, 28 Oct 2026 15:18:33 +0100 Subject: [PATCH 1/2] [io] Fix merging of objects in nested directories Instead of comparing the *file* of the source directory to the target directory, we should be comparing it to the *file* of the target directory. (cherry picked from commit 2ebe32768bd2696e00efce5c686dc20a28971fba) --- io/io/src/TFileMerger.cxx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/io/io/src/TFileMerger.cxx b/io/io/src/TFileMerger.cxx index b49aab14bf23d..ea2b7e8e81bc0 100644 --- a/io/io/src/TFileMerger.cxx +++ b/io/io/src/TFileMerger.cxx @@ -809,7 +809,7 @@ Bool_t TFileMerger::MergeOne(TDirectory *target, TList *sourcelist, Int_t type, // and we are in incremental mode (because it will be reused // and has not been written to disk (for performance reason). // coverity[var_deref_model] the IsA()->InheritsFrom guarantees that the dynamic_cast will succeed. - if (ownobj && (!(type & kIncremental) || dirobj->GetFile() != target)) { + if (ownobj && (!(type & kIncremental) || dirobj->GetFile() != target->GetFile())) { dirobj->ResetBit(kMustCleanup); delete dirobj; } @@ -1147,4 +1147,3 @@ void TFileMerger::SetMsgPrefix(const char *prefix) { fMsgPrefix = prefix; } - From 958e71ff637de866fe6d6e26e97fd98a69486dd7 Mon Sep 17 00:00:00 2001 From: Florine de Geus Date: Wed, 28 Oct 2026 15:23:06 +0100 Subject: [PATCH 2/2] [io] Add test for `SetMaxOpenedFiles` with nested dirs Regression test for #23666 Co-authored-by: Vincenzo Eduardo Padulano (cherry picked from commit 43064e044fe17f25d4f80e6827b54f9dd90153a5) --- io/io/test/TFileMergerTests.cxx | 98 +++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/io/io/test/TFileMergerTests.cxx b/io/io/test/TFileMergerTests.cxx index 66329320dc781..82a12f16894d4 100644 --- a/io/io/test/TFileMergerTests.cxx +++ b/io/io/test/TFileMergerTests.cxx @@ -219,3 +219,101 @@ TEST(TFileMerger, ChangeFile) gSystem->Unlink("file6640mergerinput_2.root"); gSystem->Unlink("file6640mergeroutput.root"); } + +// Regression test for https://github.com/root-project/root/issues/23666 +TEST(TFileMerger, NestedDirMaxOpenedFiles) +{ + struct CleanupRAII { + std::vector fFiles; + ~CleanupRAII() + { + for (const auto &file : fFiles) { + gSystem->Unlink(file.c_str()); + } + } + } cleanup; + + const std::string file1 = "test_nested_merge_in1.root"; + const std::string file2 = "test_nested_merge_in2.root"; + const std::string file3 = "test_nested_merge_in3.root"; + const std::string fileOut = "test_nested_merge_out.root"; + + cleanup.fFiles.push_back(file1); + cleanup.fFiles.push_back(file2); + cleanup.fFiles.push_back(file3); + cleanup.fFiles.push_back(fileOut); + + const std::vector> inputs = {{file1, 5}, {file2, 7}, {file3, 11}}; + + // Create 3 input files with objects inside a nested directory (depth >= 2) + for (const auto &input : inputs) { + auto f = std::make_unique(input.first.c_str(), "RECREATE"); + ASSERT_FALSE(f->IsZombie()); + auto topDir = f->mkdir("nested"); + ASSERT_NE(topDir, nullptr); + auto subDir = topDir->mkdir("dir"); + ASSERT_NE(subDir, nullptr); + subDir->cd(); + + auto tree = std::make_unique("tree", "Test Tree"); + tree->SetDirectory(subDir); + + int val = 0; + tree->Branch("val", &val, "val/I"); + for (int i = 0; i < input.second; ++i) { + val = i; + tree->Fill(); + } + + auto hist = std::make_unique("hist", "Test Hist", 10, 0, 10); + hist->SetDirectory(subDir); + + for (int i = 0; i < input.second; ++i) { + hist->Fill(i % 10); + } + + f->Write(); + } + + // Merge the input files into the output file. The original issue was triggered by the use of the `-n` parameter in + // `hadd`, which opens at most `N` files at once, set to the same number of input files (3). The merger used to only + // merge data from the first two files leaving out data from the third. + { + TFileMerger merger{false}; + EXPECT_TRUE(merger.OutputFile(fileOut.c_str())); + merger.SetMaxOpenedFiles(3); + merger.AddFile(file1.c_str(), /*cpProgress*/ false); + merger.AddFile(file2.c_str(), /*cpProgress*/ false); + merger.AddFile(file3.c_str(), /*cpProgress*/ false); + EXPECT_TRUE(merger.Merge()); + } + + // Verify correct values for merged objects in the output file + { + auto fOut = std::make_unique(fileOut.c_str(), "READ"); + ASSERT_FALSE(fOut->IsZombie()); + + auto topDir = fOut->Get("nested"); + ASSERT_NE(topDir, nullptr); + auto subDir = topDir->Get("dir"); + ASSERT_NE(subDir, nullptr); + + auto tree = subDir->Get("tree"); + ASSERT_NE(tree, nullptr); + EXPECT_EQ(tree->GetEntries(), 5 + 7 + 11); + int val = 0; + tree->SetBranchAddress("val", &val); + int entryCursor{}; + for (const auto &input : inputs) { + for (int i = 0; i < input.second; ++i) { + tree->GetEntry(entryCursor); + EXPECT_EQ(val, i); + entryCursor++; + } + } + + auto hist = subDir->Get("hist"); + ASSERT_NE(hist, nullptr); + EXPECT_EQ(hist->GetEntries(), 5 + 7 + 11); + } +}