diff --git a/internal/daemon/oracle/oracle.go b/internal/daemon/oracle/oracle.go index dd8e396..4d4c3d2 100644 --- a/internal/daemon/oracle/oracle.go +++ b/internal/daemon/oracle/oracle.go @@ -34,7 +34,9 @@ import ( "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/log" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/recovery" + mrpb "google.golang.org/genproto/googleapis/monitoring/v3" cpb "github.com/GoogleCloudPlatform/workloadagent/protos/configuration" + odpb "github.com/GoogleCloudPlatform/workloadagent/protos/oraclediscovery" gapb "github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos/guestactions" ) @@ -43,8 +45,21 @@ const ( // where the agent is running, unlike registered channels that use a producer project. defaultChannel = "oracle-operations-ephemeral-channel" defaultLockTimeout = 24 * time.Hour + // discoveryCheckInterval is the interval to check if discovery has found an Oracle process. + discoveryCheckInterval = 5 * time.Second ) +// DiscoveryClient is the interface for Oracle discovery operations. +type DiscoveryClient interface { + Discover(ctx context.Context, cloudProps *cpb.CloudProperties, processes []servicecommunication.ProcessWrapper) (*odpb.Discovery, error) +} + +// MetricCollector is the interface for Oracle metrics collection. +type MetricCollector interface { + SendHealthMetricsToCloudMonitoring(ctx context.Context) []*mrpb.TimeSeries + SendDefaultMetricsToCloudMonitoring(ctx context.Context) []*mrpb.TimeSeries +} + // guestActionsManager is an interface satisfied by guestactions.GuestActions // to allow for mocking in tests. type guestActionsManager interface { @@ -80,11 +95,20 @@ type Service struct { CloudProps *cpb.CloudProperties metricCollectionRoutine *recovery.RecoverableRoutine discoveryRoutine *recovery.RecoverableRoutine + guestActionsRoutine *recovery.RecoverableRoutine currentSIDs []string CommonCh <-chan *servicecommunication.Message isProcessPresent bool processes []servicecommunication.ProcessWrapper processesMutex sync.Mutex + + // discovery performs Oracle discovery operations. + discovery DiscoveryClient + // newMetricCollector creates a new metrics collector. + // This is a factory function because the metrics collector is stateful (holds DB connections) + // and runs inside a recoverable routine. If the routine crashes and restarts, we need + // to create a fresh collector with new connections rather than reusing a potentially broken one. + newMetricCollector func(context.Context, *cpb.Configuration) (MetricCollector, error) } type runGuestActionsArgs struct { @@ -104,29 +128,72 @@ var oraProcessPrefixes = []string{"ora_pmon_", "db_pmon_"} // Start initiates the Oracle workload agent service func (s *Service) Start(ctx context.Context, a any) { + s.initializeDependencies() + go (func() { for { s.checkServiceCommunication(ctx) } })() + + if !s.waitForWorkload(ctx) { + return + } + + if runtime.GOOS != "linux" { + log.CtxLogger(ctx).Error("Oracle service is only supported on Linux") + return + } + + s.startDiscoveryRoutine(ctx) + s.startMetricCollectionRoutine(ctx) + s.startGuestActionsRoutine(ctx) + + select { + case <-ctx.Done(): + log.CtxLogger(ctx).Info("Oracle workload agent service cancellation requested") + return + } +} + +func (s *Service) initializeDependencies() { + if s.discovery == nil { + s.discovery = oraclediscovery.New() + } + if s.newMetricCollector == nil { + s.newMetricCollector = func(ctx context.Context, cfg *cpb.Configuration) (MetricCollector, error) { + return oraclemetrics.New(ctx, cfg) + } + } +} + +// waitForWorkload checks if the service should be enabled. +// If enabled field is unset, it waits for the workload to be present. +// Returns true if the service should proceed, false otherwise. +func (s *Service) waitForWorkload(ctx context.Context) bool { // Check if the enabled field is unset. If it is, then the service is still enabled if the workload is present. if s.Config.GetOracleConfiguration().Enabled == nil { log.CtxLogger(ctx).Info("Oracle service enabled field is not set, will check for workload presence to determine if service should be enabled.") // If the workload is present, proceed with starting the service even if it is not enabled. for !s.isProcessPresent { - time.Sleep(5 * time.Second) + select { + case <-ctx.Done(): + return false + case <-time.After(discoveryCheckInterval): + continue + } } log.CtxLogger(ctx).Info("Oracle workload is present. Starting service.") - } else if !s.Config.GetOracleConfiguration().GetEnabled() { - log.CtxLogger(ctx).Info("Oracle service is disabled") - return + return true } - - if runtime.GOOS != "linux" { - log.CtxLogger(ctx).Error("Oracle service is only supported on Linux") - return + if !s.Config.GetOracleConfiguration().GetEnabled() { + log.CtxLogger(ctx).Info("Oracle service is disabled") + return false } + return true +} +func (s *Service) startDiscoveryRoutine(ctx context.Context) { if s.Config.GetOracleConfiguration().GetOracleDiscovery().GetEnabled() { dCtx := log.SetCtx(ctx, "context", "OracleDiscovery") s.discoveryRoutine = &recovery.RecoverableRoutine{ @@ -138,7 +205,9 @@ func (s *Service) Start(ctx context.Context, a any) { } s.discoveryRoutine.StartRoutine(dCtx) } +} +func (s *Service) startMetricCollectionRoutine(ctx context.Context) { if s.Config.GetOracleConfiguration().GetOracleMetrics().GetEnabled() { mcCtx := log.SetCtx(ctx, "context", "OracleMetricCollection") s.metricCollectionRoutine = &recovery.RecoverableRoutine{ @@ -150,9 +219,11 @@ func (s *Service) Start(ctx context.Context, a any) { } s.metricCollectionRoutine.StartRoutine(mcCtx) } +} +func (s *Service) startGuestActionsRoutine(ctx context.Context) { gaCtx := log.SetCtx(ctx, "context", "OracleGuestActions") - guestActionsRoutine := &recovery.RecoverableRoutine{ + s.guestActionsRoutine = &recovery.RecoverableRoutine{ Routine: runGuestActions, RoutineArg: runGuestActionsArgs{s: s, handlers: guestActionHandlers()}, ErrorCode: usagemetrics.GuestActionsFailure, @@ -160,13 +231,7 @@ func (s *Service) Start(ctx context.Context, a any) { ExpectedMinDuration: 10 * time.Second, } log.CtxLogger(ctx).Info("Starting guest actions routine") - guestActionsRoutine.StartRoutine(gaCtx) - - select { - case <-ctx.Done(): - log.CtxLogger(ctx).Info("Oracle workload agent service cancellation requested") - return - } + s.guestActionsRoutine.StartRoutine(gaCtx) } func guestActionHandlers() map[string]guestactions.GuestActionHandler { @@ -230,12 +295,12 @@ func runDiscovery(ctx context.Context, a any) { log.CtxLogger(ctx).Error("args is not of type runDiscoveryArgs") return } - s := args.s - ticker := time.NewTicker(args.s.Config.GetOracleConfiguration().GetOracleDiscovery().GetUpdateFrequency().AsDuration()) + s := args.s + ticker := time.NewTicker(s.Config.GetOracleConfiguration().GetOracleDiscovery().GetUpdateFrequency().AsDuration()) defer ticker.Stop() - ds := oraclediscovery.New() + ds := s.discovery for { // Discovery data is not used yet. @@ -244,18 +309,15 @@ func runDiscovery(ctx context.Context, a any) { s.processesMutex.Unlock() // Don't start discovery until processes are populated. for processes == nil { - time.Sleep(5 * time.Second) - s.processesMutex.Lock() - processes = s.processes - s.processesMutex.Unlock() - // Respect context cancellation. select { case <-ctx.Done(): log.CtxLogger(ctx).Info("Oracle Discovery cancellation requested") return - default: - continue + case <-time.After(discoveryCheckInterval): } + s.processesMutex.Lock() + processes = s.processes + s.processesMutex.Unlock() } _, err := ds.Discover(ctx, s.CloudProps, processes) if err != nil { @@ -284,7 +346,7 @@ func runMetricCollection(ctx context.Context, a any) { ticker := time.NewTicker(args.s.Config.GetOracleConfiguration().GetOracleMetrics().GetCollectionFrequency().AsDuration()) defer ticker.Stop() - metricCollector, err := oraclemetrics.New(ctx, args.s.Config) + metricCollector, err := args.s.newMetricCollector(ctx, args.s.Config) if err != nil { log.CtxLogger(ctx).Errorw("Failed to initialize metric collector", "error", err) return diff --git a/internal/daemon/oracle/oracle_test.go b/internal/daemon/oracle/oracle_test.go index aa5ebd5..3950092 100644 --- a/internal/daemon/oracle/oracle_test.go +++ b/internal/daemon/oracle/oracle_test.go @@ -18,14 +18,22 @@ package oracle import ( "context" + "reflect" + "sync" "testing" "time" "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" - cpb "github.com/GoogleCloudPlatform/workloadagent/protos/configuration" + "github.com/GoogleCloudPlatform/workloadagent/internal/servicecommunication" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/gce/metadataserver" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/guestactions" + + mrpb "google.golang.org/genproto/googleapis/monitoring/v3" + durationpb "google.golang.org/protobuf/types/known/durationpb" + cpb "github.com/GoogleCloudPlatform/workloadagent/protos/configuration" + odpb "github.com/GoogleCloudPlatform/workloadagent/protos/oraclediscovery" gapb "github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos/guestactions" ) @@ -33,12 +41,90 @@ import ( type fakeGuestActionsManager struct { startCalled bool startOpts guestactions.Options + started chan struct{} + onStart sync.Once } // Start captures the options passed and marks itself as called. func (f *fakeGuestActionsManager) Start(ctx context.Context, a any) { f.startCalled = true f.startOpts = a.(guestactions.Options) + if f.started != nil { + f.onStart.Do(func() { + close(f.started) + }) + } +} + +// fakeDiscoveryClient is a test double for DiscoveryClient. +type fakeDiscoveryClient struct { + discoverCalled bool + discoverErr error +} + +func (f *fakeDiscoveryClient) Discover(ctx context.Context, cloudProps *cpb.CloudProperties, processes []servicecommunication.ProcessWrapper) (*odpb.Discovery, error) { + f.discoverCalled = true + return nil, f.discoverErr +} + +// fakeMetricCollector is a test double for MetricCollector. +type fakeMetricCollector struct { + healthMetricsCalled bool + defaultMetricsCalled bool +} + +func (f *fakeMetricCollector) SendHealthMetricsToCloudMonitoring(ctx context.Context) []*mrpb.TimeSeries { + f.healthMetricsCalled = true + return nil +} + +func (f *fakeMetricCollector) SendDefaultMetricsToCloudMonitoring(ctx context.Context) []*mrpb.TimeSeries { + f.defaultMetricsCalled = true + return nil +} + +type fakeProcess struct { + name string +} + +func (p fakeProcess) Name() (string, error) { + return p.name, nil +} + +func (p fakeProcess) Exe() (string, error) { + return "", nil +} + +func (p fakeProcess) Cmdline() (string, error) { + return "", nil +} + +func (p fakeProcess) CmdlineSlice() ([]string, error) { + return nil, nil +} + +func (p fakeProcess) Pid() int32 { + return 0 +} + +func (p fakeProcess) PPID() (int32, error) { + return 0, nil +} + +func (p fakeProcess) Username() (string, error) { + return "", nil +} + +func (p fakeProcess) CreateTime() (int64, error) { + return 0, nil +} + +func (p fakeProcess) Environ() ([]string, error) { + return nil, nil +} + +func (p fakeProcess) String() string { + return "" } func TestConvertCloudProperties(t *testing.T) { @@ -286,3 +372,453 @@ func TestGuestActionHandlers(t *testing.T) { } } } + +func TestInitializeDependencies(t *testing.T) { + t.Run("InitializeNilDependencies", func(t *testing.T) { + s := &Service{} + s.initializeDependencies() + if s.discovery == nil { + t.Errorf("initializeDependencies() did not initialize discovery") + } + if s.newMetricCollector == nil { + t.Errorf("initializeDependencies() did not initialize newMetricCollector") + } + }) + + t.Run("DoNotOverwriteExistingDependencies", func(t *testing.T) { + fakeDiscovery := &fakeDiscoveryClient{} + fakeMetricCollectorFactory := func(context.Context, *cpb.Configuration) (MetricCollector, error) { + return &fakeMetricCollector{}, nil + } + s := &Service{ + discovery: fakeDiscovery, + newMetricCollector: fakeMetricCollectorFactory, + } + s.initializeDependencies() + if s.discovery != fakeDiscovery { + t.Errorf("initializeDependencies() overwrote existing discovery dependency") + } + if reflect.ValueOf(s.newMetricCollector).Pointer() != reflect.ValueOf(fakeMetricCollectorFactory).Pointer() { + t.Errorf("initializeDependencies() overwrote existing newMetricCollector dependency") + } + }) +} + +func TestWaitForWorkload(t *testing.T) { + tests := []struct { + name string + enabled *bool + setProcessPresent bool // if true, isProcessPresent will be set to true after a short delay + initialProcessPresent bool + want bool + }{ + { + name: "EnabledNilProcessEventuallyPresent", + enabled: nil, + setProcessPresent: true, + initialProcessPresent: false, + want: true, + }, + { + name: "EnabledNilProcessNeverPresent", + enabled: nil, + setProcessPresent: false, + initialProcessPresent: false, + want: false, + }, + { + name: "EnabledNilProcessPresentInitially", + enabled: nil, + setProcessPresent: false, + initialProcessPresent: true, + want: true, + }, + { + name: "EnabledTrue", + enabled: proto.Bool(true), + want: true, + }, + { + name: "EnabledFalse", + enabled: proto.Bool(false), + want: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + s := &Service{ + Config: &cpb.Configuration{ + OracleConfiguration: &cpb.OracleConfiguration{Enabled: tc.enabled}, + }, + isProcessPresent: tc.initialProcessPresent, + } + ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second) + defer cancel() + + if tc.setProcessPresent { + go func() { + time.Sleep(50 * time.Millisecond) + s.isProcessPresent = true + }() + } + + got := s.waitForWorkload(ctx) + if got != tc.want { + t.Errorf("waitForWorkload() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestCheckServiceCommunication(t *testing.T) { + tests := []struct { + name string + message *servicecommunication.Message + wantIsProcessPresent bool + wantProcesses []servicecommunication.ProcessWrapper + }{ + { + name: "discoveryWithNoProcess", + message: &servicecommunication.Message{ + Origin: servicecommunication.Discovery, + DiscoveryResult: servicecommunication.DiscoveryResult{Processes: []servicecommunication.ProcessWrapper{}}, + }, + wantIsProcessPresent: false, + wantProcesses: []servicecommunication.ProcessWrapper{}, + }, + { + name: "discoveryWithOracleProcess", + message: &servicecommunication.Message{ + Origin: servicecommunication.Discovery, + DiscoveryResult: servicecommunication.DiscoveryResult{ + Processes: []servicecommunication.ProcessWrapper{ + fakeProcess{name: "ora_pmon_ORCL"}, + }, + }, + }, + wantIsProcessPresent: true, + wantProcesses: []servicecommunication.ProcessWrapper{ + fakeProcess{name: "ora_pmon_ORCL"}, + }, + }, + { + name: "discoveryWithNonOracleProcess", + message: &servicecommunication.Message{ + Origin: servicecommunication.Discovery, + DiscoveryResult: servicecommunication.DiscoveryResult{ + Processes: []servicecommunication.ProcessWrapper{ + fakeProcess{name: "other_process"}, + }, + }, + }, + wantIsProcessPresent: false, + wantProcesses: []servicecommunication.ProcessWrapper{ + fakeProcess{name: "other_process"}, + }, + }, + { + name: "otherMessageType", + message: &servicecommunication.Message{ + Origin: servicecommunication.DWActivation, + }, + wantIsProcessPresent: false, + wantProcesses: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ch := make(chan *servicecommunication.Message, 1) + s := &Service{ + CommonCh: ch, + } + + ch <- tc.message + s.checkServiceCommunication(context.Background()) + + if s.isProcessPresent != tc.wantIsProcessPresent { + t.Errorf("checkServiceCommunication() isProcessPresent = %v, want %v", s.isProcessPresent, tc.wantIsProcessPresent) + } + + if diff := cmp.Diff(tc.wantProcesses, s.processes, protocmp.Transform(), cmp.AllowUnexported(fakeProcess{})); diff != "" { + t.Errorf("checkServiceCommunication() processes returned diff (-want +got):\n%s", diff) + } + }) + } +} + +func TestRunDiscovery(t *testing.T) { + fdc := &fakeDiscoveryClient{} + s := &Service{ + discovery: fdc, + Config: &cpb.Configuration{ + OracleConfiguration: &cpb.OracleConfiguration{ + OracleDiscovery: &cpb.OracleDiscovery{ + UpdateFrequency: durationpb.New(1 * time.Second), + }, + }, + }, + processes: []servicecommunication.ProcessWrapper{fakeProcess{name: "ora_pmon_"}}, + } + ctx, cancel := context.WithCancel(context.Background()) + go func() { + // allow one run of discover + time.Sleep(100 * time.Millisecond) + cancel() + }() + runDiscovery(ctx, runDiscoveryArgs{s}) + if !fdc.discoverCalled { + t.Errorf("runDiscovery() did not call Discover") + } +} + +func TestStartDiscoveryRoutine(t *testing.T) { + tests := []struct { + name string + enabled bool + wantRoutineStarted bool + }{ + { + name: "Enabled", + enabled: true, + wantRoutineStarted: true, + }, + { + name: "Disabled", + enabled: false, + wantRoutineStarted: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + s := &Service{ + Config: &cpb.Configuration{ + OracleConfiguration: &cpb.OracleConfiguration{ + OracleDiscovery: &cpb.OracleDiscovery{ + Enabled: proto.Bool(tc.enabled), + UpdateFrequency: durationpb.New(1 * time.Hour), + }, + }, + }, + discovery: &fakeDiscoveryClient{}, + processes: []servicecommunication.ProcessWrapper{fakeProcess{name: "ora_pmon_"}}, + } + ctx, cancel := context.WithCancel(context.Background()) + s.startDiscoveryRoutine(ctx) + cancel() + gotRoutineStarted := s.discoveryRoutine != nil + if gotRoutineStarted != tc.wantRoutineStarted { + t.Errorf("startDiscoveryRoutine() routine started = %t, want %t", gotRoutineStarted, tc.wantRoutineStarted) + } + }) + } +} + +func TestStartMetricCollectionRoutine(t *testing.T) { + tests := []struct { + name string + enabled bool + wantRoutineStarted bool + }{ + { + name: "Enabled", + enabled: true, + wantRoutineStarted: true, + }, + { + name: "Disabled", + enabled: false, + wantRoutineStarted: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + s := &Service{ + Config: &cpb.Configuration{ + OracleConfiguration: &cpb.OracleConfiguration{ + OracleMetrics: &cpb.OracleMetrics{ + Enabled: proto.Bool(tc.enabled), + CollectionFrequency: durationpb.New(1 * time.Hour), + }, + }, + }, + newMetricCollector: func(context.Context, *cpb.Configuration) (MetricCollector, error) { + return &fakeMetricCollector{}, nil + }, + } + ctx, cancel := context.WithCancel(context.Background()) + s.startMetricCollectionRoutine(ctx) + cancel() + gotRoutineStarted := s.metricCollectionRoutine != nil + if gotRoutineStarted != tc.wantRoutineStarted { + t.Errorf("startMetricCollectionRoutine() routine started = %t, want %t", gotRoutineStarted, tc.wantRoutineStarted) + } + }) + } +} + +func TestStartGuestActionsRoutine(t *testing.T) { + originalNewGuestActionsManager := newGuestActionsManager + defer func() { + newGuestActionsManager = originalNewGuestActionsManager + }() + + fakeGA := &fakeGuestActionsManager{ + started: make(chan struct{}), + } + newGuestActionsManager = func() guestActionsManager { + return fakeGA + } + + s := &Service{} + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + s.startGuestActionsRoutine(ctx) + + if s.guestActionsRoutine == nil { + t.Errorf("startGuestActionsRoutine() routine got nil, want not nil") + } + + select { + case <-fakeGA.started: + case <-time.After(1 * time.Second): + t.Errorf("guestActionsManager.Start() was not called within timeout") + } +} + +func TestRunMetricCollection(t *testing.T) { + fmc := &fakeMetricCollector{} + s := &Service{ + newMetricCollector: func(context.Context, *cpb.Configuration) (MetricCollector, error) { + return fmc, nil + }, + Config: &cpb.Configuration{ + OracleConfiguration: &cpb.OracleConfiguration{ + OracleMetrics: &cpb.OracleMetrics{ + CollectionFrequency: durationpb.New(100 * time.Millisecond), + }, + }, + }, + } + ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) + defer cancel() + + runMetricCollection(ctx, runMetricCollectionArgs{s}) + + if !fmc.healthMetricsCalled { + t.Errorf("runMetricCollection() did not call SendHealthMetricsToCloudMonitoring") + } + if !fmc.defaultMetricsCalled { + t.Errorf("runMetricCollection() did not call SendDefaultMetricsToCloudMonitoring") + } +} + +func TestStart(t *testing.T) { + tests := []struct { + name string + oracleConfig *cpb.OracleConfiguration + wantDiscoveryRoutineStarted bool + wantMetricRoutineStarted bool + wantGuestActionsRoutineStarted bool + }{ + { + name: "oracleDisabled", + oracleConfig: &cpb.OracleConfiguration{ + Enabled: proto.Bool(false), + OracleDiscovery: &cpb.OracleDiscovery{ + Enabled: proto.Bool(true), + }, + OracleMetrics: &cpb.OracleMetrics{ + Enabled: proto.Bool(true), + }, + }, + wantDiscoveryRoutineStarted: false, + wantMetricRoutineStarted: false, + wantGuestActionsRoutineStarted: false, + }, + { + name: "oracleEnabled", + oracleConfig: &cpb.OracleConfiguration{ + Enabled: proto.Bool(true), + OracleDiscovery: &cpb.OracleDiscovery{ + Enabled: proto.Bool(true), + UpdateFrequency: durationpb.New(1 * time.Hour), + }, + OracleMetrics: &cpb.OracleMetrics{ + Enabled: proto.Bool(true), + CollectionFrequency: durationpb.New(1 * time.Hour), + }, + }, + wantDiscoveryRoutineStarted: true, + wantMetricRoutineStarted: true, + wantGuestActionsRoutineStarted: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + originalNewGuestActionsManager := newGuestActionsManager + defer func() { + newGuestActionsManager = originalNewGuestActionsManager + }() + newGuestActionsManager = func() guestActionsManager { + return &fakeGuestActionsManager{} + } + + ch := make(chan *servicecommunication.Message) + s := &Service{ + Config: &cpb.Configuration{ + OracleConfiguration: tc.oracleConfig, + }, + CommonCh: ch, + discovery: &fakeDiscoveryClient{}, + newMetricCollector: func(context.Context, *cpb.Configuration) (MetricCollector, error) { + return &fakeMetricCollector{}, nil + }, + processes: []servicecommunication.ProcessWrapper{fakeProcess{name: "ora_pmon_"}}, + } + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + s.Start(ctx, nil) + }() + time.Sleep(100 * time.Millisecond) // give Start() time to run + cancel() + + if got := s.discoveryRoutine != nil; got != tc.wantDiscoveryRoutineStarted { + t.Errorf("Start() discoveryRoutine started = %t, want %t", got, tc.wantDiscoveryRoutineStarted) + } + if got := s.metricCollectionRoutine != nil; got != tc.wantMetricRoutineStarted { + t.Errorf("Start() metricCollectionRoutine started = %t, want %t", got, tc.wantMetricRoutineStarted) + } + if got := s.guestActionsRoutine != nil; got != tc.wantGuestActionsRoutineStarted { + t.Errorf("Start() guestActionsRoutine started = %t, want %t", got, tc.wantGuestActionsRoutineStarted) + } + }) + } +} + +func TestStart_InitializesDependencies(t *testing.T) { + s := &Service{ + Config: &cpb.Configuration{ + OracleConfiguration: &cpb.OracleConfiguration{ + Enabled: proto.Bool(false), + }, + }, + CommonCh: make(chan *servicecommunication.Message), + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Calling Start with disabled config will return early, but should initialize dependencies. + s.Start(ctx, nil) + + if s.discovery == nil { + t.Errorf("Start() did not initialize discovery") + } + if s.newMetricCollector == nil { + t.Errorf("Start() did not initialize newMetricCollector") + } +}