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

Dep status tree visualisation dot output #271

Merged
merged 14 commits into from
Apr 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -61,6 +61,7 @@ type statusCommand struct {
detailed bool
json bool
template string
output string
dot bool
old bool
missing bool
Expand Down Expand Up @@ -151,6 +152,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 @@ -172,6 +202,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 @@ -184,6 +220,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 @@ -247,6 +284,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some filtering that needs to be done here. Simply calling Flatten will get you the imports of ALL the packages in a ptree. We need only the subset of imports that come from the packages we're actually importing from a project.

The lock should contain the imported package list for each project, though, so this shouldn't be that bad to do.

Copy link
Contributor Author

@Rhymond Rhymond Mar 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdboyer we are doing some sort of "filtering".
https://github.com/golang/dep/pull/271/files#diff-0d223b461ecb49bf384a2c75a3893558R51
When we are generating Graphviz nodes we are only working with packages which are presented in the lock in this way we're excluding other packages.

}

// 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];
}