github.com/pachyderm/pachyderm@v1.13.4/src/server/worker/pipeline/transform/chain/chain_test.go (about)

     1  package chain
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  	"testing"
     7  	"time"
     8  
     9  	"golang.org/x/sync/errgroup"
    10  
    11  	"github.com/pachyderm/pachyderm/src/client/pfs"
    12  	"github.com/pachyderm/pachyderm/src/client/pkg/errors"
    13  	"github.com/pachyderm/pachyderm/src/client/pkg/require"
    14  	"github.com/pachyderm/pachyderm/src/server/worker/common"
    15  	"github.com/pachyderm/pachyderm/src/server/worker/datum"
    16  )
    17  
    18  type testHasher struct{}
    19  
    20  func (th *testHasher) Hash(inputs []*common.Input) string {
    21  	return common.HashDatum("", "", inputs)
    22  }
    23  
    24  func makeIndex() map[string]string {
    25  	hasher := &testHasher{}
    26  	result := make(map[string]string)
    27  	names := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
    28  	for _, name := range names {
    29  		hash := hasher.Hash(datumToInputs(name))
    30  		result[hash] = name
    31  	}
    32  	return result
    33  }
    34  
    35  var datumIndex = makeIndex()
    36  
    37  /* Useful functions for debugging, commented out to appease lint
    38  
    39  func printState(jc *jobChain) {
    40  	jc.mutex.Lock()
    41  	defer jc.mutex.Unlock()
    42  
    43  	for i, jdi := range jc.jobs {
    44  		flags := ""
    45  		if jdi.additiveOnly {
    46  			flags += " additive"
    47  		}
    48  		if jdi.finished {
    49  			if jdi.allDatums != nil {
    50  				flags += " succeeded"
    51  			} else {
    52  				flags += " failed"
    53  			}
    54  		}
    55  		fmt.Printf("Job %d:%s\n", i, flags)
    56  
    57  		ancestors := []int{}
    58  		for _, ancestor := range jdi.ancestors {
    59  			index, err := jc.indexOf(ancestor.data)
    60  			if err != nil {
    61  				index = -1
    62  			}
    63  			ancestors = append(ancestors, index)
    64  		}
    65  		fmt.Printf("ancestors: %v\n", ancestors)
    66  
    67  		printDatumSet(jc, "allDatums", jdi.allDatums)
    68  		printDatumSet(jc, "yielding", jdi.yielding)
    69  		printDatumSet(jc, "yielded", jdi.yielded)
    70  	}
    71  }
    72  
    73  func printDatumSet(jc *jobChain, name string, set DatumSet) {
    74  	arr := []string{}
    75  	for hash, count := range set {
    76  		name := datumIndex[hash]
    77  		if name == "" {
    78  			name = "unknown"
    79  		}
    80  		arr = append(arr, fmt.Sprintf("%s: %d", name, count))
    81  	}
    82  	sort.Strings(arr)
    83  	fmt.Printf(" %s (%d): %v\n", name, len(set), arr)
    84  }
    85  */
    86  
    87  type testIterator struct {
    88  	index  int
    89  	inputs [][]*common.Input
    90  }
    91  
    92  func (ti *testIterator) Reset() {
    93  	ti.index = -1
    94  }
    95  
    96  func (ti *testIterator) Len() int {
    97  	return len(ti.inputs)
    98  }
    99  
   100  func (ti *testIterator) Next() bool {
   101  	if ti.index < len(ti.inputs) {
   102  		ti.index++
   103  	}
   104  
   105  	return ti.index < len(ti.inputs)
   106  }
   107  
   108  func (ti *testIterator) Datum() []*common.Input {
   109  	if ti.index >= len(ti.inputs) {
   110  		return nil
   111  	}
   112  	return ti.inputs[ti.index]
   113  }
   114  
   115  func (ti *testIterator) DatumN(n int) []*common.Input {
   116  	return ti.inputs[n]
   117  }
   118  
   119  // Convert a test-friendly string to a real fake inputs array
   120  func datumToInputs(name string) []*common.Input {
   121  	return []*common.Input{{
   122  		Name: "inputRepo",
   123  		FileInfo: &pfs.FileInfo{
   124  			File: &pfs.File{Path: name},
   125  			Hash: []byte(name),
   126  		},
   127  	}}
   128  }
   129  
   130  func inputsToDatum(inputs []*common.Input) (string, error) {
   131  	if len(inputs) != 1 {
   132  		return "", errors.New("should only have 1 input for test datums")
   133  	}
   134  	return inputs[0].FileInfo.File.Path, nil
   135  }
   136  
   137  func newTestChain(t *testing.T, datums []string) JobChain {
   138  	hasher := &testHasher{}
   139  	baseDatums := datumsToSet(datums)
   140  	return NewJobChain(hasher, baseDatums)
   141  }
   142  
   143  func datumsToInputs(datums []string) [][]*common.Input {
   144  	inputs := [][]*common.Input{}
   145  	for _, datum := range datums {
   146  		inputs = append(inputs, datumToInputs(datum))
   147  	}
   148  	return inputs
   149  }
   150  
   151  func datumsToSet(datums []string) DatumSet {
   152  	hasher := &testHasher{}
   153  	result := make(DatumSet)
   154  	for _, datum := range datums {
   155  		result[hasher.Hash(datumToInputs(datum))]++
   156  	}
   157  	return result
   158  }
   159  
   160  func setToDatums(t *testing.T, datumSet DatumSet) []string {
   161  	result := []string{}
   162  	for hash, count := range datumSet {
   163  		name := datumIndex[hash]
   164  		require.NotEqual(t, "", name)
   165  		for i := int64(0); i < count; i++ {
   166  			result = append(result, name)
   167  		}
   168  	}
   169  	return result
   170  }
   171  
   172  func newTestIterator(datums []string) datum.Iterator {
   173  	return &testIterator{inputs: datumsToInputs(datums)}
   174  }
   175  
   176  type testJob struct {
   177  	dit datum.Iterator
   178  }
   179  
   180  func newTestJob(datums []string) JobData {
   181  	return &testJob{dit: newTestIterator(datums)}
   182  }
   183  
   184  func (tj *testJob) Iterator() (datum.Iterator, error) {
   185  	return tj.dit, nil
   186  }
   187  
   188  func requireChainEmpty(t *testing.T, chain JobChain, expectedBaseDatums []string) {
   189  	jc := chain.(*jobChain)
   190  	require.Equal(t, 1, len(jc.jobs))
   191  	require.ElementsEqual(t, expectedBaseDatums, setToDatums(t, jc.jobs[0].allDatums))
   192  }
   193  
   194  func requireIteratorContents(t *testing.T, jdi JobDatumIterator, expected []string) {
   195  	count, err := jdi.NextBatch(context.Background())
   196  	require.NoError(t, err)
   197  	require.Equal(t, int64(len(expected)), count)
   198  
   199  	found := []string{}
   200  	for range expected {
   201  		inputs, _ := jdi.NextDatum()
   202  		datum, err := inputsToDatum(inputs)
   203  		require.NoError(t, err)
   204  		found = append(found, datum)
   205  	}
   206  	require.ElementsEqual(t, expected, found)
   207  	requireIteratorDone(t, jdi)
   208  }
   209  
   210  func requireIteratorDone(t *testing.T, jdi JobDatumIterator) {
   211  	count, err := jdi.NextBatch(context.Background())
   212  	require.NoError(t, err)
   213  	require.Equal(t, int64(0), count)
   214  }
   215  
   216  func TestEmptyBase(t *testing.T) {
   217  	jobDatums := []string{"a", "b"}
   218  	chain := newTestChain(t, []string{})
   219  	job := newTestJob(jobDatums)
   220  	jdi, err := chain.Start(job)
   221  	require.NoError(t, err)
   222  	requireIteratorContents(t, jdi, jobDatums)
   223  
   224  	require.NoError(t, chain.Succeed(job))
   225  	requireChainEmpty(t, chain, jobDatums)
   226  }
   227  
   228  func TestAdditiveOnBase(t *testing.T) {
   229  	jobDatums := []string{"a", "b", "c"}
   230  	chain := newTestChain(t, []string{"a"})
   231  	job := newTestJob(jobDatums)
   232  	jdi, err := chain.Start(job)
   233  	require.NoError(t, err)
   234  	requireIteratorContents(t, jdi, []string{"b", "c"})
   235  
   236  	require.NoError(t, chain.Succeed(job))
   237  	requireChainEmpty(t, chain, jobDatums)
   238  }
   239  
   240  func TestSubtractiveOnBase(t *testing.T) {
   241  	jobDatums := []string{"a", "c"}
   242  	chain := newTestChain(t, []string{"a", "b", "c"})
   243  	job := newTestJob(jobDatums)
   244  	jdi, err := chain.Start(job)
   245  	require.NoError(t, err)
   246  	requireIteratorContents(t, jdi, jobDatums)
   247  
   248  	require.NoError(t, chain.Succeed(job))
   249  	requireChainEmpty(t, chain, jobDatums)
   250  }
   251  
   252  func TestAdditiveSubtractiveOnBase(t *testing.T) {
   253  	jobDatums := []string{"b", "c", "d", "e"}
   254  	chain := newTestChain(t, []string{"a", "b", "c"})
   255  	job := newTestJob(jobDatums)
   256  	jdi, err := chain.Start(job)
   257  	require.NoError(t, err)
   258  	requireIteratorContents(t, jdi, jobDatums)
   259  
   260  	require.NoError(t, chain.Succeed(job))
   261  	requireChainEmpty(t, chain, jobDatums)
   262  }
   263  
   264  // Read from a channel until we have the expected datums, then verify they
   265  // are correct, then make sure the channel doesn't have anything else.
   266  func requireDatums(t *testing.T, datumChan <-chan string, expected []string) {
   267  	// Recvs should be near-instant, but set a decently long timeout to avoid flakiness
   268  	actual := []string{}
   269  loop:
   270  	for range expected {
   271  		select {
   272  		case x, ok := <-datumChan:
   273  			if !ok {
   274  				require.ElementsEqual(t, expected, actual)
   275  			}
   276  			actual = append(actual, x)
   277  		case <-time.After(time.Second):
   278  			break loop
   279  		}
   280  	}
   281  	require.ElementsEqual(t, expected, actual)
   282  
   283  	select {
   284  	case x, ok := <-datumChan:
   285  		require.False(t, ok, "datum channel contains extra datum: %s", x)
   286  	default:
   287  	}
   288  }
   289  
   290  func requireChannelClosed(t *testing.T, c <-chan string) {
   291  	select {
   292  	case x, ok := <-c:
   293  		require.False(t, ok, "datum channel should be closed, but found extra datum: %s", x)
   294  	case <-time.After(time.Second):
   295  		require.True(t, false, "datum channel should be closed, but it is blocked")
   296  	}
   297  }
   298  
   299  func requireChannelBlocked(t *testing.T, c <-chan string) {
   300  	select {
   301  	case x, ok := <-c:
   302  		require.True(t, ok, "datum channel should be blocked, but it is closed")
   303  		require.True(t, false, "datum channel should be blocked, but it contains datum: %s", x)
   304  	default:
   305  	}
   306  }
   307  
   308  func superviseTestJobWithError(
   309  	ctx context.Context,
   310  	eg *errgroup.Group,
   311  	jdi JobDatumIterator,
   312  	expectedErr string,
   313  ) <-chan string {
   314  	datumsChan := make(chan string)
   315  	eg.Go(func() (retErr error) {
   316  		defer func() {
   317  			if retErr != nil && expectedErr != "" && strings.Contains(retErr.Error(), expectedErr) {
   318  				retErr = nil
   319  			}
   320  		}()
   321  
   322  		defer close(datumsChan)
   323  		for {
   324  			count, err := jdi.NextBatch(ctx)
   325  			if err != nil {
   326  				return err
   327  			}
   328  			if count == 0 {
   329  				return nil
   330  			}
   331  
   332  			for i := int64(0); i < count; i++ {
   333  				inputs, _ := jdi.NextDatum()
   334  				datum, err := inputsToDatum(inputs)
   335  				if err != nil {
   336  					return err
   337  				}
   338  
   339  				datumsChan <- datum
   340  			}
   341  		}
   342  	})
   343  
   344  	return datumsChan
   345  }
   346  
   347  func superviseTestJob(ctx context.Context, eg *errgroup.Group, jdi JobDatumIterator) <-chan string {
   348  	return superviseTestJobWithError(ctx, eg, jdi, "")
   349  }
   350  
   351  // Job 1: ABCD   -> 1. Succeed
   352  // Job 2:   CDEF  -> 2. Succeed
   353  // Job 3: AB DE GH -> 3. Succeed
   354  func TestSuccess(t *testing.T) {
   355  	chain := newTestChain(t, []string{})
   356  	job1 := newTestJob([]string{"a", "b", "c", "d"})
   357  	job2 := newTestJob([]string{"c", "d", "e", "f"})
   358  	job3 := newTestJob([]string{"a", "b", "d", "e", "g", "h"})
   359  
   360  	eg, ctx := errgroup.WithContext(context.Background())
   361  
   362  	jdi1, err := chain.Start(job1)
   363  	require.NoError(t, err)
   364  	datums1 := superviseTestJob(ctx, eg, jdi1)
   365  
   366  	jdi2, err := chain.Start(job2)
   367  	require.NoError(t, err)
   368  	datums2 := superviseTestJob(ctx, eg, jdi2)
   369  
   370  	jdi3, err := chain.Start(job3)
   371  	require.NoError(t, err)
   372  	datums3 := superviseTestJob(ctx, eg, jdi3)
   373  
   374  	requireDatums(t, datums1, []string{"a", "b", "c", "d"})
   375  	requireDatums(t, datums2, []string{"e", "f"})
   376  	requireDatums(t, datums3, []string{"g", "h"})
   377  	requireChannelClosed(t, datums1)
   378  	requireChannelBlocked(t, datums2)
   379  	requireChannelBlocked(t, datums3)
   380  
   381  	require.NoError(t, chain.Succeed(job1))
   382  	requireDatums(t, datums2, []string{"c", "d"})
   383  	requireDatums(t, datums3, []string{"a", "b"})
   384  	requireChannelClosed(t, datums2)
   385  
   386  	require.NoError(t, chain.Succeed(job2))
   387  	requireDatums(t, datums3, []string{"d", "e"})
   388  	requireChannelClosed(t, datums3)
   389  
   390  	require.NoError(t, chain.Succeed(job3))
   391  	require.NoError(t, eg.Wait())
   392  
   393  	requireChainEmpty(t, chain, []string{"a", "b", "d", "e", "g", "h"})
   394  }
   395  
   396  // Job 1: ABCD   -> 1. Fail
   397  // Job 2:   CDEF  -> 2. Fail
   398  // Job 3: AB DE GH -> 3. Succeed
   399  func TestFail(t *testing.T) {
   400  	chain := newTestChain(t, []string{})
   401  	job1 := newTestJob([]string{"a", "b", "c", "d"})
   402  	job2 := newTestJob([]string{"c", "d", "e", "f"})
   403  	job3 := newTestJob([]string{"a", "b", "d", "e", "g", "h"})
   404  
   405  	eg, ctx := errgroup.WithContext(context.Background())
   406  
   407  	jdi1, err := chain.Start(job1)
   408  	require.NoError(t, err)
   409  	datums1 := superviseTestJob(ctx, eg, jdi1)
   410  
   411  	jdi2, err := chain.Start(job2)
   412  	require.NoError(t, err)
   413  	datums2 := superviseTestJob(ctx, eg, jdi2)
   414  
   415  	jdi3, err := chain.Start(job3)
   416  	require.NoError(t, err)
   417  	datums3 := superviseTestJob(ctx, eg, jdi3)
   418  
   419  	requireDatums(t, datums1, []string{"a", "b", "c", "d"})
   420  	requireDatums(t, datums2, []string{"e", "f"})
   421  	requireDatums(t, datums3, []string{"g", "h"})
   422  	requireChannelClosed(t, datums1)
   423  	requireChannelBlocked(t, datums2)
   424  	requireChannelBlocked(t, datums3)
   425  
   426  	require.NoError(t, chain.Fail(job1))
   427  	requireDatums(t, datums2, []string{"c", "d"})
   428  	requireDatums(t, datums3, []string{"a", "b"})
   429  	requireChannelClosed(t, datums2)
   430  
   431  	require.NoError(t, chain.Fail(job2))
   432  	requireDatums(t, datums3, []string{"d", "e"})
   433  	requireChannelClosed(t, datums3)
   434  
   435  	require.NoError(t, chain.Succeed(job3))
   436  	require.NoError(t, eg.Wait())
   437  
   438  	requireChainEmpty(t, chain, []string{"a", "b", "d", "e", "g", "h"})
   439  }
   440  
   441  // Job 1: AB   -> 1. Succeed
   442  // Job 2: ABC  -> 2. Succeed
   443  func TestAdditiveSuccess(t *testing.T) {
   444  	chain := newTestChain(t, []string{})
   445  	job1 := newTestJob([]string{"a", "b"})
   446  	job2 := newTestJob([]string{"a", "b", "c"})
   447  
   448  	eg, ctx := errgroup.WithContext(context.Background())
   449  
   450  	jdi1, err := chain.Start(job1)
   451  	require.NoError(t, err)
   452  	datums1 := superviseTestJob(ctx, eg, jdi1)
   453  
   454  	jdi2, err := chain.Start(job2)
   455  	require.NoError(t, err)
   456  	datums2 := superviseTestJob(ctx, eg, jdi2)
   457  
   458  	requireDatums(t, datums1, []string{"a", "b"})
   459  	requireDatums(t, datums2, []string{"c"})
   460  	requireChannelClosed(t, datums1)
   461  	requireChannelBlocked(t, datums2)
   462  
   463  	require.NoError(t, chain.Succeed(job1))
   464  	requireChannelClosed(t, datums2)
   465  
   466  	require.NoError(t, chain.Succeed(job2))
   467  	require.NoError(t, eg.Wait())
   468  
   469  	requireChainEmpty(t, chain, []string{"a", "b", "c"})
   470  }
   471  
   472  // Job 1: AB   -> 1. Fail
   473  // Job 2: ABC  -> 2. Succeed
   474  func TestAdditiveFail(t *testing.T) {
   475  	chain := newTestChain(t, []string{})
   476  	job1 := newTestJob([]string{"a", "b"})
   477  	job2 := newTestJob([]string{"a", "b", "c"})
   478  
   479  	eg, ctx := errgroup.WithContext(context.Background())
   480  
   481  	jdi1, err := chain.Start(job1)
   482  	require.NoError(t, err)
   483  	datums1 := superviseTestJob(ctx, eg, jdi1)
   484  
   485  	jdi2, err := chain.Start(job2)
   486  	require.NoError(t, err)
   487  	datums2 := superviseTestJob(ctx, eg, jdi2)
   488  
   489  	requireDatums(t, datums1, []string{"a", "b"})
   490  	requireDatums(t, datums2, []string{"c"})
   491  	requireChannelClosed(t, datums1)
   492  	requireChannelBlocked(t, datums2)
   493  
   494  	require.NoError(t, chain.Fail(job1))
   495  	requireDatums(t, datums2, []string{"a", "b"})
   496  	requireChannelClosed(t, datums2)
   497  
   498  	require.NoError(t, chain.Succeed(job2))
   499  	require.NoError(t, eg.Wait())
   500  
   501  	requireChainEmpty(t, chain, []string{"a", "b", "c"})
   502  }
   503  
   504  // Job 1: AB   -> 1. Succeed
   505  // Job 2:  BC  -> 2. Succeed
   506  // Job 3:  BCD -> 3. Succeed
   507  func TestCascadeSuccess(t *testing.T) {
   508  	chain := newTestChain(t, []string{})
   509  	job1 := newTestJob([]string{"a", "b"})
   510  	job2 := newTestJob([]string{"b", "c"})
   511  	job3 := newTestJob([]string{"b", "c", "d"})
   512  
   513  	eg, ctx := errgroup.WithContext(context.Background())
   514  
   515  	jdi1, err := chain.Start(job1)
   516  	require.NoError(t, err)
   517  	datums1 := superviseTestJob(ctx, eg, jdi1)
   518  
   519  	jdi2, err := chain.Start(job2)
   520  	require.NoError(t, err)
   521  	datums2 := superviseTestJob(ctx, eg, jdi2)
   522  
   523  	jdi3, err := chain.Start(job3)
   524  	require.NoError(t, err)
   525  	datums3 := superviseTestJob(ctx, eg, jdi3)
   526  
   527  	requireDatums(t, datums1, []string{"a", "b"})
   528  	requireDatums(t, datums2, []string{"c"})
   529  	requireDatums(t, datums3, []string{"d"})
   530  	requireChannelClosed(t, datums1)
   531  	requireChannelBlocked(t, datums2)
   532  	requireChannelBlocked(t, datums3)
   533  
   534  	require.NoError(t, chain.Succeed(job1))
   535  	requireDatums(t, datums2, []string{"b"})
   536  	requireChannelClosed(t, datums2)
   537  	requireChannelBlocked(t, datums3)
   538  
   539  	require.NoError(t, chain.Succeed(job2))
   540  	requireChannelClosed(t, datums3)
   541  
   542  	require.NoError(t, chain.Succeed(job3))
   543  	require.NoError(t, eg.Wait())
   544  
   545  	requireChainEmpty(t, chain, []string{"b", "c", "d"})
   546  }
   547  
   548  // Job 1: AB   -> 1. Succeed
   549  // Job 2: ABC  -> 2. Fail
   550  // Job 3: ABCD -> 3. Succeed
   551  func TestCascadeFail(t *testing.T) {
   552  	chain := newTestChain(t, []string{})
   553  	job1 := newTestJob([]string{"a", "b"})
   554  	job2 := newTestJob([]string{"a", "b", "c"})
   555  	job3 := newTestJob([]string{"a", "b", "c", "d"})
   556  
   557  	eg, ctx := errgroup.WithContext(context.Background())
   558  
   559  	jdi1, err := chain.Start(job1)
   560  	require.NoError(t, err)
   561  	datums1 := superviseTestJob(ctx, eg, jdi1)
   562  
   563  	jdi2, err := chain.Start(job2)
   564  	require.NoError(t, err)
   565  	datums2 := superviseTestJob(ctx, eg, jdi2)
   566  
   567  	jdi3, err := chain.Start(job3)
   568  	require.NoError(t, err)
   569  	datums3 := superviseTestJob(ctx, eg, jdi3)
   570  
   571  	requireDatums(t, datums1, []string{"a", "b"})
   572  	requireDatums(t, datums2, []string{"c"})
   573  	requireDatums(t, datums3, []string{"d"})
   574  	requireChannelClosed(t, datums1)
   575  	requireChannelBlocked(t, datums2)
   576  	requireChannelBlocked(t, datums3)
   577  
   578  	require.NoError(t, chain.Succeed(job1))
   579  	requireChannelClosed(t, datums2)
   580  	requireChannelBlocked(t, datums3)
   581  
   582  	require.NoError(t, chain.Fail(job2))
   583  	requireDatums(t, datums3, []string{"c"})
   584  	requireChannelClosed(t, datums3)
   585  
   586  	require.NoError(t, chain.Succeed(job3))
   587  	require.NoError(t, eg.Wait())
   588  
   589  	requireChainEmpty(t, chain, []string{"a", "b", "c", "d"})
   590  }
   591  
   592  // Job 1: AB   -> 2. Succeed
   593  // Job 2:  BC  -> 1. Fail
   594  // Job 3:  BCD -> 3. Succeed
   595  func TestSplitFail(t *testing.T) {
   596  	chain := newTestChain(t, []string{})
   597  	job1 := newTestJob([]string{"a", "b"})
   598  	job2 := newTestJob([]string{"b", "c"})
   599  	job3 := newTestJob([]string{"b", "c", "d"})
   600  
   601  	eg, ctx := errgroup.WithContext(context.Background())
   602  
   603  	jdi1, err := chain.Start(job1)
   604  	require.NoError(t, err)
   605  	datums1 := superviseTestJob(ctx, eg, jdi1)
   606  
   607  	jdi2, err := chain.Start(job2)
   608  	require.NoError(t, err)
   609  	datums2 := superviseTestJobWithError(ctx, eg, jdi2, "job failed")
   610  
   611  	jdi3, err := chain.Start(job3)
   612  	require.NoError(t, err)
   613  	datums3 := superviseTestJob(ctx, eg, jdi3)
   614  
   615  	requireDatums(t, datums1, []string{"a", "b"})
   616  	requireDatums(t, datums2, []string{"c"})
   617  	requireDatums(t, datums3, []string{"d"})
   618  	requireChannelClosed(t, datums1)
   619  	requireChannelBlocked(t, datums2)
   620  	requireChannelBlocked(t, datums3)
   621  
   622  	require.NoError(t, chain.Fail(job2))
   623  	requireDatums(t, datums3, []string{"c"})
   624  	//requireChannelClosed(t, datums2)
   625  	requireChannelBlocked(t, datums3)
   626  
   627  	require.NoError(t, chain.Succeed(job1))
   628  	requireDatums(t, datums3, []string{"b"})
   629  	requireChannelClosed(t, datums3)
   630  
   631  	require.NoError(t, chain.Succeed(job3))
   632  	require.NoError(t, eg.Wait())
   633  
   634  	requireChainEmpty(t, chain, []string{"b", "c", "d"})
   635  }
   636  
   637  // Job 1: AB   -> 1. Succeed (A and B recovered)
   638  // Job 2: ABC  -> 2. Succeed (A and C recovered)
   639  // Job 3: ABCD -> 3. Succeed
   640  func TestRecoveredDatums(t *testing.T) {
   641  	chain := newTestChain(t, []string{})
   642  	job1 := newTestJob([]string{"a", "b"})
   643  	job2 := newTestJob([]string{"a", "b", "c"})
   644  	job3 := newTestJob([]string{"a", "b", "c", "d"})
   645  
   646  	eg, ctx := errgroup.WithContext(context.Background())
   647  
   648  	jdi1, err := chain.Start(job1)
   649  	require.NoError(t, err)
   650  	datums1 := superviseTestJob(ctx, eg, jdi1)
   651  
   652  	jdi2, err := chain.Start(job2)
   653  	require.NoError(t, err)
   654  	datums2 := superviseTestJob(ctx, eg, jdi2)
   655  
   656  	jdi3, err := chain.Start(job3)
   657  	require.NoError(t, err)
   658  	datums3 := superviseTestJob(ctx, eg, jdi3)
   659  
   660  	requireDatums(t, datums1, []string{"a", "b"})
   661  	requireDatums(t, datums2, []string{"c"})
   662  	requireDatums(t, datums3, []string{"d"})
   663  	requireChannelClosed(t, datums1)
   664  	requireChannelBlocked(t, datums2)
   665  	requireChannelBlocked(t, datums3)
   666  
   667  	require.NoError(t, chain.RecoveredDatums(job1, datumsToSet([]string{"a", "b"})))
   668  	require.NoError(t, chain.Succeed(job1))
   669  	requireDatums(t, datums2, []string{"a", "b"})
   670  	requireChannelClosed(t, datums2)
   671  	requireChannelBlocked(t, datums3)
   672  
   673  	require.NoError(t, chain.RecoveredDatums(job2, datumsToSet([]string{"a", "c"})))
   674  	require.NoError(t, chain.Succeed(job2))
   675  	requireDatums(t, datums3, []string{"a", "c"})
   676  	requireChannelClosed(t, datums3)
   677  
   678  	require.NoError(t, chain.RecoveredDatums(job3, datumsToSet([]string{"c", "d"})))
   679  	require.NoError(t, chain.Succeed(job3))
   680  	require.NoError(t, eg.Wait())
   681  
   682  	requireChainEmpty(t, chain, []string{"a", "b"})
   683  }
   684  
   685  func TestEarlySuccess(t *testing.T) {
   686  	chain := newTestChain(t, []string{})
   687  	job1 := newTestJob([]string{"a", "b"})
   688  
   689  	_, err := chain.Start(job1)
   690  	require.NoError(t, err)
   691  
   692  	require.YesError(t, chain.Succeed(job1), "items remaining")
   693  }
   694  
   695  func TestEarlyFail(t *testing.T) {
   696  	chain := newTestChain(t, []string{"e", "f"})
   697  	job := newTestJob([]string{"a", "b"})
   698  
   699  	_, err := chain.Start(job)
   700  	require.NoError(t, err)
   701  
   702  	require.NoError(t, chain.Fail(job))
   703  	requireChainEmpty(t, chain, []string{"e", "f"})
   704  }
   705  
   706  func TestRepeatedDatumAdditiveSubtractiveOnBase(t *testing.T) {
   707  	jobDatums := []string{"c", "c", "b"}
   708  	chain := newTestChain(t, []string{"a", "b", "a", "b", "c"})
   709  	job := newTestJob(jobDatums)
   710  
   711  	jdi, err := chain.Start(job)
   712  	require.NoError(t, err)
   713  
   714  	requireIteratorContents(t, jdi, jobDatums)
   715  	require.NoError(t, chain.Succeed(job))
   716  
   717  	requireChainEmpty(t, chain, jobDatums)
   718  }
   719  
   720  func TestRepeatedDatumSubtractiveOnBase(t *testing.T) {
   721  	jobDatums := []string{"a", "a"}
   722  	chain := newTestChain(t, []string{"a", "b", "a", "b", "c"})
   723  	job := newTestJob(jobDatums)
   724  
   725  	jdi, err := chain.Start(job)
   726  	require.NoError(t, err)
   727  
   728  	requireIteratorContents(t, jdi, jobDatums)
   729  	require.NoError(t, chain.Succeed(job))
   730  
   731  	requireChainEmpty(t, chain, jobDatums)
   732  }
   733  
   734  func TestRepeatedDatumAdditiveOnBase(t *testing.T) {
   735  	baseDatums := []string{"a", "b", "a", "b", "c"}
   736  	newDatums := []string{"a", "c", "d"}
   737  	jobDatums := append([]string{}, baseDatums...)
   738  	jobDatums = append(jobDatums, newDatums...)
   739  
   740  	chain := newTestChain(t, baseDatums)
   741  	job := newTestJob(jobDatums)
   742  
   743  	jdi, err := chain.Start(job)
   744  	require.NoError(t, err)
   745  
   746  	requireIteratorContents(t, jdi, newDatums)
   747  	require.NoError(t, chain.Succeed(job))
   748  
   749  	requireChainEmpty(t, chain, jobDatums)
   750  }
   751  
   752  func TestRepeatedDatumWithoutBase(t *testing.T) {
   753  	jobDatums := []string{"a", "b", "c", "a", "b", "a"}
   754  	chain := newTestChain(t, []string{})
   755  	job := newTestJob(jobDatums)
   756  
   757  	jdi, err := chain.Start(job)
   758  	require.NoError(t, err)
   759  
   760  	requireIteratorContents(t, jdi, jobDatums)
   761  	require.NoError(t, chain.Succeed(job))
   762  
   763  	requireChainEmpty(t, chain, jobDatums)
   764  }
   765  
   766  func TestNoSkipSuccess(t *testing.T) {
   767  	chain := NewNoSkipJobChain(&testHasher{})
   768  	job1 := newTestJob([]string{"a", "b", "c", "d"})
   769  	job2 := newTestJob([]string{"b", "c", "d", "e"})
   770  	job3 := newTestJob([]string{"a", "f", "g"})
   771  	job4 := newTestJob([]string{"h", "i"})
   772  
   773  	eg, ctx := errgroup.WithContext(context.Background())
   774  
   775  	jdi1, err := chain.Start(job1)
   776  	require.NoError(t, err)
   777  	datums1 := superviseTestJob(ctx, eg, jdi1)
   778  
   779  	jdi2, err := chain.Start(job2)
   780  	require.NoError(t, err)
   781  	datums2 := superviseTestJob(ctx, eg, jdi2)
   782  
   783  	jdi3, err := chain.Start(job3)
   784  	require.NoError(t, err)
   785  	datums3 := superviseTestJob(ctx, eg, jdi3)
   786  
   787  	jdi4, err := chain.Start(job4)
   788  	require.NoError(t, err)
   789  	datums4 := superviseTestJob(ctx, eg, jdi4)
   790  
   791  	requireDatums(t, datums1, []string{"a", "b", "c", "d"})
   792  	requireDatums(t, datums2, []string{"b", "c", "d", "e"})
   793  	requireDatums(t, datums3, []string{"a", "f", "g"})
   794  	requireDatums(t, datums4, []string{"h", "i"})
   795  	requireChannelClosed(t, datums1)
   796  	requireChannelClosed(t, datums2)
   797  	requireChannelClosed(t, datums3)
   798  	requireChannelClosed(t, datums4)
   799  
   800  	require.NoError(t, chain.Succeed(job1))
   801  	require.NoError(t, chain.Succeed(job2))
   802  	require.NoError(t, chain.Succeed(job3))
   803  	require.NoError(t, chain.Succeed(job4))
   804  	require.NoError(t, eg.Wait())
   805  }