From ab0508935fc7ba688b9447b51d06ffc071f36235 Mon Sep 17 00:00:00 2001
From: Silia TAIDER <silia.taider@cern.ch>
Date: Tue, 16 Apr 2024 17:17:17 +0200
Subject: [PATCH] Add tests for the NTuple::GenericWriter and NTuple::Writer
 algorithms

---
 GaudiTestSuite/CMakeLists.txt                 |   2 +
 .../include/Gaudi/TestSuite/NTuple/MyStruct.h |  20 ++
 GaudiTestSuite/src/IO/dict.h                  |   2 +
 GaudiTestSuite/src/IO/dict.xml                |   2 +
 .../src/NTuple/NTupleWriterImpls.cpp          |  26 ++
 .../src/NTuple/NTupleWriterProducers.cpp      |  65 +++++
 .../pytest/NTuple/test_GenericNTupleWriter.py | 248 ++++++++++++++++++
 .../tests/pytest/NTuple/test_NTupleWriter.py  | 115 ++++++++
 GaudiUtils/CMakeLists.txt                     |  13 +
 .../src/component/GenericNTupleWriter.cpp     | 142 ++++++++++
 10 files changed, 635 insertions(+)
 create mode 100644 GaudiTestSuite/include/Gaudi/TestSuite/NTuple/MyStruct.h
 create mode 100644 GaudiTestSuite/src/NTuple/NTupleWriterImpls.cpp
 create mode 100644 GaudiTestSuite/src/NTuple/NTupleWriterProducers.cpp
 create mode 100644 GaudiTestSuite/tests/pytest/NTuple/test_GenericNTupleWriter.py
 create mode 100644 GaudiTestSuite/tests/pytest/NTuple/test_NTupleWriter.py

