Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/pkg/singleflight/signleflight_test.go
1560 views
1
// Copyright 2013 The Go Authors. All rights reserved.
2
// Use of this source code is governed by a BSD-style
3
// license that can be found in the LICENSE file.
4
5
package singleflight
6
7
import (
8
"bytes"
9
"errors"
10
"fmt"
11
"os"
12
"os/exec"
13
"runtime"
14
"runtime/debug"
15
"strings"
16
"sync"
17
"sync/atomic"
18
"testing"
19
"time"
20
)
21
22
func TestDo(t *testing.T) {
23
var g Group[string]
24
v, err, _ := g.Do("key", func() (string, error) {
25
return "bar", nil
26
})
27
if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
28
t.Errorf("Do = %v; want %v", got, want)
29
}
30
if err != nil {
31
t.Errorf("Do error = %v", err)
32
}
33
}
34
35
func TestDoErr(t *testing.T) {
36
var g Group[any]
37
someErr := errors.New("Some error")
38
v, err, _ := g.Do("key", func() (any, error) {
39
return nil, someErr
40
})
41
if err != someErr {
42
t.Errorf("Do error = %v; want someErr %v", err, someErr)
43
}
44
if v != nil {
45
t.Errorf("unexpected non-nil value %#v", v)
46
}
47
}
48
49
func TestDoDupSuppress(t *testing.T) {
50
var g Group[string]
51
var wg1, wg2 sync.WaitGroup
52
c := make(chan string, 1)
53
var calls int32
54
fn := func() (string, error) {
55
if atomic.AddInt32(&calls, 1) == 1 {
56
// First invocation.
57
wg1.Done()
58
}
59
v := <-c
60
c <- v // pump; make available for any future calls
61
62
time.Sleep(10 * time.Millisecond) // let more goroutines enter Do
63
64
return v, nil
65
}
66
67
const n = 10
68
wg1.Add(1)
69
for i := 0; i < n; i++ {
70
wg1.Add(1)
71
wg2.Add(1)
72
go func() {
73
defer wg2.Done()
74
wg1.Done()
75
v, err, _ := g.Do("key", fn)
76
if err != nil {
77
t.Errorf("Do error: %v", err)
78
return
79
}
80
if v != "bar" {
81
t.Errorf("Do = %T %v; want %q", v, v, "bar")
82
}
83
}()
84
}
85
wg1.Wait()
86
// At least one goroutine is in fn now and all of them have at
87
// least reached the line before the Do.
88
c <- "bar"
89
wg2.Wait()
90
if got := atomic.LoadInt32(&calls); got <= 0 || got >= n {
91
t.Errorf("number of calls = %d; want over 0 and less than %d", got, n)
92
}
93
}
94
95
// Test that singleflight behaves correctly after Forget called.
96
// See https://github.com/golang/go/issues/31420
97
func TestForget(t *testing.T) {
98
var g Group[any]
99
100
var (
101
firstStarted = make(chan struct{})
102
unblockFirst = make(chan struct{})
103
firstFinished = make(chan struct{})
104
)
105
106
go func() {
107
g.Do("key", func() (i any, e error) {
108
close(firstStarted)
109
<-unblockFirst
110
close(firstFinished)
111
return
112
})
113
}()
114
<-firstStarted
115
g.Forget("key")
116
117
unblockSecond := make(chan struct{})
118
secondResult := g.DoChan("key", func() (i any, e error) {
119
<-unblockSecond
120
return 2, nil
121
})
122
123
close(unblockFirst)
124
<-firstFinished
125
126
thirdResult := g.DoChan("key", func() (i any, e error) {
127
return 3, nil
128
})
129
130
close(unblockSecond)
131
<-secondResult
132
r := <-thirdResult
133
if r.Val != 2 {
134
t.Errorf("We should receive result produced by second call, expected: 2, got %d", r.Val)
135
}
136
}
137
138
func TestDoChan(t *testing.T) {
139
var g Group[string]
140
ch := g.DoChan("key", func() (string, error) {
141
return "bar", nil
142
})
143
144
res := <-ch
145
v := res.Val
146
err := res.Err
147
if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
148
t.Errorf("Do = %v; want %v", got, want)
149
}
150
if err != nil {
151
t.Errorf("Do error = %v", err)
152
}
153
}
154
155
// Test singleflight behaves correctly after Do panic.
156
// See https://github.com/golang/go/issues/41133
157
func TestPanicDo(t *testing.T) {
158
var g Group[any]
159
fn := func() (any, error) {
160
panic("invalid memory address or nil pointer dereference")
161
}
162
163
const n = 5
164
waited := int32(n)
165
panicCount := int32(0)
166
done := make(chan struct{})
167
for i := 0; i < n; i++ {
168
go func() {
169
defer func() {
170
if err := recover(); err != nil {
171
t.Logf("Got panic: %v\n%s", err, debug.Stack())
172
atomic.AddInt32(&panicCount, 1)
173
}
174
175
if atomic.AddInt32(&waited, -1) == 0 {
176
close(done)
177
}
178
}()
179
180
g.Do("key", fn)
181
}()
182
}
183
184
select {
185
case <-done:
186
if panicCount != n {
187
t.Errorf("Expect %d panic, but got %d", n, panicCount)
188
}
189
case <-time.After(time.Second):
190
t.Fatalf("Do hangs")
191
}
192
}
193
194
func TestGoexitDo(t *testing.T) {
195
var g Group[any]
196
fn := func() (any, error) {
197
runtime.Goexit()
198
return nil, nil
199
}
200
201
const n = 5
202
waited := int32(n)
203
done := make(chan struct{})
204
for i := 0; i < n; i++ {
205
go func() {
206
var err error
207
defer func() {
208
if err != nil {
209
t.Errorf("Error should be nil, but got: %v", err)
210
}
211
if atomic.AddInt32(&waited, -1) == 0 {
212
close(done)
213
}
214
}()
215
_, err, _ = g.Do("key", fn)
216
}()
217
}
218
219
select {
220
case <-done:
221
case <-time.After(time.Second):
222
t.Fatalf("Do hangs")
223
}
224
}
225
226
func TestPanicDoChan(t *testing.T) {
227
if runtime.GOOS == "js" {
228
t.Skipf("js does not support exec")
229
}
230
231
if os.Getenv("TEST_PANIC_DOCHAN") != "" {
232
defer func() {
233
recover()
234
}()
235
236
g := new(Group[any])
237
ch := g.DoChan("", func() (any, error) {
238
panic("Panicking in DoChan")
239
})
240
<-ch
241
t.Fatalf("DoChan unexpectedly returned")
242
}
243
244
t.Parallel()
245
246
cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v")
247
cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
248
out := new(bytes.Buffer)
249
cmd.Stdout = out
250
cmd.Stderr = out
251
if err := cmd.Start(); err != nil {
252
t.Fatal(err)
253
}
254
255
err := cmd.Wait()
256
t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out)
257
if err == nil {
258
t.Errorf("Test subprocess passed; want a crash due to panic in DoChan")
259
}
260
if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) {
261
t.Errorf("Test subprocess failed with an unexpected failure mode.")
262
}
263
if !bytes.Contains(out.Bytes(), []byte("Panicking in DoChan")) {
264
t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in DoChan")
265
}
266
}
267
268
func TestPanicDoSharedByDoChan(t *testing.T) {
269
if runtime.GOOS == "js" {
270
t.Skipf("js does not support exec")
271
}
272
273
if os.Getenv("TEST_PANIC_DOCHAN") != "" {
274
blocked := make(chan struct{})
275
unblock := make(chan struct{})
276
277
g := new(Group[any])
278
go func() {
279
defer func() {
280
recover()
281
}()
282
g.Do("", func() (any, error) {
283
close(blocked)
284
<-unblock
285
panic("Panicking in Do")
286
})
287
}()
288
289
<-blocked
290
ch := g.DoChan("", func() (any, error) {
291
panic("DoChan unexpectedly executed callback")
292
})
293
close(unblock)
294
<-ch
295
t.Fatalf("DoChan unexpectedly returned")
296
}
297
298
t.Parallel()
299
300
cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v")
301
cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
302
out := new(bytes.Buffer)
303
cmd.Stdout = out
304
cmd.Stderr = out
305
if err := cmd.Start(); err != nil {
306
t.Fatal(err)
307
}
308
309
err := cmd.Wait()
310
t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out)
311
if err == nil {
312
t.Errorf("Test subprocess passed; want a crash due to panic in Do shared by DoChan")
313
}
314
if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) {
315
t.Errorf("Test subprocess failed with an unexpected failure mode.")
316
}
317
if !bytes.Contains(out.Bytes(), []byte("Panicking in Do")) {
318
t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in Do")
319
}
320
}
321
322