diff --git a/tree/ntuple/inc/ROOT/RFieldBase.hxx b/tree/ntuple/inc/ROOT/RFieldBase.hxx index bc444b6033ae4..cf9c5f1a09332 100644 --- a/tree/ntuple/inc/ROOT/RFieldBase.hxx +++ b/tree/ntuple/inc/ROOT/RFieldBase.hxx @@ -530,13 +530,14 @@ protected: /// Returns a combination of kDiff... flags, indicating peroperties that are different between the field at hand /// and the given on-disk field std::uint32_t CompareOnDiskField(const RFieldDescriptor &fieldDesc, std::uint32_t ignoreBits) const; - /// Compares the field to the provieded on-disk field descriptor. Throws an exception if the fields don't match. + /// Compares the field to the corresponding on-disk field information in the provided descriptor. + /// Throws an exception if the fields don't match. /// Optionally, a set of bits can be provided that should be ignored in the comparison. - RResult EnsureMatchingOnDiskField(const RFieldDescriptor &fieldDesc, std::uint32_t ignoreBits = 0) const; + RResult EnsureMatchingOnDiskField(const RNTupleDescriptor &desc, std::uint32_t ignoreBits = 0) const; /// Many fields accept a range of type prefixes for schema evolution, /// e.g. std::unique_ptr< and std::optional< for nullable fields RResult - EnsureMatchingTypePrefix(const RFieldDescriptor &fieldDesc, const std::vector &prefixes) const; + EnsureMatchingTypePrefix(const RNTupleDescriptor &desc, const std::vector &prefixes) const; /// Factory method to resurrect a field from the stored on-disk type information. This overload takes an already /// normalized type name and type alias. diff --git a/tree/ntuple/inc/ROOT/RFieldUtils.hxx b/tree/ntuple/inc/ROOT/RFieldUtils.hxx index 7017a30bad946..5724777da3573 100644 --- a/tree/ntuple/inc/ROOT/RFieldUtils.hxx +++ b/tree/ntuple/inc/ROOT/RFieldUtils.hxx @@ -15,6 +15,10 @@ class TClass; namespace ROOT { + +class RFieldBase; +class RNTupleDescriptor; + namespace Internal { /// Applies RNTuple specific type name normalization rules (see specs) that help the string parsing in @@ -62,6 +66,11 @@ std::vector TokenizeTypeList(std::string_view templateType, std::si /// however, needs to additionally check for ROOT-specific special cases. bool IsMatchingFieldType(std::string_view actualTypeName, std::string_view expectedTypeName, const std::type_info &ti); +/// Prints the hierarchy of types with their field names and field IDs for the given in-memory field and the +/// on-disk hierarchy, matching the fields on-disk ID with the information of the descriptor. +/// Useful information when the in-memory field cannot be matched to the the on-disk information. +std::string GetTypeTraceReport(const RFieldBase &field, const RNTupleDescriptor &desc); + } // namespace Internal } // namespace ROOT diff --git a/tree/ntuple/src/RField.cxx b/tree/ntuple/src/RField.cxx index 49ced471b153a..acb4ac0d6c54e 100644 --- a/tree/ntuple/src/RField.cxx +++ b/tree/ntuple/src/RField.cxx @@ -77,15 +77,18 @@ void ROOT::RCardinalityField::GenerateColumns(const ROOT::RNTupleDescriptor &des void ROOT::RCardinalityField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { + EnsureMatchingOnDiskField(desc, kDiffTypeVersion | kDiffStructure | kDiffTypeName).ThrowOnError(); + const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); - EnsureMatchingOnDiskField(fieldDesc, kDiffTypeVersion | kDiffStructure | kDiffTypeName).ThrowOnError(); if (fieldDesc.GetStructure() == ENTupleStructure::kPlain) { if (fieldDesc.GetTypeName().rfind("ROOT::RNTupleCardinality<", 0) != 0) { throw RException(R__FAIL("RCardinalityField " + GetQualifiedFieldName() + - " expects an on-disk leaf field of the same type")); + " expects an on-disk leaf field of the same type\n" + + Internal::GetTypeTraceReport(*this, desc))); } } else if (fieldDesc.GetStructure() != ENTupleStructure::kCollection) { - throw RException(R__FAIL("invalid on-disk structural role for RCardinalityField " + GetQualifiedFieldName())); + throw RException(R__FAIL("invalid on-disk structural role for RCardinalityField " + GetQualifiedFieldName() + + "\n" + Internal::GetTypeTraceReport(*this, desc))); } } @@ -109,9 +112,9 @@ const ROOT::RField> *ROOT::RCardinalityF template void ROOT::RSimpleField::ReconcileIntegralField(const RNTupleDescriptor &desc) { - const RFieldDescriptor &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); - EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName); + EnsureMatchingOnDiskField(desc, kDiffTypeName); + const RFieldDescriptor &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); if (fieldDesc.IsCustomEnum(desc)) { SetOnDiskId(desc.FindFieldId("_0", GetOnDiskId())); return; @@ -123,19 +126,19 @@ void ROOT::RSimpleField::ReconcileIntegralField(const RNTupleDescriptor &desc if (std::find(std::begin(gIntegralTypeNames), std::end(gIntegralTypeNames), fieldDesc.GetTypeName()) == std::end(gIntegralTypeNames)) { throw RException(R__FAIL("unexpected on-disk type name '" + fieldDesc.GetTypeName() + "' for field of type '" + - GetTypeName() + "'")); + GetTypeName() + "'\n" + Internal::GetTypeTraceReport(*this, desc))); } } template void ROOT::RSimpleField::ReconcileFloatingPointField(const RNTupleDescriptor &desc) { - const RFieldDescriptor &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); - EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName); + EnsureMatchingOnDiskField(desc, kDiffTypeName); + const RFieldDescriptor &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); if (!(fieldDesc.GetTypeName() == "float" || fieldDesc.GetTypeName() == "double")) { throw RException(R__FAIL("unexpected on-disk type name '" + fieldDesc.GetTypeName() + "' for field of type '" + - GetTypeName() + "'")); + GetTypeName() + "'\n" + Internal::GetTypeTraceReport(*this, desc))); } } @@ -663,12 +666,12 @@ void ROOT::RRecordField::ReconcileOnDiskField(const RNTupleDescriptor &desc) // Note that the RPairField and RTupleField descendants have their own reconcilation logic R__ASSERT(GetTypeName().empty()); - const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); - EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName | kDiffTypeVersion).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName | kDiffTypeVersion).ThrowOnError(); // The on-disk ID of subfields is matched by field name. So we inherently support reordering of fields // and we will ignore extra on-disk fields. // It remains to mark the extra in-memory fields as artificial. + const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); std::unordered_set onDiskSubfields; for (const auto &subField : desc.GetFieldIterable(fieldDesc)) { onDiskSubfields.insert(subField.GetFieldName()); @@ -878,9 +881,8 @@ void ROOT::RNullableField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { static const std::vector prefixes = {"std::optional<", "std::unique_ptr<"}; - const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); - EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError(); - EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError(); + EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError(); } ROOT::RNTupleLocalIndex ROOT::RNullableField::GetItemIndex(ROOT::NTupleSize_t globalIndex) @@ -1098,9 +1100,8 @@ void ROOT::RAtomicField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { static const std::vector prefixes = {"std::atomic<"}; - const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); - EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError(); - EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError(); + EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError(); } std::vector ROOT::RAtomicField::SplitValue(const RValue &value) const diff --git a/tree/ntuple/src/RFieldBase.cxx b/tree/ntuple/src/RFieldBase.cxx index c4dbdbccb3474..e15c4e02c247d 100644 --- a/tree/ntuple/src/RFieldBase.cxx +++ b/tree/ntuple/src/RFieldBase.cxx @@ -1015,15 +1015,15 @@ void ROOT::RFieldBase::ConnectPageSource(ROOT::Internal::RPageSource &pageSource void ROOT::RFieldBase::ReconcileOnDiskField(const RNTupleDescriptor &desc) { - // The default implementation throws an exception if the on-disk ID is set and there are any meaningful differences - // to the on-disk field. Derived classes may overwrite this and relax the checks to support automatic schema - // evolution. - EnsureMatchingOnDiskField(desc.GetFieldDescriptor(fOnDiskId)).ThrowOnError(); + // The default implementation throws an exception if there are any meaningful differences to the on-disk field. + // Derived classes may overwrite this and relax the checks to support automatic schema evolution. + EnsureMatchingOnDiskField(desc).ThrowOnError(); } ROOT::RResult -ROOT::RFieldBase::EnsureMatchingOnDiskField(const RFieldDescriptor &fieldDesc, std::uint32_t ignoreBits) const +ROOT::RFieldBase::EnsureMatchingOnDiskField(const RNTupleDescriptor &desc, std::uint32_t ignoreBits) const { + const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); const std::uint32_t diffBits = CompareOnDiskField(fieldDesc, ignoreBits); if (diffBits == 0) return RResult::Success(); @@ -1046,17 +1046,19 @@ ROOT::RFieldBase::EnsureMatchingOnDiskField(const RFieldDescriptor &fieldDesc, s if (diffBits & kDiffNRepetitions) { errMsg << " repetition count " << GetNRepetitions() << " vs. " << fieldDesc.GetNRepetitions() << ";"; } - return R__FAIL(errMsg.str()); + return R__FAIL(errMsg.str() + "\n" + Internal::GetTypeTraceReport(*this, desc)); } -ROOT::RResult ROOT::RFieldBase::EnsureMatchingTypePrefix(const RFieldDescriptor &fieldDesc, +ROOT::RResult ROOT::RFieldBase::EnsureMatchingTypePrefix(const RNTupleDescriptor &desc, const std::vector &prefixes) const { + const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); for (const auto &p : prefixes) { if (fieldDesc.GetTypeName().rfind(p, 0) == 0) return RResult::Success(); } - return R__FAIL("incompatible type " + fieldDesc.GetTypeName() + " for field " + GetQualifiedFieldName()); + return R__FAIL("incompatible type " + fieldDesc.GetTypeName() + " for field " + GetQualifiedFieldName() + "\n" + + Internal::GetTypeTraceReport(*this, desc)); } std::uint32_t ROOT::RFieldBase::CompareOnDiskField(const RFieldDescriptor &fieldDesc, std::uint32_t ignoreBits) const diff --git a/tree/ntuple/src/RFieldMeta.cxx b/tree/ntuple/src/RFieldMeta.cxx index e216f0c0d80fb..aeeade9d72892 100644 --- a/tree/ntuple/src/RFieldMeta.cxx +++ b/tree/ntuple/src/RFieldMeta.cxx @@ -521,7 +521,7 @@ std::unique_ptr ROOT::RClassField::BeforeConnectPageSource(ROO void ROOT::RClassField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { - EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeVersion | kDiffTypeName).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeVersion | kDiffTypeName).ThrowOnError(); } void ROOT::RClassField::ConstructValue(void *where) const @@ -619,7 +619,7 @@ std::unique_ptr ROOT::REnumField::CloneImpl(std::string_view n void ROOT::REnumField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { // TODO(jblomer): allow enum to enum conversion only by rename rule - EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName | kDiffTypeVersion).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName | kDiffTypeVersion).ThrowOnError(); } std::vector ROOT::REnumField::SplitValue(const RValue &value) const @@ -684,14 +684,15 @@ void ROOT::RPairField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { static const std::vector prefixes = {"std::pair<", "std::tuple<"}; - const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); - EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError(); - EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError(); + EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError(); + const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); const auto nOnDiskSubfields = fieldDesc.GetLinkIds().size(); if (nOnDiskSubfields != 2) { - throw ROOT::RException( - R__FAIL("invalid number of on-disk subfields for std::pair " + std::to_string(nOnDiskSubfields))); + throw ROOT::RException(R__FAIL("invalid number of on-disk subfields for std::pair " + + std::to_string(nOnDiskSubfields) + "\n" + + Internal::GetTypeTraceReport(*this, desc))); } } @@ -830,7 +831,7 @@ void ROOT::RProxiedCollectionField::GenerateColumns(const ROOT::RNTupleDescripto void ROOT::RProxiedCollectionField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { - EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError(); } void ROOT::RProxiedCollectionField::ConstructValue(void *where) const @@ -997,7 +998,7 @@ std::unique_ptr ROOT::RStreamerField::BeforeConnectPageSource( void ROOT::RStreamerField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { - EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName | kDiffTypeVersion).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName | kDiffTypeVersion).ThrowOnError(); } void ROOT::RStreamerField::ConstructValue(void *where) const @@ -1231,15 +1232,16 @@ void ROOT::RTupleField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { static const std::vector prefixes = {"std::pair<", "std::tuple<"}; - const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); - EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError(); - EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError(); + EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError(); + const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); const auto nOnDiskSubfields = fieldDesc.GetLinkIds().size(); const auto nSubfields = fSubfields.size(); if (nOnDiskSubfields != nSubfields) { throw ROOT::RException(R__FAIL("invalid number of on-disk subfields for std::tuple " + - std::to_string(nOnDiskSubfields) + " vs. " + std::to_string(nSubfields))); + std::to_string(nOnDiskSubfields) + " vs. " + std::to_string(nSubfields) + "\n" + + Internal::GetTypeTraceReport(*this, desc))); } } @@ -1393,12 +1395,13 @@ void ROOT::RVariantField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { static const std::vector prefixes = {"std::variant<"}; - const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); - EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError(); - EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError(); + EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError(); + const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); if (fSubfields.size() != fieldDesc.GetLinkIds().size()) { - throw RException(R__FAIL("number of variants on-disk do not match for " + GetQualifiedFieldName())); + throw RException(R__FAIL("number of variants on-disk do not match for " + GetQualifiedFieldName() + "\n" + + Internal::GetTypeTraceReport(*this, desc))); } } diff --git a/tree/ntuple/src/RFieldSequenceContainer.cxx b/tree/ntuple/src/RFieldSequenceContainer.cxx index 952f2bd717495..cd5e1cd133563 100644 --- a/tree/ntuple/src/RFieldSequenceContainer.cxx +++ b/tree/ntuple/src/RFieldSequenceContainer.cxx @@ -85,9 +85,8 @@ void ROOT::RArrayField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { static const std::vector prefixes = {"std::array<"}; - const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); - EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError(); - EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError(); + EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError(); } void ROOT::RArrayField::ConstructValue(void *where) const @@ -462,7 +461,7 @@ std::unique_ptr ROOT::RRVecField::BeforeConnectPageSource(Inte void ROOT::RRVecField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { - EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError(); } void ROOT::RRVecField::ConstructValue(void *where) const @@ -644,7 +643,7 @@ void ROOT::RVectorField::GenerateColumns(const ROOT::RNTupleDescriptor &desc) void ROOT::RVectorField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { - EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError(); } void ROOT::RVectorField::RVectorDeleter::operator()(void *objPtr, bool dtorOnly) @@ -748,7 +747,7 @@ void ROOT::RField>::GenerateColumns(const ROOT::RNTupleDescrip void ROOT::RField>::ReconcileOnDiskField(const RNTupleDescriptor &desc) { - EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName).ThrowOnError(); + EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError(); } std::vector ROOT::RField>::SplitValue(const RValue &value) const @@ -842,11 +841,12 @@ void ROOT::RArrayAsRVecField::ReadInClusterImpl(RNTupleLocalIndex localIndex, vo void ROOT::RArrayAsRVecField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { - const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); - EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName | kDiffTypeVersion | kDiffStructure | kDiffNRepetitions) + EnsureMatchingOnDiskField(desc, kDiffTypeName | kDiffTypeVersion | kDiffStructure | kDiffNRepetitions) .ThrowOnError(); + const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId()); if (fieldDesc.GetTypeName().rfind("std::array<", 0) != 0) { - throw RException(R__FAIL("RArrayAsRVecField " + GetQualifiedFieldName() + " expects an on-disk array field")); + throw RException(R__FAIL("RArrayAsRVecField " + GetQualifiedFieldName() + " expects an on-disk array field\n" + + Internal::GetTypeTraceReport(*this, desc))); } } diff --git a/tree/ntuple/src/RFieldUtils.cxx b/tree/ntuple/src/RFieldUtils.cxx index b4d6e65f1ba6f..460660a5c5256 100644 --- a/tree/ntuple/src/RFieldUtils.cxx +++ b/tree/ntuple/src/RFieldUtils.cxx @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -722,3 +723,41 @@ bool ROOT::Internal::IsMatchingFieldType(std::string_view actualTypeName, std::s // Thus, we check again using first ROOT Meta normalization followed by RNTuple re-normalization. return (actualTypeName == ROOT::Internal::GetRenormalizedTypeName(ROOT::Internal::GetDemangledTypeName(ti))); } + +std::string ROOT::Internal::GetTypeTraceReport(const RFieldBase &field, const RNTupleDescriptor &desc) +{ + std::vector inMemoryStack; + std::vector onDiskStack; + + auto fnGetLine = [](const std::string &fieldName, const std::string &fieldType, DescriptorId_t fieldId, + int level) -> std::string { + return std::string(2 * level, ' ') + fieldName + " [" + fieldType + "] (id: " + std::to_string(fieldId) + ")\n"; + }; + + const RFieldBase *fieldPtr = &field; + while (fieldPtr->GetParent()) { + inMemoryStack.push_back(fieldPtr); + fieldPtr = fieldPtr->GetParent(); + } + + auto fieldId = field.GetOnDiskId(); + while (fieldId != kInvalidDescriptorId && fieldId != desc.GetFieldZeroId()) { + const auto &fieldDesc = desc.GetFieldDescriptor(fieldId); + onDiskStack.push_back(&fieldDesc); + fieldId = fieldDesc.GetParentId(); + } + + std::string report = "In-memory field/type hierarchy:\n"; + int indentLevel = 0; + for (auto itr = inMemoryStack.rbegin(); itr != inMemoryStack.rend(); ++itr, ++indentLevel) { + report += fnGetLine((*itr)->GetFieldName(), (*itr)->GetTypeName(), (*itr)->GetOnDiskId(), indentLevel); + } + + report += "On-disk field/type hierarchy:\n"; + indentLevel = 0; + for (auto itr = onDiskStack.rbegin(); itr != onDiskStack.rend(); ++itr, ++indentLevel) { + report += fnGetLine((*itr)->GetFieldName(), (*itr)->GetTypeName(), (*itr)->GetId(), indentLevel); + } + + return report; +} diff --git a/tree/ntuple/test/ntuple_show.cxx b/tree/ntuple/test/ntuple_show.cxx index 61b6d43834fb6..2e5ce932897cb 100644 --- a/tree/ntuple/test/ntuple_show.cxx +++ b/tree/ntuple/test/ntuple_show.cxx @@ -642,3 +642,41 @@ R"({ // clang-format on EXPECT_EQ(os1.str(), expected); } + +TEST(RNTupleShow, TypeTraceReport) +{ + FileRaii fileGuard("test_ntuple_show_type_trace_report.ntuple"); + { + auto model = RNTupleModel::Create(); + model->MakeField>>>("f"); + auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); + } + + auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath()); + + // Get the field for the `bool` member of the inner `pair` + const auto field = reader->GetModel() + .GetDefaultEntry() + .begin() + ->GetField() + .GetConstSubfields()[1] + ->GetConstSubfields()[0] + ->GetConstSubfields()[1]; + + const auto report = ROOT::Internal::GetTypeTraceReport(*field, reader->GetDescriptor()); + + const std::string expected{ + R"(In-memory field/type hierarchy: +f [std::variant>>] (id: 0) + _1 [std::vector>] (id: 2) + _0 [std::pair] (id: 3) + _1 [bool] (id: 5) +On-disk field/type hierarchy: +f [std::variant>>] (id: 0) + _1 [std::vector>] (id: 2) + _0 [std::pair] (id: 3) + _1 [bool] (id: 5) +)"}; + + EXPECT_EQ(expected, report); +}