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") }) } }