Files
app/lifecycle_test.go
Elijah Duffy c3c8a2cafc context helpers, rework logging to support ongoing inheritance
Leaves module logger property as purely an override, pulling logger
information directly from the lifecycle instead.
2025-06-04 13:37:13 -07:00

621 lines
16 KiB
Go

package app
import (
"errors"
"log/slog"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewLifecycle(t *testing.T) {
assert := assert.New(t)
// Create a blank Lifecycle instance
lc := NewLifecycle()
assert.NotNil(lc, "expected Lifecycle instance to be created")
assert.Equal(slog.Default(), lc.Logger(), "expected default logger to be available")
// Create a blank Lifecycle instance with a custom logger
handler := slog.NewTextHandler(os.Stderr, nil)
logger := slog.New(handler)
lc = NewLifecycle().WithOpts(LifecycleOpts{
Logger: logger,
})
assert.NotNil(lc)
assert.Equal(logger, lc.Logger(), "expected logger to match")
assert.Len(lc.modules, 0, "expected modules to be empty")
assert.Len(lc.setupTracker, 0, "expected setup tracker to be empty")
assert.Len(lc.teardownTracker, 0, "expected teardown tracker to be empty")
// Test with modules
mod1 := NewModule("module1", ModuleOpts{})
mod2 := NewModule("module2", ModuleOpts{})
lcWithModules := NewLifecycle(mod1, mod2)
assert.NotNil(lcWithModules, "expected Lifecycle with modules to be created")
assert.Contains(lcWithModules.modules, mod1, "expected module1 to be included")
assert.Contains(lcWithModules.modules, mod2, "expected module2 to be included")
assert.Len(lcWithModules.modules, 2, "expected two modules to be present")
// Test for duplicate module names
assert.PanicsWithValue("duplicate module: [module1]", func() {
NewLifecycle(mod1, mod1)
}, "expected panic on duplicate module names")
}
func TestLifecycle_WithOpts(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
opts := LifecycleOpts{
DisableAutoload: true,
}
lc.WithOpts(opts)
assert.Equal(opts, lc.opts, "expected Lifecycle to have the provided options")
}
func TestLifecycle_WithLogger(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
lc.WithLogger(logger)
assert.Equal(logger, lc.Logger(), "expected Lifecycle to have the provided logger")
}
func TestLifecycle_Logger(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
assert.Equal(slog.Default(), lc.Logger(), "expected Logger to return a logger")
// Test that custom logger is used
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
lc.opts.Logger = logger
assert.Equal(logger, lc.Logger())
}
func TestLifecycle_Setup(t *testing.T) {
cases := []struct {
name string
modules []*Module
expectedErr string
}{
{
name: "empty lifecycle",
modules: nil,
},
{
name: "single module with setup",
modules: []*Module{
NewModule("module1", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
},
},
{
name: "multiple modules",
modules: []*Module{
NewModule("module1", ModuleOpts{}),
NewModule("module2", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
},
},
{
name: "module with setup error",
modules: []*Module{
NewModule("module1", ModuleOpts{
Setup: func(m *Module) error { return errors.New("setup failed") },
}),
},
expectedErr: "setup failed",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle(tc.modules...)
err := lc.Setup()
if tc.expectedErr == "" {
assert.NoError(err, "expected Setup to succeed")
for _, mod := range tc.modules {
if mod.setup != nil {
_, ok := lc.setupTracker[mod.name]
assert.True(ok, "expected module to be marked as set up")
}
}
} else {
assert.Error(err, "expected Setup to fail")
assert.Contains(err.Error(), tc.expectedErr, "expected error message to match")
}
})
}
}
func TestLifecycle_Teardown(t *testing.T) {
cases := []struct {
name string
modules []*Module
expectedErr []string
}{
{
name: "empty lifecycle",
modules: nil,
},
{
name: "single module with teardown",
modules: []*Module{
NewModule("module1", ModuleOpts{
Teardown: func(m *Module) error { return nil },
}),
},
},
{
name: "multiple modules",
modules: []*Module{
NewModule("module1", ModuleOpts{}),
NewModule("module2", ModuleOpts{
Teardown: func(m *Module) error { return nil },
}),
},
},
{
name: "module with teardown error",
modules: []*Module{
NewModule("module1", ModuleOpts{
Teardown: func(m *Module) error { return errors.New("teardown failed") },
}),
},
expectedErr: []string{"teardown failed"},
},
{
name: "multiple modules with teardown errors",
modules: []*Module{
NewModule("module1", ModuleOpts{
Teardown: func(m *Module) error { return errors.New("module1 teardown failed") },
}),
NewModule("module2", ModuleOpts{
Teardown: func(m *Module) error { return errors.New("module2 teardown failed") },
}),
},
expectedErr: []string{"module1 teardown failed", "module2 teardown failed"},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle(tc.modules...)
// Fake setup for all modules
for _, mod := range tc.modules {
mod.loaded = true // Mark as loaded
lc.setupTracker[mod.name] = 0 // Mark as set up
}
err := lc.Teardown()
if len(tc.expectedErr) == 0 {
assert.NoError(err, "expected Teardown to succeed")
for _, mod := range tc.modules {
if mod.teardown != nil {
_, ok := lc.teardownTracker[mod.name]
assert.Truef(ok, "expected module %s to be marked as torn down", mod)
assert.Falsef(mod.loaded, "expected module %s to be unloaded after teardown", mod)
}
}
} else {
assert.Error(err, "expected Teardown to fail")
for _, expected := range tc.expectedErr {
assert.Contains(err.Error(), expected, "expected error message to match")
}
}
})
}
}
func TestLifecycle_Require(t *testing.T) {
lc := NewLifecycle()
mod := NewModule("module1", ModuleOpts{
Setup: func(m *Module) error { return nil },
})
err := lc.Require(mod)
assert.NoError(t, err, "expected Require to succeed")
assert.Contains(t, lc.setupTracker, mod.name, "expected module to be marked as set up")
assert.Nil(t, mod.logger, "expected module logger to be nil")
assert.Equal(t, lc.Logger(), mod.Logger(), "expected module logger to match lifecycle logger")
}
func TestLifecycle_RequireUnique(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
mod := NewModule("module1", ModuleOpts{})
err := lc.RequireUnique(mod)
assert.NoError(err, "expected RequireL to succeed")
assert.Nil(mod.logger, "expected module logger to be nil")
// Requiring the same module again should fail
err = lc.RequireUnique(mod)
assert.Error(err, "expected RequireUnique to fail on duplicate module")
}
func TestLifecycle_RequireL(t *testing.T) {
// Create a logger for the test cases
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
cases := []struct {
name string
modules []*Module
existingModules []*Module
expectedErr string
}{
{
name: "no modules",
expectedErr: "no modules to require",
},
{
name: "nil module",
modules: []*Module{nil},
expectedErr: "is nil",
},
{
name: "basic module",
modules: []*Module{
NewModule("testModule", ModuleOpts{
Setup: func(m *Module) error {
if m.name != "testModule" {
return errors.New("module name mismatch")
}
return nil
},
}),
},
},
{
name: "several modules",
modules: []*Module{
NewModule("module1", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
NewModule("module2", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
},
},
{
name: "module with setup error",
modules: []*Module{
NewModule("errorModule", ModuleOpts{
Setup: func(m *Module) error { return errors.New("setup failed") },
}),
},
expectedErr: "setup failed",
},
{
name: "module with dependency",
modules: []*Module{
NewModule("dependentModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
Depends: []string{"dependencyModule"},
}),
},
existingModules: []*Module{
NewModule("dependencyModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
},
},
{
name: "duplicate module names",
modules: []*Module{
NewModule("duplicateModule", ModuleOpts{}),
NewModule("duplicateModule", ModuleOpts{}),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle(tc.existingModules...)
err := lc.RequireL(logger, tc.modules...)
if tc.expectedErr == "" {
assert.NoError(err, "expected RequireL to succeed")
for i, mod := range tc.modules {
if mod != nil {
_, ok := lc.setupTracker[mod.name]
assert.Truef(ok, "expected module %d %s to be marked as set up", i, mod)
assert.Truef(mod.loaded, "expected module %d %s to be loaded", i, mod)
assert.Equalf(logger, mod.logger, "expected module %d %s logger to match", i, mod)
}
}
} else {
assert.Error(err, "expected RequireL to fail")
assert.Contains(err.Error(), tc.expectedErr, "expected error message to match")
}
})
}
}
func TestLifecycle_RequireUniqueL(t *testing.T) {
// Create a logger for the test cases
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
cases := []struct {
name string
modules []*Module
existingModules []string
expectedErr string
}{
{
name: "no existing modules",
modules: []*Module{
NewModule("module1", ModuleOpts{}),
},
},
{
name: "module already set up",
modules: []*Module{
NewModule("module1", ModuleOpts{}),
NewModule("module2", ModuleOpts{}),
},
existingModules: []string{"module1"},
expectedErr: "module [module1] is already set up, cannot require it again",
},
{
name: "nil module",
modules: []*Module{nil},
expectedErr: "is nil",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
// Fake existing modules
for _, modName := range tc.existingModules {
lc.setupTracker[modName] = 0 // Mark as set up
}
err := lc.RequireUniqueL(logger, tc.modules...)
if tc.expectedErr == "" {
assert.NoError(err, "expected RequireUniqueL to succeed")
} else {
assert.Error(err, "expected RequireUniqueL to fail")
assert.Contains(err.Error(), tc.expectedErr, "expected error message to match")
}
})
}
}
func TestLifecycle_setupSingle(t *testing.T) {
// Create a logger for the test cases
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
cases := []struct {
name string
lc *Lifecycle
targetModule *Module
modules []*Module
expectedErr string
logger *slog.Logger
}{
{
name: "nil module",
targetModule: nil,
expectedErr: "module is nil",
},
{
name: "basic module setup",
targetModule: NewModule("testModule", ModuleOpts{
Setup: func(m *Module) error {
if m.name != "testModule" {
return errors.New("module name mismatch")
}
return nil
},
}),
},
{
name: "module with dependencies",
targetModule: NewModule("dependentModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
Depends: []string{"dependencyModule"},
}),
modules: []*Module{
NewModule("dependencyModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
}),
},
},
{
name: "module with missing dependency",
targetModule: NewModule("dependentModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
Depends: []string{"missingDependency"},
}),
expectedErr: "error getting dependency",
},
{
name: "module setup error",
targetModule: NewModule("errorModule", ModuleOpts{
Setup: func(m *Module) error { return errors.New("setup failed") },
}),
expectedErr: "setup failed",
},
{
name: "module dependency setup error",
targetModule: NewModule("moduleWithoutError", ModuleOpts{
Setup: func(m *Module) error { return nil },
Depends: []string{"errorModule"},
}),
modules: []*Module{
NewModule("errorModule", ModuleOpts{
Setup: func(m *Module) error { return errors.New("dependency setup failed") },
}),
},
expectedErr: "dependency setup failed",
},
{
name: "dependency without autoload",
lc: NewLifecycle().WithOpts(LifecycleOpts{
DisableAutoload: true,
}),
targetModule: NewModule("dependentModule", ModuleOpts{
Setup: func(m *Module) error { return nil },
Depends: []string{"dependencyModule"},
}),
expectedErr: "not satisfied",
},
{
name: "module without setup function",
targetModule: NewModule("noSetupModule", ModuleOpts{}),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
l := tc.logger
if l == nil {
l = logger
}
lc := tc.lc
if lc == nil {
lc = NewLifecycle(tc.modules...)
}
err := lc.setupSingle(l, tc.targetModule)
if tc.expectedErr == "" {
assert.NoError(err, "expected no error from setupSingle")
} else {
assert.Contains(err.Error(), tc.expectedErr, "expected error message to match")
}
if tc.targetModule != nil {
assert.Equal(l, tc.targetModule.logger, "expected module logger to match")
}
})
}
}
func TestLifecycle_teardownSingle(t *testing.T) {
cases := []struct {
name string
targetModule *Module
modules []string
expectedErr string
}{
{
name: "nil module",
expectedErr: "module is nil",
},
{
name: "basic module teardown",
targetModule: NewModule("testModule", ModuleOpts{
Teardown: func(m *Module) error {
if m.name != "testModule" {
return errors.New("module name mismatch")
}
return nil
},
}),
modules: []string{"testModule"},
},
{
name: "module not set up",
targetModule: NewModule("notSetUpModule", ModuleOpts{
Teardown: func(m *Module) error { return nil },
}),
expectedErr: "not set up, cannot tear down",
},
{
name: "module without teardown function",
targetModule: NewModule("noTeardownModule", ModuleOpts{}),
modules: []string{"noTeardownModule"},
},
{
name: "module with teardown error",
targetModule: NewModule("errorModule", ModuleOpts{
Teardown: func(m *Module) error { return errors.New("teardown failed") },
}),
modules: []string{"errorModule"},
expectedErr: "error tearing down",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
// Fake setup for all modules
var setupCount int
for _, mod := range tc.modules {
lc.setupTracker[mod] = setupCount
setupCount++
}
err := lc.teardownSingle(tc.targetModule)
if tc.expectedErr == "" {
assert.NoError(err, "expected no error from teardownSingle")
} else {
assert.Contains(err.Error(), tc.expectedErr, "expected error message to match")
}
})
}
}
func Test_mapToString(t *testing.T) {
cases := []struct {
name string
input map[string]int
expected string
}{
{
name: "empty map",
input: map[string]int{},
expected: "[]",
},
{
name: "single entry",
input: map[string]int{"key1": 0},
expected: "['key1']",
},
{
name: "multiple entries",
input: map[string]int{"key1": 0, "key2": 3, "key3": 2},
expected: "['key1' 'key3' 'key2']",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
result := mapToString(tc.input)
assert.Equal(tc.expected, result, "expected mapToString to return correct string representation")
})
}
}