go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/bisection/culpritaction/revertculprit/revert_test_failure_culprit_test.go (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package revertculprit contains the logic to revert culprits 16 package revertculprit 17 18 import ( 19 "context" 20 "fmt" 21 "testing" 22 "time" 23 24 "github.com/golang/mock/gomock" 25 . "github.com/smartystreets/goconvey/convey" 26 "go.chromium.org/luci/bisection/internal/config" 27 "go.chromium.org/luci/bisection/internal/gerrit" 28 "go.chromium.org/luci/bisection/internal/lucianalysis" 29 "go.chromium.org/luci/bisection/model" 30 configpb "go.chromium.org/luci/bisection/proto/config" 31 pb "go.chromium.org/luci/bisection/proto/v1" 32 "go.chromium.org/luci/bisection/util" 33 "go.chromium.org/luci/bisection/util/testutil" 34 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 35 "go.chromium.org/luci/common/clock" 36 "go.chromium.org/luci/common/clock/testclock" 37 "go.chromium.org/luci/common/proto" 38 gerritpb "go.chromium.org/luci/common/proto/gerrit" 39 "go.chromium.org/luci/common/tsmon" 40 "go.chromium.org/luci/gae/impl/memory" 41 "go.chromium.org/luci/gae/service/datastore" 42 "google.golang.org/protobuf/types/known/timestamppb" 43 ) 44 45 func TestProcessTestFailureCulpritTask(t *testing.T) { 46 t.Parallel() 47 client := &fakeLUCIAnalysisClient{ 48 FailedConsistently: true, 49 } 50 51 Convey("processTestFailureCulpritTask", t, func() { 52 ctx := memory.Use(context.Background()) 53 testutil.UpdateIndices(ctx) 54 55 // Set test clock 56 cl := testclock.New(testclock.TestTimeUTC) 57 cl.Set(time.Unix(10000, 0).UTC()) 58 ctx = clock.Set(ctx, cl) 59 60 // Setup tsmon 61 ctx, _ = tsmon.WithDummyInMemory(ctx) 62 63 // Set the project-level config for this test 64 gerritConfig := &configpb.GerritConfig{ 65 ActionsEnabled: true, 66 NthsectionSettings: &configpb.GerritConfig_NthSectionSettings{ 67 Enabled: true, 68 ActionWhenVerificationError: false, 69 }, 70 } 71 projectCfg := config.CreatePlaceholderProjectConfig() 72 projectCfg.TestAnalysisConfig.GerritConfig = gerritConfig 73 cfg := map[string]*configpb.ProjectConfig{"chromium": projectCfg} 74 So(config.SetTestProjectConfig(ctx, cfg), ShouldBeNil) 75 76 // Setup datastore 77 tfa := testutil.CreateTestFailureAnalysis(ctx, &testutil.TestFailureAnalysisCreationOption{ 78 Project: "chromium", 79 TestFailureKey: datastore.NewKey(ctx, "TestFailure", "", 1, nil), 80 FailedBuildID: 123, 81 }) 82 nsa := testutil.CreateTestNthSectionAnalysis(ctx, &testutil.TestNthSectionAnalysisCreationOption{ 83 ParentAnalysisKey: datastore.KeyForObj(ctx, tfa), 84 }) 85 suspect := &model.Suspect{ 86 Type: model.SuspectType_NthSection, 87 ParentAnalysis: datastore.KeyForObj(ctx, nsa), 88 GitilesCommit: buildbucketpb.GitilesCommit{ 89 Host: "test.googlesource.com", 90 Project: "chromium/src", 91 Id: "12ab34cd56ef", 92 }, 93 ReviewUrl: "https://test-review.googlesource.com/c/chromium/test/+/876543", 94 AnalysisType: pb.AnalysisType_TEST_FAILURE_ANALYSIS, 95 VerificationStatus: model.SuspectVerificationStatus_ConfirmedCulprit, 96 } 97 So(datastore.Put(ctx, suspect), ShouldBeNil) 98 tfa.VerifiedCulpritKey = datastore.KeyForObj(ctx, suspect) 99 So(datastore.Put(ctx, tfa), ShouldBeNil) 100 tf1 := testutil.CreateTestFailure(ctx, &testutil.TestFailureCreationOption{ 101 ID: 1, 102 Project: "chromium", 103 IsPrimary: true, 104 Analysis: tfa, 105 TestID: "testID1", 106 VariantHash: "varianthash1", 107 }) 108 tf2 := testutil.CreateTestFailure(ctx, &testutil.TestFailureCreationOption{ 109 ID: 2, 110 Project: "chromium", 111 IsPrimary: false, 112 Analysis: tfa, 113 TestID: "testID2", 114 VariantHash: "varianthash2", 115 }) 116 117 // Set up mock Gerrit client 118 ctl := gomock.NewController(t) 119 defer ctl.Finish() 120 mockClient := gerrit.NewMockedClient(ctx, ctl) 121 ctx = mockClient.Ctx 122 culpritRes := &gerritpb.ListChangesResponse{ 123 Changes: []*gerritpb.ChangeInfo{{ 124 Number: 876543, 125 Project: "chromium/src", 126 Status: gerritpb.ChangeStatus_MERGED, 127 Submitted: timestamppb.New(clock.Now(ctx).Add(-time.Hour * 3)), 128 CurrentRevision: "deadbeef", 129 Revisions: map[string]*gerritpb.RevisionInfo{ 130 "deadbeef": { 131 Commit: &gerritpb.CommitInfo{ 132 Message: "Title.\n\nBody is here.\n\nChange-Id: I100deadbeef", 133 Author: &gerritpb.GitPersonInfo{ 134 Name: "John Doe", 135 Email: "jdoe@example.com", 136 }, 137 }, 138 }, 139 }, 140 }}, 141 } 142 analysisURL := util.ConstructTestAnalysisURL("chromium", tfa.ID) 143 buildURL := util.ConstructBuildURL(ctx, tfa.FailedBuildID) 144 bugURL := util.ConstructBuganizerURLForAnalysis("https://test-review.googlesource.com/c/chromium/test/+/876543", analysisURL) 145 testLinks := fmt.Sprintf("[%s](%s)\n[%s](%s)", 146 tf1.TestID, 147 util.ConstructTestHistoryURL(tf1.Project, tf1.TestID, tf1.VariantHash), 148 tf2.TestID, 149 util.ConstructTestHistoryURL(tf2.Project, tf2.TestID, tf2.VariantHash)) 150 151 Convey("test no longer unexpected", func() { 152 err := processTestFailureCulpritTask(ctx, tfa.ID, &fakeLUCIAnalysisClient{ 153 FailedConsistently: false, 154 }) 155 So(err, ShouldBeNil) 156 // Suspect action has been saved. 157 So(datastore.Get(ctx, suspect), ShouldBeNil) 158 So(suspect.InactionReason, ShouldEqual, pb.CulpritInactionReason_TEST_NO_LONGER_UNEXPECTED) 159 So(suspect.HasTakenActions, ShouldBeTrue) 160 }) 161 162 Convey("gerrit action disabled", func() { 163 projectCfg.TestAnalysisConfig.GerritConfig.ActionsEnabled = false 164 cfg := map[string]*configpb.ProjectConfig{tfa.Project: projectCfg} 165 So(config.SetTestProjectConfig(ctx, cfg), ShouldBeNil) 166 167 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 168 So(err, ShouldBeNil) 169 // Suspect action has been saved. 170 So(datastore.Get(ctx, suspect), ShouldBeNil) 171 So(suspect.InactionReason, ShouldEqual, pb.CulpritInactionReason_ACTIONS_DISABLED) 172 So(suspect.HasTakenActions, ShouldBeTrue) 173 }) 174 175 Convey("has existing revert", func() { 176 Convey("has merged revert", func() { 177 revertRes := &gerritpb.ListChangesResponse{ 178 Changes: []*gerritpb.ChangeInfo{ 179 { 180 Number: 876549, 181 Project: "chromium/src", 182 Status: gerritpb.ChangeStatus_MERGED, 183 }, 184 }, 185 } 186 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 187 Return(culpritRes, nil).Times(1) 188 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 189 Return(revertRes, nil).Times(1) 190 191 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 192 So(err, ShouldBeNil) 193 // Suspect action has been saved. 194 So(datastore.Get(ctx, suspect), ShouldBeNil) 195 So(suspect.InactionReason, ShouldEqual, pb.CulpritInactionReason_REVERTED_MANUALLY) 196 So(suspect.HasTakenActions, ShouldBeTrue) 197 }) 198 199 Convey("has new revert", func() { 200 revertRes := &gerritpb.ListChangesResponse{ 201 Changes: []*gerritpb.ChangeInfo{ 202 { 203 Number: 876549, 204 Project: "chromium/src", 205 Status: gerritpb.ChangeStatus_NEW, 206 }, 207 }, 208 } 209 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 210 Return(culpritRes, nil).Times(1) 211 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 212 Return(revertRes, nil).Times(1) 213 mockClient.Client.EXPECT().SetReview(gomock.Any(), proto.MatcherEqual( 214 &gerritpb.SetReviewRequest{ 215 Project: revertRes.Changes[0].Project, 216 Number: revertRes.Changes[0].Number, 217 RevisionId: "current", 218 Message: fmt.Sprintf("LUCI Bisection recommends submitting this"+ 219 " revert because it has confirmed the target of this revert is"+ 220 " the cause of a test failure. See the analysis: %s\n\n"+ 221 "Sample build with failed test: %s\n"+ 222 "Affected test(s):\n%s\n\n"+ 223 "If this is a false positive, please report it at %s", analysisURL, buildURL, testLinks, bugURL), 224 }, 225 )).Times(1) 226 227 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 228 So(err, ShouldBeNil) 229 // Suspect action has been saved. 230 So(datastore.Get(ctx, suspect), ShouldBeNil) 231 So(suspect.HasSupportRevertComment, ShouldBeTrue) 232 So(suspect.SupportRevertCommentTime, ShouldEqual, time.Unix(10000, 0).UTC()) 233 // Check counter incremented. 234 So(culpritActionCounter.Get(ctx, "chromium", "test", "comment_revert"), ShouldEqual, 1) 235 So(suspect.HasTakenActions, ShouldBeTrue) 236 }) 237 238 Convey("only abandoned revert", func() { 239 revertRes := &gerritpb.ListChangesResponse{ 240 Changes: []*gerritpb.ChangeInfo{ 241 { 242 Number: 876549, 243 Project: "chromium/src", 244 Status: gerritpb.ChangeStatus_ABANDONED, 245 }, 246 }, 247 } 248 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 249 Return(culpritRes, nil).Times(1) 250 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 251 Return(revertRes, nil).Times(1) 252 mockClient.Client.EXPECT().SetReview(gomock.Any(), proto.MatcherEqual( 253 &gerritpb.SetReviewRequest{ 254 Project: culpritRes.Changes[0].Project, 255 Number: culpritRes.Changes[0].Number, 256 RevisionId: "current", 257 Message: fmt.Sprintf("LUCI Bisection has identified this"+ 258 " change as the cause of a test failure. See the analysis: %s\n\n"+ 259 "Sample build with failed test: %s\n"+ 260 "Affected test(s):\n%s\n"+ 261 "A revert for this change was not created because an abandoned revert already exists.\n\n"+ 262 "If this is a false positive, please report it at %s", analysisURL, buildURL, testLinks, bugURL), 263 }, 264 )).Times(1) 265 266 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 267 So(err, ShouldBeNil) 268 // Suspect action has been saved. 269 So(datastore.Get(ctx, suspect), ShouldBeNil) 270 So(suspect.HasCulpritComment, ShouldBeTrue) 271 So(suspect.CulpritCommentTime, ShouldEqual, time.Unix(10000, 0).UTC()) 272 // Check counter incremented. 273 So(culpritActionCounter.Get(ctx, "chromium", "test", "comment_culprit"), ShouldEqual, 1) 274 So(suspect.HasTakenActions, ShouldBeTrue) 275 }) 276 }) 277 278 Convey("no existing revert", func() { 279 Convey("has LUCI bisection comment", func() { 280 lbEmail, err := gerrit.ServiceAccountEmail(ctx) 281 So(err, ShouldBeNil) 282 culpritRes.Changes[0].Messages = []*gerritpb.ChangeMessageInfo{ 283 {Author: &gerritpb.AccountInfo{Email: lbEmail}}, 284 } 285 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 286 Return(culpritRes, nil).Times(1) 287 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 288 Return(&gerritpb.ListChangesResponse{Changes: []*gerritpb.ChangeInfo{}}, nil).Times(1) 289 290 err = processTestFailureCulpritTask(ctx, tfa.ID, client) 291 So(err, ShouldBeNil) 292 // Suspect action has been saved. 293 So(datastore.Get(ctx, suspect), ShouldBeNil) 294 So(suspect.InactionReason, ShouldEqual, pb.CulpritInactionReason_CULPRIT_HAS_COMMENT) 295 So(suspect.HasTakenActions, ShouldBeTrue) 296 }) 297 298 Convey("comment culprit", func() { 299 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 300 Return(culpritRes, nil).Times(1) 301 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 302 Return(&gerritpb.ListChangesResponse{Changes: []*gerritpb.ChangeInfo{}}, nil).Times(1) 303 mockClient.Client.EXPECT().SetReview(gomock.Any(), proto.MatcherEqual( 304 &gerritpb.SetReviewRequest{ 305 Project: culpritRes.Changes[0].Project, 306 Number: culpritRes.Changes[0].Number, 307 RevisionId: "current", 308 Message: fmt.Sprintf("LUCI Bisection has identified this"+ 309 " change as the cause of a test failure. See the analysis: %s\n\n"+ 310 "Sample build with failed test: %s\n"+ 311 "Affected test(s):\n%s\n"+ 312 "A revert for this change was not created because the builder of the failed test(s) is not being watched by gardeners.\n\n"+ 313 "If this is a false positive, please report it at %s", analysisURL, buildURL, testLinks, bugURL), 314 }, 315 )).Times(1) 316 317 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 318 So(err, ShouldBeNil) 319 // Suspect action has been saved. 320 So(datastore.Get(ctx, suspect), ShouldBeNil) 321 So(suspect.HasCulpritComment, ShouldBeTrue) 322 So(suspect.CulpritCommentTime, ShouldEqual, time.Unix(10000, 0).UTC()) 323 So(suspect.HasTakenActions, ShouldBeTrue) 324 // Check counter incremented. 325 So(culpritActionCounter.Get(ctx, "chromium", "test", "comment_culprit"), ShouldEqual, 1) 326 }) 327 328 Convey("comment culprit with more than 5 test failures", func() { 329 for i := 1; i < 8; i++ { 330 testutil.CreateTestFailure(ctx, &testutil.TestFailureCreationOption{ 331 ID: int64(i), 332 Project: "chromium", 333 IsPrimary: i == 1, 334 Analysis: tfa, 335 TestID: fmt.Sprintf("testID%d", i), 336 VariantHash: fmt.Sprintf("varianthash%d", i), 337 }) 338 } 339 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 340 Return(culpritRes, nil).Times(1) 341 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 342 Return(&gerritpb.ListChangesResponse{Changes: []*gerritpb.ChangeInfo{}}, nil).Times(1) 343 mockClient.Client.EXPECT().SetReview(gomock.Any(), proto.MatcherEqual( 344 &gerritpb.SetReviewRequest{ 345 Project: culpritRes.Changes[0].Project, 346 Number: culpritRes.Changes[0].Number, 347 RevisionId: "current", 348 Message: fmt.Sprintf("LUCI Bisection has identified this"+ 349 " change as the cause of a test failure. See the analysis: %s\n\n"+ 350 "Sample build with failed test: %s\n"+ 351 "Affected test(s):\n"+ 352 "[testID1](https://ci.chromium.org/ui/test/chromium/testID1?q=VHash%%3Avarianthash1)\n"+ 353 "[testID2](https://ci.chromium.org/ui/test/chromium/testID2?q=VHash%%3Avarianthash2)\n"+ 354 "[testID3](https://ci.chromium.org/ui/test/chromium/testID3?q=VHash%%3Avarianthash3)\n"+ 355 "[testID4](https://ci.chromium.org/ui/test/chromium/testID4?q=VHash%%3Avarianthash4)\n"+ 356 "[testID5](https://ci.chromium.org/ui/test/chromium/testID5?q=VHash%%3Avarianthash5)\n"+ 357 "and 2 more ...\n"+ 358 "A revert for this change was not created because the builder of the failed test(s) is not being watched by gardeners.\n\n"+ 359 "If this is a false positive, please report it at %s", analysisURL, buildURL, bugURL), 360 }, 361 )).Times(1) 362 363 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 364 So(err, ShouldBeNil) 365 // Suspect action has been saved. 366 So(datastore.Get(ctx, suspect), ShouldBeNil) 367 So(suspect.HasCulpritComment, ShouldBeTrue) 368 So(suspect.CulpritCommentTime, ShouldEqual, time.Unix(10000, 0).UTC()) 369 So(suspect.HasTakenActions, ShouldBeTrue) 370 // Check counter incremented. 371 So(culpritActionCounter.Get(ctx, "chromium", "test", "comment_culprit"), ShouldEqual, 1) 372 }) 373 }) 374 375 Convey("revert creation", func() { 376 tfa.SheriffRotations = []string{"chromium"} 377 So(datastore.Put(ctx, tfa), ShouldBeNil) 378 datastore.GetTestable(ctx).CatchupIndexes() 379 380 Convey("revert has auto-revert off flag set", func() { 381 culpritRes.Changes[0].Revisions["deadbeef"].Commit.Message = "Title.\n\nBody is here.\n\nNOAUTOREVERT=true\n\nChange-Id: I100deadbeef" 382 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 383 Return(culpritRes, nil).Times(1) 384 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 385 Return(&gerritpb.ListChangesResponse{}, nil).Times(1) 386 mockClient.Client.EXPECT().SetReview(gomock.Any(), proto.MatcherEqual( 387 &gerritpb.SetReviewRequest{ 388 Project: culpritRes.Changes[0].Project, 389 Number: culpritRes.Changes[0].Number, 390 RevisionId: "current", 391 Message: fmt.Sprintf("LUCI Bisection has identified this"+ 392 " change as the cause of a test failure. See the analysis: %s\n\n"+ 393 "Sample build with failed test: %s\n"+ 394 "Affected test(s):\n%s\n"+ 395 "A revert for this change was not created because auto-revert has been disabled for this CL by its description.\n\n"+ 396 "If this is a false positive, please report it at %s", analysisURL, buildURL, testLinks, bugURL), 397 }, 398 )).Times(1) 399 400 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 401 So(err, ShouldBeNil) 402 403 datastore.GetTestable(ctx).CatchupIndexes() 404 // Suspect action has been saved. 405 So(datastore.Get(ctx, suspect), ShouldBeNil) 406 So(suspect.HasCulpritComment, ShouldBeTrue) 407 So(suspect.CulpritCommentTime, ShouldEqual, time.Unix(10000, 0).UTC()) 408 So(suspect.HasTakenActions, ShouldBeTrue) 409 // Check counter incremented. 410 So(culpritActionCounter.Get(ctx, "chromium", "test", "comment_culprit"), ShouldEqual, 1) 411 }) 412 413 Convey("revert was from an irrevertible author", func() { 414 culpritRes.Changes[0].Revisions["deadbeef"].Commit.Author = &gerritpb.GitPersonInfo{ 415 Name: "ChromeOS Commit Bot", 416 Email: "chromeos-commit-bot@chromium.org", 417 } 418 419 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 420 Return(culpritRes, nil).Times(1) 421 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 422 Return(&gerritpb.ListChangesResponse{}, nil).Times(1) 423 mockClient.Client.EXPECT().SetReview(gomock.Any(), proto.MatcherEqual( 424 &gerritpb.SetReviewRequest{ 425 Project: culpritRes.Changes[0].Project, 426 Number: culpritRes.Changes[0].Number, 427 RevisionId: "current", 428 Message: fmt.Sprintf("LUCI Bisection has identified this"+ 429 " change as the cause of a test failure. See the analysis: %s\n\n"+ 430 "Sample build with failed test: %s\n"+ 431 "Affected test(s):\n%s\n"+ 432 "A revert for this change was not created because LUCI Bisection cannot revert changes from this CL's author.\n\n"+ 433 "If this is a false positive, please report it at %s", analysisURL, buildURL, testLinks, bugURL), 434 }, 435 )).Times(1) 436 437 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 438 So(err, ShouldBeNil) 439 440 datastore.GetTestable(ctx).CatchupIndexes() 441 // Suspect action has been saved. 442 So(datastore.Get(ctx, suspect), ShouldBeNil) 443 So(suspect.HasCulpritComment, ShouldBeTrue) 444 So(suspect.CulpritCommentTime, ShouldEqual, time.Unix(10000, 0).UTC()) 445 So(suspect.HasTakenActions, ShouldBeTrue) 446 // Check counter incremented. 447 So(culpritActionCounter.Get(ctx, "chromium", "test", "comment_culprit"), ShouldEqual, 1) 448 }) 449 450 Convey("culprit has a downstream dependency", func() { 451 revertRes := &gerritpb.ListChangesResponse{ 452 Changes: []*gerritpb.ChangeInfo{}, 453 } 454 relatedChanges := &gerritpb.GetRelatedChangesResponse{ 455 Changes: []*gerritpb.GetRelatedChangesResponse_ChangeAndCommit{ 456 { 457 Project: "chromium/src", 458 Number: 876544, 459 Status: gerritpb.ChangeStatus_MERGED, 460 }, 461 { 462 Project: "chromium/src", 463 Number: 876543, 464 Status: gerritpb.ChangeStatus_MERGED, 465 }, 466 }, 467 } 468 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 469 Return(culpritRes, nil).Times(1) 470 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 471 Return(revertRes, nil).Times(1) 472 mockClient.Client.EXPECT().GetRelatedChanges(gomock.Any(), gomock.Any()). 473 Return(relatedChanges, nil).Times(1) 474 mockClient.Client.EXPECT().SetReview(gomock.Any(), proto.MatcherEqual( 475 &gerritpb.SetReviewRequest{ 476 Project: culpritRes.Changes[0].Project, 477 Number: culpritRes.Changes[0].Number, 478 RevisionId: "current", 479 Message: fmt.Sprintf("LUCI Bisection has identified this"+ 480 " change as the cause of a test failure. See the analysis: %s\n\n"+ 481 "Sample build with failed test: %s\n"+ 482 "Affected test(s):\n%s\n"+ 483 "A revert for this change was not created because there are merged changes depending on it.\n\n"+ 484 "If this is a false positive, please report it at %s", analysisURL, buildURL, testLinks, bugURL), 485 }, 486 )).Times(1) 487 488 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 489 So(err, ShouldBeNil) 490 491 datastore.GetTestable(ctx).CatchupIndexes() 492 // Suspect action has been saved. 493 So(datastore.Get(ctx, suspect), ShouldBeNil) 494 So(suspect.HasCulpritComment, ShouldBeTrue) 495 So(suspect.CulpritCommentTime, ShouldEqual, time.Unix(10000, 0).UTC()) 496 So(suspect.HasTakenActions, ShouldBeTrue) 497 // Check counter incremented. 498 So(culpritActionCounter.Get(ctx, "chromium", "test", "comment_culprit"), ShouldEqual, 1) 499 }) 500 501 Convey("revert creation is disabled", func() { 502 projectCfg.TestAnalysisConfig.GerritConfig.CreateRevertSettings = &configpb.GerritConfig_RevertActionSettings{ 503 Enabled: false, 504 } 505 cfg := map[string]*configpb.ProjectConfig{tfa.Project: projectCfg} 506 So(config.SetTestProjectConfig(ctx, cfg), ShouldBeNil) 507 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 508 Return(culpritRes, nil).Times(1) 509 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 510 Return(&gerritpb.ListChangesResponse{}, nil).Times(1) 511 mockClient.Client.EXPECT().GetRelatedChanges(gomock.Any(), gomock.Any()). 512 Return(&gerritpb.GetRelatedChangesResponse{}, nil).Times(1) 513 mockClient.Client.EXPECT().SetReview(gomock.Any(), proto.MatcherEqual( 514 &gerritpb.SetReviewRequest{ 515 Project: culpritRes.Changes[0].Project, 516 Number: culpritRes.Changes[0].Number, 517 RevisionId: "current", 518 Message: fmt.Sprintf("LUCI Bisection has identified this"+ 519 " change as the cause of a test failure. See the analysis: %s\n\n"+ 520 "Sample build with failed test: %s\n"+ 521 "Affected test(s):\n%s\n"+ 522 "A revert for this change was not created because LUCI Bisection's revert creation has been disabled.\n\n"+ 523 "If this is a false positive, please report it at %s", analysisURL, buildURL, testLinks, bugURL), 524 }, 525 )).Times(1) 526 527 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 528 So(err, ShouldBeNil) 529 530 datastore.GetTestable(ctx).CatchupIndexes() 531 // Suspect action has been saved. 532 So(datastore.Get(ctx, suspect), ShouldBeNil) 533 So(suspect.HasCulpritComment, ShouldBeTrue) 534 So(suspect.CulpritCommentTime, ShouldEqual, time.Unix(10000, 0).UTC()) 535 So(suspect.HasTakenActions, ShouldBeTrue) 536 // Check counter incremented. 537 So(culpritActionCounter.Get(ctx, "chromium", "test", "comment_culprit"), ShouldEqual, 1) 538 }) 539 540 Convey("exceed daily limit", func() { 541 // Set up config. 542 projectCfg.TestAnalysisConfig.GerritConfig.CreateRevertSettings = &configpb.GerritConfig_RevertActionSettings{ 543 DailyLimit: 1, 544 Enabled: true, 545 } 546 cfg := map[string]*configpb.ProjectConfig{tfa.Project: projectCfg} 547 So(config.SetTestProjectConfig(ctx, cfg), ShouldBeNil) 548 549 // Add existing revert. 550 testutil.CreateSuspect(ctx, &testutil.SuspectCreationOption{ 551 AnalysisType: pb.AnalysisType_TEST_FAILURE_ANALYSIS, 552 ActionDetails: model.ActionDetails{ 553 IsRevertCreated: true, 554 RevertCreateTime: clock.Now(ctx).Add(-time.Hour), 555 }, 556 }) 557 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 558 Return(culpritRes, nil).Times(1) 559 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 560 Return(&gerritpb.ListChangesResponse{}, nil).Times(1) 561 mockClient.Client.EXPECT().GetRelatedChanges(gomock.Any(), gomock.Any()). 562 Return(&gerritpb.GetRelatedChangesResponse{}, nil).Times(1) 563 mockClient.Client.EXPECT().SetReview(gomock.Any(), proto.MatcherEqual( 564 &gerritpb.SetReviewRequest{ 565 Project: culpritRes.Changes[0].Project, 566 Number: culpritRes.Changes[0].Number, 567 RevisionId: "current", 568 Message: fmt.Sprintf("LUCI Bisection has identified this"+ 569 " change as the cause of a test failure. See the analysis: %s\n\n"+ 570 "Sample build with failed test: %s\n"+ 571 "Affected test(s):\n%s\n"+ 572 "A revert for this change was not created because LUCI Bisection's daily limit for revert creation (1) has been reached; 1 reverts have already been created.\n\n"+ 573 "If this is a false positive, please report it at %s", analysisURL, buildURL, testLinks, bugURL), 574 }, 575 )).Times(1) 576 577 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 578 So(err, ShouldBeNil) 579 580 datastore.GetTestable(ctx).CatchupIndexes() 581 // Suspect action has been saved. 582 So(datastore.Get(ctx, suspect), ShouldBeNil) 583 So(suspect.HasCulpritComment, ShouldBeTrue) 584 So(suspect.CulpritCommentTime, ShouldEqual, time.Unix(10000, 0).UTC()) 585 So(suspect.HasTakenActions, ShouldBeTrue) 586 // Check counter incremented. 587 So(culpritActionCounter.Get(ctx, "chromium", "test", "comment_culprit"), ShouldEqual, 1) 588 }) 589 590 Convey("revert created", func() { 591 // Set up config. 592 projectCfg.TestAnalysisConfig.GerritConfig.CreateRevertSettings = &configpb.GerritConfig_RevertActionSettings{ 593 DailyLimit: 10, 594 Enabled: true, 595 } 596 cfg := map[string]*configpb.ProjectConfig{tfa.Project: projectCfg} 597 So(config.SetTestProjectConfig(ctx, cfg), ShouldBeNil) 598 599 revertRes := &gerritpb.ChangeInfo{ 600 Number: 876549, 601 Project: "chromium/src", 602 Status: gerritpb.ChangeStatus_NEW, 603 } 604 605 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 606 Return(culpritRes, nil).Times(1) 607 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 608 Return(&gerritpb.ListChangesResponse{}, nil).Times(1) 609 mockClient.Client.EXPECT().GetRelatedChanges(gomock.Any(), gomock.Any()). 610 Return(&gerritpb.GetRelatedChangesResponse{}, nil).Times(1) 611 mockClient.Client.EXPECT().RevertChange(gomock.Any(), proto.MatcherEqual( 612 &gerritpb.RevertChangeRequest{ 613 Project: culpritRes.Changes[0].Project, 614 Number: culpritRes.Changes[0].Number, 615 Message: fmt.Sprintf("Revert \"chromium/src~876543\"\n\n"+ 616 "This reverts commit 12ab34cd56ef.\n\n"+ 617 "Reason for revert:\n"+ 618 "LUCI Bisection has identified this"+ 619 " change as the cause of a test failure. See the analysis: %s\n\n"+ 620 "Sample build with failed test: %s\n"+ 621 "Affected test(s):\n%s\n\n"+ 622 "If this is a false positive, please report it at %s\n\n"+ 623 "Original change's description:\n"+ 624 "> Title.\n"+ 625 ">\n"+ 626 "> Body is here.\n"+ 627 ">\n"+ 628 "> Change-Id: I100deadbeef\n\n"+ 629 "No-Presubmit: true\n"+ 630 "No-Tree-Checks: true\n"+ 631 "No-Try: true", analysisURL, buildURL, testLinks, bugURL), 632 }, 633 )). 634 Return(revertRes, nil).Times(1) 635 mockClient.Client.EXPECT().ListChanges(gomock.Any(), gomock.Any()). 636 Return(&gerritpb.ListChangesResponse{ 637 Changes: []*gerritpb.ChangeInfo{ 638 { 639 Number: 876549, 640 Project: "chromium/src", 641 Status: gerritpb.ChangeStatus_MERGED, 642 }, 643 }, 644 }, nil).Times(1) 645 646 err := processTestFailureCulpritTask(ctx, tfa.ID, client) 647 So(err, ShouldBeNil) 648 649 datastore.GetTestable(ctx).CatchupIndexes() 650 // Suspect action has been saved. 651 So(datastore.Get(ctx, suspect), ShouldBeNil) 652 So(suspect.IsRevertCreated, ShouldBeTrue) 653 So(suspect.RevertCreateTime, ShouldEqual, time.Unix(10000, 0).UTC()) 654 So(suspect.HasTakenActions, ShouldBeTrue) 655 // Check counter incremented. 656 So(culpritActionCounter.Get(ctx, "chromium", "test", "create_revert"), ShouldEqual, 1) 657 }) 658 }) 659 }) 660 } 661 662 type fakeLUCIAnalysisClient struct { 663 FailedConsistently bool 664 } 665 666 func (cl *fakeLUCIAnalysisClient) TestIsUnexpectedConsistently(ctx context.Context, project string, key lucianalysis.TestVerdictKey, sinceCommitPosition int64) (bool, error) { 667 return cl.FailedConsistently, nil 668 }