master
go 382 lines 10.1 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package ping
4
5 import (
6 "context"
7 "errors"
8 "os"
9 "slices"
10 "sync"
11 "testing"
12 "time"
13
14 "github.com/netdata/netdata/go/plugins/logger"
15 "github.com/netdata/netdata/go/plugins/pkg/confopt"
16 "github.com/netdata/netdata/go/plugins/pkg/metrix"
17 "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine"
18 "github.com/netdata/netdata/go/plugins/plugin/framework/charttpl"
19 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
20 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/pinger"
21 "github.com/stretchr/testify/assert"
22 "github.com/stretchr/testify/require"
23 )
24
25 var (
26 dataConfigJSON, _ = os.ReadFile("testdata/config.json")
27 dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
28 )
29
30 func Test_testDataIsValid(t *testing.T) {
31 for name, data := range map[string][]byte{
32 "dataConfigJSON": dataConfigJSON,
33 "dataConfigYAML": dataConfigYAML,
34 } {
35 require.NotNil(t, data, name)
36 }
37 }
38
39 func TestCollector_ConfigurationSerialize(t *testing.T) {
40 collecttest.TestConfigurationSerialize(t, &Collector{}, dataConfigJSON, dataConfigYAML)
41 }
42
43 func TestCollector_Init(t *testing.T) {
44 tests := map[string]struct {
45 wantFail bool
46 config Config
47 }{
48 "fail with default": {
49 wantFail: true,
50 config: New().Config,
51 },
52 "success when 'hosts' set": {
53 wantFail: false,
54 config: validConfig(),
55 },
56 "fail when duplicate hosts are configured": {
57 wantFail: true,
58 config: func() Config {
59 cfg := validConfig()
60 cfg.Hosts = []string{"192.0.2.0", "192.0.2.0"}
61 return cfg
62 }(),
63 },
64 }
65
66 for name, test := range tests {
67 t.Run(name, func(t *testing.T) {
68 collr := New()
69 collr.Config = test.config
70 collr.UpdateEvery = 1
71
72 if test.wantFail {
73 assert.Error(t, collr.Init(context.Background()))
74 } else {
75 assert.NoError(t, collr.Init(context.Background()))
76 }
77 })
78 }
79 }
80
81 func TestCollector_InitPassesSharedPingerConfig(t *testing.T) {
82 var gotCfg pinger.Config
83
84 collr := New()
85 collr.Hosts = []string{"192.0.2.1"}
86 collr.UpdateEvery = 5
87 collr.Network = "ip6"
88 collr.Interface = "eth0"
89 collr.Privileged = false
90 collr.Packets = 7
91 collr.Interval = confopt.Duration(200 * time.Millisecond)
92 collr.JitterEWMASamples = 32
93 collr.JitterSMAWindow = 20
94 collr.newPinger = func(cfg pinger.Config, _ *logger.Logger) (pinger.Client, error) {
95 gotCfg = cfg
96 return &mockClient{}, nil
97 }
98
99 require.NoError(t, collr.Init(context.Background()))
100
101 assert.Equal(t, pinger.Config{
102 Probe: pinger.ProbeConfig{
103 Network: "ip6",
104 Interface: "eth0",
105 Privileged: false,
106 Packets: 7,
107 Interval: confopt.Duration(200 * time.Millisecond),
108 Timeout: 4750 * time.Millisecond,
109 },
110 Analysis: pinger.AnalysisConfig{
111 JitterEWMASamples: 32,
112 JitterSMAWindow: 20,
113 },
114 }, gotCfg)
115 }
116
117 func TestCollector_Cleanup(t *testing.T) {
118 assert.NotPanics(t, func() { New().Cleanup(context.Background()) })
119 }
120
121 func TestCollector_Check(t *testing.T) {
122 tests := map[string]struct {
123 wantFail bool
124 prepare func(t *testing.T) (*Collector, *mockClient)
125 }{
126 "success when ping does not return an error": {
127 wantFail: false,
128 prepare: casePingSuccess,
129 },
130 "fail when ping returns an error": {
131 wantFail: true,
132 prepare: casePingError,
133 },
134 }
135
136 for name, test := range tests {
137 t.Run(name, func(t *testing.T) {
138 collr, _ := test.prepare(t)
139
140 if test.wantFail {
141 assert.Error(t, collr.Check(context.Background()))
142 } else {
143 assert.NoError(t, collr.Check(context.Background()))
144 }
145 })
146 }
147 }
148
149 func TestCollector_CheckUsesReadOnlyProbing(t *testing.T) {
150 collr, client := casePingSuccess(t)
151 type ctxKey struct{}
152 ctx := context.WithValue(context.Background(), ctxKey{}, "check")
153
154 require.NoError(t, collr.Check(ctx))
155
156 calls := client.probeCalls()
157 require.Len(t, calls, len(collr.Hosts))
158 for _, call := range calls {
159 assert.Equal(t, "probe", call.method)
160 assert.Equal(t, "check", call.ctx.Value(ctxKey{}))
161 }
162 }
163
164 func TestCollector_Collect(t *testing.T) {
165 tests := map[string]struct {
166 prepare func(t *testing.T) (*Collector, *mockClient)
167 wantFail bool
168 wantValues bool
169 }{
170 "success when ping does not return an error": {
171 prepare: casePingSuccess,
172 wantFail: false,
173 wantValues: true,
174 },
175 "fail when ping returns an error": {
176 prepare: casePingError,
177 wantFail: false,
178 wantValues: false,
179 },
180 }
181
182 for name, test := range tests {
183 t.Run(name, func(t *testing.T) {
184 collr, _ := test.prepare(t)
185 cc := mustCycleController(t, collr.MetricStore())
186 cc.BeginCycle()
187 err := collr.Collect(context.Background())
188 if test.wantFail {
189 cc.AbortCycle()
190 assert.Error(t, err)
191 return
192 }
193 require.NoError(t, err)
194 cc.CommitCycleSuccess()
195
196 labels := metrix.Labels{"host": "192.0.2.1"}
197 if !test.wantValues {
198 _, ok := collr.MetricStore().Read(metrix.ReadRaw()).Value("min_rtt", labels)
199 assert.False(t, ok)
200 return
201 }
202 assertMetricValue(t, collr.MetricStore().Read(metrix.ReadRaw()), "min_rtt", labels, 10000)
203 assertMetricValue(t, collr.MetricStore().Read(metrix.ReadRaw()), "max_rtt", labels, 20000)
204 assertMetricValue(t, collr.MetricStore().Read(metrix.ReadRaw()), "avg_rtt", labels, 15000)
205 assertMetricValue(t, collr.MetricStore().Read(metrix.ReadRaw()), "std_dev_rtt", labels, 5000)
206 assertMetricValue(t, collr.MetricStore().Read(metrix.ReadRaw()), "rtt_variance", labels, 25000000)
207 assertMetricValue(t, collr.MetricStore().Read(metrix.ReadRaw()), "mean_jitter", labels, 2500)
208 assertMetricValue(t, collr.MetricStore().Read(metrix.ReadRaw()), "ewma_jitter", labels, 156)
209 assertMetricValue(t, collr.MetricStore().Read(metrix.ReadRaw()), "sma_jitter", labels, 2500)
210 assertMetricValue(t, collr.MetricStore().Read(metrix.ReadRaw()), "packets_recv", labels, 5)
211 assertMetricValue(t, collr.MetricStore().Read(metrix.ReadRaw()), "packets_sent", labels, 5)
212 assertMetricValue(t, collr.MetricStore().Read(metrix.ReadRaw()), "packet_loss", labels, 0)
213 })
214 }
215 }
216
217 func TestCollector_CollectUsesTrackingProbing(t *testing.T) {
218 collr, client := casePingSuccess(t)
219 type ctxKey struct{}
220 ctx := context.WithValue(context.Background(), ctxKey{}, "collect")
221 cc := mustCycleController(t, collr.MetricStore())
222 cc.BeginCycle()
223 require.NoError(t, collr.Collect(ctx))
224 cc.CommitCycleSuccess()
225
226 calls := client.probeCalls()
227 require.Len(t, calls, len(collr.Hosts))
228 for _, call := range calls {
229 assert.Equal(t, "probe_and_track", call.method)
230 assert.Equal(t, "collect", call.ctx.Value(ctxKey{}))
231 }
232 }
233
234 func TestCollector_ChartTemplateYAML(t *testing.T) {
235 templateYAML := New().ChartTemplateYAML()
236 collecttest.AssertChartTemplateSchema(t, templateYAML)
237
238 spec, err := charttpl.DecodeYAML([]byte(templateYAML))
239 require.NoError(t, err)
240 require.NoError(t, spec.Validate())
241
242 _, err = chartengine.Compile(spec, 1)
243 require.NoError(t, err)
244 }
245
246 func casePingSuccess(t *testing.T) (*Collector, *mockClient) {
247 t.Helper()
248
249 client := &mockClient{
250 byHost: map[string]probeResult{
251 "192.0.2.1": {sample: sampleForHost("192.0.2.1")},
252 "192.0.2.2": {sample: sampleForHost("192.0.2.2")},
253 "example.com": {sample: sampleForHost("example.com")},
254 },
255 }
256
257 return newCollectorWithMockClient(t, client), client
258 }
259
260 func casePingError(t *testing.T) (*Collector, *mockClient) {
261 t.Helper()
262
263 client := &mockClient{
264 byHost: map[string]probeResult{
265 "192.0.2.1": {err: errors.New("mock probe error")},
266 "192.0.2.2": {err: errors.New("mock probe error")},
267 "example.com": {err: errors.New("mock probe error")},
268 },
269 }
270
271 return newCollectorWithMockClient(t, client), client
272 }
273
274 func newCollectorWithMockClient(t *testing.T, client *mockClient) *Collector {
275 t.Helper()
276
277 collr := New()
278 collr.UpdateEvery = 1
279 collr.Hosts = []string{"192.0.2.1", "192.0.2.2", "example.com"}
280 collr.newPinger = func(_ pinger.Config, _ *logger.Logger) (pinger.Client, error) {
281 return client, nil
282 }
283
284 require.NoError(t, collr.Init(context.Background()))
285 return collr
286 }
287
288 func validConfig() Config {
289 cfg := New().Config
290 cfg.Packets = 1
291 cfg.Hosts = []string{"192.0.2.0"}
292 return cfg
293 }
294
295 func sampleForHost(host string) pinger.Sample {
296 return pinger.Sample{
297 Host: host,
298 PacketsSent: 5,
299 PacketsRecv: 5,
300 PacketLossPct: 0,
301 RTT: pinger.RTTSummary{
302 Valid: true,
303 Min: 10 * time.Millisecond,
304 Max: 20 * time.Millisecond,
305 Avg: 15 * time.Millisecond,
306 StdDev: 5 * time.Millisecond,
307 },
308 Jitter: pinger.JitterSummary{
309 InstantValid: true,
310 Mean: 2500 * time.Microsecond,
311 SmoothedValid: true,
312 EWMA: 156250 * time.Nanosecond,
313 SMA: 2500 * time.Microsecond,
314 },
315 }
316 }
317
318 func mustCycleController(t *testing.T, store metrix.CollectorStore) metrix.CycleController {
319 t.Helper()
320 managed, ok := metrix.AsCycleManagedStore(store)
321 require.True(t, ok, "store does not expose cycle control")
322 return managed.CycleController()
323 }
324
325 func assertMetricValue(t *testing.T, r metrix.Reader, name string, labels metrix.Labels, want float64) {
326 t.Helper()
327 got, ok := r.Value(name, labels)
328 require.Truef(t, ok, "expected metric %s labels=%v", name, labels)
329 assert.InDeltaf(t, want, got, 1e-9, "unexpected metric value for %s labels=%v", name, labels)
330 }
331
332 type probeCall struct {
333 host string
334 method string
335 ctx context.Context
336 }
337
338 type probeResult struct {
339 sample pinger.Sample
340 err error
341 }
342
343 type mockClient struct {
344 mu sync.Mutex
345 byHost map[string]probeResult
346 calls []probeCall
347 }
348
349 func (m *mockClient) Probe(ctx context.Context, host string) (pinger.Sample, error) {
350 return m.recordedProbe(ctx, host, "probe")
351 }
352
353 func (m *mockClient) ProbeAndTrack(ctx context.Context, host string) (pinger.Sample, error) {
354 return m.recordedProbe(ctx, host, "probe_and_track")
355 }
356
357 func (m *mockClient) recordedProbe(ctx context.Context, host, method string) (pinger.Sample, error) {
358 m.mu.Lock()
359 defer m.mu.Unlock()
360
361 m.calls = append(m.calls, probeCall{host: host, method: method, ctx: ctx})
362
363 res, ok := m.byHost[host]
364 if !ok {
365 return pinger.Sample{}, errors.New("unexpected host")
366 }
367 if res.err != nil {
368 return pinger.Sample{}, res.err
369 }
370
371 sample := res.sample
372 if sample.Host == "" {
373 sample.Host = host
374 }
375 return sample, nil
376 }
377
378 func (m *mockClient) probeCalls() []probeCall {
379 m.mu.Lock()
380 defer m.mu.Unlock()
381 return slices.Clone(m.calls)
382 }