go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/update_build_test.go (about) 1 // Copyright 2020 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 rpc 16 17 import ( 18 "context" 19 "sort" 20 "strconv" 21 "strings" 22 "testing" 23 "time" 24 25 "google.golang.org/genproto/protobuf/field_mask" 26 "google.golang.org/grpc/codes" 27 "google.golang.org/grpc/metadata" 28 "google.golang.org/protobuf/types/known/fieldmaskpb" 29 "google.golang.org/protobuf/types/known/structpb" 30 "google.golang.org/protobuf/types/known/timestamppb" 31 32 "go.chromium.org/luci/common/clock/testclock" 33 "go.chromium.org/luci/common/data/strpair" 34 "go.chromium.org/luci/common/errors" 35 "go.chromium.org/luci/common/proto/mask" 36 "go.chromium.org/luci/common/tsmon" 37 "go.chromium.org/luci/gae/filter/featureBreaker" 38 "go.chromium.org/luci/gae/filter/txndefer" 39 "go.chromium.org/luci/gae/impl/memory" 40 "go.chromium.org/luci/gae/service/datastore" 41 "go.chromium.org/luci/server/tq" 42 "go.chromium.org/luci/server/tq/tqtesting" 43 44 "go.chromium.org/luci/buildbucket" 45 "go.chromium.org/luci/buildbucket/appengine/common" 46 "go.chromium.org/luci/buildbucket/appengine/internal/buildtoken" 47 "go.chromium.org/luci/buildbucket/appengine/internal/metrics" 48 "go.chromium.org/luci/buildbucket/appengine/model" 49 taskdefs "go.chromium.org/luci/buildbucket/appengine/tasks/defs" 50 pb "go.chromium.org/luci/buildbucket/proto" 51 "go.chromium.org/luci/buildbucket/protoutil" 52 53 . "github.com/smartystreets/goconvey/convey" 54 . "go.chromium.org/luci/common/testing/assertions" 55 ) 56 57 func TestValidateUpdate(t *testing.T) { 58 t.Parallel() 59 ctx := memory.Use(context.Background()) 60 Convey("validate UpdateMask", t, func() { 61 req := &pb.UpdateBuildRequest{Build: &pb.Build{Id: 1}} 62 63 Convey("succeeds", func() { 64 Convey("with valid paths", func() { 65 req.UpdateMask = &field_mask.FieldMask{Paths: []string{ 66 "build.tags", 67 "build.output", 68 "build.status_details", 69 "build.summary_markdown", 70 }} 71 req.Build.SummaryMarkdown = "this is a string" 72 So(validateUpdate(ctx, req, nil), ShouldBeNil) 73 }) 74 }) 75 76 Convey("fails", func() { 77 Convey("with nil request", func() { 78 So(validateUpdate(ctx, nil, nil), ShouldErrLike, "build.id: required") 79 }) 80 81 Convey("with an invalid path", func() { 82 req.UpdateMask = &field_mask.FieldMask{Paths: []string{ 83 "bucket.name", 84 }} 85 So(validateUpdate(ctx, req, nil), ShouldErrLike, `unsupported path "bucket.name"`) 86 }) 87 88 Convey("with a mix of valid and invalid paths", func() { 89 req.UpdateMask = &field_mask.FieldMask{Paths: []string{ 90 "build.tags", 91 "bucket.name", 92 "build.output", 93 }} 94 So(validateUpdate(ctx, req, nil), ShouldErrLike, `unsupported path "bucket.name"`) 95 }) 96 }) 97 }) 98 99 Convey("validate status", t, func() { 100 req := &pb.UpdateBuildRequest{Build: &pb.Build{Id: 1}} 101 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.status"}} 102 103 Convey("succeeds", func() { 104 req.Build.Status = pb.Status_SUCCESS 105 So(validateUpdate(ctx, req, nil), ShouldBeNil) 106 }) 107 108 Convey("fails", func() { 109 req.Build.Status = pb.Status_SCHEDULED 110 So(validateUpdate(ctx, req, nil), ShouldErrLike, "build.status: invalid status SCHEDULED for UpdateBuild") 111 }) 112 }) 113 114 Convey("validate tags", t, func() { 115 req := &pb.UpdateBuildRequest{Build: &pb.Build{Id: 1}} 116 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.tags"}} 117 req.Build.Tags = []*pb.StringPair{{Key: "ci:builder", Value: ""}} 118 So(validateUpdate(ctx, req, nil), ShouldErrLike, `tag key "ci:builder" cannot have a colon`) 119 }) 120 121 Convey("validate summary_markdown", t, func() { 122 req := &pb.UpdateBuildRequest{Build: &pb.Build{Id: 1}} 123 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.summary_markdown"}} 124 req.Build.SummaryMarkdown = strings.Repeat("☕", protoutil.SummaryMarkdownMaxLength) 125 So(validateUpdate(ctx, req, nil), ShouldErrLike, "too big to accept") 126 }) 127 128 Convey("validate output.gitiles_ommit", t, func() { 129 req := &pb.UpdateBuildRequest{Build: &pb.Build{Id: 1}} 130 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.output.gitiles_commit"}} 131 req.Build.Output = &pb.Build_Output{GitilesCommit: &pb.GitilesCommit{ 132 Project: "project", 133 Host: "host", 134 Id: "id", 135 }} 136 So(validateUpdate(ctx, req, nil), ShouldErrLike, "ref is required") 137 }) 138 139 Convey("validate output.properties", t, func() { 140 req := &pb.UpdateBuildRequest{Build: &pb.Build{Id: 1}} 141 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.output.properties"}} 142 143 Convey("succeeds", func() { 144 props, _ := structpb.NewStruct(map[string]any{"key": "value"}) 145 req.Build.Output = &pb.Build_Output{Properties: props} 146 So(validateUpdate(ctx, req, nil), ShouldBeNil) 147 }) 148 149 Convey("fails", func() { 150 props, _ := structpb.NewStruct(map[string]any{"key": nil}) 151 req.Build.Output = &pb.Build_Output{Properties: props} 152 So(validateUpdate(ctx, req, nil), ShouldErrLike, "value is not set") 153 }) 154 }) 155 156 Convey("validate output.status", t, func() { 157 req := &pb.UpdateBuildRequest{Build: &pb.Build{Id: 1}} 158 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.output.status"}} 159 160 Convey("succeeds", func() { 161 req.Build.Output = &pb.Build_Output{Status: pb.Status_SUCCESS} 162 So(validateUpdate(ctx, req, nil), ShouldBeNil) 163 }) 164 165 Convey("fails", func() { 166 req.Build.Output = &pb.Build_Output{Status: pb.Status_SCHEDULED} 167 So(validateUpdate(ctx, req, nil), ShouldErrLike, "build.output.status: invalid status SCHEDULED for UpdateBuild") 168 }) 169 }) 170 171 Convey("validate output.summary_markdown", t, func() { 172 req := &pb.UpdateBuildRequest{Build: &pb.Build{Id: 1}} 173 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.output.summary_markdown"}} 174 req.Build.Output = &pb.Build_Output{SummaryMarkdown: strings.Repeat("☕", protoutil.SummaryMarkdownMaxLength)} 175 So(validateUpdate(ctx, req, nil), ShouldErrLike, "too big to accept") 176 }) 177 178 Convey("validate output without sub masks", t, func() { 179 req := &pb.UpdateBuildRequest{Build: &pb.Build{Id: 1}} 180 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.output"}} 181 Convey("ok", func() { 182 props, _ := structpb.NewStruct(map[string]any{"key": "value"}) 183 req.Build.Output = &pb.Build_Output{ 184 Properties: props, 185 GitilesCommit: &pb.GitilesCommit{ 186 Host: "host", 187 Project: "project", 188 Ref: "refs/", 189 Position: 1, 190 }, 191 SummaryMarkdown: "summary", 192 } 193 So(validateUpdate(ctx, req, nil), ShouldBeNil) 194 }) 195 Convey("properties is invalid", func() { 196 props, _ := structpb.NewStruct(map[string]any{"key": nil}) 197 req.Build.Output = &pb.Build_Output{ 198 Properties: props, 199 SummaryMarkdown: "summary", 200 } 201 So(validateUpdate(ctx, req, nil), ShouldErrLike, "value is not set") 202 }) 203 Convey("summary_markdown is invalid", func() { 204 req.Build.Output = &pb.Build_Output{ 205 SummaryMarkdown: strings.Repeat("☕", protoutil.SummaryMarkdownMaxLength), 206 } 207 So(validateUpdate(ctx, req, nil), ShouldErrLike, "too big to accept") 208 }) 209 Convey("gitiles_commit is invalid", func() { 210 req.Build.Output = &pb.Build_Output{ 211 GitilesCommit: &pb.GitilesCommit{ 212 Host: "host", 213 Project: "project", 214 Position: 1, 215 }, 216 } 217 So(validateUpdate(ctx, req, nil), ShouldErrLike, "ref is required") 218 }) 219 }) 220 221 Convey("validate steps", t, func() { 222 ts := timestamppb.New(testclock.TestRecentTimeUTC) 223 bs := &model.BuildSteps{ID: 1} 224 req := &pb.UpdateBuildRequest{Build: &pb.Build{Id: 1}} 225 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.steps"}} 226 227 Convey("succeeds", func() { 228 req.Build.Steps = []*pb.Step{ 229 {Name: "step1", Status: pb.Status_SUCCESS, StartTime: ts, EndTime: ts}, 230 {Name: "step2", Status: pb.Status_SUCCESS, StartTime: ts, EndTime: ts}, 231 } 232 So(validateUpdate(ctx, req, bs), ShouldBeNil) 233 }) 234 235 Convey("fails with duplicates", func() { 236 ts := timestamppb.New(testclock.TestRecentTimeUTC) 237 req.Build.Steps = []*pb.Step{ 238 {Name: "step1", Status: pb.Status_SUCCESS, StartTime: ts, EndTime: ts}, 239 {Name: "step1", Status: pb.Status_SUCCESS, StartTime: ts, EndTime: ts}, 240 } 241 So(validateUpdate(ctx, req, bs), ShouldErrLike, `duplicate: "step1"`) 242 }) 243 244 Convey("with a parent step", func() { 245 Convey("before child", func() { 246 req.Build.Steps = []*pb.Step{ 247 {Name: "parent", Status: pb.Status_SUCCESS, StartTime: ts, EndTime: ts}, 248 {Name: "parent|child", Status: pb.Status_SUCCESS, StartTime: ts, EndTime: ts}, 249 } 250 So(validateUpdate(ctx, req, bs), ShouldBeNil) 251 }) 252 Convey("after child", func() { 253 req.Build.Steps = []*pb.Step{ 254 {Name: "parent|child", Status: pb.Status_SUCCESS, StartTime: ts, EndTime: ts}, 255 {Name: "parent", Status: pb.Status_SUCCESS, StartTime: ts, EndTime: ts}, 256 } 257 So(validateUpdate(ctx, req, bs), ShouldErrLike, `parent of "parent|child" must precede`) 258 }) 259 }) 260 }) 261 262 Convey("validate agent output", t, func() { 263 req := &pb.UpdateBuildRequest{ 264 Build: &pb.Build{ 265 Id: 1, 266 Infra: &pb.BuildInfra{ 267 Buildbucket: &pb.BuildInfra_Buildbucket{ 268 Agent: &pb.BuildInfra_Buildbucket_Agent{}, 269 }, 270 }, 271 }, 272 } 273 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.infra.buildbucket.agent.output"}} 274 275 Convey("empty", func() { 276 So(validateUpdate(ctx, req, nil), ShouldErrLike, "agent output is not set while its field path appears in update_mask") 277 }) 278 279 Convey("invalid cipd", func() { 280 // wrong or unresolved version 281 req.Build.Infra.Buildbucket.Agent.Output = &pb.BuildInfra_Buildbucket_Agent_Output{ 282 ResolvedData: map[string]*pb.ResolvedDataRef{ 283 "cipd": { 284 DataType: &pb.ResolvedDataRef_Cipd{ 285 Cipd: &pb.ResolvedDataRef_CIPD{ 286 Specs: []*pb.ResolvedDataRef_CIPD_PkgSpec{{Package: "package", Version: "unresolved_v"}}, 287 }, 288 }, 289 }, 290 }, 291 } 292 So(validateUpdate(ctx, req, nil), ShouldErrLike, `build.infra.buildbucket.agent.output: cipd.version: not a valid package instance ID "unresolved_v"`) 293 294 // wrong or unresolved package name 295 req.Build.Infra.Buildbucket.Agent.Output = &pb.BuildInfra_Buildbucket_Agent_Output{ 296 ResolvedData: map[string]*pb.ResolvedDataRef{ 297 "cipd": { 298 DataType: &pb.ResolvedDataRef_Cipd{ 299 Cipd: &pb.ResolvedDataRef_CIPD{ 300 Specs: []*pb.ResolvedDataRef_CIPD_PkgSpec{{Package: "infra/${platform}", Version: "GwXmwYBjad-WzXEyWWn8HkDsizOPFSH_gjJ35zQaA8IC"}}, 301 }, 302 }, 303 }, 304 }, 305 } 306 So(validateUpdate(ctx, req, nil), ShouldErrLike, `cipd.package: invalid package name "infra/${platform}"`) 307 308 // build.status and agent.output.status conflicts 309 req.Build.Status = pb.Status_CANCELED 310 req.Build.Infra.Buildbucket.Agent.Output.Status = pb.Status_STARTED 311 So(validateUpdate(ctx, req, nil), ShouldErrLike, "build is in an ended status while agent output status is not ended") 312 }) 313 314 Convey("valid", func() { 315 req.Build.Infra.Buildbucket.Agent.Output = &pb.BuildInfra_Buildbucket_Agent_Output{ 316 Status: pb.Status_SUCCESS, 317 AgentPlatform: "linux-amd64", 318 ResolvedData: map[string]*pb.ResolvedDataRef{ 319 "cipd": { 320 DataType: &pb.ResolvedDataRef_Cipd{ 321 Cipd: &pb.ResolvedDataRef_CIPD{ 322 Specs: []*pb.ResolvedDataRef_CIPD_PkgSpec{ 323 {Package: "infra/tools/git/linux-amd64", Version: "GwXmwYBjad-WzXEyWWn8HkDsizOPFSH_gjJ35zQaA8IC"}}, 324 }, 325 }, 326 }, 327 }, 328 } 329 So(validateUpdate(ctx, req, nil), ShouldBeNil) 330 }) 331 332 }) 333 334 Convey("validate agent purpose", t, func() { 335 req := &pb.UpdateBuildRequest{ 336 Build: &pb.Build{ 337 Id: 1, 338 Infra: &pb.BuildInfra{ 339 Buildbucket: &pb.BuildInfra_Buildbucket{ 340 Agent: &pb.BuildInfra_Buildbucket_Agent{}, 341 }, 342 }, 343 }, 344 } 345 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.infra.buildbucket.agent.purposes"}} 346 347 datastore.GetTestable(ctx).AutoIndex(true) 348 datastore.GetTestable(ctx).Consistent(true) 349 So(datastore.Put(ctx, &model.BuildInfra{ 350 Build: datastore.KeyForObj(ctx, &model.Build{ID: req.Build.Id}), 351 Proto: &pb.BuildInfra{ 352 Buildbucket: &pb.BuildInfra_Buildbucket{ 353 Agent: &pb.BuildInfra_Buildbucket_Agent{ 354 Input: &pb.BuildInfra_Buildbucket_Agent_Input{ 355 Data: map[string]*pb.InputDataRef{"p1": {}}, 356 }, 357 }, 358 }, 359 }, 360 }), ShouldBeNil) 361 362 Convey("nil", func() { 363 So(validateUpdate(ctx, req, nil), ShouldErrLike, "build.infra.buildbucket.agent.purposes: not set") 364 }) 365 366 Convey("invalid agent purpose", func() { 367 req.Build.Infra.Buildbucket.Agent.Purposes = map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{ 368 "random_p": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD, 369 } 370 So(validateUpdate(ctx, req, nil), ShouldErrLike, "build.infra.buildbucket.agent.purposes: Invalid path random_p - not in either input or output dataRef") 371 }) 372 373 Convey("valid", func() { 374 // in input data. 375 req.Build.Infra.Buildbucket.Agent.Purposes = map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{ 376 "p1": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD, 377 } 378 So(validateUpdate(ctx, req, nil), ShouldBeNil) 379 380 // in output data 381 req.UpdateMask.Paths = append(req.UpdateMask.Paths, "build.infra.buildbucket.agent.output") 382 req.Build.Infra.Buildbucket.Agent.Output = &pb.BuildInfra_Buildbucket_Agent_Output{ 383 ResolvedData: map[string]*pb.ResolvedDataRef{ 384 "output_p1": {}}, 385 } 386 req.Build.Infra.Buildbucket.Agent.Purposes = map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{ 387 "output_p1": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD, 388 } 389 So(validateUpdate(ctx, req, nil), ShouldBeNil) 390 }) 391 }) 392 } 393 394 func TestValidateStep(t *testing.T) { 395 t.Parallel() 396 397 Convey("validate", t, func() { 398 ts := timestamppb.New(testclock.TestRecentTimeUTC) 399 step := &pb.Step{Name: "step1"} 400 bStatus := pb.Status_STARTED 401 402 Convey("with status unspecified", func() { 403 step.Status = pb.Status_STATUS_UNSPECIFIED 404 So(validateStep(step, nil, bStatus), ShouldErrLike, "status: is unspecified or unknown") 405 }) 406 407 Convey("with status ENDED_MASK", func() { 408 step.Status = pb.Status_ENDED_MASK 409 So(validateStep(step, nil, bStatus), ShouldErrLike, "status: must not be ENDED_MASK") 410 }) 411 412 Convey("with non-terminal status", func() { 413 Convey("without start_time, when should have", func() { 414 step.Status = pb.Status_STARTED 415 So(validateStep(step, nil, bStatus), ShouldErrLike, `start_time: required by status "STARTED"`) 416 }) 417 418 Convey("with start_time, when should not have", func() { 419 step.Status = pb.Status_SCHEDULED 420 step.StartTime = ts 421 So(validateStep(step, nil, bStatus), ShouldErrLike, `start_time: must not be specified for status "SCHEDULED"`) 422 }) 423 424 Convey("with terminal build status", func() { 425 bStatus = pb.Status_SUCCESS 426 step.Status = pb.Status_STARTED 427 So(validateStep(step, nil, bStatus), ShouldErrLike, `status: cannot be "STARTED" because the build has a terminal status "SUCCESS"`) 428 }) 429 430 }) 431 432 Convey("with terminal status", func() { 433 step.Status = pb.Status_INFRA_FAILURE 434 435 Convey("missing start_time, but end_time", func() { 436 step.EndTime = ts 437 So(validateStep(step, nil, bStatus), ShouldErrLike, `start_time: required by status "INFRA_FAILURE"`) 438 }) 439 440 Convey("missing end_time", func() { 441 step.StartTime = ts 442 So(validateStep(step, nil, bStatus), ShouldErrLike, "end_time: must have both or neither end_time and a terminal status") 443 }) 444 445 Convey("end_time is before start_time", func() { 446 step.EndTime = ts 447 sts := timestamppb.New(testclock.TestRecentTimeUTC.AddDate(0, 0, 1)) 448 step.StartTime = sts 449 So(validateStep(step, nil, bStatus), ShouldErrLike, "end_time: is before the start_time") 450 }) 451 }) 452 453 Convey("with logs", func() { 454 step.Status = pb.Status_STARTED 455 step.StartTime = ts 456 457 Convey("missing name", func() { 458 step.Logs = []*pb.Log{{Url: "url", ViewUrl: "view_url"}} 459 So(validateStep(step, nil, bStatus), ShouldErrLike, "logs[0].name: required") 460 }) 461 462 Convey("missing url", func() { 463 step.Logs = []*pb.Log{{Name: "name", ViewUrl: "view_url"}} 464 So(validateStep(step, nil, bStatus), ShouldErrLike, "logs[0].url: required") 465 }) 466 467 Convey("missing view_url", func() { 468 step.Logs = []*pb.Log{{Name: "name", Url: "url"}} 469 So(validateStep(step, nil, bStatus), ShouldErrLike, "logs[0].view_url: required") 470 }) 471 472 Convey("duplicate name", func() { 473 step.Logs = []*pb.Log{ 474 {Name: "name", Url: "url", ViewUrl: "view_url"}, 475 {Name: "name", Url: "url", ViewUrl: "view_url"}, 476 } 477 So(validateStep(step, nil, bStatus), ShouldErrLike, `logs[1].name: duplicate: "name"`) 478 }) 479 }) 480 481 Convey("with tags", func() { 482 step.Status = pb.Status_STARTED 483 step.StartTime = ts 484 485 Convey("missing key", func() { 486 step.Tags = []*pb.StringPair{{Value: "hi"}} 487 So(validateStep(step, nil, bStatus), ShouldErrLike, "tags[0].key: required") 488 }) 489 490 Convey("reserved key", func() { 491 step.Tags = []*pb.StringPair{{Key: "luci.something", Value: "hi"}} 492 So(validateStep(step, nil, bStatus), ShouldErrLike, "tags[0].key: reserved prefix") 493 }) 494 495 Convey("missing value", func() { 496 step.Tags = []*pb.StringPair{{Key: "my-service.tag"}} 497 So(validateStep(step, nil, bStatus), ShouldErrLike, "tags[0].value: required") 498 }) 499 500 Convey("long key", func() { 501 step.Tags = []*pb.StringPair{{ 502 // len=297 503 Key: ("my-service.my-service.my-service.my-service.my-service.my-service.my-service.my-service.my-service." + 504 "my-service.my-service.my-service.my-service.my-service.my-service.my-service.my-service.my-service." + 505 "my-service.my-service.my-service.my-service.my-service.my-service.my-service.my-service.my-service."), 506 Value: "yo", 507 }} 508 So(validateStep(step, nil, bStatus), ShouldErrLike, "tags[0].key: len > 256") 509 }) 510 511 Convey("long value", func() { 512 step.Tags = []*pb.StringPair{{Key: "my-service.tag", Value: strings.Repeat("derp", 500)}} 513 So(validateStep(step, nil, bStatus), ShouldErrLike, "tags[0].value: len > 1024") 514 }) 515 }) 516 }) 517 } 518 519 func TestCheckBuildForUpdate(t *testing.T) { 520 t.Parallel() 521 updateMask := func(req *pb.UpdateBuildRequest) *mask.Mask { 522 fm, err := mask.FromFieldMask(req.UpdateMask, req, false, true) 523 So(err, ShouldBeNil) 524 return fm.MustSubmask("build") 525 } 526 527 Convey("checkBuildForUpdate", t, func() { 528 ctx := metrics.WithServiceInfo(memory.Use(context.Background()), "sv", "job", "ins") 529 datastore.GetTestable(ctx).AutoIndex(true) 530 datastore.GetTestable(ctx).Consistent(true) 531 532 build := &model.Build{ 533 ID: 1, 534 Proto: &pb.Build{ 535 Id: 1, 536 Builder: &pb.BuilderID{ 537 Project: "project", 538 Bucket: "bucket", 539 Builder: "builder", 540 }, 541 Status: pb.Status_SCHEDULED, 542 }, 543 CreateTime: testclock.TestRecentTimeUTC, 544 } 545 So(datastore.Put(ctx, build), ShouldBeNil) 546 req := &pb.UpdateBuildRequest{Build: &pb.Build{Id: 1}} 547 548 Convey("works", func() { 549 b, err := common.GetBuild(ctx, 1) 550 So(err, ShouldBeNil) 551 err = checkBuildForUpdate(updateMask(req), req, b) 552 So(err, ShouldBeNil) 553 554 Convey("with build.steps", func() { 555 req.Build.Status = pb.Status_STARTED 556 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.status", "build.steps"}} 557 b, err := common.GetBuild(ctx, 1) 558 So(err, ShouldBeNil) 559 err = checkBuildForUpdate(updateMask(req), req, b) 560 So(err, ShouldBeNil) 561 }) 562 Convey("with build.output", func() { 563 req.Build.Status = pb.Status_STARTED 564 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.status", "build.output"}} 565 b, err := common.GetBuild(ctx, 1) 566 So(err, ShouldBeNil) 567 err = checkBuildForUpdate(updateMask(req), req, b) 568 So(err, ShouldBeNil) 569 }) 570 }) 571 572 Convey("fails", func() { 573 Convey("if ended", func() { 574 build.Proto.Status = pb.Status_SUCCESS 575 So(datastore.Put(ctx, build), ShouldBeNil) 576 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.status"}} 577 b, err := common.GetBuild(ctx, 1) 578 So(err, ShouldBeNil) 579 err = checkBuildForUpdate(updateMask(req), req, b) 580 So(err, ShouldBeRPCFailedPrecondition, "cannot update an ended build") 581 }) 582 583 Convey("with build.steps", func() { 584 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.steps"}} 585 b, err := common.GetBuild(ctx, 1) 586 So(err, ShouldBeNil) 587 err = checkBuildForUpdate(updateMask(req), req, b) 588 So(err, ShouldBeRPCInvalidArgument, "cannot update steps of a SCHEDULED build") 589 }) 590 Convey("with build.output", func() { 591 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.output.properties"}} 592 b, err := common.GetBuild(ctx, 1) 593 So(err, ShouldBeNil) 594 err = checkBuildForUpdate(updateMask(req), req, b) 595 So(err, ShouldBeRPCInvalidArgument, "cannot update build output fields of a SCHEDULED build") 596 }) 597 Convey("with build.infra.buildbucket.agent.output", func() { 598 req.UpdateMask = &field_mask.FieldMask{Paths: []string{"build.infra.buildbucket.agent.output"}} 599 b, err := common.GetBuild(ctx, 1) 600 So(err, ShouldBeNil) 601 err = checkBuildForUpdate(updateMask(req), req, b) 602 So(err, ShouldBeRPCInvalidArgument, "cannot update agent output of a SCHEDULED build") 603 }) 604 }) 605 }) 606 } 607 608 func TestUpdateBuild(t *testing.T) { 609 610 updateContextForNewBuildToken := func(ctx context.Context, buildID int64) (string, context.Context) { 611 newToken, _ := buildtoken.GenerateToken(ctx, buildID, pb.TokenBody_BUILD) 612 ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, newToken)) 613 return newToken, ctx 614 } 615 sortTasksByClassName := func(tasks tqtesting.TaskList) { 616 sort.Slice(tasks, func(i, j int) bool { 617 return tasks[i].Class < tasks[j].Class 618 }) 619 } 620 621 t.Parallel() 622 623 getBuildWithDetails := func(ctx context.Context, bid int64) *model.Build { 624 b, err := common.GetBuild(ctx, bid) 625 So(err, ShouldBeNil) 626 // ensure that the below fields were cleared when the build was saved. 627 So(b.Proto.Tags, ShouldBeNil) 628 So(b.Proto.Steps, ShouldBeNil) 629 if b.Proto.Output != nil { 630 So(b.Proto.Output.Properties, ShouldBeNil) 631 } 632 m := model.HardcodedBuildMask("output.properties", "steps", "tags", "infra") 633 So(model.LoadBuildDetails(ctx, m, nil, b.Proto), ShouldBeNil) 634 return b 635 } 636 637 Convey("UpdateBuild", t, func() { 638 srv := &Builds{} 639 ctx := memory.Use(context.Background()) 640 ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins") 641 ctx = installTestSecret(ctx) 642 643 tk, ctx := updateContextForNewBuildToken(ctx, 1) 644 datastore.GetTestable(ctx).AutoIndex(true) 645 datastore.GetTestable(ctx).Consistent(true) 646 ctx, _ = tsmon.WithDummyInMemory(ctx) 647 store := tsmon.Store(ctx) 648 ctx = txndefer.FilterRDS(ctx) 649 ctx, sch := tq.TestingContext(ctx, nil) 650 651 t0 := testclock.TestRecentTimeUTC 652 ctx, tclock := testclock.UseTime(ctx, t0) 653 654 // helper function to call UpdateBuild. 655 updateBuild := func(ctx context.Context, req *pb.UpdateBuildRequest) error { 656 _, err := srv.UpdateBuild(ctx, req) 657 return err 658 } 659 660 // create and save a sample build in the datastore 661 build := &model.Build{ 662 ID: 1, 663 Proto: &pb.Build{ 664 Id: 1, 665 Builder: &pb.BuilderID{ 666 Project: "project", 667 Bucket: "bucket", 668 Builder: "builder", 669 }, 670 Status: pb.Status_STARTED, 671 Output: &pb.Build_Output{ 672 Status: pb.Status_STARTED, 673 }, 674 }, 675 CreateTime: t0, 676 UpdateToken: tk, 677 } 678 bk := datastore.KeyForObj(ctx, build) 679 infra := &model.BuildInfra{ 680 Build: bk, 681 Proto: &pb.BuildInfra{ 682 Buildbucket: &pb.BuildInfra_Buildbucket{ 683 Hostname: "bbhost", 684 Agent: &pb.BuildInfra_Buildbucket_Agent{ 685 Input: &pb.BuildInfra_Buildbucket_Agent_Input{ 686 Data: map[string]*pb.InputDataRef{}, 687 }, 688 }, 689 }, 690 }, 691 } 692 bs := &model.BuildStatus{ 693 Build: bk, 694 Status: pb.Status_STARTED, 695 } 696 So(datastore.Put(ctx, build, infra, bs), ShouldBeNil) 697 698 req := &pb.UpdateBuildRequest{ 699 Build: &pb.Build{Id: 1, SummaryMarkdown: "summary"}, 700 UpdateMask: &field_mask.FieldMask{Paths: []string{ 701 "build.summary_markdown", 702 }}, 703 } 704 705 Convey("wrong purpose token", func() { 706 tk, _ = buildtoken.GenerateToken(ctx, 1, pb.TokenBody_START_BUILD) 707 ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, tk)) 708 So(updateBuild(ctx, req), ShouldErrLike, "invalid token") 709 }) 710 711 Convey("open mask, empty request", func() { 712 validMasks := []struct { 713 name string 714 err string 715 }{ 716 {"build.output", ""}, 717 {"build.output.properties", ""}, 718 {"build.output.status", "invalid status STATUS_UNSPECIFIED"}, 719 {"build.status", "invalid status STATUS_UNSPECIFIED"}, 720 {"build.output.status_details", ""}, 721 {"build.status_details", ""}, 722 {"build.steps", ""}, 723 {"build.output.summary_markdown", ""}, 724 {"build.summary_markdown", ""}, 725 {"build.tags", ""}, 726 {"build.output.gitiles_commit", "ref is required"}, 727 } 728 for _, test := range validMasks { 729 Convey(test.name, func() { 730 req.UpdateMask.Paths[0] = test.name 731 err := updateBuild(ctx, req) 732 if test.err == "" { 733 So(err, ShouldBeNil) 734 } else { 735 So(err, ShouldErrLike, test.err) 736 } 737 }) 738 } 739 }) 740 741 Convey("build.update_time is always updated", func() { 742 req.UpdateMask = nil 743 So(updateBuild(ctx, req), ShouldBeNil) 744 b, err := common.GetBuild(ctx, req.Build.Id) 745 So(err, ShouldBeNil) 746 So(b.Proto.UpdateTime, ShouldResembleProto, timestamppb.New(t0)) 747 So(b.Proto.Status, ShouldEqual, pb.Status_STARTED) 748 749 tclock.Add(time.Second) 750 751 So(updateBuild(ctx, req), ShouldBeNil) 752 b, err = common.GetBuild(ctx, req.Build.Id) 753 So(err, ShouldBeNil) 754 So(b.Proto.UpdateTime, ShouldResembleProto, timestamppb.New(t0.Add(time.Second))) 755 }) 756 757 Convey("build.view_url", func() { 758 url := "https://redirect.com" 759 req.Build.ViewUrl = url 760 req.UpdateMask.Paths[0] = "build.view_url" 761 So(updateBuild(ctx, req), ShouldBeRPCOK) 762 b, err := common.GetBuild(ctx, req.Build.Id) 763 So(err, ShouldBeNil) 764 So(b.Proto.ViewUrl, ShouldEqual, url) 765 }) 766 767 Convey("build.output.properties", func() { 768 props, err := structpb.NewStruct(map[string]any{"key": "value"}) 769 So(err, ShouldBeNil) 770 req.Build.Output = &pb.Build_Output{Properties: props} 771 772 Convey("with mask", func() { 773 req.UpdateMask.Paths[0] = "build.output.properties" 774 So(updateBuild(ctx, req), ShouldBeRPCOK) 775 b := getBuildWithDetails(ctx, req.Build.Id) 776 m, err := structpb.NewStruct(map[string]any{"key": "value"}) 777 So(err, ShouldBeNil) 778 So(b.Proto.Output.Properties, ShouldResembleProto, m) 779 }) 780 781 Convey("without mask", func() { 782 So(updateBuild(ctx, req), ShouldBeRPCOK) 783 b := getBuildWithDetails(ctx, req.Build.Id) 784 So(b.Proto.Output.Properties, ShouldBeNil) 785 }) 786 787 }) 788 789 Convey("build.output.properties large", func() { 790 largeProps, err := structpb.NewStruct(map[string]any{}) 791 So(err, ShouldBeNil) 792 k := "laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarge_key" 793 v := "laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarge_value" 794 for i := 0; i < 10000; i++ { 795 largeProps.Fields[k+strconv.Itoa(i)] = &structpb.Value{ 796 Kind: &structpb.Value_StringValue{ 797 StringValue: v, 798 }, 799 } 800 } 801 So(err, ShouldBeNil) 802 req.Build.Output = &pb.Build_Output{Properties: largeProps} 803 804 Convey("with mask", func() { 805 req.UpdateMask.Paths[0] = "build.output" 806 So(updateBuild(ctx, req), ShouldBeRPCOK) 807 b := getBuildWithDetails(ctx, req.Build.Id) 808 So(b.Proto.Output.Properties, ShouldResembleProto, largeProps) 809 count, err := datastore.Count(ctx, datastore.NewQuery("PropertyChunk")) 810 So(err, ShouldBeNil) 811 So(count, ShouldEqual, 1) 812 }) 813 814 Convey("without mask", func() { 815 So(updateBuild(ctx, req), ShouldBeRPCOK) 816 b := getBuildWithDetails(ctx, req.Build.Id) 817 So(b.Proto.Output.Properties, ShouldBeNil) 818 }) 819 }) 820 821 Convey("build.steps", func() { 822 step := &pb.Step{ 823 Name: "step", 824 StartTime: ×tamppb.Timestamp{Seconds: 1}, 825 EndTime: ×tamppb.Timestamp{Seconds: 12}, 826 Status: pb.Status_SUCCESS, 827 } 828 req.Build.Steps = []*pb.Step{step} 829 830 Convey("with mask", func() { 831 req.UpdateMask.Paths[0] = "build.steps" 832 So(updateBuild(ctx, req), ShouldBeRPCOK) 833 b := getBuildWithDetails(ctx, req.Build.Id) 834 So(b.Proto.Steps[0], ShouldResembleProto, step) 835 }) 836 837 Convey("without mask", func() { 838 So(updateBuild(ctx, req), ShouldBeRPCOK) 839 b := getBuildWithDetails(ctx, req.Build.Id) 840 So(b.Proto.Steps, ShouldBeNil) 841 }) 842 843 Convey("incomplete steps with non-terminal Build status", func() { 844 req.UpdateMask.Paths = []string{"build.status", "build.steps"} 845 req.Build.Status = pb.Status_STARTED 846 req.Build.Steps[0].Status = pb.Status_STARTED 847 req.Build.Steps[0].EndTime = nil 848 So(updateBuild(ctx, req), ShouldBeRPCOK) 849 }) 850 851 Convey("incomplete steps with terminal Build status", func() { 852 req.UpdateMask.Paths = []string{"build.status", "build.steps"} 853 req.Build.Status = pb.Status_SUCCESS 854 855 Convey("with mask", func() { 856 req.Build.Steps[0].Status = pb.Status_STARTED 857 req.Build.Steps[0].EndTime = nil 858 859 // Should be rejected. 860 msg := `cannot be "STARTED" because the build has a terminal status "SUCCESS"` 861 So(updateBuild(ctx, req), ShouldHaveRPCCode, codes.InvalidArgument, msg) 862 }) 863 864 Convey("w/o mask", func() { 865 // update the build with incomplete steps first. 866 req.Build.Status = pb.Status_STARTED 867 req.Build.Steps[0].Status = pb.Status_STARTED 868 req.Build.Steps[0].EndTime = nil 869 So(updateBuild(ctx, req), ShouldBeRPCOK) 870 871 // update the build again with a terminal status, but w/o step mask. 872 req.UpdateMask.Paths = []string{"build.status"} 873 req.Build.Status = pb.Status_SUCCESS 874 So(updateBuild(ctx, req), ShouldBeRPCOK) 875 nbs := &model.BuildStatus{Build: bk} 876 err := datastore.Get(ctx, nbs) 877 So(err, ShouldBeNil) 878 So(nbs.Status, ShouldEqual, pb.Status_SUCCESS) 879 880 // the step should have been cancelled. 881 b := getBuildWithDetails(ctx, req.Build.Id) 882 expected := &pb.Step{ 883 Name: step.Name, 884 Status: pb.Status_CANCELED, 885 StartTime: step.StartTime, 886 EndTime: timestamppb.New(t0), 887 } 888 So(b.Proto.Steps[0], ShouldResembleProto, expected) 889 }) 890 }) 891 }) 892 893 Convey("build.tags", func() { 894 tag := &pb.StringPair{Key: "resultdb", Value: "disabled"} 895 req.Build.Tags = []*pb.StringPair{tag} 896 897 Convey("with mask", func() { 898 req.UpdateMask.Paths[0] = "build.tags" 899 So(updateBuild(ctx, req), ShouldBeRPCOK) 900 901 b := getBuildWithDetails(ctx, req.Build.Id) 902 expected := []string{strpair.Format("resultdb", "disabled")} 903 So(b.Tags, ShouldResemble, expected) 904 905 // change the value and update it again 906 tag.Value = "enabled" 907 So(updateBuild(ctx, req), ShouldBeRPCOK) 908 909 // both tags should exist 910 b = getBuildWithDetails(ctx, req.Build.Id) 911 expected = append(expected, strpair.Format("resultdb", "enabled")) 912 So(b.Tags, ShouldResemble, expected) 913 }) 914 915 Convey("without mask", func() { 916 So(updateBuild(ctx, req), ShouldBeRPCOK) 917 b := getBuildWithDetails(ctx, req.Build.Id) 918 So(b.Tags, ShouldBeNil) 919 }) 920 }) 921 922 Convey("build.infra.buildbucket.agent.output", func() { 923 agentOutput := &pb.BuildInfra_Buildbucket_Agent_Output{ 924 Status: pb.Status_SUCCESS, 925 AgentPlatform: "linux-amd64", 926 ResolvedData: map[string]*pb.ResolvedDataRef{ 927 "cipd": { 928 DataType: &pb.ResolvedDataRef_Cipd{ 929 Cipd: &pb.ResolvedDataRef_CIPD{ 930 Specs: []*pb.ResolvedDataRef_CIPD_PkgSpec{{Package: "infra/tools/git/linux-amd64", Version: "GwXmwYBjad-WzXEyWWn8HkDsizOPFSH_gjJ35zQaA8IC"}}, 931 }, 932 }, 933 }, 934 }, 935 } 936 req.Build.Infra = &pb.BuildInfra{ 937 Buildbucket: &pb.BuildInfra_Buildbucket{ 938 Agent: &pb.BuildInfra_Buildbucket_Agent{ 939 Output: agentOutput, 940 }, 941 }, 942 } 943 req.Build.Input = &pb.Build_Input{ 944 Experiments: []string{"luci.buildbucket.agent.cipd_installation"}, 945 } 946 947 Convey("with mask", func() { 948 req.UpdateMask.Paths[0] = "build.infra.buildbucket.agent.output" 949 So(updateBuild(ctx, req), ShouldBeRPCOK) 950 b := getBuildWithDetails(ctx, req.Build.Id) 951 So(b.Proto.Infra.Buildbucket.Agent.Output, ShouldResembleProto, agentOutput) 952 }) 953 954 Convey("without mask", func() { 955 So(updateBuild(ctx, req), ShouldBeRPCOK) 956 b := getBuildWithDetails(ctx, req.Build.Id) 957 So(b.Proto.Infra.Buildbucket.Agent.Output, ShouldBeNil) 958 }) 959 960 }) 961 962 Convey("build.infra.buildbucket.agent.purposes", func() { 963 req.Build.Infra = &pb.BuildInfra{ 964 Buildbucket: &pb.BuildInfra_Buildbucket{ 965 Agent: &pb.BuildInfra_Buildbucket_Agent{ 966 Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{ 967 "p1": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD, 968 }, 969 Output: &pb.BuildInfra_Buildbucket_Agent_Output{ 970 ResolvedData: map[string]*pb.ResolvedDataRef{ 971 "p1": {}, 972 }, 973 }, 974 }, 975 }, 976 } 977 978 Convey("with mask", func() { 979 req.UpdateMask = &field_mask.FieldMask{Paths: []string{ 980 "build.infra.buildbucket.agent.output", 981 "build.infra.buildbucket.agent.purposes", 982 }} 983 So(updateBuild(ctx, req), ShouldBeRPCOK) 984 b := getBuildWithDetails(ctx, req.Build.Id) 985 So(b.Proto.Infra.Buildbucket.Agent.Purposes["p1"], ShouldEqual, pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD) 986 }) 987 988 Convey("without mask", func() { 989 So(updateBuild(ctx, req), ShouldBeRPCOK) 990 b := getBuildWithDetails(ctx, req.Build.Id) 991 So(b.Proto.Infra.Buildbucket.Agent.Purposes, ShouldBeNil) 992 }) 993 }) 994 995 Convey("build-start event", func() { 996 Convey("Status_STARTED w/o status change", func() { 997 req.UpdateMask.Paths[0] = "build.status" 998 req.Build.Status = pb.Status_STARTED 999 So(updateBuild(ctx, req), ShouldBeRPCOK) 1000 1001 // no TQ tasks should be scheduled. 1002 So(sch.Tasks(), ShouldBeEmpty) 1003 1004 // no metric update, either. 1005 So(store.Get(ctx, metrics.V1.BuildCountStarted, time.Time{}, fv(false)), ShouldEqual, nil) 1006 }) 1007 1008 Convey("Status_STARTED w/ status change", func() { 1009 // create a sample task with SCHEDULED. 1010 build.Proto.Id++ 1011 build.ID++ 1012 tk, ctx = updateContextForNewBuildToken(ctx, build.ID) 1013 build.UpdateToken = tk 1014 build.Proto.Status, build.Status = pb.Status_SCHEDULED, pb.Status_SCHEDULED 1015 buildStatus := &model.BuildStatus{ 1016 Build: datastore.KeyForObj(ctx, build), 1017 Status: pb.Status_SCHEDULED, 1018 } 1019 So(datastore.Put(ctx, build, buildStatus), ShouldBeNil) 1020 1021 // update it with STARTED 1022 req.Build.Id = build.ID 1023 req.UpdateMask.Paths[0] = "build.status" 1024 req.Build.Status = pb.Status_STARTED 1025 So(updateBuild(ctx, req), ShouldBeRPCOK) 1026 1027 // TQ tasks for pubsub-notification. 1028 tasks := sch.Tasks() 1029 sortTasksByClassName(tasks) 1030 So(tasks, ShouldHaveLength, 2) 1031 So(tasks[0].Payload.(*taskdefs.NotifyPubSub).GetBuildId(), ShouldEqual, build.ID) 1032 So(tasks[1].Payload.(*taskdefs.NotifyPubSubGoProxy).GetBuildId(), ShouldEqual, 2) 1033 So(tasks[1].Payload.(*taskdefs.NotifyPubSubGoProxy).GetProject(), ShouldEqual, "project") 1034 1035 // BuildStarted metric should be set 1. 1036 So(store.Get(ctx, metrics.V1.BuildCountStarted, time.Time{}, fv(false)), ShouldEqual, 1) 1037 1038 // BuildStatus should be updated. 1039 buildStatus = &model.BuildStatus{Build: datastore.KeyForObj(ctx, build)} 1040 So(datastore.Get(ctx, buildStatus), ShouldBeNil) 1041 So(buildStatus.Status, ShouldEqual, pb.Status_STARTED) 1042 }) 1043 1044 Convey("output.status Status_STARTED w/o status change", func() { 1045 req.UpdateMask.Paths[0] = "build.output.status" 1046 req.Build.Output = &pb.Build_Output{Status: pb.Status_STARTED} 1047 So(updateBuild(ctx, req), ShouldBeRPCOK) 1048 1049 // no TQ tasks should be scheduled. 1050 So(sch.Tasks(), ShouldBeEmpty) 1051 1052 // no metric update, either. 1053 So(store.Get(ctx, metrics.V1.BuildCountStarted, time.Time{}, fv(false)), ShouldEqual, nil) 1054 }) 1055 1056 Convey("output.status Status_STARTED w/ status change", func() { 1057 // create a sample task with SCHEDULED. 1058 build.Proto.Id++ 1059 build.ID++ 1060 tk, ctx = updateContextForNewBuildToken(ctx, build.ID) 1061 build.UpdateToken = tk 1062 build.Proto.Status, build.Status = pb.Status_SCHEDULED, pb.Status_SCHEDULED 1063 buildStatus := &model.BuildStatus{ 1064 Build: datastore.KeyForObj(ctx, build), 1065 Status: pb.Status_SCHEDULED, 1066 } 1067 So(datastore.Put(ctx, build, buildStatus), ShouldBeNil) 1068 1069 // update it with STARTED 1070 req.Build.Id = build.ID 1071 req.UpdateMask.Paths[0] = "build.output.status" 1072 req.Build.Output = &pb.Build_Output{Status: pb.Status_STARTED} 1073 So(updateBuild(ctx, req), ShouldBeRPCOK) 1074 1075 // TQ tasks for pubsub-notification. 1076 tasks := sch.Tasks() 1077 sortTasksByClassName(tasks) 1078 So(tasks, ShouldHaveLength, 2) 1079 So(tasks[0].Payload.(*taskdefs.NotifyPubSub).GetBuildId(), ShouldEqual, build.ID) 1080 So(tasks[1].Payload.(*taskdefs.NotifyPubSubGoProxy).GetBuildId(), ShouldEqual, 2) 1081 So(tasks[1].Payload.(*taskdefs.NotifyPubSubGoProxy).GetProject(), ShouldEqual, "project") 1082 1083 // BuildStarted metric should be set 1. 1084 So(store.Get(ctx, metrics.V1.BuildCountStarted, time.Time{}, fv(false)), ShouldEqual, 1) 1085 1086 // BuildStatus should be updated. 1087 buildStatus = &model.BuildStatus{Build: datastore.KeyForObj(ctx, build)} 1088 So(datastore.Get(ctx, build, buildStatus), ShouldBeNil) 1089 So(buildStatus.Status, ShouldEqual, pb.Status_STARTED) 1090 So(build.Proto.Status, ShouldEqual, pb.Status_STARTED) 1091 }) 1092 }) 1093 1094 Convey("build-completion event", func() { 1095 Convey("Status_SUCCESSS w/ status change", func() { 1096 req.UpdateMask.Paths[0] = "build.status" 1097 req.Build.Status = pb.Status_SUCCESS 1098 So(updateBuild(ctx, req), ShouldBeRPCOK) 1099 1100 // TQ tasks for pubsub-notification, bq-export, and invocation-finalization. 1101 tasks := sch.Tasks() 1102 So(tasks, ShouldHaveLength, 4) 1103 sum := 0 1104 for _, task := range tasks { 1105 switch v := task.Payload.(type) { 1106 case *taskdefs.NotifyPubSub: 1107 sum++ 1108 So(v.GetBuildId(), ShouldEqual, req.Build.Id) 1109 case *taskdefs.ExportBigQuery: 1110 sum += 2 1111 So(v.GetBuildId(), ShouldEqual, req.Build.Id) 1112 case *taskdefs.FinalizeResultDBGo: 1113 sum += 4 1114 So(v.GetBuildId(), ShouldEqual, req.Build.Id) 1115 case *taskdefs.NotifyPubSubGoProxy: 1116 sum += 8 1117 So(v.GetBuildId(), ShouldEqual, req.Build.Id) 1118 default: 1119 panic("invalid task payload") 1120 } 1121 } 1122 So(sum, ShouldEqual, 15) 1123 1124 // BuildCompleted metric should be set to 1 with SUCCESS. 1125 fvs := fv(model.Success.String(), "", "", false) 1126 So(store.Get(ctx, metrics.V1.BuildCountCompleted, time.Time{}, fvs), ShouldEqual, 1) 1127 }) 1128 Convey("output.status Status_SUCCESSS w/ status change", func() { 1129 buildStatus := &model.BuildStatus{ 1130 Build: datastore.KeyForObj(ctx, build), 1131 Status: pb.Status_STARTED, 1132 } 1133 So(datastore.Put(ctx, build, buildStatus), ShouldBeNil) 1134 1135 req.UpdateMask.Paths[0] = "build.output.status" 1136 req.Build.Output = &pb.Build_Output{Status: pb.Status_SUCCESS} 1137 So(updateBuild(ctx, req), ShouldBeRPCOK) 1138 1139 // TQ tasks for pubsub-notification, bq-export, and invocation-finalization. 1140 tasks := sch.Tasks() 1141 So(tasks, ShouldHaveLength, 0) 1142 1143 // BuildCompleted metric should not be set. 1144 fvs := fv(model.Success.String(), "", "", false) 1145 So(store.Get(ctx, metrics.V1.BuildCountCompleted, time.Time{}, fvs), ShouldBeNil) 1146 1147 // BuildStatus should not be updated. 1148 buildStatus = &model.BuildStatus{Build: datastore.KeyForObj(ctx, build)} 1149 So(datastore.Get(ctx, build, buildStatus), ShouldBeNil) 1150 So(buildStatus.Status, ShouldEqual, pb.Status_STARTED) 1151 So(build.Proto.Status, ShouldEqual, pb.Status_STARTED) 1152 }) 1153 Convey("update output without output.status should not affect overall status", func() { 1154 buildStatus := &model.BuildStatus{ 1155 Build: datastore.KeyForObj(ctx, build), 1156 Status: pb.Status_STARTED, 1157 } 1158 So(datastore.Put(ctx, build, buildStatus), ShouldBeNil) 1159 1160 req.UpdateMask.Paths[0] = "build.output" 1161 req.Build.Output = &pb.Build_Output{ 1162 Status: pb.Status_SUCCESS, 1163 } 1164 req.Build.Status = pb.Status_SUCCESS 1165 So(updateBuild(ctx, req), ShouldBeRPCOK) 1166 1167 // TQ tasks for pubsub-notification, bq-export, and invocation-finalization. 1168 tasks := sch.Tasks() 1169 So(tasks, ShouldHaveLength, 0) 1170 1171 // BuildCompleted metric should not be set. 1172 fvs := fv(model.Success.String(), "", "", false) 1173 So(store.Get(ctx, metrics.V1.BuildCountCompleted, time.Time{}, fvs), ShouldBeNil) 1174 1175 // BuildStatus should not be updated. 1176 buildStatus = &model.BuildStatus{Build: datastore.KeyForObj(ctx, build)} 1177 So(datastore.Get(ctx, build, buildStatus), ShouldBeNil) 1178 So(buildStatus.Status, ShouldEqual, pb.Status_STARTED) 1179 So(build.Proto.Status, ShouldEqual, pb.Status_STARTED) 1180 So(build.Proto.Output.Status, ShouldEqual, pb.Status_STARTED) 1181 }) 1182 }) 1183 1184 Convey("read mask", func() { 1185 Convey("w/ read mask", func() { 1186 req.UpdateMask.Paths[0] = "build.status" 1187 req.Build.Status = pb.Status_SUCCESS 1188 req.Mask = &pb.BuildMask{ 1189 Fields: &fieldmaskpb.FieldMask{ 1190 Paths: []string{ 1191 "status", 1192 }, 1193 }, 1194 } 1195 b, err := srv.UpdateBuild(ctx, req) 1196 So(err, ShouldBeNil) 1197 So(b, ShouldResembleProto, &pb.Build{ 1198 Status: pb.Status_SUCCESS, 1199 }) 1200 }) 1201 }) 1202 1203 Convey("update build with parent", func() { 1204 parent := &model.Build{ 1205 ID: 10, 1206 Proto: &pb.Build{ 1207 Id: 10, 1208 Builder: &pb.BuilderID{ 1209 Project: "project", 1210 Bucket: "bucket", 1211 Builder: "builder", 1212 }, 1213 Status: pb.Status_SUCCESS, 1214 }, 1215 CreateTime: t0, 1216 UpdateToken: tk, 1217 } 1218 ps := &model.BuildStatus{ 1219 Build: datastore.KeyForObj(ctx, parent), 1220 Status: pb.Status_STARTED, 1221 } 1222 So(datastore.Put(ctx, parent, ps), ShouldBeNil) 1223 1224 Convey("child can outlive parent", func() { 1225 child := &model.Build{ 1226 ID: 11, 1227 Proto: &pb.Build{ 1228 Id: 11, 1229 Builder: &pb.BuilderID{ 1230 Project: "project", 1231 Bucket: "bucket", 1232 Builder: "builder", 1233 }, 1234 Status: pb.Status_SCHEDULED, 1235 AncestorIds: []int64{10}, 1236 CanOutliveParent: true, 1237 }, 1238 CreateTime: t0, 1239 UpdateToken: tk, 1240 } 1241 So(datastore.Put(ctx, child), ShouldBeNil) 1242 req.UpdateMask.Paths[0] = "build.status" 1243 req.Build.Status = pb.Status_STARTED 1244 So(updateBuild(ctx, req), ShouldBeRPCOK) 1245 }) 1246 1247 Convey("child cannot outlive parent", func() { 1248 child := &model.Build{ 1249 ID: 11, 1250 Proto: &pb.Build{ 1251 Id: 11, 1252 Builder: &pb.BuilderID{ 1253 Project: "project", 1254 Bucket: "bucket", 1255 Builder: "builder", 1256 }, 1257 Status: pb.Status_SCHEDULED, 1258 AncestorIds: []int64{10}, 1259 CanOutliveParent: false, 1260 }, 1261 CreateTime: t0, 1262 UpdateToken: tk, 1263 } 1264 tk, ctx = updateContextForNewBuildToken(ctx, 11) 1265 child.UpdateToken = tk 1266 cs := &model.BuildStatus{ 1267 Build: datastore.KeyForObj(ctx, child), 1268 Status: pb.Status_STARTED, 1269 } 1270 So(datastore.Put(ctx, child, cs), ShouldBeNil) 1271 1272 Convey("request is to terminate the child", func() { 1273 req.UpdateMask.Paths[0] = "build.status" 1274 req.Build.Id = 11 1275 req.Build.Status = pb.Status_SUCCESS 1276 req.Mask = &pb.BuildMask{ 1277 Fields: &fieldmaskpb.FieldMask{ 1278 Paths: []string{ 1279 "status", 1280 "cancel_time", 1281 }, 1282 }, 1283 } 1284 build, err := srv.UpdateBuild(ctx, req) 1285 So(err, ShouldBeRPCOK) 1286 So(build.Status, ShouldEqual, pb.Status_SUCCESS) 1287 So(build.CancelTime, ShouldBeNil) 1288 1289 tasks := sch.Tasks() 1290 So(tasks, ShouldHaveLength, 4) 1291 sum := 0 1292 for _, task := range tasks { 1293 switch v := task.Payload.(type) { 1294 case *taskdefs.NotifyPubSub: 1295 sum++ 1296 So(v.GetBuildId(), ShouldEqual, req.Build.Id) 1297 case *taskdefs.ExportBigQuery: 1298 sum += 2 1299 So(v.GetBuildId(), ShouldEqual, req.Build.Id) 1300 case *taskdefs.FinalizeResultDBGo: 1301 sum += 4 1302 So(v.GetBuildId(), ShouldEqual, req.Build.Id) 1303 case *taskdefs.NotifyPubSubGoProxy: 1304 sum += 8 1305 So(v.GetBuildId(), ShouldEqual, req.Build.Id) 1306 default: 1307 panic("invalid task payload") 1308 } 1309 } 1310 So(sum, ShouldEqual, 15) 1311 1312 // BuildCompleted metric should be set to 1 with SUCCESS. 1313 fvs := fv(model.Success.String(), "", "", false) 1314 So(store.Get(ctx, metrics.V1.BuildCountCompleted, time.Time{}, fvs), ShouldEqual, 1) 1315 }) 1316 1317 Convey("start the cancel process if parent has ended", func() { 1318 // Child of the requested build. 1319 So(datastore.Put(ctx, &model.Build{ 1320 ID: 12, 1321 Proto: &pb.Build{ 1322 Id: 12, 1323 Builder: &pb.BuilderID{ 1324 Project: "project", 1325 Bucket: "bucket", 1326 Builder: "builder", 1327 }, 1328 AncestorIds: []int64{11}, 1329 CanOutliveParent: false, 1330 }, 1331 UpdateToken: tk, 1332 }), ShouldBeNil) 1333 req.Build.Id = 11 1334 req.Build.Status = pb.Status_STARTED 1335 req.UpdateMask.Paths[0] = "build.status" 1336 req.Mask = &pb.BuildMask{ 1337 Fields: &fieldmaskpb.FieldMask{ 1338 Paths: []string{ 1339 "status", 1340 "cancel_time", 1341 "cancellation_markdown", 1342 }, 1343 }, 1344 } 1345 build, err := srv.UpdateBuild(ctx, req) 1346 So(err, ShouldBeRPCOK) 1347 So(build.Status, ShouldEqual, pb.Status_STARTED) 1348 So(build.CancelTime.AsTime(), ShouldEqual, t0) 1349 So(build.CancellationMarkdown, ShouldEqual, "canceled because its parent 10 has terminated") 1350 // One pubsub notification for the status update in the request, 1351 // one CancelBuildTask for the requested build, 1352 // one CancelBuildTask for the child build. 1353 So(sch.Tasks(), ShouldHaveLength, 4) 1354 1355 // BuildStatus is updated. 1356 updatedStatus := &model.BuildStatus{Build: datastore.MakeKey(ctx, "Build", 11)} 1357 So(datastore.Get(ctx, updatedStatus), ShouldBeNil) 1358 So(updatedStatus.Status, ShouldEqual, pb.Status_STARTED) 1359 }) 1360 1361 Convey("start the cancel process if parent is missing", func() { 1362 tk, ctx = updateContextForNewBuildToken(ctx, 15) 1363 b := &model.Build{ 1364 ID: 15, 1365 Proto: &pb.Build{ 1366 Id: 15, 1367 Builder: &pb.BuilderID{ 1368 Project: "project", 1369 Bucket: "bucket", 1370 Builder: "builder", 1371 }, 1372 AncestorIds: []int64{3000000}, 1373 CanOutliveParent: false, 1374 Status: pb.Status_SCHEDULED, 1375 }, 1376 UpdateToken: tk, 1377 } 1378 buildStatus := &model.BuildStatus{ 1379 Build: datastore.KeyForObj(ctx, b), 1380 Status: b.Proto.Status, 1381 } 1382 So(datastore.Put(ctx, b, buildStatus), ShouldBeNil) 1383 req.Build.Id = 15 1384 req.Build.Status = pb.Status_STARTED 1385 req.UpdateMask.Paths[0] = "build.status" 1386 req.Mask = &pb.BuildMask{ 1387 Fields: &fieldmaskpb.FieldMask{ 1388 Paths: []string{ 1389 "status", 1390 "cancel_time", 1391 "cancellation_markdown", 1392 }, 1393 }, 1394 } 1395 build, err := srv.UpdateBuild(ctx, req) 1396 So(err, ShouldBeRPCOK) 1397 So(build.Status, ShouldEqual, pb.Status_STARTED) 1398 So(build.CancelTime.AsTime(), ShouldEqual, t0) 1399 So(build.CancellationMarkdown, ShouldEqual, "canceled because its parent 3000000 is missing") 1400 So(sch.Tasks(), ShouldHaveLength, 3) 1401 1402 // BuildStatus is updated. 1403 updatedStatus := &model.BuildStatus{Build: datastore.MakeKey(ctx, "Build", 15)} 1404 So(datastore.Get(ctx, updatedStatus), ShouldBeNil) 1405 So(updatedStatus.Status, ShouldEqual, pb.Status_STARTED) 1406 }) 1407 1408 Convey("return err if failed to get parent", func() { 1409 So(datastore.Put(ctx, &model.Build{ 1410 ID: 31, 1411 Proto: &pb.Build{ 1412 Id: 31, 1413 Builder: &pb.BuilderID{ 1414 Project: "project", 1415 Bucket: "bucket", 1416 Builder: "builder", 1417 }, 1418 AncestorIds: []int64{30}, 1419 CanOutliveParent: false, 1420 }, 1421 UpdateToken: tk, 1422 }), ShouldBeNil) 1423 1424 // Mock datastore.Get failure. 1425 var fb featureBreaker.FeatureBreaker 1426 ctx, fb = featureBreaker.FilterRDS(ctx, nil) 1427 // Break GetMulti will ingest the error to datastore.Get, 1428 // directly breaking "Get" doesn't work. 1429 fb.BreakFeatures(errors.New("get error"), "GetMulti") 1430 1431 req.Build.Id = 31 1432 req.Build.Status = pb.Status_STARTED 1433 tk, ctx = updateContextForNewBuildToken(ctx, 31) 1434 req.UpdateMask.Paths[0] = "build.status" 1435 _, err := srv.UpdateBuild(ctx, req) 1436 So(err, ShouldErrLike, "get error") 1437 1438 }) 1439 1440 Convey("build is being canceled", func() { 1441 tk, ctx = updateContextForNewBuildToken(ctx, 13) 1442 So(datastore.Put(ctx, &model.Build{ 1443 ID: 13, 1444 Proto: &pb.Build{ 1445 Id: 13, 1446 Builder: &pb.BuilderID{ 1447 Project: "project", 1448 Bucket: "bucket", 1449 Builder: "builder", 1450 }, 1451 CancelTime: timestamppb.New(t0.Add(-time.Minute)), 1452 SummaryMarkdown: "original summary", 1453 }, 1454 UpdateToken: tk, 1455 }), ShouldBeNil) 1456 // Child of the requested build. 1457 So(datastore.Put(ctx, &model.Build{ 1458 ID: 14, 1459 Proto: &pb.Build{ 1460 Id: 14, 1461 Builder: &pb.BuilderID{ 1462 Project: "project", 1463 Bucket: "bucket", 1464 Builder: "builder", 1465 }, 1466 AncestorIds: []int64{13}, 1467 CanOutliveParent: false, 1468 }, 1469 UpdateToken: tk, 1470 }), ShouldBeNil) 1471 req.Build.Id = 13 1472 req.Build.SummaryMarkdown = "new summary" 1473 req.UpdateMask.Paths[0] = "build.summary_markdown" 1474 req.Mask = &pb.BuildMask{ 1475 Fields: &fieldmaskpb.FieldMask{ 1476 Paths: []string{ 1477 "cancel_time", 1478 "summary_markdown", 1479 }, 1480 }, 1481 } 1482 build, err := srv.UpdateBuild(ctx, req) 1483 So(err, ShouldBeRPCOK) 1484 So(build.CancelTime.AsTime(), ShouldEqual, t0.Add(-time.Minute)) 1485 So(build.SummaryMarkdown, ShouldEqual, "new summary") 1486 So(sch.Tasks(), ShouldBeEmpty) 1487 }) 1488 1489 Convey("build is ended, should cancel children", func() { 1490 tk, ctx = updateContextForNewBuildToken(ctx, 20) 1491 p := &model.Build{ 1492 ID: 20, 1493 Proto: &pb.Build{ 1494 Id: 20, 1495 Builder: &pb.BuilderID{ 1496 Project: "project", 1497 Bucket: "bucket", 1498 Builder: "builder", 1499 }, 1500 }, 1501 UpdateToken: tk, 1502 } 1503 ps := &model.BuildStatus{ 1504 Build: datastore.KeyForObj(ctx, p), 1505 Status: pb.Status_STARTED, 1506 } 1507 So(datastore.Put(ctx, p, ps), ShouldBeNil) 1508 // Child of the requested build. 1509 c := &model.Build{ 1510 ID: 21, 1511 Proto: &pb.Build{ 1512 Id: 21, 1513 Builder: &pb.BuilderID{ 1514 Project: "project", 1515 Bucket: "bucket", 1516 Builder: "builder", 1517 }, 1518 AncestorIds: []int64{20}, 1519 CanOutliveParent: false, 1520 }, 1521 UpdateToken: tk, 1522 } 1523 So(datastore.Put(ctx, c), ShouldBeNil) 1524 req.Build.Id = 20 1525 req.Build.Status = pb.Status_INFRA_FAILURE 1526 req.UpdateMask.Paths[0] = "build.status" 1527 _, err := srv.UpdateBuild(ctx, req) 1528 So(err, ShouldBeRPCOK) 1529 1530 child, err := common.GetBuild(ctx, 21) 1531 So(err, ShouldBeNil) 1532 So(child.Proto.CancelTime, ShouldNotBeNil) 1533 }) 1534 1535 Convey("build gets cancel signal from backend, should cancel children", func() { 1536 tk, ctx = updateContextForNewBuildToken(ctx, 20) 1537 So(datastore.Put(ctx, &model.Build{ 1538 ID: 20, 1539 Proto: &pb.Build{ 1540 Id: 20, 1541 Builder: &pb.BuilderID{ 1542 Project: "project", 1543 Bucket: "bucket", 1544 Builder: "builder", 1545 }, 1546 }, 1547 UpdateToken: tk, 1548 }), ShouldBeNil) 1549 // Child of the requested build. 1550 So(datastore.Put(ctx, &model.Build{ 1551 ID: 21, 1552 Proto: &pb.Build{ 1553 Id: 21, 1554 Builder: &pb.BuilderID{ 1555 Project: "project", 1556 Bucket: "bucket", 1557 Builder: "builder", 1558 }, 1559 AncestorIds: []int64{20}, 1560 CanOutliveParent: false, 1561 }, 1562 UpdateToken: tk, 1563 }), ShouldBeNil) 1564 req.Build.Id = 20 1565 req.UpdateMask.Paths = []string{"build.cancel_time", "build.cancellation_markdown"} 1566 req.Build.CancelTime = timestamppb.New(t0.Add(-time.Minute)) 1567 req.Build.CancellationMarkdown = "swarming task is cancelled" 1568 _, err := srv.UpdateBuild(ctx, req) 1569 So(err, ShouldBeRPCOK) 1570 1571 child, err := common.GetBuild(ctx, 21) 1572 So(err, ShouldBeNil) 1573 So(child.Proto.CancelTime, ShouldNotBeNil) 1574 1575 // One CancelBuildTask for the requested build, 1576 // one CancelBuildTask for the child build. 1577 So(sch.Tasks(), ShouldHaveLength, 2) 1578 }) 1579 }) 1580 }) 1581 }) 1582 }