diff --git a/form/core/token.cpp b/form/core/token.cpp index dfd7529ff..2ef66d8c0 100644 --- a/form/core/token.cpp +++ b/form/core/token.cpp @@ -6,12 +6,26 @@ using namespace form::detail::experimental; -/// Constructor with initialization -Token::Token(std::string fileName, std::string containerName, technology::Id technology, int id) : +/// Placement-only constructor; id is left unset +Token::Token(std::string fileName, std::string containerName, technology::Id technology) : m_technology(technology), m_fileName(std::move(fileName)), m_containerName(std::move(containerName)), - m_id(id) + m_id(0), + m_hasId(false) +{ +} + +/// Fully-specified constructor; id is set +Token::Token(std::string fileName, + std::string containerName, + technology::Id technology, + std::uint64_t id) : + m_technology(technology), + m_fileName(std::move(fileName)), + m_containerName(std::move(containerName)), + m_id(id), + m_hasId(true) { } @@ -21,6 +35,7 @@ std::string const& Token::fileName() const { return m_fileName; } std::string const& Token::containerName() const { return m_containerName; } /// Access technology type form::technology::Id Token::technology() const { return m_technology; } -/// Set technology type -/// Access identifier/entry number -int Token::id() const { return m_id; } +/// Access identifier/entry number (0-based row) +std::uint64_t Token::id() const { return m_id; } +/// Whether an id has been set on this token +bool Token::hasId() const { return m_hasId; } diff --git a/form/core/token.hpp b/form/core/token.hpp index 821ef181a..4484b0db1 100644 --- a/form/core/token.hpp +++ b/form/core/token.hpp @@ -5,6 +5,7 @@ #include "core/technology.hpp" +#include #include /* @class Token @@ -13,11 +14,17 @@ namespace form::detail::experimental { class Token { public: - /// Default constructor; delegates to the named constructor so the -1 sentinel for id is defined once + /// Default constructor; a token with no id set (delegates to the placement-only constructor) Token() : Token("", "", {}) {} - /// Named constructor; id defaults to -1 as a "not set" sentinel - Token(std::string fileName, std::string containerName, technology::Id technology, int id = -1); + /// Placement-only constructor; leaves the id unset (hasId() == false) + Token(std::string fileName, std::string containerName, technology::Id technology); + + /// Fully-specified constructor; sets the 0-based row/entry id (hasId() == true) + Token(std::string fileName, + std::string containerName, + technology::Id technology, + std::uint64_t id); /// Access file name std::string const& fileName() const; @@ -26,8 +33,10 @@ namespace form::detail::experimental { /// Access technology type technology::Id technology() const; - /// Access identifier/entry number - int id() const; + /// Access identifier/entry number (0-based row). Only meaningful when hasId() is true. + std::uint64_t id() const; + /// Whether an id has been set on this token + bool hasId() const; private: /// Technology identifier @@ -36,8 +45,10 @@ namespace form::detail::experimental { std::string m_fileName; /// Container name std::string m_containerName; - /// Identifier/entry number - int m_id; + /// Identifier/entry number (0-based row) + std::uint64_t m_id; + /// Whether m_id holds a valid, set value + bool m_hasId; }; } // namespace form::detail::experimental #endif // FORM_CORE_TOKEN_HPP diff --git a/form/persistence/ipersistence_writer.hpp b/form/persistence/ipersistence_writer.hpp index a0ff7b62c..6738d18db 100644 --- a/form/persistence/ipersistence_writer.hpp +++ b/form/persistence/ipersistence_writer.hpp @@ -3,6 +3,8 @@ #ifndef FORM_PERSISTENCE_IPERSISTENCE_WRITER_HPP #define FORM_PERSISTENCE_IPERSISTENCE_WRITER_HPP +#include "core/token.hpp" + #include #include #include @@ -27,10 +29,12 @@ namespace form::detail::experimental { virtual void createContainers(std::string const& creator, std::map const& products) = 0; - virtual void registerWrite(std::string const& creator, - std::string const& label, - void const* data, - std::type_info const& type) = 0; + // Write one product and return a Token locating it: placement plus 0-based row (entry) number + // Throws if backend isn't row-addressed, causing Token read lookup to fail + virtual Token registerWrite(std::string const& creator, + std::string const& label, + void const* data, + std::type_info const& type) = 0; virtual void commitOutput(std::string const& creator, std::string const& id) = 0; }; diff --git a/form/persistence/persistence_writer.cpp b/form/persistence/persistence_writer.cpp index c6e13241a..e808ba4b2 100644 --- a/form/persistence/persistence_writer.cpp +++ b/form/persistence/persistence_writer.cpp @@ -48,13 +48,21 @@ void PersistenceWriter::createContainers( m_store_writer->createContainers(containers, m_tech_settings); } -void PersistenceWriter::registerWrite(std::string const& creator, - std::string const& label, - void const* data, - std::type_info const& type) +Token PersistenceWriter::registerWrite(std::string const& creator, + std::string const& label, + void const* data, + std::type_info const& type) { std::unique_ptr plcmnt = getPlacement(creator, label); - m_store_writer->fillContainer(*plcmnt, data, type); + std::uint64_t const row = m_store_writer->fillContainer(*plcmnt, data, type); + // A returned Token must locate a readable product: its row is the read-side navigation key. + // kInvalidRowId means backend does not address rows,so a product routed there could not be located on read, so throw here for such an unusable Token + if (row == kInvalidRowId) { + throw std::runtime_error("PersistenceWriter::registerWrite backend for product '" + label + + "' from creator '" + creator + "' does not address rows; " + + "cannot produce a Token locating the written product"); + } + return Token{plcmnt->fileName(), plcmnt->containerName(), plcmnt->technology(), row}; } void PersistenceWriter::commitOutput(std::string const& creator, std::string const& id) diff --git a/form/persistence/persistence_writer.hpp b/form/persistence/persistence_writer.hpp index ac198a7ff..6d514255b 100644 --- a/form/persistence/persistence_writer.hpp +++ b/form/persistence/persistence_writer.hpp @@ -31,10 +31,10 @@ namespace form::detail::experimental { void createContainers(std::string const& creator, std::map const& products) override; - void registerWrite(std::string const& creator, - std::string const& label, - void const* data, - std::type_info const& type) override; + Token registerWrite(std::string const& creator, + std::string const& label, + void const* data, + std::type_info const& type) override; void commitOutput(std::string const& creator, std::string const& id) override; private: diff --git a/form/root_storage/root_rfield_write_container.cpp b/form/root_storage/root_rfield_write_container.cpp index f09a4e921..6a7b753df 100644 --- a/form/root_storage/root_rfield_write_container.cpp +++ b/form/root_storage/root_rfield_write_container.cpp @@ -61,7 +61,7 @@ namespace form::detail::experimental { m_rntuple_parent = parentDerived; } - void ROOT_RField_Write_ContainerImp::fill(void const* data) + std::uint64_t ROOT_RField_Write_ContainerImp::fill(void const* data) { if (!m_rntuple_parent) { throw std::runtime_error( @@ -79,6 +79,11 @@ namespace form::detail::experimental { m_rntuple_parent->m_entry = m_rntuple_parent->m_writer->CreateRawPtrWriteEntry(); } m_rntuple_parent->m_entry->BindRawPtr(col_name(), data); + + // Unlike a TBranch, an RNTuple entry is only written on commit(); + // every field bound before that commit shares one entry. + // Return the 0-based index that pending entry will occupy (the current entry count). + return static_cast(m_rntuple_parent->m_writer->GetNEntries()); } void ROOT_RField_Write_ContainerImp::commit() diff --git a/form/root_storage/root_rfield_write_container.hpp b/form/root_storage/root_rfield_write_container.hpp index d5387582b..833086b87 100644 --- a/form/root_storage/root_rfield_write_container.hpp +++ b/form/root_storage/root_rfield_write_container.hpp @@ -23,7 +23,7 @@ namespace form::detail::experimental { void setFile(std::shared_ptr file) override; void setupWrite(std::type_info const& type) override; void setParent(std::shared_ptr const parent) override; - void fill(void const* data) override; + std::uint64_t fill(void const* data) override; void commit() override; private: diff --git a/form/root_storage/root_rntuple_write_container.cpp b/form/root_storage/root_rntuple_write_container.cpp index 09d861da4..e3fb4bbe5 100644 --- a/form/root_storage/root_rntuple_write_container.cpp +++ b/form/root_storage/root_rntuple_write_container.cpp @@ -29,7 +29,7 @@ namespace form::detail::experimental { return; } - void ROOT_RNTuple_Write_ContainerImp::fill(void const* /*data*/) + std::uint64_t ROOT_RNTuple_Write_ContainerImp::fill(void const* /*data*/) { throw std::runtime_error("ROOT_RNTuple_Write_ContainerImp::fill not implemented"); } diff --git a/form/root_storage/root_rntuple_write_container.hpp b/form/root_storage/root_rntuple_write_container.hpp index e386e120a..8b2e2a6d5 100644 --- a/form/root_storage/root_rntuple_write_container.hpp +++ b/form/root_storage/root_rntuple_write_container.hpp @@ -52,7 +52,7 @@ namespace form::detail::experimental { void setFile(std::shared_ptr file) override; void setupWrite(std::type_info const& type) override; - void fill(void const* data) override; + std::uint64_t fill(void const* data) override; void commit() override; //State shared by ROOT_RField_ContainerImps diff --git a/form/root_storage/root_tbranch_write_container.cpp b/form/root_storage/root_tbranch_write_container.cpp index c786d00fd..06d1cd8e8 100644 --- a/form/root_storage/root_tbranch_write_container.cpp +++ b/form/root_storage/root_tbranch_write_container.cpp @@ -94,7 +94,7 @@ void ROOT_TBranch_Write_ContainerImp::setupWrite(std::type_info const& type) } } -void ROOT_TBranch_Write_ContainerImp::fill(void const* data) +std::uint64_t ROOT_TBranch_Write_ContainerImp::fill(void const* data) { // NOTE: incoming parameter `data` is `const` due to the constraints on how we // expect users to interact with the data; however, ROOT's SetBranchAddress @@ -112,8 +112,18 @@ void ROOT_TBranch_Write_ContainerImp::fill(void const* data) } else { m_branch->SetAddress(reinterpret_cast(&data)); } - m_branch->Fill(); + // TBranch::Fill() returns the number of bytes committed, or a negative value on a write error. + // ROOT increments entry count before a basket write can fail, so check return value first + Int_t const nbytes = m_branch->Fill(); m_branch->ResetAddress(); + if (nbytes < 0) { + throw std::runtime_error("ROOT_TBranch_Write_ContainerImp::fill TBranch::Fill() failed for " + + col_name()); + } + + // 0-based entries: GetEntries() is the total count after this Fill(); row = count - 1. + // GetEntries() >= 1 here (Fill() succeeded), so the row is non-negative. + return static_cast(m_branch->GetEntries() - 1); } void ROOT_TBranch_Write_ContainerImp::commit() diff --git a/form/root_storage/root_tbranch_write_container.hpp b/form/root_storage/root_tbranch_write_container.hpp index d61533342..154ceadb7 100644 --- a/form/root_storage/root_tbranch_write_container.hpp +++ b/form/root_storage/root_tbranch_write_container.hpp @@ -25,7 +25,7 @@ namespace form::detail::experimental { void setParent(std::shared_ptr parent) override; void setupWrite(std::type_info const& type = typeid(void)) override; - void fill(void const* data) override; + std::uint64_t fill(void const* data) override; void commit() override; private: diff --git a/form/root_storage/root_ttree_write_container.cpp b/form/root_storage/root_ttree_write_container.cpp index 1a769d100..7f74a4cc8 100644 --- a/form/root_storage/root_ttree_write_container.cpp +++ b/form/root_storage/root_ttree_write_container.cpp @@ -45,7 +45,7 @@ void ROOT_TTree_Write_ContainerImp::setupWrite(std::type_info const& /* type*/) } } -void ROOT_TTree_Write_ContainerImp::fill(void const* /* data*/) +std::uint64_t ROOT_TTree_Write_ContainerImp::fill(void const* /* data*/) { throw std::runtime_error("ROOT_TTree_Write_ContainerImp::fill not implemented"); } diff --git a/form/root_storage/root_ttree_write_container.hpp b/form/root_storage/root_ttree_write_container.hpp index 6a5c90a2a..cc34a7a24 100644 --- a/form/root_storage/root_ttree_write_container.hpp +++ b/form/root_storage/root_ttree_write_container.hpp @@ -25,7 +25,7 @@ namespace form::detail::experimental { void setFile(std::shared_ptr file) override; void setupWrite(std::type_info const& type) override; - void fill(void const* data) override; + std::uint64_t fill(void const* data) override; void commit() override; TTree* getTTree(); diff --git a/form/storage/istorage.hpp b/form/storage/istorage.hpp index a755e4279..71f9fd169 100644 --- a/form/storage/istorage.hpp +++ b/form/storage/istorage.hpp @@ -7,6 +7,8 @@ #include "core/token.hpp" #include "form/config.hpp" +#include +#include #include #include #include @@ -14,6 +16,10 @@ namespace form::detail::experimental { + // Sentinel returned by the write chain when no addressable row was written + // (e.g. the generic no-op container). A real row is always < this value. + inline constexpr std::uint64_t kInvalidRowId = std::numeric_limits::max(); + class IStorageReader { public: IStorageReader() = default; @@ -41,9 +47,10 @@ namespace form::detail::experimental { virtual void createContainers( std::map, std::type_info const*> const& containers, form::experimental::config::tech_setting_config const& settings) = 0; - virtual void fillContainer(Placement const& plcmnt, - void const* data, - std::type_info const& type) = 0; + // Returns the 0-based row (entry) number written, or kInvalidRowId if no rows + virtual std::uint64_t fillContainer(Placement const& plcmnt, + void const* data, + std::type_info const& type) = 0; virtual void commitContainers(Placement const& plcmnt) = 0; }; @@ -67,7 +74,8 @@ namespace form::detail::experimental { virtual void setFile(std::shared_ptr file) = 0; virtual void setupWrite(std::type_info const& type = typeid(void)) = 0; - virtual void fill(void const* data) = 0; + // Returns the 0-based row (entry) number written, or kInvalidRowId if no rows + virtual std::uint64_t fill(void const* data) = 0; virtual void commit() = 0; virtual void setAttribute(std::string const& name, std::string const& value) = 0; diff --git a/form/storage/storage_reader.cpp b/form/storage/storage_reader.cpp index ec642483a..42bdbcc80 100644 --- a/form/storage/storage_reader.cpp +++ b/form/storage/storage_reader.cpp @@ -388,5 +388,6 @@ void StorageReader::readContainer(Token const& token, cont->second->setAttribute(key, value); } } - cont->second->read(token.id(), data, type); + // TODO: Token::id() is a 64-bit row; the read container interface still takes an int entry. Narrow explicitly here (exact for all realistic row counts). Widening the read path to 64-bit is a follow-up PR. + cont->second->read(static_cast(token.id()), data, type); } diff --git a/form/storage/storage_write_container.cpp b/form/storage/storage_write_container.cpp index 785207754..cabead3a8 100644 --- a/form/storage/storage_write_container.cpp +++ b/form/storage/storage_write_container.cpp @@ -18,7 +18,7 @@ void Storage_Write_Container::setFile(std::shared_ptr file) { m_f void Storage_Write_Container::setupWrite(std::type_info const& /* type*/) {} -void Storage_Write_Container::fill(void const* /* data*/) {} +std::uint64_t Storage_Write_Container::fill(void const* /* data*/) { return kInvalidRowId; } void Storage_Write_Container::commit() {} diff --git a/form/storage/storage_write_container.hpp b/form/storage/storage_write_container.hpp index 997e47b17..a68ef2987 100644 --- a/form/storage/storage_write_container.hpp +++ b/form/storage/storage_write_container.hpp @@ -20,7 +20,7 @@ namespace form::detail::experimental { void setFile(std::shared_ptr file) override; void setupWrite(std::type_info const& type = typeid(void)) override; - void fill(void const* data) override; + std::uint64_t fill(void const* data) override; void commit() override; void setAttribute(std::string const& name, std::string const& value) override; diff --git a/form/storage/storage_writer.cpp b/form/storage/storage_writer.cpp index a6b081ee7..42ed42831 100644 --- a/form/storage/storage_writer.cpp +++ b/form/storage/storage_writer.cpp @@ -103,9 +103,9 @@ void StorageWriter::createContainers( } } -void StorageWriter::fillContainer(Placement const& plcmnt, - void const* data, - std::type_info const& /* type*/) +std::uint64_t StorageWriter::fillContainer(Placement const& plcmnt, + void const* data, + std::type_info const& /* type*/) { // Use file+container as composite key auto contKey = std::make_pair(plcmnt.fileName(), plcmnt.containerName()); @@ -115,7 +115,7 @@ void StorageWriter::fillContainer(Placement const& plcmnt, throw std::runtime_error("StorageWriter::fillContainer Container doesn't exist: " + plcmnt.containerName()); } - cont->second->fill(data); + return cont->second->fill(data); } void StorageWriter::commitContainers(Placement const& plcmnt) diff --git a/form/storage/storage_writer.hpp b/form/storage/storage_writer.hpp index 50679d317..28e2f2e41 100644 --- a/form/storage/storage_writer.hpp +++ b/form/storage/storage_writer.hpp @@ -23,9 +23,9 @@ namespace form::detail::experimental { void createContainers( std::map, std::type_info const*> const& containers, form::experimental::config::tech_setting_config const& settings) override; - void fillContainer(Placement const& plcmnt, - void const* data, - std::type_info const& type) override; + std::uint64_t fillContainer(Placement const& plcmnt, + void const* data, + std::type_info const& type) override; void commitContainers(Placement const& plcmnt) override; private: diff --git a/test/form/form_basics_test.cpp b/test/form/form_basics_test.cpp index 12b6e4c21..0e2340929 100644 --- a/test/form/form_basics_test.cpp +++ b/test/form/form_basics_test.cpp @@ -37,8 +37,8 @@ TEST_CASE("Token default constructor", "[form]") CHECK(t.fileName().empty()); CHECK(t.containerName().empty()); CHECK(t.technology() == form::technology::Id{}); - // Default-constructed token must carry the -1 sentinel for id - CHECK(t.id() == -1); + // Default-constructed token has no id set + CHECK_FALSE(t.hasId()); } TEST_CASE("Token basics", "[form]") @@ -47,7 +47,8 @@ TEST_CASE("Token basics", "[form]") CHECK(t.fileName() == "file.root"); CHECK(t.containerName() == "container"); CHECK(t.technology() == form::technology::ROOT_TTREE); - CHECK(t.id() == 42); + CHECK(t.hasId()); + CHECK(t.id() == 42u); } TEST_CASE("technology::Id string conversions", "[form]") diff --git a/test/form/form_storage_test.cpp b/test/form/form_storage_test.cpp index e3632f4c0..21a7e128f 100644 --- a/test/form/form_storage_test.cpp +++ b/test/form/form_storage_test.cpp @@ -5,16 +5,20 @@ #include "form/config.hpp" #include "persistence/persistence_reader.hpp" #include "persistence/persistence_writer.hpp" +#include "root_storage/root_tfile.hpp" +#include "root_storage/root_ttree_write_container.hpp" #include "storage/storage_file.hpp" #include "storage/storage_reader.hpp" #include "storage/storage_write_container.hpp" +#include "TBranch.h" #include "TFile.h" #include "TTree.h" #include #include +#include #include #include #include @@ -281,6 +285,68 @@ TEST_CASE("Root TTree write container: fill and commit are not implemented", "[f CHECK_THROWS_AS(writeAssoc->commit(), std::runtime_error); } +TEST_CASE("Root TBranch fill: throws when TBranch::Fill() reports a write error", "[form]") +{ + // Exercises the defensive guard in ROOT_TBranch_Write_ContainerImp::fill(): + // TBranch::Fill() returns a negative value when a basket flush to disk fails, and + // fill() must throw rather than hand back a row id for data that was never persisted. + // + // TBranch is ROOT_TTREE-specific, so this test hard-codes that technology instead of + // using the (CLI-overridable) global `technology`, which may select ROOT_RNTUPLE. + // + // To provoke a deterministic write failure we (1) shrink the branch basket so that a + // handful of fills force a basket flush to disk, and (2) mark the underlying TFile + // non-writable so that flush fails and Fill() returns a negative value. + auto const tech = form::technology::ROOT_TTREE; + + auto file = createFile(tech, "tbranch_fill_write_error.root", 'o'); + auto tree = createWriteAssociation(tech, "faketree"); + auto branch = createWriteContainer(tech, "faketree/fakebranch"); + + tree->setFile(file); + tree->setupWrite(typeid(double)); + + auto branchAssoc = dynamic_pointer_cast(branch); + REQUIRE(branchAssoc != nullptr); + branchAssoc->setParent(tree); + branch->setFile(file); + branch->setupWrite(typeid(double)); + + // Reach the raw ROOT objects created through the factory wiring above. + auto root_file = dynamic_pointer_cast(file); + REQUIRE(root_file != nullptr); + auto* root_tree = dynamic_cast(tree.get()); + REQUIRE(root_tree != nullptr); + + TTree* raw_tree = root_tree->getTTree(); + REQUIRE(raw_tree != nullptr); + TBranch* raw_branch = raw_tree->GetBranch("fakebranch"); + REQUIRE(raw_branch != nullptr); + + // A tiny basket forces a flush after only a few fills. ROOT clamps the minimum to 100 + // bytes, so a handful of 8-byte doubles is enough to overflow it. + raw_branch->SetBasketSize(100); + + // Make the file non-writable so the forced basket flush fails. + std::shared_ptr raw_tfile = root_file->getTFile(); + REQUIRE(raw_tfile != nullptr); + raw_tfile->SetWritable(false); + + // Keep filling until a basket flush is triggered; the flush cannot reach the read-only + // file, TBranch::Fill() returns a negative value, and fill() surfaces it as a throw. + double value = std::numbers::pi; + CHECK_THROWS_AS( + [&] { + for (int i = 0; i < 100000; ++i) { + branch->fill(&value); + } + }(), + std::runtime_error); + + // Restore writability so container teardown (which writes the tree) does not error. + raw_tfile->SetWritable(true); +} + TEST_CASE("Persistence round-trip: structured index normalization and listing", "[form]") { using namespace form::experimental::config; @@ -331,6 +397,91 @@ TEST_CASE("Persistence round-trip: structured index normalization and listing", CHECK((*read_first == first || *read_first == second)); } +TEST_CASE("registerWrite returns a Token locating the written product", "[form]") +{ + using namespace form::experimental::config; + + std::string const file_name = + "registerwrite_rowid_" + form::technology::to_string(technology) + ".root"; + std::string const creator = "rowid_creator"; + std::string const container = creator + "/prod"; + + ItemConfig cfg; + cfg.addItem("prod", file_name, technology); + + std::vector const first = {11, 22, 33}; + std::vector const second = {44, 55, 66}; + + Token token_first; + Token token_second; + { + auto writer = createPersistenceWriter(); + REQUIRE(writer != nullptr); + writer->configure(cfg); + writer->configureTechSettings(tech_setting_config{}); + writer->createContainers(creator, {{"prod", &typeid(std::vector)}}); + + token_first = writer->registerWrite(creator, "prod", &first, typeid(std::vector)); + writer->commitOutput(creator, "[event:1, segment:1]"); + + token_second = writer->registerWrite(creator, "prod", &second, typeid(std::vector)); + writer->commitOutput(creator, "[event:1, segment:2]"); + } + + // The returned Token carries the placement and the 0-based, monotonically increasing row + CHECK(token_first.hasId()); + CHECK(token_first.id() == 0u); + CHECK(token_first.containerName() == container); + CHECK(token_second.hasId()); + CHECK(token_second.id() == 1u); + + // Token returned by the write is directly usable on the read side: no hand-buit Token or re-scan + StorageReader reader; + tech_setting_config const settings{}; + + // readContainer allocates the payload and transfers ownership to the caller. + void const* raw = nullptr; + reader.readContainer(token_first, &raw, typeid(std::vector), settings); + std::unique_ptr const> const got_first( + static_cast const*>(raw)); + REQUIRE(got_first != nullptr); + CHECK(*got_first == first); + + raw = nullptr; + reader.readContainer(token_second, &raw, typeid(std::vector), settings); + std::unique_ptr const> const got_second( + static_cast const*>(raw)); + REQUIRE(got_second != nullptr); + CHECK(*got_second == second); +} + +TEST_CASE("registerWrite throws when the backend does not address rows", "[form]") +{ + using namespace form::experimental::config; + + // The generic ("no technology specified") backend's write container is a no-op whose fill() + // returns kInvalidRowId, and its read side is a no-op too, so a product routed there could + // never be located on read. registerWrite must reject that rather than return an unusable + // Token whose row would later be used as the read-side navigation key. + form::technology::Id const generic{}; + std::string const file_name = "registerwrite_notset_row.generic"; + std::string const creator = "notset_creator"; + + ItemConfig cfg; + cfg.addItem("prod", file_name, generic); + + std::vector const payload = {1, 2, 3}; + + auto writer = createPersistenceWriter(); + REQUIRE(writer != nullptr); + writer->configure(cfg); + writer->configureTechSettings(tech_setting_config{}); + writer->createContainers(creator, {{"prod", &typeid(std::vector)}}); + + CHECK_THROWS_AS(writer->registerWrite(creator, "prod", &payload, typeid(std::vector)), + std::runtime_error); +} + TEST_CASE("Persistence round-trip: all-zero structured id fallback", "[form]") { using namespace form::experimental::config;