Skip to content

Commit 9933534

Browse files
authored
acquisition tests: do not rely on hardcoded timeout to finish tests (#4610)
1 parent d4fa928 commit 9933534

3 files changed

Lines changed: 86 additions & 35 deletions

File tree

pkg/acquisition/modules/journalctl/journalctl_test.go

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ import (
2121
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
2222
)
2323

24+
// readTimeout is only ever reached when a line never arrives: it's generous on
25+
// purpose, so that a slow machine doesn't turn into a test failure.
26+
const readTimeout = 10 * time.Second
27+
28+
// quietPeriod is how long we wait to confirm that no extra line shows up. Kept
29+
// short on purpose: a line slower than this is missed, which is better than
30+
// failing at random on a loaded machine.
31+
const quietPeriod = 100 * time.Millisecond
32+
2433
func TestConfigureDSN(t *testing.T) {
2534
cstest.SkipOnWindows(t)
2635

@@ -147,33 +156,53 @@ journalctl_filter:
147156
}
148157
for idx, ts := range tests {
149158
t.Run(strconv.Itoa(idx), func(t *testing.T) {
150-
ctx, cancel := context.WithCancel(ctx)
159+
// not shadowing ctx: the zombie check at the end needs a context that
160+
// is still alive after the stream has been stopped
161+
streamCtx, cancel := context.WithCancel(ctx)
151162
out := make(chan pipeline.Event)
152163
j := Source{}
153164

154165
logger, _ := logtest.NewNullLogger()
155166

156-
err := j.Configure(ctx, []byte(ts.config), logrus.NewEntry(logger), metrics.AcquisitionMetricsLevelNone)
167+
err := j.Configure(streamCtx, []byte(ts.config), logrus.NewEntry(logger), metrics.AcquisitionMetricsLevelNone)
157168
require.NoError(t, err)
158169

159-
gotLines := 0
160-
var wg sync.WaitGroup
170+
var (
171+
wg sync.WaitGroup
172+
extraLines int
173+
)
161174

162175
if ts.wantLines != 0 {
163176
wg.Go(func() {
177+
// whatever happens below, the stream has to be stopped or
178+
// j.Stream() would never return
179+
defer cancel()
180+
181+
// consuming every line lets journalctl exit on its own
182+
for i := range ts.wantLines {
183+
select {
184+
case <-out:
185+
case <-time.After(readTimeout):
186+
t.Errorf("timed out waiting for line %d/%d", i+1, ts.wantLines)
187+
return
188+
}
189+
}
190+
191+
// keep draining for a moment: it catches extra lines, and it keeps a
192+
// source that sends too many from deadlocking on the unbuffered
193+
// channel instead of failing the test
164194
for {
165195
select {
166196
case <-out:
167-
gotLines++
168-
case <-time.After(1 * time.Second):
169-
cancel()
197+
extraLines++
198+
case <-time.After(quietPeriod):
170199
return
171200
}
172201
}
173202
})
174203
}
175204

176-
err = j.Stream(ctx, out)
205+
err = j.Stream(streamCtx, out)
177206
cstest.RequireErrorContains(t, err, ts.wantErr)
178207

179208
if ts.wantErr != "" {
@@ -183,11 +212,13 @@ journalctl_filter:
183212

184213
if ts.wantLines != 0 {
185214
wg.Wait()
186-
assert.Equal(t, ts.wantLines, gotLines)
215+
assert.Zero(t, extraLines, "source emitted more lines than expected")
187216
}
188217

189218
cancel()
190219

220+
// streamCtx is canceled by now, and running pgrep with it would make
221+
// it return nothing and the check below always pass
191222
output, _ := exec.CommandContext(ctx, "pgrep", "-x", "journalctl").CombinedOutput()
192223
assert.Empty(t, output, "zombie journalctl process detected!")
193224
})

pkg/acquisition/modules/kafka/kafka_test.go

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,39 @@ import (
1818
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
1919
)
2020

21+
// readTimeout is only ever reached when a message never arrives: it's generous on
22+
// purpose, so that a slow broker doesn't turn into a test failure.
23+
const readTimeout = 10 * time.Second
24+
25+
// quietPeriod is how long we wait to confirm that no extra message shows up. Kept
26+
// short on purpose: an unexpected message slower than this is missed, which is
27+
// better than failing at random on a loaded machine.
28+
const quietPeriod = 100 * time.Millisecond
29+
30+
// readEvents reads exactly n events, failing if they don't all show up in time.
31+
func readEvents(t *testing.T, out chan pipeline.Event, n int) {
32+
t.Helper()
33+
34+
for i := range n {
35+
select {
36+
case <-out:
37+
case <-time.After(readTimeout):
38+
t.Fatalf("timed out waiting for message %d/%d", i+1, n)
39+
}
40+
}
41+
}
42+
43+
// requireNoMoreEvents checks that the datasource doesn't emit more than expected.
44+
func requireNoMoreEvents(t *testing.T, out chan pipeline.Event) {
45+
t.Helper()
46+
47+
select {
48+
case evt := <-out:
49+
t.Fatalf("unexpected extra message: %q", evt.Line.Raw)
50+
case <-time.After(quietPeriod):
51+
}
52+
}
53+
2154
func writeToKafka(ctx context.Context, w *kafka.Writer, logs []string) {
2255
for idx, log := range logs {
2356
err := w.WriteMessages(ctx, kafka.Message{
@@ -118,20 +151,11 @@ topic: crowdsecplaintext`), subLogger, metrics.AcquisitionMetricsLevelNone)
118151
err = k.StreamingAcquisition(ctx, out, &tomb)
119152
cstest.AssertErrorContains(t, err, ts.expectedErr)
120153

121-
actualLines := 0
122-
123154
go writeToKafka(ctx, w, ts.logs)
124-
READLOOP:
125-
for {
126-
select {
127-
case <-out:
128-
actualLines++
129-
case <-time.After(2 * time.Second):
130-
break READLOOP
131-
}
132-
}
133155

134-
require.Equal(t, ts.expectedLines, actualLines)
156+
readEvents(t, out, ts.expectedLines)
157+
requireNoMoreEvents(t, out)
158+
135159
tomb.Kill(nil)
136160
err = tomb.Wait()
137161
require.NoError(t, err)
@@ -196,20 +220,11 @@ tls:
196220
err = k.StreamingAcquisition(ctx, out, &tomb)
197221
cstest.AssertErrorContains(t, err, ts.expectedErr)
198222

199-
actualLines := 0
200-
201223
go writeToKafka(ctx, w2, ts.logs)
202-
READLOOP:
203-
for {
204-
select {
205-
case <-out:
206-
actualLines++
207-
case <-time.After(2 * time.Second):
208-
break READLOOP
209-
}
210-
}
211224

212-
require.Equal(t, ts.expectedLines, actualLines)
225+
readEvents(t, out, ts.expectedLines)
226+
requireNoMoreEvents(t, out)
227+
213228
tomb.Kill(nil)
214229
err = tomb.Wait()
215230
require.NoError(t, err)

pkg/acquisition/modules/kubernetes/run_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ import (
2727
// asserting against the exact backoff duration.
2828
const retryBackoffMidpoint = 500 * time.Millisecond
2929

30+
// waitTimeout is only ever reached when what we're waiting for never happens:
31+
// it's generous on purpose, so that a loaded machine doesn't turn into a test
32+
// failure.
33+
const waitTimeout = 10 * time.Second
34+
3035
func testLogger() *log.Entry {
3136
return log.WithField("type", ModuleName)
3237
}
@@ -183,7 +188,7 @@ func TestTailPod_StartsWorkerAndStopPodCancelsIt(t *testing.T) {
183188
select {
184189
case evt := <-out:
185190
assert.Equal(t, "hello", evt.Line.Raw)
186-
case <-time.After(time.Second):
191+
case <-time.After(waitTimeout):
187192
t.Fatal("timed out waiting for a log line from the tailed pod")
188193
}
189194

@@ -197,7 +202,7 @@ func TestTailPod_StartsWorkerAndStopPodCancelsIt(t *testing.T) {
197202

198203
select {
199204
case <-done:
200-
case <-time.After(time.Second):
205+
case <-time.After(waitTimeout):
201206
t.Fatal("stopPod did not stop the worker goroutine in time")
202207
}
203208

0 commit comments

Comments
 (0)