449 lines
13 KiB
Go
449 lines
13 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|