diff --git a/lifecycle.go b/lifecycle.go index ec3db23..9c7de38 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -1,16 +1,13 @@ package app import ( + "errors" "fmt" "log/slog" + "sort" + "strings" ) -// setupFn is a function that runs setup logic for a module. -type setupFn func() error - -// teardownFn is a function that runs teardown logic for a module. -type teardownFn func() - // LifecycleOpts contains user-exposed options when defining a lifecycle. type LifecycleOpts struct { // DisableAutoload disables dependency autoloading (enabled by default). @@ -24,10 +21,13 @@ type Lifecycle struct { // by the lifecycle itself using the default logger. logger *slog.Logger - modules []*Module - setupTracker map[string]bool - teardownTracker map[string]bool - opts LifecycleOpts + modules []*Module + opts LifecycleOpts + + setupCount int + setupTracker map[string]int + teardownCount int + teardownTracker map[string]int } // NewLifecycle creates a new Lifecycle instance with a default logger and the @@ -56,8 +56,8 @@ func NewLifecycleL(defaultLogger *slog.Logger, modules ...*Module) *Lifecycle { return &Lifecycle{ logger: defaultLogger, modules: modules, - setupTracker: make(map[string]bool), - teardownTracker: make(map[string]bool), + setupTracker: make(map[string]int), + teardownTracker: make(map[string]int), } } @@ -80,36 +80,37 @@ func (app *Lifecycle) Logger() *slog.Logger { // if dependencies are not satisfied. Lifecycle.Teardown should always be run at the end // of the application lifecycle to ensure all resources are cleaned up properly. func (app *Lifecycle) Setup() error { - setupCount := 0 for _, mod := range app.modules { - if ok, err := app.setupSingle(mod, app.logger); err != nil { + if err := app.setupSingle(mod, app.logger); err != nil { return err - } else if ok { - setupCount++ } } app.logger.Info("Lifecycle modules initialized, backend setup complete", - "count", setupCount, "modules", mapToString(app.setupTracker)) + "modules", mapToString(app.setupTracker)) return nil } // Teardown runs all module teardown functions in reverse order of setup. // Teardown should always be run at the end of the application lifecycle -// to ensure all resources are cleaned up properly. +// to ensure all resources are cleaned up properly. All module tear down +// errors are returned as a single error (non-blocking). func (app *Lifecycle) Teardown() error { - teardownCount := 0 + var err error for i := len(app.modules) - 1; i >= 0; i-- { - if ok, err := app.singleTeardown(app.modules[i]); err != nil { - return err - } else if ok { - teardownCount++ + if singleErr := app.teardownSingle(app.modules[i]); singleErr != nil { + err = errors.Join(err, singleErr) } } + if err != nil { + app.logger.Error("Error tearing down modules", "failures", app.setupCount-app.teardownCount, "error", err) + return err + } + app.logger.Info("All modules torn down, backend teardown complete", - "count", teardownCount, "modules", mapToString(app.teardownTracker)) + "modules", mapToString(app.teardownTracker)) return nil } @@ -127,6 +128,14 @@ func (app *Lifecycle) Require(modules ...*Module) error { // This variation is useful when you need to set up modules with a non- // default logger. func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error { + if logger == nil { + logger = app.logger + } + + if len(modules) == 0 { + return fmt.Errorf("no modules to require") + } + for _, mod := range modules { if mod == nil { return fmt.Errorf("module is nil") @@ -135,6 +144,8 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error { // Check if the module is already set up if _, ok := app.setupTracker[mod.name]; ok { app.logger.Warn("module already set up, ignoring", "module", mod.name) + mod.loaded = true + mod.logger = logger continue } @@ -142,7 +153,7 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error { app.modules = append(app.modules, mod) // Run the setup function for the module - if _, err := app.setupSingle(mod, logger); err != nil { + if err := app.setupSingle(mod, logger); err != nil { return fmt.Errorf("error setting up required module '%s': %w", mod.name, err) } } @@ -152,10 +163,11 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error { return nil } -// setupSingle is a helper function to set up a single module. -func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) (bool, error) { +// setupSingle is a helper function to set up a single module. Returns an error +// if the module cannot be set up or if dependencies are not satisfied. +func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) error { if mod == nil { - return false, fmt.Errorf("module is nil") + return fmt.Errorf("module is nil") } // Set the logger for the module @@ -165,59 +177,59 @@ func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) (bool, error for _, dep := range mod.depends { if _, ok := app.setupTracker[dep]; !ok { if app.opts.DisableAutoload { - return false, fmt.Errorf("dependency '%s' not satisfied for '%s'", dep, mod.name) + return fmt.Errorf("dependency '%s' not satisfied for '%s'", dep, mod.name) } else { // Attempt to set up the dependency - mod, err := app.getModuleByName(dep) + depmod, err := app.getModuleByName(dep) if err != nil { - return false, err + return fmt.Errorf("error getting dependency '%s' for '%s': %w", dep, mod.name, err) } - if _, err := app.setupSingle(mod, logger); err != nil { - return false, fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep, mod.name, err) + if err := app.setupSingle(depmod, logger); err != nil { + return fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep, mod.name, err) } - } } } if mod.setup != nil { // Run the setup function for the module - if err := mod.setup(); err != nil { - return false, fmt.Errorf("error initializing '%s': %w", mod.name, err) + if err := mod.setup(mod); err != nil { + return fmt.Errorf("error initializing '%s': %w", mod.name, err) } - - // Mark this module as setup - app.setupTracker[mod.name] = true - mod.loaded = true - return true, nil } - return false, nil + // Mark this module as setup + app.setupTracker[mod.name] = app.setupCount + app.setupCount++ + mod.loaded = true + return nil } -// singleTeardown is a helper function to tear down a single module. -func (app *Lifecycle) singleTeardown(mod *Module) (bool, error) { +// teardownSingle is a helper function to tear down a single module. Returns +// an error if anything goes wrong. +func (app *Lifecycle) teardownSingle(mod *Module) error { if mod == nil { - return false, fmt.Errorf("module is nil") + return fmt.Errorf("module is nil") } // Check if the module was set up if _, ok := app.setupTracker[mod.name]; !ok { - return false, nil + return fmt.Errorf("module '%s' is not set up, cannot tear down", mod.name) } // Run the teardown function for the module if mod.teardown != nil { - mod.teardown() - - // Mark this module as torn down - app.teardownTracker[mod.name] = true - mod.loaded = false - return true, nil + if err := mod.teardown(mod); err != nil { + return fmt.Errorf("error tearing down '%s': %w", mod.name, err) + } } - return false, nil + // Mark this module as torn down + app.teardownTracker[mod.name] = app.teardownCount + app.teardownCount++ + mod.loaded = false + return nil } // getModuleByName retrieves a module by its name from the lifecycle. @@ -227,21 +239,30 @@ func (app *Lifecycle) getModuleByName(name string) (*Module, error) { return mod, nil } } - return nil, fmt.Errorf("module '%s' not found", name) + return nil, ErrModuleNotFound } -// mapToString converts a map to an opinionated string representation. -func mapToString(m map[string]bool) string { +// mapToString converts a map to an ordered, opinionated string representation. +func mapToString(m map[string]int) string { if len(m) == 0 { return "[]" } - result := "[" + // Sort the map by value + values := make([]int, 0, len(m)) + reverseMap := make(map[int]string, len(m)) for k, v := range m { - if v { - result += fmt.Sprintf("'%s' ", k) + values = append(values, v) + reverseMap[v] = k + } + sort.Ints(values) + + result := make([]string, 0, len(m)) + for _, v := range values { + if name, ok := reverseMap[v]; ok { + result = append(result, fmt.Sprintf("'%s'", name)) } } - result = result[:len(result)-1] + "]" - return result + + return "[" + strings.Join(result, " ") + "]" } diff --git a/lifecycle_test.go b/lifecycle_test.go new file mode 100644 index 0000000..0d381e2 --- /dev/null +++ b/lifecycle_test.go @@ -0,0 +1,541 @@ +package app + +import ( + "errors" + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewLifecycle(t *testing.T) { + assert := assert.New(t) + + lc := NewLifecycle() + assert.NotNil(lc, "expected Lifecycle instance to be created") + assert.Equal(slog.Default(), lc.logger, "expected default logger to be set") +} + +func TestNewLifecycleL(t *testing.T) { + assert := assert.New(t) + + // Create a blank Lifecycle instance with a custom logger + logger := slog.Default() + lc := NewLifecycleL(logger) + + assert.NotNil(lc) + assert.Equal(logger, lc.logger, "expected logger to be set") + 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 := NewLifecycleL(logger, 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 name: module1", func() { + NewLifecycleL(logger, 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_Logger(t *testing.T) { + assert := assert.New(t) + + lc := NewLifecycle() + assert.Equal(slog.Default(), lc.Logger(), "expected Logger to return a logger") + + // Test panic when logger is not initialized + lc.logger = nil + assert.PanicsWithValue("lifecycle logger is not initialized", func() { + lc.Logger() + }, "expected Logger to panic when logger is not set") +} + +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 { + 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.True(ok, "expected module to be marked as torn down") + } + } + } 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 }, + }) + lc.Require(mod) + assert.Equal(t, lc.logger, mod.logger, "expected module logger to match lifecycle logger") +} + +func TestLifecycle_RequireL(t *testing.T) { + cases := []struct { + name string + logger *slog.Logger + modules []*Module + existingModules []*Module + expectedErr string + }{ + { + name: "no modules", + expectedErr: "no modules to require", + }, + { + name: "nil module", + modules: []*Module{nil}, + expectedErr: "module 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...) + + l := tc.logger + if l == nil { + l = lc.logger + } + + err := lc.RequireL(tc.logger, tc.modules...) + + if tc.expectedErr == "" { + assert.NoError(err, "expected RequireL to succeed") + for _, mod := range tc.modules { + if mod != nil { + _, ok := lc.setupTracker[mod.name] + assert.True(ok, "expected module to be marked as set up") + assert.True(mod.loaded, "expected module to be loaded") + assert.Equal(l, mod.logger, "expected module logger to match") + } + } + } else { + assert.Error(err, "expected RequireL to fail") + assert.Contains(err.Error(), tc.expectedErr, "expected error message to match") + } + }) + } + +} + +func TestLifecycle_setupSingle(t *testing.T) { + 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: "error initializing 'errorModule'", + }, + { + 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 = slog.Default() + } + + lc := tc.lc + if lc == nil { + lc = NewLifecycle(tc.modules...) + } + + err := lc.setupSingle(tc.targetModule, l) + + if tc.targetModule != nil { + assert.Equal(l, tc.targetModule.logger, "expected module logger to match") + } + + if tc.expectedErr == "" { + assert.NoError(err, "expected no error from setupSingle") + } else { + assert.Contains(err.Error(), tc.expectedErr, "expected error message 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") + }) + } +} diff --git a/module.go b/module.go index cc619fe..55bc551 100644 --- a/module.go +++ b/module.go @@ -1,17 +1,24 @@ package app import ( + "errors" "fmt" "log/slog" ) +// ErrModuleNotFound is returned when a module is not found in the lifecycle. +var ErrModuleNotFound = errors.New("module not found") + +// moduleFn is a function type for module setup and teardown functions. +type moduleFn func(*Module) error + // Module represents a sub-system of the application, with its setup and // teardown functions and dependencies. type Module struct { logger *slog.Logger name string - setup setupFn - teardown teardownFn + setup moduleFn + teardown moduleFn depends []string loaded bool // loaded indicates if the module has been set up } @@ -19,9 +26,9 @@ type Module struct { // ModuleOpts contains user-exposed options when defining a module. type ModuleOpts struct { // Setup is the setup function for the module. - Setup setupFn + Setup moduleFn // Teardown is the teardown function for the module. - Teardown teardownFn + Teardown moduleFn // Depends is a list of modules that this module depends on. Each entry must // be the exact name of a module registered in the lifecycle. Depends []string diff --git a/module_test.go b/module_test.go new file mode 100644 index 0000000..a6428c4 --- /dev/null +++ b/module_test.go @@ -0,0 +1,61 @@ +package app + +import ( + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewModule(t *testing.T) { + assert := assert.New(t) + + // Test creating a module with no options + mod := NewModule("testModule", ModuleOpts{}) + assert.NotNil(mod, "expected module to be created") + assert.Equal("testModule", mod.name, "expected module name to match") + assert.Nil(mod.setup, "expected setup function to be nil") + assert.Nil(mod.teardown, "expected teardown function to be nil") + assert.Empty(mod.depends, "expected dependencies to be empty") + assert.False(mod.loaded, "expected module to not be loaded") + + // Test creating a module with setup and teardown functions and dependencies + setupFn := func(m *Module) error { return nil } + teardownFn := func(m *Module) error { return nil } + + modWithOpts := NewModule("testModuleWithOpts", ModuleOpts{ + Setup: setupFn, + Teardown: teardownFn, + Depends: []string{"dependency1", "dependency2"}, + }) + assert.NotNil(modWithOpts, "expected module with options to be created") + assert.NotNil(modWithOpts.setup, "expected setup function to be set") + assert.NotNil(modWithOpts.teardown, "expected teardown function to be set") + assert.Equal([]string{"dependency1", "dependency2"}, modWithOpts.depends, "expected dependencies to match") +} + +func TestModule(t *testing.T) { + assert := assert.New(t) + + // Create a module and set its logger + mod := NewModule("testModule", ModuleOpts{}) + assert.Panics(func() { mod.Logger() }, "expected Logger to panic when logger is not set") + logger := slog.Default() + mod.logger = logger + + // Test Logger method + assert.Equal(logger, mod.Logger(), "expected Logger to return the correct logger") + + // Test Name method + assert.Equal("testModule", mod.Name(), "expected Name to return the module's name") + + // Test Loaded method + assert.False(mod.Loaded(), "expected Loaded to return false initially") + + // Test RequireLoaded method + assert.Panics(func() { mod.RequireLoaded() }, "expected RequireLoaded to panic when module is not loaded") + + // Test setting the module as loaded + mod.loaded = true + assert.NotPanics(func() { mod.RequireLoaded() }, "expected RequireLoaded to not panic when module is loaded") +}