go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/scheduler/appengine/engine/invquery_test.go (about)

     1  // Copyright 2018 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 engine
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  
    22  	"google.golang.org/protobuf/types/known/timestamppb"
    23  
    24  	"go.chromium.org/luci/common/clock"
    25  	"go.chromium.org/luci/common/clock/testclock"
    26  	"go.chromium.org/luci/gae/filter/featureBreaker"
    27  	"go.chromium.org/luci/gae/impl/memory"
    28  	"go.chromium.org/luci/gae/service/datastore"
    29  
    30  	"go.chromium.org/luci/scheduler/appengine/internal"
    31  	"go.chromium.org/luci/scheduler/appengine/task"
    32  
    33  	. "github.com/smartystreets/goconvey/convey"
    34  )
    35  
    36  func makeInvListQ(ids ...int64) *invListQuery {
    37  	invs := make([]*Invocation, len(ids))
    38  	for i, id := range ids {
    39  		invs[i] = &Invocation{ID: id}
    40  	}
    41  	return &invListQuery{invs, 0}
    42  }
    43  
    44  func invIDs(invs []*Invocation) []int64 {
    45  	out := make([]int64, len(invs))
    46  	for i, inv := range invs {
    47  		out[i] = inv.ID
    48  	}
    49  	return out
    50  }
    51  
    52  func TestMergeInvQueries(t *testing.T) {
    53  	t.Parallel()
    54  
    55  	Convey("Empty", t, func() {
    56  		invs, done, err := mergeInvQueries([]invQuery{
    57  			makeInvListQ(), makeInvListQ(),
    58  		}, 100, nil)
    59  		So(invs, ShouldBeEmpty)
    60  		So(done, ShouldBeTrue)
    61  		So(err, ShouldBeNil)
    62  	})
    63  
    64  	Convey("Single source, with limit", t, func() {
    65  		invs, done, err := mergeInvQueries([]invQuery{
    66  			makeInvListQ(1, 2, 3, 4, 5),
    67  		}, 3, nil)
    68  		So(invIDs(invs), ShouldResemble, []int64{1, 2, 3})
    69  		So(done, ShouldBeFalse)
    70  		So(err, ShouldBeNil)
    71  	})
    72  
    73  	Convey("Single source, with limit, appends", t, func() {
    74  		invs := []*Invocation{{ID: 1}, {ID: 2}}
    75  		invs, done, err := mergeInvQueries([]invQuery{
    76  			makeInvListQ(3, 4, 5, 6),
    77  		}, 3, invs)
    78  		So(invIDs(invs), ShouldResemble, []int64{1, 2, 3, 4, 5})
    79  		So(done, ShouldBeFalse)
    80  		So(err, ShouldBeNil)
    81  	})
    82  
    83  	Convey("Single source, dups and out of order", t, func() {
    84  		invs, done, err := mergeInvQueries([]invQuery{
    85  			makeInvListQ(1, 2, 2, 3, 2, 4, 5),
    86  		}, 100, nil)
    87  		So(invIDs(invs), ShouldResemble, []int64{1, 2, 3, 4, 5})
    88  		So(done, ShouldBeTrue)
    89  		So(err, ShouldBeNil)
    90  	})
    91  
    92  	Convey("Merging", t, func() {
    93  		invs, done, err := mergeInvQueries([]invQuery{
    94  			makeInvListQ(1, 3, 5),
    95  			makeInvListQ(2, 4, 6),
    96  		}, 100, nil)
    97  		So(invIDs(invs), ShouldResemble, []int64{1, 2, 3, 4, 5, 6})
    98  		So(done, ShouldBeTrue)
    99  		So(err, ShouldBeNil)
   100  	})
   101  
   102  	Convey("Merging with dups and limit", t, func() {
   103  		invs, done, err := mergeInvQueries([]invQuery{
   104  			makeInvListQ(1, 2, 3, 4, 5),
   105  			makeInvListQ(1, 2, 3, 4, 5),
   106  		}, 3, nil)
   107  		So(invIDs(invs), ShouldResemble, []int64{1, 2, 3})
   108  		So(done, ShouldBeFalse)
   109  		So(err, ShouldBeNil)
   110  	})
   111  
   112  	Convey("Merging with limit that exactly matches query size", t, func() {
   113  		invs, done, err := mergeInvQueries([]invQuery{
   114  			makeInvListQ(1, 2, 3, 4, 5),
   115  			makeInvListQ(1, 2, 3, 4, 5),
   116  		}, 5, nil)
   117  		So(invIDs(invs), ShouldResemble, []int64{1, 2, 3, 4, 5})
   118  		So(done, ShouldBeTrue) // true here! this is important, otherwise we'll get empty pages
   119  		So(err, ShouldBeNil)
   120  	})
   121  }
   122  
   123  func TestActiveInvQuery(t *testing.T) {
   124  	t.Parallel()
   125  
   126  	Convey("Works", t, func() {
   127  		q := activeInvQuery(context.Background(), &Job{
   128  			ActiveInvocations: []int64{1, 2, 3, 4, 5, 8, 6},
   129  		}, 3)
   130  		So(invIDs(q.invs), ShouldResemble, []int64{4, 5, 6, 8})
   131  	})
   132  }
   133  
   134  func TestRecentInvQuery(t *testing.T) {
   135  	t.Parallel()
   136  
   137  	c, _ := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC)
   138  	now := timestamppb.New(clock.Now(c))
   139  
   140  	Convey("Works", t, func() {
   141  		q := recentInvQuery(c, &Job{
   142  			FinishedInvocationsRaw: marshalFinishedInvs([]*internal.FinishedInvocation{
   143  				{InvocationId: 1, Finished: now},
   144  				{InvocationId: 2, Finished: now},
   145  				{InvocationId: 3, Finished: now},
   146  				{InvocationId: 4, Finished: now},
   147  				{InvocationId: 5, Finished: now},
   148  				{InvocationId: 8, Finished: now},
   149  				{InvocationId: 6, Finished: now},
   150  				// And this one should be ignored, as it is "too old".
   151  				{InvocationId: 9, Finished: timestamppb.New(clock.Now(c).Add(-FinishedInvocationsHorizon - 1))},
   152  			}),
   153  		}, 3)
   154  		So(invIDs(q.invs), ShouldResemble, []int64{4, 5, 6, 8})
   155  	})
   156  }
   157  
   158  func TestInvDatastoreIter(t *testing.T) {
   159  	t.Parallel()
   160  
   161  	run := func(c context.Context, query *datastore.Query, limit int) ([]*Invocation, error) {
   162  		it := invDatastoreIter{}
   163  		it.start(c, query)
   164  		defer it.stop()
   165  		invs := []*Invocation{}
   166  		for len(invs) != limit {
   167  			switch inv, err := it.next(); {
   168  			case err != nil:
   169  				return nil, err
   170  			case inv == nil:
   171  				return invs, nil // fetched everything we had
   172  			default:
   173  				invs = append(invs, inv)
   174  			}
   175  		}
   176  		return invs, nil
   177  	}
   178  
   179  	c := memory.Use(context.Background())
   180  
   181  	Convey("Empty", t, func() {
   182  		invs, err := run(c, datastore.NewQuery("Invocation"), 100)
   183  		So(err, ShouldBeNil)
   184  		So(len(invs), ShouldEqual, 0)
   185  	})
   186  
   187  	Convey("Not empty", t, func() {
   188  		original := []*Invocation{
   189  			{ID: 1},
   190  			{ID: 2},
   191  			{ID: 3},
   192  			{ID: 4},
   193  			{ID: 5},
   194  		}
   195  		datastore.Put(c, original)
   196  		datastore.GetTestable(c).CatchupIndexes()
   197  
   198  		Convey("No limit", func() {
   199  			q := datastore.NewQuery("Invocation").Order("__key__")
   200  			invs, err := run(c, q, 100)
   201  			So(err, ShouldBeNil)
   202  			So(invs, ShouldResemble, original)
   203  		})
   204  
   205  		Convey("With limit", func() {
   206  			q := datastore.NewQuery("Invocation").Order("__key__")
   207  
   208  			gtq := q
   209  			invs, err := run(c, gtq, 2)
   210  			So(err, ShouldBeNil)
   211  			So(invs, ShouldResemble, original[:2])
   212  
   213  			gtq = q.Gt("__key__", datastore.KeyForObj(c, invs[1]))
   214  			invs, err = run(c, gtq, 2)
   215  			So(err, ShouldBeNil)
   216  			So(invs, ShouldResemble, original[2:4])
   217  
   218  			gtq = q.Gt("__key__", datastore.KeyForObj(c, invs[1]))
   219  			invs, err = run(c, gtq, 2)
   220  			So(err, ShouldBeNil)
   221  			So(invs, ShouldResemble, original[4:5])
   222  
   223  			gtq = q.Gt("__key__", datastore.KeyForObj(c, invs[0]))
   224  			invs, err = run(c, gtq, 2)
   225  			So(err, ShouldBeNil)
   226  			So(invs, ShouldBeEmpty)
   227  		})
   228  
   229  		Convey("With error", func() {
   230  			dsErr := fmt.Errorf("boo")
   231  
   232  			brokenC, breaker := featureBreaker.FilterRDS(c, nil)
   233  			breaker.BreakFeatures(dsErr, "Run")
   234  
   235  			q := datastore.NewQuery("Invocation").Order("__key__")
   236  			invs, err := run(brokenC, q, 100)
   237  			So(err, ShouldEqual, dsErr)
   238  			So(len(invs), ShouldEqual, 0)
   239  		})
   240  	})
   241  }
   242  
   243  func insertInv(c context.Context, jobID string, invID int64, status task.Status) *Invocation {
   244  	inv := &Invocation{
   245  		ID:     invID,
   246  		JobID:  jobID,
   247  		Status: status,
   248  	}
   249  	if status.Final() {
   250  		inv.IndexedJobID = jobID
   251  	}
   252  	if err := datastore.Put(c, inv); err != nil {
   253  		panic(err)
   254  	}
   255  	datastore.GetTestable(c).CatchupIndexes()
   256  	return inv
   257  }
   258  
   259  func TestFinishedInvQuery(t *testing.T) {
   260  	t.Parallel()
   261  
   262  	fetchAll := func(q *invDatastoreQuery) (out []*Invocation) {
   263  		defer q.close()
   264  		for {
   265  			inv, err := q.peek()
   266  			if err != nil {
   267  				panic(err)
   268  			}
   269  			if inv == nil {
   270  				return
   271  			}
   272  			out = append(out, inv)
   273  			if err := q.advance(); err != nil {
   274  				panic(err)
   275  			}
   276  		}
   277  	}
   278  
   279  	Convey("With context", t, func() {
   280  		c := memory.Use(context.Background())
   281  
   282  		Convey("Empty", func() {
   283  			q := finishedInvQuery(c, &Job{JobID: "proj/job"}, 0)
   284  			So(fetchAll(q), ShouldBeEmpty)
   285  		})
   286  
   287  		Convey("Non empty", func() {
   288  			insertInv(c, "proj/job", 3, task.StatusSucceeded)
   289  			insertInv(c, "proj/job", 2, task.StatusSucceeded)
   290  			insertInv(c, "proj/job", 1, task.StatusSucceeded)
   291  
   292  			Convey("no cursor", func() {
   293  				q := finishedInvQuery(c, &Job{JobID: "proj/job"}, 0)
   294  				So(invIDs(fetchAll(q)), ShouldResemble, []int64{1, 2, 3})
   295  			})
   296  
   297  			Convey("with cursor", func() {
   298  				q := finishedInvQuery(c, &Job{JobID: "proj/job"}, 1)
   299  				So(invIDs(fetchAll(q)), ShouldResemble, []int64{2, 3})
   300  			})
   301  		})
   302  	})
   303  }
   304  
   305  func TestFetchInvsPage(t *testing.T) {
   306  	t.Parallel()
   307  
   308  	Convey("With context", t, func() {
   309  		const jobID = "proj/job"
   310  
   311  		c := memory.Use(context.Background())
   312  
   313  		// Note: we use only two queries here for simplicity, since various cases
   314  		// involving 3 queries are not significantly different (and differences are
   315  		// tested separately by other tests).
   316  		makeQS := func(job *Job, opts ListInvocationsOpts, lastScanned int64) []invQuery {
   317  			qs := []invQuery{}
   318  			if !opts.ActiveOnly {
   319  				ds := finishedInvQuery(c, job, lastScanned)
   320  				Reset(ds.close)
   321  				qs = append(qs, ds)
   322  			}
   323  			if !opts.FinishedOnly {
   324  				qs = append(qs, activeInvQuery(c, job, lastScanned))
   325  			}
   326  			return qs
   327  		}
   328  
   329  		fetchAllPages := func(qs []invQuery, opts ListInvocationsOpts) (invs []*Invocation, pages []invsPage) {
   330  			var page invsPage
   331  			var err error
   332  			for {
   333  				before := len(invs)
   334  				invs, page, err = fetchInvsPage(c, qs, opts, invs)
   335  				So(err, ShouldBeNil)
   336  				So(len(invs)-before, ShouldEqual, page.count)
   337  				So(page.count, ShouldBeLessThanOrEqualTo, opts.PageSize)
   338  				pages = append(pages, page)
   339  				if page.final {
   340  					return
   341  				}
   342  			}
   343  		}
   344  
   345  		Convey("ActiveInvocations list is consistent with datastore", func() {
   346  			// List of finished invocations, oldest to newest.
   347  			i6 := insertInv(c, jobID, 6, task.StatusSucceeded)
   348  			i5 := insertInv(c, jobID, 5, task.StatusFailed)
   349  			i4 := insertInv(c, jobID, 4, task.StatusSucceeded)
   350  			// List of still running invocations, oldest to newest.
   351  			i3 := insertInv(c, jobID, 3, task.StatusRunning)
   352  			i2 := insertInv(c, jobID, 2, task.StatusRunning)
   353  			i1 := insertInv(c, jobID, 1, task.StatusRunning)
   354  
   355  			job := &Job{
   356  				JobID: jobID,
   357  				ActiveInvocations: []int64{
   358  					3, 1, 2, // the set of active invocations, unordered
   359  				},
   360  			}
   361  
   362  			Convey("No paging", func() {
   363  				opts := ListInvocationsOpts{PageSize: 7}
   364  				invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil)
   365  				So(err, ShouldBeNil)
   366  				So(page, ShouldResemble, invsPage{6, true, 6})
   367  				So(invs, ShouldResemble, []*Invocation{i1, i2, i3, i4, i5, i6})
   368  			})
   369  
   370  			Convey("No paging, active only", func() {
   371  				opts := ListInvocationsOpts{PageSize: 7, ActiveOnly: true}
   372  				invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil)
   373  				So(err, ShouldBeNil)
   374  				So(page, ShouldResemble, invsPage{3, true, 3})
   375  				So(invs, ShouldResemble, []*Invocation{i1, i2, i3})
   376  			})
   377  
   378  			Convey("No paging, finished only", func() {
   379  				opts := ListInvocationsOpts{PageSize: 7, FinishedOnly: true}
   380  				invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil)
   381  				So(err, ShouldBeNil)
   382  				So(page, ShouldResemble, invsPage{3, true, 6})
   383  				So(invs, ShouldResemble, []*Invocation{i4, i5, i6})
   384  			})
   385  
   386  			Convey("Paging", func() {
   387  				opts := ListInvocationsOpts{PageSize: 2}
   388  				invs, pages := fetchAllPages(makeQS(job, opts, 0), opts)
   389  				So(invs, ShouldResemble, []*Invocation{i1, i2, i3, i4, i5, i6})
   390  				So(pages, ShouldResemble, []invsPage{
   391  					{2, false, 2},
   392  					{2, false, 4},
   393  					{2, true, 6},
   394  				})
   395  			})
   396  
   397  			Convey("Paging, resuming from cursor", func() {
   398  				opts := ListInvocationsOpts{PageSize: 2}
   399  				invs, pages := fetchAllPages(makeQS(job, opts, 3), opts)
   400  				So(invs, ShouldResemble, []*Invocation{i4, i5, i6})
   401  				So(pages, ShouldResemble, []invsPage{
   402  					{2, false, 5},
   403  					{1, true, 6},
   404  				})
   405  			})
   406  
   407  			Convey("Paging, active only", func() {
   408  				opts := ListInvocationsOpts{PageSize: 2, ActiveOnly: true}
   409  				invs, pages := fetchAllPages(makeQS(job, opts, 0), opts)
   410  				So(invs, ShouldResemble, []*Invocation{i1, i2, i3})
   411  				So(pages, ShouldResemble, []invsPage{
   412  					{2, false, 2},
   413  					{1, true, 3},
   414  				})
   415  			})
   416  
   417  			Convey("Paging, finished only", func() {
   418  				opts := ListInvocationsOpts{PageSize: 2, FinishedOnly: true}
   419  				invs, pages := fetchAllPages(makeQS(job, opts, 0), opts)
   420  				So(invs, ShouldResemble, []*Invocation{i4, i5, i6})
   421  				So(pages, ShouldResemble, []invsPage{
   422  					{2, false, 5},
   423  					{1, true, 6},
   424  				})
   425  			})
   426  		})
   427  
   428  		Convey("ActiveInvocations list is stale", func() {
   429  			// List of finished invocations, oldest to newest.
   430  			i6 := insertInv(c, jobID, 6, task.StatusSucceeded)
   431  			i5 := insertInv(c, jobID, 5, task.StatusFailed)
   432  			i4 := insertInv(c, jobID, 4, task.StatusSucceeded)
   433  			// List of still invocations referenced by ActiveInvocations.
   434  			i3 := insertInv(c, jobID, 3, task.StatusSucceeded) // actually done!
   435  			i2 := insertInv(c, jobID, 2, task.StatusRunning)
   436  			i1 := insertInv(c, jobID, 1, task.StatusRunning)
   437  
   438  			job := &Job{
   439  				JobID:             jobID,
   440  				ActiveInvocations: []int64{3, 1, 2},
   441  			}
   442  
   443  			Convey("No paging", func() {
   444  				opts := ListInvocationsOpts{PageSize: 7}
   445  				invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil)
   446  				So(err, ShouldBeNil)
   447  				So(page, ShouldResemble, invsPage{6, true, 6})
   448  				So(invs, ShouldResemble, []*Invocation{i1, i2, i3, i4, i5, i6})
   449  			})
   450  
   451  			Convey("No paging, active only", func() {
   452  				opts := ListInvocationsOpts{PageSize: 7, ActiveOnly: true}
   453  				invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil)
   454  				So(err, ShouldBeNil)
   455  				So(page, ShouldResemble, invsPage{2, true, 3}) // 3 was scanned and skipped!
   456  				So(invs, ShouldResemble, []*Invocation{i1, i2})
   457  			})
   458  
   459  			Convey("No paging, finished only", func() {
   460  				opts := ListInvocationsOpts{PageSize: 7, FinishedOnly: true}
   461  				invs, page, err := fetchInvsPage(c, makeQS(job, opts, 0), opts, nil)
   462  				So(err, ShouldBeNil)
   463  				So(page, ShouldResemble, invsPage{4, true, 6})
   464  				So(invs, ShouldResemble, []*Invocation{i3, i4, i5, i6})
   465  			})
   466  
   467  			Convey("Paging", func() {
   468  				opts := ListInvocationsOpts{PageSize: 2}
   469  				invs, pages := fetchAllPages(makeQS(job, opts, 0), opts)
   470  				So(invs, ShouldResemble, []*Invocation{i1, i2, i3, i4, i5, i6})
   471  				So(pages, ShouldResemble, []invsPage{
   472  					{2, false, 2},
   473  					{2, false, 4},
   474  					{2, true, 6},
   475  				})
   476  			})
   477  
   478  			Convey("Paging, resuming from cursor", func() {
   479  				opts := ListInvocationsOpts{PageSize: 2}
   480  				invs, pages := fetchAllPages(makeQS(job, opts, 3), opts)
   481  				So(invs, ShouldResemble, []*Invocation{i4, i5, i6})
   482  				So(pages, ShouldResemble, []invsPage{
   483  					{2, false, 5},
   484  					{1, true, 6},
   485  				})
   486  			})
   487  
   488  			Convey("Paging, active only", func() {
   489  				opts := ListInvocationsOpts{PageSize: 1, ActiveOnly: true}
   490  				invs, pages := fetchAllPages(makeQS(job, opts, 0), opts)
   491  				So(invs, ShouldResemble, []*Invocation{i1, i2})
   492  				So(pages, ShouldResemble, []invsPage{
   493  					{1, false, 1},
   494  					{1, false, 2},
   495  					{0, true, 3}, // empty mini-page, but advanced cursor
   496  				})
   497  			})
   498  
   499  			Convey("Paging, finished only", func() {
   500  				opts := ListInvocationsOpts{PageSize: 2, FinishedOnly: true}
   501  				invs, pages := fetchAllPages(makeQS(job, opts, 0), opts)
   502  				So(invs, ShouldResemble, []*Invocation{i3, i4, i5, i6})
   503  				So(pages, ShouldResemble, []invsPage{
   504  					{2, false, 4},
   505  					{2, true, 6},
   506  				})
   507  			})
   508  		})
   509  	})
   510  }
   511  
   512  func TestFillShallowInvs(t *testing.T) {
   513  	t.Parallel()
   514  
   515  	Convey("With context", t, func() {
   516  		c := memory.Use(context.Background())
   517  
   518  		// Bodies for inflated items.
   519  		datastore.Put(c, []*Invocation{
   520  			{ID: 1, Status: task.StatusSucceeded},
   521  			{ID: 10, Status: task.StatusRunning},
   522  		})
   523  
   524  		shallow := []*Invocation{
   525  			{ID: 1}, // to be inflated
   526  			{ID: 2, Status: task.StatusSucceeded},
   527  			{ID: 3, Status: task.StatusRunning},
   528  			{ID: 10}, // to be inflated
   529  			{ID: 10}, // to be inflated, again... as an edge case
   530  			{ID: 11, Status: task.StatusSucceeded},
   531  		}
   532  
   533  		Convey("no filtering", func() {
   534  			fat, err := fillShallowInvs(c, shallow, ListInvocationsOpts{})
   535  			So(err, ShouldBeNil)
   536  			So(fat, ShouldResemble, []*Invocation{
   537  				{ID: 1, Status: task.StatusSucceeded},
   538  				{ID: 2, Status: task.StatusSucceeded},
   539  				{ID: 3, Status: task.StatusRunning},
   540  				{ID: 10, Status: task.StatusRunning},
   541  				{ID: 10, Status: task.StatusRunning},
   542  				{ID: 11, Status: task.StatusSucceeded},
   543  			})
   544  			So(&shallow[0], ShouldEqual, &fat[0]) // same backing array
   545  		})
   546  
   547  		Convey("finished only", func() {
   548  			fat, err := fillShallowInvs(c, shallow, ListInvocationsOpts{
   549  				FinishedOnly: true,
   550  			})
   551  			So(err, ShouldBeNil)
   552  			So(fat, ShouldResemble, []*Invocation{
   553  				{ID: 1, Status: task.StatusSucceeded},
   554  				{ID: 2, Status: task.StatusSucceeded},
   555  				{ID: 11, Status: task.StatusSucceeded},
   556  			})
   557  			So(&shallow[0], ShouldEqual, &fat[0]) // same backing array
   558  		})
   559  
   560  		Convey("active only", func() {
   561  			fat, err := fillShallowInvs(c, shallow, ListInvocationsOpts{
   562  				ActiveOnly: true,
   563  			})
   564  			So(err, ShouldBeNil)
   565  			So(fat, ShouldResemble, []*Invocation{
   566  				{ID: 3, Status: task.StatusRunning},
   567  				{ID: 10, Status: task.StatusRunning},
   568  				{ID: 10, Status: task.StatusRunning},
   569  			})
   570  			So(&shallow[0], ShouldEqual, &fat[0]) // same backing array
   571  		})
   572  	})
   573  }