frostfs-api-go/util/proto/marshal_test.go
Evgenii Stratonikov fdf56fabe5
Some checks failed
Tests and linters / Lint (pull_request) Failing after 38s
DCO action / DCO (pull_request) Successful in 1m12s
Tests and linters / Tests (pull_request) Successful in 1m38s
Tests and linters / Tests with -race (pull_request) Successful in 1m39s
[#123] protogen: Treat bytes field as non-nullable
In protobuf 3.12 they have added an support for `optional` keyword,
which has made it into the main branch in 3.15.
https://github.com/protocolbuffers/protobuf/blob/main/docs/implementing_proto3_presence.md
https://github.com/protocolbuffers/protobuf/blob/v3.12.0/docs/field_presence.md#presence-in-proto3-apis

This means that without an explicit `optional` keyword field presence
for scalars is not tracked, thus empty string in JSON should be
unmarshaled to a nil byte slice. Relevant decoding code and tests from
protojson:
fb995f184a/internal/impl/message_reflect_field.go (L327)
fb995f184a/encoding/protojson/decode_test.go (L134)
fb995f184a/encoding/protojson/decode_test.go (L156)

We do not support `optional` keyword and the generator will fail if it sees on.
So only implement the default behaviour.

Refs #122

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-10-11 10:21:17 +03:00

245 lines
9 KiB
Go

package proto_test
import (
"math"
"math/rand"
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/proto/test"
generated "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/proto/test/custom"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
goproto "google.golang.org/protobuf/proto"
)
type protoInt interface {
~int32 | ~uint32 | ~int64 | ~uint64
}
func nonZero[T protoInt]() T {
var r T
for r == 0 {
r = T(rand.Uint64())
}
return r
}
func TestStableMarshalSingle(t *testing.T) {
t.Run("empty", func(t *testing.T) {
t.Run("proto", func(t *testing.T) {
input := &generated.Primitives{}
require.Zero(t, input.StableSize())
r := input.MarshalProtobuf(nil)
require.Empty(t, r)
})
t.Run("json", func(t *testing.T) {
input := &generated.Primitives{}
r, err := input.MarshalJSON()
require.NoError(t, err)
require.NotEmpty(t, r)
var actual test.Primitives
require.NoError(t, protojson.Unmarshal(r, &actual))
t.Run("protojson compatibility", func(t *testing.T) {
data, err := protojson.MarshalOptions{EmitUnpopulated: true}.Marshal(&actual)
require.NoError(t, err)
require.JSONEq(t, string(data), string(r))
})
var actualFrostfs generated.Primitives
require.NoError(t, actualFrostfs.UnmarshalJSON(r))
require.Equal(t, input, &actualFrostfs)
primitivesEqual(t, input, &actual)
})
})
marshalCases := []struct {
name string
input *generated.Primitives
}{
{name: "bytes", input: &generated.Primitives{FieldA: []byte{1, 2, 3}}},
{name: "string", input: &generated.Primitives{FieldB: "123"}},
{name: "bool", input: &generated.Primitives{FieldC: true}},
{name: "int32", input: &generated.Primitives{FieldD: -10}},
{name: "uint32", input: &generated.Primitives{FieldE: nonZero[uint32]()}},
{name: "int64", input: &generated.Primitives{FieldF: nonZero[int64]()}},
{name: "uint64", input: &generated.Primitives{FieldG: nonZero[uint64]()}},
{name: "uint64", input: &generated.Primitives{FieldI: nonZero[uint64]()}},
{name: "float64", input: &generated.Primitives{FieldJ: math.Float64frombits(12345677890)}},
{name: "fixed32", input: &generated.Primitives{FieldK: nonZero[uint32]()}},
{name: "enum, positive", input: &generated.Primitives{FieldH: generated.Primitives_POSITIVE}},
{name: "enum, negative", input: &generated.Primitives{FieldH: generated.Primitives_NEGATIVE}},
{name: "oneof, first", input: &generated.Primitives{FieldM: &generated.Primitives_FieldMa{FieldMa: []byte{4, 2}}}},
{name: "oneof, second", input: &generated.Primitives{FieldM: &generated.Primitives_FieldMe{FieldMe: nonZero[uint32]()}}},
}
for _, tc := range marshalCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("proto", func(t *testing.T) {
r := tc.input.MarshalProtobuf(nil)
require.Equal(t, len(r), tc.input.StableSize())
require.NotEmpty(t, r)
var actual test.Primitives
require.NoError(t, goproto.Unmarshal(r, &actual))
var actualFrostfs generated.Primitives
require.NoError(t, actualFrostfs.UnmarshalProtobuf(r))
require.Equal(t, tc.input, &actualFrostfs)
primitivesEqual(t, tc.input, &actual)
})
t.Run("json", func(t *testing.T) {
r, err := tc.input.MarshalJSON()
require.NoError(t, err)
require.NotEmpty(t, r)
var actual test.Primitives
require.NoError(t, protojson.Unmarshal(r, &actual))
t.Run("protojson compatibility", func(t *testing.T) {
data, err := protojson.MarshalOptions{EmitUnpopulated: true}.Marshal(&actual)
require.NoError(t, err)
require.JSONEq(t, string(data), string(r))
})
var actualFrostfs generated.Primitives
require.NoError(t, actualFrostfs.UnmarshalJSON(r))
require.Equal(t, tc.input, &actualFrostfs)
primitivesEqual(t, &actualFrostfs, &actual)
})
})
}
}
func primitivesEqual(t *testing.T, a *generated.Primitives, b *test.Primitives) {
// Compare each field directly, because proto-generated code has private fields.
require.Equal(t, len(a.FieldA), len(b.FieldA))
require.Equal(t, a.FieldA, b.FieldA)
require.Equal(t, a.FieldB, b.FieldB)
require.Equal(t, a.FieldC, b.FieldC)
require.Equal(t, a.FieldD, b.FieldD)
require.Equal(t, a.FieldE, b.FieldE)
require.Equal(t, a.FieldF, b.FieldF)
require.Equal(t, a.FieldG, b.FieldG)
require.Equal(t, a.FieldI, b.FieldI)
require.Equal(t, a.FieldJ, b.FieldJ)
require.Equal(t, a.FieldK, b.FieldK)
require.EqualValues(t, a.FieldH, b.FieldH)
require.Equal(t, a.GetFieldMa(), b.GetFieldMa())
require.Equal(t, a.GetFieldMe(), b.GetFieldMe())
require.Equal(t, a.GetFieldAux().GetInnerField(), b.GetFieldAux().GetInnerField())
}
func repPrimitivesEqual(t *testing.T, a *generated.RepPrimitives, b *test.RepPrimitives) {
// Compare each field directly, because proto-generated code has private fields.
require.Equal(t, a.FieldA, b.FieldA)
require.Equal(t, a.FieldB, b.FieldB)
require.Equal(t, a.FieldC, b.FieldC)
require.Equal(t, a.FieldD, b.FieldD)
require.Equal(t, a.FieldE, b.FieldE)
require.Equal(t, a.FieldF, b.FieldF)
require.Equal(t, a.FieldFu, b.FieldFu)
require.Equal(t, len(a.GetFieldAux()), len(b.GetFieldAux()))
for i := range a.FieldAux {
require.Equal(t, a.GetFieldAux()[i].GetInnerField(), b.GetFieldAux()[i].GetInnerField())
}
}
func randIntSlice[T protoInt](n int, includeZero bool) []T {
r := make([]T, n)
if n == 0 {
return r
}
for i := range r {
r[i] = T(rand.Uint64())
}
if includeZero {
r[0] = 0
}
return r
}
func uint32SliceToAux(s []uint32) []generated.RepPrimitives_Aux {
r := make([]generated.RepPrimitives_Aux, len(s))
for i := range s {
r[i] = generated.RepPrimitives_Aux{InnerField: s[i]}
}
return r
}
func TestStableMarshalRep(t *testing.T) {
t.Run("empty", func(t *testing.T) {
marshalCases := []struct {
name string
input *generated.RepPrimitives
}{
{name: "default", input: &generated.RepPrimitives{}},
{name: "bytes", input: &generated.RepPrimitives{FieldA: [][]byte{}}},
{name: "string", input: &generated.RepPrimitives{FieldB: []string{}}},
{name: "int32", input: &generated.RepPrimitives{FieldC: []int32{}}},
{name: "uint32", input: &generated.RepPrimitives{FieldD: []uint32{}}},
{name: "int64", input: &generated.RepPrimitives{FieldE: []int64{}}},
{name: "uint64", input: &generated.RepPrimitives{FieldF: []uint64{}}},
{name: "uint64", input: &generated.RepPrimitives{FieldFu: []uint64{}}},
}
for _, tc := range marshalCases {
t.Run(tc.name, func(t *testing.T) {
require.Zero(t, tc.input.StableSize())
r := tc.input.MarshalProtobuf(nil)
require.Empty(t, r)
})
}
})
marshalCases := []struct {
name string
input *generated.RepPrimitives
}{
{name: "bytes", input: &generated.RepPrimitives{FieldA: [][]byte{{1, 2, 3}}}},
{name: "string", input: &generated.RepPrimitives{FieldB: []string{"123"}}},
{name: "int32", input: &generated.RepPrimitives{FieldC: randIntSlice[int32](1, true)}},
{name: "int32", input: &generated.RepPrimitives{FieldC: randIntSlice[int32](2, true)}},
{name: "int32", input: &generated.RepPrimitives{FieldC: randIntSlice[int32](2, false)}},
{name: "uint32", input: &generated.RepPrimitives{FieldD: randIntSlice[uint32](1, true)}},
{name: "uint32", input: &generated.RepPrimitives{FieldD: randIntSlice[uint32](2, true)}},
{name: "uint32", input: &generated.RepPrimitives{FieldD: randIntSlice[uint32](2, false)}},
{name: "int64", input: &generated.RepPrimitives{FieldE: randIntSlice[int64](1, true)}},
{name: "int64", input: &generated.RepPrimitives{FieldE: randIntSlice[int64](2, true)}},
{name: "int64", input: &generated.RepPrimitives{FieldE: randIntSlice[int64](2, false)}},
{name: "uint64", input: &generated.RepPrimitives{FieldF: randIntSlice[uint64](1, true)}},
{name: "uint64", input: &generated.RepPrimitives{FieldF: randIntSlice[uint64](2, true)}},
{name: "uint64", input: &generated.RepPrimitives{FieldF: randIntSlice[uint64](2, false)}},
{name: "uint64", input: &generated.RepPrimitives{FieldFu: randIntSlice[uint64](1, true)}},
{name: "uint64", input: &generated.RepPrimitives{FieldFu: randIntSlice[uint64](2, true)}},
{name: "uint64", input: &generated.RepPrimitives{FieldFu: randIntSlice[uint64](2, false)}},
{name: "message", input: &generated.RepPrimitives{FieldAux: uint32SliceToAux(randIntSlice[uint32](1, true))}},
{name: "message", input: &generated.RepPrimitives{FieldAux: uint32SliceToAux(randIntSlice[uint32](2, true))}},
{name: "message", input: &generated.RepPrimitives{FieldAux: uint32SliceToAux(randIntSlice[uint32](2, false))}},
}
for _, tc := range marshalCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("proto", func(t *testing.T) {
r := tc.input.MarshalProtobuf(nil)
require.Equal(t, len(r), tc.input.StableSize())
require.NotEmpty(t, r)
var actual test.RepPrimitives
require.NoError(t, goproto.Unmarshal(r, &actual))
repPrimitivesEqual(t, tc.input, &actual)
})
t.Run("json", func(t *testing.T) {
r, err := tc.input.MarshalJSON()
require.NoError(t, err)
require.NotEmpty(t, r)
var actual test.RepPrimitives
require.NoError(t, protojson.Unmarshal(r, &actual))
repPrimitivesEqual(t, tc.input, &actual)
})
})
}
}