Files
aego/engine/engine_test.go
sanspie dae4bc19e1 Stage 1: engine core, platform, gpu, vfs
Foundation for the aego engine.

core/    errs, log, handle arena, mathx, clock, stats
platform SDL3 backend plus a headless one, input state, events
gpu      device abstraction with gl33 and null backends
vfs      scheme-based virtual filesystem with overlay mounts
engine   builder, module registry, phase-ordered system loop, jobs
demos    basic, resize, input, headless; sandbox driver

OpenGL bindings are generated into gpu/gl33/gl by tools/gen-gl.sh,
so the module has no external dependencies.
2026-09-18 20:51:54 +03:00

237 lines
5.3 KiB
Go

package engine
import (
"testing"
"aego/core/errs"
"aego/core/log"
"aego/platform"
"aego/platform/headless"
"aego/platform/input"
)
type testApp struct {
e *Engine
fixed int
updates int
renders int
events int
aDown bool
aUp bool
}
func (a *testApp) Init(e *Engine) error {
a.e = e
return nil
}
func (a *testApp) Event(platform.Event) {
a.events++
}
func (a *testApp) FixedUpdate(float64) {
a.fixed++
}
func (a *testApp) Update(float64) {
a.updates++
a.aDown = a.e.Input().KeyDown(input.KeyA)
a.aUp = a.e.Input().KeyReleased(input.KeyA)
}
func (a *testApp) Render(float64) {
a.renders++
}
func (a *testApp) Shutdown() {}
func newTestEngine(t *testing.T) (*Engine, *headless.Platform) {
t.Helper()
hp := headless.New(headless.Options{FrameTime: 1.0 / 60})
e, err := NewBuilder(Config{
Headless: true,
HeadlessFrameTime: 1.0 / 60,
ResRoot: t.TempDir(),
UserRoot: t.TempDir(),
AppID: "aego-test",
}).WithLogger(log.Nop()).WithPlatform(hp).Build()
if err != nil {
t.Fatal(err)
}
return e, hp
}
func TestFixedStepCount(t *testing.T) {
e, _ := newTestEngine(t)
defer e.Close()
app := &testApp{}
for i := 0; i < 120; i++ {
if !e.Step(app) {
t.Fatalf("stopped at frame %d: %v", i, e.Err())
}
}
if app.updates != 120 || app.renders != 120 {
t.Fatalf("updates=%d renders=%d", app.updates, app.renders)
}
if app.fixed < 118 || app.fixed > 120 {
t.Fatalf("fixed=%d", app.fixed)
}
if e.Stats().DrawCalls != 0 {
t.Fatalf("null device reported draws: %d", e.Stats().DrawCalls)
}
}
func TestEventsAndFocusReset(t *testing.T) {
e, hp := newTestEngine(t)
defer e.Close()
app := &testApp{}
e.Step(app)
hp.Push(platform.Event{Kind: platform.EvKeyDown, Key: input.KeyEvent{Scan: input.KeyA}})
e.Step(app)
if !app.aDown {
t.Fatal("key down not visible in update")
}
e.Step(app)
if !app.aDown {
t.Fatal("key must stay down across frames")
}
hp.Push(platform.Event{Kind: platform.EvWindowFocusLost})
e.Step(app)
if app.aDown || !app.aUp {
t.Fatalf("focus loss did not reset input: down=%v released=%v", app.aDown, app.aUp)
}
if app.events == 0 {
t.Fatal("app did not receive events")
}
}
func TestQuitEvent(t *testing.T) {
e, hp := newTestEngine(t)
defer e.Close()
app := &testApp{}
e.Step(app)
hp.Push(platform.Event{Kind: platform.EvQuit})
if e.Step(app) {
t.Fatal("engine kept running after quit")
}
}
func TestResizeUpdatesMetrics(t *testing.T) {
e, hp := newTestEngine(t)
defer e.Close()
app := &testApp{}
e.Step(app)
hp.Push(platform.Event{
Kind: platform.EvWindowResized,
Window: platform.WindowMetrics{
LogicalW: 640, LogicalH: 480,
PixelW: 1280, PixelH: 960,
PixelDensity: 2, DisplayScale: 2,
},
})
e.Step(app)
if m := e.Metrics(); m.PixelW != 1280 || m.LogicalW != 640 || m.PixelDensity != 2 {
t.Fatalf("metrics = %+v", m)
}
}
func TestJobsOnMain(t *testing.T) {
e, _ := newTestEngine(t)
defer e.Close()
app := &testApp{}
e.Step(app)
done := make(chan struct{})
ran := 0
e.Jobs().Submit(func() {
e.Jobs().OnMain(func() { ran++ })
close(done)
})
<-done
e.Step(app)
if ran != 1 {
t.Fatalf("main queue ran %d times", ran)
}
}
type testModule struct {
id string
log *[]string
initErr error
gpuOK bool
}
func (m *testModule) Info() ModuleInfo { return ModuleInfo{ID: m.id, Version: "1"} }
func (m *testModule) Register(*Registry) error {
*m.log = append(*m.log, "register:"+m.id)
return nil
}
func (m *testModule) Init(e *Engine) error {
*m.log = append(*m.log, "init:"+m.id)
m.gpuOK = e.GPU() != nil && e.VFS() != nil && e.Jobs() != nil
return m.initErr
}
// bareModule has no Init; Build must skip it rather than fail.
type bareModule struct{ log *[]string }
func (bareModule) Info() ModuleInfo { return ModuleInfo{ID: "bare", Version: "1"} }
func (m bareModule) Register(*Registry) error {
*m.log = append(*m.log, "register:bare")
return nil
}
func buildWithModules(t *testing.T, mods ...Module) (*Engine, error) {
t.Helper()
b := NewBuilder(Config{
Headless: true, HeadlessFrameTime: 1.0 / 60,
ResRoot: t.TempDir(), UserRoot: t.TempDir(), AppID: "aego-test",
}).WithLogger(log.Nop()).WithPlatform(headless.New(headless.Options{FrameTime: 1.0 / 60}))
for _, m := range mods {
b.Use(m)
}
return b.Build()
}
func TestModuleInitRunsAfterBackends(t *testing.T) {
var order []string
a := &testModule{id: "a", log: &order}
c := &testModule{id: "c", log: &order}
e, err := buildWithModules(t, a, bareModule{log: &order}, c)
if err != nil {
t.Fatal(err)
}
defer e.Close()
want := []string{"register:a", "register:bare", "register:c", "init:a", "init:c"}
if len(order) != len(want) {
t.Fatalf("order = %v, want %v", order, want)
}
for i := range want {
if order[i] != want[i] {
t.Fatalf("order = %v, want %v", order, want)
}
}
if !a.gpuOK || !c.gpuOK {
t.Fatal("Init ran before the device/vfs/jobs were available")
}
}
func TestModuleInitErrorFailsBuild(t *testing.T) {
var order []string
bad := &testModule{id: "bad", log: &order, initErr: errBoom}
e, err := buildWithModules(t, bad)
if err == nil {
e.Close()
t.Fatal("Build must fail when a module Init fails")
}
if !errs.Has(err, errs.EngineModule) {
t.Fatalf("want EngineModule code, got %v", err)
}
}
var errBoom = errs.New(errs.GPUShaderCompile, "boom")