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) } } } }