diff --git a/python/podio/base_writer.py b/python/podio/base_writer.py index 7b49ca4b5..dffb94588 100644 --- a/python/podio/base_writer.py +++ b/python/podio/base_writer.py @@ -53,6 +53,7 @@ def write_frame(self, frame, category, collections=None): write. If None, all collections are written """ # pylint: disable=protected-access - self._writer.writeFrame( - frame._frame, category, collections or frame.getAvailableCollections() - ) + args = [frame._frame, category] + if collections is not None: + args.append(collections) + self._writer.writeFrame(*args) diff --git a/tests/root_io/CMakeLists.txt b/tests/root_io/CMakeLists.txt index 132e01606..ee6d46c9a 100644 --- a/tests/root_io/CMakeLists.txt +++ b/tests/root_io/CMakeLists.txt @@ -10,6 +10,7 @@ set(root_dependent_tests read_interface_root.cpp read_glob.cpp selected_colls_roundtrip_root.cpp + write_empty_collections_root.cpp ) if(ENABLE_RNTUPLE) set(root_dependent_tests @@ -61,6 +62,10 @@ add_test(NAME read_python_multiple COMMAND python3 ${PROJECT_SOURCE_DIR}/tests/r PODIO_SET_TEST_ENV(read_python_multiple) set_tests_properties(read_python_multiple PROPERTIES FIXTURES_REQUIRED podio_write_root_fixture) +add_test(NAME write_python_empty_colls_root COMMAND python3 ${PROJECT_SOURCE_DIR}/tests/write_empty_collections.py empty_colls_frame_with_py.root) +PODIO_SET_TEST_ENV(write_python_empty_colls_root) +set_tests_properties(write_python_empty_colls_root PROPERTIES FIXTURES_SETUP podio_write_python_empty_colls_root_fixture) + if(ENABLE_RNTUPLE) set_tests_properties( read_rntuple diff --git a/tests/root_io/write_empty_collections_root.cpp b/tests/root_io/write_empty_collections_root.cpp new file mode 100644 index 000000000..02cdbd9ca --- /dev/null +++ b/tests/root_io/write_empty_collections_root.cpp @@ -0,0 +1,74 @@ +#include "datamodel/ExampleHitCollection.h" + +#include "podio/Frame.h" +#include "podio/ROOTReader.h" +#include "podio/ROOTWriter.h" + +#include +#include +#include + +namespace { + +int checkEmptyCollectionsFrame(const podio::Frame& frame) { + const auto colls = frame.getAvailableCollections(); + if (!colls.empty()) { + std::cerr << "expected no collections, got " << colls.size() << std::endl; + return 1; + } + + if (frame.get("hits") != nullptr) { + std::cerr << "collection 'hits' should not be persisted" << std::endl; + return 1; + } + + const auto anInt = frame.getParameter("an_int"); + if (!anInt.has_value() || anInt.value() != 42) { + std::cerr << "parameter an_int not stored correctly" << std::endl; + return 1; + } + + const auto greetings = frame.getParameter>("greetings"); + const std::vector expectedGreetings{"from", "python"}; + if (!greetings.has_value() || greetings.value() != expectedGreetings) { + std::cerr << "parameter greetings not stored correctly" << std::endl; + return 1; + } + + return 0; +} + +} // namespace + +int main(int, char**) { + const auto filename = std::string{"empty_colls_frame_cpp.root"}; + + podio::Frame frame; + auto hits = ExampleHitCollection(); + hits.create(0xBADull, 0.0f, 0.0f, 0.0f, 23.0f); + frame.put(std::move(hits), "hits"); + + frame.putParameter("an_int", 42); + frame.putParameter("greetings", std::vector{"from", "python"}); + + auto writer = podio::ROOTWriter(filename); + const std::vector noCollections{}; + writer.writeFrame(frame, "events", noCollections); + writer.finish(); + + auto reader = podio::ROOTReader(); + reader.openFile(filename); + + if (reader.getEntries("events") != 1) { + std::cerr << "expected exactly one entry" << std::endl; + return 1; + } + + auto data = reader.readEntry("events", 0); + if (!data) { + std::cerr << "could not read entry 0" << std::endl; + return 1; + } + + return checkEmptyCollectionsFrame(podio::Frame(std::move(data))); +} diff --git a/tests/write_empty_collections.py b/tests/write_empty_collections.py new file mode 100644 index 000000000..6086de9e4 --- /dev/null +++ b/tests/write_empty_collections.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Write a frame while explicitly passing an empty collections list. + +This is a regression test helper for the python writer bindings to ensure that +passing an empty list of collections behaves like the C++ writers: +- collections=None -> write all collections +- collections=[] -> write no collections (parameters only) +""" + +import ROOT # type: ignore + +# ROOT is a dynamic module; silence static type checkers. +if ROOT.gSystem.Load("libTestDataModelDict") < 0: # type: ignore[attr-defined] + raise RuntimeError("Could not load TestDataModel dictionary") + +from ROOT import ExampleHitCollection # pylint: disable=wrong-import-position + +from podio import Frame, reading, root_io # pylint: disable=wrong-import-position + + +def create_frame(): + """Create a frame with one collection and some parameters""" + frame = Frame() + + hits = ExampleHitCollection() + hits.create(0xBAD, 0.0, 0.0, 0.0, 23.0) + frame.put(hits, "hits") + + frame.put_parameter("an_int", 42) + frame.put_parameter("greetings", ["from", "python"]) + + return frame + + +def assert_empty_collections(frame): + """Assert that the given frame has no persisted collections""" + if frame.getAvailableCollections(): + raise RuntimeError("Expected no persisted collections") + + try: + frame.get("hits") + except KeyError: + pass + else: + raise RuntimeError("Collection 'hits' should not be persisted") + + if frame.get_parameter("an_int") != 42: + raise RuntimeError("Parameter 'an_int' not stored correctly") + if frame.get_parameter("greetings") != ["from", "python"]: + raise RuntimeError("Parameter 'greetings' not stored correctly") + + +def write_file(filename): + """Write a ROOT file passing an empty collections list""" + if not filename.endswith(".root"): + raise ValueError("This test helper expects a .root output file") + + writer = root_io.Writer(filename) + frame = create_frame() + + # The important part: explicitly pass an empty list + writer.write_frame(frame, "events", []) + writer._writer.finish() # pylint: disable=protected-access + + # Use the standard (TTree) reader inference and validate contents. + reader = reading.get_reader(filename) + if not isinstance(reader, root_io.Reader): + raise RuntimeError("Expected the regular ROOT TTree reader") + + events = reader.get("events") + read_frame = next(iter(events)) + assert_empty_collections(read_frame) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("outputfile", help="Output file name") + + args = parser.parse_args() + write_file(args.outputfile)