master
go 280 lines 6.53 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package pinger
4
5 import (
6 "context"
7 "sync"
8 "syscall"
9 "testing"
10 "time"
11
12 "github.com/netdata/netdata/go/plugins/logger"
13 "github.com/netdata/netdata/go/plugins/pkg/confopt"
14 probing "github.com/prometheus-community/pro-bing"
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/require"
17 )
18
19 type fakeResult struct {
20 stats *probing.Statistics
21 err error
22 sleep time.Duration
23 }
24
25 type fakeRunner struct {
26 mu sync.Mutex
27 byHost map[string][]fakeResult
28 called []string
29 lastCfg ProbeConfig
30 lastCtx context.Context
31 }
32
33 func (r *fakeRunner) probe(ctx context.Context, host string, cfg ProbeConfig) (*probing.Statistics, error) {
34 r.mu.Lock()
35 r.called = append(r.called, host)
36 r.lastCfg = cfg
37 r.lastCtx = ctx
38
39 queue := r.byHost[host]
40 if len(queue) == 0 {
41 r.mu.Unlock()
42 return nil, syscall.ENOENT
43 }
44
45 res := queue[0]
46 r.byHost[host] = queue[1:]
47 r.mu.Unlock()
48
49 if res.sleep > 0 {
50 timer := time.NewTimer(res.sleep)
51 defer timer.Stop()
52
53 select {
54 case <-ctx.Done():
55 return nil, ctx.Err()
56 case <-timer.C:
57 }
58 }
59
60 select {
61 case <-ctx.Done():
62 return nil, ctx.Err()
63 default:
64 }
65
66 return res.stats, res.err
67 }
68
69 func TestClient_ProbeDoesNotMutateState(t *testing.T) {
70 runner := &fakeRunner{
71 byHost: map[string][]fakeResult{
72 "host": {{stats: testStats("host")}},
73 },
74 }
75
76 c, err := newClient(testConfig(), logger.NewWithWriter(nil), runner)
77 require.NoError(t, err)
78
79 sample, err := c.Probe(context.Background(), "host")
80 require.NoError(t, err)
81
82 assert.True(t, sample.Jitter.InstantValid)
83 assert.False(t, sample.Jitter.SmoothedValid)
84 assert.Empty(t, c.state.byHost)
85 }
86
87 func TestClient_ProbeAndTrackMutatesState(t *testing.T) {
88 runner := &fakeRunner{
89 byHost: map[string][]fakeResult{
90 "host": {
91 {stats: testStats("host")},
92 {stats: testStats("host")},
93 },
94 },
95 }
96
97 c, err := newClient(testConfig(), logger.NewWithWriter(nil), runner)
98 require.NoError(t, err)
99
100 sample, err := c.ProbeAndTrack(context.Background(), "host")
101 require.NoError(t, err)
102 assert.True(t, sample.Jitter.SmoothedValid)
103 assert.Equal(t, time.Duration(156250), sample.Jitter.EWMA)
104 assert.Equal(t, 2500*time.Microsecond, sample.Jitter.SMA)
105
106 sample, err = c.ProbeAndTrack(context.Background(), "host")
107 require.NoError(t, err)
108 assert.Equal(t, time.Duration(302734), sample.Jitter.EWMA)
109 assert.Equal(t, 2500*time.Microsecond, sample.Jitter.SMA)
110 }
111
112 func TestClient_ProbeNoReplyReturnsCountsAndLoss(t *testing.T) {
113 runner := &fakeRunner{
114 byHost: map[string][]fakeResult{
115 "host": {{
116 stats: &probing.Statistics{
117 PacketsSent: 5,
118 PacketsRecv: 0,
119 PacketLoss: 100,
120 },
121 }},
122 },
123 }
124
125 c, err := newClient(testConfig(), logger.NewWithWriter(nil), runner)
126 require.NoError(t, err)
127
128 sample, err := c.ProbeAndTrack(context.Background(), "host")
129 require.NoError(t, err)
130
131 assert.Equal(t, int64(5), sample.PacketsSent)
132 assert.Equal(t, int64(0), sample.PacketsRecv)
133 assert.Equal(t, 100.0, sample.PacketLossPct)
134 assert.False(t, sample.RTT.Valid)
135 assert.False(t, sample.Jitter.InstantValid)
136 assert.False(t, sample.Jitter.SmoothedValid)
137 assert.Empty(t, c.state.byHost)
138 }
139
140 func TestClient_ProbeWithSingleRTTDoesNotUpdateJitterState(t *testing.T) {
141 runner := &fakeRunner{
142 byHost: map[string][]fakeResult{
143 "host": {{
144 stats: &probing.Statistics{
145 PacketsSent: 1,
146 PacketsRecv: 1,
147 PacketLoss: 0,
148 Rtts: []time.Duration{10 * time.Millisecond},
149 MinRtt: 10 * time.Millisecond,
150 MaxRtt: 10 * time.Millisecond,
151 AvgRtt: 10 * time.Millisecond,
152 },
153 }},
154 },
155 }
156
157 c, err := newClient(testConfig(), logger.NewWithWriter(nil), runner)
158 require.NoError(t, err)
159
160 sample, err := c.ProbeAndTrack(context.Background(), "host")
161 require.NoError(t, err)
162
163 assert.True(t, sample.RTT.Valid)
164 assert.False(t, sample.Jitter.InstantValid)
165 assert.False(t, sample.Jitter.SmoothedValid)
166 assert.Empty(t, c.state.byHost)
167 }
168
169 func TestClient_ProbeConcurrentDifferentHosts(t *testing.T) {
170 runner := &fakeRunner{
171 byHost: map[string][]fakeResult{
172 "host1": {{stats: testStats("host1"), sleep: 10 * time.Millisecond}},
173 "host2": {{stats: testStats("host2"), sleep: 10 * time.Millisecond}},
174 },
175 }
176
177 c, err := newClient(testConfig(), logger.NewWithWriter(nil), runner)
178 require.NoError(t, err)
179
180 var wg sync.WaitGroup
181 errCh := make(chan error, 2)
182
183 for _, host := range []string{"host1", "host2"} {
184 wg.Go(func() {
185 _, err := c.ProbeAndTrack(context.Background(), host)
186 errCh <- err
187 })
188 }
189
190 wg.Wait()
191 close(errCh)
192
193 for err := range errCh {
194 require.NoError(t, err)
195 }
196 }
197
198 func TestClient_ProbeUsesFakeRunnerSeam(t *testing.T) {
199 runner := &fakeRunner{
200 byHost: map[string][]fakeResult{
201 "host": {{stats: testStats("host")}},
202 },
203 }
204
205 c, err := newClient(testConfig(), logger.NewWithWriter(nil), runner)
206 require.NoError(t, err)
207
208 _, err = c.Probe(context.Background(), "host")
209 require.NoError(t, err)
210 assert.Equal(t, []string{"host"}, runner.called)
211 assert.Equal(t, testConfig().Probe.Timeout, runner.lastCfg.Timeout)
212 }
213
214 func TestClient_ProbePassesContextToRunner(t *testing.T) {
215 type ctxKey struct{}
216
217 runner := &fakeRunner{
218 byHost: map[string][]fakeResult{
219 "host": {{stats: testStats("host")}},
220 },
221 }
222
223 c, err := newClient(testConfig(), logger.NewWithWriter(nil), runner)
224 require.NoError(t, err)
225
226 ctx := context.WithValue(context.Background(), ctxKey{}, "probe")
227
228 _, err = c.Probe(ctx, "host")
229 require.NoError(t, err)
230 require.NotNil(t, runner.lastCtx)
231 assert.Equal(t, "probe", runner.lastCtx.Value(ctxKey{}))
232 }
233
234 func TestClient_ProbeContextCancellation(t *testing.T) {
235 runner := &fakeRunner{
236 byHost: map[string][]fakeResult{
237 "host": {{stats: testStats("host"), sleep: 100 * time.Millisecond}},
238 },
239 }
240
241 c, err := newClient(testConfig(), logger.NewWithWriter(nil), runner)
242 require.NoError(t, err)
243
244 ctx, cancel := context.WithCancel(context.Background())
245 cancel()
246
247 _, err = c.Probe(ctx, "host")
248 require.Error(t, err)
249 assert.ErrorIs(t, err, context.Canceled)
250 }
251
252 func testConfig() Config {
253 return Config{
254 Probe: ProbeConfig{
255 Packets: 5,
256 Interval: confopt.Duration(100 * time.Millisecond),
257 Timeout: time.Second,
258 },
259 }
260 }
261
262 func testStats(host string) *probing.Statistics {
263 return &probing.Statistics{
264 Addr: host,
265 PacketsRecv: 5,
266 PacketsSent: 5,
267 PacketLoss: 0,
268 Rtts: []time.Duration{
269 10 * time.Millisecond,
270 12 * time.Millisecond,
271 15 * time.Millisecond,
272 18 * time.Millisecond,
273 20 * time.Millisecond,
274 },
275 MinRtt: 10 * time.Millisecond,
276 MaxRtt: 20 * time.Millisecond,
277 AvgRtt: 15 * time.Millisecond,
278 StdDevRtt: 5 * time.Millisecond,
279 }
280 }