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 }