Skip to content

Commit 134f967

Browse files
committed
add more tests
1 parent f58ac88 commit 134f967

File tree

2 files changed

+219
-43
lines changed

2 files changed

+219
-43
lines changed

errsizedgroup_test.go

Lines changed: 145 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package syncs
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"log"
@@ -123,34 +124,35 @@ func TestErrorSizedGroup_Term(t *testing.T) {
123124
assert.True(t, c < uint32(1000), fmt.Sprintf("%d, some of routines has to be terminated early", c))
124125
}
125126

126-
func TestErrorSizedGroup_WaitWithoutGo(t *testing.T) {
127-
ewg := NewErrSizedGroup(10)
128-
assert.NoError(t, ewg.Wait())
129-
}
130-
131-
// illustrates the use of a SizedGroup for concurrent, limited execution of goroutines.
132-
func ExampleErrSizedGroup_go() {
127+
func TestErrorSizedGroup_TermOnErr(t *testing.T) {
128+
ewg := NewErrSizedGroup(10, TermOnErr)
129+
var c uint32
133130

134-
// create sized waiting group allowing maximum 10 goroutines
135-
grp := NewErrSizedGroup(10)
131+
const N = 1000
132+
const errIndex = 100 // index of a function that will return an error
136133

137-
var c uint32
138-
for i := 0; i < 1000; i++ {
139-
// Go call is non-blocking, like regular go statement
140-
grp.Go(func() error {
141-
// do some work in 10 goroutines in parallel
142-
atomic.AddUint32(&c, 1)
143-
time.Sleep(10 * time.Millisecond)
134+
for i := 0; i < N; i++ {
135+
i := i
136+
ewg.Go(func() error {
137+
val := atomic.AddUint32(&c, 1)
138+
if i == errIndex || val > uint32(errIndex+1) {
139+
return fmt.Errorf("err from function %d", i)
140+
}
144141
return nil
145142
})
146143
}
147-
// Note: grp.Go acts like go command - never blocks. This code will be executed right away
148-
log.Print("all 1000 jobs submitted")
149144

150-
// wait for completion
151-
if err := grp.Wait(); err != nil {
152-
panic(err)
153-
}
145+
err := ewg.Wait()
146+
147+
require.NotNil(t, err)
148+
require.Contains(t, err.Error(), "err from function 100")
149+
// we don't know how many routines will be executed before the error, but it should be less than 10
150+
require.LessOrEqual(t, c, uint32(errIndex+100), fmt.Sprintf("%d, routines have to be terminated early", c))
151+
}
152+
153+
func TestErrorSizedGroup_WaitWithoutGo(t *testing.T) {
154+
ewg := NewErrSizedGroup(10)
155+
assert.NoError(t, ewg.Wait())
154156
}
155157

156158
func TestErrorSizedGroup_TermAndPreemptive(t *testing.T) {
@@ -181,7 +183,127 @@ func TestErrorSizedGroup_TermAndPreemptive(t *testing.T) {
181183

182184
select {
183185
case <-time.After(5 * time.Second):
184-
t.Fatal("timeout deadlock may happy")
186+
t.Fatal("timeout deadlock may happen")
185187
case <-done:
186188
}
187189
}
190+
191+
func TestErrorSizedGroup_ConcurrencyLimit(t *testing.T) {
192+
concurrentGoroutines := int32(0)
193+
maxConcurrentGoroutines := int32(0)
194+
ewg := NewErrSizedGroup(5) // Limit of concurrent goroutines set to 5
195+
196+
for i := 0; i < 100; i++ {
197+
ewg.Go(func() error {
198+
atomic.AddInt32(&concurrentGoroutines, 1)
199+
defer atomic.AddInt32(&concurrentGoroutines, -1)
200+
201+
if v := atomic.LoadInt32(&concurrentGoroutines); v > atomic.LoadInt32(&maxConcurrentGoroutines) {
202+
atomic.StoreInt32(&maxConcurrentGoroutines, v)
203+
}
204+
205+
time.Sleep(time.Millisecond * 50)
206+
return nil
207+
})
208+
}
209+
210+
err := ewg.Wait()
211+
assert.Nil(t, err)
212+
assert.Equal(t, int32(5), maxConcurrentGoroutines)
213+
}
214+
215+
func TestErrorSizedGroup_MultiError(t *testing.T) {
216+
ewg := NewErrSizedGroup(10)
217+
218+
for i := 0; i < 10; i++ {
219+
i := i
220+
ewg.Go(func() error {
221+
return fmt.Errorf("error from goroutine %d", i)
222+
})
223+
}
224+
225+
err := ewg.Wait()
226+
assert.NotNil(t, err)
227+
228+
for i := 0; i < 10; i++ {
229+
assert.Contains(t, err.Error(), fmt.Sprintf("error from goroutine %d", i))
230+
}
231+
}
232+
233+
func TestErrorSizedGroup_Cancel(t *testing.T) {
234+
ctx, cancel := context.WithCancel(context.Background())
235+
ewg := NewErrSizedGroup(10, Context(ctx))
236+
237+
var c uint32
238+
const N = 1000
239+
240+
for i := 0; i < N; i++ {
241+
i := i
242+
time.Sleep(1 * time.Millisecond) // prevent all the goroutines to be started at once
243+
ewg.Go(func() error {
244+
atomic.AddUint32(&c, 1)
245+
if i == 100 {
246+
cancel()
247+
}
248+
time.Sleep(1 * time.Millisecond) // simulate some work
249+
return nil
250+
})
251+
}
252+
253+
err := ewg.Wait()
254+
require.EqualError(t, err, "1 error(s) occurred: [0] {context canceled}")
255+
assert.ErrorIs(t, ctx.Err(), context.Canceled, ctx.Err())
256+
t.Logf("completed: %d", c)
257+
require.LessOrEqual(t, c, uint32(110), "some of goroutines has to be terminated early")
258+
}
259+
260+
func TestErrorSizedGroup_CancelWithPreemptive(t *testing.T) {
261+
ctx, cancel := context.WithCancel(context.Background())
262+
ewg := NewErrSizedGroup(10, Context(ctx), Preemptive)
263+
264+
var c uint32
265+
const N = 1000
266+
267+
for i := 0; i < N; i++ {
268+
i := i
269+
ewg.Go(func() error {
270+
atomic.AddUint32(&c, 1)
271+
if i == 100 {
272+
cancel()
273+
}
274+
time.Sleep(1 * time.Millisecond) // simulate some work
275+
return nil
276+
})
277+
}
278+
279+
err := ewg.Wait()
280+
require.EqualError(t, err, "1 error(s) occurred: [0] {context canceled}")
281+
assert.ErrorIs(t, ctx.Err(), context.Canceled, ctx.Err())
282+
t.Logf("completed: %d", c)
283+
require.LessOrEqual(t, c, uint32(110), "some of goroutines has to be terminated early")
284+
}
285+
286+
// illustrates the use of a SizedGroup for concurrent, limited execution of goroutines.
287+
func ExampleErrSizedGroup_go() {
288+
289+
// create sized waiting group allowing maximum 10 goroutines
290+
grp := NewErrSizedGroup(10)
291+
292+
var c uint32
293+
for i := 0; i < 1000; i++ {
294+
// Go call is non-blocking, like regular go statement
295+
grp.Go(func() error {
296+
// do some work in 10 goroutines in parallel
297+
atomic.AddUint32(&c, 1)
298+
time.Sleep(10 * time.Millisecond)
299+
return nil
300+
})
301+
}
302+
// Note: grp.Go acts like go command - never blocks. This code will be executed right away
303+
log.Print("all 1000 jobs submitted")
304+
305+
// wait for completion
306+
if err := grp.Wait(); err != nil {
307+
panic(err)
308+
}
309+
}

semaphore_test.go

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,78 @@ import (
1010
)
1111

1212
func TestSemaphore(t *testing.T) {
13-
var locks int32
14-
var sema sync.Locker
15-
go func() {
16-
sema = NewSemaphore(3)
17-
sema.Lock()
18-
atomic.AddInt32(&locks, 1)
19-
sema.Lock()
20-
atomic.AddInt32(&locks, 1)
21-
sema.Lock()
22-
atomic.AddInt32(&locks, 1)
23-
sema.Lock()
24-
atomic.AddInt32(&locks, 1)
25-
}()
26-
27-
time.Sleep(100 * time.Millisecond)
28-
assert.Equal(t, int32(3), atomic.LoadInt32(&locks), "3 locks ok, hangs on 4th")
29-
30-
sema.Unlock()
31-
time.Sleep(100 * time.Millisecond)
32-
assert.Equal(t, int32(4), atomic.LoadInt32(&locks), "4 locks should happen")
13+
tbl := []struct {
14+
name string
15+
capacity int
16+
lockTimes int
17+
expectedErr bool
18+
}{
19+
{"ZeroCapacity", 0, 0, false},
20+
{"CapacityOne", 1, 1, false},
21+
{"CapacityTwo", 2, 2, false},
22+
{"ExceedCapacity", 2, 3, true},
23+
}
24+
25+
for _, tt := range tbl {
26+
t.Run(tt.name, func(t *testing.T) {
27+
var locks int32
28+
sema := NewSemaphore(tt.capacity)
29+
30+
wg := sync.WaitGroup{}
31+
wg.Add(tt.lockTimes)
32+
for i := 0; i < tt.lockTimes; i++ {
33+
go func() {
34+
sema.Lock()
35+
atomic.AddInt32(&locks, 1)
36+
wg.Done()
37+
}()
38+
}
39+
40+
time.Sleep(10 * time.Millisecond) // wait a little for locks to acquire
41+
42+
// if number of locks are less than capacity, all should be acquired
43+
if tt.lockTimes <= tt.capacity {
44+
assert.Equal(t, int32(tt.lockTimes), atomic.LoadInt32(&locks))
45+
wg.Wait()
46+
return
47+
}
48+
// if number of locks exceed capacity, it should hang after reaching the capacity
49+
assert.Equal(t, int32(tt.capacity), atomic.LoadInt32(&locks))
50+
sema.Unlock()
51+
time.Sleep(10 * time.Millisecond)
52+
// after unlock, it should be able to acquire another lock
53+
assert.Equal(t, int32(tt.capacity+1), atomic.LoadInt32(&locks))
54+
wg.Wait()
55+
})
56+
}
57+
}
58+
59+
func TestSemaphore_TryLock(t *testing.T) {
60+
tbl := []struct {
61+
name string
62+
capacity int
63+
lockTimes int
64+
expectedLocks int
65+
}{
66+
{"ZeroCapacity", 0, 1, 1},
67+
{"CapacityOne", 1, 1, 1},
68+
{"CapacityTwo", 2, 2, 2},
69+
{"ExceedCapacity", 2, 3, 2},
70+
}
71+
72+
for _, tt := range tbl {
73+
t.Run(tt.name, func(t *testing.T) {
74+
var locks int32
75+
sema := NewSemaphore(tt.capacity)
76+
77+
for i := 0; i < tt.lockTimes; i++ {
78+
if sema.TryLock() {
79+
atomic.AddInt32(&locks, 1)
80+
}
81+
}
82+
83+
// Check the acquired locks, it should not exceed capacity.
84+
assert.Equal(t, int32(tt.expectedLocks), atomic.LoadInt32(&locks))
85+
})
86+
}
3387
}

0 commit comments

Comments
 (0)