github.com/GoogleCloudPlatform/testgrid@v0.0.174/config/snapshot/config_snapshot_test.go (about)

     1  /*
     2  Copyright 2022 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 snapshot
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"sync"
    23  	"testing"
    24  	"time"
    25  
    26  	"cloud.google.com/go/storage"
    27  	configpb "github.com/GoogleCloudPlatform/testgrid/pb/config"
    28  	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
    29  	"github.com/GoogleCloudPlatform/testgrid/util/gcs/fake"
    30  	"github.com/google/go-cmp/cmp"
    31  	"google.golang.org/protobuf/proto"
    32  	"google.golang.org/protobuf/testing/protocmp"
    33  )
    34  
    35  func TestObserve_OnInit(t *testing.T) {
    36  	tests := []struct {
    37  		name             string
    38  		config           *configpb.Configuration
    39  		configGeneration int64
    40  		gcsErr           error
    41  		expectInitial    *configpb.Dashboard
    42  		expectError      bool
    43  	}{
    44  		{
    45  			name: "Reads configs",
    46  			config: &configpb.Configuration{
    47  				Dashboards: []*configpb.Dashboard{
    48  					{
    49  						Name: "dashboard",
    50  					},
    51  				},
    52  			},
    53  			configGeneration: 1,
    54  			expectInitial: &configpb.Dashboard{
    55  				Name: "dashboard",
    56  			},
    57  		},
    58  		{
    59  			name:        "Returns error if config isn't present at startup",
    60  			gcsErr:      errors.New("file missing"),
    61  			expectError: true,
    62  		},
    63  	}
    64  
    65  	path, err := gcs.NewPath("gs://config/example")
    66  	if err != nil {
    67  		t.Fatal("could not path")
    68  	}
    69  
    70  	for _, test := range tests {
    71  		t.Run(test.name, func(t *testing.T) {
    72  			ctx, cancel := context.WithCancel(context.Background())
    73  			defer cancel()
    74  
    75  			client := fakeClient()
    76  			client.Opener.Paths[*path] = fake.Object{
    77  				Data: string(mustMarshalConfig(test.config)),
    78  				Attrs: &storage.ReaderObjectAttrs{
    79  					Generation: test.configGeneration,
    80  				},
    81  				ReadErr: test.gcsErr,
    82  			}
    83  			client.Stater[*path] = fake.Stat{
    84  				Attrs: storage.ObjectAttrs{
    85  					Generation: test.configGeneration,
    86  				},
    87  				Err: test.gcsErr,
    88  			}
    89  
    90  			snaps, err := Observe(ctx, nil, client, *path, nil)
    91  
    92  			if err != nil {
    93  				if !test.expectError {
    94  					t.Errorf("got unexpected error: %v", err)
    95  				}
    96  				return
    97  			}
    98  
    99  			select {
   100  			case cs := <-snaps:
   101  				if result := cs.Dashboards["dashboard"]; !proto.Equal(result, test.expectInitial) {
   102  					t.Errorf("got dashboard %v, expected %v", result, test.expectInitial)
   103  				}
   104  			case <-time.After(5 * time.Second):
   105  				t.Error("expected an initial snapshot, but got none")
   106  			}
   107  		})
   108  	}
   109  }
   110  
   111  func TestObserve_OnInitRetry(t *testing.T) {
   112  	tests := []struct {
   113  		name             string
   114  		config           *configpb.Configuration
   115  		configGeneration int64
   116  		openErr          error
   117  		openOnRetry      bool
   118  		expectInitial    *configpb.Dashboard
   119  		expectError      bool
   120  	}{
   121  		{
   122  			name: "Reads config on retry",
   123  			config: &configpb.Configuration{
   124  				Dashboards: []*configpb.Dashboard{
   125  					{
   126  						Name: "dashboard",
   127  					},
   128  				},
   129  			},
   130  			openErr:     errors.New("fake error"),
   131  			openOnRetry: true,
   132  			expectInitial: &configpb.Dashboard{
   133  				Name: "dashboard",
   134  			},
   135  		},
   136  		{
   137  			name:        "Returns error if config isn't present on retry",
   138  			openErr:     errors.New("fake error"),
   139  			expectError: true,
   140  		},
   141  	}
   142  
   143  	path, err := gcs.NewPath("gs://config/example")
   144  	if err != nil {
   145  		t.Fatal("could not path")
   146  	}
   147  
   148  	for _, test := range tests {
   149  		t.Run(test.name, func(t *testing.T) {
   150  			ctx, cancel := context.WithCancel(context.Background())
   151  			defer cancel()
   152  
   153  			client := fakeClient()
   154  			client.Opener.Paths[*path] = fake.Object{
   155  				Data: string(mustMarshalConfig(test.config)),
   156  				Attrs: &storage.ReaderObjectAttrs{
   157  					Generation: 1,
   158  				},
   159  				OpenErr:     test.openErr,
   160  				OpenOnRetry: test.openOnRetry,
   161  			}
   162  			client.Stater[*path] = fake.Stat{
   163  				Attrs: storage.ObjectAttrs{
   164  					Generation: 1,
   165  				},
   166  			}
   167  
   168  			snaps, err := Observe(ctx, nil, client, *path, nil)
   169  
   170  			if !test.expectError && err != nil {
   171  				t.Errorf("Observe() got unexpected error: %v", err)
   172  			} else if test.expectError && err == nil {
   173  				t.Errorf("Observe() did not error as expected.")
   174  			}
   175  
   176  			if test.expectInitial == nil {
   177  				return
   178  			}
   179  
   180  			select {
   181  			case cs := <-snaps:
   182  				if result := cs.Dashboards["dashboard"]; !proto.Equal(result, test.expectInitial) {
   183  					t.Errorf("got dashboard %v, expected %v", result, test.expectInitial)
   184  				}
   185  			case <-time.After(30 * time.Second):
   186  				t.Error("expected an initial snapshot, but got none")
   187  			}
   188  		})
   189  	}
   190  }
   191  
   192  func TestObserve_OnTick(t *testing.T) {
   193  	tests := []struct {
   194  		name             string
   195  		config           *configpb.Configuration
   196  		configGeneration int64
   197  		gcsErr           error
   198  		expectDashboard  *configpb.Dashboard
   199  	}{
   200  		{
   201  			name: "Reads new configs",
   202  			config: &configpb.Configuration{
   203  				Dashboards: []*configpb.Dashboard{
   204  					{
   205  						Name: "dashboard",
   206  					},
   207  				},
   208  			},
   209  			configGeneration: 2,
   210  			expectDashboard: &configpb.Dashboard{
   211  				Name: "dashboard",
   212  			},
   213  		},
   214  		{
   215  			name: "Does not snapshot if generation match",
   216  			config: &configpb.Configuration{
   217  				Dashboards: []*configpb.Dashboard{
   218  					{
   219  						Name: "dashboard",
   220  					},
   221  				},
   222  			},
   223  			configGeneration: 1,
   224  		},
   225  		{
   226  			name:             "Handles read error",
   227  			configGeneration: 2,
   228  			gcsErr:           errors.New("reading fails after init"),
   229  		},
   230  	}
   231  
   232  	path, err := gcs.NewPath("gs://config/example")
   233  	if err != nil {
   234  		t.Fatal("could not path")
   235  	}
   236  
   237  	initialConfig := &configpb.Configuration{
   238  		Dashboards: []*configpb.Dashboard{
   239  			{
   240  				Name: "old-dashboard",
   241  			},
   242  		},
   243  	}
   244  
   245  	now := time.Now()
   246  
   247  	for _, test := range tests {
   248  		t.Run(test.name, func(t *testing.T) {
   249  			ctx, cancel := context.WithCancel(context.Background())
   250  			defer cancel()
   251  
   252  			client := fakeClient()
   253  			client.Opener.Paths[*path] = fake.Object{
   254  				Data: string(mustMarshalConfig(initialConfig)),
   255  				Attrs: &storage.ReaderObjectAttrs{
   256  					Generation: 1,
   257  				},
   258  			}
   259  			client.Stater[*path] = fake.Stat{
   260  				Attrs: storage.ObjectAttrs{
   261  					Generation: 1,
   262  				},
   263  			}
   264  
   265  			ticker := make(chan time.Time)
   266  			defer close(ticker)
   267  			snaps, err := Observe(ctx, nil, client, *path, ticker)
   268  			if err != nil {
   269  				t.Fatalf("error in initial observe: %v", err)
   270  			}
   271  			<-snaps
   272  
   273  			// Change the config
   274  			client.Opener.Paths[*path] = fake.Object{
   275  				Data: string(mustMarshalConfig(test.config)),
   276  				Attrs: &storage.ReaderObjectAttrs{
   277  					Generation: test.configGeneration,
   278  				},
   279  				ReadErr: test.gcsErr,
   280  			}
   281  			client.Stater[*path] = fake.Stat{
   282  				Attrs: storage.ObjectAttrs{
   283  					Generation: test.configGeneration,
   284  				},
   285  				Err: test.gcsErr,
   286  			}
   287  
   288  			ticker <- now
   289  
   290  			if test.expectDashboard != nil {
   291  				select {
   292  				case cs := <-snaps:
   293  					if result := cs.Dashboards["dashboard"]; !proto.Equal(result, test.expectDashboard) {
   294  						t.Errorf("got dashboard %v, expected %v", result, test.expectDashboard)
   295  					}
   296  				case <-time.After(3 * time.Second):
   297  					t.Error("expected a snapshot after tick, but got none")
   298  				}
   299  			} else {
   300  				select {
   301  				case cs := <-snaps:
   302  					t.Errorf("did not expect a snapshot, but got %v", cs)
   303  				case <-time.After(3 * time.Second):
   304  				}
   305  			}
   306  		})
   307  	}
   308  }
   309  
   310  func TestObserve_Data(t *testing.T) {
   311  	tests := []struct {
   312  		name     string
   313  		config   *configpb.Configuration
   314  		expected *Config
   315  	}{
   316  		{
   317  			name: "Empty config",
   318  			expected: &Config{
   319  				DashboardGroups: map[string]*configpb.DashboardGroup{},
   320  				Dashboards:      map[string]*configpb.Dashboard{},
   321  				Groups:          map[string]*configpb.TestGroup{},
   322  				Attrs: storage.ReaderObjectAttrs{
   323  					Generation: 1,
   324  				},
   325  			},
   326  		},
   327  		{
   328  			name: "Dashboards and TestGroups",
   329  			config: &configpb.Configuration{
   330  				Dashboards: []*configpb.Dashboard{
   331  					{
   332  						Name:       "chess",
   333  						DefaultTab: "Ke5",
   334  					},
   335  					{
   336  						Name:       "checkers",
   337  						DefaultTab: "10-15",
   338  					},
   339  				},
   340  				TestGroups: []*configpb.TestGroup{
   341  					{
   342  						Name:          "king",
   343  						DaysOfResults: 17,
   344  					},
   345  					{
   346  						Name:          "pawn",
   347  						DaysOfResults: 1,
   348  					},
   349  				},
   350  			},
   351  			expected: &Config{
   352  				DashboardGroups: map[string]*configpb.DashboardGroup{},
   353  				Dashboards: map[string]*configpb.Dashboard{
   354  					"chess": {
   355  						Name:       "chess",
   356  						DefaultTab: "Ke5",
   357  					},
   358  					"checkers": {
   359  						Name:       "checkers",
   360  						DefaultTab: "10-15",
   361  					},
   362  				},
   363  				Groups: map[string]*configpb.TestGroup{
   364  					"king": {
   365  						Name:          "king",
   366  						DaysOfResults: 17,
   367  					},
   368  					"pawn": {
   369  						Name:          "pawn",
   370  						DaysOfResults: 1,
   371  					},
   372  				},
   373  				Attrs: storage.ReaderObjectAttrs{
   374  					Generation: 1,
   375  				},
   376  			},
   377  		},
   378  		{
   379  			name: "Dashboards and DashboardGroups",
   380  			config: &configpb.Configuration{
   381  				DashboardGroups: []*configpb.DashboardGroup{
   382  					{
   383  						Name:           "games",
   384  						DashboardNames: []string{"chess", "checkers"},
   385  					},
   386  				},
   387  				Dashboards: []*configpb.Dashboard{
   388  					{
   389  						Name:       "chess",
   390  						DefaultTab: "Ke5",
   391  					},
   392  					{
   393  						Name:       "checkers",
   394  						DefaultTab: "10-15",
   395  					},
   396  				},
   397  			},
   398  			expected: &Config{
   399  				DashboardGroups: map[string]*configpb.DashboardGroup{
   400  					"games": {
   401  						Name:           "games",
   402  						DashboardNames: []string{"chess", "checkers"},
   403  					},
   404  				},
   405  				Dashboards: map[string]*configpb.Dashboard{
   406  					"chess": {
   407  						Name:       "chess",
   408  						DefaultTab: "Ke5",
   409  					},
   410  					"checkers": {
   411  						Name:       "checkers",
   412  						DefaultTab: "10-15",
   413  					},
   414  				},
   415  				Groups: map[string]*configpb.TestGroup{},
   416  				Attrs: storage.ReaderObjectAttrs{
   417  					Generation: 1,
   418  				},
   419  			},
   420  		},
   421  	}
   422  
   423  	path, err := gcs.NewPath("gs://config/example")
   424  	if err != nil {
   425  		t.Fatal("could not path")
   426  	}
   427  
   428  	for _, test := range tests {
   429  		t.Run(test.name, func(t *testing.T) {
   430  			ctx, cancel := context.WithCancel(context.Background())
   431  			defer cancel()
   432  
   433  			client := fakeClient()
   434  			client.Opener.Paths[*path] = fake.Object{
   435  				Data: string(mustMarshalConfig(test.config)),
   436  				Attrs: &storage.ReaderObjectAttrs{
   437  					Generation: 1,
   438  				},
   439  			}
   440  			client.Stater[*path] = fake.Stat{
   441  				Attrs: storage.ObjectAttrs{
   442  					Generation: 1,
   443  				},
   444  			}
   445  
   446  			snaps, err := Observe(ctx, nil, client, *path, nil)
   447  			if err != nil {
   448  				t.Fatalf("error in initial observe: %v", err)
   449  			}
   450  
   451  			select {
   452  			case cs := <-snaps:
   453  				if diff := cmp.Diff(test.expected, cs, protocmp.Transform()); diff != "" {
   454  					t.Errorf("(-want +got): %v", diff)
   455  				}
   456  			case <-time.After(5 * time.Second):
   457  				t.Error("expected an initial snapshot, but got none")
   458  			}
   459  
   460  		})
   461  	}
   462  }
   463  
   464  func mustMarshalConfig(c *configpb.Configuration) []byte {
   465  	b, err := proto.Marshal(c)
   466  	if err != nil {
   467  		panic(err)
   468  	}
   469  	return b
   470  }
   471  
   472  func fakeClient() *fake.ConditionalClient {
   473  	return &fake.ConditionalClient{
   474  		UploadClient: fake.UploadClient{
   475  			Uploader: fake.Uploader{},
   476  			Client: fake.Client{
   477  				Lister: fake.Lister{},
   478  				Opener: fake.Opener{
   479  					Paths: map[gcs.Path]fake.Object{},
   480  					Lock:  &sync.RWMutex{},
   481  				},
   482  			},
   483  			Stater: fake.Stater{},
   484  		},
   485  		Lock: &sync.RWMutex{},
   486  	}
   487  }