Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ntuple] Remove RFieldBase::GetNElements() #16768

Merged
1 change: 1 addition & 0 deletions tree/ntuple/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ SOURCES
v7/src/RNTupleUtil.cxx
v7/src/RNTupleWriteOptions.cxx
v7/src/RNTupleWriter.cxx
v7/src/RNTupleView.cxx
v7/src/RPage.cxx
v7/src/RPageAllocator.cxx
v7/src/RPagePool.cxx
Expand Down
6 changes: 0 additions & 6 deletions tree/ntuple/v7/inc/ROOT/RFieldBase.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -515,12 +515,6 @@ public:
const std::string &GetTypeAlias() const { return fTypeAlias; }
ENTupleStructure GetStructure() const { return fStructure; }
std::size_t GetNRepetitions() const { return fNRepetitions; }
NTupleSize_t GetNElements() const
{
if (fState == EState::kUnconnected)
throw RException(R__FAIL("Cannot call GetNElements() on an unconnected field!"));
return fPrincipalColumn->GetNElements();
}
const RFieldBase *GetParent() const { return fParent; }
std::vector<RFieldBase *> GetSubFields();
std::vector<const RFieldBase *> GetSubFields() const;
Expand Down
3 changes: 2 additions & 1 deletion tree/ntuple/v7/inc/ROOT/RNTupleDescriptor.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ public:
using iterator = RIterator;
using value_type = RFieldDescriptor;
using difference_type = std::ptrdiff_t;
using pointer = RColumnDescriptor *;
using pointer = const RColumnDescriptor *;
using reference = const RColumnDescriptor &;

RIterator(const RNTupleDescriptor &ntuple, const std::vector<DescriptorId_t> &columns, std::size_t index)
Expand All @@ -734,6 +734,7 @@ public:
return *this;
}
reference operator*() { return fNTuple.GetColumnDescriptor(fColumns.at(fIndex)); }
pointer operator->() { return &fNTuple.GetColumnDescriptor(fColumns.at(fIndex)); }
bool operator!=(const iterator &rh) const { return fIndex != rh.fIndex; }
bool operator==(const iterator &rh) const { return fIndex == rh.fIndex; }
};
Expand Down
16 changes: 12 additions & 4 deletions tree/ntuple/v7/inc/ROOT/RNTupleReader.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -286,19 +286,25 @@ public:
template <typename T>
RNTupleView<T> GetView(DescriptorId_t fieldId)
{
return RNTupleView<T>(RNTupleView<T>::CreateField(fieldId, fSource.get()));
auto field = RNTupleView<T>::CreateField(fieldId, *fSource);
auto range = Internal::GetFieldRange(*field, *fSource);
return RNTupleView<T>(std::move(field), range);
}

template <typename T>
RNTupleView<T> GetView(DescriptorId_t fieldId, std::shared_ptr<T> objPtr)
{
return RNTupleView<T>(RNTupleView<T>::CreateField(fieldId, fSource.get()), objPtr);
auto field = RNTupleView<T>::CreateField(fieldId, *fSource);
auto range = Internal::GetFieldRange(*field, *fSource);
return RNTupleView<T>(std::move(field), range, objPtr);
}

template <typename T>
RNTupleView<T> GetView(DescriptorId_t fieldId, T *rawPtr)
{
return RNTupleView<T>(RNTupleView<T>::CreateField(fieldId, fSource.get()), rawPtr);
auto field = RNTupleView<T>::CreateField(fieldId, *fSource);
auto range = Internal::GetFieldRange(*field, *fSource);
return RNTupleView<T>(std::move(field), range, rawPtr);
}

template <typename T>
Expand All @@ -310,7 +316,9 @@ public:
template <typename T>
RNTupleDirectAccessView<T> GetDirectAccessView(DescriptorId_t fieldId)
{
return RNTupleDirectAccessView<T>(RNTupleDirectAccessView<T>::CreateField(fieldId, fSource.get()));
auto field = RNTupleDirectAccessView<T>::CreateField(fieldId, *fSource);
auto range = Internal::GetFieldRange(field, *fSource);
return RNTupleDirectAccessView<T>(std::move(field), range);
}

