github.com/quantumghost/awgo@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  }