cosmos-sdk/tx/textual/valuerenderer/valuerenderer_test.go
Emmanuel T Odeke 8c23f6f957
perf: fix: tx/textual/valuerender: use io.WriteString to skip str->byteslice + fix negative sign dropping (#12815)
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
2022-08-04 01:27:54 -07:00

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)
}
}