github.com/GoogleCloudPlatform/testgrid@v0.0.174/util/gcs/read_test.go (about) 1 /* 2 Copyright 2018 The Kubernetes 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 gcs 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/xml" 23 "errors" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "net/url" 28 "reflect" 29 "sort" 30 "sync" 31 "testing" 32 "time" 33 34 "cloud.google.com/go/storage" 35 "github.com/GoogleCloudPlatform/testgrid/metadata" 36 "github.com/GoogleCloudPlatform/testgrid/metadata/junit" 37 "github.com/google/go-cmp/cmp" 38 "google.golang.org/api/iterator" 39 core "k8s.io/api/core/v1" 40 ) 41 42 func podCondition(who core.PodConditionType, what core.ConditionStatus, why string) core.PodCondition { 43 return core.PodCondition{ 44 Type: who, 45 Status: what, 46 Message: why, 47 } 48 } 49 50 func containerStatus(name string, ready, completed bool, exitCode int32) core.ContainerStatus { 51 status := core.ContainerStatus{ 52 Name: name, 53 Ready: ready, 54 } 55 56 if completed { 57 status.State.Terminated = &core.ContainerStateTerminated{ExitCode: exitCode} 58 } 59 return status 60 } 61 62 func containerWaiting(name string, msg string) core.ContainerStatus { 63 status := containerStatus(name, false, false, 0) 64 status.State.Waiting = &core.ContainerStateWaiting{Message: msg} 65 return status 66 } 67 68 func TestPodInfoSummarize(t *testing.T) { 69 cases := []struct { 70 name string 71 info PodInfo 72 pass bool 73 msg string 74 }{ 75 { 76 name: "basically works", 77 msg: MissingPodInfo, 78 }, 79 { 80 name: "passing pod works", 81 info: PodInfo{ 82 Pod: &core.Pod{ 83 Status: core.PodStatus{Phase: core.PodSucceeded}, 84 }, 85 }, 86 pass: true, 87 }, 88 { 89 // https://storage.googleapis.com/kubernetes-jenkins/logs/ci-kubernetes-e2e-gce-ubuntu1-k8sstable1-serial/1364737725537718272/podinfo.json 90 // Initialized, not ready/containersready with only test container 91 // no initcontainers 92 name: "non-pod-utils failure works", 93 info: PodInfo{ 94 Pod: &core.Pod{ 95 Status: core.PodStatus{ 96 Phase: core.PodFailed, 97 Conditions: []core.PodCondition{ 98 podCondition(core.PodScheduled, core.ConditionTrue, ""), 99 podCondition(core.PodInitialized, core.ConditionTrue, ""), 100 podCondition(core.PodReady, core.ConditionFalse, ""), 101 }, 102 ContainerStatuses: []core.ContainerStatus{ 103 containerStatus("test", false, true, 1), 104 }, 105 InitContainerStatuses: []core.ContainerStatus{ 106 {}, 107 }, 108 }, 109 }, 110 }, 111 pass: true, 112 msg: NoPodUtils, 113 }, 114 { 115 // https://storage.googleapis.com/kubernetes-jenkins/pr-logs/pull/test-infra/21014/pull-test-infra-bazel/1364742867209162752/podinfo.json 116 // init'd, not ready/containersready with test sidecar 117 name: "normal failure works", 118 info: PodInfo{ 119 Pod: &core.Pod{ 120 Status: core.PodStatus{ 121 Phase: core.PodFailed, 122 Conditions: []core.PodCondition{ 123 podCondition(core.PodScheduled, core.ConditionTrue, ""), 124 podCondition(core.PodInitialized, core.ConditionTrue, ""), 125 podCondition(core.PodReady, core.ConditionFalse, ""), 126 }, 127 ContainerStatuses: []core.ContainerStatus{ 128 containerStatus("sidecar", false, true, 0), 129 containerStatus("test", false, true, 1), 130 }, 131 InitContainerStatuses: []core.ContainerStatus{ 132 containerStatus("init-upload", false, true, 0), 133 containerStatus("place-entrypoint", false, true, 0), 134 containerStatus("clonerefs", false, true, 0), 135 }, 136 }, 137 }, 138 }, 139 pass: true, 140 }, 141 { 142 // https://storage.googleapis.com/kubernetes-jenkins/logs/ci-benchmark-scheduler-master/1364668262104698880/podinfo.json 143 // aka pending status, podscheduled false with message. 144 name: "detect scheduling failure", 145 info: PodInfo{ 146 Pod: &core.Pod{ 147 Status: core.PodStatus{ 148 Phase: core.PodPending, 149 Conditions: []core.PodCondition{ 150 podCondition(core.PodScheduled, core.ConditionFalse, "0/159 nodes available"), 151 }, 152 }, 153 }, 154 }, 155 msg: "pod did not schedule: 0/159 nodes available", 156 }, 157 { 158 // TODO(fejta): find public example 159 // Initialized false, "message": "containers with incomplete status: [clonerefs initupload place-entrypoint]" 160 name: "detect initialization issue", 161 info: PodInfo{ 162 Pod: &core.Pod{ 163 Status: core.PodStatus{ 164 Phase: core.PodFailed, 165 Conditions: []core.PodCondition{ 166 podCondition(core.PodScheduled, core.ConditionTrue, ""), 167 podCondition(core.PodInitialized, core.ConditionFalse, "beep boop bop"), 168 }, 169 }, 170 }, 171 }, 172 msg: "pod could not initialize: beep boop bop", 173 }, 174 { 175 // https://storage.googleapis.com/kubernetes-jenkins/logs/tf-minigo-periodic/1364608678237310976/podinfo.json 176 // failed to pull image 177 name: "detect image pull failure", 178 info: PodInfo{ 179 Pod: &core.Pod{ 180 Status: core.PodStatus{ 181 Phase: core.PodFailed, 182 Conditions: []core.PodCondition{ 183 podCondition(core.PodScheduled, core.ConditionTrue, ""), 184 podCondition(core.PodInitialized, core.ConditionTrue, ""), 185 podCondition(core.PodReady, core.ConditionFalse, ""), 186 }, 187 ContainerStatuses: []core.ContainerStatus{ 188 containerStatus("sidecar", false, true, 0), 189 containerWaiting("test", "failed to resolve image \"gcr.io/minigo-testing/minigo-prow-harness-v2:latest"), 190 }, 191 InitContainerStatuses: []core.ContainerStatus{ 192 containerStatus("init-upload", false, true, 0), 193 containerStatus("place-entrypoint", false, true, 0), 194 containerStatus("clonerefs", false, true, 0), 195 }, 196 }, 197 }, 198 }, 199 msg: "test still waiting: failed to resolve image \"gcr.io/minigo-testing/minigo-prow-harness-v2:latest", 200 }, 201 } 202 203 for _, tc := range cases { 204 t.Run(tc.name, func(t *testing.T) { 205 pass, msg := tc.info.Summarize() 206 if pass != tc.pass { 207 t.Errorf("Summarize() got %t, want %t", pass, tc.pass) 208 } 209 if msg != tc.msg { 210 t.Errorf("Summarize() got %q, want %q", msg, tc.msg) 211 } 212 }) 213 } 214 } 215 216 func subdir(prefix string) storage.ObjectAttrs { 217 return storage.ObjectAttrs{Prefix: prefix} 218 } 219 220 func link(path Path, name, other string) storage.ObjectAttrs { 221 return storage.ObjectAttrs{ 222 Metadata: map[string]string{"x-goog-meta-link": other}, 223 Name: resolveOrDie(path, name).Object(), 224 } 225 } 226 227 func TestBuildJob(t *testing.T) { 228 cases := []struct { 229 path string 230 build string 231 job string 232 }{ 233 { 234 path: "gs://bucket/path/job/hello", 235 build: "hello", 236 job: "job", 237 }, 238 { 239 path: "gs://bucket/path/job/hello/", 240 build: "hello", 241 job: "job", 242 }, 243 } 244 245 for _, tc := range cases { 246 t.Run(tc.path, func(t *testing.T) { 247 p, err := NewPath(tc.path) 248 if err != nil { 249 t.Fatalf("NewPath(%q) got unexpected error: %v", tc.path, err) 250 } 251 b := Build{Path: *p} 252 job, build := b.Job(), b.Build() 253 if job != tc.job { 254 t.Errorf("Job got %q want %q", job, tc.job) 255 } 256 if build != tc.build { 257 t.Errorf("Build got %q want %q", build, tc.build) 258 } 259 }) 260 } 261 } 262 263 func TestOffsetHack(t *testing.T) { 264 cases := []struct { 265 name string 266 input string 267 output string 268 base string 269 }{ 270 { 271 name: "basically works", 272 }, 273 { 274 name: "normal prow builds work with trailing slash", 275 input: "logs/ci-benchmark-scheduler/1364607429106470912/", 276 output: "logs/ci-benchmark-scheduler/1364607429106470912", 277 base: "1364607429106470912", 278 }, 279 { 280 name: "normal prow builds work", 281 input: "logs/ci-benchmark-scheduler/1364607429106470912", 282 output: "logs/ci-benchmark-scheduler/1364607429106470912", 283 base: "1364607429106470912", 284 }, 285 { 286 name: "hack tot style with trailing slash", 287 input: "logs/ci-benchmark-scheduler/10/", 288 output: "logs/ci-benchmark-scheduler/0", 289 base: "10", 290 }, 291 { 292 name: "hack tot style", 293 input: "logs/ci-benchmark-scheduler/10", 294 output: "logs/ci-benchmark-scheduler/0", 295 base: "10", 296 }, 297 { 298 name: "non-numerical builds work", 299 input: "logs/ci-benchmark-scheduler/fancy4u", 300 output: "logs/ci-benchmark-scheduler/fancy4u", 301 base: "fancy4u", 302 }, 303 } 304 305 for _, tc := range cases { 306 t.Run(tc.name, func(t *testing.T) { 307 output := tc.input 308 base := hackOffset(&output) 309 if output != tc.output { 310 t.Errorf("hackOffset(%q) became %q, want %q", tc.input, output, tc.output) 311 } 312 if base != tc.base { 313 t.Errorf("hackOffset(%q) returned %q, want %q", tc.input, base, tc.base) 314 } 315 }) 316 } 317 } 318 319 func TestListBuilds(t *testing.T) { 320 path := newPathOrDie("gs://bucket/path/to/build/") 321 cases := []struct { 322 name string 323 ctx context.Context 324 iterator fakeIterator 325 offset *Path 326 327 expected []Build 328 err bool 329 }{ 330 { 331 name: "basically works", 332 }, 333 { 334 name: "multiple paths", 335 iterator: fakeIterator{ 336 objects: []storage.ObjectAttrs{ 337 subdir(resolveOrDie(path, "hello").Object()), 338 subdir(resolveOrDie(path, "world").Object()), 339 }, 340 }, 341 expected: []Build{ 342 { 343 Path: resolveOrDie(path, "world"), 344 baseName: "world", 345 }, 346 { 347 Path: newPathOrDie("gs://bucket/path/to/build/hello"), 348 baseName: "hello", 349 }, 350 }, 351 }, 352 { 353 name: "presubmit symlinks work correctly", 354 iterator: fakeIterator{ 355 objects: []storage.ObjectAttrs{ 356 link(path, "first", "gs://another-bucket/path/inside"), 357 link(path, "second", "gs://second-bucket/somewhere"), 358 }, 359 }, 360 expected: []Build{ 361 { 362 Path: newPathOrDie("gs://second-bucket/somewhere/"), 363 baseName: "second", 364 }, 365 { 366 Path: newPathOrDie("gs://another-bucket/path/inside/"), 367 baseName: "first", 368 }, 369 }, 370 }, 371 { 372 name: "cancelled context returns error", 373 iterator: fakeIterator{ 374 objects: []storage.ObjectAttrs{ 375 subdir(resolveOrDie(path, "hello").Object()), 376 subdir(resolveOrDie(path, "world").Object()), 377 }, 378 }, 379 ctx: func() context.Context { 380 ctx, cancel := context.WithCancel(context.Background()) 381 cancel() 382 return ctx 383 }(), 384 err: true, 385 }, 386 { 387 name: "iteration error returns error", 388 iterator: fakeIterator{ 389 objects: []storage.ObjectAttrs{ 390 subdir(resolveOrDie(path, "hello").Object()), 391 subdir(resolveOrDie(path, "world").Object()), 392 subdir(resolveOrDie(path, "more").Object()), 393 }, 394 err: 1, 395 }, 396 err: true, 397 }, 398 { 399 name: "listing latest builds works correctly", 400 iterator: fakeIterator{ 401 objects: []storage.ObjectAttrs{ 402 subdir(resolveOrDie(path, "hello").Object()), 403 subdir(resolveOrDie(path, "more").Object()), 404 subdir(resolveOrDie(path, "world").Object()), 405 }, 406 }, 407 offset: pResolveOrDie(path, "more"), 408 expected: []Build{ 409 { 410 Path: resolveOrDie(path, "world"), 411 baseName: "world", 412 }, 413 }, 414 }, 415 { 416 name: "drop results naturally before, include results naturally after", 417 iterator: fakeIterator{ 418 objects: []storage.ObjectAttrs{ 419 subdir(resolveOrDie(path, "100").Object()), 420 subdir(resolveOrDie(path, "1000").Object()), 421 subdir(resolveOrDie(path, "1100").Object()), 422 subdir(resolveOrDie(path, "1200").Object()), 423 subdir(resolveOrDie(path, "200").Object()), 424 subdir(resolveOrDie(path, "300").Object()), 425 subdir(resolveOrDie(path, "400").Object()), 426 subdir(resolveOrDie(path, "500").Object()), 427 subdir(resolveOrDie(path, "600").Object()), 428 subdir(resolveOrDie(path, "700").Object()), 429 subdir(resolveOrDie(path, "800").Object()), 430 subdir(resolveOrDie(path, "900").Object()), 431 }, 432 }, 433 offset: pResolveOrDie(path, "500"), 434 expected: []Build{ 435 { 436 Path: resolveOrDie(path, "1200"), 437 baseName: "1200", 438 }, 439 { 440 Path: resolveOrDie(path, "1100"), 441 baseName: "1100", 442 }, 443 { 444 Path: resolveOrDie(path, "1000"), 445 baseName: "1000", 446 }, 447 { 448 Path: resolveOrDie(path, "900"), 449 baseName: "900", 450 }, 451 { 452 Path: resolveOrDie(path, "800"), 453 baseName: "800", 454 }, 455 { 456 Path: resolveOrDie(path, "700"), 457 baseName: "700", 458 }, 459 { 460 Path: resolveOrDie(path, "600"), 461 baseName: "600", 462 }, 463 }, 464 }, 465 { 466 name: "listing latest builds handles numbers correctly", 467 iterator: fakeIterator{ 468 objects: []storage.ObjectAttrs{ 469 subdir(resolveOrDie(path, "hello100").Object()), 470 subdir(resolveOrDie(path, "hello101").Object()), 471 subdir(resolveOrDie(path, "hello2000").Object()), 472 subdir(resolveOrDie(path, "hello30").Object()), 473 subdir(resolveOrDie(path, "hello31").Object()), 474 subdir(resolveOrDie(path, "hello300").Object()), 475 }, 476 }, 477 offset: pResolveOrDie(path, "hello100"), 478 expected: []Build{ 479 { 480 Path: resolveOrDie(path, "hello2000"), 481 baseName: "hello2000", 482 }, 483 { 484 Path: resolveOrDie(path, "hello300"), 485 baseName: "hello300", 486 }, 487 { 488 Path: resolveOrDie(path, "hello101"), 489 baseName: "hello101", 490 }, 491 }, 492 }, 493 { 494 name: "listing latest presubmit symlinks handles numbers", 495 iterator: fakeIterator{ 496 objects: []storage.ObjectAttrs{ 497 link(path, "100", "gs://another-bucket/path/inside/100"), 498 link(path, "101", "gs://second-bucket/somewhere/101"), 499 link(path, "202", "gs://third-bucket/else/202"), 500 link(path, "2004", "gs://third-bucket/else/2004"), 501 link(path, "30", "gs://third-bucket/else/30"), 502 link(path, "303", "gs://third-bucket/else/303"), 503 }, 504 }, 505 offset: pResolveOrDie(path, "100"), 506 expected: []Build{ 507 { 508 Path: newPathOrDie("gs://third-bucket/else/2004/"), 509 baseName: "2004", 510 }, 511 { 512 Path: newPathOrDie("gs://third-bucket/else/303/"), 513 baseName: "303", 514 }, 515 { 516 Path: newPathOrDie("gs://third-bucket/else/202/"), 517 baseName: "202", 518 }, 519 { 520 Path: newPathOrDie("gs://second-bucket/somewhere/101/"), 521 baseName: "101", 522 }, 523 }, 524 }, 525 { 526 name: "listing latest presubmit symlinks work correctly", 527 iterator: fakeIterator{ 528 objects: []storage.ObjectAttrs{ 529 link(path, "first", "gs://another-bucket/path/inside"), 530 link(path, "second", "gs://second-bucket/somewhere"), 531 link(path, "third", "gs://third-bucket/else"), 532 }, 533 }, 534 offset: pResolveOrDie(path, "second"), 535 expected: []Build{ 536 { 537 Path: newPathOrDie("gs://third-bucket/else/"), 538 baseName: "third", 539 }, 540 }, 541 }, 542 } 543 544 for _, tc := range cases { 545 t.Run(tc.name, func(t *testing.T) { 546 fl := fakeLister{path: tc.iterator} 547 ctx := tc.ctx 548 if ctx == nil { 549 ctx = context.Background() 550 } 551 actual, err := ListBuilds(ctx, fl, path, tc.offset) 552 switch { 553 case err != nil: 554 if !tc.err { 555 t.Errorf("ListBuilds(): unexpected error: %v", err) 556 } 557 case tc.err: 558 t.Errorf("ListBuilds(): failed to receive an error") 559 default: 560 if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(Build{}, Path{})); diff != "" { 561 t.Errorf("ListBuilds(): got unexpected diff (-have, +want):\n%s", diff) 562 } 563 } 564 }) 565 } 566 } 567 568 func TestReadLink(t *testing.T) { 569 cases := []struct { 570 name string 571 meta map[string]string 572 expected string 573 }{ 574 { 575 name: "basically works", 576 meta: map[string]string{}, 577 }, 578 { 579 name: "find link", 580 meta: map[string]string{ 581 "link": "foo", 582 }, 583 expected: "foo", 584 }, 585 { 586 name: "find x-goog-meta-link", 587 meta: map[string]string{ 588 "x-goog-meta-link": "foo", 589 }, 590 expected: "foo", 591 }, 592 { 593 name: "ignore random", 594 meta: map[string]string{ 595 "x-random-link": "foo", 596 }, 597 }, 598 { 599 name: "prefer x-goog-meta-link", 600 meta: map[string]string{ 601 "x-goog-meta-link": "yes", 602 "link": "no", 603 }, 604 expected: "yes", 605 }, 606 } 607 608 for _, tc := range cases { 609 t.Run(tc.name, func(t *testing.T) { 610 var oa storage.ObjectAttrs 611 oa.Metadata = tc.meta 612 if actual := readLink(&oa); actual != tc.expected { 613 t.Errorf("readLink(%v) got %q want %q", oa, actual, tc.expected) 614 } 615 }) 616 } 617 } 618 619 func TestParseSuitesMeta(t *testing.T) { 620 cases := []struct { 621 name string 622 input string 623 context string 624 timestamp string 625 thread string 626 empty bool 627 }{ 628 629 { 630 name: "not junit", 631 input: "./started.json", 632 empty: true, 633 }, 634 { 635 name: "forgot suffix", 636 input: "./junit", 637 empty: true, 638 }, 639 { 640 name: "basic", 641 input: "./junit.xml", 642 }, 643 { 644 name: "context", 645 input: "./junit_hello world isn't-this exciting!.xml", 646 context: "hello world isn't-this exciting!", 647 }, 648 { 649 name: "numeric context", 650 input: "./junit_12345.xml", 651 context: "12345", 652 }, 653 { 654 name: "context and thread", 655 input: "./junit_context_12345.xml", 656 context: "context", 657 thread: "12345", 658 }, 659 { 660 name: "context and timestamp", 661 input: "./junit_context_20180102-1234.xml", 662 context: "context", 663 timestamp: "20180102-1234", 664 }, 665 { 666 name: "context thread timestamp", 667 input: "./junit_context_20180102-1234_5555.xml", 668 context: "context", 669 timestamp: "20180102-1234", 670 thread: "5555", 671 }, 672 { 673 name: "accept weird junit name", 674 input: "./junit.e2e_suite.3.xml", 675 context: ".e2e_suite.3", 676 }, 677 { 678 name: "bazel format", 679 input: "./test.xml", 680 }, 681 } 682 683 for _, tc := range cases { 684 actual := parseSuitesMeta(tc.input) 685 switch { 686 case actual == nil && !tc.empty: 687 t.Errorf("%s: unexpected nil map", tc.name) 688 case actual != nil && tc.empty: 689 t.Errorf("%s: should not have returned a map: %v", tc.name, actual) 690 case actual != nil: 691 for k, expected := range map[string]string{ 692 "Context": tc.context, 693 "Thread": tc.thread, 694 "Timestamp": tc.timestamp, 695 } { 696 if a, ok := actual[k]; !ok { 697 t.Errorf("%s: missing key %s", tc.name, k) 698 } else if a != expected { 699 t.Errorf("%s: %s actual %s != expected %s", tc.name, k, a, expected) 700 } 701 } 702 } 703 } 704 705 } 706 707 func TestReadJSON(t *testing.T) { 708 cases := []struct { 709 name string 710 obj *fakeObject 711 actual interface{} 712 expected interface{} 713 is error 714 }{ 715 { 716 name: "basically works", 717 obj: &fakeObject{data: "{}"}, 718 actual: &Started{}, 719 expected: &Started{}, 720 }, 721 { 722 name: "read a json object", 723 obj: &fakeObject{data: "{\"hello\": 5}"}, 724 actual: &struct { 725 Hello int `json:"hello"` 726 }{}, 727 expected: &struct { 728 Hello int `json:"hello"` 729 }{5}, 730 }, 731 { 732 name: "ErrObjectNotExist on open returns an ErrObjectNotExist error", 733 is: storage.ErrObjectNotExist, 734 }, 735 { 736 name: "other open errors also error", 737 obj: &fakeObject{openErr: errors.New("injected open error")}, 738 }, 739 { 740 name: "read error errors", 741 obj: &fakeObject{ 742 data: "{}", 743 readErr: errors.New("injected read error"), 744 }, 745 }, 746 { 747 name: "close error errors", 748 obj: &fakeObject{ 749 data: "{}", 750 closeErr: errors.New("injected close error"), 751 }, 752 }, 753 { 754 name: "invalid json errors", 755 obj: &fakeObject{ 756 data: "{\"json\": \"hates trailing commas\",}", 757 closeErr: errors.New("injected close error"), 758 }, 759 }, 760 } 761 762 path := newPathOrDie("gs://bucket/path/to/something") 763 for _, tc := range cases { 764 t.Run(tc.name, func(t *testing.T) { 765 fo := fakeOpener{} 766 if tc.obj != nil { 767 fo[path] = *tc.obj 768 } 769 err := readJSON(context.Background(), fo, path, tc.actual) 770 switch { 771 case err != nil: 772 if tc.expected != nil { 773 t.Errorf("unexpected error: %v", err) 774 } 775 if tc.is != nil && !errors.Is(err, tc.is) { 776 t.Errorf("bad error: %v, wanted %v", err, tc.is) 777 } 778 case tc.expected == nil: 779 t.Error("failed to receive expected error") 780 default: 781 if !reflect.DeepEqual(tc.actual, tc.expected) { 782 t.Errorf("got %v, want %v", tc.actual, tc.expected) 783 } 784 } 785 }) 786 } 787 } 788 789 type fakeOpener map[Path]fakeObject 790 791 func (fo fakeOpener) Open(ctx context.Context, path Path) (io.ReadCloser, *storage.ReaderObjectAttrs, error) { 792 o, ok := fo[path] 793 if !ok { 794 return nil, nil, fmt.Errorf("wrap not exist: %w", storage.ErrObjectNotExist) 795 } 796 if o.openErr != nil { 797 return nil, nil, o.openErr 798 } 799 return ioutil.NopCloser(&fakeReader{ 800 buf: bytes.NewBufferString(o.data), 801 readErr: o.readErr, 802 closeErr: o.closeErr, 803 }), o.attrs, nil 804 } 805 806 type fakeObject struct { 807 data string 808 attrs *storage.ReaderObjectAttrs 809 openErr error 810 readErr error 811 closeErr error 812 } 813 814 type fakeReader struct { 815 buf *bytes.Buffer 816 readErr error 817 closeErr error 818 } 819 820 func (fr *fakeReader) Read(p []byte) (int, error) { 821 if fr.readErr != nil { 822 return 0, fr.readErr 823 } 824 return fr.buf.Read(p) 825 } 826 827 func (fr *fakeReader) Close() error { 828 if fr.closeErr != nil { 829 return fr.closeErr 830 } 831 fr.readErr = errors.New("already closed") 832 fr.closeErr = fr.readErr 833 return nil 834 } 835 836 type fakeLister map[Path]fakeIterator 837 838 func (fl fakeLister) Objects(ctx context.Context, path Path, _, offset string) Iterator { 839 f := fl[path] 840 f.ctx = ctx 841 f.offset = offset 842 return &f 843 } 844 845 type fakeIterator struct { 846 objects []storage.ObjectAttrs 847 idx int 848 err int // must be > 0 849 ctx context.Context 850 offset string 851 } 852 853 func (fi *fakeIterator) Next() (*storage.ObjectAttrs, error) { 854 if fi.ctx.Err() != nil { 855 return nil, fi.ctx.Err() 856 } 857 for fi.idx < len(fi.objects) { 858 if fi.offset == "" { 859 break 860 } 861 name, prefix := fi.objects[fi.idx].Name, fi.objects[fi.idx].Prefix 862 if (name == "" || name >= fi.offset) && (prefix == "" || prefix >= fi.offset) { 863 break 864 } 865 fi.idx++ 866 } 867 if fi.idx >= len(fi.objects) { 868 return nil, iterator.Done 869 } 870 if fi.idx > 0 && fi.idx == fi.err { 871 return nil, errors.New("injected fakeIterator error") 872 } 873 874 o := fi.objects[fi.idx] 875 fi.idx++ 876 return &o, nil 877 } 878 879 func TestStarted(t *testing.T) { 880 path := newPathOrDie("gs://bucket/path/") 881 started := resolveOrDie(path, "started.json") 882 cases := []struct { 883 name string 884 ctx context.Context 885 object *fakeObject 886 expected *Started 887 checkErr error 888 }{ 889 { 890 name: "basically works", 891 object: &fakeObject{data: "{}"}, 892 expected: &Started{}, 893 }, 894 { 895 name: "canceled context returns error", 896 object: &fakeObject{}, 897 ctx: func() context.Context { 898 ctx, cancel := context.WithCancel(context.Background()) 899 cancel() 900 return ctx 901 }(), 902 }, 903 { 904 name: "all fields parsed", 905 object: &fakeObject{ 906 data: `{ 907 "timestamp": 1234, 908 "node": "machine", 909 "pull": "your leg", 910 "repos": { 911 "main": "deadbeef" 912 }, 913 "repo-commit": "11111", 914 "metadata": { 915 "version": "fun", 916 "float": 1.2, 917 "object": {"yes": true} 918 } 919 }`, 920 }, 921 expected: &Started{ 922 Started: metadata.Started{ 923 Timestamp: 1234, 924 Node: "machine", 925 Pull: "your leg", 926 Repos: map[string]string{ 927 "main": "deadbeef", 928 }, 929 RepoCommit: "11111", 930 Metadata: metadata.Metadata{ 931 "version": "fun", 932 "float": 1.2, 933 "object": map[string]interface{}{ 934 "yes": true, 935 }, 936 }, 937 }, 938 }, 939 }, 940 { 941 name: "missing object means pending", 942 expected: &Started{Pending: true}, 943 }, 944 { 945 name: "read error returns an error", 946 object: &fakeObject{readErr: errors.New("injected read error")}, 947 }, 948 } 949 950 for _, tc := range cases { 951 t.Run(tc.name, func(t *testing.T) { 952 fo := fakeOpener{} 953 if tc.object != nil { 954 fo[started] = *tc.object 955 } 956 b := Build{Path: path} 957 if tc.ctx == nil { 958 tc.ctx = context.Background() 959 } 960 ctx, cancel := context.WithCancel(tc.ctx) 961 defer cancel() 962 actual, err := b.Started(ctx, fo) 963 switch { 964 case err != nil: 965 if tc.expected != nil { 966 t.Errorf("Started(): unexpected error: %v", err) 967 } 968 default: 969 if !reflect.DeepEqual(actual, tc.expected) { 970 t.Errorf("Started(): got %v, want %v", actual, tc.expected) 971 } 972 } 973 974 }) 975 } 976 } 977 978 func TestFinished(t *testing.T) { 979 yes := true 980 path := newPathOrDie("gs://bucket/path/") 981 finished := resolveOrDie(path, "finished.json") 982 cases := []struct { 983 name string 984 ctx context.Context 985 object *fakeObject 986 expected *Finished 987 checkErr error 988 }{ 989 { 990 name: "basically works", 991 object: &fakeObject{data: "{}"}, 992 expected: &Finished{}, 993 }, 994 { 995 name: "canceled context returns error", 996 object: &fakeObject{}, 997 ctx: func() context.Context { 998 ctx, cancel := context.WithCancel(context.Background()) 999 cancel() 1000 return ctx 1001 }(), 1002 }, 1003 { 1004 name: "all fields parsed", 1005 object: &fakeObject{ 1006 data: `{ 1007 "timestamp": 1234, 1008 "passed": true, 1009 "metadata": { 1010 "version": "fun", 1011 "float": 1.2, 1012 "object": {"yes": true} 1013 } 1014 }`, 1015 }, 1016 expected: &Finished{ 1017 Finished: metadata.Finished{ 1018 Timestamp: func() *int64 { 1019 var out int64 = 1234 1020 return &out 1021 }(), 1022 Passed: &yes, 1023 Metadata: metadata.Metadata{ 1024 "version": "fun", 1025 "float": 1.2, 1026 "object": map[string]interface{}{ 1027 "yes": true, 1028 }, 1029 }, 1030 }, 1031 }, 1032 }, 1033 { 1034 name: "missing object means running", 1035 expected: &Finished{Running: true}, 1036 }, 1037 { 1038 name: "read error returns an error", 1039 object: &fakeObject{readErr: errors.New("injected read error")}, 1040 }, 1041 } 1042 1043 for _, tc := range cases { 1044 t.Run(tc.name, func(t *testing.T) { 1045 fo := fakeOpener{} 1046 if tc.object != nil { 1047 fo[finished] = *tc.object 1048 } 1049 b := Build{Path: path} 1050 if tc.ctx == nil { 1051 tc.ctx = context.Background() 1052 } 1053 ctx, cancel := context.WithCancel(tc.ctx) 1054 defer cancel() 1055 actual, err := b.Finished(ctx, fo) 1056 switch { 1057 case err != nil: 1058 if tc.expected != nil { 1059 t.Errorf("Finished(): unexpected error: %v", err) 1060 } 1061 default: 1062 if !reflect.DeepEqual(actual, tc.expected) { 1063 t.Errorf("Finished(): got %v, want %v", actual, tc.expected) 1064 } 1065 } 1066 1067 }) 1068 } 1069 } 1070 1071 func resolveOrDie(p Path, s string) Path { 1072 out, err := p.ResolveReference(&url.URL{Path: s}) 1073 if err != nil { 1074 panic(fmt.Sprintf("%s - %s", p, err)) 1075 } 1076 return *out 1077 } 1078 1079 func pResolveOrDie(p Path, s string) *Path { 1080 out := resolveOrDie(p, s) 1081 return &out 1082 } 1083 1084 func newPathOrDie(s string) Path { 1085 p, err := NewPath(s) 1086 if err != nil { 1087 panic(err) 1088 } 1089 return *p 1090 } 1091 1092 func TestReadSuites(t *testing.T) { 1093 path := newPathOrDie("gs://bucket/object") 1094 cases := []struct { 1095 name string 1096 ctx context.Context 1097 opener fakeOpener 1098 expected *junit.Suites 1099 checkErr error 1100 }{ 1101 { 1102 name: "basically works", 1103 opener: fakeOpener{ 1104 path: { 1105 data: `<testsuites><testsuite><testcase name="foo"/></testsuite></testsuites>`, 1106 }, 1107 }, 1108 expected: &junit.Suites{ 1109 XMLName: xml.Name{Local: "testsuites"}, 1110 Suites: []junit.Suite{ 1111 { 1112 XMLName: xml.Name{Local: "testsuite"}, 1113 Results: []junit.Result{ 1114 { 1115 Name: "foo", 1116 }, 1117 }, 1118 }, 1119 }, 1120 }, 1121 }, 1122 { 1123 name: "not found returns not found error", 1124 checkErr: storage.ErrObjectNotExist, 1125 }, 1126 { 1127 name: "invalid junit returns error", 1128 opener: fakeOpener{ 1129 path: {data: `<wrong><type></type></wrong>`}, 1130 }, 1131 }, 1132 { 1133 name: "reject large artifacts", 1134 opener: fakeOpener{ 1135 path: { 1136 data: `<testsuites><testsuite><testcase name="foo"/></testsuite></testsuites>`, 1137 attrs: &storage.ReaderObjectAttrs{Size: maxSize + 1}, 1138 }, 1139 }, 1140 }, 1141 { 1142 name: "read max size", 1143 opener: fakeOpener{ 1144 path: { 1145 data: `<testsuites><testsuite><testcase name="foo"/></testsuite></testsuites>`, 1146 attrs: &storage.ReaderObjectAttrs{Size: maxSize}, 1147 }, 1148 }, 1149 expected: &junit.Suites{ 1150 XMLName: xml.Name{Local: "testsuites"}, 1151 Suites: []junit.Suite{ 1152 { 1153 XMLName: xml.Name{Local: "testsuite"}, 1154 Results: []junit.Result{ 1155 { 1156 Name: "foo", 1157 }, 1158 }, 1159 }, 1160 }, 1161 }, 1162 }, 1163 { 1164 name: "read error returns error", 1165 opener: fakeOpener{ 1166 path: { 1167 readErr: errors.New("injected read error"), 1168 }, 1169 }, 1170 }, 1171 } 1172 1173 for _, tc := range cases { 1174 t.Run(tc.name, func(t *testing.T) { 1175 actual, err := readSuites(tc.ctx, tc.opener, path) 1176 switch { 1177 case err != nil: 1178 if tc.expected != nil { 1179 t.Errorf("readSuites(): unexpected error: %v", err) 1180 } else if tc.checkErr != nil && !errors.Is(err, tc.checkErr) { 1181 t.Errorf("readSuites(): bad error %v, wanted %v", err, tc.checkErr) 1182 } 1183 case tc.expected == nil: 1184 t.Error("readSuites(): failed to receive an error") 1185 default: 1186 if !reflect.DeepEqual(actual, tc.expected) { 1187 t.Errorf("readSuites(): got %v, want %v", actual, tc.expected) 1188 } 1189 } 1190 }) 1191 } 1192 } 1193 1194 func TestArtifacts(t *testing.T) { 1195 path := newPathOrDie("gs://bucket/path/") 1196 cases := []struct { 1197 name string 1198 ctx context.Context 1199 iterator fakeIterator 1200 expected []string 1201 err bool 1202 }{ 1203 { 1204 name: "basically works", 1205 }, 1206 { 1207 name: "cancelled context returns error", 1208 iterator: fakeIterator{ 1209 objects: []storage.ObjectAttrs{ 1210 {Name: "whatever"}, 1211 {Name: "stuff"}, 1212 }, 1213 }, 1214 ctx: func() context.Context { 1215 ctx, cancel := context.WithCancel(context.Background()) 1216 cancel() 1217 return ctx 1218 }(), 1219 err: true, 1220 }, 1221 { 1222 name: "iteration error returns error", 1223 iterator: fakeIterator{ 1224 objects: []storage.ObjectAttrs{ 1225 {Name: "hello"}, 1226 {Name: "boom"}, 1227 {Name: "world"}, 1228 }, 1229 err: 1, 1230 }, 1231 err: true, 1232 }, 1233 { 1234 name: "multiple objects work", 1235 iterator: fakeIterator{ 1236 objects: []storage.ObjectAttrs{ 1237 {Name: "hello"}, 1238 {Name: "world"}, 1239 }, 1240 }, 1241 expected: []string{"hello", "world"}, 1242 }, 1243 } 1244 1245 for _, tc := range cases { 1246 t.Run(tc.name, func(t *testing.T) { 1247 b := Build{ 1248 Path: path, 1249 } 1250 var actual []string 1251 ch := make(chan string) 1252 var lock sync.Mutex 1253 lock.Lock() 1254 go func() { 1255 defer lock.Unlock() 1256 for a := range ch { 1257 actual = append(actual, a) 1258 } 1259 }() 1260 if tc.ctx == nil { 1261 tc.ctx = context.Background() 1262 } 1263 fl := fakeLister{path: tc.iterator} 1264 err := b.Artifacts(tc.ctx, fl, ch) 1265 close(ch) 1266 lock.Lock() 1267 switch { 1268 case err != nil: 1269 if !tc.err { 1270 t.Errorf("Artifacts(): unexpected error: %v", err) 1271 } 1272 case tc.err: 1273 t.Errorf("Artifacts(): failed to receive an error") 1274 default: 1275 if !reflect.DeepEqual(actual, tc.expected) { 1276 t.Errorf("Artifacts(): got %v, want %v", actual, tc.expected) 1277 } 1278 } 1279 }) 1280 } 1281 } 1282 1283 func TestSuites(t *testing.T) { 1284 cases := []struct { 1285 name string 1286 ctx context.Context 1287 path Path 1288 artifacts map[string]string 1289 max int 1290 1291 expected []SuitesMeta 1292 err bool 1293 }{ 1294 { 1295 name: "basically works", 1296 }, 1297 { 1298 name: "ignore random file", 1299 path: newPathOrDie("gs://where/whatever"), 1300 artifacts: map[string]string{ 1301 "/something/ignore.txt": "hello", 1302 "/something/ignore.json": "{}", 1303 }, 1304 }, 1305 { 1306 name: "support testsuite", 1307 path: newPathOrDie("gs://where/whatever"), 1308 artifacts: map[string]string{ 1309 "/something/junit.xml": `<testsuites><testsuite><testcase name="foo"/></testsuite></testsuites>`, 1310 }, 1311 expected: []SuitesMeta{ 1312 { 1313 Suites: &junit.Suites{ 1314 XMLName: xml.Name{Local: "testsuites"}, 1315 Suites: []junit.Suite{ 1316 { 1317 XMLName: xml.Name{Local: "testsuite"}, 1318 Results: []junit.Result{ 1319 { 1320 Name: "foo", 1321 }, 1322 }, 1323 }, 1324 }, 1325 }, 1326 Metadata: parseSuitesMeta("/something/junit.xml"), 1327 Path: "gs://where/something/junit.xml", 1328 }, 1329 }, 1330 }, 1331 { 1332 name: "support testsuites", 1333 path: newPathOrDie("gs://where/whatever"), 1334 artifacts: map[string]string{ 1335 "/something/junit.xml": `<testsuite><testcase name="foo"/></testsuite>`, 1336 }, 1337 expected: []SuitesMeta{ 1338 { 1339 Suites: &junit.Suites{ 1340 Suites: []junit.Suite{ 1341 { 1342 XMLName: xml.Name{Local: "testsuite"}, 1343 Results: []junit.Result{ 1344 { 1345 Name: "foo", 1346 }, 1347 }, 1348 }, 1349 }, 1350 }, 1351 Metadata: parseSuitesMeta("/something/junit.xml"), 1352 Path: "gs://where/something/junit.xml", 1353 }, 1354 }, 1355 }, 1356 { 1357 name: "capture metadata", 1358 path: newPathOrDie("gs://where/whatever"), 1359 artifacts: map[string]string{ 1360 "/something/junit_foo-context_20200708-1234_88.xml": `<testsuite><testcase name="foo"/></testsuite>`, 1361 "/something/junit_bar-context_20211234-0808_33.xml": `<testsuite><testcase name="bar"/></testsuite>`, 1362 }, 1363 expected: []SuitesMeta{ 1364 { 1365 Suites: &junit.Suites{ 1366 Suites: []junit.Suite{ 1367 { 1368 XMLName: xml.Name{Local: "testsuite"}, 1369 Results: []junit.Result{ 1370 { 1371 Name: "foo", 1372 }, 1373 }, 1374 }, 1375 }, 1376 }, 1377 Metadata: parseSuitesMeta("/something/junit_foo-context_20200708-1234_88.xml"), 1378 Path: "gs://where/something/junit_foo-context_20200708-1234_88.xml", 1379 }, 1380 { 1381 Suites: &junit.Suites{ 1382 Suites: []junit.Suite{ 1383 { 1384 XMLName: xml.Name{Local: "testsuite"}, 1385 Results: []junit.Result{ 1386 { 1387 Name: "bar", 1388 }, 1389 }, 1390 }, 1391 }, 1392 }, 1393 Metadata: parseSuitesMeta("/something/junit_bar-context_20211234-0808_33.xml"), 1394 Path: "gs://where/something/junit_bar-context_20211234-0808_33.xml", 1395 }, 1396 }, 1397 }, 1398 { 1399 name: "read suites error contains error", 1400 path: newPathOrDie("gs://where/whatever"), 1401 artifacts: map[string]string{ 1402 "something/junit.xml": `<this is invalid json`, 1403 }, 1404 expected: []SuitesMeta{ 1405 { 1406 Metadata: parseSuitesMeta("something/junit.xml"), 1407 Err: errors.New("boom"), 1408 Path: "gs://where/something/junit.xml", 1409 }, 1410 }, 1411 }, 1412 { 1413 name: "interrupted context returns error", 1414 ctx: func() context.Context { 1415 ctx, cancel := context.WithCancel(context.Background()) 1416 cancel() 1417 return ctx 1418 }(), 1419 path: newPathOrDie("gs://where/whatever"), 1420 artifacts: map[string]string{ 1421 "/something/junit_foo-context_20200708-1234_88.xml": `<testsuite><testcase name="foo"/></testsuite>`, 1422 }, 1423 err: true, 1424 }, 1425 } 1426 1427 for _, tc := range cases { 1428 t.Run(tc.name, func(t *testing.T) { 1429 fo := fakeOpener{} 1430 b := Build{Path: tc.path} 1431 for s, data := range tc.artifacts { 1432 fo[resolveOrDie(b.Path, s)] = fakeObject{data: data} 1433 } 1434 1435 parent, cancel := context.WithCancel(context.Background()) 1436 defer cancel() 1437 if tc.ctx == nil { 1438 tc.ctx = parent 1439 } 1440 arts := make(chan string) 1441 go func() { 1442 defer close(arts) 1443 for a := range tc.artifacts { 1444 select { 1445 case arts <- a: 1446 case <-parent.Done(): 1447 return 1448 } 1449 } 1450 }() 1451 1452 var actual []SuitesMeta 1453 suites := make(chan SuitesMeta) 1454 var lock sync.Mutex 1455 lock.Lock() 1456 go func() { 1457 defer lock.Unlock() 1458 time.Sleep(10 * time.Millisecond) // Allow time for ctx to expire 1459 for sm := range suites { 1460 actual = append(actual, sm) 1461 } 1462 }() 1463 1464 err := b.Suites(tc.ctx, fo, arts, suites, tc.max) 1465 close(suites) 1466 lock.Lock() // ensure actual is up to date 1467 defer lock.Unlock() 1468 // actual items appended in random order, so sort for consistency. 1469 sort.SliceStable(actual, func(i, j int) bool { 1470 return actual[i].Path < actual[j].Path 1471 }) 1472 sort.SliceStable(tc.expected, func(i, j int) bool { 1473 return tc.expected[i].Path < tc.expected[j].Path 1474 }) 1475 switch { 1476 case err != nil: 1477 if !tc.err { 1478 t.Errorf("Suites() unexpected error: %v", err) 1479 } 1480 case tc.err: 1481 t.Errorf("Suites() failed to receive expected error") 1482 default: 1483 cmpErrs := func(x, y error) bool { 1484 return (x == nil) == (y == nil) 1485 } 1486 if diff := cmp.Diff(tc.expected, actual, cmp.Comparer(cmpErrs)); diff != "" { 1487 t.Errorf("Suites() got unexpectec diff (-want +got):\n%s", diff) 1488 } 1489 } 1490 }) 1491 } 1492 }