Skip to content

Commit 70e0f17

Browse files
committed
Add errgroup tests
By geClaude Signed-off-by: Nelo-T. Wallus <red.brush9525@fastmail.com> Signed-off-by: Nelo-T. Wallus <n.wallus@sap.com>
1 parent 9aedc92 commit 70e0f17

1 file changed

Lines changed: 392 additions & 0 deletions

File tree

pkg/errgroup/errgroup_test.go

Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
/*
2+
Copyright 2025 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package errgroup
18+
19+
import (
20+
"context"
21+
"errors"
22+
"sync/atomic"
23+
"testing"
24+
"time"
25+
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
)
29+
30+
func TestGroup_NoContext_NoRunners(t *testing.T) {
31+
var g Group
32+
err := g.Wait()
33+
require.NoError(t, err)
34+
}
35+
36+
func TestGroup_WithContext_NoRunners(t *testing.T) {
37+
g := WithContext(context.Background())
38+
require.NotNil(t, g)
39+
err := g.Wait()
40+
require.NoError(t, err)
41+
}
42+
43+
func TestGroup_NoContext_NilRunner(t *testing.T) {
44+
var g Group
45+
g.Go(nil)
46+
err := g.Wait()
47+
require.NoError(t, err)
48+
}
49+
50+
func TestGroup_WithContext_NilRunner(t *testing.T) {
51+
g := WithContext(context.Background())
52+
g.Go(nil)
53+
err := g.Wait()
54+
require.NoError(t, err)
55+
}
56+
57+
func TestGroup_NoContext_SuccessfulRunners(t *testing.T) {
58+
var g Group
59+
var count atomic.Int32
60+
61+
for range 5 {
62+
g.Go(func(_ context.Context) error {
63+
count.Add(1)
64+
return nil
65+
})
66+
}
67+
68+
err := g.Wait()
69+
require.NoError(t, err)
70+
assert.Equal(t, int32(5), count.Load())
71+
}
72+
73+
func TestGroup_WithContext_SuccessfulRunners(t *testing.T) {
74+
g := WithContext(context.Background())
75+
var count atomic.Int32
76+
77+
for range 5 {
78+
g.Go(func(_ context.Context) error {
79+
count.Add(1)
80+
return nil
81+
})
82+
}
83+
84+
err := g.Wait()
85+
require.NoError(t, err)
86+
assert.Equal(t, int32(5), count.Load())
87+
}
88+
89+
func TestGroup_NoContext_SingleError(t *testing.T) {
90+
var g Group
91+
expected := errors.New("test error")
92+
93+
g.Go(func(_ context.Context) error {
94+
return expected
95+
})
96+
97+
err := g.Wait()
98+
require.Error(t, err)
99+
assert.Contains(t, err.Error(), expected.Error())
100+
}
101+
102+
func TestGroup_WithContext_SingleError(t *testing.T) {
103+
g := WithContext(context.Background())
104+
expected := errors.New("test error")
105+
106+
g.Go(func(_ context.Context) error {
107+
return expected
108+
})
109+
110+
err := g.Wait()
111+
require.Error(t, err)
112+
assert.Contains(t, err.Error(), expected.Error())
113+
}
114+
115+
func TestGroup_NoContext_MultipleErrors(t *testing.T) {
116+
var g Group
117+
err1 := errors.New("error one")
118+
err2 := errors.New("error two")
119+
120+
g.Go(func(_ context.Context) error { return err1 })
121+
g.Go(func(_ context.Context) error { return err2 })
122+
123+
err := g.Wait()
124+
require.Error(t, err)
125+
assert.Contains(t, err.Error(), err1.Error())
126+
assert.Contains(t, err.Error(), err2.Error())
127+
}
128+
129+
func TestGroup_WithContext_MultipleErrors(t *testing.T) {
130+
g := WithContext(context.Background())
131+
err1 := errors.New("error one")
132+
err2 := errors.New("error two")
133+
134+
g.Go(func(_ context.Context) error { return err1 })
135+
g.Go(func(_ context.Context) error { return err2 })
136+
137+
err := g.Wait()
138+
require.Error(t, err)
139+
assert.Contains(t, err.Error(), err1.Error())
140+
assert.Contains(t, err.Error(), err2.Error())
141+
}
142+
143+
func TestGroup_NoContext_MixedResults(t *testing.T) {
144+
var g Group
145+
expected := errors.New("some error")
146+
var okCount atomic.Int32
147+
148+
g.Go(func(_ context.Context) error {
149+
okCount.Add(1)
150+
return nil
151+
})
152+
g.Go(func(_ context.Context) error {
153+
return expected
154+
})
155+
g.Go(func(_ context.Context) error {
156+
okCount.Add(1)
157+
return nil
158+
})
159+
160+
err := g.Wait()
161+
require.Error(t, err)
162+
assert.Contains(t, err.Error(), expected.Error())
163+
assert.Equal(t, int32(2), okCount.Load())
164+
}
165+
166+
func TestGroup_WithContext_MixedResults(t *testing.T) {
167+
g := WithContext(context.Background())
168+
expected := errors.New("some error")
169+
var okCount atomic.Int32
170+
171+
g.Go(func(_ context.Context) error {
172+
okCount.Add(1)
173+
return nil
174+
})
175+
g.Go(func(_ context.Context) error {
176+
return expected
177+
})
178+
g.Go(func(_ context.Context) error {
179+
okCount.Add(1)
180+
return nil
181+
})
182+
183+
err := g.Wait()
184+
require.Error(t, err)
185+
assert.Contains(t, err.Error(), expected.Error())
186+
assert.Equal(t, int32(2), okCount.Load())
187+
}
188+
189+
func TestGroup_WithContext_ContextPassedToRunners(t *testing.T) {
190+
type key struct{}
191+
ctx := context.WithValue(context.Background(), key{}, "hello")
192+
g := WithContext(ctx)
193+
194+
var got atomic.Value
195+
g.Go(func(ctx context.Context) error {
196+
got.Store(ctx.Value(key{}))
197+
return nil
198+
})
199+
200+
err := g.Wait()
201+
require.NoError(t, err)
202+
assert.Equal(t, "hello", got.Load())
203+
}
204+
205+
func TestGroup_NoContext_RunnersGetBackgroundDerivedContext(t *testing.T) {
206+
var g Group
207+
208+
var gotCtx atomic.Value
209+
g.Go(func(ctx context.Context) error {
210+
gotCtx.Store(ctx)
211+
return nil
212+
})
213+
214+
err := g.Wait()
215+
require.NoError(t, err)
216+
require.NotNil(t, gotCtx.Load())
217+
}
218+
219+
func TestGroup_WithContext_CancelledParentContext(t *testing.T) {
220+
ctx, cancel := context.WithCancel(context.Background())
221+
cancel() // cancel immediately
222+
223+
g := WithContext(ctx)
224+
225+
var gotErr atomic.Value
226+
g.Go(func(ctx context.Context) error {
227+
<-ctx.Done()
228+
gotErr.Store(ctx.Err())
229+
return ctx.Err()
230+
})
231+
232+
err := g.Wait()
233+
require.Error(t, err)
234+
assert.ErrorIs(t, gotErr.Load().(error), context.Canceled)
235+
}
236+
237+
func TestGroup_NoContext_FailFast(t *testing.T) {
238+
var g Group
239+
g.FailFast = true
240+
241+
started := make(chan struct{})
242+
g.Go(func(ctx context.Context) error {
243+
close(started)
244+
<-ctx.Done()
245+
return ctx.Err()
246+
})
247+
248+
// Ensure the first goroutine is running before triggering the error.
249+
<-started
250+
251+
g.Go(func(_ context.Context) error {
252+
return errors.New("fail")
253+
})
254+
255+
err := g.Wait()
256+
require.Error(t, err)
257+
}
258+
259+
func TestGroup_WithContext_FailFast(t *testing.T) {
260+
g := WithContext(context.Background())
261+
g.FailFast = true
262+
263+
started := make(chan struct{})
264+
g.Go(func(ctx context.Context) error {
265+
close(started)
266+
<-ctx.Done()
267+
return ctx.Err()
268+
})
269+
270+
// Ensure the first goroutine is running before triggering the error.
271+
<-started
272+
273+
g.Go(func(_ context.Context) error {
274+
return errors.New("fail")
275+
})
276+
277+
err := g.Wait()
278+
require.Error(t, err)
279+
}
280+
281+
func TestGroup_WithContext_FailFastCancelsContext(t *testing.T) {
282+
g := WithContext(context.Background())
283+
g.FailFast = true
284+
285+
cancelled := make(chan struct{})
286+
g.Go(func(ctx context.Context) error {
287+
<-ctx.Done()
288+
close(cancelled)
289+
return nil
290+
})
291+
292+
g.Go(func(_ context.Context) error {
293+
return errors.New("trigger fail fast")
294+
})
295+
296+
err := g.Wait()
297+
require.Error(t, err)
298+
299+
// The cancelled channel should be closed because fail-fast
300+
// cancels the context.
301+
select {
302+
case <-cancelled:
303+
// expected
304+
case <-time.After(time.Second):
305+
t.Fatal("expected context to be cancelled by fail fast")
306+
}
307+
}
308+
309+
func TestGroup_NoContext_FailFastFalseDoesNotCancel(t *testing.T) {
310+
var g Group
311+
g.FailFast = false
312+
313+
var completed atomic.Bool
314+
g.Go(func(ctx context.Context) error {
315+
// Small sleep to ensure the error goroutine finishes first.
316+
time.Sleep(50 * time.Millisecond)
317+
completed.Store(true)
318+
return nil
319+
})
320+
321+
g.Go(func(_ context.Context) error {
322+
return errors.New("error")
323+
})
324+
325+
err := g.Wait()
326+
require.Error(t, err)
327+
assert.True(t, completed.Load(), "goroutine should complete even after another errors")
328+
}
329+
330+
func TestGroup_WithContext_FailFastFalseDoesNotCancel(t *testing.T) {
331+
g := WithContext(context.Background())
332+
g.FailFast = false
333+
334+
var completed atomic.Bool
335+
g.Go(func(ctx context.Context) error {
336+
// Small sleep to ensure the error goroutine finishes first.
337+
time.Sleep(50 * time.Millisecond)
338+
completed.Store(true)
339+
return nil
340+
})
341+
342+
g.Go(func(_ context.Context) error {
343+
return errors.New("error")
344+
})
345+
346+
err := g.Wait()
347+
require.Error(t, err)
348+
assert.True(t, completed.Load(), "goroutine should complete even after another errors")
349+
}
350+
351+
func TestGroup_WaitCalledMultipleTimes(t *testing.T) {
352+
var g Group
353+
g.Go(func(_ context.Context) error {
354+
return errors.New("err")
355+
})
356+
357+
err1 := g.Wait()
358+
err2 := g.Wait()
359+
require.Error(t, err1)
360+
assert.Equal(t, err1.Error(), err2.Error(), "wait should return the same error on repeated calls")
361+
}
362+
363+
func TestGroup_NilGroup_Wait(t *testing.T) {
364+
// A zero-value Group should work without explicit initialization.
365+
g := &Group{}
366+
err := g.Wait()
367+
require.NoError(t, err)
368+
}
369+
370+
func TestGroup_NilGroup_GoNil(t *testing.T) {
371+
g := &Group{}
372+
g.Go(nil)
373+
err := g.Wait()
374+
require.NoError(t, err)
375+
}
376+
377+
func TestGroup_WithContext_WaitCancelsContext(t *testing.T) {
378+
ctx := context.Background()
379+
g := WithContext(ctx)
380+
381+
var runnerCtx atomic.Value
382+
g.Go(func(ctx context.Context) error {
383+
runnerCtx.Store(ctx)
384+
return nil
385+
})
386+
387+
err := g.Wait()
388+
require.NoError(t, err)
389+
390+
storedCtx := runnerCtx.Load().(context.Context)
391+
assert.Error(t, storedCtx.Err(), "context should be cancelled after Wait returns")
392+
}

0 commit comments

Comments
 (0)