From 707ee63456c60a4b37275018ad6a044e3700e1c7 Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Wed, 2 Oct 2024 16:38:54 -0400 Subject: [PATCH] Test concrete type changes in unstructured float64 JSON roundtrips. During JSON roundtrips of unstructured objects, float64 values with no fractional component will in certain cases roundtrip to int64. This is potentially surprising existing behavior. Adding dedicated test coverage for this will help codify the behavior and mitigate future regression risk. --- .../pkg/runtime/serializer/json/json_test.go | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json/json_test.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json/json_test.go index 5149692b4b4..d1ad3a9c5eb 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json/json_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json/json_test.go @@ -1042,3 +1042,50 @@ func TestEncode(t *testing.T) { }) } } + +// TestRoundtripUnstructuredFloat64 demonstrates that encoding a fractionless float64 value to JSON +// then decoding into interface{} can produce a value with concrete type int64. This is a +// consequence of two specific behaviors. First, there is nothing in the JSON encoding of a +// fractionless float64 value to distinguish it from the JSON encoding of an integer value. Second, +// if, when unmarshaling into interface{}, the decoder encounters a JSON number with no decimal +// point in the input, it produces a value with concrete type int64 as long as the number can be +// precisely represented by an int64. +func TestRoundtripUnstructuredFractionlessFloat64(t *testing.T) { + s := json.NewSerializerWithOptions(json.DefaultMetaFactory, runtime.NewScheme(), runtime.NewScheme(), json.SerializerOptions{}) + + initial := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Test", + "with-fraction": float64(1.5), + "without-fraction": float64(1), + "without-fraction-big-positive": float64(9223372036854776000), + "without-fraction-big-negative": float64(-9223372036854776000), + }} + + var buf bytes.Buffer + if err := s.Encode(initial, &buf); err != nil { + t.Fatal(err) + } + + final := &unstructured.Unstructured{} + got, _, err := s.Decode(buf.Bytes(), nil, final) + if err != nil { + t.Fatal(err) + } + if got != final { + t.Fatalf("expected Decode to return target Unstructured object but got: %v", got) + } + + expected := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Test", + "with-fraction": float64(1.5), + "without-fraction": int64(1), // note the change in concrete type + "without-fraction-big-positive": float64(9223372036854776000), + "without-fraction-big-negative": float64(-9223372036854776000), + }} + + if diff := cmp.Diff(expected, final); diff != "" { + t.Fatalf("unexpected diff:\n%s", diff) + } +}