go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/rpc/test_variant_branches_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 rpc 16 17 import ( 18 "context" 19 "encoding/hex" 20 "fmt" 21 "testing" 22 "time" 23 24 "cloud.google.com/go/bigquery" 25 "google.golang.org/grpc/codes" 26 "google.golang.org/protobuf/types/known/anypb" 27 "google.golang.org/protobuf/types/known/durationpb" 28 "google.golang.org/protobuf/types/known/timestamppb" 29 30 "go.chromium.org/luci/common/proto/git" 31 "go.chromium.org/luci/resultdb/rdbperms" 32 "go.chromium.org/luci/server/auth" 33 "go.chromium.org/luci/server/auth/authtest" 34 35 "go.chromium.org/luci/analysis/internal/changepoints/inputbuffer" 36 cpb "go.chromium.org/luci/analysis/internal/changepoints/proto" 37 "go.chromium.org/luci/analysis/internal/changepoints/testvariantbranch" 38 "go.chromium.org/luci/analysis/internal/gitiles" 39 "go.chromium.org/luci/analysis/internal/pagination" 40 "go.chromium.org/luci/analysis/internal/testutil" 41 "go.chromium.org/luci/analysis/internal/testverdicts" 42 "go.chromium.org/luci/analysis/pbutil" 43 pb "go.chromium.org/luci/analysis/proto/v1" 44 45 . "github.com/smartystreets/goconvey/convey" 46 . "go.chromium.org/luci/common/testing/assertions" 47 ) 48 49 func TestTestVariantAnalysesServer(t *testing.T) { 50 Convey("TestVariantAnalysesServer", t, func() { 51 ctx := testutil.IntegrationTestContext(t) 52 53 tvc := testverdicts.FakeReadClient{} 54 server := NewTestVariantBranchesServer(&tvc) 55 Convey("GetRaw", func() { 56 Convey("permission denied", func() { 57 ctx = auth.WithState(ctx, &authtest.FakeState{ 58 Identity: "anonymous:anonymous", 59 }) 60 req := &pb.GetRawTestVariantBranchRequest{} 61 res, err := server.GetRaw(ctx, req) 62 So(err, ShouldNotBeNil) 63 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 64 So(res, ShouldBeNil) 65 }) 66 67 Convey("invalid request", func() { 68 ctx = adminContext(ctx) 69 req := &pb.GetRawTestVariantBranchRequest{ 70 Name: "Project/abc/xyz", 71 } 72 res, err := server.GetRaw(ctx, req) 73 So(err, ShouldNotBeNil) 74 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 75 So(res, ShouldBeNil) 76 }) 77 78 Convey("not found", func() { 79 ctx = adminContext(ctx) 80 req := &pb.GetRawTestVariantBranchRequest{ 81 Name: "projects/project/tests/test/variants/abababababababab/refs/abababababababab", 82 } 83 res, err := server.GetRaw(ctx, req) 84 So(err, ShouldNotBeNil) 85 So(err, ShouldHaveGRPCStatus, codes.NotFound) 86 So(res, ShouldBeNil) 87 }) 88 89 Convey("invalid ref_hash", func() { 90 ctx = adminContext(ctx) 91 req := &pb.GetRawTestVariantBranchRequest{ 92 Name: "projects/project/tests/this//is/a/test/variants/abababababababab/refs/abababababababgh", 93 } 94 res, err := server.GetRaw(ctx, req) 95 So(err, ShouldNotBeNil) 96 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 97 So(res, ShouldBeNil) 98 }) 99 100 Convey("invalid test id", func() { 101 ctx = adminContext(ctx) 102 Convey("bad structure", func() { 103 ctx = adminContext(ctx) 104 req := &pb.GetRawTestVariantBranchRequest{ 105 Name: "projects/project/tests/a/variants/0123456789abcdef/refs/7265665f68617368/bad/subpath", 106 } 107 res, err := server.GetRaw(ctx, req) 108 So(err, ShouldNotBeNil) 109 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 110 So(err, ShouldErrLike, "name must be of format projects/{PROJECT}/tests/{URL_ESCAPED_TEST_ID}/variants/{VARIANT_HASH}/refs/{REF_HASH}") 111 So(res, ShouldBeNil) 112 }) 113 Convey("bad URL escaping", func() { 114 req := &pb.GetRawTestVariantBranchRequest{ 115 Name: "projects/project/tests/abcdef%test/variants/0123456789abcdef/refs/7265665f68617368", 116 } 117 res, err := server.GetRaw(ctx, req) 118 So(err, ShouldNotBeNil) 119 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 120 So(err, ShouldErrLike, "malformed test id: invalid URL escape \"%te\"") 121 So(res, ShouldBeNil) 122 }) 123 Convey("bad value", func() { 124 req := &pb.GetRawTestVariantBranchRequest{ 125 Name: "projects/project/tests/\u0001atest/variants/0123456789abcdef/refs/7265665f68617368", 126 } 127 res, err := server.GetRaw(ctx, req) 128 So(err, ShouldNotBeNil) 129 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 130 So(err, ShouldErrLike, `test id "\x01atest": non-printable rune`) 131 So(res, ShouldBeNil) 132 }) 133 }) 134 Convey("ok", func() { 135 ctx = adminContext(ctx) 136 // Insert test variant branch to Spanner. 137 tvb := &testvariantbranch.Entry{ 138 IsNew: true, 139 Project: "project", 140 TestID: "this//is/a/test", 141 VariantHash: "0123456789abcdef", 142 RefHash: []byte("ref_hash"), 143 SourceRef: &pb.SourceRef{ 144 System: &pb.SourceRef_Gitiles{ 145 Gitiles: &pb.GitilesRef{ 146 Host: "host", 147 Project: "proj", 148 Ref: "ref", 149 }, 150 }, 151 }, 152 Variant: &pb.Variant{ 153 Def: map[string]string{ 154 "k": "v", 155 }, 156 }, 157 InputBuffer: &inputbuffer.Buffer{ 158 HotBuffer: inputbuffer.History{ 159 Verdicts: []inputbuffer.PositionVerdict{ 160 { 161 CommitPosition: 20, 162 IsSimpleExpectedPass: true, 163 Hour: time.Unix(3600, 0), 164 }, 165 }, 166 }, 167 ColdBuffer: inputbuffer.History{ 168 Verdicts: []inputbuffer.PositionVerdict{ 169 { 170 CommitPosition: 30, 171 Hour: time.Unix(7200, 0), 172 Details: inputbuffer.VerdictDetails{ 173 IsExonerated: true, 174 Runs: []inputbuffer.Run{ 175 { 176 Expected: inputbuffer.ResultCounts{ 177 PassCount: 1, 178 FailCount: 2, 179 }, 180 Unexpected: inputbuffer.ResultCounts{ 181 CrashCount: 3, 182 AbortCount: 4, 183 }, 184 IsDuplicate: true, 185 }, 186 { 187 Expected: inputbuffer.ResultCounts{ 188 CrashCount: 5, 189 AbortCount: 6, 190 }, 191 Unexpected: inputbuffer.ResultCounts{ 192 PassCount: 7, 193 AbortCount: 8, 194 }, 195 }, 196 }, 197 }, 198 }, 199 }, 200 }, 201 }, 202 FinalizingSegment: &cpb.Segment{ 203 State: cpb.SegmentState_FINALIZING, 204 HasStartChangepoint: true, 205 StartPosition: 100, 206 StartHour: timestamppb.New(time.Unix(3600, 0)), 207 StartPositionLowerBound_99Th: 95, 208 StartPositionUpperBound_99Th: 105, 209 FinalizedCounts: &cpb.Counts{ 210 UnexpectedResults: 1, 211 }, 212 }, 213 FinalizedSegments: &cpb.Segments{ 214 Segments: []*cpb.Segment{ 215 { 216 State: cpb.SegmentState_FINALIZED, 217 StartPosition: 50, 218 StartHour: timestamppb.New(time.Unix(3600, 0)), 219 StartPositionLowerBound_99Th: 45, 220 StartPositionUpperBound_99Th: 55, 221 FinalizedCounts: &cpb.Counts{ 222 UnexpectedResults: 2, 223 }, 224 }, 225 }, 226 }, 227 Statistics: &cpb.Statistics{ 228 HourlyBuckets: []*cpb.Statistics_HourBucket{ 229 { 230 Hour: 123456, 231 UnexpectedVerdicts: 1, 232 FlakyVerdicts: 3, 233 TotalVerdicts: 12, 234 }, 235 { 236 Hour: 123500, 237 UnexpectedVerdicts: 3, 238 FlakyVerdicts: 7, 239 TotalVerdicts: 93, 240 }, 241 }, 242 }, 243 } 244 var hs inputbuffer.HistorySerializer 245 mutation, err := tvb.ToMutation(&hs) 246 So(err, ShouldBeNil) 247 testutil.MustApply(ctx, mutation) 248 249 hexStr := "7265665f68617368" // hex string of "ref_hash". 250 req := &pb.GetRawTestVariantBranchRequest{ 251 Name: "projects/project/tests/this%2F%2Fis%2Fa%2Ftest/variants/0123456789abcdef/refs/7265665f68617368", 252 } 253 res, err := server.GetRaw(ctx, req) 254 So(err, ShouldBeNil) 255 256 expectedFinalizingSegment, err := anypb.New(tvb.FinalizingSegment) 257 So(err, ShouldBeNil) 258 259 expectedFinalizedSegments, err := anypb.New(tvb.FinalizedSegments) 260 So(err, ShouldBeNil) 261 262 expectedStatistics, err := anypb.New(tvb.Statistics) 263 So(err, ShouldBeNil) 264 265 So(res, ShouldResembleProto, &pb.TestVariantBranchRaw{ 266 Name: "projects/project/tests/this%2F%2Fis%2Fa%2Ftest/variants/0123456789abcdef/refs/7265665f68617368", 267 Project: "project", 268 TestId: "this//is/a/test", 269 VariantHash: "0123456789abcdef", 270 RefHash: hexStr, 271 Variant: tvb.Variant, 272 Ref: tvb.SourceRef, 273 FinalizingSegment: expectedFinalizingSegment, 274 FinalizedSegments: expectedFinalizedSegments, 275 Statistics: expectedStatistics, 276 HotBuffer: &pb.InputBuffer{ 277 Length: 1, 278 Verdicts: []*pb.PositionVerdict{ 279 { 280 CommitPosition: 20, 281 Hour: timestamppb.New(time.Unix(3600, 0)), 282 Runs: []*pb.PositionVerdict_Run{ 283 { 284 ExpectedPassCount: 1, 285 }, 286 }, 287 }, 288 }, 289 }, 290 ColdBuffer: &pb.InputBuffer{ 291 Length: 1, 292 Verdicts: []*pb.PositionVerdict{ 293 { 294 CommitPosition: 30, 295 Hour: timestamppb.New(time.Unix(7200, 0)), 296 IsExonerated: true, 297 Runs: []*pb.PositionVerdict_Run{ 298 { 299 ExpectedPassCount: 1, 300 ExpectedFailCount: 2, 301 UnexpectedCrashCount: 3, 302 UnexpectedAbortCount: 4, 303 IsDuplicate: true, 304 }, 305 { 306 ExpectedCrashCount: 5, 307 ExpectedAbortCount: 6, 308 UnexpectedPassCount: 7, 309 UnexpectedAbortCount: 8, 310 }, 311 }, 312 }, 313 }, 314 }, 315 }) 316 }) 317 }) 318 319 Convey("BatchGet", func() { 320 Convey("permission denied", func() { 321 ctx = auth.WithState(ctx, &authtest.FakeState{ 322 Identity: "user:someone@example.com", 323 IdentityGroups: []string{"luci-analysis-access"}, 324 }) 325 req := &pb.BatchGetTestVariantBranchRequest{} 326 327 res, err := server.BatchGet(ctx, req) 328 So(err, ShouldNotBeNil) 329 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 330 So(res, ShouldBeNil) 331 }) 332 333 Convey("invalid request", func() { 334 ctx = auth.WithState(ctx, &authtest.FakeState{ 335 Identity: "user:someone@example.com", 336 IdentityGroups: []string{"googlers", "luci-analysis-access"}, 337 }) 338 Convey("invalid name", func() { 339 req := &pb.BatchGetTestVariantBranchRequest{ 340 Names: []string{"projects/abc/xyz"}, 341 } 342 343 res, err := server.BatchGet(ctx, req) 344 So(err, ShouldNotBeNil) 345 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 346 So(res, ShouldBeNil) 347 }) 348 349 Convey("too many test variant branch requested", func() { 350 names := []string{} 351 for i := 0; i < 200; i++ { 352 names = append(names, "projects/project/tests/this%2F%2Fis%2Fa%2Ftest/variants/0123456789abcdef/refs/7265665f68617368") 353 } 354 req := &pb.BatchGetTestVariantBranchRequest{ 355 Names: names, 356 } 357 358 res, err := server.BatchGet(ctx, req) 359 So(err, ShouldNotBeNil) 360 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 361 So(err, ShouldErrLike, "names: no more than 100 may be queried at a time") 362 So(res, ShouldBeNil) 363 }) 364 }) 365 366 Convey("e2e", func() { 367 ctx = auth.WithState(ctx, &authtest.FakeState{ 368 Identity: "user:someone@example.com", 369 IdentityGroups: []string{"googlers", "luci-analysis-access"}, 370 }) 371 // Insert test variant branch to Spanner. 372 tvb := &testvariantbranch.Entry{ 373 IsNew: true, 374 Project: "project", 375 TestID: "this//is/a/test", 376 VariantHash: "0123456789abcdef", 377 RefHash: []byte("ref_hash"), 378 SourceRef: &pb.SourceRef{ 379 System: &pb.SourceRef_Gitiles{ 380 Gitiles: &pb.GitilesRef{ 381 Host: "host", 382 Project: "proj", 383 Ref: "ref", 384 }, 385 }, 386 }, 387 Variant: &pb.Variant{ 388 Def: map[string]string{ 389 "k": "v", 390 }, 391 }, 392 InputBuffer: &inputbuffer.Buffer{ 393 HotBuffer: inputbuffer.History{ 394 Verdicts: []inputbuffer.PositionVerdict{ 395 { 396 CommitPosition: 200, 397 IsSimpleExpectedPass: true, 398 Hour: time.Unix(3700, 0), 399 }, 400 }, 401 }, 402 ColdBuffer: inputbuffer.History{ 403 Verdicts: []inputbuffer.PositionVerdict{}, 404 }, 405 }, 406 FinalizingSegment: &cpb.Segment{ 407 State: cpb.SegmentState_FINALIZING, 408 HasStartChangepoint: true, 409 StartPosition: 100, 410 StartHour: timestamppb.New(time.Unix(3600, 0)), 411 StartPositionLowerBound_99Th: 95, 412 StartPositionUpperBound_99Th: 105, 413 FinalizedCounts: &cpb.Counts{ 414 UnexpectedVerdicts: 1, 415 TotalVerdicts: 1, 416 }, 417 }, 418 FinalizedSegments: &cpb.Segments{ 419 Segments: []*cpb.Segment{ 420 { 421 State: cpb.SegmentState_FINALIZED, 422 StartHour: timestamppb.New(time.Unix(3600, 0)), 423 StartPositionLowerBound_99Th: 45, 424 StartPositionUpperBound_99Th: 55, 425 FinalizedCounts: &cpb.Counts{ 426 UnexpectedVerdicts: 2, 427 TotalVerdicts: 2, 428 }, 429 }, 430 }, 431 }, 432 } 433 var hs inputbuffer.HistorySerializer 434 mutation, err := tvb.ToMutation(&hs) 435 So(err, ShouldBeNil) 436 testutil.MustApply(ctx, mutation) 437 req := &pb.BatchGetTestVariantBranchRequest{ 438 Names: []string{ 439 "projects/project/tests/not%2Fexist%2Ftest/variants/0123456789abcdef/refs/7265665f68617368", 440 "projects/project/tests/this%2F%2Fis%2Fa%2Ftest/variants/0123456789abcdef/refs/7265665f68617368", 441 }, 442 } 443 444 res, err := server.BatchGet(ctx, req) 445 So(err, ShouldBeNil) 446 So(res.TestVariantBranches, ShouldHaveLength, 2) 447 So(res.TestVariantBranches[0], ShouldBeNil) 448 So(res.TestVariantBranches[1], ShouldResembleProto, &pb.TestVariantBranch{ 449 Name: "projects/project/tests/this%2F%2Fis%2Fa%2Ftest/variants/0123456789abcdef/refs/7265665f68617368", 450 Project: "project", 451 TestId: "this//is/a/test", 452 VariantHash: "0123456789abcdef", 453 RefHash: "7265665f68617368", 454 Ref: &pb.SourceRef{ 455 System: &pb.SourceRef_Gitiles{ 456 Gitiles: &pb.GitilesRef{ 457 Host: "host", 458 Project: "proj", 459 Ref: "ref", 460 }, 461 }, 462 }, 463 Variant: &pb.Variant{ 464 Def: map[string]string{ 465 "k": "v", 466 }, 467 }, 468 Segments: []*pb.Segment{ 469 { 470 HasStartChangepoint: true, 471 StartPosition: 100, 472 StartPositionLowerBound_99Th: 95, 473 StartPositionUpperBound_99Th: 105, 474 StartHour: timestamppb.New(time.Unix(3600, 0)), 475 EndPosition: 200, 476 EndHour: timestamppb.New(time.Unix(3600, 0)), 477 Counts: &pb.Segment_Counts{ 478 UnexpectedVerdicts: 1, 479 FlakyVerdicts: 0, 480 TotalVerdicts: 2, 481 }, 482 }, 483 { 484 StartPositionLowerBound_99Th: 45, 485 StartPositionUpperBound_99Th: 55, 486 StartHour: timestamppb.New(time.Unix(3600, 0)), 487 EndHour: timestamppb.New(time.Unix(0, 0)), 488 Counts: &pb.Segment_Counts{ 489 UnexpectedVerdicts: 2, 490 FlakyVerdicts: 0, 491 TotalVerdicts: 2, 492 }, 493 }, 494 }, 495 }) 496 }) 497 }) 498 499 Convey("QuerySourcePositions", func() { 500 ctx = auth.WithState(ctx, &authtest.FakeState{ 501 Identity: "user:someone@example.com", 502 IdentityPermissions: []authtest.RealmPermission{ 503 { 504 Realm: "project:realm", 505 Permission: rdbperms.PermListTestResults, 506 }, 507 { 508 Realm: "project:realm", 509 Permission: rdbperms.PermListTestExonerations, 510 }, 511 }, 512 IdentityGroups: []string{"luci-analysis-access"}, 513 }) 514 var1 := pbutil.Variant("key1", "val1", "key2", "val1") 515 ref := &pb.SourceRef{ 516 System: &pb.SourceRef_Gitiles{ 517 Gitiles: &pb.GitilesRef{ 518 Host: "host", 519 Project: "project", 520 Ref: "ref", 521 }, 522 }, 523 } 524 refhash := hex.EncodeToString(pbutil.SourceRefHash(ref)) 525 req := &pb.QuerySourcePositionsRequest{ 526 Project: "project", 527 TestId: "testid", 528 VariantHash: pbutil.VariantHash(var1), 529 RefHash: refhash, 530 StartSourcePosition: 1100, 531 PageToken: "", 532 PageSize: 111, 533 } 534 535 Convey("unauthorised requests are rejected", func() { 536 ctx = auth.WithState(ctx, &authtest.FakeState{ 537 Identity: "user:someone@example.com", 538 IdentityGroups: []string{"luci-analysis-access"}, 539 }) 540 res, err := server.QuerySourcePositions(ctx, req) 541 So(err, ShouldErrLike, `caller does not have permission`, `in any realm in project "project"`) 542 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 543 So(res, ShouldBeNil) 544 }) 545 546 Convey("invalid requests are rejected", func() { 547 req.PageSize = -1 548 res, err := server.QuerySourcePositions(ctx, req) 549 So(err, ShouldNotBeNil) 550 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument) 551 So(res, ShouldBeNil) 552 }) 553 554 bqRef := &testverdicts.Ref{ 555 Gitiles: &testverdicts.Gitiles{ 556 Host: bigquery.NullString{StringVal: "chromium.googlesource.com", Valid: true}, 557 Project: bigquery.NullString{StringVal: "project", Valid: true}, 558 Ref: bigquery.NullString{StringVal: "ref", Valid: true}, 559 }, 560 } 561 Convey("no test verdicts that is close enough to start_source_position", func() { 562 tvc.CommitsWithVerdicts = []*testverdicts.CommitWithVerdicts{ 563 // Verdict at position 10990. 564 // This is the smallest position that is greater than the requested position. 565 { 566 Position: 10990, // we need 10990 - 1100 + 111 (10001) commits from gitiles. 567 CommitHash: "commithash", 568 Ref: bqRef, 569 TestVerdicts: []*testverdicts.TestVerdict{}, 570 }, 571 // Verdict at position 1002. 572 { 573 Position: 1002, 574 CommitHash: "commithash", 575 Ref: bqRef, 576 TestVerdicts: []*testverdicts.TestVerdict{}, 577 }, 578 } 579 580 res, err := server.QuerySourcePositions(ctx, req) 581 So(err, ShouldNotBeNil) 582 So(err, ShouldErrLike, `cannot find source positions because test verdicts is too sparse`) 583 So(err, ShouldHaveGRPCStatus, codes.NotFound) 584 So(res, ShouldBeNil) 585 }) 586 587 Convey("no test verdicts after start_source_position", func() { 588 tvc.CommitsWithVerdicts = []*testverdicts.CommitWithVerdicts{ 589 // Verdict at position 1002. 590 { 591 Position: 1002, 592 CommitHash: "commithash", 593 Ref: bqRef, 594 TestVerdicts: []*testverdicts.TestVerdict{}, 595 }, 596 } 597 598 res, err := server.QuerySourcePositions(ctx, req) 599 So(err, ShouldNotBeNil) 600 So(err, ShouldErrLike, `no commit at or after the requested start position`) 601 So(err, ShouldHaveGRPCStatus, codes.NotFound) 602 So(res, ShouldBeNil) 603 }) 604 605 Convey("e2e", func() { 606 tvc.CommitsWithVerdicts = []*testverdicts.CommitWithVerdicts{ 607 // Verdict at position 1200. 608 // This is the smallest position that is greater than the requested position. 609 // We use its commit hash to query gitiles. 610 { 611 Position: 1200, 612 CommitHash: "commithash", 613 Ref: bqRef, 614 TestVerdicts: []*testverdicts.TestVerdict{}, 615 }, 616 // Verdict at position 1002. 617 // The caller doesn't have access to this verdict, this verdict will be excluded from the response. 618 { 619 Position: 1002, 620 CommitHash: "commithash", 621 Ref: bqRef, 622 TestVerdicts: []*testverdicts.TestVerdict{ 623 { 624 TestID: "testid", 625 VariantHash: pbutil.VariantHash(var1), 626 RefHash: refhash, 627 InvocationID: "invocation-123", 628 Status: "EXPECTED", 629 PartitionTime: time.Unix(1000, 0), 630 PassedAvgDurationUsec: bigquery.NullFloat64{ 631 Float64: 0.001, 632 Valid: true, 633 }, 634 Changelists: []*testverdicts.Changelist{}, 635 HasAccess: false, 636 }, 637 }, 638 }, 639 // Verdict at position 1001. 640 // This is within the queried range, verdict will be included in the response. 641 { 642 Position: 1001, 643 CommitHash: "commithash", 644 Ref: bqRef, 645 TestVerdicts: []*testverdicts.TestVerdict{ 646 { 647 TestID: "testid", 648 VariantHash: pbutil.VariantHash(var1), 649 RefHash: refhash, 650 InvocationID: "invocation-123", 651 Status: "EXPECTED", 652 PartitionTime: time.Unix(1000, 0), 653 PassedAvgDurationUsec: bigquery.NullFloat64{ 654 Float64: 0.001, 655 Valid: true, 656 }, 657 Changelists: []*testverdicts.Changelist{}, 658 HasAccess: true, 659 }, 660 }, 661 }, 662 } 663 makeCommit := func(i int32) *git.Commit { 664 return &git.Commit{ 665 Id: fmt.Sprintf("id %d", i), 666 Tree: "tree", 667 Parents: []string{}, 668 Author: &git.Commit_User{ 669 Name: "userX", 670 Email: "userx@google.com", 671 Time: timestamppb.New(time.Unix(1000, 0)), 672 }, 673 Committer: &git.Commit_User{ 674 Name: "userY", 675 Email: "usery@google.com", 676 Time: timestamppb.New(time.Unix(1100, 0)), 677 }, 678 Message: fmt.Sprintf("message %d", i), 679 } 680 } 681 ctx := gitiles.UseFakeClient(ctx, makeCommit) 682 683 res, err := server.QuerySourcePositions(ctx, req) 684 So(err, ShouldBeNil) 685 cwvs := []*pb.SourcePosition{} 686 for i := req.StartSourcePosition; i > req.StartSourcePosition-int64(req.PageSize); i-- { 687 cwv := &pb.SourcePosition{ 688 Commit: makeCommit(int32(i - req.StartSourcePosition + int64(req.PageSize))), 689 Position: i, 690 } 691 // Attach verdicts. 692 if i == 1001 { 693 cwv.Verdicts = []*pb.TestVerdict{{ 694 TestId: "testid", 695 VariantHash: pbutil.VariantHash(var1), 696 InvocationId: "invocation-123", 697 Status: pb.TestVerdictStatus_EXPECTED, 698 PartitionTime: timestamppb.New(time.Unix(1000, 0)), 699 PassedAvgDuration: durationpb.New(time.Duration(1) * time.Millisecond), 700 Changelists: []*pb.Changelist{}, 701 }} 702 } 703 cwvs = append(cwvs, cwv) 704 } 705 // Query commits 1100 to 990 (111 commits). Next page will start from 989. 706 nextPageToken := pagination.Token(fmt.Sprintf("%d", 989)) 707 So(res, ShouldResembleProto, &pb.QuerySourcePositionsResponse{ 708 SourcePositions: cwvs, 709 NextPageToken: nextPageToken, 710 }) 711 }) 712 713 }) 714 715 }) 716 } 717 718 func TestValidateQuerySourcePositionsRequest(t *testing.T) { 719 t.Parallel() 720 721 Convey("validateQuerySourcePositionsRequest", t, func() { 722 ref := &pb.SourceRef{ 723 System: &pb.SourceRef_Gitiles{ 724 Gitiles: &pb.GitilesRef{ 725 Host: "host", 726 Project: "project", 727 Ref: "ref", 728 }, 729 }, 730 } 731 refhash := hex.EncodeToString(pbutil.SourceRefHash(ref)) 732 req := &pb.QuerySourcePositionsRequest{ 733 Project: "project", 734 TestId: "testid", 735 VariantHash: pbutil.VariantHash(pbutil.Variant("key1", "val1", "key2", "val1")), 736 RefHash: refhash, 737 StartSourcePosition: 110, 738 PageToken: "", 739 PageSize: 1, 740 } 741 742 Convey("valid", func() { 743 err := validateQuerySourcePositionsRequest(req) 744 So(err, ShouldBeNil) 745 }) 746 747 Convey("no project", func() { 748 req.Project = "" 749 err := validateQuerySourcePositionsRequest(req) 750 So(err, ShouldErrLike, "project: unspecified") 751 }) 752 753 Convey("invalid project", func() { 754 req.Project = "project:realm" 755 err := validateQuerySourcePositionsRequest(req) 756 So(err, ShouldErrLike, `project: must match ^[a-z0-9\-]{1,40}$`) 757 }) 758 759 Convey("no test id", func() { 760 req.TestId = "" 761 err := validateQuerySourcePositionsRequest(req) 762 So(err, ShouldErrLike, "test_id: unspecified") 763 }) 764 765 Convey("invalid test id", func() { 766 req.TestId = "\xFF" 767 err := validateQuerySourcePositionsRequest(req) 768 So(err, ShouldErrLike, "test_id: not a valid utf8 string") 769 }) 770 771 Convey("invalid variant hash", func() { 772 req.VariantHash = "invalid" 773 err := validateQuerySourcePositionsRequest(req) 774 So(err, ShouldErrLike, "variant_hash", "must match ^[0-9a-f]{16}$") 775 }) 776 777 Convey("invalid ref hash", func() { 778 req.RefHash = "invalid" 779 err := validateQuerySourcePositionsRequest(req) 780 So(err, ShouldErrLike, "ref_hash:", "must match ^[0-9a-f]{16}$") 781 }) 782 783 Convey("invalid start commit position", func() { 784 req.StartSourcePosition = 0 785 err := validateQuerySourcePositionsRequest(req) 786 So(err, ShouldErrLike, "start_source_position: must be a positive number") 787 }) 788 789 Convey("no page size", func() { 790 req.PageSize = 0 791 err := validateQuerySourcePositionsRequest(req) 792 So(err, ShouldBeNil) 793 }) 794 795 Convey("negative page size", func() { 796 req.PageSize = -1 797 err := validateQuerySourcePositionsRequest(req) 798 So(err, ShouldErrLike, "page_size", "negative") 799 }) 800 }) 801 } 802 803 func adminContext(ctx context.Context) context.Context { 804 return auth.WithState(ctx, &authtest.FakeState{ 805 Identity: "user:admin@example.com", 806 IdentityGroups: []string{"service-luci-analysis-admins", "luci-analysis-access"}, 807 }) 808 }