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