github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/merger/merger_test.go (about)

     1  /*
     2  Copyright 2021 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 merger
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"testing"
    27  	"time"
    28  
    29  	"cloud.google.com/go/storage"
    30  	"github.com/golang/protobuf/proto"
    31  	"github.com/google/go-cmp/cmp"
    32  
    33  	configpb "github.com/GoogleCloudPlatform/testgrid/pb/config"
    34  	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
    35  )
    36  
    37  func newPathOrDie(s string) *gcs.Path {
    38  	p, err := gcs.NewPath(s)
    39  	if err != nil {
    40  		panic(err)
    41  	}
    42  	return p
    43  }
    44  
    45  func Test_ParseAndCheck(t *testing.T) {
    46  	tc := []struct {
    47  		name         string
    48  		input        []byte
    49  		expectedList MergeList
    50  		expectError  bool
    51  	}{
    52  		{
    53  			name:        "Empty MergeList will return an error",
    54  			expectError: true,
    55  		},
    56  		{
    57  			name: "Parses YAML examples",
    58  			input: []byte(`target: "gs://path/to/write/config"
    59  sources:
    60  - name: "red"
    61    location: "gs://example/red-team/config"
    62    contact: "red-admin@example.com"
    63  - name: "blue"
    64    location: "gs://example/blue-team/config"
    65    contact: "blue.team.contact@example.com"`),
    66  			expectedList: MergeList{
    67  				Target: "gs://path/to/write/config",
    68  				Path:   newPathOrDie("gs://path/to/write/config"),
    69  				Sources: []Source{
    70  					{
    71  						Name:     "red",
    72  						Location: "gs://example/red-team/config",
    73  						Path:     newPathOrDie("gs://example/red-team/config"),
    74  						Contact:  "red-admin@example.com",
    75  					},
    76  					{
    77  						Name:     "blue",
    78  						Location: "gs://example/blue-team/config",
    79  						Path:     newPathOrDie("gs://example/blue-team/config"),
    80  						Contact:  "blue.team.contact@example.com",
    81  					},
    82  				},
    83  			},
    84  		},
    85  		{
    86  			name: "Tolerate missing contacts",
    87  			input: []byte(`target: "gs://path/to/write/config"
    88  sources:
    89  - name: "red"
    90    location: "gs://example/red-team/config"
    91  - name: "blue"
    92    location: "gs://example/blue-team/config"`),
    93  			expectedList: MergeList{
    94  				Target: "gs://path/to/write/config",
    95  				Path:   newPathOrDie("gs://path/to/write/config"),
    96  				Sources: []Source{
    97  					{
    98  						Name:     "red",
    99  						Location: "gs://example/red-team/config",
   100  						Path:     newPathOrDie("gs://example/red-team/config"),
   101  					},
   102  					{
   103  						Name:     "blue",
   104  						Location: "gs://example/blue-team/config",
   105  						Path:     newPathOrDie("gs://example/blue-team/config"),
   106  					},
   107  				},
   108  			},
   109  		},
   110  		{
   111  			name: "Target is local filesystem path",
   112  			input: []byte(`target: "/tmp/config"
   113  sources:
   114  - name: "red"
   115    location: "gs://example/red-team/config"
   116    contact: "red-admin@example.com"
   117  - name: "blue"
   118    location: "gs://example/blue-team/config"
   119    contact: "blue.team.contact@example.com"`),
   120  			expectedList: MergeList{
   121  				Target: "/tmp/config",
   122  				Path:   newPathOrDie("/tmp/config"),
   123  				Sources: []Source{
   124  					{
   125  						Name:     "red",
   126  						Location: "gs://example/red-team/config",
   127  						Path:     newPathOrDie("gs://example/red-team/config"),
   128  						Contact:  "red-admin@example.com",
   129  					},
   130  					{
   131  						Name:     "blue",
   132  						Location: "gs://example/blue-team/config",
   133  						Path:     newPathOrDie("gs://example/blue-team/config"),
   134  						Contact:  "blue.team.contact@example.com",
   135  					},
   136  				},
   137  			},
   138  		},
   139  		{
   140  			name: "Target is file:// path",
   141  			input: []byte(`target: "file://tmp/config"
   142  sources:
   143  - name: "red"
   144    location: "gs://example/red-team/config"
   145    contact: "red-admin@example.com"
   146  - name: "blue"
   147    location: "gs://example/blue-team/config"
   148    contact: "blue.team.contact@example.com"`),
   149  			expectedList: MergeList{
   150  				Target: "file://tmp/config",
   151  				Path:   newPathOrDie("file://tmp/config"),
   152  				Sources: []Source{
   153  					{
   154  						Name:     "red",
   155  						Location: "gs://example/red-team/config",
   156  						Path:     newPathOrDie("gs://example/red-team/config"),
   157  						Contact:  "red-admin@example.com",
   158  					},
   159  					{
   160  						Name:     "blue",
   161  						Location: "gs://example/blue-team/config",
   162  						Path:     newPathOrDie("gs://example/blue-team/config"),
   163  						Contact:  "blue.team.contact@example.com",
   164  					},
   165  				},
   166  			},
   167  		},
   168  		{
   169  			name: "Target is invalid path",
   170  			input: []byte(`target: "foo://config"
   171  sources:
   172  - name: "red"
   173    location: "gs://example/red-team/config"
   174    contact: "red-admin@example.com"
   175  - name: "blue"
   176    location: "gs://example/blue-team/config"
   177    contact: "blue.team.contact@example.com"`),
   178  			expectError: true,
   179  		},
   180  		{
   181  			name: "Source contains a local filesystem path",
   182  			input: []byte(`target: "gs://path/to/write/config"
   183  sources:
   184  - name: "red"
   185    location: "/tmp/config"
   186    contact: "red-admin@example.com"
   187  - name: "blue"
   188    location: "file://example/blue-team/config"
   189    contact: "blue.team.contact@example.com"`),
   190  			expectedList: MergeList{
   191  				Target: "gs://path/to/write/config",
   192  				Path:   newPathOrDie("gs://path/to/write/config"),
   193  				Sources: []Source{
   194  					{
   195  						Name:     "red",
   196  						Location: "/tmp/config",
   197  						Path:     newPathOrDie("/tmp/config"),
   198  						Contact:  "red-admin@example.com",
   199  					},
   200  					{
   201  						Name:     "blue",
   202  						Location: "file://example/blue-team/config",
   203  						Path:     newPathOrDie("file://example/blue-team/config"),
   204  						Contact:  "blue.team.contact@example.com",
   205  					},
   206  				},
   207  			},
   208  		},
   209  		{
   210  			name: "Source contains an invalid path, returns error",
   211  			input: []byte(`target: "gs://path/to/write/config"
   212  sources:
   213  - name: "red"
   214    location: "foo://config"
   215    contact: "red-admin@example.com"
   216  - name: "blue"
   217    location: "gs://example/blue-team/config"
   218    contact: "blue.team.contact@example.com"`),
   219  			expectError: true,
   220  		},
   221  		{
   222  			name: "Contains a duplicated name, returns error",
   223  			input: []byte(`target: "gs://path/to/write/config"
   224  sources:
   225  - name: "red"
   226    location: "gs://example/red-team/config"
   227  - name: "red"
   228    location: "gs://example/new-red-team/config"`),
   229  			expectError: true,
   230  		},
   231  	}
   232  
   233  	for _, test := range tc {
   234  		t.Run(test.name, func(t *testing.T) {
   235  			resultList, err := ParseAndCheck(test.input)
   236  			if test.expectError {
   237  				if err == nil {
   238  					t.Fatal("Expected error, but got none")
   239  				}
   240  				return
   241  			}
   242  			if err != nil {
   243  				t.Errorf("Unexpected error %v", err)
   244  			}
   245  			if diff := cmp.Diff(test.expectedList, resultList, cmp.AllowUnexported(gcs.Path{})); diff != "" {
   246  				t.Errorf("ParseAndCheck(%q) differed (-got, +want): %s", test.input, diff)
   247  			}
   248  		})
   249  	}
   250  }
   251  
   252  func Test_MergeAndUpdate(t *testing.T) {
   253  	cases := []struct {
   254  		name                string
   255  		paths               map[string]*gcs.Path
   256  		uploadInjectedError error
   257  		skipValidate        bool
   258  		confirm             bool
   259  		expectError         bool
   260  		expectUpload        bool
   261  	}{
   262  		{
   263  			name:        "No paths to read from; fails",
   264  			confirm:     true,
   265  			expectError: true,
   266  		},
   267  		{
   268  			name: "Intended upload; succeeds",
   269  			paths: map[string]*gcs.Path{
   270  				"first": newPathOrDie("gs://valid/config"),
   271  			},
   272  			confirm:      true,
   273  			expectUpload: true,
   274  		},
   275  		{
   276  			name: "Given nil path; fails",
   277  			paths: map[string]*gcs.Path{
   278  				"first":  newPathOrDie("gs://valid/config"),
   279  				"second": nil,
   280  			},
   281  			confirm:     true,
   282  			expectError: true,
   283  		},
   284  		{
   285  			name: "Open fails; fails",
   286  			paths: map[string]*gcs.Path{
   287  				"first":  newPathOrDie("gs://valid/config"),
   288  				"second": newPathOrDie("gs://read/error"),
   289  			},
   290  			confirm:     true,
   291  			expectError: true,
   292  		},
   293  		{
   294  			name: "Validate fails; skips and succeeds",
   295  			paths: map[string]*gcs.Path{
   296  				"first":  newPathOrDie("gs://valid/config"),
   297  				"second": newPathOrDie("gs://invalid/config"),
   298  			},
   299  			confirm:      true,
   300  			expectUpload: true,
   301  		},
   302  		{
   303  			name: "Validate fails for all targets; fails",
   304  			paths: map[string]*gcs.Path{
   305  				"first": newPathOrDie("gs://invalid/config"),
   306  			},
   307  			confirm:     true,
   308  			expectError: true,
   309  		},
   310  		{
   311  			name: "Upload fails; fails",
   312  			paths: map[string]*gcs.Path{
   313  				"first": newPathOrDie("gs://valid/config"),
   314  			},
   315  			uploadInjectedError: errors.New("upload error"),
   316  			confirm:             true,
   317  			expectError:         true,
   318  		},
   319  		{
   320  			name: "no-confirm; succeeds with no upload",
   321  			paths: map[string]*gcs.Path{
   322  				"first": newPathOrDie("gs://valid/config"),
   323  			},
   324  		},
   325  		{
   326  			name: "skip-validate with invalid proto; succeeds",
   327  			paths: map[string]*gcs.Path{
   328  				"second": newPathOrDie("gs://invalid/config"),
   329  			},
   330  			skipValidate: true,
   331  			confirm:      true,
   332  			expectUpload: true,
   333  		},
   334  	}
   335  
   336  	for _, tc := range cases {
   337  		existingData := fakeOpener{
   338  			"gs://valid/config": configInFake(&configpb.Configuration{
   339  				Dashboards: []*configpb.Dashboard{
   340  					{
   341  						Name: "dash_1",
   342  						DashboardTab: []*configpb.DashboardTab{
   343  							{
   344  								Name:          "tab_1",
   345  								TestGroupName: "test_group_1",
   346  							},
   347  						},
   348  					},
   349  				},
   350  				TestGroups: []*configpb.TestGroup{
   351  					{
   352  						Name:             "test_group_1",
   353  						GcsPrefix:        "tests_live_here",
   354  						DaysOfResults:    1,
   355  						NumColumnsRecent: 1,
   356  					},
   357  				},
   358  			}),
   359  			"gs://invalid/config": configInFake(&configpb.Configuration{
   360  				Dashboards: []*configpb.Dashboard{
   361  					{Name: "dash_1"},
   362  					{Name: "dash_1"},
   363  				},
   364  			}),
   365  			"gs://read/error": fakeObject{
   366  				err: errors.New("read error"),
   367  			},
   368  		}
   369  
   370  		t.Run(tc.name, func(t *testing.T) {
   371  			client := fakeMergeClient{
   372  				fakeUploader: fakeUploader{
   373  					err: tc.uploadInjectedError,
   374  				},
   375  				fakeOpener: existingData,
   376  			}
   377  
   378  			mergeList := MergeList{
   379  				Target:  "gs://result/config",
   380  				Path:    newPathOrDie("gs://result/config"),
   381  				Sources: nil,
   382  			}
   383  
   384  			for name, path := range tc.paths {
   385  				mergeList.Sources = append(mergeList.Sources, Source{
   386  					Name: name,
   387  					Path: path,
   388  				})
   389  			}
   390  
   391  			_, resultErr := MergeAndUpdate(context.Background(), &client, nil, mergeList, tc.skipValidate, tc.confirm)
   392  
   393  			if tc.expectUpload && !client.uploaded {
   394  				t.Errorf("Expected upload, but there was none")
   395  			}
   396  
   397  			if !tc.expectUpload && client.uploaded {
   398  				t.Errorf("Unexpected upload")
   399  			}
   400  
   401  			if tc.expectError && resultErr == nil {
   402  				t.Errorf("Expected error, but got none")
   403  			}
   404  
   405  			if !tc.expectError && resultErr != nil {
   406  				t.Errorf("Unexpected error %v", resultErr)
   407  			}
   408  		})
   409  	}
   410  }
   411  func Test_RecordLastModified(t *testing.T) {
   412  	cases := []struct {
   413  		name          string
   414  		attrs         *storage.ReaderObjectAttrs
   415  		mets          *Metrics
   416  		source        string
   417  		expectAtLeast int64
   418  	}{
   419  		{
   420  			name:   "nil attrs; succeeds",
   421  			attrs:  nil,
   422  			mets:   &Metrics{},
   423  			source: "fake-source",
   424  		},
   425  		{
   426  			name:   "nil mets; succeeds",
   427  			attrs:  &storage.ReaderObjectAttrs{},
   428  			mets:   nil,
   429  			source: "fake-source",
   430  		},
   431  		{
   432  			name:   "nil; succeeds",
   433  			attrs:  nil,
   434  			mets:   nil,
   435  			source: "",
   436  		}, {
   437  			name: "non-nil mets and attrs; succeeds",
   438  			attrs: &storage.ReaderObjectAttrs{
   439  				LastModified: time.Now().Add(-5 * time.Second),
   440  			},
   441  			mets:          &Metrics{},
   442  			source:        "",
   443  			expectAtLeast: 5,
   444  		},
   445  	}
   446  
   447  	for _, tc := range cases {
   448  		t.Run(tc.name, func(t *testing.T) {
   449  			if tc.attrs == nil || tc.mets == nil {
   450  				recordLastModified(tc.attrs, tc.mets, tc.source)
   451  			} else {
   452  				var fakeMetric fakeInt64
   453  				tc.mets.LastModified = &fakeMetric
   454  				recordLastModified(tc.attrs, tc.mets, tc.source)
   455  				if fakeMetric.last < tc.expectAtLeast {
   456  					t.Errorf("want at least %d, got %d", tc.expectAtLeast, fakeMetric.last)
   457  				}
   458  			}
   459  		})
   460  	}
   461  }
   462  
   463  type fakeInt64 struct {
   464  	set  bool
   465  	last int64
   466  }
   467  
   468  func (f *fakeInt64) Name() string {
   469  	return "fakeInt64"
   470  }
   471  
   472  func (f *fakeInt64) Set(n int64, _ ...string) {
   473  	f.last = n
   474  	f.set = true
   475  }
   476  
   477  type fakeMergeClient struct {
   478  	fakeOpener
   479  	fakeUploader
   480  }
   481  
   482  type fakeOpener map[string]fakeObject
   483  
   484  func (fo fakeOpener) Open(_ context.Context, path gcs.Path) (io.ReadCloser, *storage.ReaderObjectAttrs, error) {
   485  	o, ok := fo[path.String()]
   486  	if !ok {
   487  		return nil, nil, fmt.Errorf("wrap not exist: %w", storage.ErrObjectNotExist)
   488  	}
   489  	if o.err != nil {
   490  		return nil, nil, fmt.Errorf("injected open error: %w", o.err)
   491  	}
   492  	return ioutil.NopCloser(bytes.NewReader(o.buf)), &storage.ReaderObjectAttrs{}, nil
   493  }
   494  
   495  type fakeObject struct {
   496  	buf []byte
   497  	err error
   498  }
   499  
   500  func configInFake(cfg *configpb.Configuration) (fo fakeObject) {
   501  	b, err := proto.Marshal(cfg)
   502  	if err != nil {
   503  		panic(err)
   504  	}
   505  	fo.buf = b
   506  	return
   507  }
   508  
   509  type fakeUploader struct {
   510  	uploaded bool
   511  	err      error
   512  }
   513  
   514  func (fu *fakeUploader) Upload(context.Context, gcs.Path, []byte, bool, string) (*storage.ObjectAttrs, error) {
   515  	if fu.err != nil {
   516  		return nil, fmt.Errorf("injected upload error: %w", fu.err)
   517  	}
   518  	fu.uploaded = true
   519  	return nil, nil
   520  }