193 lines
6.0 KiB
Go
193 lines
6.0 KiB
Go
package copyx_test
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.yuntuo.hk/k2safe/go-omni/copyx"
|
|
)
|
|
|
|
type checkSource struct{ Rows []checkSourceRow }
|
|
type checkSourceRow struct{ Value string }
|
|
type checkTarget struct{ Rows []checkTargetRow }
|
|
type checkTargetRow struct{ Value int }
|
|
type recursiveSource struct {
|
|
Children []*recursiveSource
|
|
Value string
|
|
}
|
|
type recursiveTarget struct {
|
|
Children []*recursiveTarget
|
|
Value int
|
|
}
|
|
type recursiveCompatible struct {
|
|
Children []*recursiveCompatible
|
|
Value string
|
|
}
|
|
|
|
func TestCheckTypesAndEmptyCollections(t *testing.T) {
|
|
err := copyx.Check[checkSource, checkTarget]()
|
|
var field *copyx.Error
|
|
if !errors.Is(err, copyx.ErrType) || !errors.As(err, &field) || field.Path != "$.Rows[].Value" {
|
|
t.Fatal(err)
|
|
}
|
|
for _, rows := range [][]checkSourceRow{nil, {}, {{Value: "42"}}} {
|
|
out := checkTarget{Rows: []checkTargetRow{{Value: 99}}}
|
|
if err := copyx.Copy(&out, checkSource{rows}); !errors.Is(err, copyx.ErrType) {
|
|
t.Fatalf("rows %v: %v", rows, err)
|
|
}
|
|
if len(out.Rows) != 1 || out.Rows[0].Value != 99 {
|
|
t.Fatal("partial target committed")
|
|
}
|
|
}
|
|
if err := copyx.Check[map[string]checkSourceRow, map[string]checkTargetRow](); !errors.Is(err, copyx.ErrType) {
|
|
t.Fatal(err)
|
|
}
|
|
var dst map[string]checkTargetRow
|
|
if err := copyx.Copy(&dst, map[string]checkSourceRow{}); !errors.Is(err, copyx.ErrType) {
|
|
t.Fatal(err)
|
|
}
|
|
var array [0]checkTargetRow
|
|
if err := copyx.Copy(&array, [0]checkSourceRow{}); !errors.Is(err, copyx.ErrType) {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyx.Check[map[string]int, map[int]int](); !errors.Is(err, copyx.ErrType) {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyx.Check[[2]int, [3]int](); !errors.Is(err, copyx.ErrType) {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyx.Check[*recursiveSource, *recursiveTarget](); !errors.Is(err, copyx.ErrType) {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyx.Check[*recursiveSource, *recursiveCompatible](copyx.WithDeepCopy()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
func TestCheckRulesAndConverterBoundaries(t *testing.T) {
|
|
type from struct {
|
|
Name string
|
|
Extra bool
|
|
}
|
|
type to struct {
|
|
Label string
|
|
Keep int
|
|
}
|
|
options := []copyx.Option{copyx.WithStrictFields(), copyx.WithFieldNames[from, to](map[string]string{"Name": "Label"}), copyx.WithIgnoreSource[from, to]("Extra"), copyx.WithIgnoreTarget[from, to]("Keep")}
|
|
if err := copyx.Check[from, to](options...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyx.Check[from, to](); !errors.Is(err, copyx.ErrField) {
|
|
t.Fatal(err)
|
|
}
|
|
type short struct{ Name string }
|
|
if err := copyx.Check[from, short](copyx.WithStrictFields()); !errors.Is(err, copyx.ErrField) {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyx.Check[checkSource, checkTarget](copyx.WithConverter(func(string) (int, error) { panic("check must not execute converters") })); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyx.Check[struct{ Value any }, struct{ Value int }](); !errors.Is(err, copyx.ErrDynamicType) {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyx.Check[any, int](copyx.WithConverter(func(any) (int, error) { panic("not called") })); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyx.Check[*int, int](); err != nil {
|
|
t.Fatal(err)
|
|
} // Actual nil remains a runtime error.
|
|
var src *int
|
|
dst := 1
|
|
if err := copyx.Copy(&dst, src); !errors.Is(err, copyx.ErrNil) || dst != 1 {
|
|
t.Fatal(err)
|
|
}
|
|
var empty []int
|
|
if err := copyx.Copy(&empty, []any{}); err != nil || empty == nil {
|
|
t.Fatal("dynamic emptiness changed", err)
|
|
}
|
|
if err := copyx.Check[sync.Mutex, sync.Mutex](); !errors.Is(err, copyx.ErrUnsupported) {
|
|
t.Fatal(err)
|
|
}
|
|
option := copyx.WithConverter(func(v []int) ([]int, error) { return v, nil })
|
|
if err := copyx.Check[[]int, []int](copyx.WithDeepCopy(), option); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := copyx.Clone([]int{1}, option); !errors.Is(err, copyx.ErrSharedReference) {
|
|
t.Fatal("type check bypassed graph audit", err)
|
|
}
|
|
}
|
|
func TestCopierCheckConcurrentAndOptionsIsolation(t *testing.T) {
|
|
good, _ := copyx.New(copyx.WithConverter(func(string) (int, error) { return 42, nil }))
|
|
bad, _ := copyx.New()
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 24; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 20; j++ {
|
|
if err := good.Check(reflect.TypeFor[checkSource](), reflect.TypeFor[checkTarget]()); err != nil {
|
|
t.Error(err)
|
|
}
|
|
if err := bad.Check(reflect.TypeFor[checkSource](), reflect.TypeFor[checkTarget]()); !errors.Is(err, copyx.ErrType) {
|
|
t.Error(err)
|
|
}
|
|
var target checkTarget
|
|
if err := good.Copy(&target, checkSource{Rows: []checkSourceRow{}}); err != nil || target.Rows == nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
if err := good.Check(nil, reflect.TypeFor[int]()); err == nil {
|
|
t.Fatal("nil type accepted")
|
|
}
|
|
var absent *copyx.Copier
|
|
if err := absent.Check(reflect.TypeFor[int](), reflect.TypeFor[int]()); err == nil {
|
|
t.Fatal("nil copier accepted")
|
|
}
|
|
if err := copyx.Check[int, string](); err == nil || !strings.Contains(err.Error(), "int -> string") {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
func BenchmarkTypeCheck(b *testing.B) {
|
|
c, _ := copyx.New(copyx.WithConverter(func(string) (int, error) { return 1, nil }))
|
|
from, to := reflect.TypeFor[checkSource](), reflect.TypeFor[checkTarget]()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
if err := c.Check(from, to); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEmptyTypeCheckPreservesAbsentValueSemantics(t *testing.T) {
|
|
var callback func()
|
|
if out, err := copyx.Clone(callback); err != nil || out != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out, err := copyx.Clone([]func(){}); err != nil || out == nil || len(out) != 0 {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := copyx.Clone([]func(){func() {}}); !errors.Is(err, copyx.ErrUnsupported) {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyx.Check[[]func(), []func()](copyx.WithDeepCopy()); !errors.Is(err, copyx.ErrUnsupported) {
|
|
t.Fatal("preflight must report potentially non-nil handles", err)
|
|
}
|
|
type from struct {
|
|
Ignored *int
|
|
Value string
|
|
}
|
|
type to struct {
|
|
Ignored *int
|
|
Value int
|
|
}
|
|
var dst []to
|
|
if err := copyx.Copy(&dst, []from{}, copyx.WithDeepCopy()); !errors.Is(err, copyx.ErrType) {
|
|
t.Fatal(err)
|
|
}
|
|
}
|