Noticed in an audit that the differeent value renderers perform an expensive and unnecessary string->byteslice in cases where the output write implements io.StringWriter. This change instead invokes io.WriteString(w, formatted) instead of: w.Write([]byte(formatted)) and added benchmarks that show an improvement from just the 1 line change: ```shell $ benchstat before.txt after.txt name old time/op new time/op delta IntValueRendererFormat-8 4.13µs ± 3% 3.95µs ± 6% -4.55% (p=0.000 n=15+14) BytesValueRendererFormat-8 5.22ms ± 3% 4.77ms ± 5% -8.60% (p=0.000 n=15+14) name old alloc/op new alloc/op delta IntValueRendererFormat-8 3.64kB ± 0% 3.31kB ± 0% -9.01% (p=0.000 n=15+15) BytesValueRendererFormat-8 12.6MB ± 0% 8.4MB ± 0% -33.22% (p=0.000 n=15+15) name old allocs/op new allocs/op delta IntValueRendererFormat-8 76.0 ± 0% 67.0 ± 0% -11.84% (p=0.000 n=15+15) BytesValueRendererFormat-8 27.0 ± 0% 18.0 ± 0% -33.33% (p=0.000 n=15+15) ``` While here, implemented negative sign preservation because previously the code wasn't tested for negative values so passing in negative values such as: "-10000000.11" would produce: "10'000'000.11" instead of the proper value with the negative sign preserved: "-10'000'000.11" Fixes #12810 Fixes #12812
152 lines
4.1 KiB
Go
152 lines
4.1 KiB
Go
package valuerenderer_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/reflect/protoreflect"
|
|
|
|
"cosmossdk.io/math"
|
|
"cosmossdk.io/tx/textual/internal/testpb"
|
|
"cosmossdk.io/tx/textual/valuerenderer"
|
|
)
|
|
|
|
func TestFormatInteger(t *testing.T) {
|
|
type integerTest []string
|
|
var testcases []integerTest
|
|
raw, err := ioutil.ReadFile("../internal/testdata/integers.json")
|
|
require.NoError(t, err)
|
|
err = json.Unmarshal(raw, &testcases)
|
|
require.NoError(t, err)
|
|
|
|
for _, tc := range testcases {
|
|
// Parse test case strings as protobuf uint64
|
|
i, err := strconv.ParseUint(tc[0], 10, 64)
|
|
if err == nil {
|
|
r, err := valueRendererOf(i)
|
|
require.NoError(t, err)
|
|
b := new(strings.Builder)
|
|
err = r.Format(context.Background(), protoreflect.ValueOf(i), b)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc[1], b.String())
|
|
}
|
|
|
|
// Parse test case strings as protobuf uint32
|
|
i, err = strconv.ParseUint(tc[0], 10, 32)
|
|
if err == nil {
|
|
r, err := valueRendererOf(i)
|
|
require.NoError(t, err)
|
|
b := new(strings.Builder)
|
|
err = r.Format(context.Background(), protoreflect.ValueOf(i), b)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc[1], b.String())
|
|
}
|
|
|
|
// Parse test case strings as sdk.Ints
|
|
sdkInt, ok := math.NewIntFromString(tc[0])
|
|
if ok {
|
|
r, err := valueRendererOf(sdkInt)
|
|
require.NoError(t, err)
|
|
b := new(strings.Builder)
|
|
err = r.Format(context.Background(), protoreflect.ValueOf(tc[0]), b)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc[1], b.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatDecimal(t *testing.T) {
|
|
type decimalTest []string
|
|
var testcases []decimalTest
|
|
raw, err := ioutil.ReadFile("../internal/testdata/decimals.json")
|
|
require.NoError(t, err)
|
|
err = json.Unmarshal(raw, &testcases)
|
|
require.NoError(t, err)
|
|
|
|
for _, tc := range testcases {
|
|
tc := tc
|
|
t.Run(tc[0], func(t *testing.T) {
|
|
d, err := math.LegacyNewDecFromStr(tc[0])
|
|
require.NoError(t, err)
|
|
r, err := valueRendererOf(d)
|
|
require.NoError(t, err)
|
|
b := new(strings.Builder)
|
|
err = r.Format(context.Background(), protoreflect.ValueOf(tc[0]), b)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc[1], b.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetADR050ValueRenderer(t *testing.T) {
|
|
testcases := []struct {
|
|
name string
|
|
v interface{}
|
|
expErr bool
|
|
}{
|
|
{"uint32", uint32(1), false},
|
|
{"uint64", uint64(1), false},
|
|
{"sdk.Int", math.NewInt(1), false},
|
|
{"sdk.Dec", math.LegacyNewDec(1), false},
|
|
{"[]byte", []byte{1}, false},
|
|
{"float32", float32(1), true},
|
|
{"float64", float64(1), true},
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := valueRendererOf(tc.v)
|
|
if tc.expErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// valueRendererOf is like GetADR050ValueRenderer, but taking a Go type
|
|
// as input instead of a protoreflect.FieldDescriptor.
|
|
func valueRendererOf(v interface{}) (valuerenderer.ValueRenderer, error) {
|
|
a, b := (&testpb.A{}).ProtoReflect().Descriptor().Fields(), (&testpb.B{}).ProtoReflect().Descriptor().Fields()
|
|
|
|
textual := valuerenderer.NewTextual()
|
|
switch v := v.(type) {
|
|
// Valid types for SIGN_MODE_TEXTUAL
|
|
case uint32:
|
|
return textual.GetValueRenderer(a.ByName(protoreflect.Name("UINT32")))
|
|
case uint64:
|
|
return textual.GetValueRenderer(a.ByName(protoreflect.Name("UINT64")))
|
|
case int32:
|
|
return textual.GetValueRenderer(a.ByName(protoreflect.Name("INT32")))
|
|
case int64:
|
|
return textual.GetValueRenderer(a.ByName(protoreflect.Name("INT64")))
|
|
case []byte:
|
|
return textual.GetValueRenderer(a.ByName(protoreflect.Name("BYTES")))
|
|
case math.Int:
|
|
return textual.GetValueRenderer(a.ByName(protoreflect.Name("SDKINT")))
|
|
case math.LegacyDec:
|
|
return textual.GetValueRenderer(a.ByName(protoreflect.Name("SDKDEC")))
|
|
|
|
// Invalid types for SIGN_MODE_TEXTUAL
|
|
case float32:
|
|
return textual.GetValueRenderer(b.ByName(protoreflect.Name("FLOAT")))
|
|
case float64:
|
|
return textual.GetValueRenderer(b.ByName(protoreflect.Name("FLOAT")))
|
|
|
|
default:
|
|
return nil, fmt.Errorf("value %s of type %T not recognized", v, v)
|
|
}
|
|
}
|