/// Raises an exception if:
Expand Down
85 changes: 59 additions & 26 deletions tree/ntuple/v7/inc/ROOT/RNTupleView.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ namespace Experimental {
// clang-format on
class RNTupleGlobalRange {
private:
const NTupleSize_t fStart;
const NTupleSize_t fEnd;
NTupleSize_t fStart;
NTupleSize_t fEnd;

public:
class RIterator {
private:
Expand Down Expand Up @@ -68,6 +69,7 @@ public:
RNTupleGlobalRange(NTupleSize_t start, NTupleSize_t end) : fStart(start), fEnd(end) {}
RIterator begin() { return RIterator(fStart); }
RIterator end() { return RIterator(fEnd); }
NTupleSize_t size() { return fEnd - fStart; }
};


Expand Down Expand Up @@ -113,6 +115,16 @@ public:
RIterator end() { return RIterator(RClusterIndex(fClusterId, fEnd)); }
};

namespace Internal {

/// Helper to get the iteration space of the given field that needs to be connected to the given page source.
/// The indexes are given by the number of elements of the principal column of the field or, if none exists,
/// by the number of elements of the first principal column found in the subfields searched by BFS.
/// If the field hierarchy is empty on columns, throw an exception.
RNTupleGlobalRange GetFieldRange(const RFieldBase &field, const RPageSource &pageSource);

} // namespace Internal

// clang-format off
/**
\class ROOT::Experimental::RNTupleViewBase
Expand All @@ -132,13 +144,14 @@ template <typename T>
class RNTupleViewBase {
protected:
std::unique_ptr<RFieldBase> fField;
RNTupleGlobalRange fFieldRange;
RFieldBase::RValue fValue;

static std::unique_ptr<RFieldBase> CreateField(DescriptorId_t fieldId, Internal::RPageSource *pageSource)
static std::unique_ptr<RFieldBase> CreateField(DescriptorId_t fieldId, Internal::RPageSource &pageSource)
{
std::unique_ptr<RFieldBase> field;
{
const auto &desc = pageSource->GetSharedDescriptorGuard().GetRef();
const auto &desc = pageSource.GetSharedDescriptorGuard().GetRef();
const auto &fieldDesc = desc.GetFieldDescriptor(fieldId);
if constexpr (std::is_void_v<T>) {
field = fieldDesc.CreateField(desc);
Expand All @@ -147,19 +160,22 @@ protected:
}
}
field->SetOnDiskId(fieldId);
Internal::CallConnectPageSourceOnField(*field, *pageSource);
Internal::CallConnectPageSourceOnField(*field, pageSource);
return field;
}

RNTupleViewBase(std::unique_ptr<RFieldBase> field) : fField(std::move(field)), fValue(fField->CreateValue()) {}
RNTupleViewBase(std::unique_ptr<RFieldBase> field, RNTupleGlobalRange range)
: fField(std::move(field)), fFieldRange(range), fValue(fField->CreateValue())
{
}

RNTupleViewBase(std::unique_ptr<RFieldBase> field, std::shared_ptr<T> objPtr)
: fField(std::move(field)), fValue(fField->BindValue(objPtr))
RNTupleViewBase(std::unique_ptr<RFieldBase> field, RNTupleGlobalRange range, std::shared_ptr<T> objPtr)
: fField(std::move(field)), fFieldRange(range), fValue(fField->BindValue(objPtr))
{
}

RNTupleViewBase(std::unique_ptr<RFieldBase> field, T *rawPtr)
: fField(std::move(field)), fValue(fField->BindValue(Internal::MakeAliasedSharedPtr(rawPtr)))
RNTupleViewBase(std::unique_ptr<RFieldBase> field, RNTupleGlobalRange range, T *rawPtr)
: fField(std::move(field)), fFieldRange(range), fValue(fField->BindValue(Internal::MakeAliasedSharedPtr(rawPtr)))
{
}

Expand All @@ -172,7 +188,7 @@ public:

const RFieldBase &GetField() const { return *fField; }
const RFieldBase::RValue &GetValue() const { return fValue; }
RNTupleGlobalRange GetFieldRange() const { return RNTupleGlobalRange(0, fField->GetNElements()); }
RNTupleGlobalRange GetFieldRange() const { return fFieldRange; }

void Bind(std::shared_ptr<T> objPtr) { fValue.Bind(objPtr); }
void BindRawPtr(T *rawPtr) { fValue.BindRawPtr(rawPtr); }
Expand All @@ -192,14 +208,20 @@ class RNTupleView : public RNTupleViewBase<T> {
friend class RNTupleCollectionView;

protected:
RNTupleView(std::unique_ptr<RFieldBase> field) : RNTupleViewBase<T>(std::move(field)) {}
RNTupleView(std::unique_ptr<RFieldBase> field, RNTupleGlobalRange range)
: RNTupleViewBase<T>(std::move(field), range)
{
}

RNTupleView(std::unique_ptr<RFieldBase> field, std::shared_ptr<T> objPtr)
: RNTupleViewBase<T>(std::move(field), objPtr)
RNTupleView(std::unique_ptr<RFieldBase> field, RNTupleGlobalRange range, std::shared_ptr<T> objPtr)
: RNTupleViewBase<T>(std::move(field), range, objPtr)
{
}

RNTupleView(std::unique_ptr<RFieldBase> field, T *rawPtr) : RNTupleViewBase<T>(std::move(field), rawPtr) {}
RNTupleView(std::unique_ptr<RFieldBase> field, RNTupleGlobalRange range, T *rawPtr)
: RNTupleViewBase<T>(std::move(field), range, rawPtr)
{
}

public:
RNTupleView(const RNTupleView &other) = delete;
Expand Down Expand Up @@ -234,14 +256,20 @@ class RNTupleView<void> final : public RNTupleViewBase<void> {
friend class RNTupleCollectionView;

protected:
RNTupleView(std::unique_ptr<RFieldBase> field) : RNTupleViewBase<void>(std::move(field)) {}
RNTupleView(std::unique_ptr<RFieldBase> field, RNTupleGlobalRange range)
: RNTupleViewBase<void>(std::move(field), range)
{
}

RNTupleView(std::unique_ptr<RFieldBase> field, std::shared_ptr<void> objPtr)
: RNTupleViewBase<void>(std::move(field), objPtr)
RNTupleView(std::unique_ptr<RFieldBase> field, RNTupleGlobalRange range, std::shared_ptr<void> objPtr)
: RNTupleViewBase<void>(std::move(field), range, objPtr)
{
}

RNTupleView(std::unique_ptr<RFieldBase> field, void *rawPtr) : RNTupleViewBase<void>(std::move(field), rawPtr) {}
RNTupleView(std::unique_ptr<RFieldBase> field, RNTupleGlobalRange range, void *rawPtr)
: RNTupleViewBase<void>(std::move(field), range, rawPtr)
{
}

public:
RNTupleView(const RNTupleView &other) = delete;
Expand All @@ -268,22 +296,23 @@ class RNTupleDirectAccessView {

protected:
RField<T> fField;
RNTupleGlobalRange fFieldRange;

static RField<T> CreateField(DescriptorId_t fieldId, Internal::RPageSource *pageSource)
static RField<T> CreateField(DescriptorId_t fieldId, Internal::RPageSource &pageSource)
{
const auto &desc = pageSource->GetSharedDescriptorGuard().GetRef();
const auto &desc = pageSource.GetSharedDescriptorGuard().GetRef();
const auto &fieldDesc = desc.GetFieldDescriptor(fieldId);
if (fieldDesc.GetTypeName() != RField<T>::TypeName()) {
throw RException(R__FAIL("type mismatch for field " + fieldDesc.GetFieldName() + ": " +
fieldDesc.GetTypeName() + " vs. " + RField<T>::TypeName()));
}
RField<T> field(fieldDesc.GetFieldName());
field.SetOnDiskId(fieldId);
Internal::CallConnectPageSourceOnField(field, *pageSource);
Internal::CallConnectPageSourceOnField(field, pageSource);
return field;
}

RNTupleDirectAccessView(RField<T> field) : fField(std::move(field)) {}
RNTupleDirectAccessView(RField<T> field, RNTupleGlobalRange range) : fField(std::move(field)), fFieldRange(range) {}

public:
RNTupleDirectAccessView(const RNTupleDirectAccessView &other) = delete;
Expand All @@ -293,7 +322,7 @@ public:
~RNTupleDirectAccessView() = default;

const RFieldBase &GetField() const { return fField; }
RNTupleGlobalRange GetFieldRange() const { return RNTupleGlobalRange(0, fField.GetNElements()); }
RNTupleGlobalRange GetFieldRange() const { return fFieldRange; }

const T &operator()(NTupleSize_t globalIndex) { return *fField.Map(globalIndex); }
const T &operator()(RClusterIndex clusterIndex) { return *fField.Map(clusterIndex); }
Expand Down Expand Up @@ -374,14 +403,18 @@ public:
template <typename T>
RNTupleView<T> GetView(std::string_view fieldName)
{
return RNTupleView<T>(RNTupleView<T>::CreateField(GetFieldId(fieldName), fSource));
auto field = RNTupleView<T>::CreateField(GetFieldId(fieldName), *fSource);
auto range = Internal::GetFieldRange(*field, *fSource);
return RNTupleView<T>(std::move(field), range);
}

/// Raises an exception if there is no field with the given name.
template <typename T>
RNTupleDirectAccessView<T> GetDirectAccessView(std::string_view fieldName)
{
return RNTupleDirectAccessView<T>(RNTupleDirectAccessView<T>::CreateField(GetFieldId(fieldName), fSource));
auto field = RNTupleDirectAccessView<T>::CreateField(GetFieldId(fieldName), *fSource);
auto range = Internal::GetFieldRange(field, *fSource);
return RNTupleDirectAccessView<T>(std::move(field), range);
}

/// Raises an exception if there is no field with the given name.
Expand Down
48 changes: 48 additions & 0 deletions tree/ntuple/v7/src/RNTupleView.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/// \file RNTupleView.cxx
/// \ingroup NTuple ROOT7
/// \author Jakob Blomer <[email protected]>
/// \date 2024-10-28
/// \warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback
/// is welcome!

/*************************************************************************
* Copyright (C) 1995-2024, Rene Brun and Fons Rademakers. *
* All rights reserved. *
* *
* For the licensing terms see $ROOTSYS/LICENSE. *
* For the list of contributors see $ROOTSYS/README/CREDITS. *
*************************************************************************/

#include <ROOT/RError.hxx>
#include <ROOT/RFieldBase.hxx>
#include <ROOT/RNTupleDescriptor.hxx>
#include <ROOT/RNTupleView.hxx>
#include <ROOT/RPageStorage.hxx>

ROOT::Experimental::RNTupleGlobalRange
ROOT::Experimental::Internal::GetFieldRange(const RFieldBase &field, const RPageSource &pageSource)
{
const auto &desc = pageSource.GetSharedDescriptorGuard().GetRef();

auto fnGetPrincipalColumnId = [&desc](DescriptorId_t fieldId) -> DescriptorId_t {
auto columnIterable = desc.GetColumnIterable(fieldId);
return (columnIterable.size() > 0) ? columnIterable.begin()->GetPhysicalId() : kInvalidDescriptorId;
};

auto columnId = fnGetPrincipalColumnId(field.GetOnDiskId());
if (columnId == kInvalidDescriptorId) {
for (const auto &f : field) {
columnId = fnGetPrincipalColumnId(f.GetOnDiskId());
if (columnId != kInvalidDescriptorId)
break;
}
}

if (columnId == kInvalidDescriptorId) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it strictly necessary to throw in this case? I would imagine iterating over an empty field behaves the same as iterating over an empty vector or other collection, where a for-each loops just directly terminates and any direct access will throw an index-out-of-bounds error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's more tricky: you can have an empty struct with a read rule. In this case, you'd still like to call the read rule in every iteration, but it is tricky to figure out how often one should call it... So this is why I thought it's easiest for now to forbid this special case. We may, for the special case of top-level fields, pass the entry range to the created view.

Copy link
Member

Choose a reason for hiding this comment

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

I would agree that it make sense to at least delay the implementation until we have a clearer picture of the use cases.

Copy link

Choose a reason for hiding this comment

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

Just to note that this seems to break ATLAS' production workflow tests where we're reading RNTuple inputs.

Copy link
Member

Choose a reason for hiding this comment

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

What is the failures? Have the file been regenerated?

Copy link

@amete amete Nov 5, 2024

Choose a reason for hiding this comment

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

This is in a mutli-chain workflow. The first step creates an RNTuple that is read by the second step. We throw on the very first event of the second step, seemingly related to reading an empty top-level field, e.g. (the messages need to be improved but that's independent):

12:37:41 ERROR (pool):
12:37:41 Unknown Source> field iteration over empty fields is unsupported: xAOD__EventInfo_v1_EventInfo
12:37:41 At:
12:37:41   ROOT::Experimental::RNTupleGlobalRange ROOT::Experimental::Internal::GetFieldRange(const ROOT::Experimental::RFieldBase&, const RPageSource&) [/build/jenkins/workspace/lcg_nightly_pipeline/build/projects/ROOT-HEAD/src/ROOT/HEAD/tree/ntuple/v7/src/RNTupleView.cxx:42]
12:37:41 
12:37:41 EventData(xAOD::EventInfo_v1/EventInfo)> Cannot open ROOT container(Tree/Branch)
12:37:41 StorageSvc                                             0     0   ERROR Could not read object: [DB=7316ACAF-6478-5C4A-B7E8-29498AC3D2AB][CNT=EventData(xAOD::EventInfo_v1/EventInfo)][CLID=AE8BED6D-1D41-4CAF-994B-42613FC91A0A][TECH=00000205][OID=0000097700000026-0000097700000000]
12:37:41 AthenaPoolConverter                                    0     0   ERROR poolToObject: Could not get object for Token = [DB=7316ACAF-6478-5C4A-B7E8-29498AC3D2AB][CNT=EventData(xAOD::EventInfo_v1/EventInfo)][CLID=AE8BED6D-1D41-4CAF-994B-42613FC91A0A][TECH=00000205][OID=0000097700000026-0000097700000000]
12:37:41 Exception: POOL read failed. Token = [DB=7316ACAF-6478-5C4A-B7E8-29498AC3D2AB][CNT=EventData(xAOD::EventInfo_v1/EventInfo)][CLID=AE8BED6D-1D41-4CAF-994B-42613FC91A0A][TECH=00000205][OID=0000097700000026-0000097700000000] (no backtrace available).
12:37:41 AthenaPoolConverter                                    0     0   ERROR createObj - caught exception: AthenaPoolCnvSvc::::ExcCaughtException: Caught exception in StatusCode T_AthenaPoolCustomCnvWithKey<TRANS, PERS>::PoolToDataObject(DataObject*&, const Token*, const std::string&) [with TRANS = xAOD::EventInfo_v1; PERS = xAOD::EventInfo_v1; std::string = std::__cxx11::basic_string<char>] while creating transient objectxAOD::EventInfo_v1/EventInfo: std::runtime_error: POOL read failed. Token = [DB=7316ACAF-6478-5C4A-B7E8-29498AC3D2AB][CNT=EventData(xAOD::EventInfo_v1/EventInfo)][CLID=AE8BED6D-1D41-4CAF-994B-42613FC91A0A][TECH=00000205][OID=0000097700000026-0000097700000000]
12:37:41 AthenaPoolConverter                                    0     0   ERROR createObj failed to get DataObject, Token = [DB=7316ACAF-6478-5C4A-B7E8-29498AC3D2AB][CNT=EventData(xAOD::EventInfo_v1/EventInfo)][CLID=AE8BED6D-1D41-4CAF-994B-42613FC91A0A][TECH=00000205][OID=0000097700000026-0000097700000000]
12:37:41 DataProxy                                              0     0 WARNING accessData: conversion failed for data object 45903698/EventInfo
12:37:41  Returning NULL DataObject pointer
12:37:41 AthenaHiveEventLoopMgr                                 0     0   ERROR Unable to retrieve Event root object
12:37:41 AthenaHiveEventLoopMgr                                 0     0   ERROR declareEventRootAddress for context s: 0  e: 0 failed
12:37:41 AthenaHiveEventLoopMgr                                 0     0   ERROR Terminating event processing loop due to errors

Copy link
Member

Choose a reason for hiding this comment

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

Let's continue the conversation at #16826

throw RException(R__FAIL("field iteration over empty fields is unsupported: " +
desc.GetQualifiedFieldName(field.GetOnDiskId())));
}

auto arraySize = std::max(std::uint64_t(1), desc.GetFieldDescriptor(field.GetOnDiskId()).GetNRepetitions());
return RNTupleGlobalRange(0, desc.GetNElements(columnId) / arraySize);
}
2 changes: 1 addition & 1 deletion tree/ntuple/v7/test/ntuple_multi_column.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ TEST(RNTuple, MultiColumnRepresentationSimple)
}

auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath());
EXPECT_EQ(3u, reader->GetModel().GetConstField("px").GetNElements());
EXPECT_EQ(3u, reader->GetView<float>("px").GetFieldRange().size());

const auto &desc = reader->GetDescriptor();
EXPECT_EQ(3u, desc.GetNClusters());
Expand Down
2 changes: 1 addition & 1 deletion tree/ntuple/v7/test/ntuple_parallel_writer.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ TEST(RNTupleParallelWriter, StagedMultiColumn)
}

auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath());
EXPECT_EQ(3u, reader->GetModel().GetConstField("px").GetNElements());
EXPECT_EQ(3u, reader->GetView<float>("px").GetFieldRange().size());

const auto &desc = reader->GetDescriptor();
EXPECT_EQ(3u, desc.GetNClusters());
Expand Down
41 changes: 41 additions & 0 deletions tree/ntuple/v7/test/ntuple_view.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,44 @@ TEST(RNTuple, ViewOutOfBounds)
EXPECT_THAT(ex.what(), testing::HasSubstr("out of bounds"));
}
}

TEST(RNTuple, ViewFieldIteration)
{
FileRaii fileGuard("test_ntuple_viewfielditeration.root");

{
auto model = RNTupleModel::Create();
model->MakeField<float>("pt");
model->MakeField<std::vector<float>>("vec");
model->MakeField<std::atomic<int>>("atomic");
model->MakeField<CustomEnum>("enum");
model->MakeField<std::array<CustomEnum, 2>>("array");
model->MakeField<CustomStruct>("struct");
model->MakeField<EmptyStruct>("empty");

auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath());
writer->Fill();
}

auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath());

auto viewPt = reader->GetView<void>("pt");
EXPECT_EQ(1u, viewPt.GetFieldRange().size());
auto viewVec = reader->GetView<void>("vec");
EXPECT_EQ(1u, viewVec.GetFieldRange().size());
auto viewAtomic = reader->GetView<void>("atomic");
EXPECT_EQ(1u, viewAtomic.GetFieldRange().size());
auto viewEnum = reader->GetView<void>("enum");
EXPECT_EQ(1u, viewEnum.GetFieldRange().size());
auto viewStruct = reader->GetView<void>("struct");
EXPECT_EQ(1u, viewStruct.GetFieldRange().size());
auto viewArray = reader->GetView<void>("array");
EXPECT_EQ(1u, viewArray.GetFieldRange().size());

try {
auto viewEmpty = reader->GetView<void>("empty");
FAIL() << "creating a view on an empty field should throw";
} catch (const RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("field iteration over empty fields is unsupported"));
}
}
Loading