diff --git a/private/mud/dot.go b/private/mud/dot.go new file mode 100644 index 000000000000..c229ed7e95de --- /dev/null +++ b/private/mud/dot.go @@ -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) +} diff --git a/private/mud/dot_test.go b/private/mud/dot_test.go new file mode 100644 index 000000000000..02685b943d2b --- /dev/null +++ b/private/mud/dot_test.go @@ -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) +}