go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/common/eventbox/box_test.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package eventbox
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"testing"
    22  
    23  	"go.chromium.org/luci/common/logging"
    24  	"go.chromium.org/luci/common/retry/transient"
    25  	"go.chromium.org/luci/gae/service/datastore"
    26  
    27  	"go.chromium.org/luci/cv/internal/common"
    28  	"go.chromium.org/luci/cv/internal/cvtesting"
    29  
    30  	. "github.com/smartystreets/goconvey/convey"
    31  	. "go.chromium.org/luci/common/testing/assertions"
    32  )
    33  
    34  // processor simulates a variant of game of life on one cell in an array of
    35  // cells.
    36  type processor struct {
    37  	index int
    38  }
    39  
    40  type cell struct {
    41  	Index      int      `gae:"$id"`
    42  	EVersion   EVersion `gae:",noindex"`
    43  	Population int      `gae:",noindex"`
    44  }
    45  
    46  func (p *processor) LoadState(ctx context.Context) (State, EVersion, error) {
    47  	c, err := get(ctx, p.index)
    48  	if err != nil {
    49  		return nil, 0, err
    50  	}
    51  	return State(&c.Population), c.EVersion, nil
    52  }
    53  
    54  func (p *processor) FetchEVersion(ctx context.Context) (EVersion, error) {
    55  	c, err := get(ctx, p.index)
    56  	if err != nil {
    57  		return 0, err
    58  	}
    59  	return c.EVersion, nil
    60  }
    61  
    62  func (p *processor) SaveState(ctx context.Context, s State, e EVersion) error {
    63  	c := cell{Index: p.index, EVersion: e, Population: *(s.(*int))}
    64  	return transient.Tag.Apply(datastore.Put(ctx, &c))
    65  }
    66  
    67  func (p *processor) PrepareMutation(ctx context.Context, events Events, s State) (ts []Transition, _ Events, err error) {
    68  	ctx = logging.SetField(ctx, "index", p.index)
    69  	// Simulate variation of game of life.
    70  	population := s.(*int)
    71  	add := func(delta int) *int {
    72  		n := new(int)
    73  		*n = delta + (*population)
    74  		return n
    75  	}
    76  
    77  	if len(events) == 0 {
    78  		switch {
    79  		case *population == 0:
    80  			ts = append(ts, Transition{
    81  				SideEffectFn: func(ctx context.Context) error {
    82  					logging.Debugf(ctx, "advertised to %d to migrate", p.index+1)
    83  					return Emit(ctx, []byte{'-'}, mkRecipient(ctx, p.index+1))
    84  				},
    85  				Events:       nil,        // Don't consume any events.
    86  				TransitionTo: population, // Same state.
    87  			})
    88  		case *population < 3:
    89  			population = add(+3)
    90  			logging.Debugf(ctx, "growing +3=> %d", *population)
    91  			ts = append(ts, Transition{
    92  				SideEffectFn: nil,
    93  				Events:       nil, // Don't consume any events.
    94  				TransitionTo: population,
    95  			})
    96  		}
    97  		return
    98  	}
    99  
   100  	// Triage events.
   101  	var minus, plus Events
   102  	for _, e := range events {
   103  		if e.Value[0] == '-' {
   104  			minus = append(minus, e)
   105  		} else {
   106  			plus = append(plus, e)
   107  		}
   108  	}
   109  
   110  	if len(plus) > 0 {
   111  		// Accept at most 1 at a time.
   112  		population = add(1)
   113  		logging.Debugf(ctx, "welcoming +1 out of %d => %d", len(plus), *population)
   114  		ts = append(ts, Transition{
   115  			SideEffectFn: nil,
   116  			Events:       plus[:1], // Consume only 1 event.
   117  			TransitionTo: population,
   118  		})
   119  	}
   120  	if len(minus) > 0 {
   121  		t := Transition{
   122  			Events: minus, // Always consume all advertisements to emigrate.
   123  		}
   124  		if *population <= 1 {
   125  			logging.Debugf(ctx, "consuming %d ads", len(minus))
   126  		} else {
   127  			population = add(-1)
   128  			t.SideEffectFn = func(ctx context.Context) error {
   129  				logging.Debugf(ctx, "emigrated to %d", p.index-1)
   130  				return Emit(ctx, []byte{'+'}, mkRecipient(ctx, p.index-1))
   131  			}
   132  		}
   133  		t.TransitionTo = population
   134  		ts = append(ts, t)
   135  	}
   136  	return
   137  }
   138  
   139  func mkRecipient(ctx context.Context, id int) Recipient {
   140  	return Recipient{
   141  		Key:              datastore.MakeKey(ctx, "cell", id),
   142  		MonitoringString: fmt.Sprintf("cell-%d", id),
   143  	}
   144  }
   145  
   146  func TestEventboxWorks(t *testing.T) {
   147  	t.Parallel()
   148  
   149  	Convey("eventbox works", t, func() {
   150  		ct := cvtesting.Test{}
   151  		ctx, cancel := ct.SetUp(t)
   152  		defer cancel()
   153  
   154  		const limit = 10000
   155  
   156  		// Seed the first cell.
   157  		So(Emit(ctx, []byte{'+'}, mkRecipient(ctx, 65)), ShouldBeNil)
   158  		l, err := List(ctx, mkRecipient(ctx, 65))
   159  		So(err, ShouldBeNil)
   160  		So(l, ShouldHaveLength, 1)
   161  
   162  		ppfns, err := ProcessBatch(ctx, mkRecipient(ctx, 65), &processor{65}, limit)
   163  		So(err, ShouldBeNil)
   164  		So(ppfns, ShouldBeEmpty)
   165  		So(mustGet(ctx, 65).EVersion, ShouldEqual, 1)
   166  		So(mustGet(ctx, 65).Population, ShouldEqual, 1)
   167  		So(mustList(ctx, 65), ShouldHaveLength, 0)
   168  
   169  		// Let the cell grow without incoming events.
   170  		ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 65), &processor{65}, limit)
   171  		So(err, ShouldBeNil)
   172  		So(ppfns, ShouldBeEmpty)
   173  		So(mustGet(ctx, 65).EVersion, ShouldEqual, 2)
   174  		So(mustGet(ctx, 65).Population, ShouldEqual, 1+3)
   175  		// Can't grow any more, no change to anything.
   176  		ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 65), &processor{65}, limit)
   177  		So(err, ShouldBeNil)
   178  		So(ppfns, ShouldBeEmpty)
   179  		So(mustGet(ctx, 65).EVersion, ShouldEqual, 2)
   180  		So(mustGet(ctx, 65).Population, ShouldEqual, 1+3)
   181  
   182  		// Advertise from nearby cell, twice.
   183  		ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 64), &processor{64}, limit)
   184  		So(err, ShouldBeNil)
   185  		So(ppfns, ShouldBeEmpty)
   186  		ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 64), &processor{64}, limit)
   187  		So(err, ShouldBeNil)
   188  		So(ppfns, ShouldBeEmpty)
   189  		So(mustList(ctx, 65), ShouldHaveLength, 2)
   190  		// Emigrate, at most once.
   191  		ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 65), &processor{65}, limit)
   192  		So(err, ShouldBeNil)
   193  		So(ppfns, ShouldBeEmpty)
   194  		So(mustGet(ctx, 65).EVersion, ShouldEqual, 3)
   195  		So(mustGet(ctx, 65).Population, ShouldEqual, 4-1)
   196  		So(mustList(ctx, 65), ShouldHaveLength, 0)
   197  
   198  		// Accept immigrants.
   199  		ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 64), &processor{64}, limit)
   200  		So(err, ShouldBeNil)
   201  		So(ppfns, ShouldBeEmpty)
   202  		So(mustGet(ctx, 64).Population, ShouldEqual, +1)
   203  
   204  		// Advertise to a cell with population = 1 is a noop.
   205  		ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 63), &processor{63}, limit)
   206  		So(err, ShouldBeNil)
   207  		So(ppfns, ShouldBeEmpty)
   208  		ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 64), &processor{64}, limit)
   209  		So(err, ShouldBeNil)
   210  		So(ppfns, ShouldBeEmpty)
   211  
   212  		// Lots of events at once.
   213  		So(Emit(ctx, []byte{'+'}, mkRecipient(ctx, 49)), ShouldBeNil)
   214  		So(Emit(ctx, []byte{'+'}, mkRecipient(ctx, 49)), ShouldBeNil) // will have to wait
   215  		So(Emit(ctx, []byte{'+'}, mkRecipient(ctx, 49)), ShouldBeNil) // will have to wait
   216  		So(Emit(ctx, []byte{'-'}, mkRecipient(ctx, 49)), ShouldBeNil) // not enough people, ignored.
   217  		So(Emit(ctx, []byte{'-'}, mkRecipient(ctx, 49)), ShouldBeNil) // not enough people, ignored.
   218  		So(mustList(ctx, 49), ShouldHaveLength, 5)
   219  		ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 49), &processor{49}, limit)
   220  		So(err, ShouldBeNil)
   221  		So(ppfns, ShouldBeEmpty)
   222  		So(mustGet(ctx, 49).EVersion, ShouldEqual, 1)
   223  		So(mustGet(ctx, 49).Population, ShouldEqual, 1)
   224  		So(mustList(ctx, 49), ShouldHaveLength, 2) // 2x'+' are waiting
   225  		// Slowly welcome remaining newcomers.
   226  		ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 49), &processor{49}, limit)
   227  		So(err, ShouldBeNil)
   228  		So(ppfns, ShouldBeEmpty)
   229  		So(mustGet(ctx, 49).Population, ShouldEqual, 2)
   230  		ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 49), &processor{49}, limit)
   231  		So(err, ShouldBeNil)
   232  		So(ppfns, ShouldBeEmpty)
   233  		So(mustGet(ctx, 49).Population, ShouldEqual, 3)
   234  		// Finally, must be done.
   235  		So(mustList(ctx, 49), ShouldHaveLength, 0)
   236  	})
   237  }
   238  
   239  func get(ctx context.Context, index int) (*cell, error) {
   240  	c := &cell{Index: index}
   241  	switch err := datastore.Get(ctx, c); {
   242  	case err == datastore.ErrNoSuchEntity || err == nil:
   243  		return c, nil
   244  	default:
   245  		return nil, transient.Tag.Apply(err)
   246  	}
   247  }
   248  
   249  func mustGet(ctx context.Context, index int) *cell {
   250  	c, err := get(ctx, index)
   251  	So(err, ShouldBeNil)
   252  	return c
   253  }
   254  
   255  func mustList(ctx context.Context, index int) Events {
   256  	l, err := List(ctx, mkRecipient(ctx, index))
   257  	So(err, ShouldBeNil)
   258  	return l
   259  }
   260  
   261  func TestEventboxPostProcessFn(t *testing.T) {
   262  	t.Parallel()
   263  
   264  	Convey("eventbox", t, func() {
   265  		ct := cvtesting.Test{}
   266  		ctx, cancel := ct.SetUp(t)
   267  		defer cancel()
   268  
   269  		const limit = 10000
   270  		recipient := mkRecipient(ctx, 753)
   271  
   272  		initState := int(149)
   273  		p := &mockProc{
   274  			loadState: func(context.Context) (State, EVersion, error) {
   275  				return State(&initState), EVersion(0), nil
   276  			},
   277  			fetchEVersion: func(context.Context) (EVersion, error) {
   278  				return 0, nil
   279  			},
   280  			saveState: func(context.Context, State, EVersion) error {
   281  				return nil
   282  			},
   283  		}
   284  		Convey("Returns PostProcessFns for successful state transitions", func() {
   285  			var calledForStates []int
   286  			p.prepareMutation = func(ctx context.Context, es Events, s State) ([]Transition, Events, error) {
   287  				gotState := *(s.(*int))
   288  				return []Transition{
   289  					{
   290  						TransitionTo: gotState + 1,
   291  						PostProcessFn: func(ctx context.Context) error {
   292  							calledForStates = append(calledForStates, gotState+1)
   293  							return nil
   294  						},
   295  					},
   296  					{
   297  						TransitionTo: gotState + 2,
   298  					},
   299  					{
   300  						TransitionTo: gotState + 3,
   301  						PostProcessFn: func(ctx context.Context) error {
   302  							calledForStates = append(calledForStates, gotState+3)
   303  							return nil
   304  						},
   305  					},
   306  				}, nil, nil
   307  			}
   308  			ppfns, err := ProcessBatch(ctx, recipient, p, limit)
   309  			So(err, ShouldBeNil)
   310  			So(ppfns, ShouldHaveLength, 2)
   311  			for _, ppfn := range ppfns {
   312  				So(ppfn(ctx), ShouldBeNil)
   313  			}
   314  			So(calledForStates, ShouldResemble, []int{150, 152})
   315  		})
   316  	})
   317  }
   318  
   319  func TestEventboxFails(t *testing.T) {
   320  	t.Parallel()
   321  
   322  	Convey("eventbox fails as intended in failure cases", t, func() {
   323  		ct := cvtesting.Test{}
   324  		ctx, cancel := ct.SetUp(t)
   325  		defer cancel()
   326  
   327  		const limit = 100000
   328  		recipient := mkRecipient(ctx, 77)
   329  
   330  		So(Emit(ctx, []byte{'+'}, recipient), ShouldBeNil)
   331  		So(Emit(ctx, []byte{'-'}, recipient), ShouldBeNil)
   332  
   333  		initState := int(99)
   334  		p := &mockProc{
   335  			loadState: func(_ context.Context) (State, EVersion, error) {
   336  				return State(&initState), EVersion(0), nil
   337  			},
   338  			// since 3 other funcs are nil, calling their Upper-case counterparts will
   339  			// panic (see mockProc implementation below).
   340  		}
   341  		Convey("Mutate() failure aborts", func() {
   342  			p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) {
   343  				return nil, nil, errors.New("oops")
   344  			}
   345  			ppfns, err := ProcessBatch(ctx, recipient, p, limit)
   346  			So(err, ShouldErrLike, "oops")
   347  			So(ppfns, ShouldBeEmpty)
   348  		})
   349  
   350  		firstSideEffectCalled := false
   351  		const firstIndex = 88
   352  		secondState := initState + 1
   353  		var second SideEffectFn
   354  		p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) {
   355  			return []Transition{
   356  				{
   357  					SideEffectFn: func(ctx context.Context) error {
   358  						firstSideEffectCalled = true
   359  						return datastore.Put(ctx, &cell{Index: firstIndex})
   360  					},
   361  					Events:       es[:1],
   362  					TransitionTo: s,
   363  				},
   364  				{
   365  					SideEffectFn: second,
   366  					Events:       es[1:],
   367  					TransitionTo: State(&secondState),
   368  				},
   369  			}, nil, nil
   370  		}
   371  
   372  		Convey("Eversion must be checked", func() {
   373  			p.fetchEVersion = func(_ context.Context) (EVersion, error) {
   374  				return 0, errors.New("ev error")
   375  			}
   376  			ppfns, err := ProcessBatch(ctx, recipient, p, limit)
   377  			So(err, ShouldErrLike, "ev error")
   378  			So(ppfns, ShouldBeEmpty)
   379  			p.fetchEVersion = func(_ context.Context) (EVersion, error) {
   380  				return 1, nil
   381  			}
   382  			ppfns, err = ProcessBatch(ctx, recipient, p, limit)
   383  			So(common.DSContentionTag.In(err), ShouldBeTrue)
   384  			So(ppfns, ShouldBeEmpty)
   385  			So(firstSideEffectCalled, ShouldBeFalse)
   386  		})
   387  
   388  		p.fetchEVersion = func(_ context.Context) (EVersion, error) {
   389  			return 0, nil
   390  		}
   391  
   392  		Convey("No call to save if any Transition fails", func() {
   393  			second = func(_ context.Context) error {
   394  				return transient.Tag.Apply(errors.New("2nd failed"))
   395  			}
   396  			ppfns, err := ProcessBatch(ctx, recipient, p, limit)
   397  			So(err, ShouldErrLike, "2nd failed")
   398  			So(ppfns, ShouldBeEmpty)
   399  			So(firstSideEffectCalled, ShouldBeTrue)
   400  			// ... but w/o any effect since transaction should have been aborted
   401  			So(datastore.Get(ctx, &cell{Index: firstIndex}),
   402  				ShouldEqual, datastore.ErrNoSuchEntity)
   403  		})
   404  
   405  		second = func(_ context.Context) error { return nil }
   406  		Convey("Failed Save aborts any side effects, too", func() {
   407  			p.saveState = func(ctx context.Context, st State, ev EVersion) error {
   408  				s := *(st.(*int))
   409  				So(ev, ShouldEqual, 1)
   410  				So(s, ShouldNotEqual, initState)
   411  				So(s, ShouldEqual, secondState)
   412  				return transient.Tag.Apply(errors.New("savvvvvvvvvvvvvvvvvvvvvvvvvv hung"))
   413  			}
   414  			ppfns, err := ProcessBatch(ctx, recipient, p, limit)
   415  			So(err, ShouldErrLike, "savvvvvvvvvvvvvvvv")
   416  			So(ppfns, ShouldBeEmpty)
   417  			// ... still no side effect.
   418  			So(datastore.Get(ctx, &cell{Index: firstIndex}),
   419  				ShouldEqual, datastore.ErrNoSuchEntity)
   420  		})
   421  
   422  		// In all cases, there must still be 2 unconsumed events.
   423  		l, err := List(ctx, recipient)
   424  		So(err, ShouldBeNil)
   425  		So(l, ShouldHaveLength, 2)
   426  
   427  		// Finally, check that first side effect is real, otherwise assertions above
   428  		// might be giving false sense of correctness.
   429  		p.saveState = func(context.Context, State, EVersion) error { return nil }
   430  		ppfns, err := ProcessBatch(ctx, recipient, p, limit)
   431  		So(err, ShouldBeNil)
   432  		So(ppfns, ShouldBeEmpty)
   433  		So(mustGet(ctx, firstIndex), ShouldNotBeNil)
   434  	})
   435  }
   436  
   437  func TestEventboxNoopTransitions(t *testing.T) {
   438  	t.Parallel()
   439  
   440  	Convey("Noop Transitions are detected", t, func() {
   441  		t := Transition{}
   442  		So(t.isNoop(nil), ShouldBeTrue)
   443  		initState := int(99)
   444  		t.TransitionTo = initState
   445  		So(t.isNoop(nil), ShouldBeFalse)
   446  		So(t.isNoop(initState), ShouldBeTrue)
   447  		t.Events = Events{Event{}}
   448  		So(t.isNoop(initState), ShouldBeFalse)
   449  		t.Events = nil
   450  		t.SideEffectFn = func(context.Context) error { return nil }
   451  		So(t.isNoop(initState), ShouldBeFalse)
   452  		t.SideEffectFn = nil
   453  		t.PostProcessFn = func(context.Context) error { return nil }
   454  		So(t.isNoop(initState), ShouldBeFalse)
   455  	})
   456  
   457  	Convey("eventbox doesn't transact on nil transitions", t, func() {
   458  		ct := cvtesting.Test{}
   459  		ctx, cancel := ct.SetUp(t)
   460  		defer cancel()
   461  
   462  		const limit = 100000
   463  		recipient := mkRecipient(ctx, 77)
   464  		initState := int(99)
   465  		panicErr := errors.New("must not be transact!")
   466  
   467  		p := &mockProc{
   468  			loadState: func(_ context.Context) (State, EVersion, error) {
   469  				return State(&initState), EVersion(0), nil
   470  			},
   471  			fetchEVersion: func(_ context.Context) (EVersion, error) {
   472  				panic(panicErr)
   473  			},
   474  		}
   475  
   476  		Convey("Mutate returns no transitions", func() {
   477  			p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) {
   478  				return nil, nil, nil
   479  			}
   480  			ppfns, err := ProcessBatch(ctx, recipient, p, limit)
   481  			So(err, ShouldBeNil)
   482  			So(ppfns, ShouldBeEmpty)
   483  		})
   484  		Convey("Mutate returns no transitions, but some semantic garbage is still cleaned up", func() {
   485  			So(Emit(ctx, []byte("msg"), recipient), ShouldBeNil)
   486  			So(Emit(ctx, []byte("msg"), recipient), ShouldBeNil)
   487  			p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) {
   488  				return nil, es[:1], nil
   489  			}
   490  			ppfns, err := ProcessBatch(ctx, recipient, p, limit)
   491  			So(err, ShouldBeNil)
   492  			So(ppfns, ShouldBeEmpty)
   493  			l, err := List(ctx, recipient)
   494  			So(err, ShouldBeNil)
   495  			So(l, ShouldHaveLength, 1)
   496  			So(l[0].Value, ShouldResemble, []byte("msg"))
   497  		})
   498  		Convey("Garbage is cleaned up even if Mutate also returns error", func() {
   499  			So(Emit(ctx, []byte("msg"), recipient), ShouldBeNil)
   500  			So(Emit(ctx, []byte("msg"), recipient), ShouldBeNil)
   501  			p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) {
   502  				return nil, es[:1], errors.New("boom")
   503  			}
   504  			_, err := ProcessBatch(ctx, recipient, p, limit)
   505  			So(err, ShouldErrLike, "boom")
   506  			l, err := List(ctx, recipient)
   507  			So(err, ShouldBeNil)
   508  			So(l, ShouldHaveLength, 1)
   509  			So(l[0].Value, ShouldResemble, []byte("msg"))
   510  		})
   511  		Convey("Mutate returns empty slice of transitions", func() {
   512  			p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) {
   513  				return []Transition{}, nil, nil
   514  			}
   515  			ppfns, err := ProcessBatch(ctx, recipient, p, limit)
   516  			So(err, ShouldBeNil)
   517  			So(ppfns, ShouldBeEmpty)
   518  		})
   519  		Convey("Mutate returns noop transitions only", func() {
   520  			p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) {
   521  				return []Transition{
   522  					{TransitionTo: s},
   523  				}, nil, nil
   524  			}
   525  			ppfns, err := ProcessBatch(ctx, recipient, p, limit)
   526  			So(err, ShouldBeNil)
   527  			So(ppfns, ShouldBeEmpty)
   528  		})
   529  
   530  		Convey("Test's own sanity check that fetchEVersion is called and panics", func() {
   531  			p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) {
   532  				return []Transition{
   533  					{TransitionTo: new(int)},
   534  				}, nil, nil
   535  			}
   536  			So(func() { ProcessBatch(ctx, recipient, p, limit) }, ShouldPanicLike, panicErr)
   537  		})
   538  	})
   539  }
   540  
   541  type mockProc struct {
   542  	loadState       func(_ context.Context) (State, EVersion, error)
   543  	prepareMutation func(_ context.Context, _ Events, _ State) ([]Transition, Events, error)
   544  	fetchEVersion   func(_ context.Context) (EVersion, error)
   545  	saveState       func(_ context.Context, _ State, _ EVersion) error
   546  }
   547  
   548  func (m *mockProc) LoadState(ctx context.Context) (State, EVersion, error) {
   549  	return m.loadState(ctx)
   550  }
   551  func (m *mockProc) PrepareMutation(ctx context.Context, e Events, s State) ([]Transition, Events, error) {
   552  	return m.prepareMutation(ctx, e, s)
   553  }
   554  func (m *mockProc) FetchEVersion(ctx context.Context) (EVersion, error) {
   555  	return m.fetchEVersion(ctx)
   556  }
   557  func (m *mockProc) SaveState(ctx context.Context, s State, e EVersion) error {
   558  	return m.saveState(ctx, s, e)
   559  }
   560  
   561  func PrepareMutation(t *testing.T) {
   562  	t.Parallel()
   563  
   564  	Convey("Chain of SideEffectFn works", t, func() {
   565  		ctx := context.Background()
   566  		var ops []string
   567  		f1 := func(context.Context) error {
   568  			ops = append(ops, "f1")
   569  			return nil
   570  		}
   571  		f2 := func(context.Context) error {
   572  			ops = append(ops, "f2")
   573  			return nil
   574  		}
   575  		breakChain := errors.New("break")
   576  		ferr := func(context.Context) error {
   577  			ops = append(ops, "ferr")
   578  			return breakChain
   579  		}
   580  		Convey("all nils chain to nil", func() {
   581  			So(Chain(), ShouldBeNil)
   582  			So(Chain(nil), ShouldBeNil)
   583  			So(Chain(nil, nil), ShouldBeNil)
   584  		})
   585  		Convey("order is respected", func() {
   586  			So(Chain(nil, f2, nil, f1, f2, f1, nil)(ctx), ShouldBeNil)
   587  			So(ops, ShouldResemble, []string{"f2", "f1", "f2", "f1"})
   588  		})
   589  		Convey("error aborts", func() {
   590  			So(Chain(f1, nil, ferr, f2)(ctx), ShouldErrLike, breakChain)
   591  			So(ops, ShouldResemble, []string{"f1", "ferr"})
   592  		})
   593  	})
   594  }