Files
aego/tests/integration/shutdown_test.go
sanspie 1030dc9a43
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 4m58s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 27m47s
ci / release (push) Successful in 11m57s
Stage 10d2: audio fast path, shutdown order, profiler and monitor sections
2026-10-22 14:41:24 +03:00

154 lines
3.8 KiB
Go

package integration
import (
"sync"
"testing"
"aego/app"
"aego/core/log"
"aego/engine"
"aego/engine/module"
"aego/platform"
"aego/platform/headless"
"aego/project"
)
// shutdownLog records the order in which modules and the platform go down.
type shutdownLog struct {
mu sync.Mutex
events []string
late int
}
func (l *shutdownLog) add(e string) {
l.mu.Lock()
l.events = append(l.events, e)
l.mu.Unlock()
}
// liveOut stands for a device output: once the platform shuts down, any Write is a write into a closed device.
type liveOut struct {
log *shutdownLog
spec platform.AudioSpec
mu sync.Mutex
dead bool
writes int
}
func (o *liveOut) Spec() platform.AudioSpec { return o.spec }
func (o *liveOut) Queued() int { return 0 }
func (o *liveOut) Pause(bool) {}
func (o *liveOut) Write(s []float32) int {
o.mu.Lock()
defer o.mu.Unlock()
if o.dead {
o.log.mu.Lock()
o.log.late++
o.log.mu.Unlock()
return 0
}
o.writes++
return len(s) / o.spec.Channels
}
func (o *liveOut) Close() {}
func (o *liveOut) kill() {
o.mu.Lock()
o.dead = true
o.mu.Unlock()
}
type shutdownPlatform struct {
*headless.Platform
log *shutdownLog
live bool
out *liveOut
}
func (p *shutdownPlatform) OpenAudio(want platform.AudioSpec) (platform.AudioOutput, error) {
if !p.live {
return p.Platform.OpenAudio(want)
}
p.out = &liveOut{log: p.log, spec: want.WithDefaults()}
return p.out, nil
}
func (p *shutdownPlatform) Shutdown() {
p.log.add("platform")
if p.out != nil {
p.out.kill()
}
p.Platform.Shutdown()
}
type shutdownProbe struct{ log *shutdownLog }
func (shutdownProbe) Info() module.Info { return module.Info{ID: "shutdownprobe"} }
func (shutdownProbe) Register(*module.Registry) error { return nil }
func (p shutdownProbe) Init(*module.InitContext) error { return nil }
func (p shutdownProbe) Shutdown() { p.log.add("modules") }
type nopApp struct{}
func (nopApp) Init(*engine.Engine) error { return nil }
func (nopApp) Event(platform.Event) {}
func (nopApp) FixedUpdate(float64) {}
func (nopApp) Update(float64) {}
func (nopApp) Render(float64) {}
func (nopApp) Shutdown() {}
func shutdownRun(t *testing.T, live bool, frames int) *shutdownLog {
t.Helper()
p, err := project.Load(copyExample(t, "platformer"))
if err != nil {
t.Fatal(err)
}
l := &shutdownLog{}
plat := &shutdownPlatform{Platform: headless.New(headless.Options{FrameTime: 1.0 / 60}), log: l, live: live}
h, err := app.Build(p, app.Config{Engine: engine.Config{Headless: true, HeadlessFrameTime: 1.0 / 60}, Logger: log.Nop(), Platform: plat}, shutdownProbe{log: l})
if err != nil {
t.Fatal(err)
}
for range frames {
h.Engine.Step(nopApp{})
}
if live && plat.out == nil {
t.Fatal("the audio module did not open the platform output")
}
h.Engine.Close()
h.Close()
if live {
plat.out.mu.Lock()
w := plat.out.writes
plat.out.mu.Unlock()
if w == 0 {
t.Fatal("the pump never wrote to the live output")
}
}
return l
}
func checkShutdown(t *testing.T, l *shutdownLog) {
t.Helper()
l.mu.Lock()
defer l.mu.Unlock()
if len(l.events) != 2 || l.events[0] != "modules" || l.events[1] != "platform" {
t.Fatalf("shutdown order %v, want modules before platform", l.events)
}
if l.late != 0 {
t.Fatalf("%d write(s) reached the output after the platform shut down", l.late)
}
}
// Engine.Close shuts the modules down before the platform, even when the engine is closed directly.
func TestShutdownOrder(t *testing.T) {
checkShutdown(t, shutdownRun(t, false, 30))
}
// With a device-like output the pump runs on its own goroutine; it must stop before the platform closes the device.
func TestShutdownOrderWithPump(t *testing.T) {
checkShutdown(t, shutdownRun(t, true, 30))
}