github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/resultstore/resultstore_test.go (about) 1 /* 2 Copyright 2023 The TestGrid Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package resultstore fetches and process results from ResultStore. 18 package resultstore 19 20 import ( 21 "context" 22 "fmt" 23 "regexp" 24 "sync" 25 "testing" 26 "time" 27 28 configpb "github.com/GoogleCloudPlatform/testgrid/pb/config" 29 evalpb "github.com/GoogleCloudPlatform/testgrid/pb/custom_evaluator" 30 statepb "github.com/GoogleCloudPlatform/testgrid/pb/state" 31 teststatuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status" 32 "github.com/GoogleCloudPlatform/testgrid/pkg/updater" 33 durationpb "github.com/golang/protobuf/ptypes/duration" 34 timestamppb "github.com/golang/protobuf/ptypes/timestamp" 35 "github.com/google/go-cmp/cmp" 36 "github.com/google/go-cmp/cmp/cmpopts" 37 "github.com/sirupsen/logrus" 38 "google.golang.org/genproto/googleapis/devtools/resultstore/v2" 39 "google.golang.org/grpc" 40 "google.golang.org/protobuf/testing/protocmp" 41 ) 42 43 type fakeClient struct { 44 searches map[string][]string 45 invocations map[string]FetchResult 46 } 47 48 func (c *fakeClient) search(query string) ([]string, error) { 49 notFound := fmt.Errorf("no results found for %q", query) 50 if c.searches == nil { 51 return nil, notFound 52 } 53 invocationIDs, ok := c.searches[query] 54 if !ok { 55 return nil, notFound 56 } 57 return invocationIDs, nil 58 } 59 60 func (c *fakeClient) SearchInvocations(ctx context.Context, req *resultstore.SearchInvocationsRequest, opts ...grpc.CallOption) (*resultstore.SearchInvocationsResponse, error) { 61 invocationIDs, err := c.search(req.GetQuery()) 62 if err != nil { 63 return nil, err 64 } 65 var invocations []*resultstore.Invocation 66 for _, invocationID := range invocationIDs { 67 invoc := &resultstore.Invocation{ 68 Id: &resultstore.Invocation_Id{InvocationId: invocationID}, 69 } 70 invocations = append(invocations, invoc) 71 } 72 return &resultstore.SearchInvocationsResponse{Invocations: invocations}, nil 73 } 74 75 func (c *fakeClient) SearchConfiguredTargets(ctx context.Context, req *resultstore.SearchConfiguredTargetsRequest, opts ...grpc.CallOption) (*resultstore.SearchConfiguredTargetsResponse, error) { 76 invocationIDs, err := c.search(req.GetQuery()) 77 if err != nil { 78 return nil, err 79 } 80 var configuredTargets []*resultstore.ConfiguredTarget 81 for _, invocationID := range invocationIDs { 82 configuredTarget := &resultstore.ConfiguredTarget{ 83 Id: &resultstore.ConfiguredTarget_Id{InvocationId: invocationID}, 84 } 85 configuredTargets = append(configuredTargets, configuredTarget) 86 } 87 return &resultstore.SearchConfiguredTargetsResponse{ConfiguredTargets: configuredTargets}, nil 88 } 89 90 func (c *fakeClient) ExportInvocation(ctx context.Context, req *resultstore.ExportInvocationRequest, opts ...grpc.CallOption) (*resultstore.ExportInvocationResponse, error) { 91 notFound := fmt.Errorf("no result found for invocation %q", req.GetName()) 92 if c.invocations == nil { 93 return nil, notFound 94 } 95 result, ok := c.invocations[req.GetName()] 96 if !ok { 97 return nil, notFound 98 } 99 return &resultstore.ExportInvocationResponse{ 100 Invocation: result.Invocation, 101 Actions: result.Actions, 102 ConfiguredTargets: result.ConfiguredTargets, 103 Targets: result.Targets, 104 }, nil 105 } 106 107 func invocationName(invocationID string) string { 108 return fmt.Sprintf("invocations/%s", invocationID) 109 } 110 111 func targetName(targetID, invocationID string) string { 112 return fmt.Sprintf("invocations/%s/targets/%s", invocationID, targetID) 113 } 114 115 func timeMustText(t time.Time) string { 116 s, err := t.MarshalText() 117 if err != nil { 118 panic("timeMustText() panicked") 119 } 120 return string(s) 121 } 122 123 func TestExtractGroupID(t *testing.T) { 124 cases := []struct { 125 name string 126 tg *configpb.TestGroup 127 pr *invocation 128 want string 129 }{ 130 { 131 name: "nil", 132 }, 133 { 134 name: "primary grouping BUILD by override config value", 135 tg: &configpb.TestGroup{ 136 DaysOfResults: 7, 137 BuildOverrideConfigurationValue: "test-key-1", 138 PrimaryGrouping: configpb.TestGroup_PRIMARY_GROUPING_BUILD, 139 }, 140 pr: &invocation{ 141 InvocationProto: &resultstore.Invocation{ 142 Id: &resultstore.Invocation_Id{ 143 InvocationId: "id-1", 144 }, 145 Properties: []*resultstore.Property{ 146 { 147 Key: "test-key-1", 148 Value: "test-val-1", 149 }, 150 }, 151 Name: invocationName("id-1"), 152 Timing: &resultstore.Timing{ 153 StartTime: ×tamppb.Timestamp{ 154 Seconds: 1234, 155 }, 156 }, 157 }, 158 }, 159 want: "test-val-1", 160 }, 161 { 162 name: "fallback grouping BUILD resort to default", 163 tg: &configpb.TestGroup{ 164 DaysOfResults: 7, 165 BuildOverrideConfigurationValue: "test-key-1", 166 FallbackGrouping: configpb.TestGroup_FALLBACK_GROUPING_BUILD, 167 }, 168 pr: &invocation{ 169 InvocationProto: &resultstore.Invocation{ 170 Id: &resultstore.Invocation_Id{ 171 InvocationId: "id-1", 172 }, 173 Properties: []*resultstore.Property{ 174 { 175 Key: "test-key-1", 176 Value: "test-val-1", 177 }, 178 }, 179 Name: invocationName("id-1"), 180 Timing: &resultstore.Timing{ 181 StartTime: ×tamppb.Timestamp{ 182 Seconds: 1234, 183 }, 184 }, 185 }, 186 }, 187 want: "id-1", 188 }, 189 } 190 for _, tc := range cases { 191 t.Run(tc.name, func(t *testing.T) { 192 got := extractGroupID(tc.tg, tc.pr) 193 if diff := cmp.Diff(tc.want, got); diff != "" { 194 t.Errorf("extractGroupID() differed (-want, +got): %s", diff) 195 } 196 }) 197 } 198 } 199 200 func TestColumnReader(t *testing.T) { 201 // We already have functions testing 'stop' logic. 202 // Scope this test to whether the column reader fetches and returns ascending results. 203 oneMonthConfig := &configpb.TestGroup{ 204 Name: "a-test-group", 205 DaysOfResults: 30, 206 } 207 now := time.Now() 208 oneDayAgo := now.AddDate(0, 0, -1) 209 twoDaysAgo := now.AddDate(0, 0, -2) 210 threeDaysAgo := now.AddDate(0, 0, -3) 211 oneMonthAgo := now.AddDate(0, 0, -30) 212 testQueryAfter := queryAfter(prowLabel, oneMonthAgo) 213 cases := []struct { 214 name string 215 client *fakeClient 216 tg *configpb.TestGroup 217 want []updater.InflatedColumn 218 wantErr bool 219 }{ 220 { 221 name: "empty", 222 tg: oneMonthConfig, 223 wantErr: true, 224 }, 225 { 226 name: "basic", 227 tg: &configpb.TestGroup{ 228 DaysOfResults: 30, 229 }, 230 client: &fakeClient{ 231 searches: map[string][]string{ 232 testQueryAfter: {"id-1", "id-2"}, 233 }, 234 invocations: map[string]FetchResult{ 235 invocationName("id-1"): { 236 Invocation: &resultstore.Invocation{ 237 Id: &resultstore.Invocation_Id{ 238 InvocationId: "id-1", 239 }, 240 Name: invocationName("id-1"), 241 Timing: &resultstore.Timing{ 242 StartTime: ×tamppb.Timestamp{ 243 Seconds: oneDayAgo.Unix(), 244 }, 245 }, 246 }, 247 Targets: []*resultstore.Target{ 248 { 249 Id: &resultstore.Target_Id{ 250 TargetId: "tgt-id-1", 251 }, 252 StatusAttributes: &resultstore.StatusAttributes{ 253 Status: resultstore.Status_PASSED, 254 }, 255 }, 256 }, 257 ConfiguredTargets: []*resultstore.ConfiguredTarget{ 258 { 259 Id: &resultstore.ConfiguredTarget_Id{ 260 TargetId: "tgt-id-1", 261 }, 262 StatusAttributes: &resultstore.StatusAttributes{ 263 Status: resultstore.Status_PASSED, 264 }, 265 }, 266 }, 267 Actions: []*resultstore.Action{ 268 { 269 Id: &resultstore.Action_Id{ 270 TargetId: "tgt-id-1", 271 ActionId: "build", 272 }, 273 }, 274 }, 275 }, 276 invocationName("id-2"): { 277 Invocation: &resultstore.Invocation{ 278 Id: &resultstore.Invocation_Id{ 279 InvocationId: "id-2", 280 }, 281 Name: invocationName("id-2"), 282 Timing: &resultstore.Timing{ 283 StartTime: ×tamppb.Timestamp{ 284 Seconds: twoDaysAgo.Unix(), 285 }, 286 }, 287 }, 288 Targets: []*resultstore.Target{ 289 { 290 Id: &resultstore.Target_Id{ 291 TargetId: "tgt-id-1", 292 }, 293 StatusAttributes: &resultstore.StatusAttributes{ 294 Status: resultstore.Status_FAILED, 295 }, 296 }, 297 }, 298 ConfiguredTargets: []*resultstore.ConfiguredTarget{ 299 { 300 Id: &resultstore.ConfiguredTarget_Id{ 301 TargetId: "tgt-id-1", 302 }, 303 StatusAttributes: &resultstore.StatusAttributes{ 304 Status: resultstore.Status_FAILED, 305 }, 306 }, 307 }, 308 Actions: []*resultstore.Action{ 309 { 310 Id: &resultstore.Action_Id{ 311 TargetId: "tgt-id-1", 312 ActionId: "build", 313 }, 314 }, 315 }, 316 }, 317 }, 318 }, 319 want: []updater.InflatedColumn{ 320 { 321 Column: &statepb.Column{ 322 Build: "id-1", 323 Name: "id-1", 324 Started: float64(oneDayAgo.Unix() * 1000), 325 Hint: timeMustText(oneDayAgo.Local().Truncate(time.Second)), 326 }, 327 Cells: map[string]updater.Cell{ 328 "tgt-id-1": { 329 ID: "tgt-id-1", 330 CellID: "id-1", 331 Result: teststatuspb.TestStatus_PASS, 332 }, 333 }, 334 }, 335 { 336 Column: &statepb.Column{ 337 Build: "id-2", 338 Name: "id-2", 339 Started: float64(twoDaysAgo.Unix() * 1000), 340 Hint: timeMustText(twoDaysAgo.Truncate(time.Second)), 341 }, 342 Cells: map[string]updater.Cell{ 343 "tgt-id-1": { 344 ID: "tgt-id-1", 345 CellID: "id-2", 346 Result: teststatuspb.TestStatus_FAIL, 347 }, 348 }, 349 }, 350 }, 351 }, 352 { 353 name: "no results from query", 354 tg: oneMonthConfig, 355 client: &fakeClient{ 356 searches: map[string][]string{}, 357 invocations: map[string]FetchResult{ 358 invocationName("id-1"): { 359 Invocation: &resultstore.Invocation{ 360 Id: &resultstore.Invocation_Id{ 361 InvocationId: "id-1", 362 }, 363 Name: invocationName("id-1"), 364 Timing: &resultstore.Timing{ 365 StartTime: ×tamppb.Timestamp{ 366 Seconds: oneDayAgo.Unix(), 367 }, 368 }, 369 }, 370 }, 371 invocationName("id-2"): { 372 Invocation: &resultstore.Invocation{ 373 Id: &resultstore.Invocation_Id{ 374 InvocationId: "id-2", 375 }, 376 Name: invocationName("id-2"), 377 Timing: &resultstore.Timing{ 378 StartTime: ×tamppb.Timestamp{ 379 Seconds: twoDaysAgo.Unix(), 380 }, 381 }, 382 }, 383 }, 384 invocationName("id-3"): { 385 Invocation: &resultstore.Invocation{ 386 Id: &resultstore.Invocation_Id{ 387 InvocationId: "id-3", 388 }, 389 Name: invocationName("id-3"), 390 Timing: &resultstore.Timing{ 391 StartTime: ×tamppb.Timestamp{ 392 Seconds: threeDaysAgo.Unix(), 393 }, 394 }, 395 }, 396 }, 397 }, 398 }, 399 wantErr: true, 400 }, 401 { 402 name: "no invocations found", 403 client: &fakeClient{ 404 searches: map[string][]string{ 405 testQueryAfter: {"id-2", "id-3", "id-1"}, 406 }, 407 invocations: map[string]FetchResult{}, 408 }, 409 want: nil, 410 }, 411 { 412 name: "ids not in order", 413 client: &fakeClient{ 414 searches: map[string][]string{ 415 testQueryAfter: {"id-2", "id-3", "id-1"}, 416 }, 417 invocations: map[string]FetchResult{ 418 invocationName("id-1"): { 419 Invocation: &resultstore.Invocation{ 420 Id: &resultstore.Invocation_Id{ 421 InvocationId: "id-1", 422 }, 423 Name: invocationName("id-1"), 424 Timing: &resultstore.Timing{ 425 StartTime: ×tamppb.Timestamp{ 426 Seconds: oneDayAgo.Unix(), 427 }, 428 }, 429 }, 430 }, 431 invocationName("id-2"): { 432 Invocation: &resultstore.Invocation{ 433 Id: &resultstore.Invocation_Id{ 434 InvocationId: "id-2", 435 }, 436 Name: invocationName("id-2"), 437 Timing: &resultstore.Timing{ 438 StartTime: ×tamppb.Timestamp{ 439 Seconds: twoDaysAgo.Unix(), 440 }, 441 }, 442 }, 443 }, 444 invocationName("id-3"): { 445 Invocation: &resultstore.Invocation{ 446 Id: &resultstore.Invocation_Id{ 447 InvocationId: "id-3", 448 }, 449 Name: invocationName("id-3"), 450 Timing: &resultstore.Timing{ 451 StartTime: ×tamppb.Timestamp{ 452 Seconds: threeDaysAgo.Unix(), 453 }, 454 }, 455 }, 456 }, 457 }, 458 }, 459 want: []updater.InflatedColumn{ 460 { 461 Column: &statepb.Column{ 462 Build: "id-1", 463 Name: "id-1", 464 Started: float64(oneDayAgo.Unix() * 1000), 465 Hint: timeMustText(oneDayAgo.Truncate(time.Second)), 466 }, 467 Cells: map[string]updater.Cell{}, 468 }, 469 { 470 Column: &statepb.Column{ 471 Build: "id-2", 472 Name: "id-2", 473 Started: float64(twoDaysAgo.Unix() * 1000), 474 Hint: timeMustText(twoDaysAgo.Truncate(time.Second)), 475 }, 476 Cells: map[string]updater.Cell{}, 477 }, 478 { 479 Column: &statepb.Column{ 480 Build: "id-3", 481 Name: "id-3", 482 Started: float64(threeDaysAgo.Unix() * 1000), 483 Hint: timeMustText(threeDaysAgo.Truncate(time.Second)), 484 }, 485 Cells: map[string]updater.Cell{}, 486 }, 487 }, 488 }, 489 } 490 for _, tc := range cases { 491 t.Run(tc.name, func(t *testing.T) { 492 var dlClient *DownloadClient 493 if tc.client != nil { 494 dlClient = &DownloadClient{client: tc.client} 495 } 496 columnReader := ColumnReader(dlClient, 0) 497 var got []updater.InflatedColumn 498 ch := make(chan updater.InflatedColumn) 499 var wg sync.WaitGroup 500 wg.Add(1) 501 go func() { 502 defer wg.Done() 503 for col := range ch { 504 got = append(got, col) 505 } 506 }() 507 err := columnReader(context.Background(), logrus.WithField("case", tc.name), oneMonthConfig, nil, oneMonthAgo, ch) 508 close(ch) 509 wg.Wait() 510 if err != nil && !tc.wantErr { 511 t.Errorf("columnReader() errored: %v", err) 512 } else if err == nil && tc.wantErr { 513 t.Errorf("columnReader() did not error as expected") 514 } 515 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 516 t.Errorf("columnReader() differed (-want, +got): %s", diff) 517 } 518 }) 519 } 520 } 521 522 func TestCellMessageIcon(t *testing.T) { 523 cases := []struct { 524 name string 525 annotations []*configpb.TestGroup_TestAnnotation 526 properties map[string][]string 527 tags []string 528 message string 529 icon string 530 }{ 531 { 532 name: "basically works", 533 }, 534 { 535 name: "find annotation from property", 536 annotations: []*configpb.TestGroup_TestAnnotation{ 537 { 538 ShortText: "icon", 539 ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{ 540 PropertyName: "props", 541 }, 542 }, 543 }, 544 properties: map[string][]string{ 545 "props": {"first", "second"}, 546 }, 547 message: "first", 548 icon: "icon", 549 }, 550 { 551 name: "find annotation from tag", 552 annotations: []*configpb.TestGroup_TestAnnotation{ 553 { 554 ShortText: "icon", 555 ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{ 556 PropertyName: "actually-a-tag", 557 }, 558 }, 559 }, 560 tags: []string{"actually-a-tag"}, 561 message: "actually-a-tag", 562 icon: "icon", 563 }, 564 { 565 name: "find annotation from tag", 566 annotations: []*configpb.TestGroup_TestAnnotation{ 567 { 568 ShortText: "icon", 569 ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{ 570 PropertyName: "actually-a-tag", 571 }, 572 }, 573 { 574 ShortText: "icon-2", 575 ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{ 576 PropertyName: "actually-a-tag-2", 577 }, 578 }, 579 }, 580 tags: []string{"actually-a-tag"}, 581 message: "actually-a-tag", 582 icon: "icon", 583 }, 584 } 585 586 for _, tc := range cases { 587 message, icon := cellMessageIcon(tc.annotations, tc.properties, tc.tags) 588 if tc.message != message { 589 t.Errorf("cellMessageIcon() got unexpected message %q, want %q", message, tc.message) 590 } 591 if tc.icon != icon { 592 t.Errorf("cellMessageIcon() got unexpected icon %q, want %q", icon, tc.icon) 593 } 594 } 595 } 596 597 func TestTimestampMilliseconds(t *testing.T) { 598 cases := []struct { 599 name string 600 timestamp *timestamppb.Timestamp 601 want float64 602 }{ 603 { 604 name: "nil", 605 timestamp: nil, 606 want: 0, 607 }, 608 { 609 name: "zero", 610 timestamp: ×tamppb.Timestamp{}, 611 want: 0, 612 }, 613 { 614 name: "basic", 615 timestamp: ×tamppb.Timestamp{ 616 Seconds: 1234, 617 Nanos: 5678, 618 }, 619 want: 1234005.678, 620 }, 621 } 622 for _, tc := range cases { 623 t.Run(tc.name, func(t *testing.T) { 624 got := timestampMilliseconds(tc.timestamp) 625 approx := cmpopts.EquateApprox(.01, 0) 626 if diff := cmp.Diff(tc.want, got, approx); diff != "" { 627 t.Errorf("timestampMilliseconds(%v) differed (-want, +got): %s", tc.timestamp, diff) 628 } 629 }) 630 } 631 } 632 633 func TestProcessRawResult(t *testing.T) { 634 cases := []struct { 635 name string 636 result *FetchResult 637 want *invocation 638 }{ 639 { 640 name: "just invocation", 641 result: &FetchResult{ 642 Invocation: &resultstore.Invocation{ 643 Name: invocationName("Best invocation"), 644 Id: &resultstore.Invocation_Id{ 645 InvocationId: "uuid-222", 646 }, 647 }, 648 }, 649 want: &invocation{ 650 InvocationProto: &resultstore.Invocation{ 651 Name: invocationName("Best invocation"), 652 Id: &resultstore.Invocation_Id{ 653 InvocationId: "uuid-222", 654 }, 655 }, 656 TargetResults: make(map[string][]*singleActionResult), 657 }, 658 }, 659 { 660 name: "invocation + targets + configured targets", 661 result: &FetchResult{ 662 Invocation: &resultstore.Invocation{ 663 Name: invocationName("Best invocation"), 664 Id: &resultstore.Invocation_Id{ 665 InvocationId: "uuid-222", 666 }, 667 }, 668 Targets: []*resultstore.Target{ 669 { 670 Name: targetName("updater", "uuid-222"), 671 Id: &resultstore.Target_Id{ 672 InvocationId: "uuid-222", 673 TargetId: "tgt-uuid-1", 674 }, 675 }, 676 { 677 Name: targetName("tabulator", "uuid-222"), 678 Id: &resultstore.Target_Id{ 679 InvocationId: "uuid-222", 680 TargetId: "tgt-uuid-2", 681 }, 682 }, 683 }, 684 ConfiguredTargets: []*resultstore.ConfiguredTarget{ 685 { 686 Name: targetName("updater", "uuid-222"), 687 Id: &resultstore.ConfiguredTarget_Id{ 688 InvocationId: "uuid-222", 689 TargetId: "tgt-uuid-1", 690 }, 691 }, 692 { 693 Name: targetName("tabulator", "uuid-222"), 694 Id: &resultstore.ConfiguredTarget_Id{ 695 InvocationId: "uuid-222", 696 TargetId: "tgt-uuid-2", 697 }, 698 }, 699 }, 700 }, 701 want: &invocation{ 702 InvocationProto: &resultstore.Invocation{ 703 Name: invocationName("Best invocation"), 704 Id: &resultstore.Invocation_Id{ 705 InvocationId: "uuid-222", 706 }, 707 }, 708 TargetResults: map[string][]*singleActionResult{ 709 "tgt-uuid-1": { 710 { 711 TargetProto: &resultstore.Target{ 712 Name: targetName("updater", "uuid-222"), 713 Id: &resultstore.Target_Id{ 714 InvocationId: "uuid-222", 715 TargetId: "tgt-uuid-1", 716 }, 717 }, 718 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 719 Name: targetName("updater", "uuid-222"), 720 Id: &resultstore.ConfiguredTarget_Id{ 721 InvocationId: "uuid-222", 722 TargetId: "tgt-uuid-1", 723 }, 724 }, 725 }, 726 }, 727 "tgt-uuid-2": { 728 { 729 TargetProto: &resultstore.Target{ 730 Name: targetName("tabulator", "uuid-222"), 731 Id: &resultstore.Target_Id{ 732 InvocationId: "uuid-222", 733 TargetId: "tgt-uuid-2", 734 }, 735 }, 736 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 737 Name: targetName("tabulator", "uuid-222"), 738 Id: &resultstore.ConfiguredTarget_Id{ 739 InvocationId: "uuid-222", 740 TargetId: "tgt-uuid-2", 741 }, 742 }, 743 }, 744 }, 745 }, 746 }, 747 }, 748 { 749 name: "all together + extra actions", 750 result: &FetchResult{ 751 Invocation: &resultstore.Invocation{ 752 Name: invocationName("Best invocation"), 753 Id: &resultstore.Invocation_Id{ 754 InvocationId: "uuid-222", 755 }, 756 }, 757 Targets: []*resultstore.Target{ 758 { 759 Name: "/testgrid/backend:updater", 760 Id: &resultstore.Target_Id{ 761 InvocationId: "uuid-222", 762 TargetId: "tgt-uuid-1", 763 }, 764 }, 765 { 766 Name: "/testgrid/backend:tabulator", 767 Id: &resultstore.Target_Id{ 768 InvocationId: "uuid-222", 769 TargetId: "tgt-uuid-2", 770 }, 771 }, 772 }, 773 ConfiguredTargets: []*resultstore.ConfiguredTarget{ 774 { 775 Name: "/testgrid/backend:updater", 776 Id: &resultstore.ConfiguredTarget_Id{ 777 InvocationId: "uuid-222", 778 TargetId: "tgt-uuid-1", 779 }, 780 }, 781 { 782 Name: "/testgrid/backend:tabulator", 783 Id: &resultstore.ConfiguredTarget_Id{ 784 InvocationId: "uuid-222", 785 TargetId: "tgt-uuid-2", 786 }, 787 }, 788 }, 789 Actions: []*resultstore.Action{ 790 { 791 Name: "/testgrid/backend:updater", 792 Id: &resultstore.Action_Id{ 793 InvocationId: "uuid-222", 794 TargetId: "tgt-uuid-1", 795 ActionId: "flying", 796 }, 797 }, 798 { 799 Name: "/testgrid/backend:tabulator", 800 Id: &resultstore.Action_Id{ 801 InvocationId: "uuid-222", 802 TargetId: "tgt-uuid-2", 803 ActionId: "walking", 804 }, 805 }, 806 { 807 Name: "/testgrid/backend:tabulator", 808 Id: &resultstore.Action_Id{ 809 InvocationId: "uuid-222", 810 TargetId: "tgt-uuid-2", 811 ActionId: "flying", 812 }, 813 }, 814 }, 815 }, 816 want: &invocation{ 817 InvocationProto: &resultstore.Invocation{ 818 Name: invocationName("Best invocation"), 819 Id: &resultstore.Invocation_Id{ 820 InvocationId: "uuid-222", 821 }, 822 }, 823 TargetResults: map[string][]*singleActionResult{ 824 "tgt-uuid-1": { 825 { 826 TargetProto: &resultstore.Target{ 827 Name: "/testgrid/backend:updater", 828 Id: &resultstore.Target_Id{ 829 InvocationId: "uuid-222", 830 TargetId: "tgt-uuid-1", 831 }, 832 }, 833 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 834 Name: "/testgrid/backend:updater", 835 Id: &resultstore.ConfiguredTarget_Id{ 836 InvocationId: "uuid-222", 837 TargetId: "tgt-uuid-1", 838 }, 839 }, 840 ActionProto: &resultstore.Action{ 841 Name: "/testgrid/backend:updater", 842 Id: &resultstore.Action_Id{ 843 InvocationId: "uuid-222", 844 TargetId: "tgt-uuid-1", 845 ActionId: "flying", 846 }, 847 }, 848 }, 849 }, 850 "tgt-uuid-2": { 851 { 852 TargetProto: &resultstore.Target{ 853 Name: "/testgrid/backend:tabulator", 854 Id: &resultstore.Target_Id{ 855 InvocationId: "uuid-222", 856 TargetId: "tgt-uuid-2", 857 }, 858 }, 859 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 860 Name: "/testgrid/backend:tabulator", 861 Id: &resultstore.ConfiguredTarget_Id{ 862 InvocationId: "uuid-222", 863 TargetId: "tgt-uuid-2", 864 }, 865 }, 866 ActionProto: &resultstore.Action{ 867 Name: "/testgrid/backend:tabulator", 868 Id: &resultstore.Action_Id{ 869 InvocationId: "uuid-222", 870 TargetId: "tgt-uuid-2", 871 ActionId: "walking", 872 }, 873 }, 874 }, { 875 TargetProto: &resultstore.Target{ 876 Name: "/testgrid/backend:tabulator", 877 Id: &resultstore.Target_Id{ 878 InvocationId: "uuid-222", 879 TargetId: "tgt-uuid-2", 880 }, 881 }, 882 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 883 Name: "/testgrid/backend:tabulator", 884 Id: &resultstore.ConfiguredTarget_Id{ 885 InvocationId: "uuid-222", 886 TargetId: "tgt-uuid-2", 887 }, 888 }, 889 ActionProto: &resultstore.Action{ 890 Name: "/testgrid/backend:tabulator", 891 Id: &resultstore.Action_Id{ 892 InvocationId: "uuid-222", 893 TargetId: "tgt-uuid-2", 894 ActionId: "flying", 895 }, 896 }, 897 }, 898 }, 899 }, 900 }, 901 }, 902 } 903 904 for _, tc := range cases { 905 t.Run(tc.name, func(t *testing.T) { 906 got := processRawResult(logrus.WithField("case", tc.name), tc.result) 907 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 908 t.Errorf("processRawResult(...) differed (-want, +got): %s", diff) 909 } 910 }) 911 } 912 913 } 914 func TestProcessGroup(t *testing.T) { 915 916 cases := []struct { 917 name string 918 tg *configpb.TestGroup 919 group *invocationGroup 920 want *updater.InflatedColumn 921 }{ 922 { 923 name: "nil", 924 want: nil, 925 }, 926 { 927 name: "empty", 928 group: &invocationGroup{}, 929 want: nil, 930 }, 931 { 932 name: "basic invocation group", 933 group: &invocationGroup{ 934 GroupID: "uuid-123", 935 Invocations: []*invocation{ 936 { 937 InvocationProto: &resultstore.Invocation{ 938 Name: invocationName("uuid-123"), 939 Id: &resultstore.Invocation_Id{ 940 InvocationId: "uuid-123", 941 }, 942 Timing: &resultstore.Timing{ 943 StartTime: ×tamppb.Timestamp{ 944 Seconds: 1234, 945 }, 946 }, 947 }, 948 TargetResults: map[string][]*singleActionResult{ 949 "tgt-id-1": { 950 { 951 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 952 Id: &resultstore.ConfiguredTarget_Id{ 953 TargetId: "tgt-id-1", 954 }, 955 StatusAttributes: &resultstore.StatusAttributes{ 956 Status: resultstore.Status_PASSED, 957 }, 958 }, 959 }, 960 }, 961 "tgt-id-2": { 962 { 963 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 964 Id: &resultstore.ConfiguredTarget_Id{ 965 TargetId: "tgt-id-2", 966 }, 967 StatusAttributes: &resultstore.StatusAttributes{ 968 Status: resultstore.Status_FAILED, 969 }, 970 }, 971 }, 972 }, 973 }, 974 }, 975 }, 976 }, 977 want: &updater.InflatedColumn{ 978 Column: &statepb.Column{ 979 Name: "uuid-123", 980 Build: "uuid-123", 981 Started: 1234000, 982 Hint: "1970-01-01T00:20:34Z", 983 }, 984 Cells: map[string]updater.Cell{ 985 "tgt-id-1": { 986 ID: "tgt-id-1", 987 CellID: "uuid-123", 988 Result: teststatuspb.TestStatus_PASS, 989 }, 990 "tgt-id-2": { 991 ID: "tgt-id-2", 992 CellID: "uuid-123", 993 Result: teststatuspb.TestStatus_FAIL, 994 }, 995 }, 996 }, 997 }, 998 { 999 name: "advanced invocation group with several invocations and repeated targets", 1000 tg: &configpb.TestGroup{ 1001 BuildOverrideConfigurationValue: "pi-key-chu", 1002 }, 1003 group: &invocationGroup{ 1004 GroupID: "snorlax", 1005 Invocations: []*invocation{ 1006 { 1007 InvocationProto: &resultstore.Invocation{ 1008 Name: invocationName("uuid-123"), 1009 Id: &resultstore.Invocation_Id{ 1010 InvocationId: "uuid-123", 1011 }, 1012 Timing: &resultstore.Timing{ 1013 StartTime: ×tamppb.Timestamp{ 1014 Seconds: 1234, 1015 }, 1016 }, 1017 Properties: []*resultstore.Property{ 1018 { 1019 Key: "pi-key-chu", 1020 Value: "snorlax", 1021 }, 1022 }, 1023 }, 1024 TargetResults: map[string][]*singleActionResult{ 1025 "tgt-id-1": { 1026 { 1027 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 1028 Id: &resultstore.ConfiguredTarget_Id{ 1029 TargetId: "tgt-id-1", 1030 }, 1031 StatusAttributes: &resultstore.StatusAttributes{ 1032 Status: resultstore.Status_PASSED, 1033 }, 1034 }, 1035 }, 1036 }, 1037 "tgt-id-2": { 1038 { 1039 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 1040 Id: &resultstore.ConfiguredTarget_Id{ 1041 TargetId: "tgt-id-2", 1042 }, 1043 StatusAttributes: &resultstore.StatusAttributes{ 1044 Status: resultstore.Status_FAILED, 1045 }, 1046 }, 1047 }, 1048 }, 1049 }, 1050 }, 1051 { 1052 InvocationProto: &resultstore.Invocation{ 1053 Name: invocationName("uuid-124"), 1054 Id: &resultstore.Invocation_Id{ 1055 InvocationId: "uuid-124", 1056 }, 1057 Timing: &resultstore.Timing{ 1058 StartTime: ×tamppb.Timestamp{ 1059 Seconds: 1334, 1060 }, 1061 }, 1062 Properties: []*resultstore.Property{ 1063 { 1064 Key: "pi-key-chu", 1065 Value: "snorlax", 1066 }, 1067 }, 1068 }, 1069 TargetResults: map[string][]*singleActionResult{ 1070 "tgt-id-1": { 1071 { 1072 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 1073 Id: &resultstore.ConfiguredTarget_Id{ 1074 TargetId: "tgt-id-1", 1075 }, 1076 StatusAttributes: &resultstore.StatusAttributes{ 1077 Status: resultstore.Status_PASSED, 1078 }, 1079 }, 1080 }, 1081 }, 1082 "tgt-id-2": { 1083 { 1084 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 1085 Id: &resultstore.ConfiguredTarget_Id{ 1086 TargetId: "tgt-id-2", 1087 }, 1088 StatusAttributes: &resultstore.StatusAttributes{ 1089 Status: resultstore.Status_FAILED, 1090 }, 1091 }, 1092 }, 1093 }, 1094 }, 1095 }, 1096 }, 1097 }, 1098 want: &updater.InflatedColumn{ 1099 Column: &statepb.Column{ 1100 Name: "snorlax", 1101 Build: "snorlax", 1102 Started: 1234000, 1103 Hint: "1970-01-01T00:22:14Z", 1104 }, 1105 Cells: map[string]updater.Cell{ 1106 "tgt-id-1": { 1107 ID: "tgt-id-1", 1108 CellID: "uuid-123", 1109 Result: teststatuspb.TestStatus_PASS, 1110 }, 1111 "tgt-id-2": { 1112 ID: "tgt-id-2", 1113 CellID: "uuid-123", 1114 Result: teststatuspb.TestStatus_FAIL, 1115 }, 1116 "tgt-id-1 [1]": { 1117 ID: "tgt-id-1", 1118 CellID: "uuid-124", 1119 Result: teststatuspb.TestStatus_PASS, 1120 }, 1121 "tgt-id-2 [1]": { 1122 ID: "tgt-id-2", 1123 CellID: "uuid-124", 1124 Result: teststatuspb.TestStatus_FAIL, 1125 }, 1126 }, 1127 }, 1128 }, 1129 { 1130 name: "invocation group with single invocation and disabled test result", 1131 tg: &configpb.TestGroup{ 1132 BuildOverrideConfigurationValue: "pi-key-chu", 1133 MaxTestMethodsPerTest: 10, 1134 EnableTestMethods: true, 1135 }, 1136 group: &invocationGroup{ 1137 GroupID: "snorlax", 1138 Invocations: []*invocation{ 1139 { 1140 InvocationProto: &resultstore.Invocation{ 1141 Name: invocationName("uuid-123"), 1142 Id: &resultstore.Invocation_Id{ 1143 InvocationId: "uuid-123", 1144 }, 1145 Timing: &resultstore.Timing{ 1146 StartTime: ×tamppb.Timestamp{ 1147 Seconds: 1234, 1148 }, 1149 }, 1150 Properties: []*resultstore.Property{ 1151 { 1152 Key: "pi-key-chu", 1153 Value: "snorlax", 1154 }, 1155 }, 1156 }, 1157 TargetResults: map[string][]*singleActionResult{ 1158 "tgt-id-1": { 1159 { 1160 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 1161 Id: &resultstore.ConfiguredTarget_Id{ 1162 TargetId: "tgt-id-1", 1163 }, 1164 StatusAttributes: &resultstore.StatusAttributes{ 1165 Status: resultstore.Status_PASSED, 1166 }, 1167 }, 1168 ActionProto: &resultstore.Action{ 1169 ActionType: &resultstore.Action_TestAction{ 1170 TestAction: &resultstore.TestAction{ 1171 TestSuite: &resultstore.TestSuite{ 1172 SuiteName: "TestDetectJSError", 1173 Tests: []*resultstore.Test{ 1174 { 1175 TestType: &resultstore.Test_TestCase{ 1176 TestCase: &resultstore.TestCase{ 1177 CaseName: "DISABLED_case", 1178 Result: resultstore.TestCase_SKIPPED, 1179 }, 1180 }, 1181 }, 1182 }, 1183 }, 1184 }, 1185 }, 1186 }, 1187 }, 1188 }, 1189 }, 1190 }, 1191 }, 1192 }, 1193 want: &updater.InflatedColumn{ 1194 Column: &statepb.Column{ 1195 Name: "snorlax", 1196 Build: "snorlax", 1197 Started: 1234000, 1198 Hint: "1970-01-01T00:20:34Z", 1199 }, 1200 Cells: map[string]updater.Cell{ 1201 "tgt-id-1": { 1202 ID: "tgt-id-1", 1203 CellID: "uuid-123", 1204 Result: teststatuspb.TestStatus_PASS, 1205 }, 1206 "tgt-id-1@TESTGRID@DISABLED_case": { 1207 Result: teststatuspb.TestStatus_PASS_WITH_SKIPS, 1208 ID: "tgt-id-1", 1209 CellID: "uuid-123"}, 1210 }, 1211 }, 1212 }, 1213 { 1214 name: "invocation group with single invocation and test result with failure and error", 1215 tg: &configpb.TestGroup{ 1216 BuildOverrideConfigurationValue: "pi-key-chu", 1217 MaxTestMethodsPerTest: 10, 1218 EnableTestMethods: true, 1219 }, 1220 group: &invocationGroup{ 1221 GroupID: "snorlax", 1222 Invocations: []*invocation{ 1223 { 1224 InvocationProto: &resultstore.Invocation{ 1225 Name: invocationName("uuid-123"), 1226 Id: &resultstore.Invocation_Id{ 1227 InvocationId: "uuid-123", 1228 }, 1229 Timing: &resultstore.Timing{ 1230 StartTime: ×tamppb.Timestamp{ 1231 Seconds: 1234, 1232 }, 1233 }, 1234 Properties: []*resultstore.Property{ 1235 { 1236 Key: "pi-key-chu", 1237 Value: "snorlax", 1238 }, 1239 }, 1240 }, 1241 TargetResults: map[string][]*singleActionResult{ 1242 "tgt-id-1": { 1243 { 1244 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 1245 Id: &resultstore.ConfiguredTarget_Id{ 1246 TargetId: "tgt-id-1", 1247 }, 1248 StatusAttributes: &resultstore.StatusAttributes{ 1249 Status: resultstore.Status_PASSED, 1250 }, 1251 }, 1252 ActionProto: &resultstore.Action{ 1253 ActionType: &resultstore.Action_TestAction{ 1254 TestAction: &resultstore.TestAction{ 1255 TestSuite: &resultstore.TestSuite{ 1256 SuiteName: "TestDetectJSError", 1257 Tests: []*resultstore.Test{ 1258 { 1259 TestType: &resultstore.Test_TestCase{ 1260 TestCase: &resultstore.TestCase{ 1261 CaseName: "Not_working_case", 1262 Failures: []*resultstore.TestFailure{ 1263 { 1264 FailureMessage: "foo", 1265 }, 1266 }, 1267 Errors: []*resultstore.TestError{ 1268 { 1269 ErrorMessage: "bar", 1270 }, 1271 }, 1272 }, 1273 }, 1274 }, 1275 }, 1276 }, 1277 }, 1278 }, 1279 }, 1280 }, 1281 }, 1282 }, 1283 }, 1284 }, 1285 }, 1286 want: &updater.InflatedColumn{ 1287 Column: &statepb.Column{ 1288 Name: "snorlax", 1289 Build: "snorlax", 1290 Started: 1234000, 1291 Hint: "1970-01-01T00:20:34Z", 1292 }, 1293 Cells: map[string]updater.Cell{ 1294 "tgt-id-1": { 1295 ID: "tgt-id-1", 1296 CellID: "uuid-123", 1297 Result: teststatuspb.TestStatus_PASS, 1298 }, 1299 "tgt-id-1@TESTGRID@Not_working_case": { 1300 Result: teststatuspb.TestStatus_FAIL, 1301 ID: "tgt-id-1", 1302 CellID: "uuid-123"}, 1303 }, 1304 }, 1305 }, 1306 { 1307 name: "invocation group with ignored statuses and custom target status evaluator", 1308 tg: &configpb.TestGroup{ 1309 IgnorePending: true, 1310 CustomEvaluatorRuleSet: &evalpb.RuleSet{ 1311 Rules: []*evalpb.Rule{ 1312 { 1313 ComputedStatus: teststatuspb.TestStatus_CATEGORIZED_ABORT, 1314 TestResultComparisons: []*evalpb.TestResultComparison{ 1315 { 1316 TestResultInfo: &evalpb.TestResultComparison_TargetStatus{ 1317 TargetStatus: true, 1318 }, 1319 Comparison: &evalpb.Comparison{ 1320 Op: evalpb.Comparison_OP_EQ, 1321 ComparisonValue: &evalpb.Comparison_TargetStatusValue{ 1322 TargetStatusValue: teststatuspb.TestStatus_TIMED_OUT, 1323 }, 1324 }, 1325 }, 1326 }, 1327 }, 1328 }, 1329 }, 1330 }, 1331 group: &invocationGroup{ 1332 GroupID: "uuid-123", 1333 Invocations: []*invocation{ 1334 { 1335 InvocationProto: &resultstore.Invocation{ 1336 Name: invocationName("uuid-123"), 1337 Id: &resultstore.Invocation_Id{ 1338 InvocationId: "uuid-123", 1339 }, 1340 Timing: &resultstore.Timing{ 1341 StartTime: ×tamppb.Timestamp{ 1342 Seconds: 1234, 1343 }, 1344 }, 1345 }, 1346 TargetResults: map[string][]*singleActionResult{ 1347 "tgt-id-1": { 1348 { 1349 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 1350 Id: &resultstore.ConfiguredTarget_Id{ 1351 TargetId: "tgt-id-1", 1352 }, 1353 StatusAttributes: &resultstore.StatusAttributes{ 1354 Status: resultstore.Status_PASSED, 1355 }, 1356 }, 1357 }, 1358 }, 1359 "tgt-id-2": { 1360 { 1361 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 1362 Id: &resultstore.ConfiguredTarget_Id{ 1363 TargetId: "tgt-id-2", 1364 }, 1365 StatusAttributes: &resultstore.StatusAttributes{ 1366 Status: resultstore.Status_TESTING, 1367 }, 1368 }, 1369 }, 1370 }, 1371 "tgt-id-3": { 1372 { 1373 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 1374 Id: &resultstore.ConfiguredTarget_Id{ 1375 TargetId: "tgt-id-3", 1376 }, 1377 StatusAttributes: &resultstore.StatusAttributes{ 1378 Status: resultstore.Status_TIMED_OUT, 1379 }, 1380 }, 1381 }, 1382 }, 1383 }, 1384 }, 1385 }, 1386 }, 1387 want: &updater.InflatedColumn{ 1388 Column: &statepb.Column{ 1389 Name: "uuid-123", 1390 Build: "uuid-123", 1391 Started: 1234000, 1392 Hint: "1970-01-01T00:20:34Z", 1393 }, 1394 Cells: map[string]updater.Cell{ 1395 "tgt-id-1": { 1396 ID: "tgt-id-1", 1397 CellID: "uuid-123", 1398 Result: teststatuspb.TestStatus_PASS, 1399 }, 1400 "tgt-id-3": { 1401 ID: "tgt-id-3", 1402 CellID: "uuid-123", 1403 Result: teststatuspb.TestStatus_CATEGORIZED_ABORT, 1404 }, 1405 }, 1406 }, 1407 }, 1408 } 1409 for _, tc := range cases { 1410 t.Run(tc.name, func(t *testing.T) { 1411 got := processGroup(tc.tg, tc.group) 1412 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 1413 t.Errorf("processGroup() differed (-want, +got): %s", diff) 1414 } 1415 }) 1416 } 1417 } 1418 1419 func TestFilterResults(t *testing.T) { 1420 cases := []struct { 1421 name string 1422 results []*resultstore.Test 1423 properties []*configpb.TestGroup_KeyValue 1424 match *string 1425 unmatch *string 1426 expected []*resultstore.Test 1427 filtered bool 1428 }{ 1429 { 1430 name: "basically works", 1431 results: []*resultstore.Test{ 1432 { 1433 TestType: &resultstore.Test_TestCase{ 1434 TestCase: &resultstore.TestCase{ 1435 CaseName: "every", 1436 }, 1437 }, 1438 }, 1439 { 1440 TestType: &resultstore.Test_TestCase{ 1441 TestCase: &resultstore.TestCase{ 1442 CaseName: "thing", 1443 }, 1444 }, 1445 }, 1446 }, 1447 expected: []*resultstore.Test{ 1448 { 1449 TestType: &resultstore.Test_TestCase{ 1450 TestCase: &resultstore.TestCase{ 1451 CaseName: "every", 1452 }, 1453 }, 1454 }, 1455 { 1456 TestType: &resultstore.Test_TestCase{ 1457 TestCase: &resultstore.TestCase{ 1458 CaseName: "thing", 1459 }, 1460 }, 1461 }, 1462 }, 1463 }, 1464 { 1465 name: "match nothing", 1466 results: []*resultstore.Test{ 1467 { 1468 TestType: &resultstore.Test_TestCase{ 1469 TestCase: &resultstore.TestCase{ 1470 CaseName: "every", 1471 }, 1472 }, 1473 }, 1474 { 1475 TestType: &resultstore.Test_TestCase{ 1476 TestCase: &resultstore.TestCase{ 1477 CaseName: "thing", 1478 }, 1479 }, 1480 }, 1481 }, 1482 match: pstr("sandwiches"), 1483 filtered: true, 1484 }, 1485 { 1486 name: "all wrong properties", 1487 results: []*resultstore.Test{ 1488 { 1489 TestType: &resultstore.Test_TestCase{ 1490 TestCase: &resultstore.TestCase{ 1491 CaseName: "every", 1492 }, 1493 }, 1494 }, 1495 { 1496 TestType: &resultstore.Test_TestCase{ 1497 TestCase: &resultstore.TestCase{ 1498 CaseName: "thing", 1499 }, 1500 }, 1501 }, 1502 }, 1503 properties: []*configpb.TestGroup_KeyValue{ 1504 { 1505 Key: "medal", 1506 Value: "gold", 1507 }, 1508 }, 1509 filtered: true, 1510 }, 1511 { 1512 name: "properties flter", 1513 properties: []*configpb.TestGroup_KeyValue{{}}, 1514 filtered: true, 1515 }, 1516 { 1517 name: "match filters", 1518 match: pstr(".*"), 1519 filtered: true, 1520 }, 1521 { 1522 name: "unmatch filters", 1523 match: pstr("^$"), 1524 filtered: true, 1525 }, 1526 { 1527 name: "match fruit", 1528 match: pstr("tomato|apple|orange"), 1529 unmatch: pstr("tomato"), 1530 results: []*resultstore.Test{ 1531 { 1532 TestType: &resultstore.Test_TestCase{ 1533 TestCase: &resultstore.TestCase{ 1534 CaseName: "steak", 1535 }, 1536 }, 1537 }, 1538 { 1539 TestType: &resultstore.Test_TestCase{ 1540 TestCase: &resultstore.TestCase{ 1541 CaseName: "tomato", 1542 }, 1543 }, 1544 }, 1545 { 1546 TestType: &resultstore.Test_TestCase{ 1547 TestCase: &resultstore.TestCase{ 1548 CaseName: "apple", 1549 }, 1550 }, 1551 }, 1552 { 1553 TestType: &resultstore.Test_TestCase{ 1554 TestCase: &resultstore.TestCase{ 1555 CaseName: "orange", 1556 }, 1557 }, 1558 }, 1559 }, 1560 expected: []*resultstore.Test{ 1561 { 1562 TestType: &resultstore.Test_TestCase{ 1563 TestCase: &resultstore.TestCase{ 1564 CaseName: "apple", 1565 }, 1566 }, 1567 }, 1568 { 1569 TestType: &resultstore.Test_TestCase{ 1570 TestCase: &resultstore.TestCase{ 1571 CaseName: "orange", 1572 }, 1573 }, 1574 }, 1575 }, 1576 filtered: true, 1577 }, 1578 { 1579 name: "good properties", 1580 properties: []*configpb.TestGroup_KeyValue{ 1581 { 1582 Key: "tastes", 1583 Value: "good", 1584 }, 1585 }, 1586 results: []*resultstore.Test{ 1587 { 1588 TestType: &resultstore.Test_TestCase{ 1589 TestCase: &resultstore.TestCase{ 1590 CaseName: "potion", 1591 Properties: []*resultstore.Property{ 1592 { 1593 Key: "tastes", 1594 Value: "bad", 1595 }, 1596 }, 1597 }, 1598 }, 1599 }, 1600 { 1601 TestType: &resultstore.Test_TestCase{ 1602 TestCase: &resultstore.TestCase{ 1603 CaseName: "fruit", 1604 Properties: []*resultstore.Property{ 1605 { 1606 Key: "tastes", 1607 Value: "good", 1608 }, 1609 }, 1610 }, 1611 }, 1612 }, 1613 }, 1614 expected: []*resultstore.Test{ 1615 { 1616 TestType: &resultstore.Test_TestCase{ 1617 TestCase: &resultstore.TestCase{ 1618 CaseName: "fruit", 1619 Properties: []*resultstore.Property{ 1620 { 1621 Key: "tastes", 1622 Value: "good", 1623 }, 1624 }, 1625 }, 1626 }, 1627 }, 1628 }, 1629 filtered: true, 1630 }, 1631 { 1632 name: "both filter", 1633 properties: []*configpb.TestGroup_KeyValue{ 1634 { 1635 Key: "tastes", 1636 Value: "good", 1637 }, 1638 }, 1639 unmatch: pstr("steak"), 1640 results: []*resultstore.Test{ 1641 { 1642 TestType: &resultstore.Test_TestCase{ 1643 TestCase: &resultstore.TestCase{ 1644 CaseName: "potion", 1645 Properties: []*resultstore.Property{ 1646 { 1647 Key: "tastes", 1648 Value: "bad", 1649 }, 1650 }, 1651 }, 1652 }, 1653 }, 1654 { 1655 TestType: &resultstore.Test_TestCase{ 1656 TestCase: &resultstore.TestCase{ 1657 CaseName: "fruit", 1658 Properties: []*resultstore.Property{ 1659 { 1660 Key: "tastes", 1661 Value: "good", 1662 }, 1663 }, 1664 }, 1665 }, 1666 }, 1667 { 1668 TestType: &resultstore.Test_TestCase{ 1669 TestCase: &resultstore.TestCase{ 1670 CaseName: "steak", 1671 Properties: []*resultstore.Property{ 1672 { 1673 Key: "tastes", 1674 Value: "good", 1675 }, 1676 }, 1677 }, 1678 }, 1679 }, 1680 }, 1681 expected: []*resultstore.Test{ 1682 { 1683 TestType: &resultstore.Test_TestCase{ 1684 TestCase: &resultstore.TestCase{ 1685 CaseName: "fruit", 1686 Properties: []*resultstore.Property{ 1687 { 1688 Key: "tastes", 1689 Value: "good", 1690 }, 1691 }, 1692 }, 1693 }, 1694 }, 1695 }, 1696 filtered: true, 1697 }, 1698 } 1699 1700 for _, tc := range cases { 1701 t.Run(tc.name, func(t *testing.T) { 1702 var match, unmatch *regexp.Regexp 1703 if tc.match != nil { 1704 match = regexp.MustCompile(*tc.match) 1705 } 1706 if tc.unmatch != nil { 1707 unmatch = regexp.MustCompile(*tc.unmatch) 1708 } 1709 actual, filtered := filterResults(tc.results, tc.properties, match, unmatch) 1710 if diff := cmp.Diff(actual, tc.expected, protocmp.Transform()); diff != "" { 1711 t.Errorf("filterResults() got unexpected diff (-have, +want):\n%s", diff) 1712 } 1713 if filtered != tc.filtered { 1714 t.Errorf("filterResults() got filtered %t, want %t", filtered, tc.filtered) 1715 } 1716 }) 1717 } 1718 } 1719 1720 func TestFilterProperties(t *testing.T) { 1721 cases := []struct { 1722 name string 1723 results []*resultstore.Test 1724 properties []*configpb.TestGroup_KeyValue 1725 expected []*resultstore.Test 1726 }{ 1727 { 1728 name: "basically works", 1729 results: []*resultstore.Test{ 1730 { 1731 TestType: &resultstore.Test_TestCase{ 1732 TestCase: &resultstore.TestCase{ 1733 CaseName: "every", 1734 }, 1735 }, 1736 }, 1737 { 1738 TestType: &resultstore.Test_TestCase{ 1739 TestCase: &resultstore.TestCase{ 1740 CaseName: "every", 1741 }, 1742 }, 1743 }, 1744 }, 1745 expected: []*resultstore.Test{ 1746 { 1747 TestType: &resultstore.Test_TestCase{ 1748 TestCase: &resultstore.TestCase{ 1749 CaseName: "every", 1750 }, 1751 }, 1752 }, 1753 { 1754 TestType: &resultstore.Test_TestCase{ 1755 TestCase: &resultstore.TestCase{ 1756 CaseName: "every", 1757 }, 1758 }, 1759 }, 1760 }, 1761 }, 1762 { 1763 name: "must have correct key and value", 1764 properties: []*configpb.TestGroup_KeyValue{ 1765 { 1766 Key: "goal", 1767 Value: "gold", 1768 }, 1769 }, 1770 results: []*resultstore.Test{ 1771 { 1772 TestType: &resultstore.Test_TestCase{ 1773 TestCase: &resultstore.TestCase{ 1774 CaseName: "wrong-key", 1775 Properties: []*resultstore.Property{ 1776 { 1777 Key: "random", 1778 Value: "gold", 1779 }, 1780 }, 1781 }, 1782 }, 1783 }, 1784 { 1785 TestType: &resultstore.Test_TestCase{ 1786 TestCase: &resultstore.TestCase{ 1787 CaseName: "correct-key", 1788 Properties: []*resultstore.Property{ 1789 { 1790 Key: "goal", 1791 Value: "gold", 1792 }, 1793 }, 1794 }, 1795 }, 1796 }, 1797 { 1798 TestType: &resultstore.Test_TestCase{ 1799 TestCase: &resultstore.TestCase{ 1800 CaseName: "wrong-value", 1801 Properties: []*resultstore.Property{ 1802 { 1803 Key: "goal", 1804 Value: "silver", 1805 }, 1806 }, 1807 }, 1808 }, 1809 }, 1810 { 1811 TestType: &resultstore.Test_TestCase{ 1812 TestCase: &resultstore.TestCase{ 1813 CaseName: "multiple-key-value-pairs", 1814 Properties: []*resultstore.Property{ 1815 { 1816 Key: "silver", 1817 Value: "medal", 1818 }, 1819 { 1820 Key: "goal", 1821 Value: "gold", 1822 }, 1823 { 1824 Key: "critical", 1825 Value: "information", 1826 }, 1827 }, 1828 }, 1829 }, 1830 }, 1831 }, 1832 expected: []*resultstore.Test{ 1833 { 1834 TestType: &resultstore.Test_TestCase{ 1835 TestCase: &resultstore.TestCase{ 1836 CaseName: "correct-key", 1837 Properties: []*resultstore.Property{ 1838 { 1839 Key: "goal", 1840 Value: "gold", 1841 }, 1842 }, 1843 }, 1844 }, 1845 }, 1846 { 1847 TestType: &resultstore.Test_TestCase{ 1848 TestCase: &resultstore.TestCase{ 1849 CaseName: "multiple-key-value-pairs", 1850 Properties: []*resultstore.Property{ 1851 { 1852 Key: "silver", 1853 Value: "medal", 1854 }, 1855 { 1856 Key: "goal", 1857 Value: "gold", 1858 }, 1859 { 1860 Key: "critical", 1861 Value: "information", 1862 }, 1863 }, 1864 }, 1865 }, 1866 }, 1867 }, 1868 }, 1869 { 1870 name: "must match all properties", 1871 properties: []*configpb.TestGroup_KeyValue{ 1872 { 1873 Key: "medal", 1874 Value: "gold", 1875 }, 1876 { 1877 Key: "ribbon", 1878 Value: "blue", 1879 }, 1880 }, 1881 results: []*resultstore.Test{ 1882 { 1883 TestType: &resultstore.Test_TestCase{ 1884 TestCase: &resultstore.TestCase{ 1885 CaseName: "zero", 1886 }, 1887 }, 1888 }, 1889 { 1890 TestType: &resultstore.Test_TestCase{ 1891 TestCase: &resultstore.TestCase{ 1892 CaseName: "wrong-medal", 1893 Properties: []*resultstore.Property{ 1894 { 1895 Key: "ribbon", 1896 Value: "blue", 1897 }, 1898 }, 1899 }, 1900 }, 1901 }, 1902 { 1903 TestType: &resultstore.Test_TestCase{ 1904 TestCase: &resultstore.TestCase{ 1905 CaseName: "wrong-ribbon", 1906 Properties: []*resultstore.Property{ 1907 { 1908 Key: "medal", 1909 Value: "gold", 1910 }, 1911 }, 1912 }, 1913 }, 1914 }, 1915 { 1916 TestType: &resultstore.Test_TestCase{ 1917 TestCase: &resultstore.TestCase{ 1918 CaseName: "whole-deal", 1919 Properties: []*resultstore.Property{ 1920 { 1921 Key: "medal", 1922 Value: "gold", 1923 }, 1924 { 1925 Key: "ribbon", 1926 Value: "blue", 1927 }, 1928 }, 1929 }, 1930 }, 1931 }, 1932 }, 1933 expected: []*resultstore.Test{ 1934 { 1935 TestType: &resultstore.Test_TestCase{ 1936 TestCase: &resultstore.TestCase{ 1937 CaseName: "whole-deal", 1938 Properties: []*resultstore.Property{ 1939 { 1940 Key: "medal", 1941 Value: "gold", 1942 }, 1943 { 1944 Key: "ribbon", 1945 Value: "blue", 1946 }, 1947 }, 1948 }, 1949 }, 1950 }, 1951 }, 1952 }, 1953 } 1954 1955 for _, tc := range cases { 1956 t.Run(tc.name, func(t *testing.T) { 1957 actual := filterProperties(tc.results, tc.properties) 1958 if diff := cmp.Diff(actual, tc.expected, protocmp.Transform()); diff != "" { 1959 t.Errorf("filterProperties() got unexpected diff (-have, +want):\n%s", diff) 1960 } 1961 }) 1962 } 1963 } 1964 1965 func TestFillProperties(t *testing.T) { 1966 cases := []struct { 1967 name string 1968 properties map[string]string 1969 result *resultstore.Test 1970 match map[string]bool 1971 want map[string]string 1972 }{ 1973 { 1974 name: "basically works", 1975 }, 1976 { 1977 name: "still basically works", 1978 result: &resultstore.Test{}, 1979 }, 1980 { 1981 name: "simple case no match", 1982 properties: map[string]string{ 1983 "spongebob": "yellow", 1984 "patrick": "pink", 1985 }, 1986 result: &resultstore.Test{ 1987 TestType: &resultstore.Test_TestCase{ 1988 TestCase: &resultstore.TestCase{ 1989 Properties: []*resultstore.Property{ 1990 {Key: "squidward", Value: "green"}, 1991 }, 1992 }, 1993 }, 1994 }, 1995 want: map[string]string{ 1996 "spongebob": "yellow", 1997 "patrick": "pink", 1998 "squidward": "green", 1999 }, 2000 }, 2001 { 2002 name: "simple case with match", 2003 properties: map[string]string{ 2004 "spongebob": "yellow", 2005 "mrkrabs": "red", 2006 }, 2007 result: &resultstore.Test{ 2008 TestType: &resultstore.Test_TestCase{ 2009 TestCase: &resultstore.TestCase{ 2010 Properties: []*resultstore.Property{ 2011 {Key: "squidward", Value: "blue"}, 2012 }, 2013 }, 2014 }, 2015 }, 2016 match: map[string]bool{ 2017 "squidward": true, 2018 }, 2019 want: map[string]string{ 2020 "spongebob": "yellow", 2021 "mrkrabs": "red", 2022 "squidward": "blue", 2023 }, 2024 }, 2025 } 2026 for _, tc := range cases { 2027 t.Run(tc.name, func(t *testing.T) { 2028 fillProperties(tc.properties, tc.result, tc.match) 2029 if diff := cmp.Diff(tc.want, tc.properties, protocmp.Transform()); diff != "" { 2030 t.Errorf("fillProperties() differed (-want, +got): [%s]", diff) 2031 } 2032 }) 2033 } 2034 } 2035 2036 func pstr(s string) *string { return &s } 2037 2038 func TestMatchResults(t *testing.T) { 2039 cases := []struct { 2040 name string 2041 results []*resultstore.Test 2042 match *string 2043 unmatch *string 2044 expected []*resultstore.Test 2045 }{ 2046 { 2047 name: "basically works", 2048 results: []*resultstore.Test{ 2049 { 2050 TestType: &resultstore.Test_TestCase{ 2051 TestCase: &resultstore.TestCase{ 2052 CaseName: "every", 2053 }, 2054 }, 2055 }, 2056 { 2057 TestType: &resultstore.Test_TestCase{ 2058 TestCase: &resultstore.TestCase{ 2059 CaseName: "thing", 2060 }, 2061 }, 2062 }, 2063 }, 2064 expected: []*resultstore.Test{ 2065 { 2066 TestType: &resultstore.Test_TestCase{ 2067 TestCase: &resultstore.TestCase{ 2068 CaseName: "every", 2069 }, 2070 }, 2071 }, 2072 { 2073 TestType: &resultstore.Test_TestCase{ 2074 TestCase: &resultstore.TestCase{ 2075 CaseName: "thing", 2076 }, 2077 }, 2078 }, 2079 }, 2080 }, 2081 { 2082 name: "match results", 2083 results: []*resultstore.Test{ 2084 { 2085 TestType: &resultstore.Test_TestCase{ 2086 TestCase: &resultstore.TestCase{ 2087 CaseName: "miss", 2088 }, 2089 }, 2090 }, 2091 { 2092 TestType: &resultstore.Test_TestCase{ 2093 TestCase: &resultstore.TestCase{ 2094 CaseName: "yesgopher", 2095 }, 2096 }, 2097 }, 2098 { 2099 TestType: &resultstore.Test_TestCase{ 2100 TestCase: &resultstore.TestCase{ 2101 CaseName: "gopher-yes", 2102 }, 2103 }, 2104 }, 2105 { 2106 TestType: &resultstore.Test_TestCase{ 2107 TestCase: &resultstore.TestCase{ 2108 CaseName: "no", 2109 }, 2110 }, 2111 }, 2112 }, 2113 match: pstr("gopher"), 2114 expected: []*resultstore.Test{ 2115 { 2116 TestType: &resultstore.Test_TestCase{ 2117 TestCase: &resultstore.TestCase{ 2118 CaseName: "yesgopher", 2119 }, 2120 }, 2121 }, 2122 { 2123 TestType: &resultstore.Test_TestCase{ 2124 TestCase: &resultstore.TestCase{ 2125 CaseName: "gopher-yes", 2126 }, 2127 }, 2128 }, 2129 }, 2130 }, 2131 { 2132 name: "exclude results with neutral alignments", 2133 results: []*resultstore.Test{ 2134 { 2135 TestType: &resultstore.Test_TestCase{ 2136 TestCase: &resultstore.TestCase{ 2137 CaseName: "yesgopher", 2138 }, 2139 }, 2140 }, 2141 { 2142 TestType: &resultstore.Test_TestCase{ 2143 TestCase: &resultstore.TestCase{ 2144 CaseName: "lawful good", 2145 }, 2146 }, 2147 }, 2148 { 2149 TestType: &resultstore.Test_TestCase{ 2150 TestCase: &resultstore.TestCase{ 2151 CaseName: "neutral good", 2152 }, 2153 }, 2154 }, 2155 { 2156 TestType: &resultstore.Test_TestCase{ 2157 TestCase: &resultstore.TestCase{ 2158 CaseName: "chaotic good", 2159 }, 2160 }, 2161 }, 2162 { 2163 TestType: &resultstore.Test_TestCase{ 2164 TestCase: &resultstore.TestCase{ 2165 CaseName: "lawful neutral", 2166 }, 2167 }, 2168 }, 2169 { 2170 TestType: &resultstore.Test_TestCase{ 2171 TestCase: &resultstore.TestCase{ 2172 CaseName: "true neutral", 2173 }, 2174 }, 2175 }, 2176 { 2177 TestType: &resultstore.Test_TestCase{ 2178 TestCase: &resultstore.TestCase{ 2179 CaseName: "chaotic neutral", 2180 }, 2181 }, 2182 }, 2183 { 2184 TestType: &resultstore.Test_TestCase{ 2185 TestCase: &resultstore.TestCase{ 2186 CaseName: "lawful evil", 2187 }, 2188 }, 2189 }, 2190 { 2191 TestType: &resultstore.Test_TestCase{ 2192 TestCase: &resultstore.TestCase{ 2193 CaseName: "neutral evil", 2194 }, 2195 }, 2196 }, 2197 { 2198 TestType: &resultstore.Test_TestCase{ 2199 TestCase: &resultstore.TestCase{ 2200 CaseName: "chaotic evil", 2201 }, 2202 }, 2203 }, 2204 }, 2205 unmatch: pstr("neutral"), 2206 expected: []*resultstore.Test{ 2207 { 2208 TestType: &resultstore.Test_TestCase{ 2209 TestCase: &resultstore.TestCase{ 2210 CaseName: "yesgopher", 2211 }, 2212 }, 2213 }, 2214 { 2215 TestType: &resultstore.Test_TestCase{ 2216 TestCase: &resultstore.TestCase{ 2217 CaseName: "lawful good", 2218 }, 2219 }, 2220 }, 2221 { 2222 TestType: &resultstore.Test_TestCase{ 2223 TestCase: &resultstore.TestCase{ 2224 CaseName: "chaotic good", 2225 }, 2226 }, 2227 }, 2228 { 2229 TestType: &resultstore.Test_TestCase{ 2230 TestCase: &resultstore.TestCase{ 2231 CaseName: "lawful evil", 2232 }, 2233 }, 2234 }, 2235 { 2236 TestType: &resultstore.Test_TestCase{ 2237 TestCase: &resultstore.TestCase{ 2238 CaseName: "chaotic evil", 2239 }, 2240 }, 2241 }, 2242 }, 2243 }, 2244 { 2245 name: "exclude the included results", 2246 results: []*resultstore.Test{ 2247 { 2248 TestType: &resultstore.Test_TestCase{ 2249 TestCase: &resultstore.TestCase{ 2250 CaseName: "lawful good", 2251 }, 2252 }, 2253 }, 2254 { 2255 TestType: &resultstore.Test_TestCase{ 2256 TestCase: &resultstore.TestCase{ 2257 CaseName: "neutral good", 2258 }, 2259 }, 2260 }, 2261 { 2262 TestType: &resultstore.Test_TestCase{ 2263 TestCase: &resultstore.TestCase{ 2264 CaseName: "chaotic good", 2265 }, 2266 }, 2267 }, 2268 { 2269 TestType: &resultstore.Test_TestCase{ 2270 TestCase: &resultstore.TestCase{ 2271 CaseName: "lawful neutral", 2272 }, 2273 }, 2274 }, 2275 { 2276 TestType: &resultstore.Test_TestCase{ 2277 TestCase: &resultstore.TestCase{ 2278 CaseName: "true neutral", 2279 }, 2280 }, 2281 }, 2282 { 2283 TestType: &resultstore.Test_TestCase{ 2284 TestCase: &resultstore.TestCase{ 2285 CaseName: "chaotic neutral", 2286 }, 2287 }, 2288 }, 2289 { 2290 TestType: &resultstore.Test_TestCase{ 2291 TestCase: &resultstore.TestCase{ 2292 CaseName: "lawful evil", 2293 }, 2294 }, 2295 }, 2296 { 2297 TestType: &resultstore.Test_TestCase{ 2298 TestCase: &resultstore.TestCase{ 2299 CaseName: "neutral evil", 2300 }, 2301 }, 2302 }, 2303 { 2304 TestType: &resultstore.Test_TestCase{ 2305 TestCase: &resultstore.TestCase{ 2306 CaseName: "chaotic evil", 2307 }, 2308 }, 2309 }, 2310 }, 2311 match: pstr("good"), 2312 unmatch: pstr("neutral"), 2313 expected: []*resultstore.Test{ 2314 { 2315 TestType: &resultstore.Test_TestCase{ 2316 TestCase: &resultstore.TestCase{ 2317 CaseName: "lawful good", 2318 }, 2319 }, 2320 }, 2321 { 2322 TestType: &resultstore.Test_TestCase{ 2323 TestCase: &resultstore.TestCase{ 2324 CaseName: "chaotic good", 2325 }, 2326 }, 2327 }, 2328 }, 2329 }, 2330 } 2331 2332 for _, tc := range cases { 2333 t.Run(tc.name, func(t *testing.T) { 2334 var match, unmatch *regexp.Regexp 2335 if tc.match != nil { 2336 match = regexp.MustCompile(*tc.match) 2337 } 2338 if tc.unmatch != nil { 2339 unmatch = regexp.MustCompile(*tc.unmatch) 2340 } 2341 actual := matchResults(tc.results, match, unmatch) 2342 for i, r := range actual { 2343 if diff := cmp.Diff(r, tc.expected[i], protocmp.Transform()); diff != "" { 2344 t.Errorf("matchResults() got unexpected diff (-have, +want):\n%s", diff) 2345 } 2346 } 2347 2348 }) 2349 } 2350 } 2351 2352 func TestGetTestResults(t *testing.T) { 2353 cases := []struct { 2354 name string 2355 testsuite *resultstore.TestSuite 2356 want []*resultstore.Test 2357 }{ 2358 { 2359 name: "empty test suite", 2360 testsuite: &resultstore.TestSuite{ 2361 SuiteName: "Empty Test", 2362 }, 2363 want: []*resultstore.Test{ 2364 { 2365 TestType: &resultstore.Test_TestSuite{ 2366 TestSuite: &resultstore.TestSuite{ 2367 SuiteName: "Empty Test", 2368 }, 2369 }, 2370 }, 2371 }, 2372 }, 2373 { 2374 name: "nil test suite", 2375 testsuite: nil, 2376 want: nil, 2377 }, 2378 { 2379 name: "standard test suite", 2380 testsuite: &resultstore.TestSuite{ 2381 SuiteName: "Standard test suite", 2382 Tests: []*resultstore.Test{ 2383 { 2384 TestType: &resultstore.Test_TestSuite{ 2385 TestSuite: &resultstore.TestSuite{ 2386 SuiteName: "TestDetectJSError", 2387 Tests: []*resultstore.Test{ 2388 { 2389 TestType: &resultstore.Test_TestCase{ 2390 TestCase: &resultstore.TestCase{ 2391 CaseName: "TestDetectJSError/Main", 2392 }, 2393 }, 2394 }, 2395 { 2396 TestType: &resultstore.Test_TestCase{ 2397 TestCase: &resultstore.TestCase{ 2398 CaseName: "TestDetectJSError/Summary", 2399 }, 2400 }, 2401 }, 2402 { 2403 TestType: &resultstore.Test_TestCase{ 2404 TestCase: &resultstore.TestCase{ 2405 CaseName: "TestDetectJSError/Dashboard", 2406 }, 2407 }, 2408 }, 2409 }, 2410 }, 2411 }, 2412 }, 2413 }, 2414 }, 2415 want: []*resultstore.Test{ 2416 { 2417 TestType: &resultstore.Test_TestCase{ 2418 TestCase: &resultstore.TestCase{ 2419 CaseName: "TestDetectJSError/Main", 2420 }, 2421 }, 2422 }, 2423 { 2424 TestType: &resultstore.Test_TestCase{ 2425 TestCase: &resultstore.TestCase{ 2426 CaseName: "TestDetectJSError/Summary", 2427 }, 2428 }, 2429 }, 2430 { 2431 TestType: &resultstore.Test_TestCase{ 2432 TestCase: &resultstore.TestCase{ 2433 CaseName: "TestDetectJSError/Dashboard", 2434 }, 2435 }, 2436 }, 2437 }, 2438 }, 2439 { 2440 name: "nested test suite", 2441 testsuite: &resultstore.TestSuite{ 2442 SuiteName: "Nested test suite", 2443 Tests: []*resultstore.Test{ 2444 { 2445 TestType: &resultstore.Test_TestSuite{ 2446 TestSuite: &resultstore.TestSuite{ 2447 SuiteName: "TestDetectJSError", 2448 Tests: []*resultstore.Test{ 2449 { 2450 TestType: &resultstore.Test_TestCase{ 2451 TestCase: &resultstore.TestCase{ 2452 CaseName: "TestDetectJSError/Main", 2453 }, 2454 }, 2455 }, 2456 { 2457 TestType: &resultstore.Test_TestCase{ 2458 TestCase: &resultstore.TestCase{ 2459 CaseName: "TestDetectJSError/Summary", 2460 }, 2461 }, 2462 }, 2463 { 2464 TestType: &resultstore.Test_TestSuite{ 2465 TestSuite: &resultstore.TestSuite{ 2466 SuiteName: "TestDetectJSError/Other", 2467 Tests: []*resultstore.Test{ 2468 { 2469 TestType: &resultstore.Test_TestCase{ 2470 TestCase: &resultstore.TestCase{ 2471 CaseName: "TestDetectJSError/Misc", 2472 }, 2473 }, 2474 }, 2475 }, 2476 }, 2477 }, 2478 }, 2479 }, 2480 }, 2481 }, 2482 }, 2483 }, 2484 }, 2485 want: []*resultstore.Test{ 2486 { 2487 TestType: &resultstore.Test_TestCase{ 2488 TestCase: &resultstore.TestCase{ 2489 CaseName: "TestDetectJSError/Main", 2490 }, 2491 }, 2492 }, 2493 { 2494 TestType: &resultstore.Test_TestCase{ 2495 TestCase: &resultstore.TestCase{ 2496 CaseName: "TestDetectJSError/Summary", 2497 }, 2498 }, 2499 }, 2500 { 2501 TestType: &resultstore.Test_TestCase{ 2502 TestCase: &resultstore.TestCase{ 2503 CaseName: "TestDetectJSError/Misc", 2504 }, 2505 }, 2506 }, 2507 }, 2508 }, 2509 } 2510 for _, tc := range cases { 2511 t.Run(tc.name, func(t *testing.T) { 2512 got := getTestResults(tc.testsuite) 2513 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 2514 t.Errorf("getTestResults(%+v) differed (-want, +got): [%s]", tc.testsuite, diff) 2515 } 2516 }) 2517 } 2518 } 2519 2520 func TestMethodRegex(t *testing.T) { 2521 regexErrYes := regexp.MustCompile("yes") 2522 regexErrNo := regexp.MustCompile("no") 2523 type result struct { 2524 matchMethods *regexp.Regexp 2525 unmatchMethods *regexp.Regexp 2526 matchMethodsErr error 2527 unmatchMethodsErr error 2528 } 2529 cases := []struct { 2530 name string 2531 tg *configpb.TestGroup 2532 want result 2533 }{ 2534 { 2535 name: "basically works", 2536 tg: &configpb.TestGroup{}, 2537 }, 2538 { 2539 name: "basic test", 2540 tg: &configpb.TestGroup{ 2541 TestMethodMatchRegex: "yes", 2542 TestMethodUnmatchRegex: "no", 2543 }, 2544 want: result{ 2545 matchMethods: regexErrYes, 2546 unmatchMethods: regexErrNo, 2547 matchMethodsErr: nil, 2548 unmatchMethodsErr: nil, 2549 }, 2550 }, 2551 { 2552 name: "invalid regex test", 2553 tg: &configpb.TestGroup{ 2554 TestMethodMatchRegex: "\x8A", 2555 TestMethodUnmatchRegex: "\x8A", 2556 }, 2557 want: result{ 2558 matchMethods: nil, 2559 unmatchMethods: nil, 2560 matchMethodsErr: nil, 2561 unmatchMethodsErr: nil, 2562 }, 2563 }, 2564 } 2565 for _, tc := range cases { 2566 t.Run(tc.name, func(t *testing.T) { 2567 mm, umm, mmErr, ummErr := testMethodRegex(tc.tg) 2568 res := result{ 2569 matchMethods: mm, 2570 unmatchMethods: umm, 2571 matchMethodsErr: mmErr, 2572 unmatchMethodsErr: ummErr, 2573 } 2574 if diff := cmp.Diff(tc.want.matchMethods, res.matchMethods, protocmp.Transform(), cmp.AllowUnexported(regexp.Regexp{})); diff != "" { 2575 t.Errorf("testMethodRegex(%+v) differed (-want, +got): [%s]", tc.tg, diff) 2576 } 2577 if diff := cmp.Diff(tc.want.unmatchMethods, res.unmatchMethods, protocmp.Transform(), cmp.AllowUnexported(regexp.Regexp{})); diff != "" { 2578 t.Errorf("testMethodRegex(%+v) differed (-want, +got): [%s]", tc.tg, diff) 2579 } 2580 }) 2581 } 2582 } 2583 2584 func TestQueryAfter(t *testing.T) { 2585 now := time.Now() 2586 cases := []struct { 2587 name string 2588 query string 2589 when time.Time 2590 want string 2591 }{ 2592 { 2593 name: "empty", 2594 want: "", 2595 }, 2596 { 2597 name: "zero", 2598 query: prowLabel, 2599 when: time.Time{}, 2600 want: "invocation_attributes.labels:\"prow\" timing.start_time>=\"0001-01-01T00:00:00Z\"", 2601 }, 2602 { 2603 name: "basic", 2604 query: prowLabel, 2605 when: now, 2606 want: fmt.Sprintf("invocation_attributes.labels:\"prow\" timing.start_time>=\"%s\"", now.UTC().Format(time.RFC3339)), 2607 }, 2608 } 2609 for _, tc := range cases { 2610 t.Run(tc.name, func(t *testing.T) { 2611 got := queryAfter(tc.query, tc.when) 2612 if diff := cmp.Diff(tc.want, got); diff != "" { 2613 t.Errorf("queryAfter(%q, %v) differed (-want, +got): %s", tc.query, tc.when, diff) 2614 } 2615 }) 2616 } 2617 } 2618 2619 func TestQueryProw(t *testing.T) { 2620 now := time.Now() 2621 cases := []struct { 2622 name string 2623 query string 2624 when time.Time 2625 want string 2626 wantError bool 2627 }{ 2628 { 2629 name: "empty", 2630 want: "invocation_attributes.labels:\"prow\" timing.start_time>=\"0001-01-01T00:00:00Z\"", 2631 }, 2632 { 2633 name: "zero", 2634 query: "", 2635 when: time.Time{}, 2636 want: "invocation_attributes.labels:\"prow\" timing.start_time>=\"0001-01-01T00:00:00Z\"", 2637 }, 2638 { 2639 name: "basic", 2640 query: "", 2641 when: now, 2642 want: fmt.Sprintf("invocation_attributes.labels:\"prow\" timing.start_time>=\"%s\"", now.UTC().Format(time.RFC3339)), 2643 }, 2644 { 2645 name: "target", 2646 query: `target:"my-job"`, 2647 when: now, 2648 want: fmt.Sprintf("id.target_id=\"my-job\" invocation.invocation_attributes.labels:\"prow\" timing.start_time>=\"%s\"", now.UTC().Format(time.RFC3339)), 2649 }, 2650 { 2651 name: "invalid query", 2652 query: `label:foo bar`, 2653 when: now, 2654 wantError: true, 2655 }, 2656 } 2657 for _, tc := range cases { 2658 t.Run(tc.name, func(t *testing.T) { 2659 got, err := queryProw(tc.query, tc.when) 2660 if !tc.wantError && err != nil { 2661 t.Errorf("queryProw(%q, %v) errored: %v", tc.query, tc.when, err) 2662 } else if tc.wantError && err == nil { 2663 t.Errorf("queryProw(%q, %v) did not error as expected", tc.query, tc.when) 2664 } 2665 if diff := cmp.Diff(tc.want, got); diff != "" { 2666 t.Errorf("queryProw(%q, %v) differed (-want, +got): %s", tc.query, tc.when, diff) 2667 } 2668 }) 2669 } 2670 } 2671 2672 func TestSearch(t *testing.T) { 2673 twoDaysAgo := time.Now().AddDate(0, 0, -2) 2674 testQueryAfter := queryAfter(prowLabel, twoDaysAgo) 2675 testTargetQueryAfter, _ := queryProw(`target:"my-job"`, twoDaysAgo) 2676 cases := []struct { 2677 name string 2678 stop time.Time 2679 client *fakeClient 2680 rsConfig *configpb.ResultStoreConfig 2681 want []string 2682 wantErr bool 2683 }{ 2684 { 2685 name: "nil", 2686 wantErr: true, 2687 }, 2688 { 2689 name: "empty", 2690 rsConfig: &configpb.ResultStoreConfig{}, 2691 client: &fakeClient{}, 2692 wantErr: true, 2693 }, 2694 { 2695 name: "no results", 2696 rsConfig: &configpb.ResultStoreConfig{ 2697 Project: "my-project", 2698 }, 2699 client: &fakeClient{}, 2700 wantErr: true, 2701 }, 2702 { 2703 name: "basic", 2704 rsConfig: &configpb.ResultStoreConfig{ 2705 Project: "my-project", 2706 }, 2707 client: &fakeClient{ 2708 searches: map[string][]string{ 2709 testQueryAfter: {"id-1", "id-2", "id-3"}, 2710 }, 2711 }, 2712 stop: twoDaysAgo, 2713 want: []string{"id-1", "id-2", "id-3"}, 2714 }, 2715 { 2716 name: "target", 2717 rsConfig: &configpb.ResultStoreConfig{ 2718 Project: "my-project", 2719 Query: `target:"my-job"`, 2720 }, 2721 client: &fakeClient{ 2722 searches: map[string][]string{ 2723 testQueryAfter: {"id-1", "id-2", "id-3"}, 2724 testTargetQueryAfter: {"id-1", "id-3"}, 2725 }, 2726 }, 2727 stop: twoDaysAgo, 2728 want: []string{"id-1", "id-3"}, 2729 }, 2730 { 2731 name: "invalid query", 2732 rsConfig: &configpb.ResultStoreConfig{ 2733 Project: "my-project", 2734 Query: "label:foo bar", 2735 }, 2736 client: &fakeClient{ 2737 searches: map[string][]string{ 2738 testQueryAfter: {"id-1", "id-2", "id-3"}, 2739 testTargetQueryAfter: {"id-1", "id-3"}, 2740 }, 2741 }, 2742 stop: twoDaysAgo, 2743 wantErr: true, 2744 }, 2745 } 2746 for _, tc := range cases { 2747 t.Run(tc.name, func(t *testing.T) { 2748 var dlClient *DownloadClient 2749 if tc.client != nil { 2750 dlClient = &DownloadClient{client: tc.client} 2751 } 2752 got, err := search(context.Background(), logrus.WithField("case", tc.name), dlClient, tc.rsConfig, tc.stop) 2753 if err != nil && !tc.wantErr { 2754 t.Errorf("search() errored: %v", err) 2755 } else if err == nil && tc.wantErr { 2756 t.Errorf("search() did not error as expected") 2757 } 2758 if diff := cmp.Diff(tc.want, got); diff != "" { 2759 t.Errorf("search() differed (-want, +got): %s", diff) 2760 } 2761 }) 2762 } 2763 } 2764 2765 func TestMostRecent(t *testing.T) { 2766 now := time.Now() 2767 oneHourAgo := now.Add(-1 * time.Hour) 2768 sixHoursAgo := now.Add(-6 * time.Hour) 2769 cases := []struct { 2770 name string 2771 times []time.Time 2772 want time.Time 2773 }{ 2774 { 2775 name: "empty", 2776 want: time.Time{}, 2777 }, 2778 { 2779 name: "single", 2780 times: []time.Time{oneHourAgo}, 2781 want: oneHourAgo, 2782 }, 2783 { 2784 name: "mix", 2785 times: []time.Time{now, oneHourAgo, sixHoursAgo}, 2786 want: now, 2787 }, 2788 } 2789 for _, tc := range cases { 2790 t.Run(tc.name, func(t *testing.T) { 2791 got := mostRecent(tc.times) 2792 if !tc.want.Equal(got) { 2793 t.Errorf("stopFromColumns() differed; got %v, want %v", got, tc.want) 2794 } 2795 }) 2796 } 2797 } 2798 2799 func TestStopFromColumns(t *testing.T) { 2800 now := time.Now() 2801 oneHourAgo := now.Add(-1 * time.Hour) 2802 sixHoursAgo := now.Add(-6 * time.Hour) 2803 b, _ := oneHourAgo.MarshalText() 2804 oneHourHint := string(b) 2805 cases := []struct { 2806 name string 2807 cols []updater.InflatedColumn 2808 want time.Time 2809 }{ 2810 { 2811 name: "empty", 2812 want: time.Time{}, 2813 }, 2814 { 2815 name: "column start", 2816 cols: []updater.InflatedColumn{ 2817 { 2818 Column: &statepb.Column{ 2819 Started: float64(oneHourAgo.Unix() * 1000), 2820 }, 2821 }, 2822 }, 2823 want: oneHourAgo.Truncate(time.Second), 2824 }, 2825 { 2826 name: "column hint", 2827 cols: []updater.InflatedColumn{ 2828 { 2829 Column: &statepb.Column{ 2830 Started: float64(sixHoursAgo.Unix() * 1000), 2831 Hint: oneHourHint, 2832 }, 2833 }, 2834 }, 2835 want: oneHourAgo.Truncate(time.Second), 2836 }, 2837 } 2838 for _, tc := range cases { 2839 t.Run(tc.name, func(t *testing.T) { 2840 got := stopFromColumns(logrus.WithField("case", tc.name), tc.cols) 2841 if !tc.want.Equal(got) { 2842 t.Errorf("stopFromColumns() differed; got %v, want %v", got, tc.want) 2843 } 2844 }) 2845 } 2846 } 2847 2848 func TestUpdateStop(t *testing.T) { 2849 now := time.Now() 2850 oneHourAgo := now.Add(-1 * time.Hour) 2851 sixHoursAgo := now.Add(-6 * time.Hour) 2852 twoDaysAgo := now.AddDate(0, 0, -2) 2853 twoWeeksAgo := now.AddDate(0, 0, -14) 2854 oneMonthAgo := now.AddDate(0, 0, -30) 2855 b, _ := oneHourAgo.MarshalText() 2856 oneHourHint := string(b) 2857 cases := []struct { 2858 name string 2859 tg *configpb.TestGroup 2860 cols []updater.InflatedColumn 2861 defaultStop time.Time 2862 reprocess time.Duration 2863 want time.Time 2864 }{ 2865 { 2866 name: "empty", 2867 want: twoDaysAgo.Truncate(time.Second), 2868 }, 2869 { 2870 name: "reprocess", 2871 reprocess: 14 * 24 * time.Hour, 2872 want: twoWeeksAgo.Truncate(time.Second), 2873 }, 2874 { 2875 name: "days of results", 2876 tg: &configpb.TestGroup{ 2877 DaysOfResults: 7, 2878 }, 2879 want: twoWeeksAgo.Truncate(time.Second), 2880 }, 2881 { 2882 name: "default stop, no days of results", 2883 defaultStop: oneMonthAgo, 2884 want: twoDaysAgo.Truncate(time.Second), 2885 }, 2886 { 2887 name: "default stop earlier than days of results", 2888 tg: &configpb.TestGroup{ 2889 DaysOfResults: 7, 2890 }, 2891 defaultStop: oneMonthAgo, 2892 want: twoWeeksAgo.Truncate(time.Second), 2893 }, 2894 { 2895 name: "default stop later than days of results", 2896 tg: &configpb.TestGroup{ 2897 DaysOfResults: 30, 2898 }, 2899 defaultStop: twoWeeksAgo, 2900 want: twoWeeksAgo.Truncate(time.Second), 2901 }, 2902 { 2903 name: "column start", 2904 cols: []updater.InflatedColumn{ 2905 { 2906 Column: &statepb.Column{ 2907 Started: float64(oneHourAgo.Unix() * 1000), 2908 }, 2909 }, 2910 }, 2911 defaultStop: twoWeeksAgo, 2912 want: oneHourAgo.Truncate(time.Second), 2913 }, 2914 { 2915 name: "column hint", 2916 cols: []updater.InflatedColumn{ 2917 { 2918 Column: &statepb.Column{ 2919 Started: float64(sixHoursAgo.Unix() * 1000), 2920 Hint: oneHourHint, 2921 }, 2922 }, 2923 }, 2924 defaultStop: twoWeeksAgo, 2925 want: oneHourAgo.Truncate(time.Second), 2926 }, 2927 } 2928 for _, tc := range cases { 2929 t.Run(tc.name, func(t *testing.T) { 2930 got := updateStop(logrus.WithField("testcase", tc.name), tc.tg, now, tc.cols, tc.defaultStop, tc.reprocess) 2931 if !tc.want.Equal(got) { 2932 t.Errorf("updateStop() differed; got %v, want %v", got, tc.want) 2933 } 2934 }) 2935 } 2936 } 2937 2938 func TestIdentifyBuild(t *testing.T) { 2939 cases := []struct { 2940 name string 2941 result *invocation 2942 tg *configpb.TestGroup 2943 want string 2944 }{ 2945 { 2946 name: "no override configurations", 2947 result: &invocation{ 2948 InvocationProto: &resultstore.Invocation{ 2949 Name: invocationName("id-123"), 2950 Id: &resultstore.Invocation_Id{ 2951 InvocationId: "id-123", 2952 }, 2953 }, 2954 }, 2955 want: "", 2956 }, 2957 { 2958 name: "override by non-existent property key", 2959 result: &invocation{ 2960 InvocationProto: &resultstore.Invocation{ 2961 Name: invocationName("id-1234"), 2962 Id: &resultstore.Invocation_Id{ 2963 InvocationId: "id-1234", 2964 }, 2965 Properties: []*resultstore.Property{ 2966 {Key: "Luigi", Value: "Peaches"}, 2967 {Key: "Bowser", Value: "Pingui"}, 2968 }, 2969 }, 2970 }, 2971 tg: &configpb.TestGroup{ 2972 BuildOverrideConfigurationValue: "Mario", 2973 }, 2974 want: "", 2975 }, 2976 { 2977 name: "override by existent property key", 2978 result: &invocation{ 2979 InvocationProto: &resultstore.Invocation{ 2980 Name: invocationName("id-1234"), 2981 Id: &resultstore.Invocation_Id{ 2982 InvocationId: "id-1234", 2983 }, 2984 Properties: []*resultstore.Property{ 2985 {Key: "Luigi", Value: "Peaches"}, 2986 {Key: "Bowser", Value: "Pingui"}, 2987 {Key: "Waluigi", Value: "Wapeaches"}, 2988 }, 2989 }, 2990 }, 2991 tg: &configpb.TestGroup{ 2992 BuildOverrideConfigurationValue: "Waluigi", 2993 }, 2994 want: "Wapeaches", 2995 }, 2996 { 2997 name: "override by build time strf", 2998 result: &invocation{ 2999 InvocationProto: &resultstore.Invocation{ 3000 Name: invocationName("id-1234"), 3001 Id: &resultstore.Invocation_Id{ 3002 InvocationId: "id-1234", 3003 }, 3004 Timing: &resultstore.Timing{ 3005 StartTime: ×tamppb.Timestamp{ 3006 Seconds: 1689881216, 3007 Nanos: 27847, 3008 }, 3009 }, 3010 }, 3011 }, 3012 tg: &configpb.TestGroup{ 3013 BuildOverrideStrftime: "%Y-%m-%d-%H", 3014 }, 3015 want: "2023-07-20-19", 3016 }, 3017 } 3018 3019 for _, tc := range cases { 3020 t.Run(tc.name, func(t *testing.T) { 3021 got := identifyBuild(tc.tg, tc.result) 3022 if diff := cmp.Diff(tc.want, got); diff != "" { 3023 t.Errorf("queryAfter(...) differed (-want, +got): %s", diff) 3024 } 3025 }) 3026 } 3027 } 3028 3029 func TestIncludeStatus(t *testing.T) { 3030 cases := []struct { 3031 name string 3032 tg *configpb.TestGroup 3033 sar *singleActionResult 3034 want bool 3035 }{ 3036 { 3037 name: "unspecifies status - not included", 3038 sar: &singleActionResult{ 3039 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 3040 StatusAttributes: &resultstore.StatusAttributes{ 3041 Status: resultstore.Status_STATUS_UNSPECIFIED, 3042 }, 3043 }, 3044 }, 3045 want: false, 3046 }, 3047 { 3048 name: "built status and ignored - not included", 3049 tg: &configpb.TestGroup{ 3050 IgnoreBuilt: true, 3051 }, 3052 sar: &singleActionResult{ 3053 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 3054 StatusAttributes: &resultstore.StatusAttributes{ 3055 Status: resultstore.Status_BUILT, 3056 }, 3057 }, 3058 }, 3059 want: false, 3060 }, 3061 { 3062 name: "built status and not ignored - included", 3063 tg: &configpb.TestGroup{ 3064 IgnoreSkip: true, 3065 }, 3066 sar: &singleActionResult{ 3067 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 3068 StatusAttributes: &resultstore.StatusAttributes{ 3069 Status: resultstore.Status_BUILT, 3070 }, 3071 }, 3072 }, 3073 want: true, 3074 }, 3075 { 3076 name: "running status and ignored - not included", 3077 tg: &configpb.TestGroup{ 3078 IgnorePending: true, 3079 }, 3080 sar: &singleActionResult{ 3081 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 3082 StatusAttributes: &resultstore.StatusAttributes{ 3083 Status: resultstore.Status_TESTING, 3084 }, 3085 }, 3086 }, 3087 want: false, 3088 }, 3089 { 3090 name: "running status and not ignored - included", 3091 tg: &configpb.TestGroup{ 3092 IgnoreSkip: true, 3093 }, 3094 sar: &singleActionResult{ 3095 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 3096 StatusAttributes: &resultstore.StatusAttributes{ 3097 Status: resultstore.Status_TESTING, 3098 }, 3099 }, 3100 }, 3101 want: true, 3102 }, 3103 { 3104 name: "skipped status and ignored - not included", 3105 tg: &configpb.TestGroup{ 3106 IgnoreSkip: true, 3107 }, 3108 sar: &singleActionResult{ 3109 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 3110 StatusAttributes: &resultstore.StatusAttributes{ 3111 Status: resultstore.Status_SKIPPED, 3112 }, 3113 }, 3114 }, 3115 want: false, 3116 }, 3117 { 3118 name: "other status - included", 3119 tg: &configpb.TestGroup{ 3120 IgnoreSkip: true, 3121 }, 3122 sar: &singleActionResult{ 3123 ConfiguredTargetProto: &resultstore.ConfiguredTarget{ 3124 StatusAttributes: &resultstore.StatusAttributes{ 3125 Status: resultstore.Status_FAILED, 3126 }, 3127 }, 3128 }, 3129 want: true, 3130 }, 3131 } 3132 3133 for _, tc := range cases { 3134 t.Run(tc.name, func(t *testing.T) { 3135 got := includeStatus(tc.tg, tc.sar) 3136 if diff := cmp.Diff(tc.want, got); diff != "" { 3137 t.Errorf("includeStatus(...) differed (-want, +got): %s", diff) 3138 } 3139 }) 3140 } 3141 } 3142 3143 func TestGroupInvocations(t *testing.T) { 3144 cases := []struct { 3145 name string 3146 tg *configpb.TestGroup 3147 invocations []*invocation 3148 want []*invocationGroup 3149 }{ 3150 { 3151 name: "grouping by build - build override by configuration value", 3152 tg: &configpb.TestGroup{ 3153 PrimaryGrouping: configpb.TestGroup_PRIMARY_GROUPING_BUILD, 3154 BuildOverrideConfigurationValue: "my-property", 3155 }, 3156 invocations: []*invocation{ 3157 { 3158 InvocationProto: &resultstore.Invocation{ 3159 Name: invocationName("uuid-123"), 3160 Id: &resultstore.Invocation_Id{ 3161 InvocationId: "uuid-123", 3162 }, 3163 Properties: []*resultstore.Property{ 3164 { 3165 Key: "my-property", 3166 Value: "my-value-1", 3167 }, 3168 }, 3169 Timing: &resultstore.Timing{ 3170 StartTime: ×tamppb.Timestamp{ 3171 Seconds: 1234, 3172 }, 3173 }, 3174 }, 3175 }, 3176 { 3177 InvocationProto: &resultstore.Invocation{ 3178 Name: invocationName("uuid-321"), 3179 Id: &resultstore.Invocation_Id{ 3180 InvocationId: "uuid-321", 3181 }, 3182 Properties: []*resultstore.Property{ 3183 { 3184 Key: "my-property", 3185 Value: "my-value-2", 3186 }, 3187 }, 3188 }, 3189 }, 3190 }, 3191 want: []*invocationGroup{ 3192 { 3193 GroupID: "my-value-1", 3194 Invocations: []*invocation{ 3195 { 3196 InvocationProto: &resultstore.Invocation{ 3197 Name: invocationName("uuid-123"), 3198 Id: &resultstore.Invocation_Id{ 3199 InvocationId: "uuid-123", 3200 }, 3201 Properties: []*resultstore.Property{ 3202 { 3203 Key: "my-property", 3204 Value: "my-value-1", 3205 }, 3206 }, 3207 Timing: &resultstore.Timing{ 3208 StartTime: ×tamppb.Timestamp{ 3209 Seconds: 1234, 3210 }, 3211 }, 3212 }, 3213 }, 3214 }, 3215 }, 3216 { 3217 GroupID: "my-value-2", 3218 Invocations: []*invocation{ 3219 { 3220 InvocationProto: &resultstore.Invocation{ 3221 Name: invocationName("uuid-321"), 3222 Id: &resultstore.Invocation_Id{ 3223 InvocationId: "uuid-321", 3224 }, 3225 Properties: []*resultstore.Property{ 3226 { 3227 Key: "my-property", 3228 Value: "my-value-2", 3229 }, 3230 }, 3231 }, 3232 }, 3233 }, 3234 }, 3235 }, 3236 }, 3237 { 3238 name: "grouping by invocation id", 3239 invocations: []*invocation{ 3240 { 3241 InvocationProto: &resultstore.Invocation{ 3242 Name: invocationName("uuid-123"), 3243 Id: &resultstore.Invocation_Id{ 3244 InvocationId: "uuid-123", 3245 }, 3246 Timing: &resultstore.Timing{ 3247 StartTime: ×tamppb.Timestamp{ 3248 Seconds: 1234, 3249 }, 3250 }, 3251 }, 3252 }, 3253 { 3254 InvocationProto: &resultstore.Invocation{ 3255 Name: invocationName("uuid-321"), 3256 Id: &resultstore.Invocation_Id{ 3257 InvocationId: "uuid-321", 3258 }, 3259 }, 3260 }, 3261 }, 3262 want: []*invocationGroup{ 3263 { 3264 GroupID: "uuid-123", 3265 Invocations: []*invocation{ 3266 { 3267 InvocationProto: &resultstore.Invocation{ 3268 Name: invocationName("uuid-123"), 3269 Id: &resultstore.Invocation_Id{ 3270 InvocationId: "uuid-123", 3271 }, 3272 Timing: &resultstore.Timing{ 3273 StartTime: ×tamppb.Timestamp{ 3274 Seconds: 1234, 3275 }, 3276 }, 3277 }, 3278 }, 3279 }, 3280 }, 3281 { 3282 GroupID: "uuid-321", 3283 Invocations: []*invocation{ 3284 { 3285 InvocationProto: &resultstore.Invocation{ 3286 Name: invocationName("uuid-321"), 3287 Id: &resultstore.Invocation_Id{ 3288 InvocationId: "uuid-321", 3289 }, 3290 }, 3291 }, 3292 }, 3293 }, 3294 }, 3295 }, 3296 } 3297 3298 for _, tc := range cases { 3299 t.Run(tc.name, func(t *testing.T) { 3300 got := groupInvocations(logrus.WithField("case", tc.name), tc.tg, tc.invocations) 3301 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 3302 t.Errorf("groupInvocations(...) differed (-want, +got): %s", diff) 3303 } 3304 }) 3305 } 3306 } 3307 3308 func TestExtractHeaders(t *testing.T) { 3309 cases := []struct { 3310 name string 3311 isInv bool 3312 inv *invocation 3313 sar *singleActionResult 3314 headerConf *configpb.TestGroup_ColumnHeader 3315 want []string 3316 }{ 3317 { 3318 name: "empty invocation", 3319 isInv: true, 3320 want: nil, 3321 }, 3322 { 3323 name: "empty target results", 3324 isInv: false, 3325 want: nil, 3326 }, 3327 { 3328 name: "invocation has a config value present", 3329 isInv: true, 3330 inv: &invocation{ 3331 InvocationProto: &resultstore.Invocation{ 3332 Properties: []*resultstore.Property{ 3333 {Key: "field", Value: "green"}, 3334 {Key: "os", Value: "linux"}, 3335 }, 3336 }, 3337 }, 3338 headerConf: &configpb.TestGroup_ColumnHeader{ 3339 ConfigurationValue: "os", 3340 }, 3341 want: []string{"linux"}, 3342 }, 3343 { 3344 name: "invocation doesn't have a config value present", 3345 isInv: true, 3346 inv: &invocation{ 3347 InvocationProto: &resultstore.Invocation{ 3348 Properties: []*resultstore.Property{ 3349 {Key: "radio", Value: "head"}, 3350 }, 3351 }, 3352 }, 3353 headerConf: &configpb.TestGroup_ColumnHeader{ 3354 ConfigurationValue: "rainbows", 3355 }, 3356 want: nil, 3357 }, 3358 { 3359 name: "invocation has labels with prefixes", 3360 isInv: true, 3361 inv: &invocation{ 3362 InvocationProto: &resultstore.Invocation{ 3363 InvocationAttributes: &resultstore.InvocationAttributes{ 3364 Labels: []string{"os=linux", "env=prod", "test=fast", "test=hermetic"}, 3365 }, 3366 }, 3367 }, 3368 headerConf: &configpb.TestGroup_ColumnHeader{ 3369 Label: "test=", 3370 }, 3371 want: []string{"fast", "hermetic"}, 3372 }, 3373 { 3374 name: "target result has existing properties", 3375 sar: &singleActionResult{ 3376 ActionProto: &resultstore.Action{ 3377 ActionType: &resultstore.Action_TestAction{ 3378 TestAction: &resultstore.TestAction{ 3379 TestSuite: &resultstore.TestSuite{ 3380 Properties: []*resultstore.Property{ 3381 {Key: "test-property", Value: "fast"}, 3382 }, 3383 Tests: []*resultstore.Test{ 3384 { 3385 TestType: &resultstore.Test_TestCase{ 3386 TestCase: &resultstore.TestCase{ 3387 Properties: []*resultstore.Property{ 3388 {Key: "test-property", Value: "hermetic"}, 3389 }, 3390 }, 3391 }, 3392 }, 3393 }, 3394 }, 3395 }, 3396 }, 3397 }, 3398 }, 3399 headerConf: &configpb.TestGroup_ColumnHeader{ 3400 Property: "test-property", 3401 }, 3402 want: []string{"fast", "hermetic"}, 3403 }, 3404 { 3405 name: "target results doesn't have properties", 3406 sar: &singleActionResult{ 3407 ActionProto: &resultstore.Action{}, 3408 }, 3409 want: nil, 3410 }, 3411 } 3412 3413 for _, tc := range cases { 3414 t.Run(tc.name, func(t *testing.T) { 3415 var got []string 3416 switch { 3417 case tc.isInv: 3418 got = tc.inv.extractHeaders(tc.headerConf) 3419 default: 3420 got = tc.sar.extractHeaders(tc.headerConf) 3421 } 3422 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 3423 t.Errorf("extractHeaders(...) differed (-want, +got): %s", diff) 3424 } 3425 }) 3426 } 3427 } 3428 3429 func TestCompileHeaders(t *testing.T) { 3430 cases := []struct { 3431 name string 3432 columnHeaders []*configpb.TestGroup_ColumnHeader 3433 headers [][]string 3434 want []string 3435 }{ 3436 { 3437 name: "no custom headers configured", 3438 want: nil, 3439 }, 3440 { 3441 name: "single custom header set with no values fetched", 3442 columnHeaders: []*configpb.TestGroup_ColumnHeader{ 3443 {Label: "rapid="}, 3444 }, 3445 headers: make([][]string, 1), 3446 want: []string{""}, 3447 }, 3448 { 3449 name: "single custom header set with one value fetched", 3450 columnHeaders: []*configpb.TestGroup_ColumnHeader{ 3451 {ConfigurationValue: "os"}, 3452 }, 3453 headers: [][]string{ 3454 {"linux"}, 3455 }, 3456 want: []string{"linux"}, 3457 }, 3458 { 3459 name: "multiple custom headers set, don't list all", 3460 columnHeaders: []*configpb.TestGroup_ColumnHeader{ 3461 {Label: "os="}, 3462 {ConfigurationValue: "test-duration"}, 3463 }, 3464 headers: [][]string{ 3465 {"linux", "ubuntu"}, 3466 {"30m"}, 3467 }, 3468 want: []string{"*", "30m"}, 3469 }, 3470 { 3471 name: "multiple custom headers, list 'em all", 3472 columnHeaders: []*configpb.TestGroup_ColumnHeader{ 3473 {Property: "type", ListAllValues: true}, 3474 {Label: "test=", ListAllValues: true}, 3475 }, 3476 headers: [][]string{ 3477 {"grass", "flying"}, 3478 {"fast", "unit", "hermetic"}, 3479 }, 3480 want: []string{ 3481 "flying||grass", 3482 "fast||hermetic||unit", 3483 }, 3484 }, 3485 } 3486 3487 for _, tc := range cases { 3488 t.Run(tc.name, func(t *testing.T) { 3489 got := compileHeaders(tc.columnHeaders, tc.headers) 3490 if diff := cmp.Diff(tc.want, got); diff != "" { 3491 t.Fatalf("compileHeaders(...) differed (-want,+got): %s", diff) 3492 } 3493 }) 3494 } 3495 } 3496 3497 func TestCalculateMetrics(t *testing.T) { 3498 cases := []struct { 3499 name string 3500 sar *singleActionResult 3501 want map[string]float64 3502 }{ 3503 { 3504 name: "no numeric properties, no duration", 3505 sar: &singleActionResult{ 3506 ActionProto: &resultstore.Action{ 3507 ActionType: &resultstore.Action_TestAction{ 3508 TestAction: &resultstore.TestAction{ 3509 TestSuite: &resultstore.TestSuite{ 3510 Properties: []*resultstore.Property{ 3511 {Key: "marco", Value: "polo"}, 3512 }, 3513 }, 3514 }, 3515 }, 3516 }, 3517 }, 3518 want: map[string]float64{}, 3519 }, 3520 { 3521 name: "no numeric properties, suite duration", 3522 sar: &singleActionResult{ 3523 ActionProto: &resultstore.Action{ 3524 ActionType: &resultstore.Action_TestAction{ 3525 TestAction: &resultstore.TestAction{ 3526 TestSuite: &resultstore.TestSuite{ 3527 Properties: []*resultstore.Property{ 3528 {Key: "marco", Value: "polo"}, 3529 }, 3530 Timing: &resultstore.Timing{ 3531 Duration: &durationpb.Duration{ 3532 Seconds: 120, 3533 }, 3534 }, 3535 }, 3536 }, 3537 }, 3538 }, 3539 }, 3540 want: map[string]float64{ 3541 updater.TestMethodsElapsedKey: 2, 3542 }, 3543 }, 3544 { 3545 name: "no numeric properties, cases duration only", 3546 sar: &singleActionResult{ 3547 ActionProto: &resultstore.Action{ 3548 ActionType: &resultstore.Action_TestAction{ 3549 TestAction: &resultstore.TestAction{ 3550 TestSuite: &resultstore.TestSuite{ 3551 Properties: []*resultstore.Property{ 3552 {Key: "marco", Value: "polo"}, 3553 }, 3554 Tests: []*resultstore.Test{ 3555 { 3556 TestType: &resultstore.Test_TestCase{ 3557 TestCase: &resultstore.TestCase{ 3558 Timing: &resultstore.Timing{ 3559 Duration: &durationpb.Duration{ 3560 Seconds: 60, 3561 }, 3562 }, 3563 }, 3564 }, 3565 }, 3566 { 3567 TestType: &resultstore.Test_TestCase{ 3568 TestCase: &resultstore.TestCase{ 3569 Timing: &resultstore.Timing{ 3570 Duration: &durationpb.Duration{ 3571 Seconds: 60, 3572 }, 3573 }, 3574 }, 3575 }, 3576 }, 3577 }, 3578 }, 3579 }, 3580 }, 3581 }, 3582 }, 3583 want: map[string]float64{ 3584 updater.TestMethodsElapsedKey: 2, 3585 }, 3586 }, 3587 { 3588 name: "numeric properties and durations", 3589 sar: &singleActionResult{ 3590 ActionProto: &resultstore.Action{ 3591 ActionType: &resultstore.Action_TestAction{ 3592 TestAction: &resultstore.TestAction{ 3593 TestSuite: &resultstore.TestSuite{ 3594 Properties: []*resultstore.Property{ 3595 {Key: "pizza", Value: "12"}, 3596 }, 3597 Tests: []*resultstore.Test{ 3598 { 3599 TestType: &resultstore.Test_TestCase{ 3600 TestCase: &resultstore.TestCase{ 3601 Timing: &resultstore.Timing{ 3602 Duration: &durationpb.Duration{ 3603 Seconds: 60, 3604 }, 3605 }, 3606 Properties: []*resultstore.Property{ 3607 {Key: "pizza", Value: "6"}, 3608 }, 3609 }, 3610 }, 3611 }, 3612 { 3613 TestType: &resultstore.Test_TestCase{ 3614 TestCase: &resultstore.TestCase{ 3615 Timing: &resultstore.Timing{ 3616 Duration: &durationpb.Duration{ 3617 Seconds: 60, 3618 }, 3619 }, 3620 Properties: []*resultstore.Property{ 3621 {Key: "pizza", Value: "6"}, 3622 }, 3623 }, 3624 }, 3625 }, 3626 }, 3627 }, 3628 }, 3629 }, 3630 }, 3631 }, 3632 want: map[string]float64{ 3633 "pizza": 8, 3634 updater.TestMethodsElapsedKey: 2, 3635 }, 3636 }, 3637 { 3638 name: "numeric properties and durations", 3639 sar: &singleActionResult{ 3640 TargetProto: &resultstore.Target{ 3641 Timing: &resultstore.Timing{ 3642 Duration: &durationpb.Duration{ 3643 Seconds: 600, 3644 }, 3645 }, 3646 }, 3647 }, 3648 want: map[string]float64{ 3649 updater.ElapsedKey: 10, 3650 }, 3651 }, 3652 } 3653 3654 for _, tc := range cases { 3655 t.Run(tc.name, func(t *testing.T) { 3656 got := calculateMetrics(tc.sar) 3657 if diff := cmp.Diff(tc.want, got); diff != "" { 3658 t.Fatalf("calculateMetrics(...) differed (-want,+got): %s", diff) 3659 } 3660 }) 3661 } 3662 } 3663 3664 func TestShouldUpdate(t *testing.T) { 3665 cases := []struct { 3666 name string 3667 tg *configpb.TestGroup 3668 clientExists bool 3669 want bool 3670 }{ 3671 { 3672 name: "nil", 3673 want: false, 3674 }, 3675 { 3676 name: "test group config only", 3677 tg: &configpb.TestGroup{ 3678 ResultSource: &configpb.TestGroup_ResultSource{ 3679 ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{}, 3680 }, 3681 }, 3682 clientExists: false, 3683 want: false, 3684 }, 3685 { 3686 name: "client only", 3687 clientExists: true, 3688 want: false, 3689 }, 3690 { 3691 name: "client and non-ResultStore config", 3692 tg: &configpb.TestGroup{ 3693 ResultSource: &configpb.TestGroup_ResultSource{ 3694 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{}, 3695 }, 3696 }, 3697 clientExists: false, 3698 want: false, 3699 }, 3700 { 3701 name: "basically works", 3702 tg: &configpb.TestGroup{ 3703 ResultSource: &configpb.TestGroup_ResultSource{ 3704 ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{ 3705 ResultstoreConfig: &configpb.ResultStoreConfig{ 3706 Project: "my-gcp-project", 3707 }, 3708 }, 3709 }, 3710 }, 3711 clientExists: true, 3712 want: true, 3713 }, 3714 } 3715 for _, tc := range cases { 3716 t.Run(tc.name, func(t *testing.T) { 3717 // Create a fake client if specified. 3718 var dlClient *DownloadClient 3719 if tc.clientExists { 3720 dlClient = &DownloadClient{client: &fakeClient{}} 3721 } 3722 if got := shouldUpdate(logrus.WithField("name", tc.name), tc.tg, dlClient); tc.want != got { 3723 t.Errorf("shouldUpdate(%v, clientExists = %t) got %t, want %t", tc.tg, tc.clientExists, got, tc.want) 3724 } 3725 }) 3726 } 3727 }