feat(core): add coretest package (#20487)

Co-authored-by: unknown unknown <unknown@unknown>
This commit is contained in:
testinginprod
2024-06-05 22:11:33 +00:00
committed by GitHub
co-authored by unknown unknown
parent 80ba18e39e
commit fe22e9a5da
53 changed files with 641 additions and 375 deletions
+38
View File
@@ -0,0 +1,38 @@
<!--
Guiding Principles:
Changelogs are for humans, not machines.
There should be an entry for every single version.
The same types of changes should be grouped.
Versions and sections should be linkable.
The latest version comes first.
The release date of each version is displayed.
Mention whether you follow Semantic Versioning.
Usage:
Change log entries are to be added to the Unreleased section under the
appropriate stanza (see below). Each entry should ideally include a tag and
the Github issue reference in the following format:
* (<tag>) \#<issue-number> message
The issue numbers will later be link-ified during the release process so you do
not have to worry about including a link manually, but you can if you wish.
Types of changes (Stanzas):
"Features" for new features.
"Improvements" for changes in existing functionality.
"Deprecated" for soon-to-be removed features.
"Bug Fixes" for any bug fixes.
"Client Breaking" for breaking Protobuf, gRPC and REST routes used by end-users.
"CLI Breaking" for breaking CLI commands.
"API Breaking" for breaking exported APIs used by developers building on SDK.
Ref: https://keepachangelog.com/en/1.0.0/
-->
# Changelog
## [Unreleased]
+31
View File
@@ -0,0 +1,31 @@
package coretesting
import (
"context"
"cosmossdk.io/core/store"
)
type dummyKey struct{}
func Context() context.Context {
dummy := &dummyCtx{
stores: map[string]store.KVStore{},
}
ctx := context.WithValue(context.Background(), dummyKey{}, dummy)
return ctx
}
type dummyCtx struct {
stores map[string]store.KVStore
}
func unwrap(ctx context.Context) *dummyCtx {
dummy := ctx.Value(dummyKey{})
if dummy == nil {
panic("invalid ctx without dummy")
}
return dummy.(*dummyCtx)
}
+18
View File
@@ -0,0 +1,18 @@
module cosmossdk.io/core/testing
go 1.20
replace cosmossdk.io/core => ../
require (
cosmossdk.io/core v0.12.0
github.com/stretchr/testify v1.9.0
github.com/tidwall/btree v1.7.0
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+17
View File
@@ -0,0 +1,17 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+254
View File
@@ -0,0 +1,254 @@
package coretesting
import (
"bytes"
"errors"
"github.com/tidwall/btree"
"cosmossdk.io/core/store"
)
const (
// The approximate number of items and children per B-tree node. Tuned with benchmarks.
// copied from memdb.
bTreeDegree = 32
)
var errKeyEmpty = errors.New("key cannot be empty")
var _ store.KVStore = (*memDB)(nil)
// memDB a lightweight memory db
type memDB struct {
tree *btree.BTreeG[item]
}
// newMemDB creates a wrapper around `btree.BTreeG`.
func newMemDB() memDB {
return memDB{
tree: btree.NewBTreeGOptions(byKeys, btree.Options{
Degree: bTreeDegree,
NoLocks: true,
}),
}
}
// set adds a new key-value pair to the change set's tree.
func (bt memDB) set(key, value []byte) {
bt.tree.Set(newItem(key, value))
}
// get retrieves the value associated with the given key from the memDB's tree.
func (bt memDB) get(key []byte) (value []byte, found bool) {
it, found := bt.tree.Get(item{key: key})
return it.value, found
}
// delete removes the value associated with the given key from the change set.
// If the key does not exist in the change set, this method does nothing.
func (bt memDB) delete(key []byte) {
bt.tree.Delete(item{key: key})
}
// iterator returns a new iterator over the key-value pairs in the memDB
// that have keys greater than or equal to the start key and less than the end key.
func (bt memDB) iterator(start, end []byte) (store.Iterator, error) {
if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) {
return nil, errKeyEmpty
}
return newMemIterator(start, end, bt.tree, true), nil
}
// reverseIterator returns a new iterator that iterates over the key-value pairs in reverse order
// within the specified range [start, end) in the memDB's tree.
// If start or end is an empty byte slice, it returns an error indicating that the key is empty.
func (bt memDB) reverseIterator(start, end []byte) (store.Iterator, error) {
if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) {
return nil, errKeyEmpty
}
return newMemIterator(start, end, bt.tree, false), nil
}
// KV impl
func (bt memDB) Get(key []byte) ([]byte, error) {
value, _ := bt.get(key)
return value, nil
}
func (bt memDB) Has(key []byte) (bool, error) {
_, found := bt.get(key)
return found, nil
}
func (bt memDB) Set(key, value []byte) error {
bt.set(key, value)
return nil
}
func (bt memDB) Delete(key []byte) error {
bt.delete(key)
return nil
}
func (bt memDB) Iterator(start, end []byte) (store.Iterator, error) {
return bt.iterator(start, end)
}
func (bt memDB) ReverseIterator(start, end []byte) (store.Iterator, error) {
return bt.reverseIterator(start, end)
}
// item is a btree item with byte slices as keys and values
type item struct {
key []byte
value []byte
}
// byKeys compares the items by key
func byKeys(a, b item) bool {
return bytes.Compare(a.key, b.key) == -1
}
// newItem creates a new pair item.
func newItem(key, value []byte) item {
return item{key: key, value: value}
}
// memIterator iterates over iterKVCache items.
// if value is nil, means it was deleted.
// Implements Iterator.
type memIterator struct {
iter btree.IterG[item]
start []byte
end []byte
ascending bool
valid bool
}
// newMemIterator creates a new memory iterator for a given range of keys in a B-tree.
// The iterator starts at the specified start key and ends at the specified end key.
// The `tree` parameter is the B-tree to iterate over.
// The `ascending` parameter determines the direction of iteration.
// If `ascending` is true, the iterator will iterate in ascending order.
// If `ascending` is false, the iterator will iterate in descending order.
// The returned iterator is positioned at the first key that is greater than or equal to the start key.
// If the start key is nil, the iterator is positioned at the first key in the B-tree.
// If the end key is nil, the iterator is positioned at the last key in the B-tree.
// The iterator is inclusive of the start key and exclusive of the end key.
// The `valid` field of the iterator indicates whether the iterator is positioned at a valid key.
// The `start` and `end` fields of the iterator store the start and end keys respectively.
func newMemIterator(start, end []byte, tree *btree.BTreeG[item], ascending bool) *memIterator {
iter := tree.Iter()
var valid bool
if ascending {
if start != nil {
valid = iter.Seek(newItem(start, nil))
} else {
valid = iter.First()
}
} else {
if end != nil {
valid = iter.Seek(newItem(end, nil))
if !valid {
valid = iter.Last()
} else {
// end is exclusive
valid = iter.Prev()
}
} else {
valid = iter.Last()
}
}
mi := &memIterator{
iter: iter,
start: start,
end: end,
ascending: ascending,
valid: valid,
}
if mi.valid {
mi.valid = mi.keyInRange(mi.Key())
}
return mi
}
// Domain returns the start and end keys of the iterator's domain.
func (mi *memIterator) Domain() (start, end []byte) {
return mi.start, mi.end
}
// Close releases any resources held by the iterator.
func (mi *memIterator) Close() error {
mi.iter.Release()
return nil
}
var errInvalidIterator = errors.New("invalid iterator")
// Error returns the error state of the iterator.
// If the iterator is not valid, it returns the errInvalidIterator error.
// Otherwise, it returns nil.
func (mi *memIterator) Error() error {
if !mi.Valid() {
return errInvalidIterator
}
return nil
}
// Valid returns whether the iterator is currently pointing to a valid entry.
// It returns true if the iterator is valid, and false otherwise.
func (mi *memIterator) Valid() bool {
return mi.valid
}
// Next advances the iterator to the next key-value pair.
// If the iterator is in ascending order, it moves to the next key-value pair.
// If the iterator is in descending order, it moves to the previous key-value pair.
// It also checks if the new key-value pair is within the specified range.
func (mi *memIterator) Next() {
mi.assertValid()
if mi.ascending {
mi.valid = mi.iter.Next()
} else {
mi.valid = mi.iter.Prev()
}
if mi.valid {
mi.valid = mi.keyInRange(mi.Key())
}
}
func (mi *memIterator) keyInRange(key []byte) bool {
if mi.ascending && mi.end != nil && bytes.Compare(key, mi.end) >= 0 {
return false
}
if !mi.ascending && mi.start != nil && bytes.Compare(key, mi.start) < 0 {
return false
}
return true
}
// Key returns the key of the current item in the iterator.
func (mi *memIterator) Key() []byte {
return mi.iter.Item().key
}
// Value returns the value of the current item in the iterator.
func (mi *memIterator) Value() []byte {
return mi.iter.Item().value
}
// assertValid checks if the memIterator is in a valid state.
// If there is an error, it panics with the error message.
func (mi *memIterator) assertValid() {
if err := mi.Error(); err != nil {
panic(err)
}
}
+59
View File
@@ -0,0 +1,59 @@
package coretesting
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"cosmossdk.io/core/store"
)
func TestMemDB(t *testing.T) {
var db store.KVStore = newMemDB()
key, value := []byte("key"), []byte("value")
require.NoError(t, db.Set(key, value))
val, err := db.Get(key)
require.NoError(t, err)
require.Equal(t, value, val)
require.NoError(t, db.Delete(key))
has, err := db.Has(key)
require.NoError(t, err)
require.False(t, has)
// test iter
makeKey := func(i int) []byte {
return []byte(fmt.Sprintf("key_%d", i))
}
for i := 0; i < 10; i++ {
require.NoError(t, db.Set(makeKey(i), makeKey(i)))
}
iter, err := db.Iterator(nil, nil)
require.NoError(t, err)
key = iter.Key()
value = iter.Value()
require.Equal(t, makeKey(0), key)
require.Equal(t, makeKey(0), value)
require.NoError(t, iter.Error())
iter.Next()
key, value = iter.Key(), iter.Value()
require.Equal(t, makeKey(1), key)
require.Equal(t, makeKey(1), value)
require.NoError(t, iter.Close())
// test reverse iter
iter, err = db.ReverseIterator(nil, nil)
require.NoError(t, err)
key = iter.Key()
value = iter.Value()
require.Equal(t, makeKey(9), key)
require.Equal(t, makeKey(9), value)
require.NoError(t, iter.Error())
iter.Next()
key, value = iter.Key(), iter.Value()
require.Equal(t, makeKey(8), key)
require.Equal(t, makeKey(8), value)
require.NoError(t, iter.Close())
}
+35
View File
@@ -0,0 +1,35 @@
package coretesting
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestKVStoreService(t *testing.T) {
ctx := Context()
svc1 := KVStoreService(ctx, "bank")
// must panic
t.Run("must panic on invalid ctx", func(t *testing.T) {
require.Panics(t, func() {
svc1.OpenKVStore(context.Background())
})
})
t.Run("success", func(t *testing.T) {
kv := svc1.OpenKVStore(ctx)
require.NoError(t, kv.Set([]byte("key"), []byte("value")))
value, err := kv.Get([]byte("key"))
require.NoError(t, err)
require.Equal(t, []byte("value"), value)
})
t.Run("contains module name", func(t *testing.T) {
KVStoreService(ctx, "auth")
_, ok := unwrap(ctx).stores["auth"]
require.True(t, ok)
})
}
+16
View File
@@ -0,0 +1,16 @@
sonar.projectKey=cosmos-sdk-core-testing
sonar.organization=cosmos
sonar.projectName=Cosmos SDK - Core Testing
sonar.project.monorepo.enabled=true
sonar.sources=.
sonar.exclusions=**/*_test.go,**/*.pb.go,**/*.pulsar.go,**/*.pb.gw.go
sonar.coverage.exclusions=**/*_test.go,**/testutil/**,**/*.pb.go,**/*.pb.gw.go,**/*.pulsar.go,test_helpers.go,docs/**
sonar.tests=.
sonar.test.inclusions=**/*_test.go
sonar.go.coverage.reportPaths=coverage.out
sonar.sourceEncoding=UTF-8
sonar.scm.provider=git
sonar.scm.forceReloadAll=true
+27
View File
@@ -0,0 +1,27 @@
package coretesting
import (
"context"
"fmt"
"cosmossdk.io/core/store"
)
func KVStoreService(ctx context.Context, moduleName string) store.KVStoreService {
unwrap(ctx).stores[moduleName] = newMemDB()
return kvStoreService{
moduleName: moduleName,
}
}
type kvStoreService struct {
moduleName string
}
func (k kvStoreService) OpenKVStore(ctx context.Context) store.KVStore {
kv, ok := unwrap(ctx).stores[k.moduleName]
if !ok {
panic(fmt.Sprintf("KVStoreService %s not found", k.moduleName))
}
return kv
}