//go:build sqlite_vtable || vtable package resx_test import ( "reflect" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/samber/do/v2" "git.fsdpf.net/go/db/engine" "git.fsdpf.net/go/req" "git.fsdpf.net/go/req/resx" ) func newCacheTestResource(t testing.TB) req.Resource { t.Helper() mockDB, _, err := sqlmock.New() if err != nil { t.Fatalf("sqlmock: %v", err) } app := do.New() do.Provide(app, func(i do.Injector) (engine.Engine, error) { return engine.Mock(map[string]engine.MockDBConfig{ "default": {Driver: "mysql", Mock: mockDB}, }), nil }) return resx.New(app, "User", "users", resx.WithConn("default"), resx.WithFields( resx.NewResField("id", "User", resx.FieldWithName("Id"), resx.FieldWithDataType(req.ResInteger)), resx.NewResField("name", "User", resx.FieldWithName("Name"), resx.FieldWithDataType(req.ResString)), resx.NewResField("email", "User", resx.FieldWithName("Email"), resx.FieldWithDataType(req.ResString)), resx.NewResField("status", "User", resx.FieldWithName("Status"), resx.FieldWithDataType(req.ResInteger)), ), ) } // 缓存只应影响速度, 不影响结果: 多次调用必须拿到同一个类型, 但仍是新实例。 func TestGetStructTypeIsStable(t *testing.T) { res := newCacheTestResource(t) t1 := reflect.TypeOf(res.GetStruct()) t2 := reflect.TypeOf(res.GetStruct()) if t1 != t2 { t.Fatalf("无 extends 时两次 GetStruct 类型不一致: %v vs %v", t1, t2) } if t1.Kind() != reflect.Ptr || t1.Elem().Kind() != reflect.Struct { t.Fatalf("GetStruct 应返回 *struct, 得到 %v", t1) } if res.GetStruct() == res.GetStruct() { t.Fatal("GetStruct 应该每次返回新实例, 只复用类型") } // 带 extends ext := reflect.StructField{Name: "Extra", Type: reflect.TypeOf("")} e1 := reflect.TypeOf(res.GetStruct(ext)) e2 := reflect.TypeOf(res.GetStruct(ext)) if e1 != e2 { t.Fatalf("带 extends 时两次类型不一致: %v vs %v", e1, e2) } if e1 == t1 { t.Fatal("带 extends 的类型不应与不带 extends 的相同") } if _, ok := e1.Elem().FieldByName("Extra"); !ok { t.Fatal("extends 字段没有出现在结果类型里") } // 不同的 extends 必须得到不同的类型, 且互不污染 ext2 := reflect.StructField{Name: "Other", Type: reflect.TypeOf(0)} o1 := reflect.TypeOf(res.GetStruct(ext2)) if o1 == e1 { t.Fatal("不同 extends 却拿到同一个类型, 缓存键有问题") } if _, ok := o1.Elem().FieldByName("Other"); !ok { t.Fatal("第二组 extends 的字段没有出现") } if reflect.TypeOf(res.GetStruct(ext)) != e1 { t.Fatal("缓存被后续调用污染了") } } // 调用方复用 extends 底层数组时, 缓存键不能跟着被改写。 func TestGetStructExtendsAliasing(t *testing.T) { res := newCacheTestResource(t) buf := make([]reflect.StructField, 0, 4) buf = append(buf, reflect.StructField{Name: "A", Type: reflect.TypeOf("")}) tA := reflect.TypeOf(res.GetStruct(buf...)) // 复用同一个底层数组换成另一组字段 buf = buf[:0] buf = append(buf, reflect.StructField{Name: "B", Type: reflect.TypeOf("")}) tB := reflect.TypeOf(res.GetStruct(buf...)) if tA == tB { t.Fatal("复用底层数组导致缓存键被改写, 拿到了错误的类型") } if _, ok := tA.Elem().FieldByName("A"); !ok { t.Fatal("第一次的类型被污染了") } if _, ok := tB.Elem().FieldByName("B"); !ok { t.Fatal("第二次的类型不对") } } func TestGetSliceStructType(t *testing.T) { res := newCacheTestResource(t) st := reflect.TypeOf(res.GetSliceStruct()) if st.Kind() != reflect.Ptr || st.Elem().Kind() != reflect.Slice { t.Fatalf("GetSliceStruct 应返回 *[]struct, 得到 %v", st) } if st.Elem().Elem() != reflect.TypeOf(res.GetStruct()).Elem() { t.Fatal("GetSliceStruct 的元素类型应与 GetStruct 的一致") } if reflect.TypeOf(res.GetSliceStruct()) != st { t.Fatal("两次 GetSliceStruct 类型不一致") } } // 并发调用不能出现数据竞争, 且拿到的类型一致(配合 -race 跑)。 func TestGetStructConcurrent(t *testing.T) { res := newCacheTestResource(t) want := reflect.TypeOf(res.GetStruct()) const n = 16 done := make(chan reflect.Type, n) for i := 0; i < n; i++ { go func() { var got reflect.Type for j := 0; j < 100; j++ { got = reflect.TypeOf(res.GetStruct()) } done <- got }() } for i := 0; i < n; i++ { if got := <-done; got != want { t.Fatalf("并发下拿到了不同的类型: %v vs %v", got, want) } } } func BenchmarkGetStruct(b *testing.B) { res := newCacheTestResource(b) res.GetStruct() // 预热 b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = res.GetStruct() } } func BenchmarkGetStructWithExtends(b *testing.B) { res := newCacheTestResource(b) ext := reflect.StructField{Name: "Extra", Type: reflect.TypeOf("")} res.GetStruct(ext) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = res.GetStruct(ext) } } func BenchmarkGetSliceStruct(b *testing.B) { res := newCacheTestResource(b) res.GetSliceStruct() b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = res.GetSliceStruct() } }