go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/rpc/query_blamelist_test.go (about) 1 // Copyright 2020 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 rpc 16 17 import ( 18 "context" 19 "fmt" 20 "testing" 21 22 "github.com/golang/mock/gomock" 23 24 . "github.com/smartystreets/goconvey/convey" 25 "go.chromium.org/luci/appengine/gaetesting" 26 "go.chromium.org/luci/auth/identity" 27 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 28 "go.chromium.org/luci/buildbucket/protoutil" 29 gitpb "go.chromium.org/luci/common/proto/git" 30 "go.chromium.org/luci/common/proto/gitiles" 31 "go.chromium.org/luci/common/proto/gitiles/mock_gitiles" 32 . "go.chromium.org/luci/common/testing/assertions" 33 "go.chromium.org/luci/gae/service/datastore" 34 "go.chromium.org/luci/milo/internal/model" 35 "go.chromium.org/luci/milo/internal/projectconfig" 36 "go.chromium.org/luci/milo/internal/utils" 37 milopb "go.chromium.org/luci/milo/proto/v1" 38 "go.chromium.org/luci/server/auth" 39 "go.chromium.org/luci/server/auth/authtest" 40 ) 41 42 func TestPrepareQueryBlamelistRequest(t *testing.T) { 43 t.Parallel() 44 Convey(`TestPrepareQueryBlamelistRequest`, t, func() { 45 Convey(`extract commit ID correctly`, func() { 46 Convey(`when there's no page token`, func() { 47 startRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{ 48 GitilesCommit: &buildbucketpb.GitilesCommit{ 49 Host: "host", 50 Project: "project/src", 51 Id: "commit-id", 52 Ref: "commit-ref", 53 }, 54 Builder: &buildbucketpb.BuilderID{ 55 Project: "project", 56 Bucket: "bucket", 57 Builder: "builder", 58 }, 59 }) 60 So(err, ShouldBeNil) 61 // commit ID should take priority. 62 So(startRev, ShouldEqual, "commit-id") 63 }) 64 65 Convey(`when there's no page token or commit ID`, func() { 66 startRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{ 67 GitilesCommit: &buildbucketpb.GitilesCommit{ 68 Host: "host", 69 Project: "project/src", 70 Ref: "commit-ref", 71 }, 72 Builder: &buildbucketpb.BuilderID{ 73 Project: "project", 74 Bucket: "bucket", 75 Builder: "builder", 76 }, 77 }) 78 So(err, ShouldBeNil) 79 So(startRev, ShouldEqual, "commit-ref") 80 }) 81 82 Convey(`when there's a page token`, func() { 83 startRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{ 84 GitilesCommit: &buildbucketpb.GitilesCommit{ 85 Host: "host", 86 Project: "project/src", 87 Id: "commit-id-1", 88 }, 89 Builder: &buildbucketpb.BuilderID{ 90 Project: "project", 91 Bucket: "bucket", 92 Builder: "builder", 93 }, 94 }) 95 So(err, ShouldBeNil) 96 So(startRev, ShouldEqual, "commit-id-1") 97 98 pageToken, err := serializeQueryBlamelistPageToken(&milopb.QueryBlamelistPageToken{ 99 NextCommitId: "commit-id-2", 100 }) 101 So(err, ShouldBeNil) 102 103 nextCommitRev, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{ 104 GitilesCommit: &buildbucketpb.GitilesCommit{ 105 Host: "host", 106 Project: "project/src", 107 Id: "commit-id-1", 108 }, 109 Builder: &buildbucketpb.BuilderID{ 110 Project: "project", 111 Bucket: "bucket", 112 Builder: "builder", 113 }, 114 PageToken: pageToken, 115 }) 116 So(err, ShouldBeNil) 117 So(nextCommitRev, ShouldEqual, "commit-id-2") 118 }) 119 }) 120 121 Convey(`reject the page token when the page token is invalid`, func() { 122 _, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{ 123 GitilesCommit: &buildbucketpb.GitilesCommit{ 124 Host: "host", 125 Project: "project/src", 126 Id: "commit-id-1", 127 }, 128 Builder: &buildbucketpb.BuilderID{ 129 Project: "project", 130 Bucket: "bucket", 131 Builder: "builder", 132 }, 133 PageToken: "abc", 134 }) 135 So(err, ShouldNotBeNil) 136 }) 137 138 Convey("no builder", func() { 139 _, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{ 140 GitilesCommit: &buildbucketpb.GitilesCommit{ 141 Host: "host", 142 Project: "project/src", 143 Id: "commit-id", 144 Ref: "commit-ref", 145 }, 146 }) 147 So(err, ShouldErrLike, "builder: project must match") 148 }) 149 150 Convey("invalid builder", func() { 151 _, err := prepareQueryBlamelistRequest(&milopb.QueryBlamelistRequest{ 152 GitilesCommit: &buildbucketpb.GitilesCommit{ 153 Host: "host", 154 Project: "project/src", 155 Id: "commit-id", 156 Ref: "commit-ref", 157 }, 158 Builder: &buildbucketpb.BuilderID{ 159 Project: "fake_proj", 160 Bucket: "fake[]ucket", 161 Builder: "fake_/uilder1", 162 }, 163 }) 164 So(err, ShouldErrLike, "builder: bucket must match") 165 }) 166 }) 167 } 168 169 func TestQueryBlamelist(t *testing.T) { 170 t.Parallel() 171 Convey(`TestQueryBlamelist`, t, func() { 172 c := gaetesting.TestingContextWithAppID("luci-milo-dev") 173 datastore.GetTestable(c).Consistent(true) 174 ctrl := gomock.NewController(t) 175 defer ctrl.Finish() 176 gitMock := mock_gitiles.NewMockGitilesClient(ctrl) 177 srv := &MiloInternalService{ 178 GetGitilesClient: func(c context.Context, host string, as auth.RPCAuthorityKind) (gitiles.GitilesClient, error) { 179 return gitMock, nil 180 }, 181 } 182 c = auth.WithState(c, &authtest.FakeState{Identity: "user"}) 183 184 builder1 := &buildbucketpb.BuilderID{ 185 Project: "fake_project", 186 Bucket: "fake_bucket", 187 Builder: "fake_builder1", 188 } 189 builder2 := &buildbucketpb.BuilderID{ 190 Project: "fake_project", 191 Bucket: "fake_bucket", 192 Builder: "fake_builder2", 193 } 194 195 commits := []*gitpb.Commit{ 196 {Id: "commit8"}, 197 {Id: "commit7"}, 198 {Id: "commit6"}, 199 {Id: "commit5"}, 200 {Id: "commit4"}, 201 {Id: "commit3"}, 202 {Id: "commit2"}, 203 {Id: "commit1"}, 204 } 205 206 createFakeBuild := func(builder *buildbucketpb.BuilderID, buildNum int, commitID string, additionalBlamelistPins ...string) *model.BuildSummary { 207 builderID := utils.LegacyBuilderIDString(builder) 208 buildID := fmt.Sprintf("%s/%d", builderID, buildNum) 209 buildSet := protoutil.GitilesBuildSet(&buildbucketpb.GitilesCommit{ 210 Host: "fake_gitiles_host", 211 Project: "fake_gitiles_project", 212 Id: commitID, 213 }) 214 blamelistPins := append(additionalBlamelistPins, buildSet) 215 return &model.BuildSummary{ 216 BuildKey: datastore.MakeKey(c, "build", buildID), 217 ProjectID: builder.Project, 218 BuilderID: builderID, 219 BuildID: buildID, 220 BuildSet: []string{buildSet}, 221 BlamelistPins: blamelistPins, 222 } 223 } 224 225 builds := []*model.BuildSummary{ 226 createFakeBuild(builder1, 1, "commit8", protoutil.GitilesBuildSet(&buildbucketpb.GitilesCommit{ 227 Host: "fake_gitiles_host", 228 Project: "other_fake_gitiles_project", 229 Id: "commit3", 230 })), 231 createFakeBuild(builder2, 1, "commit7"), 232 createFakeBuild(builder1, 2, "commit5", protoutil.GitilesBuildSet(&buildbucketpb.GitilesCommit{ 233 Host: "fake_gitiles_host", 234 Project: "other_fake_gitiles_project", 235 Id: "commit1", 236 })), 237 createFakeBuild(builder1, 3, "commit3"), 238 } 239 240 err := datastore.Put(c, builds) 241 So(err, ShouldBeNil) 242 243 err = datastore.Put(c, &projectconfig.Project{ 244 ID: "fake_project", 245 ACL: projectconfig.ACL{Identities: []identity.Identity{"user"}}, 246 LogoURL: "https://logo.com", 247 }) 248 So(err, ShouldBeNil) 249 250 Convey(`reject users with no access`, func() { 251 req := &milopb.QueryBlamelistRequest{ 252 GitilesCommit: &buildbucketpb.GitilesCommit{ 253 Host: "fake_gitiles_host", 254 Project: "fake_gitiles_project", 255 Id: "commit1", 256 }, 257 Builder: &buildbucketpb.BuilderID{ 258 Project: "secret_fake_project", 259 Bucket: "secret_fake_bucket", 260 Builder: "secret_fake_builder", 261 }, 262 PageSize: 2000, 263 } 264 _, err := srv.QueryBlamelist(c, req) 265 So(err, ShouldNotBeNil) 266 }) 267 268 Convey(`coerce page_size`, func() { 269 Convey(`to 1000 if it's greater than 1000`, func() { 270 req := &milopb.QueryBlamelistRequest{ 271 GitilesCommit: &buildbucketpb.GitilesCommit{ 272 Host: "fake_gitiles_host", 273 Project: "fake_gitiles_project", 274 Id: "commit1", 275 }, 276 Builder: builder1, 277 PageSize: 2000, 278 } 279 gitMock. 280 EXPECT(). 281 Log(gomock.Any(), &gitiles.LogRequest{ 282 Project: "fake_gitiles_project", 283 Committish: "commit1", 284 PageSize: 1001, 285 TreeDiff: true, 286 }). 287 Return(&gitiles.LogResponse{ 288 Log: commits, 289 }, nil) 290 291 _, err := srv.QueryBlamelist(c, req) 292 So(err, ShouldBeNil) 293 }) 294 295 Convey(`to 100 if it's not set`, func() { 296 req := &milopb.QueryBlamelistRequest{ 297 GitilesCommit: &buildbucketpb.GitilesCommit{ 298 Host: "fake_gitiles_host", 299 Project: "fake_gitiles_project", 300 Id: "commit8", 301 }, 302 Builder: builder1, 303 } 304 gitMock. 305 EXPECT(). 306 Log(gomock.Any(), &gitiles.LogRequest{ 307 Project: req.GitilesCommit.Project, 308 Committish: req.GitilesCommit.Id, 309 PageSize: 101, 310 TreeDiff: true, 311 }). 312 Return(&gitiles.LogResponse{ 313 Log: commits, 314 }, nil) 315 316 _, err := srv.QueryBlamelist(c, req) 317 So(err, ShouldBeNil) 318 }) 319 }) 320 321 Convey(`get all the commits in the blamelist`, func() { 322 Convey(`in one page`, func() { 323 Convey(`when we found the previous build`, func() { 324 req := &milopb.QueryBlamelistRequest{ 325 GitilesCommit: &buildbucketpb.GitilesCommit{ 326 Host: "fake_gitiles_host", 327 Project: "fake_gitiles_project", 328 Id: "commit8", 329 }, 330 Builder: builder1, 331 } 332 gitMock. 333 EXPECT(). 334 Log(gomock.Any(), &gitiles.LogRequest{ 335 Project: req.GitilesCommit.Project, 336 Committish: req.GitilesCommit.Id, 337 PageSize: 101, 338 TreeDiff: true, 339 }). 340 Return(&gitiles.LogResponse{ 341 Log: commits, 342 }, nil) 343 344 res, err := srv.QueryBlamelist(c, req) 345 So(err, ShouldBeNil) 346 So(res.Commits, ShouldHaveLength, 3) 347 So(res.Commits[0].Id, ShouldEqual, "commit8") 348 So(res.Commits[1].Id, ShouldEqual, "commit7") 349 So(res.Commits[2].Id, ShouldEqual, "commit6") 350 So(res.PrecedingCommit.Id, ShouldEqual, "commit5") 351 }) 352 353 Convey(`when there's no previous build`, func() { 354 req := &milopb.QueryBlamelistRequest{ 355 GitilesCommit: &buildbucketpb.GitilesCommit{ 356 Host: "fake_gitiles_host", 357 Project: "fake_gitiles_project", 358 Id: "commit3", 359 }, 360 Builder: builder1, 361 } 362 gitMock. 363 EXPECT(). 364 Log(gomock.Any(), &gitiles.LogRequest{ 365 Project: req.GitilesCommit.Project, 366 Committish: req.GitilesCommit.Id, 367 PageSize: 101, 368 TreeDiff: true, 369 }). 370 Return(&gitiles.LogResponse{ 371 Log: commits[5:], 372 }, nil) 373 374 res, err := srv.QueryBlamelist(c, req) 375 So(err, ShouldBeNil) 376 So(res.Commits, ShouldHaveLength, 3) 377 So(res.Commits[0].Id, ShouldEqual, "commit3") 378 So(res.Commits[1].Id, ShouldEqual, "commit2") 379 So(res.Commits[2].Id, ShouldEqual, "commit1") 380 So(res.PrecedingCommit, ShouldBeZeroValue) 381 }) 382 }) 383 384 Convey(`in multiple pages`, func() { 385 Convey(`when we found the previous build`, func() { 386 // Query the first page. 387 req := &milopb.QueryBlamelistRequest{ 388 GitilesCommit: &buildbucketpb.GitilesCommit{ 389 Host: "fake_gitiles_host", 390 Project: "fake_gitiles_project", 391 Id: "commit8", 392 }, 393 Builder: builder1, 394 PageSize: 2, 395 } 396 397 gitMock. 398 EXPECT(). 399 Log(gomock.Any(), &gitiles.LogRequest{ 400 Project: req.GitilesCommit.Project, 401 Committish: req.GitilesCommit.Id, 402 PageSize: 3, 403 TreeDiff: true, 404 }). 405 Return(&gitiles.LogResponse{ 406 Log: commits[0:3], 407 }, nil) 408 409 res, err := srv.QueryBlamelist(c, req) 410 So(err, ShouldBeNil) 411 So(res.Commits, ShouldHaveLength, 2) 412 So(res.Commits[0].Id, ShouldEqual, "commit8") 413 So(res.Commits[1].Id, ShouldEqual, "commit7") 414 So(res.NextPageToken, ShouldNotBeZeroValue) 415 So(res.PrecedingCommit.Id, ShouldEqual, "commit6") 416 417 // Query the second page. 418 req = &milopb.QueryBlamelistRequest{ 419 GitilesCommit: &buildbucketpb.GitilesCommit{ 420 Host: "fake_gitiles_host", 421 Project: "fake_gitiles_project", 422 Id: "commit8", 423 }, 424 Builder: builder1, 425 PageSize: 2, 426 PageToken: res.NextPageToken, 427 } 428 429 gitMock. 430 EXPECT(). 431 Log(gomock.Any(), &gitiles.LogRequest{ 432 Project: req.GitilesCommit.Project, 433 Committish: "commit6", 434 PageSize: 3, 435 TreeDiff: true, 436 }). 437 Return(&gitiles.LogResponse{ 438 Log: commits[2:5], 439 }, nil) 440 441 res, err = srv.QueryBlamelist(c, req) 442 So(err, ShouldBeNil) 443 So(res.Commits, ShouldHaveLength, 1) 444 So(res.Commits[0].Id, ShouldEqual, "commit6") 445 So(res.NextPageToken, ShouldBeZeroValue) 446 So(res.PrecedingCommit.Id, ShouldEqual, "commit5") 447 }) 448 449 Convey(`when there's no previous build`, func() { 450 // Query the first page. 451 req := &milopb.QueryBlamelistRequest{ 452 GitilesCommit: &buildbucketpb.GitilesCommit{ 453 Host: "fake_gitiles_host", 454 Project: "fake_gitiles_project", 455 Id: "commit3", 456 }, 457 Builder: builder1, 458 PageSize: 2, 459 } 460 461 gitMock. 462 EXPECT(). 463 Log(gomock.Any(), &gitiles.LogRequest{ 464 Project: req.GitilesCommit.Project, 465 Committish: req.GitilesCommit.Id, 466 PageSize: 3, 467 TreeDiff: true, 468 }). 469 Return(&gitiles.LogResponse{ 470 Log: commits[5:8], 471 }, nil) 472 473 res, err := srv.QueryBlamelist(c, req) 474 So(err, ShouldBeNil) 475 So(res.Commits, ShouldHaveLength, 2) 476 So(res.Commits[0].Id, ShouldEqual, "commit3") 477 So(res.Commits[1].Id, ShouldEqual, "commit2") 478 So(res.NextPageToken, ShouldNotBeZeroValue) 479 So(res.PrecedingCommit.Id, ShouldEqual, "commit1") 480 481 // Query the second page. 482 req = &milopb.QueryBlamelistRequest{ 483 GitilesCommit: &buildbucketpb.GitilesCommit{ 484 Host: "fake_gitiles_host", 485 Project: "fake_gitiles_project", 486 Id: "commit3", 487 }, 488 Builder: builder1, 489 PageSize: 2, 490 PageToken: res.NextPageToken, 491 } 492 493 gitMock. 494 EXPECT(). 495 Log(gomock.Any(), &gitiles.LogRequest{ 496 Project: req.GitilesCommit.Project, 497 Committish: "commit1", 498 PageSize: 3, 499 TreeDiff: true, 500 }). 501 Return(&gitiles.LogResponse{ 502 Log: commits[7:], 503 }, nil) 504 505 res, err = srv.QueryBlamelist(c, req) 506 So(err, ShouldBeNil) 507 So(res.Commits, ShouldHaveLength, 1) 508 So(res.Commits[0].Id, ShouldEqual, "commit1") 509 So(res.NextPageToken, ShouldBeZeroValue) 510 So(res.PrecedingCommit, ShouldBeZeroValue) 511 }) 512 }) 513 }) 514 515 Convey(`get blamelist of other projects`, func() { 516 req := &milopb.QueryBlamelistRequest{ 517 GitilesCommit: &buildbucketpb.GitilesCommit{ 518 Host: "fake_gitiles_host", 519 Project: "other_fake_gitiles_project", 520 Id: "commit3", 521 }, 522 Builder: builder1, 523 } 524 gitMock. 525 EXPECT(). 526 Log(gomock.Any(), &gitiles.LogRequest{ 527 Project: req.GitilesCommit.Project, 528 Committish: req.GitilesCommit.Id, 529 PageSize: 101, 530 TreeDiff: true, 531 }). 532 Return(&gitiles.LogResponse{ 533 Log: commits[5:], 534 }, nil) 535 536 res, err := srv.QueryBlamelist(c, req) 537 So(err, ShouldBeNil) 538 So(res.Commits, ShouldHaveLength, 2) 539 So(res.Commits[0].Id, ShouldEqual, "commit3") 540 So(res.Commits[1].Id, ShouldEqual, "commit2") 541 So(res.PrecedingCommit.Id, ShouldEqual, "commit1") 542 }) 543 }) 544 }