addrOf 是 unsafeptr.go 里唯一没有测试覆盖的函数。它的触发路径比较隐蔽: interface 字段里装着一个 Accessor, 且它的 Raw() 返回**可寻址的非指针**值, 此时 fromReflect 才会走 CanAddr 分支。老实现 refx 的 Get 结果正是这种形态。 补测之后核心实现(rfx/typedesc/unsafeptr/path/generic)达到 100% 函数覆盖, unsafe 代码逐个函数都有测试经过。
337 lines
9.7 KiB
Go
337 lines
9.7 KiB
Go
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 "<nil>"
|
|
}
|
|
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
|
|
}
|
|
|
|
// addrOf 是 unsafeptr.go 里唯一一个此前没有测试覆盖的函数, 这里补上。
|
|
//
|
|
// 触发路径: interface 字段里装着一个 Accessor, 且它的 Raw() 返回**可寻址的
|
|
// 非指针** reflect.Value —— 此时 fromReflect 会走 rv.CanAddr() 分支调 addrOf。
|
|
// 老实现 refx 的 Get 结果正是这种形态(字段值可寻址, 但不是指针)。
|
|
func TestAddrOfPathViaAddressableAccessor(t *testing.T) {
|
|
type leaf struct{ Name string }
|
|
type holder struct{ Any any }
|
|
|
|
src := &struct{ L leaf }{L: leaf{Name: "addressable"}}
|
|
// refx.Get 返回的 R, 其 Raw() 是可寻址的非指针值
|
|
acc := newLegacy(src).Get("L")
|
|
if k := acc.Raw().Kind(); k != reflect.Struct {
|
|
t.Fatalf("前置条件不成立: Raw().Kind() = %v, 期望 Struct", k)
|
|
}
|
|
if !acc.Raw().CanAddr() {
|
|
t.Fatal("前置条件不成立: Raw() 应该是可寻址的")
|
|
}
|
|
|
|
h := &holder{Any: acc}
|
|
r := New(h)
|
|
|
|
if got := r.Get("Any", "Name").String(); got != "addressable" {
|
|
t.Fatalf("经可寻址 Accessor 取值 = %q, 期望 addressable", got)
|
|
}
|
|
// 与参照实现一致
|
|
if want := newLegacy(&holder{Any: acc}).Get("Any", "Name").String(); want != "addressable" {
|
|
t.Fatalf("参照实现结果 = %q", want)
|
|
}
|
|
|
|
// 可寻址意味着写回去能反映到原对象上
|
|
r.Set("Any.Name", "changed")
|
|
if src.L.Name != "changed" {
|
|
t.Fatalf("经可寻址 Accessor 写入未落到原对象: %q", src.L.Name)
|
|
}
|
|
}
|