cosmos-sdk/collections/colltest/codec.go
2025-10-10 08:52:49 -04:00

164 lines
5.1 KiB
Go

package colltest
import (
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/stretchr/testify/require"
"cosmossdk.io/collections"
"cosmossdk.io/collections/codec"
)
// TestKeyCodec asserts the correct behavior of a KeyCodec over the type T.
func TestKeyCodec[T any](t *testing.T, keyCodec codec.KeyCodec[T], key T) {
buffer := make([]byte, keyCodec.Size(key))
written, err := keyCodec.Encode(buffer, key)
require.NoError(t, err)
require.Equal(t, len(buffer), written, "the length of the buffer and the written bytes do not match")
read, decodedKey, err := keyCodec.Decode(buffer)
require.NoError(t, err)
require.Equal(t, len(buffer), read, "encoded key and read bytes must have same size")
require.Equal(t, key, decodedKey, "encoding and decoding produces different keys")
// test if terminality is correctly applied
pairCodec := collections.PairKeyCodec(keyCodec, collections.StringKey)
pairKey := collections.Join(key, "TEST")
buffer = make([]byte, pairCodec.Size(pairKey))
written, err = pairCodec.Encode(buffer, pairKey)
require.Equal(t, len(buffer), written, "the pair buffer should have been fully written")
require.NoError(t, err)
read, decodedPairKey, err := pairCodec.Decode(buffer)
require.NoError(t, err)
require.Equal(t, len(buffer), read, "encoded non terminal key and pair key read bytes must have same size")
require.Equal(t, pairKey, decodedPairKey, "encoding and decoding produces different keys with non terminal encoding")
// check JSON
keyJSON, err := keyCodec.EncodeJSON(key)
require.NoError(t, err)
decoded, err := keyCodec.DecodeJSON(keyJSON)
require.NoError(t, err)
require.Equal(t, key, decoded, "json encoding and decoding did not produce the same results")
// check type
require.NotEmpty(t, keyCodec.KeyType())
// check string
_ = keyCodec.Stringify(key)
}
// TestValueCodec asserts the correct behavior of a ValueCodec over the type T.
func TestValueCodec[T any](t *testing.T, encoder codec.ValueCodec[T], value T) {
encodedValue, err := encoder.Encode(value)
require.NoError(t, err)
decodedValue, err := encoder.Decode(encodedValue)
require.NoError(t, err)
require.Equal(t, value, decodedValue, "encoding and decoding produces different values")
encodedJSONValue, err := encoder.EncodeJSON(value)
require.NoError(t, err)
decodedJSONValue, err := encoder.DecodeJSON(encodedJSONValue)
require.NoError(t, err)
require.Equal(t, value, decodedJSONValue, "encoding and decoding in json format produces different values")
require.NotEmpty(t, encoder.ValueType())
_ = encoder.Stringify(value)
}
// MockValueCodec returns a mock of collections.ValueCodec for type T, it
// can be used for collections Values testing. It also supports interfaces.
// For the interfaces cases, in order for an interface to be decoded it must
// have been encoded first. Not concurrency safe.
// EG:
// Let's say the value is interface Animal
// if I want to decode Dog which implements Animal, then I need to first encode
// it in order to make the type known by the MockValueCodec.
func MockValueCodec[T any]() codec.ValueCodec[T] {
typ := reflect.ValueOf(new(T)).Elem().Type()
isInterface := typ.Kind() == reflect.Interface
return &mockValueCodec[T]{
isInterface: isInterface,
seenTypes: map[string]reflect.Type{},
valueType: fmt.Sprintf("%s.%s", typ.PkgPath(), typ.Name()),
}
}
type mockValueJSON struct {
TypeName string `json:"type_name"`
Value json.RawMessage `json:"value"`
}
type mockValueCodec[T any] struct {
isInterface bool
seenTypes map[string]reflect.Type
valueType string
}
func (m mockValueCodec[T]) Encode(value T) ([]byte, error) {
typeName := m.getTypeName(value)
valueBytes, err := json.Marshal(value)
if err != nil {
return nil, err
}
return json.Marshal(mockValueJSON{
TypeName: typeName,
Value: valueBytes,
})
}
func (m mockValueCodec[T]) Decode(b []byte) (t T, err error) {
wrappedValue := mockValueJSON{}
err = json.Unmarshal(b, &wrappedValue)
if err != nil {
return t, err
}
if !m.isInterface {
err = json.Unmarshal(wrappedValue.Value, &t)
return t, err
}
typ, exists := m.seenTypes[wrappedValue.TypeName]
if !exists {
return t, fmt.Errorf("unknown type %s, you're dealing with interfaces... in order to make the interface types known for the MockValueCodec, you need to first encode them", wrappedValue.TypeName)
}
newT := reflect.New(typ).Interface()
err = json.Unmarshal(wrappedValue.Value, newT)
if err != nil {
return t, err
}
iface := new(T)
reflect.ValueOf(iface).Elem().Set(reflect.ValueOf(newT).Elem())
return *iface, nil
}
func (m mockValueCodec[T]) EncodeJSON(value T) ([]byte, error) {
return m.Encode(value)
}
func (m mockValueCodec[T]) DecodeJSON(b []byte) (T, error) {
return m.Decode(b)
}
func (m mockValueCodec[T]) Stringify(value T) string {
return fmt.Sprintf("%#v", value)
}
func (m mockValueCodec[T]) ValueType() string {
return m.valueType
}
func (m mockValueCodec[T]) getTypeName(value T) string {
if !m.isInterface {
return m.valueType
}
typ := reflect.TypeOf(value)
name := fmt.Sprintf("%s.%s", typ.PkgPath(), typ.Name())
m.seenTypes[name] = typ
return name
}