package reflux // 本文件里的测试用**老的 reflect 实现 refx 作为参照**, 逐项对拍新实现 rfx。 // // 这么做的价值在于: refx 现在仍然作为冷路径存在于包内, 所以这套差分测试 // 可以长期保留 —— 任何时候改动 rfx 的快路径, 都能立刻和参照实现比出差异。 // 合并过程中 11 个真实的行为偏差就是这么发现的。 import ( "encoding/json" "fmt" "reflect" "testing" "git.fsdpf.net/go/reflux/valuex" ) // newLegacy 用老的 reflect 实现构造 R, 逻辑与合并前的 New 完全一致。 func newLegacy(v any) R { switch t := v.(type) { case nil: return Nil case R: return t case []R: return &refx{value: reflect.ValueOf(t)} default: if t == valuex.Nil { return Nil } } rv, isPtr, err := normalizeInputValue(v) if err != nil { panic(err) } if !rv.IsValid() { panic(ErrInvalidValue) } actualValue := rv for actualValue.Kind() == reflect.Ptr || actualValue.Kind() == reflect.Interface { if actualValue.IsNil() { panic(ErrTargetNilPointer) } actualValue = actualValue.Elem() if actualValue.Kind() == reflect.Ptr { isPtr = true } } switch actualValue.Kind() { case reflect.Map, reflect.Struct, reflect.Slice, reflect.Array, reflect.String, reflect.Bool, reflect.Float32, reflect.Float64: default: panic(NewErrUnsupportedTargetType(actualValue.Kind())) } if !isPtr { rv = DeepClone(actualValue) } return &refx{value: rv} } // R 的每一个方法都在这里与 reflux 对拍一遍。 // 前面的 compat 套件只能覆盖 reflux 自己测到的部分(45.2%), 这个文件补齐 // 整个接口契约, 回答"是不是真的全兼容"。 type contractDoc struct { Title string Count int Ratio float64 Ok bool Ptr *string UPtr uintptr I8 int8 I16 int16 I32 int32 U uint U8 uint8 U16 uint16 U32 uint32 U64 uint64 F32 float32 Tags []string Nums []int Flags []bool Anys []any SMap map[string]string IMap map[string]int I64Map map[string]int64 BMap map[string]bool AMap map[string]any SSMap map[string][]string Sub struct{ A, B string } } func newContractDoc() *contractDoc { s := "ptr" return &contractDoc{ Title: "doc", Count: 7, Ratio: 2.5, Ok: true, Ptr: &s, UPtr: 12, I8: 8, I16: 16, I32: 32, U: 1, U8: 2, U16: 3, U32: 4, U64: 5, F32: 1.25, Tags: []string{"a", "b"}, Nums: []int{1, 2, 3}, Flags: []bool{true, false}, Anys: []any{"x", 2, true}, SMap: map[string]string{"k": "v"}, IMap: map[string]int{"k": 1}, I64Map: map[string]int64{"k": 2}, BMap: map[string]bool{"k": true}, AMap: map[string]any{"k": "v"}, SSMap: map[string][]string{"k": {"a", "b"}}, Sub: struct{ A, B string }{"a", "b"}, } } // call 调用一个方法, 把结果格式化成可比较的字符串。 // // panic 也算一种结果, 而且**连错误信息一起比对** —— 错误文案属于行为契约的一部分, // 之前 "set failed at path 'Items.10': set failed at path '10': ..." 那次双重包装 // 就是只比对"有没有 panic"发现不了的。 func call(r R, f func(R) any) string { var out string func() { defer func() { if rec := recover(); rec != nil { out = "PANIC: " + fmt.Sprintf("%v", rec) } }() out = fmt.Sprintf("%v", f(r)) }() return out } // TestFullInterfaceContract 遍历 R 的全部方法, 逐个与 reflux 对拍。 func TestFullInterfaceContract(t *testing.T) { cases := []struct { name string f func(R) any }{ // --- 路径与结构 --- {"Get 单层", func(r R) any { return r.Get("Title").String() }}, {"Get 嵌套", func(r R) any { return r.Get("Sub", "A").String() }}, {"Get 点号", func(r R) any { return r.Get("Sub.B").String() }}, {"Get 指针字段", func(r R) any { return r.Get("Ptr").String() }}, {"Get slice 下标", func(r R) any { return r.Get("Tags", "1").String() }}, {"Get map 键", func(r R) any { return r.Get("SMap", "k").String() }}, {"Get 不存在", func(r R) any { return r.Get("Nope").String() }}, {"Exists 真", func(r R) any { return r.Exists("Sub", "A") }}, {"Exists 假", func(r R) any { return r.Exists("Nope") }}, {"Keys struct", func(r R) any { return r.Keys() }}, {"Keys map", func(r R) any { return r.Get("SMap").Keys() }}, {"Array slice", func(r R) any { return len(r.Get("Tags").Array()) }}, {"Array 元素值", func(r R) any { return r.Get("Tags").Array()[0].String() }}, {"Scope", func(r R) any { return r.Scope("Sub").Get("A").String() }}, // --- Lookuper (reflux 自己的测试完全没覆盖) --- {"Lookup 命中", func(r R) any { a, ok := r.Lookup("Sub.A") return fmt.Sprintf("%v/%v", a.String(), ok) }}, {"Lookup 未命中", func(r R) any { a, ok := r.Lookup("Nope") return fmt.Sprintf("%v/%v", a.String(), ok) }}, {"MustLookup 命中", func(r R) any { return r.MustLookup("Sub.A").String() }}, {"MustLookup 未命中", func(r R) any { return r.MustLookup("Nope").String() }}, // --- 标量访问器(全宽度) --- {"String", func(r R) any { return r.Get("Title").String() }}, {"Bool", func(r R) any { return r.Get("Ok").Bool() }}, {"Int", func(r R) any { return r.Get("Count").Int() }}, {"Int8", func(r R) any { return r.Get("I8").Int8() }}, {"Int16", func(r R) any { return r.Get("I16").Int16() }}, {"Int32", func(r R) any { return r.Get("I32").Int32() }}, {"Int64", func(r R) any { return r.Get("Count").Int64() }}, {"Uint", func(r R) any { return r.Get("U").Uint() }}, {"Uint8", func(r R) any { return r.Get("U8").Uint8() }}, {"Uint16", func(r R) any { return r.Get("U16").Uint16() }}, {"Uint32", func(r R) any { return r.Get("U32").Uint32() }}, {"Uint64", func(r R) any { return r.Get("U64").Uint64() }}, {"Float32", func(r R) any { return r.Get("F32").Float32() }}, {"Float64", func(r R) any { return r.Get("Ratio").Float64() }}, {"uintptr 字段读 Uint64", func(r R) any { return r.Get("UPtr").Uint64() }}, {"Any", func(r R) any { return r.Get("Count").Any() }}, {"Raw Kind", func(r R) any { return r.Get("Title").Raw().Kind().String() }}, {"Raw Kind 根", func(r R) any { return r.Raw().Kind().String() }}, {"Ptr 类型", func(r R) any { return fmt.Sprintf("%T", r.Get("Title").Ptr()) }}, // --- 容器转换(StringMap / StringMapInt64 两边都没测过) --- {"StringMapString", func(r R) any { return r.Get("SMap").StringMapString() }}, {"StringMapInt", func(r R) any { return r.Get("IMap").StringMapInt() }}, {"StringMapInt64", func(r R) any { return r.Get("I64Map").StringMapInt64() }}, {"StringMapBool", func(r R) any { return r.Get("BMap").StringMapBool() }}, {"StringMap", func(r R) any { return r.Get("AMap").StringMap() }}, {"StringMapStringSlice", func(r R) any { return r.Get("SSMap").StringMapStringSlice() }}, {"Slice", func(r R) any { return r.Get("Anys").Slice() }}, {"StringSlice", func(r R) any { return r.Get("Tags").StringSlice() }}, {"IntSlice", func(r R) any { return r.Get("Nums").IntSlice() }}, {"BoolSlice", func(r R) any { return r.Get("Flags").BoolSlice() }}, // --- JSON --- {"MarshalJSON", func(r R) any { b, err := json.Marshal(r.Get("Sub")) return fmt.Sprintf("%s/%v", b, err) }}, // --- 写操作 --- {"Set 标量", func(r R) any { r.Set("Title", "x"); return r.Get("Title").String() }}, {"Set 嵌套", func(r R) any { r.Set("Sub.A", "y"); return r.Get("Sub.A").String() }}, {"Set 跨类型", func(r R) any { r.Set("Count", "42"); return r.Get("Count").Int() }}, {"Set 指针字段", func(r R) any { r.Set("Ptr", "z"); return r.Get("Ptr").String() }}, {"Set slice 元素", func(r R) any { r.Set("Tags.0", "q"); return r.Get("Tags.0").String() }}, {"Set map 键", func(r R) any { r.Set("SMap.k", "w"); return r.Get("SMap.k").String() }}, {"Set 新 map 键", func(r R) any { r.Set("SMap.new", "n"); return r.Get("SMap.new").String() }}, {"Set 切片整体", func(r R) any { r.Set("Tags", []string{"p"}); return r.Get("Tags").StringSlice() }}, {"Set []any 转切片", func(r R) any { r.Set("Nums", []any{9, 8}); return r.Get("Nums").IntSlice() }}, {"Set 不存在字段", func(r R) any { r.Set("Nope", 1); return "no-panic" }}, {"Set slice 越界", func(r R) any { r.Set("Tags.99", "x"); return "no-panic" }}, {"Append", func(r R) any { r.Get("Tags").Append("c"); return r.Get("Tags").StringSlice() }}, {"Delete map 键", func(r R) any { r.Get("SMap").Delete("k"); return r.Get("SMap").Keys() }}, // --- 失败路径: 错误信息必须逐字一致 --- {"Set 复合目标类型不符", func(r R) any { r.Set("Sub", 42); return "no-panic" }}, {"Set 切片目标类型不符", func(r R) any { r.Set("Tags", 42); return "no-panic" }}, {"Set map 目标类型不符", func(r R) any { r.Set("SMap", 42); return "no-panic" }}, {"Set 嵌套路径不存在", func(r R) any { r.Set("Sub.Nope", "x"); return "no-panic" }}, {"Set 空路径", func(r R) any { r.Set("", 1); return "no-panic" }}, {"Set 未导出字段", func(r R) any { r.Set("unexported", 1); return "no-panic" }}, {"Set 到标量的子路径", func(r R) any { r.Set("Title.X", 1); return "no-panic" }}, {"Int 无法转换", func(r R) any { return r.Get("Title").Int() }}, {"Uint 负数", func(r R) any { r.Set("Count", -1); return r.Get("Count").Uint8() }}, {"Bool 无法转换", func(r R) any { return r.Get("Title").Bool() }}, {"Append 到非切片", func(r R) any { r.Get("Title").Append("x"); return "no-panic" }}, {"Delete 不存在的键", func(r R) any { r.Get("SMap").Delete("nope"); return r.Get("SMap").Keys() }}, {"UnmarshalJSON", func(r R) any { sub := r.Get("Sub") err := json.Unmarshal([]byte(`{"A":"ja","B":"jb"}`), sub) return fmt.Sprintf("%v/%v/%v", err, r.Get("Sub.A").String(), r.Get("Sub.B").String()) }}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { // 每个用例都用全新的对象, 避免写操作互相干扰 want := call(newLegacy(newContractDoc()), c.f) got := call(New(newContractDoc()), c.f) if want != got { t.Fatalf("行为不一致\n reflux = %s\n fastx = %s", want, got) } }) } }