go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/tryjob/tjcancel/cancellator_test.go (about)

     1  // Copyright 2022 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 tjcancel
    16  
    17  import (
    18  	"context"
    19  	"crypto/sha1"
    20  	"testing"
    21  
    22  	"go.chromium.org/luci/common/clock"
    23  	"go.chromium.org/luci/gae/service/datastore"
    24  
    25  	"go.chromium.org/luci/cv/internal/changelist"
    26  	"go.chromium.org/luci/cv/internal/common"
    27  	"go.chromium.org/luci/cv/internal/cvtesting"
    28  	"go.chromium.org/luci/cv/internal/run"
    29  	"go.chromium.org/luci/cv/internal/tryjob"
    30  
    31  	. "github.com/smartystreets/goconvey/convey"
    32  	. "go.chromium.org/luci/common/testing/assertions"
    33  )
    34  
    35  func TestTaskHandler(t *testing.T) {
    36  	Convey("handleTask", t, func() {
    37  		Convey("panics", func() {
    38  			c := &Cancellator{}
    39  			ctx := context.Background()
    40  
    41  			panicker := func() {
    42  				_ = c.handleTask(ctx, &tryjob.CancelStaleTryjobsTask{
    43  					Clid:                     42,
    44  					PreviousMinEquivPatchset: 2,
    45  					CurrentMinEquivPatchset:  2,
    46  				})
    47  			}
    48  			So(panicker, ShouldPanicLike, "patchset numbers expected to increase")
    49  		})
    50  		Convey("works with", func() {
    51  			cvt := &cvtesting.Test{}
    52  			ctx, cancel := cvt.SetUp(t)
    53  			defer cancel()
    54  			n := tryjob.NewNotifier(cvt.TQDispatcher)
    55  			c := NewCancellator(n)
    56  			mb := &mockBackend{}
    57  			c.RegisterBackend(mb)
    58  			const clid = common.CLID(100)
    59  			cl := &changelist.CL{ID: clid}
    60  			So(datastore.Put(ctx, cl), ShouldBeNil)
    61  			Convey("no tryjobs", func() {
    62  				err := c.handleTask(ctx, &tryjob.CancelStaleTryjobsTask{
    63  					Clid:                     int64(clid),
    64  					PreviousMinEquivPatchset: 2,
    65  					CurrentMinEquivPatchset:  5,
    66  				})
    67  				So(err, ShouldBeNil)
    68  				So(mb.calledWith, ShouldHaveLength, 0)
    69  			})
    70  			Convey("all tryjobs ended", func() {
    71  				tj1 := putTryjob(ctx, clid, 2, tryjob.Status_ENDED, 1, run.Status_FAILED, nil)
    72  				tj2 := putTryjob(ctx, clid, 2, tryjob.Status_ENDED, 2, run.Status_CANCELLED, nil)
    73  				err := c.handleTask(ctx, &tryjob.CancelStaleTryjobsTask{
    74  					Clid:                     int64(clid),
    75  					PreviousMinEquivPatchset: 2,
    76  					CurrentMinEquivPatchset:  5,
    77  				})
    78  				So(err, ShouldBeNil)
    79  				// Should not call backend.
    80  				So(mb.calledWith, ShouldHaveLength, 0)
    81  
    82  				So(datastore.Get(ctx, tj1, tj2), ShouldBeNil)
    83  				// Should not modify entities.
    84  				So(tj1.EVersion, ShouldEqual, 1)
    85  				So(tj2.EVersion, ShouldEqual, 1)
    86  			})
    87  			Convey("some tryjobs ended, others cancellable", func() {
    88  				tj11 := putTryjob(ctx, clid, 2, tryjob.Status_ENDED, 11, run.Status_FAILED, nil)
    89  				tj12 := putTryjob(ctx, clid, 2, tryjob.Status_TRIGGERED, 12, run.Status_CANCELLED, nil)
    90  				err := c.handleTask(ctx, &tryjob.CancelStaleTryjobsTask{
    91  					Clid:                     int64(clid),
    92  					PreviousMinEquivPatchset: 2,
    93  					CurrentMinEquivPatchset:  5,
    94  				})
    95  				So(err, ShouldBeNil)
    96  				// Should call backend once, with tj12.
    97  				So(mb.calledWith, ShouldHaveLength, 1)
    98  				So(mb.calledWith[0].ExternalID, ShouldEqual, tj12.ExternalID)
    99  
   100  				So(datastore.Get(ctx, tj11, tj12), ShouldBeNil)
   101  				// Should modify only tj12.
   102  				So(tj11.EVersion, ShouldEqual, 1)
   103  				So(tj12.EVersion, ShouldEqual, 2)
   104  				So(tj12.Status, ShouldEqual, tryjob.Status_CANCELLED)
   105  			})
   106  			Convey("tryjob still watched", func() {
   107  				tj21 := putTryjob(ctx, clid, 2, tryjob.Status_TRIGGERED, 21, run.Status_RUNNING, nil)
   108  				task := &tryjob.CancelStaleTryjobsTask{
   109  					Clid:                     int64(clid),
   110  					PreviousMinEquivPatchset: 2,
   111  					CurrentMinEquivPatchset:  5,
   112  				}
   113  				err := c.handleTask(ctx, task)
   114  				So(err, ShouldBeNil)
   115  				// Should not call backend.
   116  				So(mb.calledWith, ShouldHaveLength, 0)
   117  
   118  				So(datastore.Get(ctx, tj21), ShouldBeNil)
   119  				// Should not modify the entity.
   120  				So(tj21.EVersion, ShouldEqual, 1)
   121  				So(tj21.Status, ShouldEqual, tryjob.Status_TRIGGERED)
   122  				So(cvt.TQ.Tasks(), ShouldHaveLength, 1)
   123  				So(cvt.TQ.Tasks()[0].Payload, ShouldResembleProto, task)
   124  				So(cvt.TQ.Tasks()[0].ETA, ShouldEqual, cvt.Clock.Now().Add(cancelLaterDuration))
   125  			})
   126  			Convey("tryjob not triggered by cv", func() {
   127  				tj31 := putTryjob(ctx, clid, 2, tryjob.Status_TRIGGERED, 31, run.Status_CANCELLED, func(tj *tryjob.Tryjob) {
   128  					tj.LaunchedBy = ""
   129  				})
   130  				err := c.handleTask(ctx, &tryjob.CancelStaleTryjobsTask{
   131  					Clid:                     int64(clid),
   132  					PreviousMinEquivPatchset: 2,
   133  					CurrentMinEquivPatchset:  5,
   134  				})
   135  				So(err, ShouldBeNil)
   136  				// Should not call backend.
   137  				So(mb.calledWith, ShouldHaveLength, 0)
   138  				So(datastore.Get(ctx, tj31), ShouldBeNil)
   139  				// Should not modify the entity.
   140  				So(tj31.EVersion, ShouldEqual, 1)
   141  				So(tj31.Status, ShouldNotEqual, tryjob.Status_CANCELLED)
   142  			})
   143  			Convey("tryjob configured to skip stale check", func() {
   144  				tj41 := putTryjob(ctx, clid, 2, tryjob.Status_TRIGGERED, 41, run.Status_CANCELLED, func(tj *tryjob.Tryjob) {
   145  					tj.Definition.SkipStaleCheck = true
   146  				})
   147  				err := c.handleTask(ctx, &tryjob.CancelStaleTryjobsTask{
   148  					Clid:                     int64(clid),
   149  					PreviousMinEquivPatchset: 2,
   150  					CurrentMinEquivPatchset:  5,
   151  				})
   152  				So(err, ShouldBeNil)
   153  				// Should not call backend.
   154  				So(mb.calledWith, ShouldHaveLength, 0)
   155  				So(datastore.Get(ctx, tj41), ShouldBeNil)
   156  				// Should not modify the entity.
   157  				So(tj41.EVersion, ShouldEqual, 1)
   158  				So(tj41.Status, ShouldNotEqual, tryjob.Status_CANCELLED)
   159  			})
   160  			Convey("CL has Cq-Do-Not-Cancel-Tryjobs footer", func() {
   161  				tj51 := putTryjob(ctx, clid, 2, tryjob.Status_TRIGGERED, 12, run.Status_CANCELLED, nil)
   162  				cl.Snapshot = &changelist.Snapshot{
   163  					Metadata: []*changelist.StringPair{
   164  						{
   165  							Key:   common.FooterCQDoNotCancelTryjobs,
   166  							Value: "True",
   167  						},
   168  					},
   169  				}
   170  				So(datastore.Put(ctx, cl), ShouldBeNil)
   171  				err := c.handleTask(ctx, &tryjob.CancelStaleTryjobsTask{
   172  					Clid:                     int64(clid),
   173  					PreviousMinEquivPatchset: 2,
   174  					CurrentMinEquivPatchset:  5,
   175  				})
   176  				So(err, ShouldBeNil)
   177  				// Should not call backend.
   178  				So(mb.calledWith, ShouldHaveLength, 0)
   179  				So(datastore.Get(ctx, tj51), ShouldBeNil)
   180  				// Should not modify the entity.
   181  				So(tj51.EVersion, ShouldEqual, 1)
   182  				So(tj51.Status, ShouldNotEqual, tryjob.Status_CANCELLED)
   183  			})
   184  		})
   185  	})
   186  }
   187  
   188  // putTryjob creates a mock Tryjob and its triggering Run.
   189  //
   190  // It must be called inside a Convey() context as it contains
   191  // assertions.
   192  func putTryjob(ctx context.Context, clid common.CLID, patchset int32, tjStatus tryjob.Status, buildNumber int64, runStatus run.Status, modify func(*tryjob.Tryjob)) *tryjob.Tryjob {
   193  	now := datastore.RoundTime(clock.Now(ctx).UTC())
   194  	tjID := tryjob.MustBuildbucketID("test.com", buildNumber)
   195  	digest := mockDigest(string(tjID))
   196  	r := &run.Run{
   197  		ID:     common.MakeRunID("test", now, 1, digest),
   198  		Status: runStatus,
   199  	}
   200  	So(datastore.Put(ctx, r), ShouldBeNil)
   201  	tj := &tryjob.Tryjob{
   202  		ExternalID:       tjID,
   203  		CLPatchsets:      []tryjob.CLPatchset{tryjob.MakeCLPatchset(clid, patchset)},
   204  		Status:           tjStatus,
   205  		EVersion:         1,
   206  		EntityCreateTime: now,
   207  		EntityUpdateTime: now,
   208  		LaunchedBy:       r.ID,
   209  		Definition:       &tryjob.Definition{},
   210  	}
   211  	if modify != nil {
   212  		modify(tj)
   213  	}
   214  	So(datastore.Put(ctx, tj), ShouldBeNil)
   215  	return tj
   216  }
   217  
   218  // mockDigest hashes a string.
   219  func mockDigest(s string) []byte {
   220  	h := sha1.New()
   221  	h.Write([]byte(s))
   222  	return h.Sum(nil)
   223  }
   224  
   225  type mockBackend struct {
   226  	calledWith []*tryjob.Tryjob
   227  }
   228  
   229  func (mb *mockBackend) Kind() string {
   230  	return "buildbucket"
   231  }
   232  
   233  func (mb *mockBackend) CancelTryjob(ctx context.Context, tj *tryjob.Tryjob, reason string) error {
   234  	mb.calledWith = append(mb.calledWith, tj)
   235  	return nil
   236  }