Skip to content

Commit 4183493

Browse files
committed
[scraperhelper]: add more unit tests
Add tests to the internal/controller package, so we have baseline coverage before making the changes in open-telemetry#15116.
1 parent 28265fc commit 4183493

1 file changed

Lines changed: 354 additions & 0 deletions

File tree

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package controller // import "go.opentelemetry.io/collector/scraper/scraperhelper/internal/controller"
5+
6+
import (
7+
"context"
8+
"errors"
9+
"sync/atomic"
10+
"testing"
11+
"time"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
"go.opentelemetry.io/collector/component"
17+
"go.opentelemetry.io/collector/component/componenttest"
18+
"go.opentelemetry.io/collector/receiver/receivertest"
19+
)
20+
21+
// mockScraper implements component.Component for testing.
22+
type mockScraper struct {
23+
component.StartFunc
24+
component.ShutdownFunc
25+
}
26+
27+
func newTestController(t *testing.T, cfg *ControllerConfig, scrapers []component.Component, tickerCh <-chan time.Time) *Controller[component.Component] {
28+
t.Helper()
29+
ctrl, err := NewController(
30+
cfg,
31+
receivertest.NewNopSettings(receivertest.NopType),
32+
scrapers,
33+
func(*Controller[component.Component]) {},
34+
tickerCh,
35+
)
36+
require.NoError(t, err)
37+
return ctrl
38+
}
39+
40+
func TestNewController(t *testing.T) {
41+
t.Parallel()
42+
43+
scrapeFunc := func(*Controller[component.Component]) {}
44+
45+
for _, tc := range []struct {
46+
name string
47+
cfg *ControllerConfig
48+
scrapers []component.Component
49+
tickerCh <-chan time.Time
50+
}{
51+
{
52+
name: "default config",
53+
cfg: func() *ControllerConfig {
54+
cfg := NewDefaultControllerConfig()
55+
return &cfg
56+
}(),
57+
},
58+
{
59+
name: "custom collection interval and timeout",
60+
cfg: &ControllerConfig{
61+
CollectionInterval: 5 * time.Second,
62+
InitialDelay: 2 * time.Second,
63+
Timeout: 10 * time.Second,
64+
},
65+
},
66+
{
67+
name: "with ticker channel",
68+
cfg: func() *ControllerConfig {
69+
cfg := NewDefaultControllerConfig()
70+
return &cfg
71+
}(),
72+
tickerCh: make(<-chan time.Time),
73+
},
74+
} {
75+
t.Run(tc.name, func(t *testing.T) {
76+
t.Parallel()
77+
78+
scrapers := tc.scrapers
79+
if scrapers == nil {
80+
scrapers = []component.Component{}
81+
}
82+
83+
ctrl, err := NewController(
84+
tc.cfg,
85+
receivertest.NewNopSettings(receivertest.NopType),
86+
scrapers,
87+
scrapeFunc,
88+
tc.tickerCh,
89+
)
90+
91+
require.NoError(t, err)
92+
require.NotNil(t, ctrl)
93+
94+
assert.Equal(t, tc.cfg.CollectionInterval, ctrl.collectionInterval)
95+
assert.Equal(t, tc.cfg.InitialDelay, ctrl.initialDelay)
96+
assert.Equal(t, tc.cfg.Timeout, ctrl.Timeout)
97+
assert.Equal(t, scrapers, ctrl.Scrapers)
98+
assert.NotNil(t, ctrl.Obsrecv)
99+
assert.NotNil(t, ctrl.done)
100+
})
101+
}
102+
}
103+
104+
func TestStartScrapersStarted(t *testing.T) {
105+
t.Parallel()
106+
107+
var started int
108+
startFunc := component.StartFunc(func(context.Context, component.Host) error {
109+
started++
110+
return nil
111+
})
112+
scrapers := []component.Component{
113+
&mockScraper{StartFunc: startFunc},
114+
&mockScraper{StartFunc: startFunc},
115+
}
116+
117+
tickerCh := make(chan time.Time)
118+
cfg := &ControllerConfig{CollectionInterval: time.Minute}
119+
ctrl := newTestController(t, cfg, scrapers, tickerCh)
120+
121+
require.NoError(t, ctrl.Start(context.Background(), componenttest.NewNopHost()))
122+
assert.Equal(t, 2, started)
123+
require.NoError(t, ctrl.Shutdown(context.Background()))
124+
}
125+
126+
func TestStartScraperError(t *testing.T) {
127+
t.Parallel()
128+
129+
errScraper := errors.New("scraper start failed")
130+
scrapers := []component.Component{
131+
&mockScraper{StartFunc: component.StartFunc(func(context.Context, component.Host) error {
132+
return errScraper
133+
})},
134+
}
135+
136+
tickerCh := make(chan time.Time)
137+
cfg := &ControllerConfig{CollectionInterval: time.Minute}
138+
ctrl := newTestController(t, cfg, scrapers, tickerCh)
139+
140+
err := ctrl.Start(context.Background(), componenttest.NewNopHost())
141+
require.ErrorIs(t, err, errScraper)
142+
}
143+
144+
func TestStartScrapingWithNilTickerCh(t *testing.T) {
145+
t.Parallel()
146+
147+
var scrapeCount atomic.Int32
148+
scrapeFunc := func(*Controller[component.Component]) {
149+
scrapeCount.Add(1)
150+
}
151+
152+
cfg := &ControllerConfig{
153+
CollectionInterval: 50 * time.Millisecond,
154+
}
155+
ctrl, err := NewController(
156+
cfg,
157+
receivertest.NewNopSettings(receivertest.NopType),
158+
[]component.Component{},
159+
scrapeFunc,
160+
nil, // nil tickerCh — will create a real ticker internally.
161+
)
162+
require.NoError(t, err)
163+
164+
require.NoError(t, ctrl.Start(context.Background(), componenttest.NewNopHost()))
165+
166+
// Should get initial scrape + at least one ticker scrape.
167+
require.Eventually(t, func() bool {
168+
return scrapeCount.Load() >= 2
169+
}, time.Second, 10*time.Millisecond)
170+
171+
require.NoError(t, ctrl.Shutdown(context.Background()))
172+
}
173+
174+
func TestShutdownScrapers(t *testing.T) {
175+
t.Parallel()
176+
177+
var shutdownOrder []int
178+
scrapers := []component.Component{
179+
&mockScraper{ShutdownFunc: component.ShutdownFunc(func(context.Context) error {
180+
shutdownOrder = append(shutdownOrder, 1)
181+
return nil
182+
})},
183+
&mockScraper{ShutdownFunc: component.ShutdownFunc(func(context.Context) error {
184+
shutdownOrder = append(shutdownOrder, 2)
185+
return nil
186+
})},
187+
}
188+
189+
tickerCh := make(chan time.Time)
190+
cfg := &ControllerConfig{CollectionInterval: time.Minute}
191+
ctrl := newTestController(t, cfg, scrapers, tickerCh)
192+
193+
require.NoError(t, ctrl.Start(context.Background(), componenttest.NewNopHost()))
194+
require.NoError(t, ctrl.Shutdown(context.Background()))
195+
196+
assert.Equal(t, []int{1, 2}, shutdownOrder)
197+
}
198+
199+
func TestShutdownScraperErrors(t *testing.T) {
200+
t.Parallel()
201+
202+
errShutdown1 := errors.New("shutdown error 1")
203+
errShutdown2 := errors.New("shutdown error 2")
204+
scrapers := []component.Component{
205+
&mockScraper{ShutdownFunc: component.ShutdownFunc(func(context.Context) error {
206+
return errShutdown1
207+
})},
208+
&mockScraper{ShutdownFunc: component.ShutdownFunc(func(context.Context) error {
209+
return errShutdown2
210+
})},
211+
}
212+
213+
tickerCh := make(chan time.Time)
214+
cfg := &ControllerConfig{CollectionInterval: time.Minute}
215+
ctrl := newTestController(t, cfg, scrapers, tickerCh)
216+
217+
require.NoError(t, ctrl.Start(context.Background(), componenttest.NewNopHost()))
218+
err := ctrl.Shutdown(context.Background())
219+
require.Error(t, err)
220+
require.ErrorIs(t, err, errShutdown1)
221+
require.ErrorIs(t, err, errShutdown2)
222+
}
223+
224+
func TestStartScraping(t *testing.T) {
225+
t.Parallel()
226+
227+
tickerCh := make(chan time.Time)
228+
var scrapeCount atomic.Int32
229+
scrapeFunc := func(*Controller[component.Component]) {
230+
scrapeCount.Add(1)
231+
}
232+
233+
cfg := &ControllerConfig{
234+
CollectionInterval: time.Minute,
235+
}
236+
ctrl, err := NewController(
237+
cfg,
238+
receivertest.NewNopSettings(receivertest.NopType),
239+
[]component.Component{},
240+
scrapeFunc,
241+
tickerCh,
242+
)
243+
require.NoError(t, err)
244+
245+
require.NoError(t, ctrl.Start(context.Background(), componenttest.NewNopHost()))
246+
247+
// startScraping calls scrapeFunc immediately on start.
248+
require.Eventually(t, func() bool {
249+
return scrapeCount.Load() >= 1
250+
}, time.Second, 10*time.Millisecond)
251+
252+
// Simulate a tick — should trigger another scrape.
253+
tickerCh <- time.Now()
254+
require.Eventually(t, func() bool {
255+
return scrapeCount.Load() >= 2
256+
}, time.Second, 10*time.Millisecond)
257+
258+
require.NoError(t, ctrl.Shutdown(context.Background()))
259+
}
260+
261+
func TestStartScrapingWithInitialDelay(t *testing.T) {
262+
t.Parallel()
263+
264+
tickerCh := make(chan time.Time)
265+
var scrapeCount atomic.Int32
266+
scrapeFunc := func(*Controller[component.Component]) {
267+
scrapeCount.Add(1)
268+
}
269+
270+
cfg := &ControllerConfig{
271+
CollectionInterval: time.Minute,
272+
InitialDelay: 50 * time.Millisecond,
273+
}
274+
ctrl, err := NewController(
275+
cfg,
276+
receivertest.NewNopSettings(receivertest.NopType),
277+
[]component.Component{},
278+
scrapeFunc,
279+
tickerCh,
280+
)
281+
require.NoError(t, err)
282+
283+
require.NoError(t, ctrl.Start(context.Background(), componenttest.NewNopHost()))
284+
285+
// The initial scrape should happen after the initial delay.
286+
require.Eventually(t, func() bool {
287+
return scrapeCount.Load() >= 1
288+
}, time.Second, 10*time.Millisecond)
289+
290+
require.NoError(t, ctrl.Shutdown(context.Background()))
291+
}
292+
293+
func TestStartScrapingShutdownDuringInitialDelay(t *testing.T) {
294+
t.Parallel()
295+
296+
var scraped atomic.Bool
297+
scrapeFunc := func(*Controller[component.Component]) {
298+
scraped.Store(true)
299+
}
300+
301+
cfg := &ControllerConfig{
302+
CollectionInterval: time.Minute,
303+
InitialDelay: time.Hour, // Very long delay — we'll shut down before it expires.
304+
}
305+
ctrl, err := NewController(
306+
cfg,
307+
receivertest.NewNopSettings(receivertest.NopType),
308+
[]component.Component{},
309+
scrapeFunc,
310+
nil,
311+
)
312+
require.NoError(t, err)
313+
314+
require.NoError(t, ctrl.Start(context.Background(), componenttest.NewNopHost()))
315+
// Shutdown immediately, which should cancel the initial delay wait.
316+
require.NoError(t, ctrl.Shutdown(context.Background()))
317+
318+
assert.False(t, scraped.Load(), "scrapeFunc should not have been called")
319+
}
320+
321+
func TestGetSettings(t *testing.T) {
322+
t.Parallel()
323+
324+
sType := component.MustNewType("test_scraper")
325+
rSet := receivertest.NewNopSettings(receivertest.NopType)
326+
327+
sSet := GetSettings(sType, rSet)
328+
329+
assert.Equal(t, component.NewID(sType), sSet.ID)
330+
assert.Equal(t, rSet.BuildInfo, sSet.BuildInfo)
331+
}
332+
333+
func TestWithScrapeContext(t *testing.T) {
334+
t.Parallel()
335+
336+
t.Run("zero timeout returns context without deadline", func(t *testing.T) {
337+
t.Parallel()
338+
ctx, cancel := WithScrapeContext(0)
339+
defer cancel()
340+
_, hasDeadline := ctx.Deadline()
341+
assert.False(t, hasDeadline)
342+
})
343+
344+
t.Run("positive timeout returns context with deadline", func(t *testing.T) {
345+
t.Parallel()
346+
timeout := 5 * time.Second
347+
ctx, cancel := WithScrapeContext(timeout)
348+
defer cancel()
349+
deadline, hasDeadline := ctx.Deadline()
350+
assert.True(t, hasDeadline)
351+
// The deadline should be approximately now + timeout.
352+
assert.WithinDuration(t, time.Now().Add(timeout), deadline, time.Second)
353+
})
354+
}

0 commit comments

Comments
 (0)