package reflux import ( "reflect" "testing" "time" ) // New(值) 的语义是"改副本不影响调用方的原数据"。为了省掉深拷贝,现在会先看 // 类型描述符里的 needsClone —— 不含引用成分的类型走逐字节浅拷贝。 // // 这一组测试守两件事: // 1. needsClone 的判定不能漏(漏了就会浅拷贝一个含引用的类型,写入穿透到原数据) // 2. 无论走哪条路,隔离语义都必须成立 // // 第 1 条是这次改动唯一的风险点:判错的后果是静默的数据串改,不报错也不 panic。 // ---- needsClone 判定 ---- type cPure struct { A, B string N int F float64 Ok bool Arr [3]int Sub struct{ X, Y string } } type cSlice struct { A string Tags []string } type cMap struct { A string Meta map[string]string } type cPtr struct { A string P *string } type cIface struct { A string V any } // 引用成分藏在两层嵌套之下 type cInner struct{ Tags []string } type cMid struct{ Inner cInner } type cDeepRef struct { A string Mid cMid } // 引用成分藏在数组元素里 type cArrRef struct { A string Arrs [2]cInner } // 同时含切片和 map, 用于验证传指针不触发深拷贝 type cBoth struct { A string Tags []string Meta map[string]string } // 自引用 type cNode struct { Name string Next *cNode } func TestNeedsCloneDetection(t *testing.T) { cases := []struct { name string typ reflect.Type want bool }{ {"纯值 struct", reflect.TypeOf(cPure{}), false}, {"含切片", reflect.TypeOf(cSlice{}), true}, {"含 map", reflect.TypeOf(cMap{}), true}, {"含指针", reflect.TypeOf(cPtr{}), true}, {"含 interface", reflect.TypeOf(cIface{}), true}, {"引用藏在两层嵌套下", reflect.TypeOf(cDeepRef{}), true}, {"引用藏在数组元素里", reflect.TypeOf(cArrRef{}), true}, {"自引用类型", reflect.TypeOf(cNode{}), true}, {"time.Time(内含 *Location)", reflect.TypeOf(time.Time{}), true}, {"裸切片", reflect.TypeOf([]string{}), true}, {"裸 map", reflect.TypeOf(map[string]string{}), true}, {"纯值数组", reflect.TypeOf([3]int{}), false}, {"字符串不算引用", reflect.TypeOf(""), false}, {"纯值嵌套 struct", reflect.TypeOf(struct{ S struct{ A string } }{}), false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := rfxDescriptorOf(c.typ).needsClone; got != c.want { t.Fatalf("needsClone = %v, 期望 %v", got, c.want) } }) } } // ---- 隔离语义:无论走哪条路都必须成立 ---- func TestNewByValueIsolation(t *testing.T) { t.Run("纯值 struct(走浅拷贝)", func(t *testing.T) { orig := cPure{A: "a", N: 1, Arr: [3]int{1, 2, 3}} orig.Sub.X = "x" r := New(orig) r.Set("A", "changed") r.Set("N", 99) r.Set("Arr.0", 100) r.Set("Sub.X", "changed") if orig.A != "a" || orig.N != 1 || orig.Arr[0] != 1 || orig.Sub.X != "x" { t.Fatalf("原数据被改写了: %+v", orig) } if r.Get("A").String() != "changed" || r.Get("Arr.0").Int() != 100 { t.Fatal("副本上的写入没生效") } }) t.Run("含切片(走深拷贝)", func(t *testing.T) { orig := cSlice{A: "a", Tags: []string{"x", "y"}} r := New(orig) r.Set("Tags.0", "changed") if orig.Tags[0] != "x" { t.Fatalf("原切片被改写了: %v", orig.Tags) } }) t.Run("含 map(走深拷贝)", func(t *testing.T) { orig := cMap{A: "a", Meta: map[string]string{"k": "v"}} r := New(orig) r.Set("Meta.k", "changed") if orig.Meta["k"] != "v" { t.Fatalf("原 map 被改写了: %v", orig.Meta) } }) t.Run("引用藏在两层嵌套下", func(t *testing.T) { orig := cDeepRef{A: "a", Mid: cMid{Inner: cInner{Tags: []string{"x"}}}} r := New(orig) r.Set("Mid.Inner.Tags.0", "changed") if orig.Mid.Inner.Tags[0] != "x" { t.Fatalf("深层的原切片被改写了: %v", orig.Mid.Inner.Tags) } }) t.Run("引用藏在数组元素里", func(t *testing.T) { orig := cArrRef{A: "a"} orig.Arrs[0] = cInner{Tags: []string{"x"}} r := New(orig) r.Set("Arrs.0.Tags.0", "changed") if orig.Arrs[0].Tags[0] != "x" { t.Fatalf("数组元素里的原切片被改写了: %v", orig.Arrs[0].Tags) } }) t.Run("裸 map", func(t *testing.T) { orig := map[string]any{"k": "v"} r := New(orig) r.Set("k", "changed") if orig["k"] != "v" { t.Fatalf("原 map 被改写了: %v", orig) } }) t.Run("裸切片", func(t *testing.T) { orig := []string{"x", "y"} r := New(orig) r.Set("0", "changed") if orig[0] != "x" { t.Fatalf("原切片被改写了: %v", orig) } }) } // 与旧实现逐格对拍:浅拷贝路径的可观察行为必须与 DeepClone 完全一致。 func TestNewByValueMatchesLegacy(t *testing.T) { mk := []struct { name string val func() any }{ {"纯值 struct", func() any { return cPure{A: "a", N: 7, F: 1.5, Ok: true, Arr: [3]int{1, 2, 3}} }}, {"纯值数组", func() any { return [3]int{1, 2, 3} }}, {"含切片", func() any { return cSlice{A: "a", Tags: []string{"x"}} }}, {"含 map", func() any { return cMap{A: "a", Meta: map[string]string{"k": "v"}} }}, {"纯值嵌套", func() any { return struct{ S struct{ A string } }{} }}, } ops := []struct { name string f func(R) any }{ {"RawKind", func(r R) any { return r.Raw().Kind().String() }}, {"Keys", func(r R) any { return r.Keys() }}, {"JSON", func(r R) any { b, e := r.MarshalJSON(); return string(b) + "/" + errStr(e) }}, {"Any", func(r R) any { return r.Any() }}, {"PtrType", func(r R) any { return reflect.TypeOf(r.Ptr()).String() }}, } for _, m := range mk { for _, op := range ops { t.Run(m.name+"/"+op.name, func(t *testing.T) { want := call(newLegacy(m.val()), op.f) got := call(New(m.val()), op.f) if want != got { t.Fatalf("行为不一致\n 旧 = %s\n 新 = %s", want, got) } }) } } } func errStr(e error) string { if e == nil { return "" } return e.Error() } // README 建议"数据量大或想改原数据时传指针"。这一组把那个承诺钉住: // 传指针必须共享底层数据、且不做任何复制。 func TestPointerInputSharesAndSkipsClone(t *testing.T) { t.Run("map 写入穿透", func(t *testing.T) { m := map[string]any{"k": "v"} New(&m).Set("k", "changed") if m["k"] != "changed" { t.Fatalf("传指针应当共享底层 map, 调用方看到 %v", m["k"]) } }) t.Run("slice 元素写入穿透", func(t *testing.T) { s := []string{"a"} New(&s).Set("0", "changed") if s[0] != "changed" { t.Fatalf("传指针应当共享底层数组, 调用方看到 %v", s) } }) t.Run("struct 字段写回调用方变量", func(t *testing.T) { d := cPure{A: "a"} New(&d).Set("A", "changed") if d.A != "changed" { t.Fatal("传指针应当写回调用方的 struct 变量") } }) t.Run("含引用的大对象传指针不触发深拷贝", func(t *testing.T) { v := cBoth{A: "a", Tags: []string{"x"}, Meta: map[string]string{"k": "v"}} // 传指针路径不该调用 DeepClone —— 用"写入穿透"反证没有复制 New(&v).Set("Tags.0", "changed") New(&v).Set("Meta.k", "changed") if v.Tags[0] != "changed" || v.Meta["k"] != "changed" { t.Fatalf("传指针不该复制, 实际 %+v", v) } }) t.Run("Scope 仍可在传指针的基础上取隔离副本", func(t *testing.T) { v := cBoth{A: "a", Tags: []string{"x"}} s := New(&v).Scope() s.Set("Tags.0", "changed") if v.Tags[0] != "x" { t.Fatalf("Scope 应当隔离, 原数据被改成 %v", v.Tags) } }) }