Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions python/podio/base_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 3 additions & 0 deletions tests/CTestCustom.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ if ((NOT "@FORCE_RUN_ALL_TESTS@" STREQUAL "ON") AND (NOT "@USE_SANITIZER@" STREQ
write_python_frame_root
read_python_frame_root

write_python_empty_colls_root

param_reading_rdataframe

read_python_multiple
Expand Down Expand Up @@ -155,6 +157,7 @@ if ((NOT "@FORCE_RUN_ALL_TESTS@" STREQUAL "ON") AND (NOT "@USE_SANITIZER@" STREQ
read_frame_root_multiple
read_and_write_frame_root
selected_colls_roundtrip_root
write_empty_collections_root

podio-dump-root
podio-dump-detailed-root
Expand Down
5 changes: 5 additions & 0 deletions tests/root_io/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be excluded from the sanitizer runs as they do not usually work with anything that invokes the python interpreter.

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
Expand Down
74 changes: 74 additions & 0 deletions tests/root_io/write_empty_collections_root.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#include "datamodel/ExampleHitCollection.h"

#include "podio/Frame.h"
#include "podio/ROOTReader.h"
#include "podio/ROOTWriter.h"

#include <iostream>
#include <string>
#include <vector>

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<int>("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<std::vector<std::string>>("greetings");
const std::vector<std::string> 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<std::string>{"from", "python"});

auto writer = podio::ROOTWriter(filename);
const std::vector<std::string> 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)));
}
82 changes: 82 additions & 0 deletions tests/write_empty_collections.py
Original file line number Diff line number Diff line change
@@ -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)
Loading