package reflux import ( "encoding/json" "fmt" "os" "os/exec" "reflect" "sort" "strings" "testing" "git.fsdpf.net/go/reflux/valuex" ) // 两个之前没验证过的边角场景: // 1. 深度嵌套的 []any 里放 R // 2. 循环引用对象 // --------------------------------------------------------------------------- // 1. []any / []R 里放 R // --------------------------------------------------------------------------- // mkR 用给定构造器建出一组嵌套的 R, 供两边分别构造同构的输入。 func mkR(newR func(any) R) (R, R) { return newR(map[string]any{"n": "one", "deep": map[string]any{"k": "v"}}), newR(map[string]any{"n": "two"}) } func TestAnySliceContainingR(t *testing.T) { cases := []struct { name string f func(newR func(any) R) any }{ {"New([]any{R,R}) 长度", func(n func(any) R) any { a, b := mkR(n) return len(n([]any{a, b}).Array()) }}, {"New([]any{R,R}) 取元素", func(n func(any) R) any { a, b := mkR(n) return n([]any{a, b}).Get("0", "n").String() }}, {"New([]R{R,R}) 取元素", func(n func(any) R) any { a, b := mkR(n) return n([]R{a, b}).Get("1", "n").String() }}, {"嵌套一层 []any{[]any{R}}", func(n func(any) R) any { a, _ := mkR(n) return n([]any{[]any{a}}).Get("0", "0", "n").String() }}, {"嵌套三层 + 穿透到 R 内部", func(n func(any) R) any { a, _ := mkR(n) return n([]any{[]any{[]any{a}}}).Get("0.0.0.deep.k").String() }}, {"R 装在 map 里再嵌套", func(n func(any) R) any { a, _ := mkR(n) return n(map[string]any{"list": []any{a}}).Get("list", "0", "n").String() }}, {"Array 元素再取值", func(n func(any) R) any { a, b := mkR(n) return n([]any{a, b}).Array()[1].Get("n").String() }}, {"Slice()", func(n func(any) R) any { a, b := mkR(n) return fmt.Sprintf("%v", n([]any{a, b}).Slice()) }}, {"Keys 穿透到 R", func(n func(any) R) any { a, _ := mkR(n) // map 的 Keys 顺序在 Go 里不确定, 排序后再比 k := n([]any{a}).Get("0").Keys() sort.Strings(k) return k }}, {"json.Marshal([]any{R})", func(n func(any) R) any { a, _ := mkR(n) b, err := json.Marshal(n([]any{a})) return fmt.Sprintf("%s / err=%v", b, err) }}, {"json.Marshal 嵌套两层", func(n func(any) R) any { a, _ := mkR(n) b, err := json.Marshal(n([]any{[]any{a}})) return fmt.Sprintf("%s / err=%v", b, err) }}, {"Set 字段为 []any{R,R}", func(n func(any) R) any { a, b := mkR(n) d := &struct{ Items []any }{} n(d).Set("Items", []any{a, b}) return fmtDeref(d.Items) }}, {"Set 字段为 []R", func(n func(any) R) any { a, b := mkR(n) d := &struct{ Items []any }{} n(d).Set("Items", []R{a, b}) return fmtDeref(d.Items) }}, {"Append R 到 []any", func(n func(any) R) any { a, _ := mkR(n) d := &struct{ Items []any }{} n(d).Get("Items").Append(a) return fmtDeref(d.Items) }}, {"Scope 穿透 R", func(n func(any) R) any { a, _ := mkR(n) return n([]any{a}).Scope("0").Get("n").String() }}, {"Exists 穿透 R", func(n func(any) R) any { a, _ := mkR(n) return n([]any{a}).Exists("0", "deep", "k") }}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { want := callGen(newLegacy, c.f) got := callGen(New, c.f) if want != got { t.Fatalf("行为不一致\n reflux = %s\n fastx = %s", want, got) } }) } } // fmtDeref 按内容格式化 []any, 把指针和 Accessor 解开。 // // 两边存进去的都是指针, 直接 %v 比的是**地址**, 永远不会相等 —— // 那是测试写法的问题, 不是行为差异。这里比内容。 func fmtDeref(items []any) string { parts := make([]string, len(items)) for i, it := range items { parts[i] = fmt.Sprintf("%v", derefAny(it)) } return "[" + strings.Join(parts, " ") + "]" } func derefAny(v any) any { if acc, ok := v.(valuex.Accessor); ok { if raw := acc.Raw(); raw.IsValid() { return derefReflectAny(raw) } return nil } rv := reflect.ValueOf(v) return derefReflectAny(rv) } func derefReflectAny(rv reflect.Value) any { for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface { if rv.IsNil() { return nil } rv = rv.Elem() } if !rv.IsValid() { return nil } return rv.Interface() } // callGen 与 contract_test.go 的 call 同理, 但把构造器也参数化, // 保证两边拿到的是**各自构造**的同构输入(而不是共用 reflux 造的 R)。 func callGen(newR func(any) R, f func(func(any) 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(newR)) }() return out } // --------------------------------------------------------------------------- // 2. 循环引用 // --------------------------------------------------------------------------- type cycNode struct { Name string Next *cycNode Self any } func newCycle() *cycNode { n := &cycNode{Name: "a"} n.Next = n n.Self = n return n } func TestCircularReference(t *testing.T) { cases := []struct { name string f func(newR func(any) R) any }{ {"沿指针环 Get 一圈", func(n func(any) R) any { return n(newCycle()).Get("Next", "Next", "Next", "Name").String() }}, {"点号路径绕环", func(n func(any) R) any { return n(newCycle()).Get("Next.Next.Next.Next.Name").String() }}, {"经 interface 字段绕环", func(n func(any) R) any { return n(newCycle()).Get("Self", "Self", "Name").String() }}, {"环上 Set", func(n func(any) R) any { c := newCycle() n(c).Set("Next.Next.Name", "changed") return c.Name // 环回自身, 应该改到同一个对象 }}, {"Exists 绕环", func(n func(any) R) any { return n(newCycle()).Exists("Next", "Next", "Next", "Name") }}, {"Keys 绕环", func(n func(any) R) any { return n(newCycle()).Get("Next", "Next").Keys() }}, {"json.Marshal 循环对象", func(n func(any) R) any { b, err := json.Marshal(n(newCycle())) return fmt.Sprintf("%s / err=%v", b, cycleErrText(err)) }}, {"json.Marshal 环上某一跳", func(n func(any) R) any { b, err := json.Marshal(n(newCycle()).Get("Next")) return fmt.Sprintf("%s / err=%v", b, cycleErrText(err)) }}, {"map 自引用 Get", func(n func(any) R) any { m := map[string]any{"name": "m"} m["self"] = m return n(&m).Get("self", "self", "name").String() }}, {"map 自引用 json.Marshal", func(n func(any) R) any { m := map[string]any{"name": "m"} m["self"] = m b, err := json.Marshal(n(&m)) return fmt.Sprintf("%s / err=%v", b, cycleErrText(err)) }}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { want := callGen(newLegacy, c.f) got := callGen(New, c.f) if want != got { t.Fatalf("行为不一致\n reflux = %s\n fastx = %s", want, got) } }) } } // cycleErrText 把错误里的具体类型名抹掉 —— reflux 报的是 *reflux.rfx, // fastx 报的是 *fastx.Value, 这属于实现细节而非行为差异。 // 保留"是否检测到环"这个关键信息。 func cycleErrText(err error) string { if err == nil { return "" } s := err.Error() if strings.Contains(s, "encountered a cycle") { return "cycle-detected" } return s } // --------------------------------------------------------------------------- // DeepClone 遇到循环引用会栈溢出(fatal, 不可 recover), 只能在子进程里验证。 // 这是 reflux 既有的限制, fastx 复用 reflux.DeepClone 所以行为一致。 // --------------------------------------------------------------------------- func TestDeepCloneOnCycleIsFatalForBoth(t *testing.T) { if impl := os.Getenv("FASTX_CYCLE_IMPL"); impl != "" { // 子进程: 真的去做深拷贝, 预期栈溢出 n := newCycle() if impl == "reflux" { newLegacy(n).Scope() } else { New(n).Scope() } return } for _, impl := range []string{"reflux", "fastx"} { t.Run(impl, func(t *testing.T) { cmd := exec.Command(os.Args[0], "-test.run=^TestDeepCloneOnCycleIsFatalForBoth$", "-test.timeout=60s") cmd.Env = append(os.Environ(), "FASTX_CYCLE_IMPL="+impl) out, err := cmd.CombinedOutput() if err == nil { t.Fatalf("%s: 预期栈溢出, 却正常返回了", impl) } if !strings.Contains(string(out), "stack overflow") { t.Fatalf("%s: 预期 stack overflow, 实际输出:\n%s", impl, truncate(string(out), 300)) } }) } } func truncate(s string, n int) string { if len(s) > n { return s[:n] + "..." } return s }