Skip to content
This repository has been archived by the owner on Sep 9, 2020. It is now read-only.

Commit

Permalink
Merge pull request #271 from Rhymond/dot_output
Browse files Browse the repository at this point in the history
Dep status tree visualisation dot output
  • Loading branch information
sdboyer authored Apr 13, 2017
2 parents 864d348 + 7fa0203 commit e4f1f3e
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 0 deletions.
110 changes: 110 additions & 0 deletions cmd/dep/graphviz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2016 The Go 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 main

import (
"bytes"
"fmt"
"hash/fnv"
"strings"
)

type graphviz struct {
ps []*gvnode
b bytes.Buffer
h map[string]uint32
}

type gvnode struct {
project string
version string
children []string
}

func (g graphviz) New() *graphviz {
ga := &graphviz{
ps: []*gvnode{},
h: make(map[string]uint32),
}
return ga
}

func (g graphviz) output() bytes.Buffer {
g.b.WriteString("digraph {\n\tnode [shape=box];")

for _, gvp := range g.ps {
// Create node string
g.b.WriteString(fmt.Sprintf("\n\t%d [label=\"%s\"];", gvp.hash(), gvp.label()))
}

// Store relations to avoid duplication
rels := make(map[string]bool)

// Create relations
for _, dp := range g.ps {
for _, bsc := range dp.children {
for pr, hsh := range g.h {
if isPathPrefix(bsc, pr) {
r := fmt.Sprintf("\n\t%d -> %d", g.h[dp.project], hsh)

if _, ex := rels[r]; !ex {
g.b.WriteString(r + ";")
rels[r] = true
}

}
}
}
}

g.b.WriteString("\n}")
return g.b
}

func (g *graphviz) createNode(project, version string, children []string) {
pr := &gvnode{
project: project,
version: version,
children: children,
}

g.h[pr.project] = pr.hash()
g.ps = append(g.ps, pr)
}

func (dp gvnode) hash() uint32 {
h := fnv.New32a()
h.Write([]byte(dp.project))
return h.Sum32()
}

func (dp gvnode) label() string {
label := []string{dp.project}

if dp.version != "" {
label = append(label, dp.version)
}

return strings.Join(label, "\\n")
}

// isPathPrefix ensures that the literal string prefix is a path tree match and
// guards against possibilities like this:
//
// github.com/sdboyer/foo
// github.com/sdboyer/foobar/baz
//
// Verify that prefix is path match and either the input is the same length as
// the match (in which case we know they're equal), or that the next character
// is a "/". (Import paths are defined to always use "/", not the OS-specific
// path separator.)
func isPathPrefix(path, pre string) bool {
pathlen, prflen := len(path), len(pre)
if pathlen < prflen || path[0:prflen] != pre {
return false
}

return prflen == pathlen || strings.Index(path[prflen:], "/") == 0
}
75 changes: 75 additions & 0 deletions cmd/dep/graphviz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2016 The Go 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 main

import (
"testing"

"github.com/golang/dep/test"
)

func TestEmptyProject(t *testing.T) {
g := new(graphviz).New()
h := test.NewHelper(t)
defer h.Cleanup()

b := g.output()
want := h.GetTestFileString("graphviz/empty.dot")

if b.String() != want {
t.Fatalf("expected '%v', got '%v'", want, b.String())
}
}

func TestSimpleProject(t *testing.T) {
g := new(graphviz).New()
h := test.NewHelper(t)
defer h.Cleanup()

g.createNode("project", "", []string{"foo", "bar"})
g.createNode("foo", "master", []string{"bar"})
g.createNode("bar", "dev", []string{})

b := g.output()
want := h.GetTestFileString("graphviz/case1.dot")
if b.String() != want {
t.Fatalf("expected '%v', got '%v'", want, b.String())
}
}

func TestNoLinks(t *testing.T) {
g := new(graphviz).New()
h := test.NewHelper(t)
defer h.Cleanup()

g.createNode("project", "", []string{})

b := g.output()
want := h.GetTestFileString("graphviz/case2.dot")
if b.String() != want {
t.Fatalf("expected '%v', got '%v'", want, b.String())
}
}

