mirror of
https://github.com/gonum/gonum.git
synced 2025-10-06 15:47:01 +08:00
497 lines
10 KiB
Go
497 lines
10 KiB
Go
// 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 dot
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"gonum.org/v1/gonum/graph"
|
|
"gonum.org/v1/gonum/graph/encoding"
|
|
"gonum.org/v1/gonum/graph/simple"
|
|
)
|
|
|
|
func TestRoundTrip(t *testing.T) {
|
|
golden := []struct {
|
|
want string
|
|
directed bool
|
|
}{
|
|
{
|
|
want: directed,
|
|
directed: true,
|
|
},
|
|
{
|
|
want: undirected,
|
|
directed: false,
|
|
},
|
|
{
|
|
want: directedID,
|
|
directed: true,
|
|
},
|
|
{
|
|
want: undirectedID,
|
|
directed: false,
|
|
},
|
|
{
|
|
want: directedWithPorts,
|
|
directed: true,
|
|
},
|
|
{
|
|
want: undirectedWithPorts,
|
|
directed: false,
|
|
},
|
|
}
|
|
for i, g := range golden {
|
|
var dst encoding.Builder
|
|
if g.directed {
|
|
dst = newDotDirectedGraph()
|
|
} else {
|
|
dst = newDotUndirectedGraph()
|
|
}
|
|
data := []byte(g.want)
|
|
if err := Unmarshal(data, dst); err != nil {
|
|
t.Errorf("i=%d: unable to unmarshal DOT graph; %v", i, err)
|
|
continue
|
|
}
|
|
buf, err := Marshal(dst, "", "", "\t")
|
|
if err != nil {
|
|
t.Errorf("i=%d: unable to marshal graph; %v", i, dst)
|
|
continue
|
|
}
|
|
got := string(buf)
|
|
if got != g.want {
|
|
t.Errorf("i=%d: graph content mismatch; want:\n%s\n\ngot:\n%s", i, g.want, got)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
const directed = `strict digraph {
|
|
graph [
|
|
outputorder=edgesfirst
|
|
];
|
|
node [
|
|
shape=circle
|
|
style=filled
|
|
];
|
|
edge [
|
|
penwidth=5
|
|
color=gray
|
|
];
|
|
|
|
// Node definitions.
|
|
A [label="foo 2"];
|
|
B [label="bar 2"];
|
|
|
|
// Edge definitions.
|
|
A -> B [label="baz 2"];
|
|
}`
|
|
|
|
const undirected = `strict graph {
|
|
graph [
|
|
outputorder=edgesfirst
|
|
];
|
|
node [
|
|
shape=circle
|
|
style=filled
|
|
];
|
|
edge [
|
|
penwidth=5
|
|
color=gray
|
|
];
|
|
|
|
// Node definitions.
|
|
A [label="foo 2"];
|
|
B [label="bar 2"];
|
|
|
|
// Edge definitions.
|
|
A -- B [label="baz 2"];
|
|
}`
|
|
|
|
const directedID = `strict digraph G {
|
|
// Node definitions.
|
|
A;
|
|
B;
|
|
|
|
// Edge definitions.
|
|
A -> B;
|
|
}`
|
|
|
|
const undirectedID = `strict graph H {
|
|
// Node definitions.
|
|
A;
|
|
B;
|
|
|
|
// Edge definitions.
|
|
A -- B;
|
|
}`
|
|
|
|
const directedWithPorts = `strict digraph {
|
|
// Node definitions.
|
|
A;
|
|
B;
|
|
C;
|
|
D;
|
|
E;
|
|
F;
|
|
|
|
// Edge definitions.
|
|
A:foo -> B:bar;
|
|
A -> C:bar;
|
|
B:foo -> C;
|
|
D:foo:n -> E:bar:s;
|
|
D:e -> F:bar:w;
|
|
E:_ -> F:c;
|
|
}`
|
|
|
|
const undirectedWithPorts = `strict graph {
|
|
// Node definitions.
|
|
A;
|
|
B;
|
|
C;
|
|
D;
|
|
E;
|
|
F;
|
|
|
|
// Edge definitions.
|
|
A:foo -- B:bar;
|
|
A -- C:bar;
|
|
B:foo -- C;
|
|
D:foo:n -- E:bar:s;
|
|
D:e -- F:bar:w;
|
|
E:_ -- F:c;
|
|
}`
|
|
|
|
func TestChainedEdgeAttributes(t *testing.T) {
|
|
golden := []struct {
|
|
in, want string
|
|
directed bool
|
|
}{
|
|
{
|
|
in: directedChained,
|
|
want: directedNonchained,
|
|
directed: true,
|
|
},
|
|
{
|
|
in: undirectedChained,
|
|
want: undirectedNonchained,
|
|
directed: false,
|
|
},
|
|
}
|
|
for i, g := range golden {
|
|
var dst encoding.Builder
|
|
if g.directed {
|
|
dst = newDotDirectedGraph()
|
|
} else {
|
|
dst = newDotUndirectedGraph()
|
|
}
|
|
data := []byte(g.in)
|
|
if err := Unmarshal(data, dst); err != nil {
|
|
t.Errorf("i=%d: unable to unmarshal DOT graph; %v", i, err)
|
|
continue
|
|
}
|
|
buf, err := Marshal(dst, "", "", "\t")
|
|
if err != nil {
|
|
t.Errorf("i=%d: unable to marshal graph; %v", i, dst)
|
|
continue
|
|
}
|
|
got := string(buf)
|
|
if got != g.want {
|
|
t.Errorf("i=%d: graph content mismatch; want:\n%s\n\ngot:\n%s", i, g.want, got)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
const directedChained = `strict digraph {
|
|
graph [
|
|
outputorder=edgesfirst
|
|
];
|
|
node [
|
|
shape=circle
|
|
style=filled
|
|
];
|
|
edge [
|
|
penwidth=5
|
|
color=gray
|
|
];
|
|
|
|
// Node definitions.
|
|
A [label="foo 2"];
|
|
B [label="bar 2"];
|
|
|
|
// Edge definitions.
|
|
A -> B -> A [label="baz 2"];
|
|
}`
|
|
|
|
const directedNonchained = `strict digraph {
|
|
graph [
|
|
outputorder=edgesfirst
|
|
];
|
|
node [
|
|
shape=circle
|
|
style=filled
|
|
];
|
|
edge [
|
|
penwidth=5
|
|
color=gray
|
|
];
|
|
|
|
// Node definitions.
|
|
A [label="foo 2"];
|
|
B [label="bar 2"];
|
|
|
|
// Edge definitions.
|
|
A -> B [label="baz 2"];
|
|
B -> A [label="baz 2"];
|
|
}`
|
|
|
|
const undirectedChained = `graph {
|
|
graph [
|
|
outputorder=edgesfirst
|
|
];
|
|
node [
|
|
shape=circle
|
|
style=filled
|
|
];
|
|
edge [
|
|
penwidth=5
|
|
color=gray
|
|
];
|
|
|
|
// Node definitions.
|
|
A [label="foo 2"];
|
|
B [label="bar 2"];
|
|
C [label="bif 2"];
|
|
|
|
// Edge definitions.
|
|
A -- B -- C [label="baz 2"];
|
|
}`
|
|
|
|
const undirectedNonchained = `strict graph {
|
|
graph [
|
|
outputorder=edgesfirst
|
|
];
|
|
node [
|
|
shape=circle
|
|
style=filled
|
|
];
|
|
edge [
|
|
penwidth=5
|
|
color=gray
|
|
];
|
|
|
|
// Node definitions.
|
|
A [label="foo 2"];
|
|
B [label="bar 2"];
|
|
C [label="bif 2"];
|
|
|
|
// Edge definitions.
|
|
A -- B [label="baz 2"];
|
|
B -- C [label="baz 2"];
|
|
}`
|
|
|
|
// Below follows a minimal implementation of a graph capable of validating the
|
|
// round-trip encoding and decoding of DOT graphs with nodes and edges
|
|
// containing DOT attributes.
|
|
|
|
// dotDirectedGraph extends simple.DirectedGraph to add NewNode and NewEdge
|
|
// methods for creating user-defined nodes and edges.
|
|
//
|
|
// dotDirectedGraph implements the encoding.Builder and the dot.Graph
|
|
// interfaces.
|
|
type dotDirectedGraph struct {
|
|
*simple.DirectedGraph
|
|
id string
|
|
graph, node, edge attributes
|
|
}
|
|
|
|
// newDotDirectedGraph returns a new directed capable of creating user-defined
|
|
// nodes and edges.
|
|
func newDotDirectedGraph() *dotDirectedGraph {
|
|
return &dotDirectedGraph{DirectedGraph: simple.NewDirectedGraph()}
|
|
}
|
|
|
|
// NewNode returns a new node with a unique node ID for the graph.
|
|
func (g *dotDirectedGraph) NewNode() graph.Node {
|
|
return &dotNode{Node: g.DirectedGraph.NewNode()}
|
|
}
|
|
|
|
// NewEdge returns a new Edge from the source to the destination node.
|
|
func (g *dotDirectedGraph) NewEdge(from, to graph.Node) graph.Edge {
|
|
return &dotEdge{Edge: g.DirectedGraph.NewEdge(from, to)}
|
|
}
|
|
|
|
// DOTAttributers implements the dot.Attributers interface.
|
|
func (g *dotDirectedGraph) DOTAttributers() (graph, node, edge encoding.Attributer) {
|
|
return g.graph, g.node, g.edge
|
|
}
|
|
|
|
// DOTAttributeSetters implements the dot.AttributeSetters interface.
|
|
func (g *dotDirectedGraph) DOTAttributeSetters() (graph, node, edge encoding.AttributeSetter) {
|
|
return &g.graph, &g.node, &g.edge
|
|
}
|
|
|
|
// SetDOTID sets the DOT ID of the graph.
|
|
func (g *dotDirectedGraph) SetDOTID(id string) {
|
|
g.id = id
|
|
}
|
|
|
|
// DOTID returns the DOT ID of the graph.
|
|
func (g *dotDirectedGraph) DOTID() string {
|
|
return g.id
|
|
}
|
|
|
|
// dotUndirectedGraph extends simple.UndirectedGraph to add NewNode and NewEdge
|
|
// methods for creating user-defined nodes and edges.
|
|
//
|
|
// dotUndirectedGraph implements the encoding.Builder and the dot.Graph
|
|
// interfaces.
|
|
type dotUndirectedGraph struct {
|
|
*simple.UndirectedGraph
|
|
id string
|
|
graph, node, edge attributes
|
|
}
|
|
|
|
// newDotUndirectedGraph returns a new undirected capable of creating user-
|
|
// defined nodes and edges.
|
|
func newDotUndirectedGraph() *dotUndirectedGraph {
|
|
return &dotUndirectedGraph{UndirectedGraph: simple.NewUndirectedGraph()}
|
|
}
|
|
|
|
// NewNode adds a new node with a unique node ID to the graph.
|
|
func (g *dotUndirectedGraph) NewNode() graph.Node {
|
|
return &dotNode{Node: g.UndirectedGraph.NewNode()}
|
|
}
|
|
|
|
// NewEdge returns a new Edge from the source to the destination node.
|
|
func (g *dotUndirectedGraph) NewEdge(from, to graph.Node) graph.Edge {
|
|
return &dotEdge{Edge: g.UndirectedGraph.NewEdge(from, to)}
|
|
}
|
|
|
|
// DOTAttributers implements the dot.Attributers interface.
|
|
func (g *dotUndirectedGraph) DOTAttributers() (graph, node, edge encoding.Attributer) {
|
|
return g.graph, g.node, g.edge
|
|
}
|
|
|
|
// DOTUnmarshalerAttrs implements the dot.UnmarshalerAttrs interface.
|
|
func (g *dotUndirectedGraph) DOTAttributeSetters() (graph, node, edge encoding.AttributeSetter) {
|
|
return &g.graph, &g.node, &g.edge
|
|
}
|
|
|
|
// SetDOTID sets the DOT ID of the graph.
|
|
func (g *dotUndirectedGraph) SetDOTID(id string) {
|
|
g.id = id
|
|
}
|
|
|
|
// DOTID returns the DOT ID of the graph.
|
|
func (g *dotUndirectedGraph) DOTID() string {
|
|
return g.id
|
|
}
|
|
|
|
// dotNode extends simple.Node with a label field to test round-trip encoding
|
|
// and decoding of node DOT label attributes.
|
|
type dotNode struct {
|
|
graph.Node
|
|
dotID string
|
|
// Node label.
|
|
Label string
|
|
}
|
|
|
|
// DOTID returns the node's DOT ID.
|
|
func (n *dotNode) DOTID() string {
|
|
return n.dotID
|
|
}
|
|
|
|
// SetDOTID sets a DOT ID.
|
|
func (n *dotNode) SetDOTID(id string) {
|
|
n.dotID = id
|
|
}
|
|
|
|
// SetAttribute sets a DOT attribute.
|
|
func (n *dotNode) SetAttribute(attr encoding.Attribute) error {
|
|
if attr.Key != "label" {
|
|
return fmt.Errorf("unable to unmarshal node DOT attribute with key %q", attr.Key)
|
|
}
|
|
n.Label = attr.Value
|
|
return nil
|
|
}
|
|
|
|
// Attributes returns the DOT attributes of the node.
|
|
func (n *dotNode) Attributes() []encoding.Attribute {
|
|
if len(n.Label) == 0 {
|
|
return nil
|
|
}
|
|
return []encoding.Attribute{{
|
|
Key: "label",
|
|
Value: n.Label,
|
|
}}
|
|
}
|
|
|
|
type dotPortLabels struct {
|
|
Port, Compass string
|
|
}
|
|
|
|
// dotEdge extends simple.Edge with a label field to test round-trip encoding and
|
|
// decoding of edge DOT label attributes.
|
|
type dotEdge struct {
|
|
graph.Edge
|
|
// Edge label.
|
|
Label string
|
|
FromPortLabels dotPortLabels
|
|
ToPortLabels dotPortLabels
|
|
}
|
|
|
|
// SetAttribute sets a DOT attribute.
|
|
func (e *dotEdge) SetAttribute(attr encoding.Attribute) error {
|
|
if attr.Key != "label" {
|
|
return fmt.Errorf("unable to unmarshal node DOT attribute with key %q", attr.Key)
|
|
}
|
|
e.Label = attr.Value
|
|
return nil
|
|
}
|
|
|
|
// Attributes returns the DOT attributes of the edge.
|
|
func (e *dotEdge) Attributes() []encoding.Attribute {
|
|
if len(e.Label) == 0 {
|
|
return nil
|
|
}
|
|
return []encoding.Attribute{{
|
|
Key: "label",
|
|
Value: e.Label,
|
|
}}
|
|
}
|
|
|
|
func (e *dotEdge) SetFromPort(port, compass string) error {
|
|
e.FromPortLabels.Port = port
|
|
e.FromPortLabels.Compass = compass
|
|
return nil
|
|
}
|
|
|
|
func (e *dotEdge) SetToPort(port, compass string) error {
|
|
e.ToPortLabels.Port = port
|
|
e.ToPortLabels.Compass = compass
|
|
return nil
|
|
}
|
|
|
|
func (e *dotEdge) FromPort() (port, compass string) {
|
|
return e.FromPortLabels.Port, e.FromPortLabels.Compass
|
|
}
|
|
|
|
func (e *dotEdge) ToPort() (port, compass string) {
|
|
return e.ToPortLabels.Port, e.ToPortLabels.Compass
|
|
}
|
|
|
|
// attributes is a helper for global attributes.
|
|
type attributes []encoding.Attribute
|
|
|
|
func (a attributes) Attributes() []encoding.Attribute {
|
|
return []encoding.Attribute(a)
|
|
}
|
|
func (a *attributes) SetAttribute(attr encoding.Attribute) error {
|
|
*a = append(*a, attr)
|
|
return nil
|
|
}
|