Files
felhom-controller/controller/internal/stacks/r591_copy_meta_alias_test.go
T

110 lines
3.0 KiB
Go

package stacks
import (
"reflect"
"testing"
)
// R-591 follow-up: deepCopyStack's contract is "a snapshot the caller may mutate", and the I18n hole
// was not the only one — DataPaths, AfterLoad (and every other reference added to Metadata after the
// copy was written) stayed shared. Instead of one assertion per field, this fills EVERY pointer, slice
// and map reachable from Stack.Meta with a non-empty value and then walks the copy beside the original:
// any reference the two share is a field a caller's write would leak through. A field added to
// Metadata tomorrow is covered the day it is added.
func TestDeepCopyStackMetaSharesNoReference(t *testing.T) {
orig := &Stack{Name: "demo"}
fillAll(reflect.ValueOf(&orig.Meta).Elem(), 0)
cp := deepCopyStack(orig)
var shared []string
findAliases(reflect.ValueOf(orig.Meta), reflect.ValueOf(cp.Meta), "Meta", &shared)
for _, p := range shared {
t.Errorf("R-591: deepCopyStack leaves %s shared with the original — a write through the copy changes the stack", p)
}
}
// fillAll gives every pointer, slice (one element) and map (one entry) under v a non-nil value.
func fillAll(v reflect.Value, depth int) {
if depth > 6 || !v.CanSet() && v.Kind() != reflect.Struct {
return
}
switch v.Kind() {
case reflect.Ptr:
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
fillAll(v.Elem(), depth+1)
case reflect.Slice:
if v.Len() == 0 {
v.Set(reflect.MakeSlice(v.Type(), 1, 1))
}
fillAll(v.Index(0), depth+1)
case reflect.Map:
if v.Len() == 0 {
m := reflect.MakeMap(v.Type())
k := reflect.New(v.Type().Key()).Elem()
if k.Kind() == reflect.String {
k.SetString("k")
}
e := reflect.New(v.Type().Elem()).Elem()
fillAll(e, depth+1)
m.SetMapIndex(k, e)
v.Set(m)
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
if v.Type().Field(i).IsExported() {
fillAll(v.Field(i), depth+1)
}
}
case reflect.String:
if v.String() == "" {
v.SetString("x")
}
}
}
// findAliases records every pointer/slice/map under a and b that points at the same memory.
func findAliases(a, b reflect.Value, path string, out *[]string) {
switch a.Kind() {
case reflect.Ptr:
if a.IsNil() || b.IsNil() {
return
}
if a.Pointer() == b.Pointer() {
*out = append(*out, path)
return
}
findAliases(a.Elem(), b.Elem(), path, out)
case reflect.Slice:
if a.Len() == 0 || b.Len() == 0 {
return
}
if a.Pointer() == b.Pointer() {
*out = append(*out, path)
return
}
for i := 0; i < a.Len() && i < b.Len(); i++ {
findAliases(a.Index(i), b.Index(i), path+"[]", out)
}
case reflect.Map:
if a.Len() == 0 || b.Len() == 0 {
return
}
if a.Pointer() == b.Pointer() {
*out = append(*out, path)
return
}
for _, k := range a.MapKeys() {
if bv := b.MapIndex(k); bv.IsValid() {
findAliases(a.MapIndex(k), bv, path+"[k]", out)
}
}
case reflect.Struct:
for i := 0; i < a.NumField(); i++ {
if a.Type().Field(i).IsExported() {
findAliases(a.Field(i), b.Field(i), path+"."+a.Type().Field(i).Name, out)
}
}
}
}