modgraphviz: colour nodes based on whether they were picked by MVS or not

Green nodes are those picked by MVS, whilst gray nodes are those not picked.

Example output on the tools repo: https://user-images.githubusercontent.com/3584893/61019393-45338f80-a357-11e9-8d28-b06c41357c0f.png

This CL includes a refactor of the input into a graph: this is one part for
organization, and another part in preparation for cycle detection.

Change-Id: I2ff717642480270659640f098cdf509d479d3ca3
Reviewed-on: https://go-review.googlesource.com/c/exp/+/185657
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
This commit is contained in:
Jean de Klerk 2019-07-10 21:09:56 -06:00 коммит произвёл Jay Conrod
Родитель fd42eb6b33
Коммит cfdd5522f6
4 изменённых файлов: 206 добавлений и 29 удалений

Просмотреть файл

@ -14,6 +14,10 @@
// generated by “go mod graph” on standard input and writes DOT language
// on standard output.
//
// For each module, the node representing the greatest version (i.e., the
// version chosen by Go's minimal version selection algorithm) is colored green.
// Other nodes, which aren't in the final build list, are colored grey.
//
// See http://www.graphviz.org/doc/info/lang.html for details of the DOT language
// and http://www.graphviz.org/about/ for Graphviz itself.
//
@ -29,11 +33,19 @@ import (
"io"
"log"
"os"
"sort"
"strings"
"golang.org/x/mod/semver"
)
func usage() {
fmt.Fprintf(os.Stderr, "Usage: go mod graph | modgraphviz | dot -Tpng -o graph.png\n")
fmt.Fprintf(os.Stderr, `Usage: go mod graph | modgraphviz | dot -Tpng -o graph.png
For each module, the node representing the greatest version (i.e., the
version chosen by Go's minimal version selection algorithm) is colored green.
Other nodes, which aren't in the final build list, are colored grey.
`)
os.Exit(2)
}
@ -47,33 +59,108 @@ func main() {
usage()
}
graph, err := convert(os.Stdin)
if err != nil {
log.Fatal(err)
}
if _, err := os.Stdout.Write(graph); err != nil {
if err := modgraphviz(os.Stdin, os.Stdout); err != nil {
log.Fatal(err)
}
}
// convert reads “go mod graph” output from r
// and returns the equivalent DOT digraph.
func convert(r io.Reader) ([]byte, error) {
var buf bytes.Buffer
fmt.Fprintf(&buf, "digraph gomodgraph {\n")
func modgraphviz(in io.Reader, out io.Writer) error {
graph, err := convert(in)
if err != nil {
return err
}
fmt.Fprintf(out, "digraph gomodgraph {\n")
out.Write(graph.edgesAsDOT())
for _, n := range graph.mvsPicked {
fmt.Fprintf(out, "\t%q [style = filled, fillcolor = green]\n", n)
}
for _, n := range graph.mvsUnpicked {
fmt.Fprintf(out, "\t%q [style = filled, fillcolor = gray]\n", n)
}
fmt.Fprintf(out, "}\n")
return nil
}
type edge struct{ from, to string }
type graph struct {
edges []edge
mvsPicked []string
mvsUnpicked []string
}
// convert reads “go mod graph” output from r and returns a graph, recording
// MVS picked and unpicked nodes along the way.
func convert(r io.Reader) (*graph, error) {
scanner := bufio.NewScanner(r)
var g graph
seen := map[string]bool{}
mvsPicked := map[string]string{} // module name -> module version
for scanner.Scan() {
parts := strings.Fields(scanner.Text())
if len(parts) != 2 {
l := scanner.Text()
if l == "" {
continue
}
fmt.Fprintf(&buf, "\t%q -> %q\n", parts[0], parts[1])
parts := strings.Fields(l)
if len(parts) != 2 {
return nil, fmt.Errorf("expected 2 words in line, but got %d: %s", len(parts), l)
}
from := parts[0]
to := parts[1]
g.edges = append(g.edges, edge{from: from, to: to})
for _, node := range []string{from, to} {
if _, ok := seen[node]; ok {
// Skip over nodes we've already seen.
continue
}
seen[node] = true
var m, v string
if i := strings.IndexByte(node, '@'); i >= 0 {
m, v = node[:i], node[i+1:]
} else {
// Root node doesn't have a version.
continue
}
if maxV, ok := mvsPicked[m]; ok {
if semver.Compare(maxV, v) < 0 {
// This version is higher - replace it and consign the old
// max to the unpicked list.
g.mvsUnpicked = append(g.mvsUnpicked, m+"@"+maxV)
mvsPicked[m] = v
} else {
// Other version is higher - stick this version in the
// unpicked list.
g.mvsUnpicked = append(g.mvsUnpicked, node)
}
} else {
mvsPicked[m] = v
}
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
fmt.Fprintf(&buf, "}\n")
return buf.Bytes(), nil
for m, v := range mvsPicked {
g.mvsPicked = append(g.mvsPicked, m+"@"+v)
}
// Make this function deterministic.
sort.Strings(g.mvsPicked)
return &g, nil
}
// edgesAsDOT returns the edges in DOT notation.
func (g *graph) edgesAsDOT() []byte {
var buf bytes.Buffer
for _, e := range g.edges {
fmt.Fprintf(&buf, "\t%q -> %q\n", e.from, e.to)
}
return buf.Bytes()
}

Просмотреть файл

@ -6,25 +6,110 @@ package main
import (
"bytes"
"fmt"
"reflect"
"testing"
)
func TestRun(t *testing.T) {
out := &bytes.Buffer{}
in := bytes.NewBuffer([]byte(`
test.com/A test.com/B@v1.2.3
test.com/B test.com/C@v4.5.6
test.com/A@v1.0.0 test.com/B@v1.2.3
test.com/B@v1.0.0 test.com/C@v4.5.6
`))
graph, err := convert(in)
if err != nil {
if err := modgraphviz(in, out); err != nil {
t.Fatal(err)
}
want := `digraph gomodgraph {
"test.com/A" -> "test.com/B@v1.2.3"
"test.com/B" -> "test.com/C@v4.5.6"
gotGraph := string(out.Bytes())
wantGraph := `digraph gomodgraph {
"test.com/A@v1.0.0" -> "test.com/B@v1.2.3"
"test.com/B@v1.0.0" -> "test.com/C@v4.5.6"
"test.com/A@v1.0.0" [style = filled, fillcolor = green]
"test.com/B@v1.2.3" [style = filled, fillcolor = green]
"test.com/C@v4.5.6" [style = filled, fillcolor = green]
"test.com/B@v1.0.0" [style = filled, fillcolor = gray]
}
`
if string(graph) != want {
t.Fatalf("\ngot: %s\nwant: %s", string(graph), want)
if gotGraph != wantGraph {
t.Fatalf("\ngot: %s\nwant: %s", gotGraph, wantGraph)
}
}
func TestMVSPicking(t *testing.T) {
for _, tc := range []struct {
name string
in []string
wantPicked []string
wantUnpicked []string
}{
{
name: "single node",
in: []string{"foo@v0.0.1"},
wantPicked: []string{"foo@v0.0.1"},
wantUnpicked: nil,
},
{
name: "duplicate same node",
in: []string{"foo@v0.0.1", "foo@v0.0.1"},
wantPicked: []string{"foo@v0.0.1"},
wantUnpicked: nil,
},
{
name: "multiple semver - same major",
in: []string{"foo@v1.0.0", "foo@v1.3.7", "foo@v1.2.0", "foo@v1.0.1"},
wantPicked: []string{"foo@v1.3.7"},
wantUnpicked: []string{"foo@v1.0.0", "foo@v1.2.0", "foo@v1.0.1"},
},
{
name: "multiple semver - multiple major",
in: []string{"foo@v1.0.0", "foo@v1.3.7", "foo/v2@v2.2.0", "foo/v2@v2.0.1", "foo@v1.1.1"},
wantPicked: []string{"foo/v2@v2.2.0", "foo@v1.3.7"},
wantUnpicked: []string{"foo@v1.0.0", "foo/v2@v2.0.1", "foo@v1.1.1"},
},
{
name: "semver and pseudo version",
in: []string{"foo@v1.0.0", "foo@v1.3.7", "foo/v2@v2.2.0", "foo/v2@v2.0.1", "foo@v1.1.1", "foo@v0.0.0-20190311183353-d8887717615a"},
wantPicked: []string{"foo/v2@v2.2.0", "foo@v1.3.7"},
wantUnpicked: []string{"foo@v1.0.0", "foo/v2@v2.0.1", "foo@v1.1.1", "foo@v0.0.0-20190311183353-d8887717615a"},
},
{
name: "multiple pseudo version",
in: []string{
"foo@v0.0.0-20190311183353-d8887717615a",
"foo@v0.0.0-20190227222117-0694c2d4d067",
"foo@v0.0.0-20190312151545-0bb0c0a6e846",
},
wantPicked: []string{"foo@v0.0.0-20190312151545-0bb0c0a6e846"},
wantUnpicked: []string{
"foo@v0.0.0-20190227222117-0694c2d4d067",
"foo@v0.0.0-20190311183353-d8887717615a",
},
},
{
name: "semver and suffix",
in: []string{"foo@v1.0.0", "foo@v1.3.8-rc1", "foo@v1.3.7"},
wantPicked: []string{"foo@v1.3.8-rc1"},
wantUnpicked: []string{"foo@v1.0.0", "foo@v1.3.7"},
},
} {
t.Run(tc.name, func(t *testing.T) {
buf := bytes.Buffer{}
for _, node := range tc.in {
fmt.Fprintf(&buf, "A %s\n", node)
}
g, err := convert(&buf)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(g.mvsPicked, tc.wantPicked) {
t.Fatalf("picked: got %v, want %v", g.mvsPicked, tc.wantPicked)
}
if !reflect.DeepEqual(g.mvsUnpicked, tc.wantUnpicked) {
t.Fatalf("unpicked: got %v, want %v", g.mvsUnpicked, tc.wantUnpicked)
}
})
}
}

5
go.mod
Просмотреть файл

@ -1,11 +1,12 @@
module golang.org/x/exp
go 1.11
go 1.12
require (
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6
golang.org/x/sys v0.0.0-20190312061237-fead79001313
golang.org/x/mod v0.1.0
golang.org/x/sys v0.0.0-20190412213103-97732733099d
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846
)

8
go.sum
Просмотреть файл

@ -1,14 +1,18 @@
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6 h1:Tus/Y4w3V77xDsGwKUC8a/QrV7jScpU557J77lFffNs=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mod v0.1.0 h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846 h1:0oJP+9s5Z3MT6dym56c4f7nVeujVpL1QyD2Vp/bTql0=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=