github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/read_test.go (about) 1 /* 2 Copyright 2020 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 updater 18 19 import ( 20 "context" 21 "encoding/json" 22 "encoding/xml" 23 "errors" 24 "fmt" 25 "net/url" 26 "sort" 27 "sync" 28 "testing" 29 "time" 30 31 "cloud.google.com/go/storage" 32 "github.com/GoogleCloudPlatform/testgrid/metadata" 33 "github.com/GoogleCloudPlatform/testgrid/metadata/junit" 34 configpb "github.com/GoogleCloudPlatform/testgrid/pb/config" 35 statepb "github.com/GoogleCloudPlatform/testgrid/pb/state" 36 statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status" 37 "github.com/GoogleCloudPlatform/testgrid/util/gcs" 38 "github.com/GoogleCloudPlatform/testgrid/util/gcs/fake" 39 "github.com/google/go-cmp/cmp" 40 "github.com/sirupsen/logrus" 41 "google.golang.org/protobuf/testing/protocmp" 42 core "k8s.io/api/core/v1" 43 ) 44 45 type fakeObject = fake.Object 46 type fakeClient = fake.Client 47 type fakeIterator = fake.Iterator 48 49 func TestDownloadGrid(t *testing.T) { 50 cases := []struct { 51 name string 52 }{ 53 {}, 54 } 55 56 for _, tc := range cases { 57 t.Run(tc.name, func(t *testing.T) { 58 }) 59 } 60 } 61 62 func resolveOrDie(path *gcs.Path, s string) *gcs.Path { 63 p, err := path.ResolveReference(&url.URL{Path: s}) 64 if err != nil { 65 panic(err) 66 } 67 return p 68 } 69 70 func jsonData(i interface{}) string { 71 buf, err := json.Marshal(i) 72 if err != nil { 73 panic(err) 74 } 75 return string(buf) 76 } 77 78 func xmlData(i interface{}) string { 79 buf, err := xml.Marshal(i) 80 if err != nil { 81 panic(err) 82 } 83 return string(buf) 84 } 85 86 func makeJunit(passed, failed []string) string { 87 var suite junit.Suite 88 for _, name := range passed { 89 suite.Results = append(suite.Results, junit.Result{Name: name}) 90 } 91 92 for _, name := range failed { 93 f := name 94 suite.Results = append(suite.Results, junit.Result{ 95 Name: name, 96 Failure: &junit.Failure{Value: f}, 97 }) 98 } 99 return xmlData(suite) 100 } 101 102 func pint64(n int64) *int64 { 103 return &n 104 } 105 106 func TestHintStarted(t *testing.T) { 107 cases := []struct { 108 name string 109 cols []InflatedColumn 110 want string 111 }{ 112 { 113 name: "basic", 114 }, 115 { 116 name: "ordered", 117 cols: []InflatedColumn{ 118 { 119 Column: &statepb.Column{ 120 Hint: "b", 121 Started: 1200, 122 }, 123 }, 124 { 125 Column: &statepb.Column{ 126 Hint: "a", 127 Started: 1100, 128 }, 129 }, 130 }, 131 want: "b", 132 }, 133 { 134 name: "reversed", 135 cols: []InflatedColumn{ 136 { 137 Column: &statepb.Column{ 138 Hint: "a", 139 Started: 1100, 140 }, 141 }, 142 { 143 Column: &statepb.Column{ 144 Hint: "b", 145 Started: 1200, 146 }, 147 }, 148 }, 149 want: "b", 150 }, 151 { 152 name: "different", // hint and started come from diff cols 153 cols: []InflatedColumn{ 154 { 155 Column: &statepb.Column{ 156 Hint: "a", 157 Started: 1100, 158 }, 159 }, 160 { 161 Column: &statepb.Column{ 162 Hint: "b", 163 Started: 900, 164 }, 165 }, 166 }, 167 want: "b", 168 }, 169 { 170 name: "numerical", // hint10 > hint2 171 cols: []InflatedColumn{ 172 { 173 Column: &statepb.Column{ 174 Hint: "hint2", 175 }, 176 }, 177 { 178 Column: &statepb.Column{ 179 Hint: "hint10", 180 }, 181 }, 182 }, 183 want: "hint10", 184 }, 185 } 186 187 for _, tc := range cases { 188 t.Run(tc.name, func(t *testing.T) { 189 got := hintStarted(tc.cols) 190 if tc.want != got { 191 t.Errorf("hintStarted() got hint %q, want %q", got, tc.want) 192 } 193 }) 194 } 195 } 196 197 func pstr(s string) *string { return &s } 198 199 func TestReadColumns(t *testing.T) { 200 now := time.Now().Unix() 201 yes := true 202 var no bool 203 var noStartErr *noStartError 204 cases := []struct { 205 name string 206 ctx context.Context 207 builds []fakeBuild 208 group *configpb.TestGroup 209 stop time.Time 210 dur time.Duration 211 concurrency int 212 readResultOverride *resultReader 213 enableIgnoreSkip bool 214 215 expected []InflatedColumn 216 err bool 217 }{ 218 { 219 name: "basically works", 220 }, 221 { 222 name: "convert results correctly", 223 builds: []fakeBuild{ 224 { 225 id: "11", 226 started: &fakeObject{ 227 Data: jsonData(metadata.Started{Timestamp: now + 11}), 228 }, 229 finished: &fakeObject{ 230 Data: jsonData(metadata.Finished{ 231 Timestamp: pint64(now + 22), 232 Passed: &no, 233 }), 234 }, 235 podInfo: podInfoSuccess, 236 }, 237 { 238 id: "10", 239 started: &fakeObject{ 240 Data: jsonData(metadata.Started{Timestamp: now + 10}), 241 }, 242 finished: &fakeObject{ 243 Data: jsonData(metadata.Finished{ 244 Timestamp: pint64(now + 20), 245 Passed: &yes, 246 }), 247 }, 248 }, 249 }, 250 group: &configpb.TestGroup{ 251 GcsPrefix: "bucket/path/to/build/", 252 }, 253 expected: []InflatedColumn{ 254 { 255 Column: &statepb.Column{ 256 Build: "10", 257 Hint: "10", 258 Started: float64(now+10) * 1000, 259 }, 260 Cells: map[string]cell{ 261 "build." + overallRow: { 262 Result: statuspb.TestStatus_PASS, 263 Metrics: map[string]float64{ 264 "test-duration-minutes": 10 / 60.0, 265 }, 266 }, 267 "build." + podInfoRow: podInfoMissingCell, 268 }, 269 }, 270 { 271 Column: &statepb.Column{ 272 Build: "11", 273 Hint: "11", 274 Started: float64(now+11) * 1000, 275 }, 276 Cells: map[string]cell{ 277 "build." + overallRow: { 278 Result: statuspb.TestStatus_FAIL, 279 Icon: "F", 280 Message: "Build failed outside of test results", 281 Metrics: map[string]float64{ 282 "test-duration-minutes": 11 / 60.0, 283 }, 284 }, 285 "build." + podInfoRow: podInfoPassCell, 286 }, 287 }, 288 }, 289 }, 290 { 291 name: "column headers processed correctly", 292 builds: []fakeBuild{ 293 { 294 id: "11", 295 started: &fakeObject{ 296 Data: jsonData(metadata.Started{Timestamp: now + 11}), 297 }, 298 finished: &fakeObject{ 299 Data: jsonData(metadata.Finished{ 300 Timestamp: pint64(now + 22), 301 Passed: &no, 302 Metadata: metadata.Metadata{ 303 metadata.JobVersion: "v0.0.0-alpha.0+build11", 304 "random": "new information", 305 }, 306 }), 307 }, 308 podInfo: podInfoSuccess, 309 }, 310 { 311 id: "10", 312 started: &fakeObject{ 313 Data: jsonData(metadata.Started{Timestamp: now + 10}), 314 }, 315 finished: &fakeObject{ 316 Data: jsonData(metadata.Finished{ 317 Timestamp: pint64(now + 20), 318 Passed: &yes, 319 Metadata: metadata.Metadata{ 320 metadata.JobVersion: "v0.0.0-alpha.0+build10", 321 "random": "old information", 322 }, 323 }), 324 }, 325 }, 326 }, 327 group: &configpb.TestGroup{ 328 GcsPrefix: "bucket/path/to/build/", 329 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 330 { 331 ConfigurationValue: "Commit", 332 }, 333 { 334 ConfigurationValue: "random", 335 }, 336 }, 337 }, 338 expected: []InflatedColumn{ 339 { 340 Column: &statepb.Column{ 341 Build: "10", 342 Hint: "10", 343 Started: float64(now+10) * 1000, 344 Extra: []string{ 345 "build10", 346 "old information", 347 }, 348 }, 349 Cells: map[string]cell{ 350 "build." + overallRow: { 351 Result: statuspb.TestStatus_PASS, 352 Metrics: map[string]float64{ 353 "test-duration-minutes": 10 / 60.0, 354 }, 355 }, 356 "build." + podInfoRow: podInfoMissingCell, 357 }, 358 }, 359 { 360 Column: &statepb.Column{ 361 Build: "11", 362 Hint: "11", 363 Started: float64(now+11) * 1000, 364 Extra: []string{ 365 "build11", 366 "new information", 367 }, 368 }, 369 Cells: map[string]cell{ 370 "build." + overallRow: { 371 Result: statuspb.TestStatus_FAIL, 372 Icon: "F", 373 Message: "Build failed outside of test results", 374 Metrics: map[string]float64{ 375 "test-duration-minutes": 11 / 60.0, 376 }, 377 }, 378 "build." + podInfoRow: podInfoPassCell, 379 }, 380 }, 381 }, 382 }, 383 { 384 name: "name config works correctly", 385 builds: []fakeBuild{ 386 { 387 id: "10", 388 started: &fakeObject{ 389 Data: jsonData(metadata.Started{Timestamp: now + 10}), 390 }, 391 podInfo: podInfoSuccess, 392 finished: &fakeObject{ 393 Data: jsonData(metadata.Finished{ 394 Timestamp: pint64(now + 20), 395 Passed: &yes, 396 }), 397 }, 398 artifacts: map[string]fakeObject{ 399 "junit_context-a_33.xml": { 400 Data: makeJunit([]string{"good"}, []string{"bad"}), 401 }, 402 "junit_context-b_44.xml": { 403 Data: makeJunit([]string{"good"}, []string{"bad"}), 404 }, 405 }, 406 }, 407 }, 408 group: &configpb.TestGroup{ 409 GcsPrefix: "bucket/path/to/build/", 410 TestNameConfig: &configpb.TestNameConfig{ 411 NameFormat: "name %s - context %s - thread %s", 412 NameElements: []*configpb.TestNameConfig_NameElement{ 413 { 414 TargetConfig: "Tests name", 415 }, 416 { 417 TargetConfig: "Context", 418 }, 419 { 420 TargetConfig: "Thread", 421 }, 422 }, 423 }, 424 }, 425 expected: []InflatedColumn{ 426 { 427 Column: &statepb.Column{ 428 Build: "10", 429 Hint: "10", 430 Started: float64(now+10) * 1000, 431 }, 432 Cells: map[string]cell{ 433 "build." + overallRow: { 434 Result: statuspb.TestStatus_PASS, 435 Metrics: map[string]float64{ 436 "test-duration-minutes": 10 / 60.0, 437 }, 438 }, 439 "build." + podInfoRow: podInfoPassCell, 440 "name good - context context-a - thread 33": { 441 Result: statuspb.TestStatus_PASS, 442 }, 443 "name bad - context context-a - thread 33": { 444 Result: statuspb.TestStatus_FAIL, 445 Icon: "F", 446 Message: "bad", 447 }, 448 "name good - context context-b - thread 44": { 449 Result: statuspb.TestStatus_PASS, 450 }, 451 "name bad - context context-b - thread 44": { 452 Result: statuspb.TestStatus_FAIL, 453 Icon: "F", 454 Message: "bad", 455 }, 456 }, 457 }, 458 }, 459 }, 460 { 461 name: "truncate columns after the newest old result", 462 stop: time.Unix(now+13, 0), // should capture 14 and 13 463 builds: []fakeBuild{ 464 { 465 id: "14", 466 started: &fakeObject{ 467 Data: jsonData(metadata.Started{Timestamp: now + 14}), 468 }, 469 finished: &fakeObject{ 470 Data: jsonData(metadata.Finished{ 471 Timestamp: pint64(now + 28), 472 Passed: &yes, 473 }), 474 }, 475 podInfo: podInfoSuccess, 476 }, 477 { 478 id: "13", 479 started: &fakeObject{ 480 Data: jsonData(metadata.Started{Timestamp: now + 13}), 481 }, 482 finished: &fakeObject{ 483 Data: jsonData(metadata.Finished{ 484 Timestamp: pint64(now + 26), 485 Passed: &yes, 486 }), 487 }, 488 podInfo: podInfoSuccess, 489 }, 490 { 491 id: "12", 492 started: &fakeObject{ 493 Data: jsonData(metadata.Started{Timestamp: now + 12}), 494 }, 495 finished: &fakeObject{ 496 Data: jsonData(metadata.Finished{ 497 Timestamp: pint64(now + 24), 498 Passed: &yes, 499 }), 500 }, 501 }, 502 { 503 id: "11", 504 started: &fakeObject{ 505 Data: jsonData(metadata.Started{Timestamp: now + 11}), 506 }, 507 finished: &fakeObject{ 508 Data: jsonData(metadata.Finished{ 509 Timestamp: pint64(now + 22), 510 Passed: &yes, 511 }), 512 }, 513 }, 514 { 515 id: "10", 516 started: &fakeObject{ 517 Data: jsonData(metadata.Started{Timestamp: now + 10}), 518 }, 519 finished: &fakeObject{ 520 Data: jsonData(metadata.Finished{ 521 Timestamp: pint64(now + 20), 522 Passed: &yes, 523 }), 524 }, 525 }, 526 }, 527 group: &configpb.TestGroup{ 528 GcsPrefix: "bucket/path/to/build/", 529 }, 530 expected: []InflatedColumn{ 531 ancientColumn("10", .01, nil, fmt.Sprintf("build too old; started %v before %v)", now+10, now+13)), 532 ancientColumn("11", .02, nil, fmt.Sprintf("build too old; started %v before %v)", now+11, now+13)), 533 ancientColumn("12", .03, nil, fmt.Sprintf("build too old; started %v before %v)", now+12, now+13)), 534 { 535 Column: &statepb.Column{ 536 Build: "13", 537 Hint: "13", 538 Started: float64(now+13) * 1000, 539 }, 540 Cells: map[string]cell{ 541 "build." + overallRow: { 542 Result: statuspb.TestStatus_PASS, 543 Metrics: map[string]float64{ 544 "test-duration-minutes": 13 / 60.0, 545 }, 546 }, 547 "build." + podInfoRow: podInfoPassCell, 548 }, 549 }, 550 { 551 Column: &statepb.Column{ 552 Build: "14", 553 Hint: "14", 554 Started: float64(now+14) * 1000, 555 }, 556 Cells: map[string]cell{ 557 "build." + overallRow: { 558 Result: statuspb.TestStatus_PASS, 559 Metrics: map[string]float64{ 560 "test-duration-minutes": 14 / 60.0, 561 }, 562 }, 563 "build." + podInfoRow: podInfoPassCell, 564 }, 565 }, 566 }, 567 }, 568 { 569 name: "include no-start-time column", 570 stop: time.Unix(now+13, 0), // should capture 15, 14, 13 571 builds: []fakeBuild{ 572 { 573 id: "15", 574 started: &fakeObject{ 575 Data: jsonData(metadata.Started{Timestamp: now + 15}), 576 }, 577 finished: &fakeObject{ 578 Data: jsonData(metadata.Finished{ 579 Timestamp: pint64(now + 30), 580 Passed: &yes, 581 }), 582 }, 583 podInfo: podInfoSuccess, 584 }, 585 { 586 id: "14", 587 started: &fakeObject{ 588 Data: jsonData(metadata.Started{Timestamp: 0}), 589 }, 590 finished: &fakeObject{ 591 Data: jsonData(metadata.Finished{ 592 Timestamp: pint64(now + 28), 593 Passed: &yes, 594 }), 595 }, 596 podInfo: podInfoSuccess, 597 }, 598 { 599 id: "13", 600 started: &fakeObject{ 601 Data: jsonData(metadata.Started{Timestamp: now + 13}), 602 }, 603 finished: &fakeObject{ 604 Data: jsonData(metadata.Finished{ 605 Timestamp: pint64(now + 26), 606 Passed: &yes, 607 }), 608 }, 609 podInfo: podInfoSuccess, 610 }, 611 }, 612 group: &configpb.TestGroup{ 613 GcsPrefix: "bucket/path/to/build/", 614 }, 615 expected: []InflatedColumn{ 616 { 617 Column: &statepb.Column{ 618 Build: "13", 619 Hint: "13", 620 Started: float64(now+13) * 1000, 621 }, 622 Cells: map[string]cell{ 623 "build." + overallRow: { 624 Result: statuspb.TestStatus_PASS, 625 Metrics: map[string]float64{ 626 "test-duration-minutes": 13 / 60.0, 627 }, 628 }, 629 "build." + podInfoRow: podInfoPassCell, 630 }, 631 }, 632 noStartColumn("14", float64(now+13)*1000+0.01, nil, noStartErr.Error()), // start * 1000 + 0.01 * failures (1) 633 { 634 Column: &statepb.Column{ 635 Build: "15", 636 Hint: "15", 637 Started: float64(now+15) * 1000, 638 }, 639 Cells: map[string]cell{ 640 "build." + overallRow: { 641 Result: statuspb.TestStatus_PASS, 642 Metrics: map[string]float64{ 643 "test-duration-minutes": 15 / 60.0, 644 }, 645 }, 646 "build." + podInfoRow: podInfoPassCell, 647 }, 648 }, 649 }, 650 }, 651 { 652 name: "high concurrency works", 653 concurrency: 4, 654 builds: []fakeBuild{ 655 { 656 id: "13", 657 started: &fakeObject{ 658 Data: jsonData(metadata.Started{Timestamp: now + 13}), 659 }, 660 finished: &fakeObject{ 661 Data: jsonData(metadata.Finished{ 662 Timestamp: pint64(now + 26), 663 Passed: &yes, 664 }), 665 }, 666 podInfo: podInfoSuccess, 667 }, 668 { 669 id: "12", 670 started: &fakeObject{ 671 Data: jsonData(metadata.Started{Timestamp: now + 12}), 672 }, 673 finished: &fakeObject{ 674 Data: jsonData(metadata.Finished{ 675 Timestamp: pint64(now + 24), 676 Passed: &yes, 677 }), 678 }, 679 }, 680 { 681 id: "11", 682 started: &fakeObject{ 683 Data: jsonData(metadata.Started{Timestamp: now + 11}), 684 }, 685 finished: &fakeObject{ 686 Data: jsonData(metadata.Finished{ 687 Timestamp: pint64(now + 22), 688 Passed: &yes, 689 }), 690 }, 691 podInfo: podInfoSuccess, 692 }, 693 { 694 id: "10", 695 started: &fakeObject{ 696 Data: jsonData(metadata.Started{Timestamp: now + 10}), 697 }, 698 finished: &fakeObject{ 699 Data: jsonData(metadata.Finished{ 700 Timestamp: pint64(now + 20), 701 Passed: &yes, 702 }), 703 }, 704 }, 705 }, 706 group: &configpb.TestGroup{ 707 GcsPrefix: "bucket/path/to/build/", 708 }, 709 expected: []InflatedColumn{ 710 { 711 Column: &statepb.Column{ 712 Build: "13", 713 Hint: "13", 714 Started: float64(now+13) * 1000, 715 }, 716 Cells: map[string]cell{ 717 "build." + overallRow: { 718 Result: statuspb.TestStatus_PASS, 719 Metrics: map[string]float64{ 720 "test-duration-minutes": 13 / 60.0, 721 }, 722 }, 723 "build." + podInfoRow: podInfoPassCell, 724 }, 725 }, 726 { 727 Column: &statepb.Column{ 728 Build: "12", 729 Hint: "12", 730 Started: float64(now+12) * 1000, 731 }, 732 Cells: map[string]cell{ 733 "build." + overallRow: { 734 Result: statuspb.TestStatus_PASS, 735 Metrics: map[string]float64{ 736 "test-duration-minutes": 12 / 60.0, 737 }, 738 }, 739 "build." + podInfoRow: podInfoMissingCell, 740 }, 741 }, 742 { 743 Column: &statepb.Column{ 744 Build: "11", 745 Hint: "11", 746 Started: float64(now+11) * 1000, 747 }, 748 Cells: map[string]cell{ 749 "build." + overallRow: { 750 Result: statuspb.TestStatus_PASS, 751 Metrics: map[string]float64{ 752 "test-duration-minutes": 11 / 60.0, 753 }, 754 }, 755 "build." + podInfoRow: podInfoPassCell, 756 }, 757 }, 758 { 759 Column: &statepb.Column{ 760 Build: "10", 761 Hint: "10", 762 Started: float64(now+10) * 1000, 763 }, 764 Cells: map[string]cell{ 765 "build." + overallRow: { 766 Result: statuspb.TestStatus_PASS, 767 Metrics: map[string]float64{ 768 "test-duration-minutes": 10 / 60.0, 769 }, 770 }, 771 "build." + podInfoRow: podInfoMissingCell, 772 }, 773 }, 774 }, 775 }, 776 { 777 name: "truncate columns after the newest old result with high concurrency", 778 concurrency: 30, 779 stop: time.Unix(now+13, 0), // should capture 13 and 12 780 builds: []fakeBuild{ 781 { 782 id: "13", 783 started: &fakeObject{ 784 Data: jsonData(metadata.Started{Timestamp: now + 13}), 785 }, 786 finished: &fakeObject{ 787 Data: jsonData(metadata.Finished{ 788 Timestamp: pint64(now + 26), 789 Passed: &yes, 790 }), 791 }, 792 }, 793 { 794 id: "12", 795 started: &fakeObject{ 796 Data: jsonData(metadata.Started{Timestamp: now + 12}), 797 }, 798 finished: &fakeObject{ 799 Data: jsonData(metadata.Finished{ 800 Timestamp: pint64(now + 24), 801 Passed: &yes, 802 }), 803 }, 804 podInfo: podInfoSuccess, 805 }, 806 { 807 id: "11", 808 started: &fakeObject{ 809 Data: jsonData(metadata.Started{Timestamp: now + 11}), 810 }, 811 finished: &fakeObject{ 812 Data: jsonData(metadata.Finished{ 813 Timestamp: pint64(now + 22), 814 Passed: &yes, 815 }), 816 }, 817 }, 818 { 819 id: "10", 820 started: &fakeObject{ 821 Data: jsonData(metadata.Started{Timestamp: now + 10}), 822 }, 823 finished: &fakeObject{ 824 Data: jsonData(metadata.Finished{ 825 Timestamp: pint64(now + 20), 826 Passed: &yes, 827 }), 828 }, 829 }, 830 }, 831 group: &configpb.TestGroup{ 832 GcsPrefix: "bucket/path/to/build/", 833 }, 834 expected: []InflatedColumn{ 835 { 836 Column: &statepb.Column{ 837 Build: "13", 838 Hint: "13", 839 Started: float64(now+13) * 1000, 840 }, 841 Cells: map[string]cell{ 842 "build." + overallRow: { 843 Result: statuspb.TestStatus_PASS, 844 Metrics: map[string]float64{ 845 "test-duration-minutes": 13 / 60.0, 846 }, 847 }, 848 "build." + podInfoRow: podInfoMissingCell, 849 }, 850 }, 851 { 852 Column: &statepb.Column{ 853 Build: "12", 854 Hint: "12", 855 Started: float64(now+12) * 1000, 856 }, 857 Cells: map[string]cell{ 858 "build." + overallRow: { 859 Result: statuspb.TestStatus_PASS, 860 Metrics: map[string]float64{ 861 "test-duration-minutes": 12 / 60.0, 862 }, 863 }, 864 "build." + podInfoRow: podInfoPassCell, 865 }, 866 }, 867 // drop 11 and 10 868 }, 869 }, 870 { 871 name: "cancelled context returns error", 872 builds: []fakeBuild{ 873 {id: "10"}, 874 }, 875 ctx: func() context.Context { 876 ctx, cancel := context.WithCancel(context.Background()) 877 cancel() 878 ctx.Err() 879 return ctx 880 }(), 881 }, 882 { 883 name: "some errors", 884 builds: []fakeBuild{ 885 { 886 id: "14-err", 887 started: &fakeObject{ 888 OpenErr: errors.New("fake open 14-err"), 889 }, 890 }, 891 { 892 id: "13", 893 started: &fakeObject{ 894 Data: jsonData(metadata.Started{Timestamp: now + 13}), 895 }, 896 finished: &fakeObject{ 897 Data: jsonData(metadata.Finished{ 898 Timestamp: pint64(now + 26), 899 Passed: &no, 900 }), 901 }, 902 podInfo: podInfoSuccess, 903 }, 904 { 905 id: "10-b-err", 906 started: &fakeObject{ 907 OpenErr: errors.New("fake open 10-b-err"), 908 }, 909 }, 910 { 911 id: "10-a-err", 912 started: &fakeObject{ 913 ReadErr: errors.New("fake read 10-a-err"), 914 }, 915 }, 916 { 917 id: "9", 918 started: &fakeObject{ 919 Data: jsonData(metadata.Started{Timestamp: now + 9}), 920 }, 921 finished: &fakeObject{ 922 Data: jsonData(metadata.Finished{ 923 Timestamp: pint64(now + 18), 924 Passed: &yes, 925 }), 926 }, 927 }, 928 { 929 id: "8-err", 930 started: &fakeObject{ 931 ReadErr: errors.New("fake read 8-err"), 932 }, 933 }, 934 }, 935 group: &configpb.TestGroup{ 936 GcsPrefix: "bucket/path/to/build/", 937 }, 938 expected: []InflatedColumn{ 939 { 940 Column: &statepb.Column{ 941 Build: "8-err", 942 Hint: "8-err", 943 Started: .01, 944 }, 945 Cells: map[string]cell{ 946 overallRow: { 947 Result: statuspb.TestStatus_TOOL_FAIL, 948 Message: "Failed to download gs://bucket/path/to/build/8-err/: started: read: decode: fake read 8-err", 949 }, 950 }, 951 }, 952 { 953 Column: &statepb.Column{ 954 Build: "9", 955 Hint: "9", 956 Started: float64(now+9) * 1000, 957 }, 958 Cells: map[string]cell{ 959 "build." + overallRow: { 960 Result: statuspb.TestStatus_PASS, 961 Metrics: map[string]float64{ 962 "test-duration-minutes": 9 / 60.0, 963 }, 964 }, 965 "build." + podInfoRow: podInfoMissingCell, 966 }, 967 }, 968 { 969 Column: &statepb.Column{ 970 Build: "10-a-err", 971 Hint: "10-a-err", 972 Started: float64(now+9)*1000 + .01, 973 }, 974 Cells: map[string]cell{ 975 overallRow: { 976 Result: statuspb.TestStatus_TOOL_FAIL, 977 Message: "Failed to download gs://bucket/path/to/build/10-a-err/: started: read: decode: fake read 10-a-err", 978 }, 979 }, 980 }, 981 { 982 Column: &statepb.Column{ 983 Build: "10-b-err", 984 Hint: "10-b-err", 985 Started: float64(now+9)*1000 + 0.02, 986 }, 987 Cells: map[string]cell{ 988 overallRow: { 989 Result: statuspb.TestStatus_TOOL_FAIL, 990 Message: "Failed to download gs://bucket/path/to/build/10-b-err/: started: read: open: fake open 10-b-err", 991 }, 992 }, 993 }, 994 { 995 Column: &statepb.Column{ 996 Build: "13", 997 Hint: "13", 998 Started: float64(now+13) * 1000, 999 }, 1000 Cells: map[string]cell{ 1001 "build." + overallRow: { 1002 Result: statuspb.TestStatus_FAIL, 1003 Icon: "F", 1004 Message: "Build failed outside of test results", 1005 Metrics: map[string]float64{ 1006 "test-duration-minutes": 13 / 60.0, 1007 }, 1008 }, 1009 "build." + podInfoRow: podInfoPassCell, 1010 }, 1011 }, 1012 { 1013 Column: &statepb.Column{ 1014 Build: "14-err", 1015 Hint: "14-err", 1016 Started: float64(now+13)*1000 + 0.01, 1017 }, 1018 Cells: map[string]cell{ 1019 overallRow: { 1020 Result: statuspb.TestStatus_TOOL_FAIL, 1021 Message: "Failed to download gs://bucket/path/to/build/14-err/: started: read: open: fake open 14-err", 1022 }, 1023 }, 1024 }, 1025 }, 1026 }, 1027 { 1028 name: "only errors", 1029 builds: []fakeBuild{ 1030 { 1031 id: "10-b-err", 1032 started: &fakeObject{ 1033 OpenErr: errors.New("fake open 10-b-err"), 1034 }, 1035 }, 1036 { 1037 id: "10-a-err", 1038 started: &fakeObject{ 1039 ReadErr: errors.New("fake read 10-a-err"), 1040 }, 1041 }, 1042 }, 1043 group: &configpb.TestGroup{ 1044 GcsPrefix: "bucket/path/to/build/", 1045 }, 1046 expected: []InflatedColumn{ 1047 erroredColumn("10-a-err", 0.01, nil, "Failed to download gs://bucket/path/to/build/10-a-err/: started: read: decode: fake read 10-a-err"), 1048 erroredColumn("10-b-err", 0.02, nil, "Failed to download gs://bucket/path/to/build/10-b-err/: started: read: open: fake open 10-b-err"), 1049 }, 1050 }, 1051 { 1052 name: "ignore_skip works when enabled", 1053 group: &configpb.TestGroup{ 1054 IgnoreSkip: true, 1055 }, 1056 enableIgnoreSkip: true, 1057 builds: []fakeBuild{ 1058 { 1059 id: "build-1", 1060 podInfo: podInfoSuccess, 1061 started: &fakeObject{ 1062 Data: jsonData(metadata.Started{Timestamp: now}), 1063 }, 1064 finished: &fakeObject{ 1065 Data: jsonData(metadata.Finished{ 1066 Timestamp: pint64(now + 10), 1067 Passed: &yes, 1068 }), 1069 }, 1070 artifacts: map[string]fakeObject{ 1071 "junit_context-a_33.xml": { 1072 Data: xmlData( 1073 junit.Suite{ 1074 Results: []junit.Result{ 1075 { 1076 Name: "visible skip non-default msg", 1077 Skipped: &junit.Skipped{Message: *pstr("non-default message")}, 1078 }, 1079 }, 1080 }), 1081 }, 1082 }, 1083 }, 1084 }, 1085 expected: []InflatedColumn{ 1086 { 1087 Column: &statepb.Column{ 1088 Build: "build-1", 1089 Started: float64(now * 1000), 1090 Hint: "build-1", 1091 }, 1092 Cells: map[string]Cell{ 1093 ".." + overallRow: { 1094 Result: statuspb.TestStatus_PASS, 1095 Metrics: map[string]float64{ 1096 "test-duration-minutes": 10 / 60.0, 1097 }, 1098 }, 1099 ".." + podInfoRow: podInfoPassCell, 1100 }, 1101 }, 1102 }, 1103 }, 1104 { 1105 name: "ignore_skip ignored when disabled", 1106 group: &configpb.TestGroup{ 1107 IgnoreSkip: true, 1108 }, 1109 builds: []fakeBuild{ 1110 { 1111 id: "build-1", 1112 podInfo: podInfoSuccess, 1113 started: &fakeObject{ 1114 Data: jsonData(metadata.Started{Timestamp: now}), 1115 }, 1116 finished: &fakeObject{ 1117 Data: jsonData(metadata.Finished{ 1118 Timestamp: pint64(now + 10), 1119 Passed: &yes, 1120 }), 1121 }, 1122 artifacts: map[string]fakeObject{ 1123 "junit_context-a_33.xml": { 1124 Data: xmlData( 1125 junit.Suite{ 1126 Results: []junit.Result{ 1127 { 1128 Name: "visible skip non-default msg", 1129 Skipped: &junit.Skipped{Message: *pstr("non-default message")}, 1130 }, 1131 }, 1132 }), 1133 }, 1134 }, 1135 }, 1136 }, 1137 expected: []InflatedColumn{ 1138 { 1139 Column: &statepb.Column{ 1140 Build: "build-1", 1141 Started: float64(now * 1000), 1142 Hint: "build-1", 1143 }, 1144 Cells: map[string]Cell{ 1145 ".." + overallRow: { 1146 Result: statuspb.TestStatus_PASS, 1147 Metrics: map[string]float64{ 1148 "test-duration-minutes": 10 / 60.0, 1149 }, 1150 }, 1151 ".." + podInfoRow: podInfoPassCell, 1152 "visible skip non-default msg": { 1153 Result: statuspb.TestStatus_PASS_WITH_SKIPS, 1154 Icon: "S", 1155 Message: "non-default message", 1156 }, 1157 }, 1158 }, 1159 }, 1160 }, 1161 } 1162 1163 poolCtx, poolCancel := context.WithCancel(context.Background()) 1164 defer poolCancel() 1165 readResultPool := resultReaderPool(poolCtx, logrus.WithField("pool", "readResult"), 10) 1166 1167 for _, tc := range cases { 1168 t.Run(tc.name, func(t *testing.T) { 1169 if tc.group == nil { 1170 tc.group = &configpb.TestGroup{} 1171 } 1172 path := newPathOrDie("gs://" + tc.group.GcsPrefix) 1173 if tc.ctx == nil { 1174 tc.ctx = context.Background() 1175 } 1176 ctx, cancel := context.WithCancel(tc.ctx) 1177 ctx.Err() 1178 defer cancel() 1179 client := fakeClient{ 1180 Lister: fake.Lister{}, 1181 Opener: fake.Opener{ 1182 Paths: map[gcs.Path]fake.Object{}, 1183 Lock: &sync.RWMutex{}, 1184 }, 1185 } 1186 1187 builds := addBuilds(&client, path, tc.builds...) 1188 1189 if tc.concurrency == 0 { 1190 tc.concurrency = 1 1191 } else { 1192 t.Skip("TODO(fejta): re-add concurrent build reading") 1193 } 1194 1195 if tc.dur == 0 { 1196 tc.dur = 5 * time.Minute 1197 } 1198 1199 var actual []InflatedColumn 1200 1201 ch := make(chan InflatedColumn) 1202 var wg sync.WaitGroup 1203 1204 wg.Add(1) 1205 go func() { 1206 defer wg.Done() 1207 time.Sleep(10 * time.Millisecond) // Give time for context to expire 1208 for col := range ch { 1209 actual = append(actual, col) 1210 } 1211 1212 }() 1213 1214 readResult := tc.readResultOverride 1215 if readResult == nil { 1216 readResult = readResultPool 1217 } 1218 1219 readColumns(ctx, client, logrus.WithField("name", tc.name), tc.group, builds, tc.stop, tc.dur, ch, readResult, tc.enableIgnoreSkip) 1220 close(ch) 1221 wg.Wait() 1222 1223 if diff := cmp.Diff(tc.expected, actual, protocmp.Transform()); diff != "" { 1224 t.Errorf("readColumns() got unexpected diff (-want +got):\n%s", diff) 1225 } 1226 }) 1227 } 1228 } 1229 1230 func TestRender(t *testing.T) { 1231 cases := []struct { 1232 name string 1233 format string 1234 parts []string 1235 job string 1236 test string 1237 metadatas []map[string]string 1238 expected string 1239 }{ 1240 { 1241 name: "basically works", 1242 }, 1243 { 1244 name: "test name works", 1245 format: "%s", 1246 parts: []string{"Tests name"}, // keep the literal 1247 test: "hello", 1248 expected: "hello", 1249 }, 1250 { 1251 name: "missing fields work", 1252 format: "%s -(%s)- %s", 1253 parts: []string{testsName, "something", jobName}, 1254 job: "this", 1255 test: "hi", 1256 expected: "hi -()- this", 1257 }, 1258 { 1259 name: "first and second metadata work", 1260 format: "first %s, second %s", 1261 parts: []string{"first", "second"}, 1262 metadatas: []map[string]string{ 1263 { 1264 "first": "hi", 1265 }, 1266 { 1267 "second": "there", 1268 "first": "ignore this", 1269 }, 1270 }, 1271 expected: "first hi, second there", 1272 }, 1273 { 1274 name: "prefer first metadata value over second", 1275 format: "test: %s, job: %s, meta: %s", 1276 parts: []string{testsName, jobName, "meta"}, 1277 test: "fancy", 1278 job: "work", 1279 metadatas: []map[string]string{ 1280 { 1281 "meta": "yes", 1282 testsName: "ignore", 1283 }, 1284 { 1285 "meta": "no", 1286 jobName: "wrong", 1287 }, 1288 }, 1289 expected: "test: fancy, job: work, meta: yes", 1290 }, 1291 } 1292 1293 for _, tc := range cases { 1294 t.Run(tc.name, func(t *testing.T) { 1295 nc := nameConfig{ 1296 format: tc.format, 1297 parts: tc.parts, 1298 } 1299 actual := nc.render(tc.job, tc.test, tc.metadatas...) 1300 if actual != tc.expected { 1301 t.Errorf("render() got %q want %q", actual, tc.expected) 1302 } 1303 }) 1304 } 1305 } 1306 1307 func TestMakeNameConfig(t *testing.T) { 1308 cases := []struct { 1309 name string 1310 group *configpb.TestGroup 1311 expected nameConfig 1312 }{ 1313 { 1314 name: "basically works", 1315 group: &configpb.TestGroup{}, 1316 expected: nameConfig{ 1317 format: "%s", 1318 parts: []string{testsName}, 1319 }, 1320 }, 1321 { 1322 name: "explicit config works", 1323 group: &configpb.TestGroup{ 1324 TestNameConfig: &configpb.TestNameConfig{ 1325 NameFormat: "%s %s", 1326 NameElements: []*configpb.TestNameConfig_NameElement{ 1327 { 1328 TargetConfig: "hello", 1329 }, 1330 { 1331 TargetConfig: "world", 1332 }, 1333 }, 1334 }, 1335 }, 1336 expected: nameConfig{ 1337 format: "%s %s", 1338 parts: []string{"hello", "world"}, 1339 }, 1340 }, 1341 { 1342 name: "test properties work", 1343 group: &configpb.TestGroup{ 1344 TestNameConfig: &configpb.TestNameConfig{ 1345 NameFormat: "%s %s", 1346 NameElements: []*configpb.TestNameConfig_NameElement{ 1347 { 1348 TargetConfig: "hello", 1349 }, 1350 { 1351 TestProperty: "world", 1352 }, 1353 }, 1354 }, 1355 }, 1356 expected: nameConfig{ 1357 format: "%s %s", 1358 parts: []string{"hello", "world"}, 1359 }, 1360 }, 1361 { 1362 name: "target config precedes test property", 1363 group: &configpb.TestGroup{ 1364 TestNameConfig: &configpb.TestNameConfig{ 1365 NameFormat: "%s works", 1366 NameElements: []*configpb.TestNameConfig_NameElement{ 1367 { 1368 TargetConfig: "good-target", 1369 TestProperty: "nope-property", 1370 }, 1371 }, 1372 }, 1373 }, 1374 expected: nameConfig{ 1375 format: "%s works", 1376 parts: []string{"good-target"}, 1377 }, 1378 }, 1379 { 1380 name: "auto-inject job name into default config", 1381 group: &configpb.TestGroup{ 1382 GcsPrefix: "this,that", 1383 }, 1384 expected: nameConfig{ 1385 format: "%s.%s", 1386 parts: []string{jobName, testsName}, 1387 multiJob: true, 1388 }, 1389 }, 1390 { 1391 name: "auto-inject job name into explicit config", 1392 group: &configpb.TestGroup{ 1393 GcsPrefix: "this,that", 1394 TestNameConfig: &configpb.TestNameConfig{ 1395 NameFormat: "%s %s", 1396 NameElements: []*configpb.TestNameConfig_NameElement{ 1397 { 1398 TargetConfig: "hello", 1399 }, 1400 { 1401 TargetConfig: "world", 1402 }, 1403 }, 1404 }, 1405 }, 1406 expected: nameConfig{ 1407 format: "%s.%s %s", 1408 parts: []string{jobName, "hello", "world"}, 1409 multiJob: true, 1410 }, 1411 }, 1412 { 1413 name: "allow explicit job name config", 1414 group: &configpb.TestGroup{ 1415 GcsPrefix: "this,that", 1416 TestNameConfig: &configpb.TestNameConfig{ 1417 NameFormat: "%s %s (%s)", 1418 NameElements: []*configpb.TestNameConfig_NameElement{ 1419 { 1420 TargetConfig: "hello", 1421 }, 1422 { 1423 TargetConfig: "world", 1424 }, 1425 { 1426 TargetConfig: jobName, 1427 }, 1428 }, 1429 }, 1430 }, 1431 expected: nameConfig{ 1432 format: "%s %s (%s)", 1433 parts: []string{"hello", "world", jobName}, 1434 multiJob: true, 1435 }, 1436 }, 1437 } 1438 1439 for _, tc := range cases { 1440 t.Run(tc.name, func(t *testing.T) { 1441 actual := makeNameConfig(tc.group) 1442 if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(nameConfig{})); diff != "" { 1443 t.Errorf("makeNameConfig() got unexpected diff (-got +want):\n%s", diff) 1444 } 1445 }) 1446 } 1447 } 1448 1449 func TestReadResult(t *testing.T) { 1450 path := newPathOrDie("gs://bucket/path/to/some/build/") 1451 yes := true 1452 cases := []struct { 1453 name string 1454 ctx context.Context 1455 data map[string]fakeObject 1456 stop time.Time 1457 1458 expected *gcsResult 1459 }{ 1460 { 1461 name: "basically works", 1462 expected: &gcsResult{ 1463 started: gcs.Started{ 1464 Pending: true, 1465 }, 1466 finished: gcs.Finished{ 1467 Running: true, 1468 }, 1469 job: "some", 1470 build: "build", 1471 }, 1472 }, 1473 { 1474 name: "cancelled context returns error", 1475 ctx: func() context.Context { 1476 ctx, cancel := context.WithCancel(context.Background()) 1477 cancel() 1478 return ctx 1479 }(), 1480 }, 1481 { 1482 name: "all info present", 1483 data: map[string]fakeObject{ 1484 "podinfo.json": {Data: `{"pod":{"metadata":{"name":"woot"}}}`}, 1485 "started.json": {Data: `{"node": "fun"}`}, 1486 "finished.json": {Data: `{"passed": true}`}, 1487 "junit_super_88.xml": {Data: `<testsuite><testcase name="foo"/></testsuite>`}, 1488 }, 1489 expected: &gcsResult{ 1490 podInfo: func() gcs.PodInfo { 1491 out := gcs.PodInfo{Pod: &core.Pod{}} 1492 out.Pod.Name = "woot" 1493 return out 1494 }(), 1495 started: gcs.Started{ 1496 Started: metadata.Started{Node: "fun"}, 1497 }, 1498 finished: gcs.Finished{ 1499 Finished: metadata.Finished{Passed: &yes}, 1500 }, 1501 suites: []gcs.SuitesMeta{ 1502 { 1503 Suites: &junit.Suites{ 1504 Suites: []junit.Suite{ 1505 { 1506 XMLName: xml.Name{Local: "testsuite"}, 1507 Results: []junit.Result{ 1508 {Name: "foo"}, 1509 }, 1510 }, 1511 }, 1512 }, 1513 Metadata: map[string]string{ 1514 "Context": "super", 1515 "Thread": "88", 1516 "Timestamp": "", 1517 }, 1518 Path: "gs://bucket/path/to/some/build/junit_super_88.xml", 1519 }, 1520 }, 1521 }, 1522 }, 1523 { 1524 name: "empty files report missing", 1525 data: map[string]fakeObject{ 1526 "finished.json": {Data: ""}, 1527 "started.json": {Data: ""}, 1528 "podinfo.json": {Data: ""}, 1529 }, 1530 expected: &gcsResult{ 1531 malformed: []string{ 1532 "finished.json", 1533 "podinfo.json", 1534 "started.json", 1535 }, 1536 }, 1537 }, 1538 { 1539 name: "missing started.json reports pending", 1540 data: map[string]fakeObject{ 1541 "finished.json": {Data: `{"passed": true}`}, 1542 "junit_super_88.xml": {Data: `<testsuite><testcase name="foo"/></testsuite>`}, 1543 }, 1544 expected: &gcsResult{ 1545 started: gcs.Started{ 1546 Pending: true, 1547 }, 1548 finished: gcs.Finished{ 1549 Finished: metadata.Finished{Passed: &yes}, 1550 }, 1551 suites: []gcs.SuitesMeta{ 1552 { 1553 Suites: &junit.Suites{ 1554 Suites: []junit.Suite{ 1555 { 1556 XMLName: xml.Name{Local: "testsuite"}, 1557 Results: []junit.Result{ 1558 {Name: "foo"}, 1559 }, 1560 }, 1561 }, 1562 }, 1563 Metadata: map[string]string{ 1564 "Context": "super", 1565 "Thread": "88", 1566 "Timestamp": "", 1567 }, 1568 Path: "gs://bucket/path/to/some/build/junit_super_88.xml", 1569 }, 1570 }, 1571 }, 1572 }, 1573 { 1574 name: "no finished reports running", 1575 data: map[string]fakeObject{ 1576 "started.json": {Data: `{"node": "fun"}`}, 1577 "junit_super_88.xml": {Data: `<testsuite><testcase name="foo"/></testsuite>`}, 1578 }, 1579 expected: &gcsResult{ 1580 started: gcs.Started{ 1581 Started: metadata.Started{Node: "fun"}, 1582 }, 1583 finished: gcs.Finished{ 1584 Running: true, 1585 }, 1586 suites: []gcs.SuitesMeta{ 1587 { 1588 Suites: &junit.Suites{ 1589 Suites: []junit.Suite{ 1590 { 1591 XMLName: xml.Name{Local: "testsuite"}, 1592 Results: []junit.Result{ 1593 {Name: "foo"}, 1594 }, 1595 }, 1596 }, 1597 }, 1598 Metadata: map[string]string{ 1599 "Context": "super", 1600 "Thread": "88", 1601 "Timestamp": "", 1602 }, 1603 Path: "gs://bucket/path/to/some/build/junit_super_88.xml", 1604 }, 1605 }, 1606 }, 1607 }, 1608 { 1609 name: "no artifacts report no suites", 1610 data: map[string]fakeObject{ 1611 "started.json": {Data: `{"node": "fun"}`}, 1612 "finished.json": {Data: `{"passed": true}`}, 1613 }, 1614 expected: &gcsResult{ 1615 started: gcs.Started{ 1616 Started: metadata.Started{Node: "fun"}, 1617 }, 1618 finished: gcs.Finished{ 1619 Finished: metadata.Finished{Passed: &yes}, 1620 }, 1621 }, 1622 }, 1623 { 1624 name: "started error returns error", 1625 data: map[string]fakeObject{ 1626 "started.json": { 1627 Data: "{}", 1628 CloseErr: errors.New("injected closer error"), 1629 }, 1630 "finished.json": {Data: `{"passed": true}`}, 1631 "junit_super_88.xml": {Data: `<testsuite><testcase name="foo"/></testsuite>`}, 1632 }, 1633 }, 1634 { 1635 name: "finished error returns error", 1636 data: map[string]fakeObject{ 1637 "started.json": {Data: `{"node": "fun"}`}, 1638 "finished.json": {ReadErr: errors.New("injected read error")}, 1639 "junit_super_88.xml": {Data: `<testsuite><testcase name="foo"/></testsuite>`}, 1640 }, 1641 }, 1642 { 1643 name: "artifact error added to malformed list", 1644 data: map[string]fakeObject{ 1645 "started.json": {Data: `{"node": "fun"}`}, 1646 "finished.json": {Data: `{"passed": true}`}, 1647 "junit_super_88.xml": {OpenErr: errors.New("injected open error")}, 1648 }, 1649 expected: &gcsResult{ 1650 started: gcs.Started{ 1651 Started: metadata.Started{Node: "fun"}, 1652 }, 1653 finished: gcs.Finished{ 1654 Finished: metadata.Finished{Passed: &yes}, 1655 }, 1656 malformed: []string{"junit_super_88.xml: open: injected open error"}, 1657 }, 1658 }, 1659 } 1660 1661 for _, tc := range cases { 1662 t.Run(tc.name, func(t *testing.T) { 1663 if tc.ctx == nil { 1664 tc.ctx = context.Background() 1665 } 1666 if tc.expected != nil { 1667 tc.expected.job = "some" 1668 tc.expected.build = "build" 1669 } 1670 ctx, cancel := context.WithCancel(tc.ctx) 1671 defer cancel() 1672 client := fakeClient{ 1673 Lister: fake.Lister{}, 1674 Opener: fake.Opener{ 1675 Paths: map[gcs.Path]fake.Object{}, 1676 Lock: &sync.RWMutex{}, 1677 }, 1678 } 1679 1680 fi := fakeIterator{} 1681 for name, fo := range tc.data { 1682 p, err := path.ResolveReference(&url.URL{Path: name}) 1683 if err != nil { 1684 t.Fatalf("path.ResolveReference(%q): %v", name, err) 1685 } 1686 fi.Objects = append(fi.Objects, storage.ObjectAttrs{ 1687 Name: p.Object(), 1688 }) 1689 client.Opener.Paths[*p] = fo 1690 } 1691 client.Lister[path] = fi 1692 1693 build := gcs.Build{ 1694 Path: path, 1695 } 1696 actual, err := readResult(ctx, client, build, tc.stop) 1697 switch { 1698 case err != nil: 1699 if tc.expected != nil { 1700 t.Errorf("readResult(): unexpected error: %v", err) 1701 } 1702 case tc.expected == nil: 1703 t.Error("readResult(): failed to receive expected error") 1704 default: 1705 if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(gcsResult{})); diff != "" { 1706 t.Errorf("readResult() got unexpected diff (-have, +want):\n%s", diff) 1707 } 1708 } 1709 }) 1710 } 1711 } 1712 1713 func newPathOrDie(s string) gcs.Path { 1714 p, err := gcs.NewPath(s) 1715 if err != nil { 1716 panic(err) 1717 } 1718 return *p 1719 } 1720 1721 func TestReadSuites(t *testing.T) { 1722 path := newPathOrDie("gs://bucket/path/to/build/") 1723 cases := []struct { 1724 name string 1725 data map[string]fakeObject 1726 listIdxErr int 1727 expected []gcs.SuitesMeta 1728 err bool 1729 ctx context.Context 1730 }{ 1731 { 1732 name: "basically works", 1733 }, 1734 { 1735 name: "multiple suites from multiple artifacts work", 1736 data: map[string]fakeObject{ 1737 "ignore-this": {Data: "<invalid></xml>"}, 1738 "junit.xml": {Data: `<testsuite><testcase name="hi"/></testsuite>`}, 1739 "ignore-that": {Data: "<invalid></xml>"}, 1740 "nested/junit_context_20201122-1234_88.xml": { 1741 Data: ` 1742 <testsuites> 1743 <testsuite name="fun"> 1744 <testsuite name="knee"> 1745 <testcase name="bone" time="6" /> 1746 </testsuite> 1747 <testcase name="word" time="7" /> 1748 </testsuite> 1749 </testsuites> 1750 `, 1751 }, 1752 }, 1753 expected: []gcs.SuitesMeta{ 1754 { 1755 Suites: &junit.Suites{ 1756 Suites: []junit.Suite{ 1757 { 1758 XMLName: xml.Name{Local: "testsuite"}, 1759 Results: []junit.Result{ 1760 {Name: "hi"}, 1761 }, 1762 }, 1763 }, 1764 }, 1765 Metadata: map[string]string{ 1766 "Context": "", 1767 "Thread": "", 1768 "Timestamp": "", 1769 }, 1770 Path: "gs://bucket/path/to/build/junit.xml", 1771 }, 1772 { 1773 Suites: &junit.Suites{ 1774 XMLName: xml.Name{Local: "testsuites"}, 1775 Suites: []junit.Suite{ 1776 { 1777 XMLName: xml.Name{Local: "testsuite"}, 1778 Name: "fun", 1779 Suites: []junit.Suite{ 1780 { 1781 XMLName: xml.Name{Local: "testsuite"}, 1782 Name: "knee", 1783 Results: []junit.Result{ 1784 { 1785 Name: "bone", 1786 Time: 6, 1787 }, 1788 }, 1789 }, 1790 }, 1791 Results: []junit.Result{ 1792 { 1793 Name: "word", 1794 Time: 7, 1795 }, 1796 }, 1797 }, 1798 }, 1799 }, 1800 Metadata: map[string]string{ 1801 "Context": "context", 1802 "Thread": "88", 1803 "Timestamp": "20201122-1234", 1804 }, 1805 Path: "gs://bucket/path/to/build/nested/junit_context_20201122-1234_88.xml", 1806 }, 1807 }, 1808 }, 1809 { 1810 name: "list error returns error", 1811 data: map[string]fakeObject{ 1812 "ignore-this": {Data: "<invalid></xml>"}, 1813 "junit.xml": {Data: `<testsuite><testcase name="hi"/></testsuite>`}, 1814 "ignore-that": {Data: "<invalid></xml>"}, 1815 }, 1816 listIdxErr: 1, 1817 err: true, 1818 }, 1819 { 1820 name: "cancelled context returns err", 1821 data: map[string]fakeObject{ 1822 "junit.xml": {Data: `<testsuite><testcase name="hi"/></testsuite>`}, 1823 }, 1824 ctx: func() context.Context { 1825 ctx, cancel := context.WithCancel(context.Background()) 1826 cancel() 1827 return ctx 1828 }(), 1829 err: true, 1830 }, 1831 { 1832 name: "suites error contains error", 1833 data: map[string]fakeObject{ 1834 "junit.xml": {Data: "<invalid></xml>"}, 1835 }, 1836 expected: []gcs.SuitesMeta{ 1837 { 1838 Metadata: map[string]string{ 1839 "Context": "", 1840 "Thread": "", 1841 "Timestamp": "", 1842 }, 1843 Path: "gs://bucket/path/to/build/junit.xml", 1844 Err: errors.New("foo"), 1845 }, 1846 }, 1847 }, 1848 } 1849 1850 for _, tc := range cases { 1851 t.Run(tc.name, func(t *testing.T) { 1852 if tc.ctx == nil { 1853 tc.ctx = context.Background() 1854 } 1855 ctx, cancel := context.WithCancel(tc.ctx) 1856 defer cancel() 1857 client := fakeClient{ 1858 Lister: fake.Lister{}, 1859 Opener: fake.Opener{ 1860 Paths: map[gcs.Path]fake.Object{}, 1861 }, 1862 } 1863 1864 fi := fakeIterator{ 1865 Err: tc.listIdxErr, 1866 } 1867 for name, fo := range tc.data { 1868 p, err := path.ResolveReference(&url.URL{Path: name}) 1869 if err != nil { 1870 t.Fatalf("path.ResolveReference(%q): %v", name, err) 1871 } 1872 fi.Objects = append(fi.Objects, storage.ObjectAttrs{ 1873 Name: p.Object(), 1874 }) 1875 client.Opener.Paths[*p] = fo 1876 } 1877 client.Lister[path] = fi 1878 1879 build := gcs.Build{ 1880 Path: path, 1881 } 1882 actual, err := readSuites(ctx, &client, build) 1883 sort.SliceStable(actual, func(i, j int) bool { 1884 return actual[i].Path < actual[j].Path 1885 }) 1886 sort.SliceStable(tc.expected, func(i, j int) bool { 1887 return tc.expected[i].Path < tc.expected[j].Path 1888 }) 1889 switch { 1890 case err != nil: 1891 if !tc.err { 1892 t.Errorf("readSuites(): unexpected error: %v", err) 1893 } 1894 case tc.err: 1895 t.Error("readSuites(): failed to receive an error") 1896 default: 1897 cmpErrs := func(x, y error) bool { 1898 return (x == nil) == (y == nil) 1899 } 1900 if diff := cmp.Diff(tc.expected, actual, cmp.Comparer(cmpErrs)); diff != "" { 1901 t.Errorf("readSuites() got unexpected diff (-want +got):\n%s", diff) 1902 } 1903 } 1904 }) 1905 } 1906 } 1907 1908 func addBuilds(fc *fake.Client, path gcs.Path, s ...fakeBuild) []gcs.Build { 1909 if fc.Opener.Lock != nil { 1910 fc.Opener.Lock.Lock() 1911 defer fc.Opener.Lock.Unlock() 1912 } 1913 var builds []gcs.Build 1914 for _, build := range s { 1915 buildPath := resolveOrDie(&path, build.id+"/") 1916 builds = append(builds, gcs.Build{Path: *buildPath}) 1917 fi := fake.Iterator{} 1918 1919 if build.podInfo != nil { 1920 p := resolveOrDie(buildPath, "podinfo.json") 1921 fi.Objects = append(fi.Objects, storage.ObjectAttrs{ 1922 Name: p.Object(), 1923 }) 1924 fc.Opener.Paths[*p] = *build.podInfo 1925 } 1926 if build.started != nil { 1927 p := resolveOrDie(buildPath, "started.json") 1928 fi.Objects = append(fi.Objects, storage.ObjectAttrs{ 1929 Name: p.Object(), 1930 }) 1931 fc.Opener.Paths[*p] = *build.started 1932 } 1933 if build.finished != nil { 1934 p := resolveOrDie(buildPath, "finished.json") 1935 fi.Objects = append(fi.Objects, storage.ObjectAttrs{ 1936 Name: p.Object(), 1937 }) 1938 fc.Opener.Paths[*p] = *build.finished 1939 } 1940 if len(build.passed)+len(build.failed) > 0 { 1941 p := resolveOrDie(buildPath, "junit_automatic.xml") 1942 fc.Opener.Paths[*p] = fake.Object{Data: makeJunit(build.passed, build.failed)} 1943 fi.Objects = append(fi.Objects, storage.ObjectAttrs{ 1944 Name: p.Object(), 1945 }) 1946 } 1947 for n, fo := range build.artifacts { 1948 p := resolveOrDie(buildPath, n) 1949 fi.Objects = append(fi.Objects, storage.ObjectAttrs{ 1950 Name: p.Object(), 1951 }) 1952 fc.Opener.Paths[*p] = fo 1953 } 1954 fc.Lister[*buildPath] = fi 1955 } 1956 return builds 1957 1958 } 1959 1960 type fakeBuild struct { 1961 id string 1962 started *fakeObject 1963 finished *fakeObject 1964 podInfo *fakeObject 1965 artifacts map[string]fakeObject 1966 rawJunit *fakeObject 1967 passed []string 1968 failed []string 1969 }