Files
app/lifecycle_test.go
2025-06-04 14:03:53 -07:00

631 lines
16 KiB
Go

package app
import (
"context"
"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 TestLifecycleToFromContext(t *testing.T) {
assert := assert.New(t)
// Create a Lifecycle instance
lc := NewLifecycle()
// Add the Lifecycle to the context
ctx := LifecycleToContext(context.Background(), lc)
// Retrieve the Lifecycle from the context
retrievedLC := LifecycleFromContext(ctx)
assert.NotNil(retrievedLC, "expected Lifecycle to be retrieved from context")
assert.Equal(lc, retrievedLC, "expected retrieved Lifecycle to match original")
// Test with nil Lifecycle
nilCtx := LifecycleToContext(context.Background(), nil)
retrievedNilLC := LifecycleFromContext(nilCtx)
assert.Nil(retrievedNilLC, "expected nil Lifecycle to return nil from context")
}
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")
assert.PanicsWithValue("logger cannot be nil", func() {
lc.WithLogger(nil)
}, "expected panic when setting nil 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())
// Test with nil Lifecycle
assert.PanicsWithValue("lifecycle is nil, cannot get logger", func() {
var nilLC *Lifecycle
nilLC.Logger()
}, "expected panic when calling Logger on nil Lifecycle")
}
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) {
assert := assert.New(t)
lc := NewLifecycle()
// Test that duplicate modules are allowed
mod1 := NewModule("module1", ModuleOpts{})
mod2 := NewModule("module1", ModuleOpts{})
err := lc.Require(mod1, mod2)
assert.NoError(err, "expected Require to succeed with duplicate modules")
assert.Len(lc.modules, 1, "expected only one instance of the module to be added")
assert.Equal(lc.Logger(), mod1.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, mod)
assert.Error(err, "expected RequireUnique to fail on duplicate module")
}
func TestLifecycle_RequireL(t *testing.T) {
assert := assert.New(t)
// Create a logger for the test cases
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
lc := NewLifecycle()
// Test that duplicate modules are allowed with RequireL
mod1 := NewModule("module1", ModuleOpts{})
mod2 := NewModule("module1", ModuleOpts{})
err := lc.RequireL(logger, mod1, mod2)
assert.NoError(err, "expected RequireL to succeed with duplicate modules")
assert.Len(lc.modules, 1, "expected only one instance of the module to be added")
assert.Equal(logger, mod1.Logger(), "expected module logger to match provided logger")
// Test with nil logger
err = lc.RequireL(nil, mod1)
assert.Error(err, "expected RequireL to fail with nil logger")
}
func TestLifecycle_RequireUniqueL(t *testing.T) {
assert := assert.New(t)
// Create a logger for the test cases
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
lc := NewLifecycle()
// Test that duplicate modules are not allowed with RequireUniqueL
mod1 := NewModule("module1", ModuleOpts{})
mod2 := NewModule("module1", ModuleOpts{})
err := lc.RequireUniqueL(logger, mod1, mod2)
assert.Error(err, "expected RequireUniqueL to fail on duplicate module")
assert.Contains(err.Error(), "cannot require again", "expected error message to match")
// Test with nil logger
err = lc.RequireUniqueL(nil, mod1)
assert.Error(err, "expected RequireUniqueL to fail with nil logger")
}
func TestLifecycle_require(t *testing.T) {
cases := []struct {
name string
logger *slog.Logger
unique bool
modules []*Module
expectedErr string
length int
}{
{
name: "no modules",
expectedErr: "no modules to require",
},
{
name: "nil module",
modules: []*Module{
NewModule("", ModuleOpts{}),
nil,
},
expectedErr: "module 1 is nil",
},
{
name: "basic modules",
modules: []*Module{
NewModule("module1", ModuleOpts{}),
NewModule("module2", ModuleOpts{}),
},
},
{
name: "module with setup error",
modules: []*Module{
NewModule("errorModule", ModuleOpts{
Setup: func(m *Module) error { return errors.New("setup failed") },
}),
},
expectedErr: "error setting up required module",
},
{
name: "duplicate module names",
modules: []*Module{
NewModule("duplicateModule", ModuleOpts{}),
NewModule("duplicateModule", ModuleOpts{}),
},
length: 1, // Expect only one instance to be added
},
{
name: "duplicate module names with unique",
unique: true,
modules: []*Module{
NewModule("duplicateModule", ModuleOpts{}),
NewModule("duplicateModule", ModuleOpts{}),
},
expectedErr: "cannot require again",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
err := lc.require(tc.logger, tc.unique, tc.modules...)
if tc.expectedErr == "" {
assert.NoError(err, "expected require to succeed")
expectedLength := tc.length
if expectedLength == 0 {
expectedLength = len(tc.modules)
}
assert.Equal(expectedLength, len(lc.modules), "expected all modules to be added to lifecycle")
for _, mod := range tc.modules {
if mod != nil {
_, ok := lc.setupTracker[mod.name]
assert.Truef(ok, "expected module %s to be marked as set up", mod)
assert.Truef(mod.loaded, "expected module %s to be loaded", mod)
assert.Equalf(lc, mod.lifecycle, "expected module %s lifecycle to match", mod)
if tc.logger != nil {
assert.Equalf(tc.logger, mod.Logger(), "expected module %s logger to match", mod)
} else {
assert.Equalf(lc.Logger(), mod.Logger(), "expected module %s logger to match lifecycle logger", mod)
}
}
}
} else {
assert.Error(err, "expected require 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")
})
}
}