Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
private/mud: helpers to print dependency graph with dot
Change-Id: Ieb49a5c71d680ab03b12c7e0cf38ebbe3badba5e
- Loading branch information
Showing
2 changed files
with
146 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
// Copyright (C) 2024 Storj Labs, Inc. | ||
// See LICENSE for copying information. | ||
|
||
package mud | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"reflect" | ||
"strings" | ||
|
||
"github.com/zeebo/errs" | ||
) | ||
|
||
// DotAll generates graph report of the modules in dot format. | ||
func DotAll(w io.Writer, ball *Ball) (err error) { | ||
return Dot(w, ball.registry) | ||
} | ||
|
||
// Dot generates graph report of the modules in dot format, but only the selected components are included. | ||
func Dot(w io.Writer, components []*Component) (err error) { | ||
p := func(args ...any) { | ||
if err != nil { | ||
return | ||
} | ||
_, err = fmt.Fprint(w, args...) | ||
} | ||
pf := func(format string, args ...any) { | ||
if err != nil { | ||
return | ||
} | ||
_, err = fmt.Fprintf(w, format, args...) | ||
} | ||
|
||
p("digraph G {\n") | ||
p("\tnode [style=filled, shape=box, fillcolor=white];\n") | ||
defer p("}\n") | ||
|
||
annotationStr := func(c *Component) string { | ||
annotations := []string{} | ||
for _, tag := range c.tags { | ||
annotations = append(annotations, fmt.Sprintf("%s", tag)) | ||
} | ||
|
||
annotationStr := strings.Join(annotations, "\n") | ||
if len(annotationStr) > 0 { | ||
annotationStr = "\n" + annotationStr | ||
} | ||
return annotationStr | ||
} | ||
|
||
covered := map[reflect.Type]struct{}{} | ||
for _, component := range components { | ||
componentID := typeLabel(component.target) | ||
|
||
entries := []string{"label=\"" + component.Name() + annotationStr(component) + "\""} | ||
if component.instance == nil { | ||
entries = append(entries, "color=darkgray", "fontcolor=darkgray") | ||
} | ||
if component.run != nil && !component.run.started.IsZero() && component.run.finished.IsZero() { | ||
entries = append(entries, "fillcolor=green") | ||
} | ||
|
||
entries = append(entries, "URL=\"./"+strings.ReplaceAll(componentID, "/", "_")+"\"") | ||
|
||
pf("%q [%v];\n", componentID, strings.Join(entries, " ")) | ||
|
||
covered[component.target] = struct{}{} | ||
} | ||
|
||
for _, component := range components { | ||
for _, dep := range component.requirements { | ||
if _, found := covered[dep]; !found { | ||
continue | ||
} | ||
componentID := typeLabel(component.target) | ||
pf("%q -> %q;\n", componentID, typeLabel(dep)) | ||
} | ||
|
||
} | ||
return err | ||
|
||
} | ||
|
||
// GenerateComponentsGraph generates dot and svg file including the selected components. | ||
func GenerateComponentsGraph(fileprefix string, components []*Component) error { | ||
var b bytes.Buffer | ||
if err := Dot(&b, components); err != nil { | ||
_, err = fmt.Fprintf(os.Stderr, "fail: %v\n", err) | ||
if err != nil { | ||
return errs.Wrap(err) | ||
} | ||
} else { | ||
err = os.WriteFile(fileprefix+".dot", b.Bytes(), 0644) | ||
if err != nil { | ||
return errs.Wrap(err) | ||
} | ||
output, err := exec.Command("dot", "-Tsvg", fileprefix+".dot", "-o", fileprefix+".svg").CombinedOutput() | ||
if err != nil { | ||
return errs.New("Execution of dot is failed with %s, %v", output, err) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// MustGenerateGraph generates dot and svg files from components selected by the selector. | ||
func MustGenerateGraph(ball *Ball, fileprefix string, selector ComponentSelector) { | ||
var components []*Component | ||
for _, c := range ball.registry { | ||
if selector(c) { | ||
components = append(components, c) | ||
} | ||
} | ||
err := GenerateComponentsGraph(fileprefix, components) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
func typeLabel(t reflect.Type) string { | ||
return fullyQualifiedTypeName(t) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Copyright (C) 2024 Storj Labs, Inc. | ||
// See LICENSE for copying information. | ||
|
||
package mud | ||
|
||
import ( | ||
"path/filepath" | ||
"testing" | ||
) | ||
|
||
func TestDot(t *testing.T) { | ||
t.Skip("This test required dot executable") | ||
dir := t.TempDir() | ||
ball := NewBall() | ||
Provide[DB](ball, NewDB) | ||
Provide[Service1](ball, NewService1) | ||
Provide[Service2](ball, NewService2) | ||
|
||
// We don't really assert the results, as it may be changed, but it should be executed. | ||
MustGenerateGraph(ball, filepath.Join(dir, "graph"), All) | ||
} |