go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/tasks/cancel_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 tasks 16 17 import ( 18 "context" 19 "sort" 20 "testing" 21 "time" 22 23 "google.golang.org/protobuf/types/known/timestamppb" 24 25 "go.chromium.org/luci/common/clock/testclock" 26 "go.chromium.org/luci/gae/filter/txndefer" 27 "go.chromium.org/luci/gae/impl/memory" 28 "go.chromium.org/luci/gae/service/datastore" 29 "go.chromium.org/luci/server/tq" 30 31 "go.chromium.org/luci/buildbucket/appengine/internal/metrics" 32 "go.chromium.org/luci/buildbucket/appengine/model" 33 taskdefs "go.chromium.org/luci/buildbucket/appengine/tasks/defs" 34 pb "go.chromium.org/luci/buildbucket/proto" 35 36 . "github.com/smartystreets/goconvey/convey" 37 . "go.chromium.org/luci/common/testing/assertions" 38 ) 39 40 func TestCancelBuild(t *testing.T) { 41 Convey("Do", t, func() { 42 ctx := txndefer.FilterRDS(memory.Use(context.Background())) 43 ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins") 44 datastore.GetTestable(ctx).AutoIndex(true) 45 datastore.GetTestable(ctx).Consistent(true) 46 ctx, sch := tq.TestingContext(ctx, nil) 47 now := testclock.TestRecentTimeLocal 48 ctx, _ = testclock.UseTime(ctx, now) 49 50 Convey("not found", func() { 51 bld, err := Cancel(ctx, 1) 52 So(err, ShouldErrLike, "not found") 53 So(bld, ShouldBeNil) 54 So(sch.Tasks(), ShouldBeEmpty) 55 }) 56 57 Convey("found", func() { 58 So(datastore.Put(ctx, &model.Build{ 59 Proto: &pb.Build{ 60 Id: 1, 61 Builder: &pb.BuilderID{ 62 Project: "project", 63 Bucket: "bucket", 64 Builder: "builder", 65 }, 66 }, 67 }), ShouldBeNil) 68 So(datastore.Put(ctx, &model.BuildStatus{ 69 Build: datastore.MakeKey(ctx, "Build", 1), 70 Status: pb.Status_SCHEDULED, 71 }), ShouldBeNil) 72 bld, err := Cancel(ctx, 1) 73 So(err, ShouldBeNil) 74 So(bld.Proto, ShouldResembleProto, &pb.Build{ 75 Id: 1, 76 Builder: &pb.BuilderID{ 77 Project: "project", 78 Bucket: "bucket", 79 Builder: "builder", 80 }, 81 UpdateTime: timestamppb.New(now), 82 EndTime: timestamppb.New(now), 83 Status: pb.Status_CANCELED, 84 }) 85 So(sch.Tasks(), ShouldHaveLength, 3) 86 bs := &model.BuildStatus{ 87 Build: datastore.MakeKey(ctx, "Build", 1), 88 } 89 So(datastore.Get(ctx, bs), ShouldBeNil) 90 So(bs.Status, ShouldEqual, pb.Status_CANCELED) 91 }) 92 93 Convey("ended", func() { 94 So(datastore.Put(ctx, &model.Build{ 95 Proto: &pb.Build{ 96 Id: 1, 97 Builder: &pb.BuilderID{ 98 Project: "project", 99 Bucket: "bucket", 100 Builder: "builder", 101 }, 102 Status: pb.Status_SUCCESS, 103 }, 104 }), ShouldBeNil) 105 So(datastore.Put(ctx, &model.BuildStatus{ 106 Build: datastore.MakeKey(ctx, "Build", 1), 107 Status: pb.Status_SUCCESS, 108 }), ShouldBeNil) 109 bld, err := Cancel(ctx, 1) 110 So(err, ShouldBeNil) 111 So(bld.Proto, ShouldResembleProto, &pb.Build{ 112 Id: 1, 113 Builder: &pb.BuilderID{ 114 Project: "project", 115 Bucket: "bucket", 116 Builder: "builder", 117 }, 118 Status: pb.Status_SUCCESS, 119 }) 120 So(sch.Tasks(), ShouldBeEmpty) 121 }) 122 123 Convey("swarming task cancellation", func() { 124 So(datastore.Put(ctx, &model.Build{ 125 Proto: &pb.Build{ 126 Id: 1, 127 Builder: &pb.BuilderID{ 128 Project: "project", 129 Bucket: "bucket", 130 Builder: "builder", 131 }, 132 }, 133 }), ShouldBeNil) 134 So(datastore.Put(ctx, &model.BuildInfra{ 135 Build: datastore.MakeKey(ctx, "Build", 1), 136 Proto: &pb.BuildInfra{ 137 Swarming: &pb.BuildInfra_Swarming{ 138 Hostname: "example.com", 139 TaskId: "id", 140 }, 141 }, 142 }), ShouldBeNil) 143 So(datastore.Put(ctx, &model.BuildStatus{ 144 Build: datastore.MakeKey(ctx, "Build", 1), 145 Status: pb.Status_STARTED, 146 }), ShouldBeNil) 147 bld, err := Cancel(ctx, 1) 148 So(err, ShouldBeNil) 149 So(bld.Proto, ShouldResembleProto, &pb.Build{ 150 Id: 1, 151 Builder: &pb.BuilderID{ 152 Project: "project", 153 Bucket: "bucket", 154 Builder: "builder", 155 }, 156 UpdateTime: timestamppb.New(now), 157 EndTime: timestamppb.New(now), 158 Status: pb.Status_CANCELED, 159 }) 160 So(sch.Tasks(), ShouldHaveLength, 4) 161 bs := &model.BuildStatus{ 162 Build: datastore.MakeKey(ctx, "Build", 1), 163 } 164 So(datastore.Get(ctx, bs), ShouldBeNil) 165 So(bs.Status, ShouldEqual, pb.Status_CANCELED) 166 }) 167 168 Convey("backend task cancellation", func() { 169 So(datastore.Put(ctx, &model.Build{ 170 Proto: &pb.Build{ 171 Id: 1, 172 Builder: &pb.BuilderID{ 173 Project: "project", 174 Bucket: "bucket", 175 Builder: "builder", 176 }, 177 }, 178 }), ShouldBeNil) 179 So(datastore.Put(ctx, &model.BuildInfra{ 180 Build: datastore.MakeKey(ctx, "Build", 1), 181 Proto: &pb.BuildInfra{ 182 Backend: &pb.BuildInfra_Backend{ 183 Hostname: "example.com", 184 Task: &pb.Task{ 185 Id: &pb.TaskID{ 186 Id: "123", 187 Target: "swarming://chromium-swarmin-dev", 188 }, 189 Status: pb.Status_STARTED, 190 }, 191 }, 192 }, 193 }), ShouldBeNil) 194 So(datastore.Put(ctx, &model.BuildStatus{ 195 Build: datastore.MakeKey(ctx, "Build", 1), 196 Status: pb.Status_STARTED, 197 }), ShouldBeNil) 198 bld, err := Cancel(ctx, 1) 199 So(err, ShouldBeNil) 200 So(bld.Proto, ShouldResembleProto, &pb.Build{ 201 Id: 1, 202 Builder: &pb.BuilderID{ 203 Project: "project", 204 Bucket: "bucket", 205 Builder: "builder", 206 }, 207 UpdateTime: timestamppb.New(now), 208 EndTime: timestamppb.New(now), 209 Status: pb.Status_CANCELED, 210 }) 211 So(sch.Tasks(), ShouldHaveLength, 4) 212 bs := &model.BuildStatus{ 213 Build: datastore.MakeKey(ctx, "Build", 1), 214 } 215 So(datastore.Get(ctx, bs), ShouldBeNil) 216 So(bs.Status, ShouldEqual, pb.Status_CANCELED) 217 }) 218 219 Convey("resultdb finalization", func() { 220 So(datastore.Put(ctx, &model.Build{ 221 Proto: &pb.Build{ 222 Id: 1, 223 Builder: &pb.BuilderID{ 224 Project: "project", 225 Bucket: "bucket", 226 Builder: "builder", 227 }, 228 }, 229 }), ShouldBeNil) 230 So(datastore.Put(ctx, &model.BuildInfra{ 231 Build: datastore.MakeKey(ctx, "Build", 1), 232 Proto: &pb.BuildInfra{ 233 Resultdb: &pb.BuildInfra_ResultDB{ 234 Hostname: "example.com", 235 Invocation: "id", 236 }, 237 }, 238 }), ShouldBeNil) 239 So(datastore.Put(ctx, &model.BuildStatus{ 240 Build: datastore.MakeKey(ctx, "Build", 1), 241 Status: pb.Status_STARTED, 242 }), ShouldBeNil) 243 bld, err := Cancel(ctx, 1) 244 So(err, ShouldBeNil) 245 So(bld.Proto, ShouldResembleProto, &pb.Build{ 246 Id: 1, 247 Builder: &pb.BuilderID{ 248 Project: "project", 249 Bucket: "bucket", 250 Builder: "builder", 251 }, 252 UpdateTime: timestamppb.New(now), 253 EndTime: timestamppb.New(now), 254 Status: pb.Status_CANCELED, 255 }) 256 So(sch.Tasks(), ShouldHaveLength, 4) 257 bs := &model.BuildStatus{ 258 Build: datastore.MakeKey(ctx, "Build", 1), 259 } 260 So(datastore.Get(ctx, bs), ShouldBeNil) 261 So(bs.Status, ShouldEqual, pb.Status_CANCELED) 262 }) 263 }) 264 265 Convey("Start", t, func() { 266 ctx := txndefer.FilterRDS(memory.Use(context.Background())) 267 datastore.GetTestable(ctx).AutoIndex(true) 268 datastore.GetTestable(ctx).Consistent(true) 269 ctx, sch := tq.TestingContext(ctx, nil) 270 now := testclock.TestRecentTimeLocal 271 ctx, _ = testclock.UseTime(ctx, now) 272 273 Convey("not found", func() { 274 _, err := StartCancel(ctx, 1, "") 275 So(err, ShouldErrLike, "not found") 276 So(sch.Tasks(), ShouldBeEmpty) 277 }) 278 279 Convey("found", func() { 280 So(datastore.Put(ctx, &model.Build{ 281 Proto: &pb.Build{ 282 Id: 1, 283 Builder: &pb.BuilderID{ 284 Project: "project", 285 Bucket: "bucket", 286 Builder: "builder", 287 }, 288 Status: pb.Status_STARTED, 289 }, 290 }), ShouldBeNil) 291 bld, err := StartCancel(ctx, 1, "summary") 292 So(err, ShouldBeNil) 293 So(bld.Proto, ShouldResembleProto, &pb.Build{ 294 Id: 1, 295 Builder: &pb.BuilderID{ 296 Project: "project", 297 Bucket: "bucket", 298 Builder: "builder", 299 }, 300 UpdateTime: timestamppb.New(now), 301 Status: pb.Status_STARTED, 302 CancelTime: timestamppb.New(now), 303 CanceledBy: "buildbucket", 304 CancellationMarkdown: "summary", 305 }) 306 So(sch.Tasks(), ShouldHaveLength, 1) 307 }) 308 309 Convey("ended", func() { 310 So(datastore.Put(ctx, &model.Build{ 311 Proto: &pb.Build{ 312 Id: 1, 313 Builder: &pb.BuilderID{ 314 Project: "project", 315 Bucket: "bucket", 316 Builder: "builder", 317 }, 318 Status: pb.Status_SUCCESS, 319 }, 320 }), ShouldBeNil) 321 bld, err := StartCancel(ctx, 1, "summary") 322 So(err, ShouldBeNil) 323 So(bld.Proto, ShouldResembleProto, &pb.Build{ 324 Id: 1, 325 Builder: &pb.BuilderID{ 326 Project: "project", 327 Bucket: "bucket", 328 Builder: "builder", 329 }, 330 Status: pb.Status_SUCCESS, 331 }) 332 So(sch.Tasks(), ShouldBeEmpty) 333 }) 334 335 Convey("in cancel process", func() { 336 So(datastore.Put(ctx, &model.Build{ 337 Proto: &pb.Build{ 338 Id: 1, 339 Builder: &pb.BuilderID{ 340 Project: "project", 341 Bucket: "bucket", 342 Builder: "builder", 343 }, 344 Status: pb.Status_STARTED, 345 CancelTime: timestamppb.New(now.Add(-time.Minute)), 346 }, 347 }), ShouldBeNil) 348 bld, err := StartCancel(ctx, 1, "summary") 349 So(err, ShouldBeNil) 350 So(bld.Proto, ShouldResembleProto, &pb.Build{ 351 Id: 1, 352 Builder: &pb.BuilderID{ 353 Project: "project", 354 Bucket: "bucket", 355 Builder: "builder", 356 }, 357 Status: pb.Status_STARTED, 358 CancelTime: timestamppb.New(now.Add(-time.Minute)), 359 }) 360 So(sch.Tasks(), ShouldBeEmpty) 361 }) 362 363 Convey("w/ decedents", func() { 364 So(datastore.Put(ctx, &model.Build{ 365 ID: 1, 366 Proto: &pb.Build{ 367 Id: 1, 368 Builder: &pb.BuilderID{ 369 Project: "project", 370 Bucket: "bucket", 371 Builder: "builder", 372 }, 373 }, 374 }), ShouldBeNil) 375 // Child can outlive parent. 376 So(datastore.Put(ctx, &model.Build{ 377 ID: 2, 378 Proto: &pb.Build{ 379 Id: 2, 380 Builder: &pb.BuilderID{ 381 Project: "project", 382 Bucket: "bucket", 383 Builder: "builder", 384 }, 385 AncestorIds: []int64{1}, 386 CanOutliveParent: true, 387 }, 388 }), ShouldBeNil) 389 // Child cannot outlive parent. 390 So(datastore.Put(ctx, &model.Build{ 391 ID: 3, 392 Proto: &pb.Build{ 393 Id: 3, 394 Builder: &pb.BuilderID{ 395 Project: "project", 396 Bucket: "bucket", 397 Builder: "builder", 398 }, 399 AncestorIds: []int64{1}, 400 CanOutliveParent: false, 401 }, 402 }), ShouldBeNil) 403 // Grandchild. 404 So(datastore.Put(ctx, &model.Build{ 405 ID: 4, 406 Proto: &pb.Build{ 407 Id: 3, 408 Builder: &pb.BuilderID{ 409 Project: "project", 410 Bucket: "bucket", 411 Builder: "builder", 412 }, 413 AncestorIds: []int64{1, 3}, 414 CanOutliveParent: false, 415 }, 416 }), ShouldBeNil) 417 bld, err := StartCancel(ctx, 1, "summary") 418 So(err, ShouldBeNil) 419 So(bld.Proto, ShouldResembleProto, &pb.Build{ 420 Id: 1, 421 Builder: &pb.BuilderID{ 422 Project: "project", 423 Bucket: "bucket", 424 Builder: "builder", 425 }, 426 UpdateTime: timestamppb.New(now), 427 CancelTime: timestamppb.New(now), 428 CancellationMarkdown: "summary", 429 CanceledBy: "buildbucket", 430 }) 431 ids := make([]int, len(sch.Tasks())) 432 for i, task := range sch.Tasks() { 433 switch v := task.Payload.(type) { 434 case *taskdefs.CancelBuildTask: 435 ids[i] = int(v.BuildId) 436 default: 437 panic("unexpected task payload") 438 } 439 } 440 So(sch.Tasks(), ShouldHaveLength, 3) 441 sort.Ints(ids) 442 So(ids, ShouldResemble, []int{1, 3, 4}) 443 }) 444 }) 445 }