fix: correct lifecycle setup and teardown semantics

Guard against double setup when autoload runs dependencies early, tear down
in reverse setup order, roll back on partial failure, and detect circular
Depends chains.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 17:58:00 -07:00
parent b7adafed67
commit 4cd218f658
4 changed files with 287 additions and 204 deletions
+81
View File
@@ -0,0 +1,81 @@
package app_test
import (
"fmt"
"testing"
"gitea.auvem.com/go-toolkit/app"
"github.com/stretchr/testify/assert"
)
func TestSetupTeardownIntegration(t *testing.T) {
var order []string
modA := app.NewModule("a", app.ModuleOpts{
Setup: func(m *app.Module) error { order = append(order, "setup:a"); return nil },
Teardown: func(m *app.Module) error { order = append(order, "teardown:a"); return nil },
})
modB := app.NewModule("b", app.ModuleOpts{
Setup: func(m *app.Module) error { order = append(order, "setup:b"); return nil },
Teardown: func(m *app.Module) error { order = append(order, "teardown:b"); return nil },
Depends: []string{"a"},
})
lc := app.NewLifecycle(modB, modA)
assert.NoError(t, lc.Setup())
assert.Equal(t, []string{"setup:a", "setup:b"}, order)
order = nil
assert.NoError(t, lc.Teardown())
assert.Equal(t, []string{"teardown:b", "teardown:a"}, order)
}
func TestSetupNoDoubleSetupWithAutoload(t *testing.T) {
var count int
modA := app.NewModule("a", app.ModuleOpts{
Setup: func(m *app.Module) error { count++; return nil },
})
modB := app.NewModule("b", app.ModuleOpts{
Setup: func(m *app.Module) error { count++; return nil },
Depends: []string{"a"},
})
lc := app.NewLifecycle(modB, modA)
assert.NoError(t, lc.Setup())
assert.Equal(t, 2, count)
}
func TestSetupPartialFailureRollback(t *testing.T) {
var tornDown bool
modA := app.NewModule("a", app.ModuleOpts{
Setup: func(m *app.Module) error { return nil },
Teardown: func(m *app.Module) error { tornDown = true; return nil },
})
modB := app.NewModule("b", app.ModuleOpts{
Setup: func(m *app.Module) error { return fmt.Errorf("fail b") },
})
lc := app.NewLifecycle(modA, modB)
err := lc.Setup()
assert.Error(t, err)
assert.True(t, tornDown)
assert.False(t, modA.Loaded())
}
func TestSetupCircularDependency(t *testing.T) {
modA := app.NewModule("a", app.ModuleOpts{Depends: []string{"b"}})
modB := app.NewModule("b", app.ModuleOpts{Depends: []string{"a"}})
lc := app.NewLifecycle(modA, modB)
err := lc.Setup()
assert.Error(t, err)
assert.Contains(t, err.Error(), "circular dependency")
}
func TestGetModule(t *testing.T) {
mod := app.NewModule("db", app.ModuleOpts{})
lc := app.NewLifecycle(mod)
got, err := lc.GetModule("db")
assert.NoError(t, err)
assert.Equal(t, mod, got)
_, err = lc.GetModule("missing")
assert.Error(t, err)
}