diff --git a/.github/labeler.yml b/.github/labeler.yml index 65310f6f15b..42d2a50eb9a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -52,3 +52,5 @@ - x/*/client/**/* "Type: ADR": - docs/architecture/**/* +"C:container": + - container/**/* diff --git a/Makefile b/Makefile index 5f2e27bae54..06d6d5cdbc3 100644 --- a/Makefile +++ b/Makefile @@ -238,11 +238,23 @@ check-test-unit-amino: ARGS=-tags='ledger test_ledger_mock test_amino norace' $(CHECK_TEST_TARGETS): EXTRA_ARGS=-run=none $(CHECK_TEST_TARGETS): run-tests +SUB_MODULES = $(shell find . -type f -name 'go.mod' -print0 | xargs -0 -n1 dirname | sort) +CURRENT_DIR = $(shell pwd) run-tests: ifneq (,$(shell which tparse 2>/dev/null)) - go test -mod=readonly -json $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES) | tparse + @echo "Starting unit tests"; \ + for module in $(SUB_MODULES); do \ + cd ${CURRENT_DIR}/$$module; \ + echo "Running unit tests for module $$module"; \ + go test -mod=readonly -json $(ARGS) $(TEST_PACKAGES) ./... | tparse; \ + done else - go test -mod=readonly $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES) + @echo "Starting unit tests"; \ + for module in $(SUB_MODULES); do \ + cd ${CURRENT_DIR}/$$module; \ + echo "Running unit tests for module $$module"; \ + go test -mod=readonly $(ARGS) $(TEST_PACKAGES) ./... ; \ + done endif .PHONY: run-tests test test-all $(TEST_TARGETS) diff --git a/container/constructor_info.go b/container/constructor_info.go new file mode 100644 index 00000000000..b3b5e7b5002 --- /dev/null +++ b/container/constructor_info.go @@ -0,0 +1,22 @@ +package container + +import "reflect" + +// ConstructorInfo defines a special constructor type that is defined by +// reflection. It should be passed as a value to the Provide function. +// Ex: +// option.Provide(ConstructorInfo{ ... }) +type ConstructorInfo struct { + // In defines the in parameter types to Fn. + In []reflect.Type + + // Out defines the out parameter types to Fn. + Out []reflect.Type + + // Fn defines the constructor function. + Fn func([]reflect.Value) []reflect.Value + + // Location defines the source code location to be used for this constructor + // in error messages. + Location Location +} diff --git a/container/container_test.go b/container/container_test.go new file mode 100644 index 00000000000..091ab3ab298 --- /dev/null +++ b/container/container_test.go @@ -0,0 +1,128 @@ +package container_test + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/container" +) + +type KVStoreKey struct { + name string +} + +type ModuleKey string + +type MsgClientA struct { + key ModuleKey +} + +type KeeperA struct { + key KVStoreKey +} + +type KeeperB struct { + key KVStoreKey + msgClientA MsgClientA +} + +type Handler struct { + Handle func() +} + +type Command struct { + Run func() +} + +func ProvideKVStoreKey(scope container.Scope) KVStoreKey { + return KVStoreKey{name: scope.Name()} +} + +func ProvideModuleKey(scope container.Scope) ModuleKey { + return ModuleKey(scope.Name()) +} + +func ProvideMsgClientA(_ container.Scope, key ModuleKey) MsgClientA { + return MsgClientA{key} +} + +type ModuleA struct{} + +func (ModuleA) Provide(key KVStoreKey) (KeeperA, Handler, Command) { + return KeeperA{key}, Handler{}, Command{} +} + +type ModuleB struct{} + +type BDependencies struct { + container.StructArgs + + Key KVStoreKey + A MsgClientA +} + +type BProvides struct { + KeeperB KeeperB + Handler Handler + Commands []Command +} + +func (ModuleB) Provide(dependencies BDependencies) BProvides { + return BProvides{ + KeeperB: KeeperB{ + key: dependencies.Key, + msgClientA: dependencies.A, + }, + Handler: Handler{}, + Commands: []Command{{}, {}}, + } +} + +func TestRun(t *testing.T) { + t.Skip("Expecting this test to fail for now") + require.NoError(t, + container.Run( + func(handlers map[container.Scope]Handler, commands []Command, a KeeperA, b KeeperB) { + // TODO: + // require one Handler for module a and a scopes + // require 3 commands + // require KeeperA have store key a + // require KeeperB have store key b and MsgClientA + }), + container.AutoGroupTypes(reflect.TypeOf(Command{})), + container.OnePerScopeTypes(reflect.TypeOf(Handler{})), + container.Provide( + ProvideKVStoreKey, + ProvideModuleKey, + ProvideMsgClientA, + ), + container.ProvideWithScope(container.NewScope("a"), wrapProvideMethod(ModuleA{})), + container.ProvideWithScope(container.NewScope("b"), wrapProvideMethod(ModuleB{})), + ) +} + +func wrapProvideMethod(module interface{}) container.ConstructorInfo { + method := reflect.TypeOf(module).Method(0) + methodTy := method.Type + var in []reflect.Type + var out []reflect.Type + + for i := 1; i < methodTy.NumIn(); i++ { + in = append(in, methodTy.In(i)) + } + for i := 0; i < methodTy.NumOut(); i++ { + out = append(out, methodTy.Out(i)) + } + + return container.ConstructorInfo{ + In: in, + Out: out, + Fn: func(values []reflect.Value) []reflect.Value { + values = append([]reflect.Value{reflect.ValueOf(module)}, values...) + return method.Func.Call(values) + }, + Location: container.LocationFromPC(method.Func.Pointer()), + } +} diff --git a/container/go.mod b/container/go.mod new file mode 100644 index 00000000000..f38fd6433f4 --- /dev/null +++ b/container/go.mod @@ -0,0 +1,5 @@ +module github.com/cosmos/cosmos-sdk/container + +go 1.16 + +require github.com/stretchr/testify v1.7.0 diff --git a/container/go.sum b/container/go.sum new file mode 100644 index 00000000000..acb88a48f68 --- /dev/null +++ b/container/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/container/location.go b/container/location.go new file mode 100644 index 00000000000..7b3d42dac11 --- /dev/null +++ b/container/location.go @@ -0,0 +1,19 @@ +package container + +import ( + "fmt" +) + +// Location describes the source code location of a dependency injection +// constructor. +type Location interface { + isLocation() + fmt.Stringer + fmt.Formatter +} + +// LocationFromPC builds a Location from a function program counter location, +// such as that returned by reflect.Value.Pointer() or runtime.Caller(). +func LocationFromPC(pc uintptr) Location { + panic("TODO") +} diff --git a/container/option.go b/container/option.go new file mode 100644 index 00000000000..e1388728643 --- /dev/null +++ b/container/option.go @@ -0,0 +1,50 @@ +package container + +import "reflect" + +// Option is a functional option for a container. +type Option interface { + isOption() +} + +// Provide creates a container option which registers the provided dependency +// injection constructors. Each constructor will be called at most once with the +// exception of scoped constructors which are called at most once per scope +// (see Scope). +func Provide(constructors ...interface{}) Option { + panic("TODO") +} + +// ProvideWithScope creates a container option which registers the provided dependency +// injection constructors that are to be run in the provided scope. Each constructor +// will be called at most once. +func ProvideWithScope(scope Scope, constructors ...interface{}) Option { + panic("TODO") +} + +// AutoGroupTypes creates an option which registers the provided types as types which +// will automatically get grouped together. For a given type T, T and []T can +// be declared as output parameters for constructors as many times within the container +// as desired. All of the provided values for T can be retrieved by declaring an +// []T input parameter. +func AutoGroupTypes(types ...reflect.Type) Option { + panic("TODO") +} + +// OnePerScopeTypes creates an option which registers the provided types as types which +// can have up to one value per scope. All of the values for a one-per-scope type T +// and their respective scopes, can be retrieved by declaring an input parameter map[Scope]T. +func OnePerScopeTypes(types ...reflect.Type) Option { + panic("TODO") +} + +// Error creates an option which causes the dependency injection container to +// fail immediately. +func Error(err error) Option { + panic("TODO") +} + +// Options creates an option which bundles together other options. +func Options(opts ...Option) Option { + panic("TODO") +} diff --git a/container/run.go b/container/run.go new file mode 100644 index 00000000000..9c410b76a24 --- /dev/null +++ b/container/run.go @@ -0,0 +1,14 @@ +package container + +import "fmt" + +// Run runs the provided invoker function with values provided by the provided +// options. It is the single entry point for building and running a dependency +// injection container. Invoker should be a function taking one or more +// dependencies from the container, optionally returning an error. +// +// Ex: +// Run(func (x int) error { println(x) }, Provide(func() int { return 1 })) +func Run(invoker interface{}, opts ...Option) error { + return fmt.Errorf("not implemented") +} diff --git a/container/scope.go b/container/scope.go new file mode 100644 index 00000000000..f854201ef61 --- /dev/null +++ b/container/scope.go @@ -0,0 +1,33 @@ +package container + +// Scope is a special type used to define a provider scope. +// +// Special scoped constructors can be used with Provide by declaring a +// constructor with its first input parameter of type Scope. These constructors +// should construct an unique value for each dependency based on scope and will +// be called at most once per scope. +// +// Constructors passed to ProvideWithScope can also declare an input parameter +// of type Scope to retrieve their scope. +type Scope interface { + isScope() + + // Name returns the name of the scope which is unique within a container. + Name() string +} + +// NewScope creates a new scope with the provided name. Only one scope with a +// given name can be created per container. +func NewScope(name string) Scope { + return &scope{name: name} +} + +type scope struct { + name string +} + +func (s *scope) isScope() {} + +func (s *scope) Name() string { + return s.name +} diff --git a/container/struct_args.go b/container/struct_args.go new file mode 100644 index 00000000000..8c1ae72e827 --- /dev/null +++ b/container/struct_args.go @@ -0,0 +1,11 @@ +package container + +// StructArgs is a type which can be embedded in another struct to alert the +// container that the fields of the struct are dependency inputs/outputs. That +// is, the container will not look to resolve a value with StructArgs embedded +// directly, but will instead use the struct's fields to resolve or populate +// dependencies. Types with embedded StructArgs can be used in both the input +// and output parameter positions. +type StructArgs struct{} + +func (StructArgs) isStructArgs() {}