go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/impl/handler/tryjobs_test.go (about) 1 // Copyright 2021 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 handler 16 17 import ( 18 "context" 19 "fmt" 20 "testing" 21 "time" 22 23 bbpb "go.chromium.org/luci/buildbucket/proto" 24 "google.golang.org/protobuf/proto" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 gerritpb "go.chromium.org/luci/common/proto/gerrit" 28 "go.chromium.org/luci/gae/service/datastore" 29 30 cfgpb "go.chromium.org/luci/cv/api/config/v2" 31 "go.chromium.org/luci/cv/internal/changelist" 32 "go.chromium.org/luci/cv/internal/common" 33 "go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest" 34 "go.chromium.org/luci/cv/internal/cvtesting" 35 "go.chromium.org/luci/cv/internal/gerrit" 36 gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake" 37 "go.chromium.org/luci/cv/internal/run" 38 "go.chromium.org/luci/cv/internal/run/eventpb" 39 "go.chromium.org/luci/cv/internal/run/impl/state" 40 "go.chromium.org/luci/cv/internal/run/impl/submit" 41 "go.chromium.org/luci/cv/internal/tryjob" 42 43 . "github.com/smartystreets/goconvey/convey" 44 . "go.chromium.org/luci/common/testing/assertions" 45 ) 46 47 func TestOnTryjobsUpdated(t *testing.T) { 48 t.Parallel() 49 50 Convey("OnTryjobsUpdated", t, func() { 51 ct := cvtesting.Test{} 52 ctx, cancel := ct.SetUp(t) 53 defer cancel() 54 55 const lProject = "infra" 56 57 now := ct.Clock.Now().UTC() 58 rid := common.MakeRunID(lProject, now, 1, []byte("deadbeef")) 59 rs := &state.RunState{ 60 Run: run.Run{ 61 ID: rid, 62 Status: run.Status_RUNNING, 63 CreateTime: now.Add(-2 * time.Minute), 64 StartTime: now.Add(-1 * time.Minute), 65 CLs: common.CLIDs{1}, 66 }, 67 } 68 h, _ := makeTestHandler(&ct) 69 70 Convey("Enqueue longop", func() { 71 res, err := h.OnTryjobsUpdated(ctx, rs, common.MakeTryjobIDs(456, 789, 456)) 72 So(err, ShouldBeNil) 73 So(res.SideEffectFn, ShouldBeNil) 74 So(res.PreserveEvents, ShouldBeFalse) 75 So(res.State.OngoingLongOps.GetOps(), ShouldHaveLength, 1) 76 for _, op := range res.State.OngoingLongOps.GetOps() { 77 So(op, ShouldResembleProto, &run.OngoingLongOps_Op{ 78 Deadline: timestamppb.New(ct.Clock.Now().UTC().Add(maxTryjobExecutorDuration)), 79 Work: &run.OngoingLongOps_Op_ExecuteTryjobs{ 80 ExecuteTryjobs: &tryjob.ExecuteTryjobsPayload{ 81 TryjobsUpdated: []int64{456, 789}, // Also deduped 456 82 }, 83 }, 84 }) 85 } 86 }) 87 88 Convey("Defer if an tryjob execute task is ongoing", func() { 89 enqueueTryjobsUpdatedTask(ctx, rs, common.TryjobIDs{123}) 90 res, err := h.OnTryjobsUpdated(ctx, rs, common.MakeTryjobIDs(456, 789, 456)) 91 So(err, ShouldBeNil) 92 So(res.SideEffectFn, ShouldBeNil) 93 So(res.PreserveEvents, ShouldBeTrue) 94 }) 95 96 statuses := []run.Status{ 97 run.Status_SUCCEEDED, 98 run.Status_FAILED, 99 run.Status_CANCELLED, 100 run.Status_WAITING_FOR_SUBMISSION, 101 run.Status_SUBMITTING, 102 } 103 for _, status := range statuses { 104 Convey(fmt.Sprintf("Noop when Run is %s", status), func() { 105 rs.Status = status 106 res, err := h.OnTryjobsUpdated(ctx, rs, common.TryjobIDs{123}) 107 So(err, ShouldBeNil) 108 So(res.State, ShouldEqual, rs) 109 So(res.SideEffectFn, ShouldBeNil) 110 So(res.PreserveEvents, ShouldBeFalse) 111 }) 112 } 113 }) 114 } 115 116 func TestOnCompletedExecuteTryjobs(t *testing.T) { 117 t.Parallel() 118 119 Convey("OnCompletedExecuteTryjobs works", t, func() { 120 ct := cvtesting.Test{} 121 ctx, cancel := ct.SetUp(t) 122 defer cancel() 123 124 const ( 125 lProject = "chromium" 126 gHost = "example-review.googlesource.com" 127 opID = "1-1" 128 ) 129 now := ct.Clock.Now().UTC() 130 131 prjcfgtest.Create(ctx, lProject, &cfgpb.Config{ConfigGroups: []*cfgpb.ConfigGroup{{Name: "single"}}}) 132 rs := &state.RunState{ 133 Run: run.Run{ 134 ID: common.MakeRunID(lProject, now, 1, []byte("deadbeef")), 135 Status: run.Status_RUNNING, 136 CreateTime: now.Add(-2 * time.Minute), 137 StartTime: now.Add(-1 * time.Minute), 138 Mode: run.DryRun, 139 ConfigGroupID: prjcfgtest.MustExist(ctx, lProject).ConfigGroupIDs[0], 140 CLs: common.CLIDs{1}, 141 OngoingLongOps: &run.OngoingLongOps{ 142 Ops: map[string]*run.OngoingLongOps_Op{ 143 opID: { 144 Work: &run.OngoingLongOps_Op_ExecuteTryjobs{ 145 ExecuteTryjobs: &tryjob.ExecuteTryjobsPayload{ 146 TryjobsUpdated: []int64{123}, 147 }, 148 }, 149 }, 150 }, 151 }, 152 }, 153 } 154 155 for _, clid := range rs.CLs { 156 ci := gf.CI(100 + int(clid)) 157 So(datastore.Put(ctx, 158 &run.RunCL{ 159 ID: clid, 160 Run: datastore.MakeKey(ctx, common.RunKind, string(rs.ID)), 161 ExternalID: changelist.MustGobID(gHost, ci.GetNumber()), 162 Detail: &changelist.Snapshot{ 163 LuciProject: lProject, 164 Kind: &changelist.Snapshot_Gerrit{ 165 Gerrit: &changelist.Gerrit{ 166 Host: gHost, 167 Info: proto.Clone(ci).(*gerritpb.ChangeInfo), 168 }, 169 }, 170 Patchset: 2, 171 }, 172 }, 173 ), ShouldBeNil) 174 } 175 176 result := &eventpb.LongOpCompleted{ 177 OperationId: opID, 178 } 179 h, _ := makeTestHandler(&ct) 180 181 for _, longOpStatus := range []eventpb.LongOpCompleted_Status{ 182 eventpb.LongOpCompleted_EXPIRED, 183 eventpb.LongOpCompleted_FAILED, 184 } { 185 Convey(fmt.Sprintf("on long op %s", longOpStatus), func() { 186 result.Status = longOpStatus 187 res, err := h.OnLongOpCompleted(ctx, rs, result) 188 So(err, ShouldBeNil) 189 So(res.State.Status, ShouldEqual, run.Status_RUNNING) 190 So(res.State.OngoingLongOps.GetOps(), ShouldHaveLength, 1) 191 for _, op := range res.State.OngoingLongOps.GetOps() { 192 So(op.GetResetTriggers(), ShouldNotBeNil) 193 So(op.GetResetTriggers().GetRunStatusIfSucceeded(), ShouldEqual, run.Status_FAILED) 194 } 195 So(res.SideEffectFn, ShouldBeNil) 196 So(res.PreserveEvents, ShouldBeFalse) 197 }) 198 } 199 200 Convey("on long op success", func() { 201 result.Status = eventpb.LongOpCompleted_SUCCEEDED 202 203 Convey("Tryjob execution still running", func() { 204 err := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 205 return tryjob.SaveExecutionState(ctx, rs.ID, &tryjob.ExecutionState{ 206 Status: tryjob.ExecutionState_RUNNING, 207 }, 0, nil) 208 }, nil) 209 So(err, ShouldBeNil) 210 res, err := h.OnLongOpCompleted(ctx, rs, result) 211 So(err, ShouldBeNil) 212 So(res.State.Status, ShouldEqual, run.Status_RUNNING) 213 So(res.State.Tryjobs, ShouldResembleProto, &run.Tryjobs{ 214 State: &tryjob.ExecutionState{ 215 Status: tryjob.ExecutionState_RUNNING, 216 }, 217 }) 218 So(res.State.OngoingLongOps, ShouldBeNil) 219 So(res.SideEffectFn, ShouldBeNil) 220 So(res.PreserveEvents, ShouldBeFalse) 221 }) 222 223 Convey("Tryjob execution succeeds", func() { 224 err := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 225 return tryjob.SaveExecutionState(ctx, rs.ID, &tryjob.ExecutionState{ 226 Status: tryjob.ExecutionState_SUCCEEDED, 227 }, 0, nil) 228 }, nil) 229 So(err, ShouldBeNil) 230 Convey("Full Run", func() { 231 rs.Mode = run.FullRun 232 ctx = context.WithValue(ctx, &fakeTaskIDKey, "task-foo") 233 res, err := h.OnLongOpCompleted(ctx, rs, result) 234 So(err, ShouldBeNil) 235 So(res.State.Status, ShouldEqual, run.Status_SUBMITTING) 236 So(res.State.Tryjobs, ShouldResembleProto, &run.Tryjobs{ 237 State: &tryjob.ExecutionState{ 238 Status: tryjob.ExecutionState_SUCCEEDED, 239 }, 240 }) 241 So(res.State.OngoingLongOps, ShouldBeNil) 242 So(res.State.Submission, ShouldNotBeNil) 243 So(submit.MustCurrentRun(ctx, lProject), ShouldEqual, rs.ID) 244 So(res.SideEffectFn, ShouldBeNil) 245 So(res.PreserveEvents, ShouldBeFalse) 246 }) 247 Convey("Dry Run", func() { 248 rs.Mode = run.DryRun 249 res, err := h.OnLongOpCompleted(ctx, rs, result) 250 So(err, ShouldBeNil) 251 So(res.State.Status, ShouldEqual, run.Status_RUNNING) 252 So(res.State.Tryjobs, ShouldResembleProto, &run.Tryjobs{ 253 State: &tryjob.ExecutionState{ 254 Status: tryjob.ExecutionState_SUCCEEDED, 255 }, 256 }) 257 So(res.State.OngoingLongOps.GetOps(), ShouldHaveLength, 1) 258 for _, op := range res.State.OngoingLongOps.GetOps() { 259 So(op.GetResetTriggers(), ShouldNotBeNil) 260 So(op.GetResetTriggers().GetRequests(), ShouldResembleProto, []*run.OngoingLongOps_Op_ResetTriggers_Request{ 261 { 262 Clid: int64(rs.CLs[0]), 263 Message: "This CL has passed the run", 264 Notify: gerrit.Whoms{ 265 gerrit.Whom_OWNER, 266 gerrit.Whom_CQ_VOTERS, 267 }, 268 }, 269 }) 270 So(op.GetResetTriggers().GetRunStatusIfSucceeded(), ShouldEqual, run.Status_SUCCEEDED) 271 } 272 So(res.SideEffectFn, ShouldBeNil) 273 So(res.PreserveEvents, ShouldBeFalse) 274 }) 275 Convey("New Patchset Run, no message is posted", func() { 276 rs.Mode = run.NewPatchsetRun 277 res, err := h.OnLongOpCompleted(ctx, rs, result) 278 So(err, ShouldBeNil) 279 So(res.State.Status, ShouldEqual, run.Status_RUNNING) 280 So(res.State.Tryjobs, ShouldResembleProto, &run.Tryjobs{ 281 State: &tryjob.ExecutionState{ 282 Status: tryjob.ExecutionState_SUCCEEDED, 283 }, 284 }) 285 So(res.State.OngoingLongOps.GetOps(), ShouldHaveLength, 1) 286 for _, op := range res.State.OngoingLongOps.GetOps() { 287 So(op.GetResetTriggers(), ShouldNotBeNil) 288 So(op.GetResetTriggers().GetRequests(), ShouldResembleProto, []*run.OngoingLongOps_Op_ResetTriggers_Request{ 289 {Clid: int64(rs.CLs[0])}, 290 }) 291 So(op.GetResetTriggers().GetRunStatusIfSucceeded(), ShouldEqual, run.Status_SUCCEEDED) 292 } 293 So(res.SideEffectFn, ShouldBeNil) 294 So(res.PreserveEvents, ShouldBeFalse) 295 }) 296 }) 297 298 Convey("Tryjob execution failed", func() { 299 Convey("tryjobs result failure", func() { 300 tj := tryjob.MustBuildbucketID("example.com", 12345).MustCreateIfNotExists(ctx) 301 tj.Result = &tryjob.Result{ 302 Backend: &tryjob.Result_Buildbucket_{ 303 Buildbucket: &tryjob.Result_Buildbucket{ 304 Builder: &bbpb.BuilderID{ 305 Project: lProject, 306 Bucket: "test", 307 Builder: "foo", 308 }, 309 SummaryMarkdown: "this is the summary", 310 }, 311 }, 312 } 313 So(datastore.Put(ctx, tj), ShouldBeNil) 314 315 err := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 316 return tryjob.SaveExecutionState(ctx, rs.ID, &tryjob.ExecutionState{ 317 Status: tryjob.ExecutionState_FAILED, 318 Failures: &tryjob.ExecutionState_Failures{ 319 UnsuccessfulResults: []*tryjob.ExecutionState_Failures_UnsuccessfulResult{ 320 {TryjobId: int64(tj.ID)}, 321 }, 322 }, 323 }, 0, nil) 324 }, nil) 325 So(err, ShouldBeNil) 326 res, err := h.OnLongOpCompleted(ctx, rs, result) 327 So(err, ShouldBeNil) 328 So(res.State.Status, ShouldEqual, run.Status_RUNNING) 329 So(res.State.Tryjobs, ShouldResembleProto, &run.Tryjobs{ 330 State: &tryjob.ExecutionState{ 331 Status: tryjob.ExecutionState_FAILED, 332 Failures: &tryjob.ExecutionState_Failures{ 333 UnsuccessfulResults: []*tryjob.ExecutionState_Failures_UnsuccessfulResult{ 334 {TryjobId: int64(tj.ID)}, 335 }, 336 }, 337 }, 338 }) 339 So(res.State.OngoingLongOps.GetOps(), ShouldHaveLength, 1) 340 for _, op := range res.State.OngoingLongOps.GetOps() { 341 So(op.GetResetTriggers(), ShouldNotBeNil) 342 So(op.GetResetTriggers().GetRequests(), ShouldResembleProto, []*run.OngoingLongOps_Op_ResetTriggers_Request{ 343 { 344 Clid: int64(rs.CLs[0]), 345 Message: "This CL has failed the run. Reason:\n\nTryjob [chromium/test/foo](https://example.com/build/12345) has failed with summary ([view all results](https://example-review.googlesource.com/c/101?checksPatchset=2&tab=checks)):\n\n---\nthis is the summary", 346 Notify: gerrit.Whoms{ 347 gerrit.Whom_OWNER, 348 gerrit.Whom_CQ_VOTERS, 349 }, 350 AddToAttention: gerrit.Whoms{ 351 gerrit.Whom_OWNER, 352 gerrit.Whom_CQ_VOTERS, 353 }, 354 AddToAttentionReason: "Tryjobs failed", 355 }, 356 }) 357 So(op.GetResetTriggers().GetRunStatusIfSucceeded(), ShouldEqual, run.Status_FAILED) 358 } 359 So(res.SideEffectFn, ShouldBeNil) 360 So(res.PreserveEvents, ShouldBeFalse) 361 }) 362 363 Convey("failed to launch tryjobs", func() { 364 def := &tryjob.Definition{ 365 Backend: &tryjob.Definition_Buildbucket_{ 366 Buildbucket: &tryjob.Definition_Buildbucket{ 367 Host: "example.com", 368 Builder: &bbpb.BuilderID{ 369 Project: lProject, 370 Bucket: "test", 371 Builder: "foo", 372 }, 373 }, 374 }, 375 } 376 err := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 377 return tryjob.SaveExecutionState(ctx, rs.ID, &tryjob.ExecutionState{ 378 Status: tryjob.ExecutionState_FAILED, 379 Failures: &tryjob.ExecutionState_Failures{ 380 LaunchFailures: []*tryjob.ExecutionState_Failures_LaunchFailure{ 381 { 382 Definition: def, 383 Reason: "permission denied", 384 }, 385 }, 386 }, 387 }, 0, nil) 388 }, nil) 389 So(err, ShouldBeNil) 390 res, err := h.OnLongOpCompleted(ctx, rs, result) 391 So(err, ShouldBeNil) 392 So(res.State.Status, ShouldEqual, run.Status_RUNNING) 393 So(res.State.Tryjobs, ShouldResembleProto, &run.Tryjobs{ 394 State: &tryjob.ExecutionState{ 395 Status: tryjob.ExecutionState_FAILED, 396 Failures: &tryjob.ExecutionState_Failures{ 397 LaunchFailures: []*tryjob.ExecutionState_Failures_LaunchFailure{ 398 { 399 Definition: def, 400 Reason: "permission denied", 401 }, 402 }, 403 }, 404 }, 405 }) 406 So(res.State.OngoingLongOps.GetOps(), ShouldHaveLength, 1) 407 for _, op := range res.State.OngoingLongOps.GetOps() { 408 So(op.GetResetTriggers(), ShouldNotBeNil) 409 So(op.GetResetTriggers().GetRequests(), ShouldResembleProto, []*run.OngoingLongOps_Op_ResetTriggers_Request{ 410 { 411 Clid: int64(rs.CLs[0]), 412 Message: "Failed to launch tryjob `chromium/test/foo`. Reason: permission denied", 413 Notify: gerrit.Whoms{ 414 gerrit.Whom_OWNER, 415 gerrit.Whom_CQ_VOTERS, 416 }, 417 AddToAttention: gerrit.Whoms{ 418 gerrit.Whom_OWNER, 419 gerrit.Whom_CQ_VOTERS, 420 }, 421 AddToAttentionReason: "Tryjobs failed", 422 }, 423 }) 424 So(op.GetResetTriggers().GetRunStatusIfSucceeded(), ShouldEqual, run.Status_FAILED) 425 } 426 So(res.SideEffectFn, ShouldBeNil) 427 So(res.PreserveEvents, ShouldBeFalse) 428 }) 429 430 Convey("Unknown error", func() { 431 err := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 432 return tryjob.SaveExecutionState(ctx, rs.ID, &tryjob.ExecutionState{ 433 Status: tryjob.ExecutionState_FAILED, 434 }, 0, nil) 435 }, nil) 436 So(err, ShouldBeNil) 437 res, err := h.OnLongOpCompleted(ctx, rs, result) 438 So(err, ShouldBeNil) 439 So(res.State.Status, ShouldEqual, run.Status_RUNNING) 440 So(res.State.Tryjobs, ShouldResembleProto, &run.Tryjobs{ 441 State: &tryjob.ExecutionState{ 442 Status: tryjob.ExecutionState_FAILED, 443 }, 444 }) 445 So(res.State.OngoingLongOps.GetOps(), ShouldHaveLength, 1) 446 for _, op := range res.State.OngoingLongOps.GetOps() { 447 So(op.GetResetTriggers(), ShouldNotBeNil) 448 So(op.GetResetTriggers().GetRequests(), ShouldResembleProto, []*run.OngoingLongOps_Op_ResetTriggers_Request{ 449 { 450 Clid: int64(rs.CLs[0]), 451 Message: "Unexpected error when processing Tryjobs. Please retry. If retry continues to fail, please contact LUCI team.\n\n" + cvBugLink, 452 Notify: gerrit.Whoms{ 453 gerrit.Whom_OWNER, 454 gerrit.Whom_CQ_VOTERS, 455 }, 456 AddToAttention: gerrit.Whoms{ 457 gerrit.Whom_OWNER, 458 gerrit.Whom_CQ_VOTERS, 459 }, 460 AddToAttentionReason: "Run failed", 461 }, 462 }) 463 So(op.GetResetTriggers().GetRunStatusIfSucceeded(), ShouldEqual, run.Status_FAILED) 464 } 465 So(res.SideEffectFn, ShouldBeNil) 466 So(res.PreserveEvents, ShouldBeFalse) 467 }) 468 469 Convey("Only reset trigger on root CL", func() { 470 err := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 471 return tryjob.SaveExecutionState(ctx, rs.ID, &tryjob.ExecutionState{ 472 Status: tryjob.ExecutionState_FAILED, 473 }, 0, nil) 474 }, nil) 475 So(err, ShouldBeNil) 476 anotherCL := &run.RunCL{ 477 ID: 1002, 478 Run: datastore.MakeKey(ctx, common.RunKind, string(rs.ID)), 479 ExternalID: changelist.MustGobID(gHost, 1002), 480 Detail: &changelist.Snapshot{ 481 LuciProject: lProject, 482 Kind: &changelist.Snapshot_Gerrit{ 483 Gerrit: &changelist.Gerrit{ 484 Host: gHost, 485 Info: gf.CI(1002), 486 }, 487 }, 488 Patchset: 2, 489 }, 490 } 491 So(datastore.Put(ctx, anotherCL), ShouldBeNil) 492 rs.CLs = append(rs.CLs, anotherCL.ID) 493 494 rs.RootCL = anotherCL.ID 495 res, err := h.OnLongOpCompleted(ctx, rs, result) 496 So(err, ShouldBeNil) 497 So(res.State.OngoingLongOps.GetOps(), ShouldHaveLength, 1) 498 for _, op := range res.State.OngoingLongOps.GetOps() { 499 So(op.GetResetTriggers(), ShouldNotBeNil) 500 So(op.GetResetTriggers().GetRequests(), ShouldResembleProto, []*run.OngoingLongOps_Op_ResetTriggers_Request{ 501 { 502 Clid: int64(anotherCL.ID), 503 Message: "Unexpected error when processing Tryjobs. Please retry. If retry continues to fail, please contact LUCI team.\n\n" + cvBugLink, 504 Notify: gerrit.Whoms{ 505 gerrit.Whom_OWNER, 506 gerrit.Whom_CQ_VOTERS, 507 }, 508 AddToAttention: gerrit.Whoms{ 509 gerrit.Whom_OWNER, 510 gerrit.Whom_CQ_VOTERS, 511 }, 512 AddToAttentionReason: "Run failed", 513 }, 514 }) 515 So(op.GetResetTriggers().GetRunStatusIfSucceeded(), ShouldEqual, run.Status_FAILED) 516 } 517 }) 518 }) 519 }) 520 }) 521 } 522 523 func TestComposeTryjobsFailureReason(t *testing.T) { 524 Convey("ComposeTryjobsFailureReason", t, func() { 525 cl := &run.RunCL{ 526 ID: 1111, 527 ExternalID: changelist.MustGobID("example.review.com", 123), 528 Detail: &changelist.Snapshot{ 529 LuciProject: "test", 530 Kind: &changelist.Snapshot_Gerrit{ 531 Gerrit: &changelist.Gerrit{ 532 Host: "example.review.com", 533 }, 534 }, 535 Patchset: 2, 536 }, 537 } 538 Convey("panics", func() { 539 So(func() { 540 _ = composeTryjobsResultFailureReason(cl, nil) 541 }, ShouldPanicLike, "called without tryjobs") 542 }) 543 const bbHost = "test.com" 544 builder := &bbpb.BuilderID{ 545 Project: "test_proj", 546 Bucket: "test_bucket", 547 Builder: "test_builder", 548 } 549 Convey("works", func() { 550 Convey("single", func() { 551 Convey("restricted", func() { 552 r := composeTryjobsResultFailureReason(cl, []*tryjob.Tryjob{ 553 { 554 ExternalID: tryjob.MustBuildbucketID(bbHost, 123456790), 555 Definition: &tryjob.Definition{ 556 Backend: &tryjob.Definition_Buildbucket_{ 557 Buildbucket: &tryjob.Definition_Buildbucket{ 558 Builder: builder, 559 Host: bbHost, 560 }, 561 }, 562 ResultVisibility: cfgpb.CommentLevel_COMMENT_LEVEL_RESTRICTED, 563 }, 564 Result: &tryjob.Result{ 565 Backend: &tryjob.Result_Buildbucket_{ 566 Buildbucket: &tryjob.Result_Buildbucket{ 567 Builder: builder, 568 SummaryMarkdown: "A couple\nof lines\nwith secret details", 569 }, 570 }, 571 }, 572 }, 573 }) 574 So(r, ShouldEqual, "[Tryjob](https://test.com/build/123456790) has failed") 575 }) 576 Convey("not restricted", func() { 577 r := composeTryjobsResultFailureReason(cl, []*tryjob.Tryjob{ 578 { 579 ExternalID: tryjob.MustBuildbucketID(bbHost, 123456790), 580 Definition: &tryjob.Definition{ 581 Backend: &tryjob.Definition_Buildbucket_{ 582 Buildbucket: &tryjob.Definition_Buildbucket{ 583 Builder: builder, 584 Host: bbHost, 585 }, 586 }, 587 ResultVisibility: cfgpb.CommentLevel_COMMENT_LEVEL_FULL, 588 }, 589 Result: &tryjob.Result{ 590 Backend: &tryjob.Result_Buildbucket_{ 591 Buildbucket: &tryjob.Result_Buildbucket{ 592 Builder: builder, 593 SummaryMarkdown: "A couple\nof lines\nwith public details", 594 }, 595 }, 596 }, 597 }, 598 }) 599 So(r, ShouldEqual, "Tryjob [test_proj/test_bucket/test_builder](https://test.com/build/123456790) has failed with summary ([view all results](https://example.review.com/c/123?checksPatchset=2&tab=checks)):\n\n---\nA couple\nof lines\nwith public details") 600 }) 601 }) 602 603 Convey("multiple tryjobs", func() { 604 tjs := []*tryjob.Tryjob{ 605 // restricted. 606 { 607 ExternalID: tryjob.MustBuildbucketID("test.com", 123456790), 608 Definition: &tryjob.Definition{ 609 Backend: &tryjob.Definition_Buildbucket_{ 610 Buildbucket: &tryjob.Definition_Buildbucket{ 611 Builder: builder, 612 Host: bbHost, 613 }, 614 }, 615 ResultVisibility: cfgpb.CommentLevel_COMMENT_LEVEL_RESTRICTED, 616 }, 617 Result: &tryjob.Result{ 618 Backend: &tryjob.Result_Buildbucket_{ 619 Buildbucket: &tryjob.Result_Buildbucket{ 620 Builder: builder, 621 SummaryMarkdown: "A couple\nof lines\nwith secret details", 622 }, 623 }, 624 }, 625 }, 626 // un-restricted but empty summary markdown. 627 { 628 ExternalID: tryjob.MustBuildbucketID("test.com", 123456791), 629 Definition: &tryjob.Definition{ 630 Backend: &tryjob.Definition_Buildbucket_{ 631 Buildbucket: &tryjob.Definition_Buildbucket{ 632 Builder: builder, 633 Host: bbHost, 634 }, 635 }, 636 ResultVisibility: cfgpb.CommentLevel_COMMENT_LEVEL_FULL, 637 }, 638 Result: &tryjob.Result{ 639 Backend: &tryjob.Result_Buildbucket_{ 640 Buildbucket: &tryjob.Result_Buildbucket{ 641 Builder: builder, 642 }, 643 }, 644 }, 645 }, 646 // un-restricted. 647 { 648 ExternalID: tryjob.MustBuildbucketID("test.com", 123456792), 649 Definition: &tryjob.Definition{ 650 Backend: &tryjob.Definition_Buildbucket_{ 651 Buildbucket: &tryjob.Definition_Buildbucket{ 652 Builder: builder, 653 Host: bbHost, 654 }, 655 }, 656 ResultVisibility: cfgpb.CommentLevel_COMMENT_LEVEL_FULL, 657 }, 658 Result: &tryjob.Result{ 659 Backend: &tryjob.Result_Buildbucket_{ 660 Buildbucket: &tryjob.Result_Buildbucket{ 661 SummaryMarkdown: "A couple\nof lines\nwith public details", 662 }, 663 }, 664 }, 665 }, 666 } 667 668 Convey("all public visibility", func() { 669 r := composeTryjobsResultFailureReason(cl, tjs[1:]) 670 So(r, ShouldEqual, "Failed Tryjobs:\n* [test_proj/test_bucket/test_builder](https://test.com/build/123456791)\n* [test_proj/test_bucket/test_builder](https://test.com/build/123456792). Summary ([view all results](https://example.review.com/c/123?checksPatchset=2&tab=checks)):\n\n---\nA couple\nof lines\nwith public details\n\n---") 671 }) 672 673 Convey("with one restricted visibility", func() { 674 r := composeTryjobsResultFailureReason(cl, tjs) 675 So(r, ShouldEqual, "Failed Tryjobs:\n* https://test.com/build/123456790\n* https://test.com/build/123456791\n* https://test.com/build/123456792") 676 }) 677 }) 678 }) 679 }) 680 } 681 682 func TestComposeLaunchFailureReason(t *testing.T) { 683 Convey("Compose Launch Failure Reason", t, func() { 684 defFoo := &tryjob.Definition{ 685 Backend: &tryjob.Definition_Buildbucket_{ 686 Buildbucket: &tryjob.Definition_Buildbucket{ 687 Host: "buildbucket.example.com", 688 Builder: &bbpb.BuilderID{ 689 Project: "ProjectFoo", 690 Bucket: "BucketFoo", 691 Builder: "BuilderFoo", 692 }, 693 }, 694 }, 695 } 696 Convey("Single", func() { 697 Convey("restricted", func() { 698 defFoo.ResultVisibility = cfgpb.CommentLevel_COMMENT_LEVEL_RESTRICTED 699 reason := composeLaunchFailureReason( 700 []*tryjob.ExecutionState_Failures_LaunchFailure{ 701 {Definition: defFoo, Reason: "permission denied"}, 702 }) 703 So(reason, ShouldEqual, "Failed to launch one tryjob. The tryjob name can't be shown due to configuration. Please contact your Project admin for help.") 704 }) 705 Convey("public", func() { 706 reason := composeLaunchFailureReason( 707 []*tryjob.ExecutionState_Failures_LaunchFailure{ 708 {Definition: defFoo, Reason: "permission denied"}, 709 }) 710 So(reason, ShouldEqual, "Failed to launch tryjob `ProjectFoo/BucketFoo/BuilderFoo`. Reason: permission denied") 711 }) 712 }) 713 defBar := &tryjob.Definition{ 714 Backend: &tryjob.Definition_Buildbucket_{ 715 Buildbucket: &tryjob.Definition_Buildbucket{ 716 Host: "buildbucket.example.com", 717 Builder: &bbpb.BuilderID{ 718 Project: "ProjectBar", 719 Bucket: "BucketBar", 720 Builder: "BuilderBar", 721 }, 722 }, 723 }, 724 } 725 Convey("Multiple", func() { 726 Convey("All restricted", func() { 727 defFoo.ResultVisibility = cfgpb.CommentLevel_COMMENT_LEVEL_RESTRICTED 728 defBar.ResultVisibility = cfgpb.CommentLevel_COMMENT_LEVEL_RESTRICTED 729 reason := composeLaunchFailureReason( 730 []*tryjob.ExecutionState_Failures_LaunchFailure{ 731 {Definition: defFoo, Reason: "permission denied"}, 732 {Definition: defBar, Reason: "builder not found"}, 733 }) 734 So(reason, ShouldEqual, "Failed to launch 2 tryjobs. The tryjob names can't be shown due to configuration. Please contact your Project admin for help.") 735 }) 736 Convey("Partial restricted", func() { 737 defBar.ResultVisibility = cfgpb.CommentLevel_COMMENT_LEVEL_RESTRICTED 738 reason := composeLaunchFailureReason( 739 []*tryjob.ExecutionState_Failures_LaunchFailure{ 740 {Definition: defFoo, Reason: "permission denied"}, 741 {Definition: defBar, Reason: "builder not found"}, 742 }) 743 So(reason, ShouldEqual, "Failed to launch the following tryjobs:\n* `ProjectFoo/BucketFoo/BuilderFoo`; Failure reason: permission denied\n\nIn addition to the tryjobs above, failed to launch 1 tryjob. But the tryjob names can't be shown due to configuration. Please contact your Project admin for help.") 744 }) 745 Convey("All public", func() { 746 reason := composeLaunchFailureReason( 747 []*tryjob.ExecutionState_Failures_LaunchFailure{ 748 {Definition: defFoo, Reason: "permission denied"}, 749 {Definition: defBar, Reason: "builder not found"}, 750 }) 751 So(reason, ShouldEqual, "Failed to launch the following tryjobs:\n* `ProjectBar/BucketBar/BuilderBar`; Failure reason: builder not found\n* `ProjectFoo/BucketFoo/BuilderFoo`; Failure reason: permission denied") 752 }) 753 }) 754 }) 755 }