github.com/ipld/go-ipld-prime@v0.21.0/traversal/walk_test.go (about)

     1  package traversal_test
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"testing"
     7  
     8  	qt "github.com/frankban/quicktest"
     9  
    10  	"github.com/ipld/go-ipld-prime"
    11  	_ "github.com/ipld/go-ipld-prime/codec/dagjson"
    12  	"github.com/ipld/go-ipld-prime/datamodel"
    13  	"github.com/ipld/go-ipld-prime/fluent"
    14  	"github.com/ipld/go-ipld-prime/linking"
    15  	cidlink "github.com/ipld/go-ipld-prime/linking/cid"
    16  	"github.com/ipld/go-ipld-prime/linking/preload"
    17  	"github.com/ipld/go-ipld-prime/node/basicnode"
    18  	nodetests "github.com/ipld/go-ipld-prime/node/tests"
    19  	"github.com/ipld/go-ipld-prime/storage"
    20  	"github.com/ipld/go-ipld-prime/traversal"
    21  	"github.com/ipld/go-ipld-prime/traversal/selector"
    22  	"github.com/ipld/go-ipld-prime/traversal/selector/builder"
    23  	selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
    24  )
    25  
    26  /* Remember, we've got the following fixtures in scope:
    27  var (
    28  	// baguqeeyexkjwnfy
    29  	leafAlpha, leafAlphaLnk = encode(basicnode.NewString("alpha"))
    30  	// baguqeeyeqvc7t3a
    31  	leafBeta, leafBetaLnk = encode(basicnode.NewString("beta"))
    32  	// baguqeeyezhlahvq
    33  	middleMapNode, middleMapNodeLnk = encode(fluent.MustBuildMap(basicnode.Prototype.Map, 3, func(na fluent.MapAssembler) {
    34  		na.AssembleEntry("foo").AssignBool(true)
    35  		na.AssembleEntry("bar").AssignBool(false)
    36  		na.AssembleEntry("nested").CreateMap(2, func(na fluent.MapAssembler) {
    37  			na.AssembleEntry("alink").AssignLink(leafAlphaLnk)
    38  			na.AssembleEntry("nonlink").AssignString("zoo")
    39  		})
    40  	}))
    41  	// baguqeeyehfkkfwa
    42  	middleListNode, middleListNodeLnk = encode(fluent.MustBuildList(basicnode.Prototype.List, 4, func(na fluent.ListAssembler) {
    43  		na.AssembleValue().AssignLink(leafAlphaLnk)
    44  		na.AssembleValue().AssignLink(leafAlphaLnk)
    45  		na.AssembleValue().AssignLink(leafBetaLnk)
    46  		na.AssembleValue().AssignLink(leafAlphaLnk)
    47  	}))
    48  	// baguqeeyeie4ajfy
    49  	rootNode, rootNodeLnk = encode(fluent.MustBuildMap(basicnode.Prototype.Map, 4, func(na fluent.MapAssembler) {
    50  		na.AssembleEntry("plain").AssignString("olde string")
    51  		na.AssembleEntry("linkedString").AssignLink(leafAlphaLnk)
    52  		na.AssembleEntry("linkedMap").AssignLink(middleMapNodeLnk)
    53  		na.AssembleEntry("linkedList").AssignLink(middleListNodeLnk)
    54  	})))
    55  */
    56  
    57  // covers traverse using a variety of selectors.
    58  // all cases here use one already-loaded Node; no link-loading exercised.
    59  
    60  func TestWalkMatching(t *testing.T) {
    61  	ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any)
    62  	t.Run("traverse selecting true should visit the root", func(t *testing.T) {
    63  		err := traversal.WalkMatching(basicnode.NewString("x"), selector.Matcher{}, func(prog traversal.Progress, n datamodel.Node) error {
    64  			qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("x"))
    65  			qt.Check(t, prog.Path.String(), qt.Equals, datamodel.Path{}.String())
    66  			return nil
    67  		})
    68  		qt.Check(t, err, qt.IsNil)
    69  	})
    70  	t.Run("traverse selecting true should visit only the root and no deeper", func(t *testing.T) {
    71  		err := traversal.WalkMatching(middleMapNode, selector.Matcher{}, func(prog traversal.Progress, n datamodel.Node) error {
    72  			qt.Check(t, n, qt.Equals, middleMapNode)
    73  			qt.Check(t, prog.Path.String(), qt.Equals, datamodel.Path{}.String())
    74  			return nil
    75  		})
    76  		qt.Check(t, err, qt.IsNil)
    77  	})
    78  	t.Run("traverse selecting fields should work", func(t *testing.T) {
    79  		ss := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
    80  			efsb.Insert("foo", ssb.Matcher())
    81  			efsb.Insert("bar", ssb.Matcher())
    82  		})
    83  		s, err := ss.Selector()
    84  		qt.Assert(t, err, qt.IsNil)
    85  		var order int
    86  		err = traversal.WalkMatching(middleMapNode, s, func(prog traversal.Progress, n datamodel.Node) error {
    87  			switch order {
    88  			case 0:
    89  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewBool(true))
    90  				qt.Check(t, prog.Path.String(), qt.Equals, "foo")
    91  			case 1:
    92  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewBool(false))
    93  				qt.Check(t, prog.Path.String(), qt.Equals, "bar")
    94  			}
    95  			order++
    96  			return nil
    97  		})
    98  		qt.Check(t, err, qt.IsNil)
    99  		qt.Check(t, order, qt.Equals, 2)
   100  	})
   101  	t.Run("traverse selecting fields recursively should work", func(t *testing.T) {
   102  		ss := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   103  			efsb.Insert("foo", ssb.Matcher())
   104  			efsb.Insert("nested", ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   105  				efsb.Insert("nonlink", ssb.Matcher())
   106  			}))
   107  		})
   108  		s, err := ss.Selector()
   109  		qt.Assert(t, err, qt.IsNil)
   110  		var order int
   111  		err = traversal.WalkMatching(middleMapNode, s, func(prog traversal.Progress, n datamodel.Node) error {
   112  			switch order {
   113  			case 0:
   114  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewBool(true))
   115  				qt.Check(t, prog.Path.String(), qt.Equals, "foo")
   116  			case 1:
   117  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("zoo"))
   118  				qt.Check(t, prog.Path.String(), qt.Equals, "nested/nonlink")
   119  			}
   120  			order++
   121  			return nil
   122  		})
   123  		qt.Check(t, err, qt.IsNil)
   124  		qt.Check(t, order, qt.Equals, 2)
   125  	})
   126  	t.Run("traversing across nodes should work", func(t *testing.T) {
   127  		ss := ssb.ExploreRecursive(selector.RecursionLimitDepth(3), ssb.ExploreUnion(
   128  			ssb.Matcher(),
   129  			ssb.ExploreAll(ssb.ExploreRecursiveEdge()),
   130  		))
   131  		s, err := ss.Selector()
   132  		qt.Check(t, err, qt.IsNil)
   133  		var order int
   134  		lsys := cidlink.DefaultLinkSystem()
   135  		lsys.SetReadStorage(&store)
   136  		err = traversal.Progress{
   137  			Cfg: &traversal.Config{
   138  				LinkSystem:                     lsys,
   139  				LinkTargetNodePrototypeChooser: basicnode.Chooser,
   140  			},
   141  		}.WalkMatching(middleMapNode, s, func(prog traversal.Progress, n datamodel.Node) error {
   142  			switch order {
   143  			case 0:
   144  				qt.Check(t, n, qt.Equals, middleMapNode)
   145  				qt.Check(t, prog.Path.String(), qt.Equals, "")
   146  			case 1:
   147  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewBool(true))
   148  				qt.Check(t, prog.Path.String(), qt.Equals, "foo")
   149  			case 2:
   150  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewBool(false))
   151  				qt.Check(t, prog.Path.String(), qt.Equals, "bar")
   152  			case 3:
   153  				qt.Check(t, n, nodetests.NodeContentEquals, fluent.MustBuildMap(basicnode.Prototype.Map, 2, func(na fluent.MapAssembler) {
   154  					na.AssembleEntry("alink").AssignLink(leafAlphaLnk)
   155  					na.AssembleEntry("nonlink").AssignString("zoo")
   156  				}))
   157  				qt.Check(t, prog.Path.String(), qt.Equals, "nested")
   158  			case 4:
   159  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
   160  				qt.Check(t, prog.Path.String(), qt.Equals, "nested/alink")
   161  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "nested/alink")
   162  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, leafAlphaLnk.String())
   163  
   164  			case 5:
   165  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("zoo"))
   166  				qt.Check(t, prog.Path.String(), qt.Equals, "nested/nonlink")
   167  			}
   168  			order++
   169  			return nil
   170  		})
   171  		qt.Check(t, err, qt.IsNil)
   172  		qt.Check(t, order, qt.Equals, 6)
   173  	})
   174  	t.Run("traversing lists should work", func(t *testing.T) {
   175  		ss := ssb.ExploreRange(0, 3, ssb.Matcher())
   176  		s, err := ss.Selector()
   177  		qt.Check(t, err, qt.IsNil)
   178  		var order int
   179  		lsys := cidlink.DefaultLinkSystem()
   180  		lsys.SetReadStorage(&store)
   181  		err = traversal.Progress{
   182  			Cfg: &traversal.Config{
   183  				LinkSystem:                     lsys,
   184  				LinkTargetNodePrototypeChooser: basicnode.Chooser,
   185  			},
   186  		}.WalkMatching(middleListNode, s, func(prog traversal.Progress, n datamodel.Node) error {
   187  			switch order {
   188  			case 0:
   189  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
   190  				qt.Check(t, prog.Path.String(), qt.Equals, "0")
   191  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "0")
   192  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, leafAlphaLnk.String())
   193  			case 1:
   194  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
   195  				qt.Check(t, prog.Path.String(), qt.Equals, "1")
   196  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "1")
   197  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, leafAlphaLnk.String())
   198  			case 2:
   199  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("beta"))
   200  				qt.Check(t, prog.Path.String(), qt.Equals, "2")
   201  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "2")
   202  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, leafBetaLnk.String())
   203  			}
   204  			order++
   205  			return nil
   206  		})
   207  		qt.Check(t, err, qt.IsNil)
   208  		qt.Check(t, order, qt.Equals, 3)
   209  	})
   210  	t.Run("multiple layers of link traversal should work", func(t *testing.T) {
   211  		ss := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   212  			efsb.Insert("linkedList", ssb.ExploreAll(ssb.Matcher()))
   213  			efsb.Insert("linkedMap", ssb.ExploreRecursive(selector.RecursionLimitDepth(3), ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   214  				efsb.Insert("foo", ssb.Matcher())
   215  				efsb.Insert("nonlink", ssb.Matcher())
   216  				efsb.Insert("alink", ssb.Matcher())
   217  				efsb.Insert("nested", ssb.ExploreRecursiveEdge())
   218  			})))
   219  		})
   220  		s, err := ss.Selector()
   221  		qt.Check(t, err, qt.IsNil)
   222  		var order int
   223  		lsys := cidlink.DefaultLinkSystem()
   224  		lsys.SetReadStorage(&store)
   225  		err = traversal.Progress{
   226  			Cfg: &traversal.Config{
   227  				LinkSystem:                     lsys,
   228  				LinkTargetNodePrototypeChooser: basicnode.Chooser,
   229  			},
   230  		}.WalkMatching(rootNode, s, func(prog traversal.Progress, n datamodel.Node) error {
   231  			switch order {
   232  			case 0:
   233  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
   234  				qt.Check(t, prog.Path.String(), qt.Equals, "linkedList/0")
   235  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "linkedList/0")
   236  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, leafAlphaLnk.String())
   237  			case 1:
   238  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
   239  				qt.Check(t, prog.Path.String(), qt.Equals, "linkedList/1")
   240  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "linkedList/1")
   241  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, leafAlphaLnk.String())
   242  			case 2:
   243  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("beta"))
   244  				qt.Check(t, prog.Path.String(), qt.Equals, "linkedList/2")
   245  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "linkedList/2")
   246  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, leafBetaLnk.String())
   247  			case 3:
   248  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
   249  				qt.Check(t, prog.Path.String(), qt.Equals, "linkedList/3")
   250  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "linkedList/3")
   251  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, leafAlphaLnk.String())
   252  			case 4:
   253  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewBool(true))
   254  				qt.Check(t, prog.Path.String(), qt.Equals, "linkedMap/foo")
   255  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "linkedMap")
   256  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, middleMapNodeLnk.String())
   257  			case 5:
   258  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("zoo"))
   259  				qt.Check(t, prog.Path.String(), qt.Equals, "linkedMap/nested/nonlink")
   260  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "linkedMap")
   261  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, middleMapNodeLnk.String())
   262  			case 6:
   263  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
   264  				qt.Check(t, prog.Path.String(), qt.Equals, "linkedMap/nested/alink")
   265  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "linkedMap/nested/alink")
   266  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, leafAlphaLnk.String())
   267  			}
   268  			order++
   269  			return nil
   270  		})
   271  		qt.Check(t, err, qt.IsNil)
   272  		qt.Check(t, order, qt.Equals, 7)
   273  	})
   274  
   275  	t.Run("no visiting of nodes before start path", func(t *testing.T) {
   276  		ss := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   277  			efsb.Insert("linkedList", ssb.ExploreRecursive(
   278  				selector.RecursionLimitNone(),
   279  				ssb.ExploreUnion(ssb.Matcher(), ssb.ExploreAll(ssb.ExploreRecursiveEdge()))))
   280  			efsb.Insert("plain", ssb.ExploreAll(ssb.Matcher()))
   281  			efsb.Insert("linkedString", ssb.ExploreAll(ssb.Matcher()))
   282  			efsb.Insert("linkedMap", ssb.ExploreUnion(ssb.Matcher(),
   283  				ssb.ExploreRecursive(selector.RecursionLimitDepth(3), ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   284  					efsb.Insert("foo", ssb.Matcher())
   285  					efsb.Insert("nonlink", ssb.Matcher())
   286  					efsb.Insert("alink", ssb.Matcher())
   287  					efsb.Insert("nested", ssb.ExploreRecursiveEdge())
   288  				}))))
   289  		})
   290  		s, err := ss.Selector()
   291  		qt.Check(t, err, qt.IsNil)
   292  		var order int
   293  		lsys := cidlink.DefaultLinkSystem()
   294  		lsys.SetReadStorage(&store)
   295  		visitedCids := make([]string, 0)
   296  		lsys.StorageReadOpener = func(lnkCtx ipld.LinkContext, lnk ipld.Link) (io.Reader, error) {
   297  			visitedCids = append(visitedCids, lnk.(cidlink.Link).Cid.String())
   298  			return store.GetStream(lnkCtx.Ctx, lnk.(cidlink.Link).Cid.KeyString())
   299  		}
   300  		err = traversal.Progress{
   301  			Cfg: &traversal.Config{
   302  				LinkSystem:                     lsys,
   303  				LinkTargetNodePrototypeChooser: basicnode.Chooser,
   304  				StartAtPath:                    datamodel.ParsePath("linkedMap/nested/nonlink"),
   305  			},
   306  		}.WalkMatching(rootNode, s, func(prog traversal.Progress, n datamodel.Node) error {
   307  			switch order {
   308  			case 0:
   309  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("zoo"))
   310  				qt.Check(t, prog.Path.String(), qt.Equals, "linkedMap/nested/nonlink")
   311  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "linkedMap")
   312  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, middleMapNodeLnk.String())
   313  			case 1:
   314  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
   315  				qt.Check(t, prog.Path.String(), qt.Equals, "linkedMap/nested/alink")
   316  				qt.Check(t, prog.LastBlock.Path.String(), qt.Equals, "linkedMap/nested/alink")
   317  				qt.Check(t, prog.LastBlock.Link.String(), qt.Equals, leafAlphaLnk.String())
   318  			}
   319  			order++
   320  			return nil
   321  		})
   322  		qt.Check(t, err, qt.IsNil)
   323  		qt.Check(t, order, qt.Equals, 2)
   324  		// linkedMap=baguqeeyezhlahvq, alink=baguqeeyexkjwnfy
   325  		qt.Check(t, visitedCids, qt.DeepEquals, []string{"baguqeeyezhlahvq", "baguqeeyexkjwnfy"})
   326  	})
   327  
   328  	t.Run("no loading of unnecessary nodes before start path", func(t *testing.T) {
   329  		ss := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   330  			efsb.Insert("linkedList", ssb.ExploreRecursive(selector.RecursionLimitNone(), ssb.ExploreAll(ssb.ExploreRecursiveEdge())))
   331  			efsb.Insert("plain", ssb.ExploreRecursive(selector.RecursionLimitNone(), ssb.ExploreAll(ssb.ExploreRecursiveEdge())))
   332  			efsb.Insert("linkedString", ssb.ExploreRecursive(selector.RecursionLimitNone(), ssb.ExploreAll(ssb.ExploreRecursiveEdge())))
   333  			efsb.Insert("linkedMap",
   334  				ssb.ExploreRecursive(selector.RecursionLimitDepth(3), ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   335  					efsb.Insert("foo", ssb.ExploreRecursive(selector.RecursionLimitNone(), ssb.ExploreAll(ssb.ExploreRecursiveEdge())))
   336  					efsb.Insert("nonlink", ssb.ExploreRecursive(selector.RecursionLimitNone(), ssb.ExploreAll(ssb.ExploreRecursiveEdge())))
   337  					efsb.Insert("alink", ssb.ExploreRecursive(selector.RecursionLimitNone(), ssb.ExploreAll(ssb.ExploreRecursiveEdge())))
   338  					efsb.Insert("nested", ssb.ExploreRecursiveEdge())
   339  				})))
   340  		})
   341  		s, err := ss.Selector()
   342  		qt.Check(t, err, qt.IsNil)
   343  		lsys := cidlink.DefaultLinkSystem()
   344  		lsys.SetReadStorage(&store)
   345  		visitedCids := make([]string, 0)
   346  		lsys.StorageReadOpener = func(lnkCtx ipld.LinkContext, lnk ipld.Link) (io.Reader, error) {
   347  			visitedCids = append(visitedCids, lnk.(cidlink.Link).Cid.String())
   348  			return store.GetStream(lnkCtx.Ctx, lnk.(cidlink.Link).Cid.KeyString())
   349  		}
   350  		err = traversal.Progress{
   351  			Cfg: &traversal.Config{
   352  				LinkSystem:                     lsys,
   353  				LinkTargetNodePrototypeChooser: basicnode.Chooser,
   354  				StartAtPath:                    datamodel.ParsePath("linkedMap/nested/nonlink"),
   355  			},
   356  		}.WalkMatching(rootNode, s, func(prog traversal.Progress, n datamodel.Node) error {
   357  			return nil
   358  		})
   359  		qt.Check(t, err, qt.IsNil)
   360  		// linkedMap=baguqeeyezhlahvq, alink=baguqeeyexkjwnfy
   361  		qt.Check(t, visitedCids, qt.DeepEquals, []string{"baguqeeyezhlahvq", "baguqeeyexkjwnfy"})
   362  	})
   363  }
   364  
   365  func TestWalkBudgets(t *testing.T) {
   366  	for _, preloader := range []bool{false, true} {
   367  		t.Run(fmt.Sprintf("preloader=%v", preloader), func(t *testing.T) {
   368  			t.Run("node-budget-halts", func(t *testing.T) {
   369  				ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any)
   370  				ss := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   371  					efsb.Insert("foo", ssb.Matcher())
   372  					efsb.Insert("bar", ssb.Matcher())
   373  				})
   374  				s, err := ss.Selector()
   375  				qt.Assert(t, err, qt.Equals, nil)
   376  				var order int
   377  				prog := traversal.Progress{}
   378  				prog.Budget = &traversal.Budget{
   379  					NodeBudget: 2, // should reach root, then "foo", then stop.
   380  				}
   381  				preloadLinks := make([]preload.Link, 0)
   382  				if preloader {
   383  					// having a preloader shouldn't change budgeting
   384  					prog.Cfg = &traversal.Config{
   385  						Preloader: func(_ preload.PreloadContext, link preload.Link) {
   386  							preloadLinks = append(preloadLinks, link)
   387  						},
   388  					}
   389  				}
   390  				err = prog.WalkMatching(middleMapNode, s, func(prog traversal.Progress, n datamodel.Node) error {
   391  					switch order {
   392  					case 0:
   393  						qt.Assert(t, n, nodetests.NodeContentEquals, basicnode.NewBool(true))
   394  						qt.Assert(t, prog.Path.String(), qt.Equals, "foo")
   395  					}
   396  					order++
   397  					return nil
   398  				})
   399  				if preloader {
   400  					qt.Assert(t, preloadLinks, qt.HasLen, 0)
   401  				}
   402  				qt.Check(t, order, qt.Equals, 1) // because it should've stopped early
   403  				qt.Assert(t, err, qt.Not(qt.Equals), nil)
   404  				qt.Check(t, err.Error(), qt.Equals, `traversal budget exceeded: budget for nodes reached zero while on path "bar"`)
   405  			})
   406  
   407  			t.Run("link-budget-halts", func(t *testing.T) {
   408  				ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any)
   409  				ss := ssb.ExploreAll(ssb.Matcher())
   410  				s, err := ss.Selector()
   411  				qt.Assert(t, err, qt.Equals, nil)
   412  				var order int
   413  				lsys := cidlink.DefaultLinkSystem()
   414  				lsys.SetReadStorage(&store)
   415  				prog := traversal.Progress{
   416  					Cfg: &traversal.Config{
   417  						LinkSystem:                     lsys,
   418  						LinkTargetNodePrototypeChooser: basicnode.Chooser,
   419  					},
   420  					Budget: &traversal.Budget{
   421  						NodeBudget: 9000,
   422  						LinkBudget: 3,
   423  					},
   424  				}
   425  				preloadLinks := make([]preload.Link, 0)
   426  				if preloader {
   427  					// having a preloader shouldn't change budgeting
   428  					prog.Cfg.Preloader = func(_ preload.PreloadContext, link preload.Link) {
   429  						preloadLinks = append(preloadLinks, link)
   430  					}
   431  				}
   432  				err = prog.WalkMatching(middleListNode, s, func(prog traversal.Progress, n datamodel.Node) error {
   433  					switch order {
   434  					case 0:
   435  						qt.Assert(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
   436  						qt.Assert(t, prog.Path.String(), qt.Equals, "0")
   437  					case 1:
   438  						qt.Assert(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
   439  						qt.Assert(t, prog.Path.String(), qt.Equals, "1")
   440  					case 2:
   441  						qt.Assert(t, n, nodetests.NodeContentEquals, basicnode.NewString("beta"))
   442  						qt.Assert(t, prog.Path.String(), qt.Equals, "2")
   443  					}
   444  					order++
   445  					return nil
   446  				})
   447  				qt.Check(t, order, qt.Equals, 3)
   448  				qt.Assert(t, err, qt.Not(qt.Equals), nil)
   449  				qt.Check(t, err.Error(), qt.Equals, `traversal budget exceeded: budget for links reached zero while on path "3" (link: "baguqeeyexkjwnfy")`)
   450  				if preloader {
   451  					qt.Assert(t, preloadLinks, qt.HasLen, 3)
   452  					qt.Check(t, preloadLinks[0].Link, qt.Equals, leafAlphaLnk)
   453  					qt.Check(t, preloadLinks[1].Link, qt.Equals, leafAlphaLnk)
   454  					qt.Check(t, preloadLinks[2].Link, qt.Equals, leafBetaLnk)
   455  				}
   456  			})
   457  		})
   458  	}
   459  }
   460  
   461  func TestWalkBlockLoadOrder(t *testing.T) {
   462  	// a more nested root that we can use to test SkipMe as well
   463  	// note that in using `rootNodeLnk` here rather than `rootNode` we're using the
   464  	// dag-json round-trip version which will have different field ordering
   465  	newRootNode, newRootLink := encode(fluent.MustBuildList(basicnode.Prototype.List, 6, func(na fluent.ListAssembler) {
   466  		na.AssembleValue().AssignLink(rootNodeLnk)
   467  		na.AssembleValue().AssignLink(middleListNodeLnk)
   468  		na.AssembleValue().AssignLink(rootNodeLnk)
   469  		na.AssembleValue().AssignLink(middleListNodeLnk)
   470  		na.AssembleValue().AssignLink(rootNodeLnk)
   471  		na.AssembleValue().AssignLink(middleListNodeLnk)
   472  	}))
   473  
   474  	linkNames := make(map[datamodel.Link]string)
   475  	linkNames[newRootLink] = "newRootLink"
   476  	linkNames[rootNodeLnk] = "rootNodeLnk"
   477  	linkNames[leafAlphaLnk] = "leafAlphaLnk"
   478  	linkNames[middleMapNodeLnk] = "middleMapNodeLnk"
   479  	linkNames[leafAlphaLnk] = "leafAlphaLnk"
   480  	linkNames[middleListNodeLnk] = "middleListNodeLnk"
   481  	linkNames[leafAlphaLnk] = "leafAlphaLnk"
   482  	linkNames[leafBetaLnk] = "leafBetaLnk"
   483  	/* useful to know CIDs for these when debugging
   484  	for v, n := range linkNames {
   485  		t.Logf("n:%v:%v\n", n, v)
   486  	}
   487  	*/
   488  	// the links that we expect from the root node, starting _at_ the root node itself
   489  	rootNodeExpectedLinks := []datamodel.Link{
   490  		rootNodeLnk,
   491  		middleListNodeLnk,
   492  		leafAlphaLnk,
   493  		leafAlphaLnk,
   494  		leafBetaLnk,
   495  		leafAlphaLnk,
   496  		middleMapNodeLnk,
   497  		leafAlphaLnk,
   498  		leafAlphaLnk,
   499  	}
   500  	// same thing but just for middleListNode
   501  	middleListNodeLinks := []datamodel.Link{
   502  		middleListNodeLnk,
   503  		leafAlphaLnk,
   504  		leafAlphaLnk,
   505  		leafBetaLnk,
   506  		leafAlphaLnk,
   507  	}
   508  	// our newRootNode is a list that contains 3 consecutive links to rootNode
   509  	expectedAllBlocks := make([]datamodel.Link, 3*(len(rootNodeExpectedLinks)+len(middleListNodeLinks)))
   510  	for i := 0; i < 3; i++ {
   511  		copy(expectedAllBlocks[i*len(rootNodeExpectedLinks)+i*len(middleListNodeLinks):], rootNodeExpectedLinks[:])
   512  		copy(expectedAllBlocks[(i+1)*len(rootNodeExpectedLinks)+i*len(middleListNodeLinks):], middleListNodeLinks[:])
   513  	}
   514  
   515  	verifySelectorLoads := func(
   516  		t *testing.T,
   517  		rootNode datamodel.Node,
   518  		expected []datamodel.Link,
   519  		s datamodel.Node,
   520  		linkVisitOnce bool,
   521  		startAtPath datamodel.Path,
   522  		preloader preload.Loader,
   523  		readFn func(lc linking.LinkContext, l datamodel.Link) (io.Reader, error)) {
   524  
   525  		var count int
   526  		lsys := cidlink.DefaultLinkSystem()
   527  		lsys.StorageReadOpener = func(lc linking.LinkContext, l datamodel.Link) (io.Reader, error) {
   528  			// t.Logf("load %d: %v (%s) <> %v (%s) - %s", count, expected[count].String(), linkNames[expected[count]], l.String(), linkNames[l], lc.LinkPath)
   529  			// t.Logf("%v (%v) %s<> %v (%v)\n", l, linkNames[l], strings.Repeat(" ", 17-len(linkNames[l])), expected[count], linkNames[expected[count]])
   530  			qt.Check(t, l.String(), qt.Equals, expected[count].String())
   531  			count++
   532  			return readFn(lc, l)
   533  		}
   534  		sel, err := selector.CompileSelector(s)
   535  		qt.Check(t, err, qt.IsNil)
   536  		err = traversal.Progress{
   537  			Cfg: &traversal.Config{
   538  				LinkSystem:                     lsys,
   539  				LinkTargetNodePrototypeChooser: basicnode.Chooser,
   540  				LinkVisitOnlyOnce:              linkVisitOnce,
   541  				StartAtPath:                    startAtPath,
   542  				Preloader:                      preloader,
   543  			},
   544  		}.WalkMatching(rootNode, sel, func(prog traversal.Progress, n datamodel.Node) error {
   545  			return nil
   546  		})
   547  		qt.Check(t, err, qt.IsNil)
   548  		qt.Check(t, count, qt.Equals, len(expected))
   549  	}
   550  
   551  	t.Run("CommonSelector_MatchAllRecursively", func(t *testing.T) {
   552  		s := selectorparse.CommonSelector_MatchAllRecursively
   553  		verifySelectorLoads(t, newRootNode, expectedAllBlocks, s, false, datamodel.NewPath(nil), nil, func(lctx linking.LinkContext, lnk datamodel.Link) (io.Reader, error) {
   554  			return storage.GetStream(lctx.Ctx, &store, lnk.Binary())
   555  		})
   556  	})
   557  
   558  	t.Run("CommonSelector_ExploreAllRecursively", func(t *testing.T) {
   559  		s := selectorparse.CommonSelector_ExploreAllRecursively
   560  		verifySelectorLoads(t, newRootNode, expectedAllBlocks, s, false, datamodel.NewPath(nil), nil, func(lctx linking.LinkContext, lnk datamodel.Link) (io.Reader, error) {
   561  			return storage.GetStream(lctx.Ctx, &store, lnk.Binary())
   562  		})
   563  	})
   564  
   565  	t.Run("explore all with preload", func(t *testing.T) {
   566  		s := selectorparse.CommonSelector_ExploreAllRecursively
   567  
   568  		newNestedRootNode, _ := encode(fluent.MustBuildList(basicnode.Prototype.List, 2, func(na fluent.ListAssembler) {
   569  			na.AssembleValue().CreateMap(3, func(ma fluent.MapAssembler) {
   570  				ma.AssembleEntry("a").AssignLink(rootNodeLnk)
   571  				ma.AssembleEntry("b").AssignLink(middleListNodeLnk)
   572  				ma.AssembleEntry("c").AssignLink(rootNodeLnk)
   573  			})
   574  			na.AssembleValue().CreateMap(3, func(ma fluent.MapAssembler) {
   575  				ma.AssembleEntry("d").AssignLink(middleListNodeLnk)
   576  				ma.AssembleEntry("e").AssignLink(rootNodeLnk)
   577  				ma.AssembleEntry("f").AssignLink(middleListNodeLnk)
   578  			})
   579  		}))
   580  
   581  		rootNodePreloads := []datamodel.Link{middleListNodeLnk, middleMapNodeLnk, leafAlphaLnk}
   582  		middleListNodePreloads := []datamodel.Link{leafAlphaLnk, leafAlphaLnk, leafBetaLnk, leafAlphaLnk}
   583  		middleMapNodePreloads := []datamodel.Link{leafAlphaLnk}
   584  		rootNodePreloadsRecursive := [][]datamodel.Link{rootNodePreloads, middleListNodePreloads, middleMapNodePreloads}
   585  		el := [][]datamodel.Link{
   586  			{rootNodeLnk, middleListNodeLnk, rootNodeLnk, middleListNodeLnk, rootNodeLnk, middleListNodeLnk},
   587  		}
   588  		for i := 0; i < 3; i++ {
   589  			el = append(el, rootNodePreloadsRecursive...)
   590  			el = append(el, middleListNodePreloads)
   591  		}
   592  		expectedLinks := make([]datamodel.Link, 0)
   593  		for _, l := range el {
   594  			expectedLinks = append(expectedLinks, l...)
   595  		}
   596  		preloadIndex := 0
   597  		preloader := func(_ preload.PreloadContext, link preload.Link) {
   598  			if preloadIndex >= len(expectedLinks) {
   599  				t.Fatal("too many preloads")
   600  			}
   601  			qt.Check(t, link.Link, qt.Equals, expectedLinks[preloadIndex])
   602  			preloadIndex++
   603  		}
   604  
   605  		verifySelectorLoads(t, newNestedRootNode, expectedAllBlocks, s, false, datamodel.NewPath(nil), preloader, func(lctx linking.LinkContext, lnk datamodel.Link) (io.Reader, error) {
   606  			return storage.GetStream(lctx.Ctx, &store, lnk.Binary())
   607  		})
   608  		qt.Check(t, preloadIndex, qt.Equals, len(expectedLinks))
   609  	})
   610  
   611  	t.Run("constructed explore-all selector", func(t *testing.T) {
   612  		// used commonly in Filecoin and other places to "visit all blocks in stable order"
   613  		ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any)
   614  		s := ssb.ExploreRecursive(selector.RecursionLimitNone(),
   615  			ssb.ExploreAll(ssb.ExploreRecursiveEdge())).
   616  			Node()
   617  		verifySelectorLoads(t, newRootNode, expectedAllBlocks, s, false, datamodel.NewPath(nil), nil, func(lctx linking.LinkContext, lnk datamodel.Link) (io.Reader, error) {
   618  			return storage.GetStream(lctx.Ctx, &store, lnk.Binary())
   619  		})
   620  	})
   621  
   622  	t.Run("explore-all with duplicate load skips via SkipMe", func(t *testing.T) {
   623  		// when we use SkipMe to skip loading of already visited blocks we expect to
   624  		// see the links show up in Loads but the lack of the links inside rootNode
   625  		// and middleListNode in this list beyond the first set of loads show that
   626  		// the block is not traversed when the SkipMe is received
   627  		expectedSkipMeBlocks := []datamodel.Link{
   628  			rootNodeLnk,
   629  			middleListNodeLnk,
   630  			leafAlphaLnk,
   631  			leafAlphaLnk,
   632  			leafBetaLnk,
   633  			leafAlphaLnk,
   634  			middleMapNodeLnk,
   635  			leafAlphaLnk,
   636  			leafAlphaLnk,
   637  			middleListNodeLnk,
   638  			rootNodeLnk,
   639  			middleListNodeLnk,
   640  			rootNodeLnk,
   641  			middleListNodeLnk,
   642  		}
   643  
   644  		s := selectorparse.CommonSelector_ExploreAllRecursively
   645  		visited := make(map[datamodel.Link]bool)
   646  		verifySelectorLoads(t, newRootNode, expectedSkipMeBlocks, s, false, datamodel.NewPath(nil), nil, func(lc linking.LinkContext, l datamodel.Link) (io.Reader, error) {
   647  			// t.Logf("load %v [%v]\n", l, visited[l])
   648  			if visited[l] {
   649  				return nil, traversal.SkipMe{}
   650  			}
   651  			visited[l] = true
   652  			return storage.GetStream(lc.Ctx, &store, l.Binary())
   653  		})
   654  	})
   655  
   656  	t.Run("explore-all with duplicate load skips via LinkVisitOnlyOnce:true", func(t *testing.T) {
   657  		// when using LinkRevisit:false to skip duplicate block loads, our loader
   658  		// doesn't even get to see the load attempts (unlike SkipMe, where the
   659  		// loader signals the skips)
   660  		expectedLinkRevisitBlocks := []datamodel.Link{
   661  			rootNodeLnk,
   662  			middleListNodeLnk,
   663  			leafAlphaLnk,
   664  			leafBetaLnk,
   665  			middleMapNodeLnk,
   666  		}
   667  		s := selectorparse.CommonSelector_ExploreAllRecursively
   668  		verifySelectorLoads(t, newRootNode, expectedLinkRevisitBlocks, s, true, datamodel.NewPath(nil), nil, func(lctx linking.LinkContext, lnk datamodel.Link) (io.Reader, error) {
   669  			return storage.GetStream(lctx.Ctx, &store, lnk.Binary())
   670  		})
   671  	})
   672  
   673  	t.Run("explore-all with duplicate load and preloader skips via LinkVisitOnlyOnce:true", func(t *testing.T) {
   674  		// same as above but make sure the preloader doesn't get in the way
   675  		expectedLinkRevisitBlocks := []datamodel.Link{
   676  			rootNodeLnk,
   677  			middleListNodeLnk,
   678  			leafAlphaLnk,
   679  			leafBetaLnk,
   680  			middleMapNodeLnk,
   681  		}
   682  		s := selectorparse.CommonSelector_ExploreAllRecursively
   683  		preloadLinks := make(map[datamodel.Link]struct{})
   684  		preloader := func(_ preload.PreloadContext, link preload.Link) {
   685  			preloadLinks[link.Link] = struct{}{}
   686  		}
   687  		verifySelectorLoads(t, newRootNode, expectedLinkRevisitBlocks, s, true, datamodel.NewPath(nil), preloader, func(lctx linking.LinkContext, lnk datamodel.Link) (io.Reader, error) {
   688  			return storage.GetStream(lctx.Ctx, &store, lnk.Binary())
   689  		})
   690  		for _, l := range expectedLinkRevisitBlocks {
   691  			qt.Check(t, preloadLinks[l], qt.IsNotNil)
   692  		}
   693  	})
   694  
   695  	t.Run("explore-all with duplicate traversal skip via load at path", func(t *testing.T) {
   696  		// when using LinkRevisit:false to skip duplicate block loads, our loader
   697  		// doesn't even get to see the load attempts (unlike SkipMe, where the
   698  		// loader signals the skips)
   699  		testPathsToBlocksSkipped := []struct {
   700  			path               string
   701  			expectedLinkVisits []datamodel.Link
   702  		}{
   703  			// 5th node in load sequence for rootNode
   704  			{"0/linkedList/2", append([]datamodel.Link{rootNodeLnk, middleListNodeLnk}, expectedAllBlocks[4:]...)},
   705  			// LinkedMap is 7th no, foo doesn't affect loading
   706  			{"0/linkedMap/foo", append([]datamodel.Link{rootNodeLnk}, expectedAllBlocks[6:]...)},
   707  			// 8th node in load sequence for rootNode
   708  			{"0/linkedMap/nested/alink", append([]datamodel.Link{rootNodeLnk, middleMapNodeLnk}, expectedAllBlocks[7:]...)},
   709  			{"0/linkedString", append([]datamodel.Link{rootNodeLnk}, expectedAllBlocks[8:]...)},
   710  			// pash through all nodes first root block, then go load middle list block
   711  			{"1/2", append([]datamodel.Link{middleListNodeLnk}, expectedAllBlocks[len(rootNodeExpectedLinks)+3:]...)},
   712  			{"3/1", append([]datamodel.Link{middleListNodeLnk}, expectedAllBlocks[2*len(rootNodeExpectedLinks)+len(middleListNodeLinks)+2:]...)},
   713  		}
   714  		for _, testCase := range testPathsToBlocksSkipped {
   715  			t.Run(testCase.path, func(t *testing.T) {
   716  				startAtPath := datamodel.ParsePath(testCase.path)
   717  				s := selectorparse.CommonSelector_ExploreAllRecursively
   718  				verifySelectorLoads(t, newRootNode, testCase.expectedLinkVisits, s, false, startAtPath, nil, func(lctx linking.LinkContext, lnk datamodel.Link) (io.Reader, error) {
   719  					return storage.GetStream(lctx.Ctx, &store, lnk.Binary())
   720  				})
   721  			})
   722  		}
   723  	})
   724  }
   725  
   726  func TestWalk_ADLs(t *testing.T) {
   727  	// we'll make a reifier that when it sees a list returns a custom element instead.
   728  	customReifier := func(_ linking.LinkContext, n datamodel.Node, _ *linking.LinkSystem) (datamodel.Node, error) {
   729  		if n.Kind() == datamodel.Kind_List {
   730  			return leafAlpha, nil
   731  		}
   732  		return n, nil
   733  	}
   734  
   735  	ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any)
   736  	ss := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   737  		efsb.Insert("linkedList", ssb.ExploreInterpretAs("linkJumper", ssb.Matcher()))
   738  	})
   739  	s, err := ss.Selector()
   740  	qt.Check(t, err, qt.IsNil)
   741  	lsys := cidlink.DefaultLinkSystem()
   742  	lsys.KnownReifiers = map[string]linking.NodeReifier{"linkJumper": customReifier}
   743  	lsys.SetReadStorage(&store)
   744  	var order int
   745  	err = traversal.Progress{
   746  		Cfg: &traversal.Config{
   747  			LinkSystem:                     lsys,
   748  			LinkTargetNodePrototypeChooser: basicnode.Chooser,
   749  		},
   750  	}.WalkMatching(rootNode, s, func(prog traversal.Progress, n datamodel.Node) error {
   751  		switch order {
   752  		case 0:
   753  			qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("alpha"))
   754  			qt.Check(t, prog.Path.String(), qt.Equals, "linkedList")
   755  		}
   756  		order++
   757  		return nil
   758  	})
   759  	qt.Check(t, err, qt.IsNil)
   760  	qt.Check(t, order, qt.Equals, 1)
   761  }
   762  
   763  func TestWalkTransforming(t *testing.T) {
   764  	ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any)
   765  	t.Run("transform selecting true should transform the root", func(t *testing.T) {
   766  		n, err := traversal.WalkTransforming(basicnode.NewString("x"), selector.Matcher{}, func(prog traversal.Progress, n datamodel.Node) (datamodel.Node, error) {
   767  			qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("x"))
   768  			qt.Check(t, prog.Path.String(), qt.Equals, datamodel.Path{}.String())
   769  			return basicnode.NewString("replaced"), nil
   770  		})
   771  		qt.Check(t, err, qt.IsNil)
   772  		qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("replaced"))
   773  	})
   774  	t.Run("transforming selecting fields recursively should work", func(t *testing.T) {
   775  		ss := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   776  			efsb.Insert("foo", ssb.Matcher())
   777  			efsb.Insert("nested", ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
   778  				efsb.Insert("nonlink", ssb.Matcher())
   779  			}))
   780  		})
   781  		s, err := ss.Selector()
   782  		qt.Assert(t, err, qt.IsNil)
   783  		var order int
   784  		n, err := traversal.WalkTransforming(middleMapNode, s, func(prog traversal.Progress, n datamodel.Node) (datamodel.Node, error) {
   785  			switch order {
   786  			case 0:
   787  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewBool(true))
   788  				qt.Check(t, prog.Path.String(), qt.Equals, "foo")
   789  			case 1:
   790  				qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("zoo"))
   791  				qt.Check(t, prog.Path.String(), qt.Equals, "nested/nonlink")
   792  			}
   793  			order++
   794  			return basicnode.NewString("replaced"), nil
   795  		})
   796  		qt.Check(t, err, qt.IsNil)
   797  		qt.Check(t, order, qt.Equals, 2)
   798  		qt.Check(t, n, nodetests.NodeContentEquals, fluent.MustBuildMap(basicnode.Prototype.Map, 3, func(na fluent.MapAssembler) {
   799  			na.AssembleEntry("foo").AssignString("replaced")
   800  			na.AssembleEntry("bar").AssignBool(false)
   801  			na.AssembleEntry("nested").CreateMap(2, func(na fluent.MapAssembler) {
   802  				na.AssembleEntry("alink").AssignLink(leafAlphaLnk)
   803  				na.AssembleEntry("nonlink").AssignString("replaced")
   804  			})
   805  		}))
   806  	})
   807  }