func TestIsPathPrefix(t *testing.T) {
tcs := []struct {
path string
pre string
want bool
}{
{"github.com/sdboyer/foo/bar", "github.com/sdboyer/foo", true},
{"github.com/sdboyer/foobar", "github.com/sdboyer/foo", false},
{"github.com/sdboyer/bar/foo", "github.com/sdboyer/foo", false},
{"golang.org/sdboyer/bar/foo", "github.com/sdboyer/foo", false},
{"golang.org/sdboyer/FOO", "github.com/sdboyer/foo", false},
}

for _, tc := range tcs {
r := isPathPrefix(tc.path, tc.pre)
if tc.want != r {
t.Fatalf("expected '%v', got '%v'", tc.want, r)
}
}
}
51 changes: 51 additions & 0 deletions cmd/dep/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type statusCommand struct {
detailed bool
json bool
template string
output string
dot bool
old bool
missing bool
Expand Down Expand Up @@ -152,6 +153,35 @@ func (out *jsonOutput) MissingFooter() {
json.NewEncoder(out.w).Encode(out.missing)
}

type dotOutput struct {
w io.Writer
o string
g *graphviz
p *dep.Project
}

func (out *dotOutput) BasicHeader() {
out.g = new(graphviz).New()

ptree, _ := pkgtree.ListPackages(out.p.AbsRoot, string(out.p.ImportRoot))
prm, _ := ptree.ToReachMap(true, false, false, nil)

out.g.createNode(string(out.p.ImportRoot), "", prm.Flatten(false))
}

func (out *dotOutput) BasicFooter() {
gvo := out.g.output()
fmt.Fprintf(out.w, gvo.String())
}

func (out *dotOutput) BasicLine(bs *BasicStatus) {
out.g.createNode(bs.ProjectRoot, bs.Version.String(), bs.Children)
}

func (out *dotOutput) MissingHeader() {}
func (out *dotOutput) MissingLine(ms *MissingStatus) {}
func (out *dotOutput) MissingFooter() {}

func (cmd *statusCommand) Run(ctx *dep.Ctx, args []string) error {
p, err := ctx.LoadProject("")
if err != nil {
Expand All @@ -173,6 +203,12 @@ func (cmd *statusCommand) Run(ctx *dep.Ctx, args []string) error {
out = &jsonOutput{
w: os.Stdout,
}
case cmd.dot:
out = &dotOutput{
p: p,
o: cmd.output,
w: os.Stdout,
}
default:
out = &tableOutput{
w: tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0),
Expand All @@ -185,6 +221,7 @@ func (cmd *statusCommand) Run(ctx *dep.Ctx, args []string) error {
// in the summary/list status output mode.
type BasicStatus struct {
ProjectRoot string
Children []string
Constraint gps.Constraint
Version gps.UnpairedVersion
Revision gps.Revision
Expand Down Expand Up @@ -248,6 +285,20 @@ func runStatusAll(out outputter, p *dep.Project, sm *gps.SourceMgr) error {
PackageCount: len(proj.Packages()),
}

// Get children only for specific outputers
// in order to avoid slower status process
switch out.(type) {
case *dotOutput:
ptr, err := sm.ListPackages(proj.Ident(), proj.Version())

if err != nil {
return fmt.Errorf("analysis of %s package failed: %v", proj.Ident().ProjectRoot, err)
}

prm, _ := ptr.ToReachMap(true, false, false, nil)
bs.Children = prm.Flatten(false)
}

// Split apart the version from the lock into its constituent parts
switch tv := proj.Version().(type) {
case gps.UnpairedVersion:
Expand Down
9 changes: 9 additions & 0 deletions cmd/dep/testdata/graphviz/case1.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
digraph {
node [shape=box];
4106060478 [label="project"];
2851307223 [label="foo\nmaster"];
1991736602 [label="bar\ndev"];
4106060478 -> 2851307223;
4106060478 -> 1991736602;
2851307223 -> 1991736602;
}
4 changes: 4 additions & 0 deletions cmd/dep/testdata/graphviz/case2.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
digraph {
node [shape=box];
4106060478 [label="project"];
}
3 changes: 3 additions & 0 deletions cmd/dep/testdata/graphviz/empty.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
digraph {
node [shape=box];
}

0 comments on commit e4f1f3e

Please sign in to comment.