blob: 91c3361dd0acaf09780524ce42d592f3d6dc479c [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include <gtest/gtest.h>
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/container/map.h>
#include <tvm/ffi/error.h>
#include <tvm/ffi/extra/structural_equal.h>
#include <tvm/ffi/extra/visit_error_context.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/reflection/access_path.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ffi/string.h>
namespace {
using namespace tvm::ffi;
namespace refl = tvm::ffi::reflection;
// ---------------------------------------------------------------------------
// Test fixture: TPair — a minimal two-field Object with lhs and rhs slots.
//
// Covers kAttr AccessKind via its named fields.
// Array<ObjectRef> and Map<Any, Any> are used alongside TPair in individual
// tests to cover kArrayItem and kMapItem.
// ---------------------------------------------------------------------------
class TPair : public Object {
public:
ObjectRef lhs;
ObjectRef rhs;
static constexpr TVMFFISEqHashKind _type_s_eq_hash_kind = kTVMFFISEqHashKindTreeNode;
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("test.TPair", TPair, Object);
};
class TPairRef : public ObjectRef {
public:
TPairRef(ObjectRef lhs, ObjectRef rhs) {
ObjectPtr<TPair> n = make_object<TPair>();
n->lhs = std::move(lhs);
n->rhs = std::move(rhs);
data_ = std::move(n);
}
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(TPairRef, ObjectRef, TPair);
};
TVM_FFI_STATIC_INIT_BLOCK() {
namespace r = tvm::ffi::reflection;
r::ObjectDef<TPair>().def_ro("lhs", &TPair::lhs).def_ro("rhs", &TPair::rhs);
}
// ---------------------------------------------------------------------------
// Helper: Visit — recursively walks a tree and throws when it reaches
// throw_target, wrapping each recursion level with the visit
// macros so the error chain accumulates on unwind.
// ---------------------------------------------------------------------------
void Visit(const ObjectRef& node, const ObjectRef& throw_target);
void Visit(const ObjectRef& node, const ObjectRef& throw_target) {
TVM_FFI_VISIT_BEGIN();
if (node.same_as(throw_target)) {
TVM_FFI_THROW(ValueError) << "boom";
} else if (Optional<TPairRef> pair = node.as<TPairRef>()) {
Visit(pair.value()->lhs, throw_target);
Visit(pair.value()->rhs, throw_target);
} else if (Optional<Array<ObjectRef>> arr = node.as<Array<ObjectRef>>()) {
for (const ObjectRef& child : *arr) {
Visit(child, throw_target);
}
} else if (Optional<Map<Any, Any>> m = node.as<Map<Any, Any>>()) {
for (const std::pair<Any, Any>& kv : *m) {
Visit(kv.second.cast<ObjectRef>(), throw_target);
}
}
TVM_FFI_VISIT_END(node);
}
// ===========================================================================
// Layer A — Macro → Chain
//
// Verify that TVM_FFI_VISIT_BEGIN/_END correctly builds the
// reverse_visit_pattern as an exception unwinds through visit levels.
// ===========================================================================
// ---------------------------------------------------------------------------
// MacroBuildsChain: throw deep in a two-level visit; confirm the chain
// records nodes in innermost-first order (throw site first, root last).
// ---------------------------------------------------------------------------
TEST(VisitErrorContext, MacroBuildsChain) {
// Tree: root = (lhs=leaf, rhs=empty)
// Visiting root -> descends into leaf -> throws.
Array<ObjectRef> empty;
TPairRef leaf(empty, empty);
TPairRef root(leaf, empty);
Error caught("RuntimeError", "", "");
bool did_catch = false;
try {
Visit(root, leaf);
} catch (Error& err) {
caught = err;
did_catch = true;
}
EXPECT_TRUE(did_catch);
Optional<VisitErrorContext> visit_context = VisitErrorContext::TryGetFromError(caught);
ASSERT_TRUE(visit_context.has_value());
// reverse_visit_pattern is innermost-first: leaf pushed first (inner catch
// fires first during unwind), root pushed last.
const List<ObjectRef>& chain = visit_context.value()->reverse_visit_pattern;
EXPECT_GE(chain.size(), 2u);
// Innermost entry is leaf (the throw site).
EXPECT_TRUE(chain[0].same_as(leaf));
// Outermost entry is root.
EXPECT_TRUE(chain[chain.size() - 1].same_as(root));
// Verify order structurally: [leaf, ..., root].
List<ObjectRef> expected_chain = {leaf, root};
EXPECT_TRUE(StructuralEqual::Equal(expected_chain, chain));
}
// ---------------------------------------------------------------------------
// MacroPreExistingPayloadWrap: when UpdateVisitErrorContext is called on
// an error that already has a non-context extra_context payload, the existing
// payload is stashed in prev_error_context and the visit chain starts
// fresh. Subsequent pushes append to the same context object; prev_error_context
// is not modified.
// ---------------------------------------------------------------------------
TEST(VisitErrorContext, MacroPreExistingPayloadWrap) {
// Stand-in for a pre-existing extra_context payload.
Array<ObjectRef> empty;
TPairRef existing_payload(empty, empty);
TPairRef leaf(empty, empty);
TPairRef root(leaf, empty);
// Construct an error that already carries a non-context extra_context payload.
Error err("RuntimeError", "test", "", std::nullopt, std::optional<ObjectRef>(existing_payload));
ASSERT_TRUE(err.extra_context().has_value());
// First push: existing payload is not a VisitErrorContext → wrapped.
tvm::ffi::details::UpdateVisitErrorContext(err, leaf);
Optional<VisitErrorContext> visit_context = VisitErrorContext::TryGetFromError(err);
ASSERT_TRUE(visit_context.has_value());
ASSERT_EQ(visit_context.value()->reverse_visit_pattern.size(), 1u);
EXPECT_TRUE(visit_context.value()->reverse_visit_pattern[0].same_as(leaf));
ASSERT_TRUE(visit_context.value()->prev_error_context.has_value());
EXPECT_TRUE(visit_context.value()->prev_error_context.value().same_as(existing_payload));
// Second push: context already exists → node appended; prev_error_context unchanged.
tvm::ffi::details::UpdateVisitErrorContext(err, root);
Optional<VisitErrorContext> visit_context_after = VisitErrorContext::TryGetFromError(err);
ASSERT_TRUE(visit_context_after.has_value());
List<ObjectRef> expected_chain = {leaf, root};
EXPECT_TRUE(
StructuralEqual::Equal(expected_chain, visit_context_after.value()->reverse_visit_pattern));
ASSERT_TRUE(visit_context_after.value()->prev_error_context.has_value());
EXPECT_TRUE(visit_context_after.value()->prev_error_context.value().same_as(existing_payload));
}
// ---------------------------------------------------------------------------
// MacroThrowBuildsChain: TVM_FFI_VISIT_THROW seeds the
// VisitErrorContext with the throw-site node directly (no separate
// UpdateVisitErrorContext call). Enclosing VISIT_BEGIN/END frames then
// append parent nodes on rethrow.
//
// Sub-scenario A — standalone: THROW outside any BEGIN/END wrapper produces
// a context whose reverse_visit_pattern is exactly [throw_site].
//
// Sub-scenario B — nested under BEGIN/END: when THROW fires inside a
// VISIT_BEGIN/END(root) pair, the enclosing END appends root. The throw site
// itself is recorded by THROW. If the throw_target happens to equal `node`
// at the surrounding level (a common case), FindAccessPaths' cleanup
// collapses the consecutive duplicate; tested separately in
// FindAccessPaths.RecordsCleanup.
// ---------------------------------------------------------------------------
TEST(VisitErrorContext, MacroThrowBuildsChain) {
Array<ObjectRef> empty;
// Sub-scenario A — standalone THROW (no surrounding BEGIN/END).
{
TPairRef throw_site(empty, empty);
bool did_catch = false;
Error caught("placeholder", "", "");
try {
TVM_FFI_VISIT_THROW(ValueError, throw_site) << "boom";
} catch (Error& err) {
caught = err;
did_catch = true;
}
ASSERT_TRUE(did_catch);
Optional<VisitErrorContext> visit_context = VisitErrorContext::TryGetFromError(caught);
ASSERT_TRUE(visit_context.has_value());
const List<ObjectRef>& chain = visit_context.value()->reverse_visit_pattern;
ASSERT_EQ(chain.size(), 1u);
EXPECT_TRUE(chain[0].same_as(throw_site));
}
// Sub-scenario B — THROW inside a BEGIN/END(root) wrapper.
{
TPairRef throw_site(empty, empty);
TPairRef root(throw_site, empty);
bool did_catch = false;
Error caught("placeholder", "", "");
try {
TVM_FFI_VISIT_BEGIN();
TVM_FFI_VISIT_THROW(ValueError, throw_site) << "boom";
TVM_FFI_VISIT_END(root);
} catch (Error& err) {
caught = err;
did_catch = true;
}
ASSERT_TRUE(did_catch);
Optional<VisitErrorContext> visit_context = VisitErrorContext::TryGetFromError(caught);
ASSERT_TRUE(visit_context.has_value());
const List<ObjectRef>& chain = visit_context.value()->reverse_visit_pattern;
// THROW seeds with throw_site; END appends root.
List<ObjectRef> expected = {throw_site, root};
EXPECT_TRUE(StructuralEqual::Equal(expected, chain));
}
}
// ===========================================================================
// Layer B — Chain → AccessPaths
//
// Verify that FindAccessPaths(root, ctx) resolves a manually-constructed
// VisitErrorContext against a tree, producing AccessPaths that describe
// where matched nodes live in the tree.
//
// Each test:
// 1. Builds a root tree.
// 2. Constructs a VisitErrorContext with a specific reverse_visit_pattern.
// 3. Calls FindAccessPaths(root, ctx).
// 4. Asserts on the result using StructuralEqual::Equal on AccessPath
// (mirrors the pattern used in test_structural_equal_hash.cc).
// ===========================================================================
// ---------------------------------------------------------------------------
// BasicMatch: unambiguous single-node path via two Attr steps; and CSE
// multi-match when the same pointer appears in two sibling slots.
//
// Sub-scenario A — SingleMatch:
// Tree: root.lhs = mid, mid.lhs = leaf
// Pattern: [leaf, mid, root] (innermost-first)
// Expected path: Root->Attr("lhs")->Attr("lhs")
//
// Sub-scenario B — CSEMultiMatch:
// Tree: root.lhs = shared, root.rhs = shared (identical pointer)
// Pattern: [shared, root]
// Expected: two paths, one for each slot (Attr("lhs") and Attr("rhs")).
// ---------------------------------------------------------------------------
TEST(FindAccessPaths, BasicMatch) {
Array<ObjectRef> empty;
// Sub-scenario A: single unambiguous path.
{
TPairRef leaf(empty, empty);
TPairRef mid(leaf, empty);
TPairRef root(mid, empty);
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{leaf, mid, root};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
ASSERT_EQ(paths.size(), 1u);
refl::AccessPath expected = refl::AccessPath::Root()->Attr("lhs")->Attr("lhs");
EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
}
// Sub-scenario B: same pointer in two slots → two paths.
{
TPairRef shared(empty, empty);
TPairRef root(shared, shared);
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{shared, root};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
ASSERT_EQ(paths.size(), 2u);
refl::AccessPath expected_lhs = refl::AccessPath::Root()->Attr("lhs");
refl::AccessPath expected_rhs = refl::AccessPath::Root()->Attr("rhs");
bool found_lhs = false;
bool found_rhs = false;
for (const refl::AccessPath& p : paths) {
if (StructuralEqual::Equal(p, expected_lhs)) found_lhs = true;
if (StructuralEqual::Equal(p, expected_rhs)) found_rhs = true;
}
EXPECT_TRUE(found_lhs);
EXPECT_TRUE(found_rhs);
}
}
// ---------------------------------------------------------------------------
// AccessKindCoverage: exercises Attr (TPair fields), ArrayItem
// (Array<ObjectRef>), and MapItem (Map<Any, Any>) access kinds.
//
// Sub-scenario A — Attr (kAttr):
// Tree: root.lhs = mid, mid.lhs = leaf
// Pattern: [leaf, root]
// Expected path: Root->Attr("lhs")->Attr("lhs")
//
// Sub-scenario B — ArrayItem (kArrayItem):
// Tree: root.lhs = [leaf1, leaf2] (Array in ObjectRef slot)
// Pattern: [leaf1, root]
// Expected path: Root->Attr("lhs")->ArrayItem(0)
//
// Sub-scenario C — MapItem (kMapItem):
// Tree: root.lhs = {"key" -> target} (Map in ObjectRef slot)
// Pattern: [target, root]
// Expected path: Root->Attr("lhs")->MapItem("key")
// ---------------------------------------------------------------------------
TEST(FindAccessPaths, AccessKindCoverage) {
Array<ObjectRef> empty;
// Sub-scenario A: Attr steps.
{
TPairRef leaf(empty, empty);
TPairRef mid(leaf, empty);
TPairRef root(mid, empty);
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{leaf, root};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
ASSERT_EQ(paths.size(), 1u);
refl::AccessPath expected = refl::AccessPath::Root()->Attr("lhs")->Attr("lhs");
EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
}
// Sub-scenario B: ArrayItem step.
{
TPairRef leaf1(empty, empty);
TPairRef leaf2(empty, empty);
Array<ObjectRef> arr = {leaf1, leaf2};
TPairRef root(arr, empty);
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{leaf1, root};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
ASSERT_EQ(paths.size(), 1u);
refl::AccessPath expected = refl::AccessPath::Root()->Attr("lhs")->ArrayItem(0);
EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
}
// Sub-scenario C: MapItem step.
{
TPairRef target(empty, empty);
Map<Any, Any> m;
m.Set(String("key"), target);
TPairRef root(m, empty);
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{target, root};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
ASSERT_EQ(paths.size(), 1u);
refl::AccessPath expected = refl::AccessPath::Root()->Attr("lhs")->MapItem(String("key"));
EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
}
}
// ---------------------------------------------------------------------------
// SparsePatternAnchors: demonstrates that intermediate breadcrumb anchors in
// the pattern disambiguate when an innermost target appears in multiple branches.
//
// Tree layout:
// root
// ├─ branch_a: tpa → m_inner_a (m_inner shared, no m_outer ancestor)
// └─ branch_b: tpb → m_outer → tpc → m_inner_b (same m_inner pointer)
//
// m_inner is a POINTER-IDENTICAL ObjectRef (ref-count shared) used as both
// m_inner_a (under branch_a) and m_inner_b (under branch_b via m_outer).
// m_outer appears only under branch_b.
//
// Scenario 1: pattern with only innermost — 2 candidate paths.
// Pattern: [m_inner, root]
// Expected: paths.size() == 2
//
// Scenario 2: anchor narrows to branch_b only.
// Pattern: [m_inner, m_outer, root]
// Expected: paths.size() == 1, path through branch_b
// ---------------------------------------------------------------------------
TEST(FindAccessPaths, SparsePatternAnchors) {
Array<ObjectRef> empty;
// Shared inner node — identical pointer in both branches.
TPairRef m_inner(empty, empty);
// branch_a: tpa.lhs = m_inner (no m_outer ancestor)
TPairRef tpa(m_inner, empty);
// branch_b: tpb.lhs = m_outer, m_outer.lhs = tpc, tpc.lhs = m_inner
TPairRef tpc(m_inner, empty);
TPairRef m_outer(tpc, empty);
TPairRef tpb(m_outer, empty);
// root: lhs = tpa (branch_a), rhs = tpb (branch_b)
TPairRef root(tpa, tpb);
// Scenario 1: pattern = [m_inner, root] → 2 paths (one per branch).
{
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{m_inner, root};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
EXPECT_EQ(paths.size(), 2u);
}
// Scenario 2: pattern = [m_inner, m_outer, root] → 1 path through branch_b.
{
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{m_inner, m_outer, root};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
ASSERT_EQ(paths.size(), 1u);
// Path must go through branch_b (rhs), then m_outer (lhs), then tpc (lhs), then m_inner (lhs).
refl::AccessPath expected =
refl::AccessPath::Root()->Attr("rhs")->Attr("lhs")->Attr("lhs")->Attr("lhs");
EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
}
}
// ---------------------------------------------------------------------------
// PartialChain: strict mode returns nothing when the innermost pattern entry
// is unreachable; prefix-match mode returns the deepest matched prefix path.
//
// Sub-scenario A — strict (allow_prefix_match=false):
// Pattern: [unreachable, child, root]
// Expected: 0 results (unreachable breaks full match)
//
// Sub-scenario B — prefix match (allow_prefix_match=true):
// Same pattern, same tree.
// Expected: at least one result at the path to child (Root->Attr("lhs")).
// ---------------------------------------------------------------------------
TEST(FindAccessPaths, PartialChain) {
Array<ObjectRef> empty;
// unreachable is not present anywhere in the tree below root.
TPairRef unreachable(empty, empty);
TPairRef child(empty, empty);
TPairRef root(child, empty);
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{unreachable, child, root};
VisitErrorContext ctx(std::move(ctx_obj));
// Sub-scenario A: strict mode — no full match.
{
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
EXPECT_EQ(paths.size(), 0u);
}
// Sub-scenario B: prefix-match mode — path to child reported.
{
Array<refl::AccessPath> paths =
VisitErrorContext::FindAccessPaths(root, ctx, /*allow_prefix_match=*/true);
ASSERT_GE(paths.size(), 1u);
refl::AccessPath expected = refl::AccessPath::Root()->Attr("lhs");
bool found_expected = false;
for (const refl::AccessPath& p : paths) {
if (StructuralEqual::Equal(p, expected)) {
found_expected = true;
break;
}
}
EXPECT_TRUE(found_expected);
}
}
// ---------------------------------------------------------------------------
// EdgeCases: empty pattern produces no results; null entries in the pattern
// never match any live node and do not crash.
// ---------------------------------------------------------------------------
TEST(FindAccessPaths, EdgeCases) {
Array<ObjectRef> empty;
// Sub-scenario A: empty pattern.
{
TPairRef root(empty, empty);
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
// reverse_visit_pattern left empty.
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
EXPECT_EQ(paths.size(), 0u);
}
// Sub-scenario A2: root-itself match — pattern is [root], the throw fired
// before any descent. Expected: a single AccessPath equal to Root() (no
// crash from materializing on an empty descent stack).
{
TPairRef root(empty, empty);
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{root};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
ASSERT_EQ(paths.size(), 1u);
EXPECT_TRUE(StructuralEqual::Equal(refl::AccessPath::Root(), paths[0]));
}
// Sub-scenario B: null entries in pattern.
{
TPairRef root(empty, empty);
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ObjectRef null_ref;
ctx_obj->reverse_visit_pattern = List<ObjectRef>{null_ref, null_ref};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths;
ASSERT_NO_THROW({ paths = VisitErrorContext::FindAccessPaths(root, ctx); });
EXPECT_EQ(paths.size(), 0u);
}
}
// ---------------------------------------------------------------------------
// RecordsCleanup: FindAccessPaths normalizes reverse_visit_pattern before
// matching by (a) dropping null entries and (b) collapsing runs of
// consecutive duplicates. Both arise naturally — null from stale/torn-down
// nodes; duplicates when TVM_FFI_VISIT_THROW(node) is wrapped by
// a TVM_FFI_VISIT_END(node) at the same level. Without cleanup
// the redundant frame would over-constrain the match and yield zero paths.
// ---------------------------------------------------------------------------
TEST(FindAccessPaths, RecordsCleanup) {
Array<ObjectRef> empty;
TPairRef leaf(empty, empty);
TPairRef root(leaf, empty);
ObjectRef null_ref;
refl::AccessPath expected = refl::AccessPath::Root()->Attr("lhs");
// Sub-scenario A — consecutive duplicates collapse.
// Raw pattern: [leaf, leaf, root, root]; effective: [leaf, root].
{
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{leaf, leaf, root, root};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
ASSERT_EQ(paths.size(), 1u);
EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
}
// Sub-scenario B — nulls are skipped, surrounding entries still match.
// Raw pattern: [null, leaf, null, root, null]; effective: [leaf, root].
{
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{null_ref, leaf, null_ref, root, null_ref};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
ASSERT_EQ(paths.size(), 1u);
EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
}
// Sub-scenario C — combined: nulls plus dup-consecutive.
// Raw pattern: [leaf, null, leaf, root, root, null]; effective: [leaf, root].
{
ObjectPtr<VisitErrorContextObj> ctx_obj = make_object<VisitErrorContextObj>();
ctx_obj->reverse_visit_pattern = List<ObjectRef>{leaf, null_ref, leaf, root, root, null_ref};
VisitErrorContext ctx(std::move(ctx_obj));
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, ctx);
ASSERT_EQ(paths.size(), 1u);
EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
}
}
// ---------------------------------------------------------------------------
// TryGetFromError: returns NullOpt when absent, the context when present.
// Composes correctly with FindAccessPaths.
// ---------------------------------------------------------------------------
TEST(VisitErrorContext, TryGetFromError) {
Array<ObjectRef> empty;
TPairRef leaf(empty, empty);
TPairRef root(leaf, empty);
Error err("RuntimeError", "test", "");
// No context attached → TryGetFromError returns NullOpt.
Optional<VisitErrorContext> no_context = VisitErrorContext::TryGetFromError(err);
EXPECT_FALSE(no_context.has_value());
// Attach context via UpdateVisitErrorContext.
tvm::ffi::details::UpdateVisitErrorContext(err, leaf);
tvm::ffi::details::UpdateVisitErrorContext(err, root);
Optional<VisitErrorContext> visit_context = VisitErrorContext::TryGetFromError(err);
ASSERT_TRUE(visit_context.has_value());
// Compose TryGetFromError + FindAccessPaths → should resolve to root.lhs.
Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, visit_context.value());
ASSERT_EQ(paths.size(), 1u);
refl::AccessPath expected = refl::AccessPath::Root()->Attr("lhs");
EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
}
} // namespace