go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/bugs/buganizer/manager_test.go (about) 1 // Copyright 2023 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 buganizer 16 17 import ( 18 "context" 19 "strconv" 20 "strings" 21 "testing" 22 "time" 23 24 "google.golang.org/protobuf/proto" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/third_party/google.golang.org/genproto/googleapis/devtools/issuetracker/v1" 31 32 "go.chromium.org/luci/analysis/internal/bugs" 33 bugspb "go.chromium.org/luci/analysis/internal/bugs/proto" 34 "go.chromium.org/luci/analysis/internal/clustering" 35 "go.chromium.org/luci/analysis/internal/config" 36 configpb "go.chromium.org/luci/analysis/proto/config" 37 38 . "github.com/smartystreets/goconvey/convey" 39 . "go.chromium.org/luci/common/testing/assertions" 40 ) 41 42 func TestBugManager(t *testing.T) { 43 t.Parallel() 44 45 Convey("With Bug Manager", t, func() { 46 ctx := context.Background() 47 fakeClient := NewFakeClient() 48 fakeStore := fakeClient.FakeStore 49 buganizerCfg := ChromeOSTestConfig() 50 51 policyA := config.CreatePlaceholderBugManagementPolicy("policy-a") 52 policyA.HumanReadableName = "Problem A" 53 policyA.Priority = configpb.BuganizerPriority_P4 54 policyA.BugTemplate.Buganizer.Hotlists = []int64{1001} 55 56 policyB := config.CreatePlaceholderBugManagementPolicy("policy-b") 57 policyB.HumanReadableName = "Problem B" 58 policyB.Priority = configpb.BuganizerPriority_P0 59 policyB.BugTemplate.Buganizer.Hotlists = []int64{1002} 60 61 policyC := config.CreatePlaceholderBugManagementPolicy("policy-c") 62 policyC.HumanReadableName = "Problem C" 63 policyC.Priority = configpb.BuganizerPriority_P1 64 policyC.BugTemplate.Buganizer.Hotlists = []int64{1003} 65 66 projectCfg := &configpb.ProjectConfig{ 67 BugManagement: &configpb.BugManagement{ 68 DefaultBugSystem: configpb.BugSystem_BUGANIZER, 69 Buganizer: buganizerCfg, 70 Policies: []*configpb.BugManagementPolicy{ 71 policyA, 72 policyB, 73 policyC, 74 }, 75 }, 76 } 77 78 bm, err := NewBugManager(fakeClient, "https://luci-analysis-test.appspot.com", "chromeos", "email@test.com", projectCfg, false) 79 So(err, ShouldBeNil) 80 now := time.Date(2044, time.April, 4, 4, 4, 4, 4, time.UTC) 81 ctx, tc := testclock.UseTime(ctx, now) 82 83 Convey("Create", func() { 84 createRequest := newCreateRequest() 85 createRequest.ActivePolicyIDs = map[bugs.PolicyID]struct{}{ 86 "policy-a": {}, // P4 87 } 88 expectedIssue := &issuetracker.Issue{ 89 IssueId: 1, 90 IssueState: &issuetracker.IssueState{ 91 ComponentId: buganizerCfg.DefaultComponent.Id, 92 Type: issuetracker.Issue_BUG, 93 Status: issuetracker.Issue_NEW, 94 Severity: issuetracker.Issue_S2, 95 Priority: issuetracker.Issue_P4, 96 Title: "Tests are failing: Expected equality of these values: \"Expected_Value\" my_expr.evaluate(123) Which is: \"Unexpected_Value\"", 97 Ccs: []*issuetracker.User{ 98 { 99 EmailAddress: "testcc1@google.com", 100 }, 101 { 102 EmailAddress: "testcc2@google.com", 103 }, 104 }, 105 HotlistIds: []int64{1001}, 106 AccessLimit: &issuetracker.IssueAccessLimit{ 107 AccessLevel: issuetracker.IssueAccessLimit_LIMIT_NONE, 108 }, 109 }, 110 CreatedTime: timestamppb.New(clock.Now(ctx)), 111 ModifiedTime: timestamppb.New(clock.Now(ctx)), 112 } 113 114 Convey("With reason-based failure cluster", func() { 115 reason := `Expected equality of these values: 116 "Expected_Value" 117 my_expr.evaluate(123) 118 Which is: "Unexpected_Value"` 119 createRequest.Description.Title = reason 120 createRequest.Description.Description = "A cluster of failures has been found with reason: " + reason 121 122 expectedIssue.Description = &issuetracker.IssueComment{ 123 CommentNumber: 1, 124 Comment: "A cluster of failures has been found with reason: Expected equality " + 125 "of these values:\n\t\t\t\t\t\"Expected_Value\"\n\t\t\t\t\tmy_expr.evaluate(123)\n\t\t\t\t\t\t" + 126 "Which is: \"Unexpected_Value\"\n" + 127 "\n" + 128 "These test failures are causing problem(s) which require your attention, including:\n" + 129 "- Problem A\n" + 130 "\n" + 131 "See current problems, failure examples and more in LUCI Analysis at: https://luci-analysis-test.appspot.com/p/chromeos/rules/new-rule-id\n" + 132 "\n" + 133 "How to action this bug: https://luci-analysis-test.appspot.com/help#new-bug-filed\n" + 134 "Provide feedback: https://luci-analysis-test.appspot.com/help#feedback\n" + 135 "Was this bug filed in the wrong component? See: https://luci-analysis-test.appspot.com/help#component-selection", 136 } 137 138 Convey("Base case", func() { 139 response := bm.Create(ctx, createRequest) 140 So(response, ShouldResemble, bugs.BugCreateResponse{ 141 ID: "1", 142 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 143 "policy-a": {}, 144 }, 145 }) 146 So(len(fakeStore.Issues), ShouldEqual, 1) 147 148 issueData := fakeStore.Issues[1] 149 So(issueData.Issue, ShouldResembleProto, expectedIssue) 150 }) 151 152 Convey("Policy with comment template", func() { 153 policyA.BugTemplate.CommentTemplate = "RuleURL:{{.RuleURL}},BugID:{{if .BugID.IsBuganizer}}{{.BugID.BuganizerBugID}}{{end}}" 154 155 bm, err := NewBugManager(fakeClient, "https://luci-analysis-test.appspot.com", "chromeos", "email@test.com", projectCfg, false) 156 So(err, ShouldBeNil) 157 158 response := bm.Create(ctx, createRequest) 159 So(response, ShouldResemble, bugs.BugCreateResponse{ 160 ID: "1", 161 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 162 "policy-a": {}, 163 }, 164 }) 165 So(len(fakeStore.Issues), ShouldEqual, 1) 166 167 issueData := fakeStore.Issues[1] 168 So(issueData.Issue, ShouldResembleProto, expectedIssue) 169 // Expect no comment for policy-a's activation, just the initial issue description. 170 So(len(issueData.Comments), ShouldEqual, 2) 171 So(issueData.Comments[1].Comment, ShouldEqual, "RuleURL:https://luci-analysis-test.appspot.com/p/chromeos/rules/new-rule-id,BugID:1\n\n"+ 172 "Why LUCI Analysis posted this comment: https://luci-analysis-test.appspot.com/help#policy-activated (Policy ID: policy-a)") 173 }) 174 Convey("Policy has no comment template", func() { 175 policyA.BugTemplate.CommentTemplate = "" 176 177 bm, err := NewBugManager(fakeClient, "https://luci-analysis-test.appspot.com", "chromeos", "email@test.com", projectCfg, false) 178 So(err, ShouldBeNil) 179 180 response := bm.Create(ctx, createRequest) 181 So(response, ShouldResemble, bugs.BugCreateResponse{ 182 ID: "1", 183 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 184 "policy-a": {}, 185 }, 186 }) 187 So(len(fakeStore.Issues), ShouldEqual, 1) 188 189 issueData := fakeStore.Issues[1] 190 So(issueData.Issue, ShouldResembleProto, expectedIssue) 191 // Expect no comment for policy-a's activation, just the initial issue description. 192 So(len(issueData.Comments), ShouldEqual, 1) 193 }) 194 Convey("Multiple policies activated", func() { 195 createRequest.ActivePolicyIDs = map[bugs.PolicyID]struct{}{ 196 "policy-a": {}, // P4 197 "policy-b": {}, // P0 198 "policy-c": {}, // P1 199 } 200 expectedIssue.Description.Comment = strings.Replace(expectedIssue.Description.Comment, "- Problem A\n", "- Problem B\n- Problem C\n- Problem A\n", 1) 201 expectedIssue.IssueState.Priority = issuetracker.Issue_P0 202 expectedIssue.IssueState.HotlistIds = []int64{1001, 1002, 1003} 203 204 // Act 205 response := bm.Create(ctx, createRequest) 206 207 // Verify 208 So(response, ShouldResemble, bugs.BugCreateResponse{ 209 ID: "1", 210 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 211 "policy-a": {}, 212 "policy-b": {}, 213 "policy-c": {}, 214 }, 215 }) 216 So(len(fakeStore.Issues), ShouldEqual, 1) 217 218 issueData := fakeStore.Issues[1] 219 So(issueData.Issue, ShouldResembleProto, expectedIssue) 220 So(len(issueData.Comments), ShouldEqual, 4) 221 // Policy notifications should be in order of priority. 222 So(issueData.Comments[1].Comment, ShouldStartWith, "Policy ID: policy-b") 223 So(issueData.Comments[2].Comment, ShouldStartWith, "Policy ID: policy-c") 224 So(issueData.Comments[3].Comment, ShouldStartWith, "Policy ID: policy-a") 225 }) 226 }) 227 Convey("With test name failure cluster", func() { 228 createRequest.Description.Title = "ninja://:blink_web_tests/media/my-suite/my-test.html" 229 createRequest.Description.Description = "A test is failing " + createRequest.Description.Title 230 expectedIssue.Description = &issuetracker.IssueComment{ 231 CommentNumber: 1, 232 Comment: "A test is failing ninja://:blink_web_tests/media/my-suite/my-test.html\n" + 233 "\n" + 234 "These test failures are causing problem(s) which require your attention, including:\n" + 235 "- Problem A\n" + 236 "\n" + 237 "See current problems, failure examples and more in LUCI Analysis at: https://luci-analysis-test.appspot.com/p/chromeos/rules/new-rule-id\n" + 238 "\n" + 239 "How to action this bug: https://luci-analysis-test.appspot.com/help#new-bug-filed\n" + 240 "Provide feedback: https://luci-analysis-test.appspot.com/help#feedback\n" + 241 "Was this bug filed in the wrong component? See: https://luci-analysis-test.appspot.com/help#component-selection", 242 } 243 expectedIssue.IssueState.Title = "Tests are failing: ninja://:blink_web_tests/media/my-suite/my-test.html" 244 245 response := bm.Create(ctx, createRequest) 246 So(response, ShouldResemble, bugs.BugCreateResponse{ 247 ID: "1", 248 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 249 "policy-a": {}, 250 }, 251 }) 252 So(len(fakeStore.Issues), ShouldEqual, 1) 253 issue := fakeStore.Issues[1] 254 255 So(issue.Issue, ShouldResembleProto, expectedIssue) 256 So(len(issue.Comments), ShouldEqual, 2) 257 So(issue.Comments[0].Comment, ShouldContainSubstring, "https://luci-analysis-test.appspot.com/p/chromeos/rules/new-rule-id") 258 So(issue.Comments[1].Comment, ShouldStartWith, "Policy ID: policy-a") 259 }) 260 261 Convey("Does nothing if in simulation mode", func() { 262 bm.Simulate = true 263 response := bm.Create(ctx, createRequest) 264 So(response, ShouldResemble, bugs.BugCreateResponse{ 265 Simulated: true, 266 ID: "123456", 267 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 268 "policy-a": {}, 269 }, 270 }) 271 So(len(fakeStore.Issues), ShouldEqual, 0) 272 }) 273 274 Convey("With provided component id", func() { 275 createRequest.BuganizerComponent = 7890 276 response := bm.Create(ctx, createRequest) 277 So(response, ShouldResemble, bugs.BugCreateResponse{ 278 ID: "1", 279 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 280 "policy-a": {}, 281 }, 282 }) 283 So(len(fakeStore.Issues), ShouldEqual, 1) 284 issue := fakeStore.Issues[1] 285 So(issue.Issue.IssueState.ComponentId, ShouldEqual, 7890) 286 }) 287 288 Convey("With provided component id without permission", func() { 289 createRequest.BuganizerComponent = ComponentWithNoAccess 290 // TODO: Mock permission call to fail. 291 response := bm.Create(ctx, createRequest) 292 So(response, ShouldResemble, bugs.BugCreateResponse{ 293 ID: "1", 294 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 295 "policy-a": {}, 296 }, 297 }) 298 So(len(fakeStore.Issues), ShouldEqual, 1) 299 issue := fakeStore.Issues[1] 300 // Should have fallback component ID because no permission to wanted component. 301 So(issue.Issue.IssueState.ComponentId, ShouldEqual, buganizerCfg.DefaultComponent.Id) 302 // No permission to component should appear in comments. 303 So(len(issue.Comments), ShouldEqual, 3) 304 So(issue.Comments[1].Comment, ShouldContainSubstring, strconv.Itoa(ComponentWithNoAccess)) 305 So(issue.Comments[2].Comment, ShouldStartWith, "Policy ID: policy-a") 306 }) 307 308 Convey("With Buganizer test mode", func() { 309 createRequest.BuganizerComponent = 1234 310 // TODO: Mock permission call to fail. 311 ctx = context.WithValue(ctx, &BuganizerTestModeKey, true) 312 response := bm.Create(ctx, createRequest) 313 So(response, ShouldResemble, bugs.BugCreateResponse{ 314 ID: "1", 315 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 316 "policy-a": {}, 317 }, 318 }) 319 So(len(fakeStore.Issues), ShouldEqual, 1) 320 issue := fakeStore.Issues[1] 321 // Should have fallback component ID because no permission to wanted component. 322 So(issue.Issue.IssueState.ComponentId, ShouldEqual, buganizerCfg.DefaultComponent.Id) 323 So(len(issue.Comments), ShouldEqual, 3) 324 So(issue.Comments[1].Comment, ShouldContainSubstring, "This bug was filed in the fallback component") 325 So(issue.Comments[2].Comment, ShouldStartWith, "Policy ID: policy-a") 326 }) 327 328 Convey("With Limit View Trusted", func() { 329 // Check config is respected and we file with Limit View Trusted if the 330 // config option to file without it is not set. 331 buganizerCfg.FileWithoutLimitViewTrusted = false 332 bm, err := NewBugManager(fakeClient, "https://luci-analysis-test.appspot.com", "chromeos", "email@test.com", projectCfg, false) 333 So(err, ShouldBeNil) 334 335 response := bm.Create(ctx, createRequest) 336 So(response, ShouldResemble, bugs.BugCreateResponse{ 337 ID: "1", 338 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 339 "policy-a": {}, 340 }, 341 }) 342 So(len(fakeStore.Issues), ShouldEqual, 1) 343 issue := fakeStore.Issues[1] 344 So(issue.Issue.IssueState.AccessLimit.AccessLevel, ShouldEqual, issuetracker.IssueAccessLimit_LIMIT_VIEW_TRUSTED) 345 }) 346 }) 347 Convey("Update", func() { 348 c := newCreateRequest() 349 c.ActivePolicyIDs = map[bugs.PolicyID]struct{}{ 350 "policy-a": {}, // P4 351 "policy-c": {}, // P1 352 } 353 response := bm.Create(ctx, c) 354 So(response, ShouldResemble, bugs.BugCreateResponse{ 355 ID: "1", 356 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 357 "policy-a": {}, 358 "policy-c": {}, 359 }, 360 }) 361 So(len(fakeStore.Issues), ShouldEqual, 1) 362 So(fakeStore.Issues[1].Issue.IssueState.Priority, ShouldEqual, issuetracker.Issue_P1) 363 So(fakeStore.Issues[1].Comments, ShouldHaveLength, 3) 364 365 originalCommentCount := len(fakeStore.Issues[1].Comments) 366 367 activationTime := time.Date(2025, 1, 1, 1, 0, 0, 0, time.UTC) 368 state := &bugspb.BugManagementState{ 369 RuleAssociationNotified: true, 370 PolicyState: map[string]*bugspb.BugManagementState_PolicyState{ 371 "policy-a": { // P4 372 IsActive: true, 373 LastActivationTime: timestamppb.New(activationTime), 374 ActivationNotified: true, 375 }, 376 "policy-b": { // P0 377 IsActive: false, 378 LastActivationTime: timestamppb.New(activationTime.Add(-time.Hour)), 379 LastDeactivationTime: timestamppb.New(activationTime), 380 ActivationNotified: false, 381 }, 382 "policy-c": { // P1 383 IsActive: true, 384 LastActivationTime: timestamppb.New(activationTime), 385 ActivationNotified: true, 386 }, 387 }, 388 } 389 390 bugsToUpdate := []bugs.BugUpdateRequest{ 391 { 392 Bug: bugs.BugID{System: bugs.BuganizerSystem, ID: response.ID}, 393 BugManagementState: state, 394 IsManagingBug: true, 395 RuleID: "rule-id", 396 IsManagingBugPriority: true, 397 IsManagingBugPriorityLastUpdated: clock.Now(ctx), 398 }, 399 } 400 expectedResponse := []bugs.BugUpdateResponse{ 401 { 402 IsDuplicate: false, 403 ShouldArchive: false, 404 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{}, 405 }, 406 } 407 verifyUpdateDoesNothing := func() error { 408 originalIssue := proto.Clone(fakeStore.Issues[1].Issue).(*issuetracker.Issue) 409 response, err := bm.Update(ctx, bugsToUpdate) 410 if err != nil { 411 return errors.Annotate(err, "update bugs").Err() 412 } 413 if diff := ShouldResemble(response, expectedResponse); diff != "" { 414 return errors.Reason("response: %s", diff).Err() 415 } 416 if diff := ShouldResembleProto(fakeStore.Issues[1].Issue, originalIssue); diff != "" { 417 return errors.Reason("issue 1: %s", diff).Err() 418 } 419 return nil 420 } 421 422 Convey("If less than expected issues are returned, should not fail", func() { 423 fakeStore.Issues = map[int64]*IssueData{} 424 425 bugsToUpdate := []bugs.BugUpdateRequest{ 426 { 427 Bug: bugs.BugID{System: bugs.BuganizerSystem, ID: response.ID}, 428 RuleID: "rule-id", 429 IsManagingBug: true, 430 IsManagingBugPriority: true, 431 IsManagingBugPriorityLastUpdated: clock.Now(ctx), 432 }, 433 } 434 expectedResponse = []bugs.BugUpdateResponse{ 435 { 436 IsDuplicate: false, 437 ShouldArchive: false, 438 PolicyActivationsNotified: make(map[bugs.PolicyID]struct{}), 439 }, 440 } 441 response, err := bm.Update(ctx, bugsToUpdate) 442 So(err, ShouldBeNil) 443 So(response, ShouldResemble, expectedResponse) 444 }) 445 446 Convey("If active policies unchanged and no rule notification pending, does nothing", func() { 447 So(verifyUpdateDoesNothing(), ShouldBeNil) 448 }) 449 Convey("Notifies association between bug and rule", func() { 450 // Setup 451 // When RuleAssociationNotified is false. 452 bugsToUpdate[0].BugManagementState.RuleAssociationNotified = false 453 // Even if ManagingBug is also false. 454 bugsToUpdate[0].IsManagingBug = false 455 456 // Act 457 response, err := bm.Update(ctx, bugsToUpdate) 458 459 // Verify 460 So(err, ShouldBeNil) 461 462 // RuleAssociationNotified is set. 463 expectedResponse[0].RuleAssociationNotified = true 464 So(response, ShouldResemble, expectedResponse) 465 466 // Expect a comment on the bug notifying us about the association. 467 So(fakeStore.Issues[1].Comments, ShouldHaveLength, originalCommentCount+1) 468 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldEqual, 469 "This bug has been associated with failures in LUCI Analysis. "+ 470 "To view failure examples or update the association, go to LUCI Analysis at: https://luci-analysis-test.appspot.com/p/chromeos/rules/rule-id") 471 }) 472 Convey("If active policies changed", func() { 473 // De-activates policy-c (P1), leaving only policy-a (P4) active. 474 bugsToUpdate[0].BugManagementState.PolicyState["policy-c"].IsActive = false 475 bugsToUpdate[0].BugManagementState.PolicyState["policy-c"].LastDeactivationTime = timestamppb.New(activationTime.Add(time.Hour)) 476 477 Convey("Does not update bug priority/verified if IsManagingBug false", func() { 478 bugsToUpdate[0].IsManagingBug = false 479 480 So(verifyUpdateDoesNothing(), ShouldBeNil) 481 }) 482 Convey("Notifies policy activation, even if IsManagingBug false", func() { 483 bugsToUpdate[0].IsManagingBug = false 484 485 // Activate policy B (P0). 486 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].IsActive = true 487 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].LastActivationTime = timestamppb.New(activationTime.Add(time.Hour)) 488 489 expectedResponse[0].PolicyActivationsNotified = map[bugs.PolicyID]struct{}{ 490 "policy-b": {}, 491 } 492 493 // Act 494 response, err := bm.Update(ctx, bugsToUpdate) 495 496 // Verify 497 So(err, ShouldBeNil) 498 So(response, ShouldResemble, expectedResponse) 499 500 // Bug priority should NOT be increased to P0, because IsManagingBug is false. 501 So(fakeStore.Issues[1].Issue.IssueState.Priority, ShouldNotEqual, issuetracker.Issue_P0) 502 503 // Expect the policy B activation comment to appear. 504 So(fakeStore.Issues[1].Comments, ShouldHaveLength, originalCommentCount+1) 505 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 506 "Policy ID: policy-b") 507 508 // Expect policy B's hotlist to be added to the bug. 509 So(fakeStore.Issues[1].Issue.IssueState.HotlistIds, ShouldResemble, []int64{1001, 1002, 1003}) 510 511 // Verify repeated update has no effect. 512 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].ActivationNotified = true 513 expectedResponse[0].PolicyActivationsNotified = map[bugs.PolicyID]struct{}{} 514 So(verifyUpdateDoesNothing(), ShouldBeNil) 515 }) 516 Convey("Reduces priority in response to policies de-activating", func() { 517 // Act 518 response, err := bm.Update(ctx, bugsToUpdate) 519 520 // Verify 521 So(err, ShouldBeNil) 522 So(response, ShouldResemble, expectedResponse) 523 So(fakeStore.Issues[1].Issue.IssueState.Priority, ShouldEqual, issuetracker.Issue_P4) 524 So(fakeStore.Issues[1].Comments, ShouldHaveLength, originalCommentCount+1) 525 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 526 "Because the following problem(s) have stopped:\n"+ 527 "- Problem C (P1)\n"+ 528 "The bug priority has been decreased from P1 to P4.") 529 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 530 "https://luci-analysis-test.appspot.com/help#priority-update") 531 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 532 "https://luci-analysis-test.appspot.com/p/chromeos/rules/rule-id") 533 534 // Verify repeated update has no effect. 535 So(verifyUpdateDoesNothing(), ShouldBeNil) 536 }) 537 Convey("Increases priority in response to priority policies activating", func() { 538 // Activate policy B (P0). 539 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].IsActive = true 540 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].LastActivationTime = timestamppb.New(activationTime.Add(time.Hour)) 541 542 expectedResponse[0].PolicyActivationsNotified = map[bugs.PolicyID]struct{}{ 543 "policy-b": {}, 544 } 545 546 // Act 547 response, err := bm.Update(ctx, bugsToUpdate) 548 549 // Verify 550 So(err, ShouldBeNil) 551 So(response, ShouldResemble, expectedResponse) 552 So(fakeStore.Issues[1].Issue.IssueState.Priority, ShouldEqual, issuetracker.Issue_P0) 553 So(fakeStore.Issues[1].Comments, ShouldHaveLength, originalCommentCount+2) 554 // Expect the policy B activation comment to appear, followed by the priority update comment. 555 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 556 "Policy ID: policy-b") 557 So(fakeStore.Issues[1].Comments[originalCommentCount+1].Comment, ShouldContainSubstring, 558 "Because the following problem(s) have started:\n"+ 559 "- Problem B (P0)\n"+ 560 "The bug priority has been increased from P1 to P0.") 561 So(fakeStore.Issues[1].Comments[originalCommentCount+1].Comment, ShouldContainSubstring, 562 "https://luci-analysis-test.appspot.com/help#priority-update") 563 So(fakeStore.Issues[1].Comments[originalCommentCount+1].Comment, ShouldContainSubstring, 564 "https://luci-analysis-test.appspot.com/p/chromeos/rules/rule-id") 565 566 // Verify repeated update has no effect. 567 So(verifyUpdateDoesNothing(), ShouldBeNil) 568 }) 569 Convey("Does not adjust priority if priority manually set", func() { 570 ctx := context.WithValue(ctx, &BuganizerSelfEmailKey, "luci-analysis@prod.google.com") 571 fakeStore.Issues[1].Issue.IssueState.Priority = issuetracker.Issue_P0 572 fakeStore.Issues[1].IssueUpdates = append(fakeStore.Issues[1].IssueUpdates, &issuetracker.IssueUpdate{ 573 Author: &issuetracker.User{ 574 EmailAddress: "testuser@google.com", 575 }, 576 Timestamp: timestamppb.New(clock.Now(ctx).Add(time.Minute * 4)), 577 FieldUpdates: []*issuetracker.FieldUpdate{ 578 { 579 Field: "priority", 580 }, 581 }, 582 }) 583 response, err := bm.Update(ctx, bugsToUpdate) 584 So(err, ShouldBeNil) 585 So(fakeStore.Issues[1].Issue.IssueState.Priority, ShouldEqual, issuetracker.Issue_P0) 586 expectedResponse[0].DisableRulePriorityUpdates = true 587 So(response[0].DisableRulePriorityUpdates, ShouldBeTrue) 588 So(fakeStore.Issues[1].Comments, ShouldHaveLength, originalCommentCount+1) 589 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldEqual, 590 "The bug priority has been manually set. To re-enable automatic priority updates by LUCI Analysis,"+ 591 " enable the update priority flag on the rule.\n\nSee failure impact and configure the failure"+ 592 " association rule for this bug at: https://luci-analysis-test.appspot.com/p/chromeos/rules/rule-id") 593 594 // Normally, the caller would update IsManagingBugPriority to false 595 // now, but as this is a test, we have to do it manually. 596 // As priority updates are now off, DisableRulePriorityUpdates 597 // should henceforth also return false (as they are already 598 // disabled). 599 expectedResponse[0].DisableRulePriorityUpdates = false 600 bugsToUpdate[0].IsManagingBugPriority = false 601 bugsToUpdate[0].IsManagingBugPriorityLastUpdated = tc.Now().Add(1 * time.Minute) 602 603 // Check repeated update does nothing more. 604 initialComments := len(fakeStore.Issues[1].Comments) 605 So(verifyUpdateDoesNothing(), ShouldBeNil) 606 So(len(fakeStore.Issues[1].Comments), ShouldEqual, initialComments) 607 608 Convey("Unless IsManagingBugPriority manually updated", func() { 609 bugsToUpdate[0].IsManagingBugPriority = true 610 bugsToUpdate[0].IsManagingBugPriorityLastUpdated = clock.Now(ctx).Add(time.Minute * 15) 611 612 response, err := bm.Update(ctx, bugsToUpdate) 613 So(response, ShouldResemble, expectedResponse) 614 So(err, ShouldBeNil) 615 So(fakeStore.Issues[1].Issue.IssueState.Priority, ShouldEqual, issuetracker.Issue_P4) 616 So(fakeStore.Issues[1].Comments, ShouldHaveLength, initialComments+1) 617 So(fakeStore.Issues[1].Comments[initialComments].Comment, ShouldContainSubstring, 618 "Because the following problem(s) are active:\n"+ 619 "- Problem A (P4)\n"+ 620 "\n"+ 621 "The bug priority has been set to P4.") 622 So(fakeStore.Issues[1].Comments[initialComments].Comment, ShouldContainSubstring, 623 "https://luci-analysis-test.appspot.com/help#priority-update") 624 So(fakeStore.Issues[1].Comments[initialComments].Comment, ShouldContainSubstring, 625 "https://luci-analysis-test.appspot.com/p/chromeos/rules/rule-id") 626 627 // Verify repeated update has no effect. 628 So(verifyUpdateDoesNothing(), ShouldBeNil) 629 }) 630 }) 631 Convey("Does nothing if in simulation mode", func() { 632 bm.Simulate = true 633 So(verifyUpdateDoesNothing(), ShouldBeNil) 634 }) 635 }) 636 Convey("If all policies deactivate", func() { 637 // De-activate all policies, so the bug would normally be marked verified. 638 for _, policyState := range bugsToUpdate[0].BugManagementState.PolicyState { 639 if policyState.IsActive { 640 policyState.IsActive = false 641 policyState.LastDeactivationTime = timestamppb.New(activationTime.Add(time.Hour)) 642 } 643 } 644 645 Convey("Does not update bug if IsManagingBug false", func() { 646 bugsToUpdate[0].IsManagingBug = false 647 648 So(verifyUpdateDoesNothing(), ShouldBeNil) 649 }) 650 Convey("Sets verifier and assignee to luci analysis if assignee is nil", func() { 651 fakeStore.Issues[1].Issue.IssueState.Assignee = nil 652 653 response, err := bm.Update(ctx, bugsToUpdate) 654 655 So(err, ShouldBeNil) 656 So(response, ShouldResemble, expectedResponse) 657 So(fakeStore.Issues[1].Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_VERIFIED) 658 So(fakeStore.Issues[1].Issue.IssueState.Verifier.EmailAddress, ShouldEqual, "email@test.com") 659 So(fakeStore.Issues[1].Issue.IssueState.Assignee.EmailAddress, ShouldEqual, "email@test.com") 660 661 Convey("If re-opening, LUCI Analysis assignee is removed", func() { 662 bugsToUpdate[0].BugManagementState.PolicyState["policy-a"].IsActive = true 663 bugsToUpdate[0].BugManagementState.PolicyState["policy-a"].LastActivationTime = timestamppb.New(activationTime.Add(2 * time.Hour)) 664 665 response, err := bm.Update(ctx, bugsToUpdate) 666 667 So(err, ShouldBeNil) 668 So(response, ShouldResemble, expectedResponse) 669 So(fakeStore.Issues[1].Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_NEW) 670 So(fakeStore.Issues[1].Issue.IssueState.Assignee, ShouldBeNil) 671 }) 672 }) 673 674 Convey("Update closes bug", func() { 675 fakeStore.Issues[1].Issue.IssueState.Assignee = &issuetracker.User{ 676 EmailAddress: "user@google.com", 677 } 678 679 response, err := bm.Update(ctx, bugsToUpdate) 680 So(err, ShouldBeNil) 681 So(response, ShouldResemble, expectedResponse) 682 So(fakeStore.Issues[1].Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_VERIFIED) 683 684 expectedComment := "Because the following problem(s) have stopped:\n" + 685 "- Problem C (P1)\n" + 686 "- Problem A (P4)\n" + 687 "The bug has been verified." 688 So(fakeStore.Issues[1].Comments, ShouldHaveLength, originalCommentCount+1) 689 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, expectedComment) 690 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 691 "https://luci-analysis-test.appspot.com/help#bug-verified") 692 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 693 "https://luci-analysis-test.appspot.com/p/chromeos/rules/rule-id") 694 695 // Verify repeated update has no effect. 696 So(verifyUpdateDoesNothing(), ShouldBeNil) 697 698 Convey("Rules for verified bugs archived after 30 days", func() { 699 tc.Add(time.Hour * 24 * 30) 700 701 expectedResponse := []bugs.BugUpdateResponse{ 702 { 703 ShouldArchive: true, 704 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{}, 705 }, 706 } 707 tc.Add(time.Minute * 2) 708 response, err := bm.Update(ctx, bugsToUpdate) 709 So(err, ShouldBeNil) 710 So(response, ShouldResemble, expectedResponse) 711 So(fakeStore.Issues[1].Issue.ModifiedTime, ShouldResembleProto, timestamppb.New(now)) 712 }) 713 714 Convey("If policies re-activate, bug is re-opened with correct priority", func() { 715 // policy-b has priority P0. 716 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].IsActive = true 717 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].LastActivationTime = timestamppb.New(activationTime.Add(2 * time.Hour)) 718 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].ActivationNotified = true 719 720 originalCommentCount := len(fakeStore.Issues[1].Comments) 721 722 Convey("Issue has owner", func() { 723 fakeStore.Issues[1].Issue.IssueState.Assignee = &issuetracker.User{ 724 EmailAddress: "testuser@google.com", 725 } 726 727 // Issue should return to "Assigned" status. 728 response, err := bm.Update(ctx, bugsToUpdate) 729 So(err, ShouldBeNil) 730 So(response, ShouldResemble, expectedResponse) 731 So(fakeStore.Issues[1].Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_ASSIGNED) 732 So(fakeStore.Issues[1].Issue.IssueState.Priority, ShouldEqual, issuetracker.Issue_P0) 733 734 expectedComment := "Because the following problem(s) have started:\n" + 735 "- Problem B (P0)\n" + 736 "The bug has been re-opened as P0." 737 So(fakeStore.Issues[1].Comments, ShouldHaveLength, originalCommentCount+1) 738 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, expectedComment) 739 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 740 "https://luci-analysis-test.appspot.com/help#bug-reopened") 741 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 742 "https://luci-analysis-test.appspot.com/p/chromeos/rules/rule-id") 743 744 // Verify repeated update has no effect. 745 So(verifyUpdateDoesNothing(), ShouldBeNil) 746 }) 747 Convey("Issue has no assignee", func() { 748 fakeStore.Issues[1].Issue.IssueState.Assignee = nil 749 750 // Issue should return to "Untriaged" status. 751 response, err := bm.Update(ctx, bugsToUpdate) 752 So(err, ShouldBeNil) 753 So(response, ShouldResemble, expectedResponse) 754 So(fakeStore.Issues[1].Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_NEW) 755 So(fakeStore.Issues[1].Issue.IssueState.Priority, ShouldEqual, issuetracker.Issue_P0) 756 757 expectedComment := "Because the following problem(s) have started:\n" + 758 "- Problem B (P0)\n" + 759 "The bug has been re-opened as P0." 760 So(fakeStore.Issues[1].Comments, ShouldHaveLength, originalCommentCount+1) 761 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, expectedComment) 762 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 763 "https://luci-analysis-test.appspot.com/help#priority-update") 764 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 765 "https://luci-analysis-test.appspot.com/help#bug-reopened") 766 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 767 "https://luci-analysis-test.appspot.com/p/chromeos/rules/rule-id") 768 769 // Verify repeated update has no effect. 770 So(verifyUpdateDoesNothing(), ShouldBeNil) 771 }) 772 }) 773 }) 774 }) 775 Convey("If bug duplicate", func() { 776 fakeStore.Issues[1].Issue.IssueState.Status = issuetracker.Issue_DUPLICATE 777 expectedResponse := []bugs.BugUpdateResponse{ 778 { 779 IsDuplicate: true, 780 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{}, 781 }, 782 } 783 784 // Act 785 response, err := bm.Update(ctx, bugsToUpdate) 786 787 // Verify 788 So(err, ShouldBeNil) 789 So(response, ShouldResemble, expectedResponse) 790 So(fakeStore.Issues[1].Issue.ModifiedTime, ShouldResembleProto, timestamppb.New(clock.Now(ctx))) 791 }) 792 Convey("Rule not managing a bug archived after 30 days of the bug being in any closed state", func() { 793 bugsToUpdate[0].IsManagingBug = false 794 fakeStore.Issues[1].Issue.IssueState.Status = issuetracker.Issue_FIXED 795 fakeStore.Issues[1].Issue.ResolvedTime = timestamppb.New(tc.Now()) 796 797 tc.Add(time.Hour * 24 * 30) 798 799 expectedResponse := []bugs.BugUpdateResponse{ 800 { 801 ShouldArchive: true, 802 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{}, 803 }, 804 } 805 originalTime := timestamppb.New(fakeStore.Issues[1].Issue.ModifiedTime.AsTime()) 806 807 // Act 808 response, err := bm.Update(ctx, bugsToUpdate) 809 810 // Verify 811 So(err, ShouldBeNil) 812 So(response, ShouldResemble, expectedResponse) 813 So(fakeStore.Issues[1].Issue.ModifiedTime, ShouldResembleProto, originalTime) 814 }) 815 Convey("Rule managing a bug not archived after 30 days of the bug being in fixed state", func() { 816 // If LUCI Analysis is mangaging the bug state, the fixed state 817 // means the bug is still not verified. Do not archive the 818 // rule. 819 bugsToUpdate[0].IsManagingBug = true 820 fakeStore.Issues[1].Issue.IssueState.Status = issuetracker.Issue_FIXED 821 fakeStore.Issues[1].Issue.ResolvedTime = timestamppb.New(tc.Now()) 822 823 tc.Add(time.Hour * 24 * 30) 824 825 So(verifyUpdateDoesNothing(), ShouldBeNil) 826 }) 827 828 Convey("Rules archived immediately if bug archived", func() { 829 fakeStore.Issues[1].Issue.IsArchived = true 830 831 expectedResponse := []bugs.BugUpdateResponse{ 832 { 833 ShouldArchive: true, 834 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{}, 835 }, 836 } 837 838 // Act 839 response, err := bm.Update(ctx, bugsToUpdate) 840 841 // Verify 842 So(err, ShouldBeNil) 843 So(response, ShouldResemble, expectedResponse) 844 }) 845 Convey("If issue does not exist, does nothing", func() { 846 fakeStore.Issues = nil 847 response, err := bm.Update(ctx, bugsToUpdate) 848 So(err, ShouldBeNil) 849 So(len(response), ShouldEqual, len(bugsToUpdate)) 850 So(fakeStore.Issues, ShouldBeNil) 851 }) 852 }) 853 Convey("GetMergedInto", func() { 854 c := newCreateRequest() 855 c.ActivePolicyIDs = map[bugs.PolicyID]struct{}{"policy-a": {}} 856 response := bm.Create(ctx, c) 857 So(response, ShouldResemble, bugs.BugCreateResponse{ 858 ID: "1", 859 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 860 "policy-a": {}, 861 }, 862 }) 863 So(len(fakeStore.Issues), ShouldEqual, 1) 864 865 bugID := bugs.BugID{System: bugs.BuganizerSystem, ID: "1"} 866 Convey("Merged into Buganizer bug", func() { 867 fakeStore.Issues[1].Issue.IssueState.Status = issuetracker.Issue_DUPLICATE 868 fakeStore.Issues[1].Issue.IssueState.CanonicalIssueId = 2 869 870 result, err := bm.GetMergedInto(ctx, bugID) 871 So(err, ShouldEqual, nil) 872 So(result, ShouldResemble, &bugs.BugID{ 873 System: bugs.BuganizerSystem, 874 ID: "2", 875 }) 876 }) 877 Convey("Not merged into any bug", func() { 878 // While MergedIntoIssueRef is set, the bug status is not 879 // set to "Duplicate", so this value should be ignored. 880 fakeStore.Issues[1].Issue.IssueState.Status = issuetracker.Issue_NEW 881 fakeStore.Issues[1].Issue.IssueState.CanonicalIssueId = 2 882 883 result, err := bm.GetMergedInto(ctx, bugID) 884 So(err, ShouldEqual, nil) 885 So(result, ShouldBeNil) 886 }) 887 }) 888 Convey("UpdateDuplicateSource", func() { 889 c := newCreateRequest() 890 c.ActivePolicyIDs = map[bugs.PolicyID]struct{}{"policy-a": {}} 891 response := bm.Create(ctx, c) 892 So(response, ShouldResemble, bugs.BugCreateResponse{ 893 ID: "1", 894 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 895 "policy-a": {}, 896 }, 897 }) 898 So(fakeStore.Issues, ShouldHaveLength, 1) 899 So(fakeStore.Issues[1].Comments, ShouldHaveLength, 2) 900 originalCommentCount := len(fakeStore.Issues[1].Comments) 901 902 fakeStore.Issues[1].Issue.IssueState.Status = issuetracker.Issue_DUPLICATE 903 fakeStore.Issues[1].Issue.IssueState.CanonicalIssueId = 2 904 905 Convey("With ErrorMessage", func() { 906 request := bugs.UpdateDuplicateSourceRequest{ 907 BugDetails: bugs.DuplicateBugDetails{ 908 RuleID: "source-rule-id", 909 Bug: bugs.BugID{System: bugs.BuganizerSystem, ID: "1"}, 910 }, 911 ErrorMessage: "Some error.", 912 } 913 err := bm.UpdateDuplicateSource(ctx, request) 914 So(err, ShouldBeNil) 915 916 So(fakeStore.Issues[1].Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_NEW) 917 So(fakeStore.Issues[1].Comments, ShouldHaveLength, originalCommentCount+1) 918 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, "Some error.") 919 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 920 "https://luci-analysis-test.appspot.com/p/chromeos/rules/source-rule-id") 921 }) 922 Convey("With ErrorMessage and IsAssigned is true", func() { 923 request := bugs.UpdateDuplicateSourceRequest{ 924 BugDetails: bugs.DuplicateBugDetails{ 925 RuleID: "source-rule-id", 926 Bug: bugs.BugID{System: bugs.BuganizerSystem, ID: "1"}, 927 IsAssigned: true, 928 }, 929 ErrorMessage: "Some error.", 930 } 931 err := bm.UpdateDuplicateSource(ctx, request) 932 So(err, ShouldBeNil) 933 934 So(fakeStore.Issues[1].Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_ASSIGNED) 935 So(fakeStore.Issues[1].Comments, ShouldHaveLength, originalCommentCount+1) 936 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, "Some error.") 937 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 938 "https://luci-analysis-test.appspot.com/p/chromeos/rules/source-rule-id") 939 }) 940 Convey("Without ErrorMessage", func() { 941 request := bugs.UpdateDuplicateSourceRequest{ 942 BugDetails: bugs.DuplicateBugDetails{ 943 RuleID: "source-bug-rule-id", 944 Bug: bugs.BugID{System: bugs.BuganizerSystem, ID: "1"}, 945 }, 946 DestinationRuleID: "12345abcdef", 947 } 948 err := bm.UpdateDuplicateSource(ctx, request) 949 So(err, ShouldBeNil) 950 951 So(fakeStore.Issues[1].Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_DUPLICATE) 952 So(fakeStore.Issues[1].Comments, ShouldHaveLength, originalCommentCount+1) 953 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, "merged the failure association rule for this bug into the rule for the canonical bug.") 954 So(fakeStore.Issues[1].Comments[originalCommentCount].Comment, ShouldContainSubstring, 955 "https://luci-analysis-test.appspot.com/p/chromeos/rules/12345abcdef") 956 }) 957 }) 958 }) 959 } 960 961 func newCreateRequest() bugs.BugCreateRequest { 962 cluster := bugs.BugCreateRequest{ 963 Description: &clustering.ClusterDescription{ 964 Title: "ClusterID", 965 Description: "Tests are failing with reason: Some failure reason.", 966 }, 967 RuleID: "new-rule-id", 968 } 969 return cluster 970 }