package adapter import ( "context" "errors" "reflect" "testing" "time" "github.com/grafana/dskit/services" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/registry" ) const testTimeout = 200 * time.Millisecond func TestNewManagerAdapter(t *testing.T) { reg := &mockBackgroundServiceRegistry{} adapter := NewManagerAdapter(reg) require.NotNil(t, adapter) require.Equal(t, reg, adapter.reg) require.Nil(t, adapter.manager) require.NotNil(t, adapter.dependencyMap) } func TestManagerAdapter_Starting(t *testing.T) { t.Run("empty registry initializes manager", func(t *testing.T) { reg := &mockBackgroundServiceRegistry{services: []registry.BackgroundService{}} adapter := NewManagerAdapter(reg).WithDependencies(map[string][]string{ BackgroundServices: {Core}, Core: {}, }) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.StartAsync(ctx) require.NoError(t, err) err = adapter.AwaitRunning(ctx) require.NoError(t, err) }) t.Run("registers enabled services and skips disabled", func(t *testing.T) { enabledSvc := &mockService{} // Create a different type for the disabled service to distinguish them type disabledMockService struct{ mockService } disabledSvc := &disabledMockService{mockService{disabled: true}} namedSvc := &mockNamedService{name: "test-service"} reg := &mockBackgroundServiceRegistry{ services: []registry.BackgroundService{enabledSvc, disabledSvc, namedSvc}, } adapter := NewManagerAdapter(reg).WithDependencies(map[string][]string{ BackgroundServices: {Core}, Core: {}, }) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.StartAsync(ctx) require.NoError(t, err) err = adapter.AwaitRunning(ctx) require.NoError(t, err) // Verify dependency map was updated correctly // Should have entries for enabled services but not disabled ones require.Contains(t, adapter.dependencyMap, "*adapter.mockNamedService") // Named service require.Contains(t, adapter.dependencyMap, reflect.TypeOf(enabledSvc).String()) // Wrapped service require.NotContains(t, adapter.dependencyMap, reflect.TypeOf(disabledSvc).String()) // Disabled service should not be in map // Check that BackgroundServices depends on the enabled services bgDeps := adapter.dependencyMap[BackgroundServices] require.Contains(t, bgDeps, "*adapter.mockNamedService") require.Contains(t, bgDeps, reflect.TypeOf(enabledSvc).String()) require.NotContains(t, bgDeps, reflect.TypeOf(disabledSvc).String()) }) t.Run("handles services that are already NamedService", func(t *testing.T) { namedSvc := &mockNamedService{name: "already-named"} reg := &mockBackgroundServiceRegistry{services: []registry.BackgroundService{namedSvc}} adapter := NewManagerAdapter(reg) adapter.dependencyMap = map[string][]string{ BackgroundServices: {}, } ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.StartAsync(ctx) require.NoError(t, err) err = adapter.AwaitRunning(ctx) require.NoError(t, err) // Verify named service was added to dependency map require.Contains(t, adapter.dependencyMap, "*adapter.mockNamedService") }) t.Run("service already in dependency map is not added again", func(t *testing.T) { namedSvc := &mockNamedService{name: "existing-service"} reg := &mockBackgroundServiceRegistry{services: []registry.BackgroundService{namedSvc}} adapter := NewManagerAdapter(reg) // Pre-populate the dependency map with the service using the actual service name that will be used serviceName := "*adapter.mockNamedService" adapter.WithDependencies(map[string][]string{ serviceName: {BackgroundServices}, Core: {}, BackgroundServices: {Core}, }) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.starting(ctx) require.NoError(t, err) require.NotNil(t, adapter.manager) // Verify the existing dependency was not overwritten require.Equal(t, []string{BackgroundServices}, adapter.dependencyMap[serviceName]) // Verify BackgroundServices dependencies were not modified (should not contain the service twice) finalBgDeps := adapter.dependencyMap[BackgroundServices] require.Equal(t, []string{Core}, finalBgDeps) }) t.Run("service without NamedService interface gets wrapped", func(t *testing.T) { plainSvc := &mockService{} reg := &mockBackgroundServiceRegistry{services: []registry.BackgroundService{plainSvc}} adapter := NewManagerAdapter(reg).WithDependencies(map[string][]string{ BackgroundServices: {Core}, Core: {}, }) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.starting(ctx) require.NoError(t, err) require.NotNil(t, adapter.manager) // Verify the service was wrapped and added to dependency map using its type name expectedServiceName := reflect.TypeOf(plainSvc).String() require.Contains(t, adapter.dependencyMap, expectedServiceName) // Verify it was added to BackgroundServices dependencies bgDeps := adapter.dependencyMap[BackgroundServices] require.Contains(t, bgDeps, expectedServiceName) }) t.Run("service without CanBeDisabled interface is always enabled", func(t *testing.T) { simpleSvc := &simpleBackgroundService{} reg := &mockBackgroundServiceRegistry{services: []registry.BackgroundService{simpleSvc}} adapter := NewManagerAdapter(reg).WithDependencies(map[string][]string{ BackgroundServices: {Core}, Core: {}, }) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.starting(ctx) require.NoError(t, err) require.NotNil(t, adapter.manager) // Verify the service was added (since it doesn't implement CanBeDisabled, it's always enabled) expectedServiceName := reflect.TypeOf(simpleSvc).String() require.Contains(t, adapter.dependencyMap, expectedServiceName) }) } func TestManagerAdapter_Running(t *testing.T) { t.Run("runs with manager", func(t *testing.T) { mock := &mockNamedService{name: "mock"} reg := &mockBackgroundServiceRegistry{services: []registry.BackgroundService{ mock, }} adapter := NewManagerAdapter(reg).WithDependencies(map[string][]string{ BackgroundServices: {Core}, Core: {}, }) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.StartAsync(ctx) require.NoError(t, err) err = adapter.AwaitRunning(ctx) require.NoError(t, err) }) } func TestManagerAdapter_Stopping(t *testing.T) { t.Run("stopping method delegates to manager", func(t *testing.T) { mock := &mockNamedService{name: "test-service"} reg := &mockBackgroundServiceRegistry{services: []registry.BackgroundService{mock}} adapter := NewManagerAdapter(reg) adapter.dependencyMap = map[string][]string{ BackgroundServices: {}, } ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() // Initialize the manager first - need to go through starting to initialize manager err := adapter.starting(ctx) require.NoError(t, err) require.NotNil(t, adapter.manager) // Test the stopping method directly - this covers the stopping function err = adapter.stopping(nil) require.NoError(t, err) }) t.Run("stopping with failure reason", func(t *testing.T) { mock := &mockNamedService{name: "test-service"} reg := &mockBackgroundServiceRegistry{services: []registry.BackgroundService{mock}} adapter := NewManagerAdapter(reg) adapter.dependencyMap = map[string][]string{ BackgroundServices: {}, } ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.starting(ctx) require.NoError(t, err) require.NotNil(t, adapter.manager) // Test stopping with failure reason failure := errors.New("test failure") err = adapter.stopping(failure) require.NoError(t, err) }) } func TestManagerAdapter_Run(t *testing.T) { t.Run("successful run lifecycle", func(t *testing.T) { reg := &mockBackgroundServiceRegistry{} adapter := NewManagerAdapter(reg) // Create a mock basic service that we can control mockBasicService := &mockBasicService{} adapter.NamedService = mockBasicService ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.Run(ctx) require.NoError(t, err) require.True(t, mockBasicService.startAsyncCalled) require.True(t, mockBasicService.awaitTerminatedCalled) }) t.Run("returns StartAsync error", func(t *testing.T) { reg := &mockBackgroundServiceRegistry{} adapter := NewManagerAdapter(reg) expectedErr := errors.New("start error") mockBasicService := &mockBasicService{startAsyncError: expectedErr} adapter.NamedService = mockBasicService ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.Run(ctx) require.Error(t, err) require.Equal(t, expectedErr, err) require.True(t, mockBasicService.startAsyncCalled) require.False(t, mockBasicService.awaitTerminatedCalled) }) t.Run("returns AwaitTerminated error", func(t *testing.T) { reg := &mockBackgroundServiceRegistry{} adapter := NewManagerAdapter(reg) expectedErr := errors.New("await error") mockBasicService := &mockBasicService{awaitTerminatedError: expectedErr} adapter.NamedService = mockBasicService ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.Run(ctx) require.Error(t, err) require.Equal(t, expectedErr, err) require.True(t, mockBasicService.startAsyncCalled) require.True(t, mockBasicService.awaitTerminatedCalled) }) } func TestManagerAdapter_Shutdown(t *testing.T) { t.Run("calls StopAsync and AwaitTerminated", func(t *testing.T) { reg := &mockBackgroundServiceRegistry{} adapter := NewManagerAdapter(reg) mockBasicService := &mockBasicService{} adapter.NamedService = mockBasicService ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.Shutdown(ctx, "test shutdown") require.NoError(t, err) require.True(t, mockBasicService.stopAsyncCalled) require.True(t, mockBasicService.awaitTerminatedCalled) }) t.Run("returns AwaitTerminated error", func(t *testing.T) { reg := &mockBackgroundServiceRegistry{} adapter := NewManagerAdapter(reg) expectedErr := errors.New("await error") mockBasicService := &mockBasicService{awaitTerminatedError: expectedErr} adapter.NamedService = mockBasicService ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() err := adapter.Shutdown(ctx, "test shutdown") require.Error(t, err) require.Equal(t, expectedErr, err) }) } type mockBackgroundServiceRegistry struct { services []registry.BackgroundService } func (m *mockBackgroundServiceRegistry) GetServices() []registry.BackgroundService { return m.services } type mockService struct { runFunc func(ctx context.Context) error runCalled bool runContext context.Context runError error disabled bool } func (m *mockService) Run(ctx context.Context) error { m.runCalled = true m.runContext = ctx if m.runFunc != nil { return m.runFunc(ctx) } return m.runError } func (m *mockService) IsDisabled() bool { return m.disabled } type mockNamedService struct { mockService name string } func (m *mockNamedService) ServiceName() string { return m.name } func (m *mockNamedService) State() services.State { return services.New } func (m *mockNamedService) AddListener(listener services.Listener) func() { return func() {} } func (m *mockNamedService) FailureCase() error { return nil } type mockBasicService struct { startAsyncCalled bool startAsyncError error awaitTerminatedCalled bool awaitTerminatedError error stopAsyncCalled bool } func (m *mockBasicService) StartAsync(ctx context.Context) error { m.startAsyncCalled = true return m.startAsyncError } func (m *mockBasicService) AwaitRunning(ctx context.Context) error { return nil } func (m *mockBasicService) StopAsync() { m.stopAsyncCalled = true } func (m *mockBasicService) AwaitTerminated(ctx context.Context) error { m.awaitTerminatedCalled = true return m.awaitTerminatedError } func (m *mockBasicService) State() services.State { return services.New } func (m *mockBasicService) ServiceName() string { return "mockBasicService" } func (m *mockBasicService) AddListener(listener services.Listener) func() { return func() {} } func (m *mockBasicService) FailureCase() error { return nil } // simpleBackgroundService only implements BackgroundService, not CanBeDisabled type simpleBackgroundService struct { runCalled bool } func (s *simpleBackgroundService) Run(ctx context.Context) error { s.runCalled = true return nil }