feat: replace mapping generation with checked copy utilities

This commit is contained in:
2026-09-25 17:27:28 +08:00
parent ad9257d202
commit fd83235ce7
45 changed files with 2366 additions and 1872 deletions

448
copyx/copy_test.go Normal file
View File

@@ -0,0 +1,448 @@
package copyx_test
import (
"errors"
"fmt"
"reflect"
"strings"
"sync"
"testing"
"time"
"git.yuntuo.hk/k2safe/go-omni/copyx"
)
type detail struct {
Values []int
Index map[string][]int
Next *detail
}
func TestDeepGraph(t *testing.T) {
src := &detail{Values: []int{1, 2}, Index: map[string][]int{"a": {3}}}
src.Next = src
dst, err := copyx.Clone(src)
if err != nil {
t.Fatal(err)
}
if dst == src || dst.Next != dst {
t.Fatal("pointer cycle not preserved")
}
dst.Values[0] = 9
dst.Index["a"][0] = 8
if src.Values[0] != 1 || src.Index["a"][0] != 3 {
t.Fatal("deep alias")
}
m := map[string]any{}
m["self"] = m
n, err := copyx.Clone(m)
if err != nil {
t.Fatal(err)
}
n["self"].(map[string]any)["new"] = 1
if n["new"] != 1 || m["new"] != nil {
t.Fatal("map cycle changed")
}
x := []any{nil}
x[0] = x
y, err := copyx.Clone(x)
if err != nil {
t.Fatal(err)
}
y[0].([]any)[0] = "changed"
if y[0] != "changed" {
t.Fatal("slice cycle changed")
}
if _, ok := x[0].([]any); !ok {
t.Fatal("source slice changed")
}
}
func TestShallowAndNil(t *testing.T) {
src := detail{Values: []int{1}}
var dst detail
if err := copyx.Copy(&dst, src); err != nil {
t.Fatal(err)
}
dst.Values[0] = 2
if src.Values[0] != 2 {
t.Fatal("shallow unexpectedly detached")
}
for _, slice := range [][]int{nil, {}} {
out, err := copyx.Clone(slice)
if err != nil || (out == nil) != (slice == nil) {
t.Fatal(out, err)
}
}
for _, m := range []map[string]int{nil, {}} {
out, err := copyx.Clone(m)
if err != nil || (out == nil) != (m == nil) {
t.Fatal(out, err)
}
}
var p *int
n := 7
if err := copyx.Copy(&n, p); !errors.Is(err, copyx.ErrNil) || n != 7 {
t.Fatal(n, err)
}
if err := copyx.Copy(&n, p, copyx.WithNilPolicy(copyx.NilSkip)); err != nil || n != 7 {
t.Fatal(n, err)
}
if err := copyx.Copy(&n, p, copyx.WithNilPolicy(copyx.NilZero)); err != nil || n != 0 {
t.Fatal(n, err)
}
x := 3
out := &x
if err := copyx.Copy(&out, p); err != nil || out != nil {
t.Fatal(out, err)
}
flag := true
if err := copyx.Copy(&flag, false); err != nil || flag {
t.Fatal(flag, err)
}
flag = true
if err := copyx.Copy(&flag, false, copyx.WithSkipZero()); err != nil || !flag {
t.Fatal(flag, err)
}
}
func TestConverterAliasesRejected(t *testing.T) {
for _, kind := range []string{"same", "nested", "subslice", "capacity", "fieldPointer", "private", "map", "sibling"} {
t.Run(kind, func(t *testing.T) {
src := &detail{Values: []int{1, 2, 3}, Index: map[string][]int{"x": {1}}}
var err error
switch kind {
case "same":
_, err = copyx.Clone(src, copyx.WithConverter(func(v *detail) (*detail, error) { return v, nil }))
case "nested":
_, err = copyx.Clone(src, copyx.WithConverter(func(v *detail) (*detail, error) { return &detail{Values: v.Values}, nil }))
case "subslice":
_, err = copyx.Clone(src.Values, copyx.WithConverter(func(v []int) ([]int, error) { return v[1:], nil }))
case "capacity":
_, err = copyx.Clone(src.Values, copyx.WithConverter(func(v []int) ([]int, error) { return v[:0], nil }))
case "map":
_, err = copyx.Clone(src.Index, copyx.WithConverter(func(v map[string][]int) (map[string][]int, error) { return v, nil }))
case "fieldPointer":
var dst *int
err = copyx.Copy(&dst, src.Values, copyx.WithDeepCopy(), copyx.WithConverter(func(v []int) (*int, error) { return &v[1], nil }))
case "private":
type opaque struct{ data []int }
v := opaque{src.Values}
_, err = copyx.Clone(v, copyx.WithConverter(func(v opaque) (opaque, error) { return v, nil }))
case "sibling":
type object struct{ A, B []int }
v := object{[]int{1}, []int{2}}
_, err = copyx.Clone(v, copyx.WithConverter(func(_ []int) ([]int, error) { return v.B, nil }))
}
if !errors.Is(err, copyx.ErrSharedReference) {
t.Fatalf("got %v", err)
}
})
}
_, err := copyx.Clone(4, copyx.WithConverter(func(v int) (int, error) { return v, nil }))
if err != nil {
t.Fatal("equal primitive rejected", err)
}
_, err = copyx.Clone("same", copyx.WithConverter(func(v string) (string, error) { return v, nil }))
if err != nil {
t.Fatal(err)
}
good, err := copyx.Clone([]int{1}, copyx.WithConverter(func(v []int) ([]int, error) { return append([]int(nil), v...), nil }))
if err != nil || good[0] != 1 {
t.Fatal(good, err)
}
}
func TestPrivateAndUnsupported(t *testing.T) {
type money struct{ cents *int64 }
n := int64(9007199254740991)
src := money{&n}
if _, err := copyx.Clone(src); !errors.Is(err, copyx.ErrUnsupported) {
t.Fatal(err)
}
out, err := copyx.Clone(src, copyx.WithConverter(func(v money) (money, error) { n := *v.cents; return money{&n}, nil }))
if err != nil || *out.cents != n || out.cents == src.cents {
t.Fatal(out, err)
}
tm := time.Now()
cloned, err := copyx.Clone(tm)
if err != nil || !cloned.Equal(tm) {
t.Fatal(cloned, err)
}
for _, v := range []any{make(chan int), func() {}, &sync.Mutex{}, sync.Mutex{}} {
if _, err := copyx.Clone(v); !errors.Is(err, copyx.ErrUnsupported) {
t.Fatalf("%T: %v", v, err)
}
}
}
func TestNilTypeMismatchAndInterfaceConverter(t *testing.T) {
var source *string
target := 7
if err := copyx.Copy(&target, source, copyx.WithNilPolicy(copyx.NilZero)); !errors.Is(err, copyx.ErrType) || target != 7 {
t.Fatal(target, err)
}
var list []int
var targetMap map[string]int
if err := copyx.Copy(&targetMap, list); !errors.Is(err, copyx.ErrType) {
t.Fatal(err)
}
type input struct{ Value any }
type output struct{ Value string }
var out output
err := copyx.Copy(&out, input{}, copyx.WithConverter(func(v any) (string, error) {
if v == nil {
return "nil handled", nil
}
return "value", nil
}))
if err != nil || out.Value != "nil handled" {
t.Fatal(out, err)
}
var lock sync.Mutex
if err := copyx.Copy(&lock, sync.Mutex{}); !errors.Is(err, copyx.ErrUnsupported) {
t.Fatal(err)
}
type locked struct {
mu sync.Mutex
Value int
}
var l locked
if err := copyx.Copy(&l, locked{}); !errors.Is(err, copyx.ErrUnsupported) {
t.Fatal(err)
}
}
func TestAllocationLimitAndPointerRoot(t *testing.T) {
src := make([]byte, (64<<20)+1)
if _, err := copyx.Clone(src); !errors.Is(err, copyx.ErrLimit) {
t.Fatal(err)
}
n := &detail{}
n.Next = n
var value detail
if err := copyx.Copy(&value, n, copyx.WithDeepCopy()); !errors.Is(err, copyx.ErrUnsupported) {
t.Fatal(err)
}
var p *detail
if err := copyx.Copy(&p, n, copyx.WithDeepCopy()); err != nil || p.Next != p || p == n {
t.Fatal(p, err)
}
}
func TestFieldsConvertersAndAtomicity(t *testing.T) {
type input struct {
Name string
Age int
Extra string
}
type output struct {
Label string
Age int
Local string
}
rename := map[string]string{"Name": "Label"}
c, err := copyx.New(copyx.WithFieldNames[input, output](rename), copyx.WithIgnoreTarget[input, output]("Local"))
if err != nil {
t.Fatal(err)
}
rename["Name"] = "Missing"
dst := output{Local: "preserved"}
if err = c.Copy(&dst, input{Name: "hello", Age: 2}); err != nil || dst.Label != "hello" || dst.Local != "preserved" {
t.Fatal(dst, err)
}
if err = copyx.Copy(&dst, input{}, copyx.WithStrictFields(), copyx.WithFieldNames[input, output](map[string]string{"Name": "Label"}), copyx.WithIgnoreTarget[input, output]("Local")); !errors.Is(err, copyx.ErrField) {
t.Fatal(err)
}
if err = copyx.Copy(&dst, input{}, copyx.WithStrictFields(), copyx.WithFieldNames[input, output](map[string]string{"Name": "Label"}), copyx.WithIgnoreTarget[input, output]("Local"), copyx.WithIgnoreSource[input, output]("Extra")); err != nil {
t.Fatal(err)
}
if err = copyx.Copy(&dst, input{}); !errors.Is(err, copyx.ErrField) {
t.Fatal(err)
}
if _, err = copyx.New(copyx.WithFieldNames[input, output](map[string]string{"Nope": "Age"})); err == nil {
t.Fatal("invalid rule accepted")
}
if _, err = copyx.New(copyx.WithConverter(func(v int) (string, error) { return "", nil }), copyx.WithConverter(func(v int) (string, error) { return "", nil })); err == nil {
t.Fatal("duplicate converter accepted")
}
sentinel := errors.New("conversion failed")
type node struct {
A []int
Z string
}
old := node{[]int{8}, "old"}
before := old.A
err = copyx.Copy(&old, node{[]int{1}, "bad"}, copyx.WithDeepCopy(), copyx.WithConverter(func(v string) (string, error) { return "", sentinel }))
if !errors.Is(err, sentinel) || !strings.Contains(err.Error(), "Z") || old.Z != "old" || old.A[0] != 8 || &old.A[0] != &before[0] {
t.Fatal(old, err)
}
// Exact typed nil converter must get control before the global nil policy.
var ptr *int
var value int
err = copyx.Copy(&value, ptr, copyx.WithConverter(func(v *int) (int, error) {
if v == nil {
return 0, sentinel
}
return *v, nil
}))
if !errors.Is(err, sentinel) {
t.Fatal(err)
}
var small int8 = 3
if err = copyx.Copy(&small, int64(300)); !errors.Is(err, copyx.ErrType) || small != 3 {
t.Fatal(small, err)
}
}
type Embedded struct {
A int
Z string
}
func TestEmbeddedDestinationDetached(t *testing.T) {
type target struct{ *Embedded }
type source struct {
A int
Z int
}
ptr := &Embedded{5, "keep"}
dst := target{ptr}
if err := copyx.Copy(&dst, source{6, 7}); !errors.Is(err, copyx.ErrType) {
t.Fatal(err)
}
if ptr.A != 5 || dst.Embedded != ptr {
t.Fatal("failure mutated embedded target")
}
if err := copyx.Copy(&dst, Embedded{6, "new"}); err != nil {
t.Fatal(err)
}
if ptr.A != 5 || dst.A != 6 {
t.Fatal("success mutated old embedded object")
}
}
func TestConcurrentCopier(t *testing.T) {
c, err := copyx.New(copyx.WithDeepCopy())
if err != nil {
t.Fatal(err)
}
src := &detail{Values: []int{7}, Index: map[string][]int{"a": {9}}}
var wg sync.WaitGroup
for i := 0; i < 64; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for n := 0; n < 20; n++ {
var dst *detail
if err := c.Copy(&dst, src); err != nil {
t.Error(err)
return
}
dst.Values[0] = n
if src.Values[0] != 7 {
t.Error("shared")
}
}
}()
}
wg.Wait()
}
func TestBoundAndCollections(t *testing.T) {
src := &detail{}
p := src
for i := 0; i < 300; i++ {
p.Next = &detail{}
p = p.Next
}
if _, err := copyx.Clone(src); !errors.Is(err, copyx.ErrLimit) {
t.Fatal(err)
}
type from struct{ Value string }
type to struct{ Value string }
var out []to
if err := copyx.Copy(&out, []from{{"a"}, {"b"}}); err != nil || len(out) != 2 || out[1].Value != "b" {
t.Fatal(out, err)
}
var array [2]to
if err := copyx.Copy(&array, [2]from{{"a"}, {"b"}}); err != nil {
t.Fatal(err)
}
if err := copyx.Copy(&array, [1]from{{"a"}}); !errors.Is(err, copyx.ErrType) {
t.Fatal(err)
}
var outMap map[string]int
err := copyx.Copy(&outMap, map[int]int{1: 1, 2: 2}, copyx.WithConverter(func(int) (string, error) { return "same", nil }))
if err == nil || outMap != nil {
t.Fatal(outMap, err)
}
}
func FuzzClone(f *testing.F) {
f.Add([]byte{1, 2, 3})
f.Add([]byte{})
f.Fuzz(func(t *testing.T, data []byte) {
if len(data) > 1024 {
data = data[:1024]
}
src := map[string]any{"bytes": data, "other": []string{"x"}}
out, err := copyx.Clone(src)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(src, out) {
t.Fatal("changed value")
}
if len(data) > 0 {
out["bytes"].([]byte)[0] ^= 255
if data[0] == out["bytes"].([]byte)[0] {
t.Fatal("alias")
}
}
})
}
func TestTraversalNodeLimit(t *testing.T) {
source := make([]any, 200001)
if _, err := copyx.Clone(source); !errors.Is(err, copyx.ErrLimit) {
t.Fatal("expected bounded traversal", err)
}
}
func TestNilCannotHideFieldMismatch(t *testing.T) {
type source struct{ Value string }
type target struct{ Value int }
var src *source
var dst *target
for _, policy := range []copyx.NilPolicy{copyx.NilError, copyx.NilZero, copyx.NilSkip} {
if err := copyx.Copy(&dst, src, copyx.WithNilPolicy(policy)); !errors.Is(err, copyx.ErrType) {
t.Fatal("nil concealed incompatible fields", err)
}
}
if err := copyx.Copy(&dst, src, copyx.WithConverter(func(string) (int, error) { return 0, nil })); err != nil {
t.Fatal(err)
}
text := "keep"
if err := copyx.Copy(&text, 0, copyx.WithSkipZero()); !errors.Is(err, copyx.ErrType) || text != "keep" {
t.Fatal("zero concealed incompatible types", text, err)
}
}
func BenchmarkCopy(b *testing.B) {
for _, n := range []int{1, 100, 1000} {
for _, deep := range []bool{false, true} {
b.Run(fmt.Sprintf("fields%d/deep%t", n, deep), func(b *testing.B) {
src := detail{Values: make([]int, n), Index: map[string][]int{"one": {1, 2}}}
var opts []copyx.Option
if deep {
opts = append(opts, copyx.WithDeepCopy())
}
c, _ := copyx.New(opts...)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var dst detail
if err := c.Copy(&dst, src); err != nil {
b.Fatal(err)
}
}
})
}
}
}