github.com/m3db/m3@v1.5.0/src/x/config/config_test.go (about) 1 // Copyright (c) 2017 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package config 22 23 import ( 24 "errors" 25 "fmt" 26 "io/ioutil" 27 "os" 28 "testing" 29 30 "github.com/stretchr/testify/assert" 31 "github.com/stretchr/testify/require" 32 "go.uber.org/config" 33 ) 34 35 const ( 36 goodConfig = ` 37 listen_address: localhost:4385 38 buffer_space: 1024 39 servers: 40 - server1:8090 41 - server2:8010 42 ` 43 badConfigInvalidKey = ` 44 unknown_key: unknown_key_value 45 listen_address: localhost:4385 46 buffer_space: 1024 47 servers: 48 - server1:8090 49 - server2:8010 50 ` 51 badConfigInvalidValue = ` 52 listen_address: localhost:4385 53 buffer_space: 254 54 servers: 55 - server1:8090 56 - server2:8010 57 ` 58 ) 59 60 type configuration struct { 61 ListenAddress string `yaml:"listen_address" validate:"nonzero"` 62 BufferSpace int `yaml:"buffer_space" validate:"min=255"` 63 Servers []string `validate:"nonzero"` 64 } 65 66 type commitlogPolicyConfiguration struct { 67 FlushMaxBytes int `yaml:"flushMaxBytes" validate:"nonzero"` 68 FlushEvery string `yaml:"flushEvery" validate:"nonzero"` 69 DeprecatedBlockSize int `yaml:"blockSize"` 70 } 71 72 type configurationDeprecated struct { 73 ListenAddress string `yaml:"listen_address" validate:"nonzero"` 74 BufferSpace int `yaml:"buffer_space" validate:"min=255"` 75 Servers []string `validate:"nonzero"` 76 DeprecatedFoo string `yaml:"foo"` 77 DeprecatedBar int `yaml:"bar"` 78 DeprecatedBaz *int `yaml:"baz"` 79 } 80 81 type nestedConfigurationDeprecated struct { 82 ListenAddress string `yaml:"listen_address" validate:"nonzero"` 83 BufferSpace int `yaml:"buffer_space" validate:"min=255"` 84 Servers []string `validate:"nonzero"` 85 CommitLog commitlogPolicyConfiguration `yaml:"commitlog"` 86 } 87 88 type nestedConfigurationMultipleDeprecated struct { 89 ListenAddress string `yaml:"listen_address" validate:"nonzero"` 90 BufferSpace int `yaml:"buffer_space" validate:"min=255"` 91 Servers []string `validate:"nonzero"` 92 CommitLog commitlogPolicyConfiguration `yaml:"commitlog"` 93 DeprecatedMultiple configurationDeprecated `yaml:"multiple"` 94 } 95 96 func TestLoadFile(t *testing.T) { 97 var cfg configuration 98 99 err := LoadFile(&cfg, "./no-config.yaml", Options{}) 100 require.Error(t, err) 101 102 // invalid yaml file 103 err = LoadFile(&cfg, "./config.go", Options{}) 104 require.Error(t, err) 105 106 fname := writeFile(t, goodConfig) 107 defer func() { 108 require.NoError(t, os.Remove(fname)) 109 }() 110 111 err = LoadFile(&cfg, fname, Options{}) 112 require.NoError(t, err) 113 require.Equal(t, "localhost:4385", cfg.ListenAddress) 114 require.Equal(t, 1024, cfg.BufferSpace) 115 require.Equal(t, []string{"server1:8090", "server2:8010"}, cfg.Servers) 116 } 117 118 func TestLoadWithInvalidFile(t *testing.T) { 119 var cfg configuration 120 121 // no file provided 122 err := LoadFiles(&cfg, nil, Options{}) 123 require.Error(t, err) 124 require.Equal(t, errNoFilesToLoad, err) 125 126 // non-exist file provided 127 err = LoadFiles(&cfg, []string{"./no-config.yaml"}, Options{}) 128 require.Error(t, err) 129 130 // invalid yaml file 131 err = LoadFiles(&cfg, []string{"./config.go"}, Options{}) 132 require.Error(t, err) 133 134 fname := writeFile(t, goodConfig) 135 defer func() { 136 require.NoError(t, os.Remove(fname)) 137 }() 138 139 // non-exist file in the file list 140 err = LoadFiles(&cfg, []string{fname, "./no-config.yaml"}, Options{}) 141 require.Error(t, err) 142 143 // invalid file in the file list 144 err = LoadFiles(&cfg, []string{fname, "./config.go"}, Options{}) 145 require.Error(t, err) 146 } 147 148 func TestLoadFileInvalidKey(t *testing.T) { 149 var cfg configuration 150 151 fname := writeFile(t, badConfigInvalidKey) 152 defer func() { 153 require.NoError(t, os.Remove(fname)) 154 }() 155 156 err := LoadFile(&cfg, fname, Options{}) 157 require.Error(t, err) 158 } 159 160 func TestLoadFileInvalidKeyDisableMarshalStrict(t *testing.T) { 161 var cfg configuration 162 163 fname := writeFile(t, badConfigInvalidKey) 164 defer func() { 165 require.NoError(t, os.Remove(fname)) 166 }() 167 168 err := LoadFile(&cfg, fname, Options{DisableUnmarshalStrict: true}) 169 require.NoError(t, err) 170 require.Equal(t, "localhost:4385", cfg.ListenAddress) 171 require.Equal(t, 1024, cfg.BufferSpace) 172 require.Equal(t, []string{"server1:8090", "server2:8010"}, cfg.Servers) 173 } 174 175 func TestLoadFileInvalidValue(t *testing.T) { 176 var cfg configuration 177 178 fname := writeFile(t, badConfigInvalidValue) 179 defer func() { 180 require.NoError(t, os.Remove(fname)) 181 }() 182 183 err := LoadFile(&cfg, fname, Options{}) 184 require.Error(t, err) 185 } 186 187 func TestLoadFileInvalidValueDisableValidate(t *testing.T) { 188 var cfg configuration 189 190 fname := writeFile(t, badConfigInvalidValue) 191 defer func() { 192 require.NoError(t, os.Remove(fname)) 193 }() 194 195 err := LoadFile(&cfg, fname, Options{DisableValidate: true}) 196 require.NoError(t, err) 197 require.Equal(t, "localhost:4385", cfg.ListenAddress) 198 require.Equal(t, 254, cfg.BufferSpace) 199 require.Equal(t, []string{"server1:8090", "server2:8010"}, cfg.Servers) 200 } 201 202 func TestLoadFilesExtends(t *testing.T) { 203 fname := writeFile(t, goodConfig) 204 defer func() { 205 require.NoError(t, os.Remove(fname)) 206 }() 207 208 partialConfig := ` 209 buffer_space: 8080 210 servers: 211 - server3:8080 212 - server4:8080 213 ` 214 partial := writeFile(t, partialConfig) 215 defer func() { 216 require.NoError(t, os.Remove(partial)) 217 }() 218 219 var cfg configuration 220 err := LoadFiles(&cfg, []string{fname, partial}, Options{}) 221 require.NoError(t, err) 222 223 require.Equal(t, "localhost:4385", cfg.ListenAddress) 224 require.Equal(t, 8080, cfg.BufferSpace) 225 require.Equal(t, []string{"server3:8080", "server4:8080"}, cfg.Servers) 226 } 227 228 func TestLoadFilesDeepExtends(t *testing.T) { 229 type innerConfig struct { 230 K1 string `yaml:"k1"` 231 K2 string `yaml:"k2"` 232 } 233 234 type nestedConfig struct { 235 Foo innerConfig `yaml:"foo"` 236 } 237 238 const ( 239 base = ` 240 foo: 241 k1: v1_base 242 k2: v2_base 243 ` 244 override = ` 245 foo: 246 k1: v1_override 247 ` 248 ) 249 250 baseFile, overrideFile := writeFile(t, base), writeFile(t, override) 251 252 var cfg nestedConfig 253 require.NoError(t, LoadFiles(&cfg, []string{baseFile, overrideFile}, Options{})) 254 255 assert.Equal(t, nestedConfig{ 256 Foo: innerConfig{ 257 K1: "v1_override", 258 K2: "v2_base", 259 }, 260 }, cfg) 261 262 } 263 264 func TestLoadFilesValidateOnce(t *testing.T) { 265 const invalidConfig1 = ` 266 listen_address: 267 buffer_space: 256 268 servers: 269 ` 270 271 const invalidConfig2 = ` 272 listen_address: "localhost:8080" 273 servers: 274 - server2:8010 275 ` 276 277 fname1 := writeFile(t, invalidConfig1) 278 defer func() { 279 require.NoError(t, os.Remove(fname1)) 280 }() 281 282 fname2 := writeFile(t, invalidConfig2) 283 defer func() { 284 require.NoError(t, os.Remove(fname2)) 285 }() 286 287 // Either config by itself will not pass validation. 288 var cfg1 configuration 289 err := LoadFiles(&cfg1, []string{fname1}, Options{}) 290 require.Error(t, err) 291 292 var cfg2 configuration 293 err = LoadFiles(&cfg2, []string{fname2}, Options{}) 294 require.Error(t, err) 295 296 // But merging load has no error. 297 var mergedCfg configuration 298 err = LoadFiles(&mergedCfg, []string{fname1, fname2}, Options{}) 299 require.NoError(t, err) 300 301 require.Equal(t, "localhost:8080", mergedCfg.ListenAddress) 302 require.Equal(t, 256, mergedCfg.BufferSpace) 303 require.Equal(t, []string{"server2:8010"}, mergedCfg.Servers) 304 } 305 306 func TestLoadFilesEnvExpansion(t *testing.T) { 307 const withEnv = ` 308 listen_address: localhost:${PORT:8080} 309 buffer_space: ${BUFFER_SPACE} 310 servers: 311 - server2:8010 312 ` 313 314 mapLookup := func(m map[string]string) config.LookupFunc { 315 return func(s string) (string, bool) { 316 r, ok := m[s] 317 return r, ok 318 } 319 } 320 321 newMapLookupOptions := func(m map[string]string) Options { 322 return Options{ 323 Expand: mapLookup(m), 324 } 325 } 326 327 type testCase struct { 328 Name string 329 Options Options 330 Expected configuration 331 ExpectedErr error 332 } 333 334 cases := []testCase{{ 335 Name: "all_provided", 336 Options: newMapLookupOptions(map[string]string{ 337 "PORT": "9090", 338 "BUFFER_SPACE": "256", 339 }), 340 Expected: configuration{ 341 ListenAddress: "localhost:9090", 342 BufferSpace: 256, 343 Servers: []string{"server2:8010"}, 344 }, 345 }, { 346 Name: "missing_no_default", 347 Options: newMapLookupOptions(map[string]string{ 348 "PORT": "9090", 349 // missing BUFFER_SPACE, 350 }), 351 ExpectedErr: errors.New("couldn't expand environment: default is empty for \"BUFFER_SPACE\" (use \"\" for empty string)"), 352 }, { 353 Name: "missing_with_default", 354 Options: newMapLookupOptions(map[string]string{ 355 "BUFFER_SPACE": "256", 356 }), 357 Expected: configuration{ 358 ListenAddress: "localhost:8080", 359 BufferSpace: 256, 360 Servers: []string{"server2:8010"}, 361 }, 362 }} 363 364 doTest := func(t *testing.T, tc testCase) { 365 fname1 := writeFile(t, withEnv) 366 defer func() { 367 require.NoError(t, os.Remove(fname1)) 368 }() 369 var cfg configuration 370 371 err := LoadFile(&cfg, fname1, tc.Options) 372 if tc.ExpectedErr != nil { 373 require.EqualError(t, err, tc.ExpectedErr.Error()) 374 return 375 } 376 377 require.NoError(t, err) 378 assert.Equal(t, tc.Expected, cfg) 379 } 380 381 for _, tc := range cases { 382 t.Run(tc.Name, func(t *testing.T) { 383 doTest(t, tc) 384 }) 385 } 386 387 t.Run("uses os.LookupEnv by default", func(t *testing.T) { 388 curOSLookupEnv := osLookupEnv 389 osLookupEnv = mapLookup(map[string]string{ 390 "PORT": "9090", 391 "BUFFER_SPACE": "256", 392 }) 393 defer func() { 394 osLookupEnv = curOSLookupEnv 395 }() 396 397 doTest(t, testCase{ 398 // use defaults 399 Options: Options{}, 400 Expected: configuration{ 401 ListenAddress: "localhost:9090", 402 BufferSpace: 256, 403 Servers: []string{"server2:8010"}, 404 }, 405 }) 406 }) 407 } 408 409 func TestDeprecationCheck(t *testing.T) { 410 t.Run("StandardConfig", func(t *testing.T) { 411 // OK 412 var cfg configuration 413 fname := writeFile(t, goodConfig) 414 defer func() { 415 require.NoError(t, os.Remove(fname)) 416 }() 417 418 err := LoadFile(&cfg, fname, Options{}) 419 require.NoError(t, err) 420 421 df := []string{} 422 ss := deprecationCheck(cfg, df) 423 require.Len(t, ss, 0) 424 425 // Deprecated 426 badConfig := ` 427 listen_address: localhost:4385 428 buffer_space: 1024 429 servers: 430 - server1:8090 431 - server2:8010 432 foo: ok 433 bar: 42 434 ` 435 var cfg2 configurationDeprecated 436 fname2 := writeFile(t, badConfig) 437 defer func() { 438 require.NoError(t, os.Remove(fname2)) 439 }() 440 441 err = LoadFile(&cfg2, fname2, Options{}) 442 require.NoError(t, err) 443 444 actual := deprecationCheck(cfg2, df) 445 expect := []string{"DeprecatedFoo", "DeprecatedBar"} 446 require.Equal(t, len(expect), len(actual), 447 fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual)) 448 require.Equal(t, expect, actual, 449 fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual)) 450 }) 451 452 t.Run("DeprecatedZeroValue", func(t *testing.T) { 453 // OK 454 var cfg configuration 455 fname := writeFile(t, goodConfig) 456 defer func() { 457 require.NoError(t, os.Remove(fname)) 458 }() 459 460 err := LoadFile(&cfg, fname, Options{}) 461 require.NoError(t, err) 462 463 df := []string{} 464 ss := deprecationCheck(cfg, df) 465 require.Equal(t, 0, len(ss)) 466 467 // Deprecated zero value should be ok and not printed 468 badConfig := ` 469 listen_address: localhost:4385 470 buffer_space: 1024 471 servers: 472 - server1:8090 473 - server2:8010 474 foo: ok 475 bar: 42 476 baz: null 477 ` 478 var cfg2 configurationDeprecated 479 fname2 := writeFile(t, badConfig) 480 defer func() { 481 require.NoError(t, os.Remove(fname2)) 482 }() 483 484 err = LoadFile(&cfg2, fname2, Options{}) 485 require.NoError(t, err) 486 487 actual := deprecationCheck(cfg2, df) 488 expect := []string{"DeprecatedFoo", "DeprecatedBar"} 489 require.Equal(t, len(expect), len(actual), 490 fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual)) 491 require.Equal(t, expect, actual, 492 fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual)) 493 }) 494 495 t.Run("DeprecatedNilValue", func(t *testing.T) { 496 // OK 497 var cfg configuration 498 fname := writeFile(t, goodConfig) 499 defer func() { 500 require.NoError(t, os.Remove(fname)) 501 }() 502 503 err := LoadFile(&cfg, fname, Options{}) 504 require.NoError(t, err) 505 506 df := []string{} 507 ss := deprecationCheck(cfg, df) 508 require.Equal(t, 0, len(ss)) 509 510 // Deprecated nil/unset value should be ok and not printed 511 validConfig := ` 512 listen_address: localhost:4385 513 buffer_space: 1024 514 servers: 515 - server1:8090 516 - server2:8010 517 ` 518 var cfg2 configurationDeprecated 519 fname2 := writeFile(t, validConfig) 520 defer func() { 521 require.NoError(t, os.Remove(fname2)) 522 }() 523 524 err = LoadFile(&cfg2, fname2, Options{}) 525 require.NoError(t, err) 526 527 actual := deprecationCheck(cfg2, df) 528 require.Equal(t, 0, len(actual), 529 fmt.Sprintf("expect %#v should be equal actual %#v", 0, actual)) 530 }) 531 532 t.Run("NestedConfig", func(t *testing.T) { 533 // Single Deprecation 534 var cfg nestedConfigurationDeprecated 535 nc := ` 536 listen_address: localhost:4385 537 buffer_space: 1024 538 servers: 539 - server1:8090 540 - server2:8010 541 commitlog: 542 flushMaxBytes: 42 543 flushEvery: second 544 blockSize: 23 545 ` 546 fname := writeFile(t, nc) 547 defer func() { 548 require.NoError(t, os.Remove(fname)) 549 }() 550 551 err := LoadFile(&cfg, fname, Options{}) 552 require.NoError(t, err) 553 554 df := []string{} 555 actual := deprecationCheck(cfg, df) 556 expect := []string{"DeprecatedBlockSize"} 557 require.Equal(t, len(expect), len(actual), 558 fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual)) 559 require.Equal(t, expect, actual, 560 fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual)) 561 562 // Multiple deprecation 563 var cfg2 nestedConfigurationMultipleDeprecated 564 nc = ` 565 listen_address: localhost:4385 566 buffer_space: 1024 567 servers: 568 - server1:8090 569 - server2:8010 570 commitlog: 571 flushMaxBytes: 42 572 flushEvery: second 573 multiple: 574 listen_address: localhost:4385 575 buffer_space: 1024 576 servers: 577 - server1:8090 578 - server2:8010 579 foo: ok 580 bar: 42 581 ` 582 583 fname2 := writeFile(t, nc) 584 defer func() { 585 require.NoError(t, os.Remove(fname2)) 586 }() 587 588 err = LoadFile(&cfg2, fname2, Options{}) 589 require.NoError(t, err) 590 591 df = []string{} 592 actual = deprecationCheck(cfg2, df) 593 expect = []string{ 594 "DeprecatedMultiple", 595 "DeprecatedFoo", 596 "DeprecatedBar", 597 } 598 require.Equal(t, len(expect), len(actual), 599 fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual)) 600 require.True(t, slicesContainSameStrings(expect, actual), 601 fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual)) 602 }) 603 } 604 605 func slicesContainSameStrings(s1, s2 []string) bool { 606 if len(s1) != len(s2) { 607 return false 608 } 609 610 m := make(map[string]bool, len(s1)) 611 for _, v := range s1 { 612 m[v] = true 613 } 614 for _, v := range s2 { 615 if _, ok := m[v]; !ok { 616 return false 617 } 618 } 619 return true 620 } 621 622 func writeFile(t *testing.T, contents string) string { 623 f, err := ioutil.TempFile("", "configtest") 624 require.NoError(t, err) 625 626 defer f.Close() 627 628 _, err = f.Write([]byte(contents)) 629 require.NoError(t, err) 630 631 return f.Name() 632 }