Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(container)!: remove dependency on C graphviz #11934

Merged
merged 23 commits into from
May 12, 2022
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
4 changes: 4 additions & 0 deletions container/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
update-testdata-examples:
go test . -test.update-golden
dot -Tsvg testdata/example.dot > testdata/example.svg
dot -Tsvg testdata/example_error.dot > testdata/example_error.svg
34 changes: 34 additions & 0 deletions container/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Cosmos SDK Dependency Injection `container` Module

## Overview

TODO

## Usage

TODO
Comment on lines +5 to +9
Copy link
Contributor

Choose a reason for hiding this comment

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

We should ideally fill these out, albeit it can be concise for now.

Copy link
Member Author

Choose a reason for hiding this comment

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

I added an issue for this: #11944


## Debugging

Issues with resolving dependencies in the container can be done with logs
and [Graphviz](https://graphviz.org) renderings of the container tree. By default, whenever there is an error, logs will
be printed to stderr and a rendering of the dependency graph in Graphviz DOT format will be saved to
`debug_container.dot`.

Here is an example Graphviz rendering of a successful build of a dependency graph:
![Graphviz Example](./testdata/example.svg)

Rectangles represent functions, ovals represent types, rounded rectangles represent modules and the single hexagon
represents the function which called `Build`. Black-colored shapes mark functions and types that were called/resolved
without an error. Gray-colored nodes mark functions and types that could have been called/resolved in the container but
were left unused.

Here is an example Graphviz rendering of a dependency graph build which failed:
![Graphviz Error Example](./testdata/example_error.svg)

Graphviz DOT files can be converted into SVG's for viewing in a web browser using the `dot` command-line tool, ex:
```
> dot -Tsvg debug_container.dot > debug_container.svg
```

Many other tools including some IDEs support working with DOT files.
1 change: 1 addition & 0 deletions container/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func build(loc Location, debugOpt DebugOption, option Option, outputs ...interfa

err = doBuild(cfg, loc, debugOpt, option, outputs...)
if err != nil {
cfg.logf("Error: %v", err)
if cfg.onError != nil {
err2 := cfg.onError.applyConfig(cfg)
if err2 != nil {
Expand Down
70 changes: 22 additions & 48 deletions container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import (
"fmt"
"reflect"

"github.com/goccy/go-graphviz/cgraph"
"github.com/pkg/errors"

"github.com/cosmos/cosmos-sdk/container/internal/graphviz"
)

type container struct {
Expand Down Expand Up @@ -38,10 +39,8 @@ func newContainer(cfg *debugConfig) *container {

func (c *container) call(provider *ProviderDescriptor, moduleKey *moduleKey) ([]reflect.Value, error) {
loc := provider.Location
graphNode, err := c.locationGraphNode(loc, moduleKey)
if err != nil {
return nil, err
}
graphNode := c.locationGraphNode(loc, moduleKey)

markGraphNodeAsFailed(graphNode)

if c.callerMap[loc] {
Expand Down Expand Up @@ -87,17 +86,13 @@ func (c *container) getResolver(typ reflect.Type) (resolver, error) {
elemType = elemType.Elem()
}

var typeGraphNode *cgraph.Node
var err error
var typeGraphNode *graphviz.Node

if isAutoGroupType(elemType) {
c.logf("Registering resolver for auto-group type %v", elemType)
sliceType := reflect.SliceOf(elemType)

typeGraphNode, err = c.typeGraphNode(sliceType)
if err != nil {
return nil, err
}
typeGraphNode = c.typeGraphNode(sliceType)
typeGraphNode.SetComment("auto-group")

r := &groupResolver{
Expand All @@ -112,10 +107,7 @@ func (c *container) getResolver(typ reflect.Type) (resolver, error) {
c.logf("Registering resolver for one-per-module type %v", elemType)
mapType := reflect.MapOf(stringType, elemType)

typeGraphNode, err = c.typeGraphNode(mapType)
if err != nil {
return nil, err
}
typeGraphNode = c.typeGraphNode(mapType)
typeGraphNode.SetComment("one-per-module")

r := &onePerModuleResolver{
Expand All @@ -136,11 +128,7 @@ func (c *container) getResolver(typ reflect.Type) (resolver, error) {
var stringType = reflect.TypeOf("")

func (c *container) addNode(provider *ProviderDescriptor, key *moduleKey) (interface{}, error) {
providerGraphNode, err := c.locationGraphNode(provider.Location, key)
if err != nil {
return nil, err
}

providerGraphNode := c.locationGraphNode(provider.Location, key)
hasModuleKeyParam := false
hasOwnModuleKeyParam := false
for _, in := range provider.Inputs {
Expand All @@ -164,11 +152,11 @@ func (c *container) addNode(provider *ProviderDescriptor, key *moduleKey) (inter
return nil, err
}

var typeGraphNode *cgraph.Node
var typeGraphNode *graphviz.Node
if vr != nil {
typeGraphNode = vr.typeGraphNode()
} else {
typeGraphNode, err = c.typeGraphNode(typ)
typeGraphNode = c.typeGraphNode(typ)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -215,11 +203,7 @@ func (c *container) addNode(provider *ProviderDescriptor, key *moduleKey) (inter
} else {
c.logf("Registering resolver for simple type %v", typ)

typeGraphNode, err := c.typeGraphNode(typ)
if err != nil {
return nil, err
}

typeGraphNode := c.typeGraphNode(typ)
vr = &simpleResolver{
node: sp,
typ: typ,
Expand Down Expand Up @@ -260,11 +244,7 @@ func (c *container) addNode(provider *ProviderDescriptor, key *moduleKey) (inter
typ, provider.Location, existing.describeLocation())
}

typeGraphNode, err := c.typeGraphNode(typ)
if err != nil {
return reflect.Value{}, err
}

typeGraphNode := c.typeGraphNode(typ)
c.resolvers[typ] = &moduleDepResolver{
typ: typ,
idxInValues: i,
Expand All @@ -282,17 +262,9 @@ func (c *container) addNode(provider *ProviderDescriptor, key *moduleKey) (inter

func (c *container) supply(value reflect.Value, location Location) error {
typ := value.Type()
locGrapNode, err := c.locationGraphNode(location, nil)
if err != nil {
return err
}
locGrapNode := c.locationGraphNode(location, nil)
markGraphNodeAsUsed(locGrapNode)

typeGraphNode, err := c.typeGraphNode(typ)
if err != nil {
return err
}

typeGraphNode := c.typeGraphNode(typ)
c.addGraphEdge(locGrapNode, typeGraphNode)

if existing, ok := c.resolvers[typ]; ok {
Expand All @@ -312,10 +284,7 @@ func (c *container) supply(value reflect.Value, location Location) error {
func (c *container) resolve(in ProviderInput, moduleKey *moduleKey, caller Location) (reflect.Value, error) {
c.resolveStack = append(c.resolveStack, resolveFrame{loc: caller, typ: in.Type})

typeGraphNode, err := c.typeGraphNode(in.Type)
if err != nil {
return reflect.Value{}, err
}
typeGraphNode := c.typeGraphNode(in.Type)

if in.Type == moduleKeyType {
if moduleKey == nil {
Expand Down Expand Up @@ -392,6 +361,8 @@ func (c *container) build(loc Location, outputs ...interface{}) error {
},
Location: loc,
}
callerGraphNode := c.locationGraphNode(loc, nil)
callerGraphNode.SetShape("hexagon")

desc, err := expandStructArgsProvider(desc)
if err != nil {
Expand Down Expand Up @@ -443,10 +414,13 @@ func (c container) formatResolveStack() string {
return buf.String()
}

func markGraphNodeAsUsed(node *cgraph.Node) {
func markGraphNodeAsUsed(node *graphviz.Node) {
node.SetColor("black")
node.SetPenWidth("1.5")
node.SetFontColor("black")
}

func markGraphNodeAsFailed(node *cgraph.Node) {
func markGraphNodeAsFailed(node *graphviz.Node) {
node.SetColor("red")
node.SetFontColor("red")
}
40 changes: 31 additions & 9 deletions container/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/stretchr/testify/require"
"gotest.tools/v3/golden"

"github.com/cosmos/cosmos-sdk/container"
)
Expand Down Expand Up @@ -81,6 +82,13 @@ func (ModuleB) Provide(dependencies BDependencies) (BProvides, Handler, error) {
}, Handler{}, nil
}

var scenarioConfig = container.Options(
container.Provide(ProvideMsgClientA),
container.ProvideInModule("runtime", ProvideKVStoreKey),
container.ProvideInModule("a", wrapMethod0(ModuleA{})),
container.ProvideInModule("b", wrapMethod0(ModuleB{})),
)

func TestScenario(t *testing.T) {
var (
handlers map[string]Handler
Expand All @@ -90,12 +98,7 @@ func TestScenario(t *testing.T) {
)
require.NoError(t,
container.Build(
container.Options(
container.Provide(ProvideMsgClientA),
container.ProvideInModule("runtime", ProvideKVStoreKey),
container.ProvideInModule("a", wrapMethod0(ModuleA{})),
container.ProvideInModule("b", wrapMethod0(ModuleB{})),
),
scenarioConfig,
&handlers,
&commands,
&a,
Expand Down Expand Up @@ -539,7 +542,7 @@ func TestStructArgs(t *testing.T) {
))
}

func TestLogging(t *testing.T) {
func TestDebugOptions(t *testing.T) {
var logOut string
var dotGraph string

Expand All @@ -563,7 +566,7 @@ func TestLogging(t *testing.T) {
dotGraph = g
}),
container.LogVisualizer(),
container.FileVisualizer(graphfile.Name(), "svg"),
container.FileVisualizer(graphfile.Name()),
container.StdoutLogger(),
),
container.Options(),
Expand All @@ -578,7 +581,26 @@ func TestLogging(t *testing.T) {

graphfileContents, err := os.ReadFile(graphfile.Name())
require.NoError(t, err)
require.Contains(t, string(graphfileContents), "<svg")
require.Contains(t, string(graphfileContents), "digraph")
}

func TestGraphAndLogOutput(t *testing.T) {
var graphOut string
var b KeeperB
debugOpts := container.DebugOptions(
container.Visualizer(func(dotGraph string) {
graphOut = dotGraph
}))
require.NoError(t, container.BuildDebug(debugOpts, scenarioConfig, &b))
golden.Assert(t, graphOut, "example.dot")

badConfig := container.Options(
container.ProvideInModule("runtime", ProvideKVStoreKey),
container.ProvideInModule("a", wrapMethod0(ModuleA{})),
container.ProvideInModule("b", wrapMethod0(ModuleB{})),
)
require.Error(t, container.BuildDebug(debugOpts, badConfig, &b))
golden.Assert(t, graphOut, "example_error.dot")
}

func TestConditionalDebugging(t *testing.T) {
Expand Down
Loading