Merge pull request #123267 from benluddy/cbor-marshaling-tests

KEP-4222: Add decode and roundtrip tests for CBOR marshaling.
This commit is contained in:
Kubernetes Prow Robot 2024-02-20 13:03:42 -08:00 committed by GitHub
commit 930f60173b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 503 additions and 0 deletions

View File

@ -0,0 +1,137 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package modes_test
import (
"encoding/hex"
"fmt"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes"
"github.com/fxamacker/cbor/v2"
"github.com/google/go-cmp/cmp"
)
func TestDecode(t *testing.T) {
hex := func(h string) []byte {
b, err := hex.DecodeString(h)
if err != nil {
t.Fatal(err)
}
return b
}
for _, tc := range []struct {
name string
modes []cbor.DecMode
in []byte
into interface{} // prototype for concrete destination type. if nil, decode into empty interface value.
want interface{}
assertOnError func(t *testing.T, e error)
}{
{
name: "reject text string containing invalid utf-8 sequence",
in: hex("6180"), // text string beginning with continuation byte 0x80
assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.SemanticError) {
const expected = "cbor: invalid UTF-8 string"
if msg := e.Error(); msg != expected {
t.Errorf("expected %v, got %v", expected, msg)
}
}),
},
{
name: "unsigned integer decodes to interface{} as int64",
in: hex("0a"), // 10
want: int64(10),
assertOnError: assertNilError,
},
{
name: "unknown field error",
modes: []cbor.DecMode{modes.Decode},
in: hex("a1616101"), // {"a": 1}
into: struct{}{},
assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnknownFieldError) {
if e.Index != 0 {
t.Errorf("expected %#v, got %#v", &cbor.UnknownFieldError{Index: 0}, e)
}
}),
},
{
name: "no unknown field error in lax mode",
modes: []cbor.DecMode{modes.DecodeLax},
in: hex("a1616101"), // {"a": 1}
into: struct{}{},
want: struct{}{},
assertOnError: assertNilError,
},
{
name: "indefinite-length text string",
in: hex("7f616161626163ff"), // (_ "a", "b", "c")
want: "abc",
assertOnError: assertNilError,
},
{
name: "nested indefinite-length array",
in: hex("9f9f8080ff9f8080ffff"), // [_ [_ [] []] [_ [][]]]
want: []interface{}{
[]interface{}{[]interface{}{}, []interface{}{}},
[]interface{}{[]interface{}{}, []interface{}{}},
},
assertOnError: assertNilError,
},
{
name: "nested indefinite-length map",
in: hex("bf6141bf616101616202ff6142bf616901616a02ffff"), // {_ "A": {_ "a": 1, "b": 2}, "B": {_ "i": 1, "j": 2}}
want: map[string]interface{}{
"A": map[string]interface{}{"a": int64(1), "b": int64(2)},
"B": map[string]interface{}{"i": int64(1), "j": int64(2)},
},
assertOnError: assertNilError,
},
} {
ms := tc.modes
if len(ms) == 0 {
ms = allDecModes
}
for _, dm := range ms {
modeName, ok := decModeNames[dm]
if !ok {
t.Fatal("test case configured to run against unrecognized mode")
}
t.Run(fmt.Sprintf("mode=%s/%s", modeName, tc.name), func(t *testing.T) {
var dst reflect.Value
if tc.into == nil {
var i interface{}
dst = reflect.ValueOf(&i)
} else {
dst = reflect.New(reflect.TypeOf(tc.into))
}
err := dm.Unmarshal(tc.in, dst.Interface())
tc.assertOnError(t, err)
if tc.want != nil {
if diff := cmp.Diff(tc.want, dst.Elem().Interface()); diff != "" {
t.Errorf("unexpected output:\n%s", diff)
}
}
})
}
}
}

View File

@ -0,0 +1,62 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package modes_test
import (
"errors"
"testing"
"github.com/fxamacker/cbor/v2"
"k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes"
)
var encModeNames = map[cbor.EncMode]string{
modes.Encode: "Encode",
modes.EncodeNondeterministic: "EncodeNondeterministic",
}
var allEncModes = []cbor.EncMode{
modes.Encode,
modes.EncodeNondeterministic,
}
var decModeNames = map[cbor.DecMode]string{
modes.Decode: "Decode",
modes.DecodeLax: "DecodeLax",
}
var allDecModes = []cbor.DecMode{
modes.Decode,
modes.DecodeLax,
}
func assertNilError(t *testing.T, e error) {
if e != nil {
t.Errorf("expected nil error, got: %v", e)
}
}
func assertOnConcreteError[E error](fn func(*testing.T, E)) func(t *testing.T, e error) {
return func(t *testing.T, ei error) {
var ec E
if !errors.As(ei, &ec) {
t.Errorf("expected concrete error type %T, got %T: %v", ec, ei, ei)
return
}
fn(t, ec)
}
}

View File

