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 }