github.com/songzhibin97/gkit@v1.2.13/cache/singleflight/singleflight_test.go (about) 1 package singleflight 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "os" 8 "os/exec" 9 "runtime" 10 "runtime/debug" 11 "strings" 12 "sync" 13 "sync/atomic" 14 "testing" 15 "time" 16 ) 17 18 func TestDo(t *testing.T) { 19 g := NewSingleFlight() 20 v, err, _ := g.Do("key", func() (interface{}, error) { 21 return "bar", nil 22 }) 23 if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want { 24 t.Errorf("Do = %v; want %v", got, want) 25 } 26 if err != nil { 27 t.Errorf("Do error = %v", err) 28 } 29 } 30 31 func TestDoErr(t *testing.T) { 32 g := NewSingleFlight() 33 someErr := errors.New("Some error") 34 v, err, _ := g.Do("key", func() (interface{}, error) { 35 return nil, someErr 36 }) 37 if err != someErr { 38 t.Errorf("Do error = %v; want someErr %v", err, someErr) 39 } 40 if v != nil { 41 t.Errorf("unexpected non-nil value %#v", v) 42 } 43 } 44 45 func TestDoDupSuppress(t *testing.T) { 46 g := NewSingleFlight() 47 var wg1, wg2 sync.WaitGroup 48 c := make(chan string, 1) 49 var calls int32 50 fn := func() (interface{}, error) { 51 if atomic.AddInt32(&calls, 1) == 1 { 52 // First invocation. 53 wg1.Done() 54 } 55 v := <-c 56 c <- v // pump; make available for any future calls 57 58 time.Sleep(10 * time.Millisecond) // let more goroutines enter Do 59 60 return v, nil 61 } 62 63 const n = 10 64 wg1.Add(1) 65 for i := 0; i < n; i++ { 66 wg1.Add(1) 67 wg2.Add(1) 68 go func() { 69 defer wg2.Done() 70 wg1.Done() 71 v, err, _ := g.Do("key", fn) 72 if err != nil { 73 t.Errorf("Do error: %v", err) 74 return 75 } 76 if s, _ := v.(string); s != "bar" { 77 t.Errorf("Do = %T %v; want %q", v, v, "bar") 78 } 79 }() 80 } 81 wg1.Wait() 82 83 c <- "bar" 84 wg2.Wait() 85 if got := atomic.LoadInt32(&calls); got <= 0 || got >= n { 86 t.Errorf("number of calls = %d; want over 0 and less than %d", got, n) 87 } 88 } 89 90 func TestPanicDo(t *testing.T) { 91 g := NewSingleFlight() 92 fn := func() (interface{}, error) { 93 panic("invalid memory address or nil pointer dereference") 94 } 95 96 const n = 5 97 waited := int32(n) 98 panicCount := int32(0) 99 done := make(chan struct{}) 100 for i := 0; i < n; i++ { 101 go func() { 102 defer func() { 103 if err := recover(); err != nil { 104 t.Logf("Got panic: %v\n%s", err, debug.Stack()) 105 atomic.AddInt32(&panicCount, 1) 106 } 107 108 if atomic.AddInt32(&waited, -1) == 0 { 109 close(done) 110 } 111 }() 112 113 _, _, _ = g.Do("key", fn) 114 }() 115 } 116 117 select { 118 case <-done: 119 if panicCount != n { 120 t.Errorf("Expect %d panic, but got %d", n, panicCount) 121 } 122 case <-time.After(time.Second): 123 t.Fatalf("Do hangs") 124 } 125 } 126 127 func TestGoexitDo(t *testing.T) { 128 g := NewSingleFlight() 129 fn := func() (interface{}, error) { 130 runtime.Goexit() 131 return nil, nil 132 } 133 134 const n = 5 135 waited := int32(n) 136 done := make(chan struct{}) 137 for i := 0; i < n; i++ { 138 go func() { 139 var err error 140 defer func() { 141 if err != nil { 142 t.Errorf("Error should be nil, but got: %v", err) 143 } 144 if atomic.AddInt32(&waited, -1) == 0 { 145 close(done) 146 } 147 }() 148 _, err, _ = g.Do("key", fn) 149 }() 150 } 151 152 select { 153 case <-done: 154 case <-time.After(time.Second): 155 t.Fatalf("Do hangs") 156 } 157 } 158 159 func TestPanicDoChan(t *testing.T) { 160 if runtime.GOOS == "js" { 161 t.Skipf("js does not support exec") 162 } 163 164 if os.Getenv("TEST_PANIC_DOCHAN") != "" { 165 defer func() { 166 _ = recover() 167 }() 168 169 g := NewSingleFlight() 170 ch := g.DoChan("", func() (interface{}, error) { 171 panic("Panicking in DoChan") 172 }) 173 t.Log(<-ch) 174 t.Fatalf("DoChan unexpectedly returned") 175 } 176 177 t.Parallel() 178 179 cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v") 180 cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1") 181 out := new(bytes.Buffer) 182 cmd.Stdout = out 183 cmd.Stderr = out 184 if err := cmd.Start(); err != nil { 185 t.Fatal(err) 186 } 187 188 err := cmd.Wait() 189 t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out) 190 if err == nil { 191 t.Errorf("Test subprocess passed; want a crash due to panic in DoChan") 192 } 193 if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) { 194 t.Errorf("Test subprocess failed with an unexpected failure mode.") 195 } 196 if !bytes.Contains(out.Bytes(), []byte("Panicking in DoChan")) { 197 t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in DoChan") 198 } 199 } 200 201 func TestPanicDoSharedByDoChan(t *testing.T) { 202 if runtime.GOOS == "js" { 203 t.Skipf("js does not support exec") 204 } 205 206 if os.Getenv("TEST_PANIC_DOCHAN") != "" { 207 blocked := make(chan struct{}) 208 unblock := make(chan struct{}) 209 210 g := NewSingleFlight() 211 go func() { 212 defer func() { 213 _ = recover() 214 }() 215 _, _, _ = g.Do("", func() (interface{}, error) { 216 close(blocked) 217 <-unblock 218 panic("Panicking in Do") 219 }) 220 }() 221 222 <-blocked 223 ch := g.DoChan("", func() (interface{}, error) { 224 panic("DoChan unexpectedly executed callback") 225 }) 226 close(unblock) 227 <-ch 228 t.Fatalf("DoChan unexpectedly returned") 229 } 230 231 t.Parallel() 232 233 cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v") 234 cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1") 235 out := new(bytes.Buffer) 236 cmd.Stdout = out 237 cmd.Stderr = out 238 if err := cmd.Start(); err != nil { 239 t.Fatal(err) 240 } 241 242 err := cmd.Wait() 243 t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out) 244 if err == nil { 245 t.Errorf("Test subprocess passed; want a crash due to panic in Do shared by DoChan") 246 } 247 if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) { 248 t.Errorf("Test subprocess failed with an unexpected failure mode.") 249 } 250 if !bytes.Contains(out.Bytes(), []byte("Panicking in Do")) { 251 t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in Do") 252 } 253 }