diff --git a/GaudiTestSuite/CMakeLists.txt b/GaudiTestSuite/CMakeLists.txt
index 92d40dfaf4..4a84a4fd6b 100644
--- a/GaudiTestSuite/CMakeLists.txt
+++ b/GaudiTestSuite/CMakeLists.txt
@@ -94,6 +94,8 @@ gaudi_add_module(GaudiTestSuiteComponents
                          src/Timing/TimingAlg.cpp
                          src/ToolHandles/Algorithms.cpp
                          src/ToolHandles/FloatTool.cpp
+                         src/NTuple/NTupleWriterProducers.cpp
+                         src/NTuple/NTupleWriterImpls.cpp
                  LINK GaudiKernel
                       Gaudi::Functional
                       GaudiUtilsLib
diff --git a/GaudiTestSuite/include/Gaudi/TestSuite/NTuple/MyStruct.h b/GaudiTestSuite/include/Gaudi/TestSuite/NTuple/MyStruct.h
new file mode 100644
index 0000000000..d74d1b98a0
--- /dev/null
+++ b/GaudiTestSuite/include/Gaudi/TestSuite/NTuple/MyStruct.h
@@ -0,0 +1,20 @@
+/*****************************************************************************\
+* (c) Copyright 2024 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 <string>
+
+namespace Gaudi::TestSuite::NTuple {
+  struct MyStruct {
+    int         id{};
+    std::string name;
+  };
+} // namespace Gaudi::TestSuite::NTuple
diff --git a/GaudiTestSuite/src/IO/dict.h b/GaudiTestSuite/src/IO/dict.h
index c97bf2bfa4..0028b94559 100644
--- a/GaudiTestSuite/src/IO/dict.h
+++ b/GaudiTestSuite/src/IO/dict.h
@@ -19,6 +19,8 @@
 #include "GaudiTestSuite/MyTrack.h"
 #include "GaudiTestSuite/MyVertex.h"
 
+#include <Gaudi/TestSuite/NTuple/MyStruct.h>
+
 #include "GaudiExamples/Collision.h"
 #include "GaudiExamples/Counter.h"
 #include "GaudiExamples/Event.h"
diff --git a/GaudiTestSuite/src/IO/dict.xml b/GaudiTestSuite/src/IO/dict.xml
index 125bf61a02..7d183a1240 100644
--- a/GaudiTestSuite/src/IO/dict.xml
+++ b/GaudiTestSuite/src/IO/dict.xml
@@ -116,4 +116,6 @@
     <field name="m_random" transient="true"/>
   </class>
 
+  <class name="Gaudi::TestSuite::NTuple::MyStruct"/>
+
 </lcgdict>
diff --git a/GaudiTestSuite/src/NTuple/NTupleWriterImpls.cpp b/GaudiTestSuite/src/NTuple/NTupleWriterImpls.cpp
new file mode 100644
index 0000000000..376d97933e
--- /dev/null
+++ b/GaudiTestSuite/src/NTuple/NTupleWriterImpls.cpp
@@ -0,0 +1,26 @@
+/***********************************************************************************\
+* (c) Copyright 2024 CERN for the benefit of the LHCb and ATLAS collaborations      *
+*                                                                                   *
+* This software is distributed under the terms of the Apache version 2 licence,     *
+* copied verbatim in the file "LICENSE".                                            *
+*                                                                                   *
+* 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 <Gaudi/NTuple/Writer.h>
+#include <numeric>
+#include <tuple>
+
+namespace Gaudi::TestSuite::NTuple {
+  struct NTupleWriter_V : Gaudi::NTuple::Writer<std::tuple<int, size_t, float>( const std::vector<int>& )> {
+    NTupleWriter_V( const std::string& name, ISvcLocator* svcLoc )
+        : Writer( name, svcLoc, { KeyValue( "InputLocation", { "MyVector" } ) } ) {}
+
+    std::tuple<int, size_t, float> transform( const std::vector<int>& vector ) const override {
+      return std::make_tuple( std::accumulate( vector.begin(), vector.end(), 0 ), vector.size(), std::rand() % 20 );
+    }
+  };
+
+  DECLARE_COMPONENT( NTupleWriter_V )
+} // namespace Gaudi::TestSuite::NTuple
diff --git a/GaudiTestSuite/src/NTuple/NTupleWriterProducers.cpp b/GaudiTestSuite/src/NTuple/NTupleWriterProducers.cpp
new file mode 100644
index 0000000000..30bb0b9c0d
--- /dev/null
+++ b/GaudiTestSuite/src/NTuple/NTupleWriterProducers.cpp
@@ -0,0 +1,65 @@
+/***********************************************************************************\
+* (c) Copyright 2024 CERN for the benefit of the LHCb and ATLAS collaborations      *
+*                                                                                   *
+* This software is distributed under the terms of the Apache version 2 licence,     *
+* copied verbatim in the file "LICENSE".                                            *
+*                                                                                   *
+* 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 <Gaudi/Algorithm.h>
+#include <Gaudi/Functional/Producer.h>
+#include <Gaudi/TestSuite/NTuple/MyStruct.h>
+#include <string>
+#include <vector>
+
+namespace Gaudi::TestSuite::NTuple {
+  struct IntVectorDataProducer final
+      : Gaudi::Functional::Producer<std::vector<int>(), Gaudi::Functional::Traits::BaseClass_t<Gaudi::Algorithm>> {
+    IntVectorDataProducer( const std::string& name, ISvcLocator* svcLoc )
+        : Producer( name, svcLoc, KeyValue( "OutputLocation", "MyVector" ) ) {}
+
+    std::vector<int> operator()() const override { return std::vector<int>{ 0, 1, 2, 3, 4 }; }
+  };
+
+  DECLARE_COMPONENT( IntVectorDataProducer )
+
+  struct FloatDataProducer final
+      : Gaudi::Functional::Producer<float(), Gaudi::Functional::Traits::BaseClass_t<Gaudi::Algorithm>> {
+    FloatDataProducer( const std::string& name, ISvcLocator* svcLoc )
+        : Producer( name, svcLoc, KeyValue( "OutputLocation", "MyFloat" ) ) {}
+
+    float operator()() const override { return 2.5; }
+  };
+
+  DECLARE_COMPONENT( FloatDataProducer )
+
+  struct StrDataProducer final
+      : Gaudi::Functional::Producer<std::string(), Gaudi::Functional::Traits::BaseClass_t<Gaudi::Algorithm>> {
+    StrDataProducer( const std::string& name, ISvcLocator* svcLoc )
+        : Producer( name, svcLoc, KeyValue( "OutputLocation", "MyString" ) ) {}
+
+    std::string operator()() const override { return m_stringValue; }
+
+  private:
+    Gaudi::Property<std::string> m_stringValue{ this, "StringValue", "Default string", "Specify the string to write" };
+  };
+
+  DECLARE_COMPONENT( StrDataProducer )
+
+  struct StructDataProducer final
+      : Gaudi::Functional::Producer<Gaudi::TestSuite::NTuple::MyStruct(),
+                                    Gaudi::Functional::Traits::BaseClass_t<Gaudi::Algorithm>> {
+    StructDataProducer( const std::string& name, ISvcLocator* svcLoc )
+        : Producer( name, svcLoc, KeyValue( "OutputLocation", "MyStruct" ) ) {}
+
+    Gaudi::TestSuite::NTuple::MyStruct operator()() const override {
+      Gaudi::TestSuite::NTuple::MyStruct myStruct = { 1, "myStruct" };
+      return myStruct;
+    }
+  };
+
+  DECLARE_COMPONENT( StructDataProducer )
+
+} // namespace Gaudi::TestSuite::NTuple
diff --git a/GaudiTestSuite/tests/pytest/NTuple/test_GenericNTupleWriter.py b/GaudiTestSuite/tests/pytest/NTuple/test_GenericNTupleWriter.py
new file mode 100644
index 0000000000..e866fd49dd
--- /dev/null
+++ b/GaudiTestSuite/tests/pytest/NTuple/test_GenericNTupleWriter.py
@@ -0,0 +1,248 @@
+#####################################################################################
+# (c) Copyright 2024 CERN for the benefit of the LHCb and ATLAS collaborations      #
+#                                                                                   #
+# This software is distributed under the terms of the Apache version 2 licence,     #
+# copied verbatim in the file "LICENSE".                                            #
+#                                                                                   #
+# 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 pytest
+import ROOT
+from GaudiTests import run_gaudi
+
+# Constants for the output file name and expected values for verification
+OUTPUT_FILE_NAME = "generic_ntuple_writer_tree.root"
+EXPECTED_ENTRIES = 10
+EXPECTED_FLOAT_VALUE = 2.5
+EXPECTED_VECTOR_CONTENT = [0, 1, 2, 3, 4]
+EXPECTED_STRING_VALUE = "hello world"
+NUM_PRODUCERS = 1
+
+
+@pytest.fixture(scope="module", params=["st", "mt"])
+def setup_file_tree(tmp_path_factory, request):
+    """
+    Fixture to set up the ROOT file and tree for testing with the given configuration.
+    Args:
+        tmp_path_factory: Factory for creating temporary directories provided by pytest.
+        request: Pytest request object, which contains a parameter for the configuration to use.
+    Yields:
+        Tuple of (ROOT file, ROOT TTree) for use in tests.
+    """
+    os.chdir(tmp_path_factory.mktemp(request.param))
+    if os.path.exists(OUTPUT_FILE_NAME):
+        os.remove(OUTPUT_FILE_NAME)
+    run_gaudi(f"{__file__}:config_{request.param}", check=True)
+    file = ROOT.TFile.Open(OUTPUT_FILE_NAME)
+    tree = file.Get("GenericWriterTree")
+    yield file, tree
+    file.Close()
+
+
+def config_st():
+    """
+    Configuration function for the Gaudi application. Sets up components, services, and producers
+    """
+    from Configurables import ApplicationMgr, Gaudi__NTuple__GenericWriter
+    from Configurables import Gaudi__RootCnvSvc as RootCnvSvc
+    from Configurables import (
+        Gaudi__TestSuite__NTuple__FloatDataProducer,
+        Gaudi__TestSuite__NTuple__IntVectorDataProducer,
+        Gaudi__TestSuite__NTuple__StrDataProducer,
+        Gaudi__TestSuite__NTuple__StructDataProducer,
+        IncidentSvc,
+        MessageSvc,
+    )
+    from Gaudi.Configuration import DEBUG, INFO
+
+    # Output Levels
+    MessageSvc(OutputLevel=INFO)
+    IncidentSvc(OutputLevel=INFO)
+    RootCnvSvc(OutputLevel=INFO)
+
+    # Create producers (float/std::vector/std::string/MyStruct)
+    producers = [
+        Gaudi__TestSuite__NTuple__FloatDataProducer("FProducer", OutputLevel=DEBUG),
+        Gaudi__TestSuite__NTuple__IntVectorDataProducer("VProducer", OutputLevel=DEBUG),
+        Gaudi__TestSuite__NTuple__StrDataProducer(
+            "SProducer", OutputLevel=DEBUG, StringValue=EXPECTED_STRING_VALUE
+        ),
+        Gaudi__TestSuite__NTuple__StructDataProducer("STProducer", OutputLevel=DEBUG),
+    ]
+
+    # Configure the NTupleWriter
+    NTupleWriter = Gaudi__NTuple__GenericWriter(
+        "NTupleWriter", OutputLevel=DEBUG, TreeFilename=OUTPUT_FILE_NAME
+    )
+    NTupleWriter.ExtraInputs = [
+        ("float", "MyFloat"),
+        ("std::vector<int>", "MyVector"),
+        ("std::string", "MyString"),
+        ("Gaudi::TestSuite::NTuple::MyStruct", "MyStruct"),
+    ]
+    # NTupleWriter.ExtraInputs = [
+    #     (alg.Output.Type, str(alg.Output))
+    #     for alg in producers
+    # ]
+
+    # Application setup
+    ApplicationMgr(
+        TopAlg=producers + [NTupleWriter],
+        EvtMax=EXPECTED_ENTRIES,
+        EvtSel="NONE",
+        HistogramPersistency="NONE",
+    )
+
+
+def config_mt():
+    """
+    Configuration function for the Gaudi application. Sets up components, services, and producers
+    """
+    from Configurables import (
+        AlgResourcePool,
+        ApplicationMgr,
+        AvalancheSchedulerSvc,
+        Gaudi__NTuple__GenericWriter,
+        Gaudi__TestSuite__NTuple__FloatDataProducer,
+        Gaudi__TestSuite__NTuple__IntVectorDataProducer,
+        Gaudi__TestSuite__NTuple__StrDataProducer,
+        Gaudi__TestSuite__NTuple__StructDataProducer,
+        HiveSlimEventLoopMgr,
+        HiveWhiteBoard,
+    )
+    from Gaudi.Configuration import DEBUG, WARNING
+
+    # Configuration parameters for the multithreaded environment
+    evtslots = 4
+    evtMax = EXPECTED_ENTRIES
+    threads = 4
+
+    # Whiteboard setup
+    whiteboard = HiveWhiteBoard("EventDataSvc", EventSlots=evtslots)
+
+    # Event Loop Manager
+    slimeventloopmgr = HiveSlimEventLoopMgr(
+        SchedulerName="AvalancheSchedulerSvc", OutputLevel=WARNING
+    )
+
+    # Scheduler
+    AvalancheSchedulerSvc(ThreadPoolSize=threads, OutputLevel=WARNING)
+
+    # Algorithm Resource Pool
+    AlgResourcePool(OutputLevel=DEBUG)
+
+    # Create producers (float/std::vector/std::string/MyStruct)
+    producers = [
+        Gaudi__TestSuite__NTuple__FloatDataProducer("FProducer", OutputLevel=DEBUG),
+        Gaudi__TestSuite__NTuple__IntVectorDataProducer("VProducer", OutputLevel=DEBUG),
+        Gaudi__TestSuite__NTuple__StrDataProducer(
+            "SProducer", OutputLevel=DEBUG, StringValue=EXPECTED_STRING_VALUE
+        ),
+        Gaudi__TestSuite__NTuple__StructDataProducer("STProducer", OutputLevel=DEBUG),
+    ]
+
+    # NTupleWriter configuration
+    NTupleWriter = Gaudi__NTuple__GenericWriter(
+        "NTupleWriter", OutputLevel=DEBUG, TreeFilename=OUTPUT_FILE_NAME
+    )
+    NTupleWriter.ExtraInputs = [
+        ("float", "MyFloat"),
+        ("std::vector<int>", "MyVector"),
+        ("std::string", "MyString"),
+        ("Gaudi::TestSuite::NTuple::MyStruct", "MyStruct"),
+    ]
+    # NTupleWriter.ExtraInputs = [
+    #     (alg.Output.Type, str(alg.Output))
+    #     for alg in producers
+    # ]
+
+    # Application setup
+    ApplicationMgr(
+        EvtMax=evtMax,
+        EvtSel="NONE",
+        ExtSvc=[whiteboard],
+        EventLoop=slimeventloopmgr,
+        TopAlg=producers + [NTupleWriter],
+        MessageSvcType="InertMessageSvc",
+    )
+
+
+def test_file_creation_and_tree_structure(setup_file_tree):
+    """
+    Tests if the output ROOT file is created successfully and contains a tree
+    """
+    file, tree = setup_file_tree
+    assert file, f"expected output file {OUTPUT_FILE_NAME} not found"
+    assert tree, "TTree 'NTupleWriterTree' not found in the file"
+
+
+def test_branch_creation(setup_file_tree):
+    """
+    Verifies that the expected branches are created in the ROOT file
+    """
+    _, tree = setup_file_tree
+    assert tree.GetBranch("MyFloat") is not None, "Float branch was not created."
+    assert tree.GetBranch("MyVector") is not None, "Vector<int> branch was not created."
+    assert tree.GetBranch("MyString") is not None, "String branch was not created."
+    assert tree.GetBranch("MyStruct") is not None, "MyStruct branch was not created."
+
+
+def test_float_branch_content(setup_file_tree):
+    """
+    Tests the content of the float branch to ensure it matches the expected value
+    """
+    _, tree = setup_file_tree
+    for entry in tree:
+        assert (
+            entry.MyFloat == EXPECTED_FLOAT_VALUE
+        ), "Float branch does not contain the correct value."
+
+
+def test_string_branch_content(setup_file_tree):
+    """
+    Tests the content of the string branch to ensure it matches the expected value
+    """
+    _, tree = setup_file_tree
+    for entry in tree:
+        assert (
+            entry.MyString == EXPECTED_STRING_VALUE
+        ), "String branch does not contain the correct value."
+
+
+def test_vector_branch_content(setup_file_tree):
+    """
+    Tests the content of the std::vector<int> branch to ensure it matches the expected value
+    """
+    _, tree = setup_file_tree
+    for entry in tree:
+        assert (
+            list(entry.MyVector) == EXPECTED_VECTOR_CONTENT
+        ), "Vector<int> branch does not contain the correct values."
+
+
+def test_multiple_entries(setup_file_tree):
+    """
+    Basic check to ensure data for all expected events is present in the tree
+    """
+    _, tree = setup_file_tree
+    assert (
+        tree.GetEntries() == EXPECTED_ENTRIES
+    ), f"Expected {EXPECTED_ENTRIES} entries, found {tree.GetEntries()}."
+
+
+def test_handling_missing_input(setup_file_tree):
+    """
+    Checks how the algorithm handles a scenario where expected input data is missing
+    """
+    _, tree = setup_file_tree
+    try:
+        missing_branch = tree.GetBranch("MissingData")
+        # Attempt to access a null pointer to trigger a ReferenceError
+        missing_branch.GetName()
+        assert False, "Branch 'MissingData' unexpectedly exists."
+    except ReferenceError:
+        assert True
diff --git a/GaudiTestSuite/tests/pytest/NTuple/test_NTupleWriter.py b/GaudiTestSuite/tests/pytest/NTuple/test_NTupleWriter.py
new file mode 100644
index 0000000000..cc9a842cc3
--- /dev/null
+++ b/GaudiTestSuite/tests/pytest/NTuple/test_NTupleWriter.py
@@ -0,0 +1,115 @@
+#####################################################################################
+# (c) Copyright 2024 CERN for the benefit of the LHCb and ATLAS collaborations      #
+#                                                                                   #
+# This software is distributed under the terms of the Apache version 2 licence,     #
+# copied verbatim in the file "LICENSE".                                            #
+#                                                                                   #
+# 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 pytest
+import ROOT
+from GaudiTests import run_gaudi
+
+# Constants for the output file name
+OUTPUT_FILE_NAME = "ntuple_writer_tree.root"
+EXPECTED_ENTRIES = 10
+EXPECTED_VECTOR_SUM = 10
+EXPECTED_VECTOR_SIZE = 5
+
+
+@pytest.fixture(scope="module")
+def setup_file_tree(tmp_path_factory):
+    """
+    PyTest fixture that prepares a ROOT file and tree for testing
+    Args:
+        tmp_path_factory: Factory for creating temporary directories provided by pytest
+    Yields:
+        Tuple of (ROOT file, ROOT TTree) for use in tests
+    """
+    os.chdir(tmp_path_factory.getbasetemp())
+    if os.path.exists(OUTPUT_FILE_NAME):
+        os.remove(OUTPUT_FILE_NAME)
+    run_gaudi(f"{__file__}:config", check=True)
+    file = ROOT.TFile.Open(OUTPUT_FILE_NAME)
+    tree = file.Get("WriterTree")
+    yield file, tree
+    file.Close()
+
+
+def config():
+    """
+    Configuration function for the Gaudi application that sets up the NTupleWriter.
+    """
+    from GaudiConfig2 import Configurables as C
+
+    E = C.Gaudi
+
+    algs = [
+        E.TestSuite.NTuple.IntVectorDataProducer("IntVectorDataProducer"),
+        E.TestSuite.NTuple.NTupleWriter_V(
+            "NTupleWriter_V",
+            TreeFilename=OUTPUT_FILE_NAME,
+            BranchNames=["Branch1", "Branch2", "Branch3"],
+        ),
+    ]
+
+    loopmgr = C.HiveSlimEventLoopMgr(SchedulerName="AvalancheSchedulerSvc")
+    whiteboard = C.HiveWhiteBoard("EventDataSvc", EventSlots=5)
+    svcs = [whiteboard, C.AlgResourcePool()]
+    return (
+        [
+            C.ApplicationMgr(
+                TopAlg=algs,
+                EvtMax=EXPECTED_ENTRIES,
+                EvtSel="NONE",
+                ExtSvc=svcs,
+                EventLoop=loopmgr.name,
+            ),
+            loopmgr,
+        ]
+        + algs
+        + svcs
+    )
+
+
+def test_branch_creation(setup_file_tree):
+    """
+    Test to ensure that all expected branches are correctly created in the ROOT file.
+    """
+    _, tree = setup_file_tree
+    assert tree.GetBranch("Branch1"), "Branch1 should exist in WriterTree."
+    assert tree.GetBranch("Branch2"), "Branch2 should exist in WriterTree."
+    assert tree.GetBranch("Branch3"), "Branch3 should exist in WriterTree."
+
+
+def test_data_types(setup_file_tree):
+    """
+    Verify the data within the branches to ensure they match expected transformations.
+    """
+    _, tree = setup_file_tree
+    for entry in tree:
+        assert isinstance(
+            entry.Branch1, int
+        ), "Branch1 does not contain int values as expected."
+        assert isinstance(
+            entry.Branch2, int
+        ), "Branch2 does not contain int values as expected."
+        assert isinstance(
+            entry.Branch3, float
+        ), "Branch3 does not contain float values as expected."
+
+
+def test_data_values(setup_file_tree):
+    """ """
+    _, tree = setup_file_tree
+    for entry in tree:
+        assert (
+            entry.Branch1 == EXPECTED_VECTOR_SUM
+        ), "Branch1 does not contain the correct value."
+        assert (
+            entry.Branch2 == EXPECTED_VECTOR_SIZE
+        ), "Branch2 does not contain the correct value."
diff --git a/GaudiUtils/CMakeLists.txt b/GaudiUtils/CMakeLists.txt
index 85296f60d3..6860df35bb 100644
--- a/GaudiUtils/CMakeLists.txt
+++ b/GaudiUtils/CMakeLists.txt
@@ -96,4 +96,17 @@ if(BUILD_TESTING)
       PROPERTY DEPENDS ${package_name}.testXMLFileCatalogWrite)
   endif()
 
+  gaudi_add_executable(test_GenericNTupleWriter
+        SOURCES
+            src/component/GenericNTupleWriter.cpp
+        LINK
+            GaudiUtilsLib
+            Boost::headers
+            Boost::unit_test_framework
+            fmt::fmt
+            Gaudi::GaudiKernel
+            ROOT::Tree
+        TEST
+    )
+    target_compile_definitions(test_GenericNTupleWriter PRIVATE UNIT_TESTS)
 endif()
diff --git a/GaudiUtils/src/component/GenericNTupleWriter.cpp b/GaudiUtils/src/component/GenericNTupleWriter.cpp
index 0fbb764ebf..c380de6823 100644
--- a/GaudiUtils/src/component/GenericNTupleWriter.cpp
+++ b/GaudiUtils/src/component/GenericNTupleWriter.cpp
@@ -157,3 +157,145 @@ namespace Gaudi::NTuple {
 
   DECLARE_COMPONENT( GenericWriter )
 } // namespace Gaudi::NTuple
+
+#ifdef UNIT_TESTS
+
+#  define BOOST_TEST_MODULE test_GenericNTupleWriter
+#  include <boost/test/unit_test.hpp>
+
+/**
+ * @class MockISvcLocator
+ * @brief Mock implementation of ISvcLocator interface for unit testing.
+ */
+class MockISvcLocator : public ISvcLocator {
+public:
+  virtual StatusCode getService( const Gaudi::Utils::TypeNameString&, const InterfaceID&, IInterface*& ) override {
+    return StatusCode::SUCCESS;
+  }
+
+  virtual StatusCode getService( const Gaudi::Utils::TypeNameString&, IService*&, const bool ) override {
+    return StatusCode::SUCCESS;
+  }
+
+  virtual const std::list<IService*>& getServices() const override {
+    static std::list<IService*> dummyServices;
+    return dummyServices;
+  }
+
+  virtual bool existsService( std::string_view ) const override { return false; }
+
+  virtual SmartIF<IService>& service( const Gaudi::Utils::TypeNameString&, const bool ) override {
+    static SmartIF<IService> dummyService;
+    return dummyService;
+  }
+
+  virtual unsigned long addRef() override { return 0; }
+
+  virtual unsigned long release() override { return 0; }
+
+  virtual StatusCode queryInterface( const InterfaceID&, void** ) override { return StatusCode::SUCCESS; }
+
+  virtual std::vector<std::string> getInterfaceNames() const override { return {}; }
+
+  virtual void* i_cast( const InterfaceID& ) const override { return nullptr; }
+
+  virtual unsigned long refCount() const override { return 1; }
+};
+
+// Utility function tests
+BOOST_AUTO_TEST_CASE( testGetTypeName ) {
+  // Test type name extraction from various formats
+  BOOST_CHECK_EQUAL( getTypeName( "MyClass" ), "MyClass" );
+  BOOST_CHECK_EQUAL( getTypeName( "std::vector<double>" ), "std::vector<double>" );
+  BOOST_CHECK_EQUAL( getTypeName( "UNKNOWN_CLASS:MyCustomClass" ), "MyCustomClass" );
+}
+
+BOOST_AUTO_TEST_CASE( testGetNameFromLoc ) {
+  // Test name extraction from location strings
+  BOOST_CHECK_EQUAL( getNameFromLoc( "/Event/MyAlg/MyData" ), "MyData" );
+  BOOST_CHECK_EQUAL( getNameFromLoc( "MyAlg/MyData" ), "MyData" );
+  BOOST_CHECK_EQUAL( getNameFromLoc( "MyData" ), "MyData" );
+  BOOST_CHECK_EQUAL( getNameFromLoc( "" ), "" );
+}
+
+// Test instantiation of the GenericWriter
+BOOST_AUTO_TEST_CASE( testInit ) {
+  MockISvcLocator              mockLocator;
+  Gaudi::NTuple::GenericWriter writer( "test_writer", &mockLocator );
+
+  // Verify we can instanciate a writer
+  BOOST_CHECK_EQUAL( writer.name(), "test_writer" );
+}
+
+// Test branch creation with empty dependencies
+BOOST_AUTO_TEST_CASE( testCreateBranches_EmptyDeps ) {
+  MockISvcLocator              mockLocator;
+  Gaudi::NTuple::GenericWriter writer( "test_writer", &mockLocator );
+
+  // Expect an exception when no dependencies are provided
+  BOOST_CHECK_EXCEPTION( writer.initialize().ignore(), GaudiException, []( const GaudiException& e ) {
+    return e.message() == "No extra inputs locations specified. Please define extra inputs for the NTuple writer.";
+  } );
+}
+
+// Test branch creation with an invalid type
+BOOST_AUTO_TEST_CASE( testCreateBranches_InvalidType ) {
+  MockISvcLocator              mockLocator;
+  auto                         tree = std::make_unique<TTree>( "testTree", "test tree" );
+  Gaudi::NTuple::GenericWriter writer( "test_writer", &mockLocator );
+  writer.setTree( tree.get() );
+
+  DataObjIDColl invalidDeps{ { "InvalidType", "loc" } };
+
+  // Expect an exception when an invalid type is provided
+  BOOST_CHECK_EXCEPTION( writer.createBranches( invalidDeps ), GaudiException, []( const GaudiException& e ) {
+    return e.message() == "Cannot create branch loc for unknown class: InvalidType. Provide a dictionary please.";
+  } );
+}
+
+// Test branch creation for fundamental types
+BOOST_AUTO_TEST_CASE( testCreateBranches_BasicTypes ) {
+  MockISvcLocator              mockLocator;
+  auto                         tree = std::make_unique<TTree>( "testTree", "test tree" );
+  Gaudi::NTuple::GenericWriter writer( "test_writer", &mockLocator );
+  writer.setTree( tree.get() );
+
+  DataObjIDColl                   dependencies{ { "int", "loc1" }, { "double", "loc2" }, { "std::string", "loc3" } };
+  std::unordered_set<std::string> expectedTypes{ "int", "double", "std::string" };
+  writer.createBranches( dependencies );
+
+  // Verify that the branch wrappers' class names match the expected types
+  BOOST_CHECK_EQUAL( writer.getBranchWrappersSize(), expectedTypes.size() );
+  BOOST_CHECK( expectedTypes == writer.getBranchesClassNames() );
+}
+
+// Test branch creation for ROOT-known non-fundamental types
+BOOST_AUTO_TEST_CASE( testCreateBranches_ROOTKnownTypes ) {
+  MockISvcLocator              mockLocator;
+  auto                         tree = std::make_unique<TTree>( "testTree", "test tree" );
+  Gaudi::NTuple::GenericWriter writer( "test_writer", &mockLocator );
+  writer.setTree( tree.get() );
+
+  DataObjIDColl                   dependencies{ { "std::vector<double>", "vectorDoubleLoc" }, { "TH1D", "hist1DLoc" } };
+  std::unordered_set<std::string> expectedTypes{ "std::vector<double>", "TH1D" };
+  writer.createBranches( dependencies );
+
+  // Verify that the branch wrappers' class names match the expected types
+  BOOST_CHECK_EQUAL( writer.getBranchWrappersSize(), expectedTypes.size() );
+  BOOST_CHECK( expectedTypes == writer.getBranchesClassNames() );
+}
+
+// Test for GaudiException when the file cannot be opened
+BOOST_AUTO_TEST_CASE( testFileOpenException ) {
+  MockISvcLocator              mockLocator;
+  Gaudi::NTuple::GenericWriter writer( "test_writer", &mockLocator );
+  DataObjIDColl                dependencies{ { "float", "loc" } };
+
+  BOOST_CHECK( writer.setProperty( "ExtraInputs", dependencies ).isSuccess() );
+  BOOST_CHECK( writer.setProperty( "TreeFilename", "/invalid/path/to/file.root" ).isSuccess() );
+  BOOST_CHECK_EXCEPTION( writer.initialize().ignore(), GaudiException, []( const GaudiException& e ) {
+    return e.message() == "Failed to open file '/invalid/path/to/file.root'. Check file path and permissions.";
+  } );
+}
+
+#endif
-- 
GitLab