go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/bugs/monorail/manager_test.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package monorail 16 17 import ( 18 "context" 19 "strings" 20 "testing" 21 "time" 22 23 "google.golang.org/genproto/protobuf/field_mask" 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/status" 26 "google.golang.org/protobuf/types/known/timestamppb" 27 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/common/errors" 30 31 "go.chromium.org/luci/analysis/internal/bugs" 32 mpb "go.chromium.org/luci/analysis/internal/bugs/monorail/api_proto" 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 NewCreateRequest() bugs.BugCreateRequest { 43 cluster := bugs.BugCreateRequest{ 44 Description: &clustering.ClusterDescription{ 45 Title: "ClusterID", 46 Description: "Tests are failing with reason: Some failure reason.", 47 }, 48 MonorailComponents: []string{ 49 "Blink>Layout", 50 "Blink>Network", 51 "Blink>Invalid", 52 }, 53 RuleID: "new-rule-id", 54 } 55 return cluster 56 } 57 58 func TestManager(t *testing.T) { 59 t.Parallel() 60 61 Convey("With Bug Manager", t, func() { 62 ctx := context.Background() 63 f := &FakeIssuesStore{ 64 NextID: 100, 65 PriorityFieldName: "projects/chromium/fieldDefs/11", 66 ComponentNames: []string{ 67 "projects/chromium/componentDefs/Blink", 68 "projects/chromium/componentDefs/Blink>Layout", 69 "projects/chromium/componentDefs/Blink>Network", 70 }, 71 } 72 user := AutomationUsers[0] 73 cl, err := NewClient(UseFakeIssuesClient(ctx, f, user), "myhost") 74 So(err, ShouldBeNil) 75 monorailCfgs := ChromiumTestConfig() 76 77 policyA := config.CreatePlaceholderBugManagementPolicy("policy-a") 78 policyA.HumanReadableName = "Problem A" 79 policyA.Priority = configpb.BuganizerPriority_P4 80 policyA.BugTemplate.Monorail.Labels = []string{"Policy-A-Label"} 81 82 policyB := config.CreatePlaceholderBugManagementPolicy("policy-b") 83 policyB.HumanReadableName = "Problem B" 84 policyB.Priority = configpb.BuganizerPriority_P0 85 policyB.BugTemplate.Monorail.Labels = []string{"Policy-B-Label"} 86 87 policyC := config.CreatePlaceholderBugManagementPolicy("policy-c") 88 policyC.HumanReadableName = "Problem C" 89 policyC.Priority = configpb.BuganizerPriority_P1 90 policyC.BugTemplate.Monorail.Labels = []string{"Policy-C-Label"} 91 92 projectCfg := &configpb.ProjectConfig{ 93 BugManagement: &configpb.BugManagement{ 94 DefaultBugSystem: configpb.BugSystem_MONORAIL, 95 Monorail: monorailCfgs, 96 Policies: []*configpb.BugManagementPolicy{ 97 policyA, 98 policyB, 99 policyC, 100 }, 101 }, 102 } 103 104 bm, err := NewBugManager(cl, "https://luci-analysis-test.appspot.com", "luciproject", projectCfg) 105 So(err, ShouldBeNil) 106 now := time.Date(2040, time.January, 1, 2, 3, 4, 5, time.UTC) 107 ctx, tc := testclock.UseTime(ctx, now) 108 109 Convey("Create", func() { 110 createRequest := NewCreateRequest() 111 createRequest.ActivePolicyIDs = map[bugs.PolicyID]struct{}{ 112 "policy-a": {}, // P4 113 } 114 115 expectedIssue := &mpb.Issue{ 116 Name: "projects/chromium/issues/100", 117 Summary: "Tests are failing: ClusterID", 118 Reporter: AutomationUsers[0], 119 State: mpb.IssueContentState_ACTIVE, 120 Status: &mpb.Issue_StatusValue{Status: "Untriaged"}, 121 StatusModifyTime: timestamppb.New(now), 122 FieldValues: []*mpb.FieldValue{ 123 { 124 // Type field. 125 Field: "projects/chromium/fieldDefs/10", 126 Value: "Bug", 127 }, 128 { 129 // Priority field. 130 Field: "projects/chromium/fieldDefs/11", 131 // Monorail projects do not support priority level P4, 132 // so we use P3 for policies that require P4. 133 Value: "3", 134 }, 135 }, 136 Components: []*mpb.Issue_ComponentValue{ 137 {Component: "projects/chromium/componentDefs/Blink>Layout"}, 138 {Component: "projects/chromium/componentDefs/Blink>Network"}, 139 }, 140 Labels: []*mpb.Issue_LabelValue{{ 141 Label: "LUCI-Analysis-Auto-Filed", 142 }, { 143 Label: "Policy-A-Label", 144 }, { 145 Label: "Restrict-View-Google", 146 }}, 147 } 148 149 Convey("With reason-based failure cluster", func() { 150 reason := `Expected equality of these values: 151 "Expected_Value" 152 my_expr.evaluate(123) 153 Which is: "Unexpected_Value"` 154 createRequest.Description.Title = reason 155 createRequest.Description.Description = "A cluster of failures has been found with reason: " + reason 156 157 expectedIssue.Summary = "Tests are failing: Expected equality of these values: \"Expected_Value\" my_expr.evaluate(123) Which is: \"Unexpected_Value\"" 158 expectedComment := "A cluster of failures has been found with reason: Expected equality " + 159 "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" + 160 "Which is: \"Unexpected_Value\"\n" + 161 "\n" + 162 "These test failures are causing problem(s) which require your attention, including:\n" + 163 "- Problem A\n" + 164 "\n" + 165 "See current problems, failure examples and more in LUCI Analysis at: https://luci-analysis-test.appspot.com/p/luciproject/rules/new-rule-id\n" + 166 "\n" + 167 "How to action this bug: https://luci-analysis-test.appspot.com/help#new-bug-filed\n" + 168 "Provide feedback: https://luci-analysis-test.appspot.com/help#feedback\n" + 169 "Was this bug filed in the wrong component? See: https://luci-analysis-test.appspot.com/help#component-selection" 170 171 Convey("Base case", func() { 172 // Act 173 response := bm.Create(ctx, createRequest) 174 175 // Verify 176 So(response, ShouldResemble, bugs.BugCreateResponse{ 177 ID: "chromium/100", 178 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 179 "policy-a": {}, 180 }, 181 }) 182 So(len(f.Issues), ShouldEqual, 1) 183 184 issue := f.Issues[0] 185 So(issue.Issue, ShouldResembleProto, expectedIssue) 186 So(len(issue.Comments), ShouldEqual, 2) 187 So(issue.Comments[0].Content, ShouldEqual, expectedComment) 188 }) 189 Convey("Policy with comment template", func() { 190 policyA.BugTemplate.CommentTemplate = "RuleURL:{{.RuleURL}},BugID:{{if .BugID.IsMonorail}}{{.BugID.MonorailProject}}/{{.BugID.MonorailBugID}}{{end}}" 191 192 bm, err := NewBugManager(cl, "https://luci-analysis-test.appspot.com", "luciproject", projectCfg) 193 So(err, ShouldBeNil) 194 195 // Act 196 response := bm.Create(ctx, createRequest) 197 198 // Verify 199 So(response, ShouldResemble, bugs.BugCreateResponse{ 200 ID: "chromium/100", 201 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 202 "policy-a": {}, 203 }, 204 }) 205 So(len(f.Issues), ShouldEqual, 1) 206 207 issue := f.Issues[0] 208 So(issue.Issue, ShouldResembleProto, expectedIssue) 209 So(len(issue.Comments), ShouldEqual, 2) 210 So(issue.Comments[0].Content, ShouldEqual, expectedComment) 211 So(issue.Comments[1].Content, ShouldEqual, "RuleURL:https://luci-analysis-test.appspot.com/p/luciproject/rules/new-rule-id,BugID:chromium/100\n\n"+ 212 "Why LUCI Analysis posted this comment: https://luci-analysis-test.appspot.com/help#policy-activated (Policy ID: policy-a)") 213 So(issue.NotifyCount, ShouldEqual, 2) 214 }) 215 Convey("Policy has no comment template", func() { 216 policyA.BugTemplate.CommentTemplate = "" 217 218 bm, err := NewBugManager(cl, "https://luci-analysis-test.appspot.com", "luciproject", projectCfg) 219 So(err, ShouldBeNil) 220 221 // Act 222 response := bm.Create(ctx, createRequest) 223 224 // Verify 225 So(response, ShouldResemble, bugs.BugCreateResponse{ 226 ID: "chromium/100", 227 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 228 "policy-a": {}, 229 }, 230 }) 231 So(len(f.Issues), ShouldEqual, 1) 232 233 issue := f.Issues[0] 234 So(issue.Issue, ShouldResembleProto, expectedIssue) 235 So(len(issue.Comments), ShouldEqual, 2) 236 So(issue.Comments[0].Content, ShouldEqual, expectedComment) 237 So(issue.Comments[1].Content, ShouldBeEmpty) 238 So(issue.NotifyCount, ShouldEqual, 1) 239 }) 240 Convey("Policy has no comment template or labels", func() { 241 policyA.BugTemplate.CommentTemplate = "" 242 policyA.BugTemplate.Monorail = nil 243 244 bm, err := NewBugManager(cl, "https://luci-analysis-test.appspot.com", "luciproject", projectCfg) 245 So(err, ShouldBeNil) 246 247 expectedIssue.Labels = removeLabel(expectedIssue.Labels, "Policy-A-Label") 248 249 // Act 250 response := bm.Create(ctx, createRequest) 251 252 // Verify 253 So(response, ShouldResemble, bugs.BugCreateResponse{ 254 ID: "chromium/100", 255 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 256 "policy-a": {}, 257 }, 258 }) 259 So(len(f.Issues), ShouldEqual, 1) 260 261 issue := f.Issues[0] 262 So(issue.Issue, ShouldResembleProto, expectedIssue) 263 So(len(issue.Comments), ShouldEqual, 1) 264 So(issue.Comments[0].Content, ShouldEqual, expectedComment) 265 So(issue.NotifyCount, ShouldEqual, 1) 266 }) 267 Convey("Policy has no monorail template", func() { 268 policyA.BugTemplate.Monorail = nil 269 270 bm, err := NewBugManager(cl, "https://luci-analysis-test.appspot.com", "luciproject", projectCfg) 271 So(err, ShouldBeNil) 272 273 expectedIssue.Labels = removeLabel(expectedIssue.Labels, "Policy-A-Label") 274 275 // Act 276 response := bm.Create(ctx, createRequest) 277 278 // Verify 279 So(response, ShouldResemble, bugs.BugCreateResponse{ 280 ID: "chromium/100", 281 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 282 "policy-a": {}, 283 }, 284 }) 285 So(len(f.Issues), ShouldEqual, 1) 286 287 issue := f.Issues[0] 288 So(issue.Issue, ShouldResembleProto, expectedIssue) 289 }) 290 Convey("Multiple policies activated", func() { 291 createRequest.ActivePolicyIDs = map[bugs.PolicyID]struct{}{ 292 "policy-a": {}, // P4 293 "policy-b": {}, // P0 294 "policy-c": {}, // P1 295 } 296 expectedIssue.FieldValues[1].Value = "0" 297 expectedIssue.Labels = addLabel(expectedIssue.Labels, "Policy-B-Label") 298 expectedIssue.Labels = addLabel(expectedIssue.Labels, "Policy-C-Label") 299 expectedComment = strings.Replace(expectedComment, "- Problem A\n", "- Problem B\n- Problem C\n- Problem A\n", 1) 300 301 // Act 302 response := bm.Create(ctx, createRequest) 303 304 // Verify 305 So(response, ShouldResemble, bugs.BugCreateResponse{ 306 ID: "chromium/100", 307 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 308 "policy-a": {}, 309 "policy-b": {}, 310 "policy-c": {}, 311 }, 312 }) 313 So(len(f.Issues), ShouldEqual, 1) 314 315 issue := f.Issues[0] 316 So(issue.Issue, ShouldResembleProto, expectedIssue) 317 So(len(issue.Comments), ShouldEqual, 4) 318 So(issue.Comments[0].Content, ShouldEqual, expectedComment) 319 // Policy activation comments should appear in descending priority order. 320 So(issue.Comments[1].Content, ShouldStartWith, "Policy ID: policy-b") 321 So(issue.Comments[2].Content, ShouldStartWith, "Policy ID: policy-c") 322 So(issue.Comments[3].Content, ShouldStartWith, "Policy ID: policy-a") 323 So(issue.NotifyCount, ShouldEqual, 4) 324 }) 325 Convey("Failed to post issue comment", func() { 326 f.UpdateError = status.Errorf(codes.Internal, "internal server error") 327 328 // Act 329 response := bm.Create(ctx, createRequest) 330 331 // Verify 332 // Both ID and Error set, reflecting partial success. 333 So(response.ID, ShouldEqual, "chromium/100") 334 So(response.Error, ShouldNotBeNil) 335 So(errors.Is(response.Error, f.UpdateError), ShouldBeTrue) 336 So(response.Simulated, ShouldBeFalse) 337 So(len(f.Issues), ShouldEqual, 1) 338 339 // Do not expect label to be populated as policy activation comment 340 // did not get a chance to post. 341 expectedIssue.Labels = removeLabel(expectedIssue.Labels, "Policy-A-Label") 342 343 issue := f.Issues[0] 344 So(issue.Issue, ShouldResembleProto, expectedIssue) 345 So(len(issue.Comments), ShouldEqual, 1) 346 So(issue.Comments[0].Content, ShouldEqual, expectedComment) 347 So(issue.NotifyCount, ShouldEqual, 1) 348 }) 349 }) 350 Convey("With test name failure cluster", func() { 351 createRequest.Description.Title = "ninja://:blink_web_tests/media/my-suite/my-test.html" 352 createRequest.Description.Description = "A test is failing " + createRequest.Description.Title 353 354 expectedIssue.Summary = "Tests are failing: ninja://:blink_web_tests/media/my-suite/my-test.html" 355 expectedComment := "A test is failing ninja://:blink_web_tests/media/my-suite/my-test.html\n" + 356 "\n" + 357 "These test failures are causing problem(s) which require your attention, including:\n" + 358 "- Problem A\n" + 359 "\n" + 360 "See current problems, failure examples and more in LUCI Analysis at: https://luci-analysis-test.appspot.com/p/luciproject/rules/new-rule-id\n" + 361 "\n" + 362 "How to action this bug: https://luci-analysis-test.appspot.com/help#new-bug-filed\n" + 363 "Provide feedback: https://luci-analysis-test.appspot.com/help#feedback\n" + 364 "Was this bug filed in the wrong component? See: https://luci-analysis-test.appspot.com/help#component-selection" 365 366 // Act 367 response := bm.Create(ctx, createRequest) 368 369 // Verify 370 So(response, ShouldResemble, bugs.BugCreateResponse{ 371 ID: "chromium/100", 372 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 373 "policy-a": {}, 374 }, 375 }) 376 So(len(f.Issues), ShouldEqual, 1) 377 378 issue := f.Issues[0] 379 So(issue.Issue, ShouldResembleProto, expectedIssue) 380 So(len(issue.Comments), ShouldEqual, 2) 381 So(issue.Comments[0].Content, ShouldEqual, expectedComment) 382 So(issue.Comments[1].Content, ShouldStartWith, "Policy ID: policy-a") 383 So(issue.NotifyCount, ShouldEqual, 2) 384 }) 385 Convey("Without Restrict-View-Google", func() { 386 monorailCfgs.FileWithoutRestrictViewGoogle = true 387 388 response := bm.Create(ctx, createRequest) 389 So(response, ShouldResemble, bugs.BugCreateResponse{ 390 ID: "chromium/100", 391 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 392 "policy-a": {}, 393 }, 394 }) 395 So(len(f.Issues), ShouldEqual, 1) 396 issue := f.Issues[0] 397 398 expectedIssue.Labels = removeLabel(expectedIssue.Labels, "Restrict-View-Google") 399 So(issue.Issue, ShouldResembleProto, expectedIssue) 400 }) 401 Convey("Does nothing if in simulation mode", func() { 402 bm.Simulate = true 403 404 response := bm.Create(ctx, createRequest) 405 So(response, ShouldResemble, bugs.BugCreateResponse{ 406 Simulated: true, 407 ID: "chromium/12345678", 408 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 409 "policy-a": {}, 410 }, 411 }) 412 So(len(f.Issues), ShouldEqual, 0) 413 }) 414 }) 415 Convey("Update", func() { 416 c := NewCreateRequest() 417 c.ActivePolicyIDs = map[bugs.PolicyID]struct{}{ 418 "policy-a": {}, // P4 419 "policy-c": {}, // P1 420 } 421 response := bm.Create(ctx, c) 422 So(response, ShouldResemble, bugs.BugCreateResponse{ 423 ID: "chromium/100", 424 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 425 "policy-a": {}, 426 "policy-c": {}, 427 }, 428 }) 429 So(len(f.Issues), ShouldEqual, 1) 430 So(ChromiumTestIssuePriority(f.Issues[0].Issue), ShouldEqual, "1") 431 So(f.Issues[0].Comments, ShouldHaveLength, 3) 432 433 originalCommentCount := len(f.Issues[0].Comments) 434 435 activationTime := time.Date(2025, 1, 1, 1, 0, 0, 0, time.UTC) 436 state := &bugspb.BugManagementState{ 437 RuleAssociationNotified: true, 438 PolicyState: map[string]*bugspb.BugManagementState_PolicyState{ 439 "policy-a": { // P4 440 IsActive: true, 441 LastActivationTime: timestamppb.New(activationTime), 442 ActivationNotified: true, 443 }, 444 "policy-b": { // P0 445 IsActive: false, 446 LastActivationTime: timestamppb.New(activationTime.Add(-time.Hour)), 447 LastDeactivationTime: timestamppb.New(activationTime), 448 ActivationNotified: false, 449 }, 450 "policy-c": { // P1 451 IsActive: true, 452 LastActivationTime: timestamppb.New(activationTime), 453 ActivationNotified: true, 454 }, 455 }, 456 } 457 458 bugsToUpdate := []bugs.BugUpdateRequest{ 459 { 460 Bug: bugs.BugID{System: bugs.MonorailSystem, ID: response.ID}, 461 BugManagementState: state, 462 IsManagingBug: true, 463 RuleID: "rule-id", 464 IsManagingBugPriority: true, 465 IsManagingBugPriorityLastUpdated: tc.Now(), 466 }, 467 } 468 expectedResponse := []bugs.BugUpdateResponse{ 469 { 470 IsDuplicate: false, 471 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{}, 472 }, 473 } 474 verifyUpdateDoesNothing := func() error { 475 originalIssues := CopyIssuesStore(f) 476 response, err := bm.Update(ctx, bugsToUpdate) 477 if err != nil { 478 return errors.Annotate(err, "update bugs").Err() 479 } 480 if diff := ShouldResemble(response, expectedResponse); diff != "" { 481 return errors.Reason("response: %s", diff).Err() 482 } 483 if diff := ShouldResembleProto(f, originalIssues); diff != "" { 484 return errors.Reason("issues store: %s", diff).Err() 485 } 486 return nil 487 } 488 // Create a monorail client that interacts with monorail 489 // as an end-user. This is needed as we distinguish user 490 // updates to the bug from system updates. 491 user := "users/100" 492 usercl, err := NewClient(UseFakeIssuesClient(ctx, f, user), "myhost") 493 So(err, ShouldBeNil) 494 495 Convey("If active policies unchanged and rule association is not pending, does nothing", func() { 496 So(verifyUpdateDoesNothing(), ShouldBeNil) 497 }) 498 Convey("Notifies association between bug and rule", func() { 499 // Setup 500 // When RuleAssociationNotified is false. 501 bugsToUpdate[0].BugManagementState.RuleAssociationNotified = false 502 // Even if ManagingBug is also false. 503 bugsToUpdate[0].IsManagingBug = false 504 505 // Act 506 response, err := bm.Update(ctx, bugsToUpdate) 507 508 // Verify 509 So(err, ShouldBeNil) 510 511 // RuleAssociationNotified is set. 512 expectedResponse[0].RuleAssociationNotified = true 513 So(response, ShouldResemble, expectedResponse) 514 515 // Expect a comment on the bug notifying us about the association. 516 So(f.Issues[0].Comments, ShouldHaveLength, originalCommentCount+1) 517 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldEqual, 518 "This bug has been associated with failures in LUCI Analysis. "+ 519 "To view failure examples or update the association, go to LUCI Analysis at: https://luci-analysis-test.appspot.com/p/luciproject/rules/rule-id") 520 }) 521 Convey("If active policies changed", func() { 522 // De-activates policy-c (P1), leaving only policy-a (P4) active. 523 bugsToUpdate[0].BugManagementState.PolicyState["policy-c"].IsActive = false 524 bugsToUpdate[0].BugManagementState.PolicyState["policy-c"].LastDeactivationTime = timestamppb.New(activationTime.Add(time.Hour)) 525 526 Convey("Does not update bug priority/verified if IsManagingBug false", func() { 527 bugsToUpdate[0].IsManagingBug = false 528 529 So(verifyUpdateDoesNothing(), ShouldBeNil) 530 }) 531 Convey("Notifies policy activation, even if IsManagingBug false", func() { 532 bugsToUpdate[0].IsManagingBug = false 533 534 // Activates policy B (P0) for the first time. 535 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].IsActive = true 536 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].LastActivationTime = timestamppb.New(activationTime.Add(time.Hour)) 537 538 expectedResponse[0].PolicyActivationsNotified = map[bugs.PolicyID]struct{}{ 539 "policy-b": {}, 540 } 541 542 originalNotifyCount := f.Issues[0].NotifyCount 543 544 // Act 545 response, err := bm.Update(ctx, bugsToUpdate) 546 547 // Verify 548 So(err, ShouldBeNil) 549 So(response, ShouldResemble, expectedResponse) 550 551 // Priority was NOT raised to P0, because IsManagingBug is false. 552 So(ChromiumTestIssuePriority(f.Issues[0].Issue), ShouldNotEqual, "0") 553 554 // Expect the policy B activation comment to appear. 555 So(f.Issues[0].Comments, ShouldHaveLength, originalCommentCount+1) 556 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldStartWith, 557 "Policy ID: policy-b") 558 559 // Expect the policy B label to appear. 560 So(containsLabel(f.Issues[0].Issue.Labels, "Policy-B-Label"), ShouldBeTrue) 561 562 // The policy activation was notified. 563 So(f.Issues[0].NotifyCount, ShouldEqual, originalNotifyCount+1) 564 565 // Verify repeated update has no effect. 566 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].ActivationNotified = true 567 expectedResponse[0].PolicyActivationsNotified = map[bugs.PolicyID]struct{}{} 568 So(verifyUpdateDoesNothing(), ShouldBeNil) 569 }) 570 Convey("Reduces priority in response to policies de-activating", func() { 571 originalNotifyCount := f.Issues[0].NotifyCount 572 573 // Act 574 response, err := bm.Update(ctx, bugsToUpdate) 575 576 // Verify 577 So(err, ShouldBeNil) 578 So(response, ShouldResemble, expectedResponse) 579 So(ChromiumTestIssuePriority(f.Issues[0].Issue), ShouldEqual, "3") 580 So(f.Issues[0].Comments, ShouldHaveLength, originalCommentCount+1) 581 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 582 "Because the following problem(s) have stopped:\n"+ 583 "- Problem C (P1)\n"+ 584 "The bug priority has been decreased from P1 to P3.") 585 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 586 "https://luci-analysis-test.appspot.com/help#priority-update") 587 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 588 "https://luci-analysis-test.appspot.com/p/luciproject/rules/rule-id") 589 590 // Does not notify. 591 So(f.Issues[0].NotifyCount, ShouldEqual, originalNotifyCount) 592 593 // Verify repeated update has no effect. 594 So(verifyUpdateDoesNothing(), ShouldBeNil) 595 }) 596 Convey("Increases priority in response to priority policies activating", func() { 597 // Activates policy B (P0). 598 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].IsActive = true 599 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].LastActivationTime = timestamppb.New(activationTime.Add(time.Hour)) 600 601 expectedResponse[0].PolicyActivationsNotified = map[bugs.PolicyID]struct{}{ 602 "policy-b": {}, 603 } 604 605 originalNotifyCount := f.Issues[0].NotifyCount 606 607 // Act 608 response, err := bm.Update(ctx, bugsToUpdate) 609 610 // Verify 611 So(err, ShouldBeNil) 612 So(response, ShouldResemble, expectedResponse) 613 So(ChromiumTestIssuePriority(f.Issues[0].Issue), ShouldEqual, "0") 614 So(f.Issues[0].Comments, ShouldHaveLength, originalCommentCount+2) 615 // Expect the policy B activation comment to appear, followed by the priority update comment. 616 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldStartWith, 617 "Policy ID: policy-b") 618 So(f.Issues[0].Comments[originalCommentCount+1].Content, ShouldContainSubstring, 619 "Because the following problem(s) have started:\n"+ 620 "- Problem B (P0)\n"+ 621 "The bug priority has been increased from P1 to P0.") 622 So(f.Issues[0].Comments[originalCommentCount+1].Content, ShouldContainSubstring, 623 "https://luci-analysis-test.appspot.com/help#priority-update") 624 So(f.Issues[0].Comments[originalCommentCount+1].Content, ShouldContainSubstring, 625 "https://luci-analysis-test.appspot.com/p/luciproject/rules/rule-id") 626 627 // Notified the policy activation, and the priority increase. 628 So(f.Issues[0].NotifyCount, ShouldEqual, originalNotifyCount+2) 629 630 // Verify repeated update has no effect. 631 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].ActivationNotified = true 632 expectedResponse[0].PolicyActivationsNotified = map[bugs.PolicyID]struct{}{} 633 So(verifyUpdateDoesNothing(), ShouldBeNil) 634 }) 635 Convey("Does not adjust priority if priority manually set", func() { 636 updateReq := updateBugPriorityRequest(f.Issues[0].Issue.Name, "0") 637 err = usercl.ModifyIssues(ctx, updateReq) 638 So(err, ShouldBeNil) 639 So(ChromiumTestIssuePriority(f.Issues[0].Issue), ShouldEqual, "0") 640 641 expectedIssue := CopyIssue(f.Issues[0].Issue) 642 originalNotifyCount := f.Issues[0].NotifyCount 643 originalCommentCount = len(f.Issues[0].Comments) 644 645 // Act 646 response, err := bm.Update(ctx, bugsToUpdate) 647 648 // Verify 649 So(err, ShouldBeNil) 650 expectedResponse[0].DisableRulePriorityUpdates = true 651 So(response, ShouldResemble, expectedResponse) 652 So(f.Issues[0].Issue, ShouldResembleProto, expectedIssue) 653 So(f.Issues[0].Comments, ShouldHaveLength, originalCommentCount+1) 654 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldEqual, 655 "The bug priority has been manually set. To re-enable automatic priority updates by LUCI Analysis,"+ 656 " enable the update priority flag on the rule.\n\nSee failure impact and configure the failure"+ 657 " association rule for this bug at: https://luci-analysis-test.appspot.com/p/luciproject/rules/rule-id") 658 // Does not notify. 659 So(f.Issues[0].NotifyCount, ShouldEqual, originalNotifyCount) 660 661 // Normally, the caller would update IsManagingBugPriority to false 662 // now, but as this is a test, we have to do it manually. 663 // As priority updates are now off, DisableRulePriorityUpdates 664 // should henceforth also return false (as they are already 665 // disabled). 666 bugsToUpdate[0].IsManagingBugPriority = false 667 bugsToUpdate[0].IsManagingBugPriorityLastUpdated = tc.Now().Add(1 * time.Minute) 668 expectedResponse[0].DisableRulePriorityUpdates = false 669 670 // Check repeated update does nothing more. 671 So(verifyUpdateDoesNothing(), ShouldBeNil) 672 673 Convey("Unless IsManagingBugPriority manually updated", func() { 674 bugsToUpdate[0].IsManagingBugPriority = true 675 bugsToUpdate[0].IsManagingBugPriorityLastUpdated = tc.Now().Add(3 * time.Minute) 676 677 response, err := bm.Update(ctx, bugsToUpdate) 678 So(response, ShouldResemble, expectedResponse) 679 So(err, ShouldBeNil) 680 So(ChromiumTestIssuePriority(f.Issues[0].Issue), ShouldEqual, "3") 681 So(f.Issues[0].Comments, ShouldHaveLength, originalCommentCount+2) 682 So(f.Issues[0].Comments[originalCommentCount+1].Content, ShouldContainSubstring, 683 "Because the following problem(s) are active:\n"+ 684 "- Problem A (P3)\n"+ 685 "\n"+ 686 "The bug priority has been set to P3.") 687 So(f.Issues[0].Comments[originalCommentCount+1].Content, ShouldContainSubstring, 688 "https://luci-analysis-test.appspot.com/help#priority-update") 689 So(f.Issues[0].Comments[originalCommentCount+1].Content, ShouldContainSubstring, 690 "https://luci-analysis-test.appspot.com/p/luciproject/rules/rule-id") 691 692 // Verify repeated update has no effect. 693 So(verifyUpdateDoesNothing(), ShouldBeNil) 694 }) 695 }) 696 Convey("Does nothing if in simulation mode", func() { 697 // In simulation mode, changes should be logged but not made. 698 bm.Simulate = true 699 So(verifyUpdateDoesNothing(), ShouldBeNil) 700 }) 701 }) 702 Convey("If all policies deactivate", func() { 703 // De-activate all policies, so the bug would normally be marked verified. 704 for _, policyState := range bugsToUpdate[0].BugManagementState.PolicyState { 705 if policyState.IsActive { 706 policyState.IsActive = false 707 policyState.LastDeactivationTime = timestamppb.New(activationTime.Add(time.Hour)) 708 } 709 } 710 711 Convey("Does not update bug if IsManagingBug false", func() { 712 bugsToUpdate[0].IsManagingBug = false 713 714 So(verifyUpdateDoesNothing(), ShouldBeNil) 715 }) 716 Convey("Update closes bug", func() { 717 // Act 718 response, err := bm.Update(ctx, bugsToUpdate) 719 720 // Verify 721 So(err, ShouldBeNil) 722 So(response, ShouldResemble, expectedResponse) 723 So(f.Issues[0].Issue.Status.Status, ShouldEqual, VerifiedStatus) 724 725 expectedComment := "Because the following problem(s) have stopped:\n" + 726 "- Problem C (P1)\n" + 727 "- Problem A (P3)\n" + 728 "The bug has been verified." 729 So(f.Issues[0].Comments, ShouldHaveLength, originalCommentCount+1) 730 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, expectedComment) 731 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 732 "https://luci-analysis-test.appspot.com/help#bug-verified") 733 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 734 "https://luci-analysis-test.appspot.com/p/luciproject/rules/rule-id") 735 736 // Verify repeated update has no effect. 737 So(verifyUpdateDoesNothing(), ShouldBeNil) 738 739 Convey("Rules for verified bugs archived after 30 days", func() { 740 tc.Add(time.Hour * 24 * 30) 741 742 expectedResponse := []bugs.BugUpdateResponse{ 743 { 744 ShouldArchive: true, 745 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{}, 746 }, 747 } 748 originalIssues := CopyIssuesStore(f) 749 750 // Act 751 response, err := bm.Update(ctx, bugsToUpdate) 752 753 // Verify 754 So(err, ShouldBeNil) 755 So(response, ShouldResemble, expectedResponse) 756 So(f, ShouldResembleProto, originalIssues) 757 }) 758 759 Convey("If impact increases, bug is re-opened with correct priority", func() { 760 // policy-b has priority P0. 761 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].IsActive = true 762 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].LastActivationTime = timestamppb.New(activationTime.Add(2 * time.Hour)) 763 bugsToUpdate[0].BugManagementState.PolicyState["policy-b"].ActivationNotified = true 764 765 Convey("Issue has owner", func() { 766 // Update issue owner. 767 updateReq := updateOwnerRequest(f.Issues[0].Issue.Name, "users/100") 768 err = usercl.ModifyIssues(ctx, updateReq) 769 So(err, ShouldBeNil) 770 So(f.Issues[0].Issue.Owner.GetUser(), ShouldEqual, "users/100") 771 originalCommentCount = len(f.Issues[0].Comments) 772 773 // Issue should return to "Assigned" status. 774 response, err := bm.Update(ctx, bugsToUpdate) 775 So(err, ShouldBeNil) 776 So(response, ShouldResemble, expectedResponse) 777 So(f.Issues[0].Issue.Status.Status, ShouldEqual, AssignedStatus) 778 So(ChromiumTestIssuePriority(f.Issues[0].Issue), ShouldEqual, "0") 779 780 expectedComment := "Because the following problem(s) have started:\n" + 781 "- Problem B (P0)\n" + 782 "The bug has been re-opened as P0." 783 So(f.Issues[0].Comments, ShouldHaveLength, originalCommentCount+1) 784 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, expectedComment) 785 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 786 "https://luci-analysis-test.appspot.com/help#bug-reopened") 787 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 788 "https://luci-analysis-test.appspot.com/p/luciproject/rules/rule-id") 789 790 // Verify repeated update has no effect. 791 So(verifyUpdateDoesNothing(), ShouldBeNil) 792 }) 793 Convey("Issue has no owner", func() { 794 // Remove owner. 795 updateReq := updateOwnerRequest(f.Issues[0].Issue.Name, "") 796 err = usercl.ModifyIssues(ctx, updateReq) 797 So(err, ShouldBeNil) 798 So(f.Issues[0].Issue.Owner.GetUser(), ShouldEqual, "") 799 originalCommentCount = len(f.Issues[0].Comments) 800 801 // Issue should return to "Untriaged" status. 802 response, err := bm.Update(ctx, bugsToUpdate) 803 So(err, ShouldBeNil) 804 So(response, ShouldResemble, expectedResponse) 805 So(f.Issues[0].Issue.Status.Status, ShouldEqual, UntriagedStatus) 806 So(ChromiumTestIssuePriority(f.Issues[0].Issue), ShouldEqual, "0") 807 808 expectedComment := "Because the following problem(s) have started:\n" + 809 "- Problem B (P0)\n" + 810 "The bug has been re-opened as P0." 811 So(f.Issues[0].Comments, ShouldHaveLength, originalCommentCount+1) 812 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, expectedComment) 813 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 814 "https://luci-analysis-test.appspot.com/help#priority-update") 815 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 816 "https://luci-analysis-test.appspot.com/help#bug-reopened") 817 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 818 "https://luci-analysis-test.appspot.com/p/luciproject/rules/rule-id") 819 820 // Verify repeated update has no effect. 821 So(verifyUpdateDoesNothing(), ShouldBeNil) 822 }) 823 }) 824 }) 825 }) 826 Convey("If bug duplicate", func() { 827 f.Issues[0].Issue.Status.Status = DuplicateStatus 828 expectedResponse := []bugs.BugUpdateResponse{ 829 { 830 IsDuplicate: true, 831 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{}, 832 }, 833 } 834 Convey("Issue has no assignee", func() { 835 f.Issues[0].Issue.Owner = nil 836 837 // As there is no assignee. 838 expectedResponse[0].IsDuplicateAndAssigned = false 839 840 originalIssues := CopyIssuesStore(f) 841 842 // Act 843 response, err := bm.Update(ctx, bugsToUpdate) 844 845 // Verify 846 So(err, ShouldBeNil) 847 So(response, ShouldResemble, expectedResponse) 848 So(f, ShouldResembleProto, originalIssues) 849 }) 850 Convey("Issue has owner", func() { 851 f.Issues[0].Issue.Owner = &mpb.Issue_UserValue{ 852 User: "users/100", 853 } 854 855 // As we have an assignee. 856 expectedResponse[0].IsDuplicateAndAssigned = true 857 858 originalIssues := CopyIssuesStore(f) 859 860 // Act 861 response, err := bm.Update(ctx, bugsToUpdate) 862 863 // Verify 864 So(err, ShouldBeNil) 865 So(response, ShouldResemble, expectedResponse) 866 So(f, ShouldResembleProto, originalIssues) 867 }) 868 }) 869 Convey("Rule not managing a bug archived after 30 days of the bug being in any closed state", func() { 870 tc.Add(time.Hour * 24 * 30) 871 872 bugsToUpdate[0].IsManagingBug = false 873 f.Issues[0].Issue.Status.Status = FixedStatus 874 875 expectedResponse := []bugs.BugUpdateResponse{ 876 { 877 ShouldArchive: true, 878 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{}, 879 }, 880 } 881 originalIssues := CopyIssuesStore(f) 882 883 // Act 884 response, err := bm.Update(ctx, bugsToUpdate) 885 886 // Verify 887 So(err, ShouldBeNil) 888 So(response, ShouldResemble, expectedResponse) 889 So(f, ShouldResembleProto, originalIssues) 890 }) 891 Convey("Rule managing a bug not archived after 30 days of the bug being in fixed state", func() { 892 tc.Add(time.Hour * 24 * 30) 893 894 // If LUCI Analysis is mangaging the bug state, the fixed state 895 // means the bug is still not verified. Do not archive the 896 // rule. 897 bugsToUpdate[0].IsManagingBug = true 898 f.Issues[0].Issue.Status.Status = FixedStatus 899 900 So(verifyUpdateDoesNothing(), ShouldBeNil) 901 }) 902 Convey("Rules archived immediately if bug archived", func() { 903 f.Issues[0].Issue.Status.Status = "Archived" 904 905 expectedResponse := []bugs.BugUpdateResponse{ 906 { 907 ShouldArchive: true, 908 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{}, 909 }, 910 } 911 originalIssues := CopyIssuesStore(f) 912 913 // Act 914 response, err := bm.Update(ctx, bugsToUpdate) 915 916 // Verify 917 So(err, ShouldBeNil) 918 So(response, ShouldResemble, expectedResponse) 919 So(f, ShouldResembleProto, originalIssues) 920 }) 921 Convey("If issue does not exist, does nothing", func() { 922 f.Issues = nil 923 So(verifyUpdateDoesNothing(), ShouldBeNil) 924 }) 925 }) 926 Convey("GetMergedInto", func() { 927 c := NewCreateRequest() 928 c.ActivePolicyIDs = map[bugs.PolicyID]struct{}{ 929 "policy-a": {}, 930 } 931 response := bm.Create(ctx, c) 932 So(response, ShouldResemble, bugs.BugCreateResponse{ 933 ID: "chromium/100", 934 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 935 "policy-a": {}, 936 }, 937 }) 938 So(len(f.Issues), ShouldEqual, 1) 939 940 bugID := bugs.BugID{System: bugs.MonorailSystem, ID: "chromium/100"} 941 Convey("Merged into monorail bug", func() { 942 f.Issues[0].Issue.Status.Status = DuplicateStatus 943 f.Issues[0].Issue.MergedIntoIssueRef = &mpb.IssueRef{ 944 Issue: "projects/testproject/issues/99", 945 } 946 947 result, err := bm.GetMergedInto(ctx, bugID) 948 So(err, ShouldEqual, nil) 949 So(result, ShouldResemble, &bugs.BugID{ 950 System: bugs.MonorailSystem, 951 ID: "testproject/99", 952 }) 953 }) 954 Convey("Merged into buganizer bug", func() { 955 f.Issues[0].Issue.Status.Status = DuplicateStatus 956 f.Issues[0].Issue.MergedIntoIssueRef = &mpb.IssueRef{ 957 ExtIdentifier: "b/1234", 958 } 959 960 result, err := bm.GetMergedInto(ctx, bugID) 961 So(err, ShouldEqual, nil) 962 So(result, ShouldResemble, &bugs.BugID{ 963 System: bugs.BuganizerSystem, 964 ID: "1234", 965 }) 966 }) 967 Convey("Not merged into any bug", func() { 968 // While MergedIntoIssueRef is set, the bug status is not 969 // set to "Duplicate", so this value should be ignored. 970 f.Issues[0].Issue.Status.Status = UntriagedStatus 971 f.Issues[0].Issue.MergedIntoIssueRef = &mpb.IssueRef{ 972 ExtIdentifier: "b/1234", 973 } 974 975 result, err := bm.GetMergedInto(ctx, bugID) 976 So(err, ShouldEqual, nil) 977 So(result, ShouldBeNil) 978 }) 979 }) 980 Convey("UpdateDuplicateSource", func() { 981 c := NewCreateRequest() 982 c.ActivePolicyIDs = map[bugs.PolicyID]struct{}{ 983 "policy-a": {}, 984 } 985 response := bm.Create(ctx, c) 986 So(response, ShouldResemble, bugs.BugCreateResponse{ 987 ID: "chromium/100", 988 PolicyActivationsNotified: map[bugs.PolicyID]struct{}{ 989 "policy-a": {}, 990 }, 991 }) 992 So(f.Issues, ShouldHaveLength, 1) 993 So(f.Issues[0].Comments, ShouldHaveLength, 2) 994 originalCommentCount := len(f.Issues[0].Comments) 995 996 f.Issues[0].Issue.Status.Status = DuplicateStatus 997 f.Issues[0].Issue.MergedIntoIssueRef = &mpb.IssueRef{ 998 Issue: "projects/testproject/issues/99", 999 } 1000 1001 Convey("With ErrorMessage", func() { 1002 request := bugs.UpdateDuplicateSourceRequest{ 1003 BugDetails: bugs.DuplicateBugDetails{ 1004 RuleID: "source-rule-id", 1005 Bug: bugs.BugID{System: bugs.MonorailSystem, ID: "chromium/100"}, 1006 }, 1007 ErrorMessage: "Some error.", 1008 } 1009 err = bm.UpdateDuplicateSource(ctx, request) 1010 So(err, ShouldBeNil) 1011 1012 So(f.Issues[0].Issue.Status.Status, ShouldNotEqual, DuplicateStatus) 1013 So(f.Issues[0].Comments, ShouldHaveLength, originalCommentCount+1) 1014 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, "Some error.") 1015 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 1016 "https://luci-analysis-test.appspot.com/p/luciproject/rules/source-rule-id") 1017 }) 1018 Convey("Without ErrorMessage", func() { 1019 request := bugs.UpdateDuplicateSourceRequest{ 1020 BugDetails: bugs.DuplicateBugDetails{ 1021 RuleID: "source-rule-id", 1022 Bug: bugs.BugID{System: bugs.MonorailSystem, ID: "chromium/100"}, 1023 }, 1024 DestinationRuleID: "12345abcdef", 1025 } 1026 err = bm.UpdateDuplicateSource(ctx, request) 1027 So(err, ShouldBeNil) 1028 1029 So(f.Issues[0].Issue.Status.Status, ShouldEqual, DuplicateStatus) 1030 So(f.Issues[0].Comments, ShouldHaveLength, originalCommentCount+1) 1031 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, "merged the failure association rule for this bug into the rule for the canonical bug.") 1032 So(f.Issues[0].Comments[originalCommentCount].Content, ShouldContainSubstring, 1033 "https://luci-analysis-test.appspot.com/p/luciproject/rules/12345abcdef") 1034 }) 1035 }) 1036 }) 1037 } 1038 1039 func updateOwnerRequest(name string, owner string) *mpb.ModifyIssuesRequest { 1040 return &mpb.ModifyIssuesRequest{ 1041 Deltas: []*mpb.IssueDelta{ 1042 { 1043 Issue: &mpb.Issue{ 1044 Name: name, 1045 Owner: &mpb.Issue_UserValue{ 1046 User: owner, 1047 }, 1048 }, 1049 UpdateMask: &field_mask.FieldMask{ 1050 Paths: []string{"owner"}, 1051 }, 1052 }, 1053 }, 1054 CommentContent: "User comment.", 1055 } 1056 } 1057 1058 func updateBugPriorityRequest(name string, priority string) *mpb.ModifyIssuesRequest { 1059 return &mpb.ModifyIssuesRequest{ 1060 Deltas: []*mpb.IssueDelta{ 1061 { 1062 Issue: &mpb.Issue{ 1063 Name: name, 1064 FieldValues: []*mpb.FieldValue{ 1065 { 1066 Field: "projects/chromium/fieldDefs/11", 1067 Value: priority, 1068 }, 1069 }, 1070 }, 1071 UpdateMask: &field_mask.FieldMask{ 1072 Paths: []string{"field_values"}, 1073 }, 1074 }, 1075 }, 1076 CommentContent: "User comment.", 1077 } 1078 } 1079 1080 func addLabel(labels []*mpb.Issue_LabelValue, label string) []*mpb.Issue_LabelValue { 1081 result := append(labels, &mpb.Issue_LabelValue{Label: label}) 1082 SortLabels(result) 1083 return result 1084 } 1085 1086 func removeLabel(labels []*mpb.Issue_LabelValue, label string) []*mpb.Issue_LabelValue { 1087 var result []*mpb.Issue_LabelValue 1088 for _, item := range labels { 1089 if item.Label != label { 1090 result = append(result, item) 1091 } 1092 } 1093 SortLabels(result) 1094 return result 1095 } 1096 1097 func containsLabel(labels []*mpb.Issue_LabelValue, label string) bool { 1098 for _, item := range labels { 1099 if item.Label == label { 1100 return true 1101 } 1102 } 1103 return false 1104 }