Add decode and roundtrip tests for CBOR marshaling.
Co-authored-by: Suriyan Subbarayan <suriyansub710@gmail.com>
This commit is contained in:
		| @@ -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) | ||||
| 					} | ||||
| 				} | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
| 				} | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Ben Luddy
					Ben Luddy