github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/pubsub_test.go (about) 1 /* 2 Copyright 2021 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 "sort" 22 "strings" 23 "sync" 24 "testing" 25 "time" 26 27 gpubsub "cloud.google.com/go/pubsub" 28 "github.com/GoogleCloudPlatform/testgrid/config" 29 configpb "github.com/GoogleCloudPlatform/testgrid/pb/config" 30 "github.com/GoogleCloudPlatform/testgrid/pkg/pubsub" 31 "github.com/GoogleCloudPlatform/testgrid/util/gcs" 32 "github.com/google/go-cmp/cmp" 33 "github.com/sirupsen/logrus" 34 ) 35 36 type fakeSubscriber struct { 37 messages map[string][]*gpubsub.Message 38 wg sync.WaitGroup 39 } 40 41 func (s *fakeSubscriber) wait(cancel context.CancelFunc) { 42 s.wg.Wait() 43 cancel() 44 } 45 46 func (s *fakeSubscriber) add() { 47 n := len(s.messages) 48 s.wg.Add(n) 49 } 50 51 func (s *fakeSubscriber) Subscribe(proj, sub string, _ *gpubsub.ReceiveSettings) pubsub.Sender { 52 messages, ok := s.messages[proj+"/"+sub] 53 if !ok { 54 return func(ctx context.Context, receive func(context.Context, *gpubsub.Message)) error { 55 return nil 56 } 57 } 58 return func(ctx context.Context, receive func(context.Context, *gpubsub.Message)) error { 59 defer s.wg.Done() 60 for _, m := range messages { 61 if err := ctx.Err(); err != nil { 62 return err 63 } 64 receive(ctx, m) 65 } 66 return nil 67 } 68 } 69 70 func TestFixGCS(t *testing.T) { 71 log := logrus.WithField("test", "TestFixGCS") 72 now := time.Now().Round(time.Second) 73 cases := []struct { 74 name string 75 ctx context.Context 76 subscriber *fakeSubscriber 77 q func([]*configpb.TestGroup) *config.TestGroupQueue 78 groups []*configpb.TestGroup 79 80 want string 81 wantWhen time.Time 82 }{ 83 { 84 name: "empty", 85 q: func([]*configpb.TestGroup) *config.TestGroupQueue { 86 var q config.TestGroupQueue 87 q.Init(log, nil, now.Add(time.Hour)) 88 return &q 89 }, 90 subscriber: &fakeSubscriber{}, 91 }, 92 { 93 name: "basic", 94 q: func(groups []*configpb.TestGroup) *config.TestGroupQueue { 95 var q config.TestGroupQueue 96 q.Init(log, groups, now.Add(time.Hour)) 97 return &q 98 }, 99 subscriber: &fakeSubscriber{ 100 messages: map[string][]*gpubsub.Message{ 101 "super/duper": { 102 { 103 Attributes: map[string]string{ 104 "bucketId": "bucket", 105 "objectId": "path/finished.json", 106 "eventTime": now.Format(time.RFC3339), 107 "objectGeneration": "1", 108 }, 109 }, 110 }, 111 }, 112 }, 113 groups: []*configpb.TestGroup{ 114 { 115 Name: "foo", 116 ResultSource: &configpb.TestGroup_ResultSource{ 117 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 118 GcsConfig: &configpb.GCSConfig{ 119 GcsPrefix: "bucket/path", 120 PubsubProject: "super", 121 PubsubSubscription: "duper", 122 }, 123 }, 124 }, 125 }, 126 }, 127 want: "foo", 128 wantWhen: now.Add(namedDurations["finished.json"]), 129 }, 130 } 131 132 for _, tc := range cases { 133 t.Run(tc.name, func(t *testing.T) { 134 if tc.ctx == nil { 135 tc.ctx = context.Background() 136 } 137 ctx, cancel := context.WithCancel(tc.ctx) 138 defer cancel() 139 q := tc.q(tc.groups) 140 tc.subscriber.add() 141 go func() { 142 tc.subscriber.wait(cancel) 143 }() 144 145 fix := FixGCS(tc.subscriber) 146 fix(ctx, logrus.WithField("name", tc.name), q, tc.groups) 147 _, who, when := q.Status() 148 var got string 149 if who != nil { 150 got = who.Name 151 } 152 if got != tc.want { 153 t.Errorf("FixGCS() got unexpected next group %q, wanted %q", got, tc.want) 154 } 155 if !when.Equal(tc.wantWhen) { 156 t.Errorf("FixGCS() got unexpected next time %s, wanted %s", when, tc.wantWhen) 157 } 158 }) 159 } 160 161 } 162 163 func TestGCSSubscribedPaths(t *testing.T) { 164 origManual := manualSubs 165 defer func() { 166 manualSubs = origManual 167 }() 168 mustPath := func(s string) gcs.Path { 169 p, err := gcs.NewPath(s) 170 if err != nil { 171 t.Fatal(err) 172 } 173 return *p 174 } 175 176 cases := []struct { 177 name string 178 tgs []*configpb.TestGroup 179 manual map[string]subscription 180 181 want map[gcs.Path][]string 182 wantSubs []subscription 183 err bool 184 }{ 185 { 186 name: "empty", 187 want: map[gcs.Path][]string{}, 188 }, 189 { 190 name: "basic", 191 tgs: []*configpb.TestGroup{ 192 { 193 Name: "hello", 194 ResultSource: &configpb.TestGroup_ResultSource{ 195 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 196 GcsConfig: &configpb.GCSConfig{ 197 GcsPrefix: "bucket/path/to/job", 198 PubsubProject: "fancy", 199 PubsubSubscription: "cake", 200 }, 201 }, 202 }, 203 }, 204 { 205 Name: "multi", 206 ResultSource: &configpb.TestGroup_ResultSource{ 207 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 208 GcsConfig: &configpb.GCSConfig{ 209 GcsPrefix: "bucket/a,bucket/b", 210 PubsubProject: "super", 211 PubsubSubscription: "duper", 212 }, 213 }, 214 }, 215 }, 216 { 217 Name: "dup-a", 218 ResultSource: &configpb.TestGroup_ResultSource{ 219 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 220 GcsConfig: &configpb.GCSConfig{ 221 GcsPrefix: "bucket/dup", 222 PubsubProject: "ha", 223 PubsubSubscription: "ha", 224 }, 225 }, 226 }, 227 }, 228 { 229 Name: "dup-b", 230 ResultSource: &configpb.TestGroup_ResultSource{ 231 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 232 GcsConfig: &configpb.GCSConfig{ 233 GcsPrefix: "bucket/dup/", 234 PubsubProject: "ha", 235 PubsubSubscription: "ha", 236 }, 237 }, 238 }, 239 }, 240 }, 241 want: map[gcs.Path][]string{ 242 mustPath("gs://bucket/path/to/job/"): {"hello"}, 243 mustPath("gs://bucket/a/"): {"multi"}, 244 mustPath("gs://bucket/b/"): {"multi"}, 245 mustPath("gs://bucket/dup/"): {"dup-a", "dup-b"}, 246 }, 247 wantSubs: []subscription{ 248 {"fancy", "cake"}, 249 {"ha", "ha"}, 250 {"super", "duper"}, 251 }, 252 }, 253 { 254 name: "manually empty", 255 manual: map[string]subscription{ 256 "bucket/foo": {"this", "that"}, 257 }, 258 tgs: []*configpb.TestGroup{ 259 { 260 Name: "hello", 261 GcsPrefix: "random/stuff", 262 }, 263 }, 264 want: map[gcs.Path][]string{}, 265 }, 266 { 267 name: "manually empty", 268 manual: map[string]subscription{ 269 "bucket/foo": {"this", "that"}, 270 }, 271 tgs: []*configpb.TestGroup{ 272 { 273 Name: "hello", 274 GcsPrefix: "bucket/foo/bar", 275 }, 276 }, 277 want: map[gcs.Path][]string{ 278 mustPath("gs://bucket/foo/bar/"): {"hello"}, 279 }, 280 wantSubs: []subscription{ 281 {"this", "that"}, 282 }, 283 }, 284 { 285 name: "mixed", 286 manual: map[string]subscription{ 287 "bucket/foo": {"this", "that"}, 288 }, 289 tgs: []*configpb.TestGroup{ 290 { 291 Name: "hello", 292 GcsPrefix: "bucket/foo/bar", 293 }, 294 { 295 Name: "world", 296 ResultSource: &configpb.TestGroup_ResultSource{ 297 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 298 GcsConfig: &configpb.GCSConfig{ 299 GcsPrefix: "bucket/path/to/job", 300 PubsubProject: "fancy", 301 PubsubSubscription: "cake", 302 }, 303 }, 304 }, 305 }, 306 }, 307 want: map[gcs.Path][]string{ 308 mustPath("gs://bucket/foo/bar/"): {"hello"}, 309 mustPath("gs://bucket/path/to/job/"): {"world"}, 310 }, 311 wantSubs: []subscription{ 312 {"fancy", "cake"}, 313 {"this", "that"}, 314 }, 315 }, 316 } 317 318 for _, tc := range cases { 319 t.Run(tc.name, func(t *testing.T) { 320 manualSubs = tc.manual 321 got, gotSubs, err := gcsSubscribedPaths(tc.tgs) 322 switch { 323 case err != nil: 324 if !tc.err { 325 t.Errorf("gcsSubscribedPaths() got unexpected error: %v", err) 326 } 327 case tc.err: 328 t.Error("gcsSubscribedPaths() failed to return an error") 329 default: 330 if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(gcs.Path{})); diff != "" { 331 t.Errorf("gcsSubscribedPaths() got unexpected diff (-want +got):\n%s", diff) 332 } 333 sort.Slice(gotSubs, func(i, j int) bool { 334 switch strings.Compare(gotSubs[i].proj, gotSubs[j].proj) { 335 case -1: 336 return true 337 case 0: 338 return gotSubs[i].sub < gotSubs[j].sub 339 } 340 return false 341 }) 342 if diff := cmp.Diff(tc.wantSubs, gotSubs, cmp.AllowUnexported(subscription{})); diff != "" { 343 t.Errorf("gcsSubscribedPaths() got unexpected subscription diff (-want +got):\n%s", diff) 344 } 345 } 346 347 }) 348 } 349 } 350 351 func TestProcessGCSNotifications(t *testing.T) { 352 log := logrus.WithField("test", "TestProcessGCSNotifications") 353 mustPath := func(s string) gcs.Path { 354 p, err := gcs.NewPath(s) 355 if err != nil { 356 t.Fatal(err) 357 } 358 return *p 359 } 360 now := time.Now() 361 defer func(f func() time.Time) { 362 timeNow = f 363 }(timeNow) 364 365 timeNow = func() time.Time { 366 return now 367 } 368 cases := []struct { 369 name string 370 ctx context.Context 371 q *config.TestGroupQueue 372 paths map[gcs.Path][]string 373 notices []*pubsub.Notification 374 err bool 375 want string 376 wantWhen time.Time 377 }{ 378 { 379 name: "empty", 380 q: &config.TestGroupQueue{}, 381 }, 382 { 383 name: "basic", 384 q: func() *config.TestGroupQueue { 385 var q config.TestGroupQueue 386 q.Init(log, []*configpb.TestGroup{ 387 { 388 Name: "hello", 389 }, 390 { 391 Name: "boom", 392 }, 393 { 394 Name: "world", 395 }, 396 }, now.Add(time.Hour)) 397 if err := q.Fix("world", now.Add(30*time.Minute), false); err != nil { 398 t.Fatalf("Fixing got unexpected error: %v", err) 399 } 400 return &q 401 }(), 402 paths: map[gcs.Path][]string{ 403 mustPath("gs://foo/boom"): {"boom"}, 404 }, 405 notices: []*pubsub.Notification{ 406 { 407 Path: mustPath("gs://foo/boom/build/finished.json"), 408 Time: now, 409 }, 410 }, 411 want: "boom", 412 wantWhen: now.Add(namedDurations["finished.json"]), 413 }, 414 { 415 name: "historical", // set floor 416 q: func() *config.TestGroupQueue { 417 var q config.TestGroupQueue 418 q.Init(log, []*configpb.TestGroup{ 419 { 420 Name: "hello", 421 }, 422 { 423 Name: "boom", 424 }, 425 { 426 Name: "world", 427 }, 428 }, now.Add(time.Hour)) 429 if err := q.Fix("world", now.Add(30*time.Minute), false); err != nil { 430 t.Fatalf("Fixing got unexpected error: %v", err) 431 } 432 return &q 433 }(), 434 paths: map[gcs.Path][]string{ 435 mustPath("gs://foo/boom"): {"boom"}, 436 }, 437 notices: []*pubsub.Notification{ 438 { 439 Path: mustPath("gs://foo/boom/build/finished.json"), 440 Time: now.Add(-time.Hour), 441 }, 442 }, 443 want: "boom", 444 wantWhen: now, 445 }, 446 { 447 name: "multi", 448 q: func() *config.TestGroupQueue { 449 var q config.TestGroupQueue 450 q.Init(log, []*configpb.TestGroup{ 451 { 452 Name: "hello", 453 }, 454 { 455 Name: "boom", 456 }, 457 { 458 Name: "world", 459 }, 460 }, now.Add(time.Hour)) 461 if err := q.Fix("world", now.Add(30*time.Minute), false); err != nil { 462 t.Fatalf("Fixing got unexpected error: %v", err) 463 } 464 return &q 465 }(), 466 paths: map[gcs.Path][]string{ 467 mustPath("gs://foo/multi"): {"world", "boom"}, 468 }, 469 notices: []*pubsub.Notification{ 470 { 471 Path: mustPath("gs://foo/multi/build/finished.json"), 472 Time: now, 473 }, 474 }, 475 want: "world", 476 wantWhen: now.Add(namedDurations["finished.json"]), 477 }, 478 { 479 name: "unchanged", 480 q: func() *config.TestGroupQueue { 481 var q config.TestGroupQueue 482 q.Init(log, []*configpb.TestGroup{ 483 { 484 Name: "hello", 485 }, 486 { 487 Name: "world", 488 }, 489 { 490 Name: "boom", 491 }, 492 }, now.Add(time.Hour)) 493 if err := q.Fix("world", now.Add(30*time.Minute), false); err != nil { 494 t.Fatalf("Fixing got unexpected error: %v", err) 495 } 496 return &q 497 }(), 498 paths: map[gcs.Path][]string{ 499 mustPath("gs://foo/boom"): {"boom"}, 500 }, 501 notices: []*pubsub.Notification{ 502 { 503 Path: mustPath("gs://random/stuff"), 504 Time: now, 505 }, 506 }, 507 want: "world", 508 wantWhen: now.Add(30 * time.Minute), 509 }, 510 } 511 512 for _, tc := range cases { 513 t.Run(tc.name, func(t *testing.T) { 514 if tc.ctx == nil { 515 tc.ctx = context.Background() 516 } 517 ctx, cancel := context.WithCancel(tc.ctx) 518 defer cancel() 519 ch := make(chan *pubsub.Notification) 520 go func() { 521 for _, notice := range tc.notices { 522 select { 523 case <-ctx.Done(): 524 return 525 case ch <- notice: 526 } 527 } 528 cancel() 529 }() 530 531 err := processGCSNotifications(ctx, logrus.WithField("name", tc.name), tc.q, tc.paths, ch) 532 switch { 533 case err != nil && err != context.Canceled: 534 if !tc.err { 535 t.Errorf("processGCSNotifications() got unexpected err: %v", err) 536 } 537 case tc.err: 538 t.Error("processGCSNotifications() failed to return an error") 539 default: 540 _, who, when := tc.q.Status() 541 var got string 542 if who != nil { 543 got = who.Name 544 } 545 if diff := cmp.Diff(tc.want, got); diff != "" { 546 t.Errorf("processGCSNotifications got unexpected diff (-want +got):\n%s", diff) 547 } 548 if diff := cmp.Diff(tc.wantWhen, when); diff != "" { 549 t.Errorf("processGCSNotifications got unexpected when diff (-want +got):\n%s", diff) 550 } 551 552 } 553 }) 554 } 555 } 556 557 func TestProcessNotification(t *testing.T) { 558 mustPath := func(s string) gcs.Path { 559 p, err := gcs.NewPath(s) 560 if err != nil { 561 t.Fatal(err) 562 } 563 return *p 564 } 565 type testcase struct { 566 name string 567 paths map[gcs.Path][]string 568 n *pubsub.Notification 569 want []string 570 wantDur time.Duration 571 } 572 cases := []testcase{ 573 { 574 name: "empty", 575 n: &pubsub.Notification{}, 576 }, 577 { 578 name: "irrelevant path", 579 paths: map[gcs.Path][]string{ 580 mustPath("gs://foo/bar"): {"hello", "world"}, 581 }, 582 n: &pubsub.Notification{ 583 Path: mustPath("gs://random/job/build/finished.json"), 584 }, 585 }, 586 { 587 name: "irrelevant basename", 588 paths: map[gcs.Path][]string{ 589 mustPath("gs://foo/bar"): {"hello", "world"}, 590 }, 591 n: &pubsub.Notification{ 592 Path: mustPath("gs://foo/bar/artifacts/smile.jpeg"), 593 }, 594 }, 595 { 596 name: "not junit", 597 paths: map[gcs.Path][]string{ 598 mustPath("gs://foo/bar"): {"hello", "world"}, 599 }, 600 n: &pubsub.Notification{ 601 Path: mustPath("gs://foo/bar/artifacts/context.xml"), 602 }, 603 }, 604 { 605 name: "irrelevant extension", 606 paths: map[gcs.Path][]string{ 607 mustPath("gs://foo/bar"): {"hello", "world"}, 608 }, 609 n: &pubsub.Notification{ 610 Path: mustPath("gs://foo/bar/artifacts/junit.jpeg"), 611 }, 612 }, 613 { 614 name: "simple junit", 615 paths: map[gcs.Path][]string{ 616 mustPath("gs://foo/bar"): {"hello", "world"}, 617 mustPath("gs://not/me"): {"nope", "world"}, 618 mustPath("gs://foo/"): {"yes", "me"}, 619 }, 620 n: &pubsub.Notification{ 621 Path: mustPath("gs://foo/bar/artifacts/junit.xml"), 622 }, 623 want: []string{"hello", "me", "world", "yes"}, 624 wantDur: 5 * time.Minute, 625 }, 626 { 627 name: "normal txt", 628 paths: map[gcs.Path][]string{ 629 mustPath("gs://foo/bar"): {"yes"}, 630 }, 631 n: &pubsub.Notification{ 632 Path: mustPath("gs://foo/bar/something.txt"), 633 }, 634 }, 635 { 636 name: "directory txt", 637 paths: map[gcs.Path][]string{ 638 mustPath("gs://foo/bar/directory"): {"yes"}, 639 }, 640 n: &pubsub.Notification{ 641 Path: mustPath("gs://foo/bar/directory/something.txt"), 642 }, 643 want: []string{"yes"}, 644 wantDur: 5 * time.Minute, 645 }, 646 { 647 name: "complex junit", 648 paths: map[gcs.Path][]string{ 649 mustPath("gs://foo/bar"): {"hello", "world"}, 650 mustPath("gs://not/me"): {"nope", "world"}, 651 mustPath("gs://foo/"): {"yes", "me"}, 652 }, 653 n: &pubsub.Notification{ 654 Path: mustPath("gs://foo/bar/artifacts/junit_debian-23094820.xml"), 655 }, 656 want: []string{"hello", "me", "world", "yes"}, 657 wantDur: 5 * time.Minute, 658 }, 659 } 660 661 for name, dur := range namedDurations { 662 cases = append(cases, testcase{ 663 name: name, 664 paths: map[gcs.Path][]string{ 665 mustPath("gs://foo/bar"): {"hello", "world"}, 666 mustPath("gs://not/me"): {"nope", "world"}, 667 mustPath("gs://foo/"): {"yes", "me"}, 668 }, 669 n: &pubsub.Notification{ 670 Path: mustPath("gs://foo/bar/" + name), 671 }, 672 want: []string{"hello", "me", "world", "yes"}, 673 wantDur: dur, 674 }) 675 } 676 677 for _, tc := range cases { 678 t.Run(tc.name, func(t *testing.T) { 679 got, gotDur := processNotification(tc.paths, tc.n) 680 if diff := cmp.Diff(tc.want, got); diff != "" { 681 t.Errorf("processNotification() got unexpected diff:\n%s", diff) 682 } 683 if diff := cmp.Diff(tc.wantDur, gotDur); diff != "" { 684 t.Errorf("processNotification() got unexpected duration diff:\n%s", diff) 685 } 686 }) 687 } 688 }