git.deanishe.net/deanishe/awgo.git@v0.15.0/config_bind_test.go (about) 1 // 2 // Copyright (c) 2018 Dean Jackson <deanishe@deanishe.net> 3 // 4 // MIT Licence. See http://opensource.org/licenses/MIT 5 // 6 // Created on 2018-06-30 7 // 8 9 package aw 10 11 import ( 12 "fmt" 13 "log" 14 "os" 15 "reflect" 16 "testing" 17 "time" 18 ) 19 20 // testHost is the tagged struct tests load from and into. 21 type testHost struct { 22 ID string `env:"-"` 23 Hostname string `env:"HOST"` 24 Online bool 25 Port uint 26 Score int 27 FreeSpace int64 `env:"SPACE"` 28 PingInterval time.Duration `env:"PING"` 29 PingAverage float64 30 } 31 32 // The default values for the bind test environment. 33 var ( 34 testID = "uid34" 35 testHostname = "test.example.com" 36 testOnline = true 37 testPort uint = 3000 38 testScore = 10000 39 testFreeSpace int64 = 9876543210 40 testPingInterval = time.Second * 10 41 testPingAverage = 4.5 42 testFieldCount = 7 // Number of visible, non-ignored fields in testHost 43 ) 44 45 // Test bindDest implementation that captures saves. 46 type testDest struct { 47 Cfg *Config 48 Saves map[string]string 49 } 50 51 func (dst *testDest) setMulti(variables map[string]string, export bool) error { 52 53 for k, v := range variables { 54 dst.Saves[k] = v 55 } 56 57 return nil 58 } 59 func (dst *testDest) GetString(key string, fallback ...string) string { 60 return dst.Cfg.GetString(key, fallback...) 61 } 62 63 // Verify checks that dst.Saves has the same content as saves. 64 func (dst *testDest) Verify(saves map[string]string) error { 65 66 var err error 67 68 for k, x := range saves { 69 70 s, ok := dst.Saves[k] 71 if !ok { 72 err = fmt.Errorf("Key %s was not set", k) 73 break 74 } 75 76 if s != x { 77 err = fmt.Errorf("Bad %s. Expected=%v, Got=%v", k, x, s) 78 break 79 } 80 81 } 82 83 if err == nil && len(dst.Saves) != len(saves) { 84 err = fmt.Errorf("Different lengths. Expected=%d, Got=%d", len(saves), len(dst.Saves)) 85 } 86 87 if err != nil { 88 log.Printf("Expected=\n%#v\nGot=\n%#v", saves, dst.Saves) 89 } 90 return err 91 } 92 93 // Returns a test implementation of Env 94 func bindTestEnv() MapEnv { 95 96 return MapEnv{ 97 "ID": "not empty", 98 "HOST": testHostname, 99 "ONLINE": fmt.Sprintf("%v", testOnline), 100 "PORT": fmt.Sprintf("%d", testPort), 101 "SCORE": fmt.Sprintf("%d", testScore), 102 "SPACE": fmt.Sprintf("%d", testFreeSpace), 103 "PING": fmt.Sprintf("%s", testPingInterval), 104 "PING_AVERAGE": fmt.Sprintf("%0.1f", testPingAverage), 105 } 106 } 107 108 // TestExtract verifies extraction of struct fields and tags. 109 func TestExtract(t *testing.T) { 110 111 cfg := NewConfig() 112 th := &testHost{} 113 data := map[string]string{ 114 "Hostname": "HOST", 115 "Online": "ONLINE", 116 "Port": "PORT", 117 "Score": "SCORE", 118 "FreeSpace": "SPACE", 119 "PingInterval": "PING", 120 "PingAverage": "PING_AVERAGE", 121 } 122 123 binds, err := extract(th) 124 if err != nil { 125 t.Fatalf("couldn't extract testHost: %v", err) 126 } 127 128 if len(binds) != testFieldCount { 129 t.Errorf("Bad Bindings count. Expected=%d, Got=%d", 130 testFieldCount, len(binds)) 131 } 132 133 x := map[string]string{} 134 for _, bind := range binds { 135 x[bind.Name] = bind.EnvVar 136 } 137 138 if err := verifyMapsEqual(x, data); err != nil { 139 t.Fatalf("extract failed: %v", err) 140 } 141 142 // Field not found error 143 st := struct { 144 Host string 145 Port uint 146 }{} 147 148 binds, err = extract(&st) 149 if err != nil { 150 t.Fatalf("couldn't extract struct: %v", err) 151 } 152 // Change field numbers 153 for _, bind := range binds { 154 bind.FieldNum = bind.FieldNum + 1000 155 } 156 // Fail to load fields 157 for _, bind := range binds { 158 if err := bind.Import(cfg); err == nil { 159 t.Errorf("Accepted bad binding (%s)", bind.Name) 160 } 161 } 162 } 163 164 // TestConfigTo verifies that a struct is correctly populated from a Config. 165 func TestConfigTo(t *testing.T) { 166 167 h := &testHost{} 168 e := bindTestEnv() 169 cfg := NewConfig(e) 170 171 if err := cfg.To(h); err != nil { 172 t.Fatal(err) 173 } 174 175 if h.ID != "" { // ID is ignored 176 t.Errorf("Bad ID. Expected=, Got=%v", h.ID) 177 } 178 179 if h.Hostname != testHostname { 180 t.Errorf("Bad Hostname. Expected=%v, Got=%v", testHostname, h.Hostname) 181 } 182 183 if h.Online != testOnline { 184 t.Errorf("Bad Online. Expected=%v, Got=%v", testOnline, h.Online) 185 } 186 187 if h.Port != testPort { 188 t.Errorf("Bad Port. Expected=%v, Got=%v", testPort, h.Port) 189 } 190 191 if h.Score != testScore { 192 t.Errorf("Bad Score. Expected=%v, Got=%v", testScore, h.Score) 193 } 194 195 if h.FreeSpace != testFreeSpace { 196 t.Errorf("Bad FreeSpace. Expected=%v, Got=%v", testFreeSpace, h.FreeSpace) 197 } 198 199 if h.PingInterval != testPingInterval { 200 t.Errorf("Bad PingInterval. Expected=%v, Got=%v", testPingInterval, h.PingInterval) 201 } 202 203 if h.PingAverage != testPingAverage { 204 t.Errorf("Bad PingAverage. Expected=%v, Got=%v", testPingAverage, h.PingAverage) 205 } 206 207 } 208 209 // TestConfigFrom verifies that a bindDest is correctly populated from a (tagged) struct. 210 func TestConfigFrom(t *testing.T) { 211 212 e := MapEnv{ 213 "ID": "", 214 "HOST": "", 215 "ONLINE": "true", // must be set: "" is the same as false 216 "PORT": "", 217 "SCORE": "", 218 "SPACE": "", 219 "PING": "0s", // zero value 220 "PING_AVERAGE": "0.0", // zero value 221 } 222 223 cfg := NewConfig(e) 224 th := &testHost{} 225 226 // Check Config is set up correctly 227 for k, v := range e { 228 s := cfg.Get(k) 229 if s != v { 230 t.Errorf("Bad %s. Expected=%v, Got=%v", k, v, s) 231 } 232 } 233 234 var ( 235 newHostname = "www.aol.com" 236 newOnline = false 237 newPort uint = 2500 238 newScore = 7602 239 newFreeSpace int64 = 1234567890 240 newPingInterval = time.Minute * 2 241 newPingAverage = 3.3 242 243 // How the testDest should look afterwards 244 one = map[string]string{ 245 "HOST": newHostname, 246 "ONLINE": fmt.Sprintf("%v", newOnline), 247 } 248 two = map[string]string{ 249 "PORT": fmt.Sprintf("%d", newPort), 250 "SCORE": fmt.Sprintf("%d", newScore), 251 "SPACE": fmt.Sprintf("%d", newFreeSpace), 252 } 253 three = map[string]string{ 254 "PING": fmt.Sprintf("%s", newPingInterval), 255 "PING_AVERAGE": fmt.Sprintf("%0.1f", newPingAverage), 256 } 257 ) 258 259 // Exports v into a testDest and verifies it against x. 260 testBind := func(v interface{}, x map[string]string) { 261 262 dst := &testDest{cfg, map[string]string{}} 263 264 variables, err := cfg.bindVars(v) 265 if err != nil { 266 t.Fatal(err) 267 } 268 269 if err := dst.setMulti(variables, false); err != nil { 270 t.Fatal(err) 271 } 272 273 if err := dst.Verify(x); err != nil { 274 t.Errorf("%s", err) 275 } 276 } 277 278 // Expected testDest value 279 x := map[string]string{} 280 281 th.Hostname = newHostname 282 th.Online = newOnline 283 284 for k, v := range one { 285 x[k] = v 286 } 287 testBind(th, x) 288 289 th.Port = newPort 290 th.Score = newScore 291 th.FreeSpace = newFreeSpace 292 293 for k, v := range two { 294 x[k] = v 295 } 296 testBind(th, x) 297 298 th.PingInterval = newPingInterval 299 th.PingAverage = newPingAverage 300 301 for k, v := range three { 302 x[k] = v 303 } 304 testBind(th, x) 305 } 306 307 // TestVarName tests the envvar name algorithm. 308 func TestVarName(t *testing.T) { 309 data := []struct { 310 in, out string 311 }{ 312 {"URL", "URL"}, 313 {"Name", "NAME"}, 314 {"LastName", "LAST_NAME"}, 315 {"URLEncoding", "URL_ENCODING"}, 316 {"LongBeard", "LONG_BEARD"}, 317 {"HTML", "HTML"}, 318 {"etc", "ETC"}, 319 } 320 321 for _, td := range data { 322 v := EnvVarForField(td.in) 323 if v != td.out { 324 t.Errorf("Bad VarName (%s). Expected=%v, Got=%v", 325 td.in, td.out, v) 326 } 327 } 328 } 329 330 // Populate a struct from workflow/environment variables. See EnvVarForField 331 // for information on how fields are mapped to environment variables if 332 // no variable name is specified using an `env:"name"` tag. 333 func ExampleConfig_To() { 334 335 // Set some test values 336 os.Setenv("USERNAME", "dave") 337 os.Setenv("API_KEY", "hunter2") 338 os.Setenv("INTERVAL", "5m") 339 os.Setenv("FORCE", "1") 340 341 // Program settings to load from env 342 type Settings struct { 343 Username string 344 APIKey string 345 UpdateInterval time.Duration `env:"INTERVAL"` 346 Force bool 347 } 348 349 var ( 350 s = &Settings{} 351 cfg = NewConfig() 352 ) 353 354 // Populate Settings from workflow/environment variables 355 if err := cfg.To(s); err != nil { 356 panic(err) 357 } 358 359 fmt.Println(s.Username) 360 fmt.Println(s.APIKey) 361 fmt.Println(s.UpdateInterval) 362 fmt.Println(s.Force) 363 364 // Output: 365 // dave 366 // hunter2 367 // 5m0s 368 // true 369 370 unsetEnv( 371 "USERNAME", 372 "API_KEY", 373 "INTERVAL", 374 "FORCE", 375 ) 376 } 377 378 // Rules for generating an environment variable name from a struct field name. 379 func ExampleEnvVarForField() { 380 // Single-case words are upper-cased 381 fmt.Println(EnvVarForField("URL")) 382 fmt.Println(EnvVarForField("name")) 383 // Words that start with fewer than 3 uppercase chars are upper-cased 384 fmt.Println(EnvVarForField("Folder")) 385 fmt.Println(EnvVarForField("MTime")) 386 // But with 3+ uppercase chars, the last is treated as the first 387 // char of the next word 388 fmt.Println(EnvVarForField("VIPath")) 389 fmt.Println(EnvVarForField("URLEncoding")) 390 fmt.Println(EnvVarForField("SSLPort")) 391 // Camel-case words are split on case changes 392 fmt.Println(EnvVarForField("LastName")) 393 fmt.Println(EnvVarForField("LongHorse")) 394 fmt.Println(EnvVarForField("loginURL")) 395 fmt.Println(EnvVarForField("newHomeAddress")) 396 fmt.Println(EnvVarForField("PointA")) 397 // Digits are considered the end of a word, not the start 398 fmt.Println(EnvVarForField("b2B")) 399 400 // Output: 401 // URL 402 // NAME 403 // FOLDER 404 // MTIME 405 // VI_PATH 406 // URL_ENCODING 407 // SSL_PORT 408 // LAST_NAME 409 // LONG_HORSE 410 // LOGIN_URL 411 // NEW_HOME_ADDRESS 412 // POINT_A 413 // B2_B 414 } 415 416 // Verify zero and non-zero values returned by isZeroValue. 417 func TestIsZeroValue(t *testing.T) { 418 419 zero := struct { 420 String string 421 Int int 422 Int8 int8 423 Int16 int16 424 Int32 int32 425 Int64 int64 426 Float32 float32 427 Float64 float64 428 Duration time.Duration 429 NilPointer *time.Time 430 // struct & map not supported 431 // Time time.Time 432 // Map map[string]string 433 }{} 434 nonzero := struct { 435 String string 436 Int int 437 Int8 int8 438 Int16 int16 439 Int32 int32 440 Int64 int64 441 Float32 float32 442 Float64 float64 443 Duration time.Duration 444 Pointer *time.Time 445 Time time.Time 446 Map map[string]string 447 }{ 448 449 String: "word", 450 Int: 5, 451 Int8: 5, 452 Int16: 5, 453 Int32: 5, 454 Int64: 5, 455 Float32: 1.5, 456 Float64: 2.51, 457 Duration: time.Minute * 5, 458 Pointer: &time.Time{}, 459 Time: time.Now(), 460 Map: map[string]string{}, 461 } 462 463 rv := reflect.ValueOf(zero) 464 typ := rv.Type() 465 466 for i := 0; i < rv.NumField(); i++ { 467 468 field := typ.Field(i) 469 value := rv.Field(i) 470 471 v := isZeroValue(value) 472 if v != true { 473 t.Errorf("Bad %s. Expected=true, Got=false", field.Name) 474 } 475 } 476 477 rv = reflect.ValueOf(nonzero) 478 typ = rv.Type() 479 480 for i := 0; i < rv.NumField(); i++ { 481 482 field := typ.Field(i) 483 value := rv.Field(i) 484 485 v := isZeroValue(value) 486 if v == true { 487 t.Errorf("Bad %s. Expected=false, Got=true", field.Name) 488 } 489 } 490 } 491 492 // Verify *string* zero values for other types, e.g. "0" is zero value for int. 493 func TestIsZeroString(t *testing.T) { 494 data := []struct { 495 in string 496 kind reflect.Kind 497 x bool 498 }{ 499 {"", reflect.String, true}, 500 {" ", reflect.String, false}, 501 {"test", reflect.String, false}, 502 // Ints 503 {"", reflect.Int, true}, 504 {"0", reflect.Int, true}, 505 {"0000", reflect.Int, true}, 506 {"", reflect.Int8, true}, 507 {"0", reflect.Int8, true}, 508 {"0000", reflect.Int8, true}, 509 {"", reflect.Int16, true}, 510 {"0", reflect.Int16, true}, 511 {"0000", reflect.Int16, true}, 512 {"", reflect.Int32, true}, 513 {"0", reflect.Int32, true}, 514 {"0000", reflect.Int32, true}, 515 {"", reflect.Int64, true}, 516 {"0", reflect.Int64, true}, 517 {"0000", reflect.Int64, true}, 518 {"1,23", reflect.Int64, false}, 519 {"test", reflect.Int64, false}, 520 // Floats 521 {"", reflect.Float32, true}, 522 {"0", reflect.Float32, true}, 523 {"0.0", reflect.Float32, true}, 524 {"00.00", reflect.Float32, true}, 525 {"1,23", reflect.Float32, false}, 526 {"test", reflect.Float32, false}, 527 {"", reflect.Float64, true}, 528 {"0", reflect.Float64, true}, 529 {"0.0", reflect.Float64, true}, 530 {"00.00", reflect.Float64, true}, 531 {"1,23", reflect.Float64, false}, 532 {"test", reflect.Float64, false}, 533 // Booleans 534 {"", reflect.Bool, true}, 535 {"0", reflect.Bool, true}, 536 {"false", reflect.Bool, true}, 537 {"FALSE", reflect.Bool, true}, 538 {"f", reflect.Bool, true}, 539 {"F", reflect.Bool, true}, 540 {"False", reflect.Bool, true}, 541 // Durations 542 {"0s", reflect.Int64, true}, 543 {"0m0s", reflect.Int64, true}, 544 {"0h0m", reflect.Int64, true}, 545 {"0h0m0s", reflect.Int64, true}, 546 {"1s", reflect.Int64, false}, 547 {"2ms", reflect.Int64, false}, 548 {"1h5m", reflect.Int64, false}, 549 {"96h", reflect.Int64, false}, 550 {"12h", reflect.Int64, false}, 551 } 552 553 for _, td := range data { 554 555 v := isZeroString(td.in, td.kind) 556 if v != td.x { 557 t.Errorf("Bad ZeroString (%s). Expected=%v, Got=%v", td.in, td.x, v) 558 } 559 } 560 } 561 562 func TestIsCamelCase(t *testing.T) { 563 data := []struct { 564 s string 565 v bool 566 }{ 567 {"", false}, 568 {"URL", false}, 569 {"url", false}, 570 {"Url", false}, 571 {"HomeAddress", true}, 572 {"myHomeAddress", true}, 573 {"PlaceA", true}, 574 {"myPlaceB", true}, 575 {"myB", true}, 576 {"my2B", true}, 577 {"B2B", false}, 578 {"SSLPort", true}, 579 } 580 581 for _, td := range data { 582 b := isCamelCase(td.s) 583 if b != td.v { 584 t.Errorf("Bad CamelCase (%s). Expected=%v, Got=%v", td.s, td.v, b) 585 } 586 587 } 588 } 589 590 func TestSplitCamelCase(t *testing.T) { 591 data := []struct { 592 in string 593 out string 594 }{ 595 {"", ""}, 596 {"HomeAddress", "HOME_ADDRESS"}, 597 {"homeAddress", "HOME_ADDRESS"}, 598 {"loginURL", "LOGIN_URL"}, 599 {"SSLPort", "SSL_PORT"}, 600 {"HomeAddress", "HOME_ADDRESS"}, 601 {"myHomeAddress", "MY_HOME_ADDRESS"}, 602 {"PlaceA", "PLACE_A"}, 603 {"myPlaceB", "MY_PLACE_B"}, 604 {"myB", "MY_B"}, 605 {"my2B", "MY2_B"}, 606 {"URLEncoding", "URL_ENCODING"}, 607 } 608 609 for _, td := range data { 610 s := splitCamelCase(td.in) 611 if s != td.out { 612 t.Errorf("Bad SplitCamel (%s). Expected=%v, Got=%v", td.in, td.out, s) 613 } 614 615 } 616 } 617 618 func TestIsBindable(t *testing.T) { 619 620 data := []struct { 621 k reflect.Kind 622 x bool 623 }{ 624 {reflect.Ptr, false}, 625 {reflect.Map, false}, 626 {reflect.Slice, false}, 627 {reflect.Struct, false}, 628 {reflect.String, true}, 629 {reflect.Bool, true}, 630 {reflect.Int, true}, 631 {reflect.Int8, true}, 632 {reflect.Int16, true}, 633 {reflect.Int32, true}, 634 {reflect.Int64, true}, 635 {reflect.Uint, true}, 636 {reflect.Uint8, true}, 637 {reflect.Uint16, true}, 638 {reflect.Uint32, true}, 639 {reflect.Uint64, true}, 640 {reflect.Float32, true}, 641 {reflect.Float64, true}, 642 } 643 644 for _, td := range data { 645 v := isBindable(td.k) 646 if v != td.x { 647 t.Errorf("Bad Bindable for %v. Expected=%v, Got=%v", td.k, td.x, v) 648 } 649 } 650 }