diff --git a/DataFormats/HLTReco/README.md b/DataFormats/HLTReco/README.md
new file mode 100644
index 0000000000000..f9d1b929228ef
--- /dev/null
+++ b/DataFormats/HLTReco/README.md
@@ -0,0 +1,10 @@
+# DataFormats/HLTReco
+
+## `trigger::TriggerEvent`
+
+The class `trigger::TriggerEvent` is part of the RAW data, and any changes must be backwards compatible. In order to ensure it can be read by all future CMSSW releases, there is a `TestTriggerEventFormat` unit test, which makes use of the `TestReadTriggerEvent` analyzer and the `TestWriteTriggerEvent` producer. The unit test checks that the object can be read properly from
+
+* a file written by the same release
+* files written by (some) earlier releases
+
+If the persistent format of class `trigger::TriggerEvent` gets changed in the future, please adjust the `TestReadTriggerEvent` and `TestWriteTriggerEvent` modules accordingly. It is important that every member container has some content in this test. Please also add a new file to the [https://github.com/cms-data/DataFormats-HLTReco/](https://github.com/cms-data/DataFormats-HLTReco/) repository, and update the `TestTriggerEventFormat` unit test to read the newly created file. The file name should contain the release or pre-release with which it was written.
diff --git a/DataFormats/HLTReco/test/BuildFile.xml b/DataFormats/HLTReco/test/BuildFile.xml
new file mode 100644
index 0000000000000..ee654008c3eb8
--- /dev/null
+++ b/DataFormats/HLTReco/test/BuildFile.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/DataFormats/HLTReco/test/TestReadTriggerEvent.cc b/DataFormats/HLTReco/test/TestReadTriggerEvent.cc
new file mode 100644
index 0000000000000..fea498324e2a2
--- /dev/null
+++ b/DataFormats/HLTReco/test/TestReadTriggerEvent.cc
@@ -0,0 +1,191 @@
+// -*- C++ -*-
+//
+// Package: DataFormats/HLTReco
+// Class: TestReadTriggerEvent
+//
+/**\class edmtest::TestReadTriggerEvent
+ Description: Used as part of tests that ensure the trigger::TriggerEvent
+ data format can be persistently written and in a subsequent process
+ read. First, this is done using the current release version for writing
+ and reading. In addition, the output file of the write process should
+ be saved permanently each time the trigger::TriggerEvent persistent data
+ format changes. In unit tests, we read each of those saved files to verify
+ that the current releases can read older versions of the data format.
+*/
+// Original Author: W. David Dagenhart
+// Created: 8 May 2023
+
+#include "DataFormats/HLTReco/interface/TriggerEvent.h"
+#include "DataFormats/HLTReco/interface/TriggerTypeDefs.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/Frameworkfwd.h"
+#include "FWCore/Framework/interface/global/EDAnalyzer.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ParameterSetDescription.h"
+#include "FWCore/Utilities/interface/EDGetToken.h"
+#include "FWCore/Utilities/interface/Exception.h"
+#include "FWCore/Utilities/interface/InputTag.h"
+#include "FWCore/Utilities/interface/StreamID.h"
+
+#include
+#include
+
+namespace edmtest {
+
+ class TestReadTriggerEvent : public edm::global::EDAnalyzer<> {
+ public:
+ TestReadTriggerEvent(edm::ParameterSet const&);
+ void analyze(edm::StreamID, edm::Event const&, edm::EventSetup const&) const override;
+ void throwWithMessage(const char*) const;
+ static void fillDescriptions(edm::ConfigurationDescriptions&);
+
+ private:
+ // These expected values are meaningless other than we use them
+ // to check that values read from persistent storage match the values
+ // we know were written.
+ std::string expectedUsedProcessName_;
+ std::vector expectedCollectionTags_;
+ std::vector expectedCollectionKeys_;
+
+ std::vector expectedIds_;
+ std::vector expectedPts_;
+ std::vector expectedEtas_;
+ std::vector expectedPhis_;
+ std::vector expectedMasses_;
+
+ std::vector expectedFilterTags_;
+ unsigned int expectedElementsPerVector_;
+ std::vector expectedFilterIds_;
+ std::vector expectedFilterKeys_;
+
+ edm::EDGetTokenT triggerEventToken_;
+ };
+
+ TestReadTriggerEvent::TestReadTriggerEvent(edm::ParameterSet const& iPSet)
+ : expectedUsedProcessName_(iPSet.getParameter("expectedUsedProcessName")),
+ expectedCollectionTags_(iPSet.getParameter>("expectedCollectionTags")),
+ expectedCollectionKeys_(iPSet.getParameter>("expectedCollectionKeys")),
+ expectedIds_(iPSet.getParameter>("expectedIds")),
+ expectedPts_(iPSet.getParameter>("expectedPts")),
+ expectedEtas_(iPSet.getParameter>("expectedEtas")),
+ expectedPhis_(iPSet.getParameter>("expectedPhis")),
+ expectedMasses_(iPSet.getParameter>("expectedMasses")),
+
+ expectedFilterTags_(iPSet.getParameter>("expectedFilterTags")),
+ expectedElementsPerVector_(iPSet.getParameter("expectedElementsPerVector")),
+ expectedFilterIds_(iPSet.getParameter>("expectedFilterIds")),
+ expectedFilterKeys_(iPSet.getParameter>("expectedFilterKeys")),
+
+ triggerEventToken_(consumes(iPSet.getParameter("triggerEventTag"))) {
+ if (expectedIds_.size() != expectedPts_.size() || expectedIds_.size() != expectedEtas_.size() ||
+ expectedIds_.size() != expectedPhis_.size() || expectedIds_.size() != expectedMasses_.size()) {
+ throw cms::Exception("TestFailure")
+ << "TestReadTriggerEvent, test configuration error: "
+ "expectedIds, expectedPts, expectedEtas, expectedPhis, and expectedMasses should have the same size.";
+ }
+ if (expectedFilterIds_.size() != expectedElementsPerVector_ * expectedFilterTags_.size() ||
+ expectedFilterKeys_.size() != expectedElementsPerVector_ * expectedFilterTags_.size()) {
+ throw cms::Exception("TestFailure") << "TestReadTriggerEvent, test configuration error: "
+ "size of expectedFilterIds and size of expectedFilterKeys "
+ "should equal size of expectedFilterTags times expectedElementsPerVector";
+ }
+ }
+
+ void TestReadTriggerEvent::analyze(edm::StreamID, edm::Event const& iEvent, edm::EventSetup const&) const {
+ auto const& triggerEvent = iEvent.get(triggerEventToken_);
+
+ if (triggerEvent.usedProcessName() != expectedUsedProcessName_) {
+ throwWithMessage("usedProcessName does not have expected value");
+ }
+
+ if (triggerEvent.collectionTags() != expectedCollectionTags_) {
+ throwWithMessage("collectionTags do not have expected values");
+ }
+
+ trigger::Keys expectedKeys;
+ expectedKeys.reserve(expectedCollectionKeys_.size());
+ for (auto const& element : expectedCollectionKeys_) {
+ expectedKeys.push_back(static_cast(element));
+ }
+
+ if (triggerEvent.collectionKeys() != expectedKeys) {
+ throwWithMessage("collectionKeys do not have expected values");
+ }
+
+ trigger::TriggerObjectCollection const& triggerObjectCollection = triggerEvent.getObjects();
+ if (triggerObjectCollection.size() != expectedIds_.size()) {
+ throwWithMessage("triggerObjectCollection does not have expected size");
+ }
+ for (unsigned int i = 0; i < triggerObjectCollection.size(); ++i) {
+ trigger::TriggerObject const& triggerObject = triggerObjectCollection[i];
+ if (triggerObject.id() != expectedIds_[i]) {
+ throwWithMessage("triggerObjectCollection id does not have expected value");
+ }
+ if (triggerObject.pt() != static_cast(expectedPts_[i])) {
+ throwWithMessage("triggerObjectCollection pt does not have expected value");
+ }
+ if (triggerObject.eta() != static_cast(expectedEtas_[i])) {
+ throwWithMessage("triggerObjectCollection eta does not have expected value");
+ }
+ if (triggerObject.phi() != static_cast(expectedPhis_[i])) {
+ throwWithMessage("triggerObjectCollection phi does not have expected value");
+ }
+ if (triggerObject.mass() != static_cast(expectedMasses_[i])) {
+ throwWithMessage("triggerObjectCollection mass does not have expected value");
+ }
+ }
+
+ if (triggerEvent.sizeFilters() != expectedFilterTags_.size()) {
+ throwWithMessage("triggerFilters does not have expected size");
+ }
+
+ for (unsigned int i = 0; i < expectedFilterTags_.size(); ++i) {
+ if (triggerEvent.filterLabel(i) != expectedFilterTags_[i]) {
+ throwWithMessage("filterTags does not have expected value");
+ }
+ trigger::Vids const& filterIds = triggerEvent.filterIds(i);
+ if (filterIds.size() != expectedElementsPerVector_) {
+ throwWithMessage("filterIds does not have expected size");
+ }
+ trigger::Keys const& filterKeys = triggerEvent.filterKeys(i);
+ if (filterKeys.size() != expectedElementsPerVector_) {
+ throwWithMessage("filterKeys does not have expected size");
+ }
+ for (unsigned int j = 0; j < expectedElementsPerVector_; ++j) {
+ if (filterIds[j] != expectedFilterIds_[i * expectedElementsPerVector_ + j]) {
+ throwWithMessage("filterIds does not contain expected values");
+ }
+ if (filterKeys[j] != expectedFilterKeys_[i * expectedElementsPerVector_ + j]) {
+ throwWithMessage("filterKeys does not contain expected values");
+ }
+ }
+ }
+ }
+
+ void TestReadTriggerEvent::throwWithMessage(const char* msg) const {
+ throw cms::Exception("TestFailure") << "TestReadTriggerEvent::analyze, " << msg;
+ }
+
+ void TestReadTriggerEvent::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("expectedUsedProcessName");
+ desc.add>("expectedCollectionTags");
+ desc.add>("expectedCollectionKeys");
+ desc.add>("expectedIds");
+ desc.add>("expectedPts");
+ desc.add>("expectedEtas");
+ desc.add>("expectedPhis");
+ desc.add>("expectedMasses");
+ desc.add>("expectedFilterTags");
+ desc.add("expectedElementsPerVector");
+ desc.add>("expectedFilterIds");
+ desc.add>("expectedFilterKeys");
+ desc.add("triggerEventTag");
+ descriptions.addDefault(desc);
+ }
+} // namespace edmtest
+
+using edmtest::TestReadTriggerEvent;
+DEFINE_FWK_MODULE(TestReadTriggerEvent);
diff --git a/DataFormats/HLTReco/test/TestTriggerEventFormat.sh b/DataFormats/HLTReco/test/TestTriggerEventFormat.sh
new file mode 100755
index 0000000000000..99e724981d300
--- /dev/null
+++ b/DataFormats/HLTReco/test/TestTriggerEventFormat.sh
@@ -0,0 +1,19 @@
+#!/bin/sh -ex
+
+function die { echo $1: status $2 ; exit $2; }
+
+LOCAL_TEST_DIR=${SCRAM_TEST_PATH}
+
+cmsRun ${LOCAL_TEST_DIR}/create_TriggerEvent_test_file_cfg.py || die 'Failure using create_TriggerEvent_test_file_cfg.py' $?
+
+file=testTriggerEvent.root
+
+cmsRun ${LOCAL_TEST_DIR}/test_readTriggerEvent_cfg.py "$file" || die "Failure using test_readTriggerEvent_cfg.py $file" $?
+
+oldFiles="testTriggerEvent_CMSSW_13_0_0.root testTriggerEvent_CMSSW_13_1_0_pre3.root"
+for file in $oldFiles; do
+ inputfile=$(edmFileInPath DataFormats/HLTReco/data/$file) || die "Failure edmFileInPath DataFormats/HLTReco/data/$file" $?
+ cmsRun ${LOCAL_TEST_DIR}/test_readTriggerEvent_cfg.py "$inputfile" || die "Failed to read old file $file" $?
+done
+
+exit 0
diff --git a/DataFormats/HLTReco/test/TestWriteTriggerEvent.cc b/DataFormats/HLTReco/test/TestWriteTriggerEvent.cc
new file mode 100644
index 0000000000000..608c105965ba9
--- /dev/null
+++ b/DataFormats/HLTReco/test/TestWriteTriggerEvent.cc
@@ -0,0 +1,151 @@
+// -*- C++ -*-
+//
+// Package: DataFormats/HLTReco
+// Class: TestWriteTriggerEvent
+//
+/**\class edmtest::TestWriteTriggerEvent
+ Description: Used as part of tests that ensure the trigger::TriggerEvent
+ data format can be persistently written and in a subsequent process
+ read. First, this is done using the current release version for writing
+ and reading. In addition, the output file of the write process should
+ be saved permanently each time the trigger::TriggerEvent persistent data
+ format changes. In unit tests, we read each of those saved files to verify
+ that the current releases can read older versions of the data format.
+*/
+// Original Author: W. David Dagenhart
+// Created: 8 May 2023
+
+#include "DataFormats/HLTReco/interface/TriggerEvent.h"
+#include "DataFormats/HLTReco/interface/TriggerObject.h"
+#include "DataFormats/HLTReco/interface/TriggerTypeDefs.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/Frameworkfwd.h"
+#include "FWCore/Framework/interface/global/EDProducer.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ParameterSetDescription.h"
+#include "FWCore/Utilities/interface/EDPutToken.h"
+#include "FWCore/Utilities/interface/Exception.h"
+#include "FWCore/Utilities/interface/StreamID.h"
+
+#include
+#include
+#include
+#include
+
+namespace edmtest {
+
+ class TestWriteTriggerEvent : public edm::global::EDProducer<> {
+ public:
+ TestWriteTriggerEvent(edm::ParameterSet const&);
+ void produce(edm::StreamID, edm::Event&, edm::EventSetup const&) const override;
+ static void fillDescriptions(edm::ConfigurationDescriptions&);
+
+ private:
+ std::string usedProcessName_;
+ std::vector collectionTags_;
+ std::vector collectionKeys_;
+ std::vector ids_;
+ std::vector pts_;
+ std::vector etas_;
+ std::vector phis_;
+ std::vector masses_;
+ std::vector filterTags_;
+ unsigned int elementsPerVector_;
+ std::vector filterIds_;
+ std::vector filterKeys_;
+
+ edm::EDPutTokenT triggerEventPutToken_;
+ };
+
+ TestWriteTriggerEvent::TestWriteTriggerEvent(edm::ParameterSet const& iPSet)
+ : usedProcessName_(iPSet.getParameter("usedProcessName")),
+ collectionTags_(iPSet.getParameter>("collectionTags")),
+ collectionKeys_(iPSet.getParameter>("collectionKeys")),
+ ids_(iPSet.getParameter>("ids")),
+ pts_(iPSet.getParameter>("pts")),
+ etas_(iPSet.getParameter>("etas")),
+ phis_(iPSet.getParameter>("phis")),
+ masses_(iPSet.getParameter>("masses")),
+ filterTags_(iPSet.getParameter>("filterTags")),
+ elementsPerVector_(iPSet.getParameter("elementsPerVector")),
+ filterIds_(iPSet.getParameter>("filterIds")),
+ filterKeys_(iPSet.getParameter>("filterKeys")),
+
+ triggerEventPutToken_(produces()) {
+ if (ids_.size() != pts_.size() || ids_.size() != etas_.size() || ids_.size() != phis_.size() ||
+ ids_.size() != masses_.size()) {
+ throw cms::Exception("TestFailure") << "TestWriteTriggerEvent, test configuration error: "
+ "ids, pts, etas, phis, and masses should have the same size.";
+ }
+ if (filterIds_.size() != elementsPerVector_ * filterTags_.size() ||
+ filterKeys_.size() != elementsPerVector_ * filterTags_.size()) {
+ throw cms::Exception("TestFailure")
+ << "TestWriteTriggerEvent, test configuration error: "
+ "size of filterIds and size of filterKeys should equal size of filterTags times elementsPerVector";
+ }
+ }
+
+ void TestWriteTriggerEvent::produce(edm::StreamID, edm::Event& iEvent, edm::EventSetup const&) const {
+ // Fill a TriggerEvent object. Make sure all the containers inside
+ // of it have something in them (not empty). The values are meaningless.
+ // We will later check that after writing this object to persistent storage
+ // and then reading it in a later process we obtain matching values for
+ // all this content.
+
+ auto triggerEvent = std::make_unique(
+ usedProcessName_, collectionTags_.size(), ids_.size(), filterTags_.size());
+ trigger::Keys keys;
+ keys.reserve(collectionKeys_.size());
+ for (auto const& element : collectionKeys_) {
+ keys.push_back(static_cast(element));
+ }
+ triggerEvent->addCollections(collectionTags_, keys);
+
+ trigger::TriggerObjectCollection triggerObjectCollection;
+ triggerObjectCollection.reserve(ids_.size());
+ for (unsigned int i = 0; i < ids_.size(); ++i) {
+ triggerObjectCollection.emplace_back(ids_[i],
+ static_cast(pts_[i]),
+ static_cast(etas_[i]),
+ static_cast(phis_[i]),
+ static_cast(masses_[i]));
+ }
+ triggerEvent->addObjects(triggerObjectCollection);
+
+ for (unsigned int i = 0; i < filterTags_.size(); ++i) {
+ trigger::Vids filterIds;
+ filterIds.reserve(elementsPerVector_);
+ trigger::Keys filterKeys;
+ filterKeys.reserve(elementsPerVector_);
+ for (unsigned int j = 0; j < elementsPerVector_; ++j) {
+ filterIds.push_back(filterIds_[i * elementsPerVector_ + j]);
+ filterKeys.push_back(filterKeys_[i * elementsPerVector_ + j]);
+ }
+ triggerEvent->addFilter(edm::InputTag(filterTags_[i]), filterIds, filterKeys);
+ }
+
+ iEvent.put(triggerEventPutToken_, std::move(triggerEvent));
+ }
+
+ void TestWriteTriggerEvent::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("usedProcessName");
+ desc.add>("collectionTags");
+ desc.add>("collectionKeys");
+ desc.add>("ids");
+ desc.add>("pts");
+ desc.add>("etas");
+ desc.add>("phis");
+ desc.add>("masses");
+ desc.add>("filterTags");
+ desc.add("elementsPerVector");
+ desc.add>("filterIds");
+ desc.add>("filterKeys");
+ descriptions.addDefault(desc);
+ }
+} // namespace edmtest
+
+using edmtest::TestWriteTriggerEvent;
+DEFINE_FWK_MODULE(TestWriteTriggerEvent);
diff --git a/DataFormats/HLTReco/test/create_TriggerEvent_test_file_cfg.py b/DataFormats/HLTReco/test/create_TriggerEvent_test_file_cfg.py
new file mode 100644
index 0000000000000..591b837394ee8
--- /dev/null
+++ b/DataFormats/HLTReco/test/create_TriggerEvent_test_file_cfg.py
@@ -0,0 +1,34 @@
+import FWCore.ParameterSet.Config as cms
+
+process = cms.Process("PROD")
+
+process.load("FWCore.MessageService.MessageLogger_cfi")
+
+process.source = cms.Source("EmptySource")
+process.maxEvents.input = 1
+
+process.triggerEventProducer = cms.EDProducer("TestWriteTriggerEvent",
+ # Test values below are meaningless. We just make sure when we read
+ # we get the same values.
+ usedProcessName = cms.string("testName"),
+ collectionTags = cms.vstring('moduleA', 'moduleB', 'moduleC'),
+ collectionKeys = cms.vuint32(11, 21, 31),
+ ids = cms.vint32(1, 3, 5),
+ # I stick to values exactly convertable to float
+ # to avoid potential rounding issues in the test.
+ pts = cms.vdouble(11.0, 21.0, 31.0),
+ etas = cms.vdouble(101.0, 102.0, 103.0),
+ phis = cms.vdouble(201.0, 202.0, 203.0),
+ masses = cms.vdouble(301.0, 302.0, 303.0),
+ filterTags = cms.vstring('moduleAA', 'moduleBB'),
+ elementsPerVector = cms.uint32(2),
+ filterIds = cms.vint32(1001, 1002, 1003, 1004),
+ filterKeys = cms.vuint32(2001, 2002, 2003, 2004)
+)
+
+process.out = cms.OutputModule("PoolOutputModule",
+ fileName = cms.untracked.string('testTriggerEvent.root')
+)
+
+process.path = cms.Path(process.triggerEventProducer)
+process.endPath = cms.EndPath(process.out)
diff --git a/DataFormats/HLTReco/test/test_readTriggerEvent_cfg.py b/DataFormats/HLTReco/test/test_readTriggerEvent_cfg.py
new file mode 100644
index 0000000000000..3cf2a3cab5329
--- /dev/null
+++ b/DataFormats/HLTReco/test/test_readTriggerEvent_cfg.py
@@ -0,0 +1,35 @@
+import FWCore.ParameterSet.Config as cms
+import sys
+
+process = cms.Process("READ")
+
+process.source = cms.Source("PoolSource", fileNames = cms.untracked.vstring("file:"+sys.argv[2]))
+process.maxEvents.input = 1
+
+process.testReadTriggerEvent = cms.EDAnalyzer("TestReadTriggerEvent",
+ expectedUsedProcessName = cms.string("testName"),
+ expectedCollectionTags = cms.vstring('moduleA', 'moduleB', 'moduleC'),
+ expectedCollectionKeys = cms.vuint32(11, 21, 31),
+ expectedIds = cms.vint32(1, 3, 5),
+ # I stick to values exactly convertable from double to float
+ # to avoid potential rounding issues in the test.
+ # (configuration only supports double not float and
+ # the data format holds floats)
+ expectedPts = cms.vdouble(11.0, 21.0, 31.0),
+ expectedEtas = cms.vdouble(101.0, 102.0, 103.0),
+ expectedPhis = cms.vdouble(201.0, 202.0, 203.0),
+ expectedMasses = cms.vdouble(301.0, 302.0, 303.0),
+ expectedFilterTags = cms.vstring('moduleAA', 'moduleBB'),
+ expectedElementsPerVector = cms.uint32(2),
+ expectedFilterIds = cms.vint32(1001, 1002, 1003, 1004),
+ expectedFilterKeys = cms.vuint32(2001, 2002, 2003, 2004),
+ triggerEventTag = cms.InputTag("triggerEventProducer", "", "PROD")
+)
+
+process.out = cms.OutputModule("PoolOutputModule",
+ fileName = cms.untracked.string('testTriggerEvent2.root')
+)
+
+process.path = cms.Path(process.testReadTriggerEvent)
+
+process.endPath = cms.EndPath(process.out)