opentelemetry-collector/internal/sharedcomponent/sharedcomponent_test.go

280 lines
7.8 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package sharedcomponent
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/component/componenttest"
)
var id = component.MustNewID("test")
type baseComponent struct {
component.StartFunc
component.ShutdownFunc
telemetry *component.TelemetrySettings
}
func TestNewMap(t *testing.T) {
comps := NewMap[component.ID, *baseComponent]()
assert.Len(t, comps.components, 0)
}
func TestNewSharedComponentsCreateError(t *testing.T) {
comps := NewMap[component.ID, *baseComponent]()
assert.Len(t, comps.components, 0)
myErr := errors.New("my error")
_, err := comps.LoadOrStore(
id,
func() (*baseComponent, error) { return nil, myErr },
newNopTelemetrySettings(),
)
assert.ErrorIs(t, err, myErr)
assert.Len(t, comps.components, 0)
}
func TestSharedComponentsLoadOrStore(t *testing.T) {
nop := &baseComponent{}
comps := NewMap[component.ID, *baseComponent]()
got, err := comps.LoadOrStore(
id,
func() (*baseComponent, error) { return nop, nil },
newNopTelemetrySettings(),
)
require.NoError(t, err)
assert.Len(t, comps.components, 1)
assert.Same(t, nop, got.Unwrap())
gotSecond, err := comps.LoadOrStore(
id,
func() (*baseComponent, error) { panic("should not be called") },
newNopTelemetrySettings(),
)
require.NoError(t, err)
assert.Same(t, got, gotSecond)
// Shutdown nop will remove
assert.NoError(t, got.Shutdown(context.Background()))
assert.Len(t, comps.components, 0)
gotThird, err := comps.LoadOrStore(
id,
func() (*baseComponent, error) { return nop, nil },
newNopTelemetrySettings(),
)
require.NoError(t, err)
assert.NotSame(t, got, gotThird)
}
func TestSharedComponent(t *testing.T) {
wantErr := errors.New("my error")
calledStart := 0
calledStop := 0
comp := &baseComponent{
StartFunc: func(ctx context.Context, host component.Host) error {
calledStart++
return wantErr
},
ShutdownFunc: func(ctx context.Context) error {
calledStop++
return wantErr
}}
comps := NewMap[component.ID, *baseComponent]()
got, err := comps.LoadOrStore(
id,
func() (*baseComponent, error) { return comp, nil },
newNopTelemetrySettings(),
)
require.NoError(t, err)
assert.Equal(t, wantErr, got.Start(context.Background(), componenttest.NewNopHost()))
assert.Equal(t, 1, calledStart)
// Second time is not called anymore.
assert.NoError(t, got.Start(context.Background(), componenttest.NewNopHost()))
assert.Equal(t, 1, calledStart)
// first time, shutdown is called.
assert.Equal(t, wantErr, got.Shutdown(context.Background()))
assert.Equal(t, 1, calledStop)
// Second time is not called anymore.
assert.NoError(t, got.Shutdown(context.Background()))
assert.Equal(t, 1, calledStop)
}
func TestSharedComponentsReportStatus(t *testing.T) {
reportedStatuses := make(map[*component.InstanceID][]component.Status)
newStatusFunc := func() func(*component.StatusEvent) {
instanceID := &component.InstanceID{}
return func(ev *component.StatusEvent) {
if ev.Status() == component.StatusNone {
return
}
reportedStatuses[instanceID] = append(reportedStatuses[instanceID], ev.Status())
}
}
comp := &baseComponent{}
comps := NewMap[component.ID, *baseComponent]()
var telemetrySettings *component.TelemetrySettings
// make a shared component that represents three instances
for i := 0; i < 3; i++ {
telemetrySettings = newNopTelemetrySettings()
telemetrySettings.ReportStatus = newStatusFunc()
// The initial settings for the shared component need to match the ones passed to the first
// invocation of LoadOrStore so that underlying telemetry settings reference can be used to
// wrap ReportStatus for subsequently added "instances".
if i == 0 {
comp.telemetry = telemetrySettings
}
got, err := comps.LoadOrStore(
id,
func() (*baseComponent, error) { return comp, nil },
telemetrySettings,
)
require.NoError(t, err)
assert.Len(t, comps.components, 1)
assert.Same(t, comp, got.Unwrap())
}
// make sure we don't try to represent a fourth instance if we reuse a telemetrySettings
_, _ = comps.LoadOrStore(
id,
func() (*baseComponent, error) { return comp, nil },
telemetrySettings,
)
comp.telemetry.ReportStatus(component.NewStatusEvent(component.StatusStarting))
comp.telemetry.ReportStatus(component.NewStatusEvent(component.StatusOK))
// simulate an error
comp.telemetry.ReportStatus(component.NewStatusEvent(component.StatusNone))
// stopping
comp.telemetry.ReportStatus(component.NewStatusEvent(component.StatusStopping))
// stopped
comp.telemetry.ReportStatus(component.NewStatusEvent(component.StatusStopped))
// The shared component represents 3 component instances. Reporting status for the shared
// component should report status for each of the instances it represents.
expectedStatuses := []component.Status{
component.StatusStarting,
component.StatusOK,
component.StatusStopping,
component.StatusStopped,
}
require.Equal(t, 3, len(reportedStatuses))
for _, actualStatuses := range reportedStatuses {
require.Equal(t, expectedStatuses, actualStatuses)
}
}
func TestReportStatusOnStartShutdown(t *testing.T) {
for _, tc := range []struct {
name string
startErr error
shutdownErr error
expectedStatuses []component.Status
}{
{
name: "successful start/stop",
startErr: nil,
shutdownErr: nil,
expectedStatuses: []component.Status{
component.StatusStarting,
component.StatusOK,
component.StatusStopping,
component.StatusStopped,
},
},
{
name: "start error",
startErr: assert.AnError,
shutdownErr: nil,
expectedStatuses: []component.Status{
component.StatusStarting,
component.StatusPermanentError,
},
},
{
name: "shutdown error",
shutdownErr: assert.AnError,
expectedStatuses: []component.Status{
component.StatusStarting,
component.StatusOK,
component.StatusStopping,
component.StatusPermanentError,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
reportedStatuses := make(map[*component.InstanceID][]component.Status)
newStatusFunc := func() func(*component.StatusEvent) {
instanceID := &component.InstanceID{}
return func(ev *component.StatusEvent) {
reportedStatuses[instanceID] = append(reportedStatuses[instanceID], ev.Status())
}
}
base := &baseComponent{}
if tc.startErr != nil {
base.StartFunc = func(context.Context, component.Host) error {
return tc.startErr
}
}
if tc.shutdownErr != nil {
base.ShutdownFunc = func(context.Context) error {
return tc.shutdownErr
}
}
comps := NewMap[component.ID, *baseComponent]()
var comp *Component[*baseComponent]
var err error
for i := 0; i < 3; i++ {
telemetrySettings := newNopTelemetrySettings()
telemetrySettings.ReportStatus = newStatusFunc()
if i == 0 {
base.telemetry = telemetrySettings
}
comp, err = comps.LoadOrStore(
id,
func() (*baseComponent, error) { return base, nil },
telemetrySettings,
)
require.NoError(t, err)
}
err = comp.Start(context.Background(), componenttest.NewNopHost())
require.Equal(t, tc.startErr, err)
if tc.startErr == nil {
comp.telemetry.ReportStatus(component.NewStatusEvent(component.StatusOK))
err = comp.Shutdown(context.Background())
require.Equal(t, tc.shutdownErr, err)
}
require.Equal(t, 3, len(reportedStatuses))
for _, actualStatuses := range reportedStatuses {
require.Equal(t, tc.expectedStatuses, actualStatuses)
}
})
}
}
// newNopTelemetrySettings streamlines getting a pointer to a NopTelemetrySettings
func newNopTelemetrySettings() *component.TelemetrySettings {
set := componenttest.NewNopTelemetrySettings()
return &set
}