diff --git a/Makefile b/Makefile index 5ae34f47b9..3ef8241658 100644 --- a/Makefile +++ b/Makefile @@ -151,6 +151,7 @@ mocks: $(MOCKS_DIR) $(mockgen_cmd) -source=types/router.go -package mocks -destination tests/mocks/types_router.go $(mockgen_cmd) -package mocks -destination tests/mocks/grpc_server.go github.com/gogo/protobuf/grpc Server $(mockgen_cmd) -package mocks -destination tests/mocks/tendermint_tendermint_libs_log_DB.go github.com/tendermint/tendermint/libs/log Logger + $(mockgen_cmd) -source=orm/model/ormtable/hooks.go -package ormmocks -destination orm/testing/ormmocks/hooks.go .PHONY: mocks $(MOCKS_DIR): diff --git a/orm/go.mod b/orm/go.mod index daccd89d90..471ff370f3 100644 --- a/orm/go.mod +++ b/orm/go.mod @@ -6,6 +6,7 @@ require ( github.com/cosmos/cosmos-proto v1.0.0-alpha7 github.com/cosmos/cosmos-sdk/api v0.1.0-alpha4 github.com/cosmos/cosmos-sdk/errors v1.0.0-beta.2 + github.com/golang/mock v1.6.0 github.com/iancoleman/strcase v0.2.0 github.com/stretchr/testify v1.7.0 github.com/tendermint/tm-db v0.6.6 diff --git a/orm/go.sum b/orm/go.sum index 3c381d297a..6d07a2b76d 100644 --- a/orm/go.sum +++ b/orm/go.sum @@ -63,7 +63,10 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -154,6 +157,7 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= @@ -167,6 +171,7 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -191,6 +196,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -228,6 +234,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/orm/model/ormdb/module_test.go b/orm/model/ormdb/module_test.go index 38f65cfdb3..3dffb088e3 100644 --- a/orm/model/ormdb/module_test.go +++ b/orm/model/ormdb/module_test.go @@ -8,6 +8,10 @@ import ( "strings" "testing" + "github.com/golang/mock/gomock" + + "github.com/cosmos/cosmos-sdk/orm/testing/ormmocks" + "google.golang.org/protobuf/reflect/protoreflect" "gotest.tools/v3/assert" "gotest.tools/v3/golden" @@ -34,6 +38,19 @@ type keeper struct { store testpb.BankStore } +func NewKeeper(db ormdb.ModuleDB) (Keeper, error) { + store, err := testpb.NewBankStore(db) + return keeper{store}, err +} + +type Keeper interface { + Send(ctx context.Context, from, to, denom string, amount uint64) error + Mint(ctx context.Context, acct, denom string, amount uint64) error + Burn(ctx context.Context, acct, denom string, amount uint64) error + Balance(ctx context.Context, acct, denom string) (uint64, error) + Supply(ctx context.Context, denom string) (uint64, error) +} + func (k keeper) Send(ctx context.Context, from, to, denom string, amount uint64) error { err := k.safeSubBalance(ctx, from, denom, amount) if err != nil { @@ -151,11 +168,6 @@ func (k keeper) safeSubBalance(ctx context.Context, acct, denom string, amount u } } -func newKeeper(db ormdb.ModuleDB) (keeper, error) { - store, err := testpb.NewBankStore(db) - return keeper{store}, err -} - func TestModuleDB(t *testing.T) { // create db & debug context db, err := ormdb.NewModuleDB(TestBankSchema, ormdb.ModuleDBOptions{}) @@ -171,7 +183,7 @@ func TestModuleDB(t *testing.T) { )) // create keeper - k, err := newKeeper(db) + k, err := NewKeeper(db) assert.NilError(t, err) // mint coins @@ -250,3 +262,39 @@ func TestModuleDB(t *testing.T) { assert.NilError(t, db.ImportJSON(ctx2, source)) testkv.AssertBackendsEqual(t, backend, backend2) } + +func TestHooks(t *testing.T) { + ctrl := gomock.NewController(t) + db, err := ormdb.NewModuleDB(TestBankSchema, ormdb.ModuleDBOptions{}) + assert.NilError(t, err) + hooks := ormmocks.NewMockHooks(ctrl) + ctx := ormtable.WrapContextDefault(ormtest.NewMemoryBackend().WithHooks(hooks)) + k, err := NewKeeper(db) + assert.NilError(t, err) + + denom := "foo" + acct1 := "bob" + acct2 := "sally" + + hooks.EXPECT().OnInsert(ormmocks.Eq(&testpb.Balance{Address: acct1, Denom: denom, Amount: 10})) + hooks.EXPECT().OnInsert(ormmocks.Eq(&testpb.Supply{Denom: denom, Amount: 10})) + assert.NilError(t, k.Mint(ctx, acct1, denom, 10)) + + hooks.EXPECT().OnUpdate( + ormmocks.Eq(&testpb.Balance{Address: acct1, Denom: denom, Amount: 10}), + ormmocks.Eq(&testpb.Balance{Address: acct1, Denom: denom, Amount: 5}), + ) + hooks.EXPECT().OnInsert( + ormmocks.Eq(&testpb.Balance{Address: acct2, Denom: denom, Amount: 5}), + ) + assert.NilError(t, k.Send(ctx, acct1, acct2, denom, 5)) + + hooks.EXPECT().OnUpdate( + ormmocks.Eq(&testpb.Supply{Denom: denom, Amount: 10}), + ormmocks.Eq(&testpb.Supply{Denom: denom, Amount: 5}), + ) + hooks.EXPECT().OnDelete( + ormmocks.Eq(&testpb.Balance{Address: acct1, Denom: denom, Amount: 5}), + ) + assert.NilError(t, k.Burn(ctx, acct1, denom, 5)) +} diff --git a/orm/model/ormtable/backend.go b/orm/model/ormtable/backend.go index d0f93ae4ad..7de1d8c0de 100644 --- a/orm/model/ormtable/backend.go +++ b/orm/model/ormtable/backend.go @@ -32,6 +32,9 @@ type Backend interface { // Hooks returns a Hooks instance or nil. Hooks() Hooks + + // WithHooks returns a copy of this backend with the provided hooks. + WithHooks(Hooks) Backend } // ReadBackendOptions defines options for creating a ReadBackend. @@ -82,6 +85,11 @@ type backend struct { hooks Hooks } +func (c backend) WithHooks(hooks Hooks) Backend { + c.hooks = hooks + return c +} + func (backend) private() {} func (c backend) CommitmentStoreReader() kv.ReadonlyStore { diff --git a/orm/testing/ormmocks/docs.go b/orm/testing/ormmocks/docs.go new file mode 100644 index 0000000000..751da39a2a --- /dev/null +++ b/orm/testing/ormmocks/docs.go @@ -0,0 +1,13 @@ +// Package ormmocks contains generated mocks for orm types that can be used +// in testing. Right now, this package only contains a mock for ormtable.Hooks +// as this useful way for unit testing using an in-memory database. Rather +// than attempting to mock a whole table or database instance, instead +// a mock Hook instance can be passed in to verify that the expected +// insert/update/delete operations are happening in the database. +// +// The Eq function gomock.Matcher that compares protobuf messages can +// be used in gomock EXPECT functions. +// +// See TestHooks in ormdb/module_test.go for examples of how to use +// mock Hooks in a real-world scenario. +package ormmocks diff --git a/orm/testing/ormmocks/hooks.go b/orm/testing/ormmocks/hooks.go new file mode 100644 index 0000000000..1a63fab52c --- /dev/null +++ b/orm/testing/ormmocks/hooks.go @@ -0,0 +1,77 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: orm/model/ormtable/hooks.go + +// Package ormmocks is a generated GoMock package. +package ormmocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + proto "google.golang.org/protobuf/proto" +) + +// MockHooks is a mock of Hooks interface. +type MockHooks struct { + ctrl *gomock.Controller + recorder *MockHooksMockRecorder +} + +// MockHooksMockRecorder is the mock recorder for MockHooks. +type MockHooksMockRecorder struct { + mock *MockHooks +} + +// NewMockHooks creates a new mock instance. +func NewMockHooks(ctrl *gomock.Controller) *MockHooks { + mock := &MockHooks{ctrl: ctrl} + mock.recorder = &MockHooksMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHooks) EXPECT() *MockHooksMockRecorder { + return m.recorder +} + +// OnDelete mocks base method. +func (m *MockHooks) OnDelete(arg0 proto.Message) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OnDelete", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// OnDelete indicates an expected call of OnDelete. +func (mr *MockHooksMockRecorder) OnDelete(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnDelete", reflect.TypeOf((*MockHooks)(nil).OnDelete), arg0) +} + +// OnInsert mocks base method. +func (m *MockHooks) OnInsert(arg0 proto.Message) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OnInsert", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// OnInsert indicates an expected call of OnInsert. +func (mr *MockHooksMockRecorder) OnInsert(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnInsert", reflect.TypeOf((*MockHooks)(nil).OnInsert), arg0) +} + +// OnUpdate mocks base method. +func (m *MockHooks) OnUpdate(existing, new proto.Message) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OnUpdate", existing, new) + ret0, _ := ret[0].(error) + return ret0 +} + +// OnUpdate indicates an expected call of OnUpdate. +func (mr *MockHooksMockRecorder) OnUpdate(existing, new interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnUpdate", reflect.TypeOf((*MockHooks)(nil).OnUpdate), existing, new) +} diff --git a/orm/testing/ormmocks/match.go b/orm/testing/ormmocks/match.go new file mode 100644 index 0000000000..8c1da2c022 --- /dev/null +++ b/orm/testing/ormmocks/match.go @@ -0,0 +1,29 @@ +package ormmocks + +import ( + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +// Code adapted from MIT-licensed https://github.com/budougumi0617/cmpmock/blob/master/diffmatcher.go + +// Eq returns a gomock.Matcher which uses go-cmp to compare protobuf messages. +func Eq(message proto.Message) gomock.Matcher { + return &protoEq{message: message} +} + +type protoEq struct { + message interface{} + diff string +} + +func (p protoEq) Matches(x interface{}) bool { + p.diff = cmp.Diff(x, p.message, protocmp.Transform()) + return len(p.diff) == 0 +} + +func (p protoEq) String() string { + return p.diff +}