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  }