mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-17 20:00:07 +00:00
feat: add build-tag type switching for FieldsV1 to string that uses unique.Handle[string] for interning and associated tests + benchmarks
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
Copyright 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 v1_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
var benchmarkPayloads = []string{
|
||||
// Small managed fields payload
|
||||
`{"f:metadata":{"f:labels":{"f:app":{}},"f:annotations":{"f:revision":{}}},"f:spec":{"f:replicas":{},"f:template":{"f:metadata":{"f:labels":{"f:app":{}}},"f:spec":{"f:containers":{"k:{\"name\":\"nginx\"}":{".":{},"f:image":{},"f:name":{}}}}}}}`,
|
||||
// Larger, deeply nested valid JSON payload (representing complex managedFields)
|
||||
`{"f:metadata":{"f:annotations":{".":{},"f:kubectl.kubernetes.io/last-applied-configuration":{}},"f:labels":{".":{},"f:app.kubernetes.io/name":{}}},"f:spec":{"f:replicas":{},"f:selector":{},"f:template":{"f:metadata":{"f:labels":{".":{},"f:app.kubernetes.io/name":{}}},"f:spec":{"f:containers":{"k:{\"name\":\"app\"}":{".":{},"f:image":{},"f:name":{},"f:ports":{".":{},"k:{\"containerPort\":8080,\"protocol\":\"TCP\"}":{".":{},"f:containerPort":{},"f:protocol":{}}},"f:resources":{".":{},"f:limits":{".":{},"f:cpu":{},"f:memory":{}},"f:requests":{".":{},"f:cpu":{},"f:memory":{}}}}}}}}}`,
|
||||
}
|
||||
|
||||
// BenchmarkDecodeDuplicate measures the allocation and speed of unmarshaling
|
||||
// exactly identical payloads repeatedly, which is the most common case for
|
||||
// heavily replicated resources (DaemonSets, ReplicaSets) in the API server.
|
||||
func BenchmarkDecodeDuplicate(b *testing.B) {
|
||||
for _, payload := range benchmarkPayloads {
|
||||
b.Run(fmt.Sprintf("Size%d", len(payload)), func(b *testing.B) {
|
||||
rawJSON := []byte(payload)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
var retained []metav1.FieldsV1
|
||||
for j := 0; j < b.N; j++ {
|
||||
var f metav1.FieldsV1
|
||||
if err := json.Unmarshal(rawJSON, &f); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
retained = append(retained, f)
|
||||
}
|
||||
_ = retained
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDecodeUnique measures the allocation and speed of unmarshaling
|
||||
// completely unique payloads to measure the worst-case interning overhead.
|
||||
func BenchmarkDecodeUnique(b *testing.B) {
|
||||
for _, payload := range benchmarkPayloads {
|
||||
b.Run(fmt.Sprintf("Size%d", len(payload)), func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
var retained []metav1.FieldsV1
|
||||
for j := 0; j < b.N; j++ {
|
||||
// Inject the iteration counter to guarantee the string is unique
|
||||
novelPayload := fmt.Appendf(nil, `{"f:iter":%d,%s`, j, payload[1:])
|
||||
var f metav1.FieldsV1
|
||||
if err := json.Unmarshal(novelPayload, &f); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
retained = append(retained, f)
|
||||
}
|
||||
_ = retained
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParallelDecode tests parallel deserialization of duplicate strings
|
||||
// to verify lock contention behavior in highly concurrent environments.
|
||||
func BenchmarkParallelDecode(b *testing.B) {
|
||||
for _, payload := range benchmarkPayloads {
|
||||
b.Run(fmt.Sprintf("Size%d", len(payload)), func(b *testing.B) {
|
||||
rawJSON := []byte(payload)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
var f metav1.FieldsV1
|
||||
if err := json.Unmarshal(rawJSON, &f); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkEqual_Same measures the fast-path equality check for identical structs.
|
||||
func BenchmarkEqual_Same(b *testing.B) {
|
||||
f1 := metav1.NewFieldsV1(benchmarkPayloads[1])
|
||||
f2 := metav1.NewFieldsV1(benchmarkPayloads[1])
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = f1.Equal(*f2)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkEqual_Different measures the fallback-path equality check for differing structs.
|
||||
func BenchmarkEqual_Different(b *testing.B) {
|
||||
f1 := metav1.NewFieldsV1(benchmarkPayloads[1])
|
||||
f2 := metav1.NewFieldsV1(benchmarkPayloads[0])
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = f1.Equal(*f2)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !fieldsv1string
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
//go:build fieldsv1string
|
||||
|
||||
/*
|
||||
Copyright 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 v1
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unique"
|
||||
)
|
||||
|
||||
// FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.
|
||||
//
|
||||
// Each key is either a '.' representing the field itself, and will always map to an empty set,
|
||||
// or a string representing a sub-field or item. The string will follow one of these four formats:
|
||||
// 'f:<name>', where <name> is the name of a field in a struct, or key in a map
|
||||
// 'v:<value>', where <value> is the exact json formatted value of a list item
|
||||
// 'i:<index>', where <index> is position of a item in a list
|
||||
// 'k:<keys>', where <keys> is a map of a list item's key fields to their unique values
|
||||
// If a key maps to an empty Fields value, the field that key represents is part of the set.
|
||||
//
|
||||
// The exact format is defined in sigs.k8s.io/structured-merge-diff
|
||||
// +k8s:deepcopy-gen=false
|
||||
// +protobuf.options.marshal=false
|
||||
// +protobuf.options.(gogoproto.goproto_stringer)=false
|
||||
type FieldsV1 struct {
|
||||
// The zero value of a unique.Handle[string] has an uninitialized underlying pointer.
|
||||
// Calling .Value() on it panics. We must explicitly check for this uninitialized
|
||||
// state (f.handle == unique.Handle[string]{}) across accessors to safely support
|
||||
// uninitialized metav1.FieldsV1{} objects.
|
||||
// See ongoing golang discussion related to this here: https://github.com/golang/go/issues/73344
|
||||
handle unique.Handle[string]
|
||||
}
|
||||
|
||||
func (f FieldsV1) String() string {
|
||||
if f.handle == (unique.Handle[string]{}) {
|
||||
return ""
|
||||
}
|
||||
return f.handle.Value()
|
||||
}
|
||||
|
||||
func (f FieldsV1) Equal(f2 FieldsV1) bool {
|
||||
if f.handle == f2.handle {
|
||||
return true
|
||||
}
|
||||
// An uninitialized FieldsV1 compared to an explicitly empty
|
||||
// FieldsV1 (unique.Make("") will fail the handle check above.
|
||||
// Evaluate string contents directly as well to maintain parity with legacy
|
||||
// bytes.Equal(nil, []byte{}) == true behavior.
|
||||
return f.GetRawString() == f2.GetRawString()
|
||||
}
|
||||
|
||||
func (f *FieldsV1) GetRawReader() FieldsV1Reader {
|
||||
if f == nil || f.handle == (unique.Handle[string]{}) {
|
||||
return strings.NewReader("")
|
||||
}
|
||||
return strings.NewReader(f.handle.Value())
|
||||
}
|
||||
|
||||
// GetRawBytes returns the raw bytes.
|
||||
// These may or may not be a copy of the underlying bytes.
|
||||
// If mutating the underlying bytes is desired, the returned bytes may be mutated and then passed to SetRawBytes().
|
||||
// If mutating the underlying bytes is not desired, make a copy of the returned bytes.
|
||||
func (f *FieldsV1) GetRawBytes() []byte {
|
||||
if f == nil || f.handle == (unique.Handle[string]{}) {
|
||||
return nil
|
||||
}
|
||||
return []byte(f.handle.Value())
|
||||
}
|
||||
|
||||
// GetRawString returns the raw data as a string.
|
||||
func (f *FieldsV1) GetRawString() string {
|
||||
if f == nil || f.handle == (unique.Handle[string]{}) {
|
||||
return ""
|
||||
}
|
||||
return f.handle.Value()
|
||||
}
|
||||
|
||||
// SetRawBytes sets the raw bytes. It does not retain the passed-in byte slice.
|
||||
func (f *FieldsV1) SetRawBytes(b []byte) {
|
||||
if f != nil {
|
||||
f.handle = unique.Make(string(b))
|
||||
}
|
||||
}
|
||||
|
||||
// SetRawString sets the raw data from a string.
|
||||
func (f *FieldsV1) SetRawString(s string) {
|
||||
if f != nil {
|
||||
f.handle = unique.Make(s)
|
||||
}
|
||||
}
|
||||
|
||||
func NewFieldsV1(raw string) *FieldsV1 {
|
||||
return &FieldsV1{handle: unique.Make(raw)}
|
||||
}
|
||||
|
||||
func (f *FieldsV1) DeepCopyInto(out *FieldsV1) {
|
||||
*out = *f
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
//go:build fieldsv1string
|
||||
|
||||
/*
|
||||
Copyright 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 v1_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// requireInterned fails the test if the two strings do not point to the exact same memory address.
|
||||
func requireInterned(t *testing.T, a, b string) {
|
||||
t.Helper()
|
||||
ptrA := unsafe.StringData(a)
|
||||
ptrB := unsafe.StringData(b)
|
||||
if ptrA != ptrB {
|
||||
t.Fatalf("Expected strings to be interned (same memory address) but pointers differ: %p != %p", ptrA, ptrB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldsV1_String(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
f metav1.FieldsV1
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "zero value handle",
|
||||
f: metav1.FieldsV1{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "initialized empty handle",
|
||||
f: *metav1.NewFieldsV1(""),
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "valid payload",
|
||||
f: *metav1.NewFieldsV1(`{"f:app":{}}`),
|
||||
expected: `{"f:app":{}}`,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.f.String(); got != tc.expected {
|
||||
t.Errorf("expected %q, got %q", tc.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldsV1_Equal(t *testing.T) {
|
||||
valid := *metav1.NewFieldsV1(`{"f:app":{}}`)
|
||||
validClone := *metav1.NewFieldsV1(`{"f:app":{}}`)
|
||||
different := *metav1.NewFieldsV1(`{"f:other":{}}`)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
a metav1.FieldsV1
|
||||
b metav1.FieldsV1
|
||||
expected bool
|
||||
}{
|
||||
{"both zero value", metav1.FieldsV1{}, metav1.FieldsV1{}, true},
|
||||
{"zero value and initialized empty handle", metav1.FieldsV1{}, *metav1.NewFieldsV1(""), true},
|
||||
{"identical valid payloads", valid, validClone, true},
|
||||
{"different payloads", valid, different, false},
|
||||
{"zero value and valid payload", metav1.FieldsV1{}, valid, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.a.Equal(tc.b); got != tc.expected {
|
||||
t.Errorf("expected Equal() to be %v, got %v", tc.expected, got)
|
||||
}
|
||||
// Commutative property
|
||||
if got := tc.b.Equal(tc.a); got != tc.expected {
|
||||
t.Errorf("expected Equal() commutative to be %v, got %v", tc.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldsV1_GetRawReader(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
f *metav1.FieldsV1
|
||||
expected []byte
|
||||
}{
|
||||
{"nil receiver", nil, []byte("")},
|
||||
{"zero value handle", &metav1.FieldsV1{}, []byte("")},
|
||||
{"initialized empty handle", metav1.NewFieldsV1(""), []byte("")},
|
||||
{"valid payload", metav1.NewFieldsV1(`{"f:app":{}}`), []byte(`{"f:app":{}}`)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
reader := tc.f.GetRawReader()
|
||||
got, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading from RawReader: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, tc.expected) {
|
||||
t.Errorf("expected %q, got %q", tc.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldsV1_GetRawBytes(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
f *metav1.FieldsV1
|
||||
expected []byte
|
||||
}{
|
||||
{"nil receiver", nil, nil},
|
||||
{"zero value handle", &metav1.FieldsV1{}, nil},
|
||||
{"initialized empty handle", metav1.NewFieldsV1(""), []byte{}},
|
||||
{"valid payload", metav1.NewFieldsV1(`{"f:app":{}}`), []byte(`{"f:app":{}}`)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tc.f.GetRawBytes()
|
||||
if !bytes.Equal(got, tc.expected) {
|
||||
t.Errorf("expected %v, got %v", tc.expected, got)
|
||||
}
|
||||
// Explicitly check for nil
|
||||
if tc.expected == nil && got != nil {
|
||||
t.Errorf("expected strict nil, got %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("mutation safety", func(t *testing.T) {
|
||||
f := metav1.NewFieldsV1(`{"f:app":{}}`)
|
||||
b := f.GetRawBytes()
|
||||
|
||||
// Mutate the returned bytes
|
||||
b[2] = 'X'
|
||||
|
||||
// The original interned string must remain unchanged
|
||||
if got := f.GetRawString(); got != `{"f:app":{}}` {
|
||||
t.Errorf("GetRawBytes returned an unisolated slice! Mutating it corrupted the handle. Got: %s", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFieldsV1_GetRawString(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
f *metav1.FieldsV1
|
||||
expected string
|
||||
}{
|
||||
{"nil receiver", nil, ""},
|
||||
{"zero value handle", &metav1.FieldsV1{}, ""},
|
||||
{"initialized empty handle", metav1.NewFieldsV1(""), ""},
|
||||
{"valid payload", metav1.NewFieldsV1(`{"f:app":{}}`), `{"f:app":{}}`},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.f.GetRawString(); got != tc.expected {
|
||||
t.Errorf("expected %q, got %q", tc.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldsV1_SetRawBytes(t *testing.T) {
|
||||
t.Run("nil receiver", func(t *testing.T) {
|
||||
var f *metav1.FieldsV1 // nil
|
||||
f.SetRawBytes([]byte("test")) // Should not panic
|
||||
})
|
||||
|
||||
t.Run("empty slice input", func(t *testing.T) {
|
||||
f := &metav1.FieldsV1{}
|
||||
f.SetRawBytes([]byte{})
|
||||
if f.GetRawString() != "" {
|
||||
t.Errorf("Expected empty string, got %q", f.GetRawString())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil slice input", func(t *testing.T) {
|
||||
f := &metav1.FieldsV1{}
|
||||
f.SetRawBytes(nil)
|
||||
if f.GetRawString() != "" {
|
||||
t.Errorf("Expected empty string, got %q", f.GetRawString())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("interning behavior", func(t *testing.T) {
|
||||
payload := []byte(`{"f:app":{}}`)
|
||||
|
||||
// Two distinct slices with identical content
|
||||
b1 := append([]byte(nil), payload...)
|
||||
b2 := append([]byte(nil), payload...)
|
||||
|
||||
f1 := &metav1.FieldsV1{}
|
||||
f1.SetRawBytes(b1)
|
||||
|
||||
f2 := &metav1.FieldsV1{}
|
||||
f2.SetRawBytes(b2)
|
||||
|
||||
if f1.GetRawString() != string(payload) {
|
||||
t.Errorf("Expected GetRawString to match payload, got %q", f1.GetRawString())
|
||||
}
|
||||
|
||||
requireInterned(t, f1.GetRawString(), f2.GetRawString())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFieldsV1_SetRawString(t *testing.T) {
|
||||
t.Run("nil receiver", func(t *testing.T) {
|
||||
var f *metav1.FieldsV1 // nil
|
||||
f.SetRawString("test") // Should not panic
|
||||
})
|
||||
|
||||
t.Run("interning behavior", func(t *testing.T) {
|
||||
payload := `{"f:app":{}}`
|
||||
|
||||
// Force string copy to avoid compiler static string optimization if possible
|
||||
s1 := string(append([]byte(nil), payload...))
|
||||
s2 := string(append([]byte(nil), payload...))
|
||||
|
||||
f1 := &metav1.FieldsV1{}
|
||||
f1.SetRawString(s1)
|
||||
|
||||
f2 := &metav1.FieldsV1{}
|
||||
f2.SetRawString(s2)
|
||||
|
||||
requireInterned(t, f1.GetRawString(), f2.GetRawString())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFieldsV1_NewFieldsV1(t *testing.T) {
|
||||
t.Run("interning behavior", func(t *testing.T) {
|
||||
payload := `{"f:app":{}}`
|
||||
s1 := string(append([]byte(nil), payload...))
|
||||
s2 := string(append([]byte(nil), payload...))
|
||||
|
||||
f1 := metav1.NewFieldsV1(s1)
|
||||
f2 := metav1.NewFieldsV1(s2)
|
||||
|
||||
requireInterned(t, f1.GetRawString(), f2.GetRawString())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFieldsV1_DeepCopyInto(t *testing.T) {
|
||||
t.Run("preserves interning", func(t *testing.T) {
|
||||
orig := metav1.NewFieldsV1(`{"f:app":{}}`)
|
||||
var clone metav1.FieldsV1
|
||||
orig.DeepCopyInto(&clone)
|
||||
|
||||
if orig.GetRawString() != clone.GetRawString() {
|
||||
t.Fatalf("Expected strings to match after DeepCopyInto, got %q and %q", orig.GetRawString(), clone.GetRawString())
|
||||
}
|
||||
|
||||
requireInterned(t, orig.GetRawString(), clone.GetRawString())
|
||||
})
|
||||
|
||||
t.Run("zero value deep copy", func(t *testing.T) {
|
||||
var orig metav1.FieldsV1
|
||||
var clone metav1.FieldsV1
|
||||
orig.DeepCopyInto(&clone)
|
||||
|
||||
if clone.GetRawString() != "" {
|
||||
t.Errorf("Expected empty string from cloned zero-value, got %q", clone.GetRawString())
|
||||
}
|
||||
if !orig.Equal(clone) {
|
||||
t.Errorf("Expected original zero-value and its clone to be equal")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("independence after deep copy", func(t *testing.T) {
|
||||
orig := metav1.NewFieldsV1(`{"f:app":{}}`)
|
||||
var clone metav1.FieldsV1
|
||||
orig.DeepCopyInto(&clone)
|
||||
|
||||
// Modify the clone
|
||||
clone.SetRawString(`{"f:new":{}}`)
|
||||
|
||||
// The original should be completely untouched
|
||||
if got := orig.GetRawString(); got != `{"f:app":{}}` {
|
||||
t.Errorf("DeepCopyInto failed to isolate clone! Modifying clone corrupted original. Got: %s", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
func TestFieldsV1_UnmarshalInterning(t *testing.T) {
|
||||
jsonPayload := []byte(`{"f:metadata":{"f:labels":{"f:app":{}}}}`)
|
||||
orig := metav1.NewFieldsV1(string(jsonPayload))
|
||||
|
||||
protoPayload, err := orig.Marshal()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal protobuf payload during setup: %v", err)
|
||||
}
|
||||
|
||||
cborPayload, err := orig.MarshalCBOR()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal CBOR payload during setup: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
payload []byte
|
||||
unmarshal func(t *testing.T, f *metav1.FieldsV1, payload []byte)
|
||||
}{
|
||||
{
|
||||
name: "json unmarshal",
|
||||
payload: jsonPayload,
|
||||
unmarshal: func(t *testing.T, f *metav1.FieldsV1, payload []byte) {
|
||||
if err := json.Unmarshal(payload, f); err != nil {
|
||||
t.Fatalf("JSON Unmarshal failed: %v", err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "protobuf unmarshal",
|
||||
payload: protoPayload,
|
||||
unmarshal: func(t *testing.T, f *metav1.FieldsV1, payload []byte) {
|
||||
if err := f.Unmarshal(payload); err != nil {
|
||||
t.Fatalf("Protobuf Unmarshal failed: %v", err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cbor unmarshal",
|
||||
payload: cborPayload,
|
||||
unmarshal: func(t *testing.T, f *metav1.FieldsV1, payload []byte) {
|
||||
if err := f.UnmarshalCBOR(payload); err != nil {
|
||||
t.Fatalf("CBOR Unmarshal failed: %v", err)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Ensure independent slice memory
|
||||
b1 := append([]byte(nil), tc.payload...)
|
||||
b2 := append([]byte(nil), tc.payload...)
|
||||
|
||||
var f1 metav1.FieldsV1
|
||||
var f2 metav1.FieldsV1
|
||||
|
||||
tc.unmarshal(t, &f1, b1)
|
||||
tc.unmarshal(t, &f2, b2)
|
||||
|
||||
requireInterned(t, f1.GetRawString(), f2.GetRawString())
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user