Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,4 @@ jobs:
slug: GoCodeAlone/modular
files: total-coverage.txt
flags: total
fail_ci_if_error: true
fail_ci_if_error: ${{ secrets.CODECOV_TOKEN != '' }}
2 changes: 1 addition & 1 deletion application.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ func (app *StdApplication) InitWithApp(appToPass Application) error {
defer func() {
if r := recover(); r != nil {
mu.Lock()
errs = append(errs, fmt.Errorf("panic initializing module %s: %v", name, r))
errs = append(errs, fmt.Errorf("%w %s: %v", ErrModuleInitializationPanic, name, r))
mu.Unlock()
}
}()
Expand Down
2 changes: 1 addition & 1 deletion chimux/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ func (m *ChiMuxModule) setupMiddleware(app modular.Application) error {
middlewareProviderType := reflect.TypeOf((*MiddlewareProvider)(nil)).Elem()

if serviceType.Implements(middlewareProviderType) ||
(serviceType.Kind() == reflect.Ptr && serviceType.Elem().Implements(middlewareProviderType)) {
(serviceType.Kind() == reflect.Pointer && serviceType.Elem().Implements(middlewareProviderType)) {
if provider, ok := service.(MiddlewareProvider); ok {
middlewareProviders = append(middlewareProviders, provider)
m.logger.Debug("Found middleware provider", "name", name)
Expand Down
5 changes: 5 additions & 0 deletions chimux/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ func TestModule_CustomMiddleware(t *testing.T) {
err = mockApp.RegisterService("test.middleware.provider", middlewareProvider)
require.NoError(t, err)

// Register a pointer service that does not implement MiddlewareProvider so
// setupMiddleware exercises its pointer-kind interface check.
err = mockApp.RegisterService("test.non.middleware", &struct{ Name string }{Name: "not-middleware"})
require.NoError(t, err)

// Register observers before Init
err = module.RegisterObservers(mockApp)
require.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/modcli/cmd/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func runDebugInterface(cmd *cobra.Command, args []string) error {
fmt.Fprintf(cmd.OutOrStdout(), "1. Load type '%s' using reflection\n", typeName)
fmt.Fprintf(cmd.OutOrStdout(), "2. Load interface '%s' using reflection\n", interfaceName)
fmt.Fprintf(cmd.OutOrStdout(), "3. Check: serviceType.Implements(interfaceType)\n")
fmt.Fprintf(cmd.OutOrStdout(), "4. Check: serviceType.Kind() == reflect.Ptr && serviceType.Elem().Implements(interfaceType)\n")
fmt.Fprintf(cmd.OutOrStdout(), "4. Check: serviceType.Kind() == reflect.Pointer && serviceType.Elem().Implements(interfaceType)\n")
fmt.Fprintln(cmd.OutOrStdout())
}

Expand Down
17 changes: 17 additions & 0 deletions cmd/modcli/cmd/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,23 @@ func TestDebugDependenciesCommand(t *testing.T) {
}
}

func TestDebugInterfaceCommandUnknownPatternShowsPointerKindCheck(t *testing.T) {
cmd := NewDebugInterfaceCommand()

var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
cmd.SetArgs([]string{
"--type", "unknown.Service",
"--interface", "unknown.Interface",
})

err := cmd.Execute()
require.NoError(t, err)

assert.Contains(t, buf.String(), "serviceType.Kind() == reflect.Pointer")
}

func TestDebugConfigTreeStructure(t *testing.T) {
tmpDir := createTestProject(t)
defer os.RemoveAll(tmpDir)
Expand Down
4 changes: 2 additions & 2 deletions contract_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (v *StandardContractVerifier) runReloadWithGuard(module Reloadable, label s
go func() {
defer func() {
if r := recover(); r != nil {
ch <- result{err: fmt.Errorf("Reload panicked: %v", r)}
ch <- result{err: fmt.Errorf("%w: %v", ErrReloadPanic, r)}
}
}()
ch <- result{err: module.Reload(ctx, nil)}
Expand Down Expand Up @@ -177,7 +177,7 @@ func (v *StandardContractVerifier) VerifyHealthContract(provider HealthProvider)
go func() {
defer func() {
if r := recover(); r != nil {
ch <- result{err: fmt.Errorf("HealthCheck panicked: %v", r)}
ch <- result{err: fmt.Errorf("%w: %v", ErrHealthCheckPanic, r)}
}
}()
reports, err := provider.HealthCheck(ctx)
Expand Down
45 changes: 45 additions & 0 deletions contract_verifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package modular

import (
"context"
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -30,6 +31,15 @@ type panickyReloadable struct{ wellBehavedReloadable }

func (p *panickyReloadable) CanReload() bool { panic("boom") }

type reloadPanicReloadable struct{ wellBehavedReloadable }

func (p *reloadPanicReloadable) Reload(ctx context.Context, _ []ConfigChange) error {
if err := ctx.Err(); err != nil {
return err
}
panic("reload boom")
}

// --- Mock HealthProviders for contract tests ---

// wellBehavedHealthProvider returns a proper report and respects cancellation.
Expand Down Expand Up @@ -81,6 +91,15 @@ func (c *cancelIgnoringHealthProvider) HealthCheck(_ context.Context) ([]HealthR
}, nil
}

type panicOnActiveHealthProvider struct{}

func (p *panicOnActiveHealthProvider) HealthCheck(ctx context.Context) ([]HealthReport, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
panic("health boom")
}

// --- Tests ---

func TestContractVerifier_ReloadWellBehaved(t *testing.T) {
Expand Down Expand Up @@ -123,6 +142,23 @@ func TestContractVerifier_ReloadPanicsOnCanReload(t *testing.T) {
}
}

func TestContractVerifier_ReloadPanicUsesSentinelError(t *testing.T) {
verifier := NewStandardContractVerifier()
violations := verifier.VerifyReloadContract(&reloadPanicReloadable{})

found := false
for _, v := range violations {
if v.Rule == "empty-reload-must-be-idempotent" &&
strings.Contains(v.Description, ErrReloadPanic.Error()) {
found = true
break
}
}
if !found {
t.Fatalf("expected reload panic sentinel in violations, got: %+v", violations)
}
}

func TestContractVerifier_HealthWellBehaved(t *testing.T) {
verifier := NewStandardContractVerifier()
violations := verifier.VerifyHealthContract(&wellBehavedHealthProvider{})
Expand Down Expand Up @@ -162,3 +198,12 @@ func TestContractVerifier_HealthIgnoresCancellation(t *testing.T) {
t.Fatalf("expected violation for ignoring cancellation, got: %+v", violations)
}
}

func TestContractVerifier_HealthPanicIsGuarded(t *testing.T) {
verifier := NewStandardContractVerifier()

// The first HealthCheck call panics and is recovered by the verifier. The
// cancellation check returns ctx.Err, so the test only fails if the guard is
// not active.
_ = verifier.VerifyHealthContract(&panicOnActiveHealthProvider{})
}
15 changes: 9 additions & 6 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,15 @@ var (
ErrTenantIsolationViolation = errors.New("tenant isolation violation")

// Reload errors
ErrReloadCircuitBreakerOpen = errors.New("reload circuit breaker is open; backing off")
ErrReloadChannelFull = errors.New("reload request channel is full")
ErrReloadInProgress = errors.New("reload already in progress")
ErrReloadStopped = errors.New("reload orchestrator is stopped")
ErrReloadTimeout = errors.New("reload timed out waiting for module")
ErrDynamicReloadNotEnabled = errors.New("dynamic reload not enabled")
ErrReloadCircuitBreakerOpen = errors.New("reload circuit breaker is open; backing off")
ErrReloadChannelFull = errors.New("reload request channel is full")
ErrReloadInProgress = errors.New("reload already in progress")
ErrReloadStopped = errors.New("reload orchestrator is stopped")
ErrReloadTimeout = errors.New("reload timed out waiting for module")
ErrDynamicReloadNotEnabled = errors.New("dynamic reload not enabled")
ErrModuleInitializationPanic = errors.New("panic initializing module")
ErrReloadPanic = errors.New("reload panicked")
ErrHealthCheckPanic = errors.New("health check panicked")

// Observer/Event emission errors
ErrNoSubjectForEventEmission = errors.New("no subject available for event emission")
Expand Down
35 changes: 21 additions & 14 deletions eventlogger/bdd_error_handling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package eventlogger

import (
"context"
"errors"
"fmt"
"os"
"time"
Expand Down Expand Up @@ -62,14 +63,17 @@ func (ctx *EventLoggerBDDTestContext) errorsShouldBeHandledGracefully() error {

// Verify the module is still functional by emitting a test event
event := modular.NewCloudEvent("graceful.test", "test-source", map[string]interface{}{"test": "data"}, nil)
err := ctx.service.OnEvent(context.Background(), event)

// The module should handle this gracefully
if err != nil {
return fmt.Errorf("module should handle events gracefully: %v", err)
deadline := time.Now().Add(500 * time.Millisecond)
for {
err := ctx.service.OnEvent(context.Background(), event)
if err == nil {
return nil
}
if !errors.Is(err, ErrEventBufferFull) || time.Now().After(deadline) {
return fmt.Errorf("module should handle events gracefully: %v", err)
}
time.Sleep(10 * time.Millisecond)
}

return nil
}

func (ctx *EventLoggerBDDTestContext) otherOutputTargetsShouldContinueWorking() error {
Expand All @@ -82,14 +86,17 @@ func (ctx *EventLoggerBDDTestContext) otherOutputTargetsShouldContinueWorking()

// Emit a test event to verify other outputs still work
event := modular.NewCloudEvent("test.recovery", "test-source", map[string]interface{}{"test": "recovery"}, nil)
err := ctx.service.OnEvent(context.Background(), event)

// The error handling should ensure this succeeds even with faulty targets
if err != nil {
return fmt.Errorf("other output targets failed to work after error: %v", err)
deadline := time.Now().Add(500 * time.Millisecond)
for {
err := ctx.service.OnEvent(context.Background(), event)
if err == nil {
return nil
}
if !errors.Is(err, ErrEventBufferFull) || time.Now().After(deadline) {
return fmt.Errorf("other output targets failed to work after error: %v", err)
}
time.Sleep(10 * time.Millisecond)
}

return nil
}

func (ctx *EventLoggerBDDTestContext) iHaveAnEventLoggerWithFaultyOutputTargetAndEventObservationEnabled() error {
Expand Down
43 changes: 18 additions & 25 deletions eventlogger/bdd_queue_management_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ func (ctx *EventLoggerBDDTestContext) theEventsShouldBeQueuedWithoutErrors() err
}

func (ctx *EventLoggerBDDTestContext) theEventloggerStarts() error {
if ctx.app == nil && ctx.service != nil {
return ctx.service.Start(context.Background())
}
return ctx.app.Start()
}

Expand Down Expand Up @@ -222,35 +225,21 @@ func (ctx *EventLoggerBDDTestContext) iHaveAnEventLoggerModuleConfiguredWithQueu
return err
}

// Create config with console output
config := ctx.createConsoleConfig(10)

// Create application with the config
err = ctx.createApplicationWithConfig(config)
if err != nil {
return err
}

// Initialize the module but DON'T start it yet
err = ctx.theEventLoggerModuleIsInitialized()
if err != nil {
return err
}

// Get service reference
err = ctx.theEventLoggerServiceShouldBeAvailable()
if err != nil {
return err
}

// Inject test console output for capturing logs
ctx.testConsole = &testConsoleOutput{baseTestOutput: baseTestOutput{logs: make([]string, 0)}}
ctx.service.setOutputsForTesting([]OutputTarget{ctx.testConsole})

// Artificially reduce queue size for testing overflow
ctx.service.mutex.Lock()
ctx.service.queueMaxSize = 3 // Small queue for testing overflow
ctx.service.mutex.Unlock()
ctx.config = config
ctx.service = &EventLoggerModule{
name: ModuleName,
config: config,
outputs: []OutputTarget{ctx.testConsole},
eventChan: make(chan cloudevents.Event, config.BufferSize),
stopChan: make(chan struct{}),
eventQueue: make([]cloudevents.Event, 0, 3),
queueMaxSize: 3,
}
ctx.module = ctx.service

return nil
}
Expand Down Expand Up @@ -313,6 +302,10 @@ func (ctx *EventLoggerBDDTestContext) newerEventsShouldBePreservedInTheQueue() e
}

func (ctx *EventLoggerBDDTestContext) onlyThePreservedEventsShouldBeProcessed() error {
if ctx.app == nil && ctx.service != nil {
defer func() { _ = ctx.service.Stop(context.Background()) }()
}

// Wait longer for events to be processed with polling for queue clearance
maxWait := 2 * time.Second
checkInterval := 50 * time.Millisecond
Expand Down
6 changes: 3 additions & 3 deletions feeders/affixed_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ type AffixedEnvFeeder struct {
logger interface {
Debug(msg string, args ...any)
}
ft FieldTrackerHolder
priority int
ft FieldTrackerHolder
priority int
}

// NewAffixedEnvFeeder creates a new AffixedEnvFeeder with the specified prefix and suffix
Expand Down Expand Up @@ -79,7 +79,7 @@ func (f *AffixedEnvFeeder) Feed(structure interface{}) error {

inputType := reflect.TypeOf(structure)
if inputType != nil {
if inputType.Kind() == reflect.Ptr {
if inputType.Kind() == reflect.Pointer {
if inputType.Elem().Kind() == reflect.Struct {
return f.fillStruct(reflect.ValueOf(structure).Elem(), f.Prefix, f.Suffix)
}
Expand Down
2 changes: 1 addition & 1 deletion feeders/base_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type BaseConfigFeeder struct {
Environment string // Environment name (e.g., "prod", "staging", "dev")
verboseDebug bool
logger interface{ Debug(msg string, args ...any) }
ft FieldTrackerHolder
ft FieldTrackerHolder
}

// NewBaseConfigFeeder creates a new base configuration feeder
Expand Down
10 changes: 5 additions & 5 deletions feeders/dot_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ type DotEnvFeeder struct {
logger interface {
Debug(msg string, args ...any)
}
ft FieldTrackerHolder
envVars map[string]string // in-memory storage of parsed .env variables
priority int
ft FieldTrackerHolder
envVars map[string]string // in-memory storage of parsed .env variables
priority int
}

// NewDotEnvFeeder creates a new DotEnvFeeder that reads from the specified .env file
Expand Down Expand Up @@ -170,7 +170,7 @@ func (f *DotEnvFeeder) parseEnvLine(line string, lineNum int) error {
// populateStructFromCatalog populates struct fields from the global environment catalog
func (f *DotEnvFeeder) populateStructFromCatalog(structure interface{}, prefix string) error {
structValue := reflect.ValueOf(structure)
if structValue.Kind() != reflect.Ptr || structValue.Elem().Kind() != reflect.Struct {
if structValue.Kind() != reflect.Pointer || structValue.Elem().Kind() != reflect.Struct {
return wrapDotEnvStructureError(structure)
}

Expand Down Expand Up @@ -300,7 +300,7 @@ func (f *DotEnvFeeder) convertStringToType(value string, targetType reflect.Type
return boolVal, nil
case reflect.Invalid, reflect.Uintptr, reflect.Complex64, reflect.Complex128,
reflect.Array, reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
reflect.Ptr, reflect.Slice, reflect.Struct, reflect.UnsafePointer:
reflect.Pointer, reflect.Slice, reflect.Struct, reflect.UnsafePointer:
return nil, wrapDotEnvUnsupportedTypeError(targetType.Kind().String())
default:
return nil, wrapDotEnvUnsupportedTypeError(targetType.Kind().String())
Expand Down
Loading
Loading