go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/bugs/updater/updater_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 updater 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "strings" 22 "testing" 23 "time" 24 25 "cloud.google.com/go/bigquery" 26 "google.golang.org/grpc/codes" 27 "google.golang.org/grpc/status" 28 "google.golang.org/protobuf/proto" 29 "google.golang.org/protobuf/types/known/fieldmaskpb" 30 "google.golang.org/protobuf/types/known/timestamppb" 31 32 "go.chromium.org/luci/common/clock" 33 "go.chromium.org/luci/common/clock/testclock" 34 "go.chromium.org/luci/common/errors" 35 "go.chromium.org/luci/config/validation" 36 "go.chromium.org/luci/gae/impl/memory" 37 "go.chromium.org/luci/server/span" 38 "go.chromium.org/luci/third_party/google.golang.org/genproto/googleapis/devtools/issuetracker/v1" 39 40 "go.chromium.org/luci/analysis/internal/analysis" 41 "go.chromium.org/luci/analysis/internal/analysis/metrics" 42 "go.chromium.org/luci/analysis/internal/bugs" 43 "go.chromium.org/luci/analysis/internal/bugs/buganizer" 44 "go.chromium.org/luci/analysis/internal/bugs/monorail" 45 mpb "go.chromium.org/luci/analysis/internal/bugs/monorail/api_proto" 46 bugspb "go.chromium.org/luci/analysis/internal/bugs/proto" 47 "go.chromium.org/luci/analysis/internal/clustering/algorithms" 48 "go.chromium.org/luci/analysis/internal/clustering/rules" 49 "go.chromium.org/luci/analysis/internal/clustering/runs" 50 "go.chromium.org/luci/analysis/internal/config" 51 "go.chromium.org/luci/analysis/internal/config/compiledcfg" 52 "go.chromium.org/luci/analysis/internal/testutil" 53 configpb "go.chromium.org/luci/analysis/proto/config" 54 55 . "github.com/smartystreets/goconvey/convey" 56 . "go.chromium.org/luci/common/testing/assertions" 57 ) 58 59 func TestUpdate(t *testing.T) { 60 Convey("With bug updater", t, func() { 61 ctx := testutil.IntegrationTestContext(t) 62 ctx = memory.Use(ctx) 63 ctx = context.WithValue(ctx, &buganizer.BuganizerSelfEmailKey, "email@test.com") 64 65 const project = "chromeos" 66 67 // Has two policies: 68 // exoneration-policy (P2): 69 // - activation threshold: 100 in one day 70 // - deactivation threshold: 10 in one day 71 // cls-rejected-policy (P1): 72 // - activation threshold: 10 in one week 73 // - deactivation threshold: 1 in one week 74 projectCfg := createProjectConfig() 75 projectsCfg := map[string]*configpb.ProjectConfig{ 76 project: projectCfg, 77 } 78 err := config.SetTestProjectConfig(ctx, projectsCfg) 79 So(err, ShouldBeNil) 80 81 compiledCfg, err := compiledcfg.NewConfig(projectCfg) 82 So(err, ShouldBeNil) 83 84 suggestedClusters := []*analysis.Cluster{ 85 makeReasonCluster(compiledCfg, 0), 86 makeReasonCluster(compiledCfg, 1), 87 makeReasonCluster(compiledCfg, 2), 88 makeReasonCluster(compiledCfg, 3), 89 makeReasonCluster(compiledCfg, 4), 90 } 91 analysisClient := &fakeAnalysisClient{ 92 clusters: suggestedClusters, 93 } 94 95 buganizerClient := buganizer.NewFakeClient() 96 buganizerStore := buganizerClient.FakeStore 97 98 monorailStore := &monorail.FakeIssuesStore{ 99 NextID: 100, 100 PriorityFieldName: "projects/chromium/fieldDefs/11", 101 ComponentNames: []string{ 102 "projects/chromium/componentDefs/Blink", 103 "projects/chromium/componentDefs/Blink>Layout", 104 "projects/chromium/componentDefs/Blink>Network", 105 }, 106 } 107 user := monorail.AutomationUsers[0] 108 monorailClient, err := monorail.NewClient(monorail.UseFakeIssuesClient(ctx, monorailStore, user), "myhost") 109 So(err, ShouldBeNil) 110 111 // Unless otherwise specified, assume re-clustering has caught up to 112 // the latest version of algorithms and config. 113 err = runs.SetRunsForTesting(ctx, []*runs.ReclusteringRun{ 114 runs.NewRun(0). 115 WithProject(project). 116 WithAlgorithmsVersion(algorithms.AlgorithmsVersion). 117 WithConfigVersion(projectCfg.LastUpdated.AsTime()). 118 WithRulesVersion(rules.StartingEpoch). 119 WithCompletedProgress().Build(), 120 }) 121 So(err, ShouldBeNil) 122 123 progress, err := runs.ReadReclusteringProgress(ctx, project) 124 So(err, ShouldBeNil) 125 126 opts := UpdateOptions{ 127 UIBaseURL: "https://luci-analysis-test.appspot.com", 128 Project: project, 129 AnalysisClient: analysisClient, 130 BuganizerClient: buganizerClient, 131 MonorailClient: monorailClient, 132 MaxBugsFiledPerRun: 1, 133 ReclusteringProgress: progress, 134 RunTimestamp: time.Date(2100, 2, 2, 2, 2, 2, 2, time.UTC), 135 } 136 137 // Mock current time. This is needed to control behaviours like 138 // automatic archiving of rules after 30 days of bug being marked 139 // Closed (Verified). 140 now := time.Date(2055, time.May, 5, 5, 5, 5, 5, time.UTC) 141 ctx, tc := testclock.UseTime(ctx, now) 142 143 Convey("configuration used for testing is valid", func() { 144 c := validation.Context{Context: context.Background()} 145 config.ValidateProjectConfig(&c, project, projectCfg) 146 So(c.Finalize(), ShouldBeNil) 147 }) 148 Convey("with a suggested cluster", func() { 149 // Create a suggested cluster we should consider filing a bug for. 150 sourceClusterID := reasonClusterID(compiledCfg, "Failed to connect to 100.1.1.99.") 151 suggestedClusters[1].ClusterID = sourceClusterID 152 suggestedClusters[1].ExampleFailureReason = bigquery.NullString{StringVal: "Failed to connect to 100.1.1.105.", Valid: true} 153 suggestedClusters[1].TopTestIDs = []analysis.TopCount{ 154 {Value: "network-test-1", Count: 10}, 155 {Value: "network-test-2", Count: 10}, 156 } 157 // Meets failure dispersion thresholds. 158 suggestedClusters[1].DistinctUserCLsWithFailures7d.Residual = 3 159 160 expectedRule := &rules.Entry{ 161 Project: "chromeos", 162 RuleDefinition: `reason LIKE "Failed to connect to %.%.%.%."`, 163 BugID: bugs.BugID{System: bugs.BuganizerSystem, ID: "1"}, 164 IsActive: true, 165 IsManagingBug: true, 166 IsManagingBugPriority: true, 167 SourceCluster: sourceClusterID, 168 CreateUser: rules.LUCIAnalysisSystem, 169 LastAuditableUpdateUser: rules.LUCIAnalysisSystem, 170 BugManagementState: &bugspb.BugManagementState{ 171 RuleAssociationNotified: true, 172 PolicyState: map[string]*bugspb.BugManagementState_PolicyState{ 173 "exoneration-policy": { 174 IsActive: true, 175 LastActivationTime: timestamppb.New(opts.RunTimestamp), 176 ActivationNotified: true, 177 }, 178 "cls-rejected-policy": {}, 179 }, 180 }, 181 } 182 expectedRules := []*rules.Entry{expectedRule} 183 184 expectedBuganizerBug := buganizerBug{ 185 ID: 1, 186 Component: projectCfg.BugManagement.Buganizer.DefaultComponent.Id, 187 ExpectedTitle: "Failed to connect to 100.1.1.105.", 188 // Expect the bug description to contain the top tests. 189 ExpectedContent: []string{ 190 "https://luci-analysis-test.appspot.com/p/chromeos/rules/", // Rule ID randomly generated. 191 "network-test-1", 192 "network-test-2", 193 }, 194 ExpectedPolicyIDsActivated: []string{ 195 "exoneration-policy", 196 }, 197 } 198 199 issueCount := func() int { 200 return len(buganizerStore.Issues) + len(monorailStore.Issues) 201 } 202 203 // Bug-filing threshold met. 204 suggestedClusters[1].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{ 205 OneDay: metrics.Counts{Residual: 100}, 206 } 207 208 Convey("bug filing threshold must be met to file a new bug", func() { 209 Convey("Reason cluster", func() { 210 Convey("Above threshold", func() { 211 suggestedClusters[1].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{OneDay: metrics.Counts{Residual: 100}} 212 213 // Act 214 err = UpdateBugsForProject(ctx, opts) 215 216 // Verify 217 So(err, ShouldBeNil) 218 219 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 220 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 221 So(issueCount(), ShouldEqual, 1) 222 223 // Further updates do nothing. 224 err = UpdateBugsForProject(ctx, opts) 225 226 // Verify 227 So(err, ShouldBeNil) 228 229 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 230 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 231 So(issueCount(), ShouldEqual, 1) 232 }) 233 Convey("Below threshold", func() { 234 suggestedClusters[1].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{OneDay: metrics.Counts{Residual: 99}} 235 236 // Act 237 err = UpdateBugsForProject(ctx, opts) 238 239 // Verify 240 So(err, ShouldBeNil) 241 242 // No bug should be created. 243 So(verifyRulesResemble(ctx, nil), ShouldBeNil) 244 So(issueCount(), ShouldEqual, 0) 245 }) 246 }) 247 Convey("Test name cluster", func() { 248 suggestedClusters[1].ClusterID = testIDClusterID(compiledCfg, "ui-test-1") 249 suggestedClusters[1].TopTestIDs = []analysis.TopCount{ 250 {Value: "ui-test-1", Count: 10}, 251 } 252 expectedRule.RuleDefinition = `test = "ui-test-1"` 253 expectedRule.SourceCluster = suggestedClusters[1].ClusterID 254 expectedBuganizerBug.ExpectedTitle = "ui-test-1" 255 expectedBuganizerBug.ExpectedContent = []string{"ui-test-1"} 256 257 // 34% more impact is required for a test name cluster to 258 // be filed, compared to a failure reason cluster. 259 Convey("Above threshold", func() { 260 suggestedClusters[1].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{OneDay: metrics.Counts{Residual: 134}} 261 262 // Act 263 err = UpdateBugsForProject(ctx, opts) 264 265 // Verify 266 So(err, ShouldBeNil) 267 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 268 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 269 So(issueCount(), ShouldEqual, 1) 270 271 // Further updates do nothing. 272 err = UpdateBugsForProject(ctx, opts) 273 274 // Verify 275 So(err, ShouldBeNil) 276 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 277 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 278 So(issueCount(), ShouldEqual, 1) 279 }) 280 Convey("Below threshold", func() { 281 suggestedClusters[1].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{OneDay: metrics.Counts{Residual: 133}} 282 283 // Act 284 err = UpdateBugsForProject(ctx, opts) 285 286 // Verify 287 So(err, ShouldBeNil) 288 289 // No bug should be created. 290 So(verifyRulesResemble(ctx, nil), ShouldBeNil) 291 So(issueCount(), ShouldEqual, 0) 292 }) 293 }) 294 }) 295 Convey("policies are correctly activated when new bugs are filed", func() { 296 Convey("other policy activation threshold not met", func() { 297 suggestedClusters[1].MetricValues[metrics.HumanClsFailedPresubmit.ID] = metrics.TimewiseCounts{SevenDay: metrics.Counts{Residual: 9}} 298 299 // Act 300 err = UpdateBugsForProject(ctx, opts) 301 302 // Verify 303 So(err, ShouldBeNil) 304 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 305 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 306 So(issueCount(), ShouldEqual, 1) 307 }) 308 Convey("other policy activation threshold met", func() { 309 suggestedClusters[1].MetricValues[metrics.HumanClsFailedPresubmit.ID] = metrics.TimewiseCounts{SevenDay: metrics.Counts{Residual: 10}} 310 expectedRule.BugManagementState.PolicyState["cls-rejected-policy"].IsActive = true 311 expectedRule.BugManagementState.PolicyState["cls-rejected-policy"].LastActivationTime = timestamppb.New(opts.RunTimestamp) 312 expectedRule.BugManagementState.PolicyState["cls-rejected-policy"].ActivationNotified = true 313 expectedBuganizerBug.ExpectedPolicyIDsActivated = []string{ 314 "cls-rejected-policy", 315 "exoneration-policy", 316 } 317 318 // Act 319 err = UpdateBugsForProject(ctx, opts) 320 321 // Verify 322 So(err, ShouldBeNil) 323 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 324 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 325 So(issueCount(), ShouldEqual, 1) 326 }) 327 }) 328 Convey("dispersion criteria must be met to file a new bug", func() { 329 Convey("met via User CLs with failures", func() { 330 suggestedClusters[1].DistinctUserCLsWithFailures7d.Residual = 3 331 suggestedClusters[1].PostsubmitBuildsWithFailures7d.Residual = 0 332 333 // Act 334 err = UpdateBugsForProject(ctx, opts) 335 336 // Verify 337 So(err, ShouldBeNil) 338 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 339 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 340 So(issueCount(), ShouldEqual, 1) 341 }) 342 Convey("met via Postsubmit builds with failures", func() { 343 suggestedClusters[1].DistinctUserCLsWithFailures7d.Residual = 0 344 suggestedClusters[1].PostsubmitBuildsWithFailures7d.Residual = 1 345 346 // Act 347 err = UpdateBugsForProject(ctx, opts) 348 349 // Verify 350 So(err, ShouldBeNil) 351 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 352 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 353 So(issueCount(), ShouldEqual, 1) 354 }) 355 Convey("not met", func() { 356 suggestedClusters[1].DistinctUserCLsWithFailures7d.Residual = 0 357 suggestedClusters[1].PostsubmitBuildsWithFailures7d.Residual = 0 358 359 // Act 360 err = UpdateBugsForProject(ctx, opts) 361 362 // Verify 363 So(err, ShouldBeNil) 364 // No bug should be created. 365 So(verifyRulesResemble(ctx, nil), ShouldBeNil) 366 So(issueCount(), ShouldEqual, 0) 367 }) 368 }) 369 Convey("duplicate bugs are suppressed", func() { 370 Convey("where a rule was recently filed for the same suggested cluster, and reclustering is pending", func() { 371 createTime := time.Date(2021, time.January, 5, 12, 30, 0, 0, time.UTC) 372 buganizerStore.StoreIssue(ctx, buganizer.NewFakeIssue(1)) 373 existingRule := rules.NewRule(1). 374 WithBugSystem(bugs.BuganizerSystem). 375 WithProject(project). 376 WithCreateTime(createTime). 377 WithPredicateLastUpdateTime(createTime.Add(1 * time.Hour)). 378 WithLastAuditableUpdateTime(createTime.Add(2 * time.Hour)). 379 WithLastUpdateTime(createTime.Add(3 * time.Hour)). 380 WithBugPriorityManaged(true). 381 WithBugPriorityManagedLastUpdateTime(createTime.Add(1 * time.Hour)). 382 WithSourceCluster(sourceClusterID).Build() 383 err := rules.SetForTesting(ctx, []*rules.Entry{ 384 existingRule, 385 }) 386 So(err, ShouldBeNil) 387 388 // Initially do not expect a new bug to be filed. 389 err = UpdateBugsForProject(ctx, opts) 390 391 So(err, ShouldBeNil) 392 So(verifyRulesResemble(ctx, []*rules.Entry{existingRule}), ShouldBeNil) 393 So(issueCount(), ShouldEqual, 1) 394 395 // Once re-clustering has incorporated the version of rules 396 // that included this new rule, it is OK to file another bug 397 // for the suggested cluster if sufficient impact remains. 398 // This should only happen when the rule definition has been 399 // manually narrowed in some way from the originally filed bug. 400 err = runs.SetRunsForTesting(ctx, []*runs.ReclusteringRun{ 401 runs.NewRun(0). 402 WithProject(project). 403 WithAlgorithmsVersion(algorithms.AlgorithmsVersion). 404 WithConfigVersion(projectCfg.LastUpdated.AsTime()). 405 WithRulesVersion(createTime). 406 WithCompletedProgress().Build(), 407 }) 408 So(err, ShouldBeNil) 409 progress, err := runs.ReadReclusteringProgress(ctx, project) 410 So(err, ShouldBeNil) 411 opts.ReclusteringProgress = progress 412 413 // Act 414 err = UpdateBugsForProject(ctx, opts) 415 416 // Verify 417 So(err, ShouldBeNil) 418 expectedBuganizerBug.ID = 2 // Because we already created a bug with ID 1 above. 419 expectedRule.BugID.ID = "2" 420 So(verifyRulesResemble(ctx, []*rules.Entry{expectedRule, existingRule}), ShouldBeNil) 421 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 422 So(issueCount(), ShouldEqual, 2) 423 }) 424 Convey("when re-clustering to new algorithms", func() { 425 err = runs.SetRunsForTesting(ctx, []*runs.ReclusteringRun{ 426 runs.NewRun(0). 427 WithProject(project). 428 WithAlgorithmsVersion(algorithms.AlgorithmsVersion - 1). 429 WithConfigVersion(projectCfg.LastUpdated.AsTime()). 430 WithRulesVersion(rules.StartingEpoch). 431 WithCompletedProgress().Build(), 432 }) 433 So(err, ShouldBeNil) 434 progress, err := runs.ReadReclusteringProgress(ctx, project) 435 So(err, ShouldBeNil) 436 opts.ReclusteringProgress = progress 437 438 // Act 439 err = UpdateBugsForProject(ctx, opts) 440 441 // Verify no bugs were filed. 442 So(err, ShouldBeNil) 443 So(verifyRulesResemble(ctx, nil), ShouldBeNil) 444 So(issueCount(), ShouldEqual, 0) 445 }) 446 Convey("when re-clustering to new config", func() { 447 err = runs.SetRunsForTesting(ctx, []*runs.ReclusteringRun{ 448 runs.NewRun(0). 449 WithProject(project). 450 WithAlgorithmsVersion(algorithms.AlgorithmsVersion). 451 WithConfigVersion(projectCfg.LastUpdated.AsTime().Add(-1 * time.Hour)). 452 WithRulesVersion(rules.StartingEpoch). 453 WithCompletedProgress().Build(), 454 }) 455 So(err, ShouldBeNil) 456 progress, err := runs.ReadReclusteringProgress(ctx, project) 457 So(err, ShouldBeNil) 458 opts.ReclusteringProgress = progress 459 460 // Act 461 err = UpdateBugsForProject(ctx, opts) 462 463 // Verify no bugs were filed. 464 So(err, ShouldBeNil) 465 So(verifyRulesResemble(ctx, nil), ShouldBeNil) 466 So(issueCount(), ShouldEqual, 0) 467 }) 468 }) 469 Convey("bugs are routed to the correct issue tracker and component", func() { 470 suggestedClusters[1].TopBuganizerComponents = []analysis.TopCount{ 471 {Value: "77777", Count: 20}, 472 } 473 expectedBuganizerBug.Component = 77777 474 475 suggestedClusters[1].TopMonorailComponents = []analysis.TopCount{ 476 {Value: "Blink>Layout", Count: 40}, // >30% of failures. 477 {Value: "Blink>Network", Count: 31}, // >30% of failures. 478 {Value: "Blink>Other", Count: 4}, 479 } 480 expectedMonorailBug := monorailBug{ 481 Project: "chromium", 482 ID: 100, 483 ExpectedComponents: []string{ 484 "projects/chromium/componentDefs/Blink>Layout", 485 "projects/chromium/componentDefs/Blink>Network", 486 }, 487 ExpectedTitle: "Failed to connect to 100.1.1.105.", 488 // Expect the bug description to contain the top tests. 489 ExpectedContent: []string{ 490 "network-test-1", 491 "network-test-2", 492 }, 493 ExpectedPolicyIDsActivated: []string{ 494 "exoneration-policy", 495 }, 496 } 497 expectedRule.BugID = bugs.BugID{ 498 System: "monorail", 499 ID: "chromium/100", 500 } 501 502 Convey("if Monorail component has greatest failure count, should create Monorail issue", func() { 503 suggestedClusters[1].TopBuganizerComponents = []analysis.TopCount{{ 504 Value: "12345", 505 Count: 39, 506 }} 507 508 // Act 509 err = UpdateBugsForProject(ctx, opts) 510 511 // Verify 512 So(err, ShouldBeNil) 513 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 514 So(expectMonorailBug(monorailStore, expectedMonorailBug), ShouldBeNil) 515 So(issueCount(), ShouldEqual, 1) 516 }) 517 Convey("if Buganizer component has higher failure count, should creates Buganizer issue", func() { 518 suggestedClusters[1].TopBuganizerComponents = []analysis.TopCount{{ 519 // Check that null values are ignored. 520 Value: "", 521 Count: 100, 522 }, { 523 Value: "681721", 524 Count: 41, 525 }} 526 expectedBuganizerBug.Component = 681721 527 528 // Act 529 err = UpdateBugsForProject(ctx, opts) 530 531 // Verify 532 So(err, ShouldBeNil) 533 expectedRule.BugID = bugs.BugID{ 534 System: "buganizer", 535 ID: "1", 536 } 537 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 538 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 539 So(issueCount(), ShouldEqual, 1) 540 }) 541 Convey("with no Buganizer configuration, should use Monorail as default system", func() { 542 // Ensure Buganizer component has highest failure impact. 543 suggestedClusters[1].TopBuganizerComponents = []analysis.TopCount{{ 544 Value: "88888", 545 Count: 99999, 546 }} 547 548 // But buganizer is not configured, so we should file into monorail. 549 projectCfg.BugManagement.Buganizer = nil 550 err = config.SetTestProjectConfig(ctx, projectsCfg) 551 So(err, ShouldBeNil) 552 553 // Act 554 err = UpdateBugsForProject(ctx, opts) 555 556 // Verify 557 So(err, ShouldBeNil) 558 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 559 So(expectMonorailBug(monorailStore, expectedMonorailBug), ShouldBeNil) 560 So(issueCount(), ShouldEqual, 1) 561 }) 562 Convey("with no Monorail configuration, should use Buganizer as default system", func() { 563 // Ensure Monorail component has highest failure impact. 564 suggestedClusters[1].TopMonorailComponents = []analysis.TopCount{{ 565 Value: "Infra", 566 Count: 99999, 567 }} 568 569 // But monorail is not configured, so we should file into Buganizer. 570 projectCfg.BugManagement.Monorail = nil 571 err = config.SetTestProjectConfig(ctx, projectsCfg) 572 So(err, ShouldBeNil) 573 574 // Act 575 err = UpdateBugsForProject(ctx, opts) 576 577 // Verify 578 So(err, ShouldBeNil) 579 expectedRule.BugID = bugs.BugID{ 580 System: "buganizer", 581 ID: "1", 582 } 583 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 584 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 585 So(issueCount(), ShouldEqual, 1) 586 }) 587 Convey("in case of tied failure count between monorail/buganizer, should use default bug system", func() { 588 // The default bug system is buganizer. 589 So(projectCfg.BugManagement.DefaultBugSystem, ShouldEqual, configpb.BugSystem_BUGANIZER) 590 591 suggestedClusters[1].TopBuganizerComponents = []analysis.TopCount{{ 592 Value: "", 593 Count: 55, 594 }, { 595 Value: "681721", 596 Count: 40, // Tied with monorail. 597 }} 598 expectedRule.BugID = bugs.BugID{ 599 System: "buganizer", 600 ID: "1", 601 } 602 expectedBuganizerBug.Component = 681721 603 604 // Act 605 err = UpdateBugsForProject(ctx, opts) 606 607 // Verify we filed into Buganizer. 608 So(err, ShouldBeNil) 609 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 610 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 611 So(issueCount(), ShouldEqual, 1) 612 }) 613 }) 614 Convey("partial success creating bugs is correctly handled", func() { 615 // Inject an error updating the bug after creation. 616 buganizerClient.CreateCommentError = status.Errorf(codes.Internal, "internal error creating comment") 617 618 // Act 619 err = UpdateBugsForProject(ctx, opts) 620 621 // Do not expect policy activations to have been notified. 622 expectedBuganizerBug.ExpectedPolicyIDsActivated = []string{} 623 expectedRule.BugManagementState.PolicyState["exoneration-policy"].ActivationNotified = false 624 625 // Verify the rule was still created. 626 So(err, ShouldErrLike, "internal error creating comment") 627 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 628 So(expectBuganizerBug(buganizerStore, expectedBuganizerBug), ShouldBeNil) 629 So(issueCount(), ShouldEqual, 1) 630 }) 631 }) 632 Convey("With both failure reason and test name clusters above bug-filing threshold", func() { 633 // Reason cluster above the 1-day exoneration threshold. 634 suggestedClusters[2] = makeReasonCluster(compiledCfg, 2) 635 suggestedClusters[2].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{ 636 OneDay: metrics.Counts{Residual: 100}, 637 ThreeDay: metrics.Counts{Residual: 100}, 638 SevenDay: metrics.Counts{Residual: 100}, 639 } 640 suggestedClusters[2].PostsubmitBuildsWithFailures7d.Residual = 1 641 642 // Test name cluster with 33% more impact. 643 suggestedClusters[1] = makeTestNameCluster(compiledCfg, 3) 644 suggestedClusters[1].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{ 645 OneDay: metrics.Counts{Residual: 133}, 646 ThreeDay: metrics.Counts{Residual: 133}, 647 SevenDay: metrics.Counts{Residual: 133}, 648 } 649 suggestedClusters[1].PostsubmitBuildsWithFailures7d.Residual = 1 650 651 // Limit to one bug filed each time, so that 652 // we test change throttling. 653 opts.MaxBugsFiledPerRun = 1 654 655 Convey("reason clusters preferred over test name clusters", func() { 656 // Test name cluster has <34% more impact than the reason 657 // cluster. 658 659 // Act 660 err = UpdateBugsForProject(ctx, opts) 661 662 // Verify reason cluster filed. 663 rs, err := rules.ReadAllForTesting(span.Single(ctx)) 664 So(err, ShouldBeNil) 665 So(len(rs), ShouldEqual, 1) 666 So(rs[0].SourceCluster, ShouldResemble, suggestedClusters[2].ClusterID) 667 So(rs[0].SourceCluster.IsFailureReasonCluster(), ShouldBeTrue) 668 }) 669 Convey("test name clusters can be filed if significantly more impact", func() { 670 // Increase impact of the test name cluster so that the 671 // test name cluster has >34% more impact than the reason 672 // cluster. 673 suggestedClusters[1].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{ 674 OneDay: metrics.Counts{Residual: 135}, 675 ThreeDay: metrics.Counts{Residual: 135}, 676 SevenDay: metrics.Counts{Residual: 135}, 677 } 678 679 // Act 680 err = UpdateBugsForProject(ctx, opts) 681 682 // Verify test name cluster filed. 683 rs, err := rules.ReadAllForTesting(span.Single(ctx)) 684 So(err, ShouldBeNil) 685 So(len(rs), ShouldEqual, 1) 686 So(rs[0].SourceCluster, ShouldResemble, suggestedClusters[1].ClusterID) 687 So(rs[0].SourceCluster.IsTestNameCluster(), ShouldBeTrue) 688 }) 689 }) 690 Convey("With multiple rules / bugs on file", func() { 691 // Use a mix of test name and failure reason clusters for 692 // code path coverage. 693 suggestedClusters[0] = makeTestNameCluster(compiledCfg, 0) 694 suggestedClusters[0].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{ 695 OneDay: metrics.Counts{Residual: 940}, 696 ThreeDay: metrics.Counts{Residual: 940}, 697 SevenDay: metrics.Counts{Residual: 940}, 698 } 699 suggestedClusters[0].PostsubmitBuildsWithFailures7d.Residual = 1 700 701 suggestedClusters[1] = makeReasonCluster(compiledCfg, 1) 702 suggestedClusters[1].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{ 703 OneDay: metrics.Counts{Residual: 300}, 704 ThreeDay: metrics.Counts{Residual: 300}, 705 SevenDay: metrics.Counts{Residual: 300}, 706 } 707 suggestedClusters[1].PostsubmitBuildsWithFailures7d.Residual = 1 708 709 suggestedClusters[2] = makeReasonCluster(compiledCfg, 2) 710 suggestedClusters[2].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{ 711 OneDay: metrics.Counts{Residual: 250}, 712 ThreeDay: metrics.Counts{Residual: 250}, 713 SevenDay: metrics.Counts{Residual: 250}, 714 } 715 suggestedClusters[2].PostsubmitBuildsWithFailures7d.Residual = 1 716 suggestedClusters[2].TopMonorailComponents = []analysis.TopCount{ 717 {Value: "Monorail", Count: 250}, 718 } 719 720 suggestedClusters[3] = makeReasonCluster(compiledCfg, 3) 721 suggestedClusters[3].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{ 722 OneDay: metrics.Counts{Residual: 200}, 723 ThreeDay: metrics.Counts{Residual: 200}, 724 SevenDay: metrics.Counts{Residual: 200}, 725 } 726 suggestedClusters[3].PostsubmitBuildsWithFailures7d.Residual = 1 727 suggestedClusters[3].TopMonorailComponents = []analysis.TopCount{ 728 {Value: "Monorail", Count: 200}, 729 } 730 731 expectedRules := []*rules.Entry{ 732 { 733 Project: "chromeos", 734 RuleDefinition: `test = "testname-0"`, 735 BugID: bugs.BugID{System: bugs.BuganizerSystem, ID: "1"}, 736 SourceCluster: suggestedClusters[0].ClusterID, 737 IsActive: true, 738 IsManagingBug: true, 739 IsManagingBugPriority: true, 740 CreateUser: rules.LUCIAnalysisSystem, 741 LastAuditableUpdateUser: rules.LUCIAnalysisSystem, 742 BugManagementState: &bugspb.BugManagementState{ 743 RuleAssociationNotified: true, 744 PolicyState: map[string]*bugspb.BugManagementState_PolicyState{ 745 "exoneration-policy": { 746 IsActive: true, 747 LastActivationTime: timestamppb.New(opts.RunTimestamp), 748 ActivationNotified: true, 749 }, 750 "cls-rejected-policy": {}, 751 }, 752 }, 753 }, 754 { 755 Project: "chromeos", 756 RuleDefinition: `reason LIKE "want foo, got bar"`, 757 BugID: bugs.BugID{System: bugs.BuganizerSystem, ID: "2"}, 758 SourceCluster: suggestedClusters[1].ClusterID, 759 IsActive: true, 760 IsManagingBug: true, 761 IsManagingBugPriority: true, 762 CreateUser: rules.LUCIAnalysisSystem, 763 LastAuditableUpdateUser: rules.LUCIAnalysisSystem, 764 BugManagementState: &bugspb.BugManagementState{ 765 RuleAssociationNotified: true, 766 PolicyState: map[string]*bugspb.BugManagementState_PolicyState{ 767 "exoneration-policy": { 768 IsActive: true, 769 LastActivationTime: timestamppb.New(opts.RunTimestamp), 770 ActivationNotified: true, 771 }, 772 "cls-rejected-policy": {}, 773 }, 774 }, 775 }, 776 { 777 Project: "chromeos", 778 RuleDefinition: `reason LIKE "want foofoo, got bar"`, 779 BugID: bugs.BugID{System: bugs.MonorailSystem, ID: "chromium/100"}, 780 SourceCluster: suggestedClusters[2].ClusterID, 781 IsActive: true, 782 IsManagingBug: true, 783 IsManagingBugPriority: true, 784 CreateUser: rules.LUCIAnalysisSystem, 785 LastAuditableUpdateUser: rules.LUCIAnalysisSystem, 786 BugManagementState: &bugspb.BugManagementState{ 787 RuleAssociationNotified: true, 788 PolicyState: map[string]*bugspb.BugManagementState_PolicyState{ 789 "exoneration-policy": { 790 IsActive: true, 791 LastActivationTime: timestamppb.New(opts.RunTimestamp), 792 ActivationNotified: true, 793 }, 794 "cls-rejected-policy": {}, 795 }, 796 }, 797 }, 798 { 799 Project: "chromeos", 800 RuleDefinition: `reason LIKE "want foofoofoo, got bar"`, 801 BugID: bugs.BugID{System: bugs.MonorailSystem, ID: "chromium/101"}, 802 SourceCluster: suggestedClusters[3].ClusterID, 803 IsActive: true, 804 IsManagingBug: true, 805 IsManagingBugPriority: true, 806 CreateUser: rules.LUCIAnalysisSystem, 807 LastAuditableUpdateUser: rules.LUCIAnalysisSystem, 808 BugManagementState: &bugspb.BugManagementState{ 809 RuleAssociationNotified: true, 810 PolicyState: map[string]*bugspb.BugManagementState_PolicyState{ 811 "exoneration-policy": { 812 IsActive: true, 813 LastActivationTime: timestamppb.New(opts.RunTimestamp), 814 ActivationNotified: true, 815 }, 816 "cls-rejected-policy": {}, 817 }, 818 }, 819 }, 820 } 821 822 // The offset of the first monorail rule in the rules slice. 823 // (Rules read by rules.Read...() are sorted by bug system and bug ID, 824 // so monorail always appears after Buganizer.) 825 const firstMonorailRuleIndex = 2 826 827 // Limit to one bug filed each time, so that 828 // we test change throttling. 829 opts.MaxBugsFiledPerRun = 1 830 831 // Verify one bug is filed at a time. 832 for i := 0; i < len(expectedRules); i++ { 833 // Act 834 err = UpdateBugsForProject(ctx, opts) 835 836 // Verify 837 So(err, ShouldBeNil) 838 So(verifyRulesResemble(ctx, expectedRules[:i+1]), ShouldBeNil) 839 } 840 841 // Further updates do nothing. 842 err = UpdateBugsForProject(ctx, opts) 843 844 So(err, ShouldBeNil) 845 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 846 847 rs, err := rules.ReadAllForTesting(span.Single(ctx)) 848 So(err, ShouldBeNil) 849 850 bugClusters := []*analysis.Cluster{ 851 makeBugCluster(rs[0].RuleID), 852 makeBugCluster(rs[1].RuleID), 853 makeBugCluster(rs[2].RuleID), 854 makeBugCluster(rs[3].RuleID), 855 } 856 857 Convey("if re-clustering in progress", func() { 858 analysisClient.clusters = append(suggestedClusters, bugClusters...) 859 860 Convey("negligable cluster metrics does not affect issue priority, status or active policies", func() { 861 // The policy should already be active from previous setup. 862 So(expectedRules[0].BugManagementState.PolicyState["exoneration-policy"].IsActive, ShouldBeTrue) 863 864 issue := buganizerStore.Issues[1] 865 originalPriority := issue.Issue.IssueState.Priority 866 originalStatus := issue.Issue.IssueState.Status 867 So(originalStatus, ShouldNotEqual, issuetracker.Issue_VERIFIED) 868 869 SetResidualMetrics(bugClusters[1], bugs.ClusterMetrics{ 870 metrics.CriticalFailuresExonerated.ID: bugs.MetricValues{}, 871 }) 872 873 // Act 874 err = UpdateBugsForProject(ctx, opts) 875 876 // Verify. 877 So(err, ShouldBeNil) 878 So(issue.Issue.IssueState.Priority, ShouldEqual, originalPriority) 879 So(issue.Issue.IssueState.Status, ShouldEqual, originalStatus) 880 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 881 }) 882 }) 883 Convey("with re-clustering complete", func() { 884 analysisClient.clusters = append(suggestedClusters, bugClusters...) 885 886 // Move residual impact from suggested clusters to new bug clusters. 887 bugClusters[0].MetricValues = suggestedClusters[0].MetricValues 888 bugClusters[1].MetricValues = suggestedClusters[1].MetricValues 889 bugClusters[2].MetricValues = suggestedClusters[2].MetricValues 890 bugClusters[3].MetricValues = suggestedClusters[3].MetricValues 891 892 // Clear residual impact on suggested clusters to inhibit 893 // further bug filing. 894 suggestedClusters[0].MetricValues = emptyMetricValues() 895 suggestedClusters[1].MetricValues = emptyMetricValues() 896 suggestedClusters[2].MetricValues = emptyMetricValues() 897 suggestedClusters[3].MetricValues = emptyMetricValues() 898 899 // Mark reclustering complete. 900 err := runs.SetRunsForTesting(ctx, []*runs.ReclusteringRun{ 901 runs.NewRun(0). 902 WithProject(project). 903 WithAlgorithmsVersion(algorithms.AlgorithmsVersion). 904 WithConfigVersion(projectCfg.LastUpdated.AsTime()). 905 WithRulesVersion(rs[3].PredicateLastUpdateTime). 906 WithCompletedProgress().Build(), 907 }) 908 So(err, ShouldBeNil) 909 910 progress, err := runs.ReadReclusteringProgress(ctx, project) 911 So(err, ShouldBeNil) 912 opts.ReclusteringProgress = progress 913 914 opts.RunTimestamp = opts.RunTimestamp.Add(10 * time.Minute) 915 916 Convey("policy activation", func() { 917 // Verify updates work, even when rules are in later batches. 918 opts.UpdateRuleBatchSize = 1 919 920 Convey("policy remains inactive if activation threshold unmet", func() { 921 // The policy should be inactive from previous setup. 922 expectedPolicyState := expectedRules[1].BugManagementState.PolicyState["cls-rejected-policy"] 923 So(expectedPolicyState.IsActive, ShouldBeFalse) 924 925 // Set metrics just below the policy activation threshold. 926 bugClusters[1].MetricValues[metrics.HumanClsFailedPresubmit.ID] = metrics.TimewiseCounts{ 927 OneDay: metrics.Counts{Residual: 9}, 928 ThreeDay: metrics.Counts{Residual: 9}, 929 SevenDay: metrics.Counts{Residual: 9}, 930 } 931 932 // Act 933 err = UpdateBugsForProject(ctx, opts) 934 935 // Verify policy activation unchanged. 936 So(err, ShouldBeNil) 937 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 938 }) 939 Convey("policy activates if activation threshold met", func() { 940 // The policy should be inactive from previous setup. 941 expectedPolicyState := expectedRules[1].BugManagementState.PolicyState["cls-rejected-policy"] 942 So(expectedPolicyState.IsActive, ShouldBeFalse) 943 So(expectedPolicyState.ActivationNotified, ShouldBeFalse) 944 945 // Update metrics so that policy should activate. 946 bugClusters[1].MetricValues[metrics.HumanClsFailedPresubmit.ID] = metrics.TimewiseCounts{ 947 OneDay: metrics.Counts{Residual: 0}, 948 ThreeDay: metrics.Counts{Residual: 0}, 949 SevenDay: metrics.Counts{Residual: 10}, 950 } 951 952 issue := buganizerStore.Issues[2] 953 So(expectedRules[1].BugID, ShouldResemble, bugs.BugID{System: bugs.BuganizerSystem, ID: "2"}) 954 existingCommentCount := len(issue.Comments) 955 956 // Act 957 err = UpdateBugsForProject(ctx, opts) 958 959 // Verify policy activates. 960 So(err, ShouldBeNil) 961 expectedPolicyState.IsActive = true 962 expectedPolicyState.LastActivationTime = timestamppb.New(opts.RunTimestamp) 963 expectedPolicyState.ActivationNotified = true 964 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 965 966 // Expect comments to be posted. 967 So(issue.Comments, ShouldHaveLength, existingCommentCount+2) 968 So(issue.Comments[2].Comment, ShouldContainSubstring, 969 "Why LUCI Analysis posted this comment: https://luci-analysis-test.appspot.com/help#policy-activated (Policy ID: cls-rejected-policy)") 970 So(issue.Comments[3].Comment, ShouldContainSubstring, 971 "The bug priority has been increased from P2 to P1.") 972 }) 973 Convey("policy remains active if deactivation threshold unmet", func() { 974 // The policy should already be active from previous setup. 975 expectedPolicyState := expectedRules[0].BugManagementState.PolicyState["exoneration-policy"] 976 So(expectedPolicyState.IsActive, ShouldBeTrue) 977 978 // Metrics still meet/exceed the deactivation threshold, so deactivation is inhibited. 979 bugClusters[0].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{ 980 OneDay: metrics.Counts{Residual: 10}, 981 ThreeDay: metrics.Counts{Residual: 10}, 982 SevenDay: metrics.Counts{Residual: 10}, 983 } 984 985 // Act 986 err = UpdateBugsForProject(ctx, opts) 987 988 // Verify policy activation should be unchanged. 989 So(err, ShouldBeNil) 990 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 991 }) 992 Convey("policy deactivates if deactivation threshold met", func() { 993 // The policy should already be active from previous setup. 994 expectedPolicyState := expectedRules[0].BugManagementState.PolicyState["exoneration-policy"] 995 So(expectedPolicyState.IsActive, ShouldBeTrue) 996 997 // Update metrics so that policy should de-activate. 998 bugClusters[0].MetricValues[metrics.CriticalFailuresExonerated.ID] = metrics.TimewiseCounts{ 999 OneDay: metrics.Counts{Residual: 9}, 1000 ThreeDay: metrics.Counts{Residual: 9}, 1001 SevenDay: metrics.Counts{Residual: 9}, 1002 } 1003 1004 // Act 1005 err = UpdateBugsForProject(ctx, opts) 1006 1007 // Verify policy deactivated. 1008 So(err, ShouldBeNil) 1009 expectedPolicyState.IsActive = false 1010 expectedPolicyState.LastDeactivationTime = timestamppb.New(opts.RunTimestamp) 1011 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1012 }) 1013 Convey("policy configuration changes are handled", func() { 1014 // Delete the existing policy named "exoneration-policy", and replace it with a new policy, 1015 // "new-exoneration-policy". Activation and de-activation criteria remain the same. 1016 projectCfg.BugManagement.Policies[0].Id = "new-exoneration-policy" 1017 1018 // Act 1019 err = UpdateBugsForProject(ctx, opts) 1020 1021 // Verify state for the old policy is deleted, and state for the new policy is added. 1022 So(err, ShouldBeNil) 1023 expectedRules[0].BugManagementState.PolicyState = map[string]*bugspb.BugManagementState_PolicyState{ 1024 "new-exoneration-policy": { 1025 // The new policy should activate, because the metrics justify its activation. 1026 IsActive: true, 1027 LastActivationTime: timestamppb.New(opts.RunTimestamp), 1028 }, 1029 "cls-rejected-policy": {}, 1030 } 1031 expectedRules[1].BugManagementState.PolicyState = map[string]*bugspb.BugManagementState_PolicyState{ 1032 "new-exoneration-policy": {}, 1033 "cls-rejected-policy": {}, 1034 } 1035 expectedRules[2].BugManagementState.PolicyState = map[string]*bugspb.BugManagementState_PolicyState{ 1036 "new-exoneration-policy": {}, 1037 "cls-rejected-policy": {}, 1038 } 1039 }) 1040 }) 1041 Convey("rule associated notification", func() { 1042 Convey("buganizer", func() { 1043 // Select a Buganizer issue. 1044 issue := buganizerStore.Issues[1] 1045 1046 // Get the corresponding rule, confirming we got the right one. 1047 rule := rs[0] 1048 So(rule.BugID.ID, ShouldEqual, fmt.Sprintf("%v", issue.Issue.IssueId)) 1049 1050 // Reset RuleAssociationNotified on the rule. 1051 rule.BugManagementState.RuleAssociationNotified = false 1052 So(rules.SetForTesting(ctx, rs), ShouldBeNil) 1053 1054 originalCommentCount := len(issue.Comments) 1055 1056 // Act 1057 err = UpdateBugsForProject(ctx, opts) 1058 1059 // Verify 1060 So(err, ShouldBeNil) 1061 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1062 So(issue.Comments, ShouldHaveLength, originalCommentCount+1) 1063 So(issue.Comments[originalCommentCount].Comment, ShouldEqual, 1064 "This bug has been associated with failures in LUCI Analysis."+ 1065 " To view failure examples or update the association, go to LUCI Analysis at: https://luci-analysis-test.appspot.com/p/chromeos/rules/"+rule.RuleID) 1066 1067 // Further runs should not lead to repeated posting of the comment. 1068 err = UpdateBugsForProject(ctx, opts) 1069 So(err, ShouldBeNil) 1070 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1071 So(issue.Comments, ShouldHaveLength, originalCommentCount+1) 1072 }) 1073 Convey("monorail", func() { 1074 // Select a Monorail issue. 1075 issue := monorailStore.Issues[0] 1076 1077 // Get the corresponding rule, and confirm we got the right one. 1078 const ruleIndex = firstMonorailRuleIndex 1079 rule := rs[ruleIndex] 1080 So(rule.BugID.ID, ShouldEqual, "chromium/100") 1081 So(issue.Issue.Name, ShouldEqual, "projects/chromium/issues/100") 1082 1083 // Reset RuleAssociationNotified on the rule. 1084 rule.BugManagementState.RuleAssociationNotified = false 1085 So(rules.SetForTesting(ctx, rs), ShouldBeNil) 1086 1087 originalCommentCount := len(issue.Comments) 1088 1089 // Act 1090 err = UpdateBugsForProject(ctx, opts) 1091 1092 // Verify 1093 So(err, ShouldBeNil) 1094 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1095 So(issue.Comments, ShouldHaveLength, originalCommentCount+1) 1096 So(issue.Comments[originalCommentCount].Content, ShouldContainSubstring, 1097 "This bug has been associated with failures in LUCI Analysis."+ 1098 " To view failure examples or update the association, go to LUCI Analysis at: https://luci-analysis-test.appspot.com/p/chromeos/rules/"+rule.RuleID) 1099 1100 // Further runs should not lead to repeated posting of the comment. 1101 err = UpdateBugsForProject(ctx, opts) 1102 So(err, ShouldBeNil) 1103 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1104 So(issue.Comments, ShouldHaveLength, originalCommentCount+1) 1105 }) 1106 }) 1107 Convey("priority updates and auto-closure", func() { 1108 Convey("buganizer", func() { 1109 // Select a Buganizer issue. 1110 issue := buganizerStore.Issues[1] 1111 originalPriority := issue.Issue.IssueState.Priority 1112 originalStatus := issue.Issue.IssueState.Status 1113 So(originalStatus, ShouldEqual, issuetracker.Issue_NEW) 1114 1115 // Get the corresponding rule, confirming we got the right one. 1116 rule := rs[0] 1117 So(rule.BugID.ID, ShouldEqual, fmt.Sprintf("%v", issue.Issue.IssueId)) 1118 1119 // Activate the cls-rejected-policy, which should raise the priority to P1. 1120 So(originalPriority, ShouldNotEqual, issuetracker.Issue_P1) 1121 SetResidualMetrics(bugClusters[0], bugs.ClusterMetrics{ 1122 metrics.CriticalFailuresExonerated.ID: bugs.MetricValues{OneDay: 100}, 1123 metrics.HumanClsFailedPresubmit.ID: bugs.MetricValues{SevenDay: 10}, 1124 }) 1125 1126 Convey("priority updates to reflect active policies", func() { 1127 expectedRules[0].BugManagementState.PolicyState["cls-rejected-policy"].IsActive = true 1128 expectedRules[0].BugManagementState.PolicyState["cls-rejected-policy"].LastActivationTime = timestamppb.New(opts.RunTimestamp) 1129 expectedRules[0].BugManagementState.PolicyState["cls-rejected-policy"].ActivationNotified = true 1130 So(originalPriority, ShouldNotEqual, issuetracker.Issue_P1) 1131 1132 // Act 1133 err = UpdateBugsForProject(ctx, opts) 1134 1135 // Verify 1136 So(err, ShouldBeNil) 1137 So(issue.Issue.IssueState.Priority, ShouldEqual, issuetracker.Issue_P1) 1138 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1139 }) 1140 Convey("disabling IsManagingBugPriority prevents priority updates", func() { 1141 expectedRules[0].BugManagementState.PolicyState["cls-rejected-policy"].IsActive = true 1142 expectedRules[0].BugManagementState.PolicyState["cls-rejected-policy"].LastActivationTime = timestamppb.New(opts.RunTimestamp) 1143 expectedRules[0].BugManagementState.PolicyState["cls-rejected-policy"].ActivationNotified = true 1144 1145 // Set IsManagingBugPriority to false on the rule. 1146 rule.IsManagingBugPriority = false 1147 So(rules.SetForTesting(ctx, rs), ShouldBeNil) 1148 1149 // Act 1150 err = UpdateBugsForProject(ctx, opts) 1151 1152 // Verify 1153 So(err, ShouldBeNil) 1154 1155 // Check that the bug priority and status has not changed. 1156 So(issue.Issue.IssueState.Status, ShouldEqual, originalStatus) 1157 So(issue.Issue.IssueState.Priority, ShouldEqual, originalPriority) 1158 1159 // Check the rules have not changed except for the IsManagingBugPriority change. 1160 expectedRules[0].IsManagingBugPriority = false 1161 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1162 }) 1163 Convey("manually setting a priority prevents bug updates", func() { 1164 expectedRules[0].BugManagementState.PolicyState["cls-rejected-policy"].IsActive = true 1165 expectedRules[0].BugManagementState.PolicyState["cls-rejected-policy"].LastActivationTime = timestamppb.New(opts.RunTimestamp) 1166 expectedRules[0].BugManagementState.PolicyState["cls-rejected-policy"].ActivationNotified = true 1167 1168 issue.IssueUpdates = append(issue.IssueUpdates, &issuetracker.IssueUpdate{ 1169 Author: &issuetracker.User{ 1170 EmailAddress: "testuser@google.com", 1171 }, 1172 Timestamp: timestamppb.New(clock.Now(ctx).Add(time.Minute * 4)), 1173 FieldUpdates: []*issuetracker.FieldUpdate{ 1174 { 1175 Field: "priority", 1176 }, 1177 }, 1178 }) 1179 1180 Convey("happy path", func() { 1181 // Act 1182 err = UpdateBugsForProject(ctx, opts) 1183 1184 // Verify 1185 So(err, ShouldBeNil) 1186 So(issue.Issue.IssueState.Status, ShouldEqual, originalStatus) 1187 So(issue.Issue.IssueState.Priority, ShouldEqual, originalPriority) 1188 expectedRules[0].IsManagingBugPriority = false 1189 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1190 1191 Convey("further updates leave no comments", func() { 1192 initialComments := len(issue.Comments) 1193 1194 // Act 1195 err = UpdateBugsForProject(ctx, opts) 1196 1197 // Verify 1198 So(err, ShouldBeNil) 1199 So(len(issue.Comments), ShouldEqual, initialComments) 1200 So(issue.Issue.IssueState.Status, ShouldEqual, originalStatus) 1201 So(issue.Issue.IssueState.Priority, ShouldEqual, originalPriority) 1202 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1203 }) 1204 }) 1205 1206 Convey("errors updating other bugs", func() { 1207 // Check we handle partial success correctly: 1208 // Even if there is an error updating another bug, if we comment on a bug 1209 // to say the user took manual priority control, we must commit 1210 // the rule update setting IsManagingBugPriority to false. Otherwise 1211 // we may get stuck in a loop where we comment on the bug every 1212 // time bug filing runs. 1213 1214 // Trigger a priority update for bug 2 in addition to the 1215 // manual priority update. 1216 SetResidualMetrics(bugClusters[1], bugs.ClusterMetrics{ 1217 metrics.CriticalFailuresExonerated.ID: bugs.MetricValues{OneDay: 100}, 1218 metrics.HumanClsFailedPresubmit.ID: bugs.MetricValues{SevenDay: 10}, 1219 }) 1220 1221 // But prevent LUCI Analysis from applying that priority update, due to an error. 1222 modifyError := errors.New("this issue may not be modified") 1223 buganizerStore.Issues[2].UpdateError = modifyError 1224 1225 // Act 1226 err = UpdateBugsForProject(ctx, opts) 1227 1228 // Verify 1229 1230 // The error modifying bug 2 is bubbled up. 1231 So(err, ShouldNotBeNil) 1232 So(errors.Is(err, modifyError), ShouldBeTrue) 1233 1234 // The policy on the bug 2 was activated, and we notified 1235 // bug 2 of the policy activation, even if we did 1236 // not succeed then updating its priority. 1237 // Furthermore, we record that we notified the policy 1238 // activation, so repeated notifications do not occur. 1239 expectedRules[1].BugManagementState.PolicyState["cls-rejected-policy"].IsActive = true 1240 expectedRules[1].BugManagementState.PolicyState["cls-rejected-policy"].LastActivationTime = timestamppb.New(opts.RunTimestamp) 1241 expectedRules[1].BugManagementState.PolicyState["cls-rejected-policy"].ActivationNotified = true 1242 1243 otherIssue := buganizerStore.Issues[2] 1244 So(otherIssue.Comments[len(otherIssue.Comments)-1].Comment, ShouldContainSubstring, 1245 "Why LUCI Analysis posted this comment: https://luci-analysis-test.appspot.com/help#policy-activated (Policy ID: cls-rejected-policy)") 1246 1247 // Despite the issue with bug 2, bug 1 was commented on updated and 1248 // IsManagingBugPriority was set to false. 1249 expectedRules[0].IsManagingBugPriority = false 1250 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1251 So(issue.Comments[len(issue.Comments)-1].Comment, ShouldContainSubstring, 1252 "The bug priority has been manually set.") 1253 So(issue.Issue.IssueState.Status, ShouldEqual, originalStatus) 1254 So(issue.Issue.IssueState.Priority, ShouldEqual, originalPriority) 1255 }) 1256 }) 1257 Convey("if all policies de-activate, bug is auto-closed", func() { 1258 SetResidualMetrics(bugClusters[0], bugs.ClusterMetrics{ 1259 metrics.CriticalFailuresExonerated.ID: bugs.MetricValues{OneDay: 9}, 1260 metrics.HumanClsFailedPresubmit.ID: bugs.MetricValues{}, 1261 }) 1262 1263 // Act 1264 err = UpdateBugsForProject(ctx, opts) 1265 1266 // Verify 1267 So(err, ShouldBeNil) 1268 expectedRules[0].BugManagementState.PolicyState["exoneration-policy"].IsActive = false 1269 expectedRules[0].BugManagementState.PolicyState["exoneration-policy"].LastDeactivationTime = timestamppb.New(opts.RunTimestamp) 1270 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1271 So(issue.Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_VERIFIED) 1272 So(issue.Issue.IssueState.Priority, ShouldEqual, issuetracker.Issue_P2) 1273 }) 1274 Convey("disabling IsManagingBug prevents bug closure", func() { 1275 SetResidualMetrics(bugClusters[0], bugs.ClusterMetrics{ 1276 metrics.CriticalFailuresExonerated.ID: bugs.MetricValues{OneDay: 9}, 1277 metrics.HumanClsFailedPresubmit.ID: bugs.MetricValues{}, 1278 }) 1279 expectedRules[0].BugManagementState.PolicyState["exoneration-policy"].IsActive = false 1280 expectedRules[0].BugManagementState.PolicyState["exoneration-policy"].LastDeactivationTime = timestamppb.New(opts.RunTimestamp) 1281 1282 // Set IsManagingBug to false on the rule. 1283 rule.IsManagingBug = false 1284 So(rules.SetForTesting(ctx, rs), ShouldBeNil) 1285 1286 // Act 1287 err = UpdateBugsForProject(ctx, opts) 1288 1289 // Verify 1290 So(err, ShouldBeNil) 1291 1292 // Check the rules have not changed except for the IsManagingBug change. 1293 expectedRules[0].IsManagingBug = false 1294 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1295 1296 // Check that the bug priority and status has not changed. 1297 So(issue.Issue.IssueState.Status, ShouldEqual, originalStatus) 1298 So(issue.Issue.IssueState.Priority, ShouldEqual, originalPriority) 1299 }) 1300 Convey("cluster disappearing closes issue", func() { 1301 // Drop the corresponding bug cluster. This is consistent with 1302 // no more failures in the cluster occuring. 1303 bugClusters = bugClusters[1:] 1304 analysisClient.clusters = append(suggestedClusters, bugClusters...) 1305 1306 // Act 1307 err = UpdateBugsForProject(ctx, opts) 1308 1309 // Verify 1310 So(err, ShouldBeNil) 1311 expectedRules[0].BugManagementState.PolicyState["exoneration-policy"].IsActive = false 1312 expectedRules[0].BugManagementState.PolicyState["exoneration-policy"].LastDeactivationTime = timestamppb.New(opts.RunTimestamp) 1313 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1314 So(issue.Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_VERIFIED) 1315 1316 Convey("rule automatically archived after 30 days", func() { 1317 tc.Add(time.Hour * 24 * 30) 1318 1319 // Act 1320 err = UpdateBugsForProject(ctx, opts) 1321 1322 // Verify 1323 So(err, ShouldBeNil) 1324 expectedRules[0].IsActive = false 1325 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1326 So(issue.Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_VERIFIED) 1327 }) 1328 }) 1329 Convey("if all policies are removed, bug is auto-closed", func() { 1330 projectCfg.BugManagement.Policies = nil 1331 err := config.SetTestProjectConfig(ctx, projectsCfg) 1332 So(err, ShouldBeNil) 1333 1334 for _, expectedRule := range expectedRules { 1335 expectedRule.BugManagementState.PolicyState = nil 1336 } 1337 1338 // Act 1339 err = UpdateBugsForProject(ctx, opts) 1340 1341 // Verify 1342 So(err, ShouldBeNil) 1343 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1344 So(issue.Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_VERIFIED) 1345 So(issue.Issue.IssueState.Priority, ShouldEqual, issuetracker.Issue_P2) 1346 }) 1347 }) 1348 Convey("monorail", func() { 1349 // Select a Monorail issue. 1350 issue := monorailStore.Issues[0] 1351 originalPriority := monorail.ChromiumTestIssuePriority(issue.Issue) 1352 originalStatus := issue.Issue.Status.Status 1353 So(originalStatus, ShouldEqual, monorail.UntriagedStatus) 1354 1355 // Get the corresponding rule, and confirm we got the right one. 1356 const ruleIndex = firstMonorailRuleIndex 1357 rule := rs[ruleIndex] 1358 So(rule.BugID.ID, ShouldEqual, "chromium/100") 1359 So(issue.Issue.Name, ShouldEqual, "projects/chromium/issues/100") 1360 1361 // Activate the cls-rejected-policy, which should raise the priority to P1. 1362 So(originalPriority, ShouldNotEqual, issuetracker.Issue_P1) 1363 SetResidualMetrics(bugClusters[firstMonorailRuleIndex], bugs.ClusterMetrics{ 1364 metrics.CriticalFailuresExonerated.ID: bugs.MetricValues{OneDay: 100}, 1365 metrics.HumanClsFailedPresubmit.ID: bugs.MetricValues{SevenDay: 10}, 1366 }) 1367 1368 Convey("priority updates to reflect active policies", func() { 1369 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["cls-rejected-policy"].IsActive = true 1370 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["cls-rejected-policy"].LastActivationTime = timestamppb.New(opts.RunTimestamp) 1371 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["cls-rejected-policy"].ActivationNotified = true 1372 So(originalPriority, ShouldNotEqual, "1") 1373 1374 // Act 1375 err = UpdateBugsForProject(ctx, opts) 1376 1377 // Verify 1378 So(err, ShouldBeNil) 1379 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1380 So(issue.Issue.Status.Status, ShouldEqual, originalStatus) 1381 So(monorail.ChromiumTestIssuePriority(issue.Issue), ShouldEqual, "1") 1382 }) 1383 Convey("disabling IsManagingBugPriority prevents priority updates", func() { 1384 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["cls-rejected-policy"].IsActive = true 1385 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["cls-rejected-policy"].LastActivationTime = timestamppb.New(opts.RunTimestamp) 1386 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["cls-rejected-policy"].ActivationNotified = true 1387 1388 // Set IsManagingBugPriority to false on the rule. 1389 rule.IsManagingBugPriority = false 1390 So(rules.SetForTesting(ctx, rs), ShouldBeNil) 1391 1392 // Act 1393 err = UpdateBugsForProject(ctx, opts) 1394 1395 // Verify 1396 So(err, ShouldBeNil) 1397 1398 // Check the rules have not changed except for the IsManagingBugPriority change. 1399 expectedRules[firstMonorailRuleIndex].IsManagingBugPriority = false 1400 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1401 1402 // Check that the bug priority and status has not changed. 1403 So(issue.Issue.Status.Status, ShouldEqual, originalStatus) 1404 So(monorail.ChromiumTestIssuePriority(issue.Issue), ShouldEqual, originalPriority) 1405 }) 1406 Convey("manually setting a priority prevents bug updates", func() { 1407 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["cls-rejected-policy"].IsActive = true 1408 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["cls-rejected-policy"].LastActivationTime = timestamppb.New(opts.RunTimestamp) 1409 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["cls-rejected-policy"].ActivationNotified = true 1410 1411 // Create a fake client to interact with monorail as a user. 1412 userClient, err := monorail.NewClient(monorail.UseFakeIssuesClient(ctx, monorailStore, "user@google.com"), "myhost") 1413 So(err, ShouldBeNil) 1414 1415 // Set priority to P0 manually. 1416 updateRequest := &mpb.ModifyIssuesRequest{ 1417 Deltas: []*mpb.IssueDelta{ 1418 { 1419 Issue: &mpb.Issue{ 1420 Name: issue.Issue.Name, 1421 FieldValues: []*mpb.FieldValue{ 1422 { 1423 Field: "projects/chromium/fieldDefs/11", 1424 Value: "0", 1425 }, 1426 }, 1427 }, 1428 UpdateMask: &fieldmaskpb.FieldMask{ 1429 Paths: []string{"field_values"}, 1430 }, 1431 }, 1432 }, 1433 CommentContent: "User comment.", 1434 } 1435 err = userClient.ModifyIssues(ctx, updateRequest) 1436 So(err, ShouldBeNil) 1437 1438 Convey("happy path", func() { 1439 // Act 1440 err = UpdateBugsForProject(ctx, opts) 1441 1442 // Verify 1443 So(err, ShouldBeNil) 1444 1445 // Expect IsManagingBugPriority to get set to false. 1446 expectedRules[firstMonorailRuleIndex].IsManagingBugPriority = false 1447 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1448 1449 // Expect a comment on the bug. 1450 So(issue.Comments[len(issue.Comments)-1].Content, ShouldContainSubstring, 1451 "The bug priority has been manually set.") 1452 So(issue.Issue.Status.Status, ShouldEqual, originalStatus) 1453 So(monorail.ChromiumTestIssuePriority(issue.Issue), ShouldEqual, "0") 1454 1455 Convey("further updates leave no comments", func() { 1456 initialComments := len(issue.Comments) 1457 1458 // Act 1459 err = UpdateBugsForProject(ctx, opts) 1460 1461 // Verify 1462 So(err, ShouldBeNil) 1463 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1464 So(len(issue.Comments), ShouldEqual, initialComments) 1465 So(issue.Issue.Status.Status, ShouldEqual, originalStatus) 1466 So(monorail.ChromiumTestIssuePriority(issue.Issue), ShouldEqual, "0") 1467 }) 1468 }) 1469 Convey("errors updating other bugs", func() { 1470 // Check we handle partial success correctly: 1471 // Even if there is an error updating another bug, if we comment on a bug 1472 // to say the user took manual priority control, we must commit 1473 // the rule update setting IsManagingBugPriority to false. Otherwise 1474 // we may get stuck in a loop where we comment on the bug every 1475 // time bug filing runs. 1476 1477 // Trigger a priority update for another monorail bug in addition to the 1478 // manual priority update. 1479 SetResidualMetrics(bugClusters[firstMonorailRuleIndex+1], bugs.ClusterMetrics{ 1480 metrics.CriticalFailuresExonerated.ID: bugs.MetricValues{OneDay: 100}, 1481 metrics.HumanClsFailedPresubmit.ID: bugs.MetricValues{SevenDay: 10}, 1482 }) 1483 expectedRules[firstMonorailRuleIndex+1].BugManagementState.PolicyState["cls-rejected-policy"].IsActive = true 1484 expectedRules[firstMonorailRuleIndex+1].BugManagementState.PolicyState["cls-rejected-policy"].LastActivationTime = timestamppb.New(opts.RunTimestamp) 1485 1486 // But prevent LUCI Analysis from applying that priority update, due to an error. 1487 modifyError := errors.New("this issue may not be modified") 1488 monorailStore.Issues[1].UpdateError = modifyError 1489 1490 // Act 1491 err = UpdateBugsForProject(ctx, opts) 1492 1493 // Verify 1494 So(err, ShouldNotBeNil) 1495 1496 // The error modifying the other bug is bubbled up. 1497 So(errors.Is(err, modifyError), ShouldBeTrue) 1498 1499 // Nonetheless, our bug was commented on updated and 1500 // IsManagingBugPriority was set to false. 1501 expectedRules[firstMonorailRuleIndex].IsManagingBugPriority = false 1502 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1503 So(issue.Comments[len(issue.Comments)-1].Content, ShouldContainSubstring, 1504 "The bug priority has been manually set.") 1505 So(issue.Issue.Status.Status, ShouldEqual, originalStatus) 1506 So(monorail.ChromiumTestIssuePriority(issue.Issue), ShouldEqual, "0") 1507 }) 1508 }) 1509 Convey("if all policies de-activate, bug is auto-closed", func() { 1510 SetResidualMetrics(bugClusters[firstMonorailRuleIndex], bugs.ClusterMetrics{ 1511 metrics.CriticalFailuresExonerated.ID: bugs.MetricValues{OneDay: 9}, 1512 metrics.HumanClsFailedPresubmit.ID: bugs.MetricValues{}, 1513 }) 1514 1515 // Act 1516 err = UpdateBugsForProject(ctx, opts) 1517 1518 // Verify 1519 So(err, ShouldBeNil) 1520 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["exoneration-policy"].IsActive = false 1521 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["exoneration-policy"].LastDeactivationTime = timestamppb.New(opts.RunTimestamp) 1522 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1523 So(issue.Issue.Status.Status, ShouldEqual, monorail.VerifiedStatus) 1524 So(monorail.ChromiumTestIssuePriority(issue.Issue), ShouldEqual, originalPriority) 1525 }) 1526 Convey("disabling IsManagingBug prevents bug closure", func() { 1527 SetResidualMetrics(bugClusters[firstMonorailRuleIndex], bugs.ClusterMetrics{ 1528 metrics.CriticalFailuresExonerated.ID: bugs.MetricValues{OneDay: 9}, 1529 metrics.HumanClsFailedPresubmit.ID: bugs.MetricValues{}, 1530 }) 1531 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["exoneration-policy"].IsActive = false 1532 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["exoneration-policy"].LastDeactivationTime = timestamppb.New(opts.RunTimestamp) 1533 1534 // Set IsManagingBug to false on the rule. 1535 rule.IsManagingBug = false 1536 So(rules.SetForTesting(ctx, rs), ShouldBeNil) 1537 1538 // Act 1539 err = UpdateBugsForProject(ctx, opts) 1540 1541 // Verify 1542 So(err, ShouldBeNil) 1543 1544 // Check the rules have not changed except for the IsManagingBug change. 1545 expectedRules[firstMonorailRuleIndex].IsManagingBug = false 1546 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1547 1548 // Check that the bug priority and status has not changed. 1549 So(issue.Issue.Status.Status, ShouldEqual, originalStatus) 1550 So(monorail.ChromiumTestIssuePriority(issue.Issue), ShouldEqual, originalPriority) 1551 }) 1552 Convey("cluster disappearing closes issue", func() { 1553 // Drop the corresponding bug cluster. This is consistent with 1554 // no more failures in the cluster occuring. 1555 newBugClusters := []*analysis.Cluster{} 1556 newBugClusters = append(newBugClusters, bugClusters[:firstMonorailRuleIndex]...) 1557 newBugClusters = append(newBugClusters, bugClusters[firstMonorailRuleIndex+1:]...) 1558 analysisClient.clusters = append(suggestedClusters, newBugClusters...) 1559 1560 // Act 1561 err = UpdateBugsForProject(ctx, opts) 1562 1563 // Verify 1564 So(err, ShouldBeNil) 1565 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["exoneration-policy"].IsActive = false 1566 expectedRules[firstMonorailRuleIndex].BugManagementState.PolicyState["exoneration-policy"].LastDeactivationTime = timestamppb.New(opts.RunTimestamp) 1567 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1568 So(issue.Issue.Status.Status, ShouldEqual, monorail.VerifiedStatus) 1569 So(monorail.ChromiumTestIssuePriority(issue.Issue), ShouldEqual, originalPriority) 1570 1571 Convey("rule automatically archived after 30 days", func() { 1572 tc.Add(time.Hour * 24 * 30) 1573 1574 // Act 1575 err = UpdateBugsForProject(ctx, opts) 1576 1577 // Verify 1578 So(err, ShouldBeNil) 1579 expectedRules[firstMonorailRuleIndex].IsActive = false 1580 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1581 So(issue.Issue.Status.Status, ShouldEqual, monorail.VerifiedStatus) 1582 So(monorail.ChromiumTestIssuePriority(issue.Issue), ShouldEqual, originalPriority) 1583 }) 1584 }) 1585 Convey("if all policies are removed, bug is auto-closed", func() { 1586 projectCfg.BugManagement.Policies = nil 1587 err := config.SetTestProjectConfig(ctx, projectsCfg) 1588 So(err, ShouldBeNil) 1589 1590 for _, expectedRule := range expectedRules { 1591 expectedRule.BugManagementState.PolicyState = nil 1592 } 1593 1594 // Act 1595 err = UpdateBugsForProject(ctx, opts) 1596 1597 // Verify 1598 So(err, ShouldBeNil) 1599 So(issue.Issue.Status.Status, ShouldEqual, monorail.VerifiedStatus) 1600 So(monorail.ChromiumTestIssuePriority(issue.Issue), ShouldEqual, originalPriority) 1601 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1602 }) 1603 }) 1604 }) 1605 Convey("duplicate handling", func() { 1606 Convey("buganizer to buganizer", func() { 1607 // Setup 1608 issueOne := buganizerStore.Issues[1] 1609 issueTwo := buganizerStore.Issues[2] 1610 issueOne.Issue.IssueState.Status = issuetracker.Issue_DUPLICATE 1611 issueOne.Issue.IssueState.CanonicalIssueId = issueTwo.Issue.IssueId 1612 1613 issueOneOriginalCommentCount := len(issueOne.Comments) 1614 issueTwoOriginalCommentCount := len(issueTwo.Comments) 1615 1616 // Ensure rule association and policy activation notified, so we 1617 // can confirm whether notifications are correctly reset. 1618 rs[0].BugManagementState.RuleAssociationNotified = true 1619 for _, policyState := range rs[0].BugManagementState.PolicyState { 1620 policyState.ActivationNotified = true 1621 } 1622 So(rules.SetForTesting(ctx, rs), ShouldBeNil) 1623 1624 expectedRules[0].BugManagementState.RuleAssociationNotified = true 1625 for _, policyState := range expectedRules[0].BugManagementState.PolicyState { 1626 policyState.ActivationNotified = true 1627 } 1628 1629 Convey("happy path", func() { 1630 // Act 1631 err = UpdateBugsForProject(ctx, opts) 1632 1633 // Verify 1634 So(err, ShouldBeNil) 1635 expectedRules[0].IsActive = false 1636 expectedRules[1].RuleDefinition = "reason LIKE \"want foo, got bar\" OR\ntest = \"testname-0\"" 1637 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1638 1639 So(issueOne.Comments, ShouldHaveLength, issueOneOriginalCommentCount+1) 1640 So(issueOne.Comments[issueOneOriginalCommentCount].Comment, ShouldContainSubstring, "LUCI Analysis has merged the failure association rule for this bug into the rule for the canonical bug.") 1641 So(issueOne.Comments[issueOneOriginalCommentCount].Comment, ShouldContainSubstring, expectedRules[2].RuleID) 1642 1643 So(issueTwo.Comments, ShouldHaveLength, issueTwoOriginalCommentCount) 1644 }) 1645 Convey("happy path, with comments for duplicate bugs disabled", func() { 1646 // Setup 1647 projectCfg.BugManagement.DisableDuplicateBugComments = true 1648 projectsCfg := map[string]*configpb.ProjectConfig{ 1649 project: projectCfg, 1650 } 1651 err = config.SetTestProjectConfig(ctx, projectsCfg) 1652 So(err, ShouldBeNil) 1653 1654 // Act 1655 err = UpdateBugsForProject(ctx, opts) 1656 1657 // Verify 1658 So(err, ShouldBeNil) 1659 expectedRules[0].IsActive = false 1660 expectedRules[1].RuleDefinition = "reason LIKE \"want foo, got bar\" OR\ntest = \"testname-0\"" 1661 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1662 1663 So(issueOne.Comments, ShouldHaveLength, issueOneOriginalCommentCount) 1664 So(issueTwo.Comments, ShouldHaveLength, issueTwoOriginalCommentCount) 1665 }) 1666 Convey("happy path, bug marked as duplicate of bug without a rule in this project", func() { 1667 // Setup 1668 issueOne.Issue.IssueState.Status = issuetracker.Issue_DUPLICATE 1669 issueOne.Issue.IssueState.CanonicalIssueId = 1234 1670 1671 buganizerStore.StoreIssue(ctx, buganizer.NewFakeIssue(1234)) 1672 1673 extraRule := &rules.Entry{ 1674 Project: "otherproject", 1675 RuleDefinition: `reason LIKE "blah"`, 1676 RuleID: "1234567890abcdef1234567890abcdef", 1677 BugID: bugs.BugID{System: bugs.BuganizerSystem, ID: "1234"}, 1678 IsActive: true, 1679 IsManagingBug: true, 1680 IsManagingBugPriority: true, 1681 BugManagementState: &bugspb.BugManagementState{}, 1682 CreateUser: "user@chromium.org", 1683 LastAuditableUpdateUser: "user@chromium.org", 1684 } 1685 _, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 1686 ms, err := rules.Create(extraRule, "user@chromium.org") 1687 if err != nil { 1688 return err 1689 } 1690 span.BufferWrite(ctx, ms) 1691 return nil 1692 }) 1693 So(err, ShouldBeNil) 1694 1695 // Act 1696 err = UpdateBugsForProject(ctx, opts) 1697 1698 // Verify 1699 So(err, ShouldBeNil) 1700 expectedRules[0].BugID = bugs.BugID{System: bugs.BuganizerSystem, ID: "1234"} 1701 // Should reset to false as we didn't create the destination bug. 1702 expectedRules[0].IsManagingBug = false 1703 // Should reset because of the change in associated bug. 1704 expectedRules[0].BugManagementState.RuleAssociationNotified = false 1705 for _, policyState := range expectedRules[0].BugManagementState.PolicyState { 1706 policyState.ActivationNotified = false 1707 } 1708 expectedRules = append(expectedRules, extraRule) 1709 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1710 1711 So(issueOne.Comments, ShouldHaveLength, issueOneOriginalCommentCount+1) 1712 So(issueOne.Comments[issueOneOriginalCommentCount].Comment, ShouldContainSubstring, "LUCI Analysis has merged the failure association rule for this bug into the rule for the canonical bug.") 1713 So(issueOne.Comments[issueOneOriginalCommentCount].Comment, ShouldContainSubstring, expectedRules[0].RuleID) 1714 }) 1715 Convey("error cases", func() { 1716 Convey("bugs are in a duplicate bug cycle", func() { 1717 // Note that this is a simple cycle with only two bugs. 1718 // The implementation allows for larger cycles, however. 1719 issueTwo.Issue.IssueState.Status = issuetracker.Issue_DUPLICATE 1720 issueTwo.Issue.IssueState.CanonicalIssueId = issueOne.Issue.IssueId 1721 1722 // Act 1723 err = UpdateBugsForProject(ctx, opts) 1724 1725 // Verify 1726 So(err, ShouldBeNil) 1727 1728 // Issue one kicked out of duplicate status. 1729 So(issueOne.Issue.IssueState.Status, ShouldNotEqual, issuetracker.Issue_DUPLICATE) 1730 1731 // As the cycle is now broken, issue two is merged into 1732 // issue one. 1733 expectedRules[0].RuleDefinition = "reason LIKE \"want foo, got bar\" OR\ntest = \"testname-0\"" 1734 expectedRules[1].IsActive = false 1735 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1736 1737 So(issueOne.Comments, ShouldHaveLength, issueOneOriginalCommentCount+1) 1738 So(issueOne.Comments[issueOneOriginalCommentCount].Comment, ShouldContainSubstring, "a cycle was detected in the bug merged-into graph") 1739 }) 1740 Convey("merged rule would be too long", func() { 1741 // Setup 1742 // Make one of the rules we will be merging very close 1743 // to the rule length limit. 1744 longRule := fmt.Sprintf("test = \"%s\"", strings.Repeat("a", rules.MaxRuleDefinitionLength-10)) 1745 1746 _, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 1747 issueOneRule, err := rules.ReadByBug(ctx, bugs.BugID{System: bugs.BuganizerSystem, ID: "1"}) 1748 if err != nil { 1749 return err 1750 } 1751 issueOneRule[0].RuleDefinition = longRule 1752 1753 ms, err := rules.Update(issueOneRule[0], rules.UpdateOptions{ 1754 IsAuditableUpdate: true, 1755 PredicateUpdated: true, 1756 }, rules.LUCIAnalysisSystem) 1757 if err != nil { 1758 return err 1759 } 1760 span.BufferWrite(ctx, ms) 1761 return nil 1762 }) 1763 So(err, ShouldBeNil) 1764 1765 // Act 1766 err = UpdateBugsForProject(ctx, opts) 1767 1768 // Verify 1769 So(err, ShouldBeNil) 1770 1771 // Rules should not have changed (except for the update we made). 1772 expectedRules[0].RuleDefinition = longRule 1773 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1774 1775 // Issue one kicked out of duplicate status. 1776 So(issueOne.Issue.IssueState.Status, ShouldNotEqual, issuetracker.Issue_DUPLICATE) 1777 1778 // Comment should appear on the bug. 1779 So(issueOne.Comments, ShouldHaveLength, issueOneOriginalCommentCount+1) 1780 So(issueOne.Comments[issueOneOriginalCommentCount].Comment, ShouldContainSubstring, "the merged failure association rule would be too long") 1781 }) 1782 Convey("bug marked as duplicate of bug we cannot access", func() { 1783 issueTwo.ShouldReturnAccessPermissionError = true 1784 1785 // Act 1786 err = UpdateBugsForProject(ctx, opts) 1787 1788 // Verify issue one kicked out of duplicate status. 1789 So(err, ShouldBeNil) 1790 So(issueOne.Issue.IssueState.Status, ShouldNotEqual, issuetracker.Issue_DUPLICATE) 1791 So(issueOne.Comments, ShouldHaveLength, issueOneOriginalCommentCount+1) 1792 So(issueOne.Comments[issueOneOriginalCommentCount].Comment, ShouldContainSubstring, "LUCI Analysis cannot merge the association rule for this bug into the rule") 1793 }) 1794 Convey("failed to handle duplicate bug - bug has an assignee", func() { 1795 issueTwo.ShouldReturnAccessPermissionError = true 1796 1797 // Has an assignee. 1798 issueOne.Issue.IssueState.Assignee = &issuetracker.User{ 1799 EmailAddress: "user@google.com", 1800 } 1801 // Act 1802 err = UpdateBugsForProject(ctx, opts) 1803 1804 // Verify issue is put back to assigned status, instead of New. 1805 So(err, ShouldBeNil) 1806 So(issueOne.Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_ASSIGNED) 1807 }) 1808 Convey("failed to handle duplicate bug - bug has no assignee", func() { 1809 issueTwo.ShouldReturnAccessPermissionError = true 1810 1811 // Has no assignee. 1812 issueOne.Issue.IssueState.Assignee = nil 1813 1814 // Act 1815 err = UpdateBugsForProject(ctx, opts) 1816 1817 // Verify issue is put back to New status, instead of Assigned. 1818 So(err, ShouldBeNil) 1819 So(issueOne.Issue.IssueState.Status, ShouldEqual, issuetracker.Issue_NEW) 1820 }) 1821 }) 1822 }) 1823 Convey("monorail to monorail", func() { 1824 // Note that much of the duplicate handling logic, including error 1825 // handling, is shared code and not implemented in the bug system-specific 1826 // bug manager. As such, we do not re-test all of the error cases above, 1827 // only select cases to confirm the integration is correct. 1828 1829 issueOne := monorailStore.Issues[0] 1830 issueTwo := monorailStore.Issues[1] 1831 1832 issueOne.Issue.Status.Status = monorail.DuplicateStatus 1833 issueOne.Issue.MergedIntoIssueRef = &mpb.IssueRef{ 1834 Issue: issueTwo.Issue.Name, 1835 } 1836 1837 issueOneRule := expectedRules[firstMonorailRuleIndex] 1838 issueTwoRule := expectedRules[firstMonorailRuleIndex+1] 1839 1840 issueOneOriginalCommentCount := len(issueOne.Comments) 1841 issueTwoOriginalCommentCount := len(issueTwo.Comments) 1842 1843 Convey("happy path", func() { 1844 // Act 1845 err = UpdateBugsForProject(ctx, opts) 1846 1847 // Verify 1848 So(err, ShouldBeNil) 1849 issueOneRule.IsActive = false 1850 issueTwoRule.RuleDefinition = "reason LIKE \"want foofoo, got bar\" OR\nreason LIKE \"want foofoofoo, got bar\"" 1851 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1852 1853 So(issueOne.Comments, ShouldHaveLength, issueOneOriginalCommentCount+1) 1854 So(issueOne.Comments[issueOneOriginalCommentCount].Content, ShouldContainSubstring, "LUCI Analysis has merged the failure association rule for this bug into the rule for the canonical bug.") 1855 So(issueOne.Comments[issueOneOriginalCommentCount].Content, ShouldContainSubstring, issueOneRule.RuleID) 1856 1857 So(issueTwo.Comments, ShouldHaveLength, issueTwoOriginalCommentCount) 1858 }) 1859 Convey("error case", func() { 1860 // Note that this is a simple cycle with only two bugs. 1861 // The implementation allows for larger cycles, however. 1862 issueTwo.Issue.Status.Status = monorail.DuplicateStatus 1863 issueTwo.Issue.MergedIntoIssueRef = &mpb.IssueRef{ 1864 Issue: issueOne.Issue.Name, 1865 } 1866 1867 // Act 1868 err = UpdateBugsForProject(ctx, opts) 1869 1870 // Verify 1871 So(err, ShouldBeNil) 1872 1873 // Issue one kicked out of duplicate status. 1874 So(issueOne.Issue.Status.Status, ShouldNotEqual, monorail.DuplicateStatus) 1875 1876 // As the cycle is now broken, issue two is merged into 1877 // issue one. 1878 issueOneRule.RuleDefinition = "reason LIKE \"want foofoo, got bar\" OR\nreason LIKE \"want foofoofoo, got bar\"" 1879 issueTwoRule.IsActive = false 1880 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1881 1882 So(issueOne.Comments, ShouldHaveLength, issueOneOriginalCommentCount+1) 1883 So(issueOne.Comments[issueOneOriginalCommentCount].Content, ShouldContainSubstring, "a cycle was detected in the bug merged-into graph") 1884 }) 1885 }) 1886 Convey("monorail to buganizer", func() { 1887 issueOne := monorailStore.Issues[0] 1888 issueTwo := buganizerStore.Issues[1] 1889 1890 issueOne.Issue.Status.Status = monorail.DuplicateStatus 1891 issueOne.Issue.MergedIntoIssueRef = &mpb.IssueRef{ 1892 ExtIdentifier: fmt.Sprintf("b/%v", issueTwo.Issue.IssueId), 1893 } 1894 1895 issueOneRule := expectedRules[firstMonorailRuleIndex] 1896 issueTwoRule := expectedRules[0] 1897 1898 issueOneOriginalCommentCount := len(issueOne.Comments) 1899 issueTwoOriginalCommentCount := len(issueTwo.Comments) 1900 1901 Convey("happy path", func() { 1902 // Act 1903 err = UpdateBugsForProject(ctx, opts) 1904 1905 // Verify 1906 So(err, ShouldBeNil) 1907 issueOneRule.IsActive = false 1908 issueTwoRule.RuleDefinition = "reason LIKE \"want foofoo, got bar\" OR\ntest = \"testname-0\"" 1909 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1910 1911 So(issueOne.Comments, ShouldHaveLength, issueOneOriginalCommentCount+1) 1912 So(issueOne.Comments[issueOneOriginalCommentCount].Content, ShouldContainSubstring, "LUCI Analysis has merged the failure association rule for this bug into the rule for the canonical bug.") 1913 So(issueOne.Comments[issueOneOriginalCommentCount].Content, ShouldContainSubstring, issueOneRule.RuleID) 1914 1915 So(issueTwo.Comments, ShouldHaveLength, issueTwoOriginalCommentCount) 1916 }) 1917 }) 1918 }) 1919 Convey("bug marked as archived should archive rule", func() { 1920 Convey("buganizer", func() { 1921 issueOne := buganizerStore.Issues[1].Issue 1922 issueOne.IsArchived = true 1923 1924 // Act 1925 err = UpdateBugsForProject(ctx, opts) 1926 So(err, ShouldBeNil) 1927 1928 // Verify 1929 expectedRules[0].IsActive = false 1930 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1931 }) 1932 Convey("monorail", func() { 1933 issue := monorailStore.Issues[0] 1934 issue.Issue.Status.Status = "Archived" 1935 1936 // Act 1937 err = UpdateBugsForProject(ctx, opts) 1938 So(err, ShouldBeNil) 1939 1940 // Verify 1941 expectedRules[2].IsActive = false 1942 So(verifyRulesResemble(ctx, expectedRules), ShouldBeNil) 1943 }) 1944 }) 1945 }) 1946 }) 1947 }) 1948 } 1949 1950 func createProjectConfig() *configpb.ProjectConfig { 1951 return &configpb.ProjectConfig{ 1952 BugManagement: &configpb.BugManagement{ 1953 DefaultBugSystem: configpb.BugSystem_BUGANIZER, 1954 Buganizer: buganizer.ChromeOSTestConfig(), 1955 Monorail: monorail.ChromiumTestConfig(), 1956 Policies: []*configpb.BugManagementPolicy{ 1957 createExonerationPolicy(), 1958 createCLsRejectedPolicy(), 1959 }, 1960 }, 1961 LastUpdated: timestamppb.New(time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC)), 1962 } 1963 } 1964 1965 func createExonerationPolicy() *configpb.BugManagementPolicy { 1966 return &configpb.BugManagementPolicy{ 1967 Id: "exoneration-policy", 1968 Owners: []string{"username@google.com"}, 1969 HumanReadableName: "test variant(s) are being exonerated in presubmit", 1970 Priority: configpb.BuganizerPriority_P2, 1971 Metrics: []*configpb.BugManagementPolicy_Metric{ 1972 { 1973 MetricId: metrics.CriticalFailuresExonerated.ID.String(), 1974 ActivationThreshold: &configpb.MetricThreshold{ 1975 OneDay: proto.Int64(100), 1976 }, 1977 DeactivationThreshold: &configpb.MetricThreshold{ 1978 OneDay: proto.Int64(10), 1979 }, 1980 }, 1981 }, 1982 Explanation: &configpb.BugManagementPolicy_Explanation{ 1983 ProblemHtml: "problem", 1984 ActionHtml: "action", 1985 }, 1986 BugTemplate: &configpb.BugManagementPolicy_BugTemplate{ 1987 CommentTemplate: `{{if .BugID.IsBuganizer }}Buganizer Bug ID: {{ .BugID.BuganizerBugID }}{{end}}` + 1988 `{{if .BugID.IsMonorail }}Monorail Project: {{ .BugID.MonorailProject }}; ID: {{ .BugID.MonorailBugID }}{{end}}` + 1989 `Rule URL: {{.RuleURL}}`, 1990 Monorail: &configpb.BugManagementPolicy_BugTemplate_Monorail{ 1991 Labels: []string{"Test-Exonerated"}, 1992 }, 1993 Buganizer: &configpb.BugManagementPolicy_BugTemplate_Buganizer{ 1994 Hotlists: []int64{1234}, 1995 }, 1996 }, 1997 } 1998 } 1999 2000 func createCLsRejectedPolicy() *configpb.BugManagementPolicy { 2001 return &configpb.BugManagementPolicy{ 2002 Id: "cls-rejected-policy", 2003 Owners: []string{"username@google.com"}, 2004 HumanReadableName: "many CL(s) are being falsely rejected in presubmit", 2005 Priority: configpb.BuganizerPriority_P1, 2006 Metrics: []*configpb.BugManagementPolicy_Metric{ 2007 { 2008 MetricId: metrics.HumanClsFailedPresubmit.ID.String(), 2009 ActivationThreshold: &configpb.MetricThreshold{ 2010 SevenDay: proto.Int64(10), 2011 }, 2012 DeactivationThreshold: &configpb.MetricThreshold{ 2013 SevenDay: proto.Int64(1), 2014 }, 2015 }, 2016 }, 2017 Explanation: &configpb.BugManagementPolicy_Explanation{ 2018 ProblemHtml: "problem", 2019 ActionHtml: "action", 2020 }, 2021 BugTemplate: &configpb.BugManagementPolicy_BugTemplate{ 2022 CommentTemplate: `Many CLs are failing presubmit. Policy text goes here.`, 2023 Monorail: &configpb.BugManagementPolicy_BugTemplate_Monorail{ 2024 Labels: []string{"Test-Exonerated"}, 2025 }, 2026 Buganizer: &configpb.BugManagementPolicy_BugTemplate_Buganizer{ 2027 Hotlists: []int64{1234}, 2028 }, 2029 }, 2030 } 2031 } 2032 2033 // verifyRulesResemble verifies rules stored in Spanner resemble 2034 // the passed expectations, modulo assigned RuleIDs and 2035 // audit timestamps. 2036 func verifyRulesResemble(ctx context.Context, expectedRules []*rules.Entry) error { 2037 // Read all rules. Sorted by BugSystem, BugId, Project. 2038 rs, err := rules.ReadAllForTesting(span.Single(ctx)) 2039 if err != nil { 2040 return err 2041 } 2042 2043 // Sort expectations in the same order as rules. 2044 sortedExpected := make([]*rules.Entry, len(expectedRules)) 2045 copy(sortedExpected, expectedRules) 2046 sort.Slice(sortedExpected, func(i, j int) bool { 2047 ruleI := sortedExpected[i] 2048 ruleJ := sortedExpected[j] 2049 if ruleI.BugID.System != ruleJ.BugID.System { 2050 return ruleI.BugID.System < ruleJ.BugID.System 2051 } 2052 if ruleI.BugID.ID != ruleJ.BugID.ID { 2053 return ruleI.BugID.ID < ruleJ.BugID.ID 2054 } 2055 return ruleI.Project < ruleJ.Project 2056 }) 2057 2058 for _, r := range rs { 2059 // Accept whatever values the implementation has set 2060 // (these values are assigned non-deterministically). 2061 r.RuleID = "" 2062 r.CreateTime = time.Time{} 2063 r.LastAuditableUpdateTime = time.Time{} 2064 r.LastUpdateTime = time.Time{} 2065 r.PredicateLastUpdateTime = time.Time{} 2066 r.IsManagingBugPriorityLastUpdateTime = time.Time{} 2067 } 2068 for i, rule := range sortedExpected { 2069 expectationCopy := rule.Clone() 2070 // Clear the fields on the expectations as well. 2071 expectationCopy.RuleID = "" 2072 expectationCopy.CreateTime = time.Time{} 2073 expectationCopy.LastAuditableUpdateTime = time.Time{} 2074 expectationCopy.LastUpdateTime = time.Time{} 2075 expectationCopy.PredicateLastUpdateTime = time.Time{} 2076 expectationCopy.IsManagingBugPriorityLastUpdateTime = time.Time{} 2077 sortedExpected[i] = expectationCopy 2078 } 2079 2080 if diff := ShouldResembleProto(rs, sortedExpected); diff != "" { 2081 return errors.Reason("stored rules: %s", diff).Err() 2082 } 2083 return nil 2084 } 2085 2086 type buganizerBug struct { 2087 // Bug ID. 2088 ID int64 2089 // Expected buganizer component ID. 2090 Component int64 2091 // Content that is expected to appear in the bug title. 2092 ExpectedTitle string 2093 // Content that is expected to appear in the bug description. 2094 ExpectedContent []string 2095 // The policies which were expected to have activated, in the 2096 // order they should have reported activation. 2097 ExpectedPolicyIDsActivated []string 2098 } 2099 2100 func expectBuganizerBug(buganizerStore *buganizer.FakeIssueStore, bug buganizerBug) error { 2101 issue := buganizerStore.Issues[bug.ID].Issue 2102 if issue == nil { 2103 return errors.Reason("buganizer issue %v not found", bug.ID).Err() 2104 } 2105 if issue.IssueId != bug.ID { 2106 return errors.Reason("issue ID: got %v, want %v", issue.IssueId, bug.ID).Err() 2107 } 2108 if !strings.Contains(issue.IssueState.Title, bug.ExpectedTitle) { 2109 return errors.Reason("issue title: got %q, expected it to contain %q", issue.IssueState.Title, bug.ExpectedTitle).Err() 2110 } 2111 if issue.IssueState.ComponentId != bug.Component { 2112 return errors.Reason("component: got %v; want %v", issue.IssueState.ComponentId, bug.Component).Err() 2113 } 2114 2115 for _, expectedContent := range bug.ExpectedContent { 2116 if !strings.Contains(issue.Description.Comment, expectedContent) { 2117 return errors.Reason("issue description: got %q, expected it to contain %q", issue.Description.Comment, expectedContent).Err() 2118 } 2119 } 2120 comments := buganizerStore.Issues[bug.ID].Comments 2121 if len(comments) != 1+len(bug.ExpectedPolicyIDsActivated) { 2122 return errors.Reason("issue comments: got %v want %v", len(comments), 1+len(bug.ExpectedPolicyIDsActivated)).Err() 2123 } 2124 for i, activatedPolicyID := range bug.ExpectedPolicyIDsActivated { 2125 expectedContent := fmt.Sprintf("(Policy ID: %s)", activatedPolicyID) 2126 if !strings.Contains(comments[1+i].Comment, expectedContent) { 2127 return errors.Reason("issue comment %v: got %q, expected it to contain %q", i+1, comments[i+1].Comment, expectedContent).Err() 2128 } 2129 } 2130 return nil 2131 } 2132 2133 type monorailBug struct { 2134 // The monorail project. 2135 Project string 2136 // The monorail bug ID. 2137 ID int 2138 2139 ExpectedComponents []string 2140 // Content that is expected to appear in the bug title. 2141 ExpectedTitle string 2142 // Content that is expected to appear in the bug description. 2143 ExpectedContent []string 2144 // The policies which were expected to have activated, in the 2145 // order they should have reported activation. 2146 ExpectedPolicyIDsActivated []string 2147 } 2148 2149 func expectMonorailBug(monorailStore *monorail.FakeIssuesStore, bug monorailBug) error { 2150 var issue *monorail.IssueData 2151 name := fmt.Sprintf("projects/%s/issues/%v", bug.Project, bug.ID) 2152 for _, iss := range monorailStore.Issues { 2153 if iss.Issue.Name == name { 2154 issue = iss 2155 break 2156 } 2157 } 2158 if issue == nil { 2159 return errors.Reason("monorail issue %q not found", name).Err() 2160 } 2161 if !strings.Contains(issue.Issue.Summary, bug.ExpectedTitle) { 2162 return errors.Reason("issue title: got %q, expected it to contain %q", issue.Issue.Summary, bug.ExpectedTitle).Err() 2163 } 2164 var actualComponents []string 2165 for _, component := range issue.Issue.Components { 2166 actualComponents = append(actualComponents, component.Component) 2167 } 2168 if msg := ShouldResemble(actualComponents, bug.ExpectedComponents); msg != "" { 2169 return errors.Reason("components: %s", msg).Err() 2170 } 2171 comments := issue.Comments 2172 if len(comments) != 1+len(bug.ExpectedPolicyIDsActivated) { 2173 return errors.Reason("issue comments: got %v want %v", len(comments), 1+len(bug.ExpectedPolicyIDsActivated)).Err() 2174 } 2175 for _, expectedContent := range bug.ExpectedContent { 2176 if !strings.Contains(issue.Comments[0].Content, expectedContent) { 2177 return errors.Reason("issue description: got %q, expected it to contain %q", issue.Comments[0].Content, expectedContent).Err() 2178 } 2179 } 2180 expectedLink := "https://luci-analysis-test.appspot.com/p/chromeos/rules/" 2181 if !strings.Contains(comments[0].Content, expectedLink) { 2182 return errors.Reason("issue comment #1: got %q, expected it to contain %q", issue.Comments[0].Content, expectedLink).Err() 2183 } 2184 for i, activatedPolicyID := range bug.ExpectedPolicyIDsActivated { 2185 expectedContent := fmt.Sprintf("(Policy ID: %s)", activatedPolicyID) 2186 if !strings.Contains(comments[i+1].Content, expectedContent) { 2187 return errors.Reason("issue comment %v: got %q, expected it to contain %q", i+2, comments[i+1].Content, expectedContent).Err() 2188 } 2189 } 2190 return nil 2191 }