diff --git a/libs/dyn/value.go b/libs/dyn/value.go index e4d72a0b203..72803511b80 100644 --- a/libs/dyn/value.go +++ b/libs/dyn/value.go @@ -206,9 +206,20 @@ func (v Value) eq(w Value) bool { switch v.k { case KindMap: - // Compare pointers to the underlying map. + // Comparing &v.v to &w.v is always false (value receivers), so compare the Mapping's backing storage. + vm := v.v.(Mapping) + wm := w.v.(Mapping) + lv := vm.Len() + lw := wm.Len() + if lv == 0 && lw == 0 { + return true + } + if lv != lw { + return false + } + // Compare pointers to the underlying pairs slice. // This is safe because we don't allow maps to be mutated. - return &v.v == &w.v + return &vm.pairs[0] == &wm.pairs[0] case KindSequence: vs := v.v.([]Value) ws := w.v.([]Value) diff --git a/libs/dyn/value_eq_test.go b/libs/dyn/value_eq_test.go new file mode 100644 index 00000000000..83c255edd54 --- /dev/null +++ b/libs/dyn/value_eq_test.go @@ -0,0 +1,76 @@ +package dyn + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValueEqMap(t *testing.T) { + loc := []Location{{File: "file", Line: 1, Column: 2}} + v := NewValue(map[string]Value{"key": V("value")}, loc) + + tests := []struct { + name string + a, b Value + want bool + }{ + { + name: "same underlying mapping", + a: v, + b: v, + want: true, + }, + { + name: "cloned mapping with equal contents", + a: v, + b: Value{v: v.v.(Mapping).Clone(), k: KindMap, l: v.l}, + want: false, + }, + { + name: "different lengths", + a: v, + b: NewValue(map[string]Value{"key": V("value"), "other": V("value")}, loc), + want: false, + }, + { + name: "different locations", + a: v, + b: v.WithLocations([]Location{{File: "other", Line: 1, Column: 2}}), + want: false, + }, + { + name: "empty mappings", + a: NewValue(map[string]Value{}, loc), + b: NewValue(map[string]Value{}, loc), + want: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, tc.a.eq(tc.b)) + }) + } +} + +func TestValueEqMapVisitDoesNotRebuildAncestors(t *testing.T) { + vin := V(map[string]Value{ + "a": V(map[string]Value{ + "b": V("value"), + }), + }) + + vout, err := Map(vin, "a.b", func(_ Path, v Value) (Value, error) { + return v, nil + }) + require.NoError(t, err) + + // The identity transform must return the original value without + // cloning the ancestor maps. + vm := vin.v.(Mapping) + wm := vout.v.(Mapping) + require.Equal(t, vm.Len(), wm.Len()) + assert.Same(t, &vm.pairs[0], &wm.pairs[0]) +}