Files
reflux/rfx_edge_test.go
T
what e594e206f0 test: 补上 addrOf 的测试覆盖
addrOf 是 unsafeptr.go 里唯一没有测试覆盖的函数。它的触发路径比较隐蔽:
interface 字段里装着一个 Accessor, 且它的 Raw() 返回**可寻址的非指针**值,
此时 fromReflect 才会走 CanAddr 分支。老实现 refx 的 Get 结果正是这种形态。

补测之后核心实现(rfx/typedesc/unsafeptr/path/generic)达到 100% 函数覆盖,
unsafe 代码逐个函数都有测试经过。
2026-08-28 16:56:08 +08:00

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)
}
}