pgraph: Print cycles on error
I'm a terrible algorithmist, so who knows if this is correct, but it seems to work in my cursory testing.
This commit is contained in:
@@ -291,7 +291,14 @@ func (obj *Interpreter) Interpret(ast interfaces.Stmt, table map[interfaces.Func
|
|||||||
|
|
||||||
// ensure that we have a DAG!
|
// ensure that we have a DAG!
|
||||||
if _, err := graph.TopologicalSort(); err != nil {
|
if _, err := graph.TopologicalSort(); err != nil {
|
||||||
// TODO: print information on the cycles
|
errNotAcyclic, ok := err.(*pgraph.ErrNotAcyclic)
|
||||||
|
if !ok {
|
||||||
|
return nil, err // programming error
|
||||||
|
}
|
||||||
|
obj.Logf("%s", err)
|
||||||
|
for _, vertex := range errNotAcyclic.Cycle {
|
||||||
|
obj.Logf("* %s", vertex)
|
||||||
|
}
|
||||||
return nil, errwrap.Wrapf(err, "resource graph has cycles")
|
return nil, errwrap.Wrapf(err, "resource graph has cycles")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
package pgraph
|
package pgraph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -40,7 +39,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ErrNotAcyclic specifies that a particular graph was not found to be a dag.
|
// ErrNotAcyclic specifies that a particular graph was not found to be a dag.
|
||||||
var ErrNotAcyclic = errors.New("not a dag")
|
type ErrNotAcyclic struct {
|
||||||
|
Cycle []Vertex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error lets this satisfy the error interface.
|
||||||
|
func (obj *ErrNotAcyclic) Error() string {
|
||||||
|
//return fmt.Sprintf("not a dag: %v", obj.Cycle)
|
||||||
|
return "not a dag"
|
||||||
|
}
|
||||||
|
|
||||||
// Graph is the graph structure in this library. The graph abstract data type
|
// Graph is the graph structure in this library. The graph abstract data type
|
||||||
// (ADT) is defined as follows:
|
// (ADT) is defined as follows:
|
||||||
@@ -667,7 +674,12 @@ func (g *Graph) TopologicalSort() ([]Vertex, error) { // kahn's algorithm
|
|||||||
if in > 0 {
|
if in > 0 {
|
||||||
for n := range g.adjacency[c] {
|
for n := range g.adjacency[c] {
|
||||||
if remaining[n] > 0 {
|
if remaining[n] > 0 {
|
||||||
return nil, ErrNotAcyclic
|
cycle := g.findCycleDFS(c)
|
||||||
|
if len(cycle) == 0 {
|
||||||
|
// Hopefully this doesn't happen!
|
||||||
|
return nil, fmt.Errorf("programming error")
|
||||||
|
}
|
||||||
|
return nil, &ErrNotAcyclic{Cycle: cycle}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -676,6 +688,61 @@ func (g *Graph) TopologicalSort() ([]Vertex, error) { // kahn's algorithm
|
|||||||
return L, nil
|
return L, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findCycleDFS is a helper for the TopologicalSort functions.
|
||||||
|
// XXX: A professional should look over this function and try and find issues.
|
||||||
|
func (g *Graph) findCycleDFS(start Vertex) []Vertex {
|
||||||
|
visited := make(map[Vertex]bool)
|
||||||
|
stack := make(map[Vertex]bool)
|
||||||
|
var path []Vertex
|
||||||
|
var result []Vertex
|
||||||
|
found := false
|
||||||
|
|
||||||
|
var dfs func(Vertex) bool
|
||||||
|
dfs = func(v Vertex) bool {
|
||||||
|
if found {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
visited[v] = true
|
||||||
|
stack[v] = true
|
||||||
|
path = append(path, v)
|
||||||
|
|
||||||
|
for n := range g.adjacency[v] {
|
||||||
|
if !visited[n] {
|
||||||
|
if dfs(n) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if stack[n] {
|
||||||
|
// cycle detected
|
||||||
|
idx := len(path) - 1
|
||||||
|
for idx >= 0 && path[idx] != n {
|
||||||
|
idx--
|
||||||
|
}
|
||||||
|
if idx >= 0 {
|
||||||
|
result = append([]Vertex{}, path[idx:]...)
|
||||||
|
result = append(result, n) // close the cycle
|
||||||
|
found = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stack[v] = false
|
||||||
|
path = path[:len(path)-1]
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// run DFS from all potentially cyclic nodes
|
||||||
|
for v := range g.adjacency {
|
||||||
|
if !visited[v] {
|
||||||
|
if dfs(v) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// DeterministicTopologicalSort returns the sort of graph vertices in a stable
|
// DeterministicTopologicalSort returns the sort of graph vertices in a stable
|
||||||
// topological sort order. It's slower than the TopologicalSort implementation,
|
// topological sort order. It's slower than the TopologicalSort implementation,
|
||||||
// but guarantees that two identical graphs produce the same sort each time.
|
// but guarantees that two identical graphs produce the same sort each time.
|
||||||
@@ -731,7 +798,12 @@ func (g *Graph) DeterministicTopologicalSort() ([]Vertex, error) { // kahn's alg
|
|||||||
if in > 0 {
|
if in > 0 {
|
||||||
for n := range g.adjacency[c] {
|
for n := range g.adjacency[c] {
|
||||||
if remaining[n] > 0 {
|
if remaining[n] > 0 {
|
||||||
return nil, ErrNotAcyclic
|
cycle := g.findCycleDFS(c)
|
||||||
|
if len(cycle) == 0 {
|
||||||
|
// Hopefully this doesn't happen!
|
||||||
|
return nil, fmt.Errorf("programming error")
|
||||||
|
}
|
||||||
|
return nil, &ErrNotAcyclic{Cycle: cycle}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -464,6 +464,56 @@ func TestTopoSort2(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTopoSort3(t *testing.T) {
|
||||||
|
G, _ := NewGraph("g11")
|
||||||
|
v1 := NV("v1")
|
||||||
|
v2 := NV("v2")
|
||||||
|
v3 := NV("v3")
|
||||||
|
v4 := NV("v4")
|
||||||
|
v5 := NV("v5")
|
||||||
|
v6 := NV("v6")
|
||||||
|
e1 := NE("e1")
|
||||||
|
e2 := NE("e2")
|
||||||
|
e3 := NE("e3")
|
||||||
|
e4 := NE("e4")
|
||||||
|
e5 := NE("e5")
|
||||||
|
e6 := NE("e6")
|
||||||
|
G.AddEdge(v1, v2, e1)
|
||||||
|
G.AddEdge(v2, v3, e2)
|
||||||
|
G.AddEdge(v3, v4, e3)
|
||||||
|
G.AddEdge(v4, v5, e4)
|
||||||
|
G.AddEdge(v5, v6, e5)
|
||||||
|
G.AddEdge(v4, v2, e6) // cycle
|
||||||
|
|
||||||
|
G.ExecGraphviz("/tmp/g.dot")
|
||||||
|
|
||||||
|
_, err := G.TopologicalSort()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("topological sort passed, but graph is cyclic")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errNotAcyclic, ok := err.(*ErrNotAcyclic)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("wrong kind of error, got: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cycle := errNotAcyclic.Cycle
|
||||||
|
|
||||||
|
t.Logf("cycle: %v", cycle)
|
||||||
|
if len(cycle) < 2 {
|
||||||
|
t.Errorf("cycle is too short")
|
||||||
|
}
|
||||||
|
cycle1 := []Vertex{v2, v3, v4, v2}
|
||||||
|
cycle2 := []Vertex{v3, v4, v2, v3}
|
||||||
|
cycle3 := []Vertex{v4, v2, v3, v4}
|
||||||
|
b1 := reflect.DeepEqual(cycle, cycle1)
|
||||||
|
b2 := reflect.DeepEqual(cycle, cycle2)
|
||||||
|
b3 := reflect.DeepEqual(cycle, cycle3)
|
||||||
|
if !b1 && !b2 && !b3 {
|
||||||
|
t.Errorf("cycle didn't match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// empty
|
// empty
|
||||||
func TestReachability0(t *testing.T) {
|
func TestReachability0(t *testing.T) {
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user