go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/load_test.go (about)

     1  // Copyright 2021 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 run
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  	"time"
    22  
    23  	"google.golang.org/grpc/codes"
    24  	"google.golang.org/protobuf/types/known/timestamppb"
    25  
    26  	"go.chromium.org/luci/common/errors"
    27  	"go.chromium.org/luci/gae/service/datastore"
    28  	"go.chromium.org/luci/grpc/appstatus"
    29  
    30  	"go.chromium.org/luci/cv/internal/common"
    31  	"go.chromium.org/luci/cv/internal/cvtesting"
    32  
    33  	. "github.com/smartystreets/goconvey/convey"
    34  	. "go.chromium.org/luci/common/testing/assertions"
    35  )
    36  
    37  func TestLoadChildRuns(t *testing.T) {
    38  	t.Parallel()
    39  
    40  	Convey("LoadChildRuns works", t, func() {
    41  		ct := cvtesting.Test{}
    42  		ctx, cancel := ct.SetUp(t)
    43  		defer cancel()
    44  
    45  		put := func(runID common.RunID, depRuns common.RunIDs) {
    46  			So(datastore.Put(ctx, &Run{
    47  				ID:      runID,
    48  				DepRuns: depRuns,
    49  			}), ShouldBeNil)
    50  		}
    51  
    52  		const parentRun1 = common.RunID("parent/1-cow")
    53  
    54  		const orphanRun = common.RunID("orphan/1-chicken")
    55  		put(orphanRun, common.RunIDs{})
    56  		out1, err := LoadChildRuns(ctx, parentRun1)
    57  		So(err, ShouldBeNil)
    58  		So(out1, ShouldHaveLength, 0)
    59  
    60  		const pendingRun = common.RunID("child/1-pending")
    61  		put(pendingRun, common.RunIDs{parentRun1})
    62  		const runningRun = common.RunID("child/1-running")
    63  		put(runningRun, common.RunIDs{parentRun1})
    64  
    65  		out2, err := LoadChildRuns(ctx, parentRun1)
    66  		So(err, ShouldBeNil)
    67  		So(out2, ShouldHaveLength, 2)
    68  	})
    69  }
    70  
    71  func TestLoadRunLogEntries(t *testing.T) {
    72  	t.Parallel()
    73  
    74  	Convey("LoadRunLogEntries works", t, func() {
    75  		ct := cvtesting.Test{}
    76  		ctx, cancel := ct.SetUp(t)
    77  		defer cancel()
    78  
    79  		ev := int64(1)
    80  		put := func(runID common.RunID, entries ...*LogEntry) {
    81  			So(datastore.Put(ctx, &RunLog{
    82  				Run:     datastore.MakeKey(ctx, common.RunKind, string(runID)),
    83  				ID:      ev,
    84  				Entries: &LogEntries{Entries: entries},
    85  			}), ShouldBeNil)
    86  			ev += 1
    87  		}
    88  
    89  		const run1 = common.RunID("rust/123-1-beef")
    90  		const run2 = common.RunID("dart/789-2-cafe")
    91  
    92  		put(
    93  			run1,
    94  			&LogEntry{
    95  				Time: timestamppb.New(ct.Clock.Now()),
    96  				Kind: &LogEntry_Created_{Created: &LogEntry_Created{
    97  					ConfigGroupId: "fi/rst",
    98  				}},
    99  			},
   100  		)
   101  		ct.Clock.Add(time.Minute)
   102  		put(
   103  			run1,
   104  			&LogEntry{
   105  				Time: timestamppb.New(ct.Clock.Now()),
   106  				Kind: &LogEntry_ConfigChanged_{ConfigChanged: &LogEntry_ConfigChanged{
   107  					ConfigGroupId: "se/cond",
   108  				}},
   109  			},
   110  			&LogEntry{
   111  				Time: timestamppb.New(ct.Clock.Now()),
   112  				Kind: &LogEntry_TryjobsRequirementUpdated_{TryjobsRequirementUpdated: &LogEntry_TryjobsRequirementUpdated{}},
   113  			},
   114  		)
   115  
   116  		ct.Clock.Add(time.Minute)
   117  		put(
   118  			run2,
   119  			&LogEntry{
   120  				Time: timestamppb.New(ct.Clock.Now()),
   121  				Kind: &LogEntry_Created_{Created: &LogEntry_Created{
   122  					ConfigGroupId: "fi/rst-but-run2",
   123  				}},
   124  			},
   125  		)
   126  
   127  		out1, err := LoadRunLogEntries(ctx, run1)
   128  		So(err, ShouldBeNil)
   129  		So(out1, ShouldHaveLength, 3)
   130  		So(out1[0].GetCreated().GetConfigGroupId(), ShouldResemble, "fi/rst")
   131  		So(out1[1].GetConfigChanged().GetConfigGroupId(), ShouldResemble, "se/cond")
   132  		So(out1[2].GetTryjobsRequirementUpdated(), ShouldNotBeNil)
   133  
   134  		out2, err := LoadRunLogEntries(ctx, run2)
   135  		So(err, ShouldBeNil)
   136  		So(out2, ShouldHaveLength, 1)
   137  		So(out2[0].GetCreated().GetConfigGroupId(), ShouldResemble, "fi/rst-but-run2")
   138  	})
   139  }
   140  
   141  func TestLoadRunsBuilder(t *testing.T) {
   142  	t.Parallel()
   143  
   144  	Convey("LoadRunsBuilder works", t, func() {
   145  		ct := cvtesting.Test{}
   146  		ctx, cancel := ct.SetUp(t)
   147  		defer cancel()
   148  
   149  		const lProject = "proj"
   150  		// Run statuses are used in this test to ensure Runs were actually loaded.
   151  		makeRun := func(id int, s Status) *Run {
   152  			r := &Run{ID: common.RunID(fmt.Sprintf("%s/%03d", lProject, id)), Status: s}
   153  			So(datastore.Put(ctx, r), ShouldBeNil)
   154  			return r
   155  		}
   156  
   157  		r1 := makeRun(1, Status_RUNNING)
   158  		r2 := makeRun(2, Status_CANCELLED)
   159  		r3 := makeRun(3, Status_PENDING)
   160  		r4 := makeRun(4, Status_SUCCEEDED)
   161  		r201 := makeRun(201, Status_FAILED)
   162  		r202 := makeRun(202, Status_FAILED)
   163  		r404 := makeRun(404, Status_PENDING)
   164  		r405 := makeRun(405, Status_PENDING)
   165  		So(datastore.Delete(ctx, r404, r405), ShouldBeNil)
   166  
   167  		Convey("Without checker", func() {
   168  			Convey("Every Run exists", func() {
   169  				verify := func(b LoadRunsBuilder) {
   170  					runsA, errs := b.Do(ctx)
   171  					So(errs, ShouldResemble, make(errors.MultiError, 2))
   172  					So(runsA, ShouldResemble, []*Run{r201, r202})
   173  
   174  					runsB, err := b.DoIgnoreNotFound(ctx)
   175  					So(err, ShouldBeNil)
   176  					So(runsB, ShouldResemble, runsA)
   177  				}
   178  				Convey("IDs", func() {
   179  					verify(LoadRunsFromIDs(r201.ID, r202.ID))
   180  				})
   181  				Convey("keys", func() {
   182  					verify(LoadRunsFromKeys(
   183  						datastore.MakeKey(ctx, common.RunKind, string(r201.ID)),
   184  						datastore.MakeKey(ctx, common.RunKind, string(r202.ID)),
   185  					))
   186  				})
   187  			})
   188  
   189  			Convey("A missing Run", func() {
   190  				b := LoadRunsFromIDs(r404.ID)
   191  
   192  				runsA, errs := b.Do(ctx)
   193  				So(errs, ShouldResemble, errors.MultiError{datastore.ErrNoSuchEntity})
   194  				So(runsA, ShouldResemble, []*Run{{ID: r404.ID}})
   195  
   196  				runsB, err := b.DoIgnoreNotFound(ctx)
   197  				So(err, ShouldBeNil)
   198  				So(runsB, ShouldBeNil)
   199  			})
   200  			Convey("Mix of existing and missing", func() {
   201  				b := LoadRunsFromIDs(r201.ID, r404.ID, r202.ID, r405.ID, r4.ID)
   202  
   203  				runsA, errs := b.Do(ctx)
   204  				So(errs, ShouldResemble, errors.MultiError{nil, datastore.ErrNoSuchEntity, nil, datastore.ErrNoSuchEntity, nil})
   205  				So(runsA, ShouldResemble, []*Run{
   206  					r201,
   207  					{ID: r404.ID},
   208  					r202,
   209  					{ID: r405.ID},
   210  					r4,
   211  				})
   212  
   213  				runsB, err := b.DoIgnoreNotFound(ctx)
   214  				So(err, ShouldBeNil)
   215  				So(runsB, ShouldResemble, []*Run{r201, r202, r4})
   216  			})
   217  		})
   218  
   219  		Convey("With checker", func() {
   220  			checker := fakeRunChecker{
   221  				afterOnNotFound: appstatus.Error(codes.NotFound, "not-found-ds"),
   222  			}
   223  
   224  			Convey("No errors of any kind", func() {
   225  				b := LoadRunsFromIDs(r201.ID, r202.ID, r4.ID).Checker(checker)
   226  
   227  				runsA, errs := b.Do(ctx)
   228  				So(errs, ShouldResemble, make(errors.MultiError, 3))
   229  				So(runsA, ShouldResemble, []*Run{r201, r202, r4})
   230  
   231  				runsB, err := b.DoIgnoreNotFound(ctx)
   232  				So(err, ShouldBeNil)
   233  				So(runsB, ShouldResemble, runsA)
   234  			})
   235  
   236  			Convey("Missing in datastore", func() {
   237  				b := LoadRunsFromIDs(r404.ID).Checker(checker)
   238  
   239  				runsA, errs := b.Do(ctx)
   240  				So(errs[0], ShouldHaveAppStatus, codes.NotFound)
   241  				So(runsA, ShouldResemble, []*Run{{ID: r404.ID}})
   242  
   243  				runsB, err := b.DoIgnoreNotFound(ctx)
   244  				So(err, ShouldBeNil)
   245  				So(runsB, ShouldBeNil)
   246  			})
   247  
   248  			Convey("Mix", func() {
   249  				checker.before = map[common.RunID]error{
   250  					r1.ID: appstatus.Error(codes.NotFound, "not-found-before"),
   251  					r2.ID: errors.New("before-oops"),
   252  				}
   253  				checker.after = map[common.RunID]error{
   254  					r3.ID: appstatus.Error(codes.NotFound, "not-found-after"),
   255  					r4.ID: errors.New("after-oops"),
   256  				}
   257  				Convey("only found and not found", func() {
   258  					b := LoadRunsFromIDs(r201.ID, r1.ID, r202.ID, r3.ID, r404.ID).Checker(checker)
   259  
   260  					runsA, errs := b.Do(ctx)
   261  					So(errs[0], ShouldBeNil) // r201
   262  					So(errs[1], ShouldErrLike, "not-found-before")
   263  					So(errs[2], ShouldBeNil) // r202
   264  					So(errs[3], ShouldErrLike, "not-found-after")
   265  					So(errs[4], ShouldErrLike, "not-found-ds")
   266  					So(runsA, ShouldResemble, []*Run{
   267  						r201,
   268  						{ID: r1.ID},
   269  						r202,
   270  						r3, // loaded & returned, despite errors
   271  						{ID: r404.ID},
   272  					})
   273  
   274  					runsB, err := b.DoIgnoreNotFound(ctx)
   275  					So(err, ShouldBeNil)
   276  					So(runsB, ShouldResemble, []*Run{r201, r202})
   277  				})
   278  				Convey("of everything", func() {
   279  					b := LoadRunsFromIDs(r201.ID, r1.ID, r2.ID, r3.ID, r4.ID, r404.ID).Checker(checker)
   280  
   281  					runsA, errs := b.Do(ctx)
   282  					So(errs[0], ShouldBeNil) // r201
   283  					So(errs[1], ShouldErrLike, "not-found-before")
   284  					So(errs[2], ShouldErrLike, "before-oops")
   285  					So(errs[3], ShouldErrLike, "not-found-after")
   286  					So(errs[4], ShouldErrLike, "after-oops")
   287  					So(errs[5], ShouldErrLike, "not-found-ds")
   288  					So(runsA, ShouldResemble, []*Run{
   289  						r201,
   290  						{ID: r1.ID},
   291  						{ID: r2.ID},
   292  						r3, // loaded & returned, despite errors
   293  						r4, // loaded & returned, despite errors
   294  						{ID: r404.ID},
   295  					})
   296  
   297  					runsB, err := b.DoIgnoreNotFound(ctx)
   298  					So(err, ShouldErrLike, "before-oops")
   299  					So(runsB, ShouldBeNil)
   300  				})
   301  			})
   302  		})
   303  	})
   304  }
   305  
   306  type fakeRunChecker struct {
   307  	before          map[common.RunID]error
   308  	beforeFunc      func(common.RunID) error // applied only if Run is not in `before`
   309  	after           map[common.RunID]error
   310  	afterOnNotFound error
   311  }
   312  
   313  func (f fakeRunChecker) Before(ctx context.Context, id common.RunID) error {
   314  	err := f.before[id]
   315  	if err == nil && f.beforeFunc != nil {
   316  		err = f.beforeFunc(id)
   317  	}
   318  	return err
   319  }
   320  
   321  func (f fakeRunChecker) After(ctx context.Context, runIfFound *Run) error {
   322  	if runIfFound == nil {
   323  		return f.afterOnNotFound
   324  	}
   325  	return f.after[runIfFound.ID]
   326  }