From 57fc5d24013d5105663485ac6b5f982bb57045e7 Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Tue, 13 Feb 2024 11:05:19 -0500 Subject: [PATCH] Add decode and roundtrip tests for CBOR marshaling. Co-authored-by: Suriyan Subbarayan --- .../cbor/internal/modes/decode_test.go | 137 ++++++++ .../cbor/internal/modes/modes_test.go | 62 ++++ .../cbor/internal/modes/roundtrip_test.go | 304 ++++++++++++++++++ 3 files changed, 503 insertions(+) create mode 100644 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/decode_test.go create mode 100644 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/modes_test.go create mode 100644 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/roundtrip_test.go diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/decode_test.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/decode_test.go new file mode 100644 index 00000000000..f9e45ec6dc3 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/decode_test.go @@ -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) + } + } + }) + } + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/modes_test.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/modes_test.go new file mode 100644 index 00000000000..b49c696291a --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/modes_test.go @@ -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) + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/roundtrip_test.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/roundtrip_test.go new file mode 100644 index 00000000000..9d7d8f06a50 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/roundtrip_test.go @@ -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) + } + }) + } + } +}