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  }