Leaves module logger property as purely an override, pulling logger information directly from the lifecycle instead.
621 lines
16 KiB
Go
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")
|
|
})
|
|
}
|
|
}
|