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 }