// Copyright ©2017 The gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package graphql import ( "errors" "fmt" "sort" "strconv" "strings" "testing" "gonum.org/v1/gonum/graph" "gonum.org/v1/gonum/graph/encoding" "gonum.org/v1/gonum/graph/encoding/dot" "gonum.org/v1/gonum/graph/simple" ) var decodeTests = []struct { name string json string roots map[uint64]bool wantDOT string wantErr error }{ { name: "starwars", json: starwars, roots: map[uint64]bool{ 0xa3cff1a4c3ef3bb6: true, 0xb39aa14d66aedad5: true, }, wantDOT: `digraph { // Node definitions. 0x8a10d5a2611fd03f [name="Richard Marquand"]; 0xa3cff1a4c3ef3bb6 [ name="Star Wars: Episode V - The Empire Strikes Back" release_date=1980-05-21T00:00:00Z revenue=534000000 running_time=124 ]; 0xb39aa14d66aedad5 [ name="Star Wars: Episode VI - Return of the Jedi" release_date=1983-05-25T00:00:00Z revenue=572000000 running_time=131 ]; 0x0312de17a7ee89f9 [name="Luke Skywalker"]; 0x3da8d1dcab1bb381 [name="Han Solo"]; 0x4a7d0b5fe91e78a4 [name="Irvin Kernshner"]; 0x718337b9dcbaa7d9 [name="Princess Leia"]; // Edge definitions. 0xa3cff1a4c3ef3bb6 -> 0x0312de17a7ee89f9 [label=starring]; 0xa3cff1a4c3ef3bb6 -> 0x3da8d1dcab1bb381 [label=starring]; 0xa3cff1a4c3ef3bb6 -> 0x4a7d0b5fe91e78a4 [label=director]; 0xa3cff1a4c3ef3bb6 -> 0x718337b9dcbaa7d9 [label=starring]; 0xb39aa14d66aedad5 -> 0x8a10d5a2611fd03f [label=director]; 0xb39aa14d66aedad5 -> 0x0312de17a7ee89f9 [label=starring]; 0xb39aa14d66aedad5 -> 0x3da8d1dcab1bb381 [label=starring]; 0xb39aa14d66aedad5 -> 0x718337b9dcbaa7d9 [label=starring]; }`, }, { name: "tutorial", json: dgraphTutorial, roots: map[uint64]bool{ 0xfd90205a458151f: true, 0x52a80955d40ec819: true, }, wantDOT: `digraph { // Node definitions. 0x892a6da7ee1fbdec [ age=55 name=Sarah ]; 0x99b74c1b5ab100ec [ age=35 name=Artyom ]; 0xb9e12a67e34d6acc [ age=19 name=Catalina ]; 0xbf104824c777525d [name=Perro]; 0xf590a923ea1fccaa [name=Goldie]; 0xf92d7dbe272d680b [name="Hyung Sin"]; 0x0fd90205a458151f [ age=39 name=Michael ]; 0x37734fcf0a6fcc69 [name="Rammy the sheep"]; 0x52a80955d40ec819 [ age=35 name=Amit ]; 0x5e9ad1cd9466228c [ age=24 name="Sang Hyun" ]; // Edge definitions. 0xb9e12a67e34d6acc -> 0xbf104824c777525d [label=owns_pet]; 0xb9e12a67e34d6acc -> 0x5e9ad1cd9466228c [label=friend]; 0xf92d7dbe272d680b -> 0x5e9ad1cd9466228c [label=friend]; 0x0fd90205a458151f -> 0x892a6da7ee1fbdec [label=friend]; 0x0fd90205a458151f -> 0x99b74c1b5ab100ec [label=friend]; 0x0fd90205a458151f -> 0xb9e12a67e34d6acc [label=friend]; 0x0fd90205a458151f -> 0x37734fcf0a6fcc69 [label=owns_pet]; 0x0fd90205a458151f -> 0x52a80955d40ec819 [label=friend]; 0x0fd90205a458151f -> 0x5e9ad1cd9466228c [label=friend]; 0x52a80955d40ec819 -> 0x99b74c1b5ab100ec [label=friend]; 0x52a80955d40ec819 -> 0x0fd90205a458151f [label=friend]; 0x52a80955d40ec819 -> 0x5e9ad1cd9466228c [label=friend]; 0x5e9ad1cd9466228c -> 0xb9e12a67e34d6acc [label=friend]; 0x5e9ad1cd9466228c -> 0xf590a923ea1fccaa [label=owns_pet]; 0x5e9ad1cd9466228c -> 0xf92d7dbe272d680b [label=friend]; 0x5e9ad1cd9466228c -> 0x52a80955d40ec819 [label=friend]; }`, }, { name: "tutorial missing IDs", json: dgraphTutorialMissingIDs, wantErr: errors.New("graphql: no UID for node"), // Incomplete error string. }, } func TestDecode(t *testing.T) { for _, test := range decodeTests { dst := newDirectedGraph() err := Unmarshal([]byte(test.json), "_uid_", dst) if test.wantErr == nil && err != nil { t.Errorf("failed to unmarshal GraphQL JSON graph for %q: %v", test.name, err) } else if test.wantErr != nil { if err == nil { t.Errorf("expected error for %q: got:%v want:%v", test.name, err, test.wantErr) } continue } b, err := dot.Marshal(dst, "", "", " ", false) if err != nil { t.Fatalf("failed to DOT marshal graph %q: %v", test.name, err) } gotDOT := string(b) if gotDOT != test.wantDOT { t.Errorf("unexpected DOT encoding for %q:\ngot:\n%s\nwant:\n%s", test.name, gotDOT, test.wantDOT) } } } type directedGraph struct { *simple.DirectedGraph } func newDirectedGraph() *directedGraph { return &directedGraph{DirectedGraph: simple.NewDirectedGraph(0, 0)} } func (g *directedGraph) NewNode() graph.Node { return &node{attributes: make(attributes)} } func (g *directedGraph) NewEdge(from, to graph.Node) graph.Edge { if e := g.Edge(from, to); e != nil { return e } e := &edge{Edge: simple.Edge{F: from, T: to}} g.SetEdge(e) return e } type node struct { id uint64 attributes } func (n *node) ID() int64 { return int64(n.id) } func (n *node) DOTID() string { return fmt.Sprintf("0x%016x", uint64(n.id)) } func (n *node) SetIDFromString(uid string) error { if !strings.HasPrefix(uid, "0x") { return fmt.Errorf("uid is not hex value: %q", uid) } var err error n.id, err = strconv.ParseUint(uid[2:], 16, 64) return err } type edge struct { simple.Edge label string } func (e *edge) SetLabel(l string) { e.label = l } func (e *edge) Attributes() []encoding.Attribute { return []encoding.Attribute{{"label", e.label}} } type attributes map[string]encoding.Attribute func (a attributes) SetAttribute(attr encoding.Attribute) error { a[attr.Key] = attr return nil } func (a attributes) Attributes() []encoding.Attribute { keys := make([]string, 0, len(a)) for k := range a { keys = append(keys, k) } sort.Strings(keys) attr := make([]encoding.Attribute, 0, len(keys)) for _, k := range keys { v := a[k] if strings.Contains(v.Value, " ") { v.Value = `"` + v.Value + `"` } attr = append(attr, v) } return attr }