package reflux import ( "encoding/json" "fmt" "testing" ) // walk 命中 map 之后会切到 walkReflect 走完剩余路径, 只在最后装箱一次 // (原来是逐跳 boxCopy)。这一组守的是"切换之后行为完全不变"。 // // 重点覆盖 map 之后的各种后继类型: struct / 指针 / 切片 / 数组 / 再一层 map / // interface / Accessor, 以及各自的失败形态。 type mwInner struct { Name string Tags []string } type mwDoc struct { Meta map[string]any Objs map[string]mwInner Ptrs map[string]*mwInner Nested map[string]map[string]any Arrs map[string][3]int Ifaces map[string]any Deep map[string]any } func newMWDoc() *mwDoc { return &mwDoc{ Meta: map[string]any{"s": "v", "n": 42, "b": true}, Objs: map[string]mwInner{"o": {Name: "inner", Tags: []string{"t0", "t1"}}}, Ptrs: map[string]*mwInner{"p": {Name: "ptr", Tags: []string{"p0"}}}, Nested: map[string]map[string]any{ "lv1": {"lv2": "deep"}, }, Arrs: map[string][3]int{"a": {10, 20, 30}}, Ifaces: map[string]any{"i": mwInner{Name: "iface"}}, Deep: map[string]any{ "a": map[string]any{"b": map[string]any{"c": map[string]any{"leaf": "bottom"}}}, }, } } func TestMapWalkMatchesLegacy(t *testing.T) { paths := [][]string{ // map -> 标量 {"Meta", "s"}, {"Meta", "n"}, {"Meta", "b"}, // map -> struct -> 字段 {"Objs", "o"}, {"Objs", "o", "Name"}, // map -> struct -> 切片 -> 元素 {"Objs", "o", "Tags"}, {"Objs", "o", "Tags", "1"}, // map -> 指针 -> 字段 {"Ptrs", "p"}, {"Ptrs", "p", "Name"}, {"Ptrs", "p", "Tags", "0"}, // map -> map -> 键 {"Nested", "lv1"}, {"Nested", "lv1", "lv2"}, // map -> 数组 -> 下标 {"Arrs", "a"}, {"Arrs", "a", "2"}, // map -> interface -> struct -> 字段 {"Ifaces", "i"}, {"Ifaces", "i", "Name"}, // 四层 map 链 {"Deep", "a"}, {"Deep", "a", "b"}, {"Deep", "a", "b", "c"}, {"Deep", "a", "b", "c", "leaf"}, {"Deep.a.b.c.leaf"}, // 失败形态 {"Meta", "nope"}, {"Objs", "o", "NoField"}, {"Objs", "nokey", "Name"}, {"Objs", "o", "Tags", "99"}, {"Arrs", "a", "9"}, {"Meta", "s", "further"}, {"Ptrs", "nokey", "Name"}, {"Deep", "a", "b", "nope", "leaf"}, } ops := []struct { name string f func(R) any }{ {"String", func(r R) any { return r.String() }}, {"Int", func(r R) any { return r.Int() }}, {"Any", func(r R) any { return fmt.Sprintf("%v", r.Any()) }}, {"Exists", func(r R) any { return r.Exists() }}, {"RawKind", func(r R) any { return r.Raw().Kind().String() }}, {"Keys", func(r R) any { return fmt.Sprintf("%v", r.Keys()) }}, {"JSON", func(r R) any { b, e := json.Marshal(r); return fmt.Sprintf("%s/%v", b, e) }}, {"Array长度", func(r R) any { return len(r.Array()) }}, } for _, p := range paths { for _, op := range ops { t.Run(fmt.Sprintf("%v/%s", p, op.name), func(t *testing.T) { want := call(newLegacy(newMWDoc()).Get(p...), op.f) got := call(New(newMWDoc()).Get(p...), op.f) if want != got { t.Fatalf("行为不一致\n 旧 = %s\n 新 = %s", want, got) } }) } } } // 经 map 的写入也要与旧实现一致(含写完之后整体数据的比对) func TestMapWalkWritesMatchLegacy(t *testing.T) { writes := []struct { path string val any }{ {"Meta.s", "changed"}, {"Meta.n", 7}, {"Meta.new", "added"}, {"Objs.o.Name", "changed"}, {"Objs.o.Tags.0", "changed"}, {"Ptrs.p.Name", "changed"}, {"Ptrs.p.Tags.0", "changed"}, {"Nested.lv1.lv2", "changed"}, {"Deep.a.b.c.leaf", "changed"}, {"Objs.o.NoField", "x"}, {"Objs.nokey.Name", "x"}, {"Meta.s.deeper", "x"}, } for _, w := range writes { t.Run(w.path, func(t *testing.T) { dOld, dNew := newMWDoc(), newMWDoc() rOld, rNew := newLegacy(dOld), New(dNew) resOld := call(rOld, func(r R) any { r.Set(w.path, w.val); return "ok" }) resNew := call(rNew, func(r R) any { r.Set(w.path, w.val); return "ok" }) if resOld != resNew { t.Fatalf("Set 行为不一致\n 旧 = %s\n 新 = %s", resOld, resNew) } bOld, _ := json.Marshal(dOld) bNew, _ := json.Marshal(dNew) if string(bOld) != string(bNew) { t.Fatalf("写入后数据不一致\n 旧 = %s\n 新 = %s", bOld, bNew) } }) } } // 根就是 map 的情况(没有任何 struct 跳) func TestMapWalkRootIsMap(t *testing.T) { mk := func() map[string]any { return map[string]any{ "a": map[string]any{"b": map[string]any{"leaf": "x", "n": 1}}, "s": "top", } } for _, p := range [][]string{ {"s"}, {"a"}, {"a", "b"}, {"a", "b", "leaf"}, {"a", "b", "n"}, {"a.b.leaf"}, {"a", "nope"}, {"a", "b", "leaf", "further"}, } { t.Run(fmt.Sprintf("%v", p), func(t *testing.T) { mo, mn := mk(), mk() want := call(newLegacy(&mo).Get(p...), func(r R) any { return r.String() }) got := call(New(&mn).Get(p...), func(r R) any { return r.String() }) if want != got { t.Fatalf("旧 = %s, 新 = %s", want, got) } }) } } // walk 命中 map 后只连续走完**这一段** map, 一离开就装箱切回快路径。 // 交界处最容易出错: 离开 map 段的那一跳如果被 walkMapRun 吃掉又没交还, // 路径就会少走一跳且不报错。这一组专门压交界。 func TestMapRunBoundary(t *testing.T) { type leaf struct{ V string } type mid struct { L leaf M map[string]any } type doc struct { M1 map[string]any M2 map[string]map[string]any } mk := func() *doc { return &doc{ M1: map[string]any{ "s": mid{L: leaf{V: "a"}, M: map[string]any{"k": "in-mid"}}, "sl": []any{mid{L: leaf{V: "in-slice"}}}, }, M2: map[string]map[string]any{ "x": {"s": mid{L: leaf{V: "b"}}}, }, } } paths := [][]string{ // map 之后紧跟 struct, 再往下走(交界在第 2 跳) {"M1", "s"}, {"M1", "s", "L"}, {"M1", "s", "L", "V"}, // map -> struct -> map(离开又进入) {"M1", "s", "M"}, {"M1", "s", "M", "k"}, // map -> map -> struct -> struct(连续两跳 map 后才离开) {"M2", "x"}, {"M2", "x", "s"}, {"M2", "x", "s", "L"}, {"M2", "x", "s", "L", "V"}, // map -> slice -> struct {"M1", "sl"}, {"M1", "sl", "0"}, {"M1", "sl", "0", "L", "V"}, // 点号写法应当与多参数完全一致 {"M2.x.s.L.V"}, {"M1.s.M.k"}, // 交界处的失败形态 {"M1", "s", "NoField"}, {"M1", "s", "L", "NoField"}, {"M2", "x", "s", "L", "V", "further"}, {"M1", "nokey", "L", "V"}, {"M2", "nokey", "s", "L", "V"}, } for _, p := range paths { t.Run(fmt.Sprintf("%v", p), func(t *testing.T) { want := call(newLegacy(mk()).Get(p...), func(r R) any { return r.String() }) got := call(New(mk()).Get(p...), func(r R) any { return r.String() }) if want != got { t.Fatalf("旧 = %s, 新 = %s", want, got) } // 再比一次 Exists, 防止"取到空值"和"路径不存在"被混为一谈 we := newLegacy(mk()).Get(p...).Exists() ge := New(mk()).Get(p...).Exists() if we != ge { t.Fatalf("Exists 不一致: 旧 = %v, 新 = %v", we, ge) } }) } }