From 3a097f224aef87ac5261a5f9cdd37dec48030cb8 Mon Sep 17 00:00:00 2001 From: Suriyan S Date: Tue, 27 Feb 2024 21:28:52 -0500 Subject: [PATCH] Add unit tests for decoding CBOR into interface{} type Co-authored-by: Ben Luddy --- .../cbor/internal/modes/decode_test.go | 467 +++++++++++++++++- .../cbor/internal/modes/modes_test.go | 12 + 2 files changed, 469 insertions(+), 10 deletions(-) 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 index 86db2e4f8ab7a..57ddef9fed3e5 100644 --- 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 @@ -19,6 +19,7 @@ package modes_test import ( "encoding/hex" "fmt" + "math" "reflect" "testing" @@ -44,6 +45,11 @@ func TestDecode(t *testing.T) { into interface{} // prototype for concrete destination type. if nil, decode into empty interface value. want interface{} assertOnError func(t *testing.T, e error) + + // TODO: Some failing test cases are included for completeness. The next library + // minor version should allow them all to be fixed. In the meantime, this field + // explains the behavior reason for a particular failure. + fixme string } // Test cases are grouped by the kind of the CBOR data item being decoded, as enumerated in @@ -64,6 +70,13 @@ func TestDecode(t *testing.T) { } t.Run(fmt.Sprintf("%s/mode=%s", test.name, modeName), func(t *testing.T) { + if test.fixme != "" { + // TODO: Remove this along with the + // fixme field when the last skipped + // test case is passing. + t.Skip(test.fixme) + } + var dst reflect.Value if test.into == nil { var i interface{} @@ -73,7 +86,7 @@ func TestDecode(t *testing.T) { } err := decMode.Unmarshal(test.in, dst.Interface()) test.assertOnError(t, err) - if test.want != nil { + if err == nil { if diff := cmp.Diff(test.want, dst.Elem().Interface()); diff != "" { t.Errorf("unexpected output:\n%s", diff) } @@ -92,11 +105,65 @@ func TestDecode(t *testing.T) { want: int64(10), assertOnError: assertNilError, }, + { + name: "int64 minimum positive value", + in: hex("00"), // 0 + want: int64(0), + assertOnError: assertNilError, + }, + { + name: "int64 max positive value", + in: hex("1b7fffffffffffffff"), // 9223372036854775807 + want: int64(9223372036854775807), + assertOnError: assertNilError, + }, + { + name: "max positive integer value supported by cbor: 2^64 - 1", + in: hex("1bffffffffffffffff"), // 18446744073709551615 + assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnmarshalTypeError) { + if e == nil { + t.Error("expected non-nil error") + } else if want := "cbor: cannot unmarshal positive integer into Go value of type int64 (18446744073709551615 overflows Go's int64)"; want != e.Error() { + t.Errorf("want error %q, got %q", want, e.Error()) + } + }), + }, }) - group(t, "negative integer", []test{}) + group(t, "negative integer", []test{ + { + name: "int64 max negative value", + in: hex("20"), // -1 + want: int64(-1), + assertOnError: assertNilError, + }, + { + name: "int64 min negative value", + in: hex("3b7fffffffffffffff"), // -9223372036854775808 + want: int64(-9223372036854775808), + assertOnError: assertNilError, + }, + { + name: "min negative integer value supported by cbor: -2^64", + in: hex("3bffffffffffffffff"), // -18446744073709551616 + assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnmarshalTypeError) { + if e == nil { + t.Error("expected non-nil error") + } else if want := "cbor: cannot unmarshal negative integer into Go value of type int64 (-18446744073709551616 overflows Go's int64)"; want != e.Error() { + t.Errorf("want error %q, got %q", want, e.Error()) + } + }), + }, + }) - group(t, "byte string", []test{}) + group(t, "byte string", []test{ + { + name: "empty byte string", + in: hex("40"), // '' + want: "", + assertOnError: assertNilError, + }, + }) group(t, "text string", []test{ { @@ -115,6 +182,12 @@ func TestDecode(t *testing.T) { want: "abc", assertOnError: assertNilError, }, + { + name: "empty text string", + in: hex("60"), // "" + want: "", + assertOnError: assertNilError, + }, }) group(t, "array", []test{ @@ -329,21 +402,395 @@ func TestDecode(t *testing.T) { }, assertOnError: assertNilError, }, + { + name: "map with non-string key types", + in: hex("a1fb40091eb851eb851f63706965"), // {3.14: "pie"} + assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnmarshalTypeError) { + if e.CBORType != "primitives" || e.GoType != "string" { + t.Errorf("expected %q, got %q", &cbor.UnmarshalTypeError{CBORType: "primitives", GoType: "string"}, e) + } + }), + }, + { + name: "map with byte string key", + in: hex("a143abcdef187b"), // {h'abcdef': 123} + want: map[string]interface{}{"\xab\xcd\xef": int64(123)}, + assertOnError: assertNilError, + }, + { + name: "map with text string key", + in: hex("a143414243187b"), // {"ABC": 123} + want: map[string]interface{}{"ABC": int64(123)}, + assertOnError: assertNilError, + }, + { + name: "map with mixed string key types", + in: hex("a243abcdef187b43414243187c"), // {h'abcdef': 123, "ABC": 124} + want: map[string]interface{}{"\xab\xcd\xef": int64(123), "ABC": int64(124)}, + assertOnError: assertNilError, + }, }) - group(t, "floating-point number", []test{}) + group(t, "floating-point number", []test{ + { + name: "half precision infinity", + in: hex("f97c00"), + assertOnError: func(t *testing.T, e error) { + if e == nil { + t.Fatal("expected non-nil error") + } + }, + fixme: "NaN and positive/negative infinities should be rejected", + }, + { + name: "single precision infinity", + in: hex("fa7f800000"), + assertOnError: func(t *testing.T, e error) { + if e == nil { + t.Fatal("expected non-nil error") + } + }, + fixme: "NaN and positive/negative infinities should be rejected", + }, + { + name: "double precision infinity", + in: hex("fb7ff0000000000000"), + assertOnError: func(t *testing.T, e error) { + if e == nil { + t.Fatal("expected non-nil error") + } + }, + fixme: "NaN and positive/negative infinities should be rejected", + }, + { + name: "half precision negative infinity", + in: hex("f9fc00"), + assertOnError: func(t *testing.T, e error) { + if e == nil { + t.Fatal("expected non-nil error") + } + }, + fixme: "NaN and positive/negative infinities should be rejected", + }, + { + name: "single precision negative infinity", + in: hex("faff800000"), + assertOnError: func(t *testing.T, e error) { + if e == nil { + t.Fatal("expected non-nil error") + } + }, + fixme: "NaN and positive/negative infinities should be rejected", + }, + { + name: "double precision negative infinity", + in: hex("fbfff0000000000000"), + assertOnError: func(t *testing.T, e error) { + if e == nil { + t.Fatal("expected non-nil error") + } + }, + fixme: "NaN and positive/negative infinities should be rejected", + }, + { + name: "half precision NaN", + in: hex("f97e00"), + assertOnError: func(t *testing.T, e error) { + if e == nil { + t.Fatal("expected non-nil error") + } + }, + fixme: "NaN and positive/negative infinities should be rejected", + }, + { + name: "single precision NaN", + in: hex("fa7fc00000"), + assertOnError: func(t *testing.T, e error) { + if e == nil { + t.Fatal("expected non-nil error") + } + }, + fixme: "NaN and positive/negative infinities should be rejected", + }, + { + name: "double precision NaN", + in: hex("fb7ff8000000000000"), + assertOnError: func(t *testing.T, e error) { + if e == nil { + t.Fatal("expected non-nil error") + } + }, + fixme: "NaN and positive/negative infinities should be rejected", + }, + { + name: "smallest nonzero float64", + in: hex("fb0000000000000001"), + want: float64(math.SmallestNonzeroFloat64), + assertOnError: assertNilError, + }, + { + name: "max float64 value", + in: hex("fb7fefffffffffffff"), + want: float64(math.MaxFloat64), + assertOnError: assertNilError, + }, + { + name: "max float32 value as double precision", + in: hex("fb47efffffe0000000"), + want: float64(math.MaxFloat32), + assertOnError: assertNilError, + }, + { + name: "max float32 value as single precision", + in: hex("fa7f7fffff"), + want: float64(math.MaxFloat32), + assertOnError: assertNilError, + }, + { + name: "half precision", + in: hex("f94200"), + want: float64(3), + assertOnError: assertNilError, + }, + { + name: "double precision without fractional component", + in: hex("fb4000000000000000"), + want: float64(2), + assertOnError: assertNilError, + }, + { + name: "single precision without fractional component", + in: hex("fa40000000"), + want: float64(2), + assertOnError: assertNilError, + }, + { + name: "half precision without fractional component", + in: hex("f94000"), + want: float64(2), + assertOnError: assertNilError, + }, + }) - group(t, "simple value", []test{}) + group(t, "simple value", append([]test{ + { + name: "simple value 20", + in: hex("f4"), // false + want: false, + assertOnError: assertNilError, + }, + { + name: "simple value 21", + in: hex("f5"), // true + want: true, + assertOnError: assertNilError, + }, + { + name: "simple value 22", + in: hex("f6"), // nil + want: nil, + assertOnError: assertNilError, + }, + { + name: "simple value 23", + in: hex("f7"), // undefined + assertOnError: func(t *testing.T, e error) { + // TODO: Once this can pass, make the assertion stronger. + if e == nil { + t.Error("expected non-nil error") + } + }, + fixme: "cbor simple value 23 (\"undefined\") should not be accepted", + }, + }, func() (generated []test) { + // Generate test cases for all simple values (0 to 255) because the number of possible simple values is fixed and small. + for i := 0; i <= 255; i++ { + each := test{ + name: fmt.Sprintf("simple value %d", i), + } + if i <= 23 { + each.in = []byte{byte(0xe0) | byte(i)} + } else { + // larger simple values encode to two bytes + each.in = []byte{byte(0xe0) | byte(24), byte(i)} + } + switch i { + case 20, 21, 22, 23: // recognized values with explicit cases + continue + case 24, 25, 26, 27, 28, 29, 30, 31: // reserved + // these are considered not well-formed + each.assertOnError = assertOnConcreteError(func(t *testing.T, e *cbor.SyntaxError) { + if e == nil { + t.Error("expected non-nil error") + } else if want := fmt.Sprintf("cbor: invalid simple value %d for type primitives", i); want != e.Error() { + t.Errorf("want error %q, got %q", want, e.Error()) + } + }) + default: + // reject all unrecognized simple values + each.assertOnError = func(t *testing.T, e error) { + // TODO: Once this can pass, make the assertion stronger. + if e == nil { + t.Error("expected non-nil error") + } + } + each.fixme = "unrecognized simple values should be rejected" + } + generated = append(generated, each) + } + return + }()...)) t.Run("tag", func(t *testing.T) { - group(t, "rfc3339 time", []test{}) + group(t, "rfc3339 time", []test{ + { + name: "tag 0 RFC3339 text string", + in: hex("c074323030362d30312d30325431353a30343a30355a"), // 0("2006-01-02T15:04:05Z") + want: "2006-01-02T15:04:05Z", + fixme: "decoding RFC3339 text string tagged with 0 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 0 byte string", + in: hex("c054323030362d30312d30325431353a30343a30355a"), // 0('2006-01-02T15:04:05Z') + want: "2006-01-02T15:04:05Z", + assertOnError: assertErrorMessage("cbor: tag number 0 must be followed by text string, got byte string"), + }, + { + name: "tag 0 non-RFC3339 text string", + in: hex("c06474657874"), // 0("text") + assertOnError: assertErrorMessage(`cbor: cannot set text for time.Time: parsing time "text" as "2006-01-02T15:04:05Z07:00": cannot parse "text" as "2006"`), + }, + }) - group(t, "epoch time", []test{}) + group(t, "epoch time", []test{ + { + name: "tag 1 timestamp unsigned integer", + in: hex("c11a43b940e5"), // 1(1136214245) + want: "2006-01-02T15:04:05Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 1 with float16 value", + in: hex("c1f93c00"), // 1(1.0_1) + want: "1970-01-01T00:00:01Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 1 with float32 value", + in: hex("c1fa3f800000"), // 1(1.0_2) + want: "1970-01-01T00:00:01Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 1 with float64 value", + in: hex("c1fb3ff0000000000000"), // 1(1.0_3) + want: "1970-01-01T00:00:01Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 1 with a five digit year", + in: hex("c11b0000003afff44181"), // 1(253402300801) + want: "10000-01-01T00:00:01Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 1 with a negative integer value", + in: hex("c120"), // 1(-1) + want: "1969-12-31T23:59:59Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 1 with a negative float16 value", + in: hex("c1f9bc00"), // 1(-1.0_1) + want: "1969-12-31T23:59:59Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 1 with a negative float32 value", + in: hex("c1fabf800000"), // 1(-1.0_2) + want: "1969-12-31T23:59:59Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 1 with a negative float64 value", + in: hex("c1fbbff0000000000000"), // 1(-1.0_3) + want: "1969-12-31T23:59:59Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 1 with a positive infinity", + in: hex("c1f97c00"), // 1(Infinity) + want: "0001-01-01T00:00:00Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 1 with a negative infinity", + in: hex("c1f9fc00"), // 1(-Infinity) + want: "0001-01-01T00:00:00Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + { + name: "tag 1 with NaN", + in: hex("c1f9fc00"), // 1(NaN) + want: "0001-01-01T00:00:00Z", + fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", + assertOnError: assertNilError, + }, + }) - group(t, "unsigned bignum", []test{}) + group(t, "unsigned bignum", []test{ + { + name: "rejected", + in: hex("c249010000000000000000"), // 2(18446744073709551616) + fixme: "decoding cbor data tagged with 2 produces big.Int instead of rejecting", + assertOnError: func(t *testing.T, e error) { + // TODO: Once this can pass, make the assertion stronger. + if e == nil { + t.Error("expected non-nil error") + } + }, + }, + }) - group(t, "negative bignum", []test{}) + group(t, "negative bignum", []test{ + { + name: "rejected", + in: hex("c349010000000000000000"), // 3(-18446744073709551617) + fixme: "decoding cbor data tagged with 3 produces big.Int instead of rejecting", + assertOnError: func(t *testing.T, e error) { + // TODO: Once this can pass, make the assertion stronger. + if e == nil { + t.Error("expected non-nil error") + } + }, + }, + }) - group(t, "unrecognized", []test{}) + group(t, "unrecognized", []test{ + { + name: "decimal fraction", + in: hex("c48221196ab3"), // 4([-2, 27315]) + want: []interface{}{int64(-2), int64(27315)}, + assertOnError: assertNilError, + }, + { + name: "bigfloat", + in: hex("c5822003"), // 5([-1, 3]) + want: []interface{}{int64(-1), int64(3)}, + assertOnError: assertNilError, + }, + }) }) } 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 index b0af33bcc9502..9c2187edfb063 100644 --- 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 @@ -62,6 +62,18 @@ func assertOnConcreteError[E error](fn func(*testing.T, E)) func(t *testing.T, e } } +func assertErrorMessage(want string) func(*testing.T, error) { + return func(t *testing.T, got error) { + if got == nil { + t.Error("expected non-nil error") + return + } + if got.Error() != want { + t.Errorf("got error %q, want %q", got.Error(), want) + } + } +} + func assertIdenticalError[E error](expected E) func(*testing.T, error) { return assertOnConcreteError(func(t *testing.T, actual E) { if diff := cmp.Diff(expected, actual); diff != "" {