github.com/cayleygraph/cayley@v0.7.7/graph/path/pathtest/pathtest.go (about)

     1  // Copyright 2014 The Cayley Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package pathtest
    16  
    17  import (
    18  	"context"
    19  	"reflect"
    20  	"regexp"
    21  	"sort"
    22  	"testing"
    23  	"time"
    24  
    25  	. "github.com/cayleygraph/cayley/graph/path"
    26  
    27  	"github.com/cayleygraph/cayley/graph"
    28  	"github.com/cayleygraph/cayley/graph/graphtest/testutil"
    29  	"github.com/cayleygraph/cayley/graph/iterator"
    30  	"github.com/cayleygraph/cayley/graph/shape"
    31  	_ "github.com/cayleygraph/cayley/writer"
    32  	"github.com/cayleygraph/quad"
    33  	"github.com/stretchr/testify/require"
    34  )
    35  
    36  // This is a simple test graph.
    37  //
    38  //  +-------+                        +------+
    39  //  | alice |-----                 ->| fred |<--
    40  //  +-------+     \---->+-------+-/  +------+   \-+-------+
    41  //                ----->| #bob# |       |         | emily |
    42  //  +---------+--/  --->+-------+       |         +-------+
    43  //  | charlie |    /                    v
    44  //  +---------+   /                  +--------+
    45  //    \---    +--------+             | #greg# |
    46  //        \-->| #dani# |------------>+--------+
    47  //            +--------+
    48  
    49  func makeTestStore(t testing.TB, fnc testutil.DatabaseFunc, quads ...quad.Quad) (graph.QuadStore, func()) {
    50  	if len(quads) == 0 {
    51  		quads = testutil.LoadGraph(t, "data/testdata.nq")
    52  	}
    53  	var (
    54  		qs     graph.QuadStore
    55  		opts   graph.Options
    56  		closer = func() {}
    57  	)
    58  	if fnc != nil {
    59  		qs, opts, closer = fnc(t)
    60  	} else {
    61  		qs, _ = graph.NewQuadStore("memstore", "", nil)
    62  	}
    63  	_ = testutil.MakeWriter(t, qs, opts, quads...)
    64  	return qs, closer
    65  }
    66  
    67  func runTopLevel(qs graph.QuadStore, path *Path, opt bool) ([]quad.Value, error) {
    68  	pb := path.Iterate(context.TODO())
    69  	if !opt {
    70  		pb = pb.UnOptimized()
    71  	}
    72  	return pb.Paths(false).AllValues(qs)
    73  }
    74  
    75  func runTag(qs graph.QuadStore, path *Path, tag string, opt bool) ([]quad.Value, error) {
    76  	var out []quad.Value
    77  	pb := path.Iterate(context.TODO())
    78  	if !opt {
    79  		pb = pb.UnOptimized()
    80  	}
    81  	err := pb.Paths(true).TagEach(func(tags map[string]graph.Ref) {
    82  		if t, ok := tags[tag]; ok {
    83  			out = append(out, qs.NameOf(t))
    84  		}
    85  	})
    86  	return out, err
    87  }
    88  
    89  func runAllTags(qs graph.QuadStore, path *Path, opt bool) ([]map[string]quad.Value, error) {
    90  	var out []map[string]quad.Value
    91  	pb := path.Iterate(context.TODO())
    92  	if !opt {
    93  		pb = pb.UnOptimized()
    94  	}
    95  	err := pb.Paths(true).TagValues(qs, func(tags map[string]quad.Value) {
    96  		out = append(out, tags)
    97  	})
    98  	return out, err
    99  }
   100  
   101  type test struct {
   102  	skip      bool
   103  	message   string
   104  	path      *Path
   105  	expect    []quad.Value
   106  	expectAlt [][]quad.Value
   107  	tag       string
   108  	unsorted  bool
   109  }
   110  
   111  // Define morphisms without a QuadStore
   112  
   113  const (
   114  	vFollows   = quad.IRI("follows")
   115  	vAre       = quad.IRI("are")
   116  	vStatus    = quad.IRI("status")
   117  	vPredicate = quad.IRI("predicates")
   118  
   119  	vCool       = quad.String("cool_person")
   120  	vSmart      = quad.String("smart_person")
   121  	vSmartGraph = quad.IRI("smart_graph")
   122  
   123  	vAlice   = quad.IRI("alice")
   124  	vBob     = quad.IRI("bob")
   125  	vCharlie = quad.IRI("charlie")
   126  	vDani    = quad.IRI("dani")
   127  	vFred    = quad.IRI("fred")
   128  	vGreg    = quad.IRI("greg")
   129  	vEmily   = quad.IRI("emily")
   130  )
   131  
   132  var (
   133  	grandfollows = StartMorphism().Out(vFollows).Out(vFollows)
   134  )
   135  
   136  func testSet(qs graph.QuadStore) []test {
   137  	return []test{
   138  		{
   139  			message: "out",
   140  			path:    StartPath(qs, vAlice).Out(vFollows),
   141  			expect:  []quad.Value{vBob},
   142  		},
   143  		{
   144  			message: "out (any)",
   145  			path:    StartPath(qs, vBob).Out(),
   146  			expect:  []quad.Value{vFred, vCool},
   147  		},
   148  		{
   149  			message: "out (raw)",
   150  			path:    StartPath(qs, quad.Raw(vAlice.String())).Out(quad.Raw(vFollows.String())),
   151  			expect:  []quad.Value{vBob},
   152  		},
   153  		{
   154  			message: "in",
   155  			path:    StartPath(qs, vBob).In(vFollows),
   156  			expect:  []quad.Value{vAlice, vCharlie, vDani},
   157  		},
   158  		{
   159  			message: "in (any)",
   160  			path:    StartPath(qs, vBob).In(),
   161  			expect:  []quad.Value{vAlice, vCharlie, vDani},
   162  		},
   163  		{
   164  			message: "filter nodes",
   165  			path:    StartPath(qs).Filter(iterator.CompareGT, quad.IRI("p")),
   166  			expect:  []quad.Value{vPredicate, vSmartGraph, vStatus},
   167  		},
   168  		{
   169  			message: "in with filter",
   170  			path:    StartPath(qs, vBob).In(vFollows).Filter(iterator.CompareGT, quad.IRI("c")),
   171  			expect:  []quad.Value{vCharlie, vDani},
   172  		},
   173  		{
   174  			message: "in with regex",
   175  			path:    StartPath(qs, vBob).In(vFollows).Regex(regexp.MustCompile("ar?li.*e")),
   176  			expect:  nil,
   177  		},
   178  		{
   179  			message: "in with regex (include IRIs)",
   180  			path:    StartPath(qs, vBob).In(vFollows).RegexWithRefs(regexp.MustCompile("ar?li.*e")),
   181  			expect:  []quad.Value{vAlice, vCharlie},
   182  		},
   183  		{
   184  			message: "path Out",
   185  			path:    StartPath(qs, vBob).Out(StartPath(qs, vPredicate).Out(vAre)),
   186  			expect:  []quad.Value{vFred, vCool},
   187  		},
   188  		{
   189  			message: "path Out (raw)",
   190  			path:    StartPath(qs, quad.Raw(vBob.String())).Out(StartPath(qs, quad.Raw(vPredicate.String())).Out(quad.Raw(vAre.String()))),
   191  			expect:  []quad.Value{vFred, vCool},
   192  		},
   193  		{
   194  			message: "And",
   195  			path: StartPath(qs, vDani).Out(vFollows).And(
   196  				StartPath(qs, vCharlie).Out(vFollows)),
   197  			expect: []quad.Value{vBob},
   198  		},
   199  		{
   200  			message: "Or",
   201  			path: StartPath(qs, vFred).Out(vFollows).Or(
   202  				StartPath(qs, vAlice).Out(vFollows)),
   203  			expect: []quad.Value{vBob, vGreg},
   204  		},
   205  		{
   206  			message: "implicit All",
   207  			path:    StartPath(qs),
   208  			expect:  []quad.Value{vAlice, vBob, vCharlie, vDani, vEmily, vFred, vGreg, vFollows, vStatus, vCool, vPredicate, vAre, vSmartGraph, vSmart},
   209  		},
   210  		{
   211  			message: "follow",
   212  			path:    StartPath(qs, vCharlie).Follow(StartMorphism().Out(vFollows).Out(vFollows)),
   213  			expect:  []quad.Value{vBob, vFred, vGreg},
   214  		},
   215  		{
   216  			message: "followR",
   217  			path:    StartPath(qs, vFred).FollowReverse(StartMorphism().Out(vFollows).Out(vFollows)),
   218  			expect:  []quad.Value{vAlice, vCharlie, vDani},
   219  		},
   220  		{
   221  			message: "is, tag, instead of FollowR",
   222  			path:    StartPath(qs).Tag("first").Follow(StartMorphism().Out(vFollows).Out(vFollows)).Is(vFred),
   223  			expect:  []quad.Value{vAlice, vCharlie, vDani},
   224  			tag:     "first",
   225  		},
   226  		{
   227  			message: "Except to filter out a single vertex",
   228  			path:    StartPath(qs, vAlice, vBob).Except(StartPath(qs, vAlice)),
   229  			expect:  []quad.Value{vBob},
   230  		},
   231  		{
   232  			message: "chained Except",
   233  			path:    StartPath(qs, vAlice, vBob, vCharlie).Except(StartPath(qs, vBob)).Except(StartPath(qs, vAlice)),
   234  			expect:  []quad.Value{vCharlie},
   235  		},
   236  		{
   237  			message: "Unique",
   238  			path:    StartPath(qs, vAlice, vBob, vCharlie).Out(vFollows).Unique(),
   239  			expect:  []quad.Value{vBob, vDani, vFred},
   240  		},
   241  		{
   242  			message: "simple save",
   243  			path:    StartPath(qs).Save(vStatus, "somecool"),
   244  			tag:     "somecool",
   245  			expect:  []quad.Value{vCool, vCool, vCool, vSmart, vSmart},
   246  		},
   247  		{
   248  			message: "simple saveR",
   249  			path:    StartPath(qs, vCool).SaveReverse(vStatus, "who"),
   250  			tag:     "who",
   251  			expect:  []quad.Value{vGreg, vDani, vBob},
   252  		},
   253  		{
   254  			message: "save with a next path",
   255  			path:    StartPath(qs, vDani, vBob).Save(vFollows, "target"),
   256  			tag:     "target",
   257  			expect:  []quad.Value{vBob, vFred, vGreg},
   258  		},
   259  		{
   260  			message: "save all with a next path",
   261  			path:    StartPath(qs).Save(vFollows, "target"),
   262  			tag:     "target",
   263  			expect:  []quad.Value{vBob, vBob, vBob, vDani, vFred, vFred, vGreg, vGreg},
   264  		},
   265  		{
   266  			message: "simple Has",
   267  			path:    StartPath(qs).Has(vStatus, vCool),
   268  			expect:  []quad.Value{vGreg, vDani, vBob},
   269  		},
   270  		{
   271  			message: "filter nodes with has",
   272  			path: StartPath(qs).HasFilter(vFollows, false, shape.Comparison{
   273  				Op: iterator.CompareGT, Val: quad.IRI("f"),
   274  			}),
   275  			expect: []quad.Value{vBob, vDani, vEmily, vFred},
   276  		},
   277  		{
   278  			message: "string prefix",
   279  			path: StartPath(qs).Filters(shape.Wildcard{
   280  				Pattern: `bo%`,
   281  			}),
   282  			expect: []quad.Value{vBob},
   283  		},
   284  		{
   285  			message: "three letters and range",
   286  			path: StartPath(qs).Filters(shape.Wildcard{
   287  				Pattern: `???`,
   288  			}, shape.Comparison{
   289  				Op: iterator.CompareGT, Val: quad.IRI("b"),
   290  			}),
   291  			expect: []quad.Value{vBob},
   292  		},
   293  		{
   294  			message: "part in string",
   295  			path: StartPath(qs).Filters(shape.Wildcard{
   296  				Pattern: `%ed%`,
   297  			}),
   298  			expect: []quad.Value{vFred, vPredicate},
   299  		},
   300  		{
   301  			message: "Limit",
   302  			path:    StartPath(qs).Has(vStatus, vCool).Limit(2),
   303  			// TODO(dennwc): resolve this ordering issue
   304  			expectAlt: [][]quad.Value{
   305  				{vBob, vGreg},
   306  				{vBob, vDani},
   307  				{vDani, vGreg},
   308  			},
   309  		},
   310  		{
   311  			message: "Skip",
   312  			path:    StartPath(qs).Has(vStatus, vCool).Skip(2),
   313  			expectAlt: [][]quad.Value{
   314  				{vBob},
   315  				{vDani},
   316  				{vGreg},
   317  			},
   318  		},
   319  		{
   320  			message: "Skip and Limit",
   321  			path:    StartPath(qs).Has(vStatus, vCool).Skip(1).Limit(1),
   322  			expectAlt: [][]quad.Value{
   323  				{vBob},
   324  				{vDani},
   325  				{vGreg},
   326  			},
   327  		},
   328  		{
   329  			message: "Count",
   330  			path:    StartPath(qs).Has(vStatus).Count(),
   331  			expect:  []quad.Value{quad.Int(5)},
   332  		},
   333  		{
   334  			message: "double Has",
   335  			path:    StartPath(qs).Has(vStatus, vCool).Has(vFollows, vFred),
   336  			expect:  []quad.Value{vBob},
   337  		},
   338  		{
   339  			message: "simple HasReverse",
   340  			path:    StartPath(qs).HasReverse(vStatus, vBob),
   341  			expect:  []quad.Value{vCool},
   342  		},
   343  		{
   344  			message: ".Tag()-.Is()-.Back()",
   345  			path:    StartPath(qs, vBob).In(vFollows).Tag("foo").Out(vStatus).Is(vCool).Back("foo"),
   346  			expect:  []quad.Value{vDani},
   347  		},
   348  		{
   349  			message: "do multiple .Back()s",
   350  			path:    StartPath(qs, vEmily).Out(vFollows).Tag("f").Out(vFollows).Out(vStatus).Is(vCool).Back("f").In(vFollows).In(vFollows).Tag("acd").Out(vStatus).Is(vCool).Back("f"),
   351  			tag:     "acd",
   352  			expect:  []quad.Value{vDani},
   353  		},
   354  		{
   355  			message: "Labels()",
   356  			path:    StartPath(qs, vGreg).Labels(),
   357  			expect:  []quad.Value{vSmartGraph},
   358  		},
   359  		{
   360  			message: "InPredicates()",
   361  			path:    StartPath(qs, vBob).InPredicates(),
   362  			expect:  []quad.Value{vFollows},
   363  		},
   364  		{
   365  			message: "OutPredicates()",
   366  			path:    StartPath(qs, vBob).OutPredicates(),
   367  			expect:  []quad.Value{vFollows, vStatus},
   368  		},
   369  		{
   370  			message: "SavePredicates(in)",
   371  			path:    StartPath(qs, vBob).SavePredicates(true, "pred"),
   372  			expect:  []quad.Value{vFollows, vFollows, vFollows},
   373  			tag:     "pred",
   374  		},
   375  		{
   376  			message: "SavePredicates(out)",
   377  			path:    StartPath(qs, vBob).SavePredicates(false, "pred"),
   378  			expect:  []quad.Value{vFollows, vStatus},
   379  			tag:     "pred",
   380  		},
   381  		// Morphism tests
   382  		{
   383  			message: "simple morphism",
   384  			path:    StartPath(qs, vCharlie).Follow(grandfollows),
   385  			expect:  []quad.Value{vGreg, vFred, vBob},
   386  		},
   387  		{
   388  			message: "reverse morphism",
   389  			path:    StartPath(qs, vFred).FollowReverse(grandfollows),
   390  			expect:  []quad.Value{vAlice, vCharlie, vDani},
   391  		},
   392  		// Context tests
   393  		{
   394  			message: "query without label limitation",
   395  			path:    StartPath(qs, vGreg).Out(vStatus),
   396  			expect:  []quad.Value{vSmart, vCool},
   397  		},
   398  		{
   399  			message: "query with label limitation",
   400  			path:    StartPath(qs, vGreg).LabelContext(vSmartGraph).Out(vStatus),
   401  			expect:  []quad.Value{vSmart},
   402  		},
   403  		{
   404  			message: "reverse context",
   405  			path:    StartPath(qs, vGreg).Tag("base").LabelContext(vSmartGraph).Out(vStatus).Tag("status").Back("base"),
   406  			expect:  []quad.Value{vGreg},
   407  		},
   408  		// Optional tests
   409  		{
   410  			message: "save limits top level",
   411  			path:    StartPath(qs, vBob, vCharlie).Out(vFollows).Save(vStatus, "statustag"),
   412  			expect:  []quad.Value{vBob, vDani},
   413  		},
   414  		{
   415  			message: "optional still returns top level",
   416  			path:    StartPath(qs, vBob, vCharlie).Out(vFollows).SaveOptional(vStatus, "statustag"),
   417  			expect:  []quad.Value{vBob, vFred, vDani},
   418  		},
   419  		{
   420  			message: "optional has the appropriate tags",
   421  			path:    StartPath(qs, vBob, vCharlie).Out(vFollows).SaveOptional(vStatus, "statustag"),
   422  			tag:     "statustag",
   423  			expect:  []quad.Value{vCool, vCool},
   424  		},
   425  		{
   426  			message: "composite paths (clone paths)",
   427  			path: func() *Path {
   428  				alice_path := StartPath(qs, vAlice)
   429  				_ = alice_path.Out(vFollows)
   430  
   431  				return alice_path
   432  			}(),
   433  			expect: []quad.Value{vAlice},
   434  		},
   435  		{
   436  			message: "follow recursive",
   437  			path:    StartPath(qs, vCharlie).FollowRecursive(vFollows, 0, nil),
   438  			expect:  []quad.Value{vBob, vDani, vFred, vGreg},
   439  		},
   440  		{
   441  			message: "follow recursive (limit depth)",
   442  			path:    StartPath(qs, vCharlie).FollowRecursive(vFollows, 1, nil),
   443  			expect:  []quad.Value{vBob, vDani},
   444  		},
   445  		{
   446  			message: "find non-existent",
   447  			path:    StartPath(qs, quad.IRI("<not-existing>")),
   448  			expect:  nil,
   449  		},
   450  		{
   451  			message: "use order",
   452  			path:    StartPath(qs).Order(),
   453  			expect: []quad.Value{
   454  				vAlice,
   455  				vAre,
   456  				vBob,
   457  				vCharlie,
   458  				vDani,
   459  				vEmily,
   460  				vFollows,
   461  				vFred,
   462  				vGreg,
   463  				vPredicate,
   464  				vSmartGraph,
   465  				vStatus,
   466  				vCool,
   467  				vSmart,
   468  			},
   469  		},
   470  		{
   471  			message: "use order tags",
   472  			path:    StartPath(qs).Tag("target").Order(),
   473  			tag:     "target",
   474  			expect: []quad.Value{
   475  				vAlice,
   476  				vAre,
   477  				vBob,
   478  				vCharlie,
   479  				vDani,
   480  				vEmily,
   481  				vFollows,
   482  				vFred,
   483  				vGreg,
   484  				vPredicate,
   485  				vSmartGraph,
   486  				vStatus,
   487  				vCool,
   488  				vSmart,
   489  			},
   490  		},
   491  		{
   492  			message: "order with a next path",
   493  			path:    StartPath(qs, vDani, vBob).Save(vFollows, "target").Order(),
   494  			tag:     "target",
   495  			expect:  []quad.Value{vBob, vFred, vGreg},
   496  		},
   497  		{
   498  			message:  "order with a next path",
   499  			path:     StartPath(qs).Order().Has(vFollows, vBob),
   500  			expect:   []quad.Value{vAlice, vCharlie, vDani},
   501  			unsorted: true,
   502  			skip:     true, // TODO(dennwc): optimize Order in And properly
   503  		},
   504  	}
   505  }
   506  
   507  func RunTestMorphisms(t *testing.T, fnc testutil.DatabaseFunc) {
   508  	for _, ftest := range []func(*testing.T, testutil.DatabaseFunc){
   509  		testFollowRecursive,
   510  		testFollowRecursiveHas,
   511  	} {
   512  		ftest(t, fnc)
   513  	}
   514  	qs, closer := makeTestStore(t, fnc)
   515  	defer closer()
   516  
   517  	for _, test := range testSet(qs) {
   518  		for _, opt := range []bool{true, false} {
   519  			name := test.message
   520  			if !opt {
   521  				name += " (unoptimized)"
   522  			}
   523  			t.Run(name, func(t *testing.T) {
   524  				if test.skip {
   525  					t.SkipNow()
   526  				}
   527  				var (
   528  					got []quad.Value
   529  					err error
   530  				)
   531  				start := time.Now()
   532  				if test.tag == "" {
   533  					got, err = runTopLevel(qs, test.path, opt)
   534  				} else {
   535  					got, err = runTag(qs, test.path, test.tag, opt)
   536  				}
   537  				dt := time.Since(start)
   538  				if err != nil {
   539  					t.Error(err)
   540  					return
   541  				}
   542  				if !test.unsorted {
   543  					sort.Sort(quad.ByValueString(got))
   544  				}
   545  				var eq bool
   546  				exp := test.expect
   547  				if test.expectAlt != nil {
   548  					for _, alt := range test.expectAlt {
   549  						exp = alt
   550  						if !test.unsorted {
   551  							sort.Sort(quad.ByValueString(exp))
   552  						}
   553  						eq = reflect.DeepEqual(got, exp)
   554  						if eq {
   555  							break
   556  						}
   557  					}
   558  				} else {
   559  					if !test.unsorted {
   560  						sort.Sort(quad.ByValueString(test.expect))
   561  					}
   562  					eq = reflect.DeepEqual(got, test.expect)
   563  				}
   564  				if !eq {
   565  					t.Errorf("got: %v(%d) expected: %v(%d)", got, len(got), exp, len(exp))
   566  				} else {
   567  					t.Logf("%12v %v", dt, name)
   568  				}
   569  			})
   570  		}
   571  	}
   572  }
   573  
   574  func testFollowRecursive(t *testing.T, fnc testutil.DatabaseFunc) {
   575  	qs, closer := makeTestStore(t, fnc, []quad.Quad{
   576  		quad.MakeIRI("a", "parent", "b", ""),
   577  		quad.MakeIRI("b", "parent", "c", ""),
   578  		quad.MakeIRI("c", "parent", "d", ""),
   579  		quad.MakeIRI("c", "labels", "tag", ""),
   580  		quad.MakeIRI("d", "parent", "e", ""),
   581  		quad.MakeIRI("d", "labels", "tag", ""),
   582  	}...)
   583  	defer closer()
   584  
   585  	qu := StartPath(qs, quad.IRI("a")).FollowRecursive(
   586  		StartMorphism().Out(quad.IRI("parent")), 0, nil,
   587  	).Has(quad.IRI("labels"), quad.IRI("tag"))
   588  
   589  	expect := []quad.Value{quad.IRI("c"), quad.IRI("d")}
   590  
   591  	const msg = "follows recursive order"
   592  
   593  	for _, opt := range []bool{true, false} {
   594  		unopt := ""
   595  		if !opt {
   596  			unopt = " (unoptimized)"
   597  		}
   598  		t.Run(msg+unopt, func(t *testing.T) {
   599  			got, err := runTopLevel(qs, qu, opt)
   600  			if err != nil {
   601  				t.Errorf("Failed to check %s%s: %v", msg, unopt, err)
   602  				return
   603  			}
   604  			sort.Sort(quad.ByValueString(got))
   605  			sort.Sort(quad.ByValueString(expect))
   606  			if !reflect.DeepEqual(got, expect) {
   607  				t.Errorf("Failed to %s%s, got: %v(%d) expected: %v(%d)", msg, unopt, got, len(got), expect, len(expect))
   608  			}
   609  		})
   610  	}
   611  }
   612  
   613  type byTags struct {
   614  	tags []string
   615  	arr  []map[string]quad.Value
   616  }
   617  
   618  func (b byTags) Len() int {
   619  	return len(b.arr)
   620  }
   621  
   622  func (b byTags) Less(i, j int) bool {
   623  	m1, m2 := b.arr[i], b.arr[j]
   624  	for _, t := range b.tags {
   625  		v1, v2 := m1[t], m2[t]
   626  		s1, s2 := quad.ToString(v1), quad.ToString(v2)
   627  		if s1 < s2 {
   628  			return true
   629  		} else if s1 > s2 {
   630  			return false
   631  		}
   632  	}
   633  	return false
   634  }
   635  
   636  func (b byTags) Swap(i, j int) {
   637  	b.arr[i], b.arr[j] = b.arr[j], b.arr[i]
   638  }
   639  
   640  func testFollowRecursiveHas(t *testing.T, fnc testutil.DatabaseFunc) {
   641  	qs, closer := makeTestStore(t, fnc, []quad.Quad{
   642  		quad.MakeIRI("1", "relatesTo", "x", ""),
   643  		quad.MakeIRI("2", "relatesTo", "x", ""),
   644  		quad.MakeIRI("3", "relatesTo", "y", ""),
   645  		quad.MakeIRI("1", "knows", "2", ""),
   646  		quad.MakeIRI("2", "knows", "3", ""),
   647  		quad.MakeIRI("2", "knows", "1", ""),
   648  	}...)
   649  	defer closer()
   650  
   651  	qu := StartPath(qs, quad.IRI("1")).FollowRecursive(
   652  		StartMorphism().Tag("pid").Out(quad.IRI("knows")), 2, nil,
   653  	).Has(quad.IRI("relatesTo")).Tag("id")
   654  
   655  	expect := []map[string]quad.Value{
   656  		{"id": quad.IRI("1"), "pid": quad.IRI("2")},
   657  		{"id": quad.IRI("2"), "pid": quad.IRI("1")},
   658  		{"id": quad.IRI("3"), "pid": quad.IRI("2")},
   659  	}
   660  	sortTags := []string{"id", "pid"}
   661  	sort.Sort(byTags{
   662  		tags: sortTags,
   663  		arr:  expect,
   664  	})
   665  
   666  	const msg = "follows recursive loop"
   667  
   668  	for _, opt := range []bool{true, false} {
   669  		unopt := ""
   670  		if !opt {
   671  			unopt = " (unoptimized)"
   672  		}
   673  		t.Run(msg+unopt, func(t *testing.T) {
   674  			got, err := runAllTags(qs, qu, opt)
   675  			if err != nil {
   676  				t.Errorf("Failed to check %s%s: %v", msg, unopt, err)
   677  				return
   678  			}
   679  			sort.Sort(byTags{
   680  				tags: sortTags,
   681  				arr:  got,
   682  			})
   683  			require.Equal(t, expect, got)
   684  		})
   685  	}
   686  }