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  }