Files
aego/schema/schema_test.go
2026-09-26 16:32:40 +03:00

387 lines
9.9 KiB
Go

package schema
import (
"testing"
"unsafe"
"aego/core/errs"
"aego/core/ids"
"aego/core/mathx"
)
type testAssetRef struct {
id ids.AssetID
}
func (r *testAssetRef) AssetRefID() ids.AssetID { return r.id }
func (r *testAssetRef) SetAssetRefID(v ids.AssetID) { r.id = v }
func (r *testAssetRef) AssetKind() string { return "texture" }
type testNodeRef struct {
id ids.NodeID
}
func (r *testNodeRef) NodeRefID() ids.NodeID { return r.id }
func (r *testNodeRef) SetNodeRefID(v ids.NodeID) { r.id = v }
type blendMode uint8
type inner struct {
A float32 `aego:"a"`
B bool `aego:"b"`
}
type comp struct {
Max float32 `aego:"max,min=0,max=1000,doc=upper bound"`
Current float32 `aego:"current"`
Count int32 `aego:"count"`
Big int64 `aego:"big"`
Label string `aego:"label"`
Pos mathx.Vec2 `aego:"pos"`
Tint mathx.Color `aego:"tint"`
Area mathx.Rect `aego:"area"`
Blend blendMode `aego:"blend"`
Sprite testAssetRef `aego:"sprite"`
Target testNodeRef `aego:"target"`
Nested inner `aego:"nested"`
Tags []string `aego:"tags"`
Parts []inner `aego:"parts"`
Cache []byte `aego:"-"`
Runtime float32 `aego:"runtime,transient"`
Locked int32 `aego:"locked,readonly"`
Auto float32
hidden int
}
func newRegistry(t *testing.T) *Registry {
t.Helper()
r := NewRegistry()
if err := RegisterEnum[blendMode](r, "test.BlendMode",
EnumValue{Name: "premultiplied", Value: 0},
EnumValue{Name: "additive", Value: 1},
); err != nil {
t.Fatal(err)
}
if _, err := RegisterStruct[inner](r, Name("test.Inner")); err != nil {
t.Fatal(err)
}
return r
}
func TestRegisterFields(t *testing.T) {
r := newRegistry(t)
td, err := Register[comp](r, Name("test.Comp"), Version(2), Doc("component"))
if err != nil {
t.Fatal(err)
}
if td.ID != HashName("test.Comp") || td.Version != 2 {
t.Fatalf("descriptor = %+v", td)
}
if _, ok := td.Field("cache"); ok {
t.Fatal("skipped field was registered")
}
if _, ok := td.Field("hidden"); ok {
t.Fatal("unexported field was registered")
}
if f, ok := td.Field("auto"); !ok || f.Kind != KindFloat32 {
t.Fatal("untagged field must default to a lowerCamel name")
}
cases := []struct {
name string
kind Kind
fast bool
}{
{"max", KindFloat32, true},
{"count", KindInt32, true},
{"big", KindInt64, true},
{"label", KindString, false},
{"pos", KindVec2, true},
{"tint", KindColor, true},
{"area", KindRect, true},
{"blend", KindEnum, true},
{"sprite", KindAssetRef, false},
{"target", KindNodeRef, false},
{"nested", KindStruct, false},
{"tags", KindList, false},
{"parts", KindList, false},
}
for _, c := range cases {
f, ok := td.Field(c.name)
if !ok {
t.Fatalf("field %s missing", c.name)
}
if f.Kind != c.kind {
t.Fatalf("field %s kind = %s, want %s", c.name, f.Kind, c.kind)
}
if f.Fast != c.fast {
t.Fatalf("field %s fast = %v, want %v", c.name, f.Fast, c.fast)
}
}
if f, _ := td.Field("max"); f.Min != 0 || f.Max != 1000 || f.Doc != "upper bound" {
t.Fatalf("tag options lost: %+v", f)
}
if f, _ := td.Field("runtime"); f.Flags.Has(FlagSerialize) || !f.Flags.Has(FlagTransient) {
t.Fatal("transient flag wrong")
}
if f, _ := td.Field("locked"); !f.Flags.Has(FlagReadOnly) || !f.Flags.Has(FlagSerialize) {
t.Fatal("readonly flag wrong")
}
if f, _ := td.Field("sprite"); f.AssetKind != "texture" {
t.Fatalf("asset kind = %q", f.AssetKind)
}
if f, _ := td.Field("blend"); len(f.Enum) != 2 || f.EnumName != "test.BlendMode" {
t.Fatalf("enum metadata lost: %+v", f)
}
if f, _ := td.Field("parts"); f.Elem != KindStruct || f.Struct == 0 {
t.Fatalf("list of structs wrong: %+v", f)
}
}
func TestAccessorRoundTrip(t *testing.T) {
r := newRegistry(t)
td, err := Register[comp](r, Name("test.Comp"))
if err != nil {
t.Fatal(err)
}
var c comp
p := unsafe.Pointer(&c)
assetID := ids.NewAssetID()
nodeID := ids.NewNodeID()
writes := []struct {
field string
value Value
}{
{"max", Float32(42.5)},
{"count", Int32(7)},
{"big", Int64(1 << 40)},
{"label", String("hello")},
{"pos", Vec2(mathx.Vec2{X: 1, Y: 2})},
{"tint", Color(mathx.Color{R: 1, A: 1})},
{"area", Rect(mathx.Rect{X: 1, Y: 2, W: 3, H: 4})},
{"blend", Enum(1)},
{"sprite", Asset(assetID)},
{"target", Node(nodeID)},
{"tags", List(String("a"), String("b"))},
}
for _, w := range writes {
f, ok := td.Field(w.field)
if !ok {
t.Fatalf("field %s missing", w.field)
}
if err := f.Set(p, w.value); err != nil {
t.Fatalf("set %s: %v", w.field, err)
}
}
if c.Max != 42.5 || c.Count != 7 || c.Big != 1<<40 || c.Label != "hello" {
t.Fatalf("struct = %+v", c)
}
if c.Blend != 1 || c.Sprite.id != assetID || c.Target.id != nodeID {
t.Fatalf("refs or enum not written: %+v", c)
}
if len(c.Tags) != 2 || c.Tags[1] != "b" {
t.Fatalf("list not written: %v", c.Tags)
}
var v Value
f, _ := td.Field("max")
f.Get(p, &v)
if v.Kind != KindFloat32 || v.AsFloat() != 42.5 {
t.Fatalf("get max = %+v", v)
}
f, _ = td.Field("sprite")
f.Get(p, &v)
if v.Asset != assetID {
t.Fatal("get asset ref lost the id")
}
f, _ = td.Field("tags")
f.Get(p, &v)
if v.Kind != KindList || len(v.List) != 2 || v.List[0].S != "a" {
t.Fatalf("get list = %+v", v)
}
}
func TestNestedStructAccess(t *testing.T) {
r := newRegistry(t)
td, err := Register[comp](r, Name("test.Comp"))
if err != nil {
t.Fatal(err)
}
var c comp
p := unsafe.Pointer(&c)
f, _ := td.Field("nested")
if err := f.Set(p, Value{Kind: KindStruct, Obj: &Object{
Type: f.Struct,
Fields: []Value{Float32(3.5), Bool(true)},
}}); err != nil {
t.Fatal(err)
}
if c.Nested.A != 3.5 || !c.Nested.B {
t.Fatalf("nested = %+v", c.Nested)
}
var v Value
f.Get(p, &v)
if v.Kind != KindStruct || v.Obj == nil || v.Obj.Fields[0].AsFloat() != 3.5 {
t.Fatalf("nested get = %+v", v)
}
lf, _ := td.Field("parts")
if err := lf.Set(p, List(
Value{Kind: KindStruct, Obj: &Object{Type: lf.Struct, Fields: []Value{Float32(1), Bool(false)}}},
Value{Kind: KindStruct, Obj: &Object{Type: lf.Struct, Fields: []Value{Float32(2), Bool(true)}}},
)); err != nil {
t.Fatal(err)
}
if len(c.Parts) != 2 || c.Parts[1].A != 2 || !c.Parts[1].B {
t.Fatalf("parts = %+v", c.Parts)
}
}
func TestSetRejectsWrongKind(t *testing.T) {
r := newRegistry(t)
td, _ := Register[comp](r, Name("test.Comp"))
var c comp
f, _ := td.Field("label")
if err := f.Set(unsafe.Pointer(&c), Int32(1)); !errs.Has(err, errs.SchemaBadValue) {
t.Fatalf("err = %v", err)
}
}
func TestUnsupportedFieldIsAnError(t *testing.T) {
type bad struct {
Ch chan int `aego:"ch"`
}
r := NewRegistry()
if _, err := Register[bad](r, Name("test.Bad")); !errs.Has(err, errs.SchemaBadField) {
t.Fatalf("err = %v", err)
}
type skipped struct {
Ch chan int `aego:"-"`
OK int32 `aego:"ok"`
}
if _, err := Register[skipped](r, Name("test.Skipped")); err != nil {
t.Fatal(err)
}
}
func TestUnregisteredStructField(t *testing.T) {
type unreg struct{ X float32 }
type holder struct {
U unreg `aego:"u"`
}
r := NewRegistry()
if _, err := Register[holder](r, Name("test.Holder")); !errs.Has(err, errs.SchemaUnknownType) {
t.Fatalf("err = %v", err)
}
}
func TestDuplicatesAndCollisions(t *testing.T) {
type a struct{ X float32 }
type b struct{ X float32 }
r := NewRegistry()
if _, err := Register[a](r, Name("test.Same")); err != nil {
t.Fatal(err)
}
if _, err := Register[b](r, Name("test.Same")); !errs.Has(err, errs.SchemaDuplicate) {
t.Fatalf("duplicate name accepted: %v", err)
}
if _, err := Register[a](r, Name("test.Other")); !errs.Has(err, errs.SchemaDuplicate) {
t.Fatalf("duplicate go type accepted: %v", err)
}
}
func TestFormerNames(t *testing.T) {
type spinner struct {
Speed float32 `aego:"speed"`
}
r := NewRegistry()
td, err := Register[spinner](r, Name("game.Spinner"), FormerName("game.Rotator"),
FormerField("speed", "rate"))
if err != nil {
t.Fatal(err)
}
if err := r.Freeze(); err != nil {
t.Fatal(err)
}
old, ok := r.ByName("game.Rotator")
if !ok || old != td {
t.Fatal("former type name does not resolve")
}
if _, ok := r.ByID(HashName("game.Rotator")); !ok {
t.Fatal("former type id does not resolve")
}
if f, ok := td.Field("rate"); !ok || f.Name != "speed" {
t.Fatal("former field name does not resolve")
}
}
func TestFormerNameCollision(t *testing.T) {
type a struct{ X float32 }
type b struct{ X float32 }
r := NewRegistry()
if _, err := Register[a](r, Name("game.A")); err != nil {
t.Fatal(err)
}
if _, err := Register[b](r, Name("game.B"), FormerName("game.A")); err != nil {
t.Fatal(err)
}
if err := r.Freeze(); !errs.Has(err, errs.SchemaDuplicate) {
t.Fatalf("collision not reported: %v", err)
}
}
func TestFrozenRegistryRejectsRegistration(t *testing.T) {
type a struct{ X float32 }
type b struct{ X float32 }
r := NewRegistry()
if _, err := Register[a](r, Name("test.A")); err != nil {
t.Fatal(err)
}
if err := r.Freeze(); err != nil {
t.Fatal(err)
}
if !r.Frozen() {
t.Fatal("registry not frozen")
}
if _, err := Register[b](r, Name("test.B")); !errs.Has(err, errs.SchemaFrozen) {
t.Fatalf("err = %v", err)
}
if err := RegisterEnum[blendMode](r, "test.Late", EnumValue{}); !errs.Has(err, errs.SchemaFrozen) {
t.Fatalf("err = %v", err)
}
if err := r.Freeze(); err != nil {
t.Fatal("second Freeze must be a no-op")
}
}
func TestLowerFirst(t *testing.T) {
cases := map[string]string{
"Max": "max",
"FlipX": "flipX",
"ID": "id",
"URLPath": "urlPath",
"X": "x",
}
for in, want := range cases {
if got := lowerFirst(in); got != want {
t.Fatalf("lowerFirst(%q) = %q, want %q", in, got, want)
}
}
}
func TestComponentsAndEach(t *testing.T) {
r := newRegistry(t)
if _, err := Register[comp](r, Name("test.Comp")); err != nil {
t.Fatal(err)
}
if n := len(r.Components()); n != 1 {
t.Fatalf("components = %d", n)
}
count := 0
r.Each(func(*TypeDescriptor) { count++ })
if count != 2 {
t.Fatalf("each visited %d types", count)
}
}