From cdd0f1aeb2afa4509a1143e29cdfb585ac37cad3 Mon Sep 17 00:00:00 2001 From: Milad Abbasi Date: Tue, 12 Jan 2021 17:37:37 +0330 Subject: [PATCH] Add tests for NewInput function --- errors.go | 12 +- go.mod | 3 + go.sum | 15 ++ input.go | 20 +-- input_test.go | 437 ++++++++++++++++++++++++++++++++++++++++++++++++++ tags.go | 7 +- 6 files changed, 477 insertions(+), 17 deletions(-) create mode 100644 input_test.go diff --git a/errors.go b/errors.go index b102ea5..9b5855f 100644 --- a/errors.go +++ b/errors.go @@ -39,16 +39,20 @@ const ( // An InvalidInputError describes an invalid argument passed to Into function // The argument must be a non-nil struct pointer type InvalidInputError struct { - Type reflect.Type Value reflect.Value } func (e *InvalidInputError) Error() string { msg := "gonfig: invalid input: " + var t reflect.Type - if e.Type == nil { - msg += "nil" - } else if e.Type.Kind() != reflect.Ptr { + if e.Value.IsValid() { + t = e.Value.Type() + } + + if t == nil { + msg += "" + } else if t.Kind() != reflect.Ptr { msg += "non-pointer type" } else if e.Value.IsNil() { msg += "nil pointer" diff --git a/go.mod b/go.mod index 0536672..0e33b78 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,9 @@ go 1.15 require ( github.com/BurntSushi/toml v0.3.1 + github.com/davecgh/go-spew v1.1.1 // indirect github.com/joho/godotenv v1.3.0 + github.com/stretchr/testify v1.6.1 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 635f88a..6035769 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,22 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/input.go b/input.go index 19a8e6a..0427e1c 100644 --- a/input.go +++ b/input.go @@ -39,7 +39,7 @@ type Field struct { func NewInput(i interface{}) (*Input, error) { v := reflect.ValueOf(i) - if err := checkInput(v); err != nil { + if err := validateInput(v); err != nil { return nil, err } @@ -52,21 +52,21 @@ func NewInput(i interface{}) (*Input, error) { Tags: &ConfigTags{}, } - if err := in.traverseFiled(&f); err != nil { + if err := in.traverseField(&f); err != nil { return nil, err } return &in, nil } -// checkInput checks for a non-nil struct pointer -func checkInput(v reflect.Value) error { - if v.Type() == nil || +// validateInput checks for a non-nil struct pointer +func validateInput(v reflect.Value) error { + if !v.IsValid() || + v.Type() == nil || v.Type().Kind() != reflect.Ptr || v.IsNil() || v.Type().Elem().Kind() != reflect.Struct { return &InvalidInputError{ - Type: v.Type(), Value: v, } } @@ -74,8 +74,8 @@ func checkInput(v reflect.Value) error { return nil } -// traverseFiled recursively traverse all fields and collect their information -func (in *Input) traverseFiled(f *Field) error { +// traverseField recursively traverse all fields and collect their information +func (in *Input) traverseField(f *Field) error { if !f.Value.CanSet() || f.Tags.Ignore { return nil } @@ -95,7 +95,7 @@ func (in *Input) traverseFiled(f *Field) error { Path: append(f.Path, f.Value.Type().Field(i).Name), } - if err := in.traverseFiled(&nestedField); err != nil { + if err := in.traverseField(&nestedField); err != nil { return err } } @@ -110,7 +110,7 @@ func (in *Input) traverseFiled(f *Field) error { Path: f.Path, } - return in.traverseFiled(&pointedField) + return in.traverseField(&pointedField) case reflect.Slice, reflect.Array: switch f.Value.Type().Elem().Kind() { diff --git a/input_test.go b/input_test.go new file mode 100644 index 0000000..bf00c38 --- /dev/null +++ b/input_test.go @@ -0,0 +1,437 @@ +package gonfig + +import ( + "errors" + "fmt" + "net/url" + "reflect" + "testing" + "time" + "unsafe" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewInput(t *testing.T) { + t.Run("bad inputs", func(t *testing.T) { + t.Parallel() + + var ( + sIn string + bIn bool + iIn int + i8In int8 + i16In int16 + i32In int32 + i64In int64 + uIn uint + u8In uint8 + u16In uint16 + u32In uint32 + u64In uint64 + uiIn uintptr + f32In float32 + f64In float64 + c64In complex64 + c128In complex128 + aIn [0]string + slIn []string + chIn = make(chan int) + fuIn = func() {} + mIn = make(map[string]string) + st struct{} + ) + + tests := []interface{}{ + nil, + sIn, &sIn, + bIn, &bIn, + iIn, i8In, i16In, i32In, i64In, + &iIn, &i8In, &i16In, &i32In, &i64In, + uIn, u8In, u16In, u32In, u64In, + &uIn, &u8In, &u16In, &u32In, &u64In, + uiIn, &uiIn, + f32In, &f32In, + f64In, &f64In, + c64In, &c64In, + c128In, &c128In, + aIn, &aIn, + slIn, &slIn, + chIn, &chIn, + fuIn, &fuIn, + mIn, &mIn, + st, + } + + for _, tc := range tests { + tc := tc + t.Run(fmt.Sprint(reflect.TypeOf(tc)), func(t *testing.T) { + t.Parallel() + _, err := NewInput(tc) + assert.IsType(t, &InvalidInputError{}, err) + }) + } + }) + + t.Run("bad fields", func(t *testing.T) { + t.Parallel() + + t.Run("unexported fields", func(t *testing.T) { + unexportedFields := struct { + ue int + }{} + + in, err := NewInput(&unexportedFields) + assert.NoError(t, err) + assert.Empty(t, in.Fields) + }) + + badFields := struct { + Ui uintptr + Uip *uintptr + C chan int + Cp *chan int + F func() + Fp *func() + I interface{} + Ip *interface{} + Up unsafe.Pointer + Upp *unsafe.Pointer + Ss [][]int + Ssp *[][]int + Sa [][0]int + Sap *[][0]int + Aa [0][0]int + Aap *[0][0]int + As [0][]int + Asp *[0][]int + Sui []uintptr + SuiP *[]uintptr + Sc []chan int + Scp *[]chan int + Sf []func() + Sfp *[]func() + Si []interface{} + Sip *[]interface{} + Su []unsafe.Pointer + Sup *[]unsafe.Pointer + Aui [0]uintptr + AuiP *[0]uintptr + Ac [0]chan int + Acp *[0]chan int + Af [0]func() + Afp *[0]func() + Ai [0]interface{} + Aip *[0]interface{} + Au [0]unsafe.Pointer + Aup *[0]unsafe.Pointer + }{} + v := reflect.ValueOf(badFields) + + for i := 0; i < v.NumField(); i++ { + i := i + t.Run(fmt.Sprint(v.Field(i).Type()), func(t *testing.T) { + t.Parallel() + + structType := reflect.StructOf([]reflect.StructField{ + v.Type().Field(i), + }) + structValue := reflect.New(structType) + + in, err := NewInput(structValue.Interface()) + assert.Nil(t, in) + require.Error(t, err) + assert.Truef( + t, + errors.Is(err, ErrUnsupportedType), + "Error must wrap ErrUnsupportedType error", + ) + }) + } + }) + + t.Run("tags", func(t *testing.T) { + t.Parallel() + + req := require.New(t) + ass := assert.New(t) + + tags := struct { + Defaults int + Keys int `config:"TAGS" json:"tags,omitempty" yaml:"tags" toml:""` + Others int `default:"5" required:"true" expand:"true" separator:"," format:"good-format"` + Ignored1 int `config:"-"` + Ignored2 int `ignore:"true"` + }{} + + in, err := NewInput(&tags) + req.NoError(err) + req.NotNil(in) + req.Len(in.Fields, 3) + + defaults := in.Fields[0] + ass.Equal(defaultSeparator, defaults.Tags.Separator) + ass.Equal(defaultFormat, defaults.Tags.Format) + ass.Equal(defaultFormat, defaults.Tags.Format) + + keys := in.Fields[1] + ass.Equal("TAGS", keys.Tags.Config) + ass.Equal("tags", keys.Tags.Json) + ass.Equal("tags", keys.Tags.Yaml) + ass.Equal("", keys.Tags.Toml) + + others := in.Fields[2] + ass.Equal("5", others.Tags.Default) + ass.True(others.Tags.Required) + ass.True(others.Tags.Expand) + ass.Equal(",", others.Tags.Separator) + ass.Equal("good-format", others.Tags.Format) + }) + + t.Run("path", func(t *testing.T) { + t.Parallel() + + req := require.New(t) + ass := assert.New(t) + + paths := struct { + First struct { + Second struct { + Third int + ThirdSibling int + } + + SecondSibling int + } + }{} + + in, err := NewInput(&paths) + req.NoError(err) + req.NotNil(in) + req.Len(in.Fields, 3) + + ass.Equal([]string{"First", "Second", "Third"}, in.Fields[0].Path) + ass.Equal([]string{"First", "Second", "ThirdSibling"}, in.Fields[1].Path) + ass.Equal([]string{"First", "SecondSibling"}, in.Fields[2].Path) + }) + + t.Run("pointer field", func(t *testing.T) { + t.Parallel() + + pf := struct { + Ip *int + }{} + + in, err := NewInput(&pf) + require.NoError(t, err) + require.NotNil(t, in) + require.Len(t, in.Fields, 1) + + require.NotNil(t, pf.Ip) + assert.Zero(t, *pf.Ip) + }) + + t.Run("all possible types", func(t *testing.T) { + t.Parallel() + + type Embed struct { + Int int + String string + } + + type possibleTypes struct { + Boolean bool + BooleanPtr *bool + BooleanArray [2]bool + BooleanArrayPtr *[2]bool + BooleanPtrArray [2]*bool + BooleanSlice []bool + BooleanPtrSlice []*bool + + Uint8 uint8 + Uint8Ptr *uint8 + Uint8Array [2]uint8 + Uint8ArrayPtr *[2]uint8 + Uint8PtrArray [2]*uint8 + Uint8Slice []uint8 + Uint8PtrSlice []*uint8 + + Uint16 uint16 + Uint16Ptr *uint16 + Uint16Array [2]uint16 + Uint16ArrayPtr *[2]uint16 + Uint16PtrArray [2]*uint16 + Uint16Slice []uint16 + Uint16PtrSlice []*uint16 + + Uint32 uint32 + Uint32Ptr *uint32 + Uint32Array [2]uint32 + Uint32ArrayPtr *[2]uint32 + Uint32PtrArray [2]*uint32 + Uint32Slice []uint32 + Uint32PtrSlice []*uint32 + + Uint64 uint64 + Uint64Ptr *uint64 + Uint64Array [2]uint64 + Uint64ArrayPtr *[2]uint64 + Uint64PtrArray [2]*uint64 + Uint64Slice []uint64 + Uint64PtrSlice []*uint64 + + Uint uint + UintPtr *uint + UintArray [2]uint + UintArrayPtr *[2]uint + UintPtrArray [2]*uint + UintSlice []uint + UintPtrSlice []*uint + + Int8 int8 + Int8Ptr *int8 + Int8Array [2]int8 + Int8ArrayPtr *[2]int8 + Int8PtrArray [2]*int8 + Int8Slice []int8 + Int8PtrSlice []*int8 + + Int16 int16 + Int16Ptr *int16 + Int16Array [2]int16 + Int16ArrayPtr *[2]int16 + Int16PtrArray [2]*int16 + Int16Slice []int16 + Int16PtrSlice []*int16 + + Int32 int32 + Int32Ptr *int32 + Int32Array [2]int32 + Int32ArrayPtr *[2]int32 + Int32PtrArray [2]*int32 + Int32Slice []int32 + Int32PtrSlice []*int32 + + Int64 int64 + Int64Ptr *int64 + Int64Array [2]int64 + Int64ArrayPtr *[2]int64 + Int64PtrArray [2]*int64 + Int64Slice []int64 + Int64PtrSlice []*int64 + + Int int + IntPtr *int + IntArray [2]int + IntArrayPtr *[2]int + IntPtrArray [2]*int + IntSlice []int + IntPtrSlice []*int + + Float32 float32 + Float32Ptr *float32 + Float32Array [2]float32 + Float32ArrayPtr *[2]float32 + Float32PtrArray [2]*float32 + Float32Slice []float32 + Float32PtrSlice []*float32 + + Float64 float64 + Float64Ptr *float64 + Float64Array [2]float64 + Float64ArrayPtr *[2]float64 + Float64PtrArray [2]*float64 + Float64Slice []float64 + Float64PtrSlice []*float64 + + Complex64 complex64 + Complex64Ptr *complex64 + Complex64Array [2]complex64 + Complex64ArrayPtr *[2]complex64 + Complex64PtrArray [2]*complex64 + Complex64Slice []complex64 + Complex64PtrSlice []*complex64 + + Complex128 complex128 + Complex128Ptr *complex128 + Complex128Array [2]complex128 + Complex128ArrayPtr *[2]complex128 + Complex128PtrArray [2]*complex128 + Complex128Slice []complex128 + Complex128PtrSlice []*complex128 + + // uint8 + Byte byte + BytePtr *byte + ByteArray [2]byte + ByteArrayPtr *[2]byte + BytePtrArray [2]*byte + ByteSlice []byte + BytePtrSlice []*byte + + // int32 + Rune rune + RunePtr *rune + RuneArray [2]rune + RuneArrayPtr *[2]rune + RunePtrArray [2]*rune + RuneSlice []rune + RunePtrSlice []*rune + + String string + StringPtr *string + StringArray [2]string + StringArrayPtr *[2]string + StringPtrArray [2]*string + StringSlice []string + StringPtrSlice []*string + + // int64 + Duration time.Duration + DurationPtr *time.Duration + DurationArray [2]time.Duration + DurationArrayPtr *[2]time.Duration + DurationPtrArray [2]*time.Duration + DurationSlice []time.Duration + DurationPtrSlice []*time.Duration + + Map map[string]string + MapPtr *map[string]string + MapArray [2]map[string]string + MapArrayPtr *[2]map[string]string + MapPtrArray [2]*map[string]string + MapSlice []map[string]string + MapPtrSlice []*map[string]string + + Time time.Time + TimePtr *time.Time + TimeArray [2]time.Time + TimeArrayPtr *[2]time.Time + TimePtrArray [2]*time.Time + TimeSlice []time.Time + TimePtrSlice []*time.Time + + Url url.URL + UrlPtr *url.URL + UrlArray [2]url.URL + UrlArrayPtr *[2]url.URL + UrlPtrArray [2]*url.URL + UrlSlice []url.URL + UrlPtrSlice []*url.URL + + Struct struct { + Int int + } + StructPtr *struct { + Int int + } + Embed + } + + in, err := NewInput(new(possibleTypes)) + require.NoError(t, err) + require.NotNil(t, in) + require.Len(t, in.Fields, 158) + }) +} diff --git a/tags.go b/tags.go index 1c56f1c..c858f22 100644 --- a/tags.go +++ b/tags.go @@ -7,8 +7,9 @@ import ( ) const ( - defaultSeparator = " " ignoreCharacter = "-" + defaultSeparator = " " + defaultFormat = time.RFC3339 ) // Possible tags, all are optional @@ -49,10 +50,10 @@ type ConfigTags struct { func extractTags(st reflect.StructTag) *ConfigTags { tags := ConfigTags{ Config: st.Get("config"), - Default: st.Get("default"), Json: extractKeyName(st.Get("json")), Yaml: extractKeyName(st.Get("yaml")), Toml: extractKeyName(st.Get("toml")), + Default: st.Get("default"), Required: st.Get("required") == "true", Ignore: st.Get("ignore") == "true", Expand: st.Get("expand") == "true", @@ -67,7 +68,7 @@ func extractTags(st reflect.StructTag) *ConfigTags { tags.Separator = defaultSeparator } if tags.Format == "" { - tags.Format = time.RFC3339 + tags.Format = defaultFormat } return &tags