go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luci_notify/notify/pubsub_test.go (about) 1 // Copyright 2017 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 notify 16 17 import ( 18 "bytes" 19 "compress/zlib" 20 "context" 21 "encoding/base64" 22 "encoding/json" 23 "fmt" 24 "io" 25 "net/http" 26 "sort" 27 "strconv" 28 "testing" 29 "time" 30 31 "google.golang.org/protobuf/encoding/protojson" 32 "google.golang.org/protobuf/proto" 33 "google.golang.org/protobuf/types/known/structpb" 34 "google.golang.org/protobuf/types/known/timestamppb" 35 36 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 37 "go.chromium.org/luci/common/clock" 38 "go.chromium.org/luci/common/clock/testclock" 39 "go.chromium.org/luci/common/errors" 40 "go.chromium.org/luci/common/logging/memlogger" 41 gitpb "go.chromium.org/luci/common/proto/git" 42 "go.chromium.org/luci/gae/impl/memory" 43 "go.chromium.org/luci/gae/service/datastore" 44 "go.chromium.org/luci/server/caching" 45 "go.chromium.org/luci/server/tq" 46 47 apicfg "go.chromium.org/luci/luci_notify/api/config" 48 "go.chromium.org/luci/luci_notify/common" 49 "go.chromium.org/luci/luci_notify/config" 50 "go.chromium.org/luci/luci_notify/internal" 51 "go.chromium.org/luci/luci_notify/testutil" 52 53 . "github.com/smartystreets/goconvey/convey" 54 . "go.chromium.org/luci/common/testing/assertions" 55 ) 56 57 func dummyBuildWithEmails(builder string, status buildbucketpb.Status, creationTime time.Time, revision string, notifyEmails ...EmailNotify) *Build { 58 ret := &Build{ 59 Build: buildbucketpb.Build{ 60 Builder: &buildbucketpb.BuilderID{ 61 Project: "chromium", 62 Bucket: "ci", 63 Builder: builder, 64 }, 65 Status: status, 66 Input: &buildbucketpb.Build_Input{ 67 GitilesCommit: &buildbucketpb.GitilesCommit{ 68 Host: defaultGitilesHost, 69 Project: defaultGitilesProject, 70 Id: revision, 71 }, 72 }, 73 }, 74 EmailNotify: notifyEmails, 75 } 76 ret.Build.CreateTime = timestamppb.New(creationTime) 77 return ret 78 } 79 80 func dummyBuildWithFailingSteps(status buildbucketpb.Status, failingSteps []string) *Build { 81 build := &Build{ 82 Build: buildbucketpb.Build{ 83 Builder: &buildbucketpb.BuilderID{ 84 Project: "chromium", 85 Bucket: "ci", 86 Builder: "test-builder-tree-closer", 87 }, 88 Status: status, 89 Input: &buildbucketpb.Build_Input{ 90 GitilesCommit: &buildbucketpb.GitilesCommit{ 91 Host: defaultGitilesHost, 92 Project: defaultGitilesProject, 93 Id: "deadbeef", 94 }, 95 }, 96 EndTime: timestamppb.Now(), 97 }, 98 } 99 100 for _, stepName := range failingSteps { 101 build.Build.Steps = append(build.Build.Steps, &buildbucketpb.Step{ 102 Name: stepName, 103 Status: buildbucketpb.Status_FAILURE, 104 }) 105 } 106 107 return build 108 } 109 110 func TestExtractEmailNotifyValues(t *testing.T) { 111 Convey(`Test Environment for extractEmailNotifyValues`, t, func() { 112 extract := func(buildJSONPB string) ([]EmailNotify, error) { 113 build := &buildbucketpb.Build{} 114 err := protojson.Unmarshal([]byte(buildJSONPB), build) 115 So(err, ShouldBeNil) 116 return extractEmailNotifyValues(build, "") 117 } 118 119 Convey(`empty`, func() { 120 results, err := extract(`{}`) 121 So(err, ShouldBeNil) 122 So(results, ShouldHaveLength, 0) 123 }) 124 125 Convey(`populated without email_notify`, func() { 126 results, err := extract(`{ 127 "input": { 128 "properties": { 129 "foo": 1 130 } 131 } 132 }`) 133 So(err, ShouldBeNil) 134 So(results, ShouldHaveLength, 0) 135 }) 136 137 Convey(`single email_notify value in input`, func() { 138 results, err := extract(`{ 139 "input": { 140 "properties": { 141 "email_notify": [{"email": "test@email"}] 142 } 143 } 144 }`) 145 So(err, ShouldBeNil) 146 So(results, ShouldResemble, []EmailNotify{ 147 { 148 Email: "test@email", 149 Template: "", 150 }, 151 }) 152 }) 153 154 Convey(`single email_notify value_with_template`, func() { 155 results, err := extract(`{ 156 "input": { 157 "properties": { 158 "email_notify": [{ 159 "email": "test@email", 160 "template": "test-template" 161 }] 162 } 163 } 164 }`) 165 So(err, ShouldBeNil) 166 So(results, ShouldResemble, []EmailNotify{ 167 { 168 Email: "test@email", 169 Template: "test-template", 170 }, 171 }) 172 }) 173 174 Convey(`multiple email_notify values`, func() { 175 results, err := extract(`{ 176 "input": { 177 "properties": { 178 "email_notify": [ 179 {"email": "test@email"}, 180 {"email": "test2@email"} 181 ] 182 } 183 } 184 }`) 185 So(err, ShouldBeNil) 186 So(results, ShouldResemble, []EmailNotify{ 187 { 188 Email: "test@email", 189 Template: "", 190 }, 191 { 192 Email: "test2@email", 193 Template: "", 194 }, 195 }) 196 }) 197 198 Convey(`output takes precedence`, func() { 199 results, err := extract(`{ 200 "input": { 201 "properties": { 202 "email_notify": [ 203 {"email": "test@email"} 204 ] 205 } 206 }, 207 "output": { 208 "properties": { 209 "email_notify": [ 210 {"email": "test2@email"} 211 ] 212 } 213 } 214 }`) 215 So(err, ShouldBeNil) 216 So(results, ShouldResemble, []EmailNotify{ 217 { 218 Email: "test2@email", 219 Template: "", 220 }, 221 }) 222 }) 223 }) 224 } 225 226 func init() { 227 InitDispatcher(&tq.Default) 228 } 229 230 func TestHandleBuild(t *testing.T) { 231 t.Parallel() 232 233 Convey(`Test Environment for handleBuild`, t, func() { 234 cfgName := "basic" 235 cfg, err := testutil.LoadProjectConfig(cfgName) 236 So(err, ShouldBeNil) 237 238 c := memory.Use(context.Background()) 239 c = common.SetAppIDForTest(c, "luci-notify-test") 240 c = caching.WithEmptyProcessCache(c) 241 c = clock.Set(c, testclock.New(time.Now())) 242 c = memlogger.Use(c) 243 c, sched := tq.TestingContext(c, nil) 244 245 // Add entities to datastore and update indexes. 246 project := &config.Project{Name: "chromium"} 247 builders := makeBuilders(c, "chromium", cfg) 248 template := &config.EmailTemplate{ 249 ProjectKey: datastore.KeyForObj(c, project), 250 Name: "template", 251 SubjectTextTemplate: "Builder {{.Build.Builder.Builder}} failed on steps {{stepNames .MatchingFailedSteps}}", 252 } 253 So(datastore.Put(c, project, builders, template), ShouldBeNil) 254 datastore.GetTestable(c).CatchupIndexes() 255 256 oldTime := time.Date(2015, 2, 3, 12, 54, 3, 0, time.UTC) 257 newTime := time.Date(2015, 2, 3, 12, 58, 7, 0, time.UTC) 258 newTime2 := time.Date(2015, 2, 3, 12, 59, 8, 0, time.UTC) 259 260 assertTasks := func(build *Build, checkoutFunc CheckoutFunc, expectedRecipients ...EmailNotify) { 261 history := mockHistoryFunc(map[string][]*gitpb.Commit{ 262 "chromium/src": testCommits, 263 "third_party/hello": revTestCommits, 264 }) 265 266 // Test handleBuild. 267 err := handleBuild(c, build, checkoutFunc, history) 268 So(err, ShouldBeNil) 269 270 // Verify tasks were scheduled. 271 var actualEmails []string 272 for _, t := range sched.Tasks() { 273 et := t.Payload.(*internal.EmailTask) 274 actualEmails = append(actualEmails, et.Recipients...) 275 } 276 var expectedEmails []string 277 for _, r := range expectedRecipients { 278 expectedEmails = append(expectedEmails, r.Email) 279 } 280 sort.Strings(actualEmails) 281 sort.Strings(expectedEmails) 282 So(actualEmails, ShouldResemble, expectedEmails) 283 } 284 285 verifyBuilder := func(build *Build, revision string, checkout Checkout) { 286 datastore.GetTestable(c).CatchupIndexes() 287 id := getBuilderID(&build.Build) 288 builder := config.Builder{ 289 ProjectKey: datastore.KeyForObj(c, project), 290 ID: id, 291 } 292 So(datastore.Get(c, &builder), ShouldBeNil) 293 So(builder.Revision, ShouldResemble, revision) 294 So(builder.Status, ShouldEqual, build.Status) 295 expectCommits := checkout.ToGitilesCommits() 296 So(builder.GitilesCommits, ShouldResembleProto, expectCommits) 297 } 298 299 propEmail := EmailNotify{ 300 Email: "property@google.com", 301 } 302 successEmail := EmailNotify{ 303 Email: "test-example-success@google.com", 304 } 305 failEmail := EmailNotify{ 306 Email: "test-example-failure@google.com", 307 } 308 infraFailEmail := EmailNotify{ 309 Email: "test-example-infra-failure@google.com", 310 } 311 failAndInfraFailEmail := EmailNotify{ 312 Email: "test-example-failure-and-infra-failure@google.com", 313 } 314 changeEmail := EmailNotify{ 315 Email: "test-example-change@google.com", 316 } 317 commit1Email := EmailNotify{ 318 Email: commitEmail1, 319 } 320 commit2Email := EmailNotify{ 321 Email: commitEmail2, 322 } 323 324 grepLog := func(substring string) { 325 buf := new(bytes.Buffer) 326 _, err := memlogger.Dump(c, buf) 327 So(err, ShouldBeNil) 328 So(buf.String(), ShouldContainSubstring, substring) 329 } 330 331 Convey(`no config`, func() { 332 build := dummyBuildWithEmails("not-a-builder", buildbucketpb.Status_FAILURE, oldTime, rev1) 333 assertTasks(build, mockCheckoutFunc(nil)) 334 grepLog("No builder") 335 }) 336 337 Convey(`no config w/property`, func() { 338 build := dummyBuildWithEmails("not-a-builder", buildbucketpb.Status_FAILURE, oldTime, rev1, propEmail) 339 assertTasks(build, mockCheckoutFunc(nil), propEmail) 340 }) 341 342 Convey(`no repository in-order`, func() { 343 build := dummyBuildWithEmails("test-builder-no-repo", buildbucketpb.Status_FAILURE, oldTime, rev1) 344 assertTasks(build, mockCheckoutFunc(nil), failEmail) 345 }) 346 347 Convey(`no repository out-of-order`, func() { 348 build := dummyBuildWithEmails("test-builder-no-repo", buildbucketpb.Status_FAILURE, newTime, rev1) 349 assertTasks(build, mockCheckoutFunc(nil), failEmail) 350 351 newBuild := dummyBuildWithEmails("test-builder-no-repo", buildbucketpb.Status_SUCCESS, oldTime, rev2) 352 assertTasks(newBuild, mockCheckoutFunc(nil), failEmail, successEmail) 353 grepLog("old time") 354 }) 355 356 Convey(`no revision`, func() { 357 build := &Build{ 358 Build: buildbucketpb.Build{ 359 Builder: &buildbucketpb.BuilderID{ 360 Project: "chromium", 361 Bucket: "ci", 362 Builder: "test-builder-1", 363 }, 364 Status: buildbucketpb.Status_SUCCESS, 365 }, 366 } 367 assertTasks(build, mockCheckoutFunc(nil), successEmail) 368 grepLog("revision") 369 }) 370 371 Convey(`init builder`, func() { 372 build := dummyBuildWithEmails("test-builder-1", buildbucketpb.Status_FAILURE, oldTime, rev1) 373 assertTasks(build, mockCheckoutFunc(nil), failEmail) 374 verifyBuilder(build, rev1, nil) 375 }) 376 377 Convey(`init builder w/property`, func() { 378 build := dummyBuildWithEmails("test-builder-1", buildbucketpb.Status_FAILURE, oldTime, rev1, propEmail) 379 assertTasks(build, mockCheckoutFunc(nil), failEmail, propEmail) 380 verifyBuilder(build, rev1, nil) 381 }) 382 383 Convey(`source manifest return error`, func() { 384 build := dummyBuildWithEmails("test-builder-1", buildbucketpb.Status_FAILURE, oldTime, rev1, propEmail) 385 assertTasks(build, mockCheckoutReturnsErrorFunc(), failEmail, propEmail) 386 verifyBuilder(build, rev1, nil) 387 grepLog("Got error when getting source manifest for build") 388 }) 389 390 Convey(`repository mismatch`, func() { 391 build := dummyBuildWithEmails("test-builder-1", buildbucketpb.Status_FAILURE, oldTime, rev1, propEmail) 392 assertTasks(build, mockCheckoutFunc(nil), failEmail, propEmail) 393 verifyBuilder(build, rev1, nil) 394 395 newBuild := &Build{ 396 Build: buildbucketpb.Build{ 397 Builder: &buildbucketpb.BuilderID{ 398 Project: "chromium", 399 Bucket: "ci", 400 Builder: "test-builder-1", 401 }, 402 Status: buildbucketpb.Status_SUCCESS, 403 Input: &buildbucketpb.Build_Input{ 404 GitilesCommit: &buildbucketpb.GitilesCommit{ 405 Host: defaultGitilesHost, 406 Project: "example/src", 407 Id: rev2, 408 }, 409 }, 410 }, 411 } 412 assertTasks(newBuild, mockCheckoutFunc(nil), failEmail, propEmail, successEmail) 413 grepLog("triggered by commit") 414 }) 415 416 Convey(`out-of-order revision`, func() { 417 build := dummyBuildWithEmails("test-builder-2", buildbucketpb.Status_SUCCESS, oldTime, rev2) 418 assertTasks(build, mockCheckoutFunc(nil), successEmail) 419 verifyBuilder(build, rev2, nil) 420 421 oldRevBuild := dummyBuildWithEmails("test-builder-2", buildbucketpb.Status_FAILURE, newTime, rev1) 422 assertTasks(oldRevBuild, mockCheckoutFunc(nil), successEmail, failEmail) 423 grepLog("old commit") 424 }) 425 426 Convey(`revision update`, func() { 427 build := dummyBuildWithEmails("test-builder-3", buildbucketpb.Status_SUCCESS, oldTime, rev1) 428 assertTasks(build, mockCheckoutFunc(nil), successEmail) 429 verifyBuilder(build, rev1, nil) 430 431 newBuild := dummyBuildWithEmails("test-builder-3", buildbucketpb.Status_FAILURE, newTime, rev2) 432 newBuild.Id++ 433 assertTasks(newBuild, mockCheckoutFunc(nil), successEmail, failEmail, changeEmail) 434 verifyBuilder(newBuild, rev2, nil) 435 }) 436 437 Convey(`revision update w/property`, func() { 438 build := dummyBuildWithEmails("test-builder-3", buildbucketpb.Status_SUCCESS, oldTime, rev1, propEmail) 439 assertTasks(build, mockCheckoutFunc(nil), successEmail, propEmail) 440 verifyBuilder(build, rev1, nil) 441 442 newBuild := dummyBuildWithEmails("test-builder-3", buildbucketpb.Status_FAILURE, newTime, rev2, propEmail) 443 newBuild.Id++ 444 assertTasks(newBuild, mockCheckoutFunc(nil), successEmail, propEmail, failEmail, changeEmail, propEmail) 445 verifyBuilder(newBuild, rev2, nil) 446 }) 447 448 Convey(`out-of-order creation time`, func() { 449 build := dummyBuildWithEmails("test-builder-4", buildbucketpb.Status_SUCCESS, newTime, rev1) 450 build.Id = 2 451 assertTasks(build, mockCheckoutFunc(nil), successEmail) 452 verifyBuilder(build, rev1, nil) 453 454 oldBuild := dummyBuildWithEmails("test-builder-4", buildbucketpb.Status_FAILURE, oldTime, rev1) 455 oldBuild.Id = 1 456 assertTasks(oldBuild, mockCheckoutFunc(nil), successEmail, failEmail) 457 grepLog("old time") 458 }) 459 460 checkoutOld := Checkout{ 461 "https://chromium.googlesource.com/chromium/src": rev1, 462 "https://chromium.googlesource.com/third_party/hello": rev1, 463 } 464 checkoutNew := Checkout{ 465 "https://chromium.googlesource.com/chromium/src": rev2, 466 "https://chromium.googlesource.com/third_party/hello": rev2, 467 } 468 469 testBlamelistConfig := func(builderID string, emails ...EmailNotify) { 470 build := dummyBuildWithEmails(builderID, buildbucketpb.Status_SUCCESS, oldTime, rev1) 471 assertTasks(build, mockCheckoutFunc(checkoutOld)) 472 verifyBuilder(build, rev1, checkoutOld) 473 474 newBuild := dummyBuildWithEmails(builderID, buildbucketpb.Status_FAILURE, newTime, rev2) 475 newBuild.Id++ 476 assertTasks(newBuild, mockCheckoutFunc(checkoutNew), emails...) 477 verifyBuilder(newBuild, rev2, checkoutNew) 478 } 479 480 Convey(`blamelist no allowlist`, func() { 481 testBlamelistConfig("test-builder-blamelist-1", changeEmail, commit2Email) 482 }) 483 484 Convey(`blamelist with allowlist`, func() { 485 testBlamelistConfig("test-builder-blamelist-2", changeEmail, commit1Email) 486 }) 487 488 Convey(`blamelist against last non-empty checkout`, func() { 489 build := dummyBuildWithEmails("test-builder-blamelist-2", buildbucketpb.Status_SUCCESS, oldTime, rev1) 490 assertTasks(build, mockCheckoutFunc(checkoutOld)) 491 verifyBuilder(build, rev1, checkoutOld) 492 493 newBuild := dummyBuildWithEmails("test-builder-blamelist-2", buildbucketpb.Status_FAILURE, newTime, rev2) 494 newBuild.Id++ 495 assertTasks(newBuild, mockCheckoutFunc(nil), changeEmail) 496 verifyBuilder(newBuild, rev2, checkoutOld) 497 498 newestTime := time.Date(2017, 2, 3, 12, 59, 9, 0, time.UTC) 499 newestBuild := dummyBuildWithEmails("test-builder-blamelist-2", buildbucketpb.Status_SUCCESS, newestTime, rev2) 500 newestBuild.Id++ 501 assertTasks(newestBuild, mockCheckoutFunc(checkoutNew), changeEmail, commit1Email) 502 verifyBuilder(newestBuild, rev2, checkoutNew) 503 }) 504 505 Convey(`blamelist mixed`, func() { 506 testBlamelistConfig("test-builder-blamelist-3", commit1Email, commit2Email) 507 }) 508 509 Convey(`blamelist duplicate`, func() { 510 testBlamelistConfig("test-builder-blamelist-4", commit2Email, commit2Email, commit2Email) 511 }) 512 513 Convey(`failure type infra`, func() { 514 infra_failure_build := dummyBuildWithEmails("test-builder-infra-1", buildbucketpb.Status_SUCCESS, oldTime, rev2) 515 assertTasks(infra_failure_build, mockCheckoutFunc(nil)) 516 517 infra_failure_build = dummyBuildWithEmails("test-builder-infra-1", buildbucketpb.Status_FAILURE, newTime, rev2) 518 assertTasks(infra_failure_build, mockCheckoutFunc(nil)) 519 520 infra_failure_build = dummyBuildWithEmails("test-builder-infra-1", buildbucketpb.Status_INFRA_FAILURE, newTime2, rev2) 521 assertTasks(infra_failure_build, mockCheckoutFunc(nil), infraFailEmail) 522 }) 523 524 Convey(`failure type mixed`, func() { 525 failure_and_infra_failure_build := dummyBuildWithEmails("test-builder-failure-and-infra-failures-1", buildbucketpb.Status_SUCCESS, oldTime, rev2) 526 assertTasks(failure_and_infra_failure_build, mockCheckoutFunc(nil)) 527 528 failure_and_infra_failure_build = dummyBuildWithEmails("test-builder-failure-and-infra-failures-1", buildbucketpb.Status_FAILURE, newTime, rev2) 529 assertTasks(failure_and_infra_failure_build, mockCheckoutFunc(nil), failAndInfraFailEmail) 530 531 failure_and_infra_failure_build = dummyBuildWithEmails("test-builder-failure-and-infra-failures-1", buildbucketpb.Status_INFRA_FAILURE, newTime2, rev2) 532 assertTasks(failure_and_infra_failure_build, mockCheckoutFunc(nil), failAndInfraFailEmail) 533 }) 534 535 // Some arbitrary time guaranteed to be less than time.Now() when called from handleBuild. 536 µs, _ := time.ParseDuration("1µs") 537 initialTimestamp := time.Now().AddDate(-1, 0, 0).UTC().Round(µs) 538 539 runHandleBuild := func(buildStatus buildbucketpb.Status, initialStatus config.TreeCloserStatus, failingSteps []string) *config.TreeCloser { 540 // Insert the tree closer to test into datastore. 541 builderKey := datastore.KeyForObj(c, &config.Builder{ 542 ProjectKey: datastore.KeyForObj(c, &config.Project{Name: "chromium"}), 543 ID: "ci/test-builder-tree-closer", 544 }) 545 546 tc := &config.TreeCloser{ 547 BuilderKey: builderKey, 548 TreeStatusHost: "chromium-status.appspot.com", 549 TreeCloser: apicfg.TreeCloser{ 550 FailedStepRegexp: "include", 551 FailedStepRegexpExclude: "exclude", 552 Template: "template", 553 }, 554 Status: initialStatus, 555 Timestamp: initialTimestamp, 556 } 557 So(datastore.Put(c, tc), ShouldBeNil) 558 559 // Handle a new build. 560 build := dummyBuildWithFailingSteps(buildStatus, failingSteps) 561 history := mockHistoryFunc(map[string][]*gitpb.Commit{}) 562 So(handleBuild(c, build, mockCheckoutFunc(nil), history), ShouldBeNil) 563 564 // Fetch the new tree closer. 565 So(datastore.Get(c, tc), ShouldBeNil) 566 return tc 567 } 568 569 testStatus := func(buildStatus buildbucketpb.Status, initialStatus, expectedNewStatus config.TreeCloserStatus, expectingUpdatedTimestamp bool, failingSteps []string) { 570 tc := runHandleBuild(buildStatus, initialStatus, failingSteps) 571 572 // Assert the resulting state of the tree closer. 573 So(tc.Status, ShouldEqual, expectedNewStatus) 574 So(tc.Timestamp.After(initialTimestamp), ShouldEqual, expectingUpdatedTimestamp) 575 } 576 577 // We want to exhaustively test all combinations of the following: 578 // * Did the build succeed? 579 // * If not, do the filters (if any) match? 580 // * Is the resulting status the same as the old status? 581 // All possibilities are explored in the tests below. 582 583 Convey(`Build passed, Closed -> Open`, func() { 584 testStatus(buildbucketpb.Status_SUCCESS, config.Closed, config.Open, true, []string{}) 585 }) 586 587 Convey(`Build passed, Open -> Open`, func() { 588 testStatus(buildbucketpb.Status_SUCCESS, config.Open, config.Open, true, []string{}) 589 }) 590 591 Convey(`Build failed, filters don't match, Closed -> Open`, func() { 592 testStatus(buildbucketpb.Status_FAILURE, config.Closed, config.Open, true, []string{"exclude"}) 593 }) 594 595 Convey(`Build failed, filters don't match, Open -> Open`, func() { 596 testStatus(buildbucketpb.Status_FAILURE, config.Open, config.Open, true, []string{"exclude"}) 597 }) 598 599 Convey(`Build failed, filters match, Open -> Closed`, func() { 600 testStatus(buildbucketpb.Status_FAILURE, config.Open, config.Closed, true, []string{"include"}) 601 }) 602 603 Convey(`Build failed, filters match, Closed -> Closed`, func() { 604 testStatus(buildbucketpb.Status_FAILURE, config.Closed, config.Closed, true, []string{"include"}) 605 }) 606 607 // In addition, we want to test that statuses other than SUCCESS and FAILURE don't 608 // cause any updates, regardless of the initial state. 609 610 Convey(`Infra failure, stays Open`, func() { 611 testStatus(buildbucketpb.Status_INFRA_FAILURE, config.Open, config.Open, false, []string{"include"}) 612 }) 613 614 Convey(`Infra failure, stays Closed`, func() { 615 testStatus(buildbucketpb.Status_INFRA_FAILURE, config.Closed, config.Closed, false, []string{"include"}) 616 }) 617 618 // Test that the correct status message is generated. 619 Convey(`Status message`, func() { 620 tc := runHandleBuild(buildbucketpb.Status_FAILURE, config.Open, []string{"include"}) 621 622 So(tc.Message, ShouldEqual, `Builder test-builder-tree-closer failed on steps "include"`) 623 }) 624 625 Convey(`All failed steps listed if no filter`, func() { 626 // Insert the tree closer to test into datastore. 627 builderKey := datastore.KeyForObj(c, &config.Builder{ 628 ProjectKey: datastore.KeyForObj(c, &config.Project{Name: "chromium"}), 629 ID: "ci/test-builder-tree-closer", 630 }) 631 632 tc := &config.TreeCloser{ 633 BuilderKey: builderKey, 634 TreeStatusHost: "chromium-status.appspot.com", 635 TreeCloser: apicfg.TreeCloser{Template: "template"}, 636 Status: config.Open, 637 Timestamp: initialTimestamp, 638 } 639 So(datastore.Put(c, tc), ShouldBeNil) 640 641 // Handle a new build. 642 build := dummyBuildWithFailingSteps(buildbucketpb.Status_FAILURE, []string{"step1", "step2"}) 643 history := mockHistoryFunc(map[string][]*gitpb.Commit{}) 644 So(handleBuild(c, build, mockCheckoutFunc(nil), history), ShouldBeNil) 645 646 // Fetch the new tree closer. 647 So(datastore.Get(c, tc), ShouldBeNil) 648 649 So(tc.Message, ShouldEqual, `Builder test-builder-tree-closer failed on steps "step1", "step2"`) 650 }) 651 }) 652 } 653 654 func makeBuilders(c context.Context, projectID string, cfg *apicfg.ProjectConfig) []*config.Builder { 655 var builders []*config.Builder 656 parentKey := datastore.MakeKey(c, "Project", projectID) 657 for _, cfgNotifier := range cfg.Notifiers { 658 for _, cfgBuilder := range cfgNotifier.Builders { 659 builders = append(builders, &config.Builder{ 660 ProjectKey: parentKey, 661 ID: fmt.Sprintf("%s/%s", cfgBuilder.Bucket, cfgBuilder.Name), 662 Repository: cfgBuilder.Repository, 663 Notifications: apicfg.Notifications{ 664 Notifications: cfgNotifier.Notifications, 665 }, 666 }) 667 } 668 } 669 return builders 670 } 671 672 func mockCheckoutFunc(c Checkout) CheckoutFunc { 673 return func(_ context.Context, _ *Build) (Checkout, error) { 674 return c, nil 675 } 676 } 677 678 func mockCheckoutReturnsErrorFunc() CheckoutFunc { 679 return func(_ context.Context, _ *Build) (Checkout, error) { 680 return nil, errors.New("Some error") 681 } 682 } 683 684 func TestExtractBuild(t *testing.T) { 685 t.Parallel() 686 687 Convey("builds_v2 pubsub message", t, func() { 688 Convey("success", func() { 689 ctx := memory.Use(context.Background()) 690 props := &structpb.Struct{ 691 Fields: map[string]*structpb.Value{ 692 "email_notify": structpb.NewListValue(&structpb.ListValue{Values: []*structpb.Value{ 693 structpb.NewStructValue(&structpb.Struct{ 694 Fields: map[string]*structpb.Value{ 695 "email": { 696 Kind: &structpb.Value_StringValue{ 697 StringValue: "abc@gmail.com", 698 }, 699 }, 700 }, 701 }), 702 }}), 703 }, 704 } 705 originalBuild := &buildbucketpb.Build{ 706 Id: 123, 707 Builder: &buildbucketpb.BuilderID{ 708 Project: "project", 709 Bucket: "bucket", 710 Builder: "builder", 711 }, 712 Status: buildbucketpb.Status_SUCCESS, 713 Infra: &buildbucketpb.BuildInfra{ 714 Buildbucket: &buildbucketpb.BuildInfra_Buildbucket{ 715 Hostname: "buildbuckt.com", 716 }, 717 }, 718 Input: &buildbucketpb.Build_Input{}, 719 Output: &buildbucketpb.Build_Output{ 720 Properties: props, 721 }, 722 Steps: []*buildbucketpb.Step{{Name: "step1"}}, 723 } 724 pubsubMsg, err := makeBuildsV2PubsubMsg(originalBuild) 725 So(err, ShouldBeNil) 726 b, err := extractBuild(ctx, &http.Request{Body: pubsubMsg}) 727 So(err, ShouldBeNil) 728 So(b.Id, ShouldEqual, originalBuild.Id) 729 So(b.Builder, ShouldResembleProto, originalBuild.Builder) 730 So(b.Status, ShouldEqual, buildbucketpb.Status_SUCCESS) 731 So(b.Infra, ShouldResembleProto, originalBuild.Infra) 732 So(b.Input, ShouldResembleProto, originalBuild.Input) 733 So(b.Output, ShouldResembleProto, originalBuild.Output) 734 So(b.Steps, ShouldResembleProto, originalBuild.Steps) 735 So(b.BuildbucketHostname, ShouldEqual, originalBuild.Infra.Buildbucket.Hostname) 736 So(b.EmailNotify, ShouldResemble, []EmailNotify{{Email: "abc@gmail.com"}}) 737 }) 738 739 Convey("success with no email_notify field", func() { 740 ctx := memory.Use(context.Background()) 741 props := &structpb.Struct{ 742 Fields: map[string]*structpb.Value{ 743 "other": structpb.NewListValue(&structpb.ListValue{Values: []*structpb.Value{ 744 structpb.NewStructValue(&structpb.Struct{ 745 Fields: map[string]*structpb.Value{ 746 "other": { 747 Kind: &structpb.Value_StringValue{ 748 StringValue: "other", 749 }, 750 }, 751 }, 752 }), 753 }}), 754 }, 755 } 756 originalBuild := &buildbucketpb.Build{ 757 Id: 123, 758 Builder: &buildbucketpb.BuilderID{ 759 Project: "project", 760 Bucket: "bucket", 761 Builder: "builder", 762 }, 763 Status: buildbucketpb.Status_CANCELED, 764 Infra: &buildbucketpb.BuildInfra{ 765 Buildbucket: &buildbucketpb.BuildInfra_Buildbucket{ 766 Hostname: "buildbuckt.com", 767 }, 768 }, 769 Input: &buildbucketpb.Build_Input{}, 770 Output: &buildbucketpb.Build_Output{ 771 Properties: props, 772 }, 773 Steps: []*buildbucketpb.Step{{Name: "step1"}}, 774 } 775 pubsubMsg, err := makeBuildsV2PubsubMsg(originalBuild) 776 So(err, ShouldBeNil) 777 b, err := extractBuild(ctx, &http.Request{Body: pubsubMsg}) 778 So(err, ShouldBeNil) 779 So(b.Id, ShouldEqual, originalBuild.Id) 780 So(b.Builder, ShouldResembleProto, originalBuild.Builder) 781 So(b.Status, ShouldEqual, buildbucketpb.Status_CANCELED) 782 So(b.Infra, ShouldResembleProto, originalBuild.Infra) 783 So(b.Input, ShouldResembleProto, originalBuild.Input) 784 So(b.Output, ShouldResembleProto, originalBuild.Output) 785 So(b.Steps, ShouldResembleProto, originalBuild.Steps) 786 So(b.BuildbucketHostname, ShouldEqual, originalBuild.Infra.Buildbucket.Hostname) 787 So(b.EmailNotify, ShouldBeNil) 788 }) 789 790 Convey("incompleted build", func() { 791 ctx := memory.Use(context.Background()) 792 originalBuild := &buildbucketpb.Build{ 793 Id: 123, 794 Builder: &buildbucketpb.BuilderID{ 795 Project: "project", 796 Bucket: "bucket", 797 Builder: "builder", 798 }, 799 Status: buildbucketpb.Status_SCHEDULED, 800 Infra: &buildbucketpb.BuildInfra{ 801 Buildbucket: &buildbucketpb.BuildInfra_Buildbucket{ 802 Hostname: "buildbuckt.com", 803 }, 804 }, 805 Input: &buildbucketpb.Build_Input{}, 806 Output: &buildbucketpb.Build_Output{}, 807 Steps: []*buildbucketpb.Step{{Name: "step1"}}, 808 } 809 pubsubMsg, err := makeBuildsV2PubsubMsg(originalBuild) 810 So(err, ShouldBeNil) 811 b, err := extractBuild(ctx, &http.Request{Body: pubsubMsg}) 812 So(err, ShouldBeNil) 813 So(b, ShouldBeNil) 814 }) 815 816 }) 817 } 818 819 func makeBuildsV2PubsubMsg(b *buildbucketpb.Build) (io.ReadCloser, error) { 820 copyB := proto.Clone(b).(*buildbucketpb.Build) 821 large := &buildbucketpb.Build{ 822 Input: &buildbucketpb.Build_Input{ 823 Properties: copyB.GetInput().GetProperties(), 824 }, 825 Output: &buildbucketpb.Build_Output{ 826 Properties: copyB.GetOutput().GetProperties(), 827 }, 828 Steps: copyB.GetSteps(), 829 } 830 if copyB.Input != nil { 831 copyB.Input.Properties = nil 832 } 833 if copyB.Output != nil { 834 copyB.Output.Properties = nil 835 } 836 copyB.Steps = nil 837 compress := func(data []byte) ([]byte, error) { 838 buf := &bytes.Buffer{} 839 zw := zlib.NewWriter(buf) 840 if _, err := zw.Write(data); err != nil { 841 return nil, errors.Annotate(err, "failed to compress").Err() 842 } 843 if err := zw.Close(); err != nil { 844 return nil, errors.Annotate(err, "error closing zlib writer").Err() 845 } 846 return buf.Bytes(), nil 847 } 848 largeBytes, err := proto.Marshal(large) 849 if err != nil { 850 return nil, errors.Annotate(err, "failed to marshal large").Err() 851 } 852 compressedLarge, err := compress(largeBytes) 853 if err != nil { 854 return nil, err 855 } 856 data, _ := protojson.Marshal(&buildbucketpb.BuildsV2PubSub{ 857 Build: copyB, 858 BuildLargeFields: compressedLarge, 859 }) 860 isCompleted := copyB.Status&buildbucketpb.Status_ENDED_MASK == buildbucketpb.Status_ENDED_MASK 861 attrs := map[string]any{ 862 "project": copyB.Builder.GetProject(), 863 "bucket": copyB.Builder.GetBucket(), 864 "builder": copyB.Builder.GetBuilder(), 865 "is_completed": strconv.FormatBool(isCompleted), 866 "version": "v2", 867 } 868 msg := struct { 869 Message struct { 870 Data string 871 Attributes map[string]any 872 } 873 }{struct { 874 Data string 875 Attributes map[string]any 876 }{Data: base64.StdEncoding.EncodeToString(data), Attributes: attrs}} 877 jmsg, _ := json.Marshal(msg) 878 return io.NopCloser(bytes.NewReader(jmsg)), nil 879 }