@ -0,0 +1,304 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package modes_test
import (
"fmt"
"math"
"reflect"
"testing"
"time"
"github.com/fxamacker/cbor/v2"
)
func nilPointerFor[T interface{}]() *T {
return nil
}
func TestRoundtrip(t *testing.T) {
type modePair struct {
enc cbor.EncMode
dec cbor.DecMode
}
for _, tc := range []struct {
name string
modePairs []modePair
obj interface{}
}{
{
name: "nil slice",
obj: []interface{}(nil),
},
{
name: "nil map",
obj: map[string]interface{}(nil),
},
{
name: "empty slice",
obj: []interface{}{},
},
{
name: "empty map",
obj: map[string]interface{}{},
},
{
name: "nil pointer to slice",
obj: nilPointerFor[[]interface{}](),
},
{
name: "nil pointer to map",
obj: nilPointerFor[map[string]interface{}](),
},
{
name: "nonempty string",
obj: "hello world",
},
{
name: "empty string",
obj: "",
},
{
name: "string containing invalid UTF-8 sequence",
obj: "\x80", // first byte is a continuation byte
},
{
name: "true",
obj: true,
},
{
name: "false",
obj: false,
},
{
name: "int64",
obj: int64(5),
},
{
name: "int64 max",
obj: int64(math.MaxInt64),
},
{
name: "int64 min",
obj: int64(math.MinInt64),
},
{
name: "int64 zero",
obj: int64(math.MinInt64),
},
{
name: "uint64 max",
obj: uint64(math.MaxUint64),
},
{
name: "uint64 zero",
obj: uint64(0),
},
{
name: "int32 max",
obj: int32(math.MaxInt32),
},
{
name: "int32 min",
obj: int32(math.MinInt32),
},
{
name: "int32 zero",
obj: int32(math.MinInt32),
},
{
name: "uint32 max",
obj: uint32(math.MaxUint32),
},
{
name: "uint32 zero",
obj: uint32(0),
},
{
name: "int16 max",
obj: int16(math.MaxInt16),
},
{
name: "int16 min",
obj: int16(math.MinInt16),
},
{
name: "int16 zero",
obj: int16(math.MinInt16),
},
{
name: "uint16 max",
obj: uint16(math.MaxUint16),
},
{
name: "uint16 zero",
obj: uint16(0),
},
{
name: "int8 max",
obj: int8(math.MaxInt8),
},
{
name: "int8 min",
obj: int8(math.MinInt8),
},
{
name: "int8 zero",
obj: int8(math.MinInt8),
},
{
name: "uint8 max",
obj: uint8(math.MaxUint8),
},
{
name: "uint8 zero",
obj: uint8(0),
},
{
name: "float64",
obj: float64(2.71),
},
{
name: "float64 max",
obj: float64(math.MaxFloat64),
},
{
name: "float64 smallest nonzero",
obj: float64(math.SmallestNonzeroFloat64),
},
{
name: "float64 no fractional component",
obj: float64(5),
},
{
name: "float32",
obj: float32(2.71),
},
{
name: "float32 max",
obj: float32(math.MaxFloat32),
},
{
name: "float32 smallest nonzero",
obj: float32(math.SmallestNonzeroFloat32),
},
{
name: "float32 no fractional component",
obj: float32(5),
},
{
name: "time.Time",
obj: time.Date(2222, time.May, 4, 12, 13, 14, 123, time.UTC),
},
{
name: "int64 omitempty",
obj: struct {
V int64 `json:"v,omitempty"`
}{},
},
{
name: "float64 omitempty",
obj: struct {
V float64 `json:"v,omitempty"`
}{},
},
{
name: "string omitempty",
obj: struct {
V string `json:"v,omitempty"`
}{},
},
{
name: "bool omitempty",
obj: struct {
V bool `json:"v,omitempty"`
}{},
},
{
name: "nil pointer omitempty",
obj: struct {
V *struct{} `json:"v,omitempty"`
}{},
},
{
name: "nil pointer to slice as struct field",
obj: struct {
V *[]interface{} `json:"v"`
}{},
},
{
name: "nil pointer to slice as struct field with omitempty",
obj: struct {
V *[]interface{} `json:"v,omitempty"`
}{},
},
{
name: "nil pointer to map as struct field",
obj: struct {
V *map[string]interface{} `json:"v"`
}{},
},
{
name: "nil pointer to map as struct field with omitempty",
obj: struct {
V *map[string]interface{} `json:"v,omitempty"`
}{},
},
} {
mps := tc.modePairs
if len(mps) == 0 {
// Default is all modes to all modes.
mps = []modePair{}
for _, em := range allEncModes {
for _, dm := range allDecModes {
mps = append(mps, modePair{enc: em, dec: dm})
}
}
}
for _, mp := range mps {
encModeName, ok := encModeNames[mp.enc]
if !ok {
t.Fatal("test case configured to run against unrecognized encode mode")
}
decModeName, ok := decModeNames[mp.dec]
if !ok {
t.Fatal("test case configured to run against unrecognized decode mode")
}
t.Run(fmt.Sprintf("enc=%s/dec=%s/%s", encModeName, decModeName, tc.name), func(t *testing.T) {
original := tc.obj
b, err := mp.enc.Marshal(original)
if err != nil {
t.Fatalf("unexpected error from Marshal: %v", err)
}
final := reflect.New(reflect.TypeOf(original))
err = mp.dec.Unmarshal(b, final.Interface())
if err != nil {
t.Fatalf("unexpected error from Unmarshal: %v", err)
}
if !reflect.DeepEqual(original, final.Elem().Interface()) {
t.Errorf("roundtrip difference:\nwant: %#v\ngot: %#v", original, final)
}
})
}
}
}