whatcanGOwrong
This commit is contained in:
+166
@@ -0,0 +1,166 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Modgraphviz converts “go mod graph” output into Graphviz's DOT language,
|
||||
// for use with Graphviz visualization and analysis tools like dot, dotty, and sccmap.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go mod graph | modgraphviz > graph.dot
|
||||
// go mod graph | modgraphviz | dot -Tpng -o graph.png
|
||||
//
|
||||
// Modgraphviz takes no options or arguments; it reads a graph in the format
|
||||
// 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.
|
||||
//
|
||||
// See also golang.org/x/tools/cmd/digraph for general queries and analysis
|
||||
// of “go mod graph” output.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"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
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("modgraphviz: ")
|
||||
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if flag.NArg() != 0 {
|
||||
usage()
|
||||
}
|
||||
|
||||
if err := modgraphviz(os.Stdin, os.Stdout); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func modgraphviz(in io.Reader, out io.Writer) error {
|
||||
graph, err := convert(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "digraph gomodgraph {\n")
|
||||
fmt.Fprintf(out, "\tnode [ shape=rectangle fontsize=12 ]\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() {
|
||||
l := scanner.Text()
|
||||
if l == "" {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// Copyright 2019 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"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
in := bytes.NewBuffer([]byte(`
|
||||
test.com/A@v1.0.0 test.com/B@v1.2.3
|
||||
test.com/B@v1.0.0 test.com/C@v4.5.6
|
||||
`))
|
||||
if err := modgraphviz(in, out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotGraph := string(out.Bytes())
|
||||
wantGraph := `digraph gomodgraph {
|
||||
node [ shape=rectangle fontsize=12 ]
|
||||
"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 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user