github.com/haraldrudell/parl@v0.4.176/g0/go-group_test.go (about) 1 /* 2 © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package g0 7 8 import ( 9 "context" 10 "errors" 11 "fmt" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/haraldrudell/parl" 17 "github.com/haraldrudell/parl/pruntime" 18 ) 19 20 func TestGoGroup(t *testing.T) { 21 var messageBad = "bad" 22 var errBad = errors.New(messageBad) 23 var shortTime = time.Millisecond 24 25 var g parl.Go 26 var goError parl.GoError 27 var ok, isClosed, allowTermination, didReceive bool 28 // var count, fatals int 29 var subGo parl.SubGo 30 var subGroup parl.SubGroup 31 var ctx, ctxAct context.Context 32 var cancelFunc context.CancelFunc 33 // var threads []parl.ThreadData 34 // var onFirstFatal = func(goGen parl.GoGen) { fatals++ } 35 // var expectG0ID uint64 36 var err error 37 var noError *error 38 var errCh <-chan parl.GoError 39 // var didComplete atomic.Bool 40 var isReady, isDone parl.WaitGroupCh 41 var timer *time.Timer 42 43 // Go() SubGo() SubGroup() Ch() Wait() EnableTermination() 44 // IsEnableTermination() Cancel() Context() Threads() NamedThreads() 45 // SetDebug() 46 var goGroup parl.GoGroup 47 var goGroupImpl *GoGroup 48 var reset = func(ctx ...context.Context) { 49 var parentContext context.Context 50 if len(ctx) > 0 { 51 parentContext = ctx[0] 52 } else { 53 parentContext = context.Background() 54 } 55 goGroup = NewGoGroup(parentContext) 56 goGroupImpl = goGroup.(*GoGroup) 57 } 58 59 // NewGoGroup should not be canceled 60 reset() 61 isClosed = goGroupImpl.endCh.IsClosed() 62 if isClosed { 63 t.Error("NewGoGroup isClosed true") 64 } 65 66 // GoGroup should terminate when its last thread exits 67 // Go should return parl.Go 68 reset() 69 g = goGroup.Go() 70 g.Done(noError) 71 isClosed = goGroupImpl.endCh.IsClosed() 72 if !isClosed { 73 t.Error("NewGoGroup last exit does not terminate") 74 } 75 76 // EnableTermination should be true 77 reset() 78 allowTermination = goGroup.EnableTermination() 79 if !allowTermination { 80 t.Error("EnableTermination false") 81 } 82 83 // EnableTermination(parl.PreventTermination) should be false 84 reset() 85 allowTermination = goGroup.EnableTermination(parl.PreventTermination) 86 if allowTermination { 87 t.Error("EnableTermination true") 88 } 89 90 // EnableTermination(parl.AllowTermination) should terminate an empty ThreadGroup 91 reset() 92 allowTermination = goGroup.EnableTermination(parl.AllowTermination) 93 _ = allowTermination 94 isClosed = goGroupImpl.endCh.IsClosed() 95 if !isClosed { 96 t.Error("EnableTermination true does not terminate") 97 } 98 99 // EnableTermination(parl.PreventTermination) should prevent termination 100 reset() 101 allowTermination = goGroup.EnableTermination(parl.PreventTermination) 102 _ = allowTermination 103 g = goGroup.Go() 104 g.Done(noError) 105 isClosed = goGroupImpl.endCh.IsClosed() 106 if isClosed { 107 t.Error("EnableTermination(parl.PreventTermination) does not prevent termination") 108 } 109 allowTermination = goGroup.EnableTermination(parl.AllowTermination) 110 if !allowTermination { 111 t.Error("EnableTermination false") 112 } 113 isClosed = goGroupImpl.endCh.IsClosed() 114 if !isClosed { 115 t.Error("EnableTermination(parl.AllowTermination) did not terminate") 116 } 117 118 // Context should return a context that is different from parent context 119 ctx = context.Background() 120 reset(ctx) 121 ctxAct = goGroup.Context() 122 if ctxAct == ctx { 123 t.Error("Context is parent context") 124 } 125 126 // Context should return a context that is canceled by parent context 127 ctx, cancelFunc = context.WithCancel(context.Background()) 128 reset(ctx) 129 ctxAct = goGroup.Context() 130 if ctxAct.Err() != nil { 131 t.Error("Context is canceled") 132 } 133 cancelFunc() 134 err = ctxAct.Err() 135 if !errors.Is(err, context.Canceled) { 136 t.Error("Context not canceled by parent") 137 } 138 139 // Cancel should cancel Context 140 reset() 141 ctxAct = goGroup.Context() 142 if ctxAct.Err() != nil { 143 t.Error("Context is canceled") 144 } 145 goGroup.Cancel() 146 err = ctxAct.Err() 147 if !errors.Is(err, context.Canceled) { 148 t.Error("Cancel did not cancel Context") 149 } 150 151 // Cancel should cancel Go Context 152 reset() 153 g = goGroup.Go() 154 goGroup.Cancel() 155 err = g.Context().Err() 156 if !errors.Is(err, context.Canceled) { 157 t.Error("Cancel did not cancel Go Context") 158 } 159 160 // Cancel should cancel SubGo Context 161 // - SubGo 162 reset() 163 subGo = goGroup.SubGo() 164 goGroup.Cancel() 165 err = subGo.Context().Err() 166 if !errors.Is(err, context.Canceled) { 167 t.Error("Cancel did not cancel SubGo Context") 168 } 169 170 // Cancel should cancel SubGroup Context 171 // -SubGroup 172 reset() 173 subGroup = goGroup.SubGroup() 174 goGroup.Cancel() 175 err = subGroup.Context().Err() 176 if !errors.Is(err, context.Canceled) { 177 t.Error("Cancel did not cancel subGroup Context") 178 } 179 180 // Ch should send errors 181 reset() 182 errCh = goGroup.Ch() 183 g = goGroup.Go() 184 g.AddError(errBad) 185 goError = <-errCh 186 if !errors.Is(goError.Err(), errBad) { 187 t.Error("Ch not sending errors") 188 } 189 190 // Ch should close on termination 191 reset() 192 goGroup.EnableTermination(parl.AllowTermination) 193 select { 194 case goError, ok = <-goGroup.Ch(): 195 didReceive = true 196 default: 197 didReceive = false 198 } 199 _ = goError 200 if !didReceive || ok { 201 t.Error("Ch did not close on termination") 202 } 203 204 // Wait should wait until GoGroup terminates 205 reset() 206 isReady.Reset().Add(1) 207 isDone.Reset().Add(1) 208 go waiter(goGroup, &isReady, &isDone) 209 isReady.Wait() 210 if isDone.IsZero() { 211 t.Error("Wait completed prematurely") 212 } 213 goGroup.EnableTermination(parl.AllowTermination) 214 // there is a race condition with waiter function 215 // - waiter needs to detect that the channel closed and 216 // trigger isDone 217 // - Wait enough here, shortTime 218 timer = time.NewTimer(shortTime) 219 select { 220 case <-isDone.Ch(): 221 case <-timer.C: 222 } 223 if !isDone.IsZero() { 224 t.Error("Wait did not complete on termination") 225 } 226 227 // methods to test below here: 228 // Threads() NamedThreads() 229 // SetDebug() 230 // - first fatal feature 231 } 232 233 func TestGoGroup_Frames(t *testing.T) { 234 // goGroup and cL on same line 235 var goGroup parl.GoGroup 236 var subGo parl.SubGo 237 var subGroup parl.SubGroup 238 var g0 parl.Go 239 var cL *pruntime.CodeLocation 240 241 // NewGoGroup: GoGroup.String() includes NewGoGroup caller location 242 goGroup, cL = NewGoGroup(context.Background()), pruntime.NewCodeLocation(0) 243 if !strings.HasSuffix(goGroup.String(), cL.Short()) { 244 t.Errorf("GoGroup.String BAD: %q exp suffix: %q", goGroup.String(), cL.Short()) 245 } 246 247 // GoGroup.SubGo includes caller location 248 subGo, cL = goGroup.SubGo(), pruntime.NewCodeLocation(0) 249 if !strings.HasSuffix(subGo.String(), cL.Short()) { 250 t.Errorf("SubGo.String: %q exp suffix: %q", subGo.String(), cL.Short()) 251 } 252 253 // GoGroup.SubGroup includes caller location 254 subGroup, cL = goGroup.SubGroup(), pruntime.NewCodeLocation(0) 255 if !strings.HasSuffix(subGroup.String(), cL.Short()) { 256 t.Errorf("SubGroup.String: %q exp suffix: %q", subGroup.String(), cL.Short()) 257 } 258 259 // GoGroup.Go includes caller location 260 g0, cL = goGroup.Go(), pruntime.NewCodeLocation(0) 261 if !strings.HasSuffix(g0.String(), cL.Short()) { 262 var _ = (&GoGroup{}).Go 263 // Go.String: "subGroup#3_threads:0(0)_New:g0.TestGoGroup_Frames()-go-group_test.go:217" exp suffix: "g0.TestGoGroup_Frames()-go-group_test.go:223" 264 t.Errorf("Go.String: %q exp suffix: %q", subGroup.String(), cL.Short()) 265 } 266 } 267 268 func TestSubGo(t *testing.T) { 269 var err = errors.New("bad") 270 271 var goGroup parl.GoGroup 272 var goGroupImpl, subGoImpl *GoGroup 273 var subGo parl.SubGo 274 var goError, goError2 parl.GoError 275 var parlGo parl.Go 276 var ok bool 277 278 // SubGo non-fatal error 279 goGroup = NewGoGroup(context.Background()) 280 goGroupImpl = goGroup.(*GoGroup) 281 subGo = goGroup.SubGo() 282 subGoImpl = subGo.(*GoGroup) 283 goError = NewGoError(err, parl.GeNonFatal, nil) 284 subGoImpl.ConsumeError(goError) 285 // the non-fatal subGo error should be recevied on GoGroup error channel 286 goError2 = <-goGroup.Ch() 287 if goError2 != goError { 288 t.Errorf("bad non-fatal subgo error") 289 } 290 291 // SubGo fatal thread termination 292 parlGo = subGo.Go() 293 parlGo.Done(&err) 294 // the SubGo fatal error should be recevied on GoGroup error channel 295 goError2 = <-goGroup.Ch() 296 if !errors.Is(goError2.Err(), err) { 297 t.Error("bad fatal subgo error") 298 } 299 // subgo should now terminate after its only thread exited 300 if !subGoImpl.isEnd() { 301 t.Error("subGo did not terminate") 302 } 303 304 // gogroup should now have terminated and closed its error channel 305 // - its only thread did exit 306 goError2, ok = <-goGroup.Ch() // wait for subGroup channel to close 307 if ok { 308 t.Errorf("goGroup channel did not close: %s", goError2) 309 } 310 if !goGroupImpl.isEnd() { 311 t.Error("goGroup did not terminate") 312 } 313 } 314 315 func TestSubGroup(t *testing.T) { 316 var err = errors.New("bad") 317 318 var goGroup parl.GoGroup 319 var goGroupImpl, subGroupImpl *GoGroup 320 var subGroup parl.SubGroup 321 var goError, goError2 parl.GoError 322 var parlGo parl.Go 323 var ok bool 324 325 // non-fatal error: sent to gogroup 326 goGroup = NewGoGroup(context.Background()) 327 goGroupImpl = goGroup.(*GoGroup) 328 subGroup = goGroup.SubGroup() 329 subGroupImpl = subGroup.(*GoGroup) 330 goError = NewGoError(err, parl.GeNonFatal, nil) 331 subGroupImpl.ConsumeError(goError) 332 goError2 = <-goGroup.Ch() 333 if goError2 != goError { 334 t.Errorf("bad non-fatal subgroup error") 335 } 336 337 // fatal error: 338 // - a thread exits with g0.Done having error 339 // - the subGroup hides the fatal error from the parent 340 // - the parent receives non-fatal GeLocalChan of the error and a GeExit with no error 341 // - subgroup emits fatal error on its error channel 342 parlGo = subGroupImpl.Go() 343 parlGo.Done(&err) 344 // goGroup GeLocalChan 345 goError2 = <-goGroup.Ch() 346 if !errors.Is(goError2.Err(), err) { 347 t.Error("bad gogroup error") 348 } 349 if goError2.ErrContext() != parl.GeLocalChan { 350 t.Errorf("bad gogroup error context: %s", goError2.ErrContext()) 351 } 352 // goGroup good thread exit 353 goError2 = <-goGroup.Ch() 354 if goError2.Err() != nil { 355 t.Errorf("bad gogroup error: %s", goError2.String()) 356 } 357 if goError2.ErrContext() != parl.GeExit { 358 t.Errorf("bad gogroup error context: %s", goError2.ErrContext()) 359 } 360 // SubGroup: GeExit fatal error 361 goError2 = <-subGroup.Ch() 362 if !errors.Is(goError2.Err(), err) { 363 t.Error("bad fatal subgroup error") 364 } 365 366 // subgroup should now exit: 367 goError2, ok = <-subGroup.Ch() // wait for subGroup channel to close 368 if ok { 369 t.Errorf("subGroup channel did not close: %s", goError2) 370 } 371 if !subGroupImpl.isEnd() { 372 t.Error("subGroup did not terminate") 373 } 374 375 // gogroup exits 376 goError2, ok = <-goGroup.Ch() // wait for subGroup channel to close 377 if ok { 378 t.Errorf("goGroup channel did not close: %s", goError2) 379 } 380 if !goGroupImpl.isEnd() { 381 t.Error("goGroup did not terminate") 382 } 383 } 384 385 func TestCancel(t *testing.T) { 386 var ctx = parl.AddNotifier(context.Background(), func(stack parl.Stack) { 387 t.Logf("ALLCANCEL %s", stack) 388 }) 389 390 var threadGroup = NewGoGroup(ctx) 391 // threadGroup.(*GoGroup).addNotifier(func(slice pruntime.StackSlice) { 392 // t.Logf("CANCEL %s %s", GoChain(threadGroup), slice) 393 // }) 394 var subGroup = threadGroup.SubGroup() 395 // subGroup.(*GoGroup).addNotifier(func(slice pruntime.StackSlice) { 396 // t.Logf("CANCEL %s %s", GoChain(subGroup), slice) 397 // }) 398 t.Logf("STATE0: %t %t", threadGroup.Context().Err() != nil, subGroup.Context().Err() != nil) 399 if threadGroup.Context().Err() != nil { 400 t.Error("threadGroup canceled") 401 } 402 if subGroup.Context().Err() != nil { 403 t.Error("subGroup canceled") 404 } 405 subGroup.Cancel() 406 t.Logf("STATE1: %t %t", threadGroup.Context().Err() != nil, subGroup.Context().Err() != nil) 407 if threadGroup.Context().Err() != nil { 408 t.Error("threadGroup canceled") 409 } 410 if subGroup.Context().Err() == nil { 411 t.Error("subGroup did not cancel") 412 } 413 //t.Fail() 414 } 415 416 func GoChain(g parl.GoGen) (s string) { 417 for { 418 var s0 = GoNo(g) 419 if s == "" { 420 s = s0 421 } else { 422 s += "—" + s0 423 } 424 if g == nil { 425 return 426 } else if g = Parent(g); g == nil { 427 return 428 } 429 } 430 } 431 432 func Parent(g parl.GoGen) (parent parl.GoGen) { 433 switch g := g.(type) { 434 case *Go: 435 parent = g.goParent.(parl.GoGen) 436 case *GoGroup: 437 if p := g.parent; p != nil { 438 parent = p.(parl.GoGen) 439 } 440 } 441 return 442 } 443 444 func GoNo(g parl.GoGen) (goNo string) { 445 switch g1 := g.(type) { 446 case *Go: 447 goNo = "Go" + g1.id.String() + ":" + g1.GoID().String() 448 case *GoGroup: 449 if !g1.hasErrorChannel { 450 goNo = "SubGo" 451 } else if g1.parent != nil { 452 goNo = "SubGroup" 453 } else { 454 goNo = "GoGroup" 455 } 456 goNo += g1.id.String() 457 case nil: 458 goNo = "nil" 459 default: 460 goNo = fmt.Sprintf("?type:%T", g) 461 } 462 return 463 } 464 465 func TestGoGroupTermination(t *testing.T) { 466 var goGroup = NewGoGroup(context.Background()) 467 468 // an unused goGroup will only terminate after EnableTermination 469 goGroup.EnableTermination(parl.AllowTermination) 470 471 goGroup.Wait() 472 } 473 474 func TestSubGoTermination(t *testing.T) { 475 var goGroup = NewGoGroup(context.Background()) 476 var subGo = goGroup.SubGo() 477 478 // an unused subGo will only terminate after EnableTermination 479 subGo.EnableTermination(parl.AllowTermination) 480 481 subGo.Wait() 482 483 // CascadeTermination does this 484 //goGroup.EnableTermination(parl.AllowTermination) 485 486 goGroup.Wait() 487 } 488 489 func TestGoGroup2Termination(t *testing.T) { 490 var goGroup = NewGoGroup(context.Background()) 491 var subGroup = goGroup.SubGroup() 492 var subGo = subGroup.SubGo() 493 494 // an unused subGo will only terminate after EnableTermination 495 subGo.EnableTermination(parl.AllowTermination) 496 497 //subGo.EnableTermination(parl.AllowTermination) 498 subGo.Wait() 499 500 subGroup.Wait() 501 502 goGroup.Wait() 503 } 504 505 // waiter tests GoGroup.Wait() 506 func waiter( 507 goGroup parl.GoGroup, 508 isReady, isDone parl.Doneable, 509 ) { 510 defer isDone.Done() 511 512 isReady.Done() 513 goGroup.Wait() 514 }