github.com/mitranim/gg@v0.1.17/conc_test.go (about) 1 package gg_test 2 3 import ( 4 "context" 5 "sync" 6 "testing" 7 8 "github.com/mitranim/gg" 9 "github.com/mitranim/gg/gtest" 10 ) 11 12 var ( 13 testErr0 = error(gg.Errf(`test err 0`)) 14 testErr1 = error(gg.Errf(`test err 1`)) 15 testErr2 = error(gg.Errf(`test err 2`)) 16 ) 17 18 const ( 19 testErrA = gg.ErrStr(`test err A`) 20 testErrB = gg.ErrStr(`test err B`) 21 ) 22 23 func testPanic0() { panic(testErr0) } 24 func testPanic1() { panic(testErr1) } 25 26 func testNopCtx(context.Context) {} 27 func testPanicCtx0(context.Context) { panic(testErr0) } 28 func testPanicCtx1(context.Context) { panic(testErr1) } 29 func testPanicCtx2(context.Context) { panic(testErr2) } 30 31 func TestConc(t *testing.T) { 32 defer gtest.Catch(t) 33 34 t.Run(`no_panic`, func(t *testing.T) { 35 defer gtest.Catch(t) 36 37 gtest.Zero(gg.ConcCatch()) 38 gtest.Equal(gg.ConcCatch(nil, nil, nil), []error{nil, nil, nil}) 39 40 gtest.Equal( 41 gg.ConcCatch(gg.Nop), 42 []error{nil}, 43 ) 44 45 gtest.Equal( 46 gg.ConcCatch(gg.Nop, gg.Nop), 47 []error{nil, nil}, 48 ) 49 50 gtest.Equal( 51 gg.ConcCatch(gg.Nop, nil, gg.Nop), 52 []error{nil, nil, nil}, 53 ) 54 55 gtest.Equal( 56 gg.ConcCatch(nil, gg.Nop, nil, gg.Nop, nil), 57 []error{nil, nil, nil, nil, nil}, 58 ) 59 }) 60 61 t.Run(`only_panic`, func(t *testing.T) { 62 defer gtest.Catch(t) 63 64 gtest.Equal( 65 gg.ConcCatch(testPanic0), 66 []error{testErr0}, 67 ) 68 69 gtest.Equal( 70 gg.ConcCatch(testPanic0, testPanic1), 71 []error{testErr0, testErr1}, 72 ) 73 }) 74 75 t.Run(`mixed`, func(t *testing.T) { 76 defer gtest.Catch(t) 77 78 gtest.Equal( 79 gg.ConcCatch(gg.Nop, testPanic0, gg.Nop, testPanic1, gg.Nop), 80 gg.Errs{nil, testErr0, nil, testErr1, nil}, 81 ) 82 }) 83 } 84 85 func BenchmarkConcCatch_one(b *testing.B) { 86 for ind := 0; ind < b.N; ind++ { 87 _ = gg.ConcCatch(testPanic0) 88 } 89 } 90 91 func BenchmarkConcCatch_multi(b *testing.B) { 92 for ind := 0; ind < b.N; ind++ { 93 _ = gg.ConcCatch(gg.Nop, testPanic0, gg.Nop, testPanic1, gg.Nop) 94 } 95 } 96 97 // Needs more test cases. 98 func TestConcMapCatch(t *testing.T) { 99 defer gtest.Catch(t) 100 101 src := []int{10, 20, 30} 102 vals, errs := gg.ConcMapCatch(src, testConcMapFunc) 103 104 gtest.Len(vals, len(src)) 105 gtest.Len(errs, len(src)) 106 107 gtest.Equal(vals, []string{`10`, ``, `30`}) 108 gtest.Equal(errs, []error{nil, testErr1, nil}) 109 } 110 111 func testConcMapFunc(src int) string { 112 if src == 20 { 113 panic(testErr1) 114 } 115 return gg.String(src) 116 } 117 118 // Needs more test cases. 119 func TestConcRace(t *testing.T) { 120 defer gtest.Catch(t) 121 122 //nolint:staticcheck 123 gtest.Zero(gg.ConcRace().RunCatch(nil)) 124 //nolint:staticcheck 125 gtest.Zero(gg.ConcRace(nil).RunCatch(nil)) 126 //nolint:staticcheck 127 gtest.Zero(gg.ConcRace(testNopCtx).RunCatch(nil)) 128 129 ctx := context.Background() 130 gtest.Zero(gg.ConcRace().RunCatch(ctx)) 131 gtest.Zero(gg.ConcRace(nil).RunCatch(ctx)) 132 gtest.Zero(gg.ConcRace(nil, nil).RunCatch(ctx)) 133 gtest.Zero(gg.ConcRace(nil, nil, nil).RunCatch(ctx)) 134 135 gtest.Zero(gg.ConcRace(testNopCtx).RunCatch(ctx)) 136 gtest.Zero(gg.ConcRace(testNopCtx, testNopCtx).RunCatch(ctx)) 137 gtest.Zero(gg.ConcRace(testNopCtx, testNopCtx, testNopCtx).RunCatch(ctx)) 138 139 gtest.Is( 140 //nolint:staticcheck 141 gg.ConcRace(testPanicCtx0).RunCatch(nil), 142 testErr0, 143 ) 144 145 gtest.Is( 146 gg.ConcRace(testPanicCtx0, testNopCtx).RunCatch(ctx), 147 testErr0, 148 ) 149 150 gtest.Is( 151 gg.ConcRace(testNopCtx, testPanicCtx0).RunCatch(ctx), 152 testErr0, 153 ) 154 155 gtest.Is( 156 gg.ConcRace(testNopCtx, testPanicCtx0, testNopCtx).RunCatch(ctx), 157 testErr0, 158 ) 159 160 gtest.Is( 161 gg.ConcRace(testPanicCtx0, testPanicCtx0).RunCatch(ctx), 162 testErr0, 163 ) 164 165 gtest.Is( 166 gg.ConcRace(testPanicCtx0, testPanicCtx0, testPanicCtx0).RunCatch(ctx), 167 testErr0, 168 ) 169 170 gtest.HasEqual( 171 []error{testErr0, testErr1, testErr2}, 172 gg.ConcRace(testPanicCtx0, testPanicCtx1, testPanicCtx2).RunCatch(ctx), 173 ) 174 175 /** 176 Every function must receive the same cancelable context, and the context 177 must be canceled after completion, regardless if we have full success, 178 partial success, or full failure. 179 */ 180 { 181 test := func(funs ...func(context.Context)) { 182 conc := make(gg.ConcRaceSlice, len(funs)) 183 ctxs := make([]context.Context, len(funs)) 184 185 // This test requires additional syncing because `.Run` or `.RunCatch` 186 // terminate on the first panic, without waiting for the termination 187 // of the remaining functions. This is by design, but in this test, 188 // we must wait for their termination to ensure that the slice of 189 // contexts is fully mutated. 190 var gro sync.WaitGroup 191 192 for ind, fun := range funs { 193 ind, fun := ind, fun 194 gro.Add(1) 195 196 conc.Add(func(ctx context.Context) { 197 defer gro.Add(-1) 198 ctxs[ind] = ctx 199 fun(ctx) 200 }) 201 } 202 203 gg.Nop1(conc.RunCatch(ctx)) 204 gro.Wait() 205 206 testIsContextConsistent(ctxs...) 207 testIsCtxCanceled(ctxs[0]) 208 } 209 210 test(testNopCtx) 211 test(testNopCtx, testNopCtx) 212 test(testNopCtx, testNopCtx, testNopCtx) 213 test(testPanicCtx0, testNopCtx, testNopCtx) 214 test(testNopCtx, testPanicCtx0, testNopCtx) 215 test(testNopCtx, testNopCtx, testPanicCtx0) 216 test(testPanicCtx0, testNopCtx, testPanicCtx0) 217 test(testPanicCtx0, testPanicCtx0, testPanicCtx0) 218 } 219 220 /** 221 On the first panic, we must immediately cancel the context before returning 222 the caught error. Some of the concurrently launched functions may still 223 continue running in the background. They're expected to respect context 224 cancelation and terminate as soon as reasonably possible, but that's up 225 to the user of the library. Our responsibility is to terminate and cancel 226 as soon as the first panic is found. 227 */ 228 { 229 var gro0 sync.WaitGroup 230 var gro1 sync.WaitGroup 231 var state0 CtxState 232 var state1 CtxState 233 var state2 CtxState 234 235 gro0.Add(1) 236 gro1.Add(2) 237 238 gtest.Is( 239 /** 240 This must terminate and return the error even though some inner functions 241 are still blocked on the wait group, which is unblocked AFTER this test 242 phase. This ensures that the concurrent run terminates on the first 243 panic without waiting for all functions. Otherwise, the test would 244 deadlock and eventually time out. 245 */ 246 gg.ConcRace( 247 func(ctx context.Context) { 248 defer gro1.Add(-1) 249 gro0.Wait() 250 state0 = ToCtxState(ctx) 251 }, 252 func(ctx context.Context) { 253 state1 = ToCtxState(ctx) 254 panic(testErr0) 255 }, 256 func(ctx context.Context) { 257 defer gro1.Add(-1) 258 gro0.Wait() 259 state2 = ToCtxState(ctx) 260 }, 261 ).RunCatch(ctx), 262 testErr0, 263 ) 264 265 // This should unblock the inner functions, whose context must now be 266 // canceled. 267 gro0.Add(-1) 268 gro1.Wait() 269 270 gtest.Equal(state0, CtxState{true, context.Canceled}) 271 gtest.Equal(state1, CtxState{false, nil}) 272 gtest.Equal(state2, CtxState{true, context.Canceled}) 273 } 274 } 275 276 /* 277 Caution: this operation is prone to race conditions, and may produce 278 "corrupted" states, such as `{Done: false, Err: context.Canceled}`, 279 depending on the execution timing. Our tests ensure that we always 280 see very specific results, and anything else is considered a test 281 failure. Avoid this pattern in actual code. 282 */ 283 func ToCtxState(ctx context.Context) CtxState { 284 return CtxState{isCtxDone(ctx), ctx.Err()} 285 } 286 287 type CtxState struct { 288 Done bool 289 Err error 290 } 291 292 func testIsContextConsistent(vals ...context.Context) { 293 if len(vals) <= 1 { 294 return 295 } 296 297 exp := vals[0] 298 gtest.NotZero(exp) 299 300 for _, val := range vals { 301 if exp != val { 302 panic(gtest.ErrLines( 303 `unexpected difference between context values`, 304 gtest.MsgEqDetailed(val, exp), 305 )) 306 } 307 } 308 } 309 310 func testIsCtxCanceled(ctx context.Context) { 311 if !isCtxDone(ctx) { 312 panic(`expected context to be done`) 313 } 314 gtest.ErrorIs(ctx.Err(), context.Canceled) 315 } 316 317 /* 318 Warning: the output is correct only when it's `true`. When the output is 319 `false`, then sometimes it's incorrect and the actual result is UNKNOWN 320 because the channel may be concurrently closed before your next line of code. 321 322 In other words, the return type of this function isn't exactly a boolean. 323 It's a union of "true" and "unknowable". 324 */ 325 func isCtxDone(ctx context.Context) bool { 326 select { 327 case <-ctx.Done(): 328 return true 329 default: 330 return false 331 } 332 }